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 a984da8..862ed09 100644 --- a/README.md +++ b/README.md @@ -514,11 +514,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 @@ -535,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 cb901d0..1346f1d 100644 --- a/convert.go +++ b/convert.go @@ -1,8 +1,13 @@ package strcase -import "strings" +import ( + "strings" + "unicode" +) // 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 ( @@ -18,9 +23,36 @@ 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 ) +const ( + wordCaseMask = 0xFFFF + // 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 // The general convert could handle everything, but is not optimized // @@ -67,7 +99,7 @@ func convertWithoutInitialisms(input string, delimiter rune, wordCase WordCase) } inWord = false } - switch wordCase { + switch wordCase & wordCaseMask { case UpperCase: b.WriteRune(toUpper(curr)) case LowerCase: @@ -131,7 +163,7 @@ func convertWithGoInitialisms(input string, delimiter rune, wordCase WordCase) s } w := word.String() if golintInitialisms[w] { - if !firstWord || wordCase != CamelCase { + if !firstWord || wordCase&wordCaseMask != CamelCase || wordCase&InitialismFirstWord != 0 { b.WriteString(w) firstWord = false return @@ -141,7 +173,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: @@ -235,13 +267,30 @@ func convert(input string, fn SplitFn, delimiter rune, wordCase WordCase, } w := word.String() if initialisms[w] { - if !firstWord || wordCase != CamelCase { + if !firstWord || wordCase&wordCaseMask != CamelCase || wordCase&InitialismFirstWord != 0 { b.WriteString(w) firstWord = false return } } } + // If we're preserving initialism, check to see if the entire word is + // an initialism. + // 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 + } + } + } skipIdx := 0 for i := start; i < end; i++ { @@ -250,7 +299,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 ce56809..1736cec 100644 --- a/strcase_test.go +++ b/strcase_test.go @@ -23,6 +23,51 @@ 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 initialism will do nothing since we ony initialize + // 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) + 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, "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, '-')) + + // 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) { // Instead of testing, we can generate the outputs to make it easier to // add more test cases or functions