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
var uppercaseAcronym = map[string]bool{
"ID": true,
var uppercaseAcronym = map[string]string{
"ID": "id",
}

View File

@ -31,51 +31,49 @@ import (
// Converts a string to CamelCase
func toCamelInitCase(s string, initCase bool) string {
s = addWordBoundariesToNumbers(s)
s = strings.Trim(s, " ")
n := ""
if s == "" {
return s
}
if a, ok := uppercaseAcronym[s]; ok {
s = a
}
n := strings.Builder{}
n.Grow(len(s))
capNext := initCase
for _, v := range s {
if v >= 'A' && v <= 'Z' {
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)
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 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
} else {
capNext = false
capNext = v == '_' || v == ' ' || v == '-' || v == '.'
}
}
return n
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)
}
if r := rune(s[0]); r >= 'A' && r <= 'Z' {
s = strings.ToLower(string(r)) + s[1:]
}
return toCamelInitCase(s, false)
}

View File

@ -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.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{
{"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.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
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)
}
@ -65,49 +64,68 @@ 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)
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
nextCaseIsChanged := false
if i+1 < len(s) {
next := s[i+1]
vIsCap := v >= 'A' && v <= 'Z'
vIsLow := v >= 'a' && v <= 'z'
vIsNum := v >= '0' && v <= '9'
nextIsCap := next >= 'A' && next <= 'Z'
nextIsLow := next >= 'a' && next <= 'z'
if (vIsCap && nextIsLow) || (vIsLow && nextIsCap) {
nextCaseIsChanged = true
}
if ignore > 0 && i-1 >= 0 && s[i-1] == ignore && nextCaseIsChanged {
nextCaseIsChanged = false
}
}
if i > 0 && n[len(n)-1] != delimiter && nextCaseIsChanged {
nextIsNum := next >= '0' && next <= '9'
// 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 (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 {
n.WriteByte(delimiter)
}
}
n.WriteByte(v)
if vIsLow || vIsNum || nextIsNum {
n.WriteByte(delimiter)
}
continue
}
}
}
if (v == ' ' || v == '_' || v == '-') && uint8(v) != ignore {
// replace space/underscore/hyphen with delimiter
n.WriteByte(delimiter)
} else {
n = n + string(v)
n.WriteByte(v)
}
}
if screaming {
n = strings.ToUpper(n)
} else {
n = strings.ToLower(n)
}
return n
return n.String()
}

View File

@ -29,7 +29,7 @@ import (
"testing"
)
func TestToSnake(t *testing.T) {
func toSnake(tb testing.TB) {
cases := [][]string{
{"testCase", "test_case"},
{"TestCase", "test_case"},
@ -48,17 +48,30 @@ func TestToSnake(t *testing.T) {
{"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]
out := i[1]
result := ToSnake(in)
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{
{"testCase", "test_case"},
{"TestCase", "test_case"},
@ -75,9 +88,9 @@ func TestToSnakeWithIgnore(t *testing.T) {
{"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", "."},
{"AwesomeUsername@Awesome.Com", "awesome_username@awesome._com", "@"},
{"AwesomeActivity.UserID", "awesome_activity.user_id", "."},
{"AwesomeActivity.User.Id", "awesome_activity.user.id", "."},
{"AwesomeUsername@Awesome.Com", "awesome_username@awesome.com", "@"},
}
for _, i := range cases {
in := i[0]
@ -88,12 +101,22 @@ func TestToSnakeWithIgnore(t *testing.T) {
}
result := ToSnakeWithIgnore(in, ignore)
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{
{"testCase", "test@case"},
{"TestCase", "test@case"},
@ -119,12 +142,18 @@ func TestToDelimited(t *testing.T) {
out := i[1]
result := ToDelimited(in, '@')
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{
{"testCase", "TEST_CASE"},
}
@ -133,12 +162,18 @@ func TestToScreamingSnake(t *testing.T) {
out := i[1]
result := ToScreamingSnake(in)
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{
{"testCase", "test-case"},
}
@ -147,12 +182,18 @@ func TestToKebab(t *testing.T) {
out := i[1]
result := ToKebab(in)
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{
{"testCase", "TEST-CASE"},
}
@ -161,12 +202,18 @@ func TestToScreamingKebab(t *testing.T) {
out := i[1]
result := ToScreamingKebab(in)
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{
{"testCase", "TEST.CASE"},
}
@ -175,23 +222,45 @@ func TestToScreamingDelimited(t *testing.T) {
out := i[1]
result := ToScreamingDelimited(in, '.', 0, true)
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{
{"AnyKind of_string", "ANY.KIND OF.STRING", ".", " "},
}
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 {
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)
}
}