From 4f38deaa8eb19192bd5e12035226fca4de7a54d1 Mon Sep 17 00:00:00 2001 From: Liyan David Chang Date: Tue, 10 Jan 2023 20:57:45 -0500 Subject: [PATCH 1/4] savepoint --- convert.go | 33 ++++++++++++++++++++++++++++++--- strcase_test.go | 9 +++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/convert.go b/convert.go index 108b3cb..f6fc545 100644 --- a/convert.go +++ b/convert.go @@ -1,8 +1,11 @@ package strcase import "strings" +import "fmt" // WordCase is an enumeration of the ways to format a word. +// The first 16 bits are base casers +// The second 16 bits are options type WordCase int const ( @@ -21,6 +24,16 @@ const ( CamelCase ) +// Other word case options +const ( + WordCaseMask = 0xFFFF + WordCaseOptionMask = 0xFFFF << 16 + // If the entire word is all capitalized, keep them capitalized + // Works will all cases, but really only interesting for LowerCase, + // TitleCase, and CamelCase as it's a no-op for Original and UpperCase + PreserveInitialism = 1 << 16 +) + // We have 3 convert functions for performance reasons // The general convert could handle everything, but is not optimized // @@ -67,7 +80,7 @@ func convertWithoutInitialisms(input string, delimiter rune, wordCase WordCase) } inWord = false } - switch wordCase { + switch wordCase & WordCaseMask { case UpperCase: b.WriteRune(toUpper(curr)) case LowerCase: @@ -140,7 +153,7 @@ func convertWithGoInitialisms(input string, delimiter rune, wordCase WordCase) s for i := start; i < end; i++ { r := runes[i] - switch wordCase { + switch wordCase & WordCaseMask { case UpperCase: panic("use convertWithoutInitialisms instead") case LowerCase: @@ -241,6 +254,20 @@ func convert(input string, fn SplitFn, delimiter rune, wordCase WordCase, } } } + // If we're preserving initialism, check to see if the entire word is + // an initialism. + fmt.Println("here", wordCase, wordCase&WordCaseOptionMask, PreserveInitialism) + if wordCase&WordCaseOptionMask == PreserveInitialism { + allCaps := true + for i := start; i < end; i++ { + allCaps = allCaps && isUpper(runes[i]) + } + if allCaps { + b.WriteString(string(runes[start:end])) + firstWord = false + return + } + } skipIdx := 0 for i := start; i < end; i++ { @@ -249,7 +276,7 @@ func convert(input string, fn SplitFn, delimiter rune, wordCase WordCase, continue } r := runes[i] - switch wordCase { + switch wordCase & WordCaseMask { case UpperCase: b.WriteRune(toUpper(r)) case LowerCase: diff --git a/strcase_test.go b/strcase_test.go index 9146238..ebc738f 100644 --- a/strcase_test.go +++ b/strcase_test.go @@ -21,6 +21,15 @@ func TestEdges(t *testing.T) { }() convertWithGoInitialisms("foo", 0, UpperCase) }) + +} + +func TestOrignal(t *testing.T) { + caser := NewCaser(false, nil, nil) + assert.Equal(t, "nativeOrgURL", caser.ToCase("NativeOrgURL", CamelCase|PreserveInitialism, 0)) + fmt.Printf("%d\n", CamelCase) + fmt.Printf("%d\n", CamelCase&WordCaseMask) + fmt.Printf("%d\n", CamelCase&WordCaseOptionMask) } func TestAll(t *testing.T) { From 183c274db40d75bd9c16930fc3fe57977e54e672 Mon Sep 17 00:00:00 2001 From: Liyan David Chang Date: Sat, 14 Jan 2023 13:20:12 -0500 Subject: [PATCH 2/4] Preserving the casing of exiting initialisms --- convert.go | 50 ++++++++++++++++++++++++++++--------------------- strcase_test.go | 36 +++++++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/convert.go b/convert.go index f6fc545..7857dc3 100644 --- a/convert.go +++ b/convert.go @@ -1,7 +1,9 @@ package strcase -import "strings" -import "fmt" +import ( + "strings" + "unicode" +) // WordCase is an enumeration of the ways to format a word. // The first 16 bits are base casers @@ -26,12 +28,15 @@ const ( // Other word case options const ( - WordCaseMask = 0xFFFF - WordCaseOptionMask = 0xFFFF << 16 + wordCaseMask = 0xFFFF // If the entire word is all capitalized, keep them capitalized - // Works will all cases, but really only interesting for LowerCase, - // TitleCase, and CamelCase as it's a no-op for Original and UpperCase + // Works LowerCase, TitleCase, and CamelCase + // No impact on Original and UpperCase PreserveInitialism = 1 << 16 + // If InitialismFirstWord, camelCase jsonString is JSONString + // Only impacts camelCase + // LowerCase will initialize all initialisms that it's told to + InitialismFirstWord = 1 << 17 ) // We have 3 convert functions for performance reasons @@ -80,7 +85,7 @@ func convertWithoutInitialisms(input string, delimiter rune, wordCase WordCase) } inWord = false } - switch wordCase & WordCaseMask { + switch wordCase & wordCaseMask { case UpperCase: b.WriteRune(toUpper(curr)) case LowerCase: @@ -143,7 +148,7 @@ func convertWithGoInitialisms(input string, delimiter rune, wordCase WordCase) s word.WriteRune(toUpper(runes[i])) } if golintInitialisms[word.String()] { - if !firstWord || wordCase != CamelCase { + if !firstWord || wordCase&wordCaseMask != CamelCase || wordCase&InitialismFirstWord != 0 { b.WriteString(word.String()) firstWord = false return @@ -153,7 +158,7 @@ func convertWithGoInitialisms(input string, delimiter rune, wordCase WordCase) s for i := start; i < end; i++ { r := runes[i] - switch wordCase & WordCaseMask { + switch wordCase & wordCaseMask { case UpperCase: panic("use convertWithoutInitialisms instead") case LowerCase: @@ -247,7 +252,7 @@ func convert(input string, fn SplitFn, delimiter rune, wordCase WordCase, } key := word.String() if initialisms[key] { - if !firstWord || wordCase != CamelCase { + if !firstWord || wordCase&wordCaseMask != CamelCase || wordCase&InitialismFirstWord != 0 { b.WriteString(key) firstWord = false return @@ -256,16 +261,19 @@ func convert(input string, fn SplitFn, delimiter rune, wordCase WordCase, } // If we're preserving initialism, check to see if the entire word is // an initialism. - fmt.Println("here", wordCase, wordCase&WordCaseOptionMask, PreserveInitialism) - if wordCase&WordCaseOptionMask == PreserveInitialism { - allCaps := true - for i := start; i < end; i++ { - allCaps = allCaps && isUpper(runes[i]) - } - if allCaps { - b.WriteString(string(runes[start:end])) - firstWord = false - return + // Note we don't support preserving initialisms if they are followed + // by a number and we're not spliting before numbers + if !firstWord || wordCase&InitialismFirstWord != 0 || wordCase&wordCaseMask != CamelCase { + if wordCase&PreserveInitialism != 0 { + allCaps := true + for i := start; i < end; i++ { + allCaps = allCaps && (isUpper(runes[i]) || !unicode.IsLetter(runes[i])) + } + if allCaps { + b.WriteString(string(runes[start:end])) + firstWord = false + return + } } } @@ -276,7 +284,7 @@ func convert(input string, fn SplitFn, delimiter rune, wordCase WordCase, continue } r := runes[i] - switch wordCase & WordCaseMask { + switch wordCase & wordCaseMask { case UpperCase: b.WriteRune(toUpper(r)) case LowerCase: diff --git a/strcase_test.go b/strcase_test.go index ebc738f..503898f 100644 --- a/strcase_test.go +++ b/strcase_test.go @@ -25,11 +25,39 @@ func TestEdges(t *testing.T) { } func TestOrignal(t *testing.T) { + + // In the plain ToCase, we don't support any initialisms + assertEqual(t, "nativeOrgUrl", ToCase("NativeOrgURL", CamelCase, 0)) + assertEqual(t, "nativeOrgUrl", ToCase("NativeOrgUrl", CamelCase|PreserveInitialism, 0)) + assertEqual(t, "nativeOrgUrl", ToCase("NativeOrgUrl", CamelCase|InitialismFirstWord, 0)) + + // For ToGoCase, preserve intialism will do nothing since we ony intialize + // Go initialisms + assertEqual(t, "nativeOrgURL", ToGoCase("NativeOrgUrl", CamelCase, 0)) + assertEqual(t, "nativeOrgURL", ToGoCase("NativeOrgURL", CamelCase, 0)) + assertEqual(t, "nativeOrgURL", ToGoCase("NativeOrgURL", CamelCase|PreserveInitialism, 0)) + // But InitialismFirstWord will impact camelcase + assertEqual(t, "JSONString", ToGoCase("jsonString", CamelCase|InitialismFirstWord, 0)) + // LowerCase and others will initialize all words already + assertEqual(t, "JSON-string", ToGoCase("jsonString", LowerCase, '-')) + caser := NewCaser(false, nil, nil) - assert.Equal(t, "nativeOrgURL", caser.ToCase("NativeOrgURL", CamelCase|PreserveInitialism, 0)) - fmt.Printf("%d\n", CamelCase) - fmt.Printf("%d\n", CamelCase&WordCaseMask) - fmt.Printf("%d\n", CamelCase&WordCaseOptionMask) + assertEqual(t, "native-org-url", caser.ToCase("NativeOrgURL", LowerCase, '-')) + assertEqual(t, "native-org-URL", caser.ToCase("NativeOrgURL", LowerCase|PreserveInitialism, '-')) + assertEqual(t, "native-org-url", caser.ToCase("NativeOrgUrl", LowerCase|PreserveInitialism, '-')) + assertEqual(t, "JSON-string", caser.ToCase("JSONString", LowerCase|PreserveInitialism, '-')) + assertEqual(t, "json-string", caser.ToCase("jsonString", LowerCase|PreserveInitialism, '-')) + + assertEqual(t, "nativeOrgUrl", caser.ToCase("NativeOrgURL", CamelCase, 0)) + assertEqual(t, "nativeOrgUrl", caser.ToCase("NativeOrgUrl", CamelCase|PreserveInitialism, 0)) + assertEqual(t, "nativeOrgURL", caser.ToCase("NativeOrgURL", CamelCase|PreserveInitialism, 0)) + + assertEqual(t, "jsonString", caser.ToCase("JSONString", CamelCase|PreserveInitialism, 0)) + assertEqual(t, "jsonString", caser.ToCase("jsonString", CamelCase|PreserveInitialism, 0)) + assertEqual(t, "JSONString", caser.ToCase("JSONString", CamelCase|PreserveInitialism|InitialismFirstWord, 0)) + assertEqual(t, "jsonString", caser.ToCase("JSONString", CamelCase|InitialismFirstWord, 0)) + + assertEqual(t, "PS4", caser.ToCase("PS4", LowerCase|PreserveInitialism, '-')) } func TestAll(t *testing.T) { From 95246290ae0ff701a3765a26500f85ee4ea6f5f9 Mon Sep 17 00:00:00 2001 From: Liyan David Chang Date: Sat, 14 Jan 2023 13:35:11 -0500 Subject: [PATCH 3/4] docs --- README.md | 21 ++++++++++++++++++++- strcase_test.go | 7 +++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a984da8..c8b8c62 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ custom casers to mimic the behavior of the other package. ## Index +* [Constants](#pkg-constants) * [func ToCamel(s string) string](#func-ToCamel) * [func ToCase(s string, wordCase WordCase, delimiter rune) string](#func-ToCase) * [func ToGoCamel(s string) string](#func-ToGoCamel) @@ -196,6 +197,22 @@ custom casers to mimic the behavior of the other package. * [type WordCase](#type-WordCase) +## Constants +``` go +const ( + + // If the entire word is all capitalized, keep them capitalized + // Works LowerCase, TitleCase, and CamelCase + // No impact on Original and UpperCase + PreserveInitialism = 1 << 16 + // If InitialismFirstWord, camelCase jsonString is JSONString + // Only impacts camelCase + // LowerCase will initialize all initialisms that it's told to + InitialismFirstWord = 1 << 17 +) +``` +Other word case options + @@ -514,11 +531,13 @@ const ( -## type [WordCase](./convert.go#L6) +## type [WordCase](./convert.go#L11) ``` go type WordCase int ``` WordCase is an enumeration of the ways to format a word. +The first 16 bits are base casers +The second 16 bits are options ``` go diff --git a/strcase_test.go b/strcase_test.go index 503898f..818ab33 100644 --- a/strcase_test.go +++ b/strcase_test.go @@ -57,7 +57,14 @@ func TestOrignal(t *testing.T) { assertEqual(t, "JSONString", caser.ToCase("JSONString", CamelCase|PreserveInitialism|InitialismFirstWord, 0)) assertEqual(t, "jsonString", caser.ToCase("JSONString", CamelCase|InitialismFirstWord, 0)) + assertEqual(t, "NASA-rocket", caser.ToCase("NASARocket", LowerCase|PreserveInitialism, '-')) + assertEqual(t, "nasa-rocket", caser.ToCase("NasaRocket", LowerCase|PreserveInitialism, '-')) + assertEqual(t, "nasa-rocket", caser.ToCase("NASARocket", LowerCase, '-')) + + assertEqual(t, "ps4", caser.ToCase("ps4", LowerCase, '-')) assertEqual(t, "PS4", caser.ToCase("PS4", LowerCase|PreserveInitialism, '-')) + assertEqual(t, "ps4", caser.ToCase("Ps4", LowerCase|PreserveInitialism, '-')) + assertEqual(t, "ps4", caser.ToCase("ps4", LowerCase, '-')) } func TestAll(t *testing.T) { From e5db6a6becf333966ca9f0ed837b17b73676cc8b Mon Sep 17 00:00:00 2001 From: Liyan David Chang Date: Sat, 14 Jan 2023 13:56:58 -0500 Subject: [PATCH 4/4] Update docs --- Makefile | 1 - README.md | 46 +++++++++++++++++++++++++++++----------------- convert.go | 32 +++++++++++++++++++++++--------- strcase_test.go | 7 ++++--- 4 files changed, 56 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index ac98b4a..41ab05b 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,6 @@ test: lint: which golangci-lint || go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1 golangci-lint run - golangci-lint run benchmark/*.go go mod tidy benchmark: diff --git a/README.md b/README.md index c8b8c62..862ed09 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,6 @@ custom casers to mimic the behavior of the other package. ## Index -* [Constants](#pkg-constants) * [func ToCamel(s string) string](#func-ToCamel) * [func ToCase(s string, wordCase WordCase, delimiter rune) string](#func-ToCase) * [func ToGoCamel(s string) string](#func-ToGoCamel) @@ -197,22 +196,6 @@ custom casers to mimic the behavior of the other package. * [type WordCase](#type-WordCase) -## Constants -``` go -const ( - - // If the entire word is all capitalized, keep them capitalized - // Works LowerCase, TitleCase, and CamelCase - // No impact on Original and UpperCase - PreserveInitialism = 1 << 16 - // If InitialismFirstWord, camelCase jsonString is JSONString - // Only impacts camelCase - // LowerCase will initialize all initialisms that it's told to - InitialismFirstWord = 1 << 17 -) -``` -Other word case options - @@ -554,10 +537,39 @@ const ( // Notably, even if the first word is an initialism, it will be lower // cased. This is important for code generators where capital letters // mean exported functions. i.e. jsonString(), not JSONString() + // + // Use CamelCase|InitialismFirstWord (see options below) if you want to + // have initialisms like JSONString CamelCase ) ``` +``` go +const ( + + // InitialismFirstWord will allow CamelCase to start with an upper case + // letter if it is an initialism. Only impacts CamelCase. + // LowerCase will initialize all specified initialisms, regardless of position. + // + // e.g ToGoCase("jsonString", CamelCase|InitialismFirstWord, 0) == "JSONString" + InitialismFirstWord WordCase = 1 << 17 + + // PreserveInitialism will treat any capitalized words as initialisms + // If the entire word is all upper case, keep them upper case. + // + // Note that you may also use InitialismFirstWord with PreserveInitialism + // e.g. CamelCase|InitialismFirstWord|PreserveInitialism: NASA-rocket -> NASARocket + // e.g. CamelCase|PreserveInitialism: NASA-rocket -> nasaRocket + // + // Works for LowerCase, TitleCase, and CamelCase. No impact on Original + // and UpperCase. + // + // Not recommended when the input is in SCREAMING_SNAKE_CASE + // as all words will be treated as initialisms. + PreserveInitialism WordCase = 1 << 16 +) +``` + diff --git a/convert.go b/convert.go index cd85da9..1346f1d 100644 --- a/convert.go +++ b/convert.go @@ -23,20 +23,34 @@ const ( // Notably, even if the first word is an initialism, it will be lower // cased. This is important for code generators where capital letters // mean exported functions. i.e. jsonString(), not JSONString() + // + // Use CamelCase|InitialismFirstWord (see options below) if you want to + // have initialisms like JSONString CamelCase ) -// Other word case options const ( wordCaseMask = 0xFFFF - // If the entire word is all capitalized, keep them capitalized - // Works LowerCase, TitleCase, and CamelCase - // No impact on Original and UpperCase - PreserveInitialism = 1 << 16 - // If InitialismFirstWord, camelCase jsonString is JSONString - // Only impacts camelCase - // LowerCase will initialize all initialisms that it's told to - InitialismFirstWord = 1 << 17 + // InitialismFirstWord will allow CamelCase to start with an upper case + // letter if it is an initialism. Only impacts CamelCase. + // LowerCase will initialize all specified initialisms, regardless of position. + // + // e.g ToGoCase("jsonString", CamelCase|InitialismFirstWord, 0) == "JSONString" + InitialismFirstWord WordCase = 1 << 17 + + // PreserveInitialism will treat any capitalized words as initialisms + // If the entire word is all upper case, keep them upper case. + // + // Note that you may also use InitialismFirstWord with PreserveInitialism + // e.g. CamelCase|InitialismFirstWord|PreserveInitialism: NASA-rocket -> NASARocket + // e.g. CamelCase|PreserveInitialism: NASA-rocket -> nasaRocket + // + // Works for LowerCase, TitleCase, and CamelCase. No impact on Original + // and UpperCase. + // + // Not recommended when the input is in SCREAMING_SNAKE_CASE + // as all words will be treated as initialisms. + PreserveInitialism WordCase = 1 << 16 ) // We have 3 convert functions for performance reasons diff --git a/strcase_test.go b/strcase_test.go index 940bc85..1736cec 100644 --- a/strcase_test.go +++ b/strcase_test.go @@ -21,17 +21,15 @@ func TestEdges(t *testing.T) { }() convertWithGoInitialisms("foo", 0, UpperCase) }) - } func TestOrignal(t *testing.T) { - // In the plain ToCase, we don't support any initialisms assertEqual(t, "nativeOrgUrl", ToCase("NativeOrgURL", CamelCase, 0)) assertEqual(t, "nativeOrgUrl", ToCase("NativeOrgUrl", CamelCase|PreserveInitialism, 0)) assertEqual(t, "nativeOrgUrl", ToCase("NativeOrgUrl", CamelCase|InitialismFirstWord, 0)) - // For ToGoCase, preserve intialism will do nothing since we ony intialize + // For ToGoCase, preserve initialism will do nothing since we ony initialize // Go initialisms assertEqual(t, "nativeOrgURL", ToGoCase("NativeOrgUrl", CamelCase, 0)) assertEqual(t, "nativeOrgURL", ToGoCase("NativeOrgURL", CamelCase, 0)) @@ -65,6 +63,9 @@ func TestOrignal(t *testing.T) { assertEqual(t, "PS4", caser.ToCase("PS4", LowerCase|PreserveInitialism, '-')) assertEqual(t, "ps4", caser.ToCase("Ps4", LowerCase|PreserveInitialism, '-')) assertEqual(t, "ps4", caser.ToCase("ps4", LowerCase, '-')) + + // Not a great option if you're coming from an all-caps case + assertEqual(t, "SCREAMING-CASE", caser.ToCase("SCREAMING_CASE", LowerCase|PreserveInitialism, '-')) } func TestAll(t *testing.T) {