From 5ab870f8c1a9d6b6b489ab5f44d84db148836b90 Mon Sep 17 00:00:00 2001 From: Nathan Baulch Date: Sun, 16 Aug 2020 22:10:44 +1000 Subject: [PATCH 1/5] Benchmarks --- camel_test.go | 30 +++++++++++++++--- snake_test.go | 87 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/camel_test.go b/camel_test.go index e82e53d..c5a0101 100644 --- a/camel_test.go +++ b/camel_test.go @@ -28,7 +28,7 @@ import ( "testing" ) -func TestToCamel(t *testing.T) { +func toCamel(tb testing.TB) { cases := [][]string{ {"test_case", "TestCase"}, {"test.case", "TestCase"}, @@ -47,12 +47,20 @@ func TestToCamel(t *testing.T) { out := i[1] result := ToCamel(in) if result != out { - t.Error("'" + result + "' != '" + out + "'") + tb.Error("'" + result + "' != '" + out + "'") } } } -func TestToLowerCamel(t *testing.T) { +func TestToCamel(t *testing.T) { + toCamel(t) +} + +func BenchmarkToCamel(b *testing.B) { + benchmarkCamelTest(b, toCamel) +} + +func toLowerCamel(tb testing.TB) { cases := [][]string{ {"foo-bar", "fooBar"}, {"TestCase", "testCase"}, @@ -66,7 +74,21 @@ func TestToLowerCamel(t *testing.T) { out := i[1] result := ToLowerCamel(in) if result != out { - t.Error("'" + result + "' != '" + out + "'") + tb.Error("'" + result + "' != '" + out + "'") } } } + +func TestToLowerCamel(t *testing.T) { + toLowerCamel(t) +} + +func BenchmarkToLowerCamel(b *testing.B) { + benchmarkCamelTest(b, toLowerCamel) +} + +func benchmarkCamelTest(b *testing.B, fn func(testing.TB)) { + for n := 0; n < b.N; n++ { + fn(b) + } +} diff --git a/snake_test.go b/snake_test.go index 92f1249..fbdb29e 100644 --- a/snake_test.go +++ b/snake_test.go @@ -29,7 +29,7 @@ import ( "testing" ) -func TestToSnake(t *testing.T) { +func toSnake(tb testing.TB) { cases := [][]string{ {"testCase", "test_case"}, {"TestCase", "test_case"}, @@ -54,11 +54,18 @@ func TestToSnake(t *testing.T) { out := i[1] result := ToSnake(in) if result != out { - t.Error("'" + in + "'('" + result + "' != '" + out + "')") + tb.Error("'" + in + "'('" + result + "' != '" + out + "')") } } } -func TestToSnakeWithIgnore(t *testing.T) { + +func TestToSnake(t *testing.T) { toSnake(t) } + +func BenchmarkToSnake(b *testing.B) { + benchmarkSnakeTest(b, toSnake) +} + +func toSnakeWithIgnore(tb testing.TB) { cases := [][]string{ {"testCase", "test_case"}, {"TestCase", "test_case"}, @@ -88,12 +95,18 @@ func TestToSnakeWithIgnore(t *testing.T) { } result := ToSnakeWithIgnore(in, ignore) if result != out { - t.Error("'" + in + "'('" + result + "' != '" + out + "')") + tb.Error("'" + in + "'('" + result + "' != '" + out + "')") } } } -func TestToDelimited(t *testing.T) { +func TestToSnakeWithIgnore(t *testing.T) { toSnakeWithIgnore(t) } + +func BenchmarkToSnakeWithIgnore(b *testing.B) { + benchmarkSnakeTest(b, toSnakeWithIgnore) +} + +func toDelimited(tb testing.TB) { cases := [][]string{ {"testCase", "test@case"}, {"TestCase", "test@case"}, @@ -119,12 +132,18 @@ func TestToDelimited(t *testing.T) { out := i[1] result := ToDelimited(in, '@') if result != out { - t.Error("'" + in + "' ('" + result + "' != '" + out + "')") + tb.Error("'" + in + "' ('" + result + "' != '" + out + "')") } } } -func TestToScreamingSnake(t *testing.T) { +func TestToDelimited(t *testing.T) { toDelimited(t) } + +func BenchmarkToDelimited(b *testing.B) { + benchmarkSnakeTest(b, toDelimited) +} + +func toScreamingSnake(tb testing.TB) { cases := [][]string{ {"testCase", "TEST_CASE"}, } @@ -133,12 +152,18 @@ func TestToScreamingSnake(t *testing.T) { out := i[1] result := ToScreamingSnake(in) if result != out { - t.Error("'" + result + "' != '" + out + "'") + tb.Error("'" + result + "' != '" + out + "'") } } } -func TestToKebab(t *testing.T) { +func TestToScreamingSnake(t *testing.T) { toScreamingSnake(t) } + +func BenchmarkToScreamingSnake(b *testing.B) { + benchmarkSnakeTest(b, toScreamingSnake) +} + +func toKebab(tb testing.TB) { cases := [][]string{ {"testCase", "test-case"}, } @@ -147,12 +172,18 @@ func TestToKebab(t *testing.T) { out := i[1] result := ToKebab(in) if result != out { - t.Error("'" + result + "' != '" + out + "'") + tb.Error("'" + result + "' != '" + out + "'") } } } -func TestToScreamingKebab(t *testing.T) { +func TestToKebab(t *testing.T) { toKebab(t) } + +func BenchmarkToKebab(b *testing.B) { + benchmarkSnakeTest(b, toKebab) +} + +func toScreamingKebab(tb testing.TB) { cases := [][]string{ {"testCase", "TEST-CASE"}, } @@ -161,12 +192,18 @@ func TestToScreamingKebab(t *testing.T) { out := i[1] result := ToScreamingKebab(in) if result != out { - t.Error("'" + result + "' != '" + out + "'") + tb.Error("'" + result + "' != '" + out + "'") } } } -func TestToScreamingDelimited(t *testing.T) { +func TestToScreamingKebab(t *testing.T) { toScreamingKebab(t) } + +func BenchmarkToScreamingKebab(b *testing.B) { + benchmarkSnakeTest(b, toScreamingKebab) +} + +func toScreamingDelimited(tb testing.TB) { cases := [][]string{ {"testCase", "TEST.CASE"}, } @@ -175,12 +212,18 @@ func TestToScreamingDelimited(t *testing.T) { out := i[1] result := ToScreamingDelimited(in, '.', 0, true) if result != out { - t.Error("'" + result + "' != '" + out + "'") + tb.Error("'" + result + "' != '" + out + "'") } } } -func TestToScreamingDelimitedWithIgnore(t *testing.T) { +func TestToScreamingDelimited(t *testing.T) { toScreamingDelimited(t) } + +func BenchmarkToScreamingDelimited(b *testing.B) { + benchmarkSnakeTest(b, toScreamingDelimited) +} + +func toScreamingDelimitedWithIgnore(tb testing.TB) { cases := [][]string{ {"AnyKind of_string", "ANY.KIND OF.STRING", ".", " "}, } @@ -191,7 +234,19 @@ func TestToScreamingDelimitedWithIgnore(t *testing.T) { ignore := uint8(i[3][0]) result := ToScreamingDelimited(in, delimiter, ignore, true) if result != out { - t.Error("'" + result + "' != '" + out + "'") + tb.Error("'" + result + "' != '" + out + "'") } } } + +func TestToScreamingDelimitedWithIgnore(t *testing.T) { toScreamingDelimitedWithIgnore(t) } + +func BenchmarkToScreamingDelimitedWithIgnore(b *testing.B) { + benchmarkSnakeTest(b, toScreamingDelimitedWithIgnore) +} + +func benchmarkSnakeTest(b *testing.B, fn func(testing.TB)) { + for n := 0; n < b.N; n++ { + fn(b) + } +} From 486369db5a693767c407c57f0975767b5b65942d Mon Sep 17 00:00:00 2001 From: Nathan Baulch Date: Sun, 16 Aug 2020 22:10:31 +1000 Subject: [PATCH 2/5] Minor linting and readability improvements --- camel.go | 27 ++++++++------------------- camel_test.go | 4 ++-- snake.go | 40 ++++++++++++++++++---------------------- snake_test.go | 32 ++++++++++++++++++++------------ 4 files changed, 48 insertions(+), 55 deletions(-) diff --git a/camel.go b/camel.go index 4aab39a..bd55a1d 100644 --- a/camel.go +++ b/camel.go @@ -32,28 +32,18 @@ import ( // Converts a string to CamelCase func toCamelInitCase(s string, initCase bool) string { s = addWordBoundariesToNumbers(s) - s = strings.Trim(s, " ") n := "" capNext := initCase for _, v := range s { - if v >= 'A' && v <= 'Z' { + if capNext && v >= 'a' && v <= 'z' { + v = int32(strings.ToUpper(string(v))[0]) + } + if (v >= 'A' && v <= 'Z') || + (v >= 'a' && v <= 'z') || + (v >= '0' && v <= '9') { n += string(v) } - if v >= '0' && v <= '9' { - n += string(v) - } - if v >= 'a' && v <= 'z' { - if capNext { - n += strings.ToUpper(string(v)) - } else { - n += string(v) - } - } - if v == '_' || v == ' ' || v == '-' || v == '.' { - capNext = true - } else { - capNext = false - } + capNext = v == '_' || v == ' ' || v == '-' || v == '.' } return n } @@ -73,8 +63,7 @@ func ToLowerCamel(s string) string { } if uppercaseAcronym[s] { s = strings.ToLower(s) - } - if r := rune(s[0]); r >= 'A' && r <= 'Z' { + } else if r := s[0]; r >= 'A' && r <= 'Z' { s = strings.ToLower(string(r)) + s[1:] } return toCamelInitCase(s, false) diff --git a/camel_test.go b/camel_test.go index c5a0101..bacf8f7 100644 --- a/camel_test.go +++ b/camel_test.go @@ -47,7 +47,7 @@ func toCamel(tb testing.TB) { out := i[1] result := ToCamel(in) if result != out { - tb.Error("'" + result + "' != '" + out + "'") + tb.Errorf("%q (%q != %q)", in, result, out) } } } @@ -74,7 +74,7 @@ func toLowerCamel(tb testing.TB) { out := i[1] result := ToLowerCamel(in) if result != out { - tb.Error("'" + result + "' != '" + out + "'") + tb.Errorf("%q (%q != %q)", in, result, out) } } } diff --git a/snake.go b/snake.go index 8e8867b..30223f4 100644 --- a/snake.go +++ b/snake.go @@ -31,11 +31,10 @@ import ( // ToSnake converts a string to snake_case func ToSnake(s string) string { - return ToDelimited(s, '_') } -func ToSnakeWithIgnore(s string, ignore uint8) string { +func ToSnakeWithIgnore(s string, ignore uint8) string { return ToScreamingDelimited(s, '_', ignore, false) } @@ -70,37 +69,34 @@ func ToScreamingDelimited(s string, delimiter uint8, ignore uint8, screaming boo n := "" for i, v := range s { // treat acronyms as words, eg for JSONData -> JSON is a whole word - nextCaseIsChanged := false if i+1 < len(s) { next := s[i+1] vIsCap := v >= 'A' && v <= 'Z' vIsLow := v >= 'a' && v <= 'z' nextIsCap := next >= 'A' && next <= 'Z' nextIsLow := next >= 'a' && next <= 'z' + // add underscore if next letter case type is changed if (vIsCap && nextIsLow) || (vIsLow && nextIsCap) { - nextCaseIsChanged = true - } - if ignore > 0 && i-1 >= 0 && s[i-1] == ignore && nextCaseIsChanged { - nextCaseIsChanged = false + if prevIgnore := ignore > 0 && i > 0 && s[i-1] == ignore; !prevIgnore { + if i > 0 && vIsCap && nextIsLow { + if prevDelim := len(n) > 0 && n[len(n)-1] == delimiter; !prevDelim { + n += string(delimiter) + } + } + n += string(v) + if vIsLow { + n += string(delimiter) + } + continue + } } } - if i > 0 && n[len(n)-1] != delimiter && nextCaseIsChanged { - // add underscore if next letter case type is changed - if v >= 'A' && v <= 'Z' { - n += string(delimiter) + string(v) - } else if v >= 'a' && v <= 'z' { - n += string(v) + string(delimiter) - } - } else if v == ' ' || v == '_' || v == '-' { - // replace spaces/underscores with delimiters - if uint8(v) == ignore { - n += string(v) - } else { - n += string(delimiter) - } + if (v == ' ' || v == '_' || v == '-') && uint8(v) != ignore { + // replace space/underscore/hyphen with delimiter + n += string(delimiter) } else { - n = n + string(v) + n += string(v) } } diff --git a/snake_test.go b/snake_test.go index fbdb29e..7f89e4b 100644 --- a/snake_test.go +++ b/snake_test.go @@ -54,7 +54,7 @@ func toSnake(tb testing.TB) { out := i[1] result := ToSnake(in) if result != out { - tb.Error("'" + in + "'('" + result + "' != '" + out + "')") + tb.Errorf("%q (%q != %q)", in, result, out) } } } @@ -82,8 +82,8 @@ func toSnakeWithIgnore(tb testing.TB) { {"AnyKind of_string", "any_kind_of_string"}, {"numbers2and55with000", "numbers_2_and_55_with_000"}, {"JSONData", "json_data"}, - {"AwesomeAcitvity.UserID", "awesome_acitvity.user_id", "."}, - {"AwesomeAcitvity.User.Id", "awesome_acitvity.user.id", "."}, + {"AwesomeActivity.UserID", "awesome_activity.user_id", "."}, + {"AwesomeActivity.User.Id", "awesome_activity.user.id", "."}, {"AwesomeUsername@Awesome.Com", "awesome_username@awesome._com", "@"}, } for _, i := range cases { @@ -95,7 +95,11 @@ func toSnakeWithIgnore(tb testing.TB) { } result := ToSnakeWithIgnore(in, ignore) if result != out { - tb.Error("'" + in + "'('" + result + "' != '" + out + "')") + istr := "" + if len(i) == 3 { + istr = " ignoring '" + i[2] + "'" + } + tb.Errorf("%q (%q != %q%s)", in, result, out, istr) } } } @@ -132,7 +136,7 @@ func toDelimited(tb testing.TB) { out := i[1] result := ToDelimited(in, '@') if result != out { - tb.Error("'" + in + "' ('" + result + "' != '" + out + "')") + tb.Errorf("%q (%q != %q)", in, result, out) } } } @@ -152,7 +156,7 @@ func toScreamingSnake(tb testing.TB) { out := i[1] result := ToScreamingSnake(in) if result != out { - tb.Error("'" + result + "' != '" + out + "'") + tb.Errorf("%q (%q != %q)", in, result, out) } } } @@ -172,7 +176,7 @@ func toKebab(tb testing.TB) { out := i[1] result := ToKebab(in) if result != out { - tb.Error("'" + result + "' != '" + out + "'") + tb.Errorf("%q (%q != %q)", in, result, out) } } } @@ -192,7 +196,7 @@ func toScreamingKebab(tb testing.TB) { out := i[1] result := ToScreamingKebab(in) if result != out { - tb.Error("'" + result + "' != '" + out + "'") + tb.Errorf("%q (%q != %q)", in, result, out) } } } @@ -212,7 +216,7 @@ func toScreamingDelimited(tb testing.TB) { out := i[1] result := ToScreamingDelimited(in, '.', 0, true) if result != out { - tb.Error("'" + result + "' != '" + out + "'") + tb.Errorf("%q (%q != %q)", in, result, out) } } } @@ -230,11 +234,15 @@ func toScreamingDelimitedWithIgnore(tb testing.TB) { for _, i := range cases { in := i[0] out := i[1] - delimiter := uint8(i[2][0]) - ignore := uint8(i[3][0]) + delimiter := i[2][0] + ignore := i[3][0] result := ToScreamingDelimited(in, delimiter, ignore, true) if result != out { - tb.Error("'" + result + "' != '" + out + "'") + istr := "" + if len(i) == 4 { + istr = " ignoring '" + i[3] + "'" + } + tb.Errorf("%q (%q != %q%s)", in, result, out, istr) } } } From a6b8dcde35569b34185fc011be4f0f41cf393f3d Mon Sep 17 00:00:00 2001 From: Nathan Baulch Date: Sun, 16 Aug 2020 22:35:25 +1000 Subject: [PATCH 3/5] Fix incorrect delimiter between dot and cap --- snake.go | 4 ++-- snake_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/snake.go b/snake.go index 30223f4..159c1b5 100644 --- a/snake.go +++ b/snake.go @@ -78,8 +78,8 @@ func ToScreamingDelimited(s string, delimiter uint8, ignore uint8, screaming boo // add underscore if next letter case type is changed if (vIsCap && nextIsLow) || (vIsLow && nextIsCap) { if prevIgnore := ignore > 0 && i > 0 && s[i-1] == ignore; !prevIgnore { - if i > 0 && vIsCap && nextIsLow { - if prevDelim := len(n) > 0 && n[len(n)-1] == delimiter; !prevDelim { + if vIsCap && nextIsLow { + if prevIsCap := i > 0 && s[i-1] >= 'A' && s[i-1] <= 'Z'; prevIsCap { n += string(delimiter) } } diff --git a/snake_test.go b/snake_test.go index 7f89e4b..5482999 100644 --- a/snake_test.go +++ b/snake_test.go @@ -84,7 +84,7 @@ func toSnakeWithIgnore(tb testing.TB) { {"JSONData", "json_data"}, {"AwesomeActivity.UserID", "awesome_activity.user_id", "."}, {"AwesomeActivity.User.Id", "awesome_activity.user.id", "."}, - {"AwesomeUsername@Awesome.Com", "awesome_username@awesome._com", "@"}, + {"AwesomeUsername@Awesome.Com", "awesome_username@awesome.com", "@"}, } for _, i := range cases { in := i[0] From 820267b5d99440a88b47090af44de9127893c402 Mon Sep 17 00:00:00 2001 From: Nathan Baulch Date: Sun, 16 Aug 2020 22:42:39 +1000 Subject: [PATCH 4/5] Performance optimizations --- acronyms.go | 4 ++-- camel.go | 57 ++++++++++++++++++++++++++++++----------------------- snake.go | 53 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 71 insertions(+), 43 deletions(-) diff --git a/acronyms.go b/acronyms.go index 3b06064..ecb533e 100644 --- a/acronyms.go +++ b/acronyms.go @@ -1,5 +1,5 @@ package strcase -var uppercaseAcronym = map[string]bool{ - "ID": true, +var uppercaseAcronym = map[string]string{ + "ID": "id", } diff --git a/camel.go b/camel.go index bd55a1d..ab2d261 100644 --- a/camel.go +++ b/camel.go @@ -31,40 +31,47 @@ import ( // Converts a string to CamelCase func toCamelInitCase(s string, initCase bool) string { - s = addWordBoundariesToNumbers(s) - n := "" - capNext := initCase - for _, v := range s { - if capNext && v >= 'a' && v <= 'z' { - v = int32(strings.ToUpper(string(v))[0]) - } - if (v >= 'A' && v <= 'Z') || - (v >= 'a' && v <= 'z') || - (v >= '0' && v <= '9') { - n += string(v) - } - capNext = v == '_' || v == ' ' || v == '-' || v == '.' + if s == "" { + return s } - return n + if a, ok := uppercaseAcronym[s]; ok { + s = a + } + + s = addWordBoundariesToNumbers(s) + n := strings.Builder{} + n.Grow(len(s)) + capNext := initCase + for i, v := range []byte(s) { + vIsCap := v >= 'A' && v <= 'Z' + vIsLow := v >= 'a' && v <= 'z' + if capNext { + if vIsLow { + v += 'A' + v -= 'a' + } + } else if i == 0 { + if vIsCap { + v += 'a' + v -= 'A' + } + } + if vIsCap || vIsLow || (v >= '0' && v <= '9') { + n.WriteByte(v) + capNext = false + } else { + capNext = v == '_' || v == ' ' || v == '-' || v == '.' + } + } + return n.String() } // ToCamel converts a string to CamelCase func ToCamel(s string) string { - if uppercaseAcronym[s] { - s = strings.ToLower(s) - } return toCamelInitCase(s, true) } // ToLowerCamel converts a string to lowerCamelCase func ToLowerCamel(s string) string { - if s == "" { - return s - } - if uppercaseAcronym[s] { - s = strings.ToLower(s) - } else if r := s[0]; r >= 'A' && r <= 'Z' { - s = strings.ToLower(string(r)) + s[1:] - } return toCamelInitCase(s, false) } diff --git a/snake.go b/snake.go index 159c1b5..fa1f8f4 100644 --- a/snake.go +++ b/snake.go @@ -65,14 +65,40 @@ func ToDelimited(s string, delimiter uint8) string { // (in this case `delimiter = '.'; screaming = false`) func ToScreamingDelimited(s string, delimiter uint8, ignore uint8, screaming bool) string { s = addWordBoundariesToNumbers(s) - s = strings.Trim(s, " ") - n := "" - for i, v := range s { + n := strings.Builder{} + n.Grow(len(s) + 2) // nominal 2 bytes of extra space for inserted delimiters + start := true + spaces := 0 + for i, v := range []byte(s) { + if v == ' ' { + spaces++ + continue + } else if start { + start = false + spaces = 0 + } else { + for ; spaces > 0; spaces-- { + if ignore == ' ' { + n.WriteByte(' ') + } else { + n.WriteByte(delimiter) + } + } + } + + vIsCap := v >= 'A' && v <= 'Z' + vIsLow := v >= 'a' && v <= 'z' + if vIsLow && screaming { + v += 'A' + v -= 'a' + } else if vIsCap && !screaming { + v += 'a' + v -= 'A' + } + // treat acronyms as words, eg for JSONData -> JSON is a whole word if i+1 < len(s) { next := s[i+1] - vIsCap := v >= 'A' && v <= 'Z' - vIsLow := v >= 'a' && v <= 'z' nextIsCap := next >= 'A' && next <= 'Z' nextIsLow := next >= 'a' && next <= 'z' // add underscore if next letter case type is changed @@ -80,12 +106,12 @@ func ToScreamingDelimited(s string, delimiter uint8, ignore uint8, screaming boo if prevIgnore := ignore > 0 && i > 0 && s[i-1] == ignore; !prevIgnore { if vIsCap && nextIsLow { if prevIsCap := i > 0 && s[i-1] >= 'A' && s[i-1] <= 'Z'; prevIsCap { - n += string(delimiter) + n.WriteByte(delimiter) } } - n += string(v) + n.WriteByte(v) if vIsLow { - n += string(delimiter) + n.WriteByte(delimiter) } continue } @@ -94,16 +120,11 @@ func ToScreamingDelimited(s string, delimiter uint8, ignore uint8, screaming boo if (v == ' ' || v == '_' || v == '-') && uint8(v) != ignore { // replace space/underscore/hyphen with delimiter - n += string(delimiter) + n.WriteByte(delimiter) } else { - n += string(v) + n.WriteByte(v) } } - if screaming { - n = strings.ToUpper(n) - } else { - n = strings.ToLower(n) - } - return n + return n.String() } From 6fab6530048ce8268098c5ef39c193028c5947ec Mon Sep 17 00:00:00 2001 From: Nathan Baulch Date: Sun, 16 Aug 2020 23:02:43 +1000 Subject: [PATCH 5/5] Remove problematic numbers regexp --- camel.go | 6 ++++-- numbers.go | 38 -------------------------------------- snake.go | 7 ++++--- snake_test.go | 6 ++++++ 4 files changed, 14 insertions(+), 43 deletions(-) delete mode 100644 numbers.go diff --git a/camel.go b/camel.go index ab2d261..f31646e 100644 --- a/camel.go +++ b/camel.go @@ -38,7 +38,6 @@ func toCamelInitCase(s string, initCase bool) string { s = a } - s = addWordBoundariesToNumbers(s) n := strings.Builder{} n.Grow(len(s)) capNext := initCase @@ -56,9 +55,12 @@ func toCamelInitCase(s string, initCase bool) string { v -= 'A' } } - if vIsCap || vIsLow || (v >= '0' && v <= '9') { + if vIsCap || vIsLow { n.WriteByte(v) capNext = false + } else if vIsNum := v >= '0' && v <= '9'; vIsNum { + n.WriteByte(v) + capNext = true } else { capNext = v == '_' || v == ' ' || v == '-' || v == '.' } diff --git a/numbers.go b/numbers.go deleted file mode 100644 index fdf07cb..0000000 --- a/numbers.go +++ /dev/null @@ -1,38 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 Ian Coleman - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, Subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or Substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package strcase - -import ( - "regexp" -) - -var numberSequence = regexp.MustCompile(`([a-zA-Z])(\d+)([a-zA-Z]?)`) -var numberReplacement = []byte(`$1 $2 $3`) - -func addWordBoundariesToNumbers(s string) string { - b := []byte(s) - b = numberSequence.ReplaceAll(b, numberReplacement) - return string(b) -} diff --git a/snake.go b/snake.go index fa1f8f4..b849a58 100644 --- a/snake.go +++ b/snake.go @@ -64,7 +64,6 @@ func ToDelimited(s string, delimiter uint8) string { // or delimited.snake.case // (in this case `delimiter = '.'; screaming = false`) func ToScreamingDelimited(s string, delimiter uint8, ignore uint8, screaming bool) string { - s = addWordBoundariesToNumbers(s) n := strings.Builder{} n.Grow(len(s) + 2) // nominal 2 bytes of extra space for inserted delimiters start := true @@ -99,10 +98,12 @@ func ToScreamingDelimited(s string, delimiter uint8, ignore uint8, screaming boo // treat acronyms as words, eg for JSONData -> JSON is a whole word if i+1 < len(s) { next := s[i+1] + vIsNum := v >= '0' && v <= '9' nextIsCap := next >= 'A' && next <= 'Z' nextIsLow := next >= 'a' && next <= 'z' + nextIsNum := next >= '0' && next <= '9' // add underscore if next letter case type is changed - if (vIsCap && nextIsLow) || (vIsLow && nextIsCap) { + if (vIsCap && (nextIsLow || nextIsNum)) || (vIsLow && (nextIsCap || nextIsNum)) || (vIsNum && (nextIsCap || nextIsLow)) { if prevIgnore := ignore > 0 && i > 0 && s[i-1] == ignore; !prevIgnore { if vIsCap && nextIsLow { if prevIsCap := i > 0 && s[i-1] >= 'A' && s[i-1] <= 'Z'; prevIsCap { @@ -110,7 +111,7 @@ func ToScreamingDelimited(s string, delimiter uint8, ignore uint8, screaming boo } } n.WriteByte(v) - if vIsLow { + if vIsLow || vIsNum || nextIsNum { n.WriteByte(delimiter) } continue diff --git a/snake_test.go b/snake_test.go index 5482999..2e34372 100644 --- a/snake_test.go +++ b/snake_test.go @@ -48,6 +48,12 @@ func toSnake(tb testing.TB) { {"JSONData", "json_data"}, {"userID", "user_id"}, {"AAAbbb", "aa_abbb"}, + {"1A2", "1_a_2"}, + {"A1B", "a_1_b"}, + {"A1A2A3", "a_1_a_2_a_3"}, + {"A1 A2 A3", "a_1_a_2_a_3"}, + {"AB1AB2AB3", "ab_1_ab_2_ab_3"}, + {"AB1 AB2 AB3", "ab_1_ab_2_ab_3"}, } for _, i := range cases { in := i[0]