english 0.4.0

English inflector decliner conjugator from Wiktionary data
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# english

[![Crates.io](https://img.shields.io/crates/v/english)](https://crates.io/crates/english)
[![Docs.rs](https://docs.rs/english/badge.svg)](https://docs.rs/english)
![License](https://img.shields.io/crates/l/english)
[![Discord](https://img.shields.io/discord/123456789012345678.svg?logo=discord&logoColor=white&color=5865F2)](https://discord.gg/tDBPkdgApN)

**english** is a fast, lightweight English inflection library written in Rust.
Total bundled data is about 1 MB. It provides highly accurate verb conjugation
and noun/adjective/adverb declension from processed Wiktionary data, making it
useful for real-time procedural text generation.

## ⚡ Speed and Accuracy

Evaluation of the English inflector (`cargo xtask accuracy`, using the
2026-07-02 Wiktionary dump) and performance benchmarking
(`examples/speedmark.rs`; measured rows averaged over 10 release runs) shows:

| Part of Speech | Correct / Total | Accuracy | Variant Gap | Throughput (calls/sec) | Time per Call |
|----------------|-----------------|----------|-------------|------------------------|---------------|
| **Nouns**      | 225900 / 225900 | 100.00% | 0           | 6,072,834              | 164.67 ns     |
| **Verbs**      | 151544 / 151544 | 100.00% | 1           | 9,549,311              | 104.73 ns     |
| **Adjectives** | 121548 / 121550 | 99.998% | 8           | 6,859,637              | 145.79 ns     |
| **Adverbs**    | 25123 / 25125   | 99.99%  | 2           | 11,144,625             | 89.73 ns      |

The accuracy percentages measure **recall through any published key**: the share
of plain attested Wiktionary slots reproducible via the bare lemma **or** any
`_n` sense key. They do not measure precision, nor whether the natural bare-lemma
call returns the primary/most-standard attested form.

For that natural-call view, `cargo xtask accuracy` also reports bare-lemma
correctness:

| Part of Speech | Bare Primary / Total | Bare Accuracy | Standard Form Demoted to `_n` | Over-generated |
|----------------|----------------------|---------------|-------------------------------|----------------|
| **Nouns**      | 223505 / 225900      | 98.94%        | 1655                          | 740            |
| **Verbs**      | 150787 / 151544      | 99.50%        | 626                           | 131            |
| **Adjectives** | 120924 / 121550      | 99.48%        | 624                           | 0              |
| **Adverbs**    | 24882 / 25125        | 99.03%        | 241                           | 0              |

Benchmarks are machine- and workload-dependent; run `cargo run -p english --example speedmark --release` on your target platform for local numbers.

### Breaking change in 0.4: spelling cleanup

The possessive-determiner enum variant is now `Case::PersonalPossessive`, and the
last-occurrence string helper is now `EnglishCore::replace_last_occurrence`.

### Breaking change in 0.3: underscore sense-key format

Sense-numbered keys now use the canonical underscore format (`die_2`, `lie_2`).
The pre-0.3 adjacent-digit spelling (`die2`, `lie2`) was removed because it is
ambiguous with ordinary digit-bearing words such as `mp3`, `F16`, and `F2`, which
are now always treated opaquely unless they are exact table keys.

## 📦 Installation

```bash
cargo add english
```

Then in your code:

```rust
use english::*;
fn main() {
    // --- Mixed Sentence Example ---
    let subject_number = Number::Plural;
    let subject = format!(
        "{} {}",
        English::verb(
            "run",
            &Person::First,
            &Number::Singular,
            &Tense::Present,
            &Form::Participle
        ),
        English::noun("child", &subject_number)
    ); // running children
    let verb = English::verb(
        "steal",
        &Person::Third,
        &subject_number,
        &Tense::Past,
        &Form::Finite,
    ); // stole
    let object = count_with_number("potato", 7); // 7 potatoes

    let sentence = format!("The {} {} {}.", subject, verb, object);
    assert_eq!(sentence, "The running children stole 7 potatoes.");

    // --- Nouns ---
    assert_eq!(English::noun("cat", &Number::Plural), "cats");
    assert_eq!(English::noun("child", &Number::Plural), "children");
    // Sense-numbered keys expose homographs and attested variants.
    assert_eq!(English::noun("die_2", &Number::Plural), "dice");
    assert_eq!(count("man", 2), "men");
    assert_eq!(count_with_number("nickel", 3), "3 nickels");
    assert_eq!(English::noun("sheep", &Number::Plural), "sheep");

    // --- Verbs ---
    assert_eq!(
        English::verb(
            "pick",
            &Person::Third,
            &Number::Singular,
            &Tense::Past,
            &Form::Finite
        ),
        "picked"
    );
    assert_eq!(
        English::verb(
            "walk",
            &Person::First,
            &Number::Singular,
            &Tense::Present,
            &Form::Participle
        ),
        "walking"
    );
    assert_eq!(
        English::verb(
            "go",
            &Person::First,
            &Number::Singular,
            &Tense::Past,
            &Form::Participle
        ),
        "gone"
    );
    // Sense-numbered keys distinguish homographs: "lie" (recline) and "lie_2"
    // (tell an untruth) inflect differently.
    assert_eq!(
        English::verb(
            "lie",
            &Person::Third,
            &Number::Singular,
            &Tense::Past,
            &Form::Finite
        ),
        "lay"
    );
    assert_eq!(
        English::verb(
            "lie_2",
            &Person::Third,
            &Number::Singular,
            &Tense::Past,
            &Form::Finite
        ),
        "lied"
    );
    assert_eq!(
        English::verb(
            "be",
            &Person::First,
            &Number::Singular,
            &Tense::Present,
            &Form::Finite
        ),
        "am"
    );

    // --- Adjectives ---
    assert_eq!(English::adj("bad", &Degree::Comparative), "worse");
    assert_eq!(English::adj("bad", &Degree::Superlative), "worst");
    assert_eq!(English::adj("bad_2", &Degree::Comparative), "badder");
    assert_eq!(English::adj("bad_3", &Degree::Comparative), "more bad");
    assert_eq!(English::adj("bad_3", &Degree::Positive), "bad");

    // --- Adverbs ---
    assert_eq!(English::adverb("quickly", &Degree::Comparative), "more quickly");
    assert_eq!(English::adverb("well", &Degree::Comparative), "better");
    assert_eq!(English::adverb("badly", &Degree::Superlative), "worst");
    assert_eq!(English::adverb("fast", &Degree::Comparative), "faster");
    assert_eq!(English::adverb("early", &Degree::Superlative), "earliest");
    assert_eq!(English::adverb("far", &Degree::Comparative), "farther");
    assert_eq!(English::adverb("far_2", &Degree::Comparative), "further");

    // --- Pronouns ---
    assert_eq!(
        English::pronoun(
            &Person::First,
            &Number::Singular,
            &Gender::Neuter,
            &Case::PersonalPossessive
        ),
        "my"
    );
    assert_eq!(
        English::pronoun(
            &Person::First,
            &Number::Singular,
            &Gender::Neuter,
            &Case::Possessive
        ),
        "mine"
    );

    // --- Possessives ---
    assert_eq!(English::add_possessive("dog"), "dog's");
    assert_eq!(English::add_possessive("dogs"), "dogs'");
}
```

---

For a more involved but still minimal example of building a small domain layer on
top of `english`, see `examples/semantic_triples.rs`:

```bash
cargo run -p english --example semantic_triples
```

It shows custom noun/verb/adj/adv types, semantic triples,
perspective-sensitive rendering, modifiers, complements, adjuncts, and
agreement-driven pronoun and tense shifts.

## Case handling

`english` accepts lowercase lemmas, but the public API also has a simple casing
convenience for common sentence text:

```rust
use english::{English, Number};

assert_eq!(English::noun("child", &Number::Plural), "children");
assert_eq!(English::noun("Child", &Number::Plural), "Children");
assert_eq!(English::noun("CHILD", &Number::Plural), "CHILDREN");
assert_eq!(English::noun("McDonald", &Number::Plural), "McDonalds");
```

Title-case and ALL-CAPS words may use lowercase table rows and then restore the
input style. Mixed case is deliberately not guessed, so proper-name-like tokens
such as `McDonald` fall through to the regular rule on the original spelling.
This is **not** semantic proper-noun or acronym detection: names, brands,
initialisms, and house-style casing may need caller-side normalization.

## Helper limitations

* `count` and `count_with_number` are small conveniences for `u32` counts. Exactly
  `1` is singular; every other value is plural. Decimal, negative, formatted, or
  localized quantities are caller responsibilities.
* `English::add_possessive` uses a simple trailing-`s` rule: `dogs'`, but also
  `bus'` and `James'`. Apply your own style guide if you prefer `bus's` or
  `James's`.

## 🔧 Crate Overview

### `english`

> The public API for verb conjugation and noun/adjective/adverb declension.

* Combines optimized data generated from `extractor` with inflection logic from
  `english-core`.
* Pure Rust; one third-party dependency (`phf`) plus the first-party
  `english-core`.
* PHF-backed irregular lookups with regular-rule fallback.
* Code generation ensures no runtime penalty.

### `english-core`

> The compact fallback / prediction engine for English inflection.

* Implements small rule approximations for conjugation/declension.
* Used by the extractor to classify forms as regular or irregular.
* Has no data dependency — logic-only.
* Can be used standalone for a smaller footprint, but is not guaranteed correct
  for arbitrary out-of-vocabulary words; use `english` for the table-backed API.

### `extractor`

> A tool to process and refine Wiktionary data.

* Parses large English Wiktionary dumps.
* Extracts verb, noun, adjective, and adverb forms.
* Uses `english-core` to filter out regular forms, preserving only irregulars.
* Numbers homograph senses **deterministically** by a pure sort of their emitted
  forms (no lockfile, no identity, no human review — see below).
* Emits every plain attested variant as its own sense-numbered key (`cactus_2`  *cacti*, `cactus_3` → …), numbered in form-signature order.
* Generates the static PHF tables used in `english`.
* `cargo xtask accuracy` measures both any-key reachability and bare-lemma
  primary correctness; run it before and after any rule or table change.

---

## 📦 Obtaining Wiktionary Data & Running the Extractor

This project relies on raw data extracted from Wiktionary. Current version built
with data from 2026-07-02.

- [Wiktextract (GitHub)]https://github.com/tatuylonen/wiktextract
- [Kaikki.org raw data]https://kaikki.org/dictionary/rawdata.html

### Steps

1. Download the **raw Wiktextract JSONL dump** (~20 GB) from
   [Kaikki.org]https://kaikki.org/dictionary/rawdata.html.
2. Place the file somewhere accessible (e.g. `../rawwiki.jsonl`).
3. From the repository root, run: `cargo xtask refresh-data --dump ../rawwiki.jsonl`.
4. The generated Rust tables are written to `crates/english/generated`; intermediate
   CSV/JSONL artifacts to `data/intermediate`.
5. Review `git diff crates/english/generated/`, then run
   `cargo xtask check-registry` before committing.

After committing regenerated tables, run `cargo xtask accuracy` to score them
against the dump. It measures the currently compiled committed tables, and needs
either the cached `data/intermediate/english_filtered.jsonl` or an explicit
`--dump /path/to/raw-wiktextract.jsonl`.

## Adverb degree

Adverbs use the same two-tier design as the other parts of speech (table first,
rule fallback) but a **different rule** from adjectives:

* **Adjectives** have a linguistically informed rule — short words take suffixal
  `-er`/`-est` (`fast → faster`), longer words go periphrastic (`beautiful → more
  beautiful`).
* **Adverbs** have a deliberately **conservative, unconditional periphrastic
  rule**: `EnglishCore::comparative_adverb("quickly") == "more quickly"`. On the
  dump, 99.1% of gradable adverbs take `more`/`most`, and a suffixal guess would
  be wrong far more often than right (`quicklier`, `abruptlier`, ...).

The small closed set of adverbs that inflect otherwise is table-driven, exactly
like irregular nouns/verbs:

* flat adverbs (homographs of their adjective): `fast → faster`, `hard → harder`,
  `early → earlier`, `late → later`;
* suppletives: `well → better`, `badly → worse`, `far → farther` (with
  `far_2 → further`);
* locational/directional adverbs inflect with `farther/further`: `downhill →
  farther downhill`, `east → farther east`.

Where an adverb attests both the periphrastic and a single-word form, the
periphrastic wins the bare key (so `quickly → more quickly`) and the single-word
form is a numbered key (`quickly_2 → quicklier`). This is correct for the large
`-ly` class; the handful of flat adverbs that also list `more X` (e.g. `deep`) get
the periphrastic on the bare key and the suffixal at `deep_2 → deeper` — both stay
reachable, so any-key accuracy is unaffected.

**Accuracy:** 25123 / 25125 slots (99.99%); the two residuals are malformed
Wiktionary forms (`more ... humouredly` / `most ... humouredly`). See
`English::adverb` and `EnglishCore::adverb` for the API.

## Deterministic sense numbering

Homographs that inflect differently share a lemma and are disambiguated by a
numeric suffix (`lie` → *lay*, `lie_2` → *lied*; `die_2` → *dice*). The suffix is
assigned by a **pure, transparent sort** of the forms — no lockfile, no frozen
identity, no human-review workflow.

For each `(lemma, part of speech)` the extractor:

1. gathers every plain attested inflection pattern and drops the one the regular
   rule engine already produces (so the rule serves it at runtime);
2. sorts the survivors by emitted **form signature** (standard senses before
   slang/soft ones, then lexicographically);
3. hands out suffixes: if a regular form was dropped, the bare key is reserved
   for the rule engine and numbering starts at `_2`; otherwise the first-sorted
   survivor takes the bare lemma. The rest number upward (`_2`, `_3`, …).

Because the key is a function of the forms alone, **reordering the dump's entries
can never change the output** — generation is reproducible. `cargo xtask
check-registry` is a dump-free consistency gate: it verifies the committed tables
are well-formed, have unique and correctly shaped keys, no empty columns, and
preserve rule/table layering. It deliberately does **not** verify that a row's
irregular values are correct — those are attested data, not derivable without the
dump. `cargo xtask accuracy` (with the dump) is the authoritative value check.

**Stability guarantee — "fairly stable", not frozen.** Keys are deterministic but
**not immutable**. If Wiktionary adds, removes, or edits a lemma's attested forms,
the sort can renumber that lemma's `_<n>` keys — a lexicographically earlier new
variant deliberately shifts later ones up. Do not persist `_n` keys as stable
semantic IDs across data refreshes. What stays true is that the set of forms a
lemma exposes is reachable through some key, and that a slang-only sense never
takes the bare key from a standard one. Review the `crates/english/generated/`
diff on a refresh as you would any regenerated artifact.

Current runtime lookup is permissive for compatibility: a `_<digits>` suffix is
stripped when it resolves to a real table key or to a tabled base lemma. That
means a nonsensical key like `child_999` may behave like `child` rather than being
reported invalid. If your application needs strict key validation, keep your own
allow-list from the versioned generated tables until a strict public API exists.

## Benchmarks

Performance benchmarks were run on an M2 MacBook.

Benchmarking this kind of project requires opinionated decisions: many words have
alternative inflections, Wiktionary data is imperfect, and countability tags can
be inconsistent. Treat bundled numbers as a baseline, take them with a grain of
salt, and benchmark your own use cases. Suggestions to improve benchmarking are
welcome.

## Disclaimer

Wiktionary data is unstable and subject to upstream changes. The generated lookup
tables in `crates/english/generated/*_phf.rs` are the source of truth for a given
revision. Sense-numbered keys (`lie_2`, `die_2`, …) are deterministic for a dump
but may be renumbered when upstream forms change — see
[Deterministic sense numbering](#deterministic-sense-numbering).

## Inspirations and Thanks

- Ole in the Bevy Discord suggested `phf` instead of sorted arrays, which resulted
  in up to 40% speedups.
- <https://github.com/atteo/evo-inflector>
- <https://github.com/plurals/pluralize>

## 📄 License

- Code: Dual licensed under MIT and Apache © 2024 [gold-silver-copper]https://github.com/gold-silver-copper
  - [MIT]https://opensource.org/licenses/MIT
  - [Apache-2.0]https://www.apache.org/licenses/LICENSE-2.0

- Data: Wiktionary content is dual-licensed under
  - [CC BY-SA 4.0]https://creativecommons.org/licenses/by-sa/4.0/
  - [GNU FDL]https://www.gnu.org/licenses/fdl-1.3.html