diff --git a/strutil/string.go b/strutil/string.go index 8fd5e29..ce22a3c 100644 --- a/strutil/string.go +++ b/strutil/string.go @@ -157,24 +157,19 @@ 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 { - re := regexp.MustCompile(`[\W|_]+`) - space := " " - match := re.ReplaceAllString(s, space) - rs := strings.Split(match, space) + strs := splitIntoStrings(s, false) + return strings.Join(strs, "-") +} - var result []string - for _, v := range rs { - splitWords := splitWordsToLower(v) - if len(splitWords) > 0 { - result = append(result, splitWords...) - } - // if v != "" { - // result = append(result, strings.ToLower(v)) - // } - } - - return strings.Join(result, "-") +// 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 { + strs := splitIntoStrings(s, true) + return strings.Join(strs, "-") } // SnakeCase covert string to snake_case diff --git a/strutil/string_internal.go b/strutil/string_internal.go index 3336c37..d140682 100644 --- a/strutil/string_internal.go +++ b/strutil/string_internal.go @@ -1,6 +1,9 @@ package strutil -import "strings" +import ( + "strings" + "unicode" +) // splitWordsToLower split a string into worlds by uppercase char func splitWordsToLower(s string) []string { @@ -38,3 +41,99 @@ func upperIndex(s string) []int { return result } + +func splitIntoStrings(s string, upperCase bool) []string { + var runes [][]rune + lastCharType := 0 + charType := 0 + + // 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 + } + + if charType == lastCharType { + runes[len(runes)-1] = append(runes[len(runes)-1], r) + } else { + runes = append(runes, []rune{r}) + } + lastCharType = charType + } + + 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 1371d0e..21c5422 100644 --- a/strutil/string_test.go +++ b/strutil/string_test.go @@ -8,7 +8,9 @@ import ( func TestCamelCase(t *testing.T) { assert := internal.NewAssert(t, "TestCamelCase") + cases := map[string]string{ + "": "", "foobar": "foobar", "&FOO:BAR$BAZ": "fooBarBaz", "fooBar": "fooBar", @@ -21,14 +23,13 @@ func TestCamelCase(t *testing.T) { for k, v := range cases { assert.Equal(v, CamelCase(k)) } - - assert.Equal("", CamelCase("")) } func TestCapitalize(t *testing.T) { assert := internal.NewAssert(t, "TestCapitalize") cases := map[string]string{ + "": "", "Foo": "Foo", "_foo": "_foo", "foobar": "Foobar", @@ -41,31 +42,52 @@ func TestCapitalize(t *testing.T) { for k, v := range cases { assert.Equal(v, Capitalize(k)) } - - assert.Equal("", Capitalize("")) } func TestKebabCase(t *testing.T) { assert := internal.NewAssert(t, "TestKebabCase") cases := map[string]string{ + "": "", + "foo-bar": "foo-bar", + "--Foo---Bar-": "foo-bar", "Foo Bar-": "foo-bar", "foo_Bar": "foo-bar", "fooBar": "foo-bar", - "FOOBAR": "f-o-o-b-a-r", - "FOO_BAR": "f-o-o-b-a-r", - "__FOO_BAR__": "f-o-o-b-a-r", + "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, KebabCase(k)) } +} - assert.Equal("", KebabCase("")) +func TestUpperKebabCase(t *testing.T) { + assert := internal.NewAssert(t, "TestUpperKebabCase") - // assert.Equal("foo-1-1bar", KebabCase("Foo-#1😄$_%^&*(1bar")) + 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) {