diff --git a/camel.go b/camel.go index aec52f4..bdb38cb 100644 --- a/camel.go +++ b/camel.go @@ -9,11 +9,13 @@ import ( // Converts input string to "camelCase" (lower camel case) naming convention. // Removes all whitespace and special characters. Supports Unicode characters. func CamelCase(input string) string { + str := markLetterCaseChanges(input) + var b strings.Builder state := idle - for i := 0; i < len(input); { - r, size := utf8.DecodeRuneInString(input[i:]) + for i := 0; i < len(str); { + r, size := utf8.DecodeRuneInString(str[i:]) i += size state = state.next(r) switch state { @@ -27,5 +29,6 @@ func CamelCase(input string) string { b.WriteRune(unicode.ToLower(r)) } } + return b.String() } diff --git a/kebab.go b/kebab.go index 50a139f..feb7352 100644 --- a/kebab.go +++ b/kebab.go @@ -8,7 +8,9 @@ import ( // Converts input string to "kebab-case" naming convention. // Removes all whitespace and special characters. Supports Unicode characters. -func KebabCase(str string) string { +func KebabCase(input string) string { + str := markLetterCaseChanges(input) + var b bytes.Buffer state := idle diff --git a/parser.go b/parser.go index e3f06fb..8e03e74 100644 --- a/parser.go +++ b/parser.go @@ -1,20 +1,22 @@ package textcase import ( + "strings" "unicode" + "unicode/utf8" ) -type parserStateMachine int +type parser int const ( - _ parserStateMachine = iota // _$$_This is some text, OK?! - idle // 1 ↑↑↑↑ ↑ ↑ - firstAlphaNum // 2 ↑ ↑ ↑ ↑ ↑ - alphaNum // 3 ↑↑↑ ↑ ↑↑↑ ↑↑↑ ↑ - delimiter // 4 ↑ ↑ ↑ ↑ ↑ + _ parser = iota // _$$_This is some text, OK?! + idle // 1 ↑↑↑↑ ↑ ↑ + firstAlphaNum // 2 ↑ ↑ ↑ ↑ ↑ + alphaNum // 3 ↑↑↑ ↑ ↑↑↑ ↑↑↑ ↑ + delimiter // 4 ↑ ↑ ↑ ↑ ↑ ) -func (s parserStateMachine) next(r rune) parserStateMachine { +func (s parser) next(r rune) parser { switch s { case idle: if isAlphaNum(r) { @@ -41,3 +43,36 @@ func (s parserStateMachine) next(r rune) parserStateMachine { func isAlphaNum(r rune) bool { return unicode.IsLetter(r) || unicode.IsNumber(r) } + +// Mark letter case changes, ie. "camelCaseTEXT" -> "camel_Case_TEXT". +func markLetterCaseChanges(input string) string { + var b strings.Builder + + wasLetter := false + countConsecutiveUpperLetters := 0 + + for i := 0; i < len(input); { + r, size := utf8.DecodeRuneInString(input[i:]) + i += size + + if unicode.IsLetter(r) { + if wasLetter && countConsecutiveUpperLetters > 1 && !unicode.IsUpper(r) { + b.WriteString("_") + } + if wasLetter && countConsecutiveUpperLetters == 0 && unicode.IsUpper(r) { + b.WriteString("_") + } + } + + wasLetter = unicode.IsLetter(r) + if unicode.IsUpper(r) { + countConsecutiveUpperLetters++ + } else { + countConsecutiveUpperLetters = 0 + } + + b.WriteRune(r) + } + + return b.String() +} diff --git a/pascal.go b/pascal.go index 00e8c67..7c337b5 100644 --- a/pascal.go +++ b/pascal.go @@ -9,11 +9,13 @@ import ( // Converts input string to "PascalCase" (upper camel case) naming convention. // Removes all whitespace and special characters. Supports Unicode characters. func PascalCase(input string) string { + str := markLetterCaseChanges(input) + var b strings.Builder state := idle - for i := 0; i < len(input); { - r, size := utf8.DecodeRuneInString(input[i:]) + for i := 0; i < len(str); { + r, size := utf8.DecodeRuneInString(str[i:]) i += size state = state.next(r) switch state { @@ -23,5 +25,6 @@ func PascalCase(input string) string { b.WriteRune(unicode.ToLower(r)) } } + return b.String() } diff --git a/snake.go b/snake.go index 7035c76..7619744 100644 --- a/snake.go +++ b/snake.go @@ -8,7 +8,9 @@ import ( // Converts input string to "snake_case" naming convention. // Removes all whitespace and special characters. Supports Unicode characters. -func SnakeCase(str string) string { +func SnakeCase(input string) string { + str := markLetterCaseChanges(input) + var b bytes.Buffer state := idle diff --git a/textcase_test.go b/textcase_test.go index f394e84..b20cc27 100644 --- a/textcase_test.go +++ b/textcase_test.go @@ -6,49 +6,74 @@ import ( ) func TestTextCases(t *testing.T) { - t.Parallel() - tt := []struct { in string camel string snake string }{ - {in: "Add updated_at to users table", camel: "addUpdatedAtToUsersTable", snake: "add_updated_at_to_users_table"}, - {in: "$()&^%(_--crazy__--input$)", camel: "crazyInput", snake: "crazy_input"}, - {in: "Hey, this TEXT will have to obey some rules!!", camel: "heyThisTextWillHaveToObeySomeRules", snake: "hey_this_text_will_have_to_obey_some_rules"}, - {in: "_$$_This is some text, OK?!", camel: "thisIsSomeTextOk", snake: "this_is_some_text_ok"}, - {in: "_", camel: "", snake: ""}, - {in: "$(((*&^%$#@!)))#$%^&*", camel: "", snake: ""}, {in: "", camel: "", snake: ""}, + {in: "_", camel: "", snake: ""}, {in: "a", camel: "a", snake: "a"}, {in: "a___", camel: "a", snake: "a"}, + {in: "___a", camel: "a", snake: "a"}, + {in: "a_b", camel: "aB", snake: "a_b"}, {in: "a___b", camel: "aB", snake: "a_b"}, {in: "ax___by", camel: "axBy", snake: "ax_by"}, + {in: "someText", camel: "someText", snake: "some_text"}, + {in: "someTEXT", camel: "someText", snake: "some_text"}, + {in: "NeXT", camel: "neXt", snake: "ne_xt"}, + {in: "Add updated_at to users table", camel: "addUpdatedAtToUsersTable", snake: "add_updated_at_to_users_table"}, + {in: "Hey, this TEXT will have to obey some rules!!", camel: "heyThisTextWillHaveToObeySomeRules", snake: "hey_this_text_will_have_to_obey_some_rules"}, {in: "Háčky, čárky. Příliš žluťoučký kůň úpěl ďábelské ódy.", camel: "háčkyČárkyPřílišŽluťoučkýKůňÚpělĎábelskéÓdy", snake: "háčky_čárky_příliš_žluťoučký_kůň_úpěl_ďábelské_ódy"}, {in: "here comes O'Brian", camel: "hereComesOBrian", snake: "here_comes_o_brian"}, + {in: "thisIsCamelCase", camel: "thisIsCamelCase", snake: "this_is_camel_case"}, + {in: "this_is_snake_case", camel: "thisIsSnakeCase", snake: "this_is_snake_case"}, + {in: "__snake_case__", camel: "snakeCase", snake: "snake_case"}, + {in: "fromCamelCaseToCamelCase", camel: "fromCamelCaseToCamelCase", snake: "from_camel_case_to_camel_case"}, + {in: "$()&^%(_--crazy__--input$)", camel: "crazyInput", snake: "crazy_input"}, + {in: "_$$_This is some text, OK?!", camel: "thisIsSomeTextOk", snake: "this_is_some_text_ok"}, + {in: "$(((*&^%$#@!)))#$%^&*", camel: "", snake: ""}, } for _, test := range tt { // camelCase if got := CamelCase(test.in); got != test.camel { - t.Errorf("unexpected camelCase for input(%q), got %q, want %q", test.in, got, test.camel) + t.Errorf("unexpected camelCase for %q: got %q, want %q", test.in, got, test.camel) } // PascalCase testPascal := strings.Title(test.camel) if got := PascalCase(test.in); got != testPascal { - t.Errorf("unexpected PascalCase for input(%q), got %q, want %q", test.in, got, testPascal) + t.Errorf("unexpected PascalCase for %q: got %q, want %q", test.in, got, testPascal) } // snake_case if got := SnakeCase(test.in); got != test.snake { - t.Errorf("unexpected snake_case for input(%q), got %q, want %q", test.in, got, test.snake) + t.Errorf("unexpected snake_case for %q: got %q, want %q", test.in, got, test.snake) } // kebab-case testKebab := strings.ReplaceAll(test.snake, "_", "-") if got := KebabCase(test.in); got != testKebab { - t.Errorf("unexpected kebab-case for input(%q), got %q, want %q", test.in, got, testKebab) + t.Errorf("unexpected kebab-case for %q: got %q, want %q", test.in, got, testKebab) + } + } +} + +func TestMarkLetterCaseChanges(t *testing.T) { + tt := []struct { + in string + out string + }{ + {in: "detectUpperLowerChanges", out: "detect_Upper_Lower_Changes"}, + {in: "detectUPPERchange", out: "detect_UPPER_change"}, + {in: "detect_UPPER_change", out: "detect_UPPER_change"}, + {in: "Some camelCase and PascalCase text, OK?", out: "Some camel_Case and Pascal_Case text, OK?"}, + } + + for _, test := range tt { + if got := markLetterCaseChanges(test.in); got != test.out { + t.Errorf("unexpected markLowerUpperChanges for %q: got %q, want %q", test.in, got, test.out) } } }