├── .github ├── CONTRIBUTING.md ├── dependabot.yaml └── workflows │ ├── auto-merge.yml │ └── go-test.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── go.mod ├── inflect.go └── inflect_test.go /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribution Guidelines 2 | 3 | ### Pull requests are always welcome 4 | 5 | We are always thrilled to receive pull requests, and do our best to 6 | process them as fast as possible. Not sure if that typo is worth a pull 7 | request? Do it! We will appreciate it. 8 | 9 | If your pull request is not accepted on the first try, don't be 10 | discouraged! If there's a problem with the implementation, hopefully you 11 | received feedback on what to improve. 12 | 13 | We're trying very hard to keep go-swagger lean and focused. We don't want it 14 | to do everything for everybody. This means that we might decide against 15 | incorporating a new feature. However, there might be a way to implement 16 | that feature *on top of* go-swagger. 17 | 18 | 19 | ### Conventions 20 | 21 | Fork the repo and make changes on your fork in a feature branch: 22 | 23 | - If it's a bugfix branch, name it XXX-something where XXX is the number of the 24 | issue 25 | - If it's a feature branch, create an enhancement issue to announce your 26 | intentions, and name it XXX-something where XXX is the number of the issue. 27 | 28 | Submit unit tests for your changes. Go has a great test framework built in; use 29 | it! Take a look at existing tests for inspiration. Run the full test suite on 30 | your branch before submitting a pull request. 31 | 32 | Update the documentation when creating or modifying features. Test 33 | your documentation changes for clarity, concision, and correctness, as 34 | well as a clean documentation build. See ``docs/README.md`` for more 35 | information on building the docs and how docs get released. 36 | 37 | Write clean code. Universally formatted code promotes ease of writing, reading, 38 | and maintenance. Always run `gofmt -s -w file.go` on each changed file before 39 | committing your changes. Most editors have plugins that do this automatically. 40 | 41 | Pull requests descriptions should be as clear as possible and include a 42 | reference to all the issues that they address. 43 | 44 | Pull requests must not contain commits from other users or branches. 45 | 46 | Commit messages must start with a capitalized and short summary (max. 50 47 | chars) written in the imperative, followed by an optional, more detailed 48 | explanatory text which is separated from the summary by an empty line. 49 | 50 | Code review comments may be added to your pull request. Discuss, then make the 51 | suggested modifications and push additional commits to your feature branch. Be 52 | sure to post a comment after pushing. The new commits will show up in the pull 53 | request automatically, but the reviewers will not be notified unless you 54 | comment. 55 | 56 | Before the pull request is merged, make sure that you squash your commits into 57 | logical units of work using `git rebase -i` and `git push -f`. After every 58 | commit the test suite should be passing. Include documentation changes in the 59 | same commit so that a revert would remove all traces of the feature or fix. 60 | 61 | Commits that fix or close an issue should include a reference like `Closes #XXX` 62 | or `Fixes #XXX`, which will automatically close the issue when merged. 63 | 64 | ### Sign your work 65 | 66 | The sign-off is a simple line at the end of the explanation for the 67 | patch, which certifies that you wrote it or otherwise have the right to 68 | pass it on as an open-source patch. The rules are pretty simple: if you 69 | can certify the below (from 70 | [developercertificate.org](http://developercertificate.org/)): 71 | 72 | ``` 73 | Developer Certificate of Origin 74 | Version 1.1 75 | 76 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 77 | 660 York Street, Suite 102, 78 | San Francisco, CA 94110 USA 79 | 80 | Everyone is permitted to copy and distribute verbatim copies of this 81 | license document, but changing it is not allowed. 82 | 83 | 84 | Developer's Certificate of Origin 1.1 85 | 86 | By making a contribution to this project, I certify that: 87 | 88 | (a) The contribution was created in whole or in part by me and I 89 | have the right to submit it under the open source license 90 | indicated in the file; or 91 | 92 | (b) The contribution is based upon previous work that, to the best 93 | of my knowledge, is covered under an appropriate open source 94 | license and I have the right under that license to submit that 95 | work with modifications, whether created in whole or in part 96 | by me, under the same open source license (unless I am 97 | permitted to submit under a different license), as indicated 98 | in the file; or 99 | 100 | (c) The contribution was provided directly to me by some other 101 | person who certified (a), (b) or (c) and I have not modified 102 | it. 103 | 104 | (d) I understand and agree that this project and the contribution 105 | are public and that a record of the contribution (including all 106 | personal information I submit with it, including my sign-off) is 107 | maintained indefinitely and may be redistributed consistent with 108 | this project or the open source license(s) involved. 109 | ``` 110 | 111 | then you just add a line to every git commit message: 112 | 113 | Signed-off-by: Joe Smith 114 | 115 | using your real name (sorry, no pseudonyms or anonymous contributions.) 116 | 117 | You can add the sign off when creating the git commit via `git commit -s`. 118 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "friday" 8 | open-pull-requests-limit: 2 # <- default is 5 9 | groups: # <- group all github actions updates in a single PR 10 | # 1. development-dependencies are auto-merged 11 | development-dependencies: 12 | patterns: 13 | - '*' 14 | 15 | - package-ecosystem: "gomod" 16 | # We define 4 groups of dependencies to regroup update pull requests: 17 | # - development (e.g. test dependencies) 18 | # - go-openapi updates 19 | # - golang.org (e.g. golang.org/x/... packages) 20 | # - other dependencies (direct or indirect) 21 | # 22 | # * All groups are checked once a week and each produce at most 1 PR. 23 | # * All dependabot PRs are auto-approved 24 | # 25 | # Auto-merging policy, when requirements are met: 26 | # 1. development-dependencies are auto-merged 27 | # 2. golang.org-dependencies are auto-merged 28 | # 3. go-openapi patch updates are auto-merged. Minor/major version updates require a manual merge. 29 | # 4. other dependencies require a manual merge 30 | directory: "/" 31 | schedule: 32 | interval: "weekly" 33 | day: "friday" 34 | open-pull-requests-limit: 4 35 | groups: 36 | development-dependencies: 37 | patterns: 38 | - "github.com/stretchr/testify" 39 | 40 | golang.org-dependencies: 41 | patterns: 42 | - "golang.org/*" 43 | 44 | go-openapi-dependencies: 45 | patterns: 46 | - "github.com/go-openapi/*" 47 | 48 | other-dependencies: 49 | exclude-patterns: 50 | - "github.com/go-openapi/*" 51 | - "github.com/stretchr/testify" 52 | - "golang.org/*" 53 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: github.event.pull_request.user.login == 'dependabot[bot]' 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2 16 | 17 | - name: Auto-approve all dependabot PRs 18 | run: gh pr review --approve "$PR_URL" 19 | env: 20 | PR_URL: ${{github.event.pull_request.html_url}} 21 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 22 | 23 | - name: Auto-merge dependabot PRs for development dependencies 24 | if: contains(steps.metadata.outputs.dependency-group, 'development-dependencies') 25 | run: gh pr merge --auto --rebase "$PR_URL" 26 | env: 27 | PR_URL: ${{github.event.pull_request.html_url}} 28 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 29 | 30 | - name: Auto-merge dependabot PRs for go-openapi patches 31 | if: contains(steps.metadata.outputs.dependency-group, 'go-openapi-dependencies') && (steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch') 32 | run: gh pr merge --auto --rebase "$PR_URL" 33 | env: 34 | PR_URL: ${{github.event.pull_request.html_url}} 35 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 36 | 37 | - name: Auto-merge dependabot PRs for golang.org updates 38 | if: contains(steps.metadata.outputs.dependency-group, 'golang.org-dependencies') 39 | run: gh pr merge --auto --rebase "$PR_URL" 40 | env: 41 | PR_URL: ${{github.event.pull_request.html_url}} 42 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: go test 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - master 9 | 10 | pull_request: 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: stable 21 | check-latest: true 22 | cache: true 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v8 25 | with: 26 | version: latest 27 | only-new-issues: true 28 | skip-cache: true 29 | 30 | test: 31 | name: Unit tests 32 | runs-on: ${{ matrix.os }} 33 | 34 | strategy: 35 | matrix: 36 | os: [ ubuntu-latest, macos-latest, windows-latest ] 37 | go_version: ['oldstable', 'stable' ] 38 | 39 | steps: 40 | - uses: actions/setup-go@v5 41 | with: 42 | go-version: '${{ matrix.go_version }}' 43 | check-latest: true 44 | cache: true 45 | 46 | - uses: actions/checkout@v4 47 | - name: Run unit tests 48 | shell: bash 49 | run: go test -v -race -coverprofile="coverage-${{ matrix.os }}.${{ matrix.go_version }}.out" -covermode=atomic -coverpkg=$(go list)/... ./... 50 | 51 | - name: Upload coverage to codecov 52 | uses: codecov/codecov-action@v5 53 | with: 54 | files: './coverage-${{ matrix.os }}.${{ matrix.go_version }}.out' 55 | flags: '${{ matrix.go_version }}-${{ matrix.os }}' 56 | fail_ci_if_error: false 57 | verbose: true 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | secrets.yml 2 | coverage.out 3 | coverage.txt 4 | *.cov 5 | .idea 6 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: all 4 | disable: 5 | - cyclop 6 | - depguard 7 | - errchkjson 8 | - errorlint 9 | - exhaustruct 10 | - forcetypeassert 11 | - funlen 12 | - gochecknoglobals 13 | - gochecknoinits 14 | - gocognit 15 | - godot 16 | - godox 17 | - gosmopolitan 18 | - inamedparam 19 | - ireturn 20 | - lll 21 | - musttag 22 | - nestif 23 | - nlreturn 24 | - nonamedreturns 25 | - paralleltest 26 | - testpackage 27 | - thelper 28 | - tparallel 29 | - unparam 30 | - varnamelen 31 | - whitespace 32 | - wrapcheck 33 | - wsl 34 | settings: 35 | dupl: 36 | threshold: 200 37 | goconst: 38 | min-len: 2 39 | min-occurrences: 3 40 | gocyclo: 41 | min-complexity: 45 42 | exclusions: 43 | generated: lax 44 | presets: 45 | - comments 46 | - common-false-positives 47 | - legacy 48 | - std-error-handling 49 | paths: 50 | - third_party$ 51 | - builtin$ 52 | - examples$ 53 | formatters: 54 | enable: 55 | - gofmt 56 | - goimports 57 | exclusions: 58 | generated: lax 59 | paths: 60 | - third_party$ 61 | - builtin$ 62 | - examples$ 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Chris Farmiloe 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inflect [![Build Status](https://github.com/go-openapi/inflect/actions/workflows/go-test.yml/badge.svg)](https://github.com/go-openapi/inflect/actions?query=workflow%3A"go+test") [![codecov](https://codecov.io/gh/go-openapi/inflect/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/inflect) 2 | 3 | [![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io) 4 | [![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/inflect/master/LICENSE) 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/go-openapi/inflect.svg)](https://pkg.go.dev/github.com/go-openapi/inflect) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-openapi/inflect)](https://goreportcard.com/report/github.com/go-openapi/inflect) 7 | 8 | A package to pluralize words. 9 | 10 | Originally forked from fork of https://bitbucket.org/pkg/inflect under a MIT License. 11 | 12 | A golang library applying grammar rules to English words. 13 | 14 | > This package provides a basic set of functions applying 15 | > grammar rules to inflect English words, modify case style 16 | > (Capitalize, camelCase, snake_case, etc.). 17 | > 18 | > Acronyms are properly handled. A common use case is word pluralization. 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-openapi/inflect 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /inflect.go: -------------------------------------------------------------------------------- 1 | package inflect 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | "unicode" 9 | "unicode/utf8" 10 | ) 11 | 12 | // used by rulesets 13 | type Rule struct { 14 | suffix string 15 | replacement string 16 | exact bool 17 | } 18 | 19 | // a Ruleset is the config of pluralization rules 20 | // you can extend the rules with the Add* methods 21 | type Ruleset struct { 22 | uncountables map[string]bool 23 | plurals []*Rule 24 | singulars []*Rule 25 | humans []*Rule 26 | acronyms []*Rule 27 | } 28 | 29 | // create a blank ruleset. Unless you are going to 30 | // build your own rules from scratch you probably 31 | // won't need this and can just use the defaultRuleset 32 | // via the global inflect.* methods 33 | func NewRuleset() *Ruleset { 34 | rs := new(Ruleset) 35 | rs.uncountables = make(map[string]bool) 36 | rs.plurals = make([]*Rule, 0) 37 | rs.singulars = make([]*Rule, 0) 38 | rs.humans = make([]*Rule, 0) 39 | rs.acronyms = make([]*Rule, 0) 40 | return rs 41 | } 42 | 43 | // create a new ruleset and load it with the default 44 | // set of common English pluralization rules 45 | func NewDefaultRuleset() *Ruleset { 46 | rs := NewRuleset() 47 | rs.AddPlural("s", "s") 48 | rs.AddPlural("testis", "testes") 49 | rs.AddPlural("axis", "axes") 50 | rs.AddPlural("octopus", "octopi") 51 | rs.AddPlural("virus", "viri") 52 | rs.AddPlural("octopi", "octopi") 53 | rs.AddPlural("viri", "viri") 54 | rs.AddPlural("alias", "aliases") 55 | rs.AddPlural("status", "statuses") 56 | rs.AddPlural("bus", "buses") 57 | rs.AddPlural("buffalo", "buffaloes") 58 | rs.AddPlural("tomato", "tomatoes") 59 | rs.AddPlural("tum", "ta") 60 | rs.AddPlural("ium", "ia") 61 | rs.AddPlural("ta", "ta") 62 | rs.AddPlural("ia", "ia") 63 | rs.AddPlural("sis", "ses") 64 | rs.AddPlural("lf", "lves") 65 | rs.AddPlural("rf", "rves") 66 | rs.AddPlural("afe", "aves") 67 | rs.AddPlural("bfe", "bves") 68 | rs.AddPlural("cfe", "cves") 69 | rs.AddPlural("dfe", "dves") 70 | rs.AddPlural("efe", "eves") 71 | rs.AddPlural("gfe", "gves") 72 | rs.AddPlural("hfe", "hves") 73 | rs.AddPlural("ife", "ives") 74 | rs.AddPlural("jfe", "jves") 75 | rs.AddPlural("kfe", "kves") 76 | rs.AddPlural("lfe", "lves") 77 | rs.AddPlural("mfe", "mves") 78 | rs.AddPlural("nfe", "nves") 79 | rs.AddPlural("ofe", "oves") 80 | rs.AddPlural("pfe", "pves") 81 | rs.AddPlural("qfe", "qves") 82 | rs.AddPlural("rfe", "rves") 83 | rs.AddPlural("sfe", "sves") 84 | rs.AddPlural("tfe", "tves") 85 | rs.AddPlural("ufe", "uves") 86 | rs.AddPlural("vfe", "vves") 87 | rs.AddPlural("wfe", "wves") 88 | rs.AddPlural("xfe", "xves") 89 | rs.AddPlural("yfe", "yves") 90 | rs.AddPlural("zfe", "zves") 91 | rs.AddPlural("hive", "hives") 92 | rs.AddPlural("quy", "quies") 93 | rs.AddPlural("by", "bies") 94 | rs.AddPlural("cy", "cies") 95 | rs.AddPlural("dy", "dies") 96 | rs.AddPlural("fy", "fies") 97 | rs.AddPlural("gy", "gies") 98 | rs.AddPlural("hy", "hies") 99 | rs.AddPlural("jy", "jies") 100 | rs.AddPlural("ky", "kies") 101 | rs.AddPlural("ly", "lies") 102 | rs.AddPlural("my", "mies") 103 | rs.AddPlural("ny", "nies") 104 | rs.AddPlural("py", "pies") 105 | rs.AddPlural("qy", "qies") 106 | rs.AddPlural("ry", "ries") 107 | rs.AddPlural("sy", "sies") 108 | rs.AddPlural("ty", "ties") 109 | rs.AddPlural("vy", "vies") 110 | rs.AddPlural("wy", "wies") 111 | rs.AddPlural("xy", "xies") 112 | rs.AddPlural("zy", "zies") 113 | rs.AddPlural("x", "xes") 114 | rs.AddPlural("ch", "ches") 115 | rs.AddPlural("ss", "sses") 116 | rs.AddPlural("sh", "shes") 117 | rs.AddPlural("matrix", "matrices") 118 | rs.AddPlural("vertix", "vertices") 119 | rs.AddPlural("indix", "indices") 120 | rs.AddPlural("matrex", "matrices") 121 | rs.AddPlural("vertex", "vertices") 122 | rs.AddPlural("index", "indices") 123 | rs.AddPlural("mouse", "mice") 124 | rs.AddPlural("louse", "lice") 125 | rs.AddPlural("mice", "mice") 126 | rs.AddPlural("lice", "lice") 127 | rs.AddPluralExact("ox", "oxen", true) 128 | rs.AddPluralExact("oxen", "oxen", true) 129 | rs.AddPluralExact("quiz", "quizzes", true) 130 | rs.AddSingular("s", "") 131 | rs.AddSingular("news", "news") 132 | rs.AddSingular("ta", "tum") 133 | rs.AddSingular("ia", "ium") 134 | rs.AddSingular("analyses", "analysis") 135 | rs.AddSingular("bases", "basis") 136 | rs.AddSingular("diagnoses", "diagnosis") 137 | rs.AddSingular("parentheses", "parenthesis") 138 | rs.AddSingular("prognoses", "prognosis") 139 | rs.AddSingular("synopses", "synopsis") 140 | rs.AddSingular("theses", "thesis") 141 | rs.AddSingular("analyses", "analysis") 142 | rs.AddSingular("aves", "afe") 143 | rs.AddSingular("bves", "bfe") 144 | rs.AddSingular("cves", "cfe") 145 | rs.AddSingular("dves", "dfe") 146 | rs.AddSingular("eves", "efe") 147 | rs.AddSingular("gves", "gfe") 148 | rs.AddSingular("hves", "hfe") 149 | rs.AddSingular("ives", "ife") 150 | rs.AddSingular("jves", "jfe") 151 | rs.AddSingular("kves", "kfe") 152 | rs.AddSingular("lves", "lfe") 153 | rs.AddSingular("mves", "mfe") 154 | rs.AddSingular("nves", "nfe") 155 | rs.AddSingular("oves", "ofe") 156 | rs.AddSingular("pves", "pfe") 157 | rs.AddSingular("qves", "qfe") 158 | rs.AddSingular("rves", "rfe") 159 | rs.AddSingular("sves", "sfe") 160 | rs.AddSingular("tves", "tfe") 161 | rs.AddSingular("uves", "ufe") 162 | rs.AddSingular("vves", "vfe") 163 | rs.AddSingular("wves", "wfe") 164 | rs.AddSingular("xves", "xfe") 165 | rs.AddSingular("yves", "yfe") 166 | rs.AddSingular("zves", "zfe") 167 | rs.AddSingular("hives", "hive") 168 | rs.AddSingular("tives", "tive") 169 | rs.AddSingular("lves", "lf") 170 | rs.AddSingular("rves", "rf") 171 | rs.AddSingular("quies", "quy") 172 | rs.AddSingular("bies", "by") 173 | rs.AddSingular("cies", "cy") 174 | rs.AddSingular("dies", "dy") 175 | rs.AddSingular("fies", "fy") 176 | rs.AddSingular("gies", "gy") 177 | rs.AddSingular("hies", "hy") 178 | rs.AddSingular("jies", "jy") 179 | rs.AddSingular("kies", "ky") 180 | rs.AddSingular("lies", "ly") 181 | rs.AddSingular("mies", "my") 182 | rs.AddSingular("nies", "ny") 183 | rs.AddSingular("pies", "py") 184 | rs.AddSingular("qies", "qy") 185 | rs.AddSingular("ries", "ry") 186 | rs.AddSingular("sies", "sy") 187 | rs.AddSingular("ties", "ty") 188 | rs.AddSingular("vies", "vy") 189 | rs.AddSingular("wies", "wy") 190 | rs.AddSingular("xies", "xy") 191 | rs.AddSingular("zies", "zy") 192 | rs.AddSingular("series", "series") 193 | rs.AddSingular("movies", "movie") 194 | rs.AddSingular("xes", "x") 195 | rs.AddSingular("ches", "ch") 196 | rs.AddSingular("sses", "ss") 197 | rs.AddSingular("shes", "sh") 198 | rs.AddSingular("mice", "mouse") 199 | rs.AddSingular("lice", "louse") 200 | rs.AddSingular("buses", "bus") 201 | rs.AddSingular("oes", "o") 202 | rs.AddSingular("shoes", "shoe") 203 | rs.AddSingular("crises", "crisis") 204 | rs.AddSingular("axes", "axis") 205 | rs.AddSingular("testes", "testis") 206 | rs.AddSingular("octopi", "octopus") 207 | rs.AddSingular("viri", "virus") 208 | rs.AddSingular("statuses", "status") 209 | rs.AddSingular("aliases", "alias") 210 | rs.AddSingularExact("oxen", "ox", true) 211 | rs.AddSingular("vertices", "vertex") 212 | rs.AddSingular("indices", "index") 213 | rs.AddSingular("matrices", "matrix") 214 | rs.AddSingularExact("quizzes", "quiz", true) 215 | rs.AddSingular("databases", "database") 216 | rs.AddIrregular("person", "people") 217 | rs.AddIrregular("man", "men") 218 | rs.AddIrregular("child", "children") 219 | rs.AddIrregular("sex", "sexes") 220 | rs.AddIrregular("move", "moves") 221 | rs.AddIrregular("zombie", "zombies") 222 | rs.AddUncountable("equipment") 223 | rs.AddUncountable("information") 224 | rs.AddUncountable("rice") 225 | rs.AddUncountable("money") 226 | rs.AddUncountable("species") 227 | rs.AddUncountable("series") 228 | rs.AddUncountable("fish") 229 | rs.AddUncountable("sheep") 230 | rs.AddUncountable("jeans") 231 | rs.AddUncountable("police") 232 | return rs 233 | } 234 | 235 | func (rs *Ruleset) Uncountables() map[string]bool { 236 | return rs.uncountables 237 | } 238 | 239 | // add a pluralization rule 240 | func (rs *Ruleset) AddPlural(suffix, replacement string) { 241 | rs.AddPluralExact(suffix, replacement, false) 242 | } 243 | 244 | // add a pluralization rule with full string match 245 | func (rs *Ruleset) AddPluralExact(suffix, replacement string, exact bool) { 246 | // remove uncountable 247 | delete(rs.uncountables, suffix) 248 | // create rule 249 | r := new(Rule) 250 | r.suffix = suffix 251 | r.replacement = replacement 252 | r.exact = exact 253 | // prepend 254 | rs.plurals = append([]*Rule{r}, rs.plurals...) 255 | } 256 | 257 | // add a singular rule 258 | func (rs *Ruleset) AddSingular(suffix, replacement string) { 259 | rs.AddSingularExact(suffix, replacement, false) 260 | } 261 | 262 | // same as AddSingular but you can set `exact` to force 263 | // a full string match 264 | func (rs *Ruleset) AddSingularExact(suffix, replacement string, exact bool) { 265 | // remove from uncountable 266 | delete(rs.uncountables, suffix) 267 | // create rule 268 | r := new(Rule) 269 | r.suffix = suffix 270 | r.replacement = replacement 271 | r.exact = exact 272 | rs.singulars = append([]*Rule{r}, rs.singulars...) 273 | } 274 | 275 | // Human rules are applied by humanize to show more friendly 276 | // versions of words 277 | func (rs *Ruleset) AddHuman(suffix, replacement string) { 278 | r := new(Rule) 279 | r.suffix = suffix 280 | r.replacement = replacement 281 | rs.humans = append([]*Rule{r}, rs.humans...) 282 | } 283 | 284 | // Add any inconsistent pluralizing/sinularizing rules 285 | // to the set here. 286 | func (rs *Ruleset) AddIrregular(singular, plural string) { 287 | delete(rs.uncountables, singular) 288 | delete(rs.uncountables, plural) 289 | rs.AddPlural(singular, plural) 290 | rs.AddPlural(plural, plural) 291 | rs.AddSingular(plural, singular) 292 | } 293 | 294 | // if you use acronym you may need to add them to the ruleset 295 | // to prevent Underscored words of things like "HTML" coming out 296 | // as "h_t_m_l" 297 | func (rs *Ruleset) AddAcronym(word string) { 298 | r := new(Rule) 299 | r.suffix = word 300 | r.replacement = rs.Titleize(strings.ToLower(word)) 301 | rs.acronyms = append(rs.acronyms, r) 302 | } 303 | 304 | // add a word to this ruleset that has the same singular and plural form 305 | // for example: "rice" 306 | func (rs *Ruleset) AddUncountable(word string) { 307 | rs.uncountables[strings.ToLower(word)] = true 308 | } 309 | 310 | func (rs *Ruleset) isUncountable(word string) bool { 311 | // handle multiple words by using the last one 312 | words := strings.Split(word, " ") 313 | if _, exists := rs.uncountables[strings.ToLower(words[len(words)-1])]; exists { 314 | return true 315 | } 316 | return false 317 | } 318 | 319 | // returns the plural form of a singular word 320 | func (rs *Ruleset) Pluralize(word string) string { 321 | if len(word) == 0 { 322 | return word 323 | } 324 | if rs.isUncountable(word) { 325 | return word 326 | } 327 | for _, rule := range rs.plurals { 328 | if rule.exact { 329 | if word == rule.suffix { 330 | return rule.replacement 331 | } 332 | } else { 333 | if strings.HasSuffix(word, rule.suffix) { 334 | return replaceLast(word, rule.suffix, rule.replacement) 335 | } 336 | } 337 | } 338 | return word + "s" 339 | } 340 | 341 | // returns the singular form of a plural word 342 | func (rs *Ruleset) Singularize(word string) string { 343 | if len(word) == 0 { 344 | return word 345 | } 346 | if rs.isUncountable(word) { 347 | return word 348 | } 349 | for _, rule := range rs.singulars { 350 | if rule.exact { 351 | if word == rule.suffix { 352 | return rule.replacement 353 | } 354 | } else { 355 | if strings.HasSuffix(word, rule.suffix) { 356 | return replaceLast(word, rule.suffix, rule.replacement) 357 | } 358 | } 359 | } 360 | return word 361 | } 362 | 363 | // uppercase first character 364 | func (rs *Ruleset) Capitalize(word string) string { 365 | return strings.ToUpper(word[:1]) + word[1:] 366 | } 367 | 368 | // "dino_party" -> "DinoParty" 369 | func (rs *Ruleset) Camelize(word string) string { 370 | words := splitAtCaseChangeWithTitlecase(word) 371 | return strings.Join(words, "") 372 | } 373 | 374 | // same as Camelcase but with first letter downcased 375 | func (rs *Ruleset) CamelizeDownFirst(word string) string { 376 | word = Camelize(word) 377 | return strings.ToLower(word[:1]) + word[1:] 378 | } 379 | 380 | // Captitilize every word in sentance "hello there" -> "Hello There" 381 | func (rs *Ruleset) Titleize(word string) string { 382 | words := splitAtCaseChangeWithTitlecase(word) 383 | return strings.Join(words, " ") 384 | } 385 | 386 | func (rs *Ruleset) safeCaseAcronyms(word string) string { 387 | // convert an acroymn like HTML into Html 388 | for _, rule := range rs.acronyms { 389 | word = strings.ReplaceAll(word, rule.suffix, rule.replacement) 390 | } 391 | return word 392 | } 393 | 394 | func (rs *Ruleset) seperatedWords(word, sep string) string { 395 | word = rs.safeCaseAcronyms(word) 396 | words := splitAtCaseChange(word) 397 | return strings.Join(words, sep) 398 | } 399 | 400 | // lowercase underscore version "BigBen" -> "big_ben" 401 | func (rs *Ruleset) Underscore(word string) string { 402 | return rs.seperatedWords(word, "_") 403 | } 404 | 405 | // First letter of sentance captitilized 406 | // Uses custom friendly replacements via AddHuman() 407 | func (rs *Ruleset) Humanize(word string) string { 408 | word = replaceLast(word, "_id", "") // strip foreign key kinds 409 | // replace and strings in humans list 410 | for _, rule := range rs.humans { 411 | word = strings.ReplaceAll(word, rule.suffix, rule.replacement) 412 | } 413 | sentance := rs.seperatedWords(word, " ") 414 | return strings.ToUpper(sentance[:1]) + sentance[1:] 415 | } 416 | 417 | // an underscored foreign key name "Person" -> "person_id" 418 | func (rs *Ruleset) ForeignKey(word string) string { 419 | return rs.Underscore(rs.Singularize(word)) + "_id" 420 | } 421 | 422 | // a foreign key (with an underscore) "Person" -> "personid" 423 | func (rs *Ruleset) ForeignKeyCondensed(word string) string { 424 | return rs.Underscore(word) + "id" 425 | } 426 | 427 | // Rails style pluralized table names: "SuperPerson" -> "super_people" 428 | func (rs *Ruleset) Tableize(word string) string { 429 | return rs.Pluralize(rs.Underscore(rs.Typeify(word))) 430 | } 431 | 432 | var notURLSafe = regexp.MustCompile(`[^\w\d\-_ ]`) 433 | 434 | // param safe dasherized names like "my-param" 435 | func (rs *Ruleset) Parameterize(word string) string { 436 | return ParameterizeJoin(word, "-") 437 | } 438 | 439 | // param safe dasherized names with custom separator 440 | func (rs *Ruleset) ParameterizeJoin(word, sep string) string { 441 | word = strings.ToLower(word) 442 | word = rs.Asciify(word) 443 | word = notURLSafe.ReplaceAllString(word, "") 444 | word = strings.ReplaceAll(word, " ", sep) 445 | if len(sep) > 0 { 446 | squash, err := regexp.Compile(sep + "+") 447 | if err == nil { 448 | word = squash.ReplaceAllString(word, sep) 449 | } 450 | } 451 | word = strings.Trim(word, sep+" ") 452 | return word 453 | } 454 | 455 | var lookalikes = map[string]*regexp.Regexp{ 456 | "A": regexp.MustCompile(`À|Á|Â|Ã|Ä|Å`), 457 | "AE": regexp.MustCompile(`Æ`), 458 | "C": regexp.MustCompile(`Ç`), 459 | "E": regexp.MustCompile(`È|É|Ê|Ë`), 460 | "G": regexp.MustCompile(`Ğ`), 461 | "I": regexp.MustCompile(`Ì|Í|Î|Ï|İ`), 462 | "N": regexp.MustCompile(`Ñ`), 463 | "O": regexp.MustCompile(`Ò|Ó|Ô|Õ|Ö|Ø`), 464 | "S": regexp.MustCompile(`Ş`), 465 | "U": regexp.MustCompile(`Ù|Ú|Û|Ü`), 466 | "Y": regexp.MustCompile(`Ý`), 467 | "ss": regexp.MustCompile(`ß`), 468 | "a": regexp.MustCompile(`à|á|â|ã|ä|å`), 469 | "ae": regexp.MustCompile(`æ`), 470 | "c": regexp.MustCompile(`ç`), 471 | "e": regexp.MustCompile(`è|é|ê|ë`), 472 | "g": regexp.MustCompile(`ğ`), 473 | "i": regexp.MustCompile(`ì|í|î|ï|ı`), 474 | "n": regexp.MustCompile(`ñ`), 475 | "o": regexp.MustCompile(`ò|ó|ô|õ|ö|ø`), 476 | "s": regexp.MustCompile(`ş`), 477 | "u": regexp.MustCompile(`ù|ú|û|ü|ũ|ū|ŭ|ů|ű|ų`), 478 | "y": regexp.MustCompile(`ý|ÿ`), 479 | } 480 | 481 | // transforms latin characters like é -> e 482 | func (rs *Ruleset) Asciify(word string) string { 483 | for repl, regex := range lookalikes { 484 | word = regex.ReplaceAllString(word, repl) 485 | } 486 | return word 487 | } 488 | 489 | var tablePrefix = regexp.MustCompile(`^[^.]*\.`) 490 | 491 | // "something_like_this" -> "SomethingLikeThis" 492 | func (rs *Ruleset) Typeify(word string) string { 493 | word = tablePrefix.ReplaceAllString(word, "") 494 | return rs.Camelize(rs.Singularize(word)) 495 | } 496 | 497 | // "SomeText" -> "some-text" 498 | func (rs *Ruleset) Dasherize(word string) string { 499 | return rs.seperatedWords(word, "-") 500 | } 501 | 502 | // "1031" -> "1031st" 503 | // 504 | //nolint:mnd 505 | func (rs *Ruleset) Ordinalize(str string) string { 506 | number, err := strconv.Atoi(str) 507 | if err != nil { 508 | return str 509 | } 510 | switch abs(number) % 100 { 511 | case 11, 12, 13: 512 | return fmt.Sprintf("%dth", number) 513 | default: 514 | switch abs(number) % 10 { 515 | case 1: 516 | return fmt.Sprintf("%dst", number) 517 | case 2: 518 | return fmt.Sprintf("%dnd", number) 519 | case 3: 520 | return fmt.Sprintf("%drd", number) 521 | } 522 | } 523 | return fmt.Sprintf("%dth", number) 524 | } 525 | 526 | ///////////////////////////////////////// 527 | // the default global ruleset 528 | ////////////////////////////////////////// 529 | 530 | var defaultRuleset *Ruleset 531 | 532 | func init() { 533 | defaultRuleset = NewDefaultRuleset() 534 | } 535 | 536 | func Uncountables() map[string]bool { 537 | return defaultRuleset.Uncountables() 538 | } 539 | 540 | func AddPlural(suffix, replacement string) { 541 | defaultRuleset.AddPlural(suffix, replacement) 542 | } 543 | 544 | func AddSingular(suffix, replacement string) { 545 | defaultRuleset.AddSingular(suffix, replacement) 546 | } 547 | 548 | func AddHuman(suffix, replacement string) { 549 | defaultRuleset.AddHuman(suffix, replacement) 550 | } 551 | 552 | func AddIrregular(singular, plural string) { 553 | defaultRuleset.AddIrregular(singular, plural) 554 | } 555 | 556 | func AddAcronym(word string) { 557 | defaultRuleset.AddAcronym(word) 558 | } 559 | 560 | func AddUncountable(word string) { 561 | defaultRuleset.AddUncountable(word) 562 | } 563 | 564 | func Pluralize(word string) string { 565 | return defaultRuleset.Pluralize(word) 566 | } 567 | 568 | func Singularize(word string) string { 569 | return defaultRuleset.Singularize(word) 570 | } 571 | 572 | func Capitalize(word string) string { 573 | return defaultRuleset.Capitalize(word) 574 | } 575 | 576 | func Camelize(word string) string { 577 | return defaultRuleset.Camelize(word) 578 | } 579 | 580 | func CamelizeDownFirst(word string) string { 581 | return defaultRuleset.CamelizeDownFirst(word) 582 | } 583 | 584 | func Titleize(word string) string { 585 | return defaultRuleset.Titleize(word) 586 | } 587 | 588 | func Underscore(word string) string { 589 | return defaultRuleset.Underscore(word) 590 | } 591 | 592 | func Humanize(word string) string { 593 | return defaultRuleset.Humanize(word) 594 | } 595 | 596 | func ForeignKey(word string) string { 597 | return defaultRuleset.ForeignKey(word) 598 | } 599 | 600 | func ForeignKeyCondensed(word string) string { 601 | return defaultRuleset.ForeignKeyCondensed(word) 602 | } 603 | 604 | func Tableize(word string) string { 605 | return defaultRuleset.Tableize(word) 606 | } 607 | 608 | func Parameterize(word string) string { 609 | return defaultRuleset.Parameterize(word) 610 | } 611 | 612 | func ParameterizeJoin(word, sep string) string { 613 | return defaultRuleset.ParameterizeJoin(word, sep) 614 | } 615 | 616 | func Typeify(word string) string { 617 | return defaultRuleset.Typeify(word) 618 | } 619 | 620 | func Dasherize(word string) string { 621 | return defaultRuleset.Dasherize(word) 622 | } 623 | 624 | func Ordinalize(word string) string { 625 | return defaultRuleset.Ordinalize(word) 626 | } 627 | 628 | func Asciify(word string) string { 629 | return defaultRuleset.Asciify(word) 630 | } 631 | 632 | // helper funcs 633 | 634 | func reverse(s string) string { 635 | o := make([]rune, utf8.RuneCountInString(s)) 636 | i := len(o) 637 | for _, c := range s { 638 | i-- 639 | o[i] = c 640 | } 641 | return string(o) 642 | } 643 | 644 | func isSpacerChar(c rune) bool { 645 | switch { 646 | case c == '_': 647 | return true 648 | case c == ':': 649 | return true 650 | case c == '-': 651 | return true 652 | case unicode.IsSpace(c): 653 | return true 654 | } 655 | return false 656 | } 657 | 658 | func splitAtCaseChange(s string) []string { 659 | words := make([]string, 0) 660 | word := make([]rune, 0) 661 | for _, c := range s { 662 | spacer := isSpacerChar(c) 663 | if len(word) > 0 { 664 | if unicode.IsUpper(c) || spacer { 665 | words = append(words, string(word)) 666 | word = make([]rune, 0) 667 | } 668 | } 669 | if !spacer { 670 | word = append(word, unicode.ToLower(c)) 671 | } 672 | } 673 | words = append(words, string(word)) 674 | return words 675 | } 676 | 677 | func splitAtCaseChangeWithTitlecase(s string) []string { 678 | words := make([]string, 0) 679 | word := make([]rune, 0) 680 | for _, c := range s { 681 | spacer := isSpacerChar(c) 682 | if len(word) > 0 { 683 | if unicode.IsUpper(c) || spacer { 684 | words = append(words, string(word)) 685 | word = make([]rune, 0) 686 | } 687 | } 688 | if !spacer { 689 | if len(word) > 0 { 690 | word = append(word, unicode.ToLower(c)) 691 | } else { 692 | word = append(word, unicode.ToUpper(c)) 693 | } 694 | } 695 | } 696 | words = append(words, string(word)) 697 | return words 698 | } 699 | 700 | func replaceLast(s, match, repl string) string { 701 | // reverse strings 702 | srev := reverse(s) 703 | mrev := reverse(match) 704 | rrev := reverse(repl) 705 | // match first and reverse back 706 | return reverse(strings.Replace(srev, mrev, rrev, 1)) 707 | } 708 | 709 | func abs(x int) int { 710 | if x < 0 { 711 | return -x 712 | } 713 | return x 714 | } 715 | -------------------------------------------------------------------------------- /inflect_test.go: -------------------------------------------------------------------------------- 1 | package inflect 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPluralizePlurals(t *testing.T) { 8 | assertEqual(t, "plurals", Pluralize("plurals")) 9 | assertEqual(t, "Plurals", Pluralize("Plurals")) 10 | } 11 | 12 | func TestPluralizeEmptyString(t *testing.T) { 13 | assertEqual(t, "", Pluralize("")) 14 | } 15 | 16 | func TestUncountables(t *testing.T) { 17 | for word := range Uncountables() { 18 | assertEqual(t, word, Singularize(word)) 19 | assertEqual(t, word, Pluralize(word)) 20 | assertEqual(t, Pluralize(word), Singularize(word)) 21 | } 22 | } 23 | 24 | func TestUncountableWordIsNotGreedy(t *testing.T) { 25 | uncountableWord := "ors" 26 | countableWord := "sponsor" 27 | 28 | AddUncountable(uncountableWord) 29 | 30 | assertEqual(t, uncountableWord, Singularize(uncountableWord)) 31 | assertEqual(t, uncountableWord, Pluralize(uncountableWord)) 32 | assertEqual(t, Pluralize(uncountableWord), Singularize(uncountableWord)) 33 | assertEqual(t, "sponsor", Singularize(countableWord)) 34 | assertEqual(t, "sponsors", Pluralize(countableWord)) 35 | assertEqual(t, "sponsor", Singularize(Pluralize(countableWord))) 36 | } 37 | 38 | func TestPluralizeSingular(t *testing.T) { 39 | for singular, plural := range SingularToPlural { 40 | assertEqual(t, plural, Pluralize(singular)) 41 | assertEqual(t, Capitalize(plural), Capitalize(Pluralize(singular))) 42 | } 43 | } 44 | 45 | func TestSingularizePlural(t *testing.T) { 46 | assertEqual(t, "", Singularize("")) 47 | 48 | for singular, plural := range SingularToPlural { 49 | assertEqual(t, singular, Singularize(plural)) 50 | assertEqual(t, Capitalize(singular), Capitalize(Singularize(plural))) 51 | } 52 | } 53 | 54 | func TestPluralizePlural(t *testing.T) { 55 | for _, plural := range SingularToPlural { 56 | assertEqual(t, plural, Pluralize(plural)) 57 | assertEqual(t, Capitalize(plural), Capitalize(Pluralize(plural))) 58 | } 59 | } 60 | 61 | func TestOverwritePreviousInflectors(t *testing.T) { 62 | assertEqual(t, "series", Singularize("series")) 63 | AddSingular("series", "serie") 64 | assertEqual(t, "serie", Singularize("series")) 65 | AddUncountable("series") // reset 66 | AddPlural("wolf", "wolves") 67 | assertEqual(t, "wolves", Pluralize("wolf")) 68 | } 69 | 70 | func TestTitleize(t *testing.T) { 71 | for before, titleized := range MixtureToTitleCase { 72 | assertEqual(t, titleized, Titleize(before)) 73 | } 74 | } 75 | 76 | func TestCapitalize(t *testing.T) { 77 | for lower, capitalized := range CapitalizeMixture { 78 | assertEqual(t, capitalized, Capitalize(lower)) 79 | } 80 | } 81 | 82 | func TestCamelize(t *testing.T) { 83 | for camel, underscore := range CamelToUnderscore { 84 | assertEqual(t, camel, Camelize(underscore)) 85 | } 86 | } 87 | 88 | func TestCamelizeOtherSeparators(t *testing.T) { 89 | for camel, other := range CamelOthers { 90 | assertEqual(t, camel, Camelize(other)) 91 | } 92 | } 93 | 94 | func TestCamelizeWithLowerDowncasesTheFirstLetter(t *testing.T) { 95 | assertEqual(t, "capital", CamelizeDownFirst("Capital")) 96 | } 97 | 98 | func TestCamelizeWithUnderscores(t *testing.T) { 99 | assertEqual(t, "CamelCase", Camelize("Camel_Case")) 100 | } 101 | 102 | // func TestAcronyms(t *testing.T) { 103 | // AddAcronym("API") 104 | // AddAcronym("HTML") 105 | // AddAcronym("HTTP") 106 | // AddAcronym("RESTful") 107 | // AddAcronym("W3C") 108 | // AddAcronym("PhD") 109 | // AddAcronym("RoR") 110 | // AddAcronym("SSL") 111 | // // each in table 112 | // for _,x := range AcronymCases { 113 | // assertEqual(t, x.camel, Camelize(x.under)) 114 | // assertEqual(t, x.camel, Camelize(x.camel)) 115 | // assertEqual(t, x.under, Underscore(x.under)) 116 | // assertEqual(t, x.under, Underscore(x.camel)) 117 | // assertEqual(t, x.title, Titleize(x.under)) 118 | // assertEqual(t, x.title, Titleize(x.camel)) 119 | // assertEqual(t, x.human, Humanize(x.under)) 120 | // } 121 | // } 122 | 123 | // func TestAcronymOverride(t *testing.T) { 124 | // AddAcronym("API") 125 | // AddAcronym("LegacyApi") 126 | // assertEqual(t, "LegacyApi", Camelize("legacyapi")) 127 | // assertEqual(t, "LegacyAPI", Camelize("legacy_api")) 128 | // assertEqual(t, "SomeLegacyApi", Camelize("some_legacyapi")) 129 | // assertEqual(t, "Nonlegacyapi", Camelize("nonlegacyapi")) 130 | // } 131 | 132 | // func TestAcronymsCamelizeLower(t *testing.T) { 133 | // AddAcronym("API") 134 | // AddAcronym("HTML") 135 | // assertEqual(t, "htmlAPI", CamelizeDownFirst("html_api")) 136 | // assertEqual(t, "htmlAPI", CamelizeDownFirst("htmlAPI")) 137 | // assertEqual(t, "htmlAPI", CamelizeDownFirst("HTMLAPI")) 138 | // } 139 | 140 | func TestUnderscoreAcronymSequence(t *testing.T) { 141 | AddAcronym("API") 142 | AddAcronym("HTML5") 143 | AddAcronym("HTML") 144 | assertEqual(t, "html5_html_api", Underscore("HTML5HTMLAPI")) 145 | } 146 | 147 | func TestUnderscore(t *testing.T) { 148 | for camel, underscore := range CamelToUnderscore { 149 | assertEqual(t, underscore, Underscore(camel)) 150 | } 151 | for camel, underscore := range CamelToUnderscoreWithoutReverse { 152 | assertEqual(t, underscore, Underscore(camel)) 153 | } 154 | } 155 | 156 | func TestForeignKey(t *testing.T) { 157 | for klass, foreignKey := range ClassNameToForeignKeyWithUnderscore { 158 | assertEqual(t, foreignKey, ForeignKey(klass)) 159 | } 160 | for word, foreignKey := range PluralToForeignKeyWithUnderscore { 161 | assertEqual(t, foreignKey, ForeignKey(word)) 162 | } 163 | for klass, foreignKey := range ClassNameToForeignKeyWithoutUnderscore { 164 | assertEqual(t, foreignKey, ForeignKeyCondensed(klass)) 165 | } 166 | } 167 | 168 | func TestTableize(t *testing.T) { 169 | for klass, table := range ClassNameToTableName { 170 | assertEqual(t, table, Tableize(klass)) 171 | } 172 | } 173 | 174 | func TestParameterize(t *testing.T) { 175 | for str, parameterized := range StringToParameterized { 176 | assertEqual(t, parameterized, Parameterize(str)) 177 | } 178 | } 179 | 180 | func TestParameterizeAndNormalize(t *testing.T) { 181 | for str, parameterized := range StringToParameterizedAndNormalized { 182 | assertEqual(t, parameterized, Parameterize(str)) 183 | } 184 | } 185 | 186 | func TestParameterizeWithCustomSeparator(t *testing.T) { 187 | for str, parameterized := range StringToParameterizeWithUnderscore { 188 | assertEqual(t, parameterized, ParameterizeJoin(str, "_")) 189 | } 190 | } 191 | 192 | func TestTypeify(t *testing.T) { 193 | for klass, table := range ClassNameToTableName { 194 | assertEqual(t, klass, Typeify(table)) 195 | assertEqual(t, klass, Typeify("table_prefix."+table)) 196 | } 197 | } 198 | 199 | func TestTypeifyWithLeadingSchemaName(t *testing.T) { 200 | assertEqual(t, "FooBar", Typeify("schema.foo_bar")) 201 | } 202 | 203 | func TestHumanize(t *testing.T) { 204 | for underscore, human := range UnderscoreToHuman { 205 | assertEqual(t, human, Humanize(underscore)) 206 | } 207 | } 208 | 209 | func TestHumanizeByString(t *testing.T) { 210 | AddHuman("col_rpted_bugs", "reported bugs") 211 | assertEqual(t, "90 reported bugs recently", Humanize("90 col_rpted_bugs recently")) 212 | } 213 | 214 | func TestOrdinal(t *testing.T) { 215 | for number, ordinalized := range OrdinalNumbers { 216 | assertEqual(t, ordinalized, Ordinalize(number)) 217 | } 218 | 219 | t.Run("should not ordinalize when not a number", func(t *testing.T) { 220 | const s = "not_a_number" 221 | assertEqual(t, s, Ordinalize(s)) 222 | }) 223 | } 224 | 225 | func TestAsciify(t *testing.T) { 226 | const s, expected = "àçéä", "acea" 227 | assertEqual(t, expected, Asciify(s)) 228 | } 229 | 230 | func TestDasherize(t *testing.T) { 231 | for underscored, dasherized := range UnderscoresToDashes { 232 | assertEqual(t, dasherized, Dasherize(underscored)) 233 | } 234 | } 235 | 236 | func TestUnderscoreAsReverseOfDasherize(t *testing.T) { 237 | for underscored := range UnderscoresToDashes { 238 | assertEqual(t, underscored, Underscore(Dasherize(underscored))) 239 | } 240 | } 241 | 242 | func TestUnderscoreToLowerCamel(t *testing.T) { 243 | for underscored, lower := range UnderscoreToLowerCamel { 244 | assertEqual(t, lower, CamelizeDownFirst(underscored)) 245 | } 246 | } 247 | 248 | /* 249 | func Test_clear_all(t *testing.T) { 250 | // test a way of resetting inflexions 251 | } 252 | */ 253 | 254 | func TestIrregularityBetweenSingularAndPlural(t *testing.T) { 255 | for singular, plural := range Irregularities { 256 | AddIrregular(singular, plural) 257 | assertEqual(t, singular, Singularize(plural)) 258 | assertEqual(t, plural, Pluralize(singular)) 259 | } 260 | } 261 | 262 | func TestPluralizeOfIrregularity(t *testing.T) { 263 | for singular, plural := range Irregularities { 264 | AddIrregular(singular, plural) 265 | assertEqual(t, plural, Pluralize(plural)) 266 | } 267 | } 268 | 269 | // assert helper 270 | func assertEqual(t *testing.T, a, b string) { 271 | t.Helper() 272 | if a != b { 273 | t.Errorf("inflect: expected %v got %v", a, b) 274 | } 275 | } 276 | 277 | // test data 278 | 279 | var SingularToPlural = map[string]string{ 280 | "search": "searches", 281 | "switch": "switches", 282 | "fix": "fixes", 283 | "box": "boxes", 284 | "process": "processes", 285 | "address": "addresses", 286 | "case": "cases", 287 | "stack": "stacks", 288 | "wish": "wishes", 289 | "fish": "fish", 290 | "jeans": "jeans", 291 | "funky jeans": "funky jeans", 292 | "category": "categories", 293 | "query": "queries", 294 | "ability": "abilities", 295 | "agency": "agencies", 296 | "movie": "movies", 297 | "archive": "archives", 298 | "index": "indices", 299 | "wife": "wives", 300 | "safe": "saves", 301 | "half": "halves", 302 | "move": "moves", 303 | "salesperson": "salespeople", 304 | "person": "people", 305 | "spokesman": "spokesmen", 306 | "man": "men", 307 | "woman": "women", 308 | "basis": "bases", 309 | "diagnosis": "diagnoses", 310 | "diagnosis_a": "diagnosis_as", 311 | "datum": "data", 312 | "medium": "media", 313 | "stadium": "stadia", 314 | "analysis": "analyses", 315 | "node_child": "node_children", 316 | "child": "children", 317 | "experience": "experiences", 318 | "day": "days", 319 | "comment": "comments", 320 | "foobar": "foobars", 321 | "newsletter": "newsletters", 322 | "old_news": "old_news", 323 | "news": "news", 324 | "series": "series", 325 | "species": "species", 326 | "quiz": "quizzes", 327 | "perspective": "perspectives", 328 | "ox": "oxen", 329 | "photo": "photos", 330 | "buffalo": "buffaloes", 331 | "tomato": "tomatoes", 332 | "dwarf": "dwarves", 333 | "elf": "elves", 334 | "information": "information", 335 | "equipment": "equipment", 336 | "bus": "buses", 337 | "status": "statuses", 338 | "status_code": "status_codes", 339 | "mouse": "mice", 340 | "louse": "lice", 341 | "house": "houses", 342 | "octopus": "octopi", 343 | "virus": "viri", 344 | "alias": "aliases", 345 | "portfolio": "portfolios", 346 | "vertex": "vertices", 347 | "matrix": "matrices", 348 | "matrix_fu": "matrix_fus", 349 | "axis": "axes", 350 | "testis": "testes", 351 | "crisis": "crises", 352 | "rice": "rice", 353 | "shoe": "shoes", 354 | "horse": "horses", 355 | "prize": "prizes", 356 | "edge": "edges", 357 | "database": "databases", 358 | } 359 | 360 | var CapitalizeMixture = map[string]string{ 361 | //expected: test case 362 | "product": "Product", 363 | "special_guest": "Special_guest", 364 | "applicationController": "ApplicationController", 365 | "Area51Controller": "Area51Controller", 366 | } 367 | 368 | var CamelToUnderscore = map[string]string{ 369 | "Product": "product", 370 | "SpecialGuest": "special_guest", 371 | "ApplicationController": "application_controller", 372 | "Area51Controller": "area51_controller", 373 | } 374 | 375 | var CamelOthers = map[string]string{ 376 | // other separators 377 | "BlankController": "blank controller", 378 | "SpaceController": "space\tcontroller", 379 | "SeparatorController": "separator:controller", 380 | } 381 | 382 | var UnderscoreToLowerCamel = map[string]string{ 383 | "product": "product", 384 | "special_guest": "specialGuest", 385 | "application_controller": "applicationController", 386 | "area51_controller": "area51Controller", 387 | } 388 | 389 | var CamelToUnderscoreWithoutReverse = map[string]string{ 390 | "HTMLTidy": "html_tidy", 391 | "HTMLTidyGenerator": "html_tidy_generator", 392 | "FreeBsd": "free_bsd", 393 | "HTML": "html", 394 | } 395 | 396 | var ClassNameToForeignKeyWithUnderscore = map[string]string{ 397 | "Person": "person_id", 398 | "Account": "account_id", 399 | } 400 | 401 | var PluralToForeignKeyWithUnderscore = map[string]string{ 402 | "people": "person_id", 403 | "accounts": "account_id", 404 | } 405 | 406 | var ClassNameToForeignKeyWithoutUnderscore = map[string]string{ 407 | "Person": "personid", 408 | "Account": "accountid", 409 | } 410 | 411 | var ClassNameToTableName = map[string]string{ 412 | "PrimarySpokesman": "primary_spokesmen", 413 | "NodeChild": "node_children", 414 | } 415 | 416 | var StringToParameterized = map[string]string{ 417 | "Donald E. Knuth": "donald-e-knuth", 418 | "Random text with *(bad)* characters": "random-text-with-bad-characters", 419 | "Allow_Under_Scores": "allow_under_scores", 420 | "Trailing bad characters!@#": "trailing-bad-characters", 421 | "!@#Leading bad characters": "leading-bad-characters", 422 | "Squeeze separators": "squeeze-separators", 423 | "Test with + sign": "test-with-sign", 424 | "Test with malformed utf8 \251": "test-with-malformed-utf8", 425 | } 426 | 427 | var StringToParameterizeWithNoSeparator = map[string]string{ 428 | "Donald E. Knuth": "donaldeknuth", 429 | "With-some-dashes": "with-some-dashes", 430 | "Random text with *(bad)* characters": "randomtextwithbadcharacters", 431 | "Trailing bad characters!@#": "trailingbadcharacters", 432 | "!@#Leading bad characters": "leadingbadcharacters", 433 | "Squeeze separators": "squeezeseparators", 434 | "Test with + sign": "testwithsign", 435 | "Test with malformed utf8 \251": "testwithmalformedutf8", 436 | } 437 | 438 | var StringToParameterizeWithUnderscore = map[string]string{ 439 | "Donald E. Knuth": "donald_e_knuth", 440 | "Random text with *(bad)* characters": "random_text_with_bad_characters", 441 | "With-some-dashes": "with-some-dashes", 442 | "Retain_underscore": "retain_underscore", 443 | "Trailing bad characters!@#": "trailing_bad_characters", 444 | "!@#Leading bad characters": "leading_bad_characters", 445 | "Squeeze separators": "squeeze_separators", 446 | "Test with + sign": "test_with_sign", 447 | "Test with malformed utf8 \251": "test_with_malformed_utf8", 448 | } 449 | 450 | var StringToParameterizedAndNormalized = map[string]string{ 451 | "Malmö": "malmo", 452 | "Garçons": "garcons", 453 | "Opsů": "opsu", 454 | "Ærøskøbing": "aeroskobing", 455 | "Aßlar": "asslar", 456 | "Japanese: 日本語": "japanese", 457 | } 458 | 459 | var UnderscoreToHuman = map[string]string{ 460 | "employee_salary": "Employee salary", 461 | "employee_id": "Employee", 462 | "underground": "Underground", 463 | } 464 | 465 | var MixtureToTitleCase = map[string]string{ 466 | "active_record": "Active Record", 467 | "ActiveRecord": "Active Record", 468 | "action web service": "Action Web Service", 469 | "Action Web Service": "Action Web Service", 470 | "Action web service": "Action Web Service", 471 | "actionwebservice": "Actionwebservice", 472 | "Actionwebservice": "Actionwebservice", 473 | "david's code": "David's Code", 474 | "David's code": "David's Code", 475 | "david's Code": "David's Code", 476 | } 477 | 478 | var OrdinalNumbers = map[string]string{ 479 | "-1": "-1st", 480 | "-2": "-2nd", 481 | "-3": "-3rd", 482 | "-4": "-4th", 483 | "-5": "-5th", 484 | "-6": "-6th", 485 | "-7": "-7th", 486 | "-8": "-8th", 487 | "-9": "-9th", 488 | "-10": "-10th", 489 | "-11": "-11th", 490 | "-12": "-12th", 491 | "-13": "-13th", 492 | "-14": "-14th", 493 | "-20": "-20th", 494 | "-21": "-21st", 495 | "-22": "-22nd", 496 | "-23": "-23rd", 497 | "-24": "-24th", 498 | "-100": "-100th", 499 | "-101": "-101st", 500 | "-102": "-102nd", 501 | "-103": "-103rd", 502 | "-104": "-104th", 503 | "-110": "-110th", 504 | "-111": "-111th", 505 | "-112": "-112th", 506 | "-113": "-113th", 507 | "-1000": "-1000th", 508 | "-1001": "-1001st", 509 | "0": "0th", 510 | "1": "1st", 511 | "2": "2nd", 512 | "3": "3rd", 513 | "4": "4th", 514 | "5": "5th", 515 | "6": "6th", 516 | "7": "7th", 517 | "8": "8th", 518 | "9": "9th", 519 | "10": "10th", 520 | "11": "11th", 521 | "12": "12th", 522 | "13": "13th", 523 | "14": "14th", 524 | "20": "20th", 525 | "21": "21st", 526 | "22": "22nd", 527 | "23": "23rd", 528 | "24": "24th", 529 | "100": "100th", 530 | "101": "101st", 531 | "102": "102nd", 532 | "103": "103rd", 533 | "104": "104th", 534 | "110": "110th", 535 | "111": "111th", 536 | "112": "112th", 537 | "113": "113th", 538 | "1000": "1000th", 539 | "1001": "1001st", 540 | } 541 | 542 | var UnderscoresToDashes = map[string]string{ 543 | "street": "street", 544 | "street_address": "street-address", 545 | "person_street_address": "person-street-address", 546 | } 547 | 548 | var Irregularities = map[string]string{ 549 | "person": "people", 550 | "man": "men", 551 | "child": "children", 552 | "sex": "sexes", 553 | "move": "moves", 554 | } 555 | 556 | type AcronymCase struct { 557 | camel string 558 | under string 559 | human string 560 | title string 561 | } 562 | 563 | var AcronymCases = []*AcronymCase{ 564 | // camelize underscore humanize titleize 565 | {camel: "API", under: "api", human: "API", title: "API"}, 566 | {"APIController", "api_controller", "API controller", "API Controller"}, 567 | {"Nokogiri::HTML", "nokogiri/html", "Nokogiri/HTML", "Nokogiri/HTML"}, 568 | {"HTTPAPI", "http_api", "HTTP API", "HTTP API"}, 569 | {"HTTP::Get", "http/get", "HTTP/get", "HTTP/Get"}, 570 | {"SSLError", "ssl_error", "SSL error", "SSL Error"}, 571 | {"RESTful", "restful", "RESTful", "RESTful"}, 572 | {"RESTfulController", "restful_controller", "RESTful controller", "RESTful Controller"}, 573 | {"IHeartW3C", "i_heart_w3c", "I heart W3C", "I Heart W3C"}, 574 | {"PhDRequired", "phd_required", "PhD required", "PhD Required"}, 575 | {"IRoRU", "i_ror_u", "I RoR u", "I RoR U"}, 576 | {"RESTfulHTTPAPI", "restful_http_api", "RESTful HTTP API", "RESTful HTTP API"}, 577 | // misdirection 578 | {"Capistrano", "capistrano", "Capistrano", "Capistrano"}, 579 | {"CapiController", "capi_controller", "Capi controller", "Capi Controller"}, 580 | {"HttpsApis", "https_apis", "Https apis", "Https Apis"}, 581 | {"Html5", "html5", "Html5", "Html5"}, 582 | {"Restfully", "restfully", "Restfully", "Restfully"}, 583 | {"RoRails", "ro_rails", "Ro rails", "Ro Rails"}, 584 | } 585 | --------------------------------------------------------------------------------