├── .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 | [![Go Reference](https://pkg.go.dev/badge/rossmacarthur/anycase/go/format.svg)](https://pkg.go.dev/github.com/rossmacarthur/anycase/go) 4 | [![Build Status](https://badgers.space/github/checks/rossmacarthur/anycase/trunk?label=build)](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 | [![PyPI version](https://badgers.space/pypi/version/py-anycase)](https://pypi.org/project/py-anycase) 4 | [![Build Status](https://badgers.space/github/checks/rossmacarthur/anycase/trunk?label=build)](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 | [![Crates.io Version](https://badgers.space/crates/version/anycase)](https://crates.io/crates/anycase) 6 | [![Docs.rs Latest](https://badgers.space/badge/docs.rs/latest/blue)](https://docs.rs/anycase) 7 | [![Build Status](https://badgers.space/github/checks/rossmacarthur/anycase?label=build)](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 | --------------------------------------------------------------------------------