Skip to content

fix(itn/en): don't add up number words that aren't one number - #94

Merged
Alex-Wengg merged 3 commits into
FluidInference:mainfrom
rlchandani:fix/cardinal-no-summing
Nov 1, 2026
Merged

Alex-Wengg merged 3 commits into
FluidInference:mainfrom
rlchandani:fix/cardinal-no-summing

Conversation

@rlchandani

@rlchandani rlchandani commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Spoken number words that don't form one number are added up by the cardinal reader:

input today (2b37cd5) with this change
ten thirty 40 10 30
twenty thirty forty 2070 20 30 40
five and ten 15 5 and 10
five and six 56 5 and 6
item number three and four item number 34 item number 3 and 4
twenty one forty two 2043 21 42

grammatical_words_to_number adds every ones or tens word to the running total whatever came before it (the KNOWN_ISSUE in tests/extensive_tests.rs around line 972), and since 9035284 the digit-by-digit reading also applies once "and" has been stripped, so "five and six" became 56.

The change keeps track of what the previous word filled (unit, tens or a scale word) and returns None for a run that isn't one number: a ones word straight after a units word, or a teen/tens word after anything other than a scale word or the start. The sentence loop then falls back to shorter spans. Digit-by-digit reading now requires the run to contain no "and", so codes still read as before ("one two three" → 123), while "five and six" is a list.

Every real number reads as before: 105, 2005, 2100, 1984, 112, 3909, 1,200,000.

Tests

  • New test_runs_that_are_not_one_number.
  • Assertions that pinned the old sums are updated: "seven eighty eight" 95 → None, "twenty one forty two" 2043 → "21 42", "twenty one and forty two" → "21 and 42".
  • "twenty one five dollars" no longer gives "$26"; it now gives "2015 dollars" (the date tagger's year reading), which isn't right either, so that test only checks the "$26" is gone.
  • cargo test --release: all 14 test binaries pass (1,173 tests); the NeMo data files in tests/data/en pass at 100% before and after.

Found while using the library for dictation in a macOS app; replaying 1,179 real dictations, it no longer turns lists like "four, five and six and seven" into one number.

🤖 Generated with Claude Code

Maintainer changes (fdedaa1)

  • Telephone tagger: 4–6 spoken digits aren't a phone number. Once runs like "ten thirty" stopped being summed, whole-input normalize() fell through to the telephone tagger and formatted them as phone numbers. It now leaves them as written; normalize_sentence gives 10 30 etc. as before. The tagger's comment already said a phone number needs at least 7 digits (or 3 for a short code); only fewer than 3 was rejected.
normalize() input main this PR before now
ten thirty 40 103-0 ten thirty
twenty twenty 40 202-0 twenty twenty
twenty one forty two 2043 214-2 twenty one forty two
one two three four 123-4 123-4 1234
nine four one zero five 941-05 941-05 94105
five five five one two one two 555-1212 555-1212 555-1212
  • The doc comments that said "twenty one forty two" must read as the year 2042 now cite the time tagger instead (parse_old_year split it as 20 + "one forty two" = 2043 anyway). The digit-by-digit comment also mentions "a". The Last enum moved to module level.
  • Tests: "twenty one five dollars" is pinned to today's 2015 dollars as a KNOWN_ISSUE. test_boundary_very_long_input now asserts 100 × "twenty one" → 100 × "21" (main gave 168 ×12 and 2064), and its KNOWN_ISSUE is removed.

Checks: cargo test (1,174), --features ffi (1,182) and --release all pass, as do fst_parity (7/7) and the Swift FFI smoke test (142/142). The NeMo parity report is identical to main, with no regressions vs baseline. cargo fmt --check passes, and clippy reports only the warnings main already has.

Not changed here (follow-ups)

  • Scale words in any order ("two hundred three hundred" → 20300, "one thousand two thousand" → 3000; same as main). Rejecting a repeated or larger scale inside words_to_number alone made sentence mode worse: 203 100, "ten thousand crore" → 10 thousand 10000000. A proper fix needs the sentence span selection to prefer "two hundred" + "three hundred".
  • Whole-input normalize() drops unknown words through the telephone tagger ("twenty one five dollars" → 215, and on main "call me at five five five one two one two" → 555-1212), because its digit parser skips words it doesn't know. normalize_sentence isn't affected.

grammatical_words_to_number added every ones or tens word to the running
total whatever came before it, so "ten thirty" read as 40 and "twenty
one forty two" as 2043. Since 9035284 the digit-by-digit reading also
applied after "and" was stripped, so "five and six" read as 56.

Track what the previous word filled (unit, tens or a scale word) and
return None for a run that isn't one number; the sentence loop then
falls back to shorter spans. Digit-by-digit reading now requires the run
to contain no "and".
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-26T18:58:06.888002Z
We couldn’t complete this review. Comment @codex review to try again.
0dbd182 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Alex-Wengg Alex-Wengg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a good fix. cargo test --release passes locally (1,173 tests). I also compared 43 tricky inputs between main and this branch: every normalize_sentence change is an improvement ("ten thirty" 40 → 10 30, "it costs nine ninety nine" it costs 108 → it costs 9 99, "page twenty three twenty four" page 2027 → page 23 24). The real numbers I tried still read the same: 1984, 1905, 2021, 1776, 1105, 710, $5.50, 0.5, 21st, 12:05, 555-1212.

A few things outside the diff:

  1. Whole-input normalize() now falls through to the telephone tagger for more inputs. "ten thirty" → 103-0 (was 40), "twenty twenty" → 202-0, "ten ten" → 101-0, "twenty fifteen dollars" → 201-5. The old answers were wrong too, so this isn't a correctness regression, but 103-0 looks worse than 40. The telephone tagger already did this on main for "thirty five sixty two" → 356-2. Could you mention it in the PR description, or should we open a follow-up issue about the telephone tagger accepting 4-digit inputs?
  2. Two doc comments are now stale. The words_to_number doc (cardinal.rs:169-170) and the comment at cardinal.rs:541 say "twenty one forty two" must stay readable as the year 2042 for date::parse_old_year. words_to_number("twenty one forty two") now returns None, and parse_old_year actually split it as "twenty" + "one forty two" (giving 2043, never 2042).
  3. The KNOWN_ISSUE in test_boundary_very_long_input (tests/extensive_tests.rs around line 1187) says repeated "twenty one" gets combined into bigger numbers. That is probably no longer true after this change, so the comment and its loose assertion could likely be tightened.

Comment thread tests/en_tests.rs Outdated
assert_eq!(normalize_sentence("twenty one five dollars"), "$26");
// "twenty one five dollars" is not 26 dollars (the money tagger no longer
// sums the prefix; what wins instead is the date tagger's old-year read).
assert_ne!(normalize_sentence("twenty one five dollars"), "$26");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert_ne! here passes for almost any output. Could this pin today's actual output, "2015 dollars", with a KNOWN_ISSUE note (the date tagger reads it as a year)? That way the next change to this path shows up as a test diff.

Comment thread src/itn/en/cardinal.rs
found_number = true;
last = Last::Tens;
} else if word == "hundred" {
last = Last::Scale;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scale words are still added up in any order: "two hundred three hundred" → 20300 and "one thousand two thousand" → 3000 (same as main). It's the same kind of not-one-number run this PR fixes. Rejecting a scale that isn't smaller than the previous one would cover it. Fine as a follow-up if you'd rather keep this PR narrow.

Comment thread src/itn/en/cardinal.rs
if words.len() >= 2 && words.iter().all(|w| single_digit_char(w).is_some()) {
// Digits joined by "and" are a list ("five and six"), not a code.
if words.len() >= 2
&& words.len() == unfiltered.len()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: unfiltered also drops "a", so this check turns off the digit-by-digit reading for an "a" as well, not just "and". I couldn't think of a real input where that matters, but the comment could say both.

Comment thread src/itn/en/cardinal.rs Outdated
// A ones word may follow a tens word or a scale; a teen or tens word
// only a scale (or nothing).
#[derive(PartialEq)]
enum Last {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this enum would read a little more cleanly at module level next to ONES and TENS, instead of inside the function body.

- telephone: four to six spoken digits are not a phone number. With runs
  like "ten thirty" no longer summed, whole-input normalize() fell through
  to the telephone tagger and formatted them as "103-0" / "202-0"; they
  now stay as written, and "one two three four" / "nine four one zero five"
  read as 1234 / 94105 instead of "123-4" / "941-05".
- cardinal: move the Last enum to module level, fix the doc comments that
  still said "twenty one forty two" reads as the year 2042, and note that
  the digit-by-digit check also excludes runs with "a".
- tests: pin "twenty one five dollars" → "2015 dollars" as a KNOWN_ISSUE,
  and assert 100 × "twenty one" → 100 × "21" now that adjacent numbers
  aren't combined.
@Alex-Wengg
Alex-Wengg merged commit 1a58aba into FluidInference:main Nov 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants