README.md

# Plurality

Fast, zero-regex English inflection for Elixir. Pluralize, singularize, and detect noun forms with compile-time data and last-byte dispatch.

## Installation

Add `plurality` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:plurality, "~> 0.2.0"}
  ]
end
```

## Usage

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

```elixir
Plurality.pluralize("children", check: true)   # => "children" (not "childrens")
```

### Case preservation

```elixir
Plurality.pluralize("LEAF")                    # => "LEAVES"
Plurality.pluralize("Leaf")                    # => "Leaves"
Plurality.singularize("WOMEN")                 # => "WOMAN"
```

### Compound nouns

```elixir
Plurality.pluralize("status code")             # => "status codes"
Plurality.pluralize("field mouse")             # => "field mice"
Plurality.singularize("ice creams")            # => "ice cream"
```

### Classical mode

Pass `classical: true` to get Latin/Greek plural forms instead of modern English:

```elixir
Plurality.pluralize("aquarium")                  # => "aquariums"  (modern default)
Plurality.pluralize("aquarium", classical: true)  # => "aquaria"    (classical)
Plurality.pluralize("formula", classical: true)   # => "formulae"
Plurality.pluralize("trauma", classical: true)    # => "traumata"
```

Singularization handles both forms automatically, no flag needed:

```elixir
Plurality.singularize("aquariums")  # => "aquarium"
Plurality.singularize("aquaria")    # => "aquarium"
```

See the [Classical Mode guide](guides/classical-mode.md) for full details on
which forms are affected, app-wide config, and how default decisions were made.

### Domain customization

Override built-in data with your own irregulars and uncountables:

```elixir
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"  (falls through)
```

Or delegate globally so all `Plurality.*` calls use your overrides:

```elixir
config :plurality, custom_module: MyApp.Inflection
```

See the [Customization guide](guides/customization.md) for full documentation.

### Ash integration

Optional changes, validations, and calculations for Ash applications.
Compiles away to nothing if Ash isn't loaded.

```elixir
change {Plurality.Ash.Changes.Pluralize, attribute: :table_name, from: :name}
validate {Plurality.Ash.Validations.PluralForm, attribute: :table_name}
calculate :name_plural, :string, {Plurality.Ash.Calculations.Pluralize, attribute: :name}
```

See the [Ash Integration guide](guides/ash-integration.md) for full documentation.

## Why Plurality?

### Accuracy

Handles tricky business and technical English that other libraries miss:

| Word | Plurality |
|------|-----------|
| merchandise | merchandise (uncountable) |
| schema | schemas (modern English) |
| appendix | appendices |
| chassis | chassis (uncountable) |
| taxes | tax (singularize) |

### Performance

Zero regex at runtime. Suffix rules use last-byte dispatch via BEAM `select_val`
jump tables.

| Approach | ips | vs Regex |
|----------|-----|----------|
| Regex (typical) | 3.6K | 1x |
| **Last-byte dispatch** | **173K** | **48x** |

### Architecture

Three-tier resolution (Conway 1998, same as Rails and pluralize.js):

```
1. Uncountables (MapSet)  =>  word unchanged     (sheep, software, news)
2. Irregulars (Map)       =>  direct lookup       (child => children)
3. Suffix rules           =>  last-byte dispatch  (category => categories)
```

All data compiled into module attributes at build time. Zero runtime file I/O.

See the [Methodology guide](guides/methodology.md) for detailed documentation of
data sources, modern vs. classical decisions, corpus compliance, and suffix rule
design.

## Test suite

```
44 doctests, 2,325 tests, 0 failures
```

Validates **80,191 noun pairs** from two independent corpora in both directions:

- **AGID** -- 32,625 pairs (Automatically Generated Inflection Database)
- **NIH SPECIALIST Lexicon** -- 47,566 pairs (National Library of Medicine, 2025)
- Plus: irregular parity, classical mode, business domain, compound nouns, Ash integration, edge cases

## API

| Function | Description |
|----------|-------------|
| `pluralize/2` | Plural form, options: `check:`, `classical:` |
| `singularize/1` | Singular form |
| `plural?/1` | Check if plural |
| `singular?/1` | Check if singular |
| `inflect/3` | Count-based inflection with options |

## Guides

- [Classical Mode](guides/classical-mode.md) -- Latin/Greek plural forms
- [Customization](guides/customization.md) -- domain-specific overrides
- [Ash Integration](guides/ash-integration.md) -- changes, validations, calculations
- [Methodology](guides/methodology.md) -- data sources, design decisions, corpus compliance

## Acknowledgements

- [Exflect](https://hex.pm/packages/exflect) by Tyler Wray
- [Inflex](https://hex.pm/packages/inflex) by Miguel Palhas
- [pluralize](https://github.com/plurals/pluralize) by Blake Embrey
- [Rails ActiveSupport::Inflector](https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html)
- Damian Conway's 1998 paper [*An Algorithmic Approach to English Pluralization*](https://users.monash.edu/~damian/papers/extabs/Plurals.html)

## License

MIT -- see `LICENSE` file.