Skip to content

Commit

Permalink
Mark letter case changes to fix "camelCaseText" input
Browse files Browse the repository at this point in the history
  • Loading branch information
VojtechVitek committed Oct 19, 2022
1 parent 3740e43 commit b9525a3
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 25 deletions.
7 changes: 5 additions & 2 deletions camel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,5 +29,6 @@ func CamelCase(input string) string {
b.WriteRune(unicode.ToLower(r))
}
}

return b.String()
}
4 changes: 3 additions & 1 deletion kebab.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 42 additions & 7 deletions parser.go
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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()
}
7 changes: 5 additions & 2 deletions pascal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -23,5 +25,6 @@ func PascalCase(input string) string {
b.WriteRune(unicode.ToLower(r))
}
}

return b.String()
}
4 changes: 3 additions & 1 deletion snake.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 37 additions & 12 deletions textcase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

0 comments on commit b9525a3

Please sign in to comment.