pluralio 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pluralio/__init__.py +360 -0
- pluralio/core.py +223 -0
- pluralio/py.typed +0 -0
- pluralio/registry.py +125 -0
- pluralio/rules_en.py +176 -0
- pluralio/rules_es.py +190 -0
- pluralio-1.0.0.dist-info/METADATA +348 -0
- pluralio-1.0.0.dist-info/RECORD +10 -0
- pluralio-1.0.0.dist-info/WHEEL +4 -0
- pluralio-1.0.0.dist-info/licenses/LICENSE +21 -0
pluralio/__init__.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""pluralio — Pluralization and singularization for Python.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
- :func:`pluralize`: Convert a word to plural form.
|
|
5
|
+
- :func:`singularize`: Convert a word to singular form.
|
|
6
|
+
- :func:`supported_languages`: List registered language codes.
|
|
7
|
+
- :func:`add_irregular`: Add an irregular word pair (both directions).
|
|
8
|
+
- :func:`add_plural`: Add only the singular→plural direction.
|
|
9
|
+
- :func:`add_singular`: Add only the plural→singular direction.
|
|
10
|
+
- :func:`add_uncountable`: Mark a word as invariable.
|
|
11
|
+
- :func:`add_plural_rule`: Insert a pluralization regex rule.
|
|
12
|
+
- :func:`add_singular_rule`: Insert a singularization regex rule.
|
|
13
|
+
- :func:`register_language`: Register a new language with its rules.
|
|
14
|
+
- :func:`is_plural`: Check if a word is in plural form.
|
|
15
|
+
- :func:`is_singular`: Check if a word is in singular form.
|
|
16
|
+
- :class:`LanguageRules`: Dataclass for custom language rules.
|
|
17
|
+
- :func:`register`: Low-level registry registration.
|
|
18
|
+
- :func:`get_rules`: Low-level registry lookup.
|
|
19
|
+
|
|
20
|
+
Supported languages (built-in):
|
|
21
|
+
- English (``en``)
|
|
22
|
+
- Spanish (``es``)
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> import pluralio
|
|
26
|
+
>>> pluralio.pluralize("cat")
|
|
27
|
+
'cats'
|
|
28
|
+
>>> pluralio.pluralize("gato", lang="es")
|
|
29
|
+
'gatos'
|
|
30
|
+
>>> pluralio.singularize("cities")
|
|
31
|
+
'city'
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
36
|
+
|
|
37
|
+
from pluralio import rules_en, rules_es # noqa: F401 — triggers registration
|
|
38
|
+
from pluralio.core import pluralize, singularize
|
|
39
|
+
from pluralio.registry import (
|
|
40
|
+
LanguageRules,
|
|
41
|
+
get_rules,
|
|
42
|
+
register,
|
|
43
|
+
supported_languages,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
__version__ = version("pluralio")
|
|
48
|
+
except PackageNotFoundError:
|
|
49
|
+
__version__ = "0.0.0"
|
|
50
|
+
"""Package version string (read from installed package metadata)."""
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"pluralize",
|
|
54
|
+
"singularize",
|
|
55
|
+
"supported_languages",
|
|
56
|
+
"LanguageRules",
|
|
57
|
+
"register",
|
|
58
|
+
"get_rules",
|
|
59
|
+
"add_irregular",
|
|
60
|
+
"add_plural",
|
|
61
|
+
"add_singular",
|
|
62
|
+
"add_uncountable",
|
|
63
|
+
"add_plural_rule",
|
|
64
|
+
"add_singular_rule",
|
|
65
|
+
"register_language",
|
|
66
|
+
"is_plural",
|
|
67
|
+
"is_singular",
|
|
68
|
+
"__version__",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def add_irregular(singular: str, plural: str, lang: str = "en") -> None:
|
|
73
|
+
"""Add an irregular word pair for both pluralize and singularize.
|
|
74
|
+
|
|
75
|
+
This registers the word in both ``irregular_plurals``
|
|
76
|
+
(singular → plural) and ``irregular_singles`` (plural → singular),
|
|
77
|
+
so that both :func:`pluralize` and :func:`singularize` use the
|
|
78
|
+
mapping. The entries are stored in lowercase for case-insensitive
|
|
79
|
+
matching.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
singular: The singular form of the word.
|
|
83
|
+
plural: The plural form of the word.
|
|
84
|
+
lang: Language code to register the pair in. Defaults to ``"en"``.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> add_irregular("person", "people")
|
|
88
|
+
>>> pluralize("person")
|
|
89
|
+
'people'
|
|
90
|
+
>>> singularize("people")
|
|
91
|
+
'person'
|
|
92
|
+
"""
|
|
93
|
+
rules = get_rules(lang)
|
|
94
|
+
rules.irregular_plurals[singular.lower()] = plural.lower()
|
|
95
|
+
rules.irregular_singles[plural.lower()] = singular.lower()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def add_plural(singular: str, plural: str, lang: str = "en") -> None:
|
|
99
|
+
"""Add only the singular→plural direction for an irregular word.
|
|
100
|
+
|
|
101
|
+
Unlike :func:`add_irregular`, this only affects :func:`pluralize`.
|
|
102
|
+
:func:`singularize` will not know about the inverse mapping.
|
|
103
|
+
|
|
104
|
+
This is useful when the plural→singular direction follows a
|
|
105
|
+
different rule or needs a separate entry (e.g. Spanish accent
|
|
106
|
+
restoration).
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
singular: The singular form of the word.
|
|
110
|
+
plural: The plural form of the word.
|
|
111
|
+
lang: Language code. Defaults to ``"en"``.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> add_plural("joven", "jóvenes", lang="es")
|
|
115
|
+
>>> pluralize("joven", lang="es")
|
|
116
|
+
'jóvenes'
|
|
117
|
+
"""
|
|
118
|
+
rules = get_rules(lang)
|
|
119
|
+
rules.irregular_plurals[singular.lower()] = plural.lower()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def add_singular(plural: str, singular: str, lang: str = "en") -> None:
|
|
123
|
+
"""Add only the plural→singular direction for an irregular word.
|
|
124
|
+
|
|
125
|
+
Unlike :func:`add_irregular`, this only affects :func:`singularize`.
|
|
126
|
+
:func:`pluralize` will not know about the forward mapping.
|
|
127
|
+
|
|
128
|
+
This is useful for Spanish accent restoration where the plural
|
|
129
|
+
form has a predictable ending but the singular requires an accent
|
|
130
|
+
that the regex rules cannot infer.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
plural: The plural form of the word.
|
|
134
|
+
singular: The singular form of the word.
|
|
135
|
+
lang: Language code. Defaults to ``"en"``.
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
>>> add_singular("alemanes", "alemán", lang="es")
|
|
139
|
+
>>> singularize("alemanes", lang="es")
|
|
140
|
+
'alemán'
|
|
141
|
+
"""
|
|
142
|
+
rules = get_rules(lang)
|
|
143
|
+
rules.irregular_singles[plural.lower()] = singular.lower()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def add_uncountable(word: str, lang: str = "en") -> None:
|
|
147
|
+
"""Mark a word as uncountable (invariable).
|
|
148
|
+
|
|
149
|
+
Uncountable words are returned unchanged by both :func:`pluralize`
|
|
150
|
+
and :func:`singularize`. They are checked **before** irregulars
|
|
151
|
+
and regex rules, so they take highest priority.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
word: The word to mark as uncountable.
|
|
155
|
+
lang: Language code. Defaults to ``"en"``.
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
>>> add_uncountable("data")
|
|
159
|
+
>>> pluralize("data")
|
|
160
|
+
'data'
|
|
161
|
+
>>> singularize("data")
|
|
162
|
+
'data'
|
|
163
|
+
"""
|
|
164
|
+
rules = get_rules(lang)
|
|
165
|
+
rules.uncountable.add(word.lower())
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def add_plural_rule(pattern: str, replacement: str, lang: str = "en") -> None:
|
|
169
|
+
"""Insert a pluralization regex rule at the top (highest priority).
|
|
170
|
+
|
|
171
|
+
Rules are checked in order, and the first match wins. By inserting
|
|
172
|
+
at position 0, the new rule takes priority over all existing rules.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
pattern: Regular expression pattern to match (string, will be
|
|
176
|
+
compiled internally).
|
|
177
|
+
replacement: Replacement string for ``re.sub``.
|
|
178
|
+
lang: Language code. Defaults to ``"en"``.
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
>>> add_plural_rule(r"us$", "i")
|
|
182
|
+
>>> pluralize("cactus")
|
|
183
|
+
'cacti'
|
|
184
|
+
"""
|
|
185
|
+
rules = get_rules(lang)
|
|
186
|
+
rules.plural_rules.insert(0, (re.compile(pattern), replacement))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def add_singular_rule(pattern: str, replacement: str, lang: str = "en") -> None:
|
|
190
|
+
"""Insert a singularization regex rule at the top (highest priority).
|
|
191
|
+
|
|
192
|
+
Rules are checked in order, and the first match wins. By inserting
|
|
193
|
+
at position 0, the new rule takes priority over all existing rules.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
pattern: Regular expression pattern to match (string, will be
|
|
197
|
+
compiled internally).
|
|
198
|
+
replacement: Replacement string for ``re.sub``.
|
|
199
|
+
lang: Language code. Defaults to ``"en"``.
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
>>> add_singular_rule(r"i$", "us")
|
|
203
|
+
>>> singularize("cacti")
|
|
204
|
+
'cactus'
|
|
205
|
+
"""
|
|
206
|
+
rules = get_rules(lang)
|
|
207
|
+
rules.singular_rules.insert(0, (re.compile(pattern), replacement))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def register_language(
|
|
211
|
+
lang: str,
|
|
212
|
+
*,
|
|
213
|
+
plural_rules: list[tuple[str, str]] | None = None,
|
|
214
|
+
singular_rules: list[tuple[str, str]] | None = None,
|
|
215
|
+
irregular_plurals: dict[str, str] | None = None,
|
|
216
|
+
irregular_singles: dict[str, str] | None = None,
|
|
217
|
+
uncountable: set[str] | None = None,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Register a new language with its pluralization rules.
|
|
220
|
+
|
|
221
|
+
This is the high-level API for adding a completely new language.
|
|
222
|
+
It compiles the regex patterns, normalizes keys to lowercase,
|
|
223
|
+
auto-generates inverse irregular mappings, and registers the
|
|
224
|
+
resulting :class:`LanguageRules` in the global registry.
|
|
225
|
+
|
|
226
|
+
If ``plural_rules`` is omitted, a default rule of ``(r"$", "s")``
|
|
227
|
+
is used (append ``s``).
|
|
228
|
+
If ``singular_rules`` is omitted, a default rule of ``(r"s$", "")``
|
|
229
|
+
is used (strip trailing ``s``).
|
|
230
|
+
|
|
231
|
+
For every entry in ``irregular_plurals``, the inverse is
|
|
232
|
+
automatically added to ``irregular_singles`` unless an explicit
|
|
233
|
+
entry already exists.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
lang: ISO 639-1 language code for the new language.
|
|
237
|
+
plural_rules: List of ``(pattern, replacement)`` string tuples
|
|
238
|
+
for pluralization. Checked in order; first match wins.
|
|
239
|
+
singular_rules: List of ``(pattern, replacement)`` string tuples
|
|
240
|
+
for singularization. Checked in order; first match wins.
|
|
241
|
+
irregular_plurals: Mapping of singular → plural for irregular
|
|
242
|
+
words. Keys and values are normalized to lowercase.
|
|
243
|
+
irregular_singles: Mapping of plural → singular for irregular
|
|
244
|
+
words. Keys and values are normalized to lowercase.
|
|
245
|
+
Auto-populated from ``irregular_plurals`` where possible.
|
|
246
|
+
uncountable: Set of invariable words. Normalized to lowercase.
|
|
247
|
+
|
|
248
|
+
Example:
|
|
249
|
+
>>> register_language(
|
|
250
|
+
... "fr",
|
|
251
|
+
... plural_rules=[(r"$", "s")],
|
|
252
|
+
... singular_rules=[(r"s$", "")],
|
|
253
|
+
... irregular_plurals={"cheval": "chevaux"},
|
|
254
|
+
... uncountable={"information"},
|
|
255
|
+
... )
|
|
256
|
+
>>> pluralize("chat", lang="fr")
|
|
257
|
+
'chats'
|
|
258
|
+
>>> pluralize("cheval", lang="fr")
|
|
259
|
+
'chevaux'
|
|
260
|
+
>>> singularize("chevaux", lang="fr")
|
|
261
|
+
'cheval'
|
|
262
|
+
>>> pluralize("information", lang="fr")
|
|
263
|
+
'information'
|
|
264
|
+
"""
|
|
265
|
+
plurals = {k.lower(): v.lower() for k, v in (irregular_plurals or {}).items()}
|
|
266
|
+
singles = {k.lower(): v.lower() for k, v in (irregular_singles or {}).items()}
|
|
267
|
+
for singular, plural in plurals.items():
|
|
268
|
+
singles.setdefault(plural, singular)
|
|
269
|
+
|
|
270
|
+
compiled_plural = [(re.compile(p), r) for p, r in (plural_rules or [(r"$", "s")])]
|
|
271
|
+
compiled_singular = [(re.compile(p), r) for p, r in (singular_rules or [(r"s$", "")])]
|
|
272
|
+
|
|
273
|
+
register(LanguageRules(
|
|
274
|
+
code=lang,
|
|
275
|
+
irregular_plurals=plurals,
|
|
276
|
+
irregular_singles=singles,
|
|
277
|
+
plural_rules=compiled_plural,
|
|
278
|
+
singular_rules=compiled_singular,
|
|
279
|
+
uncountable={w.lower() for w in (uncountable or set())},
|
|
280
|
+
))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def is_plural(word: str, lang: str = "en") -> bool:
|
|
284
|
+
"""Check if a word is in its plural form.
|
|
285
|
+
|
|
286
|
+
A word is considered plural if singularizing it produces a
|
|
287
|
+
different word. Uncountable words are neither plural nor
|
|
288
|
+
singular — they return ``False`` for both checks.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
word: The word to check.
|
|
292
|
+
lang: ISO 639-1 language code. Defaults to ``"en"``.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
``True`` if the word is in plural form, ``False`` otherwise.
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
TypeError: If ``word`` is not a string.
|
|
299
|
+
ValueError: If ``lang`` is not a registered language.
|
|
300
|
+
|
|
301
|
+
Example:
|
|
302
|
+
>>> is_plural("cats")
|
|
303
|
+
True
|
|
304
|
+
>>> is_plural("cat")
|
|
305
|
+
False
|
|
306
|
+
>>> is_plural("sheep")
|
|
307
|
+
False
|
|
308
|
+
>>> is_plural("gatos", lang="es")
|
|
309
|
+
True
|
|
310
|
+
"""
|
|
311
|
+
if not isinstance(word, str):
|
|
312
|
+
raise TypeError(f"word must be str, got {type(word).__name__}")
|
|
313
|
+
stripped = word.strip()
|
|
314
|
+
if not stripped:
|
|
315
|
+
return False
|
|
316
|
+
rules = get_rules(lang)
|
|
317
|
+
lower = stripped.lower()
|
|
318
|
+
if lower in rules.uncountable:
|
|
319
|
+
return False
|
|
320
|
+
return singularize(stripped, lang=lang).lower() != lower
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def is_singular(word: str, lang: str = "en") -> bool:
|
|
324
|
+
"""Check if a word is in its singular form.
|
|
325
|
+
|
|
326
|
+
A word is considered singular if it is not plural and not
|
|
327
|
+
uncountable. Uncountable words are neither plural nor
|
|
328
|
+
singular — they return ``False`` for both checks.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
word: The word to check.
|
|
332
|
+
lang: ISO 639-1 language code. Defaults to ``"en"``.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
``True`` if the word is in singular form, ``False`` otherwise.
|
|
336
|
+
|
|
337
|
+
Raises:
|
|
338
|
+
TypeError: If ``word`` is not a string.
|
|
339
|
+
ValueError: If ``lang`` is not a registered language.
|
|
340
|
+
|
|
341
|
+
Example:
|
|
342
|
+
>>> is_singular("cat")
|
|
343
|
+
True
|
|
344
|
+
>>> is_singular("cats")
|
|
345
|
+
False
|
|
346
|
+
>>> is_singular("sheep")
|
|
347
|
+
False
|
|
348
|
+
>>> is_singular("gato", lang="es")
|
|
349
|
+
True
|
|
350
|
+
"""
|
|
351
|
+
if not isinstance(word, str):
|
|
352
|
+
raise TypeError(f"word must be str, got {type(word).__name__}")
|
|
353
|
+
stripped = word.strip()
|
|
354
|
+
if not stripped:
|
|
355
|
+
return False
|
|
356
|
+
rules = get_rules(lang)
|
|
357
|
+
lower = stripped.lower()
|
|
358
|
+
if lower in rules.uncountable:
|
|
359
|
+
return False
|
|
360
|
+
return singularize(stripped, lang=lang).lower() == lower
|
pluralio/core.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Core pluralization and singularization engine.
|
|
2
|
+
|
|
3
|
+
This module implements the main algorithm that transforms a word
|
|
4
|
+
between its singular and plural forms. The algorithm follows a
|
|
5
|
+
three-step priority chain for each word:
|
|
6
|
+
|
|
7
|
+
1. **Uncountable check** — if the word is in the language's
|
|
8
|
+
``uncountable`` set, it is returned unchanged.
|
|
9
|
+
2. **Irregular lookup** — if the word appears in the irregular
|
|
10
|
+
mapping (``irregular_plurals`` or ``irregular_singles``),
|
|
11
|
+
the mapped form is returned.
|
|
12
|
+
3. **Regex rules** — the first matching regex rule in the
|
|
13
|
+
ordered list is applied. If no rule matches, the word is
|
|
14
|
+
returned unchanged.
|
|
15
|
+
|
|
16
|
+
Additional features handled here:
|
|
17
|
+
|
|
18
|
+
- **Case preservation**: the output mirrors the casing of the
|
|
19
|
+
input (title case, all caps, lowercase).
|
|
20
|
+
- **Hyphenated words**: only the first segment is transformed
|
|
21
|
+
(e.g. ``"mother-in-law"`` → ``"mothers-in-law"``).
|
|
22
|
+
- **Count-aware pluralization**: when ``count == 1`` the word
|
|
23
|
+
is returned in singular form regardless of other rules.
|
|
24
|
+
- **Input validation**: non-string inputs raise ``TypeError``;
|
|
25
|
+
empty or whitespace-only strings are returned as-is.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
|
|
32
|
+
from pluralio.registry import LanguageRules, get_rules
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _match_case(source: str, target: str) -> str:
|
|
36
|
+
"""Apply the casing pattern of ``source`` to ``target``.
|
|
37
|
+
|
|
38
|
+
Three modes are supported:
|
|
39
|
+
- **All caps**: if ``source`` is entirely uppercase, ``target``
|
|
40
|
+
is returned in uppercase.
|
|
41
|
+
- **Title case**: if only the first character of ``source`` is
|
|
42
|
+
uppercase, ``target`` is capitalized on its first character.
|
|
43
|
+
- **Lowercase**: otherwise ``target`` is returned unchanged.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
source: The original word whose casing pattern to replicate.
|
|
47
|
+
target: The transformed word to re-case.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
``target`` with the casing of ``source`` applied.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> _match_case("Library", "libraries")
|
|
54
|
+
'Libraries'
|
|
55
|
+
>>> _match_case("LIBRARY", "libraries")
|
|
56
|
+
'LIBRARIES'
|
|
57
|
+
>>> _match_case("library", "libraries")
|
|
58
|
+
'libraries'
|
|
59
|
+
"""
|
|
60
|
+
if source.isupper():
|
|
61
|
+
return target.upper()
|
|
62
|
+
if source[0].isupper():
|
|
63
|
+
return target[0].upper() + target[1:]
|
|
64
|
+
return target
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _apply_rules(
|
|
68
|
+
word: str,
|
|
69
|
+
rules: LanguageRules,
|
|
70
|
+
irregulars: dict[str, str],
|
|
71
|
+
rule_list: list[tuple[re.Pattern[str], str]],
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Apply the three-step transformation chain to a single word.
|
|
74
|
+
|
|
75
|
+
This is the shared inner logic used by both :func:`pluralize` and
|
|
76
|
+
:func:`singularize`. It checks uncountables first, then irregulars,
|
|
77
|
+
then regex rules, preserving the input casing in the result.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
word: The word to transform (already stripped, no hyphens).
|
|
81
|
+
rules: The :class:`LanguageRules` for the target language.
|
|
82
|
+
irregulars: Either ``irregular_plurals`` or ``irregular_singles``
|
|
83
|
+
from the language rules, depending on direction.
|
|
84
|
+
rule_list: Either ``plural_rules`` or ``singular_rules`` from
|
|
85
|
+
the language rules, depending on direction.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The transformed word with original casing preserved.
|
|
89
|
+
"""
|
|
90
|
+
lower = word.lower()
|
|
91
|
+
if lower in rules.uncountable:
|
|
92
|
+
return word
|
|
93
|
+
if lower in irregulars:
|
|
94
|
+
return _match_case(word, irregulars[lower])
|
|
95
|
+
for pattern, replacement in rule_list:
|
|
96
|
+
if pattern.search(lower):
|
|
97
|
+
return _match_case(word, pattern.sub(replacement, lower, count=1))
|
|
98
|
+
return word
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _pluralize_hyphenated(word: str, lang: str, count: int | None) -> str:
|
|
102
|
+
"""Pluralize only the first segment of a hyphenated word.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
word: The hyphenated word (e.g. ``"mother-in-law"``).
|
|
106
|
+
lang: Language code to pass through.
|
|
107
|
+
count: Count value to pass through for count-aware behavior.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The word with its first segment pluralized
|
|
111
|
+
(e.g. ``"mothers-in-law"``).
|
|
112
|
+
"""
|
|
113
|
+
parts = word.split("-")
|
|
114
|
+
parts[0] = pluralize(parts[0], lang=lang, count=count)
|
|
115
|
+
return "-".join(parts)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _singularize_hyphenated(word: str, lang: str) -> str:
|
|
119
|
+
"""Singularize only the first segment of a hyphenated word.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
word: The hyphenated word (e.g. ``"mothers-in-law"``).
|
|
123
|
+
lang: Language code to pass through.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The word with its first segment singularized
|
|
127
|
+
(e.g. ``"mother-in-law"``).
|
|
128
|
+
"""
|
|
129
|
+
parts = word.split("-")
|
|
130
|
+
parts[0] = singularize(parts[0], lang=lang)
|
|
131
|
+
return "-".join(parts)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def pluralize(word: str, lang: str = "en", count: int | None = None) -> str:
|
|
135
|
+
"""Convert a word to its plural form.
|
|
136
|
+
|
|
137
|
+
The function follows the three-step priority chain
|
|
138
|
+
(uncountable → irregular → regex) and preserves the input
|
|
139
|
+
casing in the output.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
word: The singular word to pluralize.
|
|
143
|
+
lang: ISO 639-1 language code. Defaults to ``"en"``.
|
|
144
|
+
count: Optional integer count. When ``count == 1`` the word
|
|
145
|
+
is returned unchanged (singular form). Any other value
|
|
146
|
+
(including ``0``, negative numbers, and ``None``) produces
|
|
147
|
+
the plural form.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The plural form of ``word``, with original casing preserved.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
TypeError: If ``word`` is not a string.
|
|
154
|
+
ValueError: If ``lang`` is not a registered language.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> pluralize("cat")
|
|
158
|
+
'cats'
|
|
159
|
+
>>> pluralize("box")
|
|
160
|
+
'boxes'
|
|
161
|
+
>>> pluralize("child")
|
|
162
|
+
'children'
|
|
163
|
+
>>> pluralize("gato", lang="es")
|
|
164
|
+
'gatos'
|
|
165
|
+
>>> pluralize("item", count=1)
|
|
166
|
+
'item'
|
|
167
|
+
>>> pluralize("Library")
|
|
168
|
+
'Libraries'
|
|
169
|
+
>>> pluralize("mother-in-law")
|
|
170
|
+
'mothers-in-law'
|
|
171
|
+
"""
|
|
172
|
+
if not isinstance(word, str):
|
|
173
|
+
raise TypeError(f"word must be str, got {type(word).__name__}")
|
|
174
|
+
stripped = word.strip()
|
|
175
|
+
if not stripped:
|
|
176
|
+
return word
|
|
177
|
+
if count is not None and count == 1:
|
|
178
|
+
return word
|
|
179
|
+
rules = get_rules(lang)
|
|
180
|
+
if "-" in stripped:
|
|
181
|
+
return _pluralize_hyphenated(stripped, lang, count)
|
|
182
|
+
return _apply_rules(stripped, rules, rules.irregular_plurals, rules.plural_rules)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def singularize(word: str, lang: str = "en") -> str:
|
|
186
|
+
"""Convert a word to its singular form.
|
|
187
|
+
|
|
188
|
+
The function follows the three-step priority chain
|
|
189
|
+
(uncountable → irregular → regex) and preserves the input
|
|
190
|
+
casing in the output.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
word: The plural word to singularize.
|
|
194
|
+
lang: ISO 639-1 language code. Defaults to ``"en"``.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
The singular form of ``word``, with original casing preserved.
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
TypeError: If ``word`` is not a string.
|
|
201
|
+
ValueError: If ``lang`` is not a registered language.
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
>>> singularize("cities")
|
|
205
|
+
'city'
|
|
206
|
+
>>> singularize("mice")
|
|
207
|
+
'mouse'
|
|
208
|
+
>>> singularize("lápices", lang="es")
|
|
209
|
+
'lápiz'
|
|
210
|
+
>>> singularize("Libraries")
|
|
211
|
+
'Library'
|
|
212
|
+
>>> singularize("mothers-in-law")
|
|
213
|
+
'mother-in-law'
|
|
214
|
+
"""
|
|
215
|
+
if not isinstance(word, str):
|
|
216
|
+
raise TypeError(f"word must be str, got {type(word).__name__}")
|
|
217
|
+
stripped = word.strip()
|
|
218
|
+
if not stripped:
|
|
219
|
+
return word
|
|
220
|
+
rules = get_rules(lang)
|
|
221
|
+
if "-" in stripped:
|
|
222
|
+
return _singularize_hyphenated(stripped, lang)
|
|
223
|
+
return _apply_rules(stripped, rules, rules.irregular_singles, rules.singular_rules)
|
pluralio/py.typed
ADDED
|
File without changes
|