diff --git a/docs/strutil.md b/docs/strutil.md index 24e57dd..ca46147 100644 --- a/docs/strutil.md +++ b/docs/strutil.md @@ -28,14 +28,15 @@ import ( - [Capitalize](#Capitalize) - [IsString](#IsString) - [KebabCase](#KebabCase) +- [UpperKebabCase](#UpperKebabCase) - [LowerFirst](#LowerFirst) - [UpperFirst](#UpperFirst) - [PadEnd](#PadEnd) - [PadStart](#PadStart) - [ReverseStr](#ReverseStr) - [SnakeCase](#SnakeCase) +- [UpperSnakeCase](#UpperSnakeCase) - [Wrap](#Wrap) - - [Unwrap](#Unwrap) - [SplitEx](#SplitEx) @@ -169,7 +170,7 @@ func main() { ### CamelCase -

Covert string to camelCase string.

+

Coverts string to camelCase string, non letters and numbers will be ignored.

Signature: @@ -196,7 +197,9 @@ func main() { s4 := strutil.CamelCase("foo bar") fmt.Println(s4) //fooBar -} + + s4 := strutil.CamelCase("Foo-#1😄$_%^&*(1bar") + fmt.Println(s4) //foo11Bar ``` @@ -261,7 +264,7 @@ func main() { ### KebabCase -

Covert string to kebab-case.

+

KebabCase covert string to kebab-case, non letters and numbers will be ignored.

Signature: @@ -287,12 +290,44 @@ func main() { fmt.Println(s3) //foo-bar s4 := strutil.KebabCase("__FOO_BAR__") - fmt.Println(s4) //f-o-o-b-a-r + fmt.Println(s4) //foo-bar } ``` +### UpperKebabCase +

UpperKebabCase covert string to upper KEBAB-CASE, non letters and numbers will be ignored.

+ +Signature: + +```go +func KebabCase(s string) string +``` +Example: + +```go +import ( + "fmt" + "github.com/duke-git/lancet/strutil" +) + +func main() { + s1 := strutil.UpperKebabCase("Foo Bar-") + fmt.Println(s1) //FOO-BAR + + s2 := strutil.UpperKebabCase("foo_Bar") + fmt.Println(s2) //FOO-BAR + + s3 := strutil.UpperKebabCase("fooBar") + fmt.Println(s3) //FOO-BAR + + s4 := strutil.UpperKebabCase("__FOO_BAR__") + fmt.Println(s4) //FOO-BAR +} +``` + + ### LowerFirst

Convert the first character of string to lower case.

@@ -456,9 +491,8 @@ func main() { ``` - ### SnakeCase -

Covert string to snake_case.

+

Coverts string to snake_case, non letters and numbers will be ignored.

Signature: @@ -484,14 +518,48 @@ func main() { fmt.Println(s3) //foo_bar s4 := strutil.SnakeCase("__FOO_BAR__") - fmt.Println(s4) //f_o_o_b_a_r + fmt.Println(s4) //foo_bar - s5 := strutil.SnakeCase("aBbc-s$@a&%_B.B^C") - fmt.Println(s5) //a_bbc_s_a_b_b_c + s5 := strutil.SnakeCase("Foo-#1😄$_%^&*(1bar") + fmt.Println(s5) //foo_1_1_bar } ``` +### UpperSnakeCase +

Coverts string to upper KEBAB-CASE, non letters and numbers will be ignored.

+ +Signature: + +```go +func SnakeCase(s string) string +``` +Example: + +```go +import ( + "fmt" + "github.com/duke-git/lancet/strutil" +) + +func main() { + s1 := strutil.UpperSnakeCase("Foo Bar-") + fmt.Println(s1) //FOO_BAR + + s2 := strutil.UpperSnakeCase("foo_Bar") + fmt.Println(s2) //FOO_BAR + + s3 := strutil.UpperSnakeCase("fooBar") + fmt.Println(s3) //FOO_BAR + + s4 := strutil.UpperSnakeCase("__FOO_BAR__") + fmt.Println(s4) //FOO_BAR + + s5 := strutil.UpperSnakeCase("Foo-#1😄$_%^&*(1bar") + fmt.Println(s5) //FOO_1_1_BAR +} +``` + ### Wrap diff --git a/docs/strutil_zh-CN.md b/docs/strutil_zh-CN.md index ca6abb5..cd2ec65 100644 --- a/docs/strutil_zh-CN.md +++ b/docs/strutil_zh-CN.md @@ -28,14 +28,15 @@ import ( - [Capitalize](#Capitalize) - [IsString](#IsString) - [KebabCase](#KebabCase) +- [UpperKebabCase](#UpperKebabCase) - [LowerFirst](#LowerFirst) - [UpperFirst](#UpperFirst) - [PadEnd](#PadEnd) - [PadStart](#PadStart) - [ReverseStr](#ReverseStr) - [SnakeCase](#SnakeCase) +- [UpperSnakeCase](#UpperSnakeCase) - [Wrap](#Wrap) - - [Unwrap](#Unwrap) - [SplitEx](#SplitEx) @@ -170,7 +171,7 @@ func main() { ### CamelCase -

将字符串转换为驼峰式字符串

+

将字符串转换为驼峰式字符串, 非字母和数字会被忽略

函数签名: @@ -197,6 +198,9 @@ func main() { s4 := strutil.CamelCase("foo bar") fmt.Println(s4) //fooBar + + s4 := strutil.CamelCase("Foo-#1😄$_%^&*(1bar") + fmt.Println(s4) //foo11Bar } ``` @@ -260,9 +264,8 @@ func main() { ``` - ### KebabCase -

将字符串转换为kebab-case

+

将字符串转换为kebab-case, 非字母和数字会被忽略

函数签名: @@ -288,12 +291,43 @@ func main() { fmt.Println(s3) //foo-bar s4 := strutil.KebabCase("__FOO_BAR__") - fmt.Println(s4) //f-o-o-b-a-r + fmt.Println(s4) //foo-bar } ``` +### UpperKebabCase +

将字符串转换为大写KEBAB-CASE, 非字母和数字会被忽略

+ +函数签名: + +```go +func KebabCase(s string) string +``` +例子: + +```go +import ( + "fmt" + "github.com/duke-git/lancet/strutil" +) + +func main() { + s1 := strutil.UpperKebabCase("Foo Bar-") + fmt.Println(s1) //FOO-BAR + + s2 := strutil.UpperKebabCase("foo_Bar") + fmt.Println(s2) //FOO-BAR + + s3 := strutil.UpperKebabCase("fooBar") + fmt.Println(s3) //FOO-BAR + + s4 := strutil.UpperKebabCase("__FOO_BAR__") + fmt.Println(s4) //FOO-BAR +} +``` + ### LowerFirst

将字符串的第一个字符转换为小写

@@ -457,9 +491,8 @@ func main() { ``` - ### SnakeCase -

将字符串转换为snake_case形式

+

将字符串转换为snake_case形式, 非字母和数字会被忽略

函数签名: @@ -485,14 +518,48 @@ func main() { fmt.Println(s3) //foo_bar s4 := strutil.SnakeCase("__FOO_BAR__") - fmt.Println(s4) //f_o_o_b_a_r + fmt.Println(s4) //foo_bar - s5 := strutil.SnakeCase("aBbc-s$@a&%_B.B^C") - fmt.Println(s5) //a_bbc_s_a_b_b_c + s5 := strutil.SnakeCase("Foo-#1😄$_%^&*(1bar") + fmt.Println(s5) //foo_1_1_bar } ``` +### UpperSnakeCase +

将字符串转换为大写SNAKE_CASE形式, 非字母和数字会被忽略

+ +函数签名: + +```go +func SnakeCase(s string) string +``` +例子: + +```go +import ( + "fmt" + "github.com/duke-git/lancet/strutil" +) + +func main() { + s1 := strutil.UpperSnakeCase("Foo Bar-") + fmt.Println(s1) //FOO_BAR + + s2 := strutil.UpperSnakeCase("foo_Bar") + fmt.Println(s2) //FOO_BAR + + s3 := strutil.UpperSnakeCase("fooBar") + fmt.Println(s3) //FOO_BAR + + s4 := strutil.UpperSnakeCase("__FOO_BAR__") + fmt.Println(s4) //FOO_BAR + + s5 := strutil.UpperSnakeCase("Foo-#1😄$_%^&*(1bar") + fmt.Println(s5) //FOO_1_1_BAR +} +``` + ### Wrap diff --git a/strutil/string.go b/strutil/string.go index e6aa03b..7dd3e18 100644 --- a/strutil/string.go +++ b/strutil/string.go @@ -5,53 +5,41 @@ package strutil import ( - "regexp" "strings" "unicode" "unicode/utf8" ) // CamelCase covert string to camelCase string. +// non letters and numbers will be ignored +// eg. "Foo-#1😄$_%^&*(1bar" => "foo11Bar" func CamelCase(s string) string { - if len(s) == 0 { - return "" - } + var builder strings.Builder - res := "" - blankSpace := " " - regex, _ := regexp.Compile("[-_&]+") - ss := regex.ReplaceAllString(s, blankSpace) - for i, v := range strings.Split(ss, blankSpace) { - vv := []rune(v) + strs := splitIntoStrings(s, false) + for i, str := range strs { if i == 0 { - if vv[i] >= 65 && vv[i] <= 96 { - vv[0] += 32 - } - res += string(vv) + builder.WriteString(strings.ToLower(str)) } else { - res += Capitalize(v) + builder.WriteString(Capitalize(str)) } } - return res + return builder.String() } // Capitalize converts the first character of a string to upper case and the remaining to lower case. func Capitalize(s string) string { - if len(s) == 0 { - return "" - } - - out := make([]rune, len(s)) + result := make([]rune, len(s)) for i, v := range s { if i == 0 { - out[i] = unicode.ToUpper(v) + result[i] = unicode.ToUpper(v) } else { - out[i] = unicode.ToLower(v) + result[i] = unicode.ToLower(v) } } - return string(out) + return string(result) } // UpperFirst converts the first character of string to upper case. @@ -117,47 +105,35 @@ func PadStart(source string, size int, padStr string) string { } // KebabCase covert string to kebab-case +// non letters and numbers will be ignored +// eg. "Foo-#1😄$_%^&*(1bar" => "foo-1-1-bar" func KebabCase(s string) string { - if len(s) == 0 { - return "" - } + result := splitIntoStrings(s, false) + return strings.Join(result, "-") +} - regex := regexp.MustCompile(`[\W|_]+`) - blankSpace := " " - match := regex.ReplaceAllString(s, blankSpace) - rs := strings.Split(match, blankSpace) - - var res []string - for _, v := range rs { - splitWords := splitWordsToLower(v) - if len(splitWords) > 0 { - res = append(res, splitWords...) - } - } - - return strings.Join(res, "-") +// UpperKebabCase covert string to upper KEBAB-CASE +// non letters and numbers will be ignored +// eg. "Foo-#1😄$_%^&*(1bar" => "FOO-1-1-BAR" +func UpperKebabCase(s string) string { + result := splitIntoStrings(s, true) + return strings.Join(result, "-") } // SnakeCase covert string to snake_case +// non letters and numbers will be ignored +// eg. "Foo-#1😄$_%^&*(1bar" => "foo_1_1_bar" func SnakeCase(s string) string { - if len(s) == 0 { - return "" - } + result := splitIntoStrings(s, false) + return strings.Join(result, "_") +} - regex := regexp.MustCompile(`[\W|_]+`) - blankSpace := " " - match := regex.ReplaceAllString(s, blankSpace) - rs := strings.Split(match, blankSpace) - - var res []string - for _, v := range rs { - splitWords := splitWordsToLower(v) - if len(splitWords) > 0 { - res = append(res, splitWords...) - } - } - - return strings.Join(res, "_") +// UpperSnakeCase covert string to upper SNAKE_CASE +// non letters and numbers will be ignored +// eg. "Foo-#1😄$_%^&*(1bar" => "FOO_1_1_BAR" +func UpperSnakeCase(s string) string { + result := splitIntoStrings(s, true) + return strings.Join(result, "_") } // Before create substring in source string before position when char first appear diff --git a/strutil/string_internal.go b/strutil/string_internal.go index 6776d43..ca93800 100644 --- a/strutil/string_internal.go +++ b/strutil/string_internal.go @@ -1,40 +1,98 @@ package strutil -import "strings" +import "unicode" -// splitWordsToLower split a string into worlds by uppercase char -func splitWordsToLower(s string) []string { - var res []string +func splitIntoStrings(s string, upperCase bool) []string { + var runes [][]rune + lastCharType := 0 + charType := 0 - upperIndexes := upperIndex(s) - l := len(upperIndexes) - if upperIndexes == nil || l == 0 { - if s != "" { - res = append(res, s) + // split into fields based on type of unicode character + for _, r := range s { + switch true { + case isLower(r): + charType = 1 + case isUpper(r): + charType = 2 + case isDigit(r): + charType = 3 + default: + charType = 4 } - return res - } - for i := 0; i < l; i++ { - if i < l-1 { - res = append(res, strings.ToLower(s[upperIndexes[i]:upperIndexes[i+1]])) + + if charType == lastCharType { + runes[len(runes)-1] = append(runes[len(runes)-1], r) } else { - res = append(res, strings.ToLower(s[upperIndexes[i]:])) + runes = append(runes, []rune{r}) } - } - return res -} - -// upperIndex get a int slice which elements are all the uppercase char index of a string -func upperIndex(s string) []int { - var res []int - for i := 0; i < len(s); i++ { - if 64 < s[i] && s[i] < 91 { - res = append(res, i) - } - } - if len(s) > 0 && res != nil && res[0] != 0 { - res = append([]int{0}, res...) + lastCharType = charType } - return res + for i := 0; i < len(runes)-1; i++ { + if isUpper(runes[i][0]) && isLower(runes[i+1][0]) { + runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...) + runes[i] = runes[i][:len(runes[i])-1] + } + } + + // filter all none letters and none digit + var result []string + for _, rs := range runes { + if len(rs) > 0 && (unicode.IsLetter(rs[0]) || isDigit(rs[0])) { + if upperCase { + result = append(result, string(toUpperAll(rs))) + } else { + result = append(result, string(toLowerAll(rs))) + } + } + } + + return result +} + +// isDigit checks if a character is digit ('0' to '9') +func isDigit(r rune) bool { + return r >= '0' && r <= '9' +} + +// isLower checks if a character is lower case ('a' to 'z') +func isLower(r rune) bool { + return r >= 'a' && r <= 'z' +} + +// isUpper checks if a character is upper case ('A' to 'Z') +func isUpper(r rune) bool { + return r >= 'A' && r <= 'Z' +} + +// toLower converts a character 'A' to 'Z' to its lower case +func toLower(r rune) rune { + if r >= 'A' && r <= 'Z' { + return r + 32 + } + return r +} + +// toLowerAll converts a character 'A' to 'Z' to its lower case +func toLowerAll(rs []rune) []rune { + for i := range rs { + rs[i] = toLower(rs[i]) + } + return rs +} + +// toUpper converts a character 'a' to 'z' to its upper case +func toUpper(r rune) rune { + if r >= 'a' && r <= 'z' { + return r - 32 + } + return r +} + +// toUpperAll converts a character 'a' to 'z' to its upper case +func toUpperAll(rs []rune) []rune { + for i := range rs { + rs[i] = toUpper(rs[i]) + } + return rs } diff --git a/strutil/string_test.go b/strutil/string_test.go index d25e332..ac9a9a9 100644 --- a/strutil/string_test.go +++ b/strutil/string_test.go @@ -9,67 +9,163 @@ import ( func TestCamelCase(t *testing.T) { assert := internal.NewAssert(t, "TestCamelCase") - assert.Equal("fooBar", CamelCase("foo_bar")) - assert.Equal("fooBar", CamelCase("Foo-Bar")) - assert.Equal("fooBar", CamelCase("Foo&bar")) - assert.Equal("fooBar", CamelCase("foo bar")) + cases := map[string]string{ + "": "", + "foobar": "foobar", + "&FOO:BAR$BAZ": "fooBarBaz", + "fooBar": "fooBar", + "FOObar": "foObar", + "$foo%": "foo", + " $#$Foo 22 bar ": "foo22Bar", + "Foo-#1😄$_%^&*(1bar": "foo11Bar", + } - assert.NotEqual("FooBar", CamelCase("foo_bar")) + for k, v := range cases { + assert.Equal(v, CamelCase(k)) + } } func TestCapitalize(t *testing.T) { assert := internal.NewAssert(t, "TestCapitalize") - assert.Equal("Foo", Capitalize("foo")) - assert.Equal("Foo", Capitalize("Foo")) - assert.Equal("Foo", Capitalize("Foo")) + cases := map[string]string{ + "": "", + "Foo": "Foo", + "_foo": "_foo", + "foobar": "Foobar", + "fooBar": "Foobar", + "foo Bar": "Foo bar", + "foo-bar": "Foo-bar", + "$foo%": "$foo%", + } - assert.NotEqual("foo", Capitalize("Foo")) + for k, v := range cases { + assert.Equal(v, Capitalize(k)) + } } func TestKebabCase(t *testing.T) { assert := internal.NewAssert(t, "TestKebabCase") - assert.Equal("foo-bar", KebabCase("Foo Bar-")) - assert.Equal("foo-bar", KebabCase("foo_Bar")) - assert.Equal("foo-bar", KebabCase("fooBar")) - assert.Equal("f-o-o-b-a-r", KebabCase("__FOO_BAR__")) + cases := map[string]string{ + "": "", + "foo-bar": "foo-bar", + "--Foo---Bar-": "foo-bar", + "Foo Bar-": "foo-bar", + "foo_Bar": "foo-bar", + "fooBar": "foo-bar", + "FOOBAR": "foobar", + "FOO_BAR": "foo-bar", + "__FOO_BAR__": "foo-bar", + "$foo@Bar": "foo-bar", + " $#$Foo 22 bar ": "foo-22-bar", + "Foo-#1😄$_%^&*(1bar": "foo-1-1-bar", + } - assert.NotEqual("foo_bar", KebabCase("fooBar")) + for k, v := range cases { + assert.Equal(v, KebabCase(k)) + } +} + +func TestUpperKebabCase(t *testing.T) { + assert := internal.NewAssert(t, "TestUpperKebabCase") + + cases := map[string]string{ + "": "", + "foo-bar": "FOO-BAR", + "--Foo---Bar-": "FOO-BAR", + "Foo Bar-": "FOO-BAR", + "foo_Bar": "FOO-BAR", + "fooBar": "FOO-BAR", + "FOOBAR": "FOOBAR", + "FOO_BAR": "FOO-BAR", + "__FOO_BAR__": "FOO-BAR", + "$foo@Bar": "FOO-BAR", + " $#$Foo 22 bar ": "FOO-22-BAR", + "Foo-#1😄$_%^&*(1bar": "FOO-1-1-BAR", + } + + for k, v := range cases { + assert.Equal(v, UpperKebabCase(k)) + } } func TestSnakeCase(t *testing.T) { assert := internal.NewAssert(t, "TestSnakeCase") - assert.Equal("foo_bar", SnakeCase("Foo Bar-")) - assert.Equal("foo_bar", SnakeCase("foo_Bar")) - assert.Equal("foo_bar", SnakeCase("fooBar")) - assert.Equal("f_o_o_b_a_r", SnakeCase("__FOO_BAR__")) - assert.Equal("a_bbc_s_a_b_b_c", SnakeCase("aBbc-s$@a&%_B.B^C")) + cases := map[string]string{ + "": "", + "foo-bar": "foo_bar", + "--Foo---Bar-": "foo_bar", + "Foo Bar-": "foo_bar", + "foo_Bar": "foo_bar", + "fooBar": "foo_bar", + "FOOBAR": "foobar", + "FOO_BAR": "foo_bar", + "__FOO_BAR__": "foo_bar", + "$foo@Bar": "foo_bar", + " $#$Foo 22 bar ": "foo_22_bar", + "Foo-#1😄$_%^&*(1bar": "foo_1_1_bar", + } - assert.NotEqual("foo-bar", SnakeCase("foo_Bar")) + for k, v := range cases { + assert.Equal(v, SnakeCase(k)) + } +} + +func TestUpperSnakeCase(t *testing.T) { + assert := internal.NewAssert(t, "TestUpperSnakeCase") + + cases := map[string]string{ + "": "", + "foo-bar": "FOO_BAR", + "--Foo---Bar-": "FOO_BAR", + "Foo Bar-": "FOO_BAR", + "foo_Bar": "FOO_BAR", + "fooBar": "FOO_BAR", + "FOOBAR": "FOOBAR", + "FOO_BAR": "FOO_BAR", + "__FOO_BAR__": "FOO_BAR", + "$foo@Bar": "FOO_BAR", + " $#$Foo 22 bar ": "FOO_22_BAR", + "Foo-#1😄$_%^&*(1bar": "FOO_1_1_BAR", + } + + for k, v := range cases { + assert.Equal(v, UpperSnakeCase(k)) + } } func TestUpperFirst(t *testing.T) { assert := internal.NewAssert(t, "TestLowerFirst") - assert.Equal("Foo", UpperFirst("foo")) - assert.Equal("BAR", UpperFirst("bAR")) - assert.Equal("FOo", UpperFirst("FOo")) - assert.Equal("FOo大", UpperFirst("fOo大")) + cases := map[string]string{ + "": "", + "foo": "Foo", + "bAR": "BAR", + "FOo": "FOo", + "fOo大": "FOo大", + } - assert.NotEqual("Bar", UpperFirst("BAR")) + for k, v := range cases { + assert.Equal(v, UpperFirst(k)) + } } func TestLowerFirst(t *testing.T) { assert := internal.NewAssert(t, "TestLowerFirst") - assert.Equal("foo", LowerFirst("foo")) - assert.Equal("bAR", LowerFirst("BAR")) - assert.Equal("fOo", LowerFirst("FOo")) - assert.Equal("fOo大", LowerFirst("FOo大")) + cases := map[string]string{ + "": "", + "foo": "foo", + "bAR": "bAR", + "FOo": "fOo", + "fOo大": "fOo大", + } - assert.NotEqual("Bar", LowerFirst("BAR")) + for k, v := range cases { + assert.Equal(v, LowerFirst(k)) + } } func TestPadEnd(t *testing.T) {