├── .github ├── ISSUE_TEMPLATE ├── PULL_REQUEST_TEMPLATE └── workflows │ ├── golangci-lint.yml │ └── gotest.yml ├── .gitignore ├── .golangci.yml ├── .readme.tmpl ├── LICENSE ├── Makefile ├── README.md ├── assert.go ├── assert_test.go ├── benchmark ├── benchmark_test.go ├── go.mod └── go.sum ├── caser.go ├── caser_test.go ├── convert.go ├── doc.go ├── go.mod ├── go.sum ├── initialism.go ├── split.go ├── strcase.go ├── strcase_test.go ├── unicode.go └── unicode_test.go /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### What did you do? 6 | 7 | 12 | 13 | 14 | 15 | ### What did you expect to see? 16 | 17 | 18 | 19 | ### What did you see instead? 20 | 21 | 22 | 23 | ### What version of Go are you using (`go version && go env`)? 24 | 25 |
26 | $ go version
27 | 
28 | $ go env
29 | 
30 | 
31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 7 | 8 | ### What does this fix or improve? 9 | 10 | 11 | 12 | ### Is this a breaking change API change or change in behavior? 13 | 14 | 15 | 16 | ### Checklist: 17 | 24 | - [ ] I have updated the documentation accordingly, if needed. 25 | - [ ] I have added appropriate tests, if needed. 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | permissions: 10 | contents: read 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/setup-go@v3 17 | with: 18 | go-version: 1.19 19 | - uses: actions/checkout@v3 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v3 22 | with: 23 | version: v1.50 24 | -------------------------------------------------------------------------------- /.github/workflows/gotest.yml: -------------------------------------------------------------------------------- 1 | name: go test 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: [1.18.x, 1.19.x] 14 | platform: [ubuntu-latest, macos-latest, windows-latest] 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | - name: Install Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | - name: Test 24 | run: go test -v -cover ./... 25 | 26 | benchmark: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Install Go 30 | uses: actions/setup-go@v2 31 | with: 32 | go-version: 1.19.x 33 | - name: Checkout code 34 | uses: actions/checkout@v2 35 | - name: Benchmark 36 | run: cd benchmark && go test -bench=. -test.benchmem 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # CPU and memory profiles 15 | *.prof 16 | 17 | # Dependency directories 18 | vendor/ 19 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | dupl: 3 | threshold: 100 4 | gocyclo: 5 | min-complexity: 15 6 | gocritic: 7 | enabled-tags: 8 | - diagnostic 9 | - experimental 10 | - opinionated 11 | - performance 12 | - style 13 | disabled-checks: 14 | - ifElseChain 15 | - whyNoLint 16 | - wrapperFunc 17 | govet: 18 | check-shadowing: true 19 | lll: 20 | line-length: 140 21 | maligned: 22 | suggest-new: true 23 | misspell: 24 | locale: US 25 | nolintlint: 26 | allow-leading-space: false 27 | allow-unused: false 28 | require-specific: true 29 | 30 | require-explanation: true 31 | allow-no-explanation: 32 | - gocyclo 33 | 34 | linters: 35 | disable-all: true 36 | enable: 37 | - bodyclose 38 | - depguard 39 | - dogsled 40 | - dupl 41 | - errcheck 42 | - gochecknoinits 43 | - gocritic 44 | - gocyclo 45 | - gofmt 46 | - goimports 47 | - goprintffuncname 48 | - gosec 49 | - gosimple 50 | - govet 51 | - ineffassign 52 | - lll 53 | - misspell 54 | - nakedret 55 | - nolintlint 56 | - revive 57 | - rowserrcheck 58 | - staticcheck 59 | - stylecheck 60 | - typecheck 61 | - unconvert 62 | - unparam 63 | - unused 64 | - whitespace 65 | 66 | # don't enable: 67 | # - asciicheck 68 | # - gochecknoglobals 69 | # - gocognit 70 | # - godot 71 | # - godox 72 | # - goerr113 73 | # - maligned 74 | # - nestif 75 | # - prealloc 76 | # - testpackage 77 | # - wsl 78 | 79 | issues: 80 | exclude-use-default: false 81 | max-issues-per-linter: 0 82 | max-same-issues: 0 83 | -------------------------------------------------------------------------------- /.readme.tmpl: -------------------------------------------------------------------------------- 1 | {{with .PDoc}} 2 | # Go Strcase 3 | 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/ettle/strcase)](https://goreportcard.com/report/github.com/ettle/strcase) 5 | [![Coverage](http://gocover.io/_badge/github.com/ettle/strcase?0)](http://gocover.io/github.com/ettle/strcase) 6 | [![GoDoc](https://godoc.org/github.com/ettle/strcase?status.svg)](https://pkg.go.dev/github.com/ettle/strcase) 7 | 8 | Convert strings to `snake_case`, `camelCase`, `PascalCase`, `kebab-case` and more! Supports Go initialisms, customization, and Unicode. 9 | 10 | `import "{{.ImportPath}}"` 11 | 12 | ## Overview 13 | {{comment_md .Doc}} 14 | {{example_html $ ""}} 15 | 16 | ## Index{{if .Consts}} 17 | * [Constants](#pkg-constants){{end}}{{if .Vars}} 18 | * [Variables](#pkg-variables){{end}}{{- range .Funcs -}}{{$name_html := html .Name}} 19 | * [{{node_html $ .Decl false | sanitize}}](#func-{{$name_html}}){{- end}}{{- range .Types}}{{$tname_html := html .Name}} 20 | * [type {{$tname_html}}](#type-{{$tname_html}}){{- range .Funcs}}{{$name_html := html .Name}} 21 | * [{{node_html $ .Decl false | sanitize}}](#func-{{$name_html}}){{- end}}{{- range .Methods}}{{$name_html := html .Name}} 22 | * [{{node_html $ .Decl false | sanitize}}](#type-{{$tname_html}}.{{$name_html}}){{- end}}{{- end}}{{- if $.Notes}}{{- range $marker, $item := $.Notes}} 23 | * [{{noteTitle $marker | html}}s](#pkg-note-{{$marker}}){{end}}{{end}} 24 | {{if $.Examples}} 25 | #### Examples{{- range $.Examples}} 26 | * [{{example_name .Name}}](#example_{{.Name}}){{- end}}{{- end}} 27 | 28 | {{with .Consts}}## Constants 29 | {{range .}}{{node $ .Decl | pre}} 30 | {{comment_md .Doc}}{{end}}{{end}} 31 | {{with .Vars}}## Variables 32 | {{range .}}{{node $ .Decl | pre}} 33 | {{comment_md .Doc}}{{end}}{{end}} 34 | 35 | {{range .Funcs}}{{$name_html := html .Name}}## func [{{$name_html}}]({{gh_url $ .Decl}}) 36 | {{node $ .Decl | pre}} 37 | {{comment_md .Doc}} 38 | {{example_html $ .Name}} 39 | {{callgraph_html $ "" .Name}}{{end}} 40 | {{range .Types}}{{$tname := .Name}}{{$tname_html := html .Name}}## type [{{$tname_html}}]({{gh_url $ .Decl}}) 41 | {{node $ .Decl | pre}} 42 | {{comment_md .Doc}}{{range .Consts}} 43 | {{node $ .Decl | pre }} 44 | {{comment_md .Doc}}{{end}}{{range .Vars}} 45 | {{node $ .Decl | pre }} 46 | {{comment_md .Doc}}{{end}} 47 | 48 | {{example_html $ $tname}} 49 | {{implements_html $ $tname}} 50 | {{methodset_html $ $tname}} 51 | 52 | {{range .Funcs}}{{$name_html := html .Name}}### func [{{$name_html}}]({{gh_url $ .Decl}}) 53 | {{node $ .Decl | pre}} 54 | {{comment_md .Doc}} 55 | {{example_html $ .Name}}{{end}} 56 | {{callgraph_html $ "" .Name}} 57 | 58 | {{range .Methods}}{{$name_html := html .Name}}### func ({{md .Recv}}) [{{$name_html}}]({{gh_url $ .Decl}}) 59 | {{node $ .Decl | pre}} 60 | {{comment_md .Doc}} 61 | {{$name := printf "%s_%s" $tname .Name}}{{example_html $ $name}} 62 | {{callgraph_html $ .Recv .Name}} 63 | {{end}}{{end}}{{end}} 64 | 65 | {{with $.Notes}} 66 | {{range $marker, $content := .}} 67 | ## {{noteTitle $marker | html}}s 68 | 73 | {{end}} 74 | {{end}} 75 | {{if .Dirs}} 76 | ## Subdirectories 77 | {{range $.Dirs.List}} 78 | {{indent .Depth}}* [{{.Name | html}}]({{print "./" .Path}}){{if .Synopsis}} {{ .Synopsis}}{{end -}} 79 | {{end}} 80 | {{end}} 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Liyan David Chang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: benchmark docs lint test 2 | 3 | docs: 4 | which godoc2ghmd || go get github.com/DevotedHealth/godoc2ghmd 5 | godoc2ghmd -template .readme.tmpl github.com/ettle/strcase > README.md 6 | go mod tidy 7 | 8 | test: 9 | go test -cover ./... 10 | 11 | lint: 12 | which golangci-lint || go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1 13 | golangci-lint run 14 | golangci-lint run benchmark/*.go 15 | go mod tidy 16 | 17 | benchmark: 18 | cd benchmark && go test -bench=. -test.benchmem 19 | go mod tidy 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Go Strcase 3 | 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/ettle/strcase)](https://goreportcard.com/report/github.com/ettle/strcase) 5 | [![Coverage](http://gocover.io/_badge/github.com/ettle/strcase?0)](http://gocover.io/github.com/ettle/strcase) 6 | [![GoDoc](https://godoc.org/github.com/ettle/strcase?status.svg)](https://pkg.go.dev/github.com/ettle/strcase) 7 | 8 | Convert strings to `snake_case`, `camelCase`, `PascalCase`, `kebab-case` and more! Supports Go initialisms, customization, and Unicode. 9 | 10 | `import "github.com/ettle/strcase"` 11 | 12 | ## Overview 13 | Package strcase is a package for converting strings into various word cases 14 | (e.g. snake_case, camelCase) 15 | 16 | 17 | go get -u github.com/ettle/strcase 18 | 19 | Example usage 20 | 21 | 22 | strcase.ToSnake("Hello World") // hello_world 23 | strcase.ToSNAKE("Hello World") // HELLO_WORLD 24 | 25 | strcase.ToKebab("helloWorld") // hello-world 26 | strcase.ToKEBAB("helloWorld") // HELLO-WORLD 27 | 28 | strcase.ToPascal("hello-world") // HelloWorld 29 | strcase.ToCamel("hello-world") // helloWorld 30 | 31 | // Handle odd cases 32 | strcase.ToSnake("FOOBar") // foo_bar 33 | 34 | // Support Go initialisms 35 | strcase.ToGoPascal("http_response") // HTTPResponse 36 | 37 | // Specify case and delimiter 38 | strcase.ToCase("HelloWorld", strcase.UpperCase, '.') // HELLO.WORLD 39 | 40 | ## Why this package 41 | 42 | String strcase is pretty straight forward and there are a number of methods to 43 | do it. This package is fully featured, more customizable, better tested, and 44 | faster than other packages and what you would probably whip up yourself. 45 | 46 | ### Unicode support 47 | 48 | We work for with unicode strings and pay very little performance penalty for it 49 | as we optimized for the common use case of ASCII only strings. 50 | 51 | ### Customization 52 | 53 | You can create a custom caser that changes the behavior to what you want. This 54 | customization also reduces the pressure for us to change the default behavior 55 | which means that things are more stable for everyone involved. The goal is to 56 | make the common path easy and fast, while making the uncommon path possible. 57 | 58 | 59 | c := NewCaser( 60 | // Use Go's default initialisms e.g. ID, HTML 61 | true, 62 | // Override initialisms (e.g. don't initialize HTML but initialize SSL 63 | map[string]bool{"SSL": true, "HTML": false}, 64 | // Write your own custom SplitFn 65 | // 66 | NewSplitFn( 67 | []rune{'*', '.', ','}, 68 | SplitCase, 69 | SplitAcronym, 70 | PreserveNumberFormatting, 71 | SplitBeforeNumber, 72 | SplitAfterNumber, 73 | )) 74 | assert.Equal(t, "http_200", c.ToSnake("http200")) 75 | 76 | ### Initialism support 77 | 78 | By default, we use the golint intialisms list. You can customize and override 79 | the initialisms if you wish to add additional ones, such as "SSL" or "CMS" or 80 | domain specific ones to your industry. 81 | 82 | 83 | ToGoPascal("http_response") // HTTPResponse 84 | ToGoSnake("http_response") // HTTP_response 85 | 86 | ### Test coverage 87 | 88 | We have a wide ranging test suite to make sure that we understand our behavior. 89 | Test coverage isn't everything, but we aim for 100% coverage. 90 | 91 | ### Fast 92 | 93 | Optimized to reduce memory allocations with Builder. Benchmarked and optimized 94 | around common cases. 95 | 96 | We're on par with the fastest packages (that have less features) and much 97 | faster than others. We also benchmarked against code snippets. Using string 98 | builders to reduce memory allocation and reordering boolean checks for the 99 | common cases have a large performance impact. 100 | 101 | Hopefully I was fair to each library and happy to rerun benchmarks differently 102 | or reword my commentary based on suggestions or updates. 103 | 104 | 105 | // This package - faster then almost all libraries 106 | // Initialisms are more complicated and slightly slower, but still fast 107 | BenchmarkToTitle-96 9617142 125.7 ns/op 16 B/op 1 allocs/op 108 | BenchmarkToSnake-96 10659919 120.7 ns/op 16 B/op 1 allocs/op 109 | BenchmarkToSNAKE-96 9018282 126.4 ns/op 16 B/op 1 allocs/op 110 | BenchmarkToGoSnake-96 4903687 254.5 ns/op 26 B/op 4 allocs/op 111 | BenchmarkToCustomCaser-96 4434489 265.0 ns/op 28 B/op 4 allocs/op 112 | 113 | // Segment has very fast snake case and camel case libraries 114 | // No features or customization, but very very fast 115 | BenchmarkSegment-96 33625734 35.54 ns/op 16 B/op 1 allocs/op 116 | 117 | // Iancoleman has gotten some performance improvements, but remains 118 | // without unicode support and lacks fine-grained customization 119 | BenchmarkToSnakeIan-96 13141522 92.99 ns/op 16 B/op 1 allocs/op 120 | 121 | // Stdlib strings.Title is deprecated; using golang.org/x.text 122 | BenchmarkGolangOrgXTextCases-96 4665676 262.5 ns/op 272 B/op 2 allocs/op 123 | 124 | // Other libraries or code snippets 125 | // - Most are slower, by up to an order of magnitude 126 | // - No support for initialisms or customization 127 | // - Some generate only camelCase or snake_case 128 | // - Many lack unicode support 129 | BenchmarkToSnakeStoewer-96 8095468 148.9 ns/op 64 B/op 2 allocs/op 130 | // Copying small rune arrays is slow 131 | BenchmarkToSnakeSiongui-96 2912593 401.7 ns/op 112 B/op 19 allocs/op 132 | BenchmarkGoValidator-96 3493800 342.6 ns/op 184 B/op 9 allocs/op 133 | // String alloction is slow 134 | BenchmarkToSnakeFatih-96 1282648 945.1 ns/op 616 B/op 26 allocs/op 135 | // Regexp is slow 136 | BenchmarkToSnakeGolangPrograms-96 778674 1495 ns/op 227 B/op 11 allocs/op 137 | 138 | // These results aren't a surprise - my initial version of this library was 139 | // painfully slow. I think most of us, without spending some time with 140 | // profilers and benchmarks, would write also something on the slower side. 141 | 142 | ### Zero dependencies 143 | 144 | That's right - zero. We only import the Go standard library. No hassles with 145 | dependencies, licensing, security alerts. 146 | 147 | ## Why not this package 148 | 149 | If every nanosecond matters and this is used in a tight loop, use segment.io's 150 | libraries (https://github.com/segmentio/go-snakecase and 151 | https://github.com/segmentio/go-camelcase). They lack features, but make up for 152 | it by being blazing fast. 153 | 154 | ## Migrating from other packages 155 | 156 | If you are migrating from from another package, you may find slight differences 157 | in output. To reduce the delta, you may find it helpful to use the following 158 | custom casers to mimic the behavior of the other package. 159 | 160 | 161 | // From https://github.com/iancoleman/strcase 162 | var c = NewCaser(false, nil, NewSplitFn([]rune{'_', '-', '.'}, SplitCase, SplitAcronym, SplitBeforeNumber)) 163 | 164 | // From https://github.com/stoewer/go-strcase 165 | var c = NewCaser(false, nil, NewSplitFn([]rune{'_', '-'}, SplitCase), SplitAcronym) 166 | 167 | 168 | 169 | 170 | ## Index 171 | * [func ToCamel(s string) string](#func-ToCamel) 172 | * [func ToCase(s string, wordCase WordCase, delimiter rune) string](#func-ToCase) 173 | * [func ToGoCamel(s string) string](#func-ToGoCamel) 174 | * [func ToGoCase(s string, wordCase WordCase, delimiter rune) string](#func-ToGoCase) 175 | * [func ToGoKebab(s string) string](#func-ToGoKebab) 176 | * [func ToGoPascal(s string) string](#func-ToGoPascal) 177 | * [func ToGoSnake(s string) string](#func-ToGoSnake) 178 | * [func ToKEBAB(s string) string](#func-ToKEBAB) 179 | * [func ToKebab(s string) string](#func-ToKebab) 180 | * [func ToPascal(s string) string](#func-ToPascal) 181 | * [func ToSNAKE(s string) string](#func-ToSNAKE) 182 | * [func ToSnake(s string) string](#func-ToSnake) 183 | * [type Caser](#type-Caser) 184 | * [func NewCaser(goInitialisms bool, initialismOverrides map[string]bool, splitFn SplitFn) *Caser](#func-NewCaser) 185 | * [func (c *Caser) ToCamel(s string) string](#type-Caser.ToCamel) 186 | * [func (c *Caser) ToCase(s string, wordCase WordCase, delimiter rune) string](#type-Caser.ToCase) 187 | * [func (c *Caser) ToKEBAB(s string) string](#type-Caser.ToKEBAB) 188 | * [func (c *Caser) ToKebab(s string) string](#type-Caser.ToKebab) 189 | * [func (c *Caser) ToPascal(s string) string](#type-Caser.ToPascal) 190 | * [func (c *Caser) ToSNAKE(s string) string](#type-Caser.ToSNAKE) 191 | * [func (c *Caser) ToSnake(s string) string](#type-Caser.ToSnake) 192 | * [type SplitAction](#type-SplitAction) 193 | * [type SplitFn](#type-SplitFn) 194 | * [func NewSplitFn(delimiters []rune, splitOptions ...SplitOption) SplitFn](#func-NewSplitFn) 195 | * [type SplitOption](#type-SplitOption) 196 | * [type WordCase](#type-WordCase) 197 | 198 | 199 | 200 | 201 | 202 | ## func [ToCamel](./strcase.go#L57) 203 | ``` go 204 | func ToCamel(s string) string 205 | ``` 206 | ToCamel returns words in camelCase (capitalized words concatenated together, with first word lower case). 207 | Also known as lowerCamelCase or mixedCase. 208 | 209 | 210 | 211 | ## func [ToCase](./strcase.go#L72) 212 | ``` go 213 | func ToCase(s string, wordCase WordCase, delimiter rune) string 214 | ``` 215 | ToCase returns words in given case and delimiter. 216 | 217 | 218 | 219 | ## func [ToGoCamel](./strcase.go#L67) 220 | ``` go 221 | func ToGoCamel(s string) string 222 | ``` 223 | ToGoCamel returns words in camelCase (capitalized words concatenated together, with first word lower case). 224 | Also known as lowerCamelCase or mixedCase. 225 | 226 | Respects Go's common initialisms, but first word remains lowercased which is 227 | important for code generator use cases (e.g. toJson -> toJSON, httpResponse 228 | -> httpResponse). 229 | 230 | 231 | 232 | ## func [ToGoCase](./strcase.go#L79) 233 | ``` go 234 | func ToGoCase(s string, wordCase WordCase, delimiter rune) string 235 | ``` 236 | ToGoCase returns words in given case and delimiter. 237 | 238 | Respects Go's common initialisms (e.g. httpResponse -> HTTPResponse). 239 | 240 | 241 | 242 | ## func [ToGoKebab](./strcase.go#L31) 243 | ``` go 244 | func ToGoKebab(s string) string 245 | ``` 246 | ToGoKebab returns words in kebab-case (lower case words with dashes). 247 | Also known as dash-case. 248 | 249 | Respects Go's common initialisms (e.g. http-response -> HTTP-response). 250 | 251 | 252 | 253 | ## func [ToGoPascal](./strcase.go#L51) 254 | ``` go 255 | func ToGoPascal(s string) string 256 | ``` 257 | ToGoPascal returns words in PascalCase (capitalized words concatenated together). 258 | Also known as UpperPascalCase. 259 | 260 | Respects Go's common initialisms (e.g. HttpResponse -> HTTPResponse). 261 | 262 | 263 | 264 | ## func [ToGoSnake](./strcase.go#L11) 265 | ``` go 266 | func ToGoSnake(s string) string 267 | ``` 268 | ToGoSnake returns words in snake_case (lower case words with underscores). 269 | 270 | Respects Go's common initialisms (e.g. http_response -> HTTP_response). 271 | 272 | 273 | 274 | ## func [ToKEBAB](./strcase.go#L37) 275 | ``` go 276 | func ToKEBAB(s string) string 277 | ``` 278 | ToKEBAB returns words in KEBAB-CASE (upper case words with dashes). 279 | Also known as SCREAMING-KEBAB-CASE or SCREAMING-DASH-CASE. 280 | 281 | 282 | 283 | ## func [ToKebab](./strcase.go#L23) 284 | ``` go 285 | func ToKebab(s string) string 286 | ``` 287 | ToKebab returns words in kebab-case (lower case words with dashes). 288 | Also known as dash-case. 289 | 290 | 291 | 292 | ## func [ToPascal](./strcase.go#L43) 293 | ``` go 294 | func ToPascal(s string) string 295 | ``` 296 | ToPascal returns words in PascalCase (capitalized words concatenated together). 297 | Also known as UpperPascalCase. 298 | 299 | 300 | 301 | ## func [ToSNAKE](./strcase.go#L17) 302 | ``` go 303 | func ToSNAKE(s string) string 304 | ``` 305 | ToSNAKE returns words in SNAKE_CASE (upper case words with underscores). 306 | Also known as SCREAMING_SNAKE_CASE or UPPER_CASE. 307 | 308 | 309 | 310 | ## func [ToSnake](./strcase.go#L4) 311 | ``` go 312 | func ToSnake(s string) string 313 | ``` 314 | ToSnake returns words in snake_case (lower case words with underscores). 315 | 316 | 317 | 318 | 319 | ## type [Caser](./caser.go#L4-L7) 320 | ``` go 321 | type Caser struct { 322 | // contains filtered or unexported fields 323 | } 324 | 325 | ``` 326 | Caser allows for customization of parsing and intialisms 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | ### func [NewCaser](./caser.go#L24) 335 | ``` go 336 | func NewCaser(goInitialisms bool, initialismOverrides map[string]bool, splitFn SplitFn) *Caser 337 | ``` 338 | NewCaser returns a configured Caser. 339 | 340 | A Caser should be created when you want fine grained control over how the words are split. 341 | 342 | 343 | Notes on function arguments 344 | 345 | goInitialisms: Whether to use Golint's intialisms 346 | 347 | initialismOverrides: A mapping of extra initialisms 348 | Keys must be in ALL CAPS. Merged with Golint's if goInitialisms is set. 349 | Setting a key to false will override Golint's. 350 | 351 | splitFn: How to separate words 352 | Override the default split function. Consider using NewSplitFn to 353 | configure one instead of writing your own. 354 | 355 | 356 | 357 | 358 | 359 | ### func (\*Caser) [ToCamel](./caser.go#L80) 360 | ``` go 361 | func (c *Caser) ToCamel(s string) string 362 | ``` 363 | ToCamel returns words in camelCase (capitalized words concatenated together, with first word lower case). 364 | Also known as lowerCamelCase or mixedCase. 365 | 366 | 367 | 368 | 369 | ### func (\*Caser) [ToCase](./caser.go#L85) 370 | ``` go 371 | func (c *Caser) ToCase(s string, wordCase WordCase, delimiter rune) string 372 | ``` 373 | ToCase returns words with a given case and delimiter. 374 | 375 | 376 | 377 | 378 | ### func (\*Caser) [ToKEBAB](./caser.go#L68) 379 | ``` go 380 | func (c *Caser) ToKEBAB(s string) string 381 | ``` 382 | ToKEBAB returns words in KEBAB-CASE (upper case words with dashes). 383 | Also known as SCREAMING-KEBAB-CASE or SCREAMING-DASH-CASE. 384 | 385 | 386 | 387 | 388 | ### func (\*Caser) [ToKebab](./caser.go#L62) 389 | ``` go 390 | func (c *Caser) ToKebab(s string) string 391 | ``` 392 | ToKebab returns words in kebab-case (lower case words with dashes). 393 | Also known as dash-case. 394 | 395 | 396 | 397 | 398 | ### func (\*Caser) [ToPascal](./caser.go#L74) 399 | ``` go 400 | func (c *Caser) ToPascal(s string) string 401 | ``` 402 | ToPascal returns words in PascalCase (capitalized words concatenated together). 403 | Also known as UpperPascalCase. 404 | 405 | 406 | 407 | 408 | ### func (\*Caser) [ToSNAKE](./caser.go#L56) 409 | ``` go 410 | func (c *Caser) ToSNAKE(s string) string 411 | ``` 412 | ToSNAKE returns words in SNAKE_CASE (upper case words with underscores). 413 | Also known as SCREAMING_SNAKE_CASE or UPPER_CASE. 414 | 415 | 416 | 417 | 418 | ### func (\*Caser) [ToSnake](./caser.go#L50) 419 | ``` go 420 | func (c *Caser) ToSnake(s string) string 421 | ``` 422 | ToSnake returns words in snake_case (lower case words with underscores). 423 | 424 | 425 | 426 | 427 | ## type [SplitAction](./split.go#L111) 428 | ``` go 429 | type SplitAction int 430 | ``` 431 | SplitAction defines if and how to split a string 432 | 433 | 434 | ``` go 435 | const ( 436 | // Noop - Continue to next character 437 | Noop SplitAction = iota 438 | // Split - Split between words 439 | // e.g. to split between wordsWithoutDelimiters 440 | Split 441 | // SkipSplit - Split the word and drop the character 442 | // e.g. to split words with delimiters 443 | SkipSplit 444 | // Skip - Remove the character completely 445 | Skip 446 | ) 447 | ``` 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | ## type [SplitFn](./split.go#L6) 458 | ``` go 459 | type SplitFn func(prev, curr, next rune) SplitAction 460 | ``` 461 | SplitFn defines how to split a string into words 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | ### func [NewSplitFn](./split.go#L15-L18) 470 | ``` go 471 | func NewSplitFn( 472 | delimiters []rune, 473 | splitOptions ...SplitOption, 474 | ) SplitFn 475 | ``` 476 | NewSplitFn returns a SplitFn based on the options provided. 477 | 478 | NewSplitFn covers the majority of common options that other strcase 479 | libraries provide and should allow you to simply create a custom caser. 480 | For more complicated use cases, feel free to write your own SplitFn 481 | 482 | 483 | 484 | 485 | 486 | ## type [SplitOption](./split.go#L94) 487 | ``` go 488 | type SplitOption int 489 | ``` 490 | SplitOption are options that allow for configuring NewSplitFn 491 | 492 | 493 | ``` go 494 | const ( 495 | // SplitCase - FooBar -> Foo_Bar 496 | SplitCase SplitOption = iota 497 | // SplitAcronym - FOOBar -> Foo_Bar 498 | // It won't preserve FOO's case. If you want, you can set the Caser's initialisms so FOO will be in all caps 499 | SplitAcronym 500 | // SplitBeforeNumber - port80 -> port_80 501 | SplitBeforeNumber 502 | // SplitAfterNumber - 200status -> 200_status 503 | SplitAfterNumber 504 | // PreserveNumberFormatting - a.b.2,000.3.c -> a_b_2,000.3_c 505 | PreserveNumberFormatting 506 | ) 507 | ``` 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | ## type [WordCase](./convert.go#L6) 518 | ``` go 519 | type WordCase int 520 | ``` 521 | WordCase is an enumeration of the ways to format a word. 522 | 523 | 524 | ``` go 525 | const ( 526 | // Original - Preserve the original input strcase 527 | Original WordCase = iota 528 | // LowerCase - All letters lower cased (example) 529 | LowerCase 530 | // UpperCase - All letters upper cased (EXAMPLE) 531 | UpperCase 532 | // TitleCase - Only first letter upper cased (Example) 533 | TitleCase 534 | // CamelCase - TitleCase except lower case first word (exampleText) 535 | // Notably, even if the first word is an initialism, it will be lower 536 | // cased. This is important for code generators where capital letters 537 | // mean exported functions. i.e. jsonString(), not JSONString() 538 | CamelCase 539 | ) 540 | ``` 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | -------------------------------------------------------------------------------- /assert.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | // We use a lightweight replacement for testify/assert to reduce dependencies 4 | 5 | // testingT interface allows us to test our assert functions 6 | type testingT interface { 7 | Logf(format string, args ...interface{}) 8 | Fail() 9 | } 10 | 11 | // assertTrue will fail if the value is not true 12 | func assertTrue(t testingT, value bool) { 13 | if !value { 14 | t.Fail() 15 | } 16 | } 17 | 18 | // assertEqual will fail if the two strings are not equal 19 | func assertEqual(t testingT, expected, actual string) { 20 | if expected != actual { 21 | t.Logf("Expected: %s Actual: %s", expected, actual) 22 | t.Fail() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /assert_test.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type fakeT struct { 9 | fail bool 10 | log string 11 | } 12 | 13 | func (t *fakeT) Fail() { 14 | t.fail = true 15 | } 16 | func (t *fakeT) Logf(format string, args ...interface{}) { 17 | t.log = fmt.Sprintf(format, args...) 18 | } 19 | 20 | func TestAssertTrue(t *testing.T) { 21 | { 22 | f := &fakeT{} 23 | assertTrue(f, true) 24 | if f.fail == true { 25 | t.Fail() 26 | } 27 | } 28 | { 29 | f := &fakeT{} 30 | assertTrue(f, false) 31 | if f.fail != true { 32 | t.Fail() 33 | } 34 | } 35 | } 36 | 37 | func TestAssertEqual(t *testing.T) { 38 | { 39 | f := &fakeT{} 40 | assertEqual(f, "foo", "foo") 41 | if f.fail == true { 42 | t.Fail() 43 | } 44 | } 45 | { 46 | f := &fakeT{} 47 | assertEqual(f, "foo", "bar") 48 | if f.fail != true { 49 | t.Fail() 50 | } 51 | if f.log != "Expected: foo Actual: bar" { 52 | t.Fail() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /benchmark/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "testing" 7 | 8 | gv "github.com/asaskevich/govalidator" 9 | "github.com/ettle/strcase" 10 | fa "github.com/fatih/camelcase" 11 | ia "github.com/iancoleman/strcase" 12 | se "github.com/segmentio/go-camelcase" 13 | st "github.com/stoewer/go-strcase" 14 | "golang.org/x/text/cases" 15 | "golang.org/x/text/language" 16 | ) 17 | 18 | // Test cases: 19 | // Minor variations in length , but doesn't seem to vastly change the relative 20 | // results 21 | // var testLower = "id" 22 | // var testLower = "get user id" 23 | // var testLower = "filter on user permission group id request date" 24 | 25 | var testLower = "get user id" 26 | 27 | var testSnake = strcase.ToSnake(testLower) 28 | var testSNAKE = strcase.ToSNAKE(testLower) 29 | var testKebab = strcase.ToKebab(testLower) 30 | var testGoSnake = strcase.ToGoSnake(testLower) 31 | var testCamel = strcase.ToCamel(testLower) 32 | var testPascal = strcase.ToPascal(testLower) 33 | var testTitle = strcase.ToCase(testLower, strcase.TitleCase, ' ') 34 | var testSplit = strcase.ToCase(testLower, strcase.TitleCase, '_') 35 | 36 | var testCustom = "ssl*user*id" 37 | var testCustomExpected = "SSL_user_id" 38 | 39 | func BenchmarkToTitle(b *testing.B) { 40 | var s string 41 | for n := 0; n < b.N; n++ { 42 | s = strcase.ToCase(testLower, strcase.TitleCase, ' ') 43 | } 44 | expected := testTitle 45 | if expected != s { 46 | b.Fatalf("Expected %s, got %s", expected, s) 47 | } 48 | } 49 | func BenchmarkToSnake(b *testing.B) { 50 | var s string 51 | for n := 0; n < b.N; n++ { 52 | s = strcase.ToSnake(testCamel) 53 | } 54 | expected := testSnake 55 | if expected != s { 56 | b.Fatalf("Expected %s, got %s", expected, s) 57 | } 58 | } 59 | func BenchmarkToSNAKE(b *testing.B) { 60 | var s string 61 | for n := 0; n < b.N; n++ { 62 | s = strcase.ToSNAKE(testCamel) 63 | } 64 | expected := testSNAKE 65 | if expected != s { 66 | b.Fatalf("Expected %s, got %s", expected, s) 67 | } 68 | } 69 | func BenchmarkToGoSnake(b *testing.B) { 70 | var s string 71 | for n := 0; n < b.N; n++ { 72 | s = strcase.ToGoSnake(testCamel) 73 | } 74 | expected := testGoSnake 75 | if expected != s { 76 | b.Fatalf("Expected %s, got %s", expected, s) 77 | } 78 | } 79 | func BenchmarkToCustomCaser(b *testing.B) { 80 | c := strcase.NewCaser(false, map[string]bool{"SSL": true}, strcase.NewSplitFn([]rune{'*'})) 81 | b.ResetTimer() 82 | var s string 83 | for n := 0; n < b.N; n++ { 84 | s = c.ToSnake(testCustom) 85 | } 86 | expected := testCustomExpected 87 | if expected != s { 88 | b.Fatalf("Expected %s, got %s", expected, s) 89 | } 90 | } 91 | 92 | // ******************************************************** 93 | // Stdlib 94 | // 95 | 96 | // golang.org/x/text/cases is the recommended replacement 97 | // for stdlib's now deprecated strings.ToTitle 98 | func BenchmarkGolangOrgXTextCases(b *testing.B) { 99 | caser := cases.Title(language.AmericanEnglish) 100 | var s string 101 | for n := 0; n < b.N; n++ { 102 | s = caser.String(testLower) 103 | } 104 | expected := testTitle 105 | if expected != s { 106 | b.Fatalf("Expected %s, got %s", expected, s) 107 | } 108 | } 109 | 110 | // ******************************************************** 111 | // Other packages 112 | // 113 | 114 | // From github.com/segmentio/go-camelcase 115 | // MIT License 116 | // A very fast package - no unicode, intialism, or customizations 117 | // If you need speed, use or fork this library or segmentio/go-snakecase 118 | func BenchmarkSegment(b *testing.B) { 119 | var s string 120 | for n := 0; n < b.N; n++ { 121 | s = se.Camelcase(testSnake) 122 | } 123 | expected := testCamel 124 | if expected != s { 125 | b.Fatalf("Expected %s, got %s", expected, s) 126 | } 127 | } 128 | 129 | // From github.com/iancoleman/strcase 130 | // The most popular go strcase packages I found 131 | // About an order of magnitude slower 132 | func BenchmarkToSnakeIan(b *testing.B) { 133 | var s string 134 | for n := 0; n < b.N; n++ { 135 | s = ia.ToSnake(testCamel) 136 | } 137 | expected := testSnake 138 | if expected != s { 139 | b.Fatalf("Expected %s, got %s", expected, s) 140 | } 141 | } 142 | 143 | // From github.com/stoewer/go-strcase 144 | // MIT License 145 | // In most tests, it's just a smidge slower than the comparable function 146 | func BenchmarkToSnakeStoewer(b *testing.B) { 147 | var s string 148 | for n := 0; n < b.N; n++ { 149 | s = st.SnakeCase(testCamel) 150 | } 151 | expected := testSnake 152 | if expected != s { 153 | b.Fatalf("Expected %s, got %s", expected, s) 154 | } 155 | } 156 | 157 | func BenchmarkGoValidator(b *testing.B) { 158 | var s string 159 | for n := 0; n < b.N; n++ { 160 | s = gv.CamelCaseToUnderscore(testCamel) 161 | } 162 | expected := testSnake 163 | if expected != s { 164 | b.Fatalf("Expected %s, got %s", expected, s) 165 | } 166 | } 167 | 168 | // From github.com/fatih/camelcase 169 | // MIT License 170 | // This isn't quite an even comparison since it only splits and doesn't 171 | // specify how to join the words back together. Figured strings.Join was 172 | // reasonable 173 | func BenchmarkToSnakeFatih(b *testing.B) { 174 | var s string 175 | for n := 0; n < b.N; n++ { 176 | sa := fa.Split(testPascal) 177 | s = strings.Join(sa, "_") 178 | } 179 | expected := testSplit 180 | if expected != s { 181 | b.Fatalf("Expected %s, got %s", expected, s) 182 | } 183 | } 184 | 185 | // ******************************************************** 186 | // Code snippets 187 | // 188 | 189 | // From https://www.golangprograms.com/golang-convert-string-into-snake-case.html 190 | // Terms of use: https://www.golangprograms.com/terms-of-use 191 | var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") 192 | var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") 193 | 194 | func ToSnakeCaseGolangPrograms(str string) string { 195 | snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") 196 | snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") 197 | return strings.ToLower(snake) 198 | } 199 | 200 | func BenchmarkToSnakeGolangPrograms(b *testing.B) { 201 | var s string 202 | for n := 0; n < b.N; n++ { 203 | s = ToSnakeCaseGolangPrograms(testCamel) 204 | } 205 | expected := testSnake 206 | if expected != s { 207 | b.Fatalf("Expected %s, got %s", expected, s) 208 | } 209 | } 210 | 211 | // From https://siongui.github.io/2017/02/18/go-kebab-case-to-camel-case/ 212 | // License: UNLICENSE 213 | // https://github.com/siongui/userpages/blob/master/UNLICENSE 214 | func kebabToCamelCase(kebab string) (camelCase string) { 215 | isToUpper := false 216 | for _, runeValue := range kebab { 217 | if isToUpper { 218 | camelCase += strings.ToUpper(string(runeValue)) 219 | isToUpper = false 220 | } else { 221 | if runeValue == '-' { 222 | isToUpper = true 223 | } else { 224 | camelCase += string(runeValue) 225 | } 226 | } 227 | } 228 | return 229 | } 230 | 231 | func BenchmarkToSnakeSiongui(b *testing.B) { 232 | var s string 233 | for n := 0; n < b.N; n++ { 234 | s = kebabToCamelCase(testKebab) 235 | } 236 | expected := testCamel 237 | if expected != s { 238 | b.Fatalf("Expected %s, got %s", expected, s) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /benchmark/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ettle/strcase/benchmark 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d 7 | github.com/ettle/strcase v0.0.0-00000000000000-000000000000 8 | github.com/fatih/camelcase v1.0.0 9 | github.com/iancoleman/strcase v0.2.0 10 | github.com/segmentio/go-camelcase v0.0.0-20160726192923-7085f1e3c734 11 | github.com/stoewer/go-strcase v1.2.0 12 | golang.org/x/text v0.6.0 13 | ) 14 | 15 | replace github.com/ettle/strcase => ../ 16 | -------------------------------------------------------------------------------- /benchmark/go.sum: -------------------------------------------------------------------------------- 1 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= 2 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 6 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 7 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= 8 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/segmentio/go-camelcase v0.0.0-20160726192923-7085f1e3c734 h1:Cpx2WLIv6fuPvaJAHNhYOgYzk/8RcJXu/8+mOrxf2KM= 12 | github.com/segmentio/go-camelcase v0.0.0-20160726192923-7085f1e3c734/go.mod h1:hqVOMAwu+ekffC3Tvq5N1ljnXRrFKcaSjbCmQ8JgYaI= 13 | github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= 14 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 17 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 18 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 19 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 20 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 21 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 22 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 23 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 24 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 25 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 26 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 33 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 34 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 35 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 36 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 37 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 38 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 39 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 40 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 41 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 42 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 46 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 47 | -------------------------------------------------------------------------------- /caser.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | // Caser allows for customization of parsing and intialisms 4 | type Caser struct { 5 | initialisms map[string]bool 6 | splitFn SplitFn 7 | } 8 | 9 | // NewCaser returns a configured Caser. 10 | // 11 | // A Caser should be created when you want fine grained control over how the words are split. 12 | // 13 | // Notes on function arguments 14 | // 15 | // goInitialisms: Whether to use Golint's intialisms 16 | // 17 | // initialismOverrides: A mapping of extra initialisms 18 | // Keys must be in ALL CAPS. Merged with Golint's if goInitialisms is set. 19 | // Setting a key to false will override Golint's. 20 | // 21 | // splitFn: How to separate words 22 | // Override the default split function. Consider using NewSplitFn to 23 | // configure one instead of writing your own. 24 | func NewCaser(goInitialisms bool, initialismOverrides map[string]bool, splitFn SplitFn) *Caser { 25 | c := &Caser{ 26 | initialisms: golintInitialisms, 27 | splitFn: splitFn, 28 | } 29 | 30 | if c.splitFn == nil { 31 | c.splitFn = defaultSplitFn 32 | } 33 | 34 | if goInitialisms && initialismOverrides != nil { 35 | c.initialisms = map[string]bool{} 36 | for k, v := range golintInitialisms { 37 | c.initialisms[k] = v 38 | } 39 | for k, v := range initialismOverrides { 40 | c.initialisms[k] = v 41 | } 42 | } else if !goInitialisms { 43 | c.initialisms = initialismOverrides 44 | } 45 | 46 | return c 47 | } 48 | 49 | // ToSnake returns words in snake_case (lower case words with underscores). 50 | func (c *Caser) ToSnake(s string) string { 51 | return convert(s, c.splitFn, '_', LowerCase, c.initialisms) 52 | } 53 | 54 | // ToSNAKE returns words in SNAKE_CASE (upper case words with underscores). 55 | // Also known as SCREAMING_SNAKE_CASE or UPPER_CASE. 56 | func (c *Caser) ToSNAKE(s string) string { 57 | return convert(s, c.splitFn, '_', UpperCase, c.initialisms) 58 | } 59 | 60 | // ToKebab returns words in kebab-case (lower case words with dashes). 61 | // Also known as dash-case. 62 | func (c *Caser) ToKebab(s string) string { 63 | return convert(s, c.splitFn, '-', LowerCase, c.initialisms) 64 | } 65 | 66 | // ToKEBAB returns words in KEBAB-CASE (upper case words with dashes). 67 | // Also known as SCREAMING-KEBAB-CASE or SCREAMING-DASH-CASE. 68 | func (c *Caser) ToKEBAB(s string) string { 69 | return convert(s, c.splitFn, '-', UpperCase, c.initialisms) 70 | } 71 | 72 | // ToPascal returns words in PascalCase (capitalized words concatenated together). 73 | // Also known as UpperPascalCase. 74 | func (c *Caser) ToPascal(s string) string { 75 | return convert(s, c.splitFn, '\x00', TitleCase, c.initialisms) 76 | } 77 | 78 | // ToCamel returns words in camelCase (capitalized words concatenated together, with first word lower case). 79 | // Also known as lowerCamelCase or mixedCase. 80 | func (c *Caser) ToCamel(s string) string { 81 | return convert(s, c.splitFn, '\x00', CamelCase, c.initialisms) 82 | } 83 | 84 | // ToCase returns words with a given case and delimiter. 85 | func (c *Caser) ToCase(s string, wordCase WordCase, delimiter rune) string { 86 | return convert(s, c.splitFn, delimiter, wordCase, c.initialisms) 87 | } 88 | -------------------------------------------------------------------------------- /caser_test.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "unicode" 7 | ) 8 | 9 | func TestCaserAll(t *testing.T) { 10 | c := NewCaser(true, nil, nil) 11 | 12 | type data struct { 13 | input string 14 | snake string 15 | SNAKE string 16 | kebab string 17 | KEBAB string 18 | pascal string 19 | camel string 20 | title string 21 | } 22 | for _, test := range []data{ 23 | { 24 | input: "Hello world!", 25 | snake: "hello_world!", 26 | SNAKE: "HELLO_WORLD!", 27 | kebab: "hello-world!", 28 | KEBAB: "HELLO-WORLD!", 29 | pascal: "HelloWorld!", 30 | camel: "helloWorld!", 31 | title: "Hello World!", 32 | }, 33 | } { 34 | t.Run(test.input, func(t *testing.T) { 35 | output := data{ 36 | input: test.input, 37 | snake: c.ToSnake(test.input), 38 | SNAKE: c.ToSNAKE(test.input), 39 | kebab: c.ToKebab(test.input), 40 | KEBAB: c.ToKEBAB(test.input), 41 | pascal: c.ToPascal(test.input), 42 | camel: c.ToCamel(test.input), 43 | title: c.ToCase(test.input, TitleCase, ' '), 44 | } 45 | assertTrue(t, test == output) 46 | }) 47 | } 48 | } 49 | 50 | func TestNewCaser(t *testing.T) { 51 | t.Run("Has defaults when unspecified", func(t *testing.T) { 52 | c := NewCaser(true, nil, nil) 53 | assertTrue(t, reflect.DeepEqual(golintInitialisms, c.initialisms)) 54 | assertTrue(t, c.splitFn != nil) 55 | }) 56 | t.Run("Merges", func(t *testing.T) { 57 | c := NewCaser(true, map[string]bool{"SSL": true, "HTML": false}, nil) 58 | assertTrue(t, !reflect.DeepEqual(golintInitialisms, c.initialisms)) 59 | assertTrue(t, c.initialisms["UUID"]) 60 | assertTrue(t, c.initialisms["SSL"]) 61 | assertTrue(t, !c.initialisms["HTML"]) 62 | assertTrue(t, c.splitFn != nil) 63 | }) 64 | 65 | t.Run("No Go initialisms", func(t *testing.T) { 66 | c := NewCaser(false, map[string]bool{"SSL": true, "HTML": false}, NewSplitFn([]rune{' '})) 67 | assertTrue(t, !reflect.DeepEqual(golintInitialisms, c.initialisms)) 68 | assertTrue(t, !c.initialisms["UUID"]) 69 | assertTrue(t, c.initialisms["SSL"]) 70 | assertTrue(t, !c.initialisms["HTML"]) 71 | assertEqual(t, "hTml with SSL", c.ToCase("hTml with SsL", Original, ' ')) 72 | assertTrue(t, c.splitFn != nil) 73 | }) 74 | 75 | t.Run("Preserve number formatting", func(t *testing.T) { 76 | c := NewCaser( 77 | false, 78 | map[string]bool{"SSL": true, "HTML": false}, 79 | NewSplitFn( 80 | []rune{'*', '.', ','}, 81 | SplitCase, 82 | SplitAcronym, 83 | PreserveNumberFormatting, 84 | )) 85 | assertTrue(t, !reflect.DeepEqual(golintInitialisms, c.initialisms)) 86 | assertEqual(t, "http200", c.ToSnake("http200")) 87 | assertEqual(t, "VERSION2.3_R3_8A_HTTP_ERROR_CODE", c.ToSNAKE("version2.3R3*8a,HTTPErrorCode")) 88 | }) 89 | 90 | t.Run("Preserve number formatting and split before and after number", func(t *testing.T) { 91 | c := NewCaser( 92 | false, 93 | map[string]bool{"SSL": true, "HTML": false}, 94 | NewSplitFn( 95 | []rune{'*', '.', ','}, 96 | SplitCase, 97 | SplitAcronym, 98 | PreserveNumberFormatting, 99 | SplitBeforeNumber, 100 | SplitAfterNumber, 101 | )) 102 | assertEqual(t, "http_200", c.ToSnake("http200")) 103 | assertEqual(t, "VERSION_2.3_R_3_8_A_HTTP_ERROR_CODE", c.ToSNAKE("version2.3R3*8a,HTTPErrorCode")) 104 | }) 105 | 106 | t.Run("Skip non letters", func(t *testing.T) { 107 | c := NewCaser( 108 | false, 109 | nil, 110 | func(prec, curr, next rune) SplitAction { 111 | if unicode.IsNumber(curr) { 112 | return Noop 113 | } else if unicode.IsSpace(curr) { 114 | return SkipSplit 115 | } 116 | return Skip 117 | }) 118 | assertEqual(t, "", c.ToSnake("")) 119 | assertEqual(t, "1130_23_2009", c.ToCase("DateTime: 11:30 AM May 23rd, 2009", Original, '_')) 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /convert.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | import "strings" 4 | 5 | // WordCase is an enumeration of the ways to format a word. 6 | type WordCase int 7 | 8 | const ( 9 | // Original - Preserve the original input strcase 10 | Original WordCase = iota 11 | // LowerCase - All letters lower cased (example) 12 | LowerCase 13 | // UpperCase - All letters upper cased (EXAMPLE) 14 | UpperCase 15 | // TitleCase - Only first letter upper cased (Example) 16 | TitleCase 17 | // CamelCase - TitleCase except lower case first word (exampleText) 18 | // Notably, even if the first word is an initialism, it will be lower 19 | // cased. This is important for code generators where capital letters 20 | // mean exported functions. i.e. jsonString(), not JSONString() 21 | CamelCase 22 | ) 23 | 24 | // We have 3 convert functions for performance reasons 25 | // The general convert could handle everything, but is not optimized 26 | // 27 | // The other two functions are optimized for the general use cases - that is the non-custom caser functions 28 | // Case 1: Any Case and supports Go Initialisms 29 | // Case 2: UpperCase words, which don't need to support initialisms since everything is in upper case 30 | 31 | // convertWithoutInitialims only works for to UpperCase and LowerCase 32 | // 33 | //nolint:gocyclo 34 | func convertWithoutInitialisms(input string, delimiter rune, wordCase WordCase) string { 35 | input = strings.TrimSpace(input) 36 | runes := []rune(input) 37 | if len(runes) == 0 { 38 | return "" 39 | } 40 | 41 | var b strings.Builder 42 | b.Grow(len(input) + 4) // In case we need to write delimiters where they weren't before 43 | 44 | var prev, curr rune 45 | next := runes[0] // 0 length will have already returned so safe to index 46 | inWord := false 47 | firstWord := true 48 | for i := 0; i < len(runes); i++ { 49 | prev = curr 50 | curr = next 51 | if i+1 == len(runes) { 52 | next = 0 53 | } else { 54 | next = runes[i+1] 55 | } 56 | 57 | switch defaultSplitFn(prev, curr, next) { 58 | case SkipSplit: 59 | if inWord && delimiter != 0 { 60 | b.WriteRune(delimiter) 61 | } 62 | inWord = false 63 | continue 64 | case Split: 65 | if inWord && delimiter != 0 { 66 | b.WriteRune(delimiter) 67 | } 68 | inWord = false 69 | } 70 | switch wordCase { 71 | case UpperCase: 72 | b.WriteRune(toUpper(curr)) 73 | case LowerCase: 74 | b.WriteRune(toLower(curr)) 75 | case TitleCase: 76 | if inWord { 77 | b.WriteRune(toLower(curr)) 78 | } else { 79 | b.WriteRune(toUpper(curr)) 80 | } 81 | case CamelCase: 82 | if inWord { 83 | b.WriteRune(toLower(curr)) 84 | } else if firstWord { 85 | b.WriteRune(toLower(curr)) 86 | firstWord = false 87 | } else { 88 | b.WriteRune(toUpper(curr)) 89 | } 90 | default: 91 | // Must be original case 92 | b.WriteRune(curr) 93 | } 94 | inWord = true 95 | } 96 | return b.String() 97 | } 98 | 99 | // convertWithGoInitialisms changes a input string to a certain case with a 100 | // delimiter, respecting go initialisms but not skip runes 101 | // 102 | //nolint:gocyclo 103 | func convertWithGoInitialisms(input string, delimiter rune, wordCase WordCase) string { 104 | input = strings.TrimSpace(input) 105 | runes := []rune(input) 106 | if len(runes) == 0 { 107 | return "" 108 | } 109 | 110 | var b strings.Builder 111 | b.Grow(len(input) + 4) // In case we need to write delimiters where they weren't before 112 | 113 | firstWord := true 114 | 115 | addWord := func(start, end int) { 116 | if start == end { 117 | return 118 | } 119 | 120 | if !firstWord && delimiter != 0 { 121 | b.WriteRune(delimiter) 122 | } 123 | 124 | // Don't bother with initialisms if the word is longer than 5 125 | // A quick proxy to avoid the extra memory allocations 126 | if end-start <= 5 { 127 | var word strings.Builder 128 | word.Grow(end - start) 129 | for i := start; i < end; i++ { 130 | word.WriteRune(toUpper(runes[i])) 131 | } 132 | w := word.String() 133 | if golintInitialisms[w] { 134 | if !firstWord || wordCase != CamelCase { 135 | b.WriteString(w) 136 | firstWord = false 137 | return 138 | } 139 | } 140 | } 141 | 142 | for i := start; i < end; i++ { 143 | r := runes[i] 144 | switch wordCase { 145 | case UpperCase: 146 | panic("use convertWithoutInitialisms instead") 147 | case LowerCase: 148 | b.WriteRune(toLower(r)) 149 | case TitleCase: 150 | if i == start { 151 | b.WriteRune(toUpper(r)) 152 | } else { 153 | b.WriteRune(toLower(r)) 154 | } 155 | case CamelCase: 156 | if !firstWord && i == start { 157 | b.WriteRune(toUpper(r)) 158 | } else { 159 | b.WriteRune(toLower(r)) 160 | } 161 | default: 162 | b.WriteRune(r) 163 | } 164 | } 165 | firstWord = false 166 | } 167 | 168 | var prev, curr rune 169 | next := runes[0] // 0 length will have already returned so safe to index 170 | wordStart := 0 171 | for i := 0; i < len(runes); i++ { 172 | prev = curr 173 | curr = next 174 | if i+1 == len(runes) { 175 | next = 0 176 | } else { 177 | next = runes[i+1] 178 | } 179 | 180 | switch defaultSplitFn(prev, curr, next) { 181 | case Split: 182 | addWord(wordStart, i) 183 | wordStart = i 184 | case SkipSplit: 185 | addWord(wordStart, i) 186 | wordStart = i + 1 187 | } 188 | } 189 | 190 | if wordStart != len(runes) { 191 | addWord(wordStart, len(runes)) 192 | } 193 | return b.String() 194 | } 195 | 196 | // convert changes a input string to a certain case with a delimiter, 197 | // respecting arbitrary initialisms and skip characters 198 | // 199 | //nolint:gocyclo 200 | func convert(input string, fn SplitFn, delimiter rune, wordCase WordCase, 201 | initialisms map[string]bool) string { 202 | input = strings.TrimSpace(input) 203 | runes := []rune(input) 204 | if len(runes) == 0 { 205 | return "" 206 | } 207 | 208 | var b strings.Builder 209 | b.Grow(len(input) + 4) // In case we need to write delimiters where they weren't before 210 | 211 | firstWord := true 212 | var skipIndexes []int 213 | 214 | addWord := func(start, end int) { 215 | // If you have nothing good to say, say nothing at all 216 | if start == end || len(skipIndexes) == end-start { 217 | skipIndexes = nil 218 | return 219 | } 220 | 221 | // If you have something to say, start with a delimiter 222 | if !firstWord && delimiter != 0 { 223 | b.WriteRune(delimiter) 224 | } 225 | 226 | // Check if you're an initialism 227 | // Note - we don't check skip characters here since initialisms 228 | // will probably never have junk characters in between 229 | // I'm open to it if there is a use case 230 | if initialisms != nil { 231 | var word strings.Builder 232 | word.Grow(end - start) 233 | for i := start; i < end; i++ { 234 | word.WriteRune(toUpper(runes[i])) 235 | } 236 | w := word.String() 237 | if initialisms[w] { 238 | if !firstWord || wordCase != CamelCase { 239 | b.WriteString(w) 240 | firstWord = false 241 | return 242 | } 243 | } 244 | } 245 | 246 | skipIdx := 0 247 | for i := start; i < end; i++ { 248 | if len(skipIndexes) > 0 && skipIdx < len(skipIndexes) && i == skipIndexes[skipIdx] { 249 | skipIdx++ 250 | continue 251 | } 252 | r := runes[i] 253 | switch wordCase { 254 | case UpperCase: 255 | b.WriteRune(toUpper(r)) 256 | case LowerCase: 257 | b.WriteRune(toLower(r)) 258 | case TitleCase: 259 | if i == start { 260 | b.WriteRune(toUpper(r)) 261 | } else { 262 | b.WriteRune(toLower(r)) 263 | } 264 | case CamelCase: 265 | if !firstWord && i == start { 266 | b.WriteRune(toUpper(r)) 267 | } else { 268 | b.WriteRune(toLower(r)) 269 | } 270 | default: 271 | b.WriteRune(r) 272 | } 273 | } 274 | firstWord = false 275 | skipIndexes = nil 276 | } 277 | 278 | var prev, curr rune 279 | next := runes[0] // 0 length will have already returned so safe to index 280 | wordStart := 0 281 | for i := 0; i < len(runes); i++ { 282 | prev = curr 283 | curr = next 284 | if i+1 == len(runes) { 285 | next = 0 286 | } else { 287 | next = runes[i+1] 288 | } 289 | 290 | switch fn(prev, curr, next) { 291 | case Skip: 292 | skipIndexes = append(skipIndexes, i) 293 | case Split: 294 | addWord(wordStart, i) 295 | wordStart = i 296 | case SkipSplit: 297 | addWord(wordStart, i) 298 | wordStart = i + 1 299 | } 300 | } 301 | 302 | if wordStart != len(runes) { 303 | addWord(wordStart, len(runes)) 304 | } 305 | return b.String() 306 | } 307 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package strcase is a package for converting strings into various word cases 3 | (e.g. snake_case, camelCase) 4 | 5 | go get -u github.com/ettle/strcase 6 | 7 | Example usage 8 | 9 | strcase.ToSnake("Hello World") // hello_world 10 | strcase.ToSNAKE("Hello World") // HELLO_WORLD 11 | 12 | strcase.ToKebab("helloWorld") // hello-world 13 | strcase.ToKEBAB("helloWorld") // HELLO-WORLD 14 | 15 | strcase.ToPascal("hello-world") // HelloWorld 16 | strcase.ToCamel("hello-world") // helloWorld 17 | 18 | // Handle odd cases 19 | strcase.ToSnake("FOOBar") // foo_bar 20 | 21 | // Support Go initialisms 22 | strcase.ToGoPascal("http_response") // HTTPResponse 23 | 24 | // Specify case and delimiter 25 | strcase.ToCase("HelloWorld", strcase.UpperCase, '.') // HELLO.WORLD 26 | 27 | ## Why this package 28 | 29 | String strcase is pretty straight forward and there are a number of methods to 30 | do it. This package is fully featured, more customizable, better tested, and 31 | faster than other packages and what you would probably whip up yourself. 32 | 33 | ### Unicode support 34 | 35 | We work for with unicode strings and pay very little performance penalty for it 36 | as we optimized for the common use case of ASCII only strings. 37 | 38 | ### Customization 39 | 40 | You can create a custom caser that changes the behavior to what you want. This 41 | customization also reduces the pressure for us to change the default behavior 42 | which means that things are more stable for everyone involved. The goal is to 43 | make the common path easy and fast, while making the uncommon path possible. 44 | 45 | c := NewCaser( 46 | // Use Go's default initialisms e.g. ID, HTML 47 | true, 48 | // Override initialisms (e.g. don't initialize HTML but initialize SSL 49 | map[string]bool{"SSL": true, "HTML": false}, 50 | // Write your own custom SplitFn 51 | // 52 | NewSplitFn( 53 | []rune{'*', '.', ','}, 54 | SplitCase, 55 | SplitAcronym, 56 | PreserveNumberFormatting, 57 | SplitBeforeNumber, 58 | SplitAfterNumber, 59 | )) 60 | assert.Equal(t, "http_200", c.ToSnake("http200")) 61 | 62 | ### Initialism support 63 | 64 | By default, we use the golint intialisms list. You can customize and override 65 | the initialisms if you wish to add additional ones, such as "SSL" or "CMS" or 66 | domain specific ones to your industry. 67 | 68 | ToGoPascal("http_response") // HTTPResponse 69 | ToGoSnake("http_response") // HTTP_response 70 | 71 | ### Test coverage 72 | 73 | We have a wide ranging test suite to make sure that we understand our behavior. 74 | Test coverage isn't everything, but we aim for 100% coverage. 75 | 76 | ### Fast 77 | 78 | Optimized to reduce memory allocations with Builder. Benchmarked and optimized 79 | around common cases. 80 | 81 | We're on par with the fastest packages (that have less features) and much 82 | faster than others. We also benchmarked against code snippets. Using string 83 | builders to reduce memory allocation and reordering boolean checks for the 84 | common cases have a large performance impact. 85 | 86 | Hopefully I was fair to each library and happy to rerun benchmarks differently 87 | or reword my commentary based on suggestions or updates. 88 | 89 | // This package - faster then almost all libraries 90 | // Initialisms are more complicated and slightly slower, but still fast 91 | BenchmarkToTitle-96 9617142 125.7 ns/op 16 B/op 1 allocs/op 92 | BenchmarkToSnake-96 10659919 120.7 ns/op 16 B/op 1 allocs/op 93 | BenchmarkToSNAKE-96 9018282 126.4 ns/op 16 B/op 1 allocs/op 94 | BenchmarkToGoSnake-96 4903687 254.5 ns/op 26 B/op 4 allocs/op 95 | BenchmarkToCustomCaser-96 4434489 265.0 ns/op 28 B/op 4 allocs/op 96 | 97 | // Segment has very fast snake case and camel case libraries 98 | // No features or customization, but very very fast 99 | BenchmarkSegment-96 33625734 35.54 ns/op 16 B/op 1 allocs/op 100 | 101 | // Iancoleman has gotten some performance improvements, but remains 102 | // without unicode support and lacks fine-grained customization 103 | BenchmarkToSnakeIan-96 13141522 92.99 ns/op 16 B/op 1 allocs/op 104 | 105 | // Stdlib strings.Title is deprecated; using golang.org/x.text 106 | BenchmarkGolangOrgXTextCases-96 4665676 262.5 ns/op 272 B/op 2 allocs/op 107 | 108 | // Other libraries or code snippets 109 | // - Most are slower, by up to an order of magnitude 110 | // - No support for initialisms or customization 111 | // - Some generate only camelCase or snake_case 112 | // - Many lack unicode support 113 | BenchmarkToSnakeStoewer-96 8095468 148.9 ns/op 64 B/op 2 allocs/op 114 | // Copying small rune arrays is slow 115 | BenchmarkToSnakeSiongui-96 2912593 401.7 ns/op 112 B/op 19 allocs/op 116 | BenchmarkGoValidator-96 3493800 342.6 ns/op 184 B/op 9 allocs/op 117 | // String alloction is slow 118 | BenchmarkToSnakeFatih-96 1282648 945.1 ns/op 616 B/op 26 allocs/op 119 | // Regexp is slow 120 | BenchmarkToSnakeGolangPrograms-96 778674 1495 ns/op 227 B/op 11 allocs/op 121 | 122 | // These results aren't a surprise - my initial version of this library was 123 | // painfully slow. I think most of us, without spending some time with 124 | // profilers and benchmarks, would write also something on the slower side. 125 | 126 | ### Zero dependencies 127 | 128 | That's right - zero. We only import the Go standard library. No hassles with 129 | dependencies, licensing, security alerts. 130 | 131 | ## Why not this package 132 | 133 | If every nanosecond matters and this is used in a tight loop, use segment.io's 134 | libraries (https://github.com/segmentio/go-snakecase and 135 | https://github.com/segmentio/go-camelcase). They lack features, but make up for 136 | it by being blazing fast. 137 | 138 | ## Migrating from other packages 139 | 140 | If you are migrating from from another package, you may find slight differences 141 | in output. To reduce the delta, you may find it helpful to use the following 142 | custom casers to mimic the behavior of the other package. 143 | 144 | // From https://github.com/iancoleman/strcase 145 | var c = NewCaser(false, nil, NewSplitFn([]rune{'_', '-', '.'}, SplitCase, SplitAcronym, SplitBeforeNumber)) 146 | 147 | // From https://github.com/stoewer/go-strcase 148 | var c = NewCaser(false, nil, NewSplitFn([]rune{'_', '-'}, SplitCase), SplitAcronym) 149 | */ 150 | package strcase 151 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ettle/strcase 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ettle/strcase/38713c761e5266d3f216d955c109504e01feb8d9/go.sum -------------------------------------------------------------------------------- /initialism.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | // golintInitialisms are the golint initialisms 4 | var golintInitialisms = map[string]bool{ 5 | "ACL": true, 6 | "API": true, 7 | "ASCII": true, 8 | "CPU": true, 9 | "CSS": true, 10 | "DNS": true, 11 | "EOF": true, 12 | "GUID": true, 13 | "HTML": true, 14 | "HTTP": true, 15 | "HTTPS": true, 16 | "ID": true, 17 | "IP": true, 18 | "JSON": true, 19 | "LHS": true, 20 | "QPS": true, 21 | "RAM": true, 22 | "RHS": true, 23 | "RPC": true, 24 | "SLA": true, 25 | "SMTP": true, 26 | "SQL": true, 27 | "SSH": true, 28 | "TCP": true, 29 | "TLS": true, 30 | "TTL": true, 31 | "UDP": true, 32 | "UI": true, 33 | "UID": true, 34 | "UUID": true, 35 | "URI": true, 36 | "URL": true, 37 | "UTF8": true, 38 | "VM": true, 39 | "XML": true, 40 | "XMPP": true, 41 | "XSRF": true, 42 | "XSS": true, 43 | } 44 | -------------------------------------------------------------------------------- /split.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | import "unicode" 4 | 5 | // SplitFn defines how to split a string into words 6 | type SplitFn func(prev, curr, next rune) SplitAction 7 | 8 | // NewSplitFn returns a SplitFn based on the options provided. 9 | // 10 | // NewSplitFn covers the majority of common options that other strcase 11 | // libraries provide and should allow you to simply create a custom caser. 12 | // For more complicated use cases, feel free to write your own SplitFn 13 | // 14 | //nolint:gocyclo 15 | func NewSplitFn( 16 | delimiters []rune, 17 | splitOptions ...SplitOption, 18 | ) SplitFn { 19 | var splitCase, splitAcronym, splitBeforeNumber, splitAfterNumber, preserveNumberFormatting bool 20 | 21 | for _, option := range splitOptions { 22 | switch option { 23 | case SplitCase: 24 | splitCase = true 25 | case SplitAcronym: 26 | splitAcronym = true 27 | case SplitBeforeNumber: 28 | splitBeforeNumber = true 29 | case SplitAfterNumber: 30 | splitAfterNumber = true 31 | case PreserveNumberFormatting: 32 | preserveNumberFormatting = true 33 | } 34 | } 35 | 36 | return func(prev, curr, next rune) SplitAction { 37 | // The most common case will be that it's just a letter 38 | // There are safe cases to process 39 | if isLower(curr) && !isNumber(prev) { 40 | return Noop 41 | } 42 | if isUpper(prev) && isUpper(curr) && isUpper(next) { 43 | return Noop 44 | } 45 | 46 | if preserveNumberFormatting { 47 | if (curr == '.' || curr == ',') && 48 | isNumber(prev) && isNumber(next) { 49 | return Noop 50 | } 51 | } 52 | 53 | if unicode.IsSpace(curr) { 54 | return SkipSplit 55 | } 56 | for _, d := range delimiters { 57 | if curr == d { 58 | return SkipSplit 59 | } 60 | } 61 | 62 | if splitBeforeNumber { 63 | if isNumber(curr) && !isNumber(prev) { 64 | if preserveNumberFormatting && (prev == '.' || prev == ',') { 65 | return Noop 66 | } 67 | return Split 68 | } 69 | } 70 | 71 | if splitAfterNumber { 72 | if isNumber(prev) && !isNumber(curr) { 73 | return Split 74 | } 75 | } 76 | 77 | if splitCase { 78 | if !isUpper(prev) && isUpper(curr) { 79 | return Split 80 | } 81 | } 82 | 83 | if splitAcronym { 84 | if isUpper(prev) && isUpper(curr) && isLower(next) { 85 | return Split 86 | } 87 | } 88 | 89 | return Noop 90 | } 91 | } 92 | 93 | // SplitOption are options that allow for configuring NewSplitFn 94 | type SplitOption int 95 | 96 | const ( 97 | // SplitCase - FooBar -> Foo_Bar 98 | SplitCase SplitOption = iota 99 | // SplitAcronym - FOOBar -> Foo_Bar 100 | // It won't preserve FOO's case. If you want, you can set the Caser's initialisms so FOO will be in all caps 101 | SplitAcronym 102 | // SplitBeforeNumber - port80 -> port_80 103 | SplitBeforeNumber 104 | // SplitAfterNumber - 200status -> 200_status 105 | SplitAfterNumber 106 | // PreserveNumberFormatting - a.b.2,000.3.c -> a_b_2,000.3_c 107 | PreserveNumberFormatting 108 | ) 109 | 110 | // SplitAction defines if and how to split a string 111 | type SplitAction int 112 | 113 | const ( 114 | // Noop - Continue to next character 115 | Noop SplitAction = iota 116 | // Split - Split between words 117 | // e.g. to split between wordsWithoutDelimiters 118 | Split 119 | // SkipSplit - Split the word and drop the character 120 | // e.g. to split words with delimiters 121 | SkipSplit 122 | // Skip - Remove the character completely 123 | Skip 124 | ) 125 | 126 | //nolint:gocyclo 127 | func defaultSplitFn(prev, curr, next rune) SplitAction { 128 | // The most common case will be that it's just a letter so let lowercase letters return early since we know what they should do 129 | if isLower(curr) { 130 | return Noop 131 | } 132 | // Delimiters are _, -, ., and unicode spaces 133 | // Handle . lower down as it needs to happen after number exceptions 134 | if curr == '_' || curr == '-' || isSpace(curr) { 135 | return SkipSplit 136 | } 137 | 138 | if isUpper(curr) { 139 | if isLower(prev) { 140 | // fooBar 141 | return Split 142 | } else if isUpper(prev) && isLower(next) { 143 | // FOOBar 144 | return Split 145 | } 146 | } 147 | 148 | // Do numeric exceptions last to avoid perf penalty 149 | if unicode.IsNumber(prev) { 150 | // v4.3 is not split 151 | if (curr == '.' || curr == ',') && unicode.IsNumber(next) { 152 | return Noop 153 | } 154 | if !unicode.IsNumber(curr) && curr != '.' { 155 | return Split 156 | } 157 | } 158 | // While period is a default delimiter, keep it down here to avoid 159 | // penalty for other delimiters 160 | if curr == '.' { 161 | return SkipSplit 162 | } 163 | 164 | return Noop 165 | } 166 | -------------------------------------------------------------------------------- /strcase.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | // ToSnake returns words in snake_case (lower case words with underscores). 4 | func ToSnake(s string) string { 5 | return convertWithoutInitialisms(s, '_', LowerCase) 6 | } 7 | 8 | // ToGoSnake returns words in snake_case (lower case words with underscores). 9 | // 10 | // Respects Go's common initialisms (e.g. http_response -> HTTP_response). 11 | func ToGoSnake(s string) string { 12 | return convertWithGoInitialisms(s, '_', LowerCase) 13 | } 14 | 15 | // ToSNAKE returns words in SNAKE_CASE (upper case words with underscores). 16 | // Also known as SCREAMING_SNAKE_CASE or UPPER_CASE. 17 | func ToSNAKE(s string) string { 18 | return convertWithoutInitialisms(s, '_', UpperCase) 19 | } 20 | 21 | // ToKebab returns words in kebab-case (lower case words with dashes). 22 | // Also known as dash-case. 23 | func ToKebab(s string) string { 24 | return convertWithoutInitialisms(s, '-', LowerCase) 25 | } 26 | 27 | // ToGoKebab returns words in kebab-case (lower case words with dashes). 28 | // Also known as dash-case. 29 | // 30 | // Respects Go's common initialisms (e.g. http-response -> HTTP-response). 31 | func ToGoKebab(s string) string { 32 | return convertWithGoInitialisms(s, '-', LowerCase) 33 | } 34 | 35 | // ToKEBAB returns words in KEBAB-CASE (upper case words with dashes). 36 | // Also known as SCREAMING-KEBAB-CASE or SCREAMING-DASH-CASE. 37 | func ToKEBAB(s string) string { 38 | return convertWithoutInitialisms(s, '-', UpperCase) 39 | } 40 | 41 | // ToPascal returns words in PascalCase (capitalized words concatenated together). 42 | // Also known as UpperPascalCase. 43 | func ToPascal(s string) string { 44 | return convertWithoutInitialisms(s, 0, TitleCase) 45 | } 46 | 47 | // ToGoPascal returns words in PascalCase (capitalized words concatenated together). 48 | // Also known as UpperPascalCase. 49 | // 50 | // Respects Go's common initialisms (e.g. HttpResponse -> HTTPResponse). 51 | func ToGoPascal(s string) string { 52 | return convertWithGoInitialisms(s, 0, TitleCase) 53 | } 54 | 55 | // ToCamel returns words in camelCase (capitalized words concatenated together, with first word lower case). 56 | // Also known as lowerCamelCase or mixedCase. 57 | func ToCamel(s string) string { 58 | return convertWithoutInitialisms(s, 0, CamelCase) 59 | } 60 | 61 | // ToGoCamel returns words in camelCase (capitalized words concatenated together, with first word lower case). 62 | // Also known as lowerCamelCase or mixedCase. 63 | // 64 | // Respects Go's common initialisms, but first word remains lowercased which is 65 | // important for code generator use cases (e.g. toJson -> toJSON, httpResponse 66 | // -> httpResponse). 67 | func ToGoCamel(s string) string { 68 | return convertWithGoInitialisms(s, 0, CamelCase) 69 | } 70 | 71 | // ToCase returns words in given case and delimiter. 72 | func ToCase(s string, wordCase WordCase, delimiter rune) string { 73 | return convertWithoutInitialisms(s, delimiter, wordCase) 74 | } 75 | 76 | // ToGoCase returns words in given case and delimiter. 77 | // 78 | // Respects Go's common initialisms (e.g. httpResponse -> HTTPResponse). 79 | func ToGoCase(s string, wordCase WordCase, delimiter rune) string { 80 | return convertWithGoInitialisms(s, delimiter, wordCase) 81 | } 82 | -------------------------------------------------------------------------------- /strcase_test.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | // Obviously 100% test coverage isn't everything but... 11 | func TestEdges(t *testing.T) { 12 | t.Run("Original WordCase", func(t *testing.T) { 13 | assertEqual(t, "FreeBSD", convertWithoutInitialisms("FreeBSD", 0, Original)) 14 | assertEqual(t, "FreeBSD", convertWithGoInitialisms("FreeBSD", 0, Original)) 15 | }) 16 | t.Run("Don't call convertWithInitialisms for UpperCase", func(t *testing.T) { 17 | defer func() { 18 | if r := recover(); r == nil { 19 | t.Errorf("The code did not panic") 20 | } 21 | }() 22 | convertWithGoInitialisms("foo", 0, UpperCase) 23 | }) 24 | } 25 | 26 | func TestAll(t *testing.T) { 27 | // Instead of testing, we can generate the outputs to make it easier to 28 | // add more test cases or functions 29 | generate := false 30 | 31 | type data struct { 32 | input string 33 | snake string 34 | goSnake string 35 | SNAKE string 36 | kebab string 37 | goKebab string 38 | KEBAB string 39 | pascal string 40 | goPascal string 41 | camel string 42 | goCamel string 43 | // Test ToCase function 44 | title string 45 | goTitle string 46 | } 47 | for _, test := range []data{ 48 | { 49 | input: "Hello world!", 50 | snake: "hello_world!", 51 | goSnake: "hello_world!", 52 | SNAKE: "HELLO_WORLD!", 53 | kebab: "hello-world!", 54 | goKebab: "hello-world!", 55 | KEBAB: "HELLO-WORLD!", 56 | pascal: "HelloWorld!", 57 | goPascal: "HelloWorld!", 58 | camel: "helloWorld!", 59 | goCamel: "helloWorld!", 60 | title: "Hello World!", 61 | goTitle: "Hello World!", 62 | }, 63 | { 64 | input: "", 65 | snake: "", 66 | goSnake: "", 67 | SNAKE: "", 68 | kebab: "", 69 | goKebab: "", 70 | KEBAB: "", 71 | pascal: "", 72 | goPascal: "", 73 | camel: "", 74 | goCamel: "", 75 | title: "", 76 | goTitle: "", 77 | }, 78 | { 79 | input: ".", 80 | snake: "", 81 | goSnake: "", 82 | SNAKE: "", 83 | kebab: "", 84 | goKebab: "", 85 | KEBAB: "", 86 | pascal: "", 87 | goPascal: "", 88 | camel: "", 89 | goCamel: "", 90 | title: "", 91 | goTitle: "", 92 | }, 93 | { 94 | input: "A", 95 | snake: "a", 96 | goSnake: "a", 97 | SNAKE: "A", 98 | kebab: "a", 99 | goKebab: "a", 100 | KEBAB: "A", 101 | pascal: "A", 102 | goPascal: "A", 103 | camel: "a", 104 | goCamel: "a", 105 | title: "A", 106 | goTitle: "A", 107 | }, 108 | { 109 | input: "a", 110 | snake: "a", 111 | goSnake: "a", 112 | SNAKE: "A", 113 | kebab: "a", 114 | goKebab: "a", 115 | KEBAB: "A", 116 | pascal: "A", 117 | goPascal: "A", 118 | camel: "a", 119 | goCamel: "a", 120 | title: "A", 121 | goTitle: "A", 122 | }, 123 | { 124 | input: "foo", 125 | snake: "foo", 126 | goSnake: "foo", 127 | SNAKE: "FOO", 128 | kebab: "foo", 129 | goKebab: "foo", 130 | KEBAB: "FOO", 131 | pascal: "Foo", 132 | goPascal: "Foo", 133 | camel: "foo", 134 | goCamel: "foo", 135 | title: "Foo", 136 | goTitle: "Foo", 137 | }, 138 | { 139 | input: "snake_case", 140 | snake: "snake_case", 141 | goSnake: "snake_case", 142 | SNAKE: "SNAKE_CASE", 143 | kebab: "snake-case", 144 | goKebab: "snake-case", 145 | KEBAB: "SNAKE-CASE", 146 | pascal: "SnakeCase", 147 | goPascal: "SnakeCase", 148 | camel: "snakeCase", 149 | goCamel: "snakeCase", 150 | title: "Snake Case", 151 | goTitle: "Snake Case", 152 | }, 153 | { 154 | input: "SNAKE_CASE", 155 | snake: "snake_case", 156 | goSnake: "snake_case", 157 | SNAKE: "SNAKE_CASE", 158 | kebab: "snake-case", 159 | goKebab: "snake-case", 160 | KEBAB: "SNAKE-CASE", 161 | pascal: "SnakeCase", 162 | goPascal: "SnakeCase", 163 | camel: "snakeCase", 164 | goCamel: "snakeCase", 165 | title: "Snake Case", 166 | goTitle: "Snake Case", 167 | }, 168 | { 169 | input: "kebab-case", 170 | snake: "kebab_case", 171 | goSnake: "kebab_case", 172 | SNAKE: "KEBAB_CASE", 173 | kebab: "kebab-case", 174 | goKebab: "kebab-case", 175 | KEBAB: "KEBAB-CASE", 176 | pascal: "KebabCase", 177 | goPascal: "KebabCase", 178 | camel: "kebabCase", 179 | goCamel: "kebabCase", 180 | title: "Kebab Case", 181 | goTitle: "Kebab Case", 182 | }, 183 | { 184 | input: "PascalCase", 185 | snake: "pascal_case", 186 | goSnake: "pascal_case", 187 | SNAKE: "PASCAL_CASE", 188 | kebab: "pascal-case", 189 | goKebab: "pascal-case", 190 | KEBAB: "PASCAL-CASE", 191 | pascal: "PascalCase", 192 | goPascal: "PascalCase", 193 | camel: "pascalCase", 194 | goCamel: "pascalCase", 195 | title: "Pascal Case", 196 | goTitle: "Pascal Case", 197 | }, 198 | { 199 | input: "camelCase", 200 | snake: "camel_case", 201 | goSnake: "camel_case", 202 | SNAKE: "CAMEL_CASE", 203 | kebab: "camel-case", 204 | goKebab: "camel-case", 205 | KEBAB: "CAMEL-CASE", 206 | pascal: "CamelCase", 207 | goPascal: "CamelCase", 208 | camel: "camelCase", 209 | goCamel: "camelCase", 210 | title: "Camel Case", 211 | goTitle: "Camel Case", 212 | }, 213 | { 214 | input: "Title Case", 215 | snake: "title_case", 216 | goSnake: "title_case", 217 | SNAKE: "TITLE_CASE", 218 | kebab: "title-case", 219 | goKebab: "title-case", 220 | KEBAB: "TITLE-CASE", 221 | pascal: "TitleCase", 222 | goPascal: "TitleCase", 223 | camel: "titleCase", 224 | goCamel: "titleCase", 225 | title: "Title Case", 226 | goTitle: "Title Case", 227 | }, 228 | { 229 | input: "point.case", 230 | snake: "point_case", 231 | goSnake: "point_case", 232 | SNAKE: "POINT_CASE", 233 | kebab: "point-case", 234 | goKebab: "point-case", 235 | KEBAB: "POINT-CASE", 236 | pascal: "PointCase", 237 | goPascal: "PointCase", 238 | camel: "pointCase", 239 | goCamel: "pointCase", 240 | title: "Point Case", 241 | goTitle: "Point Case", 242 | }, 243 | { 244 | input: "snake_case_with_more_words", 245 | snake: "snake_case_with_more_words", 246 | goSnake: "snake_case_with_more_words", 247 | SNAKE: "SNAKE_CASE_WITH_MORE_WORDS", 248 | kebab: "snake-case-with-more-words", 249 | goKebab: "snake-case-with-more-words", 250 | KEBAB: "SNAKE-CASE-WITH-MORE-WORDS", 251 | pascal: "SnakeCaseWithMoreWords", 252 | goPascal: "SnakeCaseWithMoreWords", 253 | camel: "snakeCaseWithMoreWords", 254 | goCamel: "snakeCaseWithMoreWords", 255 | title: "Snake Case With More Words", 256 | goTitle: "Snake Case With More Words", 257 | }, 258 | { 259 | input: "SNAKE_CASE_WITH_MORE_WORDS", 260 | snake: "snake_case_with_more_words", 261 | goSnake: "snake_case_with_more_words", 262 | SNAKE: "SNAKE_CASE_WITH_MORE_WORDS", 263 | kebab: "snake-case-with-more-words", 264 | goKebab: "snake-case-with-more-words", 265 | KEBAB: "SNAKE-CASE-WITH-MORE-WORDS", 266 | pascal: "SnakeCaseWithMoreWords", 267 | goPascal: "SnakeCaseWithMoreWords", 268 | camel: "snakeCaseWithMoreWords", 269 | goCamel: "snakeCaseWithMoreWords", 270 | title: "Snake Case With More Words", 271 | goTitle: "Snake Case With More Words", 272 | }, 273 | { 274 | input: "kebab-case-with-more-words", 275 | snake: "kebab_case_with_more_words", 276 | goSnake: "kebab_case_with_more_words", 277 | SNAKE: "KEBAB_CASE_WITH_MORE_WORDS", 278 | kebab: "kebab-case-with-more-words", 279 | goKebab: "kebab-case-with-more-words", 280 | KEBAB: "KEBAB-CASE-WITH-MORE-WORDS", 281 | pascal: "KebabCaseWithMoreWords", 282 | goPascal: "KebabCaseWithMoreWords", 283 | camel: "kebabCaseWithMoreWords", 284 | goCamel: "kebabCaseWithMoreWords", 285 | title: "Kebab Case With More Words", 286 | goTitle: "Kebab Case With More Words", 287 | }, 288 | { 289 | input: "PascalCaseWithMoreWords", 290 | snake: "pascal_case_with_more_words", 291 | goSnake: "pascal_case_with_more_words", 292 | SNAKE: "PASCAL_CASE_WITH_MORE_WORDS", 293 | kebab: "pascal-case-with-more-words", 294 | goKebab: "pascal-case-with-more-words", 295 | KEBAB: "PASCAL-CASE-WITH-MORE-WORDS", 296 | pascal: "PascalCaseWithMoreWords", 297 | goPascal: "PascalCaseWithMoreWords", 298 | camel: "pascalCaseWithMoreWords", 299 | goCamel: "pascalCaseWithMoreWords", 300 | title: "Pascal Case With More Words", 301 | goTitle: "Pascal Case With More Words", 302 | }, 303 | { 304 | input: "camelCaseWithMoreWords", 305 | snake: "camel_case_with_more_words", 306 | goSnake: "camel_case_with_more_words", 307 | SNAKE: "CAMEL_CASE_WITH_MORE_WORDS", 308 | kebab: "camel-case-with-more-words", 309 | goKebab: "camel-case-with-more-words", 310 | KEBAB: "CAMEL-CASE-WITH-MORE-WORDS", 311 | pascal: "CamelCaseWithMoreWords", 312 | goPascal: "CamelCaseWithMoreWords", 313 | camel: "camelCaseWithMoreWords", 314 | goCamel: "camelCaseWithMoreWords", 315 | title: "Camel Case With More Words", 316 | goTitle: "Camel Case With More Words", 317 | }, 318 | { 319 | input: "Title Case With More Words", 320 | snake: "title_case_with_more_words", 321 | goSnake: "title_case_with_more_words", 322 | SNAKE: "TITLE_CASE_WITH_MORE_WORDS", 323 | kebab: "title-case-with-more-words", 324 | goKebab: "title-case-with-more-words", 325 | KEBAB: "TITLE-CASE-WITH-MORE-WORDS", 326 | pascal: "TitleCaseWithMoreWords", 327 | goPascal: "TitleCaseWithMoreWords", 328 | camel: "titleCaseWithMoreWords", 329 | goCamel: "titleCaseWithMoreWords", 330 | title: "Title Case With More Words", 331 | goTitle: "Title Case With More Words", 332 | }, 333 | { 334 | input: "point.case.with.more.words", 335 | snake: "point_case_with_more_words", 336 | goSnake: "point_case_with_more_words", 337 | SNAKE: "POINT_CASE_WITH_MORE_WORDS", 338 | kebab: "point-case-with-more-words", 339 | goKebab: "point-case-with-more-words", 340 | KEBAB: "POINT-CASE-WITH-MORE-WORDS", 341 | pascal: "PointCaseWithMoreWords", 342 | goPascal: "PointCaseWithMoreWords", 343 | camel: "pointCaseWithMoreWords", 344 | goCamel: "pointCaseWithMoreWords", 345 | title: "Point Case With More Words", 346 | goTitle: "Point Case With More Words", 347 | }, 348 | { 349 | input: "snake_case__with___multiple____delimiters", 350 | snake: "snake_case_with_multiple_delimiters", 351 | goSnake: "snake_case_with_multiple_delimiters", 352 | SNAKE: "SNAKE_CASE_WITH_MULTIPLE_DELIMITERS", 353 | kebab: "snake-case-with-multiple-delimiters", 354 | goKebab: "snake-case-with-multiple-delimiters", 355 | KEBAB: "SNAKE-CASE-WITH-MULTIPLE-DELIMITERS", 356 | pascal: "SnakeCaseWithMultipleDelimiters", 357 | goPascal: "SnakeCaseWithMultipleDelimiters", 358 | camel: "snakeCaseWithMultipleDelimiters", 359 | goCamel: "snakeCaseWithMultipleDelimiters", 360 | title: "Snake Case With Multiple Delimiters", 361 | goTitle: "Snake Case With Multiple Delimiters", 362 | }, 363 | { 364 | input: "SNAKE_CASE__WITH___multiple____DELIMITERS", 365 | snake: "snake_case_with_multiple_delimiters", 366 | goSnake: "snake_case_with_multiple_delimiters", 367 | SNAKE: "SNAKE_CASE_WITH_MULTIPLE_DELIMITERS", 368 | kebab: "snake-case-with-multiple-delimiters", 369 | goKebab: "snake-case-with-multiple-delimiters", 370 | KEBAB: "SNAKE-CASE-WITH-MULTIPLE-DELIMITERS", 371 | pascal: "SnakeCaseWithMultipleDelimiters", 372 | goPascal: "SnakeCaseWithMultipleDelimiters", 373 | camel: "snakeCaseWithMultipleDelimiters", 374 | goCamel: "snakeCaseWithMultipleDelimiters", 375 | title: "Snake Case With Multiple Delimiters", 376 | goTitle: "Snake Case With Multiple Delimiters", 377 | }, 378 | { 379 | input: "kebab-case--with---multiple----delimiters", 380 | snake: "kebab_case_with_multiple_delimiters", 381 | goSnake: "kebab_case_with_multiple_delimiters", 382 | SNAKE: "KEBAB_CASE_WITH_MULTIPLE_DELIMITERS", 383 | kebab: "kebab-case-with-multiple-delimiters", 384 | goKebab: "kebab-case-with-multiple-delimiters", 385 | KEBAB: "KEBAB-CASE-WITH-MULTIPLE-DELIMITERS", 386 | pascal: "KebabCaseWithMultipleDelimiters", 387 | goPascal: "KebabCaseWithMultipleDelimiters", 388 | camel: "kebabCaseWithMultipleDelimiters", 389 | goCamel: "kebabCaseWithMultipleDelimiters", 390 | title: "Kebab Case With Multiple Delimiters", 391 | goTitle: "Kebab Case With Multiple Delimiters", 392 | }, 393 | { 394 | input: "Title Case With Multiple Delimiters", 395 | snake: "title_case_with_multiple_delimiters", 396 | goSnake: "title_case_with_multiple_delimiters", 397 | SNAKE: "TITLE_CASE_WITH_MULTIPLE_DELIMITERS", 398 | kebab: "title-case-with-multiple-delimiters", 399 | goKebab: "title-case-with-multiple-delimiters", 400 | KEBAB: "TITLE-CASE-WITH-MULTIPLE-DELIMITERS", 401 | pascal: "TitleCaseWithMultipleDelimiters", 402 | goPascal: "TitleCaseWithMultipleDelimiters", 403 | camel: "titleCaseWithMultipleDelimiters", 404 | goCamel: "titleCaseWithMultipleDelimiters", 405 | title: "Title Case With Multiple Delimiters", 406 | goTitle: "Title Case With Multiple Delimiters", 407 | }, 408 | { 409 | input: "point.case..with...multiple....delimiters", 410 | snake: "point_case_with_multiple_delimiters", 411 | goSnake: "point_case_with_multiple_delimiters", 412 | SNAKE: "POINT_CASE_WITH_MULTIPLE_DELIMITERS", 413 | kebab: "point-case-with-multiple-delimiters", 414 | goKebab: "point-case-with-multiple-delimiters", 415 | KEBAB: "POINT-CASE-WITH-MULTIPLE-DELIMITERS", 416 | pascal: "PointCaseWithMultipleDelimiters", 417 | goPascal: "PointCaseWithMultipleDelimiters", 418 | camel: "pointCaseWithMultipleDelimiters", 419 | goCamel: "pointCaseWithMultipleDelimiters", 420 | title: "Point Case With Multiple Delimiters", 421 | goTitle: "Point Case With Multiple Delimiters", 422 | }, 423 | { 424 | input: " leading space", 425 | snake: "leading_space", 426 | goSnake: "leading_space", 427 | SNAKE: "LEADING_SPACE", 428 | kebab: "leading-space", 429 | goKebab: "leading-space", 430 | KEBAB: "LEADING-SPACE", 431 | pascal: "LeadingSpace", 432 | goPascal: "LeadingSpace", 433 | camel: "leadingSpace", 434 | goCamel: "leadingSpace", 435 | title: "Leading Space", 436 | goTitle: "Leading Space", 437 | }, 438 | { 439 | input: " leading spaces", 440 | snake: "leading_spaces", 441 | goSnake: "leading_spaces", 442 | SNAKE: "LEADING_SPACES", 443 | kebab: "leading-spaces", 444 | goKebab: "leading-spaces", 445 | KEBAB: "LEADING-SPACES", 446 | pascal: "LeadingSpaces", 447 | goPascal: "LeadingSpaces", 448 | camel: "leadingSpaces", 449 | goCamel: "leadingSpaces", 450 | title: "Leading Spaces", 451 | goTitle: "Leading Spaces", 452 | }, 453 | { 454 | input: "\t\t\r\n leading whitespaces", 455 | snake: "leading_whitespaces", 456 | goSnake: "leading_whitespaces", 457 | SNAKE: "LEADING_WHITESPACES", 458 | kebab: "leading-whitespaces", 459 | goKebab: "leading-whitespaces", 460 | KEBAB: "LEADING-WHITESPACES", 461 | pascal: "LeadingWhitespaces", 462 | goPascal: "LeadingWhitespaces", 463 | camel: "leadingWhitespaces", 464 | goCamel: "leadingWhitespaces", 465 | title: "Leading Whitespaces", 466 | goTitle: "Leading Whitespaces", 467 | }, 468 | { 469 | input: "trailing space ", 470 | snake: "trailing_space", 471 | goSnake: "trailing_space", 472 | SNAKE: "TRAILING_SPACE", 473 | kebab: "trailing-space", 474 | goKebab: "trailing-space", 475 | KEBAB: "TRAILING-SPACE", 476 | pascal: "TrailingSpace", 477 | goPascal: "TrailingSpace", 478 | camel: "trailingSpace", 479 | goCamel: "trailingSpace", 480 | title: "Trailing Space", 481 | goTitle: "Trailing Space", 482 | }, 483 | { 484 | input: "trailing spaces ", 485 | snake: "trailing_spaces", 486 | goSnake: "trailing_spaces", 487 | SNAKE: "TRAILING_SPACES", 488 | kebab: "trailing-spaces", 489 | goKebab: "trailing-spaces", 490 | KEBAB: "TRAILING-SPACES", 491 | pascal: "TrailingSpaces", 492 | goPascal: "TrailingSpaces", 493 | camel: "trailingSpaces", 494 | goCamel: "trailingSpaces", 495 | title: "Trailing Spaces", 496 | goTitle: "Trailing Spaces", 497 | }, 498 | { 499 | input: "trailing whitespaces\t\t\r\n", 500 | snake: "trailing_whitespaces", 501 | goSnake: "trailing_whitespaces", 502 | SNAKE: "TRAILING_WHITESPACES", 503 | kebab: "trailing-whitespaces", 504 | goKebab: "trailing-whitespaces", 505 | KEBAB: "TRAILING-WHITESPACES", 506 | pascal: "TrailingWhitespaces", 507 | goPascal: "TrailingWhitespaces", 508 | camel: "trailingWhitespaces", 509 | goCamel: "trailingWhitespaces", 510 | title: "Trailing Whitespaces", 511 | goTitle: "Trailing Whitespaces", 512 | }, 513 | { 514 | input: " on both sides ", 515 | snake: "on_both_sides", 516 | goSnake: "on_both_sides", 517 | SNAKE: "ON_BOTH_SIDES", 518 | kebab: "on-both-sides", 519 | goKebab: "on-both-sides", 520 | KEBAB: "ON-BOTH-SIDES", 521 | pascal: "OnBothSides", 522 | goPascal: "OnBothSides", 523 | camel: "onBothSides", 524 | goCamel: "onBothSides", 525 | title: "On Both Sides", 526 | goTitle: "On Both Sides", 527 | }, 528 | { 529 | input: " many on both sides ", 530 | snake: "many_on_both_sides", 531 | goSnake: "many_on_both_sides", 532 | SNAKE: "MANY_ON_BOTH_SIDES", 533 | kebab: "many-on-both-sides", 534 | goKebab: "many-on-both-sides", 535 | KEBAB: "MANY-ON-BOTH-SIDES", 536 | pascal: "ManyOnBothSides", 537 | goPascal: "ManyOnBothSides", 538 | camel: "manyOnBothSides", 539 | goCamel: "manyOnBothSides", 540 | title: "Many On Both Sides", 541 | goTitle: "Many On Both Sides", 542 | }, 543 | { 544 | input: "\rwhitespaces on both sides\t\t\r\n", 545 | snake: "whitespaces_on_both_sides", 546 | goSnake: "whitespaces_on_both_sides", 547 | SNAKE: "WHITESPACES_ON_BOTH_SIDES", 548 | kebab: "whitespaces-on-both-sides", 549 | goKebab: "whitespaces-on-both-sides", 550 | KEBAB: "WHITESPACES-ON-BOTH-SIDES", 551 | pascal: "WhitespacesOnBothSides", 552 | goPascal: "WhitespacesOnBothSides", 553 | camel: "whitespacesOnBothSides", 554 | goCamel: "whitespacesOnBothSides", 555 | title: "Whitespaces On Both Sides", 556 | goTitle: "Whitespaces On Both Sides", 557 | }, 558 | { 559 | input: " extraSpaces in_This TestCase Of MIXED_CASES\t", 560 | snake: "extra_spaces_in_this_test_case_of_mixed_cases", 561 | goSnake: "extra_spaces_in_this_test_case_of_mixed_cases", 562 | SNAKE: "EXTRA_SPACES_IN_THIS_TEST_CASE_OF_MIXED_CASES", 563 | kebab: "extra-spaces-in-this-test-case-of-mixed-cases", 564 | goKebab: "extra-spaces-in-this-test-case-of-mixed-cases", 565 | KEBAB: "EXTRA-SPACES-IN-THIS-TEST-CASE-OF-MIXED-CASES", 566 | pascal: "ExtraSpacesInThisTestCaseOfMixedCases", 567 | goPascal: "ExtraSpacesInThisTestCaseOfMixedCases", 568 | camel: "extraSpacesInThisTestCaseOfMixedCases", 569 | goCamel: "extraSpacesInThisTestCaseOfMixedCases", 570 | title: "Extra Spaces In This Test Case Of Mixed Cases", 571 | goTitle: "Extra Spaces In This Test Case Of Mixed Cases", 572 | }, 573 | { 574 | input: "CASEBreak", 575 | snake: "case_break", 576 | goSnake: "case_break", 577 | SNAKE: "CASE_BREAK", 578 | kebab: "case-break", 579 | goKebab: "case-break", 580 | KEBAB: "CASE-BREAK", 581 | pascal: "CaseBreak", 582 | goPascal: "CaseBreak", 583 | camel: "caseBreak", 584 | goCamel: "caseBreak", 585 | title: "Case Break", 586 | goTitle: "Case Break", 587 | }, 588 | { 589 | input: "ID", 590 | snake: "id", 591 | goSnake: "ID", 592 | SNAKE: "ID", 593 | kebab: "id", 594 | goKebab: "ID", 595 | KEBAB: "ID", 596 | pascal: "Id", 597 | goPascal: "ID", 598 | camel: "id", 599 | goCamel: "id", 600 | title: "Id", 601 | goTitle: "ID", 602 | }, 603 | { 604 | input: "userID", 605 | snake: "user_id", 606 | goSnake: "user_ID", 607 | SNAKE: "USER_ID", 608 | kebab: "user-id", 609 | goKebab: "user-ID", 610 | KEBAB: "USER-ID", 611 | pascal: "UserId", 612 | goPascal: "UserID", 613 | camel: "userId", 614 | goCamel: "userID", 615 | title: "User Id", 616 | goTitle: "User ID", 617 | }, 618 | { 619 | input: "JSON_blob", 620 | snake: "json_blob", 621 | goSnake: "JSON_blob", 622 | SNAKE: "JSON_BLOB", 623 | kebab: "json-blob", 624 | goKebab: "JSON-blob", 625 | KEBAB: "JSON-BLOB", 626 | pascal: "JsonBlob", 627 | goPascal: "JSONBlob", 628 | camel: "jsonBlob", 629 | goCamel: "jsonBlob", 630 | title: "Json Blob", 631 | goTitle: "JSON Blob", 632 | }, 633 | { 634 | input: "HTTPStatusCode", 635 | snake: "http_status_code", 636 | goSnake: "HTTP_status_code", 637 | SNAKE: "HTTP_STATUS_CODE", 638 | kebab: "http-status-code", 639 | goKebab: "HTTP-status-code", 640 | KEBAB: "HTTP-STATUS-CODE", 641 | pascal: "HttpStatusCode", 642 | goPascal: "HTTPStatusCode", 643 | camel: "httpStatusCode", 644 | goCamel: "httpStatusCode", 645 | title: "Http Status Code", 646 | goTitle: "HTTP Status Code", 647 | }, 648 | { 649 | input: "FreeBSD and SSLError are not golang initialisms", 650 | snake: "free_bsd_and_ssl_error_are_not_golang_initialisms", 651 | goSnake: "free_bsd_and_ssl_error_are_not_golang_initialisms", 652 | SNAKE: "FREE_BSD_AND_SSL_ERROR_ARE_NOT_GOLANG_INITIALISMS", 653 | kebab: "free-bsd-and-ssl-error-are-not-golang-initialisms", 654 | goKebab: "free-bsd-and-ssl-error-are-not-golang-initialisms", 655 | KEBAB: "FREE-BSD-AND-SSL-ERROR-ARE-NOT-GOLANG-INITIALISMS", 656 | pascal: "FreeBsdAndSslErrorAreNotGolangInitialisms", 657 | goPascal: "FreeBsdAndSslErrorAreNotGolangInitialisms", 658 | camel: "freeBsdAndSslErrorAreNotGolangInitialisms", 659 | goCamel: "freeBsdAndSslErrorAreNotGolangInitialisms", 660 | title: "Free Bsd And Ssl Error Are Not Golang Initialisms", 661 | goTitle: "Free Bsd And Ssl Error Are Not Golang Initialisms", 662 | }, 663 | { 664 | input: "David's Computer", 665 | snake: "david's_computer", 666 | goSnake: "david's_computer", 667 | SNAKE: "DAVID'S_COMPUTER", 668 | kebab: "david's-computer", 669 | goKebab: "david's-computer", 670 | KEBAB: "DAVID'S-COMPUTER", 671 | pascal: "David'sComputer", 672 | goPascal: "David'sComputer", 673 | camel: "david'sComputer", 674 | goCamel: "david'sComputer", 675 | title: "David's Computer", 676 | goTitle: "David's Computer", 677 | }, 678 | { 679 | input: "Ünicode support for Æthelred and Øyvind", 680 | snake: "ünicode_support_for_æthelred_and_øyvind", 681 | goSnake: "ünicode_support_for_æthelred_and_øyvind", 682 | SNAKE: "ÜNICODE_SUPPORT_FOR_ÆTHELRED_AND_ØYVIND", 683 | kebab: "ünicode-support-for-æthelred-and-øyvind", 684 | goKebab: "ünicode-support-for-æthelred-and-øyvind", 685 | KEBAB: "ÜNICODE-SUPPORT-FOR-ÆTHELRED-AND-ØYVIND", 686 | pascal: "ÜnicodeSupportForÆthelredAndØyvind", 687 | goPascal: "ÜnicodeSupportForÆthelredAndØyvind", 688 | camel: "ünicodeSupportForÆthelredAndØyvind", 689 | goCamel: "ünicodeSupportForÆthelredAndØyvind", 690 | title: "Ünicode Support For Æthelred And Øyvind", 691 | goTitle: "Ünicode Support For Æthelred And Øyvind", 692 | }, 693 | { 694 | input: "http200", 695 | snake: "http200", 696 | goSnake: "http200", 697 | SNAKE: "HTTP200", 698 | kebab: "http200", 699 | goKebab: "http200", 700 | KEBAB: "HTTP200", 701 | pascal: "Http200", 702 | goPascal: "Http200", 703 | camel: "http200", 704 | goCamel: "http200", 705 | title: "Http200", 706 | goTitle: "Http200", 707 | }, 708 | { 709 | input: "NumberSplittingVersion1.0r3", 710 | snake: "number_splitting_version1.0r3", 711 | goSnake: "number_splitting_version1.0r3", 712 | SNAKE: "NUMBER_SPLITTING_VERSION1.0R3", 713 | kebab: "number-splitting-version1.0r3", 714 | goKebab: "number-splitting-version1.0r3", 715 | KEBAB: "NUMBER-SPLITTING-VERSION1.0R3", 716 | pascal: "NumberSplittingVersion1.0r3", 717 | goPascal: "NumberSplittingVersion1.0r3", 718 | camel: "numberSplittingVersion1.0r3", 719 | goCamel: "numberSplittingVersion1.0r3", 720 | title: "Number Splitting Version1.0r3", 721 | goTitle: "Number Splitting Version1.0r3", 722 | }, 723 | { 724 | input: "When you have a comma, odd results", 725 | snake: "when_you_have_a_comma,_odd_results", 726 | goSnake: "when_you_have_a_comma,_odd_results", 727 | SNAKE: "WHEN_YOU_HAVE_A_COMMA,_ODD_RESULTS", 728 | kebab: "when-you-have-a-comma,-odd-results", 729 | goKebab: "when-you-have-a-comma,-odd-results", 730 | KEBAB: "WHEN-YOU-HAVE-A-COMMA,-ODD-RESULTS", 731 | pascal: "WhenYouHaveAComma,OddResults", 732 | goPascal: "WhenYouHaveAComma,OddResults", 733 | camel: "whenYouHaveAComma,OddResults", 734 | goCamel: "whenYouHaveAComma,OddResults", 735 | title: "When You Have A Comma, Odd Results", 736 | goTitle: "When You Have A Comma, Odd Results", 737 | }, 738 | { 739 | input: "Ordinal numbers work: 1st 2nd and 3rd place", 740 | snake: "ordinal_numbers_work:_1st_2nd_and_3rd_place", 741 | goSnake: "ordinal_numbers_work:_1st_2nd_and_3rd_place", 742 | SNAKE: "ORDINAL_NUMBERS_WORK:_1ST_2ND_AND_3RD_PLACE", 743 | kebab: "ordinal-numbers-work:-1st-2nd-and-3rd-place", 744 | goKebab: "ordinal-numbers-work:-1st-2nd-and-3rd-place", 745 | KEBAB: "ORDINAL-NUMBERS-WORK:-1ST-2ND-AND-3RD-PLACE", 746 | pascal: "OrdinalNumbersWork:1st2ndAnd3rdPlace", 747 | goPascal: "OrdinalNumbersWork:1st2ndAnd3rdPlace", 748 | camel: "ordinalNumbersWork:1st2ndAnd3rdPlace", 749 | goCamel: "ordinalNumbersWork:1st2ndAnd3rdPlace", 750 | title: "Ordinal Numbers Work: 1st 2nd And 3rd Place", 751 | goTitle: "Ordinal Numbers Work: 1st 2nd And 3rd Place", 752 | }, 753 | { 754 | input: "BadUTF8\xe2\xe2\xa1", 755 | snake: "bad_utf8_���", 756 | goSnake: "bad_UTF8_���", 757 | SNAKE: "BAD_UTF8_���", 758 | kebab: "bad-utf8-���", 759 | goKebab: "bad-UTF8-���", 760 | KEBAB: "BAD-UTF8-���", 761 | pascal: "BadUtf8���", 762 | goPascal: "BadUTF8���", 763 | camel: "badUtf8���", 764 | goCamel: "badUTF8���", 765 | title: "Bad Utf8 ���", 766 | goTitle: "Bad UTF8 ���", 767 | }, 768 | // Need to consider if this is worth a breaking change - currently 769 | // because the split is 'ID3', this does not match the initialism of 770 | // ID and thus renders as 'id3' 771 | // { 772 | // input: "ID3", 773 | // snake: "id3", 774 | // goSnake: "ID3", // Currently id3 775 | // SNAKE: "ID3", 776 | // kebab: "id3", 777 | // goKebab: "ID3", // Currently id3 778 | // KEBAB: "ID3", 779 | // pascal: "Id3", 780 | // goPascal: "ID3", // Currently Id3 781 | // camel: "id3", 782 | // goCamel: "id3", // Currently id3 783 | // title: "Id3", 784 | // goTitle: "ID3", // Currently Id3 785 | // }, 786 | { 787 | input: "IDENT3", 788 | snake: "ident3", 789 | goSnake: "ident3", 790 | SNAKE: "IDENT3", 791 | kebab: "ident3", 792 | goKebab: "ident3", 793 | KEBAB: "IDENT3", 794 | pascal: "Ident3", 795 | goPascal: "Ident3", 796 | camel: "ident3", 797 | goCamel: "ident3", 798 | title: "Ident3", 799 | goTitle: "Ident3", 800 | }, 801 | { 802 | input: "LogRouterS3BucketName", 803 | snake: "log_router_s3_bucket_name", 804 | goSnake: "log_router_s3_bucket_name", 805 | SNAKE: "LOG_ROUTER_S3_BUCKET_NAME", 806 | kebab: "log-router-s3-bucket-name", 807 | goKebab: "log-router-s3-bucket-name", 808 | KEBAB: "LOG-ROUTER-S3-BUCKET-NAME", 809 | pascal: "LogRouterS3BucketName", 810 | goPascal: "LogRouterS3BucketName", 811 | camel: "logRouterS3BucketName", 812 | goCamel: "logRouterS3BucketName", 813 | title: "Log Router S3 Bucket Name", 814 | goTitle: "Log Router S3 Bucket Name", 815 | }, 816 | { 817 | input: "PINEAPPLE", 818 | snake: "pineapple", 819 | goSnake: "pineapple", 820 | SNAKE: "PINEAPPLE", 821 | kebab: "pineapple", 822 | goKebab: "pineapple", 823 | KEBAB: "PINEAPPLE", 824 | pascal: "Pineapple", 825 | goPascal: "Pineapple", 826 | camel: "pineapple", 827 | goCamel: "pineapple", 828 | title: "Pineapple", 829 | goTitle: "Pineapple", 830 | }, 831 | { 832 | input: "Int8Value", 833 | snake: "int8_value", 834 | goSnake: "int8_value", 835 | SNAKE: "INT8_VALUE", 836 | kebab: "int8-value", 837 | goKebab: "int8-value", 838 | KEBAB: "INT8-VALUE", 839 | pascal: "Int8Value", 840 | goPascal: "Int8Value", 841 | camel: "int8Value", 842 | goCamel: "int8Value", 843 | title: "Int8 Value", 844 | goTitle: "Int8 Value", 845 | }, 846 | { 847 | input: "first.last", 848 | snake: "first_last", 849 | goSnake: "first_last", 850 | SNAKE: "FIRST_LAST", 851 | kebab: "first-last", 852 | goKebab: "first-last", 853 | KEBAB: "FIRST-LAST", 854 | pascal: "FirstLast", 855 | goPascal: "FirstLast", 856 | camel: "firstLast", 857 | goCamel: "firstLast", 858 | title: "First Last", 859 | goTitle: "First Last", 860 | }, 861 | } { 862 | t.Run(test.input, func(t *testing.T) { 863 | output := data{ 864 | input: test.input, 865 | snake: ToSnake(test.input), 866 | goSnake: ToGoSnake(test.input), 867 | SNAKE: ToSNAKE(test.input), 868 | kebab: ToKebab(test.input), 869 | goKebab: ToGoKebab(test.input), 870 | KEBAB: ToKEBAB(test.input), 871 | pascal: ToPascal(test.input), 872 | goPascal: ToGoPascal(test.input), 873 | camel: ToCamel(test.input), 874 | goCamel: ToGoCamel(test.input), 875 | title: ToCase(test.input, TitleCase, ' '), 876 | goTitle: ToGoCase(test.input, TitleCase, ' '), 877 | } 878 | if generate || test != output { 879 | line := fmt.Sprintf("%#v", output) 880 | line = strings.TrimPrefix(line, "strcase.data") 881 | line = strings.Replace(line, "\", ", "\",\n", -1) 882 | line = strings.Replace(line, "{", "{\n", -1) 883 | line = strings.Replace(line, "}", "\n},", -1) 884 | line = regexp.MustCompile("\"\n").ReplaceAllString(line, "\",\n") 885 | fmt.Println(line) 886 | } 887 | assertTrue(t, test == output) 888 | }) 889 | } 890 | } 891 | -------------------------------------------------------------------------------- /unicode.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | import "unicode" 4 | 5 | // Unicode functions, optimized for the common case of ascii 6 | // No performance lost by wrapping since these functions get inlined by the compiler 7 | 8 | func isUpper(r rune) bool { 9 | return unicode.IsUpper(r) 10 | } 11 | 12 | func isLower(r rune) bool { 13 | return unicode.IsLower(r) 14 | } 15 | 16 | func isNumber(r rune) bool { 17 | if r >= '0' && r <= '9' { 18 | return true 19 | } 20 | return unicode.IsNumber(r) 21 | } 22 | 23 | func isSpace(r rune) bool { 24 | if r == ' ' || r == '\t' || r == '\n' || r == '\r' { 25 | return true 26 | } else if r < 128 { 27 | return false 28 | } 29 | return unicode.IsSpace(r) 30 | } 31 | 32 | func toUpper(r rune) rune { 33 | if r >= 'a' && r <= 'z' { 34 | return r - 32 35 | } else if r < 128 { 36 | return r 37 | } 38 | return unicode.ToUpper(r) 39 | } 40 | 41 | func toLower(r rune) rune { 42 | if r >= 'A' && r <= 'Z' { 43 | return r + 32 44 | } else if r < 128 { 45 | return r 46 | } 47 | return unicode.ToLower(r) 48 | } 49 | -------------------------------------------------------------------------------- /unicode_test.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIsUnicodeType(t *testing.T) { 8 | lowers := []rune{ 9 | 'c', 10 | 'ҥ', 11 | 'ȃ', 12 | 'ñ', 13 | 'γ', 14 | } 15 | uppers := []rune{ 16 | 'C', 17 | 'Ҥ', 18 | 'Ȃ', 19 | 'Ñ', 20 | 'Γ', 21 | } 22 | numbers := []rune{ 23 | '6', 24 | '³', 25 | '0', 26 | } 27 | spaces := []rune{ 28 | ' ', 29 | '\t', 30 | '\n', 31 | '\r', 32 | 8287, // medium mathematical space 33 | } 34 | others := []rune{ 35 | 0, 36 | '.', 37 | '_', 38 | 39 | 8203, // zero width space doesn't have unicode white space property 40 | } 41 | 42 | t.Run("uppercase", func(t *testing.T) { 43 | for _, r := range uppers { 44 | t.Run(string(r), func(t *testing.T) { 45 | assertTrue(t, isUpper(r)) 46 | assertTrue(t, !isLower(r)) 47 | assertTrue(t, !isNumber(r)) 48 | assertTrue(t, !isSpace(r)) 49 | }) 50 | } 51 | }) 52 | t.Run("lowercase", func(t *testing.T) { 53 | for _, r := range lowers { 54 | t.Run(string(r), func(t *testing.T) { 55 | assertTrue(t, !isUpper(r)) 56 | assertTrue(t, isLower(r)) 57 | assertTrue(t, !isNumber(r)) 58 | assertTrue(t, !isSpace(r)) 59 | }) 60 | } 61 | }) 62 | t.Run("numbers", func(t *testing.T) { 63 | for _, r := range numbers { 64 | t.Run(string(r), func(t *testing.T) { 65 | assertTrue(t, !isUpper(r)) 66 | assertTrue(t, !isLower(r)) 67 | assertTrue(t, isNumber(r)) 68 | assertTrue(t, !isSpace(r)) 69 | }) 70 | } 71 | }) 72 | t.Run("spaces", func(t *testing.T) { 73 | for _, r := range spaces { 74 | t.Run(string(r), func(t *testing.T) { 75 | assertTrue(t, !isUpper(r)) 76 | assertTrue(t, !isLower(r)) 77 | assertTrue(t, !isNumber(r)) 78 | assertTrue(t, isSpace(r)) 79 | }) 80 | } 81 | }) 82 | t.Run("other", func(t *testing.T) { 83 | for _, r := range others { 84 | t.Run(string(r), func(t *testing.T) { 85 | assertTrue(t, !isUpper(r)) 86 | assertTrue(t, !isLower(r)) 87 | assertTrue(t, !isNumber(r)) 88 | assertTrue(t, !isSpace(r)) 89 | }) 90 | } 91 | }) 92 | } 93 | 94 | func TestToUpper(t *testing.T) { 95 | tests := []struct { 96 | r rune 97 | want rune 98 | }{ 99 | {'c', 'C'}, 100 | {'A', 'A'}, 101 | {'ñ', 'Ñ'}, 102 | {'9', '9'}, 103 | {'.', '.'}, 104 | } 105 | for _, test := range tests { 106 | t.Run(string(test.r), func(t *testing.T) { 107 | assertTrue(t, test.want == toUpper(test.r)) 108 | }) 109 | } 110 | } 111 | 112 | func TestToLower(t *testing.T) { 113 | tests := []struct { 114 | r rune 115 | want rune 116 | }{ 117 | {'C', 'c'}, 118 | {'h', 'h'}, 119 | {'Ñ', 'ñ'}, 120 | {'9', '9'}, 121 | {'.', '.'}, 122 | } 123 | for _, test := range tests { 124 | t.Run(string(test.r), func(t *testing.T) { 125 | assertTrue(t, test.want == toLower(test.r)) 126 | }) 127 | } 128 | } 129 | --------------------------------------------------------------------------------