├── .coveragerc ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── NEWS.rst ├── README.rst ├── SECURITY.md ├── docs ├── conf.py ├── history.rst └── index.rst ├── inflect ├── __init__.py ├── compat │ ├── __init__.py │ └── py38.py └── py.typed ├── mypy.ini ├── pyproject.toml ├── pytest.ini ├── ruff.toml ├── tea.yaml ├── tests ├── inflections.txt ├── test_an.py ├── test_classical_all.py ├── test_classical_ancient.py ├── test_classical_herd.py ├── test_classical_names.py ├── test_classical_person.py ├── test_classical_zero.py ├── test_compounds.py ├── test_inflections.py ├── test_join.py ├── test_numwords.py ├── test_pl_si.py ├── test_pwd.py └── test_unicode.py ├── towncrier.toml └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | # leading `*/` for pytest-dev/pytest-cov#456 4 | */.tox/* 5 | disable_warnings = 6 | couldnt-parse 7 | 8 | [report] 9 | show_missing = True 10 | exclude_also = 11 | # Exclude common false positives per 12 | # https://coverage.readthedocs.io/en/latest/excluding.html#advanced-exclusion 13 | # Ref jaraco/skeleton#97 and jaraco/skeleton#135 14 | class .*\bProtocol\): 15 | if TYPE_CHECKING: 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | insert_final_newline = true 8 | end_of_line = lf 9 | 10 | [*.py] 11 | indent_style = space 12 | max_line_length = 88 13 | 14 | [*.{yml,yaml}] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.rst] 19 | indent_style = space 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: pypi/inflect 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | merge_group: 5 | push: 6 | branches-ignore: 7 | # temporary GH branches relating to merge queues (jaraco/skeleton#93) 8 | - gh-readonly-queue/** 9 | tags: 10 | # required if branches-ignore is supplied (jaraco/skeleton#103) 11 | - '**' 12 | pull_request: 13 | workflow_dispatch: 14 | 15 | permissions: 16 | contents: read 17 | 18 | env: 19 | # Environment variable to support color support (jaraco/skeleton#66) 20 | FORCE_COLOR: 1 21 | 22 | # Suppress noisy pip warnings 23 | PIP_DISABLE_PIP_VERSION_CHECK: 'true' 24 | PIP_NO_WARN_SCRIPT_LOCATION: 'true' 25 | 26 | # Ensure tests can sense settings about the environment 27 | TOX_OVERRIDE: >- 28 | testenv.pass_env+=GITHUB_*,FORCE_COLOR 29 | 30 | 31 | jobs: 32 | test: 33 | strategy: 34 | # https://blog.jaraco.com/efficient-use-of-ci-resources/ 35 | matrix: 36 | python: 37 | - "3.9" 38 | - "3.13" 39 | platform: 40 | - ubuntu-latest 41 | - macos-latest 42 | - windows-latest 43 | include: 44 | - python: "3.10" 45 | platform: ubuntu-latest 46 | - python: "3.11" 47 | platform: ubuntu-latest 48 | - python: "3.12" 49 | platform: ubuntu-latest 50 | - python: "3.14" 51 | platform: ubuntu-latest 52 | - python: pypy3.10 53 | platform: ubuntu-latest 54 | - python: "3.x" 55 | platform: ubuntu-latest 56 | runs-on: ${{ matrix.platform }} 57 | continue-on-error: ${{ matrix.python == '3.14' }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | - name: Install build dependencies 61 | # Install dependencies for building packages on pre-release Pythons 62 | # jaraco/skeleton#161 63 | if: matrix.python == '3.14' && matrix.platform == 'ubuntu-latest' 64 | run: | 65 | sudo apt update 66 | sudo apt install -y libxml2-dev libxslt-dev 67 | - name: Setup Python 68 | uses: actions/setup-python@v5 69 | with: 70 | python-version: ${{ matrix.python }} 71 | allow-prereleases: true 72 | - name: Install tox 73 | run: python -m pip install tox 74 | - name: Run 75 | run: tox 76 | 77 | collateral: 78 | strategy: 79 | fail-fast: false 80 | matrix: 81 | job: 82 | - diffcov 83 | - docs 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v4 87 | with: 88 | fetch-depth: 0 89 | - name: Setup Python 90 | uses: actions/setup-python@v5 91 | with: 92 | python-version: 3.x 93 | - name: Install tox 94 | run: python -m pip install tox 95 | - name: Eval ${{ matrix.job }} 96 | run: tox -e ${{ matrix.job }} 97 | 98 | check: # This job does nothing and is only used for the branch protection 99 | if: always() 100 | 101 | needs: 102 | - test 103 | - collateral 104 | 105 | runs-on: ubuntu-latest 106 | 107 | steps: 108 | - name: Decide whether the needed jobs succeeded or failed 109 | uses: re-actors/alls-green@release/v1 110 | with: 111 | jobs: ${{ toJSON(needs) }} 112 | 113 | release: 114 | permissions: 115 | contents: write 116 | needs: 117 | - check 118 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 119 | runs-on: ubuntu-latest 120 | 121 | steps: 122 | - uses: actions/checkout@v4 123 | - name: Setup Python 124 | uses: actions/setup-python@v5 125 | with: 126 | python-version: 3.x 127 | - name: Install tox 128 | run: python -m pip install tox 129 | - name: Run 130 | run: tox -e release 131 | env: 132 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | MANIFEST 3 | dist/* 4 | cover/* 5 | htmlcov/* 6 | .tox/* 7 | *.egg-info 8 | .coverage 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.9.9 4 | hooks: 5 | - id: ruff 6 | args: [--fix, --unsafe-fixes] 7 | - id: ruff-format 8 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | python: 3 | install: 4 | - path: . 5 | extra_requirements: 6 | - doc 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | # required boilerplate readthedocs/readthedocs.org#10401 12 | build: 13 | os: ubuntu-lts-latest 14 | tools: 15 | python: latest 16 | # post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114 17 | jobs: 18 | post_checkout: 19 | - git fetch --unshallow || true 20 | -------------------------------------------------------------------------------- /NEWS.rst: -------------------------------------------------------------------------------- 1 | v7.5.0 2 | ====== 3 | 4 | Features 5 | -------- 6 | 7 | - Updated `ast` classes for Python 3.14 compatibility. (#225) 8 | 9 | 10 | v7.4.0 11 | ====== 12 | 13 | Features 14 | -------- 15 | 16 | - Handle a single apostrophe more gracefully. (#218) 17 | 18 | 19 | v7.3.1 20 | ====== 21 | 22 | Bugfixes 23 | -------- 24 | 25 | - Set minimum version of more-itertools to 8.5 (#215) 26 | 27 | 28 | v7.3.0 29 | ====== 30 | 31 | Features 32 | -------- 33 | 34 | - Restricted typing_extensions to Python 3.8. (#211) 35 | 36 | 37 | v7.2.1 38 | ====== 39 | 40 | Bugfixes 41 | -------- 42 | 43 | - Refactored number_to_words toward reduced complexity. 44 | 45 | 46 | v7.2.0 47 | ====== 48 | 49 | Features 50 | -------- 51 | 52 | - Replace pydantic with typeguard (#195) 53 | 54 | 55 | v7.1.0 56 | ====== 57 | 58 | Features 59 | -------- 60 | 61 | - Now handle 'pair of x' in pl_sb_uninflected_complete (#188) 62 | 63 | 64 | v7.0.0 65 | ====== 66 | 67 | Features 68 | -------- 69 | 70 | - Refine type hint for ``singular_noun`` to indicate a literal return type for ``False``. (#186) 71 | 72 | 73 | Deprecations and Removals 74 | ------------------------- 75 | 76 | - Removed methods renamed in 0.2.0. 77 | 78 | 79 | v6.2.0 80 | ====== 81 | 82 | Features 83 | -------- 84 | 85 | - Project now supports Pydantic 2 while retaining support for Pydantic 1. (#187) 86 | 87 | 88 | Bugfixes 89 | -------- 90 | 91 | - Added validation of user-defined words and amended the type declarations to match, allowing for null values but not empty strings. (#187) 92 | 93 | 94 | v6.1.1 95 | ====== 96 | 97 | Bugfixes 98 | -------- 99 | 100 | - ``ordinal`` now handles float types correctly without first coercing them to strings. (#178) 101 | 102 | 103 | v6.1.0 104 | ====== 105 | 106 | Features 107 | -------- 108 | 109 | - Require Python 3.8 or later. 110 | 111 | 112 | v6.0.5 113 | ====== 114 | 115 | * #187: Pin to Pydantic 1 to avoid breaking in Pydantic 2. 116 | 117 | v6.0.4 118 | ====== 119 | 120 | * Internal cleanup. 121 | 122 | v6.0.3 123 | ====== 124 | 125 | * #136: A/an support now more correctly honors leading 126 | capitalized words and abbreviations. 127 | 128 | * #178: Improve support for ordinals for floats. 129 | 130 | v6.0.2 131 | ====== 132 | 133 | * #169: Require pydantic 1.9.1 to avoid ``ValueError``. 134 | 135 | v6.0.1 136 | ====== 137 | 138 | * Minor tweaks and packaging refresh. 139 | 140 | v6.0.0 141 | ====== 142 | 143 | * #157: ``compare`` methods now validate their inputs 144 | and will raise a more meaningful exception if an 145 | empty string or None is passed. This expectation is now 146 | documented. 147 | 148 | * Many public methods now perform validation on arguments. 149 | An empty string is no longer allowed for words or text. 150 | Callers are expected to pass non-empty text or trap 151 | the validation errors that are raised. The exceptions 152 | raised are ``pydantic.error_wrappers.ValidationError``, 153 | which are currently a subclass of ``ValueError``, but since 154 | that 155 | `may change `_, 156 | tests check for a generic ``Exception``. 157 | 158 | v5.6.2 159 | ====== 160 | 161 | * #15: Fixes to plural edge case handling. 162 | 163 | v5.6.1 164 | ====== 165 | 166 | * Packaging refresh and docs update. 167 | 168 | v5.6.0 169 | ====== 170 | 171 | * #153: Internal refactor to simplify and unify 172 | ``_plnoun`` and ``_sinoun``. 173 | 174 | v5.5.2 175 | ====== 176 | 177 | * Fixed badges. 178 | 179 | v5.5.1 180 | ====== 181 | 182 | * #150: Rewrite to satisfy type checkers. 183 | 184 | v5.5.0 185 | ====== 186 | 187 | * #147: Enhanced type annotations. 188 | 189 | v5.4.0 190 | ====== 191 | 192 | * #133: Add a ``py.typed`` file so mypy recognizes type annotations. 193 | * Misc fixes in #128, #134, #135, #137, #138, #139, #140, #142, 194 | #143, #144. 195 | * Require Python 3.7 or later. 196 | 197 | v5.3.0 198 | ====== 199 | 200 | * #108: Add support for pluralizing open compound nouns. 201 | 202 | v5.2.0 203 | ====== 204 | 205 | * #121: Modernized the codebase. Added a lot of type annotations. 206 | 207 | v5.1.0 208 | ====== 209 | 210 | * #113: Add support for uncountable nouns. 211 | 212 | v5.0.3 213 | ====== 214 | 215 | * Refreshed package metadata. 216 | 217 | v5.0.2 218 | ====== 219 | 220 | * #102: Inflect withdraws from `Jazzband `_ 221 | in order to continue to participate in sustained maintenance 222 | and enterprise support through `Tidelift `_. 223 | The project continues to honor the guidelines and principles 224 | behind Jazzband and welcomes contributors openly. 225 | 226 | v5.0.1 227 | ====== 228 | 229 | * Identical release validating release process. 230 | 231 | v5.0.0 232 | ====== 233 | 234 | * Module no longer exposes a ``__version__`` attribute. Instead 235 | to query the version installed, use 236 | `importlib.metadata `_ 237 | or `its backport `_ 238 | to query:: 239 | 240 | importlib.metadata.version('inflect') 241 | 242 | v4.1.1 243 | ====== 244 | 245 | * Refreshed package metadata. 246 | 247 | v4.1.0 248 | ====== 249 | 250 | * #95: Certain operations now allow ignore arbitrary leading words. 251 | 252 | v4.0.0 253 | ====== 254 | 255 | * Require Python 3.6 or later. 256 | 257 | v3.0.2 258 | ====== 259 | 260 | * #88: Distribution no longer includes root ``tests`` package. 261 | 262 | v3.0.1 263 | ====== 264 | 265 | * Project now builds on jaraco/skeleton for shared package 266 | management. 267 | 268 | v3.0.0 269 | ====== 270 | 271 | * #75: Drop support for Python 3.4. 272 | 273 | v2.1.0 274 | ====== 275 | 276 | * #29: Relicensed under the more permissive MIT License. 277 | 278 | v2.0.1 279 | ====== 280 | 281 | * #57: Fix pluralization of taco. 282 | 283 | v2.0.0 284 | ====== 285 | 286 | * #37: fix inconsistencies with the inflect method 287 | 288 | We now build and parse AST to extract function arguments instead of relying 289 | on regular expressions. This also adds support for keyword arguments and 290 | built-in constants when calling functions in the string. 291 | Unfortunately, this is not backwards compatible in some cases: 292 | * Strings should now be wrapped in single or double quotes 293 | p.inflect("singular_noun(to them)") should now be p.inflect("singular_noun('to them')") 294 | * Empty second argument to a function will now be parsed as None instead of ''. 295 | p.inflect("num(%d,) eggs" % 2) now prints "2 eggs" instead of " eggs" 296 | Since None, True and False are now supported, they can be passed explicitly: 297 | p.inflect("num(%d, False) eggs" % 2) will print " eggs" 298 | p.inflect("num(%d, True) eggs" % 2) will print "2 eggs" 299 | 300 | v1.0.2 301 | ====== 302 | 303 | * #53: Improved unicode handling. 304 | * #5 and #40 via #55: Fix capitalization issues in processes where 305 | more than one word is involved. 306 | * #56: Handle correctly units containing 'degree' and 'per'. 307 | 308 | v1.0.1 309 | ====== 310 | 311 | * #31: fix extraneous close parentheses. 312 | 313 | v1.0.0 314 | ====== 315 | 316 | * Dropped support for Python 3.3. 317 | 318 | v0.3.1 319 | ====== 320 | 321 | * Fixed badges in readme. 322 | 323 | v0.3.0 324 | ====== 325 | 326 | * Moved hosting to the `jazzband project on GitHub `_. 327 | 328 | v0.2.5 329 | ====== 330 | 331 | * Fixed TypeError while parsing compounds (by yavarhusain) 332 | * Fixed encoding issue in setup.py on Python 3 333 | 334 | 335 | v0.2.4 336 | ====== 337 | 338 | * new maintainer (Alex Grönholm) 339 | * added Python 3 compatibility (by Thorben Krüger) 340 | 341 | 342 | v0.2.3 343 | ====== 344 | 345 | * fix a/an for dishonor, Honolulu, mpeg, onetime, Ugandan, Ukrainian, 346 | Unabomber, unanimous, US 347 | * merge in 'subspecies' fix by UltraNurd 348 | * add arboretum to classical plurals 349 | * prevent crash with singular_noun('ys') 350 | 351 | 352 | v0.2.2 353 | ====== 354 | 355 | * change numwords to number_to_words in strings 356 | * improve some docstrings 357 | * comment out imports for unused .inflectrc 358 | * remove unused exception class 359 | 360 | 361 | v0.2.1 362 | ====== 363 | 364 | * remove incorrect gnome_sudoku import 365 | 366 | 367 | v0.2.0 368 | ====== 369 | 370 | * add gender() to select the gender of singular pronouns 371 | * replace short named methods with longer methods. shorted method now print a message and raise DecrecationWarning 372 | 373 | pl -> plural 374 | 375 | plnoun -> plural_noun 376 | 377 | plverb -> plural_verb 378 | 379 | pladj -> plural_adjective 380 | 381 | sinoun -> singular_noun 382 | 383 | prespart -> present_participle 384 | 385 | numwords -> number_to_words 386 | 387 | plequal -> compare 388 | 389 | plnounequal -> compare_nouns 390 | 391 | plverbequal -> compare_verbs 392 | 393 | pladjequal -> compare_adjs 394 | 395 | wordlist -> join 396 | * change classical() to only accept keyword args: only one way to do it 397 | * fix bug in numwords where hundreds was giving the wrong number when group=3 398 | 399 | 400 | v0.1.8 401 | ====== 402 | 403 | * add line to setup showing that this provides 'inflect' so that 404 | inflect_dj can require it 405 | * add the rest of the tests from the Perl version 406 | 407 | 408 | v0.1.7 409 | ====== 410 | 411 | * replace most of the regular expressions in _plnoun and _sinoun. They run several times faster now. 412 | 413 | 414 | v0.1.6 415 | ====== 416 | 417 | * add method sinoun() to generate the singular of a plural noun. Phew! 418 | * add changes from new Perl version: 1.892 419 | * start adding tests from Perl version 420 | * add test to check sinoun(plnoun(word)) == word 421 | Can now use word lists to check these methods without needing to have 422 | a list of plurals. ;-) 423 | * fix die -> dice 424 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/inflect.svg 2 | :target: https://pypi.org/project/inflect 3 | 4 | .. image:: https://img.shields.io/pypi/pyversions/inflect.svg 5 | 6 | .. image:: https://github.com/jaraco/inflect/actions/workflows/main.yml/badge.svg 7 | :target: https://github.com/jaraco/inflect/actions?query=workflow%3A%22tests%22 8 | :alt: tests 9 | 10 | .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json 11 | :target: https://github.com/astral-sh/ruff 12 | :alt: Ruff 13 | 14 | .. image:: https://readthedocs.org/projects/inflect/badge/?version=latest 15 | :target: https://inflect.readthedocs.io/en/latest/?badge=latest 16 | 17 | .. image:: https://img.shields.io/badge/skeleton-2025-informational 18 | :target: https://blog.jaraco.com/skeleton 19 | 20 | .. image:: https://tidelift.com/badges/package/pypi/inflect 21 | :target: https://tidelift.com/subscription/pkg/pypi-inflect?utm_source=pypi-inflect&utm_medium=readme 22 | 23 | NAME 24 | ==== 25 | 26 | inflect.py - Accurately generate plurals, singular nouns, ordinals, indefinite articles, and word-based representations of numbers. This functionality is limited to English. 27 | 28 | SYNOPSIS 29 | ======== 30 | 31 | .. code-block:: python 32 | 33 | >>> import inflect 34 | >>> p = inflect.engine() 35 | 36 | Simple example with pluralization and word-representation of numbers: 37 | 38 | .. code-block:: python 39 | 40 | >>> count=1 41 | >>> print('There', p.plural_verb('was', count), p.number_to_words(count), p.plural_noun('person', count), 'by the door.') 42 | There was one person by the door. 43 | 44 | When ``count=243``, the same code will generate: 45 | 46 | .. code-block:: python 47 | 48 | There were two hundred and forty-three people by the door. 49 | 50 | 51 | Methods 52 | ======= 53 | 54 | - ``plural``, ``plural_noun``, ``plural_verb``, ``plural_adj``, ``singular_noun``, ``no``, ``num`` 55 | - ``compare``, ``compare_nouns``, ``compare_nouns``, ``compare_adjs`` 56 | - ``a``, ``an`` 57 | - ``present_participle`` 58 | - ``ordinal``, ``number_to_words`` 59 | - ``join`` 60 | - ``inflect``, ``classical``, ``gender`` 61 | - ``defnoun``, ``defverb``, ``defadj``, ``defa``, ``defan`` 62 | 63 | Plurality/Singularity 64 | --------------------- 65 | Unconditionally Form the Plural 66 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 67 | 68 | .. code-block:: python 69 | 70 | >>> "the plural of person is " + p.plural("person") 71 | 'the plural of person is people' 72 | 73 | Conditionally Form the Plural 74 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 75 | 76 | .. code-block:: python 77 | 78 | >>> "the plural of 1 person is " + p.plural("person", 1) 79 | 'the plural of 1 person is person' 80 | 81 | Form Plurals for Specific Parts of Speech 82 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 83 | 84 | .. code-block:: python 85 | 86 | >>> p.plural_noun("I", 2) 87 | 'we' 88 | >>> p.plural_verb("saw", 1) 89 | 'saw' 90 | >>> p.plural_adj("my", 2) 91 | 'our' 92 | >>> p.plural_noun("saw", 2) 93 | 'saws' 94 | 95 | Form the Singular of Plural Nouns 96 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 97 | 98 | .. code-block:: python 99 | 100 | >>> "The singular of people is " + p.singular_noun("people") 101 | 'The singular of people is person' 102 | 103 | Select the Gender of Singular Pronouns 104 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 105 | 106 | .. code-block:: python 107 | 108 | >>> p.singular_noun("they") 109 | 'it' 110 | >>> p.gender("feminine") 111 | >>> p.singular_noun("they") 112 | 'she' 113 | 114 | Deal with "0/1/N" -> "no/1/N" Translation 115 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 116 | 117 | .. code-block:: python 118 | 119 | >>> errors = 1 120 | >>> "There ", p.plural_verb("was", errors), p.no(" error", errors) 121 | ('There ', 'was', ' 1 error') 122 | >>> errors = 2 123 | >>> "There ", p.plural_verb("was", errors), p.no(" error", errors) 124 | ('There ', 'were', ' 2 errors') 125 | 126 | Use Default Counts 127 | ^^^^^^^^^^^^^^^^^^ 128 | 129 | .. code-block:: python 130 | 131 | >>> p.num(1, "") 132 | '' 133 | >>> p.plural("I") 134 | 'I' 135 | >>> p.plural_verb(" saw") 136 | ' saw' 137 | >>> p.num(2) 138 | '2' 139 | >>> p.plural_noun(" saw") 140 | ' saws' 141 | >>> "There ", p.num(errors, ""), p.plural_verb("was"), p.no(" error") 142 | ('There ', '', 'were', ' 2 errors') 143 | 144 | Compare Two Words Number-Intensitively 145 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 146 | 147 | .. code-block:: python 148 | 149 | >>> p.compare('person', 'person') 150 | 'eq' 151 | >>> p.compare('person', 'people') 152 | 's:p' 153 | >>> p.compare_nouns('person', 'people') 154 | 's:p' 155 | >>> p.compare_verbs('run', 'ran') 156 | False 157 | >>> p.compare_verbs('run', 'running') 158 | False 159 | >>> p.compare_verbs('run', 'run') 160 | 'eq' 161 | >>> p.compare_adjs('my', 'mine') 162 | False 163 | >>> p.compare_adjs('my', 'our') 164 | 's:p' 165 | 166 | Add Correct *a* or *an* for a Given Word 167 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 168 | 169 | .. code-block:: python 170 | 171 | >>> "Did you want ", p.a('thing'), " or ", p.a('idea') 172 | ('Did you want ', 'a thing', ' or ', 'an idea') 173 | 174 | Convert Numerals into Ordinals 175 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 176 | 177 | .. code-block:: python 178 | 179 | >>> "It was", p.ordinal(1), " from the left" 180 | ('It was', '1st', ' from the left') 181 | >>> "It was", p.ordinal(2), " from the left" 182 | ('It was', '2nd', ' from the left') 183 | >>> "It was", p.ordinal(3), " from the left" 184 | ('It was', '3rd', ' from the left') 185 | >>> "It was", p.ordinal(347), " from the left" 186 | ('It was', '347th', ' from the left') 187 | 188 | Convert Numerals to Words 189 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 190 | Note: This returns a single string. 191 | 192 | .. code-block:: python 193 | 194 | >>> p.number_to_words(1) 195 | 'one' 196 | >>> p.number_to_words(38) 197 | 'thirty-eight' 198 | >>> p.number_to_words(1234) 199 | 'one thousand, two hundred and thirty-four' 200 | >>> p.number_to_words(p.ordinal(1234)) 201 | 'one thousand, two hundred and thirty-fourth' 202 | 203 | Retrieve Words as List of Parts 204 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 205 | 206 | .. code-block:: python 207 | 208 | >>> p.number_to_words(1234, wantlist=True) 209 | ['one thousand', 'two hundred and thirty-four'] 210 | 211 | Grouping Options 212 | ^^^^^^^^^^^^^^^^ 213 | 214 | .. code-block:: python 215 | 216 | >>> p.number_to_words(12345, group=1) 217 | 'one, two, three, four, five' 218 | >>> p.number_to_words(12345, group=2) 219 | 'twelve, thirty-four, five' 220 | >>> p.number_to_words(12345, group=3) 221 | 'one twenty-three, forty-five' 222 | >>> p.number_to_words(1234, andword="") 223 | 'one thousand, two hundred thirty-four' 224 | >>> p.number_to_words(1234, andword=", plus") 225 | 'one thousand, two hundred, plus thirty-four' 226 | >>> p.number_to_words(555_1202, group=1, zero="oh") 227 | 'five, five, five, one, two, oh, two' 228 | >>> p.number_to_words(555_1202, group=1, one="unity") 229 | 'five, five, five, unity, two, zero, two' 230 | >>> p.number_to_words(123.456, group=1, decimal="mark") 231 | 'one, two, three, mark, four, five, six' 232 | 233 | Apply Threshold for Word-Representation of Numbers 234 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 235 | Above provided threshold, numberals will remain numerals 236 | 237 | .. code-block:: python 238 | 239 | >>> p.number_to_words(9, threshold=10) 240 | 'nine' 241 | >>> p.number_to_words(10, threshold=10) 242 | 'ten' 243 | >>> p.number_to_words(11, threshold=10) 244 | '11' 245 | >>> p.number_to_words(1000, threshold=10) 246 | '1,000' 247 | 248 | Join Words into a List 249 | ^^^^^^^^^^^^^^^^^^^^^^ 250 | 251 | .. code-block:: python 252 | 253 | >>> p.join(("apple", "banana", "carrot")) 254 | 'apple, banana, and carrot' 255 | >>> p.join(("apple", "banana")) 256 | 'apple and banana' 257 | >>> p.join(("apple", "banana", "carrot"), final_sep="") 258 | 'apple, banana and carrot' 259 | >>> p.join(('apples', 'bananas', 'carrots'), conj='and even') 260 | 'apples, bananas, and even carrots' 261 | >>> p.join(('apple', 'banana', 'carrot'), sep='/', sep_spaced=False, conj='', conj_spaced=False) 262 | 'apple/banana/carrot' 263 | 264 | Require Classical Plurals 265 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 266 | Adhere to conventions from Classical Latin and Classical Greek 267 | 268 | .. code-block:: python 269 | 270 | >>> p.classical() 271 | >>> p.plural_noun("focus", 2) 272 | 'foci' 273 | >>> p.plural_noun("cherubim", 2) 274 | 'cherubims' 275 | >>> p.plural_noun("cherub", 2) 276 | 'cherubim' 277 | 278 | Other options for classical plurals: 279 | 280 | .. code-block:: python 281 | 282 | p.classical(all=True) # USE ALL CLASSICAL PLURALS 283 | p.classical(all=False) # SWITCH OFF CLASSICAL MODE 284 | 285 | p.classical(zero=True) # "no error" INSTEAD OF "no errors" 286 | p.classical(zero=False) # "no errors" INSTEAD OF "no error" 287 | 288 | p.classical(herd=True) # "2 buffalo" INSTEAD OF "2 buffalos" 289 | p.classical(herd=False) # "2 buffalos" INSTEAD OF "2 buffalo" 290 | 291 | p.classical(persons=True) # "2 chairpersons" INSTEAD OF "2 chairpeople" 292 | p.classical(persons=False) # "2 chairpeople" INSTEAD OF "2 chairpersons" 293 | 294 | p.classical(ancient=True) # "2 formulae" INSTEAD OF "2 formulas" 295 | p.classical(ancient=False) # "2 formulas" INSTEAD OF "2 formulae" 296 | 297 | 298 | Support for interpolation 299 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 300 | Supports string interpolation with the following functions: ``plural()``, ``plural_noun()``, ``plural_verb()``, ``plural_adj()``, ``singular_noun()``, ``a()``, ``an()``, ``num()`` and ``ordinal()``. 301 | 302 | .. code-block:: python 303 | 304 | >>> p.inflect("The plural of {0} is plural('{0}')".format('car')) 305 | 'The plural of car is cars' 306 | >>> p.inflect("The singular of {0} is singular_noun('{0}')".format('car')) 307 | 'The singular of car is car' 308 | >>> p.inflect("I saw {0} plural('cat',{0})".format(3)) 309 | 'I saw 3 cats' 310 | >>> p.inflect( 311 | ... "plural('I',{0}) " 312 | ... "plural_verb('saw',{0}) " 313 | ... "plural('a',{1}) " 314 | ... "plural_noun('saw',{1})".format(1, 2) 315 | ... ) 316 | 'I saw some saws' 317 | >>> p.inflect( 318 | ... "num({0}, False)plural('I') " 319 | ... "plural_verb('saw') " 320 | ... "num({1}, False)plural('a') " 321 | ... "plural_noun('saw')".format(N1, 1) 322 | ... ) 323 | 'I saw a saw' 324 | >>> p.inflect( 325 | ... "num({0}, False)plural('I') " 326 | ... "plural_verb('saw') " 327 | ... "num({1}, False)plural('a') " 328 | ... "plural_noun('saw')".format(2, 2) 329 | ... ) 330 | 'we saw some saws' 331 | >>> p.inflect("I saw num({0}) plural('cat')\nnum()".format(cat_count)) 332 | 'I saw 3 cats\n' 333 | >>> p.inflect("There plural_verb('was',{0}) no('error',{0})".format(errors)) 334 | 'There were 2 errors' 335 | >>> p.inflect("There num({0}, False)plural_verb('was') no('error')".format(errors)) 336 | 'There were 2 errors' 337 | >>> p.inflect("Did you want a('{0}') or an('{1}')".format(thing, idea)) 338 | 'Did you want a thing or an idea' 339 | >>> p.inflect("It was ordinal('{0}') from the left".format(2)) 340 | 'It was 2nd from the left' 341 | 342 | Add User-Defined Inflections 343 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 344 | Allows for overriding default rules. 345 | 346 | Override noun defaults: 347 | 348 | .. code-block:: python 349 | 350 | p.defnoun("VAX", "VAXen") # SINGULAR => PLURAL 351 | 352 | Override Verb defaults: 353 | 354 | .. code-block:: python 355 | 356 | p.defverb( 357 | "will", # 1ST PERSON SINGULAR 358 | "shall", # 1ST PERSON PLURAL 359 | "will", # 2ND PERSON SINGULAR 360 | "will", # 2ND PERSON PLURAL 361 | "will", # 3RD PERSON SINGULAR 362 | "will", # 3RD PERSON PLURAL 363 | ) 364 | 365 | Override adjective defaults: 366 | 367 | .. code-block:: python 368 | 369 | >>> p.defadj('hir', 'their') 370 | 1 371 | >>> p.plural_adj('hir', 2) 372 | 'their' 373 | 374 | Override the words that use the indefinite articles "a" or "an": 375 | 376 | .. code-block:: python 377 | 378 | >>> p.a('ape', 1) 379 | 'an ape' 380 | >>> p.defa('a') 381 | 1 382 | >>> p.a('ape', 1) 383 | 'an ape' 384 | >>> p.defa('ape') 385 | 1 386 | >>> p.a('ape', 1) 387 | 'a ape' 388 | >>> p.defan('horrendous.*') 389 | 1 390 | >>> p.a('horrendous affectation', 1) 391 | 'an horrendous affectation' 392 | >>> 393 | 394 | 395 | DESCRIPTION 396 | =========== 397 | 398 | The methods of the class ``engine`` in module ``inflect.py`` provide plural 399 | inflections, singular noun inflections, "a"/"an" selection for English words, 400 | and manipulation of numbers as words. 401 | 402 | Plural forms of all nouns, most verbs, and some adjectives are 403 | provided. Where appropriate, "classical" variants (for example: "brother" -> 404 | "brethren", "dogma" -> "dogmata", etc.) are also provided. 405 | 406 | Single forms of nouns are also provided. The gender of singular pronouns 407 | can be chosen (for example "they" -> "it" or "she" or "he" or "they"). 408 | 409 | Pronunciation-based "a"/"an" selection is provided for all English 410 | words, and most initialisms. 411 | 412 | It is also possible to inflect numerals (1,2,3) to ordinals (1st, 2nd, 3rd) 413 | or to English words ("one", "two", "three"). 414 | 415 | In generating these inflections, ``inflect.py`` follows the Oxford 416 | English Dictionary and the guidelines in Fowler's Modern English 417 | Usage, preferring the former where the two disagree. 418 | 419 | The module is built around standard British spelling, but is designed 420 | to cope with common American variants as well. Slang, jargon, and 421 | other English dialects are *not* explicitly catered for. 422 | 423 | Where two or more inflected forms exist for a single word (typically a 424 | "classical" form and a "modern" form), ``inflect.py`` prefers the 425 | more common form (typically the "modern" one), unless "classical" 426 | processing has been specified 427 | (see `MODERN VS CLASSICAL INFLECTIONS`). 428 | 429 | FORMING PLURALS AND SINGULARS 430 | ============================= 431 | 432 | Inflecting Plurals and Singulars 433 | -------------------------------- 434 | 435 | All of the ``plural...`` plural inflection methods take the word to be 436 | inflected as their first argument and return the corresponding inflection. 437 | Note that all such methods expect the *singular* form of the word. The 438 | results of passing a plural form are undefined (and unlikely to be correct). 439 | Similarly, the ``si...`` singular inflection method expects the *plural* 440 | form of the word. 441 | 442 | The ``plural...`` methods also take an optional second argument, 443 | which indicates the grammatical "number" of the word (or of another word 444 | with which the word being inflected must agree). If the "number" argument is 445 | supplied and is not ``1`` (or ``"one"`` or ``"a"``, or some other adjective that 446 | implies the singular), the plural form of the word is returned. If the 447 | "number" argument *does* indicate singularity, the (uninflected) word 448 | itself is returned. If the number argument is omitted, the plural form 449 | is returned unconditionally. 450 | 451 | The ``si...`` method takes a second argument in a similar fashion. If it is 452 | some form of the number ``1``, or is omitted, the singular form is returned. 453 | Otherwise the plural is returned unaltered. 454 | 455 | 456 | The various methods of ``inflect.engine`` are: 457 | 458 | 459 | 460 | ``plural_noun(word, count=None)`` 461 | 462 | The method ``plural_noun()`` takes a *singular* English noun or 463 | pronoun and returns its plural. Pronouns in the nominative ("I" -> 464 | "we") and accusative ("me" -> "us") cases are handled, as are 465 | possessive pronouns ("mine" -> "ours"). 466 | 467 | 468 | ``plural_verb(word, count=None)`` 469 | 470 | The method ``plural_verb()`` takes the *singular* form of a 471 | conjugated verb (that is, one which is already in the correct "person" 472 | and "mood") and returns the corresponding plural conjugation. 473 | 474 | 475 | ``plural_adj(word, count=None)`` 476 | 477 | The method ``plural_adj()`` takes the *singular* form of 478 | certain types of adjectives and returns the corresponding plural form. 479 | Adjectives that are correctly handled include: "numerical" adjectives 480 | ("a" -> "some"), demonstrative adjectives ("this" -> "these", "that" -> 481 | "those"), and possessives ("my" -> "our", "cat's" -> "cats'", "child's" 482 | -> "childrens'", etc.) 483 | 484 | 485 | ``plural(word, count=None)`` 486 | 487 | The method ``plural()`` takes a *singular* English noun, 488 | pronoun, verb, or adjective and returns its plural form. Where a word 489 | has more than one inflection depending on its part of speech (for 490 | example, the noun "thought" inflects to "thoughts", the verb "thought" 491 | to "thought"), the (singular) noun sense is preferred to the (singular) 492 | verb sense. 493 | 494 | Hence ``plural("knife")`` will return "knives" ("knife" having been treated 495 | as a singular noun), whereas ``plural("knifes")`` will return "knife" 496 | ("knifes" having been treated as a 3rd person singular verb). 497 | 498 | The inherent ambiguity of such cases suggests that, 499 | where the part of speech is known, ``plural_noun``, ``plural_verb``, and 500 | ``plural_adj`` should be used in preference to ``plural``. 501 | 502 | 503 | ``singular_noun(word, count=None)`` 504 | 505 | The method ``singular_noun()`` takes a *plural* English noun or 506 | pronoun and returns its singular. Pronouns in the nominative ("we" -> 507 | "I") and accusative ("us" -> "me") cases are handled, as are 508 | possessive pronouns ("ours" -> "mine"). When third person 509 | singular pronouns are returned they take the neuter gender by default 510 | ("they" -> "it"), not ("they"-> "she") nor ("they" -> "he"). This can be 511 | changed with ``gender()``. 512 | 513 | Note that all these methods ignore any whitespace surrounding the 514 | word being inflected, but preserve that whitespace when the result is 515 | returned. For example, ``plural(" cat ")`` returns " cats ". 516 | 517 | 518 | ``gender(genderletter)`` 519 | 520 | The third person plural pronoun takes the same form for the female, male and 521 | neuter (e.g. "they"). The singular however, depends upon gender (e.g. "she", 522 | "he", "it" and "they" -- "they" being the gender neutral form.) By default 523 | ``singular_noun`` returns the neuter form, however, the gender can be selected with 524 | the ``gender`` method. Pass the first letter of the gender to 525 | ``gender`` to return the f(eminine), m(asculine), n(euter) or t(hey) 526 | form of the singular. e.g. 527 | gender('f') followed by singular_noun('themselves') returns 'herself'. 528 | 529 | Numbered plurals 530 | ---------------- 531 | 532 | The ``plural...`` methods return only the inflected word, not the count that 533 | was used to inflect it. Thus, in order to produce "I saw 3 ducks", it 534 | is necessary to use: 535 | 536 | .. code-block:: python 537 | 538 | print("I saw", N, p.plural_noun(animal, N)) 539 | 540 | Since the usual purpose of producing a plural is to make it agree with 541 | a preceding count, inflect.py provides a method 542 | (``no(word, count)``) which, given a word and a(n optional) count, returns the 543 | count followed by the correctly inflected word. Hence the previous 544 | example can be rewritten: 545 | 546 | .. code-block:: python 547 | 548 | print("I saw ", p.no(animal, N)) 549 | 550 | In addition, if the count is zero (or some other term which implies 551 | zero, such as ``"zero"``, ``"nil"``, etc.) the count is replaced by the 552 | word "no". Hence, if ``N`` had the value zero, the previous example 553 | would print (the somewhat more elegant):: 554 | 555 | I saw no animals 556 | 557 | rather than:: 558 | 559 | I saw 0 animals 560 | 561 | Note that the name of the method is a pun: the method 562 | returns either a number (a *No.*) or a ``"no"``, in front of the 563 | inflected word. 564 | 565 | 566 | Reducing the number of counts required 567 | -------------------------------------- 568 | 569 | In some contexts, the need to supply an explicit count to the various 570 | ``plural...`` methods makes for tiresome repetition. For example: 571 | 572 | .. code-block:: python 573 | 574 | print( 575 | plural_adj("This", errors), 576 | plural_noun(" error", errors), 577 | plural_verb(" was", errors), 578 | " fatal.", 579 | ) 580 | 581 | inflect.py therefore provides a method 582 | (``num(count=None, show=None)``) which may be used to set a persistent "default number" 583 | value. If such a value is set, it is subsequently used whenever an 584 | optional second "number" argument is omitted. The default value thus set 585 | can subsequently be removed by calling ``num()`` with no arguments. 586 | Hence we could rewrite the previous example: 587 | 588 | .. code-block:: python 589 | 590 | p.num(errors) 591 | print(p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal.") 592 | p.num() 593 | 594 | Normally, ``num()`` returns its first argument, so that it may also 595 | be "inlined" in contexts like: 596 | 597 | .. code-block:: python 598 | 599 | print(p.num(errors), p.plural_noun(" error"), p.plural_verb(" was"), " detected.") 600 | if severity > 1: 601 | print( 602 | p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal." 603 | ) 604 | 605 | However, in certain contexts (see `INTERPOLATING INFLECTIONS IN STRINGS`) 606 | it is preferable that ``num()`` return an empty string. Hence ``num()`` 607 | provides an optional second argument. If that argument is supplied (that is, if 608 | it is defined) and evaluates to false, ``num`` returns an empty string 609 | instead of its first argument. For example: 610 | 611 | .. code-block:: python 612 | 613 | print(p.num(errors, 0), p.no("error"), p.plural_verb(" was"), " detected.") 614 | if severity > 1: 615 | print( 616 | p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal." 617 | ) 618 | 619 | 620 | 621 | Number-insensitive equality 622 | --------------------------- 623 | 624 | inflect.py also provides a solution to the problem 625 | of comparing words of differing plurality through the methods 626 | ``compare(word1, word2)``, ``compare_nouns(word1, word2)``, 627 | ``compare_verbs(word1, word2)``, and ``compare_adjs(word1, word2)``. 628 | Each of these methods takes two strings, and compares them 629 | using the corresponding plural-inflection method (``plural()``, ``plural_noun()``, 630 | ``plural_verb()``, and ``plural_adj()`` respectively). 631 | 632 | The comparison returns true if: 633 | 634 | - the strings are equal, or 635 | - one string is equal to a plural form of the other, or 636 | - the strings are two different plural forms of the one word. 637 | 638 | 639 | Hence all of the following return true: 640 | 641 | .. code-block:: python 642 | 643 | p.compare("index", "index") # RETURNS "eq" 644 | p.compare("index", "indexes") # RETURNS "s:p" 645 | p.compare("index", "indices") # RETURNS "s:p" 646 | p.compare("indexes", "index") # RETURNS "p:s" 647 | p.compare("indices", "index") # RETURNS "p:s" 648 | p.compare("indices", "indexes") # RETURNS "p:p" 649 | p.compare("indexes", "indices") # RETURNS "p:p" 650 | p.compare("indices", "indices") # RETURNS "eq" 651 | 652 | As indicated by the comments in the previous example, the actual value 653 | returned by the various ``compare`` methods encodes which of the 654 | three equality rules succeeded: "eq" is returned if the strings were 655 | identical, "s:p" if the strings were singular and plural respectively, 656 | "p:s" for plural and singular, and "p:p" for two distinct plurals. 657 | Inequality is indicated by returning an empty string. 658 | 659 | It should be noted that two distinct singular words which happen to take 660 | the same plural form are *not* considered equal, nor are cases where 661 | one (singular) word's plural is the other (plural) word's singular. 662 | Hence all of the following return false: 663 | 664 | .. code-block:: python 665 | 666 | p.compare("base", "basis") # ALTHOUGH BOTH -> "bases" 667 | p.compare("syrinx", "syringe") # ALTHOUGH BOTH -> "syringes" 668 | p.compare("she", "he") # ALTHOUGH BOTH -> "they" 669 | 670 | p.compare("opus", "operas") # ALTHOUGH "opus" -> "opera" -> "operas" 671 | p.compare("taxi", "taxes") # ALTHOUGH "taxi" -> "taxis" -> "taxes" 672 | 673 | Note too that, although the comparison is "number-insensitive" it is *not* 674 | case-insensitive (that is, ``plural("time","Times")`` returns false. To obtain 675 | both number and case insensitivity, use the ``lower()`` method on both strings 676 | (that is, ``plural("time".lower(), "Times".lower())`` returns true). 677 | 678 | Related Functionality 679 | ===================== 680 | 681 | Shout out to these libraries that provide related functionality: 682 | 683 | * `WordSet `_ 684 | parses identifiers like variable names into sets of words suitable for re-assembling 685 | in another form. 686 | 687 | * `word2number `_ converts words to 688 | a number. 689 | 690 | 691 | For Enterprise 692 | ============== 693 | 694 | Available as part of the Tidelift Subscription. 695 | 696 | This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. 697 | 698 | `Learn more `_. 699 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Contact 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | extensions = [ 4 | 'sphinx.ext.autodoc', 5 | 'jaraco.packaging.sphinx', 6 | ] 7 | 8 | master_doc = "index" 9 | html_theme = "furo" 10 | 11 | # Link dates and other references in the changelog 12 | extensions += ['rst.linker'] 13 | link_files = { 14 | '../NEWS.rst': dict( 15 | using=dict(GH='https://github.com'), 16 | replace=[ 17 | dict( 18 | pattern=r'(Issue #|\B#)(?P\d+)', 19 | url='{package_url}/issues/{issue}', 20 | ), 21 | dict( 22 | pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', 23 | with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', 24 | ), 25 | dict( 26 | pattern=r'PEP[- ](?P\d+)', 27 | url='https://peps.python.org/pep-{pep_number:0>4}/', 28 | ), 29 | ], 30 | ) 31 | } 32 | 33 | # Be strict about any broken references 34 | nitpicky = True 35 | nitpick_ignore: list[tuple[str, str]] = [] 36 | 37 | # Include Python intersphinx mapping to prevent failures 38 | # jaraco/skeleton#51 39 | extensions += ['sphinx.ext.intersphinx'] 40 | intersphinx_mapping = { 41 | 'python': ('https://docs.python.org/3', None), 42 | } 43 | 44 | # Preserve authored syntax for defaults 45 | autodoc_preserve_defaults = True 46 | 47 | # Add support for linking usernames, PyPI projects, Wikipedia pages 48 | github_url = 'https://github.com/' 49 | extlinks = { 50 | 'user': (f'{github_url}%s', '@%s'), 51 | 'pypi': ('https://pypi.org/project/%s', '%s'), 52 | 'wiki': ('https://wikipedia.org/wiki/%s', '%s'), 53 | } 54 | extensions += ['sphinx.ext.extlinks'] 55 | 56 | # local 57 | 58 | extensions += ['jaraco.tidelift'] 59 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. _changes: 4 | 5 | History 6 | ******* 7 | 8 | .. include:: ../NEWS (links).rst 9 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to |project| documentation! 2 | =================================== 3 | 4 | .. sidebar-links:: 5 | :home: 6 | :pypi: 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | history 12 | 13 | .. tidelift-referral-banner:: 14 | 15 | .. automodule:: inflect 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /inflect/compat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaraco/inflect/4b10e63076aa60d774e16ab96c8ab816748c93ef/inflect/compat/__init__.py -------------------------------------------------------------------------------- /inflect/compat/py38.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | if sys.version_info > (3, 9): 5 | from typing import Annotated 6 | else: # pragma: no cover 7 | from typing_extensions import Annotated # noqa: F401 8 | -------------------------------------------------------------------------------- /inflect/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaraco/inflect/4b10e63076aa60d774e16ab96c8ab816748c93ef/inflect/py.typed -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | # Is the project well-typed? 3 | strict = False 4 | 5 | # Early opt-in even when strict = False 6 | warn_unused_ignores = True 7 | warn_redundant_casts = True 8 | enable_error_code = ignore-without-code 9 | 10 | # Support namespace packages per https://github.com/python/mypy/issues/14057 11 | explicit_package_bases = True 12 | 13 | disable_error_code = 14 | # Disable due to many false positives 15 | overload-overlap, 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=77", 4 | "setuptools_scm[toml]>=3.4.1", 5 | # jaraco/skeleton#174 6 | "coherent.licensed", 7 | ] 8 | build-backend = "setuptools.build_meta" 9 | 10 | [project] 11 | name = "inflect" 12 | authors = [ 13 | { name = "Paul Dyson", email = "pwdyson@yahoo.com" }, 14 | ] 15 | maintainers = [ 16 | { name = "Jason R. Coombs", email = "jaraco@jaraco.com" }, 17 | ] 18 | description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles" # convert numbers to words 19 | readme = "README.rst" 20 | classifiers = [ 21 | "Development Status :: 5 - Production/Stable", 22 | "Intended Audience :: Developers", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3 :: Only", 25 | "Natural Language :: English", 26 | "Operating System :: OS Independent", 27 | "Topic :: Software Development :: Libraries :: Python Modules", 28 | "Topic :: Text Processing :: Linguistic", 29 | ] 30 | requires-python = ">=3.9" 31 | license = "MIT" 32 | dependencies = [ 33 | "more_itertools >= 8.5.0", 34 | "typeguard >= 4.0.1", 35 | "typing_extensions ; python_version<'3.9'", 36 | ] 37 | keywords = [ 38 | "plural", 39 | "inflect", 40 | "participle", 41 | ] 42 | dynamic = ["version"] 43 | 44 | [project.urls] 45 | Source = "https://github.com/jaraco/inflect" 46 | 47 | [project.optional-dependencies] 48 | test = [ 49 | # upstream 50 | "pytest >= 6, != 8.1.*", 51 | 52 | # local 53 | "pygments", 54 | ] 55 | 56 | doc = [ 57 | # upstream 58 | "sphinx >= 3.5", 59 | "jaraco.packaging >= 9.3", 60 | "rst.linker >= 1.9", 61 | "furo", 62 | "sphinx-lint", 63 | 64 | # tidelift 65 | "jaraco.tidelift >= 1.4", 66 | 67 | # local 68 | ] 69 | 70 | check = [ 71 | "pytest-checkdocs >= 2.4", 72 | "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", 73 | ] 74 | 75 | cover = [ 76 | "pytest-cov", 77 | ] 78 | 79 | enabler = [ 80 | "pytest-enabler >= 2.2", 81 | ] 82 | 83 | type = [ 84 | # upstream 85 | "pytest-mypy", 86 | 87 | # local 88 | ] 89 | 90 | 91 | [tool.setuptools_scm] 92 | 93 | 94 | [tool.pytest-enabler.mypy] 95 | # Disabled due to jaraco/skeleton#143 96 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs=dist build .tox .eggs 3 | addopts= 4 | --doctest-modules 5 | --import-mode importlib 6 | consider_namespace_packages=true 7 | filterwarnings= 8 | ## upstream 9 | 10 | # Ensure ResourceWarnings are emitted 11 | default::ResourceWarning 12 | 13 | # realpython/pytest-mypy#152 14 | ignore:'encoding' argument not specified::pytest_mypy 15 | 16 | # python/cpython#100750 17 | ignore:'encoding' argument not specified::platform 18 | 19 | # pypa/build#615 20 | ignore:'encoding' argument not specified::build.env 21 | 22 | # dateutil/dateutil#1284 23 | ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz 24 | 25 | ## end upstream 26 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | [lint] 2 | extend-select = [ 3 | # upstream 4 | 5 | "C901", # complex-structure 6 | "I", # isort 7 | "PERF401", # manual-list-comprehension 8 | 9 | # Ensure modern type annotation syntax and best practices 10 | # Not including those covered by type-checkers or exclusive to Python 3.11+ 11 | "FA", # flake8-future-annotations 12 | "F404", # late-future-import 13 | "PYI", # flake8-pyi 14 | "UP006", # non-pep585-annotation 15 | "UP007", # non-pep604-annotation 16 | "UP010", # unnecessary-future-import 17 | "UP035", # deprecated-import 18 | "UP037", # quoted-annotation 19 | "UP043", # unnecessary-default-type-args 20 | 21 | # local 22 | ] 23 | ignore = [ 24 | # upstream 25 | 26 | # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, 27 | # irrelevant to this project. 28 | "PYI011", # typed-argument-default-in-stub 29 | # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules 30 | "W191", 31 | "E111", 32 | "E114", 33 | "E117", 34 | "D206", 35 | "D300", 36 | "Q000", 37 | "Q001", 38 | "Q002", 39 | "Q003", 40 | "COM812", 41 | "COM819", 42 | 43 | # local 44 | ] 45 | 46 | [format] 47 | # Enable preview to get hugged parenthesis unwrapping and other nice surprises 48 | # See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373 49 | preview = true 50 | # https://docs.astral.sh/ruff/settings/#format_quote-style 51 | quote-style = "preserve" 52 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x32392EaEA1FDE87733bEEc3b184C9006501c4A82' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /tests/inflections.txt: -------------------------------------------------------------------------------- 1 | a -> as # NOUN FORM 2 | TODO:sing a -> some # INDEFINITE ARTICLE 3 | TODO: A.C.R.O.N.Y.M. -> A.C.R.O.N.Y.M.s 4 | abscissa -> abscissas|abscissae 5 | accomplice -> accomplices 6 | Achinese -> Achinese 7 | acropolis -> acropolises 8 | adieu -> adieus|adieux 9 | adjutant general -> adjutant generals 10 | aegis -> aegises 11 | afflatus -> afflatuses 12 | afreet -> afreets|afreeti 13 | afrit -> afrits|afriti 14 | agendum -> agenda 15 | aide-de-camp -> aides-de-camp 16 | Alabaman -> Alabamans 17 | albino -> albinos 18 | album -> albums 19 | Alfurese -> Alfurese 20 | alga -> algae 21 | alias -> aliases 22 | alto -> altos|alti 23 | alumna -> alumnae 24 | alumnus -> alumni 25 | alveolus -> alveoli 26 | TODO:siverb am -> are 27 | TODO:siverb am going -> are going 28 | ambassador-at-large -> ambassadors-at-large 29 | Amboinese -> Amboinese 30 | Americanese -> Americanese 31 | amoeba -> amoebas|amoebae 32 | Amoyese -> Amoyese 33 | TODO:siadj an -> some # INDEFINITE ARTICLE 34 | analysis -> analyses 35 | anathema -> anathemas|anathemata 36 | Andamanese -> Andamanese 37 | Angolese -> Angolese 38 | Annamese -> Annamese 39 | antenna -> antennas|antennae 40 | anus -> anuses 41 | apex -> apexes|apices 42 | TODO:siadj apex's -> apexes'|apices' # POSSESSIVE FORM 43 | aphelion -> aphelia 44 | apparatus -> apparatuses|apparatus 45 | appendix -> appendixes|appendices 46 | apple -> apples 47 | aquarium -> aquariums|aquaria 48 | Aragonese -> Aragonese 49 | Arakanese -> Arakanese 50 | archipelago -> archipelagos 51 | TODO:siverb are -> are 52 | TODO:siverb are made -> are made 53 | armadillo -> armadillos 54 | arpeggio -> arpeggios 55 | arthritis -> arthritises|arthritides 56 | asbestos -> asbestoses 57 | asparagus -> asparaguses 58 | ass -> asses 59 | Assamese -> Assamese 60 | asylum -> asylums 61 | asyndeton -> asyndeta 62 | at it -> at them # ACCUSATIVE 63 | ataman -> atamans 64 | TODO:siverb ate -> ate 65 | atlas -> atlases|atlantes 66 | atman -> atmas 67 | TODO:singular_noun attorney general -> attorneys general 68 | attorney of record -> attorneys of record 69 | aurora -> auroras|aurorae 70 | auto -> autos 71 | auto-da-fe -> autos-da-fe 72 | aviatrix -> aviatrixes|aviatrices 73 | TODO:siadj aviatrix's -> aviatrixes'|aviatrices' 74 | Avignonese -> Avignonese 75 | axe -> axes 76 | TODO:singular_noun 2 anwers! axis -> axes 77 | axman -> axmen 78 | Azerbaijanese -> Azerbaijanese 79 | bacillus -> bacilli 80 | bacterium -> bacteria 81 | Bahaman -> Bahamans 82 | Balinese -> Balinese 83 | bamboo -> bamboos 84 | banjo -> banjoes 85 | bass -> basses # INSTRUMENT, NOT FISH 86 | basso -> bassos|bassi 87 | bathos -> bathoses 88 | beau -> beaus|beaux 89 | beef -> beefs|beeves 90 | beneath it -> beneath them # ACCUSATIVE 91 | Bengalese -> Bengalese 92 | bent -> bent # VERB FORM 93 | bent -> bents # NOUN FORM 94 | Bernese -> Bernese 95 | Bhutanese -> Bhutanese 96 | bias -> biases 97 | biceps -> biceps 98 | bison -> bisons|bison 99 | black olive -> black olives 100 | blouse -> blouses 101 | Bolognese -> Bolognese 102 | bonus -> bonuses 103 | Borghese -> Borghese 104 | boss -> bosses 105 | Bostonese -> Bostonese 106 | box -> boxes 107 | boy -> boys 108 | bravo -> bravoes 109 | bream -> bream 110 | breeches -> breeches 111 | bride-to-be -> brides-to-be 112 | Brigadier General -> Brigadier Generals 113 | britches -> britches 114 | bronchitis -> bronchitises|bronchitides 115 | bronchus -> bronchi 116 | brother -> brothers|brethren 117 | TODO: brother's -> brothers'|brethren's 118 | buffalo -> buffaloes|buffalo 119 | Buginese -> Buginese 120 | buoy -> buoys 121 | bureau -> bureaus|bureaux 122 | Burman -> Burmans 123 | Burmese -> Burmese 124 | bursitis -> bursitises|bursitides 125 | bus -> buses 126 | butter -> butter 127 | buzz -> buzzes 128 | buzzes -> buzz # VERB FORM 129 | by it -> by them # ACCUSATIVE 130 | caddis -> caddises 131 | caiman -> caimans 132 | cake -> cakes 133 | Calabrese -> Calabrese 134 | calf -> calves 135 | callus -> calluses 136 | Camaldolese -> Camaldolese 137 | cameo -> cameos 138 | campus -> campuses 139 | can -> cans # NOUN FORM 140 | can -> can # VERB FORM (all pers.) 141 | can't -> can't # VERB FORM 142 | candelabrum -> candelabra 143 | cannabis -> cannabises 144 | TODO:siverb canoes -> canoe 145 | canto -> cantos 146 | Cantonese -> Cantonese 147 | cantus -> cantus 148 | canvas -> canvases 149 | CAPITAL -> CAPITALS 150 | carcinoma -> carcinomas|carcinomata 151 | care -> cares 152 | cargo -> cargoes 153 | caribou -> caribous|caribou 154 | Carlylese -> Carlylese 155 | carmen -> carmina 156 | carp -> carp 157 | cash -> cash 158 | Cassinese -> Cassinese 159 | cat -> cats 160 | catfish -> catfish 161 | cattle -> cattles|cattle 162 | cayman -> caymans 163 | Celanese -> Celanese 164 | ceriman -> cerimans 165 | cervid -> cervids 166 | Ceylonese -> Ceylonese 167 | chairman -> chairmen 168 | chamois -> chamois 169 | chaos -> chaoses 170 | chapeau -> chapeaus|chapeaux 171 | charisma -> charismas|charismata 172 | TODO:siverb chases -> chase 173 | chassis -> chassis 174 | chateau -> chateaus|chateaux 175 | cherub -> cherubs|cherubim 176 | chickenpox -> chickenpox 177 | chief -> chiefs 178 | child -> children 179 | chili -> chilis|chilies 180 | Chinese -> Chinese 181 | oatmeal cookie -> oatmeal cookies 182 | chorus -> choruses 183 | chrysalis -> chrysalises|chrysalides 184 | church -> churches 185 | cicatrix -> cicatrixes|cicatrices 186 | circus -> circuses 187 | class -> classes 188 | classes -> class # VERB FORM 189 | clippers -> clippers 190 | clitoris -> clitorises|clitorides 191 | cod -> cod 192 | codex -> codices 193 | coitus -> coitus 194 | commando -> commandos 195 | compendium -> compendiums|compendia 196 | coney -> coneys 197 | Congoese -> Congoese 198 | Congolese -> Congolese 199 | conspectus -> conspectuses 200 | contralto -> contraltos|contralti 201 | contretemps -> contretemps 202 | conundrum -> conundrums 203 | corps -> corps 204 | corpus -> corpuses|corpora 205 | cortex -> cortexes|cortices 206 | cosmos -> cosmoses 207 | TODO:singular_noun court martial -> courts martial 208 | cow -> cows|kine 209 | cranium -> craniums|crania 210 | crescendo -> crescendos 211 | criterion -> criteria 212 | curriculum -> curriculums|curricula 213 | czech -> czechs 214 | dais -> daises 215 | data point -> data points 216 | datum -> data 217 | debris -> debris 218 | decorum -> decorums 219 | deer -> deer 220 | delphinium -> delphiniums 221 | desideratum -> desiderata 222 | desman -> desmans 223 | diabetes -> diabetes 224 | dictum -> dictums|dicta 225 | TODO:siverb did -> did 226 | TODO:siverb did need -> did need 227 | digitalis -> digitalises 228 | dingo -> dingoes 229 | diploma -> diplomas|diplomata 230 | discus -> discuses 231 | dish -> dishes 232 | ditto -> dittos 233 | djinn -> djinn 234 | TODO:siverb does -> do 235 | TODO:siverb doesn't -> don't # VERB FORM 236 | dog -> dogs 237 | dogma -> dogmas|dogmata 238 | dolman -> dolmans 239 | dominatrix -> dominatrixes|dominatrices 240 | domino -> dominoes 241 | Dongolese -> Dongolese 242 | dormouse -> dormice 243 | drama -> dramas|dramata 244 | drum -> drums 245 | dwarf -> dwarves 246 | dynamo -> dynamos 247 | edema -> edemas|edemata 248 | eland -> elands|eland 249 | elf -> elves 250 | elk -> elks|elk 251 | embryo -> embryos 252 | emporium -> emporiums|emporia 253 | encephalitis -> encephalitises|encephalitides 254 | encomium -> encomiums|encomia 255 | enema -> enemas|enemata 256 | enigma -> enigmas|enigmata 257 | epidermis -> epidermises 258 | epididymis -> epididymises|epididymides 259 | erratum -> errata 260 | ethos -> ethoses 261 | eucalyptus -> eucalyptuses 262 | eunuch -> eunuchs 263 | extremum -> extrema 264 | eyas -> eyases 265 | factotum -> factotums 266 | farman -> farmans 267 | Faroese -> Faroese 268 | fauna -> faunas|faunae 269 | fax -> faxes 270 | Ferrarese -> Ferrarese 271 | ferry -> ferries 272 | fetus -> fetuses 273 | fiance -> fiances 274 | fiancee -> fiancees 275 | fiasco -> fiascos 276 | fish -> fish 277 | fizz -> fizzes 278 | flamingo -> flamingoes 279 | flittermouse -> flittermice 280 | TODO:siverb floes -> floe 281 | flora -> floras|florae 282 | flounder -> flounder 283 | focus -> focuses|foci 284 | foetus -> foetuses 285 | folio -> folios 286 | Foochowese -> Foochowese 287 | foot -> feet 288 | TODO:siadj foot's -> feet's # POSSESSIVE FORM 289 | foramen -> foramens|foramina 290 | TODO:siverb foreshoes -> foreshoe 291 | formula -> formulas|formulae 292 | forum -> forums 293 | TODO:siverb fought -> fought 294 | fox -> foxes 295 | TODO:singular_noun 2 different returns from him -> from them 296 | from it -> from them # ACCUSATIVE 297 | fungus -> funguses|fungi 298 | Gabunese -> Gabunese 299 | gallows -> gallows 300 | ganglion -> ganglions|ganglia 301 | gas -> gases 302 | gateau -> gateaus|gateaux 303 | TODO:siverb gave -> gave 304 | general -> generals 305 | generalissimo -> generalissimos 306 | Genevese -> Genevese 307 | genie -> genies|genii 308 | TODO:singular_noun 2 diff return values! genius -> geniuses|genii 309 | Genoese -> Genoese 310 | genus -> genera 311 | German -> Germans 312 | ghetto -> ghettos 313 | Gilbertese -> Gilbertese 314 | glottis -> glottises 315 | Goanese -> Goanese 316 | goat -> goats 317 | goose -> geese 318 | TODO:singular_noun Governor General -> Governors General 319 | goy -> goys|goyim 320 | graffiti -> graffiti 321 | TODO:singular_noun 2 diff ret values graffito -> graffiti 322 | grizzly -> grizzlies 323 | guano -> guanos 324 | guardsman -> guardsmen 325 | Guianese -> Guianese 326 | gumma -> gummas|gummata 327 | TODO:siverb gumshoes -> gumshoe 328 | gunman -> gunmen 329 | gymnasium -> gymnasiums|gymnasia 330 | TODO:siverb had -> had 331 | TODO:siverb had thought -> had thought 332 | Hainanese -> Hainanese 333 | TODO:siverb hammertoes -> hammertoe 334 | handkerchief -> handkerchiefs 335 | Hararese -> Hararese 336 | Harlemese -> Harlemese 337 | harman -> harmans 338 | harmonium -> harmoniums 339 | TODO:siverb has -> have 340 | TODO:siverb has become -> have become 341 | TODO:siverb has been -> have been 342 | TODO:siverb has-been -> has-beens 343 | hasn't -> haven't # VERB FORM 344 | Havanese -> Havanese 345 | TODO:siverb have -> have 346 | TODO:siverb have conceded -> have conceded 347 | TODO:singular_noun 2 values he -> they 348 | headquarters -> headquarters 349 | Heavenese -> Heavenese 350 | helix -> helices 351 | hepatitis -> hepatitises|hepatitides 352 | TODO:singular_noun 2 values her -> them # PRONOUN 353 | TODO:singular_noun 2 values her -> their 354 | # POSSESSIVE ADJ 355 | hero -> heroes 356 | herpes -> herpes 357 | TODO:singular_noun 2 values hers -> theirs 358 | # POSSESSIVE NOUN 359 | TODO:singular_noun 2 values herself -> themselves 360 | hetman -> hetmans 361 | hiatus -> hiatuses|hiatus 362 | highlight -> highlights 363 | hijinks -> hijinks 364 | TODO:singular_noun 2 values him -> them 365 | TODO:singular_noun 2 values himself -> themselves 366 | hippopotamus -> hippopotamuses|hippopotami 367 | Hiroshiman -> Hiroshimans 368 | TODO:singular_noun 2 values his -> their 369 | # POSSESSIVE ADJ 370 | TODO:singular_noun 2 values his -> theirs 371 | # POSSESSIVE NOUN 372 | TODO:siverb hoes -> hoe 373 | honorarium -> honorariums|honoraria 374 | hoof -> hoofs|hooves 375 | Hoosierese -> Hoosierese 376 | TODO:siverb horseshoes -> horseshoe 377 | Hottentotese -> Hottentotese 378 | house -> houses 379 | housewife -> housewives 380 | hubris -> hubrises 381 | human -> humans 382 | Hunanese -> Hunanese 383 | hydra -> hydras|hydrae 384 | hyperbaton -> hyperbata 385 | hyperbola -> hyperbolas|hyperbolae 386 | I -> we 387 | ibis -> ibises 388 | ignoramus -> ignoramuses 389 | impetus -> impetuses|impetus 390 | incubus -> incubuses|incubi 391 | index -> indexes|indices 392 | Indochinese -> Indochinese 393 | inferno -> infernos 394 | infinity -> infinities|infinity 395 | information -> information 396 | innings -> innings 397 | TODO:singular_noun Inspector General -> Inspectors General 398 | interregnum -> interregnums|interregna 399 | iris -> irises|irides 400 | TODO:siverb is -> are 401 | TODO:siverb is eaten -> are eaten 402 | isn't -> aren't # VERB FORM 403 | it -> they # NOMINATIVE 404 | TODO:siadj its -> their # POSSESSIVE FORM 405 | itself -> themselves 406 | jackanapes -> jackanapes 407 | Japanese -> Japanese 408 | Javanese -> Javanese 409 | Jerry -> Jerrys 410 | jerry -> jerries 411 | jinx -> jinxes 412 | jinxes -> jinx # VERB FORM 413 | Johnsonese -> Johnsonese 414 | Jones -> Joneses 415 | jumbo -> jumbos 416 | Kanarese -> Kanarese 417 | Kiplingese -> Kiplingese 418 | knife -> knives # NOUN FORM 419 | knife -> knife # VERB FORM (1st/2nd pers.) 420 | knifes -> knife # VERB FORM (3rd pers.) 421 | Kongoese -> Kongoese 422 | Kongolese -> Kongolese 423 | lacuna -> lacunas|lacunae 424 | lady in waiting -> ladies in waiting 425 | Lapponese -> Lapponese 426 | larynx -> larynxes|larynges 427 | latex -> latexes|latices 428 | lawman -> lawmen 429 | layman -> laymen 430 | leaf -> leaves # NOUN FORM 431 | leaf -> leaf # VERB FORM (1st/2nd pers.) 432 | leafs -> leaf # VERB FORM (3rd pers.) 433 | Lebanese -> Lebanese 434 | leman -> lemans 435 | lemma -> lemmas|lemmata 436 | lens -> lenses 437 | Leonese -> Leonese 438 | lick of the cat -> licks of the cat 439 | Lieutenant General -> Lieutenant Generals 440 | lie -> lies 441 | life -> lives 442 | Liman -> Limans 443 | lingo -> lingos 444 | loaf -> loaves 445 | locus -> loci 446 | Londonese -> Londonese 447 | lore -> lores|lore 448 | Lorrainese -> Lorrainese 449 | lothario -> lotharios 450 | louse -> lice 451 | Lucchese -> Lucchese 452 | lumbago -> lumbagos 453 | lumen -> lumens|lumina 454 | lummox -> lummoxes 455 | lustrum -> lustrums|lustra 456 | lyceum -> lyceums 457 | lymphoma -> lymphomas|lymphomata 458 | lynx -> lynxes 459 | Lyonese -> Lyonese 460 | TODO: M.I.A. -> M.I.A.s 461 | Macanese -> Macanese 462 | Macassarese -> Macassarese 463 | mackerel -> mackerel 464 | macro -> macros 465 | TODO:siverb made -> made 466 | madman -> madmen 467 | Madurese -> Madurese 468 | magma -> magmas|magmata 469 | magneto -> magnetos 470 | Major General -> Major Generals 471 | Malabarese -> Malabarese 472 | Maltese -> Maltese 473 | man -> men 474 | mandamus -> mandamuses 475 | manifesto -> manifestos 476 | mantis -> mantises 477 | marquis -> marquises 478 | Mary -> Marys 479 | maximum -> maximums|maxima 480 | measles -> measles 481 | medico -> medicos 482 | medium -> mediums|media 483 | TODO:siadj medium's -> mediums'|media's 484 | medusa -> medusas|medusae 485 | memorandum -> memorandums|memoranda 486 | meniscus -> menisci 487 | merman -> mermen 488 | Messinese -> Messinese 489 | metamorphosis -> metamorphoses 490 | metropolis -> metropolises 491 | mews -> mews 492 | miasma -> miasmas|miasmata 493 | Milanese -> Milanese 494 | milieu -> milieus|milieux 495 | millennium -> millenniums|millennia 496 | minimum -> minimums|minima 497 | minx -> minxes 498 | miss -> miss # VERB FORM (1st/2nd pers.) 499 | miss -> misses # NOUN FORM 500 | misses -> miss # VERB FORM (3rd pers.) 501 | TODO:siverb mistletoes -> mistletoe 502 | mittamus -> mittamuses 503 | Modenese -> Modenese 504 | momentum -> momentums|momenta 505 | money -> monies 506 | mongoose -> mongooses 507 | moose -> moose 508 | mother-in-law -> mothers-in-law 509 | mouse -> mice 510 | mumps -> mumps 511 | Muranese -> Muranese 512 | murex -> murices 513 | museum -> museums 514 | mustachio -> mustachios 515 | TODO:siadj my -> our # POSSESSIVE FORM 516 | myself -> ourselves 517 | mythos -> mythoi 518 | Nakayaman -> Nakayamans 519 | Nankingese -> Nankingese 520 | nasturtium -> nasturtiums 521 | Navarrese -> Navarrese 522 | nebula -> nebulas|nebulae 523 | Nepalese -> Nepalese 524 | neuritis -> neuritises|neuritides 525 | neurosis -> neuroses 526 | news -> news 527 | nexus -> nexus 528 | Niasese -> Niasese 529 | Nicobarese -> Nicobarese 530 | nimbus -> nimbuses|nimbi 531 | Nipponese -> Nipponese 532 | no -> noes 533 | Norman -> Normans 534 | nostrum -> nostrums 535 | noumenon -> noumena 536 | nova -> novas|novae 537 | nucleolus -> nucleoluses|nucleoli 538 | nucleus -> nuclei 539 | numen -> numina 540 | oaf -> oafs 541 | TODO:siverb oboes -> oboe 542 | occiput -> occiputs|occipita 543 | octavo -> octavos 544 | octopus -> octopuses|octopodes 545 | oedema -> oedemas|oedemata 546 | Oklahoman -> Oklahomans 547 | omnibus -> omnibuses 548 | on it -> on them # ACCUSATIVE 549 | onus -> onuses 550 | opera -> operas 551 | optimum -> optimums|optima 552 | opus -> opuses|opera 553 | organon -> organa 554 | ottoman -> ottomans 555 | ought to be -> ought to be # VERB (UNLIKE bride to be) 556 | TODO:siverb overshoes -> overshoe 557 | TODO:siverb overtoes -> overtoe 558 | ovum -> ova 559 | ox -> oxen 560 | TODO:siadj ox's -> oxen's # POSSESSIVE FORM 561 | oxman -> oxmen 562 | oxymoron -> oxymorons|oxymora 563 | Panaman -> Panamans 564 | parabola -> parabolas|parabolae 565 | Parmese -> Parmese 566 | pathos -> pathoses 567 | pegasus -> pegasuses 568 | Pekingese -> Pekingese 569 | pelvis -> pelvises 570 | pendulum -> pendulums 571 | penis -> penises|penes 572 | penumbra -> penumbras|penumbrae 573 | perihelion -> perihelia 574 | person -> people|persons 575 | persona -> personae 576 | petroleum -> petroleums 577 | phalanx -> phalanxes|phalanges 578 | PhD -> PhDs 579 | phenomenon -> phenomena 580 | philtrum -> philtrums 581 | photo -> photos 582 | phylum -> phylums|phyla 583 | piano -> pianos|piani 584 | Piedmontese -> Piedmontese 585 | pika -> pikas 586 | TODO:singular_noun ret mul value pincer -> pincers 587 | pincers -> pincers 588 | Pistoiese -> Pistoiese 589 | plateau -> plateaus|plateaux 590 | play -> plays 591 | plexus -> plexuses|plexus 592 | pliers -> pliers 593 | plies -> ply # VERB FORM 594 | polis -> polises 595 | Polonese -> Polonese 596 | pontifex -> pontifexes|pontifices 597 | portmanteau -> portmanteaus|portmanteaux 598 | Portuguese -> Portuguese 599 | possum -> possums 600 | potato -> potatoes 601 | pox -> pox 602 | pragma -> pragmas|pragmata 603 | premium -> premiums 604 | prima donna -> prima donnas|prime donne 605 | pro -> pros 606 | proceedings -> proceedings 607 | prolegomenon -> prolegomena 608 | proof -> proofs 609 | proof of concept -> proofs of concept 610 | prosecutrix -> prosecutrixes|prosecutrices 611 | prospectus -> prospectuses|prospectus 612 | protozoan -> protozoans 613 | protozoon -> protozoa 614 | puma -> pumas 615 | TODO:siverb put -> put 616 | quantum -> quantums|quanta 617 | TODO:singular_noun quartermaster general -> quartermasters general 618 | quarto -> quartos 619 | quiz -> quizzes 620 | quizzes -> quiz # VERB FORM 621 | quorum -> quorums 622 | rabies -> rabies 623 | radius -> radiuses|radii 624 | radix -> radices 625 | ragman -> ragmen 626 | rebus -> rebuses 627 | TODO:siverb rehoes -> rehoe 628 | reindeer -> reindeer 629 | repo -> repos 630 | TODO:siverb reshoes -> reshoe 631 | rhino -> rhinos 632 | rhinoceros -> rhinoceroses|rhinoceros 633 | TODO:siverb roes -> roe 634 | Rom -> Roma 635 | Romagnese -> Romagnese 636 | Roman -> Romans 637 | Romanese -> Romanese 638 | Romany -> Romanies 639 | romeo -> romeos 640 | roof -> roofs 641 | rostrum -> rostrums|rostra 642 | ruckus -> ruckuses 643 | salmon -> salmon 644 | Sangirese -> Sangirese 645 | TODO: siverb sank -> sank 646 | Sarawakese -> Sarawakese 647 | sarcoma -> sarcomas|sarcomata 648 | sassafras -> sassafrases 649 | saw -> saw # VERB FORM (1st/2nd pers.) 650 | saw -> saws # NOUN FORM 651 | saws -> saw # VERB FORM (3rd pers.) 652 | scarf -> scarves 653 | schema -> schemas|schemata 654 | scissors -> scissors 655 | pair of scissors -> pairs of scissors 656 | pair of slippers -> pairs of slippers 657 | Scotsman -> Scotsmen 658 | sea-bass -> sea-bass 659 | seaman -> seamen 660 | self -> selves 661 | Selman -> Selmans 662 | Senegalese -> Senegalese 663 | seraph -> seraphs|seraphim 664 | series -> series 665 | TODO:siverb shall eat -> shall eat 666 | shaman -> shamans 667 | Shavese -> Shavese 668 | Shawanese -> Shawanese 669 | TODO:singular_noun multivalue she -> they 670 | sheaf -> sheaves 671 | shears -> shears 672 | sheep -> sheep 673 | shelf -> shelves 674 | TODO:siverb shoes -> shoe 675 | TODO:siverb should have -> should have 676 | Siamese -> Siamese 677 | siemens -> siemens 678 | Sienese -> Sienese 679 | Sikkimese -> Sikkimese 680 | silex -> silices 681 | simplex -> simplexes|simplices 682 | Singhalese -> Singhalese 683 | Sinhalese -> Sinhalese 684 | sinus -> sinuses|sinus 685 | size -> sizes 686 | sizes -> size #VERB FORM 687 | slice -> slices 688 | smallpox -> smallpox 689 | Smith -> Smiths 690 | TODO:siverb snowshoes -> snowshoe 691 | Sogdianese -> Sogdianese 692 | soliloquy -> soliloquies 693 | solo -> solos|soli 694 | soma -> somas|somata 695 | TODO:singular_noun tough son of a bitch -> sons of bitches 696 | Sonaman -> Sonamans 697 | soprano -> sopranos|soprani 698 | TODO:siverb sought -> sought 699 | TODO:siverb spattlehoes -> spattlehoe 700 | species -> species 701 | spectrum -> spectrums|spectra 702 | speculum -> speculums|specula 703 | TODO:siverb spent -> spent 704 | spermatozoon -> spermatozoa 705 | sphinx -> sphinxes|sphinges 706 | spokesperson -> spokespeople|spokespersons 707 | stadium -> stadiums|stadia 708 | stamen -> stamens|stamina 709 | status -> statuses|status 710 | stereo -> stereos 711 | stigma -> stigmas|stigmata 712 | stimulus -> stimuli 713 | stoma -> stomas|stomata 714 | stomach -> stomachs 715 | storey -> storeys 716 | story -> stories 717 | stratum -> strata 718 | strife -> strifes 719 | stylo -> stylos 720 | stylus -> styluses|styli 721 | succubus -> succubuses|succubi 722 | Sudanese -> Sudanese 723 | suffix -> suffixes 724 | Sundanese -> Sundanese 725 | superior -> superiors 726 | supply -> supplies 727 | TODO:singular_noun Surgeon-General -> Surgeons-General 728 | surplus -> surpluses 729 | Swahilese -> Swahilese 730 | swine -> swines|swine 731 | TODO:singular_noun multiple return syringe -> syringes 732 | syrinx -> syrinxes|syringes 733 | tableau -> tableaus|tableaux 734 | taco -> tacos 735 | Tacoman -> Tacomans 736 | talouse -> talouses 737 | tattoo -> tattoos 738 | taxman -> taxmen 739 | tempo -> tempos|tempi 740 | Tenggerese -> Tenggerese 741 | testatrix -> testatrixes|testatrices 742 | testes -> testes 743 | TODO:singular_noun multiple return testis -> testes 744 | TODO:siadj that -> those 745 | TODO:siadj their -> their 746 | # POSSESSIVE FORM (GENDER-INCLUSIVE) 747 | TODO:singular_noun multiple return themself -> themselves 748 | # ugly but gaining currency 749 | TODO:singular_noun multiple return they -> they 750 | # for indeterminate gender 751 | thief -> thiefs|thieves 752 | TODO:siadj this -> these 753 | thought -> thoughts # NOUN FORM 754 | thought -> thought # VERB FORM 755 | TODO:siverb throes -> throe 756 | TODO:siverb ticktacktoes -> ticktacktoe 757 | Times -> Timeses 758 | Timorese -> Timorese 759 | TODO:siverb tiptoes -> tiptoe 760 | Tirolese -> Tirolese 761 | titmouse -> titmice 762 | TODO:singular_noun multivalue to her -> to them 763 | TODO:singular_noun multivalue to herself -> to themselves 764 | TODO:singular_noun multivalue to him -> to them 765 | TODO:singular_noun multivalue to himself -> to themselves 766 | to it -> to them 767 | to it -> to them # ACCUSATIVE 768 | to itself -> to themselves 769 | to me -> to us 770 | to myself -> to ourselves 771 | TODO:singular_noun multivalue to them -> to them 772 | # for indeterminate gender 773 | TODO:singular_noun multivalue to themself -> to themselves 774 | # ugly but gaining currency 775 | to you -> to you 776 | to yourself -> to yourselves 777 | Tocharese -> Tocharese 778 | TODO:siverb toes -> toe 779 | tomato -> tomatoes 780 | Tonkinese -> Tonkinese 781 | tonsillitis -> tonsillitises|tonsillitides 782 | tooth -> teeth 783 | Torinese -> Torinese 784 | torus -> toruses|tori 785 | trapezium -> trapeziums|trapezia 786 | trauma -> traumas|traumata 787 | travois -> travois 788 | tranche -> tranches 789 | trellis -> trellises 790 | TODO:siverb tries -> try 791 | trilby -> trilbys 792 | trousers -> trousers 793 | trousseau -> trousseaus|trousseaux 794 | trout -> trout 795 | TODO:siverb try -> tries 796 | tuna -> tuna 797 | turf -> turfs|turves 798 | Tyrolese -> Tyrolese 799 | ultimatum -> ultimatums|ultimata 800 | umbilicus -> umbilicuses|umbilici 801 | umbra -> umbras|umbrae 802 | TODO:siverb undershoes -> undershoe 803 | TODO:siverb unshoes -> unshoe 804 | uterus -> uteruses|uteri 805 | vacuum -> vacuums|vacua 806 | vellum -> vellums 807 | velum -> velums|vela 808 | Vermontese -> Vermontese 809 | Veronese -> Veronese 810 | vertebra -> vertebrae 811 | vertex -> vertexes|vertices 812 | Viennese -> Viennese 813 | Vietnamese -> Vietnamese 814 | virtuoso -> virtuosos|virtuosi 815 | virus -> viruses 816 | vita -> vitae 817 | vixen -> vixens 818 | vortex -> vortexes|vortices 819 | walrus -> walruses 820 | TODO:siverb was -> were 821 | TODO:siverb was faced with -> were faced with 822 | TODO:siverb was hoping -> were hoping 823 | Wenchowese -> Wenchowese 824 | TODO:siverb were -> were 825 | TODO:siverb were found -> were found 826 | wharf -> wharves 827 | whiting -> whiting 828 | Whitmanese -> Whitmanese 829 | whiz -> whizzes 830 | TODO:singular_noun multivalue whizz -> whizzes 831 | widget -> widgets 832 | wife -> wives 833 | wildebeest -> wildebeests|wildebeest 834 | will -> will # VERB FORM 835 | will -> wills # NOUN FORM 836 | will eat -> will eat # VERB FORM 837 | wills -> will # VERB FORM 838 | wish -> wishes 839 | TODO:singular_noun multivalue with him -> with them 840 | with it -> with them # ACCUSATIVE 841 | TODO:siverb woes -> woe 842 | wolf -> wolves 843 | woman -> women 844 | woman of substance -> women of substance 845 | TODO:siadj woman's -> women's # POSSESSIVE FORM 846 | won't -> won't # VERB FORM 847 | woodlouse -> woodlice 848 | Yakiman -> Yakimans 849 | Yengeese -> Yengeese 850 | yeoman -> yeomen 851 | yeowoman -> yeowomen 852 | yes -> yeses 853 | Yokohaman -> Yokohamans 854 | you -> you 855 | TODO:siadj your -> your # POSSESSIVE FORM 856 | yourself -> yourselves 857 | Yuman -> Yumans 858 | Yunnanese -> Yunnanese 859 | zero -> zeros 860 | zoon -> zoa 861 | -------------------------------------------------------------------------------- /tests/test_an.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | 4 | def test_an(): 5 | p = inflect.engine() 6 | 7 | assert p.an("cat") == "a cat" 8 | assert p.an("ant") == "an ant" 9 | assert p.an("a") == "an a" 10 | assert p.an("b") == "a b" 11 | assert p.an("honest cat") == "an honest cat" 12 | assert p.an("dishonest cat") == "a dishonest cat" 13 | assert p.an("Honolulu sunset") == "a Honolulu sunset" 14 | assert p.an("mpeg") == "an mpeg" 15 | assert p.an("onetime holiday") == "a onetime holiday" 16 | assert p.an("Ugandan person") == "a Ugandan person" 17 | assert p.an("Ukrainian person") == "a Ukrainian person" 18 | assert p.an("Unabomber") == "a Unabomber" 19 | assert p.an("unanimous decision") == "a unanimous decision" 20 | assert p.an("US farmer") == "a US farmer" 21 | assert p.an("wild PIKACHU appeared") == "a wild PIKACHU appeared" 22 | 23 | 24 | def test_an_abbreviation(): 25 | p = inflect.engine() 26 | 27 | assert p.an("YAML code block") == "a YAML code block" 28 | assert p.an("Core ML function") == "a Core ML function" 29 | assert p.an("JSON code block") == "a JSON code block" 30 | -------------------------------------------------------------------------------- /tests/test_classical_all.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | 4 | class Test: 5 | def test_classical(self): 6 | p = inflect.engine() 7 | 8 | # DEFAULT... 9 | 10 | assert p.plural_noun("error", 0) == "errors", "classical 'zero' not active" 11 | assert p.plural_noun("wildebeest") == "wildebeests", ( 12 | "classical 'herd' not active" 13 | ) 14 | assert p.plural_noun("Sally") == "Sallys", "classical 'names' active" 15 | assert p.plural_noun("brother") == "brothers", "classical others not active" 16 | assert p.plural_noun("person") == "people", "classical 'persons' not active" 17 | assert p.plural_noun("formula") == "formulas", "classical 'ancient' not active" 18 | 19 | # CLASSICAL PLURALS ACTIVATED... 20 | 21 | p.classical(all=True) 22 | assert p.plural_noun("error", 0) == "error", "classical 'zero' active" 23 | assert p.plural_noun("wildebeest") == "wildebeest", "classical 'herd' active" 24 | assert p.plural_noun("Sally") == "Sallys", "classical 'names' active" 25 | assert p.plural_noun("brother") == "brethren", "classical others active" 26 | assert p.plural_noun("person") == "persons", "classical 'persons' active" 27 | assert p.plural_noun("formula") == "formulae", "classical 'ancient' active" 28 | 29 | # CLASSICAL PLURALS DEACTIVATED... 30 | 31 | p.classical(all=False) 32 | assert p.plural_noun("error", 0) == "errors", "classical 'zero' not active" 33 | assert p.plural_noun("wildebeest") == "wildebeests", ( 34 | "classical 'herd' not active" 35 | ) 36 | assert p.plural_noun("Sally") == "Sallies", "classical 'names' not active" 37 | assert p.plural_noun("brother") == "brothers", "classical others not active" 38 | assert p.plural_noun("person") == "people", "classical 'persons' not active" 39 | assert p.plural_noun("formula") == "formulas", "classical 'ancient' not active" 40 | 41 | # CLASSICAL PLURALS REREREACTIVATED... 42 | 43 | p.classical() 44 | assert p.plural_noun("error", 0) == "error", "classical 'zero' active" 45 | assert p.plural_noun("wildebeest") == "wildebeest", "classical 'herd' active" 46 | assert p.plural_noun("Sally") == "Sallys", "classical 'names' active" 47 | assert p.plural_noun("brother") == "brethren", "classical others active" 48 | assert p.plural_noun("person") == "persons", "classical 'persons' active" 49 | assert p.plural_noun("formula") == "formulae", "classical 'ancient' active" 50 | -------------------------------------------------------------------------------- /tests/test_classical_ancient.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | 4 | def test_ancient_1(): 5 | p = inflect.engine() 6 | 7 | # DEFAULT... 8 | 9 | assert p.plural_noun("formula") == "formulas" 10 | 11 | # "person" PLURALS ACTIVATED... 12 | 13 | p.classical(ancient=True) 14 | assert p.plural_noun("formula") == "formulae" 15 | 16 | # OTHER CLASSICALS NOT ACTIVATED... 17 | 18 | assert p.plural_noun("wildebeest") == "wildebeests" 19 | assert p.plural_noun("error", 0) == "errors" 20 | assert p.plural_noun("Sally") == "Sallys" 21 | assert p.plural_noun("brother") == "brothers" 22 | assert p.plural_noun("person") == "people" 23 | -------------------------------------------------------------------------------- /tests/test_classical_herd.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | 4 | def test_ancient_1(): 5 | p = inflect.engine() 6 | 7 | # DEFAULT... 8 | 9 | assert p.plural_noun("wildebeest") == "wildebeests" 10 | 11 | # "person" PLURALS ACTIVATED... 12 | 13 | p.classical(herd=True) 14 | assert p.plural_noun("wildebeest") == "wildebeest" 15 | 16 | # OTHER CLASSICALS NOT ACTIVATED... 17 | 18 | assert p.plural_noun("formula") == "formulas" 19 | assert p.plural_noun("error", 0) == "errors" 20 | assert p.plural_noun("Sally") == "Sallys" 21 | assert p.plural_noun("brother") == "brothers" 22 | assert p.plural_noun("person") == "people" 23 | -------------------------------------------------------------------------------- /tests/test_classical_names.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | 4 | def test_ancient_1(): 5 | p = inflect.engine() 6 | 7 | # DEFAULT... 8 | 9 | assert p.plural_noun("Sally") == "Sallys" 10 | assert p.plural_noun("Jones", 0) == "Joneses" 11 | 12 | # "person" PLURALS ACTIVATED... 13 | 14 | p.classical(names=True) 15 | assert p.plural_noun("Sally") == "Sallys" 16 | assert p.plural_noun("Jones", 0) == "Joneses" 17 | 18 | # OTHER CLASSICALS NOT ACTIVATED... 19 | 20 | assert p.plural_noun("wildebeest") == "wildebeests" 21 | assert p.plural_noun("formula") == "formulas" 22 | assert p.plural_noun("error", 0) == "errors" 23 | assert p.plural_noun("brother") == "brothers" 24 | assert p.plural_noun("person") == "people" 25 | -------------------------------------------------------------------------------- /tests/test_classical_person.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | 4 | def test_ancient_1(): 5 | p = inflect.engine() 6 | 7 | # DEFAULT... 8 | 9 | assert p.plural_noun("person") == "people" 10 | 11 | # "person" PLURALS ACTIVATED... 12 | 13 | p.classical(persons=True) 14 | assert p.plural_noun("person") == "persons" 15 | 16 | # OTHER CLASSICALS NOT ACTIVATED... 17 | 18 | assert p.plural_noun("wildebeest") == "wildebeests" 19 | assert p.plural_noun("formula") == "formulas" 20 | assert p.plural_noun("error", 0) == "errors" 21 | assert p.plural_noun("brother") == "brothers" 22 | assert p.plural_noun("Sally") == "Sallys" 23 | assert p.plural_noun("Jones", 0) == "Joneses" 24 | -------------------------------------------------------------------------------- /tests/test_classical_zero.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | 4 | def test_ancient_1(): 5 | p = inflect.engine() 6 | 7 | # DEFAULT... 8 | 9 | assert p.plural_noun("error", 0) == "errors" 10 | 11 | # "person" PLURALS ACTIVATED... 12 | 13 | p.classical(zero=True) 14 | assert p.plural_noun("error", 0) == "error" 15 | 16 | # OTHER CLASSICALS NOT ACTIVATED... 17 | 18 | assert p.plural_noun("wildebeest") == "wildebeests" 19 | assert p.plural_noun("formula") == "formulas" 20 | assert p.plural_noun("person") == "people" 21 | assert p.plural_noun("brother") == "brothers" 22 | assert p.plural_noun("Sally") == "Sallys" 23 | -------------------------------------------------------------------------------- /tests/test_compounds.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | p = inflect.engine() 4 | 5 | 6 | def test_compound_1(): 7 | assert p.singular_noun("hello-out-there") == "hello-out-there" 8 | 9 | 10 | def test_compound_2(): 11 | assert p.singular_noun("hello out there") == "hello out there" 12 | 13 | 14 | def test_compound_3(): 15 | assert p.singular_noun("continue-to-operate") == "continue-to-operate" 16 | 17 | 18 | def test_compound_4(): 19 | assert p.singular_noun("case of diapers") == "case of diapers" 20 | 21 | 22 | def test_unit_handling_degree(): 23 | test_cases = { 24 | "degree celsius": "degrees celsius", 25 | # 'degree Celsius': 'degrees Celsius', 26 | "degree fahrenheit": "degrees fahrenheit", 27 | "degree rankine": "degrees rankine", 28 | "degree fahrenheit second": "degree fahrenheit seconds", 29 | } 30 | for singular, plural in test_cases.items(): 31 | assert p.plural(singular) == plural 32 | 33 | 34 | def test_unit_handling_fractional(): 35 | test_cases = { 36 | "pound per square inch": "pounds per square inch", 37 | "metre per second": "metres per second", 38 | "kilometre per hour": "kilometres per hour", 39 | "cubic metre per second": "cubic metres per second", 40 | "dollar a year": "dollars a year", 41 | # Correct pluralization of denominator 42 | "foot per square second": "feet per square second", 43 | "mother-in-law per lifetime": "mothers-in-law per lifetime", 44 | "pound-force per square inch": "pounds-force per square inch", 45 | } 46 | for singular, plural in test_cases.items(): 47 | assert p.plural(singular) == plural 48 | 49 | 50 | def test_unit_handling_combined(): 51 | test_cases = { 52 | # Heat transfer coefficient unit 53 | "watt per square meter degree celsius": "watts per square meter degree celsius", 54 | "degree celsius per hour": "degrees celsius per hour", 55 | "degree fahrenheit hour square foot per btuit inch": ( 56 | "degree fahrenheit hour square feet per btuit inch" 57 | ), 58 | # 'degree Celsius per hour': 'degrees Celsius per hour', 59 | # 'degree Fahrenheit hour square foot per BtuIT inch': 60 | # 'degree Fahrenheit hour square feet per BtuIT inch' 61 | } 62 | for singular, plural in test_cases.items(): 63 | assert p.plural(singular) == plural 64 | 65 | 66 | def test_unit_open_compound_nouns(): 67 | test_cases = { 68 | "high school": "high schools", 69 | "master genie": "master genies", 70 | "MASTER genie": "MASTER genies", 71 | "Blood brother": "Blood brothers", 72 | "prima donna": "prima donnas", 73 | "prima DONNA": "prima DONNAS", 74 | } 75 | for singular, plural in test_cases.items(): 76 | assert p.plural(singular) == plural 77 | 78 | 79 | def test_unit_open_compound_nouns_classical(): 80 | p.classical(all=True) 81 | test_cases = { 82 | "master genie": "master genii", 83 | "MASTER genie": "MASTER genii", 84 | "Blood brother": "Blood brethren", 85 | "prima donna": "prime donne", 86 | "prima DONNA": "prime DONNE", 87 | } 88 | for singular, plural in test_cases.items(): 89 | assert p.plural(singular) == plural 90 | p.classical(all=False) 91 | -------------------------------------------------------------------------------- /tests/test_inflections.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | import inflect 6 | 7 | 8 | def is_eq(p, a, b): 9 | return ( 10 | p.compare(a, b) 11 | or p.plnounequal(a, b) 12 | or p.plverbequal(a, b) 13 | or p.pladjequal(a, b) 14 | ) 15 | 16 | 17 | def test_many(): # noqa: C901 18 | p = inflect.engine() 19 | 20 | data = get_data() 21 | 22 | for line in data: 23 | if "TODO:" in line: 24 | continue 25 | try: 26 | singular, rest = line.split("->", 1) 27 | except ValueError: 28 | continue 29 | singular = singular.strip() 30 | rest = rest.strip() 31 | try: 32 | plural, comment = rest.split("#", 1) 33 | except ValueError: 34 | plural = rest.strip() 35 | comment = "" 36 | try: 37 | mod_plural, class_plural = plural.split("|", 1) 38 | mod_plural = mod_plural.strip() 39 | class_plural = class_plural.strip() 40 | except ValueError: 41 | mod_plural = class_plural = plural.strip() 42 | if "verb" in comment.lower(): 43 | is_nv = "_V" 44 | elif "noun" in comment.lower(): 45 | is_nv = "_N" 46 | else: 47 | is_nv = "" 48 | 49 | p.classical(all=0, names=0) 50 | mod_PL_V = p.plural_verb(singular) 51 | mod_PL_N = p.plural_noun(singular) 52 | mod_PL = p.plural(singular) 53 | if is_nv == "_V": 54 | mod_PL_val = mod_PL_V 55 | elif is_nv == "_N": 56 | mod_PL_val = mod_PL_N 57 | else: 58 | mod_PL_val = mod_PL 59 | 60 | p.classical(all=1) 61 | class_PL_V = p.plural_verb(singular) 62 | class_PL_N = p.plural_noun(singular) 63 | class_PL = p.plural(singular) 64 | if is_nv == "_V": 65 | class_PL_val = class_PL_V 66 | elif is_nv == "_N": 67 | class_PL_val = class_PL_N 68 | else: 69 | class_PL_val = class_PL 70 | 71 | check_all( 72 | p, is_nv, singular, mod_PL_val, class_PL_val, mod_plural, class_plural 73 | ) 74 | 75 | 76 | def check_all(p, is_nv, singular, mod_PL_val, class_PL_val, mod_plural, class_plural): 77 | assert mod_plural == mod_PL_val 78 | assert class_plural == class_PL_val 79 | assert is_eq(p, singular, mod_plural) in ("s:p", "p:s", "eq") 80 | assert is_eq(p, mod_plural, singular) in ("p:s", "s:p", "eq") 81 | assert is_eq(p, singular, class_plural) in ("s:p", "p:s", "eq") 82 | assert is_eq(p, class_plural, singular) in ("p:s", "s:p", "eq") 83 | assert singular != "" 84 | expected = mod_PL_val if class_PL_val else f"{mod_PL_val}|{class_PL_val}" 85 | assert mod_PL_val == expected 86 | 87 | if is_nv != "_V": 88 | assert p.singular_noun(mod_plural, 1) == singular 89 | 90 | assert p.singular_noun(class_plural, 1) == singular 91 | 92 | 93 | def test_def(): 94 | p = inflect.engine() 95 | 96 | p.defnoun("kin", "kine") 97 | p.defnoun("(.*)x", "$1xen") 98 | 99 | p.defverb("foobar", "feebar", "foobar", "feebar", "foobars", "feebar") 100 | 101 | p.defadj("red", "red|gules") 102 | 103 | assert p.no("kin", 0) == "no kine" 104 | assert p.no("kin", 1) == "1 kin" 105 | assert p.no("kin", 2) == "2 kine" 106 | 107 | assert p.no("regex", 0) == "no regexen" 108 | 109 | assert p.plural("foobar", 2) == "feebar" 110 | assert p.plural("foobars", 2) == "feebar" 111 | 112 | assert p.plural("red", 0) == "red" 113 | assert p.plural("red", 1) == "red" 114 | assert p.plural("red", 2) == "red" 115 | p.classical(all=True) 116 | assert p.plural("red", 0) == "red" 117 | assert p.plural("red", 1) == "red" 118 | assert p.plural("red", 2) == "gules" 119 | 120 | 121 | def test_ordinal(): 122 | p = inflect.engine() 123 | assert p.ordinal(0) == "0th" 124 | assert p.ordinal(1) == p.ordinal("1") == "1st" 125 | assert p.ordinal(2) == "2nd" 126 | assert p.ordinal(3) == "3rd" 127 | assert p.ordinal(4) == "4th" 128 | assert p.ordinal(5) == "5th" 129 | assert p.ordinal(6) == "6th" 130 | assert p.ordinal(7) == "7th" 131 | assert p.ordinal(8) == "8th" 132 | assert p.ordinal(9) == "9th" 133 | assert p.ordinal(10) == "10th" 134 | assert p.ordinal(11) == "11th" 135 | assert p.ordinal(12) == "12th" 136 | assert p.ordinal(13) == "13th" 137 | assert p.ordinal(14) == "14th" 138 | assert p.ordinal(15) == "15th" 139 | assert p.ordinal(16) == "16th" 140 | assert p.ordinal(17) == "17th" 141 | assert p.ordinal(18) == "18th" 142 | assert p.ordinal(19) == "19th" 143 | assert p.ordinal(20) == "20th" 144 | assert p.ordinal(21) == "21st" 145 | assert p.ordinal(22) == "22nd" 146 | assert p.ordinal(23) == "23rd" 147 | assert p.ordinal(24) == "24th" 148 | assert p.ordinal(100) == "100th" 149 | assert p.ordinal(101) == "101st" 150 | assert p.ordinal(102) == "102nd" 151 | assert p.ordinal(103) == "103rd" 152 | assert p.ordinal(104) == "104th" 153 | 154 | assert p.ordinal(1.1) == p.ordinal("1.1") == "1.1st" 155 | assert p.ordinal(1.2) == "1.2nd" 156 | assert p.ordinal(5.502) == "5.502nd" 157 | 158 | assert p.ordinal("zero") == "zeroth" 159 | assert p.ordinal("one") == "first" 160 | assert p.ordinal("two") == "second" 161 | assert p.ordinal("three") == "third" 162 | assert p.ordinal("four") == "fourth" 163 | assert p.ordinal("five") == "fifth" 164 | assert p.ordinal("six") == "sixth" 165 | assert p.ordinal("seven") == "seventh" 166 | assert p.ordinal("eight") == "eighth" 167 | assert p.ordinal("nine") == "ninth" 168 | assert p.ordinal("ten") == "tenth" 169 | assert p.ordinal("eleven") == "eleventh" 170 | assert p.ordinal("twelve") == "twelfth" 171 | assert p.ordinal("thirteen") == "thirteenth" 172 | assert p.ordinal("fourteen") == "fourteenth" 173 | assert p.ordinal("fifteen") == "fifteenth" 174 | assert p.ordinal("sixteen") == "sixteenth" 175 | assert p.ordinal("seventeen") == "seventeenth" 176 | assert p.ordinal("eighteen") == "eighteenth" 177 | assert p.ordinal("nineteen") == "nineteenth" 178 | assert p.ordinal("twenty") == "twentieth" 179 | assert p.ordinal("twenty-one") == "twenty-first" 180 | assert p.ordinal("twenty-two") == "twenty-second" 181 | assert p.ordinal("twenty-three") == "twenty-third" 182 | assert p.ordinal("twenty-four") == "twenty-fourth" 183 | assert p.ordinal("one hundred") == "one hundredth" 184 | assert p.ordinal("one hundred and one") == "one hundred and first" 185 | assert p.ordinal("one hundred and two") == "one hundred and second" 186 | assert p.ordinal("one hundred and three") == "one hundred and third" 187 | assert p.ordinal("one hundred and four") == "one hundred and fourth" 188 | 189 | 190 | def test_decimal_ordinals(): 191 | """ 192 | Capture expectation around ordinals for decimals. 193 | 194 | This expectation is held loosely. Another expectation may be 195 | considered if appropriate. 196 | """ 197 | 198 | p = inflect.engine() 199 | assert p.ordinal("1.23") == "1.23rd" 200 | assert p.ordinal("7.09") == "7.09th" 201 | 202 | 203 | def test_prespart(): 204 | p = inflect.engine() 205 | assert p.present_participle("sees") == "seeing" 206 | assert p.present_participle("eats") == "eating" 207 | assert p.present_participle("bats") == "batting" 208 | assert p.present_participle("hates") == "hating" 209 | assert p.present_participle("spies") == "spying" 210 | assert p.present_participle("skis") == "skiing" 211 | 212 | 213 | def test_inflect_on_tuples(): 214 | p = inflect.engine() 215 | assert p.inflect("plural('egg', ('a', 'b', 'c'))") == "eggs" 216 | assert p.inflect("plural('egg', ['a', 'b', 'c'])") == "eggs" 217 | assert p.inflect("plural_noun('egg', ('a', 'b', 'c'))") == "eggs" 218 | assert p.inflect("plural_adj('a', ('a', 'b', 'c'))") == "some" 219 | assert p.inflect("plural_verb('was', ('a', 'b', 'c'))") == "were" 220 | assert p.inflect("singular_noun('eggs', ('a', 'b', 'c'))") == "eggs" 221 | assert p.inflect("an('error', ('a', 'b', 'c'))") == "('a', 'b', 'c') error" 222 | assert p.inflect("This is not a function(name)") == "This is not a function(name)" 223 | 224 | 225 | def test_inflect_on_builtin_constants(): 226 | p = inflect.engine() 227 | assert ( 228 | p.inflect("Plural of False is plural('False')") == "Plural of False is Falses" 229 | ) 230 | assert p.inflect("num(%d, False) plural('False')" % 10) == " Falses" 231 | 232 | assert p.inflect("plural('True')") == "Trues" 233 | assert p.inflect("num(%d, True) plural('False')" % 10) == "10 Falses" 234 | assert p.inflect("num(%d, %r) plural('False')" % (10, True)) == "10 Falses" 235 | 236 | assert p.inflect("plural('None')") == "Nones" 237 | assert p.inflect("num(%d, %r) plural('True')" % (10, None)) == "10 Trues" 238 | 239 | 240 | def test_inflect_keyword_args(): 241 | p = inflect.engine() 242 | assert ( 243 | p.inflect("number_to_words(1234, andword='')") 244 | == "one thousand, two hundred thirty-four" 245 | ) 246 | 247 | assert ( 248 | p.inflect("number_to_words(1234, andword='plus')") 249 | == "one thousand, two hundred plus thirty-four" 250 | ) 251 | 252 | assert ( 253 | p.inflect("number_to_words('555_1202', group=1, zero='oh')") 254 | == "five, five, five, one, two, oh, two" 255 | ) 256 | 257 | 258 | def test_NameError_in_strings(): 259 | with pytest.raises(NameError): 260 | p = inflect.engine() 261 | assert p.inflect("plural('two')") == "twoes" 262 | p.inflect("plural(two)") 263 | 264 | 265 | def get_data(): 266 | filename = os.path.join(os.path.dirname(__file__), "inflections.txt") 267 | with open(filename, encoding='utf-8') as strm: 268 | return list(map(str.strip, strm)) 269 | -------------------------------------------------------------------------------- /tests/test_join.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | 4 | def test_join(): 5 | p = inflect.engine() 6 | 7 | # Three words... 8 | words = "apple banana carrot".split() 9 | 10 | assert p.join(words), "apple, banana == and carrot" 11 | 12 | assert p.join(words, final_sep="") == "apple, banana and carrot" 13 | 14 | assert p.join(words, final_sep="...") == "apple, banana... and carrot" 15 | 16 | assert p.join(words, final_sep="...", conj="") == "apple, banana... carrot" 17 | 18 | assert p.join(words, conj="or") == "apple, banana, or carrot" 19 | 20 | # Three words with semicolons... 21 | words = ("apple,fuji", "banana", "carrot") 22 | 23 | assert p.join(words) == "apple,fuji; banana; and carrot" 24 | 25 | assert p.join(words, final_sep="") == "apple,fuji; banana and carrot" 26 | 27 | assert p.join(words, final_sep="...") == "apple,fuji; banana... and carrot" 28 | 29 | assert p.join(words, final_sep="...", conj="") == "apple,fuji; banana... carrot" 30 | 31 | assert p.join(words, conj="or") == "apple,fuji; banana; or carrot" 32 | 33 | # Two words... 34 | words = ("apple", "carrot") 35 | 36 | assert p.join(words) == "apple and carrot" 37 | 38 | assert p.join(words, final_sep="") == "apple and carrot" 39 | 40 | assert p.join(words, final_sep="...") == "apple and carrot" 41 | 42 | assert p.join(words, final_sep="...", conj="") == "apple carrot" 43 | 44 | assert p.join(words, final_sep="...", conj="", conj_spaced=False) == "applecarrot" 45 | 46 | assert p.join(words, conj="or") == "apple or carrot" 47 | 48 | # One word... 49 | words = ["carrot"] 50 | 51 | assert p.join(words) == "carrot" 52 | 53 | assert p.join(words, final_sep="") == "carrot" 54 | 55 | assert p.join(words, final_sep="...") == "carrot" 56 | 57 | assert p.join(words, final_sep="...", conj="") == "carrot" 58 | 59 | assert p.join(words, conj="or") == "carrot" 60 | -------------------------------------------------------------------------------- /tests/test_numwords.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | 4 | def test_loop(): 5 | p = inflect.engine() 6 | 7 | for thresh in range(21): 8 | for n in range(21): 9 | threshed = p.number_to_words(n, threshold=thresh) 10 | numwords = p.number_to_words(n) 11 | 12 | if n <= thresh: 13 | assert numwords == threshed 14 | else: 15 | # $threshed =~ s/\D//gxms; 16 | assert threshed == str(n) 17 | 18 | 19 | def test_lines(): 20 | p = inflect.engine() 21 | assert p.number_to_words(999, threshold=500) == "999" 22 | assert p.number_to_words(1000, threshold=500) == "1,000" 23 | assert p.number_to_words(10000, threshold=500) == "10,000" 24 | assert p.number_to_words(100000, threshold=500) == "100,000" 25 | assert p.number_to_words(1000000, threshold=500) == "1,000,000" 26 | 27 | assert p.number_to_words(999.3, threshold=500) == "999.3" 28 | assert p.number_to_words(1000.3, threshold=500) == "1,000.3" 29 | assert p.number_to_words(10000.3, threshold=500) == "10,000.3" 30 | 31 | assert p.number_to_words(100000.3, threshold=500) == "100,000.3" 32 | assert p.number_to_words(1000000.3, threshold=500) == "1,000,000.3" 33 | 34 | assert p.number_to_words(999, threshold=500, comma=0) == "999" 35 | assert p.number_to_words(1000, threshold=500, comma=0) == "1000" 36 | assert p.number_to_words(10000, threshold=500, comma=0) == "10000" 37 | assert p.number_to_words(100000, threshold=500, comma=0) == "100000" 38 | assert p.number_to_words(1000000, threshold=500, comma=0) == "1000000" 39 | 40 | assert p.number_to_words(999.3, threshold=500, comma=0) == "999.3" 41 | assert p.number_to_words(1000.3, threshold=500, comma=0) == "1000.3" 42 | assert p.number_to_words(10000.3, threshold=500, comma=0) == "10000.3" 43 | assert p.number_to_words(100000.3, threshold=500, comma=0) == "100000.3" 44 | assert p.number_to_words(1000000.3, threshold=500, comma=0) == "1000000.3" 45 | 46 | 47 | def test_array(): 48 | nw = [ 49 | ["0", "zero", "zero", "zero", "zero", "zeroth"], 50 | ["1", "one", "one", "one", "one", "first"], 51 | ["2", "two", "two", "two", "two", "second"], 52 | ["3", "three", "three", "three", "three", "third"], 53 | ["4", "four", "four", "four", "four", "fourth"], 54 | ["5", "five", "five", "five", "five", "fifth"], 55 | ["6", "six", "six", "six", "six", "sixth"], 56 | ["7", "seven", "seven", "seven", "seven", "seventh"], 57 | ["8", "eight", "eight", "eight", "eight", "eighth"], 58 | ["9", "nine", "nine", "nine", "nine", "ninth"], 59 | ["10", "ten", "one, zero", "ten", "ten", "tenth"], 60 | ["11", "eleven", "one, one", "eleven", "eleven", "eleventh"], 61 | ["12", "twelve", "one, two", "twelve", "twelve", "twelfth"], 62 | ["13", "thirteen", "one, three", "thirteen", "thirteen", "thirteenth"], 63 | ["14", "fourteen", "one, four", "fourteen", "fourteen", "fourteenth"], 64 | ["15", "fifteen", "one, five", "fifteen", "fifteen", "fifteenth"], 65 | ["16", "sixteen", "one, six", "sixteen", "sixteen", "sixteenth"], 66 | ["17", "seventeen", "one, seven", "seventeen", "seventeen", "seventeenth"], 67 | ["18", "eighteen", "one, eight", "eighteen", "eighteen", "eighteenth"], 68 | ["19", "nineteen", "one, nine", "nineteen", "nineteen", "nineteenth"], 69 | ["20", "twenty", "two, zero", "twenty", "twenty", "twentieth"], 70 | ["21", "twenty-one", "two, one", "twenty-one", "twenty-one", "twenty-first"], 71 | [ 72 | "29", 73 | "twenty-nine", 74 | "two, nine", 75 | "twenty-nine", 76 | "twenty-nine", 77 | "twenty-ninth", 78 | ], 79 | [ 80 | "99", 81 | "ninety-nine", 82 | "nine, nine", 83 | "ninety-nine", 84 | "ninety-nine", 85 | "ninety-ninth", 86 | ], 87 | [ 88 | "100", 89 | "one hundred", 90 | "one, zero, zero", 91 | "ten, zero", 92 | "one zero zero", 93 | "one hundredth", 94 | ], 95 | [ 96 | "101", 97 | "one hundred and one", 98 | "one, zero, one", 99 | "ten, one", 100 | "one zero one", 101 | "one hundred and first", 102 | ], 103 | [ 104 | "110", 105 | "one hundred and ten", 106 | "one, one, zero", 107 | "eleven, zero", 108 | "one ten", 109 | "one hundred and tenth", 110 | ], 111 | [ 112 | "111", 113 | "one hundred and eleven", 114 | "one, one, one", 115 | "eleven, one", 116 | "one eleven", 117 | "one hundred and eleventh", 118 | ], 119 | [ 120 | "900", 121 | "nine hundred", 122 | "nine, zero, zero", 123 | "ninety, zero", 124 | "nine zero zero", 125 | "nine hundredth", 126 | ], 127 | [ 128 | "999", 129 | "nine hundred and ninety-nine", 130 | "nine, nine, nine", 131 | "ninety-nine, nine", 132 | "nine ninety-nine", 133 | "nine hundred and ninety-ninth", 134 | ], 135 | [ 136 | "1000", 137 | "one thousand", 138 | "one, zero, zero, zero", 139 | "ten, zero zero", 140 | "one zero zero, zero", 141 | "one thousandth", 142 | ], 143 | [ 144 | "1001", 145 | "one thousand and one", 146 | "one, zero, zero, one", 147 | "ten, zero one", 148 | "one zero zero, one", 149 | "one thousand and first", 150 | ], 151 | [ 152 | "1010", 153 | "one thousand and ten", 154 | "one, zero, one, zero", 155 | "ten, ten", 156 | "one zero one, zero", 157 | "one thousand and tenth", 158 | ], 159 | [ 160 | "1100", 161 | "one thousand, one hundred", 162 | "one, one, zero, zero", 163 | "eleven, zero zero", 164 | "one ten, zero", 165 | "one thousand, one hundredth", 166 | ], 167 | [ 168 | "2000", 169 | "two thousand", 170 | "two, zero, zero, zero", 171 | "twenty, zero zero", 172 | "two zero zero, zero", 173 | "two thousandth", 174 | ], 175 | [ 176 | "10000", 177 | "ten thousand", 178 | "one, zero, zero, zero, zero", 179 | "ten, zero zero, zero", 180 | "one zero zero, zero zero", 181 | "ten thousandth", 182 | ], 183 | [ 184 | "100000", 185 | "one hundred thousand", 186 | "one, zero, zero, zero, zero, zero", 187 | "ten, zero zero, zero zero", 188 | "one zero zero, zero zero zero", 189 | "one hundred thousandth", 190 | ], 191 | [ 192 | "100001", 193 | "one hundred thousand and one", 194 | "one, zero, zero, zero, zero, one", 195 | "ten, zero zero, zero one", 196 | "one zero zero, zero zero one", 197 | "one hundred thousand and first", 198 | ], 199 | [ 200 | "123456", 201 | "one hundred and twenty-three thousand, four hundred and fifty-six", 202 | "one, two, three, four, five, six", 203 | "twelve, thirty-four, fifty-six", 204 | "one twenty-three, four fifty-six", 205 | "one hundred and twenty-three thousand, four hundred and fifty-sixth", 206 | ], 207 | [ 208 | "0123456", 209 | "one hundred and twenty-three thousand, four hundred and fifty-six", 210 | "zero, one, two, three, four, five, six", 211 | "zero one, twenty-three, forty-five, six", 212 | "zero twelve, three forty-five, six", 213 | "one hundred and twenty-three thousand, four hundred and fifty-sixth", 214 | ], 215 | [ 216 | "1234567", 217 | "one million, two hundred and thirty-four thousand, " 218 | "five hundred and sixty-seven", 219 | "one, two, three, four, five, six, seven", 220 | "twelve, thirty-four, fifty-six, seven", 221 | "one twenty-three, four fifty-six, seven", 222 | "one million, two hundred and thirty-four thousand, " 223 | "five hundred and sixty-seventh", 224 | ], 225 | [ 226 | "12345678", 227 | "twelve million, three hundred and forty-five thousand, " 228 | "six hundred and seventy-eight", 229 | "one, two, three, four, five, six, seven, eight", 230 | "twelve, thirty-four, fifty-six, seventy-eight", 231 | "one twenty-three, four fifty-six, seventy-eight", 232 | "twelve million, three hundred and forty-five thousand, " 233 | "six hundred and seventy-eighth", 234 | ], 235 | [ 236 | "12_345_678", 237 | "twelve million, three hundred and forty-five thousand, " 238 | "six hundred and seventy-eight", 239 | "one, two, three, four, five, six, seven, eight", 240 | "twelve, thirty-four, fifty-six, seventy-eight", 241 | "one twenty-three, four fifty-six, seventy-eight", 242 | ], 243 | [ 244 | "1234,5678", 245 | "twelve million, three hundred and forty-five thousand, " 246 | "six hundred and seventy-eight", 247 | "one, two, three, four, five, six, seven, eight", 248 | "twelve, thirty-four, fifty-six, seventy-eight", 249 | "one twenty-three, four fifty-six, seventy-eight", 250 | ], 251 | [ 252 | "1234567890", 253 | "one billion, two hundred and thirty-four million, five hundred " 254 | "and sixty-seven thousand, eight hundred and ninety", 255 | "one, two, three, four, five, six, seven, eight, nine, zero", 256 | "twelve, thirty-four, fifty-six, seventy-eight, ninety", 257 | "one twenty-three, four fifty-six, seven eighty-nine, zero", 258 | "one billion, two hundred and thirty-four million, five hundred " 259 | "and sixty-seven thousand, eight hundred and ninetieth", 260 | ], 261 | [ 262 | "123456789012345", 263 | "one hundred and twenty-three trillion, four hundred and " 264 | "fifty-six billion, seven hundred and eighty-nine million, twelve " 265 | "thousand, three hundred and forty-five", 266 | "one, two, three, four, five, six, seven, eight, nine, zero, one, " 267 | "two, three, four, five", 268 | "twelve, thirty-four, fifty-six, seventy-eight, ninety, twelve, " 269 | "thirty-four, five", 270 | "one twenty-three, four fifty-six, seven eighty-nine, " 271 | "zero twelve, three forty-five", 272 | "one hundred and twenty-three trillion, four hundred and " 273 | "fifty-six billion, seven hundred and eighty-nine million, " 274 | "twelve thousand, three hundred and forty-fifth", 275 | ], 276 | [ 277 | "12345678901234567890", 278 | "twelve quintillion, three hundred and forty-five quadrillion, " 279 | "six hundred and seventy-eight trillion, nine hundred and one " 280 | "billion, two hundred and thirty-four million, five hundred and " 281 | "sixty-seven thousand, eight hundred and ninety", 282 | "one, two, three, four, five, six, seven, eight, nine, zero, one, " 283 | "two, three, four, five, six, seven, eight, nine, zero", 284 | "twelve, thirty-four, fifty-six, seventy-eight, ninety, twelve, " 285 | "thirty-four, fifty-six, seventy-eight, ninety", 286 | "one twenty-three, four fifty-six, seven eighty-nine, " 287 | "zero twelve, three forty-five, six seventy-eight, ninety", 288 | "twelve quintillion, three hundred and forty-five quadrillion, " 289 | "six hundred and seventy-eight trillion, nine hundred and one " 290 | "billion, two hundred and thirty-four million, five hundred and " 291 | "sixty-seven thousand, eight hundred and ninetieth", 292 | ], 293 | [ 294 | "0.987654", 295 | "zero point nine eight seven six five four", 296 | "zero, point, nine, eight, seven, six, five, four", 297 | "zero, point, ninety-eight, seventy-six, fifty-four", 298 | "zero, point, nine eighty-seven, six fifty-four", 299 | "zeroth point nine eight seven six five four", 300 | "zero point nine eight seven six five fourth", 301 | ], 302 | [ 303 | ".987654", 304 | "point nine eight seven six five four", 305 | "point, nine, eight, seven, six, five, four", 306 | "point, ninety-eight, seventy-six, fifty-four", 307 | "point, nine eighty-seven, six fifty-four", 308 | "point nine eight seven six five four", 309 | "point nine eight seven six five fourth", 310 | ], 311 | [ 312 | "9.87654", 313 | "nine point eight seven six five four", 314 | "nine, point, eight, seven, six, five, four", 315 | "nine, point, eighty-seven, sixty-five, four", 316 | "nine, point, eight seventy-six, fifty-four", 317 | "ninth point eight seven six five four", 318 | "nine point eight seven six five fourth", 319 | ], 320 | [ 321 | "98.7654", 322 | "ninety-eight point seven six five four", 323 | "nine, eight, point, seven, six, five, four", 324 | "ninety-eight, point, seventy-six, fifty-four", 325 | "ninety-eight, point, seven sixty-five, four", 326 | "ninety-eighth point seven six five four", 327 | "ninety-eight point seven six five fourth", 328 | ], 329 | [ 330 | "987.654", 331 | "nine hundred and eighty-seven point six five four", 332 | "nine, eight, seven, point, six, five, four", 333 | "ninety-eight, seven, point, sixty-five, four", 334 | "nine eighty-seven, point, six fifty-four", 335 | "nine hundred and eighty-seventh point six five four", 336 | "nine hundred and eighty-seven point six five fourth", 337 | ], 338 | [ 339 | "9876.54", 340 | "nine thousand, eight hundred and seventy-six point five four", 341 | "nine, eight, seven, six, point, five, four", 342 | "ninety-eight, seventy-six, point, fifty-four", 343 | "nine eighty-seven, six, point, fifty-four", 344 | "nine thousand, eight hundred and seventy-sixth point five four", 345 | "nine thousand, eight hundred and seventy-six point five fourth", 346 | ], 347 | [ 348 | "98765.4", 349 | "ninety-eight thousand, seven hundred and sixty-five point four", 350 | "nine, eight, seven, six, five, point, four", 351 | "ninety-eight, seventy-six, five, point, four", 352 | "nine eighty-seven, sixty-five, point, four", 353 | "ninety-eight thousand, seven hundred and sixty-fifth point four", 354 | "ninety-eight thousand, seven hundred and sixty-five point fourth", 355 | ], 356 | [ 357 | "101.202.303", 358 | "one hundred and one point two zero two three zero three", 359 | "one, zero, one, point, two, zero, two, point, three, zero, three", 360 | "ten, one, point, twenty, two, point, thirty, three", 361 | "one zero one, point, two zero two, point, three zero three", 362 | ], 363 | [ 364 | "98765.", 365 | "ninety-eight thousand, seven hundred and sixty-five point", 366 | "nine, eight, seven, six, five, point", 367 | "ninety-eight, seventy-six, five, point", 368 | "nine eighty-seven, sixty-five, point", 369 | ], 370 | ] 371 | 372 | p = inflect.engine() 373 | 374 | for i in nw: 375 | go(p, i) 376 | 377 | 378 | def go(p, i): 379 | assert p.number_to_words(i[0]) == i[1] 380 | assert p.number_to_words(i[0], group=1) == i[2] 381 | assert p.number_to_words(i[0], group=2) == i[3] 382 | assert p.number_to_words(i[0], group=3) == i[4] 383 | if len(i) > 5: 384 | assert p.number_to_words(p.ordinal(i[0])) == i[5] 385 | if len(i) > 6: 386 | assert p.ordinal(p.number_to_words(i[0])) == i[6] 387 | else: 388 | if len(i) > 5: 389 | assert p.ordinal(p.number_to_words(i[0])) == i[5] 390 | 391 | # eq_ !eval { p.number_to_words(42, and=>); 1; }; 392 | # eq_ $@ =~ 'odd number of'; 393 | 394 | 395 | def test_issue_131(): 396 | p = inflect.engine() 397 | for nth_word in inflect.nth_suff: 398 | assert p.number_to_words(nth_word) == "zero" 399 | -------------------------------------------------------------------------------- /tests/test_pl_si.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import inflect 4 | 5 | 6 | @pytest.fixture(params=[False, True], ids=['classical off', 'classical on']) 7 | def classical(request): 8 | return request.param 9 | 10 | 11 | @pytest.mark.parametrize("word", ['Times', 'Jones']) 12 | def test_pl_si(classical, word): 13 | p = inflect.engine() 14 | p.classical(all=classical) 15 | assert p.singular_noun(p.plural_noun(word, 2), 1) == word 16 | -------------------------------------------------------------------------------- /tests/test_pwd.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typeguard import TypeCheckError 3 | 4 | import inflect 5 | from inflect import ( 6 | BadChunkingOptionError, 7 | BadGenderError, 8 | BadNumValueError, 9 | NumOutOfRangeError, 10 | UnknownClassicalModeError, 11 | ) 12 | 13 | missing = object() 14 | 15 | 16 | class Test: 17 | def test_enclose(self): 18 | # def enclose 19 | assert inflect.enclose("test") == "(?:test)" 20 | 21 | def test_joinstem(self): 22 | # def joinstem 23 | assert ( 24 | inflect.joinstem(-2, ["ephemeris", "iris", ".*itis"]) 25 | == "(?:ephemer|ir|.*it)" 26 | ) 27 | 28 | def test_classical(self): 29 | # classical dicts 30 | assert set(inflect.def_classical.keys()) == set(inflect.all_classical.keys()) 31 | assert set(inflect.def_classical.keys()) == set(inflect.no_classical.keys()) 32 | 33 | # def classical 34 | p = inflect.engine() 35 | assert p.classical_dict == inflect.def_classical 36 | 37 | p.classical() 38 | assert p.classical_dict == inflect.all_classical 39 | 40 | with pytest.raises(TypeError): 41 | p.classical(0) 42 | with pytest.raises(TypeError): 43 | p.classical(1) 44 | with pytest.raises(TypeError): 45 | p.classical("names") 46 | with pytest.raises(TypeError): 47 | p.classical("names", "zero") 48 | with pytest.raises(TypeError): 49 | p.classical("all") 50 | 51 | p.classical(all=False) 52 | assert p.classical_dict == inflect.no_classical 53 | 54 | p.classical(names=True, zero=True) 55 | mydict = inflect.def_classical.copy() 56 | mydict.update(dict(names=1, zero=1)) 57 | assert p.classical_dict == mydict 58 | 59 | p.classical(all=True) 60 | assert p.classical_dict == inflect.all_classical 61 | 62 | p.classical(all=False) 63 | p.classical(names=True, zero=True) 64 | mydict = inflect.def_classical.copy() 65 | mydict.update(dict(names=True, zero=True)) 66 | assert p.classical_dict == mydict 67 | 68 | p.classical(all=False) 69 | p.classical(names=True, zero=False) 70 | mydict = inflect.def_classical.copy() 71 | mydict.update(dict(names=True, zero=False)) 72 | assert p.classical_dict == mydict 73 | 74 | with pytest.raises(UnknownClassicalModeError): 75 | p.classical(bogus=True) 76 | 77 | def test_num(self): 78 | # def num 79 | p = inflect.engine() 80 | assert p.persistent_count is None 81 | 82 | p.num() 83 | assert p.persistent_count is None 84 | 85 | ret = p.num(3) 86 | assert p.persistent_count == 3 87 | assert ret == "3" 88 | 89 | p.num() 90 | ret = p.num("3") 91 | assert p.persistent_count == 3 92 | assert ret == "3" 93 | 94 | p.num() 95 | ret = p.num(count=3, show=1) 96 | assert p.persistent_count == 3 97 | assert ret == "3" 98 | 99 | p.num() 100 | ret = p.num(count=3, show=0) 101 | assert p.persistent_count == 3 102 | assert ret == "" 103 | 104 | with pytest.raises(BadNumValueError): 105 | p.num("text") 106 | 107 | def test_inflect(self): 108 | p = inflect.engine() 109 | for txt, ans in ( 110 | ("num(1)", "1"), 111 | ("num(1,0)", ""), 112 | ("num(1,1)", "1"), 113 | ("num(1) ", "1 "), 114 | (" num(1) ", " 1 "), 115 | ("num(3) num(1)", "3 1"), 116 | ): 117 | assert p.inflect(txt) == ans, f'p.inflect("{txt}") != "{ans}"' 118 | 119 | for txt, ans in ( 120 | ("plural('rock')", "rocks"), 121 | ("plural('rock') plural('child')", "rocks children"), 122 | ("num(2) plural('rock') plural('child')", "2 rocks children"), 123 | ( 124 | "plural('rock') plural_noun('rock') plural_verb('rocks') " 125 | "plural_adj('big') a('ant')", 126 | "rocks rocks rock big an ant", 127 | ), 128 | ( 129 | "an('rock') no('cat') ordinal(3) number_to_words(1234) " 130 | "present_participle('runs')", 131 | "a rock no cats 3rd one thousand, two hundred and thirty-four running", 132 | ), 133 | ("a('cat',0) a('cat',1) a('cat',2) a('cat', 2)", "0 cat a cat 2 cat 2 cat"), 134 | ): 135 | assert p.inflect(txt) == ans, f'p.inflect("{txt}") != "{ans}"' 136 | 137 | def test_user_input_fns(self): 138 | p = inflect.engine() 139 | 140 | assert p.pl_sb_user_defined == [] 141 | p.defnoun("VAX", "VAXen") 142 | assert p.plural("VAX") == "VAXEN" 143 | assert p.pl_sb_user_defined == ["VAX", "VAXen"] 144 | 145 | assert p.ud_match("word", p.pl_sb_user_defined) is None 146 | assert p.ud_match("VAX", p.pl_sb_user_defined) == "VAXen" 147 | assert p.ud_match("VVAX", p.pl_sb_user_defined) is None 148 | 149 | p.defnoun("cow", "cows|kine") 150 | assert p.plural("cow") == "cows" 151 | p.classical() 152 | assert p.plural("cow") == "kine" 153 | 154 | assert p.ud_match("cow", p.pl_sb_user_defined) == "cows|kine" 155 | 156 | p.defnoun("(.+i)o", r"$1i") 157 | assert p.plural("studio") == "studii" 158 | assert p.ud_match("studio", p.pl_sb_user_defined) == "studii" 159 | 160 | p.defnoun("aviatrix", "aviatrices") 161 | assert p.plural("aviatrix") == "aviatrices" 162 | assert p.ud_match("aviatrix", p.pl_sb_user_defined) == "aviatrices" 163 | p.defnoun("aviatrix", "aviatrixes") 164 | assert p.plural("aviatrix") == "aviatrixes" 165 | assert p.ud_match("aviatrix", p.pl_sb_user_defined) == "aviatrixes" 166 | p.defnoun("aviatrix", None) 167 | assert p.plural("aviatrix") == "aviatrices" 168 | assert p.ud_match("aviatrix", p.pl_sb_user_defined) is None 169 | 170 | p.defnoun("(cat)", r"$1s") 171 | assert p.plural("cat") == "cats" 172 | 173 | with pytest.raises(inflect.BadUserDefinedPatternError): 174 | p.defnoun("(??", None) 175 | 176 | p.defnoun(None, "any") # check None doesn't crash it 177 | 178 | # defadj 179 | p.defadj("hir", "their") 180 | assert p.plural("hir") == "their" 181 | assert p.ud_match("hir", p.pl_adj_user_defined) == "their" 182 | 183 | # defa defan 184 | p.defa("h") 185 | assert p.a("h") == "a h" 186 | assert p.ud_match("h", p.A_a_user_defined) == "a" 187 | 188 | p.defan("horrendous.*") 189 | assert p.a("horrendously") == "an horrendously" 190 | assert p.ud_match("horrendously", p.A_a_user_defined) == "an" 191 | 192 | def test_user_input_defverb(self): 193 | p = inflect.engine() 194 | p.defverb("will", "shall", "will", "will", "will", "will") 195 | assert p.ud_match("will", p.pl_v_user_defined) == "will" 196 | assert p.plural("will") == "will" 197 | 198 | @pytest.mark.xfail(reason="todo") 199 | def test_user_input_defverb_compare(self): 200 | p = inflect.engine() 201 | p.defverb("will", "shall", "will", "will", "will", "will") 202 | assert p.compare("will", "shall") == "s:p" 203 | assert p.compare_verbs("will", "shall") == "s:p" 204 | 205 | def test_postprocess(self): 206 | p = inflect.engine() 207 | for orig, infl, txt in ( 208 | ("cow", "cows", "cows"), 209 | ("I", "we", "we"), 210 | ("COW", "cows", "COWS"), 211 | ("Cow", "cows", "Cows"), 212 | ("cow", "cows|kine", "cows"), 213 | ("Entry", "entries", "Entries"), 214 | ("can of Coke", "cans of coke", "cans of Coke"), 215 | ): 216 | assert p.postprocess(orig, infl) == txt 217 | 218 | p.classical() 219 | assert p.postprocess("cow", "cows|kine") == "kine" 220 | 221 | def test_partition_word(self): 222 | p = inflect.engine() 223 | for txt, part in ( 224 | (" cow ", (" ", "cow", " ")), 225 | ("cow", ("", "cow", "")), 226 | (" cow", (" ", "cow", "")), 227 | ("cow ", ("", "cow", " ")), 228 | (" cow ", (" ", "cow", " ")), 229 | ("", ("", "", "")), 230 | ("bottle of beer", ("", "bottle of beer", "")), 231 | # spaces give weird results 232 | # (' '),('', ' ', '')), 233 | # (' '),(' ', ' ', '')), 234 | # (' '),(' ', ' ', '')), 235 | ): 236 | assert p.partition_word(txt) == part 237 | 238 | def test_pl(self): 239 | p = inflect.engine() 240 | for fn, sing, plur in ( 241 | (p.plural, "cow", "cows"), 242 | (p.plural, "thought", "thoughts"), 243 | (p.plural, "mouse", "mice"), 244 | (p.plural, "knife", "knives"), 245 | (p.plural, "knifes", "knife"), 246 | (p.plural, " cat ", " cats "), 247 | (p.plural, "court martial", "courts martial"), 248 | (p.plural, "a", "some"), 249 | (p.plural, "carmen", "carmina"), 250 | (p.plural, "quartz", "quartzes"), 251 | (p.plural, "care", "cares"), 252 | (p.plural_noun, "cow", "cows"), 253 | (p.plural_noun, "thought", "thoughts"), 254 | (p.plural_noun, "snooze", "snoozes"), 255 | (p.plural_verb, "runs", "run"), 256 | (p.plural_verb, "thought", "thought"), 257 | (p.plural_verb, "eyes", "eye"), 258 | (p.plural_adj, "a", "some"), 259 | (p.plural_adj, "this", "these"), 260 | (p.plural_adj, "that", "those"), 261 | (p.plural_adj, "my", "our"), 262 | (p.plural_adj, "cat's", "cats'"), 263 | (p.plural_adj, "child's", "children's"), 264 | ): 265 | assert fn(sing) == plur, ( 266 | f'{fn.__name__}("{sing}") == "{fn(sing)}" != "{plur}"' 267 | ) 268 | 269 | for sing, num, plur in ( 270 | ("cow", 1, "cow"), 271 | ("cow", 2, "cows"), 272 | ("cow", "one", "cow"), 273 | ("cow", "each", "cow"), 274 | ("cow", "two", "cows"), 275 | ("cow", 0, "cows"), 276 | ("cow", "zero", "cows"), 277 | ("runs", 0, "run"), 278 | ("runs", 1, "runs"), 279 | ("am", 0, "are"), 280 | ): 281 | assert p.plural(sing, num) == plur 282 | 283 | p.classical(zero=True) 284 | assert p.plural("cow", 0) == "cow" 285 | assert p.plural("cow", "zero") == "cow" 286 | assert p.plural("runs", 0) == "runs" 287 | assert p.plural("am", 0) == "am" 288 | assert p.plural_verb("runs", 1) == "runs" 289 | 290 | assert p.plural("die") == "dice" 291 | assert p.plural_noun("die") == "dice" 292 | 293 | with pytest.raises(TypeCheckError): 294 | p.plural("") 295 | with pytest.raises(TypeCheckError): 296 | p.plural_noun("") 297 | with pytest.raises(TypeCheckError): 298 | p.plural_verb("") 299 | with pytest.raises(TypeCheckError): 300 | p.plural_adj("") 301 | 302 | def test_sinoun(self): 303 | p = inflect.engine() 304 | for sing, plur in ( 305 | ("cat", "cats"), 306 | ("die", "dice"), 307 | ("goose", "geese"), 308 | ): 309 | assert p.singular_noun(plur) == sing 310 | assert p.inflect("singular_noun('%s')" % plur) == sing 311 | 312 | assert p.singular_noun("cats", count=2) == "cats" 313 | assert p.singular_noun("open valves", count=2) == "open valves" 314 | 315 | assert p.singular_noun("zombies") == "zombie" 316 | 317 | assert p.singular_noun("shoes") == "shoe" 318 | assert p.singular_noun("dancing shoes") == "dancing shoe" 319 | 320 | assert p.singular_noun("Matisses") == "Matisse" 321 | assert p.singular_noun("bouillabaisses") == "bouillabaisse" 322 | 323 | assert p.singular_noun("quartzes") == "quartz" 324 | 325 | assert p.singular_noun("Nietzsches") == "Nietzsche" 326 | assert p.singular_noun("aches") == "ache" 327 | 328 | assert p.singular_noun("Clives") == "Clive" 329 | assert p.singular_noun("weaves") == "weave" 330 | 331 | assert p.singular_noun("status") is False 332 | assert p.singular_noun("hiatus") is False 333 | 334 | def test_gender(self): 335 | p = inflect.engine() 336 | p.gender("feminine") 337 | for sing, plur in ( 338 | ("she", "they"), 339 | ("herself", "themselves"), 340 | ("hers", "theirs"), 341 | ("to her", "to them"), 342 | ("to herself", "to themselves"), 343 | ): 344 | assert p.singular_noun(plur) == sing, ( 345 | f"singular_noun({plur}) == {p.singular_noun(plur)} != {sing}" 346 | ) 347 | assert p.inflect("singular_noun('%s')" % plur) == sing 348 | 349 | p.gender("masculine") 350 | for sing, plur in ( 351 | ("he", "they"), 352 | ("himself", "themselves"), 353 | ("his", "theirs"), 354 | ("to him", "to them"), 355 | ("to himself", "to themselves"), 356 | ): 357 | assert p.singular_noun(plur) == sing, ( 358 | f"singular_noun({plur}) == {p.singular_noun(plur)} != {sing}" 359 | ) 360 | assert p.inflect("singular_noun('%s')" % plur) == sing 361 | 362 | p.gender("gender-neutral") 363 | for sing, plur in ( 364 | ("they", "they"), 365 | ("themself", "themselves"), 366 | ("theirs", "theirs"), 367 | ("to them", "to them"), 368 | ("to themself", "to themselves"), 369 | ): 370 | assert p.singular_noun(plur) == sing, ( 371 | f"singular_noun({plur}) == {p.singular_noun(plur)} != {sing}" 372 | ) 373 | assert p.inflect("singular_noun('%s')" % plur) == sing 374 | 375 | p.gender("neuter") 376 | for sing, plur in ( 377 | ("it", "they"), 378 | ("itself", "themselves"), 379 | ("its", "theirs"), 380 | ("to it", "to them"), 381 | ("to itself", "to themselves"), 382 | ): 383 | assert p.singular_noun(plur) == sing, ( 384 | f"singular_noun({plur}) == {p.singular_noun(plur)} != {sing}" 385 | ) 386 | assert p.inflect("singular_noun('%s')" % plur) == sing 387 | 388 | with pytest.raises(BadGenderError): 389 | p.gender("male") 390 | 391 | for sing, plur, gen in ( 392 | ("it", "they", "neuter"), 393 | ("she", "they", "feminine"), 394 | ("he", "they", "masculine"), 395 | ("they", "they", "gender-neutral"), 396 | ("she or he", "they", "feminine or masculine"), 397 | ("he or she", "they", "masculine or feminine"), 398 | ): 399 | assert p.singular_noun(plur, gender=gen) == sing 400 | 401 | with pytest.raises(BadGenderError): 402 | p.singular_noun("cats", gender="unknown gender") 403 | 404 | @pytest.mark.parametrize( 405 | 'sing,plur,res', 406 | ( 407 | ("index", "index", "eq"), 408 | ("index", "indexes", "s:p"), 409 | ("index", "indices", "s:p"), 410 | ("indexes", "index", "p:s"), 411 | ("indices", "index", "p:s"), 412 | ("indices", "indexes", "p:p"), 413 | ("indexes", "indices", "p:p"), 414 | ("indices", "indices", "eq"), 415 | ("inverted index", "inverted indices", "s:p"), 416 | ("inverted indices", "inverted index", "p:s"), 417 | ("inverted indexes", "inverted indices", "p:p"), 418 | ("opuses", "opera", "p:p"), 419 | ("opera", "opuses", "p:p"), 420 | ("brothers", "brethren", "p:p"), 421 | ("cats", "cats", "eq"), 422 | ("base", "basis", False), 423 | ("syrinx", "syringe", False), 424 | ("she", "he", False), 425 | ("opus", "operas", False), 426 | ("taxi", "taxes", False), 427 | ("time", "Times", False), 428 | ("time".lower(), "Times".lower(), "s:p"), 429 | ("courts martial", "court martial", "p:s"), 430 | ("my", "my", "eq"), 431 | ("my", "our", "s:p"), 432 | ("our", "our", "eq"), 433 | pytest.param( 434 | "dresses's", "dresses'", "p:p", marks=pytest.mark.xfail(reason="todo") 435 | ), 436 | pytest.param( 437 | "dress's", "dress'", "s:s", marks=pytest.mark.xfail(reason='todo') 438 | ), 439 | pytest.param( 440 | "Jess's", "Jess'", "s:s", marks=pytest.mark.xfail(reason='todo') 441 | ), 442 | ), 443 | ) 444 | def test_compare_simple(self, sing, plur, res): 445 | assert inflect.engine().compare(sing, plur) == res 446 | 447 | @pytest.mark.parametrize( 448 | 'sing,plur,res', 449 | ( 450 | ("index", "index", "eq"), 451 | ("index", "indexes", "s:p"), 452 | ("index", "indices", "s:p"), 453 | ("indexes", "index", "p:s"), 454 | ("indices", "index", "p:s"), 455 | ("indices", "indexes", "p:p"), 456 | ("indexes", "indices", "p:p"), 457 | ("indices", "indices", "eq"), 458 | ("inverted index", "inverted indices", "s:p"), 459 | ("inverted indices", "inverted index", "p:s"), 460 | ("inverted indexes", "inverted indices", "p:p"), 461 | ), 462 | ) 463 | def test_compare_nouns(self, sing, plur, res): 464 | assert inflect.engine().compare_nouns(sing, plur) == res 465 | 466 | @pytest.mark.parametrize( 467 | 'sing,plur,res', 468 | ( 469 | ("runs", "runs", "eq"), 470 | ("runs", "run", "s:p"), 471 | ("run", "run", "eq"), 472 | ), 473 | ) 474 | def test_compare_verbs(self, sing, plur, res): 475 | assert inflect.engine().compare_verbs(sing, plur) == res 476 | 477 | @pytest.mark.parametrize( 478 | 'sing,plur,res', 479 | ( 480 | ("my", "my", "eq"), 481 | ("my", "our", "s:p"), 482 | ("our", "our", "eq"), 483 | pytest.param( 484 | "dresses's", "dresses'", "p:p", marks=pytest.mark.xfail(reason="todo") 485 | ), 486 | pytest.param( 487 | "dress's", "dress'", "s:s", marks=pytest.mark.xfail(reason='todo') 488 | ), 489 | pytest.param( 490 | "Jess's", "Jess'", "s:s", marks=pytest.mark.xfail(reason='todo') 491 | ), 492 | ), 493 | ) 494 | def test_compare_adjectives(self, sing, plur, res): 495 | assert inflect.engine().compare_adjs(sing, plur) == res 496 | 497 | @pytest.mark.xfail() 498 | def test_compare_your_our(self): 499 | # multiple adjective plurals not (yet) supported 500 | p = inflect.engine() 501 | assert p.compare("your", "our") is False 502 | p.defadj("my", "our|your") # what's ours is yours 503 | assert p.compare("your", "our") == "p:p" 504 | 505 | def test__pl_reg_plurals(self): 506 | p = inflect.engine() 507 | for pair, stems, end1, end2, ans in ( 508 | ("indexes|indices", "dummy|ind", "exes", "ices", True), 509 | ("indexes|robots", "dummy|ind", "exes", "ices", False), 510 | ("beaus|beaux", ".*eau", "s", "x", True), 511 | ): 512 | assert p._pl_reg_plurals(pair, stems, end1, end2) == ans 513 | 514 | def test__pl_check_plurals_N(self): 515 | p = inflect.engine() 516 | assert p._pl_check_plurals_N("index", "indices") is False 517 | assert p._pl_check_plurals_N("indexes", "indices") is True 518 | assert p._pl_check_plurals_N("indices", "indexes") is True 519 | assert p._pl_check_plurals_N("stigmata", "stigmas") is True 520 | assert p._pl_check_plurals_N("phalanxes", "phalanges") is True 521 | 522 | def test__pl_check_plurals_adj(self): 523 | p = inflect.engine() 524 | assert p._pl_check_plurals_adj("indexes's", "indices's") is True 525 | assert p._pl_check_plurals_adj("indices's", "indexes's") is True 526 | assert p._pl_check_plurals_adj("indexes'", "indices's") is True 527 | assert p._pl_check_plurals_adj("indexes's", "indices'") is True 528 | assert p._pl_check_plurals_adj("indexes's", "indexes's") is False 529 | assert p._pl_check_plurals_adj("dogmas's", "dogmata's") is True 530 | assert p._pl_check_plurals_adj("dogmas'", "dogmata'") is True 531 | assert p._pl_check_plurals_adj("indexes'", "indices'") is True 532 | 533 | def test_count(self): 534 | p = inflect.engine() 535 | for txt, num in ( 536 | (1, 1), 537 | (2, 2), 538 | (0, 2), 539 | (87, 2), 540 | (-7, 2), 541 | ("1", 1), 542 | ("2", 2), 543 | ("0", 2), 544 | ("no", 2), 545 | ("zero", 2), 546 | ("nil", 2), 547 | ("a", 1), 548 | ("an", 1), 549 | ("one", 1), 550 | ("each", 1), 551 | ("every", 1), 552 | ("this", 1), 553 | ("that", 1), 554 | ("dummy", 2), 555 | ): 556 | assert p.get_count(txt) == num 557 | 558 | assert p.get_count() == "" 559 | p.num(3) 560 | assert p.get_count() == 2 561 | 562 | def test__plnoun(self): 563 | p = inflect.engine() 564 | for sing, plur in ( 565 | ("tuna", "tuna"), 566 | ("TUNA", "TUNA"), 567 | ("swordfish", "swordfish"), 568 | ("Governor General", "Governors General"), 569 | ("Governor-General", "Governors-General"), 570 | ("Major General", "Major Generals"), 571 | ("Major-General", "Major-Generals"), 572 | ("mother in law", "mothers in law"), 573 | ("mother-in-law", "mothers-in-law"), 574 | ("about me", "about us"), 575 | ("to it", "to them"), 576 | ("from it", "from them"), 577 | ("with it", "with them"), 578 | ("I", "we"), 579 | ("you", "you"), 580 | ("me", "us"), 581 | ("mine", "ours"), 582 | ("child", "children"), 583 | ("brainchild", "brainchilds"), 584 | ("human", "humans"), 585 | ("soliloquy", "soliloquies"), 586 | ("chairwoman", "chairwomen"), 587 | ("goose", "geese"), 588 | ("tooth", "teeth"), 589 | ("foot", "feet"), 590 | ("forceps", "forceps"), 591 | ("protozoon", "protozoa"), 592 | ("czech", "czechs"), 593 | ("codex", "codices"), 594 | ("radix", "radices"), 595 | ("bacterium", "bacteria"), 596 | ("alumnus", "alumni"), 597 | ("criterion", "criteria"), 598 | ("alumna", "alumnae"), 599 | ("bias", "biases"), 600 | ("quiz", "quizzes"), 601 | ("fox", "foxes"), 602 | ("shelf", "shelves"), 603 | ("leaf", "leaves"), 604 | ("midwife", "midwives"), 605 | ("scarf", "scarves"), 606 | ("key", "keys"), 607 | ("Sally", "Sallys"), 608 | ("sally", "sallies"), 609 | ("ado", "ados"), 610 | ("auto", "autos"), 611 | ("alto", "altos"), 612 | ("zoo", "zoos"), 613 | ("tomato", "tomatoes"), 614 | ): 615 | assert p._plnoun(sing) == plur, ( 616 | f'p._plnoun("{sing}") == {p._plnoun(sing)} != "{plur}"' 617 | ) 618 | 619 | assert p._sinoun(plur) == sing, f'p._sinoun("{plur}") != "{sing}"' 620 | 621 | # words where forming singular is ambiguous or not attempted 622 | for sing, plur in ( 623 | ("son of a gun", "sons of guns"), 624 | ("son-of-a-gun", "sons-of-guns"), 625 | ("basis", "bases"), 626 | ("Jess", "Jesses"), 627 | ): 628 | assert p._plnoun(sing) == plur, f'p._plnoun("{sing}") != "{plur}"' 629 | 630 | p.num(1) 631 | assert p._plnoun("cat") == "cat" 632 | p.num(3) 633 | 634 | p.classical(herd=True) 635 | assert p._plnoun("swine") == "swine" 636 | p.classical(herd=False) 637 | assert p._plnoun("swine") == "swines" 638 | p.classical(persons=True) 639 | assert p._plnoun("chairperson") == "chairpersons" 640 | p.classical(persons=False) 641 | assert p._plnoun("chairperson") == "chairpeople" 642 | p.classical(ancient=True) 643 | assert p._plnoun("formula") == "formulae" 644 | p.classical(ancient=False) 645 | assert p._plnoun("formula") == "formulas" 646 | 647 | p.classical() 648 | for sing, plur in ( 649 | ("matrix", "matrices"), 650 | ("gateau", "gateaux"), 651 | ("millieu", "millieux"), 652 | ("syrinx", "syringes"), 653 | ("stamen", "stamina"), 654 | ("apex", "apices"), 655 | ("appendix", "appendices"), 656 | ("maximum", "maxima"), 657 | ("focus", "foci"), 658 | ("status", "status"), 659 | ("aurora", "aurorae"), 660 | ("soma", "somata"), 661 | ("iris", "irides"), 662 | ("solo", "soli"), 663 | ("oxymoron", "oxymora"), 664 | ("goy", "goyim"), 665 | ("afrit", "afriti"), 666 | ): 667 | assert p._plnoun(sing) == plur 668 | 669 | # p.classical(0) 670 | # p.classical('names') 671 | # classical now back to the default mode 672 | 673 | @pytest.mark.parametrize( 674 | 'sing, plur', 675 | ( 676 | pytest.param( 677 | 'about ME', 678 | 'about US', 679 | marks=pytest.mark.xfail(reason='does not keep case'), 680 | ), 681 | pytest.param( 682 | 'YOU', 683 | 'YOU', 684 | marks=pytest.mark.xfail(reason='does not keep case'), 685 | ), 686 | ), 687 | ) 688 | def test_plnoun_retains_case(self, sing, plur): 689 | assert inflect.engine()._plnoun(sing) == plur 690 | 691 | def test_classical_pl(self): 692 | p = inflect.engine() 693 | p.classical() 694 | for sing, plur in (("brother", "brethren"), ("dogma", "dogmata")): 695 | assert p.plural(sing) == plur 696 | 697 | def test__pl_special_verb(self): 698 | p = inflect.engine() 699 | with pytest.raises(TypeCheckError): 700 | p._pl_special_verb("") 701 | assert p._pl_special_verb("am") == "are" 702 | assert p._pl_special_verb("am", 0) == "are" 703 | assert p._pl_special_verb("runs", 0) == "run" 704 | p.classical(zero=True) 705 | assert p._pl_special_verb("am", 0) is False 706 | assert p._pl_special_verb("am", 1) == "am" 707 | assert p._pl_special_verb("am", 2) == "are" 708 | assert p._pl_special_verb("runs", 0) is False 709 | assert p._pl_special_verb("am going to") == "are going to" 710 | assert p._pl_special_verb("did") == "did" 711 | assert p._pl_special_verb("wasn't") == "weren't" 712 | assert p._pl_special_verb("shouldn't") == "shouldn't" 713 | assert p._pl_special_verb("bias") is False 714 | assert p._pl_special_verb("news") is False 715 | assert p._pl_special_verb("Jess") is False 716 | assert p._pl_special_verb(" ") is False 717 | assert p._pl_special_verb("brushes") == "brush" 718 | assert p._pl_special_verb("fixes") == "fix" 719 | assert p._pl_special_verb("quizzes") == "quiz" 720 | assert p._pl_special_verb("fizzes") == "fizz" 721 | assert p._pl_special_verb("dresses") == "dress" 722 | assert p._pl_special_verb("flies") == "fly" 723 | assert p._pl_special_verb("canoes") == "canoe" 724 | assert p._pl_special_verb("horseshoes") == "horseshoe" 725 | assert p._pl_special_verb("does") == "do" 726 | # TODO: what's a real word to test this case? 727 | assert p._pl_special_verb("zzzoes") == "zzzo" 728 | assert p._pl_special_verb("runs") == "run" 729 | 730 | def test__pl_general_verb(self): 731 | p = inflect.engine() 732 | assert p._pl_general_verb("acts") == "act" 733 | assert p._pl_general_verb("act") == "act" 734 | assert p._pl_general_verb("saw") == "saw" 735 | assert p._pl_general_verb("runs", 1) == "runs" 736 | 737 | @pytest.mark.parametrize( 738 | 'adj,plur', 739 | ( 740 | ("a", "some"), 741 | ("my", "our"), 742 | ("John's", "Johns'"), 743 | ("tuna's", "tuna's"), 744 | ("TUNA's", "TUNA's"), 745 | ("bad", False), 746 | ("'", False), 747 | pytest.param( 748 | "JOHN's", 749 | "JOHNS'", 750 | marks=pytest.mark.xfail(reason='should this be handled?'), 751 | ), 752 | pytest.param( 753 | "JOHN'S", 754 | "JOHNS'", 755 | marks=pytest.mark.xfail(reason="can't handle capitals"), 756 | ), 757 | pytest.param( 758 | "TUNA'S", 759 | "TUNA'S", 760 | marks=pytest.mark.xfail(reason="can't handle capitals"), 761 | ), 762 | ), 763 | ) 764 | def test__pl_special_adjective(self, adj, plur): 765 | p = inflect.engine() 766 | assert p._pl_special_adjective(adj) == plur 767 | 768 | @pytest.mark.parametrize( 769 | 'sing, plur', 770 | ( 771 | ("cat", "a cat"), 772 | ("euphemism", "a euphemism"), 773 | ("Euler number", "an Euler number"), 774 | ("hour", "an hour"), 775 | ("houri", "a houri"), 776 | ("nth", "an nth"), 777 | ("rth", "an rth"), 778 | ("sth", "an sth"), 779 | ("xth", "an xth"), 780 | ("ant", "an ant"), 781 | ("book", "a book"), 782 | ("RSPCA", "an RSPCA"), 783 | ("SONAR", "a SONAR"), 784 | ("FJO", "a FJO"), 785 | ("FJ", "an FJ"), 786 | ("NASA", "a NASA"), 787 | ("UN", "a UN"), 788 | ("yak", "a yak"), 789 | ("yttrium", "an yttrium"), 790 | ("a elephant", "an elephant"), 791 | ("a giraffe", "a giraffe"), 792 | ("an ewe", "a ewe"), 793 | ("a orangutan", "an orangutan"), 794 | ("R.I.P.", "an R.I.P."), 795 | ("C.O.D.", "a C.O.D."), 796 | ("e-mail", "an e-mail"), 797 | ("X-ray", "an X-ray"), 798 | ("T-square", "a T-square"), 799 | ("LCD", "an LCD"), 800 | ("XML", "an XML"), 801 | ("YWCA", "a YWCA"), 802 | ("LED", "a LED"), 803 | ("OPEC", "an OPEC"), 804 | ("FAQ", "a FAQ"), 805 | ("UNESCO", "a UNESCO"), 806 | ("a", "an a"), 807 | ("an", "an an"), 808 | ("an ant", "an ant"), 809 | ("a cat", "a cat"), 810 | ("an cat", "a cat"), 811 | ("a ant", "an ant"), 812 | ), 813 | ) 814 | def test_a(self, sing, plur): 815 | p = inflect.engine() 816 | assert p.a(sing) == plur 817 | 818 | def test_a_alt(self): 819 | p = inflect.engine() 820 | assert p.a("cat", 1) == "a cat" 821 | assert p.a("cat", 2) == "2 cat" 822 | 823 | with pytest.raises(TypeCheckError): 824 | p.a("") 825 | 826 | def test_a_and_an_same_method(self): 827 | assert inflect.engine.a == inflect.engine.an 828 | p = inflect.engine() 829 | assert p.a == p.an 830 | 831 | def test_no(self): 832 | p = inflect.engine() 833 | assert p.no("cat") == "no cats" 834 | assert p.no("cat", count=3) == "3 cats" 835 | assert p.no("cat", count="three") == "three cats" 836 | assert p.no("cat", count=1) == "1 cat" 837 | assert p.no("cat", count="one") == "one cat" 838 | assert p.no("mouse") == "no mice" 839 | p.num(3) 840 | assert p.no("cat") == "3 cats" 841 | 842 | @pytest.mark.parametrize( 843 | 'sing, plur', 844 | ( 845 | ("runs", "running"), 846 | ("dies", "dying"), 847 | ("glues", "gluing"), 848 | ("eyes", "eying"), 849 | ("skis", "skiing"), 850 | ("names", "naming"), 851 | ("sees", "seeing"), 852 | ("hammers", "hammering"), 853 | ("bats", "batting"), 854 | ("eats", "eating"), 855 | ("loves", "loving"), 856 | ("spies", "spying"), 857 | ("hoes", "hoeing"), 858 | ("alibis", "alibiing"), 859 | ("is", "being"), 860 | ("are", "being"), 861 | ("had", "having"), 862 | ("has", "having"), 863 | ), 864 | ) 865 | def test_prespart(self, sing, plur): 866 | p = inflect.engine() 867 | assert p.present_participle(sing) == plur 868 | 869 | @pytest.mark.parametrize( 870 | 'num, ord', 871 | ( 872 | ("1", "1st"), 873 | ("2", "2nd"), 874 | ("3", "3rd"), 875 | ("4", "4th"), 876 | ("10", "10th"), 877 | ("28", "28th"), 878 | ("100", "100th"), 879 | ("101", "101st"), 880 | ("1000", "1000th"), 881 | ("1001", "1001st"), 882 | ("0", "0th"), 883 | ("one", "first"), 884 | ("two", "second"), 885 | ("four", "fourth"), 886 | ("twenty", "twentieth"), 887 | ("one hundered", "one hunderedth"), 888 | ("one hundered and one", "one hundered and first"), 889 | ("zero", "zeroth"), 890 | ("n", "nth"), # bonus! 891 | ), 892 | ) 893 | def test_ordinal(self, num, ord): 894 | p = inflect.engine() 895 | assert p.ordinal(num) == ord 896 | 897 | def test_millfn(self): 898 | p = inflect.engine() 899 | millfn = p.millfn 900 | assert millfn(1) == " thousand" 901 | assert millfn(2) == " million" 902 | assert millfn(3) == " billion" 903 | assert millfn(0) == " " 904 | assert millfn(11) == " decillion" 905 | with pytest.raises(NumOutOfRangeError): 906 | millfn(12) 907 | 908 | def test_unitfn(self): 909 | p = inflect.engine() 910 | unitfn = p.unitfn 911 | assert unitfn(1, 2) == "one million" 912 | assert unitfn(1, 3) == "one billion" 913 | assert unitfn(5, 3) == "five billion" 914 | assert unitfn(5, 0) == "five " 915 | assert unitfn(0, 0) == " " 916 | 917 | def test_tenfn(self): 918 | p = inflect.engine() 919 | tenfn = p.tenfn 920 | assert tenfn(3, 1, 2) == "thirty-one million" 921 | assert tenfn(3, 0, 2) == "thirty million" 922 | assert tenfn(0, 1, 2) == "one million" 923 | assert tenfn(1, 1, 2) == "eleven million" 924 | assert tenfn(1, 0, 2) == "ten million" 925 | assert tenfn(1, 0, 0) == "ten " 926 | assert tenfn(0, 0, 0) == " " 927 | 928 | def test_hundfn(self): 929 | p = inflect.engine() 930 | hundfn = p.hundfn 931 | p._number_args = dict(andword="and") 932 | assert hundfn(4, 3, 1, 2) == "four hundred and thirty-one million, " 933 | assert hundfn(4, 0, 0, 2) == "four hundred million, " 934 | assert hundfn(4, 0, 5, 2) == "four hundred and five million, " 935 | assert hundfn(0, 3, 1, 2) == "thirty-one million, " 936 | assert hundfn(0, 0, 7, 2) == "seven million, " 937 | 938 | def test_enword(self): 939 | p = inflect.engine() 940 | enword = p.enword 941 | assert enword("5", 1) == "five, " 942 | p._number_args = dict(zero="zero", one="one", andword="and") 943 | assert enword("0", 1) == " zero, " 944 | assert enword("1", 1) == " one, " 945 | assert enword("347", 1) == "three, four, seven, " 946 | 947 | assert enword("34", 2) == "thirty-four , " 948 | assert enword("347", 2) == "thirty-four , seven, " 949 | assert enword("34768", 2) == "thirty-four , seventy-six , eight, " 950 | assert enword("1", 2) == "one, " 951 | 952 | assert enword("134", 3) == " one thirty-four , " 953 | 954 | assert enword("0", -1) == "zero" 955 | assert enword("1", -1) == "one" 956 | 957 | assert enword("3", -1) == "three , " 958 | assert enword("12", -1) == "twelve , " 959 | assert enword("123", -1) == "one hundred and twenty-three , " 960 | assert enword("1234", -1) == "one thousand, two hundred and thirty-four , " 961 | assert ( 962 | enword("12345", -1) == "twelve thousand, three hundred and forty-five , " 963 | ) 964 | assert ( 965 | enword("123456", -1) 966 | == "one hundred and twenty-three thousand, four hundred and fifty-six , " 967 | ) 968 | assert ( 969 | enword("1234567", -1) 970 | == "one million, two hundred and thirty-four thousand, " 971 | "five hundred and sixty-seven , " 972 | ) 973 | 974 | @pytest.mark.xfail(reason="doesn't use indicated word for 'one'") 975 | def test_enword_number_args_override(self): 976 | p = inflect.engine() 977 | p._number_args["one"] = "single" 978 | assert p.enword("1", 2) == "single, " 979 | 980 | def test_numwords(self): 981 | p = inflect.engine() 982 | numwords = p.number_to_words 983 | 984 | for n, word in ( 985 | ("1", "one"), 986 | ("10", "ten"), 987 | ("100", "one hundred"), 988 | ("1000", "one thousand"), 989 | ("10000", "ten thousand"), 990 | ("100000", "one hundred thousand"), 991 | ("1000000", "one million"), 992 | ("10000000", "ten million"), 993 | ("+10", "plus ten"), 994 | ("-10", "minus ten"), 995 | ("10.", "ten point"), 996 | (".10", "point one zero"), 997 | ): 998 | assert numwords(n) == word 999 | 1000 | for n, word, _wrongword in ( 1001 | # TODO: should be one point two three 1002 | ("1.23", "one point two three", "one point twenty-three"), 1003 | ): 1004 | assert numwords(n) == word 1005 | 1006 | for n, txt in ( 1007 | (3, "three bottles of beer on the wall"), 1008 | (2, "two bottles of beer on the wall"), 1009 | (1, "a solitary bottle of beer on the wall"), 1010 | (0, "no more bottles of beer on the wall"), 1011 | ): 1012 | assert ( 1013 | "{}{}".format( 1014 | numwords(n, one="a solitary", zero="no more"), 1015 | p.plural(" bottle of beer on the wall", n), 1016 | ) 1017 | == txt 1018 | ) 1019 | 1020 | assert numwords(0, one="one", zero="zero") == "zero" 1021 | 1022 | assert numwords("1234") == "one thousand, two hundred and thirty-four" 1023 | assert numwords("1234", wantlist=True) == [ 1024 | "one thousand", 1025 | "two hundred and thirty-four", 1026 | ] 1027 | assert numwords("1234567", wantlist=True) == [ 1028 | "one million", 1029 | "two hundred and thirty-four thousand", 1030 | "five hundred and sixty-seven", 1031 | ] 1032 | assert numwords("+10", wantlist=True) == ["plus", "ten"] 1033 | assert numwords("1234", andword="") == "one thousand, two hundred thirty-four" 1034 | assert ( 1035 | numwords("1234", andword="plus") 1036 | == "one thousand, two hundred plus thirty-four" 1037 | ) 1038 | assert numwords(p.ordinal("21")) == "twenty-first" 1039 | assert numwords("9", threshold=10) == "nine" 1040 | assert numwords("10", threshold=10) == "ten" 1041 | assert numwords("11", threshold=10) == "11" 1042 | assert numwords("1000", threshold=10) == "1,000" 1043 | assert numwords("123", threshold=10) == "123" 1044 | assert numwords("1234", threshold=10) == "1,234" 1045 | assert numwords("1234.5678", threshold=10) == "1,234.5678" 1046 | assert numwords("1", decimal=None) == "one" 1047 | assert ( 1048 | numwords("1234.5678", decimal=None) 1049 | == "twelve million, three hundred and forty-five " 1050 | "thousand, six hundred and seventy-eight" 1051 | ) 1052 | 1053 | def test_numwords_group_chunking_error(self): 1054 | p = inflect.engine() 1055 | with pytest.raises(BadChunkingOptionError): 1056 | p.number_to_words("1234", group=4) 1057 | 1058 | @pytest.mark.parametrize( 1059 | 'input,kwargs,expect', 1060 | ( 1061 | ("12345", dict(group=2), "twelve, thirty-four, five"), 1062 | ("123456", dict(group=3), "one twenty-three, four fifty-six"), 1063 | ("12345", dict(group=1), "one, two, three, four, five"), 1064 | ( 1065 | "1234th", 1066 | dict(group=0, andword="and"), 1067 | "one thousand, two hundred and thirty-fourth", 1068 | ), 1069 | ( 1070 | "1234th", 1071 | dict(group=0), 1072 | "one thousand, two hundred and thirty-fourth", 1073 | ), 1074 | ("120", dict(group=2), "twelve, zero"), 1075 | ("120", dict(group=2, zero="oh", one="unity"), "twelve, oh"), 1076 | ( 1077 | "555_1202", 1078 | dict(group=1, zero="oh"), 1079 | "five, five, five, one, two, oh, two", 1080 | ), 1081 | ( 1082 | "555_1202", 1083 | dict(group=1, one="unity"), 1084 | "five, five, five, unity, two, zero, two", 1085 | ), 1086 | ( 1087 | "123.456", 1088 | dict(group=1, decimal="mark", one="one"), 1089 | "one, two, three, mark, four, five, six", 1090 | ), 1091 | pytest.param( 1092 | '12345', 1093 | dict(group=3), 1094 | 'one hundred and twenty-three', 1095 | marks=pytest.mark.xfail(reason="'hundred and' missing"), 1096 | ), 1097 | pytest.param( 1098 | '101', 1099 | dict(group=2, zero="oh", one="unity"), 1100 | "ten, unity", 1101 | marks=pytest.mark.xfail(reason="ignoring 'one' param with group=2"), 1102 | ), 1103 | ), 1104 | ) 1105 | def test_numwords_group(self, input, kwargs, expect): 1106 | p = inflect.engine() 1107 | assert p.number_to_words(input, **kwargs) == expect 1108 | 1109 | def test_wordlist(self): 1110 | p = inflect.engine() 1111 | wordlist = p.join 1112 | assert wordlist([]) == "" 1113 | assert wordlist(("apple",)) == "apple" 1114 | assert wordlist(("apple", "banana")) == "apple and banana" 1115 | assert wordlist(("apple", "banana", "carrot")) == "apple, banana, and carrot" 1116 | assert wordlist(("apple", "1,000", "carrot")) == "apple; 1,000; and carrot" 1117 | assert ( 1118 | wordlist(("apple", "1,000", "carrot"), sep=",") 1119 | == "apple, 1,000, and carrot" 1120 | ) 1121 | assert ( 1122 | wordlist(("apple", "banana", "carrot"), final_sep="") 1123 | == "apple, banana and carrot" 1124 | ) 1125 | assert ( 1126 | wordlist(("apple", "banana", "carrot"), final_sep=";") 1127 | == "apple, banana; and carrot" 1128 | ) 1129 | assert ( 1130 | wordlist(("apple", "banana", "carrot"), conj="or") 1131 | == "apple, banana, or carrot" 1132 | ) 1133 | 1134 | assert wordlist(("apple", "banana"), conj=" or ") == "apple or banana" 1135 | assert wordlist(("apple", "banana"), conj="&") == "apple & banana" 1136 | assert ( 1137 | wordlist(("apple", "banana"), conj="&", conj_spaced=False) == "apple&banana" 1138 | ) 1139 | assert ( 1140 | wordlist(("apple", "banana"), conj="& ", conj_spaced=False) 1141 | == "apple& banana" 1142 | ) 1143 | 1144 | assert ( 1145 | wordlist(("apple", "banana", "carrot"), conj=" or ") 1146 | == "apple, banana, or carrot" 1147 | ) 1148 | assert ( 1149 | wordlist(("apple", "banana", "carrot"), conj="+") 1150 | == "apple, banana, + carrot" 1151 | ) 1152 | assert ( 1153 | wordlist(("apple", "banana", "carrot"), conj="&") 1154 | == "apple, banana, & carrot" 1155 | ) 1156 | assert ( 1157 | wordlist(("apple", "banana", "carrot"), conj="&", conj_spaced=False) 1158 | == "apple, banana,&carrot" 1159 | ) 1160 | assert ( 1161 | wordlist(("apple", "banana", "carrot"), conj=" &", conj_spaced=False) 1162 | == "apple, banana, &carrot" 1163 | ) 1164 | 1165 | def test_doc_examples(self): 1166 | p = inflect.engine() 1167 | assert p.plural_noun("I") == "we" 1168 | assert p.plural_verb("saw") == "saw" 1169 | assert p.plural_adj("my") == "our" 1170 | assert p.plural_noun("saw") == "saws" 1171 | assert p.plural("was") == "were" 1172 | assert p.plural("was", 1) == "was" 1173 | assert p.plural_verb("was", 2) == "were" 1174 | assert p.plural_verb("was") == "were" 1175 | assert p.plural_verb("was", 1) == "was" 1176 | 1177 | for errors, txt in ( 1178 | (0, "There were no errors"), 1179 | (1, "There was 1 error"), 1180 | (2, "There were 2 errors"), 1181 | ): 1182 | assert ( 1183 | "There {}{}".format( 1184 | p.plural_verb("was", errors), p.no(" error", errors) 1185 | ) 1186 | == txt 1187 | ) 1188 | 1189 | assert ( 1190 | p.inflect( 1191 | "There plural_verb('was',%d) no('error',%d)" % (errors, errors) 1192 | ) 1193 | == txt 1194 | ) 1195 | 1196 | for num1, num2, txt in ((1, 2, "I saw 2 saws"), (2, 1, "we saw 1 saw")): 1197 | assert ( 1198 | "{}{}{} {}{}".format( 1199 | p.num(num1, ""), 1200 | p.plural("I"), 1201 | p.plural_verb(" saw"), 1202 | p.num(num2), 1203 | p.plural_noun(" saw"), 1204 | ) 1205 | == txt 1206 | ) 1207 | 1208 | assert ( 1209 | p.inflect( 1210 | "num(%d, False)plural('I') plural_verb('saw') " 1211 | "num(%d) plural_noun('saw')" % (num1, num2) 1212 | ) 1213 | == txt 1214 | ) 1215 | 1216 | assert p.a("a cat") == "a cat" 1217 | 1218 | for word, txt in ( 1219 | ("cat", "a cat"), 1220 | ("aardvark", "an aardvark"), 1221 | ("ewe", "a ewe"), 1222 | ("hour", "an hour"), 1223 | ): 1224 | assert p.a("{} {}".format(p.number_to_words(1, one="a"), word)) == txt 1225 | 1226 | p.num(2) 1227 | 1228 | def test_unknown_method(self): 1229 | p = inflect.engine() 1230 | with pytest.raises(AttributeError): 1231 | p.unknown_method # noqa: B018 1232 | -------------------------------------------------------------------------------- /tests/test_unicode.py: -------------------------------------------------------------------------------- 1 | import inflect 2 | 3 | 4 | class TestUnicode: 5 | """Unicode compatibility test cases""" 6 | 7 | def test_unicode_plural(self): 8 | """Unicode compatibility test cases for plural""" 9 | engine = inflect.engine() 10 | unicode_test_cases = {"cliché": "clichés", "ångström": "ångströms"} 11 | for singular, plural in unicode_test_cases.items(): 12 | assert plural == engine.plural(singular) 13 | -------------------------------------------------------------------------------- /towncrier.toml: -------------------------------------------------------------------------------- 1 | [tool.towncrier] 2 | title_format = "{version}" 3 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [testenv] 2 | description = perform primary checks (tests, style, types, coverage) 3 | deps = 4 | setenv = 5 | PYTHONWARNDEFAULTENCODING = 1 6 | commands = 7 | pytest {posargs} 8 | usedevelop = True 9 | extras = 10 | test 11 | check 12 | cover 13 | enabler 14 | type 15 | 16 | [testenv:diffcov] 17 | description = run tests and check that diff from main is covered 18 | deps = 19 | {[testenv]deps} 20 | diff-cover 21 | commands = 22 | pytest {posargs} --cov-report xml 23 | diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html 24 | diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 25 | 26 | [testenv:docs] 27 | description = build the documentation 28 | extras = 29 | doc 30 | test 31 | changedir = docs 32 | commands = 33 | python -m sphinx -W --keep-going . {toxinidir}/build/html 34 | python -m sphinxlint 35 | 36 | [testenv:finalize] 37 | description = assemble changelog and tag a release 38 | skip_install = True 39 | deps = 40 | towncrier 41 | jaraco.develop >= 7.23 42 | pass_env = * 43 | commands = 44 | python -m jaraco.develop.finalize 45 | 46 | 47 | [testenv:release] 48 | description = publish the package to PyPI and GitHub 49 | skip_install = True 50 | deps = 51 | build 52 | twine>=3 53 | jaraco.develop>=7.1 54 | pass_env = 55 | TWINE_PASSWORD 56 | GITHUB_TOKEN 57 | setenv = 58 | TWINE_USERNAME = {env:TWINE_USERNAME:__token__} 59 | commands = 60 | python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" 61 | python -m build 62 | python -m twine upload dist/* 63 | python -m jaraco.develop.create-github-release 64 | --------------------------------------------------------------------------------