Skip to content

Commit b9525a3

Browse files
committed
Mark letter case changes to fix "camelCaseText" input
1 parent 3740e43 commit b9525a3

File tree

6 files changed

+95
-25
lines changed

6 files changed

+95
-25
lines changed

camel.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import (
99
// Converts input string to "camelCase" (lower camel case) naming convention.
1010
// Removes all whitespace and special characters. Supports Unicode characters.
1111
func CamelCase(input string) string {
12+
str := markLetterCaseChanges(input)
13+
1214
var b strings.Builder
1315

1416
state := idle
15-
for i := 0; i < len(input); {
16-
r, size := utf8.DecodeRuneInString(input[i:])
17+
for i := 0; i < len(str); {
18+
r, size := utf8.DecodeRuneInString(str[i:])
1719
i += size
1820
state = state.next(r)
1921
switch state {
@@ -27,5 +29,6 @@ func CamelCase(input string) string {
2729
b.WriteRune(unicode.ToLower(r))
2830
}
2931
}
32+
3033
return b.String()
3134
}

kebab.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88

99
// Converts input string to "kebab-case" naming convention.
1010
// Removes all whitespace and special characters. Supports Unicode characters.
11-
func KebabCase(str string) string {
11+
func KebabCase(input string) string {
12+
str := markLetterCaseChanges(input)
13+
1214
var b bytes.Buffer
1315

1416
state := idle

parser.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
package textcase
22

33
import (
4+
"strings"
45
"unicode"
6+
"unicode/utf8"
57
)
68

7-
type parserStateMachine int
9+
type parser int
810

911
const (
10-
_ parserStateMachine = iota // _$$_This is some text, OK?!
11-
idle // 1 ↑↑↑↑ ↑ ↑
12-
firstAlphaNum // 2 ↑ ↑ ↑ ↑ ↑
13-
alphaNum // 3 ↑↑↑ ↑ ↑↑↑ ↑↑↑ ↑
14-
delimiter // 4 ↑ ↑ ↑ ↑ ↑
12+
_ parser = iota // _$$_This is some text, OK?!
13+
idle // 1 ↑↑↑↑ ↑ ↑
14+
firstAlphaNum // 2 ↑ ↑ ↑ ↑ ↑
15+
alphaNum // 3 ↑↑↑ ↑ ↑↑↑ ↑↑↑ ↑
16+
delimiter // 4 ↑ ↑ ↑ ↑ ↑
1517
)
1618

17-
func (s parserStateMachine) next(r rune) parserStateMachine {
19+
func (s parser) next(r rune) parser {
1820
switch s {
1921
case idle:
2022
if isAlphaNum(r) {
@@ -41,3 +43,36 @@ func (s parserStateMachine) next(r rune) parserStateMachine {
4143
func isAlphaNum(r rune) bool {
4244
return unicode.IsLetter(r) || unicode.IsNumber(r)
4345
}
46+
47+
// Mark letter case changes, ie. "camelCaseTEXT" -> "camel_Case_TEXT".
48+
func markLetterCaseChanges(input string) string {
49+
var b strings.Builder
50+
51+
wasLetter := false
52+
countConsecutiveUpperLetters := 0
53+
54+
for i := 0; i < len(input); {
55+
r, size := utf8.DecodeRuneInString(input[i:])
56+
i += size
57+
58+
if unicode.IsLetter(r) {
59+
if wasLetter && countConsecutiveUpperLetters > 1 && !unicode.IsUpper(r) {
60+
b.WriteString("_")
61+
}
62+
if wasLetter && countConsecutiveUpperLetters == 0 && unicode.IsUpper(r) {
63+
b.WriteString("_")
64+
}
65+
}
66+
67+
wasLetter = unicode.IsLetter(r)
68+
if unicode.IsUpper(r) {
69+
countConsecutiveUpperLetters++
70+
} else {
71+
countConsecutiveUpperLetters = 0
72+
}
73+
74+
b.WriteRune(r)
75+
}
76+
77+
return b.String()
78+
}

pascal.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import (
99
// Converts input string to "PascalCase" (upper camel case) naming convention.
1010
// Removes all whitespace and special characters. Supports Unicode characters.
1111
func PascalCase(input string) string {
12+
str := markLetterCaseChanges(input)
13+
1214
var b strings.Builder
1315

1416
state := idle
15-
for i := 0; i < len(input); {
16-
r, size := utf8.DecodeRuneInString(input[i:])
17+
for i := 0; i < len(str); {
18+
r, size := utf8.DecodeRuneInString(str[i:])
1719
i += size
1820
state = state.next(r)
1921
switch state {
@@ -23,5 +25,6 @@ func PascalCase(input string) string {
2325
b.WriteRune(unicode.ToLower(r))
2426
}
2527
}
28+
2629
return b.String()
2730
}

snake.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88

99
// Converts input string to "snake_case" naming convention.
1010
// Removes all whitespace and special characters. Supports Unicode characters.
11-
func SnakeCase(str string) string {
11+
func SnakeCase(input string) string {
12+
str := markLetterCaseChanges(input)
13+
1214
var b bytes.Buffer
1315

1416
state := idle

textcase_test.go

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,74 @@ import (
66
)
77

88
func TestTextCases(t *testing.T) {
9-
t.Parallel()
10-
119
tt := []struct {
1210
in string
1311
camel string
1412
snake string
1513
}{
16-
{in: "Add updated_at to users table", camel: "addUpdatedAtToUsersTable", snake: "add_updated_at_to_users_table"},
17-
{in: "$()&^%(_--crazy__--input$)", camel: "crazyInput", snake: "crazy_input"},
18-
{in: "Hey, this TEXT will have to obey some rules!!", camel: "heyThisTextWillHaveToObeySomeRules", snake: "hey_this_text_will_have_to_obey_some_rules"},
19-
{in: "_$$_This is some text, OK?!", camel: "thisIsSomeTextOk", snake: "this_is_some_text_ok"},
20-
{in: "_", camel: "", snake: ""},
21-
{in: "$(((*&^%$#@!)))#$%^&*", camel: "", snake: ""},
2214
{in: "", camel: "", snake: ""},
15+
{in: "_", camel: "", snake: ""},
2316
{in: "a", camel: "a", snake: "a"},
2417
{in: "a___", camel: "a", snake: "a"},
18+
{in: "___a", camel: "a", snake: "a"},
19+
{in: "a_b", camel: "aB", snake: "a_b"},
2520
{in: "a___b", camel: "aB", snake: "a_b"},
2621
{in: "ax___by", camel: "axBy", snake: "ax_by"},
22+
{in: "someText", camel: "someText", snake: "some_text"},
23+
{in: "someTEXT", camel: "someText", snake: "some_text"},
24+
{in: "NeXT", camel: "neXt", snake: "ne_xt"},
25+
{in: "Add updated_at to users table", camel: "addUpdatedAtToUsersTable", snake: "add_updated_at_to_users_table"},
26+
{in: "Hey, this TEXT will have to obey some rules!!", camel: "heyThisTextWillHaveToObeySomeRules", snake: "hey_this_text_will_have_to_obey_some_rules"},
2727
{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"},
2828
{in: "here comes O'Brian", camel: "hereComesOBrian", snake: "here_comes_o_brian"},
29+
{in: "thisIsCamelCase", camel: "thisIsCamelCase", snake: "this_is_camel_case"},
30+
{in: "this_is_snake_case", camel: "thisIsSnakeCase", snake: "this_is_snake_case"},
31+
{in: "__snake_case__", camel: "snakeCase", snake: "snake_case"},
32+
{in: "fromCamelCaseToCamelCase", camel: "fromCamelCaseToCamelCase", snake: "from_camel_case_to_camel_case"},
33+
{in: "$()&^%(_--crazy__--input$)", camel: "crazyInput", snake: "crazy_input"},
34+
{in: "_$$_This is some text, OK?!", camel: "thisIsSomeTextOk", snake: "this_is_some_text_ok"},
35+
{in: "$(((*&^%$#@!)))#$%^&*", camel: "", snake: ""},
2936
}
3037

3138
for _, test := range tt {
3239
// camelCase
3340
if got := CamelCase(test.in); got != test.camel {
34-
t.Errorf("unexpected camelCase for input(%q), got %q, want %q", test.in, got, test.camel)
41+
t.Errorf("unexpected camelCase for %q: got %q, want %q", test.in, got, test.camel)
3542
}
3643

3744
// PascalCase
3845
testPascal := strings.Title(test.camel)
3946
if got := PascalCase(test.in); got != testPascal {
40-
t.Errorf("unexpected PascalCase for input(%q), got %q, want %q", test.in, got, testPascal)
47+
t.Errorf("unexpected PascalCase for %q: got %q, want %q", test.in, got, testPascal)
4148
}
4249

4350
// snake_case
4451
if got := SnakeCase(test.in); got != test.snake {
45-
t.Errorf("unexpected snake_case for input(%q), got %q, want %q", test.in, got, test.snake)
52+
t.Errorf("unexpected snake_case for %q: got %q, want %q", test.in, got, test.snake)
4653
}
4754

4855
// kebab-case
4956
testKebab := strings.ReplaceAll(test.snake, "_", "-")
5057
if got := KebabCase(test.in); got != testKebab {
51-
t.Errorf("unexpected kebab-case for input(%q), got %q, want %q", test.in, got, testKebab)
58+
t.Errorf("unexpected kebab-case for %q: got %q, want %q", test.in, got, testKebab)
59+
}
60+
}
61+
}
62+
63+
func TestMarkLetterCaseChanges(t *testing.T) {
64+
tt := []struct {
65+
in string
66+
out string
67+
}{
68+
{in: "detectUpperLowerChanges", out: "detect_Upper_Lower_Changes"},
69+
{in: "detectUPPERchange", out: "detect_UPPER_change"},
70+
{in: "detect_UPPER_change", out: "detect_UPPER_change"},
71+
{in: "Some camelCase and PascalCase text, OK?", out: "Some camel_Case and Pascal_Case text, OK?"},
72+
}
73+
74+
for _, test := range tt {
75+
if got := markLetterCaseChanges(test.in); got != test.out {
76+
t.Errorf("unexpected markLowerUpperChanges for %q: got %q, want %q", test.in, got, test.out)
5277
}
5378
}
5479
}

0 commit comments

Comments
 (0)