├── .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 | [](https://goreportcard.com/report/github.com/ettle/strcase)
5 | [](http://gocover.io/github.com/ettle/strcase)
6 | [](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 |
69 | {{range .}}
70 | - ☞ {{html .Body}}
71 | {{end}}
72 |
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 | [](https://goreportcard.com/report/github.com/ettle/strcase)
5 | [](http://gocover.io/github.com/ettle/strcase)
6 | [](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 |
--------------------------------------------------------------------------------