Skip to content
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

feat: add strcase lib #229

Merged
merged 3 commits into from
Nov 12, 2019
Merged
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
58 changes: 58 additions & 0 deletions strcase/bash_arg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package strcase

import "strings"

// ToBashArg returns the Bash public name of the given string.
func ToBashArg(s string) string {
s = ToPublicGoName(s)
for _, initialism := range customInitialisms {
// catch this kind of pattern: ExampleIDs ==> ExampleIds ==> example-ids
s = strings.Replace(s, initialism[0], strings.Title(strings.ToLower(initialism[0])), -1)
}
return toKebab(s)
}

// toKebab converts a string to kebab-case.
func toKebab(s string) string {
return toDelimited(s, '-')
}

// toDelimited converts a string to delimited lowercase.
func toDelimited(s string, del uint8) string {
s = strings.Trim(s, " ")
n := ""
for i, v := range s {
// treat acronyms as words, eg for JSONData -> JSON is a whole word
nextCaseIsChanged := false
if i+1 < len(s) {
next := s[i+1]
if (isUpperLetter(v) && isLowerLetter(int32(next))) || (isLowerLetter(v) && isUpperLetter(int32(next))) {
nextCaseIsChanged = true
}
}

if i > 0 && n[len(n)-1] != del && nextCaseIsChanged {
// add delimiter if next letter case type is changed
if isUpperLetter(v) {
n += string(del) + string(v)
} else if isLowerLetter(v) {
n += string(v) + string(del)
}
} else if v == ' ' || v == '-' || v == '_' {
// replace spaces and dashes with delimiter
n += string(del)
} else {
n = n + string(v)
}
}
n = strings.ToLower(n)
return n
}

func isUpperLetter(c int32) bool {
return c >= 'A' && c <= 'Z'
}

func isLowerLetter(c int32) bool {
return c >= 'a' && c <= 'z'
}
38 changes: 38 additions & 0 deletions strcase/bash_arg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package strcase

import (
"testing"
)

func TestToKebabCase(t *testing.T) {
cases := []struct {
in string
want string
}{
{"testCase", "test-case"},
{"TestCase", "test-case"},
{"Test Case", "test-case"},
{" Test Case", "test-case"},
{"Test Case ", "test-case"},
{" Test Case ", "test-case"},
{"test", "test"},
{"test_case", "test-case"},
{"Test", "test"},
{"", ""},
{"ManyManyWords", "many-many-words"},
{"manyManyWords", "many-many-words"},
{"AnyKind of_string", "any-kind-of-string"},
{"numbers2and55with000", "numbers2and55with000"},
{"JSONData", "json-data"},
{"userID", "user-id"},
{"AAAbbb", "aa-abbb"},
}
for _, c := range cases {
t.Run(c.in, func(t *testing.T) {
got := toKebab(c.in)
if got != c.want {
t.Errorf("toKebab(%q) == %q, want %q", c.in, got, c.want)
}
})
}
}
188 changes: 188 additions & 0 deletions strcase/goname.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package strcase

import (
"strings"
"unicode"
)

// ToPrivateGoName returns the Go public name of the given string.
func ToPublicGoName(s string) string {
return toGoName(TitleFirstWord(s))
}

// ToPrivateGoName returns the Go private name of the given string.
func ToPrivateGoName(s string) string {
return toGoName(lowerCaseFirstLetterOrAcronyms(s))
}

// toGoName returns a different name if it should be different.
func toGoName(name string) (should string) {
name = strings.Replace(name, " ", "_", -1)
name = strings.Replace(name, "-", "_", -1)

// Fast path for simple cases: "_" and all lowercase.
if name == "_" {
return name
}
allLower := true
for _, r := range name {
if !unicode.IsLower(r) {
allLower = false
break
}
}
if allLower {
return name
}

// Split camelCase at any lower->upper transition, and split on underscores.
// Check each word for common initialisms.
runes := []rune(name)
w, i := 0, 0 // index of start of word, scan
for i+1 <= len(runes) {
eow := false // whether we hit the end of a word
if i+1 == len(runes) {
eow = true
} else if runes[i+1] == '_' {
// underscore; shift the remainder forward over any run of underscores
eow = true
n := 1
for i+n+1 < len(runes) && runes[i+n+1] == '_' {
n++
}

// Leave at most one underscore if the underscore is between two digits
if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) {
n--
}

copy(runes[i+1:], runes[i+n+1:])
runes = runes[:len(runes)-n]
} else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) {
// lower->non-lower
eow = true
}
i++
if !eow {
continue
}

// [w,i) is a word.
word := string(runes[w:i])
u := strings.ToUpper(word)
if commonInitialisms[u] {
// Keep consistent case, which is lowercase only at the start.
if w == 0 && unicode.IsLower(runes[w]) {
u = strings.ToLower(u)
}
// All the common initialisms are ASCII,
// so we can replace the bytes exactly.
copy(runes[w:], []rune(u))
} else if specialCase, exist := customInitialisms[u]; exist {
if w == 0 && unicode.IsLower(runes[w]) {
u = specialCase[1]
} else {
u = specialCase[0]
}

copy(runes[w:], []rune(u))
} else if w > 0 && strings.ToLower(word) == word {
// already all lowercase, and not the first word, so uppercase the first character.
runes[w] = unicode.ToUpper(runes[w])
}
w = i
}
return string(runes)
}

// commonInitialisms is a set of common initialisms.
// Only add entries that are highly unlikely to be non-initialisms.
// For instance, "ID" is fine (Freudian code is rare), but "AND" is not.
var commonInitialisms = map[string]bool{
"ACL": true,
"API": true,
"ASCII": true,
"CPU": true,
"CSS": true,
"DNS": true,
"EOF": true,
"GUID": true,
"HTML": true,
"HTTP": true,
"HTTPS": true,
"ID": true,
"IP": true,
"JSON": true,
"LHS": true,
"QPS": true,
"RAM": true,
"RHS": true,
"RPC": true,
"SLA": true,
"SMTP": true,
"SQL": true,
"SSD": true,
"SSH": true,
"TCP": true,
"TLS": true,
"TTL": true,
"UDP": true,
"UI": true,
"UID": true,
"UUID": true,
"URI": true,
"URL": true,
"UTF8": true,
"VM": true,
"XML": true,
"XMPP": true,
"XSRF": true,
"XSS": true,
}

// customInitialisms is a set of common initialisms we use at Scaleway.
// value[0] is the uppercase replacement
// value[1] is the lowercase replacement
var customInitialisms = map[string][2]string{
"ACLS": {"ACLs", "acls"},
"APIS": {"APIs", "apis"},
"CPUS": {"CPUs", "cpus"},
"IDS": {"IDs", "ids"},
"IPS": {"IPs", "ips"},
"IPV": {"IPv", "ipv"}, // handle IPV4 && IPV6
"UIDS": {"UIDs", "uids"},
"UUIDS": {"UUIDs", "uuids"},
"URIS": {"URIs", "uris"},
"URLS": {"URLs", "urls"},
}

// TitleFirstWord upper case the first letter of a string.
func TitleFirstWord(s string) string {
if len(s) == 0 {
return s
}

r := []rune(s)
r[0] = unicode.ToUpper(r[0])

return string(r)
}

// lowerCaseFirstLetterOrAcronyms lower case the first letter of a string.
func lowerCaseFirstLetterOrAcronyms(s string) string {
r := []rune(s)
if len(r) == 0 {
return ""
}

for i := 0; len(r) > i && unicode.IsUpper(r[i]); i++ {
word := string(r[:i+1])
if u := strings.ToUpper(word); commonInitialisms[u] {
copy(r[0:], []rune(strings.ToLower(u)))
break
}
}
r[0] = unicode.ToLower(r[0])

return string(r)
}
27 changes: 27 additions & 0 deletions strcase/goname_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package strcase

import "testing"

func TestLowerCaseFirstLetterOrAcronyms(t *testing.T) {
cases := []struct {
in string
want string
}{
{"", ""},
{"t", "t"},
{"Test Case", "test Case"},
{"test Case", "test Case"},
{"TEST CASE", "tEST CASE"},
{"tEST CASE", "tEST CASE"},
{"#EST CASE", "#EST CASE"},
{"APITest", "apiTest"},
{"AVATATest", "aVATATest"},
{"TestStuff", "testStuff"},
}
for _, c := range cases {
result := lowerCaseFirstLetterOrAcronyms(c.in)
if result != c.want {
t.Errorf("lowerCaseFirstLetterOrAcronyms(%q) == %q, want %q", c.in, result, c.want)
}
}
}
64 changes: 64 additions & 0 deletions strcase/strcase_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package strcase

import (
"testing"
)

func TestAllStrCases(t *testing.T) {
tests := []struct {
name, publicGoName, privateGoName, bashArgName string
}{
{"foo_bar", "FooBar", "fooBar", "foo-bar"},
{"foo_bar_baz", "FooBarBaz", "fooBarBaz", "foo-bar-baz"},
{"Foo_bar", "FooBar", "fooBar", "foo-bar"},
{"foo_WiFi", "FooWiFi", "fooWiFi", "foo-wi-fi"},
{"id", "ID", "id", "id"},
{"Id", "ID", "id", "id"},
{"foo_id", "FooID", "fooID", "foo-id"},
{"fooId", "FooID", "fooID", "foo-id"},
{"fooUid", "FooUID", "fooUID", "foo-uid"},
{"idFoo", "IDFoo", "idFoo", "id-foo"},
{"uidFoo", "UIDFoo", "uidFoo", "uid-foo"},
{"midIdDle", "MidIDDle", "midIDDle", "mid-id-dle"},
{"APIProxy", "APIProxy", "apiProxy", "api-proxy"},
{"ApiProxy", "APIProxy", "apiProxy", "api-proxy"},
{"apiProxy", "APIProxy", "apiProxy", "api-proxy"},
{"_Leading", "_Leading", "_Leading", "-leading"},
{"___Leading", "_Leading", "_Leading", "-leading"},
{"trailing_", "Trailing", "trailing", "trailing"},
{"trailing___", "Trailing", "trailing", "trailing"},
{"a_b", "AB", "aB", "ab"},
{"a__b", "AB", "aB", "ab"},
{"a___b", "AB", "aB", "ab"},
{"Rpc1150", "RPC1150", "rpc1150", "rpc1150"},
{"case3_1", "Case3_1", "case3_1", "case3-1"},
{"case3__1", "Case3_1", "case3_1", "case3-1"},
{"IEEE802_16bit", "IEEE802_16bit", "iEEE802_16bit", "ieee802-16bit"},
{"IEEE802_16Bit", "IEEE802_16Bit", "iEEE802_16Bit", "ieee802-16-bit"},
{"IPv4", "IPv4", "ipv4", "ipv4"},
{"Ipv4", "IPv4", "ipv4", "ipv4"},
{"iPV4", "IPV4", "iPV4", "ipv4"},
{"RepeatedIpv4", "RepeatedIPv4", "repeatedIPv4", "repeated-ipv4"},
{"eSport", "ESport", "eSport", "e-sport"},
{"stopped in place", "StoppedInPlace", "stoppedInPlace", "stopped-in-place"},
{"l_ssd", "LSSD", "lSSD", "lssd"},
{"ids", "IDs", "ids", "ids"},
{"my_resource_ids", "MyResourceIDs", "myResourceIDs", "my-resource-ids"},
{"acids", "Acids", "acids", "acids"},
{"secret-key", "SecretKey", "secretKey", "secret-key"},
}
for _, test := range tests {
got := ToPublicGoName(test.name)
if got != test.publicGoName {
t.Errorf("ToPublicGoName(%q) == %q, want %q", test.name, got, test.publicGoName)
}
got = ToPrivateGoName(test.name)
if got != test.privateGoName {
t.Errorf("ToPrivateGoName(%q) == %q, want %q", test.name, got, test.privateGoName)
}
got = ToBashArg(test.name)
if got != test.bashArgName {
t.Errorf("ToBashArg(%q) == %q, want %q", test.name, got, test.bashArgName)
}
}
}