Fast, zero-regex English noun inflection for Elixir.
Operates on English nouns only. Other parts of speech (pronouns, adjectives, adverbs, etc.) are not supported and will produce undefined results.
Plurality provides 99%+ accuracy on business and technical English, using compile-time binary pattern matching instead of runtime regex.
Quick start
Plurality.pluralize("leaf") #=> "leaves"
Plurality.singularize("leaves") #=> "leaf"
Plurality.plural?("leaves") #=> true
Plurality.singular?("leaf") #=> true
Plurality.inflect("leaf", 2) #=> "leaves"
Plurality.inflect("leaf", 1) #=> "leaf"Safe pluralization
Pass check: true to avoid double-pluralizing words that are already plural.
Plurality.pluralize("children", check: true) #=> "children"
Plurality.pluralize("people", check: true) #=> "people"
Plurality.pluralize("leaf", check: true) #=> "leaves"Case preservation
Input casing is detected and applied to the output automatically. Three styles are recognized: ALL CAPS, Title Case, and lowercase.
Plurality.pluralize("LEAF") #=> "LEAVES"
Plurality.pluralize("Leaf") #=> "Leaves"
Plurality.singularize("WOMEN") #=> "WOMAN"
Plurality.singularize("Children") #=> "Child"Uncountable words
Words like "sheep", "software", and "news" are returned unchanged by both
pluralize/2 and singularize/1. Detection functions plural?/1 and
singular?/1 return true for uncountables since they are valid in either
context.
Plurality.pluralize("sheep") #=> "sheep"
Plurality.singularize("sheep") #=> "sheep"
Plurality.plural?("sheep") #=> true
Plurality.singular?("sheep") #=> trueCompound nouns
Multi-word nouns are split on the last space and the final word is inflected.
Known multi-word irregulars in the data (e.g., "coup d'etat") take priority
over splitting.
Plurality.pluralize("status code") #=> "status codes"
Plurality.pluralize("field mouse") #=> "field mice"
Plurality.singularize("ice creams") #=> "ice cream"Domain customization
Use Plurality.Custom to define a module with your own irregulars and
uncountables. Overrides compile into function heads that take priority
over the built-in data.
defmodule MyApp.Inflection do
use Plurality.Custom,
irregulars: [{"regex", "regexen"}],
uncountables: ["kubernetes"]
end
MyApp.Inflection.pluralize("regex") #=> "regexen"
MyApp.Inflection.pluralize("kubernetes") #=> "kubernetes"
MyApp.Inflection.pluralize("leaf") #=> "leaves"See Plurality.Custom for full documentation.
Classical mode
Pass classical: true to get Latin/Greek plural forms instead of modern
English forms. Default behavior is unchanged — classical mode is opt-in.
Plurality.pluralize("aquarium") #=> "aquariums"
Plurality.pluralize("aquarium", classical: true) #=> "aquaria"
Plurality.pluralize("trauma", classical: true) #=> "traumata"
Plurality.pluralize("formula", classical: true) #=> "formulae"Singularization handles both forms automatically, no option needed:
Plurality.singularize("aquariums") #=> "aquarium"
Plurality.singularize("aquaria") #=> "aquarium"App-wide config
To make all Plurality.* functions automatically delegate to your custom
module, set it in your application config:
# config/config.exs
config :plurality, custom_module: MyApp.InflectionWith this config, any code calling Plurality.pluralize/2 — including
third-party libraries — will use your custom overrides transparently.
To enable classical mode globally:
# config/config.exs
config :plurality, classical: truePer-call classical: true/false overrides the app-wide setting.
Ash integration
If your project uses Ash, Plurality ships optional changes, validations, and calculations that compile away to nothing if Ash is not loaded. See:
Plurality.Ash.Changes.PluralizePlurality.Ash.Changes.SingularizePlurality.Ash.Validations.PluralFormPlurality.Ash.Validations.SingularFormPlurality.Ash.Calculations.PluralizePlurality.Ash.Calculations.Singularize
Architecture
Plurality uses a three-tier resolution engine (originated by Conway's 1998 paper, same architecture as Rails and pluralize.js):
- Uncountables (
MapSet, O(1)) — word returned unchanged - Irregulars (
Map, O(1)) — direct lookup from merged cross-ecosystem data - Suffix rules (last-byte dispatch) — BEAM
select_valjump table, zero regex
All data is compiled into module attributes at build time from TSV/TXT files
in priv/. There is zero runtime file I/O and zero regex execution.
Data
Plurality ships with ~1,110 irregular pairs, ~1,022 uncountable words, and
108 suffix rules — curated from multiple sources across the Elixir, Ruby,
JavaScript, Go, C#, and Rust ecosystems, then verified against Oxford and
Merriam-Webster dictionaries. All data is loaded at compile time from
priv/data/.
Summary
Types
Options for pluralize/2 and inflect/3.
Functions
Inflects a word based on a numeric count.
Returns true if the word is in plural form (or is uncountable).
Converts an English noun to its plural form.
Returns true if the word is in singular form (or is uncountable).
Converts an English noun from plural to singular form.
Types
Options for pluralize/2 and inflect/3.
:check— whentrue, tests whether the word is already in plural form before transforming it. If the word is already plural, it is returned unchanged. This prevents double-pluralization errors like"children"→"childrens". Defaults tofalse.:classical— whentrue, uses classical Latin/Greek plural forms instead of modern English forms (e.g.,"aquarium"→"aquaria"instead of"aquariums"). Whenfalseor omitted, falls back to the app-wide configconfig :plurality, classical: true, which defaults tofalse.
Functions
@spec inflect(word :: String.t(), count :: integer(), opts :: pluralize_opts()) :: String.t()
Inflects a word based on a numeric count.
Returns the singular form when count is exactly 1, and the plural form
for all other values (including 0, negative numbers, and numbers greater
than 1).
If a custom module is configured, delegates to that module's inflect/3.
This follows standard English convention where zero and plural counts use the plural form: "0 items", "2 items", but "1 item".
Options
Accepts the same options as pluralize/2 (:check, :classical).
Options are passed through to pluralize/2 when the count is not 1.
Examples
iex> Plurality.inflect("leaf", 1)
"leaf"
iex> Plurality.inflect("leaf", 2)
"leaves"
iex> Plurality.inflect("leaf", 0)
"leaves"
iex> Plurality.inflect("child", 1)
"child"
iex> Plurality.inflect("child", 3)
"children"
iex> Plurality.inflect("sheep", 1)
"sheep"
iex> Plurality.inflect("sheep", 100)
"sheep"
iex> Plurality.inflect("aquarium", 2, classical: true)
"aquaria"
iex> Plurality.inflect("aquarium", 1, classical: true)
"aquarium"
Returns true if the word is in plural form (or is uncountable).
Uncountable words like "sheep" and "software" return true for both
plural?/1 and singular?/1, since they are valid in either context.
If a custom module is configured, delegates to that module's plural?/1.
Detection strategy
Detection is derived from transformation (inspired by pluralize.js):
- Uncountable? →
true - Known irregular plural (in the plural→singular map)? →
true - Try
singularize/1— if it produces a different word, andpluralize/1on that result gives back the original, thentrue
Examples
iex> Plurality.plural?("leaves")
true
iex> Plurality.plural?("children")
true
iex> Plurality.plural?("leaf")
false
iex> Plurality.plural?("sheep")
true
iex> Plurality.plural?("")
false
@spec pluralize(word :: String.t(), opts :: pluralize_opts()) :: String.t()
Converts an English noun to its plural form.
Returns the word unchanged if it is uncountable (e.g., "sheep", "software").
Preserves the casing style of the input (ALL CAPS, Title Case, or lowercase).
If a custom module is configured via config :plurality, custom_module: MyApp.Inflection,
this function delegates to that module's pluralize/2 instead.
Options
:check(boolean()) - Whentrue, checks whether the word is already plural and returns it unchanged if so. Prevents double-pluralization (e.g.,"children"staying"children"instead of becoming"childrens"). Defaults tofalse.:classical(boolean()) - Whentrue, uses classical Latin/Greek plural forms instead of modern English forms. When omitted, falls back toconfig :plurality, classical: true(defaultfalse).
Resolution order
- Uncountable? → return unchanged
check: trueand already a known irregular plural? → return unchangedclassical: trueand word has a known classical form? → return classical plural- Known irregular singular? → return the mapped plural
check: trueand rule-based detection says plural? → return unchanged- Compound noun? → split on last space, inflect last word
- Apply suffix rules (last-byte dispatch, classical-aware)
Examples
iex> Plurality.pluralize("leaf")
"leaves"
iex> Plurality.pluralize("child")
"children"
iex> Plurality.pluralize("sheep")
"sheep"
iex> Plurality.pluralize("schema")
"schemas"
iex> Plurality.pluralize("children", check: true)
"children"
iex> Plurality.pluralize("LEAF")
"LEAVES"
iex> Plurality.pluralize("Leaf")
"Leaves"
iex> Plurality.pluralize("status code")
"status codes"
iex> Plurality.pluralize("")
""Classical mode
iex> Plurality.pluralize("aquarium", classical: true)
"aquaria"
iex> Plurality.pluralize("formula", classical: true)
"formulae"
iex> Plurality.pluralize("trauma", classical: true)
"traumata"
iex> Plurality.pluralize("cactus")
"cactuses"
iex> Plurality.pluralize("leaf", classical: true)
"leaves"
Returns true if the word is in singular form (or is uncountable).
Uncountable words return true for both plural?/1 and singular?/1.
If a custom module is configured, delegates to that module's singular?/1.
Detection strategy
- Uncountable? →
true - Known irregular singular (in the singular→plural map)? →
true - Known irregular plural (in the plural→singular map)? →
false - Otherwise, check whether the word appears to already be plural via rule-based round-tripping
Examples
iex> Plurality.singular?("leaf")
true
iex> Plurality.singular?("child")
true
iex> Plurality.singular?("leaves")
false
iex> Plurality.singular?("sheep")
true
iex> Plurality.singular?("")
false
Converts an English noun from plural to singular form.
Returns the word unchanged if it is uncountable. Preserves casing style.
If a custom module is configured via config :plurality, custom_module: MyApp.Inflection,
this function delegates to that module's singularize/1 instead.
For words that appear in both the uncountables set and the irregular plurals
map (e.g., "data", "graffiti"), the irregular reverse lookup takes
priority so that singularization resolves correctly:
Plurality.singularize("data") #=> "datum"
Plurality.singularize("graffiti") #=> "graffito"Singularization is mode-independent — it handles both modern and classical
plural forms without needing a classical: option:
Plurality.singularize("aquariums") #=> "aquarium"
Plurality.singularize("aquaria") #=> "aquarium"Resolution order
- Known irregular plural (including classical forms)? → return the mapped singular
- Uncountable? → return unchanged
- Compound noun? → split on last space, singularize last word
- Apply suffix rules (last-byte dispatch)
Examples
iex> Plurality.singularize("leaves")
"leaf"
iex> Plurality.singularize("children")
"child"
iex> Plurality.singularize("sheep")
"sheep"
iex> Plurality.singularize("taxes")
"tax"
iex> Plurality.singularize("statuses")
"status"
iex> Plurality.singularize("WOMEN")
"WOMAN"
iex> Plurality.singularize("status codes")
"status code"
iex> Plurality.singularize("")
""Classical plural forms
iex> Plurality.singularize("aquaria")
"aquarium"
iex> Plurality.singularize("antennae")
"antenna"
iex> Plurality.singularize("traumata")
"trauma"