Skip to content

Preserve initialism #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ test:
lint:
which golangci-lint || go get github.com/golangci/golangci-lint/cmd/[email protected]
golangci-lint run
golangci-lint run benchmark/*.go
go mod tidy

benchmark:
Expand Down
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,11 +514,13 @@ const (



## <a name="WordCase">type</a> [WordCase](./convert.go#L6)
## <a name="WordCase">type</a> [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
Expand All @@ -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
)
```




Expand Down
61 changes: 55 additions & 6 deletions convert.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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
//
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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++ {
Expand All @@ -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:
Expand Down
45 changes: 45 additions & 0 deletions strcase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down