Three-tier resolution engine for English noun inflection.
This module is the core of Plurality. It loads irregular pairs, uncountable words, and suffix rules at compile time, then exposes functions that resolve any English noun through three tiers in order:
Resolution tiers
Tier 1 — Uncountables (MapSet, O(1) membership test)
Words like "sheep", "software", and "news" that have no distinct plural
form. Returned unchanged by both pluralize/2 and singularize/1.
Built from ~1,022 words curated from multiple sources and verified against Oxford and Merriam-Webster dictionaries.
Tier 2 — Irregulars (Map, O(1) lookup)
Direct singular↔plural mappings for words whose plural form cannot be
predicted by suffix rules (e.g., "child" → "children",
"person" → "people").
Built from ~1,110 pairs curated from multiple sources, with modern English
forms preferred over classical Latin (e.g., "schema" → "schemas"
instead of "schemata").
Two maps are maintained:
singular → plural— used bypluralize/2plural → singular— used bysingularize/1, built from ALL sources (including overridden entries) so both old and new plural forms resolve
Tier 3 — Suffix rules (last-byte dispatch, O(1))
Pattern-based transformation using Plurality.Rules. Extracts the last byte
of the word, dispatches via BEAM select_val jump table, then confirms the
full suffix with a sized-skip binary match. See Plurality.Rules for details.
Compile-time data pipeline
All data is loaded from TSV and TXT files in priv/ at compile time via
module attributes. The pipeline:
- Load pre-merged data from
priv/data/irregulars.tsvandpriv/data/uncountables.txt - Build forward map (singular → plural) and reverse map (plural → singular)
- Auto-exclude words from uncountables if they appear in irregulars
with a different plural form (e.g.,
"data"is uncountable but"data"→"datum"exists in irregulars) - Apply force overrides (
@force_uncountable,@force_countable) - Build downcased lookup maps with lowercase-entry priority
There is zero runtime file I/O, zero regex, and zero ETS usage.
Case-insensitive matching
All lookups are performed against downcased maps. When case-variant entries
exist in the source data (e.g., "jerry" → "jerries" AND
"Jerry" → "Jerrys"), the lowercase entry takes priority since it
represents the common noun form. This is implemented by sorting entries
lowercase-first and using Map.put_new/3.
Singularize ordering
singularize/1 checks the irregular reverse map before the uncountables
set. This is intentional: words like "data", "graffiti", and "testes"
appear in both sets, and singularization should resolve them to their base
forms ("datum", "graffito", "testis").
Usage
This module is not typically called directly. Use the public API in
Plurality instead, which delegates to this module.
Summary
Functions
Inflects a word based on count.
Returns true if the word is in plural form or is uncountable.
Converts a word to its plural form.
Returns true if the word is in singular form or is uncountable.
Converts a word to its singular form.
Functions
@spec inflect( word :: String.t(), count :: integer(), opts :: Plurality.pluralize_opts() ) :: String.t()
Inflects a word based on count.
See Plurality.inflect/2 for full documentation.
Returns true if the word is in plural form or is uncountable.
See Plurality.plural?/1 for full documentation.
@spec pluralize(word :: String.t(), opts :: Plurality.pluralize_opts()) :: String.t()
Converts a word to its plural form.
See Plurality.pluralize/2 for full documentation.
Returns true if the word is in singular form or is uncountable.
See Plurality.singular?/1 for full documentation.
Converts a word to its singular form.
See Plurality.singularize/1 for full documentation.