├── .github
└── workflows
│ ├── go.yaml
│ ├── python.yaml
│ └── rust.yaml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── go
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── anycase.go
├── anycase_test.go
├── doc.go
├── go.mod
└── go.sum
├── python
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── anycase
│ ├── __init__.py
│ ├── __init__.pyi
│ └── py.typed
├── benches
│ └── test_bench.py
├── dev
│ ├── requirements.in
│ └── requirements.txt
├── pyproject.toml
├── src
│ └── lib.rs
└── tests
│ └── test_anycase.py
├── rust
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── benches
│ └── benches.rs
├── onedoc.toml
├── src
│ ├── lib.rs
│ └── raw.rs
└── tests
│ └── anycase.rs
└── testdata
└── common.json
/.github/workflows/go.yaml:
--------------------------------------------------------------------------------
1 | name: go
2 |
3 | on: [push, pull_request]
4 |
5 | defaults:
6 | run:
7 | shell: bash
8 | working-directory: ./go
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: actions/setup-go@v5
16 |
17 | - name: Add $GOPATH/bin to $PATH
18 | run: echo $(go env GOPATH)/bin >> $GITHUB_PATH
19 |
20 | - name: Install staticcheck
21 | run: go install honnef.co/go/tools/cmd/staticcheck@2025.1
22 |
23 | - name: Format
24 | run: diff <(echo -n) <(gofmt -s -d .)
25 |
26 | - name: Test
27 | run: go test -v ./...
28 |
29 | - name: Vet
30 | run: go vet -v ./...
31 |
32 | - name: Run staticcheck
33 | run: staticcheck ./...
34 |
--------------------------------------------------------------------------------
/.github/workflows/python.yaml:
--------------------------------------------------------------------------------
1 | name: python
2 |
3 | on: [push, pull_request, workflow_dispatch]
4 |
5 | permissions:
6 | contents: read
7 |
8 | defaults:
9 | run:
10 | shell: bash
11 | working-directory: ./python
12 |
13 | jobs:
14 | test:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: actions/setup-python@v5
19 | with:
20 | python-version: '3.11'
21 | - name: Install dependencies
22 | run: |
23 | pip install --upgrade pip wheel setuptools
24 | pip install -r dev/requirements.txt
25 | - name: Build wheel
26 | uses: PyO3/maturin-action@v1
27 | with:
28 | working-directory: ./python
29 | args: --release --out dist --find-interpreter
30 | sccache: 'true'
31 | - name: Install wheel
32 | run: pip install py-anycase --find-links dist --force-reinstall
33 | - name: Test
34 | run: pytest --benchmark-disable
35 |
36 | linux:
37 | needs: test
38 | runs-on: ubuntu-latest
39 | strategy:
40 | matrix:
41 | target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
42 |
43 | steps:
44 | - uses: actions/checkout@v4
45 | - uses: actions/setup-python@v5
46 | with:
47 | python-version: '3.11'
48 | - name: Build wheels
49 | uses: PyO3/maturin-action@v1
50 | with:
51 | working-directory: ./python
52 | target: ${{ matrix.target }}
53 | args: --release --out dist --find-interpreter
54 | sccache: 'true'
55 | manylinux: auto
56 | - name: Upload wheels
57 | uses: actions/upload-artifact@v4
58 | with:
59 | name: wheels-linux-${{ matrix.target }}
60 | path: python/dist
61 |
62 | musllinux:
63 | needs: test
64 | runs-on: ubuntu-latest
65 | strategy:
66 | matrix:
67 | target: [x86_64, x86, aarch64, armv7]
68 |
69 | steps:
70 | - uses: actions/checkout@v4
71 | - uses: actions/setup-python@v5
72 | with:
73 | python-version: '3.11'
74 | - name: Build wheels
75 | uses: PyO3/maturin-action@v1
76 | with:
77 | working-directory: ./python
78 | target: ${{ matrix.target }}
79 | args: --release --out dist --find-interpreter
80 | sccache: 'true'
81 | manylinux: musllinux_1_2
82 | - name: Upload wheels
83 | uses: actions/upload-artifact@v4
84 | with:
85 | name: wheels-musllinux-${{ matrix.target }}
86 | path: python/dist
87 |
88 | windows:
89 | needs: test
90 | runs-on: windows-latest
91 | strategy:
92 | matrix:
93 | target: [x64, x86]
94 |
95 | steps:
96 | - uses: actions/checkout@v4
97 | - uses: actions/setup-python@v5
98 | with:
99 | python-version: '3.11'
100 | architecture: ${{ matrix.target }}
101 | - name: Build wheels
102 | uses: PyO3/maturin-action@v1
103 | with:
104 | working-directory: ./python
105 | target: ${{ matrix.target }}
106 | args: --release --out dist --find-interpreter
107 | sccache: 'true'
108 | - name: Upload wheels
109 | uses: actions/upload-artifact@v4
110 | with:
111 | name: wheels-windows-${{ matrix.target }}
112 | path: python/dist
113 |
114 | macos:
115 | needs: test
116 | runs-on: ${{ matrix.platform.runner }}
117 | strategy:
118 | matrix:
119 | platform:
120 | # - runner: macos-12
121 | # target: x86_64
122 | - runner: macos-14
123 | target: aarch64
124 | steps:
125 | - uses: actions/checkout@v4
126 | - uses: actions/setup-python@v5
127 | with:
128 | python-version: '3.11'
129 | - name: Build wheels
130 | uses: PyO3/maturin-action@v1
131 | with:
132 | working-directory: ./python
133 | target: ${{ matrix.platform.target }}
134 | args: --release --out dist --find-interpreter
135 | sccache: 'true'
136 | - name: Upload wheels
137 | uses: actions/upload-artifact@v4
138 | with:
139 | name: wheels-macos-${{ matrix.platform.target }}
140 | path: python/dist
141 |
142 | sdist:
143 | needs: test
144 | runs-on: ubuntu-latest
145 | steps:
146 | - uses: actions/checkout@v4
147 | - name: Build sdist
148 | uses: PyO3/maturin-action@v1
149 | with:
150 | working-directory: ./python
151 | command: sdist
152 | args: --out dist
153 | - name: Upload sdist
154 | uses: actions/upload-artifact@v4
155 | with:
156 | name: wheels-sdist
157 | path: python/dist
158 |
159 | release:
160 | name: release
161 | runs-on: ubuntu-latest
162 | if: "startsWith(github.ref, 'refs/tags/')"
163 | needs: [linux, musllinux, windows, macos, sdist]
164 | steps:
165 | - uses: actions/download-artifact@v4
166 | - name: Publish to PyPI
167 | uses: PyO3/maturin-action@v1
168 | env:
169 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
170 | with:
171 | command: upload
172 | args: --non-interactive --skip-existing wheels-*/*
173 |
--------------------------------------------------------------------------------
/.github/workflows/rust.yaml:
--------------------------------------------------------------------------------
1 | name: rust
2 |
3 | on: [push, pull_request]
4 |
5 | defaults:
6 | run:
7 | shell: bash
8 | working-directory: ./rust
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | env:
15 | RUSTFLAGS: --deny warnings
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: dtolnay/rust-toolchain@stable
20 |
21 | - name: Rustfmt
22 | run: cargo fmt -- --check
23 |
24 | - name: Clippy
25 | run: cargo clippy --workspace --all-targets
26 |
27 | - name: Test
28 | run: cargo test --workspace --all-targets
29 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any person obtaining a copy
2 | of this software and associated documentation files (the "Software"), to deal
3 | in the Software without restriction, including without limitation the rights
4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5 | copies of the Software, and to permit persons to whom the Software is
6 | furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all
9 | copies or substantial portions of the Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17 | SOFTWARE.
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # anycase
2 |
3 | A case conversion library for [Go](./go), [Rust](./rust), and [Python](./python).
4 |
5 |
6 |
7 | Anycase provides a consistent way of converting between different case styles.
8 | And has a similar API across languages.
9 |
10 | ## License
11 |
12 | This project is distributed under the terms of both the MIT license and the
13 | Apache License (Version 2.0).
14 |
15 | See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) for details.
16 |
--------------------------------------------------------------------------------
/go/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/go/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/go/README.md:
--------------------------------------------------------------------------------
1 | # anycase
2 |
3 | [](https://pkg.go.dev/github.com/rossmacarthur/anycase/go)
4 | [](https://github.com/rossmacarthur/anycase/actions/workflows/go.yaml)
5 |
6 | 💼 A case conversion library for Go.
7 |
8 | ## Getting started
9 |
10 | Install using
11 |
12 | ```sh
13 | go get -u github.com/rossmacarthur/anycase/go
14 | ```
15 |
16 | Now convert a string using the relevant function.
17 |
18 | ```go
19 | import "github.com/rossmacarthur/anycase/go"
20 |
21 | anycase.ToSnake("XMLHttpRequest") // returns "xml_http_request"
22 | ```
23 |
24 | ## 🤸 Usage
25 |
26 | The following cases are available.
27 |
28 | | Function | Output |
29 | | :-------------------------------------- | :--------------------- |
30 | | `anycase.ToCamel(s)` | `camelCase` |
31 | | `anycase.ToPascal(s)` | `PascalCase` |
32 | | `anycase.ToSnake(s)` | `snake_case` |
33 | | `anycase.ToScreamingSnake(s)` | `SCREAMING_SNAKE_CASE` |
34 | | `anycase.ToKebab(s)` | `kebab-case` |
35 | | `anycase.ToScreamingKebab(s)` | `SCREAMING-KEBAB-CASE` |
36 | | `anycase.ToTrain(s)` | `Train-Case` |
37 | | `anycase.ToLower(s)` | `lower case` |
38 | | `anycase.ToTitle(s)` | `Title Case` |
39 | | `anycase.ToUpper(s)` | `UPPER CASE` |
40 | | `anycase.Transform(s, wordFn, delimFn)` | *your own case here* |
41 |
42 | Additionally, this library also exposes a `Transform` function which allows
43 | flexible customization of the output.
44 |
45 | For example if you wanted `dotted.snake.case` you could do the following.
46 |
47 | ```go
48 | import (
49 | "strings"
50 | "github.com/rossmacarthur/anycase/go"
51 | )
52 |
53 | func delimDot(s *strings.Builder) {
54 | s.WriteRune('.')
55 | }
56 |
57 | anycase.Transform("XmlHttpRequest", anycase.ToLower, delimDot) // returns xml.http.request
58 | ```
59 |
60 | Here is a more involved example in order to handle acronyms in `PascalCase`.
61 |
62 | ```go
63 | import (
64 | "strings"
65 | "github.com/rossmacarthur/anycase/go"
66 | )
67 |
68 | // The default ToPascal function has no understanding of acronyms
69 | anycase.ToPascal("xml_http_request") // returns "XmlHttpRequest"
70 |
71 | // We can instead use Transform directly
72 | writeFn := func(s *strings.Builder, word string) {
73 | w := strings.ToUpper(asLower)
74 | if w == "XML" || w == "HTTP" {
75 | s.WriteString(w)
76 | } else {
77 | // fallback to default
78 | anycase.WriteTitle(s, word)
79 | }
80 | }
81 | anycase.Transform("xml_http_request", writeFn, nil) // returns "XMLHTTPRequest"
82 | ```
83 |
84 | ## How does it work?
85 |
86 | This implementation divides the input string into words and applies a "word
87 | function" to each word and calls a "delimiter function" for each word boundary
88 | (the space between words).
89 |
90 | Word boundaries are defined as follows:
91 | - A set of consecutive non-letter/number/symbol e.g. `foo _bar` is two words
92 | `foo` and `bar`.
93 | - A transition from a lowercase letter to an uppercase letter e.g. `fooBar` is
94 | two words `foo` and `Bar`.
95 | - The second last uppercase letter in a word with multiple uppercase letters
96 | e.g. `FOOBar` is two words `FOO` and `Bar`.
97 |
98 | ## License
99 |
100 | This project is distributed under the terms of both the MIT license and the
101 | Apache License (Version 2.0).
102 |
103 | See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) for details.
104 |
--------------------------------------------------------------------------------
/go/anycase.go:
--------------------------------------------------------------------------------
1 | package anycase
2 |
3 | import (
4 | "strings"
5 | "unicode"
6 | )
7 |
8 | // ToCamel converts a string to camelCase.
9 | func ToCamel(s string) string {
10 | first := true
11 | writeFn := func(s *strings.Builder, word string) {
12 | if first {
13 | WriteLower(s, word)
14 | first = false
15 | } else {
16 | WriteTitle(s, word)
17 | }
18 | }
19 | return Transform(s, writeFn, nil)
20 | }
21 |
22 | // ToPascal converts a string to PascalCase.
23 | func ToPascal(s string) string {
24 | return Transform(s, WriteTitle, nil)
25 | }
26 |
27 | // ToSnake converts a string to snake_case.
28 | func ToSnake(s string) string {
29 | return Transform(s, WriteLower, DelimUnderscore)
30 | }
31 |
32 | // ToScreamingSnake converts a string to SCREAMING_SNAKE_CASE.
33 | func ToScreamingSnake(s string) string {
34 | return Transform(s, WriteUpper, DelimUnderscore)
35 | }
36 |
37 | // ToKebab converts a string to kebab-case.
38 | func ToKebab(s string) string {
39 | return Transform(s, WriteLower, DelimHyphen)
40 | }
41 |
42 | // ToScreamingKebab converts a string to SCREAMING-KEBAB-CASE.
43 | func ToScreamingKebab(s string) string {
44 | return Transform(s, WriteUpper, DelimHyphen)
45 | }
46 |
47 | // ToTrain converts a string to Train-Case.
48 | func ToTrain(s string) string {
49 | return Transform(s, WriteTitle, DelimHyphen)
50 | }
51 |
52 | // ToLower converts a string to lower case.
53 | func ToLower(s string) string {
54 | return Transform(s, WriteLower, DelimSpace)
55 | }
56 |
57 | // ToTitle converts a string to Title Case.
58 | func ToTitle(s string) string {
59 | return Transform(s, WriteTitle, DelimSpace)
60 | }
61 |
62 | // ToUpper converts a string to UPPER CASE.
63 | func ToUpper(s string) string {
64 | return Transform(s, WriteUpper, DelimSpace)
65 | }
66 |
67 | type state int
68 |
69 | const (
70 | stateUnknown state = 0
71 | stateDelims state = 1
72 | stateLower state = 2
73 | stateUpper state = 3
74 | )
75 |
76 | type delimFn = func(s *strings.Builder)
77 |
78 | type writeFn = func(s *strings.Builder, word string)
79 |
80 | // Transform reconstructs the provided string using the given "word function" and
81 | // "delimiter function".
82 | //
83 | // The word function is called for each word in the string, and the delimiter
84 | // function is called for each delimiter between words.
85 | func Transform(s string, wordFn writeFn, delimFn delimFn) string {
86 | out := strings.Builder{}
87 | out.Grow(len(s))
88 |
89 | runes := []rune(s)
90 |
91 | // when we are on the first word
92 | first := true
93 | // the byte index of the start of the current word
94 | w0 := 0
95 | // the byte index of the end of the current word
96 | w1 := -1
97 | // the current state of the word boundary machine
98 | state := stateUnknown
99 |
100 | write := func(w0, w1 int) {
101 | if w1-w0 > 0 {
102 | if first {
103 | first = false
104 | } else if delimFn != nil {
105 | delimFn(&out)
106 | }
107 | wordFn(&out, string(runes[w0:w1]))
108 | }
109 | }
110 |
111 | for i := 0; i < len(runes); i++ {
112 | r := runes[i]
113 | if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
114 | state = stateDelims
115 | if w1 == -1 {
116 | w1 = i // store the end of the previous word
117 | }
118 | continue
119 | }
120 |
121 | isLower := unicode.IsLower(r)
122 | isUpper := unicode.IsUpper(r)
123 |
124 | switch {
125 | case state == stateDelims:
126 | if w1 != -1 {
127 | write(w0, w1)
128 | }
129 | w0 = i
130 | w1 = -1
131 | case state == stateLower && isUpper:
132 | write(w0, i)
133 | w0 = i
134 | case state == stateUpper && isUpper && i+1 < len(runes) && unicode.IsLower(runes[i+1]):
135 | write(w0, i)
136 | w0 = i
137 | }
138 |
139 | if isLower {
140 | state = stateLower
141 | } else if isUpper {
142 | state = stateUpper
143 | } else if state == stateDelims {
144 | state = stateUnknown
145 | }
146 | }
147 |
148 | switch state {
149 | case stateDelims:
150 | if w1 != -1 {
151 | write(w0, w1)
152 | }
153 | default:
154 | write(w0, len(runes))
155 | }
156 |
157 | return out.String()
158 | }
159 |
160 | // DelimUnderscore is a delimiter function that inserts an underscore.
161 | func DelimUnderscore(s *strings.Builder) {
162 | s.WriteRune('_')
163 | }
164 |
165 | // DelimHyphen is a delimiter function that inserts a hyphen.
166 | func DelimHyphen(s *strings.Builder) {
167 | s.WriteRune('-')
168 | }
169 |
170 | // DelimSpace is a delimiter function that inserts a space.
171 | func DelimSpace(s *strings.Builder) {
172 | s.WriteRune(' ')
173 | }
174 |
175 | // WriteUpper writes the word in uppercase.
176 | func WriteUpper(s *strings.Builder, word string) {
177 | s.WriteString(strings.ToUpper(word))
178 | }
179 |
180 | // WriteLower writes the word in lowercase.
181 | func WriteLower(s *strings.Builder, word string) {
182 | s.WriteString(strings.ToLower(word))
183 | }
184 |
185 | // WriteTitle writes the word in title case.
186 | func WriteTitle(s *strings.Builder, word string) {
187 | for i, r := range word {
188 | if i == 0 {
189 | s.WriteRune(unicode.ToUpper(r))
190 | } else {
191 | s.WriteRune(unicode.ToLower(r))
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/go/anycase_test.go:
--------------------------------------------------------------------------------
1 | package anycase_test
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "regexp"
9 | "strings"
10 | "testing"
11 |
12 | anycase "github.com/rossmacarthur/anycase/go"
13 | "github.com/stretchr/testify/require"
14 | )
15 |
16 | type testCase struct {
17 | Input string `json:"input"`
18 | SnakeCase string `json:"snake"`
19 | CamelCase string `json:"camel"`
20 | PascalCase string `json:"pascal"`
21 | ScreamingSnake string `json:"screaming_snake"`
22 | KebabCase string `json:"kebab"`
23 | ScreamingKebab string `json:"screaming_kebab"`
24 | TrainCase string `json:"train"`
25 | LowerCase string `json:"lower"`
26 | TitleCase string `json:"title"`
27 | UpperCase string `json:"upper"`
28 | }
29 |
30 | var tests []testCase
31 |
32 | func init() {
33 | // Load test cases from common.json
34 | path := filepath.Join("..", "testdata", "common.json")
35 | data, err := os.ReadFile(path)
36 | if err != nil {
37 | panic(fmt.Sprintf("failed to read test cases: %v", err))
38 | }
39 |
40 | err = json.Unmarshal(data, &tests)
41 | if err != nil {
42 | panic(fmt.Sprintf("failed to parse test cases: %v", err))
43 | }
44 | }
45 |
46 | func TestCommon(t *testing.T) {
47 | for _, tc := range tests {
48 | if tc.Input == "XΣXΣ baffle" {
49 | continue // skip for now
50 | }
51 | t.Run(tc.Input, func(t *testing.T) {
52 | got := anycase.ToCamel(tc.Input)
53 | require.Equal(t, tc.CamelCase, got, fmt.Sprintf("'%s'", tc.Input))
54 |
55 | got = anycase.ToSnake(tc.Input)
56 | require.Equal(t, tc.SnakeCase, got, fmt.Sprintf("'%s'", tc.Input))
57 |
58 | got = anycase.ToPascal(tc.Input)
59 | require.Equal(t, tc.PascalCase, got, fmt.Sprintf("'%s'", tc.Input))
60 |
61 | got = anycase.ToScreamingSnake(tc.Input)
62 | require.Equal(t, tc.ScreamingSnake, got, fmt.Sprintf("'%s'", tc.Input))
63 |
64 | got = anycase.ToKebab(tc.Input)
65 | require.Equal(t, tc.KebabCase, got, fmt.Sprintf("'%s'", tc.Input))
66 |
67 | got = anycase.ToScreamingKebab(tc.Input)
68 | require.Equal(t, tc.ScreamingKebab, got, fmt.Sprintf("'%s'", tc.Input))
69 |
70 | got = anycase.ToTrain(tc.Input)
71 | require.Equal(t, tc.TrainCase, got, fmt.Sprintf("'%s'", tc.Input))
72 |
73 | got = anycase.ToLower(tc.Input)
74 | require.Equal(t, tc.LowerCase, got, fmt.Sprintf("'%s'", tc.Input))
75 |
76 | got = anycase.ToTitle(tc.Input)
77 | require.Equal(t, tc.TitleCase, got, fmt.Sprintf("'%s'", tc.Input))
78 |
79 | got = anycase.ToUpper(tc.Input)
80 | require.Equal(t, tc.UpperCase, got, fmt.Sprintf("'%s'", tc.Input))
81 | })
82 | }
83 | }
84 |
85 | func BenchmarkToSnake(b *testing.B) {
86 | s := strings.Repeat("ThisIsATestCase", 100)
87 |
88 | require.True(b, anycase.ToSnake(s) == regexToSnake(s))
89 |
90 | b.Run("anycase", func(b *testing.B) {
91 | for i := 0; i < b.N; i++ {
92 | anycase.ToSnake(s)
93 | }
94 | })
95 |
96 | b.Run("regex", func(b *testing.B) {
97 | for i := 0; i < b.N; i++ {
98 | regexToSnake(s)
99 | }
100 | })
101 | }
102 |
103 | // regexToSnake is a regex implementation to convert to snake case to compare
104 | // the benchmark to.
105 | //
106 | // This function doesn't support as many word boundaries as anycase.ToSnake but
107 | // it is still much slower than the anycase.ToSnake implementation.
108 | //
109 | // From https://stackoverflow.com/a/56616250/4591251
110 | func regexToSnake(s string) string {
111 | snake := matchFirstCap.ReplaceAllString(s, "${1}_${2}")
112 | snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
113 | return strings.ToLower(snake)
114 | }
115 |
116 | var (
117 | matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
118 | matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
119 | )
120 |
--------------------------------------------------------------------------------
/go/doc.go:
--------------------------------------------------------------------------------
1 | // Package anycase provides functions for converting strings between different
2 | // cases.
3 | //
4 | // The currently supported cases are:
5 | //
6 | // | Function | Output |
7 | // | ------------------- | -------------------- |
8 | // | ToCamel(s) | camelCase |
9 | // | ToPascal(s) | PascalCase |
10 | // | ToSnake(s) | snake_case |
11 | // | ToScreamingSnake(s) | SCREAMING_SNAKE_CASE |
12 | // | ToKebab(s) | kebab-case |
13 | // | ToScreamingKebab(s) | SCREAMING-KEBAB-CASE |
14 | // | ToTrain(s) | Train-Case |
15 | // | ToLower(s) | lower case |
16 | // | ToTitle(s) | Title Case |
17 | // | ToUpper(s) | UPPER CASE |
18 |
19 | package anycase
20 |
--------------------------------------------------------------------------------
/go/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/rossmacarthur/anycase/go
2 |
3 | go 1.19
4 |
5 | require github.com/stretchr/testify v1.8.2
6 |
7 | require (
8 | github.com/davecgh/go-spew v1.1.1 // indirect
9 | github.com/pmezard/go-difflib v1.0.0 // indirect
10 | gopkg.in/yaml.v3 v3.0.1 // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/go/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
9 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
11 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
12 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
16 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
17 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
18 |
--------------------------------------------------------------------------------
/python/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | .pytest_cache/
6 | *.py[cod]
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | .venv/
14 | env/
15 | bin/
16 | build/
17 | develop-eggs/
18 | dist/
19 | eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | include/
26 | man/
27 | venv/
28 | *.egg-info/
29 | .installed.cfg
30 | *.egg
31 |
32 | # Installer logs
33 | pip-log.txt
34 | pip-delete-this-directory.txt
35 | pip-selfcheck.json
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .cache
42 | nosetests.xml
43 | coverage.xml
44 |
45 | # Translations
46 | *.mo
47 |
48 | # Mr Developer
49 | .mr.developer.cfg
50 | .project
51 | .pydevproject
52 |
53 | # Rope
54 | .ropeproject
55 |
56 | # Django stuff:
57 | *.log
58 | *.pot
59 |
60 | .DS_Store
61 |
62 | # Sphinx documentation
63 | docs/_build/
64 |
65 | # PyCharm
66 | .idea/
67 |
68 | # VSCode
69 | .vscode/
70 |
71 | # Pyenv
72 | .python-version
--------------------------------------------------------------------------------
/python/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "anycase"
7 | version = "0.1.0"
8 |
9 | [[package]]
10 | name = "autocfg"
11 | version = "1.1.0"
12 | source = "registry+https://github.com/rust-lang/crates.io-index"
13 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
14 |
15 | [[package]]
16 | name = "cfg-if"
17 | version = "1.0.0"
18 | source = "registry+https://github.com/rust-lang/crates.io-index"
19 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
20 |
21 | [[package]]
22 | name = "heck"
23 | version = "0.5.0"
24 | source = "registry+https://github.com/rust-lang/crates.io-index"
25 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
26 |
27 | [[package]]
28 | name = "indoc"
29 | version = "2.0.6"
30 | source = "registry+https://github.com/rust-lang/crates.io-index"
31 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
32 |
33 | [[package]]
34 | name = "libc"
35 | version = "0.2.148"
36 | source = "registry+https://github.com/rust-lang/crates.io-index"
37 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
38 |
39 | [[package]]
40 | name = "memoffset"
41 | version = "0.9.0"
42 | source = "registry+https://github.com/rust-lang/crates.io-index"
43 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
44 | dependencies = [
45 | "autocfg",
46 | ]
47 |
48 | [[package]]
49 | name = "once_cell"
50 | version = "1.18.0"
51 | source = "registry+https://github.com/rust-lang/crates.io-index"
52 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
53 |
54 | [[package]]
55 | name = "portable-atomic"
56 | version = "1.11.0"
57 | source = "registry+https://github.com/rust-lang/crates.io-index"
58 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
59 |
60 | [[package]]
61 | name = "proc-macro2"
62 | version = "1.0.95"
63 | source = "registry+https://github.com/rust-lang/crates.io-index"
64 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
65 | dependencies = [
66 | "unicode-ident",
67 | ]
68 |
69 | [[package]]
70 | name = "py-anycase"
71 | version = "0.0.0"
72 | dependencies = [
73 | "anycase",
74 | "pyo3",
75 | ]
76 |
77 | [[package]]
78 | name = "pyo3"
79 | version = "0.24.2"
80 | source = "registry+https://github.com/rust-lang/crates.io-index"
81 | checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219"
82 | dependencies = [
83 | "cfg-if",
84 | "indoc",
85 | "libc",
86 | "memoffset",
87 | "once_cell",
88 | "portable-atomic",
89 | "pyo3-build-config",
90 | "pyo3-ffi",
91 | "pyo3-macros",
92 | "unindent",
93 | ]
94 |
95 | [[package]]
96 | name = "pyo3-build-config"
97 | version = "0.24.2"
98 | source = "registry+https://github.com/rust-lang/crates.io-index"
99 | checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999"
100 | dependencies = [
101 | "once_cell",
102 | "target-lexicon",
103 | ]
104 |
105 | [[package]]
106 | name = "pyo3-ffi"
107 | version = "0.24.2"
108 | source = "registry+https://github.com/rust-lang/crates.io-index"
109 | checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33"
110 | dependencies = [
111 | "libc",
112 | "pyo3-build-config",
113 | ]
114 |
115 | [[package]]
116 | name = "pyo3-macros"
117 | version = "0.24.2"
118 | source = "registry+https://github.com/rust-lang/crates.io-index"
119 | checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9"
120 | dependencies = [
121 | "proc-macro2",
122 | "pyo3-macros-backend",
123 | "quote",
124 | "syn",
125 | ]
126 |
127 | [[package]]
128 | name = "pyo3-macros-backend"
129 | version = "0.24.2"
130 | source = "registry+https://github.com/rust-lang/crates.io-index"
131 | checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a"
132 | dependencies = [
133 | "heck",
134 | "proc-macro2",
135 | "pyo3-build-config",
136 | "quote",
137 | "syn",
138 | ]
139 |
140 | [[package]]
141 | name = "quote"
142 | version = "1.0.40"
143 | source = "registry+https://github.com/rust-lang/crates.io-index"
144 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
145 | dependencies = [
146 | "proc-macro2",
147 | ]
148 |
149 | [[package]]
150 | name = "syn"
151 | version = "2.0.100"
152 | source = "registry+https://github.com/rust-lang/crates.io-index"
153 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
154 | dependencies = [
155 | "proc-macro2",
156 | "quote",
157 | "unicode-ident",
158 | ]
159 |
160 | [[package]]
161 | name = "target-lexicon"
162 | version = "0.13.2"
163 | source = "registry+https://github.com/rust-lang/crates.io-index"
164 | checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
165 |
166 | [[package]]
167 | name = "unicode-ident"
168 | version = "1.0.12"
169 | source = "registry+https://github.com/rust-lang/crates.io-index"
170 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
171 |
172 | [[package]]
173 | name = "unindent"
174 | version = "0.2.4"
175 | source = "registry+https://github.com/rust-lang/crates.io-index"
176 | checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
177 |
--------------------------------------------------------------------------------
/python/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "py-anycase"
3 | version = "0.0.0"
4 | edition = "2021"
5 | publish = false
6 | include = ["/src/**/*", "/*.py", "/*.pyi", "/LICENSE", "/README.md"]
7 |
8 | [lib]
9 | name = "anycase"
10 | crate-type = ["cdylib"]
11 |
12 | [dependencies]
13 | anycase = { path = "../rust" }
14 | pyo3 = "0.24.2"
15 |
--------------------------------------------------------------------------------
/python/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/python/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/python/README.md:
--------------------------------------------------------------------------------
1 | # anycase
2 |
3 | [](https://pypi.org/project/py-anycase)
4 | [](https://github.com/rossmacarthur/anycase/actions/workflows/python.yaml)
5 |
6 | 💼 A case conversion library for Python.
7 |
8 | ## Features
9 |
10 | - Automatic case detection, no need to specify the input case
11 | - Extremely fast, written in Rust ✨
12 | - Support for Unicode characters
13 | - Support for providing acronyms in title case
14 |
15 | ## 🚀 Getting started
16 |
17 | Install using
18 |
19 | ```sh
20 | pip install py-anycase
21 | ```
22 |
23 | Now convert a string using the relevant function.
24 |
25 | ```python
26 | import anycase
27 |
28 | anycase.to_snake("XMLHttpRequest") # returns "xml_http_request"
29 | ```
30 |
31 | ## 🤸 Usage
32 |
33 | The `py-anycase` package provides a set of functions to convert strings between
34 | different case styles. The following cases are available.
35 |
36 | | Function | Output |
37 | | :------------------------------ | :--------------------- |
38 | | `anycase.to_camel(s)` | `camelCase` |
39 | | `anycase.to_pascal(s)` | `PascalCase` |
40 | | `anycase.to_snake(s)` | `snake_case` |
41 | | `anycase.to_screaming_snake(s)` | `SCREAMING_SNAKE_CASE` |
42 | | `anycase.to_kebab(s)` | `kebab-case` |
43 | | `anycase.to_screaming_kebab(s)` | `SCREAMING-KEBAB-CASE` |
44 | | `anycase.to_train(s)` | `Train-Case` |
45 | | `anycase.to_lower(s)` | `lower case` |
46 | | `anycase.to_title(s)` | `Title Case` |
47 | | `anycase.to_upper(s)` | `UPPER CASE` |
48 |
49 | Additionally, functions where the "word function" is "title" accept an optional
50 | `acronyms` argument, which is a mapping of lowercase words to their output. For
51 | example:
52 |
53 | ```python
54 | >>> anycase.to_pascal("xml_http_request", acronyms={"xml": "XML"})
55 | 'XMLHttpRequest'
56 | >>> anycase.to_pascal("xml_http_request", acronyms={"xml": "XML", "http": "HTTP"})
57 | 'XMLHTTPRequest'
58 | ```
59 |
60 | ## How does it work?
61 |
62 | This implementation divides the input string into words and applies a "word
63 | function" to each word and calls a "delimiter function" for each word boundary
64 | (the space between words).
65 |
66 | Word boundaries are defined as follows:
67 | - A set of consecutive non-letter/number/symbol e.g. `foo _bar` is two words
68 | `foo` and `bar`.
69 | - A transition from a lowercase letter to an uppercase letter e.g. `fooBar` is
70 | two words `foo` and `Bar`.
71 | - The second last uppercase letter in a word with multiple uppercase letters
72 | e.g. `FOOBar` is two words `FOO` and `Bar`.
73 |
74 |
75 | ## Benchmarks
76 |
77 | A simple benchmark against various other libraries is provided in
78 | [./benches](./benches). The following table shows the results when run on my
79 | Macbook M2 Max.
80 |
81 | | Library | Min (µs) | Max (µs) | Mean (µs) |
82 | | :------------------------ | --------: | --------: | ------------: |
83 | | py-anycase | 26.666 | 176.834 | **30.909** |
84 | | pyheck | 51.000 | 131.416 | **53.565** |
85 | | pure python | 63.583 | 108.125 | **65.075** |
86 | | re | 81.916 | 171.000 | **87.856** |
87 | | stringcase | 99.250 | 222.292 | **102.197** |
88 | | pydantic.alias_generators | 182.000 | 304.458 | **189.063** |
89 | | inflection | 229.750 | 360.792 | **239.153** |
90 | | caseconversion | 1,430.042 | 1,838.375 | **1,559.019** |
91 |
92 | ## License
93 |
94 | This project is distributed under the terms of both the MIT license and the
95 | Apache License (Version 2.0).
96 |
97 | See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) for details.
98 |
--------------------------------------------------------------------------------
/python/anycase/__init__.py:
--------------------------------------------------------------------------------
1 | from .anycase import *
2 |
3 | __doc__ = anycase.__doc__
4 | __all__ = anycase.__all__
5 |
--------------------------------------------------------------------------------
/python/anycase/__init__.pyi:
--------------------------------------------------------------------------------
1 | """
2 | A case conversion library with Unicode support, implemented in Rust.
3 |
4 | This implementation divides the input string into words and applies a "word
5 | function" to each word and calls a "delimiter function" for each word boundary
6 | (the space between words).
7 |
8 | Word boundaries are defined as follows:
9 | - A set of consecutive non-letter/number/symbol e.g. `foo _bar` is two words
10 | `foo` and `bar`.
11 | - A transition from a lowercase letter to an uppercase letter e.g. `fooBar` is
12 | two words `foo` and `Bar`.
13 | - The second last uppercase letter in a word with multiple uppercase letters
14 | e.g. `FOOBar` is two words `FOO` and `Bar`.
15 | """
16 |
17 | from typing import Optional
18 |
19 | def to_camel(s: str, acronyms: Optional[dict[str, str]] = None) -> str:
20 | """
21 | Convert a string to 'camelCase'.
22 |
23 | The first word will be converted to lowercase and subsequent words to title
24 | case. See module documentation for how word boundaries are defined.
25 |
26 | For example:
27 |
28 | >>> anycase.to_camel("foo_bar")
29 | 'fooBar'
30 |
31 | The `acronyms` argument is a mapping of lowercase words to an override
32 | value. This value will be used instead of the camel case conversion.
33 |
34 | For example:
35 |
36 | >>> anycase.to_camel("xml http request", acronyms={"http": "HTTP"})
37 | 'xmlHTTPRequest'
38 |
39 | """
40 | ...
41 |
42 | def to_pascal(s: str, acronyms: Optional[dict[str, str]] = None) -> str:
43 | """
44 | Convert a string to 'PascalCase'.
45 |
46 | Each word will be converted to title case. See module documentation for how
47 | word boundaries are defined.
48 |
49 | For example:
50 |
51 | >>> anycase.to_pascal("foo_bar")
52 | 'FooBar'
53 |
54 | The `acronyms` argument is a mapping of lowercase words to an override
55 | value. This value will be used instead of the pascal case conversion.
56 |
57 | For example:
58 |
59 | >>> anycase.to_pascal("xml http request", acronyms={"http": "HTTP"})
60 | 'XmlHTTPRequest'
61 |
62 | """
63 | ...
64 |
65 | def to_snake(s: str) -> str:
66 | """
67 | Convert a string to 'snake_case'.
68 |
69 | Each word will be converted to lower case and separated with an underscore.
70 | See module documentation for how word boundaries are defined.
71 |
72 | For example:
73 |
74 | >>> anycase.to_snake("fooBar")
75 | 'foo_bar'
76 |
77 | """
78 | ...
79 |
80 | def to_screaming_snake(s: str) -> str:
81 | """
82 | Convert a string to 'SCREAMING_SNAKE_CASE'.
83 |
84 | Each word will be converted to upper case and separated with an underscore.
85 | See module documentation for how word boundaries are defined.
86 |
87 | For example:
88 |
89 | >>> anycase.to_screaming_snake("fooBar")
90 | 'FOO_BAR'
91 |
92 | """
93 | ...
94 |
95 | def to_kebab(s: str) -> str:
96 | """
97 | Convert a string to 'kebab-case'.
98 |
99 | Each word will be converted to lower case and separated with a hyphen. See
100 | module documentation for how word boundaries are defined.
101 |
102 | For example:
103 |
104 | >>> anycase.to_kebab("fooBar")
105 | 'foo-bar'
106 |
107 | """
108 | ...
109 |
110 | def to_screaming_kebab(s: str) -> str:
111 | """
112 | Convert a string to 'SCREAMING-KEBAB-CASE'.
113 |
114 | Each word will be converted to upper case and separated with a hyphen. See
115 | module documentation for how word boundaries are defined.
116 |
117 | For example:
118 |
119 | >>> anycase.to_screaming_kebab("fooBar")
120 | 'FOO-BAR'
121 |
122 | """
123 | ...
124 |
125 | def to_train(s: str, acronyms: Optional[dict[str, str]] = None) -> str:
126 | """
127 | Convert a string to 'Train-Case'.
128 |
129 | Each word will be converted to title case and separated with a hyphen. See
130 | module documentation for how word boundaries are defined.
131 |
132 | For example:
133 |
134 | >>> anycase.to_train("fooBar")
135 | 'Foo-Bar'
136 |
137 | The `acronyms` argument is a mapping of lowercase words to an override
138 | value. This value will be used instead of the train case conversion.
139 |
140 | For example:
141 |
142 | >>> anycase.to_train("xml http request", acronyms={"http": "HTTP"})
143 | 'Xml-HTTP-Request'
144 |
145 | """
146 | ...
147 |
148 | def to_lower(s: str) -> str:
149 | """
150 | Convert a string to 'lower case'.
151 |
152 | Each word will be converted to lower case and separated with a space. See
153 | module documentation for how word boundaries are defined.
154 |
155 | For example:
156 |
157 | >>> anycase.to_lower("FooBar")
158 | 'foo bar'
159 |
160 | """
161 | ...
162 |
163 | def to_title(s: str, acronyms: Optional[dict[str, str]] = None) -> str:
164 | """
165 | Convert a string to 'Title Case'.
166 |
167 | Each word will be converted to title case and separated with a space. See
168 | module documentation for how word boundaries are defined.
169 |
170 | For example:
171 |
172 | >>> anycase.to_title("foo_bar")
173 | 'Foo Bar'
174 |
175 | The `acronyms` argument is a mapping of lowercase words to an override
176 | value. This value will be used instead of the title case conversion.
177 |
178 | For example:
179 |
180 | >>> anycase.to_title("xml_http_request", acronyms={"http": "HTTP"})
181 | 'Xml HTTP Request'
182 |
183 | """
184 | ...
185 |
186 | def to_upper(s: str) -> str:
187 | """
188 | Convert a string to 'UPPER CASE'.
189 |
190 | Each word will be converted to upper case and separated with a space. See
191 | module documentation for how word boundaries are defined.
192 |
193 | For example:
194 |
195 | >>> anycase.to_upper("fooBar")
196 | 'FOO BAR'
197 |
198 | """
199 | ...
200 |
--------------------------------------------------------------------------------
/python/anycase/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rossmacarthur/anycase/31b1fc922b45b5fc92b125885c4c0ea7609edf3d/python/anycase/py.typed
--------------------------------------------------------------------------------
/python/benches/test_bench.py:
--------------------------------------------------------------------------------
1 | from pytest_benchmark.fixture import BenchmarkFixture
2 |
3 |
4 | LEN = 100
5 | INPUT = "thisIsACamelCaseString" * LEN
6 | EXPECT = "this_is_a_camel_case_string" * LEN
7 |
8 |
9 | def test_bench_to_snake_pure_python(benchmark: BenchmarkFixture):
10 | def to_snake(s: str) -> str:
11 | return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_")
12 |
13 | assert benchmark(to_snake, INPUT) == EXPECT
14 |
15 |
16 | def test_bench_to_snake_python_re(benchmark: BenchmarkFixture):
17 | import re
18 |
19 | pattern = re.compile(r"(? str:
22 | return pattern.sub("_", s).lower()
23 |
24 | assert benchmark(to_snake, INPUT) == EXPECT
25 |
26 |
27 | def test_bench_to_snake_anycase(benchmark: BenchmarkFixture):
28 | from anycase import to_snake
29 |
30 | assert benchmark(to_snake, INPUT) == EXPECT
31 |
32 |
33 | def test_bench_to_snake_caseconversion(benchmark: BenchmarkFixture):
34 | from case_conversion import snakecase as to_snake
35 |
36 | assert benchmark(to_snake, INPUT) == EXPECT
37 |
38 |
39 | def test_bench_to_snake_inflection(benchmark: BenchmarkFixture):
40 | from inflection import underscore as to_snake
41 |
42 | assert benchmark(to_snake, INPUT) == EXPECT
43 |
44 |
45 | def test_bench_to_snake_pydantic(benchmark: BenchmarkFixture):
46 | from pydantic.alias_generators import to_snake
47 |
48 | assert benchmark(to_snake, INPUT) == EXPECT
49 |
50 |
51 | def test_bench_to_snake_pyheck(benchmark: BenchmarkFixture):
52 | from pyheck import snake as to_snake
53 |
54 | assert benchmark(to_snake, INPUT) == EXPECT
55 |
56 |
57 | def test_bench_to_snake_stringcase(benchmark: BenchmarkFixture):
58 | from stringcase import snakecase as to_snake
59 |
60 | assert benchmark(to_snake, INPUT) == EXPECT
61 |
--------------------------------------------------------------------------------
/python/dev/requirements.in:
--------------------------------------------------------------------------------
1 | maturin
2 | ruff
3 | pytest
4 | pytest-benchmark
5 |
6 | case_conversion
7 | inflection
8 | pydantic
9 | pyheck
10 | stringcase
11 |
--------------------------------------------------------------------------------
/python/dev/requirements.txt:
--------------------------------------------------------------------------------
1 | # This file was autogenerated by uv via the following command:
2 | # uv pip compile -o dev/requirements.txt dev/requirements.in
3 | annotated-types==0.7.0
4 | # via pydantic
5 | case-conversion==2.1.0
6 | # via -r dev/requirements.in
7 | inflection==0.5.1
8 | # via -r dev/requirements.in
9 | iniconfig==2.0.0
10 | # via pytest
11 | maturin==1.6.0
12 | # via -r dev/requirements.in
13 | packaging==23.2
14 | # via pytest
15 | pluggy==1.3.0
16 | # via pytest
17 | py-cpuinfo==9.0.0
18 | # via pytest-benchmark
19 | pydantic==2.8.2
20 | # via -r dev/requirements.in
21 | pydantic-core==2.20.1
22 | # via pydantic
23 | pyheck==0.1.5
24 | # via -r dev/requirements.in
25 | pytest==7.4.3
26 | # via
27 | # -r dev/requirements.in
28 | # pytest-benchmark
29 | pytest-benchmark==4.0.0
30 | # via -r dev/requirements.in
31 | regex==2023.10.3
32 | # via case-conversion
33 | ruff==0.1.3
34 | # via -r dev/requirements.in
35 | stringcase==1.2.0
36 | # via -r dev/requirements.in
37 | typing-extensions==4.12.2
38 | # via
39 | # pydantic
40 | # pydantic-core
41 |
--------------------------------------------------------------------------------
/python/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["maturin>=1.2,<2.0"]
3 | build-backend = "maturin"
4 |
5 | [tool.maturin]
6 | features = ["pyo3/extension-module"]
7 |
8 | [project]
9 | name = "py-anycase"
10 | version = "0.1.0"
11 | description = "A case conversion library with Unicode support"
12 | requires-python = ">=3.7"
13 | license = { text = "MIT OR Apache-2.0" }
14 | authors = [{ name = "Ross MacArthur", email = "ross@macarthur.io" }]
15 | readme = "README.md"
16 | keywords = ["convert", "case", "snake", "camel", "pascal"]
17 | classifiers = [
18 | "Development Status :: 3 - Alpha",
19 | "Intended Audience :: Developers",
20 | "License :: OSI Approved :: MIT License",
21 | "License :: OSI Approved :: Apache Software License",
22 | "Natural Language :: English",
23 | "Operating System :: OS Independent",
24 | "Programming Language :: Rust",
25 | "Programming Language :: Python",
26 | "Programming Language :: Python :: 3.7",
27 | "Programming Language :: Python :: 3.8",
28 | "Programming Language :: Python :: 3.9",
29 | "Programming Language :: Python :: 3.10",
30 | "Programming Language :: Python :: 3.11",
31 | "Programming Language :: Python :: 3.12",
32 | "Programming Language :: Python :: 3.13",
33 | ]
34 |
35 | [project.urls]
36 | Homepage = "https://github.com/rossmacarthur/anycase"
37 | Repository = "https://github.com/rossmacarthur/anycase"
38 |
--------------------------------------------------------------------------------
/python/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::fmt;
2 | use std::fmt::Write;
3 |
4 | use pyo3::prelude::*;
5 | use pyo3::types::PyDict;
6 |
7 | use ::anycase as lib;
8 | use pyo3::types::PyString;
9 |
10 | /// Convert a string to 'camelCase'.
11 | #[pyfunction]
12 | #[pyo3(signature = (s, /, acronyms = None))]
13 | fn to_camel(s: &str, acronyms: Option<&Bound<'_, PyDict>>) -> String {
14 | let mut first = true;
15 | let word_fn = |buf: &mut String, s: &str| -> fmt::Result {
16 | if first {
17 | first = false;
18 | lib::raw::write_lower(buf, s)
19 | } else {
20 | match get_acronym(s, acronyms) {
21 | Some(acronym) => write!(buf, "{}", acronym),
22 | None => lib::raw::write_title(buf, s),
23 | }
24 | }
25 | };
26 |
27 | lib::raw::to_string(s, word_fn, lib::raw::delim_none)
28 | }
29 |
30 | /// Convert a string to 'PascalCase'.
31 | #[pyfunction]
32 | #[pyo3(signature = (s, /, acronyms = None))]
33 | fn to_pascal(s: &str, acronyms: Option<&Bound<'_, PyDict>>) -> String {
34 | let word_fn = |buf: &mut String, s: &str| -> fmt::Result {
35 | match get_acronym(s, acronyms) {
36 | Some(acronym) => write!(buf, "{}", acronym),
37 | None => lib::raw::write_title(buf, s),
38 | }
39 | };
40 |
41 | lib::raw::to_string(s, word_fn, lib::raw::delim_none)
42 | }
43 |
44 | /// Convert a string to 'snake_case'.
45 | #[pyfunction]
46 | fn to_snake(s: &str) -> String {
47 | lib::to_snake(s)
48 | }
49 |
50 | /// Convert a string to 'SCREAMING_SNAKE_CASE'.
51 | #[pyfunction]
52 | fn to_screaming_snake(s: &str) -> String {
53 | lib::to_screaming_snake(s)
54 | }
55 |
56 | /// Convert a string to 'kebab-case'.
57 | #[pyfunction]
58 | fn to_kebab(s: &str) -> String {
59 | lib::to_kebab(s)
60 | }
61 |
62 | /// Convert a string to 'SCREAMING-KEBAB-CASE'.
63 | #[pyfunction]
64 | fn to_screaming_kebab(s: &str) -> String {
65 | lib::to_screaming_kebab(s)
66 | }
67 |
68 | /// Convert a string to 'Train-Case'.
69 | #[pyfunction]
70 | #[pyo3(signature = (s, /, acronyms = None))]
71 | fn to_train(s: &str, acronyms: Option<&Bound<'_, PyDict>>) -> String {
72 | let word_fn = |buf: &mut String, s: &str| -> fmt::Result {
73 | match get_acronym(s, acronyms) {
74 | Some(acronym) => write!(buf, "{}", acronym),
75 | None => lib::raw::write_title(buf, s),
76 | }
77 | };
78 |
79 | lib::raw::to_string(s, word_fn, lib::raw::delim_fn("-"))
80 | }
81 |
82 | /// Convert a string to 'lower case'.
83 | #[pyfunction]
84 | fn to_lower(s: &str) -> String {
85 | lib::to_lower(s)
86 | }
87 |
88 | /// Convert a string to 'Title Case'.
89 | #[pyfunction]
90 | #[pyo3(signature = (s, /, acronyms = None))]
91 | fn to_title(s: &str, acronyms: Option<&Bound<'_, PyDict>>) -> String {
92 | let word_fn = |buf: &mut String, s: &str| -> fmt::Result {
93 | match get_acronym(s, acronyms) {
94 | Some(acronym) => write!(buf, "{}", acronym),
95 | None => lib::raw::write_title(buf, s),
96 | }
97 | };
98 |
99 | lib::raw::to_string(s, word_fn, lib::raw::delim_fn(" "))
100 | }
101 |
102 | /// Convert a string to 'UPPER CASE'.
103 | #[pyfunction]
104 | fn to_upper(s: &str) -> String {
105 | lib::to_upper(s)
106 | }
107 |
108 | fn get_acronym<'py>(
109 | k: &str,
110 | acronyms: Option<&Bound<'py, PyDict>>,
111 | ) -> Option> {
112 | acronyms?.get_item(k.to_lowercase()).ok()??.extract().ok()
113 | }
114 |
115 | /// A case conversion library with Unicode support, implemented in Rust.
116 | #[pymodule]
117 | fn anycase(m: &Bound<'_, PyModule>) -> PyResult<()> {
118 | m.add_function(wrap_pyfunction!(to_camel, m)?)?;
119 | m.add_function(wrap_pyfunction!(to_pascal, m)?)?;
120 | m.add_function(wrap_pyfunction!(to_snake, m)?)?;
121 | m.add_function(wrap_pyfunction!(to_screaming_snake, m)?)?;
122 | m.add_function(wrap_pyfunction!(to_kebab, m)?)?;
123 | m.add_function(wrap_pyfunction!(to_screaming_kebab, m)?)?;
124 | m.add_function(wrap_pyfunction!(to_train, m)?)?;
125 | m.add_function(wrap_pyfunction!(to_lower, m)?)?;
126 | m.add_function(wrap_pyfunction!(to_title, m)?)?;
127 | m.add_function(wrap_pyfunction!(to_upper, m)?)?;
128 | Ok(())
129 | }
130 |
--------------------------------------------------------------------------------
/python/tests/test_anycase.py:
--------------------------------------------------------------------------------
1 | import json
2 | from dataclasses import dataclass
3 | from pathlib import Path
4 | import re
5 | import os
6 |
7 | import anycase
8 | import pytest
9 |
10 |
11 | COMMON = Path(__file__).parent.parent.parent / "testdata" / "common.json"
12 |
13 |
14 | @dataclass(kw_only=True)
15 | class TestCase:
16 | input: str
17 | snake: str
18 | camel: str
19 | pascal: str
20 | screaming_snake: str
21 | kebab: str
22 | screaming_kebab: str
23 | train: str
24 | lower: str
25 | title: str
26 | upper: str
27 |
28 |
29 | TESTS = [
30 | TestCase(
31 | input=case["input"],
32 | snake=case["snake"],
33 | camel=case["camel"],
34 | pascal=case["pascal"],
35 | screaming_snake=case["screaming_snake"],
36 | kebab=case["kebab"],
37 | screaming_kebab=case["screaming_kebab"],
38 | train=case["train"],
39 | lower=case["lower"],
40 | title=case["title"],
41 | upper=case["upper"],
42 | )
43 | for case in json.loads(open(COMMON).read())
44 | ]
45 |
46 |
47 | @pytest.mark.parametrize("case", TESTS, ids=lambda case: case.input or "empty")
48 | def test_common(case: TestCase):
49 | assert anycase.to_snake(case.input) == case.snake
50 | assert anycase.to_camel(case.input) == case.camel
51 | assert anycase.to_pascal(case.input) == case.pascal
52 | assert anycase.to_screaming_snake(case.input) == case.screaming_snake
53 | assert anycase.to_kebab(case.input) == case.kebab
54 | assert anycase.to_screaming_kebab(case.input) == case.screaming_kebab
55 | assert anycase.to_train(case.input) == case.train
56 | assert anycase.to_lower(case.input) == case.lower
57 | assert anycase.to_title(case.input) == case.title
58 | assert anycase.to_upper(case.input) == case.upper
59 |
60 |
61 | def test_to_camel_with_acronyms():
62 | assert (
63 | anycase.to_camel("xml_http_request", acronyms={"xml": "XML"})
64 | == "xmlHttpRequest"
65 | )
66 | assert (
67 | anycase.to_camel("xml_http_request", acronyms={"http": "HTTP"})
68 | == "xmlHTTPRequest"
69 | )
70 |
71 |
72 | def test_to_pascal_with_acronyms():
73 | assert (
74 | anycase.to_pascal("xml_http_request", acronyms={"xml": "XML"})
75 | == "XMLHttpRequest"
76 | )
77 | assert (
78 | anycase.to_pascal("xml_http_request", acronyms={"xml": "XML", "http": "HTTP"})
79 | == "XMLHTTPRequest"
80 | )
81 | assert (
82 | anycase.to_pascal("xml_http_request", acronyms={"xml": "XML", "http": "Http"})
83 | == "XMLHttpRequest"
84 | )
85 |
86 |
87 | def test_to_train_with_acronyms():
88 | assert (
89 | anycase.to_train("xml_http_request", acronyms={"xml": "XML"})
90 | == "XML-Http-Request"
91 | )
92 | assert (
93 | anycase.to_train("xml_http_request", acronyms={"xml": "XML", "http": "HTTP"})
94 | == "XML-HTTP-Request"
95 | )
96 | assert (
97 | anycase.to_train("xml_http_request", acronyms={"xml": "XML", "http": "Http"})
98 | == "XML-Http-Request"
99 | )
100 |
101 |
102 | def test_to_title_with_acronyms():
103 | assert (
104 | anycase.to_title("xml_http_request", acronyms={"xml": "XML"})
105 | == "XML Http Request"
106 | )
107 | assert (
108 | anycase.to_title("xml_http_request", acronyms={"xml": "XML", "http": "HTTP"})
109 | == "XML HTTP Request"
110 | )
111 | assert (
112 | anycase.to_title("xml_http_request", acronyms={"xml": "XML", "http": "Http"})
113 | == "XML Http Request"
114 | )
115 |
116 |
117 | def examples() -> list[tuple[str, str]]:
118 | pyi_file = os.path.join(os.path.dirname(__file__), "..", "anycase", "__init__.pyi")
119 | with open(pyi_file) as f:
120 | contents = f.read()
121 | examples = re.findall(r"^\s*>>> (.*)\n\s*(.*)$", contents, re.MULTILINE)
122 | assert len(examples) == 14
123 | return list(examples)
124 |
125 |
126 | @pytest.mark.parametrize("case", examples())
127 | def test_doc_example(case: tuple[str, str]):
128 | code, expected = case
129 | exec(f"""result = {code}\nassert result == {expected}""")
130 |
--------------------------------------------------------------------------------
/rust/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/rust/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "aho-corasick"
7 | version = "1.1.3"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
10 | dependencies = [
11 | "memchr",
12 | ]
13 |
14 | [[package]]
15 | name = "anes"
16 | version = "0.1.6"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
19 |
20 | [[package]]
21 | name = "anstyle"
22 | version = "1.0.10"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
25 |
26 | [[package]]
27 | name = "anycase"
28 | version = "0.1.0"
29 | dependencies = [
30 | "criterion",
31 | "serde",
32 | "serde_json",
33 | ]
34 |
35 | [[package]]
36 | name = "autocfg"
37 | version = "1.4.0"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
40 |
41 | [[package]]
42 | name = "bumpalo"
43 | version = "3.17.0"
44 | source = "registry+https://github.com/rust-lang/crates.io-index"
45 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
46 |
47 | [[package]]
48 | name = "cast"
49 | version = "0.3.0"
50 | source = "registry+https://github.com/rust-lang/crates.io-index"
51 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
52 |
53 | [[package]]
54 | name = "cfg-if"
55 | version = "1.0.0"
56 | source = "registry+https://github.com/rust-lang/crates.io-index"
57 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
58 |
59 | [[package]]
60 | name = "ciborium"
61 | version = "0.2.2"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
64 | dependencies = [
65 | "ciborium-io",
66 | "ciborium-ll",
67 | "serde",
68 | ]
69 |
70 | [[package]]
71 | name = "ciborium-io"
72 | version = "0.2.2"
73 | source = "registry+https://github.com/rust-lang/crates.io-index"
74 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
75 |
76 | [[package]]
77 | name = "ciborium-ll"
78 | version = "0.2.2"
79 | source = "registry+https://github.com/rust-lang/crates.io-index"
80 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
81 | dependencies = [
82 | "ciborium-io",
83 | "half",
84 | ]
85 |
86 | [[package]]
87 | name = "clap"
88 | version = "4.5.37"
89 | source = "registry+https://github.com/rust-lang/crates.io-index"
90 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
91 | dependencies = [
92 | "clap_builder",
93 | ]
94 |
95 | [[package]]
96 | name = "clap_builder"
97 | version = "4.5.37"
98 | source = "registry+https://github.com/rust-lang/crates.io-index"
99 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
100 | dependencies = [
101 | "anstyle",
102 | "clap_lex",
103 | ]
104 |
105 | [[package]]
106 | name = "clap_lex"
107 | version = "0.7.4"
108 | source = "registry+https://github.com/rust-lang/crates.io-index"
109 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
110 |
111 | [[package]]
112 | name = "criterion"
113 | version = "0.5.1"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
116 | dependencies = [
117 | "anes",
118 | "cast",
119 | "ciborium",
120 | "clap",
121 | "criterion-plot",
122 | "is-terminal",
123 | "itertools",
124 | "num-traits",
125 | "once_cell",
126 | "oorandom",
127 | "plotters",
128 | "rayon",
129 | "regex",
130 | "serde",
131 | "serde_derive",
132 | "serde_json",
133 | "tinytemplate",
134 | "walkdir",
135 | ]
136 |
137 | [[package]]
138 | name = "criterion-plot"
139 | version = "0.5.0"
140 | source = "registry+https://github.com/rust-lang/crates.io-index"
141 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
142 | dependencies = [
143 | "cast",
144 | "itertools",
145 | ]
146 |
147 | [[package]]
148 | name = "crossbeam-deque"
149 | version = "0.8.6"
150 | source = "registry+https://github.com/rust-lang/crates.io-index"
151 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
152 | dependencies = [
153 | "crossbeam-epoch",
154 | "crossbeam-utils",
155 | ]
156 |
157 | [[package]]
158 | name = "crossbeam-epoch"
159 | version = "0.9.18"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
162 | dependencies = [
163 | "crossbeam-utils",
164 | ]
165 |
166 | [[package]]
167 | name = "crossbeam-utils"
168 | version = "0.8.21"
169 | source = "registry+https://github.com/rust-lang/crates.io-index"
170 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
171 |
172 | [[package]]
173 | name = "crunchy"
174 | version = "0.2.3"
175 | source = "registry+https://github.com/rust-lang/crates.io-index"
176 | checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
177 |
178 | [[package]]
179 | name = "either"
180 | version = "1.15.0"
181 | source = "registry+https://github.com/rust-lang/crates.io-index"
182 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
183 |
184 | [[package]]
185 | name = "half"
186 | version = "2.6.0"
187 | source = "registry+https://github.com/rust-lang/crates.io-index"
188 | checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
189 | dependencies = [
190 | "cfg-if",
191 | "crunchy",
192 | ]
193 |
194 | [[package]]
195 | name = "hermit-abi"
196 | version = "0.5.0"
197 | source = "registry+https://github.com/rust-lang/crates.io-index"
198 | checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
199 |
200 | [[package]]
201 | name = "is-terminal"
202 | version = "0.4.16"
203 | source = "registry+https://github.com/rust-lang/crates.io-index"
204 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
205 | dependencies = [
206 | "hermit-abi",
207 | "libc",
208 | "windows-sys",
209 | ]
210 |
211 | [[package]]
212 | name = "itertools"
213 | version = "0.10.5"
214 | source = "registry+https://github.com/rust-lang/crates.io-index"
215 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
216 | dependencies = [
217 | "either",
218 | ]
219 |
220 | [[package]]
221 | name = "itoa"
222 | version = "1.0.15"
223 | source = "registry+https://github.com/rust-lang/crates.io-index"
224 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
225 |
226 | [[package]]
227 | name = "js-sys"
228 | version = "0.3.77"
229 | source = "registry+https://github.com/rust-lang/crates.io-index"
230 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
231 | dependencies = [
232 | "once_cell",
233 | "wasm-bindgen",
234 | ]
235 |
236 | [[package]]
237 | name = "libc"
238 | version = "0.2.172"
239 | source = "registry+https://github.com/rust-lang/crates.io-index"
240 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
241 |
242 | [[package]]
243 | name = "log"
244 | version = "0.4.27"
245 | source = "registry+https://github.com/rust-lang/crates.io-index"
246 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
247 |
248 | [[package]]
249 | name = "memchr"
250 | version = "2.7.4"
251 | source = "registry+https://github.com/rust-lang/crates.io-index"
252 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
253 |
254 | [[package]]
255 | name = "num-traits"
256 | version = "0.2.19"
257 | source = "registry+https://github.com/rust-lang/crates.io-index"
258 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
259 | dependencies = [
260 | "autocfg",
261 | ]
262 |
263 | [[package]]
264 | name = "once_cell"
265 | version = "1.21.3"
266 | source = "registry+https://github.com/rust-lang/crates.io-index"
267 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
268 |
269 | [[package]]
270 | name = "oorandom"
271 | version = "11.1.5"
272 | source = "registry+https://github.com/rust-lang/crates.io-index"
273 | checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
274 |
275 | [[package]]
276 | name = "plotters"
277 | version = "0.3.7"
278 | source = "registry+https://github.com/rust-lang/crates.io-index"
279 | checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
280 | dependencies = [
281 | "num-traits",
282 | "plotters-backend",
283 | "plotters-svg",
284 | "wasm-bindgen",
285 | "web-sys",
286 | ]
287 |
288 | [[package]]
289 | name = "plotters-backend"
290 | version = "0.3.7"
291 | source = "registry+https://github.com/rust-lang/crates.io-index"
292 | checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
293 |
294 | [[package]]
295 | name = "plotters-svg"
296 | version = "0.3.7"
297 | source = "registry+https://github.com/rust-lang/crates.io-index"
298 | checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
299 | dependencies = [
300 | "plotters-backend",
301 | ]
302 |
303 | [[package]]
304 | name = "proc-macro2"
305 | version = "1.0.94"
306 | source = "registry+https://github.com/rust-lang/crates.io-index"
307 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
308 | dependencies = [
309 | "unicode-ident",
310 | ]
311 |
312 | [[package]]
313 | name = "quote"
314 | version = "1.0.40"
315 | source = "registry+https://github.com/rust-lang/crates.io-index"
316 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
317 | dependencies = [
318 | "proc-macro2",
319 | ]
320 |
321 | [[package]]
322 | name = "rayon"
323 | version = "1.10.0"
324 | source = "registry+https://github.com/rust-lang/crates.io-index"
325 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
326 | dependencies = [
327 | "either",
328 | "rayon-core",
329 | ]
330 |
331 | [[package]]
332 | name = "rayon-core"
333 | version = "1.12.1"
334 | source = "registry+https://github.com/rust-lang/crates.io-index"
335 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
336 | dependencies = [
337 | "crossbeam-deque",
338 | "crossbeam-utils",
339 | ]
340 |
341 | [[package]]
342 | name = "regex"
343 | version = "1.11.1"
344 | source = "registry+https://github.com/rust-lang/crates.io-index"
345 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
346 | dependencies = [
347 | "aho-corasick",
348 | "memchr",
349 | "regex-automata",
350 | "regex-syntax",
351 | ]
352 |
353 | [[package]]
354 | name = "regex-automata"
355 | version = "0.4.9"
356 | source = "registry+https://github.com/rust-lang/crates.io-index"
357 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
358 | dependencies = [
359 | "aho-corasick",
360 | "memchr",
361 | "regex-syntax",
362 | ]
363 |
364 | [[package]]
365 | name = "regex-syntax"
366 | version = "0.8.5"
367 | source = "registry+https://github.com/rust-lang/crates.io-index"
368 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
369 |
370 | [[package]]
371 | name = "rustversion"
372 | version = "1.0.20"
373 | source = "registry+https://github.com/rust-lang/crates.io-index"
374 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
375 |
376 | [[package]]
377 | name = "ryu"
378 | version = "1.0.20"
379 | source = "registry+https://github.com/rust-lang/crates.io-index"
380 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
381 |
382 | [[package]]
383 | name = "same-file"
384 | version = "1.0.6"
385 | source = "registry+https://github.com/rust-lang/crates.io-index"
386 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
387 | dependencies = [
388 | "winapi-util",
389 | ]
390 |
391 | [[package]]
392 | name = "serde"
393 | version = "1.0.219"
394 | source = "registry+https://github.com/rust-lang/crates.io-index"
395 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
396 | dependencies = [
397 | "serde_derive",
398 | ]
399 |
400 | [[package]]
401 | name = "serde_derive"
402 | version = "1.0.219"
403 | source = "registry+https://github.com/rust-lang/crates.io-index"
404 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
405 | dependencies = [
406 | "proc-macro2",
407 | "quote",
408 | "syn",
409 | ]
410 |
411 | [[package]]
412 | name = "serde_json"
413 | version = "1.0.140"
414 | source = "registry+https://github.com/rust-lang/crates.io-index"
415 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
416 | dependencies = [
417 | "itoa",
418 | "memchr",
419 | "ryu",
420 | "serde",
421 | ]
422 |
423 | [[package]]
424 | name = "syn"
425 | version = "2.0.100"
426 | source = "registry+https://github.com/rust-lang/crates.io-index"
427 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
428 | dependencies = [
429 | "proc-macro2",
430 | "quote",
431 | "unicode-ident",
432 | ]
433 |
434 | [[package]]
435 | name = "tinytemplate"
436 | version = "1.2.1"
437 | source = "registry+https://github.com/rust-lang/crates.io-index"
438 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
439 | dependencies = [
440 | "serde",
441 | "serde_json",
442 | ]
443 |
444 | [[package]]
445 | name = "unicode-ident"
446 | version = "1.0.18"
447 | source = "registry+https://github.com/rust-lang/crates.io-index"
448 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
449 |
450 | [[package]]
451 | name = "walkdir"
452 | version = "2.5.0"
453 | source = "registry+https://github.com/rust-lang/crates.io-index"
454 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
455 | dependencies = [
456 | "same-file",
457 | "winapi-util",
458 | ]
459 |
460 | [[package]]
461 | name = "wasm-bindgen"
462 | version = "0.2.100"
463 | source = "registry+https://github.com/rust-lang/crates.io-index"
464 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
465 | dependencies = [
466 | "cfg-if",
467 | "once_cell",
468 | "rustversion",
469 | "wasm-bindgen-macro",
470 | ]
471 |
472 | [[package]]
473 | name = "wasm-bindgen-backend"
474 | version = "0.2.100"
475 | source = "registry+https://github.com/rust-lang/crates.io-index"
476 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
477 | dependencies = [
478 | "bumpalo",
479 | "log",
480 | "proc-macro2",
481 | "quote",
482 | "syn",
483 | "wasm-bindgen-shared",
484 | ]
485 |
486 | [[package]]
487 | name = "wasm-bindgen-macro"
488 | version = "0.2.100"
489 | source = "registry+https://github.com/rust-lang/crates.io-index"
490 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
491 | dependencies = [
492 | "quote",
493 | "wasm-bindgen-macro-support",
494 | ]
495 |
496 | [[package]]
497 | name = "wasm-bindgen-macro-support"
498 | version = "0.2.100"
499 | source = "registry+https://github.com/rust-lang/crates.io-index"
500 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
501 | dependencies = [
502 | "proc-macro2",
503 | "quote",
504 | "syn",
505 | "wasm-bindgen-backend",
506 | "wasm-bindgen-shared",
507 | ]
508 |
509 | [[package]]
510 | name = "wasm-bindgen-shared"
511 | version = "0.2.100"
512 | source = "registry+https://github.com/rust-lang/crates.io-index"
513 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
514 | dependencies = [
515 | "unicode-ident",
516 | ]
517 |
518 | [[package]]
519 | name = "web-sys"
520 | version = "0.3.77"
521 | source = "registry+https://github.com/rust-lang/crates.io-index"
522 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
523 | dependencies = [
524 | "js-sys",
525 | "wasm-bindgen",
526 | ]
527 |
528 | [[package]]
529 | name = "winapi-util"
530 | version = "0.1.9"
531 | source = "registry+https://github.com/rust-lang/crates.io-index"
532 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
533 | dependencies = [
534 | "windows-sys",
535 | ]
536 |
537 | [[package]]
538 | name = "windows-sys"
539 | version = "0.59.0"
540 | source = "registry+https://github.com/rust-lang/crates.io-index"
541 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
542 | dependencies = [
543 | "windows-targets",
544 | ]
545 |
546 | [[package]]
547 | name = "windows-targets"
548 | version = "0.52.6"
549 | source = "registry+https://github.com/rust-lang/crates.io-index"
550 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
551 | dependencies = [
552 | "windows_aarch64_gnullvm",
553 | "windows_aarch64_msvc",
554 | "windows_i686_gnu",
555 | "windows_i686_gnullvm",
556 | "windows_i686_msvc",
557 | "windows_x86_64_gnu",
558 | "windows_x86_64_gnullvm",
559 | "windows_x86_64_msvc",
560 | ]
561 |
562 | [[package]]
563 | name = "windows_aarch64_gnullvm"
564 | version = "0.52.6"
565 | source = "registry+https://github.com/rust-lang/crates.io-index"
566 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
567 |
568 | [[package]]
569 | name = "windows_aarch64_msvc"
570 | version = "0.52.6"
571 | source = "registry+https://github.com/rust-lang/crates.io-index"
572 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
573 |
574 | [[package]]
575 | name = "windows_i686_gnu"
576 | version = "0.52.6"
577 | source = "registry+https://github.com/rust-lang/crates.io-index"
578 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
579 |
580 | [[package]]
581 | name = "windows_i686_gnullvm"
582 | version = "0.52.6"
583 | source = "registry+https://github.com/rust-lang/crates.io-index"
584 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
585 |
586 | [[package]]
587 | name = "windows_i686_msvc"
588 | version = "0.52.6"
589 | source = "registry+https://github.com/rust-lang/crates.io-index"
590 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
591 |
592 | [[package]]
593 | name = "windows_x86_64_gnu"
594 | version = "0.52.6"
595 | source = "registry+https://github.com/rust-lang/crates.io-index"
596 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
597 |
598 | [[package]]
599 | name = "windows_x86_64_gnullvm"
600 | version = "0.52.6"
601 | source = "registry+https://github.com/rust-lang/crates.io-index"
602 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
603 |
604 | [[package]]
605 | name = "windows_x86_64_msvc"
606 | version = "0.52.6"
607 | source = "registry+https://github.com/rust-lang/crates.io-index"
608 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
609 |
--------------------------------------------------------------------------------
/rust/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "anycase"
3 | version = "0.1.0"
4 | authors = ["Ross MacArthur "]
5 | edition = "2018"
6 | rust-version = "1.56.0"
7 | description = "a case conversion library for Rust"
8 | readme = "README.md"
9 | repository = "https://github.com/rossmacarthur/anycase"
10 | license = "MIT OR Apache-2.0"
11 | keywords = ["case", "snake", "camel", "pascal", "unicode"]
12 | categories = ["text-processing", "no-std"]
13 | include = ["src/**/*", "LICENSE-*", "README.md"]
14 |
15 | [package.metadata.docs.rs]
16 | rustdoc-args = ["--cfg", "docsrs"]
17 |
18 | [features]
19 | default = ["std"]
20 | std = ["alloc"]
21 | alloc = []
22 |
23 | [dev-dependencies]
24 | criterion = "0.5.1"
25 | serde = { version = "1.0.0", features = ["derive"] }
26 | serde_json = "1.0.0"
27 |
28 | [[bench]]
29 | name = "benches"
30 | harness = false
31 |
--------------------------------------------------------------------------------
/rust/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/rust/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/rust/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # anycase
4 |
5 | [](https://crates.io/crates/anycase)
6 | [](https://docs.rs/anycase)
7 | [](https://github.com/rossmacarthur/anycase/actions/workflows/rust.yaml)
8 |
9 | 💼 A case conversion library for Rust.
10 |
11 | ## 🚀 Getting started
12 |
13 | First, add the `anycase` crate to your Cargo manifest.
14 |
15 | ```sh
16 | cargo add anycase
17 | ```
18 |
19 | Then you can use the `as_` function to get a `Display` type.
20 |
21 | ```rust
22 | let s = format!("snake case: {}", anycase::as_snake("Hello world!"));
23 | assert_eq!(s, "snake case: hello_world");
24 | ```
25 |
26 | Alternatively, you can use the `to_` function to get a `String`.
27 |
28 | ```rust
29 | let s = anycase::to_snake("Hello world!");
30 | assert_eq!(s, "hello_world");
31 | ```
32 |
33 | ## 🤸 Usage
34 |
35 | The `anycase` crate provides a set of functions to convert strings between
36 | different case styles. The following cases are available.
37 |
38 | Given an input of `Hello world!`:
39 |
40 | - [`as_camel`][as_camel] displays `helloWorld`
41 | - [`as_pascal`][as_pascal] displays `HelloWorld`
42 | - [`as_snake`][as_snake] displays `hello_world`
43 | - [`as_screaming_snake`][as_screaming_snake] displays `HELLO_WORLD`
44 | - [`as_kebab`][as_kebab] displays `hello-world`
45 | - [`as_screaming_kebab`][as_screaming_kebab] displays `HELLO_WORLD`
46 | - [`as_train`][as_train] displays `Hello-World`
47 | - [`as_lower`][as_lower] displays `hello world`
48 | - [`as_title`][as_title] displays `Hello World`
49 | - [`as_upper`][as_upper] displays `HELLO WORLD`
50 |
51 | For all of the above functions, you can use the `to_` variant to get a
52 | `String` instead of a `Display` type.
53 |
54 | Additionally, the crate provides the `raw` module containing the raw
55 | functions which can be used to implement custom case conversion functions.
56 |
57 | ```rust
58 | use anycase::raw;
59 |
60 | let input = "Hello world!";
61 | let output = raw::to_string(input, raw::write_upper, raw::delim_fn("."));
62 | assert_eq!(output, "HELLO.WORLD");
63 | ```
64 |
65 | See the [module level documentation][crate::raw] for more details.
66 |
67 | ## How does it work?
68 |
69 | This implementation divides the input string into words and applies a “word
70 | function” to each word and calls a “delimiter function” for each word
71 | boundary (the space between words).
72 |
73 | Word boundaries are defined as follows:
74 |
75 | - A set of consecutive non-letter/number/symbol e.g. `foo _bar` is two words
76 | `foo` and `bar`.
77 | - A transition from a lowercase letter to an uppercase letter e.g. `fooBar`
78 | is two words `foo` and `Bar`.
79 | - The second last uppercase letter in a word with multiple uppercase letters
80 | e.g. `FOOBar` is two words `FOO` and `Bar`.
81 |
82 | The following `char` methods are used in the above conditions:
83 |
84 | - [`char::is_alphanumeric`][char::is_alphanumeric] is used to determine if a character is a
85 | letter/number/symbol
86 | - [`char::is_lowercase`][char::is_lowercase] is used to determine if a character is a lowercase
87 | letter
88 | - [`char::is_uppercase`][char::is_uppercase] is used to determine if a character is an uppercase
89 | letter
90 |
91 | ## Features
92 |
93 | This crate is designed to be `no_std` compatible. This is made possible by
94 | disabling all default features. The following features are available:
95 |
96 | - **`std`** *(enabled by default)* — Currently only enables the `alloc`
97 | feature but is here to allow for forward compatibility with any
98 | [`std`]\-only features.
99 |
100 | - **`alloc`** — Links the [`alloc`] crate and enables the use of `String`
101 | functions.
102 |
103 | ## MSRV
104 |
105 | The minimum supported Rust version (MSRV) is 1.56.0. The policy of this
106 | crate is to only increase the MSRV in a breaking release.
107 |
108 | [`std`]: http://doc.rust-lang.org/std/
109 | [`alloc`]: http://doc.rust-lang.org/alloc/
110 |
111 | ## License
112 |
113 | This project is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
114 |
115 | See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) for details.
116 |
117 |
118 | [as_camel]: https://docs.rs/anycase/latest/anycase/fn.as_camel.html
119 | [as_kebab]: https://docs.rs/anycase/latest/anycase/fn.as_kebab.html
120 | [as_lower]: https://docs.rs/anycase/latest/anycase/fn.as_lower.html
121 | [as_pascal]: https://docs.rs/anycase/latest/anycase/fn.as_pascal.html
122 | [as_screaming_kebab]: https://docs.rs/anycase/latest/anycase/fn.as_screaming_kebab.html
123 | [as_screaming_snake]: https://docs.rs/anycase/latest/anycase/fn.as_screaming_snake.html
124 | [as_snake]: https://docs.rs/anycase/latest/anycase/fn.as_snake.html
125 | [as_title]: https://docs.rs/anycase/latest/anycase/fn.as_title.html
126 | [as_train]: https://docs.rs/anycase/latest/anycase/fn.as_train.html
127 | [as_upper]: https://docs.rs/anycase/latest/anycase/fn.as_upper.html
128 | [char::is_alphanumeric]: https://doc.rust-lang.org/stable/core/primitive.char.html#method.is_alphanumeric
129 | [char::is_lowercase]: https://doc.rust-lang.org/stable/core/primitive.char.html#method.is_lowercase
130 | [char::is_uppercase]: https://doc.rust-lang.org/stable/core/primitive.char.html#method.is_uppercase
131 | [crate::raw]: https://doc.rs/anycase/latest/anycase/raw/index.html
132 |
--------------------------------------------------------------------------------
/rust/benches/benches.rs:
--------------------------------------------------------------------------------
1 | use criterion::{criterion_group, criterion_main, Criterion};
2 |
3 | criterion_main! { benches }
4 |
5 | criterion_group! { benches, bench_simple }
6 |
7 | pub fn bench_simple(c: &mut Criterion) {
8 | let mut g = c.benchmark_group("simple");
9 |
10 | let input = "thisIsACamelCaseString".repeat(1000);
11 | let expect = "this_is_a_camel_case_string".repeat(1000);
12 |
13 | assert_eq!(anycase::to_snake(&input), expect);
14 | assert_eq!(anycase::as_snake(&input).to_string(), expect);
15 |
16 | g.bench_with_input("string", &input, |b, input| {
17 | b.iter(|| anycase::to_snake(input));
18 | });
19 |
20 | g.bench_with_input("fmt", &input, |b, input| {
21 | b.iter(|| anycase::as_snake(input).to_string());
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/rust/onedoc.toml:
--------------------------------------------------------------------------------
1 | [badges]
2 | github_workflow = { label = "build", name = "rust" }
3 |
4 | [links]
5 | "crate::raw" = "https://doc.rs/anycase/latest/anycase/raw/index.html"
6 | "char::is_alphanumeric" = "https://doc.rust-lang.org/stable/core/primitive.char.html#method.is_alphanumeric"
7 | "char::is_lowercase" = "https://doc.rust-lang.org/stable/core/primitive.char.html#method.is_lowercase"
8 | "char::is_uppercase" = "https://doc.rust-lang.org/stable/core/primitive.char.html#method.is_uppercase"
9 | "as_camel" = "https://docs.rs/anycase/latest/anycase/fn.as_camel.html"
10 | "as_pascal" = "https://docs.rs/anycase/latest/anycase/fn.as_pascal.html"
11 | "as_snake" = "https://docs.rs/anycase/latest/anycase/fn.as_snake.html"
12 | "as_screaming_snake" = "https://docs.rs/anycase/latest/anycase/fn.as_screaming_snake.html"
13 | "as_kebab" = "https://docs.rs/anycase/latest/anycase/fn.as_kebab.html"
14 | "as_screaming_kebab" = "https://docs.rs/anycase/latest/anycase/fn.as_screaming_kebab.html"
15 | "as_train" = "https://docs.rs/anycase/latest/anycase/fn.as_train.html"
16 | "as_lower" = "https://docs.rs/anycase/latest/anycase/fn.as_lower.html"
17 | "as_title" = "https://docs.rs/anycase/latest/anycase/fn.as_title.html"
18 | "as_upper" = "https://docs.rs/anycase/latest/anycase/fn.as_upper.html"
19 |
--------------------------------------------------------------------------------
/rust/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! 💼 A case conversion library for Rust.
2 | //!
3 | //! # 🚀 Getting started
4 | //!
5 | //! First, add the `anycase` crate to your Cargo manifest.
6 | //!
7 | //! ```sh
8 | //! cargo add anycase
9 | //! ```
10 | //!
11 | //! Then you can use the `as_` function to get a [`Display`] type.
12 | //! ```
13 | //! let s = format!("snake case: {}", anycase::as_snake("Hello world!"));
14 | //! assert_eq!(s, "snake case: hello_world");
15 | //! ```
16 | //!
17 | //! Alternatively, you can use the `to_` function to get a `String`.
18 | //! ```
19 | //! let s = anycase::to_snake("Hello world!");
20 | //! assert_eq!(s, "hello_world");
21 | //! ```
22 | //!
23 | //! # 🤸 Usage
24 | //!
25 | //! The `anycase` crate provides a set of functions to convert strings between
26 | //! different case styles. The following cases are available.
27 | //!
28 | //! Given an input of `Hello world!`:
29 | //!
30 | //! - [`as_camel`] displays `helloWorld`
31 | //! - [`as_pascal`] displays `HelloWorld`
32 | //! - [`as_snake`] displays `hello_world`
33 | //! - [`as_screaming_snake`] displays `HELLO_WORLD`
34 | //! - [`as_kebab`] displays `hello-world`
35 | //! - [`as_screaming_kebab`] displays `HELLO_WORLD`
36 | //! - [`as_train`] displays `Hello-World`
37 | //! - [`as_lower`] displays `hello world`
38 | //! - [`as_title`] displays `Hello World`
39 | //! - [`as_upper`] displays `HELLO WORLD`
40 | //!
41 | //! For all of the above functions, you can use the `to_` variant to get a
42 | //! `String` instead of a [`Display`] type.
43 | //!
44 | //! Additionally, the crate provides the [`raw`] module containing the raw
45 | //! functions which can be used to implement custom case conversion functions.
46 | //!
47 | //! ```
48 | //! use anycase::raw;
49 | //!
50 | //! let input = "Hello world!";
51 | //! let output = raw::to_string(input, raw::write_upper, raw::delim_fn("."));
52 | //! assert_eq!(output, "HELLO.WORLD");
53 | //! ```
54 | //!
55 | //! See the [module level documentation](crate::raw) for more details.
56 | //!
57 | //! # How does it work?
58 | //!
59 | //! This implementation divides the input string into words and applies a "word
60 | //! function" to each word and calls a "delimiter function" for each word
61 | //! boundary (the space between words).
62 | //!
63 | //! Word boundaries are defined as follows:
64 | //! - A set of consecutive non-letter/number/symbol e.g. `foo _bar` is two words
65 | //! `foo` and `bar`.
66 | //! - A transition from a lowercase letter to an uppercase letter e.g. `fooBar`
67 | //! is two words `foo` and `Bar`.
68 | //! - The second last uppercase letter in a word with multiple uppercase letters
69 | //! e.g. `FOOBar` is two words `FOO` and `Bar`.
70 | //!
71 | //! The following `char` methods are used in the above conditions:
72 | //!
73 | //! - [`char::is_alphanumeric`] is used to determine if a character is a
74 | //! letter/number/symbol
75 | //! - [`char::is_lowercase`] is used to determine if a character is a lowercase
76 | //! letter
77 | //! - [`char::is_uppercase`] is used to determine if a character is an uppercase
78 | //! letter
79 | //!
80 | //! # Features
81 | //!
82 | //! This crate is designed to be `no_std` compatible. This is made possible by
83 | //! disabling all default features. The following features are available:
84 | //!
85 | //! - **`std`** _(enabled by default)_ — Currently only enables the `alloc`
86 | //! feature but is here to allow for forward compatibility with any
87 | //! [`std`]-only features.
88 | //!
89 | //! - **`alloc`** — Links the [`alloc`] crate and enables the use of `String`
90 | //! functions.
91 | //!
92 | //! # MSRV
93 | //!
94 | //! The minimum supported Rust version (MSRV) is 1.56.0. The policy of this
95 | //! crate is to only increase the MSRV in a breaking release.
96 | //!
97 | //! [`Display`]: core::fmt::Display
98 | //! [`alloc`]: http://doc.rust-lang.org/alloc/
99 | //! [`std`]: http://doc.rust-lang.org/std/
100 |
101 | #![no_std]
102 | #![cfg_attr(docsrs, feature(doc_cfg))]
103 |
104 | #[cfg(feature = "alloc")]
105 | extern crate alloc;
106 |
107 | pub mod raw;
108 |
109 | use core::fmt;
110 |
111 | #[cfg(feature = "alloc")]
112 | use alloc::string::String;
113 | #[cfg(feature = "alloc")]
114 | use alloc::string::ToString;
115 |
116 | macro_rules! as_case {
117 | { $s:ident, $wf:expr, $df:expr } => {
118 | struct AsCase(S);
119 |
120 | impl> fmt::Display for AsCase {
121 | #[inline]
122 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 | raw::transform(self.0.as_ref(), f, $wf, $df)
124 | }
125 | }
126 |
127 | AsCase($s)
128 | };
129 | }
130 |
131 | /// Display a string as 'camelCase'.
132 | pub fn as_camel>(s: S) -> impl fmt::Display {
133 | as_case! { s, raw::write_camel_fn(), raw::delim_none }
134 | }
135 | /// Transforms a string to 'camelCase'.
136 | #[cfg(feature = "alloc")]
137 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
138 | pub fn to_camel>(s: S) -> String {
139 | as_camel(s).to_string()
140 | }
141 |
142 | /// Display a string as 'PascalCase'.
143 | pub fn as_pascal>(s: S) -> impl fmt::Display {
144 | as_case! { s, raw::write_title, raw::delim_none }
145 | }
146 | /// Transforms a string to 'PascalCase'.
147 | #[cfg(feature = "alloc")]
148 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
149 | pub fn to_pascal>(s: S) -> String {
150 | as_pascal(s).to_string()
151 | }
152 |
153 | /// Display a string as 'snake_case'.
154 | pub fn as_snake>(s: S) -> impl fmt::Display {
155 | as_case! { s, raw::write_lower, raw::delim_fn("_") }
156 | }
157 | /// Transforms a string to 'snake_case'.
158 | #[cfg(feature = "alloc")]
159 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
160 | pub fn to_snake>(s: S) -> String {
161 | as_snake(s).to_string()
162 | }
163 |
164 | /// Display a string as 'SCREAMING_SNAKE_CASE'.
165 | pub fn as_screaming_snake>(s: S) -> impl fmt::Display {
166 | as_case! { s, raw::write_upper, raw::delim_fn("_") }
167 | }
168 | /// Transforms a string to 'SCREAMING_SNAKE_CASE'.
169 | #[cfg(feature = "alloc")]
170 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
171 | pub fn to_screaming_snake>(s: S) -> String {
172 | as_screaming_snake(s).to_string()
173 | }
174 |
175 | /// Display a string as 'kebab-case'.
176 | pub fn as_kebab>(s: S) -> impl fmt::Display {
177 | as_case! { s, raw::write_lower, raw::delim_fn("-") }
178 | }
179 | /// Transforms a string to 'kebab-case'.
180 | #[cfg(feature = "alloc")]
181 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
182 | pub fn to_kebab>(s: S) -> String {
183 | as_kebab(s).to_string()
184 | }
185 |
186 | /// Display a string as 'SCREAMING-KEBAB-CASE'.
187 | pub fn as_screaming_kebab>(s: S) -> impl fmt::Display {
188 | as_case! { s, raw::write_upper, raw::delim_fn("-") }
189 | }
190 | /// Transforms a string to 'SCREAMING-KEBAB-CASE'.
191 | #[cfg(feature = "alloc")]
192 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
193 | pub fn to_screaming_kebab>(s: S) -> String {
194 | as_screaming_kebab(s).to_string()
195 | }
196 |
197 | /// Display a string as 'Train-Case'.
198 | pub fn as_train>(s: S) -> impl fmt::Display {
199 | as_case! { s, raw::write_title, raw::delim_fn("-") }
200 | }
201 | /// Transforms a string to 'Train-Case'.
202 | #[cfg(feature = "alloc")]
203 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
204 | pub fn to_train>(s: S) -> String {
205 | as_train(s).to_string()
206 | }
207 |
208 | /// Display a string as 'lower case'.
209 | pub fn as_lower>(s: S) -> impl fmt::Display {
210 | as_case! { s, raw::write_lower, raw::delim_fn(" ") }
211 | }
212 | /// Transforms a string to 'lower case'.
213 | #[cfg(feature = "alloc")]
214 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
215 | pub fn to_lower>(s: S) -> String {
216 | as_lower(s).to_string()
217 | }
218 |
219 | /// Display a string as 'Title Case'.
220 | pub fn as_title>(s: S) -> impl fmt::Display {
221 | as_case! { s, raw::write_title, raw::delim_fn(" ") }
222 | }
223 | /// Transforms a string to 'Title Case'.
224 | #[cfg(feature = "alloc")]
225 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
226 | pub fn to_title>(s: S) -> String {
227 | as_title(s).to_string()
228 | }
229 |
230 | /// Display a string as 'UPPER CASE'.
231 | pub fn as_upper>(s: S) -> impl fmt::Display {
232 | as_case! { s, raw::write_upper, raw::delim_fn(" ") }
233 | }
234 | /// Transforms a string to 'UPPER CASE'.
235 | #[cfg(feature = "alloc")]
236 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
237 | pub fn to_upper>(s: S) -> String {
238 | as_upper(s).to_string()
239 | }
240 |
--------------------------------------------------------------------------------
/rust/src/raw.rs:
--------------------------------------------------------------------------------
1 | //! This module provides raw functions for transforming strings into different
2 | //! cases. It is used by the functions in the root of this crate.
3 | //!
4 | //! The main function is [`transform`], which takes a string and a buffer and
5 | //! transforms the string into the buffer using the provided "word function" and
6 | //! "delimiter function". The word function is called for each word in the
7 | //! string, and the delimiter function is called for each delimiter between
8 | //! words. For convenience [`to_string`] is provided, which is a thin wrapper
9 | //! around [`transform`] that returns a new `String` instead of writing to a
10 | //! buffer. Additionally, there are several pre-defined word functions and
11 | //! delimiter functions that can be used with [`transform`] and [`to_string`].
12 | //!
13 | //! **Word functions**
14 | //!
15 | //! - [`write_lower`]: converts the word to lowercase
16 | //! - [`write_upper`]: converts the word to uppercase
17 | //! - [`write_title`]: converts the first character (unicode code point) of the
18 | //! word to uppercase and the rest to lowercase
19 | //!
20 | //! **Delimiter functions**
21 | //!
22 | //! - [`delim_none`]: does nothing (no delimiter)
23 | //!
24 | //! - [`delim_fn`]: returns a "delimiter function" that writes the given
25 | //! delimiter to the buffer
26 | //!
27 | //!
28 | //! # Examples
29 | //!
30 | //! In this example we convert a string to `SCREAMING.DOT.CASE` a custom
31 | //! conversion that is not provided by this crate.
32 | //!
33 | //! ```
34 | //! use anycase::raw;
35 | //!
36 | //! let input = "Hello world!";
37 | //! let output = raw::to_string(input, raw::write_upper, raw::delim_fn("."));
38 | //! assert_eq!(output, "HELLO.WORLD");
39 | //! ```
40 |
41 | use core::fmt;
42 | use core::fmt::Write;
43 |
44 | #[cfg(feature = "alloc")]
45 | use alloc::string::String;
46 |
47 | /// Reconstructs the provided string, `s` as a new string using the given "word
48 | /// function" and "delimiter function".
49 | ///
50 | /// See the [module level documentation](crate::raw) for more details.
51 | #[cfg(feature = "alloc")]
52 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
53 | pub fn to_string(s: &str, word_fn: WF, delim_fn: DF) -> String
54 | where
55 | WF: FnMut(&mut String, &str) -> fmt::Result,
56 | DF: FnMut(&mut String) -> fmt::Result,
57 | {
58 | let mut buf = String::with_capacity(s.len());
59 | transform(s, &mut buf, word_fn, delim_fn).expect("fmt error");
60 | buf
61 | }
62 |
63 | #[derive(Copy, Clone, PartialEq)]
64 | enum State {
65 | Unknown,
66 | Delims,
67 | Lower,
68 | Upper,
69 | }
70 |
71 | /// Reconstructs the provided string, `s`, into the given buffer, `buf`, using
72 | /// the given "word function" and "delimiter function".
73 | ///
74 | /// See the [module level documentation](crate::raw) for more details.
75 | pub fn transform(s: &str, buf: &mut B, mut word_fn: WF, mut delim_fn: DF) -> fmt::Result
76 | where
77 | B: Write,
78 | WF: FnMut(&mut B, &str) -> fmt::Result,
79 | DF: FnMut(&mut B) -> fmt::Result,
80 | {
81 | // when we are on the first word
82 | let mut first = true;
83 | // the byte index of the start of the current word
84 | let mut w0 = 0;
85 | // the byte index of the end of the current word
86 | let mut w1 = None;
87 | // the current state of the word boundary machine
88 | let mut state = State::Unknown;
89 |
90 | let mut write = |w0: usize, w1: usize| -> fmt::Result {
91 | if w1 - w0 > 0 {
92 | if first {
93 | first = false;
94 | } else {
95 | delim_fn(buf)?;
96 | }
97 | word_fn(buf, &s[w0..w1])?;
98 | }
99 | Ok(())
100 | };
101 |
102 | let mut iter = s.char_indices().peekable();
103 |
104 | while let Some((i, c)) = iter.next() {
105 | if !c.is_alphanumeric() {
106 | state = State::Delims;
107 | w1 = w1.or(Some(i));
108 | continue;
109 | }
110 |
111 | let is_lower = c.is_lowercase();
112 | let is_upper = c.is_uppercase();
113 |
114 | match state {
115 | State::Delims => {
116 | if let Some(w1) = w1 {
117 | write(w0, w1)?;
118 | }
119 | w0 = i;
120 | w1 = None;
121 | }
122 | State::Lower if is_upper => {
123 | write(w0, i)?;
124 | w0 = i;
125 | }
126 | State::Upper
127 | if is_upper && matches!(iter.peek(), Some((_, c2)) if c2.is_lowercase()) =>
128 | {
129 | write(w0, i)?;
130 | w0 = i;
131 | }
132 | _ => {}
133 | }
134 |
135 | if is_lower {
136 | state = State::Lower;
137 | } else if is_upper {
138 | state = State::Upper;
139 | } else if state == State::Delims {
140 | state = State::Unknown;
141 | }
142 | }
143 |
144 | match state {
145 | State::Delims => {
146 | if let Some(w1) = w1 {
147 | write(w0, w1)?;
148 | }
149 | }
150 | _ => write(w0, s.len())?,
151 | }
152 |
153 | Ok(())
154 | }
155 |
156 | /// A "word function" that converts the word to lowercase.
157 | pub fn write_lower(buf: &mut W, s: &str) -> fmt::Result {
158 | for c in s.chars() {
159 | write!(buf, "{}", c.to_lowercase())?
160 | }
161 | Ok(())
162 | }
163 |
164 | /// A "word function" that converts the word to uppercase.
165 | pub fn write_upper(buf: &mut W, s: &str) -> fmt::Result {
166 | for c in s.chars() {
167 | write!(buf, "{}", c.to_uppercase())?
168 | }
169 | Ok(())
170 | }
171 |
172 | /// A "word function" that converts the first character of the word to uppercase
173 | /// and the rest to lowercase.
174 | pub fn write_title(buf: &mut W, s: &str) -> fmt::Result {
175 | let mut iter = s.chars();
176 | if let Some(c) = iter.next() {
177 | write!(buf, "{}", c.to_uppercase())?;
178 | for c in iter {
179 | write!(buf, "{}", c.to_lowercase())?;
180 | }
181 | }
182 | Ok(())
183 | }
184 |
185 | /// Returns a new stateful "word function" that writes the first word as
186 | /// lowercase and the rest as title case.
187 | pub fn write_camel_fn() -> impl FnMut(&mut W, &str) -> fmt::Result {
188 | let mut first = true;
189 | move |buf: &mut W, s: &str| -> fmt::Result {
190 | if first {
191 | first = false;
192 | write_lower(buf, s)?;
193 | } else {
194 | write_title(buf, s)?;
195 | }
196 | Ok(())
197 | }
198 | }
199 |
200 | /// A "delimiter function" that writes nothing to the buffer.
201 | pub fn delim_none(_: &mut B) -> fmt::Result
202 | where
203 | B: Write,
204 | {
205 | Ok(())
206 | }
207 |
208 | /// Returns a "delimiter function" that writes the given delimiter to the buffer.
209 | pub fn delim_fn(delim: &str) -> impl Fn(&mut B) -> fmt::Result + '_
210 | where
211 | B: Write,
212 | {
213 | move |buf| buf.write_str(delim)
214 | }
215 |
--------------------------------------------------------------------------------
/rust/tests/anycase.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 | use std::iter::FromIterator;
3 | use std::path::PathBuf;
4 | use std::sync::LazyLock;
5 |
6 | use serde::{Deserialize, Serialize};
7 | use serde_json as json;
8 |
9 | #[derive(Debug, Deserialize, Serialize)]
10 | struct TestCase {
11 | input: String,
12 | snake: String,
13 | camel: String,
14 | pascal: String,
15 | screaming_snake: String,
16 | kebab: String,
17 | screaming_kebab: String,
18 | train: String,
19 | lower: String,
20 | title: String,
21 | upper: String,
22 | }
23 |
24 | #[allow(clippy::incompatible_msrv)]
25 | static CASES: LazyLock> = LazyLock::new(|| {
26 | let path = PathBuf::from_iter([env!("CARGO_MANIFEST_DIR"), "..", "testdata", "common.json"]);
27 | let data = fs::read_to_string(path).unwrap();
28 | json::from_str(&data).unwrap()
29 | });
30 |
31 | #[test]
32 | fn common_to_string() {
33 | for case in CASES.iter() {
34 | assert_eq!(
35 | anycase::to_snake(&case.input),
36 | case.snake,
37 | "in: {:?}, conversion: snake",
38 | case.input
39 | );
40 | assert_eq!(
41 | anycase::to_camel(&case.input),
42 | case.camel,
43 | "in: {:?}, conversion: camel",
44 | case.input
45 | );
46 | assert_eq!(
47 | anycase::to_pascal(&case.input),
48 | case.pascal,
49 | "in: {:?}, conversion: pascal",
50 | case.input
51 | );
52 | assert_eq!(
53 | anycase::to_screaming_snake(&case.input),
54 | case.screaming_snake,
55 | "in: {:?}, conversion: screaming_snake",
56 | case.input
57 | );
58 | assert_eq!(
59 | anycase::to_kebab(&case.input),
60 | case.kebab,
61 | "in: {:?}, conversion: kebab",
62 | case.input
63 | );
64 | assert_eq!(
65 | anycase::to_screaming_kebab(&case.input),
66 | case.screaming_kebab,
67 | "in: {:?}, conversion: screaming_kebab",
68 | case.input
69 | );
70 | assert_eq!(
71 | anycase::to_train(&case.input),
72 | case.train,
73 | "in: {:?}, conversion: train",
74 | case.input
75 | );
76 | assert_eq!(
77 | anycase::to_lower(&case.input),
78 | case.lower,
79 | "in: {:?}, conversion: lower",
80 | case.input
81 | );
82 | assert_eq!(
83 | anycase::to_title(&case.input),
84 | case.title,
85 | "in: {:?}, conversion: title",
86 | case.input
87 | );
88 | assert_eq!(
89 | anycase::to_upper(&case.input),
90 | case.upper,
91 | "in: {:?}, conversion: upper",
92 | case.input
93 | );
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/testdata/common.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "",
4 | "snake": "",
5 | "camel": "",
6 | "pascal": "",
7 | "screaming_snake": "",
8 | "kebab": "",
9 | "screaming_kebab": "",
10 | "train": "",
11 | "lower": "",
12 | "title": "",
13 | "upper": ""
14 | },
15 | {
16 | "input": "Test",
17 | "snake": "test",
18 | "camel": "test",
19 | "pascal": "Test",
20 | "screaming_snake": "TEST",
21 | "kebab": "test",
22 | "screaming_kebab": "TEST",
23 | "train": "Test",
24 | "lower": "test",
25 | "title": "Test",
26 | "upper": "TEST"
27 | },
28 | {
29 | "input": "test case",
30 | "snake": "test_case",
31 | "camel": "testCase",
32 | "pascal": "TestCase",
33 | "screaming_snake": "TEST_CASE",
34 | "kebab": "test-case",
35 | "screaming_kebab": "TEST-CASE",
36 | "train": "Test-Case",
37 | "lower": "test case",
38 | "title": "Test Case",
39 | "upper": "TEST CASE"
40 | },
41 | {
42 | "input": " test case",
43 | "snake": "test_case",
44 | "camel": "testCase",
45 | "pascal": "TestCase",
46 | "screaming_snake": "TEST_CASE",
47 | "kebab": "test-case",
48 | "screaming_kebab": "TEST-CASE",
49 | "train": "Test-Case",
50 | "lower": "test case",
51 | "title": "Test Case",
52 | "upper": "TEST CASE"
53 | },
54 | {
55 | "input": "test case ",
56 | "snake": "test_case",
57 | "camel": "testCase",
58 | "pascal": "TestCase",
59 | "screaming_snake": "TEST_CASE",
60 | "kebab": "test-case",
61 | "screaming_kebab": "TEST-CASE",
62 | "train": "Test-Case",
63 | "lower": "test case",
64 | "title": "Test Case",
65 | "upper": "TEST CASE"
66 | },
67 | {
68 | "input": "Test Case",
69 | "snake": "test_case",
70 | "camel": "testCase",
71 | "pascal": "TestCase",
72 | "screaming_snake": "TEST_CASE",
73 | "kebab": "test-case",
74 | "screaming_kebab": "TEST-CASE",
75 | "train": "Test-Case",
76 | "lower": "test case",
77 | "title": "Test Case",
78 | "upper": "TEST CASE"
79 | },
80 | {
81 | "input": " Test Case",
82 | "snake": "test_case",
83 | "camel": "testCase",
84 | "pascal": "TestCase",
85 | "screaming_snake": "TEST_CASE",
86 | "kebab": "test-case",
87 | "screaming_kebab": "TEST-CASE",
88 | "train": "Test-Case",
89 | "lower": "test case",
90 | "title": "Test Case",
91 | "upper": "TEST CASE"
92 | },
93 | {
94 | "input": "Test Case ",
95 | "snake": "test_case",
96 | "camel": "testCase",
97 | "pascal": "TestCase",
98 | "screaming_snake": "TEST_CASE",
99 | "kebab": "test-case",
100 | "screaming_kebab": "TEST-CASE",
101 | "train": "Test-Case",
102 | "lower": "test case",
103 | "title": "Test Case",
104 | "upper": "TEST CASE"
105 | },
106 | {
107 | "input": "camelCase",
108 | "snake": "camel_case",
109 | "camel": "camelCase",
110 | "pascal": "CamelCase",
111 | "screaming_snake": "CAMEL_CASE",
112 | "kebab": "camel-case",
113 | "screaming_kebab": "CAMEL-CASE",
114 | "train": "Camel-Case",
115 | "lower": "camel case",
116 | "title": "Camel Case",
117 | "upper": "CAMEL CASE"
118 | },
119 | {
120 | "input": "PascalCase",
121 | "snake": "pascal_case",
122 | "camel": "pascalCase",
123 | "pascal": "PascalCase",
124 | "screaming_snake": "PASCAL_CASE",
125 | "kebab": "pascal-case",
126 | "screaming_kebab": "PASCAL-CASE",
127 | "train": "Pascal-Case",
128 | "lower": "pascal case",
129 | "title": "Pascal Case",
130 | "upper": "PASCAL CASE"
131 | },
132 | {
133 | "input": "snake_case",
134 | "snake": "snake_case",
135 | "camel": "snakeCase",
136 | "pascal": "SnakeCase",
137 | "screaming_snake": "SNAKE_CASE",
138 | "kebab": "snake-case",
139 | "screaming_kebab": "SNAKE-CASE",
140 | "train": "Snake-Case",
141 | "lower": "snake case",
142 | "title": "Snake Case",
143 | "upper": "SNAKE CASE"
144 | },
145 | {
146 | "input": " Test Case",
147 | "snake": "test_case",
148 | "camel": "testCase",
149 | "pascal": "TestCase",
150 | "screaming_snake": "TEST_CASE",
151 | "kebab": "test-case",
152 | "screaming_kebab": "TEST-CASE",
153 | "train": "Test-Case",
154 | "lower": "test case",
155 | "title": "Test Case",
156 | "upper": "TEST CASE"
157 | },
158 | {
159 | "input": "SCREAMING_SNAKE_CASE",
160 | "snake": "screaming_snake_case",
161 | "camel": "screamingSnakeCase",
162 | "pascal": "ScreamingSnakeCase",
163 | "screaming_snake": "SCREAMING_SNAKE_CASE",
164 | "kebab": "screaming-snake-case",
165 | "screaming_kebab": "SCREAMING-SNAKE-CASE",
166 | "train": "Screaming-Snake-Case",
167 | "lower": "screaming snake case",
168 | "title": "Screaming Snake Case",
169 | "upper": "SCREAMING SNAKE CASE"
170 | },
171 | {
172 | "input": "kebab-case",
173 | "snake": "kebab_case",
174 | "camel": "kebabCase",
175 | "pascal": "KebabCase",
176 | "screaming_snake": "KEBAB_CASE",
177 | "kebab": "kebab-case",
178 | "screaming_kebab": "KEBAB-CASE",
179 | "train": "Kebab-Case",
180 | "lower": "kebab case",
181 | "title": "Kebab Case",
182 | "upper": "KEBAB CASE"
183 | },
184 | {
185 | "input": "SCREAMING-KEBAB-CASE",
186 | "snake": "screaming_kebab_case",
187 | "camel": "screamingKebabCase",
188 | "pascal": "ScreamingKebabCase",
189 | "screaming_snake": "SCREAMING_KEBAB_CASE",
190 | "kebab": "screaming-kebab-case",
191 | "screaming_kebab": "SCREAMING-KEBAB-CASE",
192 | "train": "Screaming-Kebab-Case",
193 | "lower": "screaming kebab case",
194 | "title": "Screaming Kebab Case",
195 | "upper": "SCREAMING KEBAB CASE"
196 | },
197 | {
198 | "input": "Title Case ",
199 | "snake": "title_case",
200 | "camel": "titleCase",
201 | "pascal": "TitleCase",
202 | "screaming_snake": "TITLE_CASE",
203 | "kebab": "title-case",
204 | "screaming_kebab": "TITLE-CASE",
205 | "train": "Title-Case",
206 | "lower": "title case",
207 | "title": "Title Case",
208 | "upper": "TITLE CASE"
209 | },
210 | {
211 | "input": "Train-Case ",
212 | "snake": "train_case",
213 | "camel": "trainCase",
214 | "pascal": "TrainCase",
215 | "screaming_snake": "TRAIN_CASE",
216 | "kebab": "train-case",
217 | "screaming_kebab": "TRAIN-CASE",
218 | "train": "Train-Case",
219 | "lower": "train case",
220 | "title": "Train Case",
221 | "upper": "TRAIN CASE"
222 | },
223 | {
224 | "input": "This is a Test case.",
225 | "snake": "this_is_a_test_case",
226 | "camel": "thisIsATestCase",
227 | "pascal": "ThisIsATestCase",
228 | "screaming_snake": "THIS_IS_A_TEST_CASE",
229 | "kebab": "this-is-a-test-case",
230 | "screaming_kebab": "THIS-IS-A-TEST-CASE",
231 | "train": "This-Is-A-Test-Case",
232 | "lower": "this is a test case",
233 | "title": "This Is A Test Case",
234 | "upper": "THIS IS A TEST CASE"
235 | },
236 | {
237 | "input": "MixedUP CamelCase, with some Spaces",
238 | "snake": "mixed_up_camel_case_with_some_spaces",
239 | "camel": "mixedUpCamelCaseWithSomeSpaces",
240 | "pascal": "MixedUpCamelCaseWithSomeSpaces",
241 | "screaming_snake": "MIXED_UP_CAMEL_CASE_WITH_SOME_SPACES",
242 | "kebab": "mixed-up-camel-case-with-some-spaces",
243 | "screaming_kebab": "MIXED-UP-CAMEL-CASE-WITH-SOME-SPACES",
244 | "train": "Mixed-Up-Camel-Case-With-Some-Spaces",
245 | "lower": "mixed up camel case with some spaces",
246 | "title": "Mixed Up Camel Case With Some Spaces",
247 | "upper": "MIXED UP CAMEL CASE WITH SOME SPACES"
248 | },
249 | {
250 | "input": "mixed_up_ snake_case with some _spaces",
251 | "snake": "mixed_up_snake_case_with_some_spaces",
252 | "camel": "mixedUpSnakeCaseWithSomeSpaces",
253 | "pascal": "MixedUpSnakeCaseWithSomeSpaces",
254 | "screaming_snake": "MIXED_UP_SNAKE_CASE_WITH_SOME_SPACES",
255 | "kebab": "mixed-up-snake-case-with-some-spaces",
256 | "screaming_kebab": "MIXED-UP-SNAKE-CASE-WITH-SOME-SPACES",
257 | "train": "Mixed-Up-Snake-Case-With-Some-Spaces",
258 | "lower": "mixed up snake case with some spaces",
259 | "title": "Mixed Up Snake Case With Some Spaces",
260 | "upper": "MIXED UP SNAKE CASE WITH SOME SPACES"
261 | },
262 | {
263 | "input": "this-contains_ ALLKinds OfWord_Boundaries",
264 | "snake": "this_contains_all_kinds_of_word_boundaries",
265 | "camel": "thisContainsAllKindsOfWordBoundaries",
266 | "pascal": "ThisContainsAllKindsOfWordBoundaries",
267 | "screaming_snake": "THIS_CONTAINS_ALL_KINDS_OF_WORD_BOUNDARIES",
268 | "kebab": "this-contains-all-kinds-of-word-boundaries",
269 | "screaming_kebab": "THIS-CONTAINS-ALL-KINDS-OF-WORD-BOUNDARIES",
270 | "train": "This-Contains-All-Kinds-Of-Word-Boundaries",
271 | "lower": "this contains all kinds of word boundaries",
272 | "title": "This Contains All Kinds Of Word Boundaries",
273 | "upper": "THIS CONTAINS ALL KINDS OF WORD BOUNDARIES"
274 | },
275 | {
276 | "input": "XΣXΣ baffle",
277 | "snake": "xσxσ_baffle",
278 | "camel": "xσxσBaffle",
279 | "pascal": "XσxσBaffle",
280 | "screaming_snake": "XΣXΣ_BAFFLE",
281 | "kebab": "xσxσ-baffle",
282 | "screaming_kebab": "XΣXΣ-BAFFLE",
283 | "train": "Xσxσ-Baffle",
284 | "lower": "xσxσ baffle",
285 | "title": "Xσxσ Baffle",
286 | "upper": "XΣXΣ BAFFLE"
287 | },
288 | {
289 | "input": "XMLHttpRequest",
290 | "snake": "xml_http_request",
291 | "camel": "xmlHttpRequest",
292 | "pascal": "XmlHttpRequest",
293 | "screaming_snake": "XML_HTTP_REQUEST",
294 | "kebab": "xml-http-request",
295 | "screaming_kebab": "XML-HTTP-REQUEST",
296 | "train": "Xml-Http-Request",
297 | "lower": "xml http request",
298 | "title": "Xml Http Request",
299 | "upper": "XML HTTP REQUEST"
300 | },
301 | {
302 | "input": "FIELD_NAME11",
303 | "snake": "field_name11",
304 | "camel": "fieldName11",
305 | "pascal": "FieldName11",
306 | "screaming_snake": "FIELD_NAME11",
307 | "kebab": "field-name11",
308 | "screaming_kebab": "FIELD-NAME11",
309 | "train": "Field-Name11",
310 | "lower": "field name11",
311 | "title": "Field Name11",
312 | "upper": "FIELD NAME11"
313 | },
314 | {
315 | "input": "FIELD_NAME_11",
316 | "snake": "field_name_11",
317 | "camel": "fieldName11",
318 | "pascal": "FieldName11",
319 | "screaming_snake": "FIELD_NAME_11",
320 | "kebab": "field-name-11",
321 | "screaming_kebab": "FIELD-NAME-11",
322 | "train": "Field-Name-11",
323 | "lower": "field name 11",
324 | "title": "Field Name 11",
325 | "upper": "FIELD NAME 11"
326 | },
327 | {
328 | "input": "FIELD_NAME_1",
329 | "snake": "field_name_1",
330 | "camel": "fieldName1",
331 | "pascal": "FieldName1",
332 | "screaming_snake": "FIELD_NAME_1",
333 | "kebab": "field-name-1",
334 | "screaming_kebab": "FIELD-NAME-1",
335 | "train": "Field-Name-1",
336 | "lower": "field name 1",
337 | "title": "Field Name 1",
338 | "upper": "FIELD NAME 1"
339 | },
340 | {
341 | "input": "99BOTTLES",
342 | "snake": "99bottles",
343 | "camel": "99bottles",
344 | "pascal": "99bottles",
345 | "screaming_snake": "99BOTTLES",
346 | "kebab": "99bottles",
347 | "screaming_kebab": "99BOTTLES",
348 | "train": "99bottles",
349 | "lower": "99bottles",
350 | "title": "99bottles",
351 | "upper": "99BOTTLES"
352 | },
353 | {
354 | "input": "FieldNamE11",
355 | "snake": "field_nam_e11",
356 | "camel": "fieldNamE11",
357 | "pascal": "FieldNamE11",
358 | "screaming_snake": "FIELD_NAM_E11",
359 | "kebab": "field-nam-e11",
360 | "screaming_kebab": "FIELD-NAM-E11",
361 | "train": "Field-Nam-E11",
362 | "lower": "field nam e11",
363 | "title": "Field Nam E11",
364 | "upper": "FIELD NAM E11"
365 | },
366 | {
367 | "input": "abc123def456",
368 | "snake": "abc123def456",
369 | "camel": "abc123def456",
370 | "pascal": "Abc123def456",
371 | "screaming_snake": "ABC123DEF456",
372 | "kebab": "abc123def456",
373 | "screaming_kebab": "ABC123DEF456",
374 | "train": "Abc123def456",
375 | "lower": "abc123def456",
376 | "title": "Abc123def456",
377 | "upper": "ABC123DEF456"
378 | },
379 | {
380 | "input": "abc123DEF456",
381 | "snake": "abc123_def456",
382 | "camel": "abc123Def456",
383 | "pascal": "Abc123Def456",
384 | "screaming_snake": "ABC123_DEF456",
385 | "kebab": "abc123-def456",
386 | "screaming_kebab": "ABC123-DEF456",
387 | "train": "Abc123-Def456",
388 | "lower": "abc123 def456",
389 | "title": "Abc123 Def456",
390 | "upper": "ABC123 DEF456"
391 | },
392 | {
393 | "input": "abc123Def456",
394 | "snake": "abc123_def456",
395 | "camel": "abc123Def456",
396 | "pascal": "Abc123Def456",
397 | "screaming_snake": "ABC123_DEF456",
398 | "kebab": "abc123-def456",
399 | "screaming_kebab": "ABC123-DEF456",
400 | "train": "Abc123-Def456",
401 | "lower": "abc123 def456",
402 | "title": "Abc123 Def456",
403 | "upper": "ABC123 DEF456"
404 | },
405 | {
406 | "input": "abc123DEf456",
407 | "snake": "abc123_d_ef456",
408 | "camel": "abc123DEf456",
409 | "pascal": "Abc123DEf456",
410 | "screaming_snake": "ABC123_D_EF456",
411 | "kebab": "abc123-d-ef456",
412 | "screaming_kebab": "ABC123-D-EF456",
413 | "train": "Abc123-D-Ef456",
414 | "lower": "abc123 d ef456",
415 | "title": "Abc123 D Ef456",
416 | "upper": "ABC123 D EF456"
417 | },
418 | {
419 | "input": "ABC123def456",
420 | "snake": "abc123def456",
421 | "camel": "abc123def456",
422 | "pascal": "Abc123def456",
423 | "screaming_snake": "ABC123DEF456",
424 | "kebab": "abc123def456",
425 | "screaming_kebab": "ABC123DEF456",
426 | "train": "Abc123def456",
427 | "lower": "abc123def456",
428 | "title": "Abc123def456",
429 | "upper": "ABC123DEF456"
430 | },
431 | {
432 | "input": "ABC123DEF456",
433 | "snake": "abc123def456",
434 | "camel": "abc123def456",
435 | "pascal": "Abc123def456",
436 | "screaming_snake": "ABC123DEF456",
437 | "kebab": "abc123def456",
438 | "screaming_kebab": "ABC123DEF456",
439 | "train": "Abc123def456",
440 | "lower": "abc123def456",
441 | "title": "Abc123def456",
442 | "upper": "ABC123DEF456"
443 | },
444 | {
445 | "input": "ABC123Def456",
446 | "snake": "abc123_def456",
447 | "camel": "abc123Def456",
448 | "pascal": "Abc123Def456",
449 | "screaming_snake": "ABC123_DEF456",
450 | "kebab": "abc123-def456",
451 | "screaming_kebab": "ABC123-DEF456",
452 | "train": "Abc123-Def456",
453 | "lower": "abc123 def456",
454 | "title": "Abc123 Def456",
455 | "upper": "ABC123 DEF456"
456 | },
457 | {
458 | "input": "ABC123DEf456",
459 | "snake": "abc123d_ef456",
460 | "camel": "abc123dEf456",
461 | "pascal": "Abc123dEf456",
462 | "screaming_snake": "ABC123D_EF456",
463 | "kebab": "abc123d-ef456",
464 | "screaming_kebab": "ABC123D-EF456",
465 | "train": "Abc123d-Ef456",
466 | "lower": "abc123d ef456",
467 | "title": "Abc123d Ef456",
468 | "upper": "ABC123D EF456"
469 | },
470 | {
471 | "input": "ABC123dEEf456FOO",
472 | "snake": "abc123d_e_ef456_foo",
473 | "camel": "abc123dEEf456Foo",
474 | "pascal": "Abc123dEEf456Foo",
475 | "screaming_snake": "ABC123D_E_EF456_FOO",
476 | "kebab": "abc123d-e-ef456-foo",
477 | "screaming_kebab": "ABC123D-E-EF456-FOO",
478 | "train": "Abc123d-E-Ef456-Foo",
479 | "lower": "abc123d e ef456 foo",
480 | "title": "Abc123d E Ef456 Foo",
481 | "upper": "ABC123D E EF456 FOO"
482 | },
483 | {
484 | "input": "abcDEF",
485 | "snake": "abc_def",
486 | "camel": "abcDef",
487 | "pascal": "AbcDef",
488 | "screaming_snake": "ABC_DEF",
489 | "kebab": "abc-def",
490 | "screaming_kebab": "ABC-DEF",
491 | "train": "Abc-Def",
492 | "lower": "abc def",
493 | "title": "Abc Def",
494 | "upper": "ABC DEF"
495 | },
496 | {
497 | "input": "ABcDE",
498 | "snake": "a_bc_de",
499 | "camel": "aBcDe",
500 | "pascal": "ABcDe",
501 | "screaming_snake": "A_BC_DE",
502 | "kebab": "a-bc-de",
503 | "screaming_kebab": "A-BC-DE",
504 | "train": "A-Bc-De",
505 | "lower": "a bc de",
506 | "title": "A Bc De",
507 | "upper": "A BC DE"
508 | }
509 | ]
510 |
--------------------------------------------------------------------------------