# `Plurality`
[🔗](https://github.com/jeffhuen/plurality/blob/main/lib/plurality.ex#L1)

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")                   #=> true

## Compound 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.Inflection

With 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: true

Per-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.Pluralize`
* `Plurality.Ash.Changes.Singularize`
* `Plurality.Ash.Validations.PluralForm`
* `Plurality.Ash.Validations.SingularForm`
* `Plurality.Ash.Calculations.Pluralize`
* `Plurality.Ash.Calculations.Singularize`

## Architecture

Plurality uses a three-tier resolution engine (originated by Conway's 1998
paper, same architecture as Rails and pluralize.js):

1. **Uncountables** (`MapSet`, O(1)) — word returned unchanged
2. **Irregulars** (`Map`, O(1)) — direct lookup from merged cross-ecosystem data
3. **Suffix rules** (last-byte dispatch) — BEAM `select_val` jump 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/`.

# `pluralize_opts`

```elixir
@type pluralize_opts() :: [check: boolean(), classical: boolean()]
```

Options for `pluralize/2` and `inflect/3`.

* `:check` — when `true`, 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 to `false`.

* `:classical` — when `true`, uses classical Latin/Greek plural forms
  instead of modern English forms (e.g., `"aquarium"` → `"aquaria"`
  instead of `"aquariums"`). When `false` or omitted, falls back to the
  app-wide config `config :plurality, classical: true`, which defaults
  to `false`.

# `inflect`

```elixir
@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"

# `plural?`

```elixir
@spec plural?(word :: String.t()) :: boolean()
```

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](https://github.com/plurals/pluralize)):

1. Uncountable? → `true`
2. Known irregular plural (in the plural→singular map)? → `true`
3. Try `singularize/1` — if it produces a different word, and
   `pluralize/1` on that result gives back the original, then `true`

## 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

# `pluralize`

```elixir
@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()`) - When `true`, 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 to `false`.

  * `:classical` (`boolean()`) - When `true`, uses classical Latin/Greek
    plural forms instead of modern English forms. When omitted, falls back
    to `config :plurality, classical: true` (default `false`).

## Resolution order

1. Uncountable? → return unchanged
2. `check: true` and already a known irregular plural? → return unchanged
3. `classical: true` and word has a known classical form? → return classical plural
4. Known irregular singular? → return the mapped plural
5. `check: true` and rule-based detection says plural? → return unchanged
6. Compound noun? → split on last space, inflect last word
7. 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"

# `singular?`

```elixir
@spec singular?(word :: String.t()) :: boolean()
```

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

1. Uncountable? → `true`
2. Known irregular singular (in the singular→plural map)? → `true`
3. Known irregular plural (in the plural→singular map)? → `false`
4. 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

# `singularize`

```elixir
@spec singularize(word :: String.t()) :: String.t()
```

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

1. Known irregular plural (including classical forms)? → return the mapped singular
2. Uncountable? → return unchanged
3. Compound noun? → split on last space, singularize last word
4. 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"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
