Merge pull request #24 from NathanBaulch/master

Better number handling and general performance
This commit is contained in:
iancoleman 2020-08-18 09:00:22 +10:00 committed by GitHub
commit 23e9d4e5c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 200 additions and 131 deletions

View File

@ -1,5 +1,5 @@
package strcase package strcase
var uppercaseAcronym = map[string]bool{ var uppercaseAcronym = map[string]string{
"ID": true, "ID": "id",
} }

View File

@ -31,51 +31,49 @@ import (
// Converts a string to CamelCase // Converts a string to CamelCase
func toCamelInitCase(s string, initCase bool) string { func toCamelInitCase(s string, initCase bool) string {
s = addWordBoundariesToNumbers(s) if s == "" {
s = strings.Trim(s, " ") return s
n := "" }
if a, ok := uppercaseAcronym[s]; ok {
s = a
}
n := strings.Builder{}
n.Grow(len(s))
capNext := initCase capNext := initCase
for _, v := range s { for i, v := range []byte(s) {
if v >= 'A' && v <= 'Z' { vIsCap := v >= 'A' && v <= 'Z'
n += string(v) vIsLow := v >= 'a' && v <= 'z'
} if capNext {
if v >= '0' && v <= '9' { if vIsLow {
n += string(v) v += 'A'
} v -= 'a'
if v >= 'a' && v <= 'z' { }
if capNext { } else if i == 0 {
n += strings.ToUpper(string(v)) if vIsCap {
} else { v += 'a'
n += string(v) v -= 'A'
} }
} }
if v == '_' || v == ' ' || v == '-' || v == '.' { if vIsCap || vIsLow {
n.WriteByte(v)
capNext = false
} else if vIsNum := v >= '0' && v <= '9'; vIsNum {
n.WriteByte(v)
capNext = true capNext = true
} else { } else {
capNext = false capNext = v == '_' || v == ' ' || v == '-' || v == '.'
} }
} }
return n return n.String()
} }
// ToCamel converts a string to CamelCase // ToCamel converts a string to CamelCase
func ToCamel(s string) string { func ToCamel(s string) string {
if uppercaseAcronym[s] {
s = strings.ToLower(s)
}
return toCamelInitCase(s, true) return toCamelInitCase(s, true)
} }
// ToLowerCamel converts a string to lowerCamelCase // ToLowerCamel converts a string to lowerCamelCase
func ToLowerCamel(s string) string { func ToLowerCamel(s string) string {
if s == "" {
return s
}
if uppercaseAcronym[s] {
s = strings.ToLower(s)
}
if r := rune(s[0]); r >= 'A' && r <= 'Z' {
s = strings.ToLower(string(r)) + s[1:]
}
return toCamelInitCase(s, false) return toCamelInitCase(s, false)
} }

View File

@ -28,7 +28,7 @@ import (
"testing" "testing"
) )
func TestToCamel(t *testing.T) { func toCamel(tb testing.TB) {
cases := [][]string{ cases := [][]string{
{"test_case", "TestCase"}, {"test_case", "TestCase"},
{"test.case", "TestCase"}, {"test.case", "TestCase"},
@ -47,12 +47,20 @@ func TestToCamel(t *testing.T) {
out := i[1] out := i[1]
result := ToCamel(in) result := ToCamel(in)
if result != out { if result != out {
t.Error("'" + result + "' != '" + out + "'") tb.Errorf("%q (%q != %q)", in, 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{ cases := [][]string{
{"foo-bar", "fooBar"}, {"foo-bar", "fooBar"},
{"TestCase", "testCase"}, {"TestCase", "testCase"},
@ -66,7 +74,21 @@ func TestToLowerCamel(t *testing.T) {
out := i[1] out := i[1]
result := ToLowerCamel(in) result := ToLowerCamel(in)
if result != out { if result != out {
t.Error("'" + result + "' != '" + out + "'") tb.Errorf("%q (%q != %q)", in, 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)
}
}

View File

@ -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)
}

View File

@ -31,11 +31,10 @@ import (
// ToSnake converts a string to snake_case // ToSnake converts a string to snake_case
func ToSnake(s string) string { func ToSnake(s string) string {
return ToDelimited(s, '_') return ToDelimited(s, '_')
} }
func ToSnakeWithIgnore(s string, ignore uint8) string {
func ToSnakeWithIgnore(s string, ignore uint8) string {
return ToScreamingDelimited(s, '_', ignore, false) return ToScreamingDelimited(s, '_', ignore, false)
} }
@ -65,49 +64,68 @@ func ToDelimited(s string, delimiter uint8) string {
// or delimited.snake.case // or delimited.snake.case
// (in this case `delimiter = '.'; screaming = false`) // (in this case `delimiter = '.'; screaming = false`)
func ToScreamingDelimited(s string, delimiter uint8, ignore uint8, screaming bool) string { func ToScreamingDelimited(s string, delimiter uint8, ignore uint8, screaming bool) string {
s = addWordBoundariesToNumbers(s) n := strings.Builder{}
s = strings.Trim(s, " ") n.Grow(len(s) + 2) // nominal 2 bytes of extra space for inserted delimiters
n := "" start := true
for i, v := range s { 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 // treat acronyms as words, eg for JSONData -> JSON is a whole word
nextCaseIsChanged := false
if i+1 < len(s) { if i+1 < len(s) {
next := s[i+1] next := s[i+1]
vIsCap := v >= 'A' && v <= 'Z' vIsNum := v >= '0' && v <= '9'
vIsLow := v >= 'a' && v <= 'z'
nextIsCap := next >= 'A' && next <= 'Z' nextIsCap := next >= 'A' && next <= 'Z'
nextIsLow := next >= 'a' && next <= 'z' nextIsLow := next >= 'a' && next <= 'z'
if (vIsCap && nextIsLow) || (vIsLow && nextIsCap) { nextIsNum := next >= '0' && next <= '9'
nextCaseIsChanged = true
}
if ignore > 0 && i-1 >= 0 && s[i-1] == ignore && nextCaseIsChanged {
nextCaseIsChanged = false
}
}
if i > 0 && n[len(n)-1] != delimiter && nextCaseIsChanged {
// add underscore if next letter case type is changed // add underscore if next letter case type is changed
if v >= 'A' && v <= 'Z' { if (vIsCap && (nextIsLow || nextIsNum)) || (vIsLow && (nextIsCap || nextIsNum)) || (vIsNum && (nextIsCap || nextIsLow)) {
n += string(delimiter) + string(v) if prevIgnore := ignore > 0 && i > 0 && s[i-1] == ignore; !prevIgnore {
} else if v >= 'a' && v <= 'z' { if vIsCap && nextIsLow {
n += string(v) + string(delimiter) if prevIsCap := i > 0 && s[i-1] >= 'A' && s[i-1] <= 'Z'; prevIsCap {
} n.WriteByte(delimiter)
} else if v == ' ' || v == '_' || v == '-' { }
// replace spaces/underscores with delimiters }
if uint8(v) == ignore { n.WriteByte(v)
n += string(v) if vIsLow || vIsNum || nextIsNum {
} else { n.WriteByte(delimiter)
n += string(delimiter) }
continue
}
} }
}
if (v == ' ' || v == '_' || v == '-') && uint8(v) != ignore {
// replace space/underscore/hyphen with delimiter
n.WriteByte(delimiter)
} else { } else {
n = n + string(v) n.WriteByte(v)
} }
} }
if screaming { return n.String()
n = strings.ToUpper(n)
} else {
n = strings.ToLower(n)
}
return n
} }

View File

@ -29,7 +29,7 @@ import (
"testing" "testing"
) )
func TestToSnake(t *testing.T) { func toSnake(tb testing.TB) {
cases := [][]string{ cases := [][]string{
{"testCase", "test_case"}, {"testCase", "test_case"},
{"TestCase", "test_case"}, {"TestCase", "test_case"},
@ -48,17 +48,30 @@ func TestToSnake(t *testing.T) {
{"JSONData", "json_data"}, {"JSONData", "json_data"},
{"userID", "user_id"}, {"userID", "user_id"},
{"AAAbbb", "aa_abbb"}, {"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 { for _, i := range cases {
in := i[0] in := i[0]
out := i[1] out := i[1]
result := ToSnake(in) result := ToSnake(in)
if result != out { if result != out {
t.Error("'" + in + "'('" + result + "' != '" + out + "')") tb.Errorf("%q (%q != %q)", 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{ cases := [][]string{
{"testCase", "test_case"}, {"testCase", "test_case"},
{"TestCase", "test_case"}, {"TestCase", "test_case"},
@ -75,9 +88,9 @@ func TestToSnakeWithIgnore(t *testing.T) {
{"AnyKind of_string", "any_kind_of_string"}, {"AnyKind of_string", "any_kind_of_string"},
{"numbers2and55with000", "numbers_2_and_55_with_000"}, {"numbers2and55with000", "numbers_2_and_55_with_000"},
{"JSONData", "json_data"}, {"JSONData", "json_data"},
{"AwesomeAcitvity.UserID", "awesome_acitvity.user_id", "."}, {"AwesomeActivity.UserID", "awesome_activity.user_id", "."},
{"AwesomeAcitvity.User.Id", "awesome_acitvity.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 { for _, i := range cases {
in := i[0] in := i[0]
@ -88,12 +101,22 @@ func TestToSnakeWithIgnore(t *testing.T) {
} }
result := ToSnakeWithIgnore(in, ignore) result := ToSnakeWithIgnore(in, ignore)
if result != out { if result != out {
t.Error("'" + in + "'('" + result + "' != '" + out + "')") istr := ""
if len(i) == 3 {
istr = " ignoring '" + i[2] + "'"
}
tb.Errorf("%q (%q != %q%s)", in, result, out, istr)
} }
} }
} }
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{ cases := [][]string{
{"testCase", "test@case"}, {"testCase", "test@case"},
{"TestCase", "test@case"}, {"TestCase", "test@case"},
@ -119,12 +142,18 @@ func TestToDelimited(t *testing.T) {
out := i[1] out := i[1]
result := ToDelimited(in, '@') result := ToDelimited(in, '@')
if result != out { if result != out {
t.Error("'" + in + "' ('" + result + "' != '" + out + "')") tb.Errorf("%q (%q != %q)", 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{ cases := [][]string{
{"testCase", "TEST_CASE"}, {"testCase", "TEST_CASE"},
} }
@ -133,12 +162,18 @@ func TestToScreamingSnake(t *testing.T) {
out := i[1] out := i[1]
result := ToScreamingSnake(in) result := ToScreamingSnake(in)
if result != out { if result != out {
t.Error("'" + result + "' != '" + out + "'") tb.Errorf("%q (%q != %q)", in, 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{ cases := [][]string{
{"testCase", "test-case"}, {"testCase", "test-case"},
} }
@ -147,12 +182,18 @@ func TestToKebab(t *testing.T) {
out := i[1] out := i[1]
result := ToKebab(in) result := ToKebab(in)
if result != out { if result != out {
t.Error("'" + result + "' != '" + out + "'") tb.Errorf("%q (%q != %q)", in, 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{ cases := [][]string{
{"testCase", "TEST-CASE"}, {"testCase", "TEST-CASE"},
} }
@ -161,12 +202,18 @@ func TestToScreamingKebab(t *testing.T) {
out := i[1] out := i[1]
result := ToScreamingKebab(in) result := ToScreamingKebab(in)
if result != out { if result != out {
t.Error("'" + result + "' != '" + out + "'") tb.Errorf("%q (%q != %q)", in, 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{ cases := [][]string{
{"testCase", "TEST.CASE"}, {"testCase", "TEST.CASE"},
} }
@ -175,23 +222,45 @@ func TestToScreamingDelimited(t *testing.T) {
out := i[1] out := i[1]
result := ToScreamingDelimited(in, '.', 0, true) result := ToScreamingDelimited(in, '.', 0, true)
if result != out { if result != out {
t.Error("'" + result + "' != '" + out + "'") tb.Errorf("%q (%q != %q)", in, 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{ cases := [][]string{
{"AnyKind of_string", "ANY.KIND OF.STRING", ".", " "}, {"AnyKind of_string", "ANY.KIND OF.STRING", ".", " "},
} }
for _, i := range cases { for _, i := range cases {
in := i[0] in := i[0]
out := i[1] out := i[1]
delimiter := uint8(i[2][0]) delimiter := i[2][0]
ignore := uint8(i[3][0]) ignore := i[3][0]
result := ToScreamingDelimited(in, delimiter, ignore, true) result := ToScreamingDelimited(in, delimiter, ignore, true)
if result != out { if result != out {
t.Error("'" + result + "' != '" + out + "'") istr := ""
if len(i) == 4 {
istr = " ignoring '" + i[3] + "'"
}
tb.Errorf("%q (%q != %q%s)", in, result, out, istr)
} }
} }
} }
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)
}
}