././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1719878424.263477 inflect-7.3.1/0000755000175100001770000000000014640641430012555 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/.coveragerc0000644000175100001770000000031014640641406014673 0ustar00runnerdocker[run] omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* disable_warnings = couldnt-parse [report] show_missing = True exclude_also = # jaraco/skeleton#97 @overload if TYPE_CHECKING: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/.editorconfig0000644000175100001770000000036614640641406015242 0ustar00runnerdockerroot = true [*] charset = utf-8 indent_style = tab indent_size = 4 insert_final_newline = true end_of_line = lf [*.py] indent_style = space max_line_length = 88 [*.{yml,yaml}] indent_style = space indent_size = 2 [*.rst] indent_style = space ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719878424.2554772 inflect-7.3.1/.github/0000755000175100001770000000000014640641430014115 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/.github/FUNDING.yml0000644000175100001770000000002714640641406015734 0ustar00runnerdockertidelift: pypi/inflect ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/.github/dependabot.yml0000644000175100001770000000022414640641406016746 0ustar00runnerdockerversion: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" allow: - dependency-type: "all" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719878424.2554772 inflect-7.3.1/.github/workflows/0000755000175100001770000000000014640641430016152 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/.github/workflows/main.yml0000644000175100001770000000564014640641406017631 0ustar00runnerdockername: tests on: merge_group: push: branches-ignore: # temporary GH branches relating to merge queues (jaraco/skeleton#93) - gh-readonly-queue/** tags: # required if branches-ignore is supplied (jaraco/skeleton#103) - '**' pull_request: permissions: contents: read env: # Environment variable to support color support (jaraco/skeleton#66) FORCE_COLOR: 1 # Suppress noisy pip warnings PIP_DISABLE_PIP_VERSION_CHECK: 'true' PIP_NO_PYTHON_VERSION_WARNING: 'true' PIP_NO_WARN_SCRIPT_LOCATION: 'true' # Ensure tests can sense settings about the environment TOX_OVERRIDE: >- testenv.pass_env+=GITHUB_*,FORCE_COLOR jobs: test: strategy: # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - "3.8" - "3.12" platform: - ubuntu-latest - macos-latest - windows-latest include: - python: "3.9" platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - python: "3.11" platform: ubuntu-latest - python: pypy3.10 platform: ubuntu-latest - python: "3.x" platform: ubuntu-latest runs-on: ${{ matrix.platform }} continue-on-error: ${{ matrix.python == '3.13' }} steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} allow-prereleases: true - name: Install tox run: python -m pip install tox - name: Run run: tox collateral: strategy: fail-fast: false matrix: job: - diffcov - docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v4 with: python-version: 3.x - name: Install tox run: python -m pip install tox - name: Eval ${{ matrix.job }} run: tox -e ${{ matrix.job }} check: # This job does nothing and is only used for the branch protection if: always() needs: - test - collateral runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} release: permissions: contents: write needs: - check if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: python-version: 3.x - name: Install tox run: python -m pip install tox - name: Run run: tox -e release env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/.gitignore0000644000175100001770000000010414640641406014543 0ustar00runnerdocker*.pyc MANIFEST dist/* cover/* htmlcov/* .tox/* *.egg-info .coverage ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/.pre-commit-config.yaml0000644000175100001770000000016414640641406017042 0ustar00runnerdockerrepos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.8 hooks: - id: ruff - id: ruff-format ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/.readthedocs.yaml0000644000175100001770000000051614640641406016011 0ustar00runnerdockerversion: 2 python: install: - path: . extra_requirements: - doc # required boilerplate readthedocs/readthedocs.org#10401 build: os: ubuntu-lts-latest tools: python: latest # post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114 jobs: post_checkout: - git fetch --unshallow || true ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/LICENSE0000644000175100001770000000177714640641406013601 0ustar00runnerdockerPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/NEWS.rst0000644000175100001770000001656514640641406014103 0ustar00runnerdockerv7.3.1 ====== Bugfixes -------- - Set minimum version of more-itertools to 8.5 (#215) v7.3.0 ====== Features -------- - Restricted typing_extensions to Python 3.8. (#211) v7.2.1 ====== Bugfixes -------- - Refactored number_to_words toward reduced complexity. v7.2.0 ====== Features -------- - Replace pydantic with typeguard (#195) v7.1.0 ====== Features -------- - Now handle 'pair of x' in pl_sb_uninflected_complete (#188) v7.0.0 ====== Features -------- - Refine type hint for ``singular_noun`` to indicate a literal return type for ``False``. (#186) Deprecations and Removals ------------------------- - Removed methods renamed in 0.2.0. v6.2.0 ====== Features -------- - Project now supports Pydantic 2 while retaining support for Pydantic 1. (#187) Bugfixes -------- - Added validation of user-defined words and amended the type declarations to match, allowing for null values but not empty strings. (#187) v6.1.1 ====== Bugfixes -------- - ``ordinal`` now handles float types correctly without first coercing them to strings. (#178) v6.1.0 ====== Features -------- - Require Python 3.8 or later. v6.0.5 ====== * #187: Pin to Pydantic 1 to avoid breaking in Pydantic 2. v6.0.4 ====== * Internal cleanup. v6.0.3 ====== * #136: A/an support now more correctly honors leading capitalized words and abbreviations. * #178: Improve support for ordinals for floats. v6.0.2 ====== * #169: Require pydantic 1.9.1 to avoid ``ValueError``. v6.0.1 ====== * Minor tweaks and packaging refresh. v6.0.0 ====== * #157: ``compare`` methods now validate their inputs and will raise a more meaningful exception if an empty string or None is passed. This expectation is now documented. * Many public methods now perform validation on arguments. An empty string is no longer allowed for words or text. Callers are expected to pass non-empty text or trap the validation errors that are raised. The exceptions raised are ``pydantic.error_wrappers.ValidationError``, which are currently a subclass of ``ValueError``, but since that `may change `_, tests check for a generic ``Exception``. v5.6.2 ====== * #15: Fixes to plural edge case handling. v5.6.1 ====== * Packaging refresh and docs update. v5.6.0 ====== * #153: Internal refactor to simplify and unify ``_plnoun`` and ``_sinoun``. v5.5.2 ====== * Fixed badges. v5.5.1 ====== * #150: Rewrite to satisfy type checkers. v5.5.0 ====== * #147: Enhanced type annotations. v5.4.0 ====== * #133: Add a ``py.typed`` file so mypy recognizes type annotations. * Misc fixes in #128, #134, #135, #137, #138, #139, #140, #142, #143, #144. * Require Python 3.7 or later. v5.3.0 ====== * #108: Add support for pluralizing open compound nouns. v5.2.0 ====== * #121: Modernized the codebase. Added a lot of type annotations. v5.1.0 ====== * #113: Add support for uncountable nouns. v5.0.3 ====== * Refreshed package metadata. v5.0.2 ====== * #102: Inflect withdraws from `Jazzband `_ in order to continue to participate in sustained maintenance and enterprise support through `Tidelift `_. The project continues to honor the guidelines and principles behind Jazzband and welcomes contributors openly. v5.0.1 ====== * Identical release validating release process. v5.0.0 ====== * Module no longer exposes a ``__version__`` attribute. Instead to query the version installed, use `importlib.metadata `_ or `its backport `_ to query:: importlib.metadata.version('inflect') v4.1.1 ====== * Refreshed package metadata. v4.1.0 ====== * #95: Certain operations now allow ignore arbitrary leading words. v4.0.0 ====== * Require Python 3.6 or later. v3.0.2 ====== * #88: Distribution no longer includes root ``tests`` package. v3.0.1 ====== * Project now builds on jaraco/skeleton for shared package management. v3.0.0 ====== * #75: Drop support for Python 3.4. v2.1.0 ====== * #29: Relicensed under the more permissive MIT License. v2.0.1 ====== * #57: Fix pluralization of taco. v2.0.0 ====== * #37: fix inconsistencies with the inflect method We now build and parse AST to extract function arguments instead of relying on regular expressions. This also adds support for keyword arguments and built-in constants when calling functions in the string. Unfortunately, this is not backwards compatible in some cases: * Strings should now be wrapped in single or double quotes p.inflect("singular_noun(to them)") should now be p.inflect("singular_noun('to them')") * Empty second argument to a function will now be parsed as None instead of ''. p.inflect("num(%d,) eggs" % 2) now prints "2 eggs" instead of " eggs" Since None, True and False are now supported, they can be passed explicitly: p.inflect("num(%d, False) eggs" % 2) will print " eggs" p.inflect("num(%d, True) eggs" % 2) will print "2 eggs" v1.0.2 ====== * #53: Improved unicode handling. * #5 and #40 via #55: Fix capitalization issues in processes where more than one word is involved. * #56: Handle correctly units containing 'degree' and 'per'. v1.0.1 ====== * #31: fix extraneous close parentheses. v1.0.0 ====== * Dropped support for Python 3.3. v0.3.1 ====== * Fixed badges in readme. v0.3.0 ====== * Moved hosting to the `jazzband project on GitHub `_. v0.2.5 ====== * Fixed TypeError while parsing compounds (by yavarhusain) * Fixed encoding issue in setup.py on Python 3 v0.2.4 ====== * new maintainer (Alex Grönholm) * added Python 3 compatibility (by Thorben Krüger) v0.2.3 ====== * fix a/an for dishonor, Honolulu, mpeg, onetime, Ugandan, Ukrainian, Unabomber, unanimous, US * merge in 'subspecies' fix by UltraNurd * add arboretum to classical plurals * prevent crash with singular_noun('ys') v0.2.2 ====== * change numwords to number_to_words in strings * improve some docstrings * comment out imports for unused .inflectrc * remove unused exception class v0.2.1 ====== * remove incorrect gnome_sudoku import v0.2.0 ====== * add gender() to select the gender of singular pronouns * replace short named methods with longer methods. shorted method now print a message and raise DecrecationWarning pl -> plural plnoun -> plural_noun plverb -> plural_verb pladj -> plural_adjective sinoun -> singular_noun prespart -> present_participle numwords -> number_to_words plequal -> compare plnounequal -> compare_nouns plverbequal -> compare_verbs pladjequal -> compare_adjs wordlist -> join * change classical() to only accept keyword args: only one way to do it * fix bug in numwords where hundreds was giving the wrong number when group=3 v0.1.8 ====== * add line to setup showing that this provides 'inflect' so that inflect_dj can require it * add the rest of the tests from the Perl version v0.1.7 ====== * replace most of the regular expressions in _plnoun and _sinoun. They run several times faster now. v0.1.6 ====== * add method sinoun() to generate the singular of a plural noun. Phew! * add changes from new Perl version: 1.892 * start adding tests from Perl version * add test to check sinoun(plnoun(word)) == word Can now use word lists to check these methods without needing to have a list of plurals. ;-) * fix die -> dice ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1719878424.263477 inflect-7.3.1/PKG-INFO0000644000175100001770000005107714640641430013664 0ustar00runnerdockerMetadata-Version: 2.1 Name: inflect Version: 7.3.1 Summary: Correctly generate plurals, singular nouns, ordinals, indefinite articles Author-email: Paul Dyson Maintainer-email: "Jason R. Coombs" Project-URL: Source, https://github.com/jaraco/inflect Keywords: plural,inflect,participle Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Linguistic Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: more_itertools>=8.5.0 Requires-Dist: typeguard>=4.0.1 Requires-Dist: typing_extensions; python_version < "3.9" Provides-Extra: test Requires-Dist: pytest!=8.1.*,>=6; extra == "test" Requires-Dist: pytest-checkdocs>=2.4; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: pytest-mypy; extra == "test" Requires-Dist: pytest-enabler>=2.2; extra == "test" Requires-Dist: pytest-ruff>=0.2.1; extra == "test" Requires-Dist: pygments; extra == "test" Provides-Extra: doc Requires-Dist: sphinx>=3.5; extra == "doc" Requires-Dist: jaraco.packaging>=9.3; extra == "doc" Requires-Dist: rst.linker>=1.9; extra == "doc" Requires-Dist: furo; extra == "doc" Requires-Dist: sphinx-lint; extra == "doc" Requires-Dist: jaraco.tidelift>=1.4; extra == "doc" .. image:: https://img.shields.io/pypi/v/inflect.svg :target: https://pypi.org/project/inflect .. image:: https://img.shields.io/pypi/pyversions/inflect.svg .. image:: https://github.com/jaraco/inflect/actions/workflows/main.yml/badge.svg :target: https://github.com/jaraco/inflect/actions?query=workflow%3A%22tests%22 :alt: tests .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff .. image:: https://readthedocs.org/projects/inflect/badge/?version=latest :target: https://inflect.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/skeleton-2024-informational :target: https://blog.jaraco.com/skeleton .. image:: https://tidelift.com/badges/package/pypi/inflect :target: https://tidelift.com/subscription/pkg/pypi-inflect?utm_source=pypi-inflect&utm_medium=readme NAME ==== inflect.py - Correctly generate plurals, singular nouns, ordinals, indefinite articles; convert numbers to words. SYNOPSIS ======== .. code-block:: python import inflect p = inflect.engine() # METHODS: # plural plural_noun plural_verb plural_adj singular_noun no num # compare compare_nouns compare_nouns compare_adjs # a an # present_participle # ordinal number_to_words # join # inflect classical gender # defnoun defverb defadj defa defan # UNCONDITIONALLY FORM THE PLURAL print("The plural of ", word, " is ", p.plural(word)) # CONDITIONALLY FORM THE PLURAL print("I saw", cat_count, p.plural("cat", cat_count)) # FORM PLURALS FOR SPECIFIC PARTS OF SPEECH print( p.plural_noun("I", N1), p.plural_verb("saw", N1), p.plural_adj("my", N2), p.plural_noun("saw", N2), ) # FORM THE SINGULAR OF PLURAL NOUNS print("The singular of ", word, " is ", p.singular_noun(word)) # SELECT THE GENDER OF SINGULAR PRONOUNS print(p.singular_noun("they")) # 'it' p.gender("feminine") print(p.singular_noun("they")) # 'she' # DEAL WITH "0/1/N" -> "no/1/N" TRANSLATION: print("There ", p.plural_verb("was", errors), p.no(" error", errors)) # USE DEFAULT COUNTS: print( p.num(N1, ""), p.plural("I"), p.plural_verb(" saw"), p.num(N2), p.plural_noun(" saw"), ) print("There ", p.num(errors, ""), p.plural_verb("was"), p.no(" error")) # COMPARE TWO WORDS "NUMBER-INSENSITIVELY": if p.compare(word1, word2): print("same") if p.compare_nouns(word1, word2): print("same noun") if p.compare_verbs(word1, word2): print("same verb") if p.compare_adjs(word1, word2): print("same adj.") # ADD CORRECT "a" OR "an" FOR A GIVEN WORD: print("Did you want ", p.a(thing), " or ", p.an(idea)) # CONVERT NUMERALS INTO ORDINALS (i.e. 1->1st, 2->2nd, 3->3rd, etc.) print("It was", p.ordinal(position), " from the left\n") # CONVERT NUMERALS TO WORDS (i.e. 1->"one", 101->"one hundred and one", etc.) # RETURNS A SINGLE STRING... words = p.number_to_words(1234) # "one thousand, two hundred and thirty-four" words = p.number_to_words(p.ordinal(1234)) # "one thousand, two hundred and thirty-fourth" # GET BACK A LIST OF STRINGS, ONE FOR EACH "CHUNK"... words = p.number_to_words(1234, wantlist=True) # ("one thousand","two hundred and thirty-four") # OPTIONAL PARAMETERS CHANGE TRANSLATION: words = p.number_to_words(12345, group=1) # "one, two, three, four, five" words = p.number_to_words(12345, group=2) # "twelve, thirty-four, five" words = p.number_to_words(12345, group=3) # "one twenty-three, forty-five" words = p.number_to_words(1234, andword="") # "one thousand, two hundred thirty-four" words = p.number_to_words(1234, andword=", plus") # "one thousand, two hundred, plus thirty-four" # TODO: I get no comma before plus: check perl words = p.number_to_words(555_1202, group=1, zero="oh") # "five, five, five, one, two, oh, two" words = p.number_to_words(555_1202, group=1, one="unity") # "five, five, five, unity, two, oh, two" words = p.number_to_words(123.456, group=1, decimal="mark") # "one two three mark four five six" # TODO: DOCBUG: perl gives commas here as do I # LITERAL STYLE ONLY NAMES NUMBERS LESS THAN A CERTAIN THRESHOLD... words = p.number_to_words(9, threshold=10) # "nine" words = p.number_to_words(10, threshold=10) # "ten" words = p.number_to_words(11, threshold=10) # "11" words = p.number_to_words(1000, threshold=10) # "1,000" # JOIN WORDS INTO A LIST: mylist = p.join(("apple", "banana", "carrot")) # "apple, banana, and carrot" mylist = p.join(("apple", "banana")) # "apple and banana" mylist = p.join(("apple", "banana", "carrot"), final_sep="") # "apple, banana and carrot" # REQUIRE "CLASSICAL" PLURALS (EG: "focus"->"foci", "cherub"->"cherubim") p.classical() # USE ALL CLASSICAL PLURALS p.classical(all=True) # USE ALL CLASSICAL PLURALS p.classical(all=False) # SWITCH OFF CLASSICAL MODE p.classical(zero=True) # "no error" INSTEAD OF "no errors" p.classical(zero=False) # "no errors" INSTEAD OF "no error" p.classical(herd=True) # "2 buffalo" INSTEAD OF "2 buffalos" p.classical(herd=False) # "2 buffalos" INSTEAD OF "2 buffalo" p.classical(persons=True) # "2 chairpersons" INSTEAD OF "2 chairpeople" p.classical(persons=False) # "2 chairpeople" INSTEAD OF "2 chairpersons" p.classical(ancient=True) # "2 formulae" INSTEAD OF "2 formulas" p.classical(ancient=False) # "2 formulas" INSTEAD OF "2 formulae" # INTERPOLATE "plural()", "plural_noun()", "plural_verb()", "plural_adj()", "singular_noun()", # a()", "an()", "num()" AND "ordinal()" WITHIN STRINGS: print(p.inflect("The plural of {0} is plural('{0}')".format(word))) print(p.inflect("The singular of {0} is singular_noun('{0}')".format(word))) print(p.inflect("I saw {0} plural('cat',{0})".format(cat_count))) print( p.inflect( "plural('I',{0}) " "plural_verb('saw',{0}) " "plural('a',{1}) " "plural_noun('saw',{1})".format(N1, N2) ) ) print( p.inflect( "num({0}, False)plural('I') " "plural_verb('saw') " "num({1}, False)plural('a') " "plural_noun('saw')".format(N1, N2) ) ) print(p.inflect("I saw num({0}) plural('cat')\nnum()".format(cat_count))) print(p.inflect("There plural_verb('was',{0}) no('error',{0})".format(errors))) print(p.inflect("There num({0}, False)plural_verb('was') no('error')".format(errors))) print(p.inflect("Did you want a('{0}') or an('{1}')".format(thing, idea))) print(p.inflect("It was ordinal('{0}') from the left".format(position))) # ADD USER-DEFINED INFLECTIONS (OVERRIDING INBUILT RULES): p.defnoun("VAX", "VAXen") # SINGULAR => PLURAL p.defverb( "will", # 1ST PERSON SINGULAR "shall", # 1ST PERSON PLURAL "will", # 2ND PERSON SINGULAR "will", # 2ND PERSON PLURAL "will", # 3RD PERSON SINGULAR "will", # 3RD PERSON PLURAL ) p.defadj("hir", "their") # SINGULAR => PLURAL p.defa("h") # "AY HALWAYS SEZ 'HAITCH'!" p.defan("horrendous.*") # "AN HORRENDOUS AFFECTATION" DESCRIPTION =========== The methods of the class ``engine`` in module ``inflect.py`` provide plural inflections, singular noun inflections, "a"/"an" selection for English words, and manipulation of numbers as words. Plural forms of all nouns, most verbs, and some adjectives are provided. Where appropriate, "classical" variants (for example: "brother" -> "brethren", "dogma" -> "dogmata", etc.) are also provided. Single forms of nouns are also provided. The gender of singular pronouns can be chosen (for example "they" -> "it" or "she" or "he" or "they"). Pronunciation-based "a"/"an" selection is provided for all English words, and most initialisms. It is also possible to inflect numerals (1,2,3) to ordinals (1st, 2nd, 3rd) and to English words ("one", "two", "three"). In generating these inflections, ``inflect.py`` follows the Oxford English Dictionary and the guidelines in Fowler's Modern English Usage, preferring the former where the two disagree. The module is built around standard British spelling, but is designed to cope with common American variants as well. Slang, jargon, and other English dialects are *not* explicitly catered for. Where two or more inflected forms exist for a single word (typically a "classical" form and a "modern" form), ``inflect.py`` prefers the more common form (typically the "modern" one), unless "classical" processing has been specified (see `MODERN VS CLASSICAL INFLECTIONS`). FORMING PLURALS AND SINGULARS ============================= Inflecting Plurals and Singulars -------------------------------- All of the ``plural...`` plural inflection methods take the word to be inflected as their first argument and return the corresponding inflection. Note that all such methods expect the *singular* form of the word. The results of passing a plural form are undefined (and unlikely to be correct). Similarly, the ``si...`` singular inflection method expects the *plural* form of the word. The ``plural...`` methods also take an optional second argument, which indicates the grammatical "number" of the word (or of another word with which the word being inflected must agree). If the "number" argument is supplied and is not ``1`` (or ``"one"`` or ``"a"``, or some other adjective that implies the singular), the plural form of the word is returned. If the "number" argument *does* indicate singularity, the (uninflected) word itself is returned. If the number argument is omitted, the plural form is returned unconditionally. The ``si...`` method takes a second argument in a similar fashion. If it is some form of the number ``1``, or is omitted, the singular form is returned. Otherwise the plural is returned unaltered. The various methods of ``inflect.engine`` are: ``plural_noun(word, count=None)`` The method ``plural_noun()`` takes a *singular* English noun or pronoun and returns its plural. Pronouns in the nominative ("I" -> "we") and accusative ("me" -> "us") cases are handled, as are possessive pronouns ("mine" -> "ours"). ``plural_verb(word, count=None)`` The method ``plural_verb()`` takes the *singular* form of a conjugated verb (that is, one which is already in the correct "person" and "mood") and returns the corresponding plural conjugation. ``plural_adj(word, count=None)`` The method ``plural_adj()`` takes the *singular* form of certain types of adjectives and returns the corresponding plural form. Adjectives that are correctly handled include: "numerical" adjectives ("a" -> "some"), demonstrative adjectives ("this" -> "these", "that" -> "those"), and possessives ("my" -> "our", "cat's" -> "cats'", "child's" -> "childrens'", etc.) ``plural(word, count=None)`` The method ``plural()`` takes a *singular* English noun, pronoun, verb, or adjective and returns its plural form. Where a word has more than one inflection depending on its part of speech (for example, the noun "thought" inflects to "thoughts", the verb "thought" to "thought"), the (singular) noun sense is preferred to the (singular) verb sense. Hence ``plural("knife")`` will return "knives" ("knife" having been treated as a singular noun), whereas ``plural("knifes")`` will return "knife" ("knifes" having been treated as a 3rd person singular verb). The inherent ambiguity of such cases suggests that, where the part of speech is known, ``plural_noun``, ``plural_verb``, and ``plural_adj`` should be used in preference to ``plural``. ``singular_noun(word, count=None)`` The method ``singular_noun()`` takes a *plural* English noun or pronoun and returns its singular. Pronouns in the nominative ("we" -> "I") and accusative ("us" -> "me") cases are handled, as are possessive pronouns ("ours" -> "mine"). When third person singular pronouns are returned they take the neuter gender by default ("they" -> "it"), not ("they"-> "she") nor ("they" -> "he"). This can be changed with ``gender()``. Note that all these methods ignore any whitespace surrounding the word being inflected, but preserve that whitespace when the result is returned. For example, ``plural(" cat ")`` returns " cats ". ``gender(genderletter)`` The third person plural pronoun takes the same form for the female, male and neuter (e.g. "they"). The singular however, depends upon gender (e.g. "she", "he", "it" and "they" -- "they" being the gender neutral form.) By default ``singular_noun`` returns the neuter form, however, the gender can be selected with the ``gender`` method. Pass the first letter of the gender to ``gender`` to return the f(eminine), m(asculine), n(euter) or t(hey) form of the singular. e.g. gender('f') followed by singular_noun('themselves') returns 'herself'. Numbered plurals ---------------- The ``plural...`` methods return only the inflected word, not the count that was used to inflect it. Thus, in order to produce "I saw 3 ducks", it is necessary to use: .. code-block:: python print("I saw", N, p.plural_noun(animal, N)) Since the usual purpose of producing a plural is to make it agree with a preceding count, inflect.py provides a method (``no(word, count)``) which, given a word and a(n optional) count, returns the count followed by the correctly inflected word. Hence the previous example can be rewritten: .. code-block:: python print("I saw ", p.no(animal, N)) In addition, if the count is zero (or some other term which implies zero, such as ``"zero"``, ``"nil"``, etc.) the count is replaced by the word "no". Hence, if ``N`` had the value zero, the previous example would print (the somewhat more elegant):: I saw no animals rather than:: I saw 0 animals Note that the name of the method is a pun: the method returns either a number (a *No.*) or a ``"no"``, in front of the inflected word. Reducing the number of counts required -------------------------------------- In some contexts, the need to supply an explicit count to the various ``plural...`` methods makes for tiresome repetition. For example: .. code-block:: python print( plural_adj("This", errors), plural_noun(" error", errors), plural_verb(" was", errors), " fatal.", ) inflect.py therefore provides a method (``num(count=None, show=None)``) which may be used to set a persistent "default number" value. If such a value is set, it is subsequently used whenever an optional second "number" argument is omitted. The default value thus set can subsequently be removed by calling ``num()`` with no arguments. Hence we could rewrite the previous example: .. code-block:: python p.num(errors) print(p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal.") p.num() Normally, ``num()`` returns its first argument, so that it may also be "inlined" in contexts like: .. code-block:: python print(p.num(errors), p.plural_noun(" error"), p.plural_verb(" was"), " detected.") if severity > 1: print( p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal." ) However, in certain contexts (see `INTERPOLATING INFLECTIONS IN STRINGS`) it is preferable that ``num()`` return an empty string. Hence ``num()`` provides an optional second argument. If that argument is supplied (that is, if it is defined) and evaluates to false, ``num`` returns an empty string instead of its first argument. For example: .. code-block:: python print(p.num(errors, 0), p.no("error"), p.plural_verb(" was"), " detected.") if severity > 1: print( p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal." ) Number-insensitive equality --------------------------- inflect.py also provides a solution to the problem of comparing words of differing plurality through the methods ``compare(word1, word2)``, ``compare_nouns(word1, word2)``, ``compare_verbs(word1, word2)``, and ``compare_adjs(word1, word2)``. Each of these methods takes two strings, and compares them using the corresponding plural-inflection method (``plural()``, ``plural_noun()``, ``plural_verb()``, and ``plural_adj()`` respectively). The comparison returns true if: - the strings are equal, or - one string is equal to a plural form of the other, or - the strings are two different plural forms of the one word. Hence all of the following return true: .. code-block:: python p.compare("index", "index") # RETURNS "eq" p.compare("index", "indexes") # RETURNS "s:p" p.compare("index", "indices") # RETURNS "s:p" p.compare("indexes", "index") # RETURNS "p:s" p.compare("indices", "index") # RETURNS "p:s" p.compare("indices", "indexes") # RETURNS "p:p" p.compare("indexes", "indices") # RETURNS "p:p" p.compare("indices", "indices") # RETURNS "eq" As indicated by the comments in the previous example, the actual value returned by the various ``compare`` methods encodes which of the three equality rules succeeded: "eq" is returned if the strings were identical, "s:p" if the strings were singular and plural respectively, "p:s" for plural and singular, and "p:p" for two distinct plurals. Inequality is indicated by returning an empty string. It should be noted that two distinct singular words which happen to take the same plural form are *not* considered equal, nor are cases where one (singular) word's plural is the other (plural) word's singular. Hence all of the following return false: .. code-block:: python p.compare("base", "basis") # ALTHOUGH BOTH -> "bases" p.compare("syrinx", "syringe") # ALTHOUGH BOTH -> "syringes" p.compare("she", "he") # ALTHOUGH BOTH -> "they" p.compare("opus", "operas") # ALTHOUGH "opus" -> "opera" -> "operas" p.compare("taxi", "taxes") # ALTHOUGH "taxi" -> "taxis" -> "taxes" Note too that, although the comparison is "number-insensitive" it is *not* case-insensitive (that is, ``plural("time","Times")`` returns false. To obtain both number and case insensitivity, use the ``lower()`` method on both strings (that is, ``plural("time".lower(), "Times".lower())`` returns true). Related Functionality ===================== Shout out to these libraries that provide related functionality: * `WordSet `_ parses identifiers like variable names into sets of words suitable for re-assembling in another form. * `word2number `_ converts words to a number. For Enterprise ============== Available as part of the Tidelift Subscription. 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. `Learn more `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/README.rst0000644000175100001770000004571414640641406014262 0ustar00runnerdocker.. image:: https://img.shields.io/pypi/v/inflect.svg :target: https://pypi.org/project/inflect .. image:: https://img.shields.io/pypi/pyversions/inflect.svg .. image:: https://github.com/jaraco/inflect/actions/workflows/main.yml/badge.svg :target: https://github.com/jaraco/inflect/actions?query=workflow%3A%22tests%22 :alt: tests .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff .. image:: https://readthedocs.org/projects/inflect/badge/?version=latest :target: https://inflect.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/skeleton-2024-informational :target: https://blog.jaraco.com/skeleton .. image:: https://tidelift.com/badges/package/pypi/inflect :target: https://tidelift.com/subscription/pkg/pypi-inflect?utm_source=pypi-inflect&utm_medium=readme NAME ==== inflect.py - Correctly generate plurals, singular nouns, ordinals, indefinite articles; convert numbers to words. SYNOPSIS ======== .. code-block:: python import inflect p = inflect.engine() # METHODS: # plural plural_noun plural_verb plural_adj singular_noun no num # compare compare_nouns compare_nouns compare_adjs # a an # present_participle # ordinal number_to_words # join # inflect classical gender # defnoun defverb defadj defa defan # UNCONDITIONALLY FORM THE PLURAL print("The plural of ", word, " is ", p.plural(word)) # CONDITIONALLY FORM THE PLURAL print("I saw", cat_count, p.plural("cat", cat_count)) # FORM PLURALS FOR SPECIFIC PARTS OF SPEECH print( p.plural_noun("I", N1), p.plural_verb("saw", N1), p.plural_adj("my", N2), p.plural_noun("saw", N2), ) # FORM THE SINGULAR OF PLURAL NOUNS print("The singular of ", word, " is ", p.singular_noun(word)) # SELECT THE GENDER OF SINGULAR PRONOUNS print(p.singular_noun("they")) # 'it' p.gender("feminine") print(p.singular_noun("they")) # 'she' # DEAL WITH "0/1/N" -> "no/1/N" TRANSLATION: print("There ", p.plural_verb("was", errors), p.no(" error", errors)) # USE DEFAULT COUNTS: print( p.num(N1, ""), p.plural("I"), p.plural_verb(" saw"), p.num(N2), p.plural_noun(" saw"), ) print("There ", p.num(errors, ""), p.plural_verb("was"), p.no(" error")) # COMPARE TWO WORDS "NUMBER-INSENSITIVELY": if p.compare(word1, word2): print("same") if p.compare_nouns(word1, word2): print("same noun") if p.compare_verbs(word1, word2): print("same verb") if p.compare_adjs(word1, word2): print("same adj.") # ADD CORRECT "a" OR "an" FOR A GIVEN WORD: print("Did you want ", p.a(thing), " or ", p.an(idea)) # CONVERT NUMERALS INTO ORDINALS (i.e. 1->1st, 2->2nd, 3->3rd, etc.) print("It was", p.ordinal(position), " from the left\n") # CONVERT NUMERALS TO WORDS (i.e. 1->"one", 101->"one hundred and one", etc.) # RETURNS A SINGLE STRING... words = p.number_to_words(1234) # "one thousand, two hundred and thirty-four" words = p.number_to_words(p.ordinal(1234)) # "one thousand, two hundred and thirty-fourth" # GET BACK A LIST OF STRINGS, ONE FOR EACH "CHUNK"... words = p.number_to_words(1234, wantlist=True) # ("one thousand","two hundred and thirty-four") # OPTIONAL PARAMETERS CHANGE TRANSLATION: words = p.number_to_words(12345, group=1) # "one, two, three, four, five" words = p.number_to_words(12345, group=2) # "twelve, thirty-four, five" words = p.number_to_words(12345, group=3) # "one twenty-three, forty-five" words = p.number_to_words(1234, andword="") # "one thousand, two hundred thirty-four" words = p.number_to_words(1234, andword=", plus") # "one thousand, two hundred, plus thirty-four" # TODO: I get no comma before plus: check perl words = p.number_to_words(555_1202, group=1, zero="oh") # "five, five, five, one, two, oh, two" words = p.number_to_words(555_1202, group=1, one="unity") # "five, five, five, unity, two, oh, two" words = p.number_to_words(123.456, group=1, decimal="mark") # "one two three mark four five six" # TODO: DOCBUG: perl gives commas here as do I # LITERAL STYLE ONLY NAMES NUMBERS LESS THAN A CERTAIN THRESHOLD... words = p.number_to_words(9, threshold=10) # "nine" words = p.number_to_words(10, threshold=10) # "ten" words = p.number_to_words(11, threshold=10) # "11" words = p.number_to_words(1000, threshold=10) # "1,000" # JOIN WORDS INTO A LIST: mylist = p.join(("apple", "banana", "carrot")) # "apple, banana, and carrot" mylist = p.join(("apple", "banana")) # "apple and banana" mylist = p.join(("apple", "banana", "carrot"), final_sep="") # "apple, banana and carrot" # REQUIRE "CLASSICAL" PLURALS (EG: "focus"->"foci", "cherub"->"cherubim") p.classical() # USE ALL CLASSICAL PLURALS p.classical(all=True) # USE ALL CLASSICAL PLURALS p.classical(all=False) # SWITCH OFF CLASSICAL MODE p.classical(zero=True) # "no error" INSTEAD OF "no errors" p.classical(zero=False) # "no errors" INSTEAD OF "no error" p.classical(herd=True) # "2 buffalo" INSTEAD OF "2 buffalos" p.classical(herd=False) # "2 buffalos" INSTEAD OF "2 buffalo" p.classical(persons=True) # "2 chairpersons" INSTEAD OF "2 chairpeople" p.classical(persons=False) # "2 chairpeople" INSTEAD OF "2 chairpersons" p.classical(ancient=True) # "2 formulae" INSTEAD OF "2 formulas" p.classical(ancient=False) # "2 formulas" INSTEAD OF "2 formulae" # INTERPOLATE "plural()", "plural_noun()", "plural_verb()", "plural_adj()", "singular_noun()", # a()", "an()", "num()" AND "ordinal()" WITHIN STRINGS: print(p.inflect("The plural of {0} is plural('{0}')".format(word))) print(p.inflect("The singular of {0} is singular_noun('{0}')".format(word))) print(p.inflect("I saw {0} plural('cat',{0})".format(cat_count))) print( p.inflect( "plural('I',{0}) " "plural_verb('saw',{0}) " "plural('a',{1}) " "plural_noun('saw',{1})".format(N1, N2) ) ) print( p.inflect( "num({0}, False)plural('I') " "plural_verb('saw') " "num({1}, False)plural('a') " "plural_noun('saw')".format(N1, N2) ) ) print(p.inflect("I saw num({0}) plural('cat')\nnum()".format(cat_count))) print(p.inflect("There plural_verb('was',{0}) no('error',{0})".format(errors))) print(p.inflect("There num({0}, False)plural_verb('was') no('error')".format(errors))) print(p.inflect("Did you want a('{0}') or an('{1}')".format(thing, idea))) print(p.inflect("It was ordinal('{0}') from the left".format(position))) # ADD USER-DEFINED INFLECTIONS (OVERRIDING INBUILT RULES): p.defnoun("VAX", "VAXen") # SINGULAR => PLURAL p.defverb( "will", # 1ST PERSON SINGULAR "shall", # 1ST PERSON PLURAL "will", # 2ND PERSON SINGULAR "will", # 2ND PERSON PLURAL "will", # 3RD PERSON SINGULAR "will", # 3RD PERSON PLURAL ) p.defadj("hir", "their") # SINGULAR => PLURAL p.defa("h") # "AY HALWAYS SEZ 'HAITCH'!" p.defan("horrendous.*") # "AN HORRENDOUS AFFECTATION" DESCRIPTION =========== The methods of the class ``engine`` in module ``inflect.py`` provide plural inflections, singular noun inflections, "a"/"an" selection for English words, and manipulation of numbers as words. Plural forms of all nouns, most verbs, and some adjectives are provided. Where appropriate, "classical" variants (for example: "brother" -> "brethren", "dogma" -> "dogmata", etc.) are also provided. Single forms of nouns are also provided. The gender of singular pronouns can be chosen (for example "they" -> "it" or "she" or "he" or "they"). Pronunciation-based "a"/"an" selection is provided for all English words, and most initialisms. It is also possible to inflect numerals (1,2,3) to ordinals (1st, 2nd, 3rd) and to English words ("one", "two", "three"). In generating these inflections, ``inflect.py`` follows the Oxford English Dictionary and the guidelines in Fowler's Modern English Usage, preferring the former where the two disagree. The module is built around standard British spelling, but is designed to cope with common American variants as well. Slang, jargon, and other English dialects are *not* explicitly catered for. Where two or more inflected forms exist for a single word (typically a "classical" form and a "modern" form), ``inflect.py`` prefers the more common form (typically the "modern" one), unless "classical" processing has been specified (see `MODERN VS CLASSICAL INFLECTIONS`). FORMING PLURALS AND SINGULARS ============================= Inflecting Plurals and Singulars -------------------------------- All of the ``plural...`` plural inflection methods take the word to be inflected as their first argument and return the corresponding inflection. Note that all such methods expect the *singular* form of the word. The results of passing a plural form are undefined (and unlikely to be correct). Similarly, the ``si...`` singular inflection method expects the *plural* form of the word. The ``plural...`` methods also take an optional second argument, which indicates the grammatical "number" of the word (or of another word with which the word being inflected must agree). If the "number" argument is supplied and is not ``1`` (or ``"one"`` or ``"a"``, or some other adjective that implies the singular), the plural form of the word is returned. If the "number" argument *does* indicate singularity, the (uninflected) word itself is returned. If the number argument is omitted, the plural form is returned unconditionally. The ``si...`` method takes a second argument in a similar fashion. If it is some form of the number ``1``, or is omitted, the singular form is returned. Otherwise the plural is returned unaltered. The various methods of ``inflect.engine`` are: ``plural_noun(word, count=None)`` The method ``plural_noun()`` takes a *singular* English noun or pronoun and returns its plural. Pronouns in the nominative ("I" -> "we") and accusative ("me" -> "us") cases are handled, as are possessive pronouns ("mine" -> "ours"). ``plural_verb(word, count=None)`` The method ``plural_verb()`` takes the *singular* form of a conjugated verb (that is, one which is already in the correct "person" and "mood") and returns the corresponding plural conjugation. ``plural_adj(word, count=None)`` The method ``plural_adj()`` takes the *singular* form of certain types of adjectives and returns the corresponding plural form. Adjectives that are correctly handled include: "numerical" adjectives ("a" -> "some"), demonstrative adjectives ("this" -> "these", "that" -> "those"), and possessives ("my" -> "our", "cat's" -> "cats'", "child's" -> "childrens'", etc.) ``plural(word, count=None)`` The method ``plural()`` takes a *singular* English noun, pronoun, verb, or adjective and returns its plural form. Where a word has more than one inflection depending on its part of speech (for example, the noun "thought" inflects to "thoughts", the verb "thought" to "thought"), the (singular) noun sense is preferred to the (singular) verb sense. Hence ``plural("knife")`` will return "knives" ("knife" having been treated as a singular noun), whereas ``plural("knifes")`` will return "knife" ("knifes" having been treated as a 3rd person singular verb). The inherent ambiguity of such cases suggests that, where the part of speech is known, ``plural_noun``, ``plural_verb``, and ``plural_adj`` should be used in preference to ``plural``. ``singular_noun(word, count=None)`` The method ``singular_noun()`` takes a *plural* English noun or pronoun and returns its singular. Pronouns in the nominative ("we" -> "I") and accusative ("us" -> "me") cases are handled, as are possessive pronouns ("ours" -> "mine"). When third person singular pronouns are returned they take the neuter gender by default ("they" -> "it"), not ("they"-> "she") nor ("they" -> "he"). This can be changed with ``gender()``. Note that all these methods ignore any whitespace surrounding the word being inflected, but preserve that whitespace when the result is returned. For example, ``plural(" cat ")`` returns " cats ". ``gender(genderletter)`` The third person plural pronoun takes the same form for the female, male and neuter (e.g. "they"). The singular however, depends upon gender (e.g. "she", "he", "it" and "they" -- "they" being the gender neutral form.) By default ``singular_noun`` returns the neuter form, however, the gender can be selected with the ``gender`` method. Pass the first letter of the gender to ``gender`` to return the f(eminine), m(asculine), n(euter) or t(hey) form of the singular. e.g. gender('f') followed by singular_noun('themselves') returns 'herself'. Numbered plurals ---------------- The ``plural...`` methods return only the inflected word, not the count that was used to inflect it. Thus, in order to produce "I saw 3 ducks", it is necessary to use: .. code-block:: python print("I saw", N, p.plural_noun(animal, N)) Since the usual purpose of producing a plural is to make it agree with a preceding count, inflect.py provides a method (``no(word, count)``) which, given a word and a(n optional) count, returns the count followed by the correctly inflected word. Hence the previous example can be rewritten: .. code-block:: python print("I saw ", p.no(animal, N)) In addition, if the count is zero (or some other term which implies zero, such as ``"zero"``, ``"nil"``, etc.) the count is replaced by the word "no". Hence, if ``N`` had the value zero, the previous example would print (the somewhat more elegant):: I saw no animals rather than:: I saw 0 animals Note that the name of the method is a pun: the method returns either a number (a *No.*) or a ``"no"``, in front of the inflected word. Reducing the number of counts required -------------------------------------- In some contexts, the need to supply an explicit count to the various ``plural...`` methods makes for tiresome repetition. For example: .. code-block:: python print( plural_adj("This", errors), plural_noun(" error", errors), plural_verb(" was", errors), " fatal.", ) inflect.py therefore provides a method (``num(count=None, show=None)``) which may be used to set a persistent "default number" value. If such a value is set, it is subsequently used whenever an optional second "number" argument is omitted. The default value thus set can subsequently be removed by calling ``num()`` with no arguments. Hence we could rewrite the previous example: .. code-block:: python p.num(errors) print(p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal.") p.num() Normally, ``num()`` returns its first argument, so that it may also be "inlined" in contexts like: .. code-block:: python print(p.num(errors), p.plural_noun(" error"), p.plural_verb(" was"), " detected.") if severity > 1: print( p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal." ) However, in certain contexts (see `INTERPOLATING INFLECTIONS IN STRINGS`) it is preferable that ``num()`` return an empty string. Hence ``num()`` provides an optional second argument. If that argument is supplied (that is, if it is defined) and evaluates to false, ``num`` returns an empty string instead of its first argument. For example: .. code-block:: python print(p.num(errors, 0), p.no("error"), p.plural_verb(" was"), " detected.") if severity > 1: print( p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal." ) Number-insensitive equality --------------------------- inflect.py also provides a solution to the problem of comparing words of differing plurality through the methods ``compare(word1, word2)``, ``compare_nouns(word1, word2)``, ``compare_verbs(word1, word2)``, and ``compare_adjs(word1, word2)``. Each of these methods takes two strings, and compares them using the corresponding plural-inflection method (``plural()``, ``plural_noun()``, ``plural_verb()``, and ``plural_adj()`` respectively). The comparison returns true if: - the strings are equal, or - one string is equal to a plural form of the other, or - the strings are two different plural forms of the one word. Hence all of the following return true: .. code-block:: python p.compare("index", "index") # RETURNS "eq" p.compare("index", "indexes") # RETURNS "s:p" p.compare("index", "indices") # RETURNS "s:p" p.compare("indexes", "index") # RETURNS "p:s" p.compare("indices", "index") # RETURNS "p:s" p.compare("indices", "indexes") # RETURNS "p:p" p.compare("indexes", "indices") # RETURNS "p:p" p.compare("indices", "indices") # RETURNS "eq" As indicated by the comments in the previous example, the actual value returned by the various ``compare`` methods encodes which of the three equality rules succeeded: "eq" is returned if the strings were identical, "s:p" if the strings were singular and plural respectively, "p:s" for plural and singular, and "p:p" for two distinct plurals. Inequality is indicated by returning an empty string. It should be noted that two distinct singular words which happen to take the same plural form are *not* considered equal, nor are cases where one (singular) word's plural is the other (plural) word's singular. Hence all of the following return false: .. code-block:: python p.compare("base", "basis") # ALTHOUGH BOTH -> "bases" p.compare("syrinx", "syringe") # ALTHOUGH BOTH -> "syringes" p.compare("she", "he") # ALTHOUGH BOTH -> "they" p.compare("opus", "operas") # ALTHOUGH "opus" -> "opera" -> "operas" p.compare("taxi", "taxes") # ALTHOUGH "taxi" -> "taxis" -> "taxes" Note too that, although the comparison is "number-insensitive" it is *not* case-insensitive (that is, ``plural("time","Times")`` returns false. To obtain both number and case insensitivity, use the ``lower()`` method on both strings (that is, ``plural("time".lower(), "Times".lower())`` returns true). Related Functionality ===================== Shout out to these libraries that provide related functionality: * `WordSet `_ parses identifiers like variable names into sets of words suitable for re-assembling in another form. * `word2number `_ converts words to a number. For Enterprise ============== Available as part of the Tidelift Subscription. 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. `Learn more `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/SECURITY.md0000644000175100001770000000026414640641406014353 0ustar00runnerdocker# Security Contact To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719878424.2554772 inflect-7.3.1/docs/0000755000175100001770000000000014640641430013505 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/docs/conf.py0000644000175100001770000000220514640641406015006 0ustar00runnerdockerextensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', ] master_doc = "index" html_theme = "furo" # Link dates and other references in the changelog extensions += ['rst.linker'] link_files = { '../NEWS.rst': dict( using=dict(GH='https://github.com'), replace=[ dict( pattern=r'(Issue #|\B#)(?P\d+)', url='{package_url}/issues/{issue}', ), dict( pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', ), dict( pattern=r'PEP[- ](?P\d+)', url='https://peps.python.org/pep-{pep_number:0>4}/', ), ], ) } # Be strict about any broken references nitpicky = True # Include Python intersphinx mapping to prevent failures # jaraco/skeleton#51 extensions += ['sphinx.ext.intersphinx'] intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } # Preserve authored syntax for defaults autodoc_preserve_defaults = True extensions += ['jaraco.tidelift'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/docs/history.rst0000644000175100001770000000011614640641406015741 0ustar00runnerdocker:tocdepth: 2 .. _changes: History ******* .. include:: ../NEWS (links).rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/docs/index.rst0000644000175100001770000000055014640641406015351 0ustar00runnerdockerWelcome to |project| documentation! =================================== .. sidebar-links:: :home: :pypi: .. toctree:: :maxdepth: 1 history .. tidelift-referral-banner:: .. automodule:: inflect :members: :undoc-members: :show-inheritance: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719878424.2554772 inflect-7.3.1/inflect/0000755000175100001770000000000014640641430014201 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/inflect/__init__.py0000644000175100001770000031256414640641406016330 0ustar00runnerdocker""" inflect: english language inflection - correctly generate plurals, ordinals, indefinite articles - convert numbers to words Copyright (C) 2010 Paul Dyson Based upon the Perl module `Lingua::EN::Inflect `_. methods: classical inflect plural plural_noun plural_verb plural_adj singular_noun no num a an compare compare_nouns compare_verbs compare_adjs present_participle ordinal number_to_words join defnoun defverb defadj defa defan INFLECTIONS: classical inflect plural plural_noun plural_verb plural_adj singular_noun compare no num a an present_participle PLURALS: classical inflect plural plural_noun plural_verb plural_adj singular_noun no num compare compare_nouns compare_verbs compare_adjs COMPARISONS: classical compare compare_nouns compare_verbs compare_adjs ARTICLES: classical inflect num a an NUMERICAL: ordinal number_to_words USER_DEFINED: defnoun defverb defadj defa defan Exceptions: UnknownClassicalModeError BadNumValueError BadChunkingOptionError NumOutOfRangeError BadUserDefinedPatternError BadRcFileError BadGenderError """ from __future__ import annotations import ast import collections import contextlib import functools import itertools import re from numbers import Number from typing import ( TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Literal, Match, Optional, Sequence, Tuple, Union, cast, ) from more_itertools import windowed_complete from typeguard import typechecked from .compat.py38 import Annotated class UnknownClassicalModeError(Exception): pass class BadNumValueError(Exception): pass class BadChunkingOptionError(Exception): pass class NumOutOfRangeError(Exception): pass class BadUserDefinedPatternError(Exception): pass class BadRcFileError(Exception): pass class BadGenderError(Exception): pass def enclose(s: str) -> str: return f"(?:{s})" def joinstem(cutpoint: Optional[int] = 0, words: Optional[Iterable[str]] = None) -> str: """ Join stem of each word in words into a string for regex. Each word is truncated at cutpoint. Cutpoint is usually negative indicating the number of letters to remove from the end of each word. >>> joinstem(-2, ["ephemeris", "iris", ".*itis"]) '(?:ephemer|ir|.*it)' >>> joinstem(None, ["ephemeris"]) '(?:ephemeris)' >>> joinstem(5, None) '(?:)' """ return enclose("|".join(w[:cutpoint] for w in words or [])) def bysize(words: Iterable[str]) -> Dict[int, set]: """ From a list of words, return a dict of sets sorted by word length. >>> words = ['ant', 'cat', 'dog', 'pig', 'frog', 'goat', 'horse', 'elephant'] >>> ret = bysize(words) >>> sorted(ret[3]) ['ant', 'cat', 'dog', 'pig'] >>> ret[5] {'horse'} """ res: Dict[int, set] = collections.defaultdict(set) for w in words: res[len(w)].add(w) return res def make_pl_si_lists( lst: Iterable[str], plending: str, siendingsize: Optional[int], dojoinstem: bool = True, ): """ given a list of singular words: lst an ending to append to make the plural: plending the number of characters to remove from the singular before appending plending: siendingsize a flag whether to create a joinstem: dojoinstem return: a list of pluralised words: si_list (called si because this is what you need to look for to make the singular) the pluralised words as a dict of sets sorted by word length: si_bysize the singular words as a dict of sets sorted by word length: pl_bysize if dojoinstem is True: a regular expression that matches any of the stems: stem """ if siendingsize is not None: siendingsize = -siendingsize si_list = [w[:siendingsize] + plending for w in lst] pl_bysize = bysize(lst) si_bysize = bysize(si_list) if dojoinstem: stem = joinstem(siendingsize, lst) return si_list, si_bysize, pl_bysize, stem else: return si_list, si_bysize, pl_bysize # 1. PLURALS pl_sb_irregular_s = { "corpus": "corpuses|corpora", "opus": "opuses|opera", "genus": "genera", "mythos": "mythoi", "penis": "penises|penes", "testis": "testes", "atlas": "atlases|atlantes", "yes": "yeses", } pl_sb_irregular = { "child": "children", "chili": "chilis|chilies", "brother": "brothers|brethren", "infinity": "infinities|infinity", "loaf": "loaves", "lore": "lores|lore", "hoof": "hoofs|hooves", "beef": "beefs|beeves", "thief": "thiefs|thieves", "money": "monies", "mongoose": "mongooses", "ox": "oxen", "cow": "cows|kine", "graffito": "graffiti", "octopus": "octopuses|octopodes", "genie": "genies|genii", "ganglion": "ganglions|ganglia", "trilby": "trilbys", "turf": "turfs|turves", "numen": "numina", "atman": "atmas", "occiput": "occiputs|occipita", "sabretooth": "sabretooths", "sabertooth": "sabertooths", "lowlife": "lowlifes", "flatfoot": "flatfoots", "tenderfoot": "tenderfoots", "romany": "romanies", "jerry": "jerries", "mary": "maries", "talouse": "talouses", "rom": "roma", "carmen": "carmina", } pl_sb_irregular.update(pl_sb_irregular_s) # pl_sb_irregular_keys = enclose('|'.join(pl_sb_irregular.keys())) pl_sb_irregular_caps = { "Romany": "Romanies", "Jerry": "Jerrys", "Mary": "Marys", "Rom": "Roma", } pl_sb_irregular_compound = {"prima donna": "prima donnas|prime donne"} si_sb_irregular = {v: k for (k, v) in pl_sb_irregular.items()} for k in list(si_sb_irregular): if "|" in k: k1, k2 = k.split("|") si_sb_irregular[k1] = si_sb_irregular[k2] = si_sb_irregular[k] del si_sb_irregular[k] si_sb_irregular_caps = {v: k for (k, v) in pl_sb_irregular_caps.items()} si_sb_irregular_compound = {v: k for (k, v) in pl_sb_irregular_compound.items()} for k in list(si_sb_irregular_compound): if "|" in k: k1, k2 = k.split("|") si_sb_irregular_compound[k1] = si_sb_irregular_compound[k2] = ( si_sb_irregular_compound[k] ) del si_sb_irregular_compound[k] # si_sb_irregular_keys = enclose('|'.join(si_sb_irregular.keys())) # Z's that don't double pl_sb_z_zes_list = ("quartz", "topaz") pl_sb_z_zes_bysize = bysize(pl_sb_z_zes_list) pl_sb_ze_zes_list = ("snooze",) pl_sb_ze_zes_bysize = bysize(pl_sb_ze_zes_list) # CLASSICAL "..is" -> "..ides" pl_sb_C_is_ides_complete = [ # GENERAL WORDS... "ephemeris", "iris", "clitoris", "chrysalis", "epididymis", ] pl_sb_C_is_ides_endings = [ # INFLAMATIONS... "itis" ] pl_sb_C_is_ides = joinstem( -2, pl_sb_C_is_ides_complete + [f".*{w}" for w in pl_sb_C_is_ides_endings] ) pl_sb_C_is_ides_list = pl_sb_C_is_ides_complete + pl_sb_C_is_ides_endings ( si_sb_C_is_ides_list, si_sb_C_is_ides_bysize, pl_sb_C_is_ides_bysize, ) = make_pl_si_lists(pl_sb_C_is_ides_list, "ides", 2, dojoinstem=False) # CLASSICAL "..a" -> "..ata" pl_sb_C_a_ata_list = ( "anathema", "bema", "carcinoma", "charisma", "diploma", "dogma", "drama", "edema", "enema", "enigma", "lemma", "lymphoma", "magma", "melisma", "miasma", "oedema", "sarcoma", "schema", "soma", "stigma", "stoma", "trauma", "gumma", "pragma", ) ( si_sb_C_a_ata_list, si_sb_C_a_ata_bysize, pl_sb_C_a_ata_bysize, pl_sb_C_a_ata, ) = make_pl_si_lists(pl_sb_C_a_ata_list, "ata", 1) # UNCONDITIONAL "..a" -> "..ae" pl_sb_U_a_ae_list = ( "alumna", "alga", "vertebra", "persona", "vita", ) ( si_sb_U_a_ae_list, si_sb_U_a_ae_bysize, pl_sb_U_a_ae_bysize, pl_sb_U_a_ae, ) = make_pl_si_lists(pl_sb_U_a_ae_list, "e", None) # CLASSICAL "..a" -> "..ae" pl_sb_C_a_ae_list = ( "amoeba", "antenna", "formula", "hyperbola", "medusa", "nebula", "parabola", "abscissa", "hydra", "nova", "lacuna", "aurora", "umbra", "flora", "fauna", ) ( si_sb_C_a_ae_list, si_sb_C_a_ae_bysize, pl_sb_C_a_ae_bysize, pl_sb_C_a_ae, ) = make_pl_si_lists(pl_sb_C_a_ae_list, "e", None) # CLASSICAL "..en" -> "..ina" pl_sb_C_en_ina_list = ("stamen", "foramen", "lumen") ( si_sb_C_en_ina_list, si_sb_C_en_ina_bysize, pl_sb_C_en_ina_bysize, pl_sb_C_en_ina, ) = make_pl_si_lists(pl_sb_C_en_ina_list, "ina", 2) # UNCONDITIONAL "..um" -> "..a" pl_sb_U_um_a_list = ( "bacterium", "agendum", "desideratum", "erratum", "stratum", "datum", "ovum", "extremum", "candelabrum", ) ( si_sb_U_um_a_list, si_sb_U_um_a_bysize, pl_sb_U_um_a_bysize, pl_sb_U_um_a, ) = make_pl_si_lists(pl_sb_U_um_a_list, "a", 2) # CLASSICAL "..um" -> "..a" pl_sb_C_um_a_list = ( "maximum", "minimum", "momentum", "optimum", "quantum", "cranium", "curriculum", "dictum", "phylum", "aquarium", "compendium", "emporium", "encomium", "gymnasium", "honorarium", "interregnum", "lustrum", "memorandum", "millennium", "rostrum", "spectrum", "speculum", "stadium", "trapezium", "ultimatum", "medium", "vacuum", "velum", "consortium", "arboretum", ) ( si_sb_C_um_a_list, si_sb_C_um_a_bysize, pl_sb_C_um_a_bysize, pl_sb_C_um_a, ) = make_pl_si_lists(pl_sb_C_um_a_list, "a", 2) # UNCONDITIONAL "..us" -> "i" pl_sb_U_us_i_list = ( "alumnus", "alveolus", "bacillus", "bronchus", "locus", "nucleus", "stimulus", "meniscus", "sarcophagus", ) ( si_sb_U_us_i_list, si_sb_U_us_i_bysize, pl_sb_U_us_i_bysize, pl_sb_U_us_i, ) = make_pl_si_lists(pl_sb_U_us_i_list, "i", 2) # CLASSICAL "..us" -> "..i" pl_sb_C_us_i_list = ( "focus", "radius", "genius", "incubus", "succubus", "nimbus", "fungus", "nucleolus", "stylus", "torus", "umbilicus", "uterus", "hippopotamus", "cactus", ) ( si_sb_C_us_i_list, si_sb_C_us_i_bysize, pl_sb_C_us_i_bysize, pl_sb_C_us_i, ) = make_pl_si_lists(pl_sb_C_us_i_list, "i", 2) # CLASSICAL "..us" -> "..us" (ASSIMILATED 4TH DECLENSION LATIN NOUNS) pl_sb_C_us_us = ( "status", "apparatus", "prospectus", "sinus", "hiatus", "impetus", "plexus", ) pl_sb_C_us_us_bysize = bysize(pl_sb_C_us_us) # UNCONDITIONAL "..on" -> "a" pl_sb_U_on_a_list = ( "criterion", "perihelion", "aphelion", "phenomenon", "prolegomenon", "noumenon", "organon", "asyndeton", "hyperbaton", ) ( si_sb_U_on_a_list, si_sb_U_on_a_bysize, pl_sb_U_on_a_bysize, pl_sb_U_on_a, ) = make_pl_si_lists(pl_sb_U_on_a_list, "a", 2) # CLASSICAL "..on" -> "..a" pl_sb_C_on_a_list = ("oxymoron",) ( si_sb_C_on_a_list, si_sb_C_on_a_bysize, pl_sb_C_on_a_bysize, pl_sb_C_on_a, ) = make_pl_si_lists(pl_sb_C_on_a_list, "a", 2) # CLASSICAL "..o" -> "..i" (BUT NORMALLY -> "..os") pl_sb_C_o_i = [ "solo", "soprano", "basso", "alto", "contralto", "tempo", "piano", "virtuoso", ] # list not tuple so can concat for pl_sb_U_o_os pl_sb_C_o_i_bysize = bysize(pl_sb_C_o_i) si_sb_C_o_i_bysize = bysize([f"{w[:-1]}i" for w in pl_sb_C_o_i]) pl_sb_C_o_i_stems = joinstem(-1, pl_sb_C_o_i) # ALWAYS "..o" -> "..os" pl_sb_U_o_os_complete = {"ado", "ISO", "NATO", "NCO", "NGO", "oto"} si_sb_U_o_os_complete = {f"{w}s" for w in pl_sb_U_o_os_complete} pl_sb_U_o_os_endings = [ "aficionado", "aggro", "albino", "allegro", "ammo", "Antananarivo", "archipelago", "armadillo", "auto", "avocado", "Bamako", "Barquisimeto", "bimbo", "bingo", "Biro", "bolero", "Bolzano", "bongo", "Boto", "burro", "Cairo", "canto", "cappuccino", "casino", "cello", "Chicago", "Chimango", "cilantro", "cochito", "coco", "Colombo", "Colorado", "commando", "concertino", "contango", "credo", "crescendo", "cyano", "demo", "ditto", "Draco", "dynamo", "embryo", "Esperanto", "espresso", "euro", "falsetto", "Faro", "fiasco", "Filipino", "flamenco", "furioso", "generalissimo", "Gestapo", "ghetto", "gigolo", "gizmo", "Greensboro", "gringo", "Guaiabero", "guano", "gumbo", "gyro", "hairdo", "hippo", "Idaho", "impetigo", "inferno", "info", "intermezzo", "intertrigo", "Iquico", "jumbo", "junto", "Kakapo", "kilo", "Kinkimavo", "Kokako", "Kosovo", "Lesotho", "libero", "libido", "libretto", "lido", "Lilo", "limbo", "limo", "lineno", "lingo", "lino", "livedo", "loco", "logo", "lumbago", "macho", "macro", "mafioso", "magneto", "magnifico", "Majuro", "Malabo", "manifesto", "Maputo", "Maracaibo", "medico", "memo", "metro", "Mexico", "micro", "Milano", "Monaco", "mono", "Montenegro", "Morocco", "Muqdisho", "myo", "neutrino", "Ningbo", "octavo", "oregano", "Orinoco", "Orlando", "Oslo", "panto", "Paramaribo", "Pardusco", "pedalo", "photo", "pimento", "pinto", "pleco", "Pluto", "pogo", "polo", "poncho", "Porto-Novo", "Porto", "pro", "psycho", "pueblo", "quarto", "Quito", "repo", "rhino", "risotto", "rococo", "rondo", "Sacramento", "saddo", "sago", "salvo", "Santiago", "Sapporo", "Sarajevo", "scherzando", "scherzo", "silo", "sirocco", "sombrero", "staccato", "sterno", "stucco", "stylo", "sumo", "Taiko", "techno", "terrazzo", "testudo", "timpano", "tiro", "tobacco", "Togo", "Tokyo", "torero", "Torino", "Toronto", "torso", "tremolo", "typo", "tyro", "ufo", "UNESCO", "vaquero", "vermicello", "verso", "vibrato", "violoncello", "Virgo", "weirdo", "WHO", "WTO", "Yamoussoukro", "yo-yo", "zero", "Zibo", ] + pl_sb_C_o_i pl_sb_U_o_os_bysize = bysize(pl_sb_U_o_os_endings) si_sb_U_o_os_bysize = bysize([f"{w}s" for w in pl_sb_U_o_os_endings]) # UNCONDITIONAL "..ch" -> "..chs" pl_sb_U_ch_chs_list = ("czech", "eunuch", "stomach") ( si_sb_U_ch_chs_list, si_sb_U_ch_chs_bysize, pl_sb_U_ch_chs_bysize, pl_sb_U_ch_chs, ) = make_pl_si_lists(pl_sb_U_ch_chs_list, "s", None) # UNCONDITIONAL "..[ei]x" -> "..ices" pl_sb_U_ex_ices_list = ("codex", "murex", "silex") ( si_sb_U_ex_ices_list, si_sb_U_ex_ices_bysize, pl_sb_U_ex_ices_bysize, pl_sb_U_ex_ices, ) = make_pl_si_lists(pl_sb_U_ex_ices_list, "ices", 2) pl_sb_U_ix_ices_list = ("radix", "helix") ( si_sb_U_ix_ices_list, si_sb_U_ix_ices_bysize, pl_sb_U_ix_ices_bysize, pl_sb_U_ix_ices, ) = make_pl_si_lists(pl_sb_U_ix_ices_list, "ices", 2) # CLASSICAL "..[ei]x" -> "..ices" pl_sb_C_ex_ices_list = ( "vortex", "vertex", "cortex", "latex", "pontifex", "apex", "index", "simplex", ) ( si_sb_C_ex_ices_list, si_sb_C_ex_ices_bysize, pl_sb_C_ex_ices_bysize, pl_sb_C_ex_ices, ) = make_pl_si_lists(pl_sb_C_ex_ices_list, "ices", 2) pl_sb_C_ix_ices_list = ("appendix",) ( si_sb_C_ix_ices_list, si_sb_C_ix_ices_bysize, pl_sb_C_ix_ices_bysize, pl_sb_C_ix_ices, ) = make_pl_si_lists(pl_sb_C_ix_ices_list, "ices", 2) # ARABIC: ".." -> "..i" pl_sb_C_i_list = ("afrit", "afreet", "efreet") (si_sb_C_i_list, si_sb_C_i_bysize, pl_sb_C_i_bysize, pl_sb_C_i) = make_pl_si_lists( pl_sb_C_i_list, "i", None ) # HEBREW: ".." -> "..im" pl_sb_C_im_list = ("goy", "seraph", "cherub") (si_sb_C_im_list, si_sb_C_im_bysize, pl_sb_C_im_bysize, pl_sb_C_im) = make_pl_si_lists( pl_sb_C_im_list, "im", None ) # UNCONDITIONAL "..man" -> "..mans" pl_sb_U_man_mans_list = """ ataman caiman cayman ceriman desman dolman farman harman hetman human leman ottoman shaman talisman """.split() pl_sb_U_man_mans_caps_list = """ Alabaman Bahaman Burman German Hiroshiman Liman Nakayaman Norman Oklahoman Panaman Roman Selman Sonaman Tacoman Yakiman Yokohaman Yuman """.split() ( si_sb_U_man_mans_list, si_sb_U_man_mans_bysize, pl_sb_U_man_mans_bysize, ) = make_pl_si_lists(pl_sb_U_man_mans_list, "s", None, dojoinstem=False) ( si_sb_U_man_mans_caps_list, si_sb_U_man_mans_caps_bysize, pl_sb_U_man_mans_caps_bysize, ) = make_pl_si_lists(pl_sb_U_man_mans_caps_list, "s", None, dojoinstem=False) # UNCONDITIONAL "..louse" -> "..lice" pl_sb_U_louse_lice_list = ("booklouse", "grapelouse", "louse", "woodlouse") ( si_sb_U_louse_lice_list, si_sb_U_louse_lice_bysize, pl_sb_U_louse_lice_bysize, ) = make_pl_si_lists(pl_sb_U_louse_lice_list, "lice", 5, dojoinstem=False) pl_sb_uninflected_s_complete = [ # PAIRS OR GROUPS SUBSUMED TO A SINGULAR... "breeches", "britches", "pajamas", "pyjamas", "clippers", "gallows", "hijinks", "headquarters", "pliers", "scissors", "testes", "herpes", "pincers", "shears", "proceedings", "trousers", # UNASSIMILATED LATIN 4th DECLENSION "cantus", "coitus", "nexus", # RECENT IMPORTS... "contretemps", "corps", "debris", "siemens", # DISEASES "mumps", # MISCELLANEOUS OTHERS... "diabetes", "jackanapes", "series", "species", "subspecies", "rabies", "chassis", "innings", "news", "mews", "haggis", ] pl_sb_uninflected_s_endings = [ # RECENT IMPORTS... "ois", # DISEASES "measles", ] pl_sb_uninflected_s = pl_sb_uninflected_s_complete + [ f".*{w}" for w in pl_sb_uninflected_s_endings ] pl_sb_uninflected_herd = ( # DON'T INFLECT IN CLASSICAL MODE, OTHERWISE NORMAL INFLECTION "wildebeest", "swine", "eland", "bison", "buffalo", "cattle", "elk", "rhinoceros", "zucchini", "caribou", "dace", "grouse", "guinea fowl", "guinea-fowl", "haddock", "hake", "halibut", "herring", "mackerel", "pickerel", "pike", "roe", "seed", "shad", "snipe", "teal", "turbot", "water fowl", "water-fowl", ) pl_sb_uninflected_complete = [ # SOME FISH AND HERD ANIMALS "tuna", "salmon", "mackerel", "trout", "bream", "sea-bass", "sea bass", "carp", "cod", "flounder", "whiting", "moose", # OTHER ODDITIES "graffiti", "djinn", "samuri", "offspring", "pence", "quid", "hertz", ] + pl_sb_uninflected_s_complete # SOME WORDS ENDING IN ...s (OFTEN PAIRS TAKEN AS A WHOLE) pl_sb_uninflected_caps = [ # ALL NATIONALS ENDING IN -ese "Portuguese", "Amoyese", "Borghese", "Congoese", "Faroese", "Foochowese", "Genevese", "Genoese", "Gilbertese", "Hottentotese", "Kiplingese", "Kongoese", "Lucchese", "Maltese", "Nankingese", "Niasese", "Pekingese", "Piedmontese", "Pistoiese", "Sarawakese", "Shavese", "Vermontese", "Wenchowese", "Yengeese", ] pl_sb_uninflected_endings = [ # UNCOUNTABLE NOUNS "butter", "cash", "furniture", "information", # SOME FISH AND HERD ANIMALS "fish", "deer", "sheep", # ALL NATIONALS ENDING IN -ese "nese", "rese", "lese", "mese", # DISEASES "pox", # OTHER ODDITIES "craft", ] + pl_sb_uninflected_s_endings # SOME WORDS ENDING IN ...s (OFTEN PAIRS TAKEN AS A WHOLE) pl_sb_uninflected_bysize = bysize(pl_sb_uninflected_endings) # SINGULAR WORDS ENDING IN ...s (ALL INFLECT WITH ...es) pl_sb_singular_s_complete = [ "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis", "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais", "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris", "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis", "polis", "rhinoceros", "sassafras", "trellis", ] + pl_sb_C_is_ides_complete pl_sb_singular_s_endings = ["ss", "us"] + pl_sb_C_is_ides_endings pl_sb_singular_s_bysize = bysize(pl_sb_singular_s_endings) si_sb_singular_s_complete = [f"{w}es" for w in pl_sb_singular_s_complete] si_sb_singular_s_endings = [f"{w}es" for w in pl_sb_singular_s_endings] si_sb_singular_s_bysize = bysize(si_sb_singular_s_endings) pl_sb_singular_s_es = ["[A-Z].*es"] pl_sb_singular_s = enclose( "|".join( pl_sb_singular_s_complete + [f".*{w}" for w in pl_sb_singular_s_endings] + pl_sb_singular_s_es ) ) # PLURALS ENDING IN uses -> use si_sb_ois_oi_case = ("Bolshois", "Hanois") si_sb_uses_use_case = ("Betelgeuses", "Duses", "Meuses", "Syracuses", "Toulouses") si_sb_uses_use = ( "abuses", "applauses", "blouses", "carouses", "causes", "chartreuses", "clauses", "contuses", "douses", "excuses", "fuses", "grouses", "hypotenuses", "masseuses", "menopauses", "misuses", "muses", "overuses", "pauses", "peruses", "profuses", "recluses", "reuses", "ruses", "souses", "spouses", "suffuses", "transfuses", "uses", ) si_sb_ies_ie_case = ( "Addies", "Aggies", "Allies", "Amies", "Angies", "Annies", "Annmaries", "Archies", "Arties", "Aussies", "Barbies", "Barries", "Basies", "Bennies", "Bernies", "Berties", "Bessies", "Betties", "Billies", "Blondies", "Bobbies", "Bonnies", "Bowies", "Brandies", "Bries", "Brownies", "Callies", "Carnegies", "Carries", "Cassies", "Charlies", "Cheries", "Christies", "Connies", "Curies", "Dannies", "Debbies", "Dixies", "Dollies", "Donnies", "Drambuies", "Eddies", "Effies", "Ellies", "Elsies", "Eries", "Ernies", "Essies", "Eugenies", "Fannies", "Flossies", "Frankies", "Freddies", "Gillespies", "Goldies", "Gracies", "Guthries", "Hallies", "Hatties", "Hetties", "Hollies", "Jackies", "Jamies", "Janies", "Jannies", "Jeanies", "Jeannies", "Jennies", "Jessies", "Jimmies", "Jodies", "Johnies", "Johnnies", "Josies", "Julies", "Kalgoorlies", "Kathies", "Katies", "Kellies", "Kewpies", "Kristies", "Laramies", "Lassies", "Lauries", "Leslies", "Lessies", "Lillies", "Lizzies", "Lonnies", "Lories", "Lorries", "Lotties", "Louies", "Mackenzies", "Maggies", "Maisies", "Mamies", "Marcies", "Margies", "Maries", "Marjories", "Matties", "McKenzies", "Melanies", "Mickies", "Millies", "Minnies", "Mollies", "Mounties", "Nannies", "Natalies", "Nellies", "Netties", "Ollies", "Ozzies", "Pearlies", "Pottawatomies", "Reggies", "Richies", "Rickies", "Robbies", "Ronnies", "Rosalies", "Rosemaries", "Rosies", "Roxies", "Rushdies", "Ruthies", "Sadies", "Sallies", "Sammies", "Scotties", "Selassies", "Sherries", "Sophies", "Stacies", "Stefanies", "Stephanies", "Stevies", "Susies", "Sylvies", "Tammies", "Terries", "Tessies", "Tommies", "Tracies", "Trekkies", "Valaries", "Valeries", "Valkyries", "Vickies", "Virgies", "Willies", "Winnies", "Wylies", "Yorkies", ) si_sb_ies_ie = ( "aeries", "baggies", "belies", "biggies", "birdies", "bogies", "bonnies", "boogies", "bookies", "bourgeoisies", "brownies", "budgies", "caddies", "calories", "camaraderies", "cockamamies", "collies", "cookies", "coolies", "cooties", "coteries", "crappies", "curies", "cutesies", "dogies", "eyries", "floozies", "footsies", "freebies", "genies", "goalies", "groupies", "hies", "jalousies", "junkies", "kiddies", "laddies", "lassies", "lies", "lingeries", "magpies", "menageries", "mommies", "movies", "neckties", "newbies", "nighties", "oldies", "organdies", "overlies", "pies", "pinkies", "pixies", "potpies", "prairies", "quickies", "reveries", "rookies", "rotisseries", "softies", "sorties", "species", "stymies", "sweeties", "ties", "underlies", "unties", "veggies", "vies", "yuppies", "zombies", ) si_sb_oes_oe_case = ( "Chloes", "Crusoes", "Defoes", "Faeroes", "Ivanhoes", "Joes", "McEnroes", "Moes", "Monroes", "Noes", "Poes", "Roscoes", "Tahoes", "Tippecanoes", "Zoes", ) si_sb_oes_oe = ( "aloes", "backhoes", "canoes", "does", "floes", "foes", "hoes", "mistletoes", "oboes", "pekoes", "roes", "sloes", "throes", "tiptoes", "toes", "woes", ) si_sb_z_zes = ("quartzes", "topazes") si_sb_zzes_zz = ("buzzes", "fizzes", "frizzes", "razzes") si_sb_ches_che_case = ( "Andromaches", "Apaches", "Blanches", "Comanches", "Nietzsches", "Porsches", "Roches", ) si_sb_ches_che = ( "aches", "avalanches", "backaches", "bellyaches", "caches", "cloches", "creches", "douches", "earaches", "fiches", "headaches", "heartaches", "microfiches", "niches", "pastiches", "psyches", "quiches", "stomachaches", "toothaches", "tranches", ) si_sb_xes_xe = ("annexes", "axes", "deluxes", "pickaxes") si_sb_sses_sse_case = ("Hesses", "Jesses", "Larousses", "Matisses") si_sb_sses_sse = ( "bouillabaisses", "crevasses", "demitasses", "impasses", "mousses", "posses", ) si_sb_ves_ve_case = ( # *[nwl]ives -> [nwl]live "Clives", "Palmolives", ) si_sb_ves_ve = ( # *[^d]eaves -> eave "interweaves", "weaves", # *[nwl]ives -> [nwl]live "olives", # *[eoa]lves -> [eoa]lve "bivalves", "dissolves", "resolves", "salves", "twelves", "valves", ) plverb_special_s = enclose( "|".join( [pl_sb_singular_s] + pl_sb_uninflected_s + list(pl_sb_irregular_s) + ["(.*[csx])is", "(.*)ceps", "[A-Z].*s"] ) ) _pl_sb_postfix_adj_defn = ( ("general", enclose(r"(?!major|lieutenant|brigadier|adjutant|.*star)\S+")), ("martial", enclose("court")), ("force", enclose("pound")), ) pl_sb_postfix_adj: Iterable[str] = ( enclose(val + f"(?=(?:-|\\s+){key})") for key, val in _pl_sb_postfix_adj_defn ) pl_sb_postfix_adj_stems = f"({'|'.join(pl_sb_postfix_adj)})(.*)" # PLURAL WORDS ENDING IS es GO TO SINGULAR is si_sb_es_is = ( "amanuenses", "amniocenteses", "analyses", "antitheses", "apotheoses", "arterioscleroses", "atheroscleroses", "axes", # 'bases', # bases -> basis "catalyses", "catharses", "chasses", "cirrhoses", "cocces", "crises", "diagnoses", "dialyses", "diereses", "electrolyses", "emphases", "exegeses", "geneses", "halitoses", "hydrolyses", "hypnoses", "hypotheses", "hystereses", "metamorphoses", "metastases", "misdiagnoses", "mitoses", "mononucleoses", "narcoses", "necroses", "nemeses", "neuroses", "oases", "osmoses", "osteoporoses", "paralyses", "parentheses", "parthenogeneses", "periphrases", "photosyntheses", "probosces", "prognoses", "prophylaxes", "prostheses", "preces", "psoriases", "psychoanalyses", "psychokineses", "psychoses", "scleroses", "scolioses", "sepses", "silicoses", "symbioses", "synopses", "syntheses", "taxes", "telekineses", "theses", "thromboses", "tuberculoses", "urinalyses", ) pl_prep_list = """ about above across after among around at athwart before behind below beneath beside besides between betwixt beyond but by during except for from in into near of off on onto out over since till to under until unto upon with""".split() pl_prep_list_da = pl_prep_list + ["de", "du", "da"] pl_prep_bysize = bysize(pl_prep_list_da) pl_prep = enclose("|".join(pl_prep_list_da)) pl_sb_prep_dual_compound = rf"(.*?)((?:-|\s+)(?:{pl_prep})(?:-|\s+))a(?:-|\s+)(.*)" singular_pronoun_genders = { "neuter", "feminine", "masculine", "gender-neutral", "feminine or masculine", "masculine or feminine", } pl_pron_nom = { # NOMINATIVE REFLEXIVE "i": "we", "myself": "ourselves", "you": "you", "yourself": "yourselves", "she": "they", "herself": "themselves", "he": "they", "himself": "themselves", "it": "they", "itself": "themselves", "they": "they", "themself": "themselves", # POSSESSIVE "mine": "ours", "yours": "yours", "hers": "theirs", "his": "theirs", "its": "theirs", "theirs": "theirs", } si_pron: Dict[str, Dict[str, Union[str, Dict[str, str]]]] = { "nom": {v: k for (k, v) in pl_pron_nom.items()} } si_pron["nom"]["we"] = "I" pl_pron_acc = { # ACCUSATIVE REFLEXIVE "me": "us", "myself": "ourselves", "you": "you", "yourself": "yourselves", "her": "them", "herself": "themselves", "him": "them", "himself": "themselves", "it": "them", "itself": "themselves", "them": "them", "themself": "themselves", } pl_pron_acc_keys = enclose("|".join(pl_pron_acc)) pl_pron_acc_keys_bysize = bysize(pl_pron_acc) si_pron["acc"] = {v: k for (k, v) in pl_pron_acc.items()} for _thecase, _plur, _gend, _sing in ( ("nom", "they", "neuter", "it"), ("nom", "they", "feminine", "she"), ("nom", "they", "masculine", "he"), ("nom", "they", "gender-neutral", "they"), ("nom", "they", "feminine or masculine", "she or he"), ("nom", "they", "masculine or feminine", "he or she"), ("nom", "themselves", "neuter", "itself"), ("nom", "themselves", "feminine", "herself"), ("nom", "themselves", "masculine", "himself"), ("nom", "themselves", "gender-neutral", "themself"), ("nom", "themselves", "feminine or masculine", "herself or himself"), ("nom", "themselves", "masculine or feminine", "himself or herself"), ("nom", "theirs", "neuter", "its"), ("nom", "theirs", "feminine", "hers"), ("nom", "theirs", "masculine", "his"), ("nom", "theirs", "gender-neutral", "theirs"), ("nom", "theirs", "feminine or masculine", "hers or his"), ("nom", "theirs", "masculine or feminine", "his or hers"), ("acc", "them", "neuter", "it"), ("acc", "them", "feminine", "her"), ("acc", "them", "masculine", "him"), ("acc", "them", "gender-neutral", "them"), ("acc", "them", "feminine or masculine", "her or him"), ("acc", "them", "masculine or feminine", "him or her"), ("acc", "themselves", "neuter", "itself"), ("acc", "themselves", "feminine", "herself"), ("acc", "themselves", "masculine", "himself"), ("acc", "themselves", "gender-neutral", "themself"), ("acc", "themselves", "feminine or masculine", "herself or himself"), ("acc", "themselves", "masculine or feminine", "himself or herself"), ): try: si_pron[_thecase][_plur][_gend] = _sing # type: ignore except TypeError: si_pron[_thecase][_plur] = {} si_pron[_thecase][_plur][_gend] = _sing # type: ignore si_pron_acc_keys = enclose("|".join(si_pron["acc"])) si_pron_acc_keys_bysize = bysize(si_pron["acc"]) def get_si_pron(thecase, word, gender) -> str: try: sing = si_pron[thecase][word] except KeyError: raise # not a pronoun try: return sing[gender] # has several types due to gender except TypeError: return cast(str, sing) # answer independent of gender # These dictionaries group verbs by first, second and third person # conjugations. plverb_irregular_pres = { "am": "are", "are": "are", "is": "are", "was": "were", "were": "were", "have": "have", "has": "have", "do": "do", "does": "do", } plverb_ambiguous_pres = { "act": "act", "acts": "act", "blame": "blame", "blames": "blame", "can": "can", "must": "must", "fly": "fly", "flies": "fly", "copy": "copy", "copies": "copy", "drink": "drink", "drinks": "drink", "fight": "fight", "fights": "fight", "fire": "fire", "fires": "fire", "like": "like", "likes": "like", "look": "look", "looks": "look", "make": "make", "makes": "make", "reach": "reach", "reaches": "reach", "run": "run", "runs": "run", "sink": "sink", "sinks": "sink", "sleep": "sleep", "sleeps": "sleep", "view": "view", "views": "view", } plverb_ambiguous_pres_keys = re.compile( rf"^({enclose('|'.join(plverb_ambiguous_pres))})((\s.*)?)$", re.IGNORECASE ) plverb_irregular_non_pres = ( "did", "had", "ate", "made", "put", "spent", "fought", "sank", "gave", "sought", "shall", "could", "ought", "should", ) plverb_ambiguous_non_pres = re.compile( r"^((?:thought|saw|bent|will|might|cut))((\s.*)?)$", re.IGNORECASE ) # "..oes" -> "..oe" (the rest are "..oes" -> "o") pl_v_oes_oe = ("canoes", "floes", "oboes", "roes", "throes", "woes") pl_v_oes_oe_endings_size4 = ("hoes", "toes") pl_v_oes_oe_endings_size5 = ("shoes",) pl_count_zero = ("0", "no", "zero", "nil") pl_count_one = ("1", "a", "an", "one", "each", "every", "this", "that") pl_adj_special = {"a": "some", "an": "some", "this": "these", "that": "those"} pl_adj_special_keys = re.compile( rf"^({enclose('|'.join(pl_adj_special))})$", re.IGNORECASE ) pl_adj_poss = { "my": "our", "your": "your", "its": "their", "her": "their", "his": "their", "their": "their", } pl_adj_poss_keys = re.compile(rf"^({enclose('|'.join(pl_adj_poss))})$", re.IGNORECASE) # 2. INDEFINITE ARTICLES # THIS PATTERN MATCHES STRINGS OF CAPITALS STARTING WITH A "VOWEL-SOUND" # CONSONANT FOLLOWED BY ANOTHER CONSONANT, AND WHICH ARE NOT LIKELY # TO BE REAL WORDS (OH, ALL RIGHT THEN, IT'S JUST MAGIC!) A_abbrev = re.compile( r""" ^(?! FJO | [HLMNS]Y. | RY[EO] | SQU | ( F[LR]? | [HL] | MN? | N | RH? | S[CHKLMNPTVW]? | X(YL)?) [AEIOU]) [FHLMNRSX][A-Z] """, re.VERBOSE, ) # THIS PATTERN CODES THE BEGINNINGS OF ALL ENGLISH WORDS BEGINING WITH A # 'y' FOLLOWED BY A CONSONANT. ANY OTHER Y-CONSONANT PREFIX THEREFORE # IMPLIES AN ABBREVIATION. A_y_cons = re.compile(r"^(y(b[lor]|cl[ea]|fere|gg|p[ios]|rou|tt))", re.IGNORECASE) # EXCEPTIONS TO EXCEPTIONS A_explicit_a = re.compile(r"^((?:unabomber|unanimous|US))", re.IGNORECASE) A_explicit_an = re.compile( r"^((?:euler|hour(?!i)|heir|honest|hono[ur]|mpeg))", re.IGNORECASE ) A_ordinal_an = re.compile(r"^([aefhilmnorsx]-?th)", re.IGNORECASE) A_ordinal_a = re.compile(r"^([bcdgjkpqtuvwyz]-?th)", re.IGNORECASE) # NUMERICAL INFLECTIONS nth = { 0: "th", 1: "st", 2: "nd", 3: "rd", 4: "th", 5: "th", 6: "th", 7: "th", 8: "th", 9: "th", 11: "th", 12: "th", 13: "th", } nth_suff = set(nth.values()) ordinal = dict( ty="tieth", one="first", two="second", three="third", five="fifth", eight="eighth", nine="ninth", twelve="twelfth", ) ordinal_suff = re.compile(rf"({'|'.join(ordinal)})\Z") # NUMBERS unit = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"] teen = [ "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", ] ten = [ "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety", ] mill = [ " ", " thousand", " million", " billion", " trillion", " quadrillion", " quintillion", " sextillion", " septillion", " octillion", " nonillion", " decillion", ] # SUPPORT CLASSICAL PLURALIZATIONS def_classical = dict( all=False, zero=False, herd=False, names=True, persons=False, ancient=False ) all_classical = {k: True for k in def_classical} no_classical = {k: False for k in def_classical} # Maps strings to built-in constant types string_to_constant = {"True": True, "False": False, "None": None} # Pre-compiled regular expression objects DOLLAR_DIGITS = re.compile(r"\$(\d+)") FUNCTION_CALL = re.compile(r"((\w+)\([^)]*\)*)", re.IGNORECASE) PARTITION_WORD = re.compile(r"\A(\s*)(.+?)(\s*)\Z") PL_SB_POSTFIX_ADJ_STEMS_RE = re.compile( rf"^(?:{pl_sb_postfix_adj_stems})$", re.IGNORECASE ) PL_SB_PREP_DUAL_COMPOUND_RE = re.compile( rf"^(?:{pl_sb_prep_dual_compound})$", re.IGNORECASE ) DENOMINATOR = re.compile(r"(?P.+)( (per|a) .+)") PLVERB_SPECIAL_S_RE = re.compile(rf"^({plverb_special_s})$") WHITESPACE = re.compile(r"\s") ENDS_WITH_S = re.compile(r"^(.*[^s])s$", re.IGNORECASE) ENDS_WITH_APOSTROPHE_S = re.compile(r"^(.*)'s?$") INDEFINITE_ARTICLE_TEST = re.compile(r"\A(\s*)(?:an?\s+)?(.+?)(\s*)\Z", re.IGNORECASE) SPECIAL_AN = re.compile(r"^[aefhilmnorsx]$", re.IGNORECASE) SPECIAL_A = re.compile(r"^[bcdgjkpqtuvwyz]$", re.IGNORECASE) SPECIAL_ABBREV_AN = re.compile(r"^[aefhilmnorsx][.-]", re.IGNORECASE) SPECIAL_ABBREV_A = re.compile(r"^[a-z][.-]", re.IGNORECASE) CONSONANTS = re.compile(r"^[^aeiouy]", re.IGNORECASE) ARTICLE_SPECIAL_EU = re.compile(r"^e[uw]", re.IGNORECASE) ARTICLE_SPECIAL_ONCE = re.compile(r"^onc?e\b", re.IGNORECASE) ARTICLE_SPECIAL_ONETIME = re.compile(r"^onetime\b", re.IGNORECASE) ARTICLE_SPECIAL_UNIT = re.compile(r"^uni([^nmd]|mo)", re.IGNORECASE) ARTICLE_SPECIAL_UBA = re.compile(r"^u[bcfghjkqrst][aeiou]", re.IGNORECASE) ARTICLE_SPECIAL_UKR = re.compile(r"^ukr", re.IGNORECASE) SPECIAL_CAPITALS = re.compile(r"^U[NK][AIEO]?") VOWELS = re.compile(r"^[aeiou]", re.IGNORECASE) DIGIT_GROUP = re.compile(r"(\d)") TWO_DIGITS = re.compile(r"(\d)(\d)") THREE_DIGITS = re.compile(r"(\d)(\d)(\d)") THREE_DIGITS_WORD = re.compile(r"(\d)(\d)(\d)(?=\D*\Z)") TWO_DIGITS_WORD = re.compile(r"(\d)(\d)(?=\D*\Z)") ONE_DIGIT_WORD = re.compile(r"(\d)(?=\D*\Z)") FOUR_DIGIT_COMMA = re.compile(r"(\d)(\d{3}(?:,|\Z))") NON_DIGIT = re.compile(r"\D") WHITESPACES_COMMA = re.compile(r"\s+,") COMMA_WORD = re.compile(r", (\S+)\s+\Z") WHITESPACES = re.compile(r"\s+") PRESENT_PARTICIPLE_REPLACEMENTS = ( (re.compile(r"ie$"), r"y"), ( re.compile(r"ue$"), r"u", ), # TODO: isn't ue$ -> u encompassed in the following rule? (re.compile(r"([auy])e$"), r"\g<1>"), (re.compile(r"ski$"), r"ski"), (re.compile(r"[^b]i$"), r""), (re.compile(r"^(are|were)$"), r"be"), (re.compile(r"^(had)$"), r"hav"), (re.compile(r"^(hoe)$"), r"\g<1>"), (re.compile(r"([^e])e$"), r"\g<1>"), (re.compile(r"er$"), r"er"), (re.compile(r"([^aeiou][aeiouy]([bdgmnprst]))$"), r"\g<1>\g<2>"), ) DIGIT = re.compile(r"\d") class Words(str): lowered: str split_: List[str] first: str last: str def __init__(self, orig) -> None: self.lowered = self.lower() self.split_ = self.split() self.first = self.split_[0] self.last = self.split_[-1] Falsish = Any # ideally, falsish would only validate on bool(value) is False _STATIC_TYPE_CHECKING = TYPE_CHECKING # ^-- Workaround for typeguard AST manipulation: # https://github.com/agronholm/typeguard/issues/353#issuecomment-1556306554 if _STATIC_TYPE_CHECKING: # pragma: no cover Word = Annotated[str, "String with at least 1 character"] else: class _WordMeta(type): # Too dynamic to be supported by mypy... def __instancecheck__(self, instance: Any) -> bool: return isinstance(instance, str) and len(instance) >= 1 class Word(metaclass=_WordMeta): # type: ignore[no-redef] """String with at least 1 character""" class engine: def __init__(self) -> None: self.classical_dict = def_classical.copy() self.persistent_count: Optional[int] = None self.mill_count = 0 self.pl_sb_user_defined: List[Optional[Word]] = [] self.pl_v_user_defined: List[Optional[Word]] = [] self.pl_adj_user_defined: List[Optional[Word]] = [] self.si_sb_user_defined: List[Optional[Word]] = [] self.A_a_user_defined: List[Optional[Word]] = [] self.thegender = "neuter" self.__number_args: Optional[Dict[str, str]] = None @property def _number_args(self): return cast(Dict[str, str], self.__number_args) @_number_args.setter def _number_args(self, val): self.__number_args = val @typechecked def defnoun(self, singular: Optional[Word], plural: Optional[Word]) -> int: """ Set the noun plural of singular to plural. """ self.checkpat(singular) self.checkpatplural(plural) self.pl_sb_user_defined.extend((singular, plural)) self.si_sb_user_defined.extend((plural, singular)) return 1 @typechecked def defverb( self, s1: Optional[Word], p1: Optional[Word], s2: Optional[Word], p2: Optional[Word], s3: Optional[Word], p3: Optional[Word], ) -> int: """ Set the verb plurals for s1, s2 and s3 to p1, p2 and p3 respectively. Where 1, 2 and 3 represent the 1st, 2nd and 3rd person forms of the verb. """ self.checkpat(s1) self.checkpat(s2) self.checkpat(s3) self.checkpatplural(p1) self.checkpatplural(p2) self.checkpatplural(p3) self.pl_v_user_defined.extend((s1, p1, s2, p2, s3, p3)) return 1 @typechecked def defadj(self, singular: Optional[Word], plural: Optional[Word]) -> int: """ Set the adjective plural of singular to plural. """ self.checkpat(singular) self.checkpatplural(plural) self.pl_adj_user_defined.extend((singular, plural)) return 1 @typechecked def defa(self, pattern: Optional[Word]) -> int: """ Define the indefinite article as 'a' for words matching pattern. """ self.checkpat(pattern) self.A_a_user_defined.extend((pattern, "a")) return 1 @typechecked def defan(self, pattern: Optional[Word]) -> int: """ Define the indefinite article as 'an' for words matching pattern. """ self.checkpat(pattern) self.A_a_user_defined.extend((pattern, "an")) return 1 def checkpat(self, pattern: Optional[Word]) -> None: """ check for errors in a regex pattern """ if pattern is None: return try: re.match(pattern, "") except re.error as err: raise BadUserDefinedPatternError(pattern) from err def checkpatplural(self, pattern: Optional[Word]) -> None: """ check for errors in a regex replace pattern """ return @typechecked def ud_match(self, word: Word, wordlist: Sequence[Optional[Word]]) -> Optional[str]: for i in range(len(wordlist) - 2, -2, -2): # backwards through even elements mo = re.search(rf"^{wordlist[i]}$", word, re.IGNORECASE) if mo: if wordlist[i + 1] is None: return None pl = DOLLAR_DIGITS.sub( r"\\1", cast(Word, wordlist[i + 1]) ) # change $n to \n for expand return mo.expand(pl) return None def classical(self, **kwargs) -> None: """ turn classical mode on and off for various categories turn on all classical modes: classical() classical(all=True) turn on or off specific claassical modes: e.g. classical(herd=True) classical(names=False) By default all classical modes are off except names. unknown value in args or key in kwargs raises exception: UnknownClasicalModeError """ if not kwargs: self.classical_dict = all_classical.copy() return if "all" in kwargs: if kwargs["all"]: self.classical_dict = all_classical.copy() else: self.classical_dict = no_classical.copy() for k, v in kwargs.items(): if k in def_classical: self.classical_dict[k] = v else: raise UnknownClassicalModeError def num( self, count: Optional[int] = None, show: Optional[int] = None ) -> str: # (;$count,$show) """ Set the number to be used in other method calls. Returns count. Set show to False to return '' instead. """ if count is not None: try: self.persistent_count = int(count) except ValueError as err: raise BadNumValueError from err if (show is None) or show: return str(count) else: self.persistent_count = None return "" def gender(self, gender: str) -> None: """ set the gender for the singular of plural pronouns can be one of: 'neuter' ('they' -> 'it') 'feminine' ('they' -> 'she') 'masculine' ('they' -> 'he') 'gender-neutral' ('they' -> 'they') 'feminine or masculine' ('they' -> 'she or he') 'masculine or feminine' ('they' -> 'he or she') """ if gender in singular_pronoun_genders: self.thegender = gender else: raise BadGenderError def _get_value_from_ast(self, obj): """ Return the value of the ast object. """ if isinstance(obj, ast.Num): return obj.n elif isinstance(obj, ast.Str): return obj.s elif isinstance(obj, ast.List): return [self._get_value_from_ast(e) for e in obj.elts] elif isinstance(obj, ast.Tuple): return tuple([self._get_value_from_ast(e) for e in obj.elts]) # None, True and False are NameConstants in Py3.4 and above. elif isinstance(obj, ast.NameConstant): return obj.value # Probably passed a variable name. # Or passed a single word without wrapping it in quotes as an argument # ex: p.inflect("I plural(see)") instead of p.inflect("I plural('see')") raise NameError(f"name '{obj.id}' is not defined") def _string_to_substitute( self, mo: Match, methods_dict: Dict[str, Callable] ) -> str: """ Return the string to be substituted for the match. """ matched_text, f_name = mo.groups() # matched_text is the complete match string. e.g. plural_noun(cat) # f_name is the function name. e.g. plural_noun # Return matched_text if function name is not in methods_dict if f_name not in methods_dict: return matched_text # Parse the matched text a_tree = ast.parse(matched_text) # get the args and kwargs from ast objects args_list = [ self._get_value_from_ast(a) for a in a_tree.body[0].value.args # type: ignore[attr-defined] ] kwargs_list = { kw.arg: self._get_value_from_ast(kw.value) for kw in a_tree.body[0].value.keywords # type: ignore[attr-defined] } # Call the corresponding function return methods_dict[f_name](*args_list, **kwargs_list) # 0. PERFORM GENERAL INFLECTIONS IN A STRING @typechecked def inflect(self, text: Word) -> str: """ Perform inflections in a string. e.g. inflect('The plural of cat is plural(cat)') returns 'The plural of cat is cats' can use plural, plural_noun, plural_verb, plural_adj, singular_noun, a, an, no, ordinal, number_to_words, and prespart """ save_persistent_count = self.persistent_count # Dictionary of allowed methods methods_dict: Dict[str, Callable] = { "plural": self.plural, "plural_adj": self.plural_adj, "plural_noun": self.plural_noun, "plural_verb": self.plural_verb, "singular_noun": self.singular_noun, "a": self.a, "an": self.a, "no": self.no, "ordinal": self.ordinal, "number_to_words": self.number_to_words, "present_participle": self.present_participle, "num": self.num, } # Regular expression to find Python's function call syntax output = FUNCTION_CALL.sub( lambda mo: self._string_to_substitute(mo, methods_dict), text ) self.persistent_count = save_persistent_count return output # ## PLURAL SUBROUTINES def postprocess(self, orig: str, inflected) -> str: inflected = str(inflected) if "|" in inflected: word_options = inflected.split("|") # When two parts of a noun need to be pluralized if len(word_options[0].split(" ")) == len(word_options[1].split(" ")): result = inflected.split("|")[self.classical_dict["all"]].split(" ") # When only the last part of the noun needs to be pluralized else: result = inflected.split(" ") for index, word in enumerate(result): if "|" in word: result[index] = word.split("|")[self.classical_dict["all"]] else: result = inflected.split(" ") # Try to fix word wise capitalization for index, word in enumerate(orig.split(" ")): if word == "I": # Is this the only word for exceptions like this # Where the original is fully capitalized # without 'meaning' capitalization? # Also this fails to handle a capitalizaion in context continue if word.capitalize() == word: result[index] = result[index].capitalize() if word == word.upper(): result[index] = result[index].upper() return " ".join(result) def partition_word(self, text: str) -> Tuple[str, str, str]: mo = PARTITION_WORD.search(text) if mo: return mo.group(1), mo.group(2), mo.group(3) else: return "", "", "" @typechecked def plural(self, text: Word, count: Optional[Union[str, int, Any]] = None) -> str: """ Return the plural of text. If count supplied, then return text if count is one of: 1, a, an, one, each, every, this, that otherwise return the plural. Whitespace at the start and end is preserved. """ pre, word, post = self.partition_word(text) if not word: return text plural = self.postprocess( word, self._pl_special_adjective(word, count) or self._pl_special_verb(word, count) or self._plnoun(word, count), ) return f"{pre}{plural}{post}" @typechecked def plural_noun( self, text: Word, count: Optional[Union[str, int, Any]] = None ) -> str: """ Return the plural of text, where text is a noun. If count supplied, then return text if count is one of: 1, a, an, one, each, every, this, that otherwise return the plural. Whitespace at the start and end is preserved. """ pre, word, post = self.partition_word(text) if not word: return text plural = self.postprocess(word, self._plnoun(word, count)) return f"{pre}{plural}{post}" @typechecked def plural_verb( self, text: Word, count: Optional[Union[str, int, Any]] = None ) -> str: """ Return the plural of text, where text is a verb. If count supplied, then return text if count is one of: 1, a, an, one, each, every, this, that otherwise return the plural. Whitespace at the start and end is preserved. """ pre, word, post = self.partition_word(text) if not word: return text plural = self.postprocess( word, self._pl_special_verb(word, count) or self._pl_general_verb(word, count), ) return f"{pre}{plural}{post}" @typechecked def plural_adj( self, text: Word, count: Optional[Union[str, int, Any]] = None ) -> str: """ Return the plural of text, where text is an adjective. If count supplied, then return text if count is one of: 1, a, an, one, each, every, this, that otherwise return the plural. Whitespace at the start and end is preserved. """ pre, word, post = self.partition_word(text) if not word: return text plural = self.postprocess(word, self._pl_special_adjective(word, count) or word) return f"{pre}{plural}{post}" @typechecked def compare(self, word1: Word, word2: Word) -> Union[str, bool]: """ compare word1 and word2 for equality regardless of plurality return values: eq - the strings are equal p:s - word1 is the plural of word2 s:p - word2 is the plural of word1 p:p - word1 and word2 are two different plural forms of the one word False - otherwise >>> compare = engine().compare >>> compare("egg", "eggs") 's:p' >>> compare('egg', 'egg') 'eq' Words should not be empty. >>> compare('egg', '') Traceback (most recent call last): ... typeguard.TypeCheckError:...is not an instance of inflect.Word """ norms = self.plural_noun, self.plural_verb, self.plural_adj results = (self._plequal(word1, word2, norm) for norm in norms) return next(filter(None, results), False) @typechecked def compare_nouns(self, word1: Word, word2: Word) -> Union[str, bool]: """ compare word1 and word2 for equality regardless of plurality word1 and word2 are to be treated as nouns return values: eq - the strings are equal p:s - word1 is the plural of word2 s:p - word2 is the plural of word1 p:p - word1 and word2 are two different plural forms of the one word False - otherwise """ return self._plequal(word1, word2, self.plural_noun) @typechecked def compare_verbs(self, word1: Word, word2: Word) -> Union[str, bool]: """ compare word1 and word2 for equality regardless of plurality word1 and word2 are to be treated as verbs return values: eq - the strings are equal p:s - word1 is the plural of word2 s:p - word2 is the plural of word1 p:p - word1 and word2 are two different plural forms of the one word False - otherwise """ return self._plequal(word1, word2, self.plural_verb) @typechecked def compare_adjs(self, word1: Word, word2: Word) -> Union[str, bool]: """ compare word1 and word2 for equality regardless of plurality word1 and word2 are to be treated as adjectives return values: eq - the strings are equal p:s - word1 is the plural of word2 s:p - word2 is the plural of word1 p:p - word1 and word2 are two different plural forms of the one word False - otherwise """ return self._plequal(word1, word2, self.plural_adj) @typechecked def singular_noun( self, text: Word, count: Optional[Union[int, str, Any]] = None, gender: Optional[str] = None, ) -> Union[str, Literal[False]]: """ Return the singular of text, where text is a plural noun. If count supplied, then return the singular if count is one of: 1, a, an, one, each, every, this, that or if count is None otherwise return text unchanged. Whitespace at the start and end is preserved. >>> p = engine() >>> p.singular_noun('horses') 'horse' >>> p.singular_noun('knights') 'knight' Returns False when a singular noun is passed. >>> p.singular_noun('horse') False >>> p.singular_noun('knight') False >>> p.singular_noun('soldier') False """ pre, word, post = self.partition_word(text) if not word: return text sing = self._sinoun(word, count=count, gender=gender) if sing is not False: plural = self.postprocess(word, sing) return f"{pre}{plural}{post}" return False def _plequal(self, word1: str, word2: str, pl) -> Union[str, bool]: # noqa: C901 classval = self.classical_dict.copy() self.classical_dict = all_classical.copy() if word1 == word2: return "eq" if word1 == pl(word2): return "p:s" if pl(word1) == word2: return "s:p" self.classical_dict = no_classical.copy() if word1 == pl(word2): return "p:s" if pl(word1) == word2: return "s:p" self.classical_dict = classval.copy() if pl == self.plural or pl == self.plural_noun: if self._pl_check_plurals_N(word1, word2): return "p:p" if self._pl_check_plurals_N(word2, word1): return "p:p" if pl == self.plural or pl == self.plural_adj: if self._pl_check_plurals_adj(word1, word2): return "p:p" return False def _pl_reg_plurals(self, pair: str, stems: str, end1: str, end2: str) -> bool: pattern = rf"({stems})({end1}\|\1{end2}|{end2}\|\1{end1})" return bool(re.search(pattern, pair)) def _pl_check_plurals_N(self, word1: str, word2: str) -> bool: stem_endings = ( (pl_sb_C_a_ata, "as", "ata"), (pl_sb_C_is_ides, "is", "ides"), (pl_sb_C_a_ae, "s", "e"), (pl_sb_C_en_ina, "ens", "ina"), (pl_sb_C_um_a, "ums", "a"), (pl_sb_C_us_i, "uses", "i"), (pl_sb_C_on_a, "ons", "a"), (pl_sb_C_o_i_stems, "os", "i"), (pl_sb_C_ex_ices, "exes", "ices"), (pl_sb_C_ix_ices, "ixes", "ices"), (pl_sb_C_i, "s", "i"), (pl_sb_C_im, "s", "im"), (".*eau", "s", "x"), (".*ieu", "s", "x"), (".*tri", "xes", "ces"), (".{2,}[yia]n", "xes", "ges"), ) words = map(Words, (word1, word2)) pair = "|".join(word.last for word in words) return ( pair in pl_sb_irregular_s.values() or pair in pl_sb_irregular.values() or pair in pl_sb_irregular_caps.values() or any( self._pl_reg_plurals(pair, stems, end1, end2) for stems, end1, end2 in stem_endings ) ) def _pl_check_plurals_adj(self, word1: str, word2: str) -> bool: word1a = word1[: word1.rfind("'")] if word1.endswith(("'s", "'")) else "" word2a = word2[: word2.rfind("'")] if word2.endswith(("'s", "'")) else "" return ( bool(word1a) and bool(word2a) and ( self._pl_check_plurals_N(word1a, word2a) or self._pl_check_plurals_N(word2a, word1a) ) ) def get_count(self, count: Optional[Union[str, int]] = None) -> Union[str, int]: if count is None and self.persistent_count is not None: count = self.persistent_count if count is not None: count = ( 1 if ( (str(count) in pl_count_one) or ( self.classical_dict["zero"] and str(count).lower() in pl_count_zero ) ) else 2 ) else: count = "" return count # @profile def _plnoun( # noqa: C901 self, word: str, count: Optional[Union[str, int]] = None ) -> str: count = self.get_count(count) # DEFAULT TO PLURAL if count == 1: return word # HANDLE USER-DEFINED NOUNS value = self.ud_match(word, self.pl_sb_user_defined) if value is not None: return value # HANDLE EMPTY WORD, SINGULAR COUNT AND UNINFLECTED PLURALS if word == "": return word word = Words(word) if word.last.lower() in pl_sb_uninflected_complete: if len(word.split_) >= 3: return self._handle_long_compounds(word, count=2) or word return word if word in pl_sb_uninflected_caps: return word for k, v in pl_sb_uninflected_bysize.items(): if word.lowered[-k:] in v: return word if self.classical_dict["herd"] and word.last.lower() in pl_sb_uninflected_herd: return word # HANDLE COMPOUNDS ("Governor General", "mother-in-law", "aide-de-camp", ETC.) mo = PL_SB_POSTFIX_ADJ_STEMS_RE.search(word) if mo and mo.group(2) != "": return f"{self._plnoun(mo.group(1), 2)}{mo.group(2)}" if " a " in word.lowered or "-a-" in word.lowered: mo = PL_SB_PREP_DUAL_COMPOUND_RE.search(word) if mo and mo.group(2) != "" and mo.group(3) != "": return ( f"{self._plnoun(mo.group(1), 2)}" f"{mo.group(2)}" f"{self._plnoun(mo.group(3))}" ) if len(word.split_) >= 3: handled_words = self._handle_long_compounds(word, count=2) if handled_words is not None: return handled_words # only pluralize denominators in units mo = DENOMINATOR.search(word.lowered) if mo: index = len(mo.group("denominator")) return f"{self._plnoun(word[:index])}{word[index:]}" # handle units given in degrees (only accept if # there is no more than one word following) # degree Celsius => degrees Celsius but degree # fahrenheit hour => degree fahrenheit hours if len(word.split_) >= 2 and word.split_[-2] == "degree": return " ".join([self._plnoun(word.first)] + word.split_[1:]) with contextlib.suppress(ValueError): return self._handle_prepositional_phrase( word.lowered, functools.partial(self._plnoun, count=2), '-', ) # HANDLE PRONOUNS for k, v in pl_pron_acc_keys_bysize.items(): if word.lowered[-k:] in v: # ends with accusative pronoun for pk, pv in pl_prep_bysize.items(): if word.lowered[:pk] in pv: # starts with a prep if word.lowered.split() == [ word.lowered[:pk], word.lowered[-k:], ]: # only whitespace in between return word.lowered[:-k] + pl_pron_acc[word.lowered[-k:]] try: return pl_pron_nom[word.lowered] except KeyError: pass try: return pl_pron_acc[word.lowered] except KeyError: pass # HANDLE ISOLATED IRREGULAR PLURALS if word.last in pl_sb_irregular_caps: llen = len(word.last) return f"{word[:-llen]}{pl_sb_irregular_caps[word.last]}" lowered_last = word.last.lower() if lowered_last in pl_sb_irregular: llen = len(lowered_last) return f"{word[:-llen]}{pl_sb_irregular[lowered_last]}" dash_split = word.lowered.split('-') if (" ".join(dash_split[-2:])).lower() in pl_sb_irregular_compound: llen = len( " ".join(dash_split[-2:]) ) # TODO: what if 2 spaces between these words? return ( f"{word[:-llen]}" f"{pl_sb_irregular_compound[(' '.join(dash_split[-2:])).lower()]}" ) if word.lowered[-3:] == "quy": return f"{word[:-1]}ies" if word.lowered[-6:] == "person": if self.classical_dict["persons"]: return f"{word}s" else: return f"{word[:-4]}ople" # HANDLE FAMILIES OF IRREGULAR PLURALS if word.lowered[-3:] == "man": for k, v in pl_sb_U_man_mans_bysize.items(): if word.lowered[-k:] in v: return f"{word}s" for k, v in pl_sb_U_man_mans_caps_bysize.items(): if word[-k:] in v: return f"{word}s" return f"{word[:-3]}men" if word.lowered[-5:] == "mouse": return f"{word[:-5]}mice" if word.lowered[-5:] == "louse": v = pl_sb_U_louse_lice_bysize.get(len(word)) if v and word.lowered in v: return f"{word[:-5]}lice" return f"{word}s" if word.lowered[-5:] == "goose": return f"{word[:-5]}geese" if word.lowered[-5:] == "tooth": return f"{word[:-5]}teeth" if word.lowered[-4:] == "foot": return f"{word[:-4]}feet" if word.lowered[-4:] == "taco": return f"{word[:-5]}tacos" if word.lowered == "die": return "dice" # HANDLE UNASSIMILATED IMPORTS if word.lowered[-4:] == "ceps": return word if word.lowered[-4:] == "zoon": return f"{word[:-2]}a" if word.lowered[-3:] in ("cis", "sis", "xis"): return f"{word[:-2]}es" for lastlet, d, numend, post in ( ("h", pl_sb_U_ch_chs_bysize, None, "s"), ("x", pl_sb_U_ex_ices_bysize, -2, "ices"), ("x", pl_sb_U_ix_ices_bysize, -2, "ices"), ("m", pl_sb_U_um_a_bysize, -2, "a"), ("s", pl_sb_U_us_i_bysize, -2, "i"), ("n", pl_sb_U_on_a_bysize, -2, "a"), ("a", pl_sb_U_a_ae_bysize, None, "e"), ): if word.lowered[-1] == lastlet: # this test to add speed for k, v in d.items(): if word.lowered[-k:] in v: return word[:numend] + post # HANDLE INCOMPLETELY ASSIMILATED IMPORTS if self.classical_dict["ancient"]: if word.lowered[-4:] == "trix": return f"{word[:-1]}ces" if word.lowered[-3:] in ("eau", "ieu"): return f"{word}x" if word.lowered[-3:] in ("ynx", "inx", "anx") and len(word) > 4: return f"{word[:-1]}ges" for lastlet, d, numend, post in ( ("n", pl_sb_C_en_ina_bysize, -2, "ina"), ("x", pl_sb_C_ex_ices_bysize, -2, "ices"), ("x", pl_sb_C_ix_ices_bysize, -2, "ices"), ("m", pl_sb_C_um_a_bysize, -2, "a"), ("s", pl_sb_C_us_i_bysize, -2, "i"), ("s", pl_sb_C_us_us_bysize, None, ""), ("a", pl_sb_C_a_ae_bysize, None, "e"), ("a", pl_sb_C_a_ata_bysize, None, "ta"), ("s", pl_sb_C_is_ides_bysize, -1, "des"), ("o", pl_sb_C_o_i_bysize, -1, "i"), ("n", pl_sb_C_on_a_bysize, -2, "a"), ): if word.lowered[-1] == lastlet: # this test to add speed for k, v in d.items(): if word.lowered[-k:] in v: return word[:numend] + post for d, numend, post in ( (pl_sb_C_i_bysize, None, "i"), (pl_sb_C_im_bysize, None, "im"), ): for k, v in d.items(): if word.lowered[-k:] in v: return word[:numend] + post # HANDLE SINGULAR NOUNS ENDING IN ...s OR OTHER SILIBANTS if lowered_last in pl_sb_singular_s_complete: return f"{word}es" for k, v in pl_sb_singular_s_bysize.items(): if word.lowered[-k:] in v: return f"{word}es" if word.lowered[-2:] == "es" and word[0] == word[0].upper(): return f"{word}es" if word.lowered[-1] == "z": for k, v in pl_sb_z_zes_bysize.items(): if word.lowered[-k:] in v: return f"{word}es" if word.lowered[-2:-1] != "z": return f"{word}zes" if word.lowered[-2:] == "ze": for k, v in pl_sb_ze_zes_bysize.items(): if word.lowered[-k:] in v: return f"{word}s" if word.lowered[-2:] in ("ch", "sh", "zz", "ss") or word.lowered[-1] == "x": return f"{word}es" # HANDLE ...f -> ...ves if word.lowered[-3:] in ("elf", "alf", "olf"): return f"{word[:-1]}ves" if word.lowered[-3:] == "eaf" and word.lowered[-4:-3] != "d": return f"{word[:-1]}ves" if word.lowered[-4:] in ("nife", "life", "wife"): return f"{word[:-2]}ves" if word.lowered[-3:] == "arf": return f"{word[:-1]}ves" # HANDLE ...y if word.lowered[-1] == "y": if word.lowered[-2:-1] in "aeiou" or len(word) == 1: return f"{word}s" if self.classical_dict["names"]: if word.lowered[-1] == "y" and word[0] == word[0].upper(): return f"{word}s" return f"{word[:-1]}ies" # HANDLE ...o if lowered_last in pl_sb_U_o_os_complete: return f"{word}s" for k, v in pl_sb_U_o_os_bysize.items(): if word.lowered[-k:] in v: return f"{word}s" if word.lowered[-2:] in ("ao", "eo", "io", "oo", "uo"): return f"{word}s" if word.lowered[-1] == "o": return f"{word}es" # OTHERWISE JUST ADD ...s return f"{word}s" @classmethod def _handle_prepositional_phrase(cls, phrase, transform, sep): """ Given a word or phrase possibly separated by sep, parse out the prepositional phrase and apply the transform to the word preceding the prepositional phrase. Raise ValueError if the pivot is not found or if at least two separators are not found. >>> engine._handle_prepositional_phrase("man-of-war", str.upper, '-') 'MAN-of-war' >>> engine._handle_prepositional_phrase("man of war", str.upper, ' ') 'MAN of war' """ parts = phrase.split(sep) if len(parts) < 3: raise ValueError("Cannot handle words with fewer than two separators") pivot = cls._find_pivot(parts, pl_prep_list_da) transformed = transform(parts[pivot - 1]) or parts[pivot - 1] return " ".join( parts[: pivot - 1] + [sep.join([transformed, parts[pivot], ''])] ) + " ".join(parts[(pivot + 1) :]) def _handle_long_compounds(self, word: Words, count: int) -> Union[str, None]: """ Handles the plural and singular for compound `Words` that have three or more words, based on the given count. >>> engine()._handle_long_compounds(Words("pair of scissors"), 2) 'pairs of scissors' >>> engine()._handle_long_compounds(Words("men beyond hills"), 1) 'man beyond hills' """ inflection = self._sinoun if count == 1 else self._plnoun solutions = ( # type: ignore " ".join( itertools.chain( leader, [inflection(cand, count), prep], # type: ignore trailer, ) ) for leader, (cand, prep), trailer in windowed_complete(word.split_, 2) if prep in pl_prep_list_da # type: ignore ) return next(solutions, None) @staticmethod def _find_pivot(words, candidates): pivots = ( index for index in range(1, len(words) - 1) if words[index] in candidates ) try: return next(pivots) except StopIteration: raise ValueError("No pivot found") from None def _pl_special_verb( # noqa: C901 self, word: str, count: Optional[Union[str, int]] = None ) -> Union[str, bool]: if self.classical_dict["zero"] and str(count).lower() in pl_count_zero: return False count = self.get_count(count) if count == 1: return word # HANDLE USER-DEFINED VERBS value = self.ud_match(word, self.pl_v_user_defined) if value is not None: return value # HANDLE IRREGULAR PRESENT TENSE (SIMPLE AND COMPOUND) try: words = Words(word) except IndexError: return False # word is '' if words.first in plverb_irregular_pres: return f"{plverb_irregular_pres[words.first]}{words[len(words.first) :]}" # HANDLE IRREGULAR FUTURE, PRETERITE AND PERFECT TENSES if words.first in plverb_irregular_non_pres: return word # HANDLE PRESENT NEGATIONS (SIMPLE AND COMPOUND) if words.first.endswith("n't") and words.first[:-3] in plverb_irregular_pres: return ( f"{plverb_irregular_pres[words.first[:-3]]}n't" f"{words[len(words.first) :]}" ) if words.first.endswith("n't"): return word # HANDLE SPECIAL CASES mo = PLVERB_SPECIAL_S_RE.search(word) if mo: return False if WHITESPACE.search(word): return False if words.lowered == "quizzes": return "quiz" # HANDLE STANDARD 3RD PERSON (CHOP THE ...(e)s OFF SINGLE WORDS) if ( words.lowered[-4:] in ("ches", "shes", "zzes", "sses") or words.lowered[-3:] == "xes" ): return words[:-2] if words.lowered[-3:] == "ies" and len(words) > 3: return words.lowered[:-3] + "y" if ( words.last.lower() in pl_v_oes_oe or words.lowered[-4:] in pl_v_oes_oe_endings_size4 or words.lowered[-5:] in pl_v_oes_oe_endings_size5 ): return words[:-1] if words.lowered.endswith("oes") and len(words) > 3: return words.lowered[:-2] mo = ENDS_WITH_S.search(words) if mo: return mo.group(1) # OTHERWISE, A REGULAR VERB (HANDLE ELSEWHERE) return False def _pl_general_verb( self, word: str, count: Optional[Union[str, int]] = None ) -> str: count = self.get_count(count) if count == 1: return word # HANDLE AMBIGUOUS PRESENT TENSES (SIMPLE AND COMPOUND) mo = plverb_ambiguous_pres_keys.search(word) if mo: return f"{plverb_ambiguous_pres[mo.group(1).lower()]}{mo.group(2)}" # HANDLE AMBIGUOUS PRETERITE AND PERFECT TENSES mo = plverb_ambiguous_non_pres.search(word) if mo: return word # OTHERWISE, 1st OR 2ND PERSON IS UNINFLECTED return word def _pl_special_adjective( self, word: str, count: Optional[Union[str, int]] = None ) -> Union[str, bool]: count = self.get_count(count) if count == 1: return word # HANDLE USER-DEFINED ADJECTIVES value = self.ud_match(word, self.pl_adj_user_defined) if value is not None: return value # HANDLE KNOWN CASES mo = pl_adj_special_keys.search(word) if mo: return pl_adj_special[mo.group(1).lower()] # HANDLE POSSESSIVES mo = pl_adj_poss_keys.search(word) if mo: return pl_adj_poss[mo.group(1).lower()] mo = ENDS_WITH_APOSTROPHE_S.search(word) if mo: pl = self.plural_noun(mo.group(1)) trailing_s = "" if pl[-1] == "s" else "s" return f"{pl}'{trailing_s}" # OTHERWISE, NO IDEA return False # @profile def _sinoun( # noqa: C901 self, word: str, count: Optional[Union[str, int]] = None, gender: Optional[str] = None, ) -> Union[str, bool]: count = self.get_count(count) # DEFAULT TO PLURAL if count == 2: return word # SET THE GENDER try: if gender is None: gender = self.thegender elif gender not in singular_pronoun_genders: raise BadGenderError except (TypeError, IndexError) as err: raise BadGenderError from err # HANDLE USER-DEFINED NOUNS value = self.ud_match(word, self.si_sb_user_defined) if value is not None: return value # HANDLE EMPTY WORD, SINGULAR COUNT AND UNINFLECTED PLURALS if word == "": return word if word in si_sb_ois_oi_case: return word[:-1] words = Words(word) if words.last.lower() in pl_sb_uninflected_complete: if len(words.split_) >= 3: return self._handle_long_compounds(words, count=1) or word return word if word in pl_sb_uninflected_caps: return word for k, v in pl_sb_uninflected_bysize.items(): if words.lowered[-k:] in v: return word if self.classical_dict["herd"] and words.last.lower() in pl_sb_uninflected_herd: return word if words.last.lower() in pl_sb_C_us_us: return word if self.classical_dict["ancient"] else False # HANDLE COMPOUNDS ("Governor General", "mother-in-law", "aide-de-camp", ETC.) mo = PL_SB_POSTFIX_ADJ_STEMS_RE.search(word) if mo and mo.group(2) != "": return f"{self._sinoun(mo.group(1), 1, gender=gender)}{mo.group(2)}" with contextlib.suppress(ValueError): return self._handle_prepositional_phrase( words.lowered, functools.partial(self._sinoun, count=1, gender=gender), ' ', ) with contextlib.suppress(ValueError): return self._handle_prepositional_phrase( words.lowered, functools.partial(self._sinoun, count=1, gender=gender), '-', ) # HANDLE PRONOUNS for k, v in si_pron_acc_keys_bysize.items(): if words.lowered[-k:] in v: # ends with accusative pronoun for pk, pv in pl_prep_bysize.items(): if words.lowered[:pk] in pv: # starts with a prep if words.lowered.split() == [ words.lowered[:pk], words.lowered[-k:], ]: # only whitespace in between return words.lowered[:-k] + get_si_pron( "acc", words.lowered[-k:], gender ) try: return get_si_pron("nom", words.lowered, gender) except KeyError: pass try: return get_si_pron("acc", words.lowered, gender) except KeyError: pass # HANDLE ISOLATED IRREGULAR PLURALS if words.last in si_sb_irregular_caps: llen = len(words.last) return f"{word[:-llen]}{si_sb_irregular_caps[words.last]}" if words.last.lower() in si_sb_irregular: llen = len(words.last.lower()) return f"{word[:-llen]}{si_sb_irregular[words.last.lower()]}" dash_split = words.lowered.split("-") if (" ".join(dash_split[-2:])).lower() in si_sb_irregular_compound: llen = len( " ".join(dash_split[-2:]) ) # TODO: what if 2 spaces between these words? return "{}{}".format( word[:-llen], si_sb_irregular_compound[(" ".join(dash_split[-2:])).lower()], ) if words.lowered[-5:] == "quies": return word[:-3] + "y" if words.lowered[-7:] == "persons": return word[:-1] if words.lowered[-6:] == "people": return word[:-4] + "rson" # HANDLE FAMILIES OF IRREGULAR PLURALS if words.lowered[-4:] == "mans": for k, v in si_sb_U_man_mans_bysize.items(): if words.lowered[-k:] in v: return word[:-1] for k, v in si_sb_U_man_mans_caps_bysize.items(): if word[-k:] in v: return word[:-1] if words.lowered[-3:] == "men": return word[:-3] + "man" if words.lowered[-4:] == "mice": return word[:-4] + "mouse" if words.lowered[-4:] == "lice": v = si_sb_U_louse_lice_bysize.get(len(word)) if v and words.lowered in v: return word[:-4] + "louse" if words.lowered[-5:] == "geese": return word[:-5] + "goose" if words.lowered[-5:] == "teeth": return word[:-5] + "tooth" if words.lowered[-4:] == "feet": return word[:-4] + "foot" if words.lowered == "dice": return "die" # HANDLE UNASSIMILATED IMPORTS if words.lowered[-4:] == "ceps": return word if words.lowered[-3:] == "zoa": return word[:-1] + "on" for lastlet, d, unass_numend, post in ( ("s", si_sb_U_ch_chs_bysize, -1, ""), ("s", si_sb_U_ex_ices_bysize, -4, "ex"), ("s", si_sb_U_ix_ices_bysize, -4, "ix"), ("a", si_sb_U_um_a_bysize, -1, "um"), ("i", si_sb_U_us_i_bysize, -1, "us"), ("a", si_sb_U_on_a_bysize, -1, "on"), ("e", si_sb_U_a_ae_bysize, -1, ""), ): if words.lowered[-1] == lastlet: # this test to add speed for k, v in d.items(): if words.lowered[-k:] in v: return word[:unass_numend] + post # HANDLE INCOMPLETELY ASSIMILATED IMPORTS if self.classical_dict["ancient"]: if words.lowered[-6:] == "trices": return word[:-3] + "x" if words.lowered[-4:] in ("eaux", "ieux"): return word[:-1] if words.lowered[-5:] in ("ynges", "inges", "anges") and len(word) > 6: return word[:-3] + "x" for lastlet, d, class_numend, post in ( ("a", si_sb_C_en_ina_bysize, -3, "en"), ("s", si_sb_C_ex_ices_bysize, -4, "ex"), ("s", si_sb_C_ix_ices_bysize, -4, "ix"), ("a", si_sb_C_um_a_bysize, -1, "um"), ("i", si_sb_C_us_i_bysize, -1, "us"), ("s", pl_sb_C_us_us_bysize, None, ""), ("e", si_sb_C_a_ae_bysize, -1, ""), ("a", si_sb_C_a_ata_bysize, -2, ""), ("s", si_sb_C_is_ides_bysize, -3, "s"), ("i", si_sb_C_o_i_bysize, -1, "o"), ("a", si_sb_C_on_a_bysize, -1, "on"), ("m", si_sb_C_im_bysize, -2, ""), ("i", si_sb_C_i_bysize, -1, ""), ): if words.lowered[-1] == lastlet: # this test to add speed for k, v in d.items(): if words.lowered[-k:] in v: return word[:class_numend] + post # HANDLE PLURLS ENDING IN uses -> use if ( words.lowered[-6:] == "houses" or word in si_sb_uses_use_case or words.last.lower() in si_sb_uses_use ): return word[:-1] # HANDLE PLURLS ENDING IN ies -> ie if word in si_sb_ies_ie_case or words.last.lower() in si_sb_ies_ie: return word[:-1] # HANDLE PLURLS ENDING IN oes -> oe if ( words.lowered[-5:] == "shoes" or word in si_sb_oes_oe_case or words.last.lower() in si_sb_oes_oe ): return word[:-1] # HANDLE SINGULAR NOUNS ENDING IN ...s OR OTHER SILIBANTS if word in si_sb_sses_sse_case or words.last.lower() in si_sb_sses_sse: return word[:-1] if words.last.lower() in si_sb_singular_s_complete: return word[:-2] for k, v in si_sb_singular_s_bysize.items(): if words.lowered[-k:] in v: return word[:-2] if words.lowered[-4:] == "eses" and word[0] == word[0].upper(): return word[:-2] if words.last.lower() in si_sb_z_zes: return word[:-2] if words.last.lower() in si_sb_zzes_zz: return word[:-2] if words.lowered[-4:] == "zzes": return word[:-3] if word in si_sb_ches_che_case or words.last.lower() in si_sb_ches_che: return word[:-1] if words.lowered[-4:] in ("ches", "shes"): return word[:-2] if words.last.lower() in si_sb_xes_xe: return word[:-1] if words.lowered[-3:] == "xes": return word[:-2] # HANDLE ...f -> ...ves if word in si_sb_ves_ve_case or words.last.lower() in si_sb_ves_ve: return word[:-1] if words.lowered[-3:] == "ves": if words.lowered[-5:-3] in ("el", "al", "ol"): return word[:-3] + "f" if words.lowered[-5:-3] == "ea" and word[-6:-5] != "d": return word[:-3] + "f" if words.lowered[-5:-3] in ("ni", "li", "wi"): return word[:-3] + "fe" if words.lowered[-5:-3] == "ar": return word[:-3] + "f" # HANDLE ...y if words.lowered[-2:] == "ys": if len(words.lowered) > 2 and words.lowered[-3] in "aeiou": return word[:-1] if self.classical_dict["names"]: if words.lowered[-2:] == "ys" and word[0] == word[0].upper(): return word[:-1] if words.lowered[-3:] == "ies": return word[:-3] + "y" # HANDLE ...o if words.lowered[-2:] == "os": if words.last.lower() in si_sb_U_o_os_complete: return word[:-1] for k, v in si_sb_U_o_os_bysize.items(): if words.lowered[-k:] in v: return word[:-1] if words.lowered[-3:] in ("aos", "eos", "ios", "oos", "uos"): return word[:-1] if words.lowered[-3:] == "oes": return word[:-2] # UNASSIMILATED IMPORTS FINAL RULE if word in si_sb_es_is: return word[:-2] + "is" # OTHERWISE JUST REMOVE ...s if words.lowered[-1] == "s": return word[:-1] # COULD NOT FIND SINGULAR return False # ADJECTIVES @typechecked def a(self, text: Word, count: Optional[Union[int, str, Any]] = 1) -> str: """ Return the appropriate indefinite article followed by text. The indefinite article is either 'a' or 'an'. If count is not one, then return count followed by text instead of 'a' or 'an'. Whitespace at the start and end is preserved. """ mo = INDEFINITE_ARTICLE_TEST.search(text) if mo: word = mo.group(2) if not word: return text pre = mo.group(1) post = mo.group(3) result = self._indef_article(word, count) return f"{pre}{result}{post}" return "" an = a _indef_article_cases = ( # HANDLE ORDINAL FORMS (A_ordinal_a, "a"), (A_ordinal_an, "an"), # HANDLE SPECIAL CASES (A_explicit_an, "an"), (SPECIAL_AN, "an"), (SPECIAL_A, "a"), # HANDLE ABBREVIATIONS (A_abbrev, "an"), (SPECIAL_ABBREV_AN, "an"), (SPECIAL_ABBREV_A, "a"), # HANDLE CONSONANTS (CONSONANTS, "a"), # HANDLE SPECIAL VOWEL-FORMS (ARTICLE_SPECIAL_EU, "a"), (ARTICLE_SPECIAL_ONCE, "a"), (ARTICLE_SPECIAL_ONETIME, "a"), (ARTICLE_SPECIAL_UNIT, "a"), (ARTICLE_SPECIAL_UBA, "a"), (ARTICLE_SPECIAL_UKR, "a"), (A_explicit_a, "a"), # HANDLE SPECIAL CAPITALS (SPECIAL_CAPITALS, "a"), # HANDLE VOWELS (VOWELS, "an"), # HANDLE y... # (BEFORE CERTAIN CONSONANTS IMPLIES (UNNATURALIZED) "i.." SOUND) (A_y_cons, "an"), ) def _indef_article(self, word: str, count: Union[int, str, Any]) -> str: mycount = self.get_count(count) if mycount != 1: return f"{count} {word}" # HANDLE USER-DEFINED VARIANTS value = self.ud_match(word, self.A_a_user_defined) if value is not None: return f"{value} {word}" matches = ( f'{article} {word}' for regexen, article in self._indef_article_cases if regexen.search(word) ) # OTHERWISE, GUESS "a" fallback = f'a {word}' return next(matches, fallback) # 2. TRANSLATE ZERO-QUANTIFIED $word TO "no plural($word)" @typechecked def no(self, text: Word, count: Optional[Union[int, str]] = None) -> str: """ If count is 0, no, zero or nil, return 'no' followed by the plural of text. If count is one of: 1, a, an, one, each, every, this, that return count followed by text. Otherwise return count follow by the plural of text. In the return value count is always followed by a space. Whitespace at the start and end is preserved. """ if count is None and self.persistent_count is not None: count = self.persistent_count if count is None: count = 0 mo = PARTITION_WORD.search(text) if mo: pre = mo.group(1) word = mo.group(2) post = mo.group(3) else: pre = "" word = "" post = "" if str(count).lower() in pl_count_zero: count = 'no' return f"{pre}{count} {self.plural(word, count)}{post}" # PARTICIPLES @typechecked def present_participle(self, word: Word) -> str: """ Return the present participle for word. word is the 3rd person singular verb. """ plv = self.plural_verb(word, 2) ans = plv for regexen, repl in PRESENT_PARTICIPLE_REPLACEMENTS: ans, num = regexen.subn(repl, plv) if num: return f"{ans}ing" return f"{ans}ing" # NUMERICAL INFLECTIONS @typechecked def ordinal(self, num: Union[Number, Word]) -> str: """ Return the ordinal of num. >>> ordinal = engine().ordinal >>> ordinal(1) '1st' >>> ordinal('one') 'first' """ if DIGIT.match(str(num)): if isinstance(num, (float, int)) and int(num) == num: n = int(num) else: if "." in str(num): try: # numbers after decimal, # so only need last one for ordinal n = int(str(num)[-1]) except ValueError: # ends with '.', so need to use whole string n = int(str(num)[:-1]) else: n = int(num) # type: ignore try: post = nth[n % 100] except KeyError: post = nth[n % 10] return f"{num}{post}" else: return self._sub_ord(num) def millfn(self, ind: int = 0) -> str: if ind > len(mill) - 1: raise NumOutOfRangeError return mill[ind] def unitfn(self, units: int, mindex: int = 0) -> str: return f"{unit[units]}{self.millfn(mindex)}" def tenfn(self, tens, units, mindex=0) -> str: if tens != 1: tens_part = ten[tens] if tens and units: hyphen = "-" else: hyphen = "" unit_part = unit[units] mill_part = self.millfn(mindex) return f"{tens_part}{hyphen}{unit_part}{mill_part}" return f"{teen[units]}{mill[mindex]}" def hundfn(self, hundreds: int, tens: int, units: int, mindex: int) -> str: if hundreds: andword = f" {self._number_args['andword']} " if tens or units else "" # use unit not unitfn as simpler return ( f"{unit[hundreds]} hundred{andword}" f"{self.tenfn(tens, units)}{self.millfn(mindex)}, " ) if tens or units: return f"{self.tenfn(tens, units)}{self.millfn(mindex)}, " return "" def group1sub(self, mo: Match) -> str: units = int(mo.group(1)) if units == 1: return f" {self._number_args['one']}, " elif units: return f"{unit[units]}, " else: return f" {self._number_args['zero']}, " def group1bsub(self, mo: Match) -> str: units = int(mo.group(1)) if units: return f"{unit[units]}, " else: return f" {self._number_args['zero']}, " def group2sub(self, mo: Match) -> str: tens = int(mo.group(1)) units = int(mo.group(2)) if tens: return f"{self.tenfn(tens, units)}, " if units: return f" {self._number_args['zero']} {unit[units]}, " return f" {self._number_args['zero']} {self._number_args['zero']}, " def group3sub(self, mo: Match) -> str: hundreds = int(mo.group(1)) tens = int(mo.group(2)) units = int(mo.group(3)) if hundreds == 1: hunword = f" {self._number_args['one']}" elif hundreds: hunword = str(unit[hundreds]) else: hunword = f" {self._number_args['zero']}" if tens: tenword = self.tenfn(tens, units) elif units: tenword = f" {self._number_args['zero']} {unit[units]}" else: tenword = f" {self._number_args['zero']} {self._number_args['zero']}" return f"{hunword} {tenword}, " def hundsub(self, mo: Match) -> str: ret = self.hundfn( int(mo.group(1)), int(mo.group(2)), int(mo.group(3)), self.mill_count ) self.mill_count += 1 return ret def tensub(self, mo: Match) -> str: return f"{self.tenfn(int(mo.group(1)), int(mo.group(2)), self.mill_count)}, " def unitsub(self, mo: Match) -> str: return f"{self.unitfn(int(mo.group(1)), self.mill_count)}, " def enword(self, num: str, group: int) -> str: # import pdb # pdb.set_trace() if group == 1: num = DIGIT_GROUP.sub(self.group1sub, num) elif group == 2: num = TWO_DIGITS.sub(self.group2sub, num) num = DIGIT_GROUP.sub(self.group1bsub, num, 1) elif group == 3: num = THREE_DIGITS.sub(self.group3sub, num) num = TWO_DIGITS.sub(self.group2sub, num, 1) num = DIGIT_GROUP.sub(self.group1sub, num, 1) elif int(num) == 0: num = self._number_args["zero"] elif int(num) == 1: num = self._number_args["one"] else: num = num.lstrip().lstrip("0") self.mill_count = 0 # surely there's a better way to do the next bit mo = THREE_DIGITS_WORD.search(num) while mo: num = THREE_DIGITS_WORD.sub(self.hundsub, num, 1) mo = THREE_DIGITS_WORD.search(num) num = TWO_DIGITS_WORD.sub(self.tensub, num, 1) num = ONE_DIGIT_WORD.sub(self.unitsub, num, 1) return num @staticmethod def _sub_ord(val): new = ordinal_suff.sub(lambda match: ordinal[match.group(1)], val) return new + "th" * (new == val) @classmethod def _chunk_num(cls, num, decimal, group): if decimal: max_split = -1 if group != 0 else 1 chunks = num.split(".", max_split) else: chunks = [num] return cls._remove_last_blank(chunks) @staticmethod def _remove_last_blank(chunks): """ Remove the last item from chunks if it's a blank string. Return the resultant chunks and whether the last item was removed. """ removed = chunks[-1] == "" result = chunks[:-1] if removed else chunks return result, removed @staticmethod def _get_sign(num): return {'+': 'plus', '-': 'minus'}.get(num.lstrip()[0], '') @typechecked def number_to_words( # noqa: C901 self, num: Union[Number, Word], wantlist: bool = False, group: int = 0, comma: Union[Falsish, str] = ",", andword: str = "and", zero: str = "zero", one: str = "one", decimal: Union[Falsish, str] = "point", threshold: Optional[int] = None, ) -> Union[str, List[str]]: """ Return a number in words. group = 1, 2 or 3 to group numbers before turning into words comma: define comma andword: word for 'and'. Can be set to ''. e.g. "one hundred and one" vs "one hundred one" zero: word for '0' one: word for '1' decimal: word for decimal point threshold: numbers above threshold not turned into words parameters not remembered from last call. Departure from Perl version. """ self._number_args = {"andword": andword, "zero": zero, "one": one} num = str(num) # Handle "stylistic" conversions (up to a given threshold)... if threshold is not None and float(num) > threshold: spnum = num.split(".", 1) while comma: (spnum[0], n) = FOUR_DIGIT_COMMA.subn(r"\1,\2", spnum[0]) if n == 0: break try: return f"{spnum[0]}.{spnum[1]}" except IndexError: return str(spnum[0]) if group < 0 or group > 3: raise BadChunkingOptionError sign = self._get_sign(num) if num in nth_suff: num = zero myord = num[-2:] in nth_suff if myord: num = num[:-2] chunks, finalpoint = self._chunk_num(num, decimal, group) loopstart = chunks[0] == "" first: bool | None = not loopstart def _handle_chunk(chunk): nonlocal first # remove all non numeric \D chunk = NON_DIGIT.sub("", chunk) if chunk == "": chunk = "0" if group == 0 and not first: chunk = self.enword(chunk, 1) else: chunk = self.enword(chunk, group) if chunk[-2:] == ", ": chunk = chunk[:-2] chunk = WHITESPACES_COMMA.sub(",", chunk) if group == 0 and first: chunk = COMMA_WORD.sub(f" {andword} \\1", chunk) chunk = WHITESPACES.sub(" ", chunk) # chunk = re.sub(r"(\A\s|\s\Z)", self.blankfn, chunk) chunk = chunk.strip() if first: first = None return chunk chunks[loopstart:] = map(_handle_chunk, chunks[loopstart:]) numchunks = [] if first != 0: numchunks = chunks[0].split(f"{comma} ") if myord and numchunks: numchunks[-1] = self._sub_ord(numchunks[-1]) for chunk in chunks[1:]: numchunks.append(decimal) numchunks.extend(chunk.split(f"{comma} ")) if finalpoint: numchunks.append(decimal) if wantlist: return [sign] * bool(sign) + numchunks signout = f"{sign} " if sign else "" valout = ( ', '.join(numchunks) if group else ''.join(self._render(numchunks, decimal, comma)) ) return signout + valout @staticmethod def _render(chunks, decimal, comma): first_item = chunks.pop(0) yield first_item first = decimal is None or not first_item.endswith(decimal) for nc in chunks: if nc == decimal: first = False elif first: yield comma yield f" {nc}" @typechecked def join( self, words: Optional[Sequence[Word]], sep: Optional[str] = None, sep_spaced: bool = True, final_sep: Optional[str] = None, conj: str = "and", conj_spaced: bool = True, ) -> str: """ Join words into a list. e.g. join(['ant', 'bee', 'fly']) returns 'ant, bee, and fly' options: conj: replacement for 'and' sep: separator. default ',', unless ',' is in the list then ';' final_sep: final separator. default ',', unless ',' is in the list then ';' conj_spaced: boolean. Should conj have spaces around it """ if not words: return "" if len(words) == 1: return words[0] if conj_spaced: if conj == "": conj = " " else: conj = f" {conj} " if len(words) == 2: return f"{words[0]}{conj}{words[1]}" if sep is None: if "," in "".join(words): sep = ";" else: sep = "," if final_sep is None: final_sep = sep final_sep = f"{final_sep}{conj}" if sep_spaced: sep += " " return f"{sep.join(words[0:-1])}{final_sep}{words[-1]}" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719878424.2594771 inflect-7.3.1/inflect/compat/0000755000175100001770000000000014640641430015464 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/inflect/compat/__init__.py0000644000175100001770000000000014640641406017566 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/inflect/compat/py38.py0000644000175100001770000000024014640641406016640 0ustar00runnerdockerimport sys if sys.version_info > (3, 9): from typing import Annotated else: # pragma: no cover from typing_extensions import Annotated # noqa: F401 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/inflect/py.typed0000644000175100001770000000000014640641406015671 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719878424.2594771 inflect-7.3.1/inflect.egg-info/0000755000175100001770000000000014640641430015673 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878424.0 inflect-7.3.1/inflect.egg-info/PKG-INFO0000644000175100001770000005107714640641430017002 0ustar00runnerdockerMetadata-Version: 2.1 Name: inflect Version: 7.3.1 Summary: Correctly generate plurals, singular nouns, ordinals, indefinite articles Author-email: Paul Dyson Maintainer-email: "Jason R. Coombs" Project-URL: Source, https://github.com/jaraco/inflect Keywords: plural,inflect,participle Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Linguistic Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: more_itertools>=8.5.0 Requires-Dist: typeguard>=4.0.1 Requires-Dist: typing_extensions; python_version < "3.9" Provides-Extra: test Requires-Dist: pytest!=8.1.*,>=6; extra == "test" Requires-Dist: pytest-checkdocs>=2.4; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: pytest-mypy; extra == "test" Requires-Dist: pytest-enabler>=2.2; extra == "test" Requires-Dist: pytest-ruff>=0.2.1; extra == "test" Requires-Dist: pygments; extra == "test" Provides-Extra: doc Requires-Dist: sphinx>=3.5; extra == "doc" Requires-Dist: jaraco.packaging>=9.3; extra == "doc" Requires-Dist: rst.linker>=1.9; extra == "doc" Requires-Dist: furo; extra == "doc" Requires-Dist: sphinx-lint; extra == "doc" Requires-Dist: jaraco.tidelift>=1.4; extra == "doc" .. image:: https://img.shields.io/pypi/v/inflect.svg :target: https://pypi.org/project/inflect .. image:: https://img.shields.io/pypi/pyversions/inflect.svg .. image:: https://github.com/jaraco/inflect/actions/workflows/main.yml/badge.svg :target: https://github.com/jaraco/inflect/actions?query=workflow%3A%22tests%22 :alt: tests .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff .. image:: https://readthedocs.org/projects/inflect/badge/?version=latest :target: https://inflect.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/skeleton-2024-informational :target: https://blog.jaraco.com/skeleton .. image:: https://tidelift.com/badges/package/pypi/inflect :target: https://tidelift.com/subscription/pkg/pypi-inflect?utm_source=pypi-inflect&utm_medium=readme NAME ==== inflect.py - Correctly generate plurals, singular nouns, ordinals, indefinite articles; convert numbers to words. SYNOPSIS ======== .. code-block:: python import inflect p = inflect.engine() # METHODS: # plural plural_noun plural_verb plural_adj singular_noun no num # compare compare_nouns compare_nouns compare_adjs # a an # present_participle # ordinal number_to_words # join # inflect classical gender # defnoun defverb defadj defa defan # UNCONDITIONALLY FORM THE PLURAL print("The plural of ", word, " is ", p.plural(word)) # CONDITIONALLY FORM THE PLURAL print("I saw", cat_count, p.plural("cat", cat_count)) # FORM PLURALS FOR SPECIFIC PARTS OF SPEECH print( p.plural_noun("I", N1), p.plural_verb("saw", N1), p.plural_adj("my", N2), p.plural_noun("saw", N2), ) # FORM THE SINGULAR OF PLURAL NOUNS print("The singular of ", word, " is ", p.singular_noun(word)) # SELECT THE GENDER OF SINGULAR PRONOUNS print(p.singular_noun("they")) # 'it' p.gender("feminine") print(p.singular_noun("they")) # 'she' # DEAL WITH "0/1/N" -> "no/1/N" TRANSLATION: print("There ", p.plural_verb("was", errors), p.no(" error", errors)) # USE DEFAULT COUNTS: print( p.num(N1, ""), p.plural("I"), p.plural_verb(" saw"), p.num(N2), p.plural_noun(" saw"), ) print("There ", p.num(errors, ""), p.plural_verb("was"), p.no(" error")) # COMPARE TWO WORDS "NUMBER-INSENSITIVELY": if p.compare(word1, word2): print("same") if p.compare_nouns(word1, word2): print("same noun") if p.compare_verbs(word1, word2): print("same verb") if p.compare_adjs(word1, word2): print("same adj.") # ADD CORRECT "a" OR "an" FOR A GIVEN WORD: print("Did you want ", p.a(thing), " or ", p.an(idea)) # CONVERT NUMERALS INTO ORDINALS (i.e. 1->1st, 2->2nd, 3->3rd, etc.) print("It was", p.ordinal(position), " from the left\n") # CONVERT NUMERALS TO WORDS (i.e. 1->"one", 101->"one hundred and one", etc.) # RETURNS A SINGLE STRING... words = p.number_to_words(1234) # "one thousand, two hundred and thirty-four" words = p.number_to_words(p.ordinal(1234)) # "one thousand, two hundred and thirty-fourth" # GET BACK A LIST OF STRINGS, ONE FOR EACH "CHUNK"... words = p.number_to_words(1234, wantlist=True) # ("one thousand","two hundred and thirty-four") # OPTIONAL PARAMETERS CHANGE TRANSLATION: words = p.number_to_words(12345, group=1) # "one, two, three, four, five" words = p.number_to_words(12345, group=2) # "twelve, thirty-four, five" words = p.number_to_words(12345, group=3) # "one twenty-three, forty-five" words = p.number_to_words(1234, andword="") # "one thousand, two hundred thirty-four" words = p.number_to_words(1234, andword=", plus") # "one thousand, two hundred, plus thirty-four" # TODO: I get no comma before plus: check perl words = p.number_to_words(555_1202, group=1, zero="oh") # "five, five, five, one, two, oh, two" words = p.number_to_words(555_1202, group=1, one="unity") # "five, five, five, unity, two, oh, two" words = p.number_to_words(123.456, group=1, decimal="mark") # "one two three mark four five six" # TODO: DOCBUG: perl gives commas here as do I # LITERAL STYLE ONLY NAMES NUMBERS LESS THAN A CERTAIN THRESHOLD... words = p.number_to_words(9, threshold=10) # "nine" words = p.number_to_words(10, threshold=10) # "ten" words = p.number_to_words(11, threshold=10) # "11" words = p.number_to_words(1000, threshold=10) # "1,000" # JOIN WORDS INTO A LIST: mylist = p.join(("apple", "banana", "carrot")) # "apple, banana, and carrot" mylist = p.join(("apple", "banana")) # "apple and banana" mylist = p.join(("apple", "banana", "carrot"), final_sep="") # "apple, banana and carrot" # REQUIRE "CLASSICAL" PLURALS (EG: "focus"->"foci", "cherub"->"cherubim") p.classical() # USE ALL CLASSICAL PLURALS p.classical(all=True) # USE ALL CLASSICAL PLURALS p.classical(all=False) # SWITCH OFF CLASSICAL MODE p.classical(zero=True) # "no error" INSTEAD OF "no errors" p.classical(zero=False) # "no errors" INSTEAD OF "no error" p.classical(herd=True) # "2 buffalo" INSTEAD OF "2 buffalos" p.classical(herd=False) # "2 buffalos" INSTEAD OF "2 buffalo" p.classical(persons=True) # "2 chairpersons" INSTEAD OF "2 chairpeople" p.classical(persons=False) # "2 chairpeople" INSTEAD OF "2 chairpersons" p.classical(ancient=True) # "2 formulae" INSTEAD OF "2 formulas" p.classical(ancient=False) # "2 formulas" INSTEAD OF "2 formulae" # INTERPOLATE "plural()", "plural_noun()", "plural_verb()", "plural_adj()", "singular_noun()", # a()", "an()", "num()" AND "ordinal()" WITHIN STRINGS: print(p.inflect("The plural of {0} is plural('{0}')".format(word))) print(p.inflect("The singular of {0} is singular_noun('{0}')".format(word))) print(p.inflect("I saw {0} plural('cat',{0})".format(cat_count))) print( p.inflect( "plural('I',{0}) " "plural_verb('saw',{0}) " "plural('a',{1}) " "plural_noun('saw',{1})".format(N1, N2) ) ) print( p.inflect( "num({0}, False)plural('I') " "plural_verb('saw') " "num({1}, False)plural('a') " "plural_noun('saw')".format(N1, N2) ) ) print(p.inflect("I saw num({0}) plural('cat')\nnum()".format(cat_count))) print(p.inflect("There plural_verb('was',{0}) no('error',{0})".format(errors))) print(p.inflect("There num({0}, False)plural_verb('was') no('error')".format(errors))) print(p.inflect("Did you want a('{0}') or an('{1}')".format(thing, idea))) print(p.inflect("It was ordinal('{0}') from the left".format(position))) # ADD USER-DEFINED INFLECTIONS (OVERRIDING INBUILT RULES): p.defnoun("VAX", "VAXen") # SINGULAR => PLURAL p.defverb( "will", # 1ST PERSON SINGULAR "shall", # 1ST PERSON PLURAL "will", # 2ND PERSON SINGULAR "will", # 2ND PERSON PLURAL "will", # 3RD PERSON SINGULAR "will", # 3RD PERSON PLURAL ) p.defadj("hir", "their") # SINGULAR => PLURAL p.defa("h") # "AY HALWAYS SEZ 'HAITCH'!" p.defan("horrendous.*") # "AN HORRENDOUS AFFECTATION" DESCRIPTION =========== The methods of the class ``engine`` in module ``inflect.py`` provide plural inflections, singular noun inflections, "a"/"an" selection for English words, and manipulation of numbers as words. Plural forms of all nouns, most verbs, and some adjectives are provided. Where appropriate, "classical" variants (for example: "brother" -> "brethren", "dogma" -> "dogmata", etc.) are also provided. Single forms of nouns are also provided. The gender of singular pronouns can be chosen (for example "they" -> "it" or "she" or "he" or "they"). Pronunciation-based "a"/"an" selection is provided for all English words, and most initialisms. It is also possible to inflect numerals (1,2,3) to ordinals (1st, 2nd, 3rd) and to English words ("one", "two", "three"). In generating these inflections, ``inflect.py`` follows the Oxford English Dictionary and the guidelines in Fowler's Modern English Usage, preferring the former where the two disagree. The module is built around standard British spelling, but is designed to cope with common American variants as well. Slang, jargon, and other English dialects are *not* explicitly catered for. Where two or more inflected forms exist for a single word (typically a "classical" form and a "modern" form), ``inflect.py`` prefers the more common form (typically the "modern" one), unless "classical" processing has been specified (see `MODERN VS CLASSICAL INFLECTIONS`). FORMING PLURALS AND SINGULARS ============================= Inflecting Plurals and Singulars -------------------------------- All of the ``plural...`` plural inflection methods take the word to be inflected as their first argument and return the corresponding inflection. Note that all such methods expect the *singular* form of the word. The results of passing a plural form are undefined (and unlikely to be correct). Similarly, the ``si...`` singular inflection method expects the *plural* form of the word. The ``plural...`` methods also take an optional second argument, which indicates the grammatical "number" of the word (or of another word with which the word being inflected must agree). If the "number" argument is supplied and is not ``1`` (or ``"one"`` or ``"a"``, or some other adjective that implies the singular), the plural form of the word is returned. If the "number" argument *does* indicate singularity, the (uninflected) word itself is returned. If the number argument is omitted, the plural form is returned unconditionally. The ``si...`` method takes a second argument in a similar fashion. If it is some form of the number ``1``, or is omitted, the singular form is returned. Otherwise the plural is returned unaltered. The various methods of ``inflect.engine`` are: ``plural_noun(word, count=None)`` The method ``plural_noun()`` takes a *singular* English noun or pronoun and returns its plural. Pronouns in the nominative ("I" -> "we") and accusative ("me" -> "us") cases are handled, as are possessive pronouns ("mine" -> "ours"). ``plural_verb(word, count=None)`` The method ``plural_verb()`` takes the *singular* form of a conjugated verb (that is, one which is already in the correct "person" and "mood") and returns the corresponding plural conjugation. ``plural_adj(word, count=None)`` The method ``plural_adj()`` takes the *singular* form of certain types of adjectives and returns the corresponding plural form. Adjectives that are correctly handled include: "numerical" adjectives ("a" -> "some"), demonstrative adjectives ("this" -> "these", "that" -> "those"), and possessives ("my" -> "our", "cat's" -> "cats'", "child's" -> "childrens'", etc.) ``plural(word, count=None)`` The method ``plural()`` takes a *singular* English noun, pronoun, verb, or adjective and returns its plural form. Where a word has more than one inflection depending on its part of speech (for example, the noun "thought" inflects to "thoughts", the verb "thought" to "thought"), the (singular) noun sense is preferred to the (singular) verb sense. Hence ``plural("knife")`` will return "knives" ("knife" having been treated as a singular noun), whereas ``plural("knifes")`` will return "knife" ("knifes" having been treated as a 3rd person singular verb). The inherent ambiguity of such cases suggests that, where the part of speech is known, ``plural_noun``, ``plural_verb``, and ``plural_adj`` should be used in preference to ``plural``. ``singular_noun(word, count=None)`` The method ``singular_noun()`` takes a *plural* English noun or pronoun and returns its singular. Pronouns in the nominative ("we" -> "I") and accusative ("us" -> "me") cases are handled, as are possessive pronouns ("ours" -> "mine"). When third person singular pronouns are returned they take the neuter gender by default ("they" -> "it"), not ("they"-> "she") nor ("they" -> "he"). This can be changed with ``gender()``. Note that all these methods ignore any whitespace surrounding the word being inflected, but preserve that whitespace when the result is returned. For example, ``plural(" cat ")`` returns " cats ". ``gender(genderletter)`` The third person plural pronoun takes the same form for the female, male and neuter (e.g. "they"). The singular however, depends upon gender (e.g. "she", "he", "it" and "they" -- "they" being the gender neutral form.) By default ``singular_noun`` returns the neuter form, however, the gender can be selected with the ``gender`` method. Pass the first letter of the gender to ``gender`` to return the f(eminine), m(asculine), n(euter) or t(hey) form of the singular. e.g. gender('f') followed by singular_noun('themselves') returns 'herself'. Numbered plurals ---------------- The ``plural...`` methods return only the inflected word, not the count that was used to inflect it. Thus, in order to produce "I saw 3 ducks", it is necessary to use: .. code-block:: python print("I saw", N, p.plural_noun(animal, N)) Since the usual purpose of producing a plural is to make it agree with a preceding count, inflect.py provides a method (``no(word, count)``) which, given a word and a(n optional) count, returns the count followed by the correctly inflected word. Hence the previous example can be rewritten: .. code-block:: python print("I saw ", p.no(animal, N)) In addition, if the count is zero (or some other term which implies zero, such as ``"zero"``, ``"nil"``, etc.) the count is replaced by the word "no". Hence, if ``N`` had the value zero, the previous example would print (the somewhat more elegant):: I saw no animals rather than:: I saw 0 animals Note that the name of the method is a pun: the method returns either a number (a *No.*) or a ``"no"``, in front of the inflected word. Reducing the number of counts required -------------------------------------- In some contexts, the need to supply an explicit count to the various ``plural...`` methods makes for tiresome repetition. For example: .. code-block:: python print( plural_adj("This", errors), plural_noun(" error", errors), plural_verb(" was", errors), " fatal.", ) inflect.py therefore provides a method (``num(count=None, show=None)``) which may be used to set a persistent "default number" value. If such a value is set, it is subsequently used whenever an optional second "number" argument is omitted. The default value thus set can subsequently be removed by calling ``num()`` with no arguments. Hence we could rewrite the previous example: .. code-block:: python p.num(errors) print(p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal.") p.num() Normally, ``num()`` returns its first argument, so that it may also be "inlined" in contexts like: .. code-block:: python print(p.num(errors), p.plural_noun(" error"), p.plural_verb(" was"), " detected.") if severity > 1: print( p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal." ) However, in certain contexts (see `INTERPOLATING INFLECTIONS IN STRINGS`) it is preferable that ``num()`` return an empty string. Hence ``num()`` provides an optional second argument. If that argument is supplied (that is, if it is defined) and evaluates to false, ``num`` returns an empty string instead of its first argument. For example: .. code-block:: python print(p.num(errors, 0), p.no("error"), p.plural_verb(" was"), " detected.") if severity > 1: print( p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal." ) Number-insensitive equality --------------------------- inflect.py also provides a solution to the problem of comparing words of differing plurality through the methods ``compare(word1, word2)``, ``compare_nouns(word1, word2)``, ``compare_verbs(word1, word2)``, and ``compare_adjs(word1, word2)``. Each of these methods takes two strings, and compares them using the corresponding plural-inflection method (``plural()``, ``plural_noun()``, ``plural_verb()``, and ``plural_adj()`` respectively). The comparison returns true if: - the strings are equal, or - one string is equal to a plural form of the other, or - the strings are two different plural forms of the one word. Hence all of the following return true: .. code-block:: python p.compare("index", "index") # RETURNS "eq" p.compare("index", "indexes") # RETURNS "s:p" p.compare("index", "indices") # RETURNS "s:p" p.compare("indexes", "index") # RETURNS "p:s" p.compare("indices", "index") # RETURNS "p:s" p.compare("indices", "indexes") # RETURNS "p:p" p.compare("indexes", "indices") # RETURNS "p:p" p.compare("indices", "indices") # RETURNS "eq" As indicated by the comments in the previous example, the actual value returned by the various ``compare`` methods encodes which of the three equality rules succeeded: "eq" is returned if the strings were identical, "s:p" if the strings were singular and plural respectively, "p:s" for plural and singular, and "p:p" for two distinct plurals. Inequality is indicated by returning an empty string. It should be noted that two distinct singular words which happen to take the same plural form are *not* considered equal, nor are cases where one (singular) word's plural is the other (plural) word's singular. Hence all of the following return false: .. code-block:: python p.compare("base", "basis") # ALTHOUGH BOTH -> "bases" p.compare("syrinx", "syringe") # ALTHOUGH BOTH -> "syringes" p.compare("she", "he") # ALTHOUGH BOTH -> "they" p.compare("opus", "operas") # ALTHOUGH "opus" -> "opera" -> "operas" p.compare("taxi", "taxes") # ALTHOUGH "taxi" -> "taxis" -> "taxes" Note too that, although the comparison is "number-insensitive" it is *not* case-insensitive (that is, ``plural("time","Times")`` returns false. To obtain both number and case insensitivity, use the ``lower()`` method on both strings (that is, ``plural("time".lower(), "Times".lower())`` returns true). Related Functionality ===================== Shout out to these libraries that provide related functionality: * `WordSet `_ parses identifiers like variable names into sets of words suitable for re-assembling in another form. * `word2number `_ converts words to a number. For Enterprise ============== Available as part of the Tidelift Subscription. 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. `Learn more `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878424.0 inflect-7.3.1/inflect.egg-info/SOURCES.txt0000644000175100001770000000163114640641430017560 0ustar00runnerdocker.coveragerc .editorconfig .gitignore .pre-commit-config.yaml .readthedocs.yaml LICENSE NEWS.rst README.rst SECURITY.md mypy.ini pyproject.toml pytest.ini ruff.toml tea.yaml towncrier.toml tox.ini .github/FUNDING.yml .github/dependabot.yml .github/workflows/main.yml docs/conf.py docs/history.rst docs/index.rst inflect/__init__.py inflect/py.typed inflect.egg-info/PKG-INFO inflect.egg-info/SOURCES.txt inflect.egg-info/dependency_links.txt inflect.egg-info/requires.txt inflect.egg-info/top_level.txt inflect/compat/__init__.py inflect/compat/py38.py tests/inflections.txt tests/test_an.py tests/test_classical_all.py tests/test_classical_ancient.py tests/test_classical_herd.py tests/test_classical_names.py tests/test_classical_person.py tests/test_classical_zero.py tests/test_compounds.py tests/test_inflections.py tests/test_join.py tests/test_numwords.py tests/test_pl_si.py tests/test_pwd.py tests/test_unicode.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878424.0 inflect-7.3.1/inflect.egg-info/dependency_links.txt0000644000175100001770000000000114640641430021741 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878424.0 inflect-7.3.1/inflect.egg-info/requires.txt0000644000175100001770000000045214640641430020274 0ustar00runnerdockermore_itertools>=8.5.0 typeguard>=4.0.1 [:python_version < "3.9"] typing_extensions [doc] sphinx>=3.5 jaraco.packaging>=9.3 rst.linker>=1.9 furo sphinx-lint jaraco.tidelift>=1.4 [test] pytest!=8.1.*,>=6 pytest-checkdocs>=2.4 pytest-cov pytest-mypy pytest-enabler>=2.2 pytest-ruff>=0.2.1 pygments ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878424.0 inflect-7.3.1/inflect.egg-info/top_level.txt0000644000175100001770000000001014640641430020414 0ustar00runnerdockerinflect ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/mypy.ini0000644000175100001770000000023214640641406014254 0ustar00runnerdocker[mypy] ignore_missing_imports = True # required to support namespace packages # https://github.com/python/mypy/issues/14057 explicit_package_bases = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/pyproject.toml0000644000175100001770000000276414640641406015505 0ustar00runnerdocker[build-system] requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" [project] name = "inflect" authors = [ { name = "Paul Dyson", email = "pwdyson@yahoo.com" }, ] maintainers = [ { name = "Jason R. Coombs", email = "jaraco@jaraco.com" }, ] description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles" # convert numbers to words readme = "README.rst" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Natural Language :: English", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Text Processing :: Linguistic", ] requires-python = ">=3.8" dependencies = [ "more_itertools >= 8.5.0", "typeguard >= 4.0.1", "typing_extensions ; python_version<'3.9'", ] keywords = [ "plural", "inflect", "participle", ] dynamic = ["version"] [project.urls] Source = "https://github.com/jaraco/inflect" [project.optional-dependencies] test = [ # upstream "pytest >= 6, != 8.1.*", "pytest-checkdocs >= 2.4", "pytest-cov", "pytest-mypy", "pytest-enabler >= 2.2", "pytest-ruff >= 0.2.1", # local "pygments", ] doc = [ # upstream "sphinx >= 3.5", "jaraco.packaging >= 9.3", "rst.linker >= 1.9", "furo", "sphinx-lint", # tidelift "jaraco.tidelift >= 1.4", # local ] [tool.setuptools_scm] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/pytest.ini0000644000175100001770000000111014640641406014602 0ustar00runnerdocker[pytest] norecursedirs=dist build .tox .eggs addopts= --doctest-modules --import-mode importlib consider_namespace_packages=true filterwarnings= ## upstream # Ensure ResourceWarnings are emitted default::ResourceWarning # realpython/pytest-mypy#152 ignore:'encoding' argument not specified::pytest_mypy # python/cpython#100750 ignore:'encoding' argument not specified::platform # pypa/build#615 ignore:'encoding' argument not specified::build.env # dateutil/dateutil#1284 ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz ## end upstream ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/ruff.toml0000644000175100001770000000064314640641406014422 0ustar00runnerdocker[lint] extend-select = [ "C901", "W", ] ignore = [ # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", "E114", "E117", "D206", "D300", "Q000", "Q001", "Q002", "Q003", "COM812", "COM819", "ISC001", "ISC002", ] [format] # Enable preview, required for quote-style = "preserve" preview = true # https://docs.astral.sh/ruff/settings/#format-quote-style quote-style = "preserve" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1719878424.263477 inflect-7.3.1/setup.cfg0000644000175100001770000000004614640641430014376 0ustar00runnerdocker[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tea.yaml0000644000175100001770000000017614640641406014221 0ustar00runnerdocker# https://tea.xyz/what-is-this-file --- version: 1.0.0 codeOwners: - '0x32392EaEA1FDE87733bEEc3b184C9006501c4A82' quorum: 1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719878424.2594771 inflect-7.3.1/tests/0000755000175100001770000000000014640641430013717 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/inflections.txt0000644000175100001770000010640514640641406017006 0ustar00runnerdocker a -> as # NOUN FORM TODO:sing a -> some # INDEFINITE ARTICLE TODO: A.C.R.O.N.Y.M. -> A.C.R.O.N.Y.M.s abscissa -> abscissas|abscissae accomplice -> accomplices Achinese -> Achinese acropolis -> acropolises adieu -> adieus|adieux adjutant general -> adjutant generals aegis -> aegises afflatus -> afflatuses afreet -> afreets|afreeti afrit -> afrits|afriti agendum -> agenda aide-de-camp -> aides-de-camp Alabaman -> Alabamans albino -> albinos album -> albums Alfurese -> Alfurese alga -> algae alias -> aliases alto -> altos|alti alumna -> alumnae alumnus -> alumni alveolus -> alveoli TODO:siverb am -> are TODO:siverb am going -> are going ambassador-at-large -> ambassadors-at-large Amboinese -> Amboinese Americanese -> Americanese amoeba -> amoebas|amoebae Amoyese -> Amoyese TODO:siadj an -> some # INDEFINITE ARTICLE analysis -> analyses anathema -> anathemas|anathemata Andamanese -> Andamanese Angolese -> Angolese Annamese -> Annamese antenna -> antennas|antennae anus -> anuses apex -> apexes|apices TODO:siadj apex's -> apexes'|apices' # POSSESSIVE FORM aphelion -> aphelia apparatus -> apparatuses|apparatus appendix -> appendixes|appendices apple -> apples aquarium -> aquariums|aquaria Aragonese -> Aragonese Arakanese -> Arakanese archipelago -> archipelagos TODO:siverb are -> are TODO:siverb are made -> are made armadillo -> armadillos arpeggio -> arpeggios arthritis -> arthritises|arthritides asbestos -> asbestoses asparagus -> asparaguses ass -> asses Assamese -> Assamese asylum -> asylums asyndeton -> asyndeta at it -> at them # ACCUSATIVE ataman -> atamans TODO:siverb ate -> ate atlas -> atlases|atlantes atman -> atmas TODO:singular_noun attorney general -> attorneys general attorney of record -> attorneys of record aurora -> auroras|aurorae auto -> autos auto-da-fe -> autos-da-fe aviatrix -> aviatrixes|aviatrices TODO:siadj aviatrix's -> aviatrixes'|aviatrices' Avignonese -> Avignonese axe -> axes TODO:singular_noun 2 anwers! axis -> axes axman -> axmen Azerbaijanese -> Azerbaijanese bacillus -> bacilli bacterium -> bacteria Bahaman -> Bahamans Balinese -> Balinese bamboo -> bamboos banjo -> banjoes bass -> basses # INSTRUMENT, NOT FISH basso -> bassos|bassi bathos -> bathoses beau -> beaus|beaux beef -> beefs|beeves beneath it -> beneath them # ACCUSATIVE Bengalese -> Bengalese bent -> bent # VERB FORM bent -> bents # NOUN FORM Bernese -> Bernese Bhutanese -> Bhutanese bias -> biases biceps -> biceps bison -> bisons|bison black olive -> black olives blouse -> blouses Bolognese -> Bolognese bonus -> bonuses Borghese -> Borghese boss -> bosses Bostonese -> Bostonese box -> boxes boy -> boys bravo -> bravoes bream -> bream breeches -> breeches bride-to-be -> brides-to-be Brigadier General -> Brigadier Generals britches -> britches bronchitis -> bronchitises|bronchitides bronchus -> bronchi brother -> brothers|brethren TODO: brother's -> brothers'|brethren's buffalo -> buffaloes|buffalo Buginese -> Buginese buoy -> buoys bureau -> bureaus|bureaux Burman -> Burmans Burmese -> Burmese bursitis -> bursitises|bursitides bus -> buses butter -> butter buzz -> buzzes buzzes -> buzz # VERB FORM by it -> by them # ACCUSATIVE caddis -> caddises caiman -> caimans cake -> cakes Calabrese -> Calabrese calf -> calves callus -> calluses Camaldolese -> Camaldolese cameo -> cameos campus -> campuses can -> cans # NOUN FORM can -> can # VERB FORM (all pers.) can't -> can't # VERB FORM candelabrum -> candelabra cannabis -> cannabises TODO:siverb canoes -> canoe canto -> cantos Cantonese -> Cantonese cantus -> cantus canvas -> canvases CAPITAL -> CAPITALS carcinoma -> carcinomas|carcinomata care -> cares cargo -> cargoes caribou -> caribous|caribou Carlylese -> Carlylese carmen -> carmina carp -> carp cash -> cash Cassinese -> Cassinese cat -> cats catfish -> catfish cattle -> cattles|cattle cayman -> caymans Celanese -> Celanese ceriman -> cerimans cervid -> cervids Ceylonese -> Ceylonese chairman -> chairmen chamois -> chamois chaos -> chaoses chapeau -> chapeaus|chapeaux charisma -> charismas|charismata TODO:siverb chases -> chase chassis -> chassis chateau -> chateaus|chateaux cherub -> cherubs|cherubim chickenpox -> chickenpox chief -> chiefs child -> children chili -> chilis|chilies Chinese -> Chinese oatmeal cookie -> oatmeal cookies chorus -> choruses chrysalis -> chrysalises|chrysalides church -> churches cicatrix -> cicatrixes|cicatrices circus -> circuses class -> classes classes -> class # VERB FORM clippers -> clippers clitoris -> clitorises|clitorides cod -> cod codex -> codices coitus -> coitus commando -> commandos compendium -> compendiums|compendia coney -> coneys Congoese -> Congoese Congolese -> Congolese conspectus -> conspectuses contralto -> contraltos|contralti contretemps -> contretemps conundrum -> conundrums corps -> corps corpus -> corpuses|corpora cortex -> cortexes|cortices cosmos -> cosmoses TODO:singular_noun court martial -> courts martial cow -> cows|kine cranium -> craniums|crania crescendo -> crescendos criterion -> criteria curriculum -> curriculums|curricula czech -> czechs dais -> daises data point -> data points datum -> data debris -> debris decorum -> decorums deer -> deer delphinium -> delphiniums desideratum -> desiderata desman -> desmans diabetes -> diabetes dictum -> dictums|dicta TODO:siverb did -> did TODO:siverb did need -> did need digitalis -> digitalises dingo -> dingoes diploma -> diplomas|diplomata discus -> discuses dish -> dishes ditto -> dittos djinn -> djinn TODO:siverb does -> do TODO:siverb doesn't -> don't # VERB FORM dog -> dogs dogma -> dogmas|dogmata dolman -> dolmans dominatrix -> dominatrixes|dominatrices domino -> dominoes Dongolese -> Dongolese dormouse -> dormice drama -> dramas|dramata drum -> drums dwarf -> dwarves dynamo -> dynamos edema -> edemas|edemata eland -> elands|eland elf -> elves elk -> elks|elk embryo -> embryos emporium -> emporiums|emporia encephalitis -> encephalitises|encephalitides encomium -> encomiums|encomia enema -> enemas|enemata enigma -> enigmas|enigmata epidermis -> epidermises epididymis -> epididymises|epididymides erratum -> errata ethos -> ethoses eucalyptus -> eucalyptuses eunuch -> eunuchs extremum -> extrema eyas -> eyases factotum -> factotums farman -> farmans Faroese -> Faroese fauna -> faunas|faunae fax -> faxes Ferrarese -> Ferrarese ferry -> ferries fetus -> fetuses fiance -> fiances fiancee -> fiancees fiasco -> fiascos fish -> fish fizz -> fizzes flamingo -> flamingoes flittermouse -> flittermice TODO:siverb floes -> floe flora -> floras|florae flounder -> flounder focus -> focuses|foci foetus -> foetuses folio -> folios Foochowese -> Foochowese foot -> feet TODO:siadj foot's -> feet's # POSSESSIVE FORM foramen -> foramens|foramina TODO:siverb foreshoes -> foreshoe formula -> formulas|formulae forum -> forums TODO:siverb fought -> fought fox -> foxes TODO:singular_noun 2 different returns from him -> from them from it -> from them # ACCUSATIVE fungus -> funguses|fungi Gabunese -> Gabunese gallows -> gallows ganglion -> ganglions|ganglia gas -> gases gateau -> gateaus|gateaux TODO:siverb gave -> gave general -> generals generalissimo -> generalissimos Genevese -> Genevese genie -> genies|genii TODO:singular_noun 2 diff return values! genius -> geniuses|genii Genoese -> Genoese genus -> genera German -> Germans ghetto -> ghettos Gilbertese -> Gilbertese glottis -> glottises Goanese -> Goanese goat -> goats goose -> geese TODO:singular_noun Governor General -> Governors General goy -> goys|goyim graffiti -> graffiti TODO:singular_noun 2 diff ret values graffito -> graffiti grizzly -> grizzlies guano -> guanos guardsman -> guardsmen Guianese -> Guianese gumma -> gummas|gummata TODO:siverb gumshoes -> gumshoe gunman -> gunmen gymnasium -> gymnasiums|gymnasia TODO:siverb had -> had TODO:siverb had thought -> had thought Hainanese -> Hainanese TODO:siverb hammertoes -> hammertoe handkerchief -> handkerchiefs Hararese -> Hararese Harlemese -> Harlemese harman -> harmans harmonium -> harmoniums TODO:siverb has -> have TODO:siverb has become -> have become TODO:siverb has been -> have been TODO:siverb has-been -> has-beens hasn't -> haven't # VERB FORM Havanese -> Havanese TODO:siverb have -> have TODO:siverb have conceded -> have conceded TODO:singular_noun 2 values he -> they headquarters -> headquarters Heavenese -> Heavenese helix -> helices hepatitis -> hepatitises|hepatitides TODO:singular_noun 2 values her -> them # PRONOUN TODO:singular_noun 2 values her -> their # POSSESSIVE ADJ hero -> heroes herpes -> herpes TODO:singular_noun 2 values hers -> theirs # POSSESSIVE NOUN TODO:singular_noun 2 values herself -> themselves hetman -> hetmans hiatus -> hiatuses|hiatus highlight -> highlights hijinks -> hijinks TODO:singular_noun 2 values him -> them TODO:singular_noun 2 values himself -> themselves hippopotamus -> hippopotamuses|hippopotami Hiroshiman -> Hiroshimans TODO:singular_noun 2 values his -> their # POSSESSIVE ADJ TODO:singular_noun 2 values his -> theirs # POSSESSIVE NOUN TODO:siverb hoes -> hoe honorarium -> honorariums|honoraria hoof -> hoofs|hooves Hoosierese -> Hoosierese TODO:siverb horseshoes -> horseshoe Hottentotese -> Hottentotese house -> houses housewife -> housewives hubris -> hubrises human -> humans Hunanese -> Hunanese hydra -> hydras|hydrae hyperbaton -> hyperbata hyperbola -> hyperbolas|hyperbolae I -> we ibis -> ibises ignoramus -> ignoramuses impetus -> impetuses|impetus incubus -> incubuses|incubi index -> indexes|indices Indochinese -> Indochinese inferno -> infernos infinity -> infinities|infinity information -> information innings -> innings TODO:singular_noun Inspector General -> Inspectors General interregnum -> interregnums|interregna iris -> irises|irides TODO:siverb is -> are TODO:siverb is eaten -> are eaten isn't -> aren't # VERB FORM it -> they # NOMINATIVE TODO:siadj its -> their # POSSESSIVE FORM itself -> themselves jackanapes -> jackanapes Japanese -> Japanese Javanese -> Javanese Jerry -> Jerrys jerry -> jerries jinx -> jinxes jinxes -> jinx # VERB FORM Johnsonese -> Johnsonese Jones -> Joneses jumbo -> jumbos Kanarese -> Kanarese Kiplingese -> Kiplingese knife -> knives # NOUN FORM knife -> knife # VERB FORM (1st/2nd pers.) knifes -> knife # VERB FORM (3rd pers.) Kongoese -> Kongoese Kongolese -> Kongolese lacuna -> lacunas|lacunae lady in waiting -> ladies in waiting Lapponese -> Lapponese larynx -> larynxes|larynges latex -> latexes|latices lawman -> lawmen layman -> laymen leaf -> leaves # NOUN FORM leaf -> leaf # VERB FORM (1st/2nd pers.) leafs -> leaf # VERB FORM (3rd pers.) Lebanese -> Lebanese leman -> lemans lemma -> lemmas|lemmata lens -> lenses Leonese -> Leonese lick of the cat -> licks of the cat Lieutenant General -> Lieutenant Generals lie -> lies life -> lives Liman -> Limans lingo -> lingos loaf -> loaves locus -> loci Londonese -> Londonese lore -> lores|lore Lorrainese -> Lorrainese lothario -> lotharios louse -> lice Lucchese -> Lucchese lumbago -> lumbagos lumen -> lumens|lumina lummox -> lummoxes lustrum -> lustrums|lustra lyceum -> lyceums lymphoma -> lymphomas|lymphomata lynx -> lynxes Lyonese -> Lyonese TODO: M.I.A. -> M.I.A.s Macanese -> Macanese Macassarese -> Macassarese mackerel -> mackerel macro -> macros TODO:siverb made -> made madman -> madmen Madurese -> Madurese magma -> magmas|magmata magneto -> magnetos Major General -> Major Generals Malabarese -> Malabarese Maltese -> Maltese man -> men mandamus -> mandamuses manifesto -> manifestos mantis -> mantises marquis -> marquises Mary -> Marys maximum -> maximums|maxima measles -> measles medico -> medicos medium -> mediums|media TODO:siadj medium's -> mediums'|media's medusa -> medusas|medusae memorandum -> memorandums|memoranda meniscus -> menisci merman -> mermen Messinese -> Messinese metamorphosis -> metamorphoses metropolis -> metropolises mews -> mews miasma -> miasmas|miasmata Milanese -> Milanese milieu -> milieus|milieux millennium -> millenniums|millennia minimum -> minimums|minima minx -> minxes miss -> miss # VERB FORM (1st/2nd pers.) miss -> misses # NOUN FORM misses -> miss # VERB FORM (3rd pers.) TODO:siverb mistletoes -> mistletoe mittamus -> mittamuses Modenese -> Modenese momentum -> momentums|momenta money -> monies mongoose -> mongooses moose -> moose mother-in-law -> mothers-in-law mouse -> mice mumps -> mumps Muranese -> Muranese murex -> murices museum -> museums mustachio -> mustachios TODO:siadj my -> our # POSSESSIVE FORM myself -> ourselves mythos -> mythoi Nakayaman -> Nakayamans Nankingese -> Nankingese nasturtium -> nasturtiums Navarrese -> Navarrese nebula -> nebulas|nebulae Nepalese -> Nepalese neuritis -> neuritises|neuritides neurosis -> neuroses news -> news nexus -> nexus Niasese -> Niasese Nicobarese -> Nicobarese nimbus -> nimbuses|nimbi Nipponese -> Nipponese no -> noes Norman -> Normans nostrum -> nostrums noumenon -> noumena nova -> novas|novae nucleolus -> nucleoluses|nucleoli nucleus -> nuclei numen -> numina oaf -> oafs TODO:siverb oboes -> oboe occiput -> occiputs|occipita octavo -> octavos octopus -> octopuses|octopodes oedema -> oedemas|oedemata Oklahoman -> Oklahomans omnibus -> omnibuses on it -> on them # ACCUSATIVE onus -> onuses opera -> operas optimum -> optimums|optima opus -> opuses|opera organon -> organa ottoman -> ottomans ought to be -> ought to be # VERB (UNLIKE bride to be) TODO:siverb overshoes -> overshoe TODO:siverb overtoes -> overtoe ovum -> ova ox -> oxen TODO:siadj ox's -> oxen's # POSSESSIVE FORM oxman -> oxmen oxymoron -> oxymorons|oxymora Panaman -> Panamans parabola -> parabolas|parabolae Parmese -> Parmese pathos -> pathoses pegasus -> pegasuses Pekingese -> Pekingese pelvis -> pelvises pendulum -> pendulums penis -> penises|penes penumbra -> penumbras|penumbrae perihelion -> perihelia person -> people|persons persona -> personae petroleum -> petroleums phalanx -> phalanxes|phalanges PhD -> PhDs phenomenon -> phenomena philtrum -> philtrums photo -> photos phylum -> phylums|phyla piano -> pianos|piani Piedmontese -> Piedmontese pika -> pikas TODO:singular_noun ret mul value pincer -> pincers pincers -> pincers Pistoiese -> Pistoiese plateau -> plateaus|plateaux play -> plays plexus -> plexuses|plexus pliers -> pliers plies -> ply # VERB FORM polis -> polises Polonese -> Polonese pontifex -> pontifexes|pontifices portmanteau -> portmanteaus|portmanteaux Portuguese -> Portuguese possum -> possums potato -> potatoes pox -> pox pragma -> pragmas|pragmata premium -> premiums prima donna -> prima donnas|prime donne pro -> pros proceedings -> proceedings prolegomenon -> prolegomena proof -> proofs proof of concept -> proofs of concept prosecutrix -> prosecutrixes|prosecutrices prospectus -> prospectuses|prospectus protozoan -> protozoans protozoon -> protozoa puma -> pumas TODO:siverb put -> put quantum -> quantums|quanta TODO:singular_noun quartermaster general -> quartermasters general quarto -> quartos quiz -> quizzes quizzes -> quiz # VERB FORM quorum -> quorums rabies -> rabies radius -> radiuses|radii radix -> radices ragman -> ragmen rebus -> rebuses TODO:siverb rehoes -> rehoe reindeer -> reindeer repo -> repos TODO:siverb reshoes -> reshoe rhino -> rhinos rhinoceros -> rhinoceroses|rhinoceros TODO:siverb roes -> roe Rom -> Roma Romagnese -> Romagnese Roman -> Romans Romanese -> Romanese Romany -> Romanies romeo -> romeos roof -> roofs rostrum -> rostrums|rostra ruckus -> ruckuses salmon -> salmon Sangirese -> Sangirese TODO: siverb sank -> sank Sarawakese -> Sarawakese sarcoma -> sarcomas|sarcomata sassafras -> sassafrases saw -> saw # VERB FORM (1st/2nd pers.) saw -> saws # NOUN FORM saws -> saw # VERB FORM (3rd pers.) scarf -> scarves schema -> schemas|schemata scissors -> scissors pair of scissors -> pairs of scissors pair of slippers -> pairs of slippers Scotsman -> Scotsmen sea-bass -> sea-bass seaman -> seamen self -> selves Selman -> Selmans Senegalese -> Senegalese seraph -> seraphs|seraphim series -> series TODO:siverb shall eat -> shall eat shaman -> shamans Shavese -> Shavese Shawanese -> Shawanese TODO:singular_noun multivalue she -> they sheaf -> sheaves shears -> shears sheep -> sheep shelf -> shelves TODO:siverb shoes -> shoe TODO:siverb should have -> should have Siamese -> Siamese siemens -> siemens Sienese -> Sienese Sikkimese -> Sikkimese silex -> silices simplex -> simplexes|simplices Singhalese -> Singhalese Sinhalese -> Sinhalese sinus -> sinuses|sinus size -> sizes sizes -> size #VERB FORM slice -> slices smallpox -> smallpox Smith -> Smiths TODO:siverb snowshoes -> snowshoe Sogdianese -> Sogdianese soliloquy -> soliloquies solo -> solos|soli soma -> somas|somata TODO:singular_noun tough son of a bitch -> sons of bitches Sonaman -> Sonamans soprano -> sopranos|soprani TODO:siverb sought -> sought TODO:siverb spattlehoes -> spattlehoe species -> species spectrum -> spectrums|spectra speculum -> speculums|specula TODO:siverb spent -> spent spermatozoon -> spermatozoa sphinx -> sphinxes|sphinges spokesperson -> spokespeople|spokespersons stadium -> stadiums|stadia stamen -> stamens|stamina status -> statuses|status stereo -> stereos stigma -> stigmas|stigmata stimulus -> stimuli stoma -> stomas|stomata stomach -> stomachs storey -> storeys story -> stories stratum -> strata strife -> strifes stylo -> stylos stylus -> styluses|styli succubus -> succubuses|succubi Sudanese -> Sudanese suffix -> suffixes Sundanese -> Sundanese superior -> superiors supply -> supplies TODO:singular_noun Surgeon-General -> Surgeons-General surplus -> surpluses Swahilese -> Swahilese swine -> swines|swine TODO:singular_noun multiple return syringe -> syringes syrinx -> syrinxes|syringes tableau -> tableaus|tableaux taco -> tacos Tacoman -> Tacomans talouse -> talouses tattoo -> tattoos taxman -> taxmen tempo -> tempos|tempi Tenggerese -> Tenggerese testatrix -> testatrixes|testatrices testes -> testes TODO:singular_noun multiple return testis -> testes TODO:siadj that -> those TODO:siadj their -> their # POSSESSIVE FORM (GENDER-INCLUSIVE) TODO:singular_noun multiple return themself -> themselves # ugly but gaining currency TODO:singular_noun multiple return they -> they # for indeterminate gender thief -> thiefs|thieves TODO:siadj this -> these thought -> thoughts # NOUN FORM thought -> thought # VERB FORM TODO:siverb throes -> throe TODO:siverb ticktacktoes -> ticktacktoe Times -> Timeses Timorese -> Timorese TODO:siverb tiptoes -> tiptoe Tirolese -> Tirolese titmouse -> titmice TODO:singular_noun multivalue to her -> to them TODO:singular_noun multivalue to herself -> to themselves TODO:singular_noun multivalue to him -> to them TODO:singular_noun multivalue to himself -> to themselves to it -> to them to it -> to them # ACCUSATIVE to itself -> to themselves to me -> to us to myself -> to ourselves TODO:singular_noun multivalue to them -> to them # for indeterminate gender TODO:singular_noun multivalue to themself -> to themselves # ugly but gaining currency to you -> to you to yourself -> to yourselves Tocharese -> Tocharese TODO:siverb toes -> toe tomato -> tomatoes Tonkinese -> Tonkinese tonsillitis -> tonsillitises|tonsillitides tooth -> teeth Torinese -> Torinese torus -> toruses|tori trapezium -> trapeziums|trapezia trauma -> traumas|traumata travois -> travois tranche -> tranches trellis -> trellises TODO:siverb tries -> try trilby -> trilbys trousers -> trousers trousseau -> trousseaus|trousseaux trout -> trout TODO:siverb try -> tries tuna -> tuna turf -> turfs|turves Tyrolese -> Tyrolese ultimatum -> ultimatums|ultimata umbilicus -> umbilicuses|umbilici umbra -> umbras|umbrae TODO:siverb undershoes -> undershoe TODO:siverb unshoes -> unshoe uterus -> uteruses|uteri vacuum -> vacuums|vacua vellum -> vellums velum -> velums|vela Vermontese -> Vermontese Veronese -> Veronese vertebra -> vertebrae vertex -> vertexes|vertices Viennese -> Viennese Vietnamese -> Vietnamese virtuoso -> virtuosos|virtuosi virus -> viruses vita -> vitae vixen -> vixens vortex -> vortexes|vortices walrus -> walruses TODO:siverb was -> were TODO:siverb was faced with -> were faced with TODO:siverb was hoping -> were hoping Wenchowese -> Wenchowese TODO:siverb were -> were TODO:siverb were found -> were found wharf -> wharves whiting -> whiting Whitmanese -> Whitmanese whiz -> whizzes TODO:singular_noun multivalue whizz -> whizzes widget -> widgets wife -> wives wildebeest -> wildebeests|wildebeest will -> will # VERB FORM will -> wills # NOUN FORM will eat -> will eat # VERB FORM wills -> will # VERB FORM wish -> wishes TODO:singular_noun multivalue with him -> with them with it -> with them # ACCUSATIVE TODO:siverb woes -> woe wolf -> wolves woman -> women woman of substance -> women of substance TODO:siadj woman's -> women's # POSSESSIVE FORM won't -> won't # VERB FORM woodlouse -> woodlice Yakiman -> Yakimans Yengeese -> Yengeese yeoman -> yeomen yeowoman -> yeowomen yes -> yeses Yokohaman -> Yokohamans you -> you TODO:siadj your -> your # POSSESSIVE FORM yourself -> yourselves Yuman -> Yumans Yunnanese -> Yunnanese zero -> zeros zoon -> zoa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_an.py0000644000175100001770000000177214640641406015740 0ustar00runnerdockerimport inflect def test_an(): p = inflect.engine() assert p.an("cat") == "a cat" assert p.an("ant") == "an ant" assert p.an("a") == "an a" assert p.an("b") == "a b" assert p.an("honest cat") == "an honest cat" assert p.an("dishonest cat") == "a dishonest cat" assert p.an("Honolulu sunset") == "a Honolulu sunset" assert p.an("mpeg") == "an mpeg" assert p.an("onetime holiday") == "a onetime holiday" assert p.an("Ugandan person") == "a Ugandan person" assert p.an("Ukrainian person") == "a Ukrainian person" assert p.an("Unabomber") == "a Unabomber" assert p.an("unanimous decision") == "a unanimous decision" assert p.an("US farmer") == "a US farmer" assert p.an("wild PIKACHU appeared") == "a wild PIKACHU appeared" def test_an_abbreviation(): p = inflect.engine() assert p.an("YAML code block") == "a YAML code block" assert p.an("Core ML function") == "a Core ML function" assert p.an("JSON code block") == "a JSON code block" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_classical_all.py0000644000175100001770000000452214640641406020124 0ustar00runnerdockerimport inflect class Test: def test_classical(self): p = inflect.engine() # DEFAULT... assert p.plural_noun("error", 0) == "errors", "classical 'zero' not active" assert ( p.plural_noun("wildebeest") == "wildebeests" ), "classical 'herd' not active" assert p.plural_noun("Sally") == "Sallys", "classical 'names' active" assert p.plural_noun("brother") == "brothers", "classical others not active" assert p.plural_noun("person") == "people", "classical 'persons' not active" assert p.plural_noun("formula") == "formulas", "classical 'ancient' not active" # CLASSICAL PLURALS ACTIVATED... p.classical(all=True) assert p.plural_noun("error", 0) == "error", "classical 'zero' active" assert p.plural_noun("wildebeest") == "wildebeest", "classical 'herd' active" assert p.plural_noun("Sally") == "Sallys", "classical 'names' active" assert p.plural_noun("brother") == "brethren", "classical others active" assert p.plural_noun("person") == "persons", "classical 'persons' active" assert p.plural_noun("formula") == "formulae", "classical 'ancient' active" # CLASSICAL PLURALS DEACTIVATED... p.classical(all=False) assert p.plural_noun("error", 0) == "errors", "classical 'zero' not active" assert ( p.plural_noun("wildebeest") == "wildebeests" ), "classical 'herd' not active" assert p.plural_noun("Sally") == "Sallies", "classical 'names' not active" assert p.plural_noun("brother") == "brothers", "classical others not active" assert p.plural_noun("person") == "people", "classical 'persons' not active" assert p.plural_noun("formula") == "formulas", "classical 'ancient' not active" # CLASSICAL PLURALS REREREACTIVATED... p.classical() assert p.plural_noun("error", 0) == "error", "classical 'zero' active" assert p.plural_noun("wildebeest") == "wildebeest", "classical 'herd' active" assert p.plural_noun("Sally") == "Sallys", "classical 'names' active" assert p.plural_noun("brother") == "brethren", "classical others active" assert p.plural_noun("person") == "persons", "classical 'persons' active" assert p.plural_noun("formula") == "formulae", "classical 'ancient' active" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_classical_ancient.py0000644000175100001770000000103514640641406020771 0ustar00runnerdockerimport inflect def test_ancient_1(): p = inflect.engine() # DEFAULT... assert p.plural_noun("formula") == "formulas" # "person" PLURALS ACTIVATED... p.classical(ancient=True) assert p.plural_noun("formula") == "formulae" # OTHER CLASSICALS NOT ACTIVATED... assert p.plural_noun("wildebeest") == "wildebeests" assert p.plural_noun("error", 0) == "errors" assert p.plural_noun("Sally") == "Sallys" assert p.plural_noun("brother") == "brothers" assert p.plural_noun("person") == "people" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_classical_herd.py0000644000175100001770000000103714640641406020274 0ustar00runnerdockerimport inflect def test_ancient_1(): p = inflect.engine() # DEFAULT... assert p.plural_noun("wildebeest") == "wildebeests" # "person" PLURALS ACTIVATED... p.classical(herd=True) assert p.plural_noun("wildebeest") == "wildebeest" # OTHER CLASSICALS NOT ACTIVATED... assert p.plural_noun("formula") == "formulas" assert p.plural_noun("error", 0) == "errors" assert p.plural_noun("Sally") == "Sallys" assert p.plural_noun("brother") == "brothers" assert p.plural_noun("person") == "people" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_classical_names.py0000644000175100001770000000117314640641406020456 0ustar00runnerdockerimport inflect def test_ancient_1(): p = inflect.engine() # DEFAULT... assert p.plural_noun("Sally") == "Sallys" assert p.plural_noun("Jones", 0) == "Joneses" # "person" PLURALS ACTIVATED... p.classical(names=True) assert p.plural_noun("Sally") == "Sallys" assert p.plural_noun("Jones", 0) == "Joneses" # OTHER CLASSICALS NOT ACTIVATED... assert p.plural_noun("wildebeest") == "wildebeests" assert p.plural_noun("formula") == "formulas" assert p.plural_noun("error", 0) == "errors" assert p.plural_noun("brother") == "brothers" assert p.plural_noun("person") == "people" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_classical_person.py0000644000175100001770000000111514640641406020655 0ustar00runnerdockerimport inflect def test_ancient_1(): p = inflect.engine() # DEFAULT... assert p.plural_noun("person") == "people" # "person" PLURALS ACTIVATED... p.classical(persons=True) assert p.plural_noun("person") == "persons" # OTHER CLASSICALS NOT ACTIVATED... assert p.plural_noun("wildebeest") == "wildebeests" assert p.plural_noun("formula") == "formulas" assert p.plural_noun("error", 0) == "errors" assert p.plural_noun("brother") == "brothers" assert p.plural_noun("Sally") == "Sallys" assert p.plural_noun("Jones", 0) == "Joneses" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_classical_zero.py0000644000175100001770000000103014640641406020322 0ustar00runnerdockerimport inflect def test_ancient_1(): p = inflect.engine() # DEFAULT... assert p.plural_noun("error", 0) == "errors" # "person" PLURALS ACTIVATED... p.classical(zero=True) assert p.plural_noun("error", 0) == "error" # OTHER CLASSICALS NOT ACTIVATED... assert p.plural_noun("wildebeest") == "wildebeests" assert p.plural_noun("formula") == "formulas" assert p.plural_noun("person") == "people" assert p.plural_noun("brother") == "brothers" assert p.plural_noun("Sally") == "Sallys" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_compounds.py0000644000175100001770000000564314640641406017352 0ustar00runnerdockerimport inflect p = inflect.engine() def test_compound_1(): assert p.singular_noun("hello-out-there") == "hello-out-there" def test_compound_2(): assert p.singular_noun("hello out there") == "hello out there" def test_compound_3(): assert p.singular_noun("continue-to-operate") == "continue-to-operate" def test_compound_4(): assert p.singular_noun("case of diapers") == "case of diapers" def test_unit_handling_degree(): test_cases = { "degree celsius": "degrees celsius", # 'degree Celsius': 'degrees Celsius', "degree fahrenheit": "degrees fahrenheit", "degree rankine": "degrees rankine", "degree fahrenheit second": "degree fahrenheit seconds", } for singular, plural in test_cases.items(): assert p.plural(singular) == plural def test_unit_handling_fractional(): test_cases = { "pound per square inch": "pounds per square inch", "metre per second": "metres per second", "kilometre per hour": "kilometres per hour", "cubic metre per second": "cubic metres per second", "dollar a year": "dollars a year", # Correct pluralization of denominator "foot per square second": "feet per square second", "mother-in-law per lifetime": "mothers-in-law per lifetime", "pound-force per square inch": "pounds-force per square inch", } for singular, plural in test_cases.items(): assert p.plural(singular) == plural def test_unit_handling_combined(): test_cases = { # Heat transfer coefficient unit "watt per square meter degree celsius": "watts per square meter degree celsius", "degree celsius per hour": "degrees celsius per hour", "degree fahrenheit hour square foot per btuit inch": ( "degree fahrenheit hour square feet per btuit inch" ), # 'degree Celsius per hour': 'degrees Celsius per hour', # 'degree Fahrenheit hour square foot per BtuIT inch': # 'degree Fahrenheit hour square feet per BtuIT inch' } for singular, plural in test_cases.items(): assert p.plural(singular) == plural def test_unit_open_compound_nouns(): test_cases = { "high school": "high schools", "master genie": "master genies", "MASTER genie": "MASTER genies", "Blood brother": "Blood brothers", "prima donna": "prima donnas", "prima DONNA": "prima DONNAS", } for singular, plural in test_cases.items(): assert p.plural(singular) == plural def test_unit_open_compound_nouns_classical(): p.classical(all=True) test_cases = { "master genie": "master genii", "MASTER genie": "MASTER genii", "Blood brother": "Blood brethren", "prima donna": "prime donne", "prima DONNA": "prime DONNE", } for singular, plural in test_cases.items(): assert p.plural(singular) == plural p.classical(all=False) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_inflections.py0000644000175100001770000002104414640641406017651 0ustar00runnerdockerimport os import pytest import inflect def is_eq(p, a, b): return ( p.compare(a, b) or p.plnounequal(a, b) or p.plverbequal(a, b) or p.pladjequal(a, b) ) def test_many(): # noqa: C901 p = inflect.engine() data = get_data() for line in data: if "TODO:" in line: continue try: singular, rest = line.split("->", 1) except ValueError: continue singular = singular.strip() rest = rest.strip() try: plural, comment = rest.split("#", 1) except ValueError: plural = rest.strip() comment = "" try: mod_plural, class_plural = plural.split("|", 1) mod_plural = mod_plural.strip() class_plural = class_plural.strip() except ValueError: mod_plural = class_plural = plural.strip() if "verb" in comment.lower(): is_nv = "_V" elif "noun" in comment.lower(): is_nv = "_N" else: is_nv = "" p.classical(all=0, names=0) mod_PL_V = p.plural_verb(singular) mod_PL_N = p.plural_noun(singular) mod_PL = p.plural(singular) if is_nv == "_V": mod_PL_val = mod_PL_V elif is_nv == "_N": mod_PL_val = mod_PL_N else: mod_PL_val = mod_PL p.classical(all=1) class_PL_V = p.plural_verb(singular) class_PL_N = p.plural_noun(singular) class_PL = p.plural(singular) if is_nv == "_V": class_PL_val = class_PL_V elif is_nv == "_N": class_PL_val = class_PL_N else: class_PL_val = class_PL check_all( p, is_nv, singular, mod_PL_val, class_PL_val, mod_plural, class_plural ) def check_all(p, is_nv, singular, mod_PL_val, class_PL_val, mod_plural, class_plural): assert mod_plural == mod_PL_val assert class_plural == class_PL_val assert is_eq(p, singular, mod_plural) in ("s:p", "p:s", "eq") assert is_eq(p, mod_plural, singular) in ("p:s", "s:p", "eq") assert is_eq(p, singular, class_plural) in ("s:p", "p:s", "eq") assert is_eq(p, class_plural, singular) in ("p:s", "s:p", "eq") assert singular != "" expected = mod_PL_val if class_PL_val else f"{mod_PL_val}|{class_PL_val}" assert mod_PL_val == expected if is_nv != "_V": assert p.singular_noun(mod_plural, 1) == singular assert p.singular_noun(class_plural, 1) == singular def test_def(): p = inflect.engine() p.defnoun("kin", "kine") p.defnoun("(.*)x", "$1xen") p.defverb("foobar", "feebar", "foobar", "feebar", "foobars", "feebar") p.defadj("red", "red|gules") assert p.no("kin", 0) == "no kine" assert p.no("kin", 1) == "1 kin" assert p.no("kin", 2) == "2 kine" assert p.no("regex", 0) == "no regexen" assert p.plural("foobar", 2) == "feebar" assert p.plural("foobars", 2) == "feebar" assert p.plural("red", 0) == "red" assert p.plural("red", 1) == "red" assert p.plural("red", 2) == "red" p.classical(all=True) assert p.plural("red", 0) == "red" assert p.plural("red", 1) == "red" assert p.plural("red", 2) == "gules" def test_ordinal(): p = inflect.engine() assert p.ordinal(0) == "0th" assert p.ordinal(1) == p.ordinal("1") == "1st" assert p.ordinal(2) == "2nd" assert p.ordinal(3) == "3rd" assert p.ordinal(4) == "4th" assert p.ordinal(5) == "5th" assert p.ordinal(6) == "6th" assert p.ordinal(7) == "7th" assert p.ordinal(8) == "8th" assert p.ordinal(9) == "9th" assert p.ordinal(10) == "10th" assert p.ordinal(11) == "11th" assert p.ordinal(12) == "12th" assert p.ordinal(13) == "13th" assert p.ordinal(14) == "14th" assert p.ordinal(15) == "15th" assert p.ordinal(16) == "16th" assert p.ordinal(17) == "17th" assert p.ordinal(18) == "18th" assert p.ordinal(19) == "19th" assert p.ordinal(20) == "20th" assert p.ordinal(21) == "21st" assert p.ordinal(22) == "22nd" assert p.ordinal(23) == "23rd" assert p.ordinal(24) == "24th" assert p.ordinal(100) == "100th" assert p.ordinal(101) == "101st" assert p.ordinal(102) == "102nd" assert p.ordinal(103) == "103rd" assert p.ordinal(104) == "104th" assert p.ordinal(1.1) == p.ordinal("1.1") == "1.1st" assert p.ordinal(1.2) == "1.2nd" assert p.ordinal(5.502) == "5.502nd" assert p.ordinal("zero") == "zeroth" assert p.ordinal("one") == "first" assert p.ordinal("two") == "second" assert p.ordinal("three") == "third" assert p.ordinal("four") == "fourth" assert p.ordinal("five") == "fifth" assert p.ordinal("six") == "sixth" assert p.ordinal("seven") == "seventh" assert p.ordinal("eight") == "eighth" assert p.ordinal("nine") == "ninth" assert p.ordinal("ten") == "tenth" assert p.ordinal("eleven") == "eleventh" assert p.ordinal("twelve") == "twelfth" assert p.ordinal("thirteen") == "thirteenth" assert p.ordinal("fourteen") == "fourteenth" assert p.ordinal("fifteen") == "fifteenth" assert p.ordinal("sixteen") == "sixteenth" assert p.ordinal("seventeen") == "seventeenth" assert p.ordinal("eighteen") == "eighteenth" assert p.ordinal("nineteen") == "nineteenth" assert p.ordinal("twenty") == "twentieth" assert p.ordinal("twenty-one") == "twenty-first" assert p.ordinal("twenty-two") == "twenty-second" assert p.ordinal("twenty-three") == "twenty-third" assert p.ordinal("twenty-four") == "twenty-fourth" assert p.ordinal("one hundred") == "one hundredth" assert p.ordinal("one hundred and one") == "one hundred and first" assert p.ordinal("one hundred and two") == "one hundred and second" assert p.ordinal("one hundred and three") == "one hundred and third" assert p.ordinal("one hundred and four") == "one hundred and fourth" def test_decimal_ordinals(): """ Capture expectation around ordinals for decimals. This expectation is held loosely. Another expectation may be considered if appropriate. """ p = inflect.engine() assert p.ordinal("1.23") == "1.23rd" assert p.ordinal("7.09") == "7.09th" def test_prespart(): p = inflect.engine() assert p.present_participle("sees") == "seeing" assert p.present_participle("eats") == "eating" assert p.present_participle("bats") == "batting" assert p.present_participle("hates") == "hating" assert p.present_participle("spies") == "spying" assert p.present_participle("skis") == "skiing" def test_inflect_on_tuples(): p = inflect.engine() assert p.inflect("plural('egg', ('a', 'b', 'c'))") == "eggs" assert p.inflect("plural('egg', ['a', 'b', 'c'])") == "eggs" assert p.inflect("plural_noun('egg', ('a', 'b', 'c'))") == "eggs" assert p.inflect("plural_adj('a', ('a', 'b', 'c'))") == "some" assert p.inflect("plural_verb('was', ('a', 'b', 'c'))") == "were" assert p.inflect("singular_noun('eggs', ('a', 'b', 'c'))") == "eggs" assert p.inflect("an('error', ('a', 'b', 'c'))") == "('a', 'b', 'c') error" assert p.inflect("This is not a function(name)") == "This is not a function(name)" def test_inflect_on_builtin_constants(): p = inflect.engine() assert ( p.inflect("Plural of False is plural('False')") == "Plural of False is Falses" ) assert p.inflect("num(%d, False) plural('False')" % 10) == " Falses" assert p.inflect("plural('True')") == "Trues" assert p.inflect("num(%d, True) plural('False')" % 10) == "10 Falses" assert p.inflect("num(%d, %r) plural('False')" % (10, True)) == "10 Falses" assert p.inflect("plural('None')") == "Nones" assert p.inflect("num(%d, %r) plural('True')" % (10, None)) == "10 Trues" def test_inflect_keyword_args(): p = inflect.engine() assert ( p.inflect("number_to_words(1234, andword='')") == "one thousand, two hundred thirty-four" ) assert ( p.inflect("number_to_words(1234, andword='plus')") == "one thousand, two hundred plus thirty-four" ) assert ( p.inflect("number_to_words('555_1202', group=1, zero='oh')") == "five, five, five, one, two, oh, two" ) def test_NameError_in_strings(): with pytest.raises(NameError): p = inflect.engine() assert p.inflect("plural('two')") == "twoes" p.inflect("plural(two)") def get_data(): filename = os.path.join(os.path.dirname(__file__), "inflections.txt") with open(filename, encoding='utf-8') as strm: return list(map(str.strip, strm)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_join.py0000644000175100001770000000321614640641406016274 0ustar00runnerdockerimport inflect def test_join(): p = inflect.engine() # Three words... words = "apple banana carrot".split() assert p.join(words), "apple, banana == and carrot" assert p.join(words, final_sep="") == "apple, banana and carrot" assert p.join(words, final_sep="...") == "apple, banana... and carrot" assert p.join(words, final_sep="...", conj="") == "apple, banana... carrot" assert p.join(words, conj="or") == "apple, banana, or carrot" # Three words with semicolons... words = ("apple,fuji", "banana", "carrot") assert p.join(words) == "apple,fuji; banana; and carrot" assert p.join(words, final_sep="") == "apple,fuji; banana and carrot" assert p.join(words, final_sep="...") == "apple,fuji; banana... and carrot" assert p.join(words, final_sep="...", conj="") == "apple,fuji; banana... carrot" assert p.join(words, conj="or") == "apple,fuji; banana; or carrot" # Two words... words = ("apple", "carrot") assert p.join(words) == "apple and carrot" assert p.join(words, final_sep="") == "apple and carrot" assert p.join(words, final_sep="...") == "apple and carrot" assert p.join(words, final_sep="...", conj="") == "apple carrot" assert p.join(words, final_sep="...", conj="", conj_spaced=False) == "applecarrot" assert p.join(words, conj="or") == "apple or carrot" # One word... words = ["carrot"] assert p.join(words) == "carrot" assert p.join(words, final_sep="") == "carrot" assert p.join(words, final_sep="...") == "carrot" assert p.join(words, final_sep="...", conj="") == "carrot" assert p.join(words, conj="or") == "carrot" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_numwords.py0000644000175100001770000003643414640641406017223 0ustar00runnerdockerimport inflect def test_loop(): p = inflect.engine() for thresh in range(21): for n in range(21): threshed = p.number_to_words(n, threshold=thresh) numwords = p.number_to_words(n) if n <= thresh: assert numwords == threshed else: # $threshed =~ s/\D//gxms; assert threshed == str(n) def test_lines(): p = inflect.engine() assert p.number_to_words(999, threshold=500) == "999" assert p.number_to_words(1000, threshold=500) == "1,000" assert p.number_to_words(10000, threshold=500) == "10,000" assert p.number_to_words(100000, threshold=500) == "100,000" assert p.number_to_words(1000000, threshold=500) == "1,000,000" assert p.number_to_words(999.3, threshold=500) == "999.3" assert p.number_to_words(1000.3, threshold=500) == "1,000.3" assert p.number_to_words(10000.3, threshold=500) == "10,000.3" assert p.number_to_words(100000.3, threshold=500) == "100,000.3" assert p.number_to_words(1000000.3, threshold=500) == "1,000,000.3" assert p.number_to_words(999, threshold=500, comma=0) == "999" assert p.number_to_words(1000, threshold=500, comma=0) == "1000" assert p.number_to_words(10000, threshold=500, comma=0) == "10000" assert p.number_to_words(100000, threshold=500, comma=0) == "100000" assert p.number_to_words(1000000, threshold=500, comma=0) == "1000000" assert p.number_to_words(999.3, threshold=500, comma=0) == "999.3" assert p.number_to_words(1000.3, threshold=500, comma=0) == "1000.3" assert p.number_to_words(10000.3, threshold=500, comma=0) == "10000.3" assert p.number_to_words(100000.3, threshold=500, comma=0) == "100000.3" assert p.number_to_words(1000000.3, threshold=500, comma=0) == "1000000.3" def test_array(): nw = [ ["0", "zero", "zero", "zero", "zero", "zeroth"], ["1", "one", "one", "one", "one", "first"], ["2", "two", "two", "two", "two", "second"], ["3", "three", "three", "three", "three", "third"], ["4", "four", "four", "four", "four", "fourth"], ["5", "five", "five", "five", "five", "fifth"], ["6", "six", "six", "six", "six", "sixth"], ["7", "seven", "seven", "seven", "seven", "seventh"], ["8", "eight", "eight", "eight", "eight", "eighth"], ["9", "nine", "nine", "nine", "nine", "ninth"], ["10", "ten", "one, zero", "ten", "ten", "tenth"], ["11", "eleven", "one, one", "eleven", "eleven", "eleventh"], ["12", "twelve", "one, two", "twelve", "twelve", "twelfth"], ["13", "thirteen", "one, three", "thirteen", "thirteen", "thirteenth"], ["14", "fourteen", "one, four", "fourteen", "fourteen", "fourteenth"], ["15", "fifteen", "one, five", "fifteen", "fifteen", "fifteenth"], ["16", "sixteen", "one, six", "sixteen", "sixteen", "sixteenth"], ["17", "seventeen", "one, seven", "seventeen", "seventeen", "seventeenth"], ["18", "eighteen", "one, eight", "eighteen", "eighteen", "eighteenth"], ["19", "nineteen", "one, nine", "nineteen", "nineteen", "nineteenth"], ["20", "twenty", "two, zero", "twenty", "twenty", "twentieth"], ["21", "twenty-one", "two, one", "twenty-one", "twenty-one", "twenty-first"], [ "29", "twenty-nine", "two, nine", "twenty-nine", "twenty-nine", "twenty-ninth", ], [ "99", "ninety-nine", "nine, nine", "ninety-nine", "ninety-nine", "ninety-ninth", ], [ "100", "one hundred", "one, zero, zero", "ten, zero", "one zero zero", "one hundredth", ], [ "101", "one hundred and one", "one, zero, one", "ten, one", "one zero one", "one hundred and first", ], [ "110", "one hundred and ten", "one, one, zero", "eleven, zero", "one ten", "one hundred and tenth", ], [ "111", "one hundred and eleven", "one, one, one", "eleven, one", "one eleven", "one hundred and eleventh", ], [ "900", "nine hundred", "nine, zero, zero", "ninety, zero", "nine zero zero", "nine hundredth", ], [ "999", "nine hundred and ninety-nine", "nine, nine, nine", "ninety-nine, nine", "nine ninety-nine", "nine hundred and ninety-ninth", ], [ "1000", "one thousand", "one, zero, zero, zero", "ten, zero zero", "one zero zero, zero", "one thousandth", ], [ "1001", "one thousand and one", "one, zero, zero, one", "ten, zero one", "one zero zero, one", "one thousand and first", ], [ "1010", "one thousand and ten", "one, zero, one, zero", "ten, ten", "one zero one, zero", "one thousand and tenth", ], [ "1100", "one thousand, one hundred", "one, one, zero, zero", "eleven, zero zero", "one ten, zero", "one thousand, one hundredth", ], [ "2000", "two thousand", "two, zero, zero, zero", "twenty, zero zero", "two zero zero, zero", "two thousandth", ], [ "10000", "ten thousand", "one, zero, zero, zero, zero", "ten, zero zero, zero", "one zero zero, zero zero", "ten thousandth", ], [ "100000", "one hundred thousand", "one, zero, zero, zero, zero, zero", "ten, zero zero, zero zero", "one zero zero, zero zero zero", "one hundred thousandth", ], [ "100001", "one hundred thousand and one", "one, zero, zero, zero, zero, one", "ten, zero zero, zero one", "one zero zero, zero zero one", "one hundred thousand and first", ], [ "123456", "one hundred and twenty-three thousand, " "four hundred and fifty-six", "one, two, three, four, five, six", "twelve, thirty-four, fifty-six", "one twenty-three, four fifty-six", "one hundred and twenty-three thousand, " "four hundred and fifty-sixth", ], [ "0123456", "one hundred and twenty-three thousand, " "four hundred and fifty-six", "zero, one, two, three, four, five, six", "zero one, twenty-three, forty-five, six", "zero twelve, three forty-five, six", "one hundred and twenty-three thousand, " "four hundred and fifty-sixth", ], [ "1234567", "one million, two hundred and thirty-four thousand, " "five hundred and sixty-seven", "one, two, three, four, five, six, seven", "twelve, thirty-four, fifty-six, seven", "one twenty-three, four fifty-six, seven", "one million, two hundred and thirty-four thousand, " "five hundred and sixty-seventh", ], [ "12345678", "twelve million, three hundred and forty-five thousand, " "six hundred and seventy-eight", "one, two, three, four, five, six, seven, eight", "twelve, thirty-four, fifty-six, seventy-eight", "one twenty-three, four fifty-six, seventy-eight", "twelve million, three hundred and forty-five thousand, " "six hundred and seventy-eighth", ], [ "12_345_678", "twelve million, three hundred and forty-five thousand, " "six hundred and seventy-eight", "one, two, three, four, five, six, seven, eight", "twelve, thirty-four, fifty-six, seventy-eight", "one twenty-three, four fifty-six, seventy-eight", ], [ "1234,5678", "twelve million, three hundred and forty-five thousand, " "six hundred and seventy-eight", "one, two, three, four, five, six, seven, eight", "twelve, thirty-four, fifty-six, seventy-eight", "one twenty-three, four fifty-six, seventy-eight", ], [ "1234567890", "one billion, two hundred and thirty-four million, five hundred " "and sixty-seven thousand, eight hundred and ninety", "one, two, three, four, five, six, seven, eight, nine, zero", "twelve, thirty-four, fifty-six, seventy-eight, ninety", "one twenty-three, four fifty-six, seven eighty-nine, zero", "one billion, two hundred and thirty-four million, five hundred " "and sixty-seven thousand, eight hundred and ninetieth", ], [ "123456789012345", "one hundred and twenty-three trillion, four hundred and " "fifty-six billion, seven hundred and eighty-nine million, twelve " "thousand, three hundred and forty-five", "one, two, three, four, five, six, seven, eight, nine, zero, one, " "two, three, four, five", "twelve, thirty-four, fifty-six, seventy-eight, ninety, twelve, " "thirty-four, five", "one twenty-three, four fifty-six, seven eighty-nine, " "zero twelve, three forty-five", "one hundred and twenty-three trillion, four hundred and " "fifty-six billion, seven hundred and eighty-nine million, " "twelve thousand, three hundred and forty-fifth", ], [ "12345678901234567890", "twelve quintillion, three hundred and forty-five quadrillion, " "six hundred and seventy-eight trillion, nine hundred and one " "billion, two hundred and thirty-four million, five hundred and " "sixty-seven thousand, eight hundred and ninety", "one, two, three, four, five, six, seven, eight, nine, zero, one, " "two, three, four, five, six, seven, eight, nine, zero", "twelve, thirty-four, fifty-six, seventy-eight, ninety, twelve, " "thirty-four, fifty-six, seventy-eight, ninety", "one twenty-three, four fifty-six, seven eighty-nine, " "zero twelve, three forty-five, six seventy-eight, ninety", "twelve quintillion, three hundred and forty-five quadrillion, " "six hundred and seventy-eight trillion, nine hundred and one " "billion, two hundred and thirty-four million, five hundred and " "sixty-seven thousand, eight hundred and ninetieth", ], [ "0.987654", "zero point nine eight seven six five four", "zero, point, nine, eight, seven, six, five, four", "zero, point, ninety-eight, seventy-six, fifty-four", "zero, point, nine eighty-seven, six fifty-four", "zeroth point nine eight seven six five four", "zero point nine eight seven six five fourth", ], [ ".987654", "point nine eight seven six five four", "point, nine, eight, seven, six, five, four", "point, ninety-eight, seventy-six, fifty-four", "point, nine eighty-seven, six fifty-four", "point nine eight seven six five four", "point nine eight seven six five fourth", ], [ "9.87654", "nine point eight seven six five four", "nine, point, eight, seven, six, five, four", "nine, point, eighty-seven, sixty-five, four", "nine, point, eight seventy-six, fifty-four", "ninth point eight seven six five four", "nine point eight seven six five fourth", ], [ "98.7654", "ninety-eight point seven six five four", "nine, eight, point, seven, six, five, four", "ninety-eight, point, seventy-six, fifty-four", "ninety-eight, point, seven sixty-five, four", "ninety-eighth point seven six five four", "ninety-eight point seven six five fourth", ], [ "987.654", "nine hundred and eighty-seven point six five four", "nine, eight, seven, point, six, five, four", "ninety-eight, seven, point, sixty-five, four", "nine eighty-seven, point, six fifty-four", "nine hundred and eighty-seventh point six five four", "nine hundred and eighty-seven point six five fourth", ], [ "9876.54", "nine thousand, eight hundred and seventy-six point five four", "nine, eight, seven, six, point, five, four", "ninety-eight, seventy-six, point, fifty-four", "nine eighty-seven, six, point, fifty-four", "nine thousand, eight hundred and seventy-sixth point five four", "nine thousand, eight hundred and seventy-six point five fourth", ], [ "98765.4", "ninety-eight thousand, seven hundred and sixty-five point four", "nine, eight, seven, six, five, point, four", "ninety-eight, seventy-six, five, point, four", "nine eighty-seven, sixty-five, point, four", "ninety-eight thousand, seven hundred and sixty-fifth point four", "ninety-eight thousand, seven hundred and sixty-five point fourth", ], [ "101.202.303", "one hundred and one point two zero two three zero three", "one, zero, one, point, two, zero, two, point, three, zero, three", "ten, one, point, twenty, two, point, thirty, three", "one zero one, point, two zero two, point, three zero three", ], [ "98765.", "ninety-eight thousand, seven hundred and sixty-five point", "nine, eight, seven, six, five, point", "ninety-eight, seventy-six, five, point", "nine eighty-seven, sixty-five, point", ], ] p = inflect.engine() for i in nw: go(p, i) def go(p, i): assert p.number_to_words(i[0]) == i[1] assert p.number_to_words(i[0], group=1) == i[2] assert p.number_to_words(i[0], group=2) == i[3] assert p.number_to_words(i[0], group=3) == i[4] if len(i) > 5: assert p.number_to_words(p.ordinal(i[0])) == i[5] if len(i) > 6: assert p.ordinal(p.number_to_words(i[0])) == i[6] else: if len(i) > 5: assert p.ordinal(p.number_to_words(i[0])) == i[5] # eq_ !eval { p.number_to_words(42, and=>); 1; }; # eq_ $@ =~ 'odd number of'; def test_issue_131(): p = inflect.engine() for nth_word in inflect.nth_suff: assert p.number_to_words(nth_word) == "zero" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_pl_si.py0000644000175100001770000000055414640641406016445 0ustar00runnerdockerimport pytest import inflect @pytest.fixture(params=[False, True], ids=['classical off', 'classical on']) def classical(request): return request.param @pytest.mark.parametrize("word", ['Times', 'Jones']) def test_pl_si(classical, word): p = inflect.engine() p.classical(all=classical) assert p.singular_noun(p.plural_noun(word, 2), 1) == word ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_pwd.py0000644000175100001770000012316314640641406016133 0ustar00runnerdockerimport pytest from typeguard import TypeCheckError import inflect from inflect import ( BadChunkingOptionError, BadGenderError, BadNumValueError, NumOutOfRangeError, UnknownClassicalModeError, ) missing = object() class Test: def test_enclose(self): # def enclose assert inflect.enclose("test") == "(?:test)" def test_joinstem(self): # def joinstem assert ( inflect.joinstem(-2, ["ephemeris", "iris", ".*itis"]) == "(?:ephemer|ir|.*it)" ) def test_classical(self): # classical dicts assert set(inflect.def_classical.keys()) == set(inflect.all_classical.keys()) assert set(inflect.def_classical.keys()) == set(inflect.no_classical.keys()) # def classical p = inflect.engine() assert p.classical_dict == inflect.def_classical p.classical() assert p.classical_dict == inflect.all_classical with pytest.raises(TypeError): p.classical(0) with pytest.raises(TypeError): p.classical(1) with pytest.raises(TypeError): p.classical("names") with pytest.raises(TypeError): p.classical("names", "zero") with pytest.raises(TypeError): p.classical("all") p.classical(all=False) assert p.classical_dict == inflect.no_classical p.classical(names=True, zero=True) mydict = inflect.def_classical.copy() mydict.update(dict(names=1, zero=1)) assert p.classical_dict == mydict p.classical(all=True) assert p.classical_dict == inflect.all_classical p.classical(all=False) p.classical(names=True, zero=True) mydict = inflect.def_classical.copy() mydict.update(dict(names=True, zero=True)) assert p.classical_dict == mydict p.classical(all=False) p.classical(names=True, zero=False) mydict = inflect.def_classical.copy() mydict.update(dict(names=True, zero=False)) assert p.classical_dict == mydict with pytest.raises(UnknownClassicalModeError): p.classical(bogus=True) def test_num(self): # def num p = inflect.engine() assert p.persistent_count is None p.num() assert p.persistent_count is None ret = p.num(3) assert p.persistent_count == 3 assert ret == "3" p.num() ret = p.num("3") assert p.persistent_count == 3 assert ret == "3" p.num() ret = p.num(count=3, show=1) assert p.persistent_count == 3 assert ret == "3" p.num() ret = p.num(count=3, show=0) assert p.persistent_count == 3 assert ret == "" with pytest.raises(BadNumValueError): p.num("text") def test_inflect(self): p = inflect.engine() for txt, ans in ( ("num(1)", "1"), ("num(1,0)", ""), ("num(1,1)", "1"), ("num(1) ", "1 "), (" num(1) ", " 1 "), ("num(3) num(1)", "3 1"), ): assert p.inflect(txt) == ans, f'p.inflect("{txt}") != "{ans}"' for txt, ans in ( ("plural('rock')", "rocks"), ("plural('rock') plural('child')", "rocks children"), ("num(2) plural('rock') plural('child')", "2 rocks children"), ( "plural('rock') plural_noun('rock') plural_verb('rocks') " "plural_adj('big') a('ant')", "rocks rocks rock big an ant", ), ( "an('rock') no('cat') ordinal(3) number_to_words(1234) " "present_participle('runs')", "a rock no cats 3rd one thousand, two hundred and thirty-four running", ), ("a('cat',0) a('cat',1) a('cat',2) a('cat', 2)", "0 cat a cat 2 cat 2 cat"), ): assert p.inflect(txt) == ans, f'p.inflect("{txt}") != "{ans}"' def test_user_input_fns(self): p = inflect.engine() assert p.pl_sb_user_defined == [] p.defnoun("VAX", "VAXen") assert p.plural("VAX") == "VAXEN" assert p.pl_sb_user_defined == ["VAX", "VAXen"] assert p.ud_match("word", p.pl_sb_user_defined) is None assert p.ud_match("VAX", p.pl_sb_user_defined) == "VAXen" assert p.ud_match("VVAX", p.pl_sb_user_defined) is None p.defnoun("cow", "cows|kine") assert p.plural("cow") == "cows" p.classical() assert p.plural("cow") == "kine" assert p.ud_match("cow", p.pl_sb_user_defined) == "cows|kine" p.defnoun("(.+i)o", r"$1i") assert p.plural("studio") == "studii" assert p.ud_match("studio", p.pl_sb_user_defined) == "studii" p.defnoun("aviatrix", "aviatrices") assert p.plural("aviatrix") == "aviatrices" assert p.ud_match("aviatrix", p.pl_sb_user_defined) == "aviatrices" p.defnoun("aviatrix", "aviatrixes") assert p.plural("aviatrix") == "aviatrixes" assert p.ud_match("aviatrix", p.pl_sb_user_defined) == "aviatrixes" p.defnoun("aviatrix", None) assert p.plural("aviatrix") == "aviatrices" assert p.ud_match("aviatrix", p.pl_sb_user_defined) is None p.defnoun("(cat)", r"$1s") assert p.plural("cat") == "cats" with pytest.raises(inflect.BadUserDefinedPatternError): p.defnoun("(??", None) p.defnoun(None, "any") # check None doesn't crash it # defadj p.defadj("hir", "their") assert p.plural("hir") == "their" assert p.ud_match("hir", p.pl_adj_user_defined) == "their" # defa defan p.defa("h") assert p.a("h") == "a h" assert p.ud_match("h", p.A_a_user_defined) == "a" p.defan("horrendous.*") assert p.a("horrendously") == "an horrendously" assert p.ud_match("horrendously", p.A_a_user_defined) == "an" def test_user_input_defverb(self): p = inflect.engine() p.defverb("will", "shall", "will", "will", "will", "will") assert p.ud_match("will", p.pl_v_user_defined) == "will" assert p.plural("will") == "will" @pytest.mark.xfail(reason="todo") def test_user_input_defverb_compare(self): p = inflect.engine() p.defverb("will", "shall", "will", "will", "will", "will") assert p.compare("will", "shall") == "s:p" assert p.compare_verbs("will", "shall") == "s:p" def test_postprocess(self): p = inflect.engine() for orig, infl, txt in ( ("cow", "cows", "cows"), ("I", "we", "we"), ("COW", "cows", "COWS"), ("Cow", "cows", "Cows"), ("cow", "cows|kine", "cows"), ("Entry", "entries", "Entries"), ("can of Coke", "cans of coke", "cans of Coke"), ): assert p.postprocess(orig, infl) == txt p.classical() assert p.postprocess("cow", "cows|kine") == "kine" def test_partition_word(self): p = inflect.engine() for txt, part in ( (" cow ", (" ", "cow", " ")), ("cow", ("", "cow", "")), (" cow", (" ", "cow", "")), ("cow ", ("", "cow", " ")), (" cow ", (" ", "cow", " ")), ("", ("", "", "")), ("bottle of beer", ("", "bottle of beer", "")), # spaces give weird results # (' '),('', ' ', '')), # (' '),(' ', ' ', '')), # (' '),(' ', ' ', '')), ): assert p.partition_word(txt) == part def test_pl(self): p = inflect.engine() for fn, sing, plur in ( (p.plural, "cow", "cows"), (p.plural, "thought", "thoughts"), (p.plural, "mouse", "mice"), (p.plural, "knife", "knives"), (p.plural, "knifes", "knife"), (p.plural, " cat ", " cats "), (p.plural, "court martial", "courts martial"), (p.plural, "a", "some"), (p.plural, "carmen", "carmina"), (p.plural, "quartz", "quartzes"), (p.plural, "care", "cares"), (p.plural_noun, "cow", "cows"), (p.plural_noun, "thought", "thoughts"), (p.plural_noun, "snooze", "snoozes"), (p.plural_verb, "runs", "run"), (p.plural_verb, "thought", "thought"), (p.plural_verb, "eyes", "eye"), (p.plural_adj, "a", "some"), (p.plural_adj, "this", "these"), (p.plural_adj, "that", "those"), (p.plural_adj, "my", "our"), (p.plural_adj, "cat's", "cats'"), (p.plural_adj, "child's", "children's"), ): assert ( fn(sing) == plur ), f'{fn.__name__}("{sing}") == "{fn(sing)}" != "{plur}"' for sing, num, plur in ( ("cow", 1, "cow"), ("cow", 2, "cows"), ("cow", "one", "cow"), ("cow", "each", "cow"), ("cow", "two", "cows"), ("cow", 0, "cows"), ("cow", "zero", "cows"), ("runs", 0, "run"), ("runs", 1, "runs"), ("am", 0, "are"), ): assert p.plural(sing, num) == plur p.classical(zero=True) assert p.plural("cow", 0) == "cow" assert p.plural("cow", "zero") == "cow" assert p.plural("runs", 0) == "runs" assert p.plural("am", 0) == "am" assert p.plural_verb("runs", 1) == "runs" assert p.plural("die") == "dice" assert p.plural_noun("die") == "dice" with pytest.raises(TypeCheckError): p.plural("") with pytest.raises(TypeCheckError): p.plural_noun("") with pytest.raises(TypeCheckError): p.plural_verb("") with pytest.raises(TypeCheckError): p.plural_adj("") def test_sinoun(self): p = inflect.engine() for sing, plur in ( ("cat", "cats"), ("die", "dice"), ("goose", "geese"), ): assert p.singular_noun(plur) == sing assert p.inflect("singular_noun('%s')" % plur) == sing assert p.singular_noun("cats", count=2) == "cats" assert p.singular_noun("open valves", count=2) == "open valves" assert p.singular_noun("zombies") == "zombie" assert p.singular_noun("shoes") == "shoe" assert p.singular_noun("dancing shoes") == "dancing shoe" assert p.singular_noun("Matisses") == "Matisse" assert p.singular_noun("bouillabaisses") == "bouillabaisse" assert p.singular_noun("quartzes") == "quartz" assert p.singular_noun("Nietzsches") == "Nietzsche" assert p.singular_noun("aches") == "ache" assert p.singular_noun("Clives") == "Clive" assert p.singular_noun("weaves") == "weave" assert p.singular_noun("status") is False assert p.singular_noun("hiatus") is False def test_gender(self): p = inflect.engine() p.gender("feminine") for sing, plur in ( ("she", "they"), ("herself", "themselves"), ("hers", "theirs"), ("to her", "to them"), ("to herself", "to themselves"), ): assert ( p.singular_noun(plur) == sing ), f"singular_noun({plur}) == {p.singular_noun(plur)} != {sing}" assert p.inflect("singular_noun('%s')" % plur) == sing p.gender("masculine") for sing, plur in ( ("he", "they"), ("himself", "themselves"), ("his", "theirs"), ("to him", "to them"), ("to himself", "to themselves"), ): assert ( p.singular_noun(plur) == sing ), f"singular_noun({plur}) == {p.singular_noun(plur)} != {sing}" assert p.inflect("singular_noun('%s')" % plur) == sing p.gender("gender-neutral") for sing, plur in ( ("they", "they"), ("themself", "themselves"), ("theirs", "theirs"), ("to them", "to them"), ("to themself", "to themselves"), ): assert ( p.singular_noun(plur) == sing ), f"singular_noun({plur}) == {p.singular_noun(plur)} != {sing}" assert p.inflect("singular_noun('%s')" % plur) == sing p.gender("neuter") for sing, plur in ( ("it", "they"), ("itself", "themselves"), ("its", "theirs"), ("to it", "to them"), ("to itself", "to themselves"), ): assert ( p.singular_noun(plur) == sing ), f"singular_noun({plur}) == {p.singular_noun(plur)} != {sing}" assert p.inflect("singular_noun('%s')" % plur) == sing with pytest.raises(BadGenderError): p.gender("male") for sing, plur, gen in ( ("it", "they", "neuter"), ("she", "they", "feminine"), ("he", "they", "masculine"), ("they", "they", "gender-neutral"), ("she or he", "they", "feminine or masculine"), ("he or she", "they", "masculine or feminine"), ): assert p.singular_noun(plur, gender=gen) == sing with pytest.raises(BadGenderError): p.singular_noun("cats", gender="unknown gender") @pytest.mark.parametrize( 'sing,plur,res', ( ("index", "index", "eq"), ("index", "indexes", "s:p"), ("index", "indices", "s:p"), ("indexes", "index", "p:s"), ("indices", "index", "p:s"), ("indices", "indexes", "p:p"), ("indexes", "indices", "p:p"), ("indices", "indices", "eq"), ("inverted index", "inverted indices", "s:p"), ("inverted indices", "inverted index", "p:s"), ("inverted indexes", "inverted indices", "p:p"), ("opuses", "opera", "p:p"), ("opera", "opuses", "p:p"), ("brothers", "brethren", "p:p"), ("cats", "cats", "eq"), ("base", "basis", False), ("syrinx", "syringe", False), ("she", "he", False), ("opus", "operas", False), ("taxi", "taxes", False), ("time", "Times", False), ("time".lower(), "Times".lower(), "s:p"), ("courts martial", "court martial", "p:s"), ("my", "my", "eq"), ("my", "our", "s:p"), ("our", "our", "eq"), pytest.param( "dresses's", "dresses'", "p:p", marks=pytest.mark.xfail(reason="todo") ), pytest.param( "dress's", "dress'", "s:s", marks=pytest.mark.xfail(reason='todo') ), pytest.param( "Jess's", "Jess'", "s:s", marks=pytest.mark.xfail(reason='todo') ), ), ) def test_compare_simple(self, sing, plur, res): assert inflect.engine().compare(sing, plur) == res @pytest.mark.parametrize( 'sing,plur,res', ( ("index", "index", "eq"), ("index", "indexes", "s:p"), ("index", "indices", "s:p"), ("indexes", "index", "p:s"), ("indices", "index", "p:s"), ("indices", "indexes", "p:p"), ("indexes", "indices", "p:p"), ("indices", "indices", "eq"), ("inverted index", "inverted indices", "s:p"), ("inverted indices", "inverted index", "p:s"), ("inverted indexes", "inverted indices", "p:p"), ), ) def test_compare_nouns(self, sing, plur, res): assert inflect.engine().compare_nouns(sing, plur) == res @pytest.mark.parametrize( 'sing,plur,res', ( ("runs", "runs", "eq"), ("runs", "run", "s:p"), ("run", "run", "eq"), ), ) def test_compare_verbs(self, sing, plur, res): assert inflect.engine().compare_verbs(sing, plur) == res @pytest.mark.parametrize( 'sing,plur,res', ( ("my", "my", "eq"), ("my", "our", "s:p"), ("our", "our", "eq"), pytest.param( "dresses's", "dresses'", "p:p", marks=pytest.mark.xfail(reason="todo") ), pytest.param( "dress's", "dress'", "s:s", marks=pytest.mark.xfail(reason='todo') ), pytest.param( "Jess's", "Jess'", "s:s", marks=pytest.mark.xfail(reason='todo') ), ), ) def test_compare_adjectives(self, sing, plur, res): assert inflect.engine().compare_adjs(sing, plur) == res @pytest.mark.xfail() def test_compare_your_our(self): # multiple adjective plurals not (yet) supported p = inflect.engine() assert p.compare("your", "our") is False p.defadj("my", "our|your") # what's ours is yours assert p.compare("your", "our") == "p:p" def test__pl_reg_plurals(self): p = inflect.engine() for pair, stems, end1, end2, ans in ( ("indexes|indices", "dummy|ind", "exes", "ices", True), ("indexes|robots", "dummy|ind", "exes", "ices", False), ("beaus|beaux", ".*eau", "s", "x", True), ): assert p._pl_reg_plurals(pair, stems, end1, end2) == ans def test__pl_check_plurals_N(self): p = inflect.engine() assert p._pl_check_plurals_N("index", "indices") is False assert p._pl_check_plurals_N("indexes", "indices") is True assert p._pl_check_plurals_N("indices", "indexes") is True assert p._pl_check_plurals_N("stigmata", "stigmas") is True assert p._pl_check_plurals_N("phalanxes", "phalanges") is True def test__pl_check_plurals_adj(self): p = inflect.engine() assert p._pl_check_plurals_adj("indexes's", "indices's") is True assert p._pl_check_plurals_adj("indices's", "indexes's") is True assert p._pl_check_plurals_adj("indexes'", "indices's") is True assert p._pl_check_plurals_adj("indexes's", "indices'") is True assert p._pl_check_plurals_adj("indexes's", "indexes's") is False assert p._pl_check_plurals_adj("dogmas's", "dogmata's") is True assert p._pl_check_plurals_adj("dogmas'", "dogmata'") is True assert p._pl_check_plurals_adj("indexes'", "indices'") is True def test_count(self): p = inflect.engine() for txt, num in ( (1, 1), (2, 2), (0, 2), (87, 2), (-7, 2), ("1", 1), ("2", 2), ("0", 2), ("no", 2), ("zero", 2), ("nil", 2), ("a", 1), ("an", 1), ("one", 1), ("each", 1), ("every", 1), ("this", 1), ("that", 1), ("dummy", 2), ): assert p.get_count(txt) == num assert p.get_count() == "" p.num(3) assert p.get_count() == 2 def test__plnoun(self): p = inflect.engine() for sing, plur in ( ("tuna", "tuna"), ("TUNA", "TUNA"), ("swordfish", "swordfish"), ("Governor General", "Governors General"), ("Governor-General", "Governors-General"), ("Major General", "Major Generals"), ("Major-General", "Major-Generals"), ("mother in law", "mothers in law"), ("mother-in-law", "mothers-in-law"), ("about me", "about us"), ("to it", "to them"), ("from it", "from them"), ("with it", "with them"), ("I", "we"), ("you", "you"), ("me", "us"), ("mine", "ours"), ("child", "children"), ("brainchild", "brainchilds"), ("human", "humans"), ("soliloquy", "soliloquies"), ("chairwoman", "chairwomen"), ("goose", "geese"), ("tooth", "teeth"), ("foot", "feet"), ("forceps", "forceps"), ("protozoon", "protozoa"), ("czech", "czechs"), ("codex", "codices"), ("radix", "radices"), ("bacterium", "bacteria"), ("alumnus", "alumni"), ("criterion", "criteria"), ("alumna", "alumnae"), ("bias", "biases"), ("quiz", "quizzes"), ("fox", "foxes"), ("shelf", "shelves"), ("leaf", "leaves"), ("midwife", "midwives"), ("scarf", "scarves"), ("key", "keys"), ("Sally", "Sallys"), ("sally", "sallies"), ("ado", "ados"), ("auto", "autos"), ("alto", "altos"), ("zoo", "zoos"), ("tomato", "tomatoes"), ): assert ( p._plnoun(sing) == plur ), f'p._plnoun("{sing}") == {p._plnoun(sing)} != "{plur}"' assert p._sinoun(plur) == sing, f'p._sinoun("{plur}") != "{sing}"' # words where forming singular is ambiguous or not attempted for sing, plur in ( ("son of a gun", "sons of guns"), ("son-of-a-gun", "sons-of-guns"), ("basis", "bases"), ("Jess", "Jesses"), ): assert p._plnoun(sing) == plur, f'p._plnoun("{sing}") != "{plur}"' p.num(1) assert p._plnoun("cat") == "cat" p.num(3) p.classical(herd=True) assert p._plnoun("swine") == "swine" p.classical(herd=False) assert p._plnoun("swine") == "swines" p.classical(persons=True) assert p._plnoun("chairperson") == "chairpersons" p.classical(persons=False) assert p._plnoun("chairperson") == "chairpeople" p.classical(ancient=True) assert p._plnoun("formula") == "formulae" p.classical(ancient=False) assert p._plnoun("formula") == "formulas" p.classical() for sing, plur in ( ("matrix", "matrices"), ("gateau", "gateaux"), ("millieu", "millieux"), ("syrinx", "syringes"), ("stamen", "stamina"), ("apex", "apices"), ("appendix", "appendices"), ("maximum", "maxima"), ("focus", "foci"), ("status", "status"), ("aurora", "aurorae"), ("soma", "somata"), ("iris", "irides"), ("solo", "soli"), ("oxymoron", "oxymora"), ("goy", "goyim"), ("afrit", "afriti"), ): assert p._plnoun(sing) == plur # p.classical(0) # p.classical('names') # classical now back to the default mode @pytest.mark.parametrize( 'sing, plur', ( pytest.param( 'about ME', 'about US', marks=pytest.mark.xfail(reason='does not keep case'), ), pytest.param( 'YOU', 'YOU', marks=pytest.mark.xfail(reason='does not keep case'), ), ), ) def test_plnoun_retains_case(self, sing, plur): assert inflect.engine()._plnoun(sing) == plur def test_classical_pl(self): p = inflect.engine() p.classical() for sing, plur in (("brother", "brethren"), ("dogma", "dogmata")): assert p.plural(sing) == plur def test__pl_special_verb(self): p = inflect.engine() with pytest.raises(TypeCheckError): p._pl_special_verb("") assert p._pl_special_verb("am") == "are" assert p._pl_special_verb("am", 0) == "are" assert p._pl_special_verb("runs", 0) == "run" p.classical(zero=True) assert p._pl_special_verb("am", 0) is False assert p._pl_special_verb("am", 1) == "am" assert p._pl_special_verb("am", 2) == "are" assert p._pl_special_verb("runs", 0) is False assert p._pl_special_verb("am going to") == "are going to" assert p._pl_special_verb("did") == "did" assert p._pl_special_verb("wasn't") == "weren't" assert p._pl_special_verb("shouldn't") == "shouldn't" assert p._pl_special_verb("bias") is False assert p._pl_special_verb("news") is False assert p._pl_special_verb("Jess") is False assert p._pl_special_verb(" ") is False assert p._pl_special_verb("brushes") == "brush" assert p._pl_special_verb("fixes") == "fix" assert p._pl_special_verb("quizzes") == "quiz" assert p._pl_special_verb("fizzes") == "fizz" assert p._pl_special_verb("dresses") == "dress" assert p._pl_special_verb("flies") == "fly" assert p._pl_special_verb("canoes") == "canoe" assert p._pl_special_verb("horseshoes") == "horseshoe" assert p._pl_special_verb("does") == "do" # TODO: what's a real word to test this case? assert p._pl_special_verb("zzzoes") == "zzzo" assert p._pl_special_verb("runs") == "run" def test__pl_general_verb(self): p = inflect.engine() assert p._pl_general_verb("acts") == "act" assert p._pl_general_verb("act") == "act" assert p._pl_general_verb("saw") == "saw" assert p._pl_general_verb("runs", 1) == "runs" @pytest.mark.parametrize( 'adj,plur', ( ("a", "some"), ("my", "our"), ("John's", "Johns'"), ("tuna's", "tuna's"), ("TUNA's", "TUNA's"), ("bad", False), pytest.param( "JOHN's", "JOHNS'", marks=pytest.mark.xfail(reason='should this be handled?'), ), pytest.param( "JOHN'S", "JOHNS'", marks=pytest.mark.xfail(reason="can't handle capitals"), ), pytest.param( "TUNA'S", "TUNA'S", marks=pytest.mark.xfail(reason="can't handle capitals"), ), ), ) def test__pl_special_adjective(self, adj, plur): p = inflect.engine() assert p._pl_special_adjective(adj) == plur @pytest.mark.parametrize( 'sing, plur', ( ("cat", "a cat"), ("euphemism", "a euphemism"), ("Euler number", "an Euler number"), ("hour", "an hour"), ("houri", "a houri"), ("nth", "an nth"), ("rth", "an rth"), ("sth", "an sth"), ("xth", "an xth"), ("ant", "an ant"), ("book", "a book"), ("RSPCA", "an RSPCA"), ("SONAR", "a SONAR"), ("FJO", "a FJO"), ("FJ", "an FJ"), ("NASA", "a NASA"), ("UN", "a UN"), ("yak", "a yak"), ("yttrium", "an yttrium"), ("a elephant", "an elephant"), ("a giraffe", "a giraffe"), ("an ewe", "a ewe"), ("a orangutan", "an orangutan"), ("R.I.P.", "an R.I.P."), ("C.O.D.", "a C.O.D."), ("e-mail", "an e-mail"), ("X-ray", "an X-ray"), ("T-square", "a T-square"), ("LCD", "an LCD"), ("XML", "an XML"), ("YWCA", "a YWCA"), ("LED", "a LED"), ("OPEC", "an OPEC"), ("FAQ", "a FAQ"), ("UNESCO", "a UNESCO"), ("a", "an a"), ("an", "an an"), ("an ant", "an ant"), ("a cat", "a cat"), ("an cat", "a cat"), ("a ant", "an ant"), ), ) def test_a(self, sing, plur): p = inflect.engine() assert p.a(sing) == plur def test_a_alt(self): p = inflect.engine() assert p.a("cat", 1) == "a cat" assert p.a("cat", 2) == "2 cat" with pytest.raises(TypeCheckError): p.a("") def test_a_and_an_same_method(self): assert inflect.engine.a == inflect.engine.an p = inflect.engine() assert p.a == p.an def test_no(self): p = inflect.engine() assert p.no("cat") == "no cats" assert p.no("cat", count=3) == "3 cats" assert p.no("cat", count="three") == "three cats" assert p.no("cat", count=1) == "1 cat" assert p.no("cat", count="one") == "one cat" assert p.no("mouse") == "no mice" p.num(3) assert p.no("cat") == "3 cats" @pytest.mark.parametrize( 'sing, plur', ( ("runs", "running"), ("dies", "dying"), ("glues", "gluing"), ("eyes", "eying"), ("skis", "skiing"), ("names", "naming"), ("sees", "seeing"), ("hammers", "hammering"), ("bats", "batting"), ("eats", "eating"), ("loves", "loving"), ("spies", "spying"), ("hoes", "hoeing"), ("alibis", "alibiing"), ("is", "being"), ("are", "being"), ("had", "having"), ("has", "having"), ), ) def test_prespart(self, sing, plur): p = inflect.engine() assert p.present_participle(sing) == plur @pytest.mark.parametrize( 'num, ord', ( ("1", "1st"), ("2", "2nd"), ("3", "3rd"), ("4", "4th"), ("10", "10th"), ("28", "28th"), ("100", "100th"), ("101", "101st"), ("1000", "1000th"), ("1001", "1001st"), ("0", "0th"), ("one", "first"), ("two", "second"), ("four", "fourth"), ("twenty", "twentieth"), ("one hundered", "one hunderedth"), ("one hundered and one", "one hundered and first"), ("zero", "zeroth"), ("n", "nth"), # bonus! ), ) def test_ordinal(self, num, ord): p = inflect.engine() assert p.ordinal(num) == ord def test_millfn(self): p = inflect.engine() millfn = p.millfn assert millfn(1) == " thousand" assert millfn(2) == " million" assert millfn(3) == " billion" assert millfn(0) == " " assert millfn(11) == " decillion" with pytest.raises(NumOutOfRangeError): millfn(12) def test_unitfn(self): p = inflect.engine() unitfn = p.unitfn assert unitfn(1, 2) == "one million" assert unitfn(1, 3) == "one billion" assert unitfn(5, 3) == "five billion" assert unitfn(5, 0) == "five " assert unitfn(0, 0) == " " def test_tenfn(self): p = inflect.engine() tenfn = p.tenfn assert tenfn(3, 1, 2) == "thirty-one million" assert tenfn(3, 0, 2) == "thirty million" assert tenfn(0, 1, 2) == "one million" assert tenfn(1, 1, 2) == "eleven million" assert tenfn(1, 0, 2) == "ten million" assert tenfn(1, 0, 0) == "ten " assert tenfn(0, 0, 0) == " " def test_hundfn(self): p = inflect.engine() hundfn = p.hundfn p._number_args = dict(andword="and") assert hundfn(4, 3, 1, 2) == "four hundred and thirty-one million, " assert hundfn(4, 0, 0, 2) == "four hundred million, " assert hundfn(4, 0, 5, 2) == "four hundred and five million, " assert hundfn(0, 3, 1, 2) == "thirty-one million, " assert hundfn(0, 0, 7, 2) == "seven million, " def test_enword(self): p = inflect.engine() enword = p.enword assert enword("5", 1) == "five, " p._number_args = dict(zero="zero", one="one", andword="and") assert enword("0", 1) == " zero, " assert enword("1", 1) == " one, " assert enword("347", 1) == "three, four, seven, " assert enword("34", 2) == "thirty-four , " assert enword("347", 2) == "thirty-four , seven, " assert enword("34768", 2) == "thirty-four , seventy-six , eight, " assert enword("1", 2) == "one, " assert enword("134", 3) == " one thirty-four , " assert enword("0", -1) == "zero" assert enword("1", -1) == "one" assert enword("3", -1) == "three , " assert enword("12", -1) == "twelve , " assert enword("123", -1) == "one hundred and twenty-three , " assert enword("1234", -1) == "one thousand, two hundred and thirty-four , " assert ( enword("12345", -1) == "twelve thousand, three hundred and forty-five , " ) assert ( enword("123456", -1) == "one hundred and twenty-three thousand, four hundred and fifty-six , " ) assert ( enword("1234567", -1) == "one million, two hundred and thirty-four thousand, " "five hundred and sixty-seven , " ) @pytest.mark.xfail(reason="doesn't use indicated word for 'one'") def test_enword_number_args_override(self): p = inflect.engine() p._number_args["one"] = "single" assert p.enword("1", 2) == "single, " def test_numwords(self): p = inflect.engine() numwords = p.number_to_words for n, word in ( ("1", "one"), ("10", "ten"), ("100", "one hundred"), ("1000", "one thousand"), ("10000", "ten thousand"), ("100000", "one hundred thousand"), ("1000000", "one million"), ("10000000", "ten million"), ("+10", "plus ten"), ("-10", "minus ten"), ("10.", "ten point"), (".10", "point one zero"), ): assert numwords(n) == word for n, word, _wrongword in ( # TODO: should be one point two three ("1.23", "one point two three", "one point twenty-three"), ): assert numwords(n) == word for n, txt in ( (3, "three bottles of beer on the wall"), (2, "two bottles of beer on the wall"), (1, "a solitary bottle of beer on the wall"), (0, "no more bottles of beer on the wall"), ): assert ( "{}{}".format( numwords(n, one="a solitary", zero="no more"), p.plural(" bottle of beer on the wall", n), ) == txt ) assert numwords(0, one="one", zero="zero") == "zero" assert numwords("1234") == "one thousand, two hundred and thirty-four" assert numwords("1234", wantlist=True) == [ "one thousand", "two hundred and thirty-four", ] assert numwords("1234567", wantlist=True) == [ "one million", "two hundred and thirty-four thousand", "five hundred and sixty-seven", ] assert numwords("+10", wantlist=True) == ["plus", "ten"] assert numwords("1234", andword="") == "one thousand, two hundred thirty-four" assert ( numwords("1234", andword="plus") == "one thousand, two hundred plus thirty-four" ) assert numwords(p.ordinal("21")) == "twenty-first" assert numwords("9", threshold=10) == "nine" assert numwords("10", threshold=10) == "ten" assert numwords("11", threshold=10) == "11" assert numwords("1000", threshold=10) == "1,000" assert numwords("123", threshold=10) == "123" assert numwords("1234", threshold=10) == "1,234" assert numwords("1234.5678", threshold=10) == "1,234.5678" assert numwords("1", decimal=None) == "one" assert ( numwords("1234.5678", decimal=None) == "twelve million, three hundred and forty-five " "thousand, six hundred and seventy-eight" ) def test_numwords_group_chunking_error(self): p = inflect.engine() with pytest.raises(BadChunkingOptionError): p.number_to_words("1234", group=4) @pytest.mark.parametrize( 'input,kwargs,expect', ( ("12345", dict(group=2), "twelve, thirty-four, five"), ("123456", dict(group=3), "one twenty-three, four fifty-six"), ("12345", dict(group=1), "one, two, three, four, five"), ( "1234th", dict(group=0, andword="and"), "one thousand, two hundred and thirty-fourth", ), ( "1234th", dict(group=0), "one thousand, two hundred and thirty-fourth", ), ("120", dict(group=2), "twelve, zero"), ("120", dict(group=2, zero="oh", one="unity"), "twelve, oh"), ( "555_1202", dict(group=1, zero="oh"), "five, five, five, one, two, oh, two", ), ( "555_1202", dict(group=1, one="unity"), "five, five, five, unity, two, zero, two", ), ( "123.456", dict(group=1, decimal="mark", one="one"), "one, two, three, mark, four, five, six", ), pytest.param( '12345', dict(group=3), 'one hundred and twenty-three', marks=pytest.mark.xfail(reason="'hundred and' missing"), ), pytest.param( '101', dict(group=2, zero="oh", one="unity"), "ten, unity", marks=pytest.mark.xfail(reason="ignoring 'one' param with group=2"), ), ), ) def test_numwords_group(self, input, kwargs, expect): p = inflect.engine() assert p.number_to_words(input, **kwargs) == expect def test_wordlist(self): p = inflect.engine() wordlist = p.join assert wordlist([]) == "" assert wordlist(("apple",)) == "apple" assert wordlist(("apple", "banana")) == "apple and banana" assert wordlist(("apple", "banana", "carrot")) == "apple, banana, and carrot" assert wordlist(("apple", "1,000", "carrot")) == "apple; 1,000; and carrot" assert ( wordlist(("apple", "1,000", "carrot"), sep=",") == "apple, 1,000, and carrot" ) assert ( wordlist(("apple", "banana", "carrot"), final_sep="") == "apple, banana and carrot" ) assert ( wordlist(("apple", "banana", "carrot"), final_sep=";") == "apple, banana; and carrot" ) assert ( wordlist(("apple", "banana", "carrot"), conj="or") == "apple, banana, or carrot" ) assert wordlist(("apple", "banana"), conj=" or ") == "apple or banana" assert wordlist(("apple", "banana"), conj="&") == "apple & banana" assert ( wordlist(("apple", "banana"), conj="&", conj_spaced=False) == "apple&banana" ) assert ( wordlist(("apple", "banana"), conj="& ", conj_spaced=False) == "apple& banana" ) assert ( wordlist(("apple", "banana", "carrot"), conj=" or ") == "apple, banana, or carrot" ) assert ( wordlist(("apple", "banana", "carrot"), conj="+") == "apple, banana, + carrot" ) assert ( wordlist(("apple", "banana", "carrot"), conj="&") == "apple, banana, & carrot" ) assert ( wordlist(("apple", "banana", "carrot"), conj="&", conj_spaced=False) == "apple, banana,&carrot" ) assert ( wordlist(("apple", "banana", "carrot"), conj=" &", conj_spaced=False) == "apple, banana, &carrot" ) def test_doc_examples(self): p = inflect.engine() assert p.plural_noun("I") == "we" assert p.plural_verb("saw") == "saw" assert p.plural_adj("my") == "our" assert p.plural_noun("saw") == "saws" assert p.plural("was") == "were" assert p.plural("was", 1) == "was" assert p.plural_verb("was", 2) == "were" assert p.plural_verb("was") == "were" assert p.plural_verb("was", 1) == "was" for errors, txt in ( (0, "There were no errors"), (1, "There was 1 error"), (2, "There were 2 errors"), ): assert ( "There {}{}".format( p.plural_verb("was", errors), p.no(" error", errors) ) == txt ) assert ( p.inflect( "There plural_verb('was',%d) no('error',%d)" % (errors, errors) ) == txt ) for num1, num2, txt in ((1, 2, "I saw 2 saws"), (2, 1, "we saw 1 saw")): assert ( "{}{}{} {}{}".format( p.num(num1, ""), p.plural("I"), p.plural_verb(" saw"), p.num(num2), p.plural_noun(" saw"), ) == txt ) assert ( p.inflect( "num(%d, False)plural('I') plural_verb('saw') " "num(%d) plural_noun('saw')" % (num1, num2) ) == txt ) assert p.a("a cat") == "a cat" for word, txt in ( ("cat", "a cat"), ("aardvark", "an aardvark"), ("ewe", "a ewe"), ("hour", "an hour"), ): assert p.a("{} {}".format(p.number_to_words(1, one="a"), word)) == txt p.num(2) def test_unknown_method(self): p = inflect.engine() with pytest.raises(AttributeError): p.unknown_method # noqa: B018 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tests/test_unicode.py0000644000175100001770000000062214640641406016761 0ustar00runnerdockerimport inflect class TestUnicode: """Unicode compatibility test cases""" def test_unicode_plural(self): """Unicode compatibility test cases for plural""" engine = inflect.engine() unicode_test_cases = {"cliché": "clichés", "ångström": "ångströms"} for singular, plural in unicode_test_cases.items(): assert plural == engine.plural(singular) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/towncrier.toml0000644000175100001770000000005414640641406015470 0ustar00runnerdocker[tool.towncrier] title_format = "{version}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719878406.0 inflect-7.3.1/tox.ini0000644000175100001770000000251514640641406014076 0ustar00runnerdocker[testenv] description = perform primary checks (tests, style, types, coverage) deps = setenv = PYTHONWARNDEFAULTENCODING = 1 commands = pytest {posargs} usedevelop = True extras = test [testenv:diffcov] description = run tests and check that diff from main is covered deps = {[testenv]deps} diff-cover commands = pytest {posargs} --cov-report xml diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 [testenv:docs] description = build the documentation extras = doc test changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html python -m sphinxlint \ # workaround for sphinx-contrib/sphinx-lint#83 --jobs 1 [testenv:finalize] description = assemble changelog and tag a release skip_install = True deps = towncrier jaraco.develop >= 7.23 pass_env = * commands = python -m jaraco.develop.finalize [testenv:release] description = publish the package to PyPI and GitHub skip_install = True deps = build twine>=3 jaraco.develop>=7.1 pass_env = TWINE_PASSWORD GITHUB_TOKEN setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" python -m build python -m twine upload dist/* python -m jaraco.develop.create-github-release