Read decades like 1990s as plural years in TTS normalizer (#3930)

* Read decades like 1990s as plural years in TTS normalizer

Found while testing crates/tts-text/src/normalize.rs:~654 (cardinal/year branch) and parse_continuation ~line 680

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* tts-text: decade tests in the it binary, with boundary cases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
NetterSilver
2026-11-02 06:38:18 +01:00
committed by GitHub
parent 5c73ee4af6
commit 68919ed87a
3 changed files with 50 additions and 0 deletions
+22
View File
@@ -654,9 +654,31 @@ fn parse_number_core(rest: &str, lang: Lang) -> Option<(Vec<Token>, usize)> {
if !grouped && digits.len() == 4 && (1100..=2099).contains(&value) && year_is_standalone(rest, used) {
return Some((year_words(value, lang), used));
}
// A decade such as "1990s" reads as a plural year: "nineteen nineties".
if !grouped
&& digits.len() == 4
&& (1100..=2099).contains(&value)
&& value.is_multiple_of(10)
&& matches!(peek(rest, used), Some('s'))
&& !peek(rest, used + 1).is_some_and(char::is_alphanumeric)
{
let mut words = year_words(value, lang);
if let Some(Token::Word(last)) = words.last_mut() {
*last = pluralize_decade(last);
}
return Some((words, used + 1));
}
Some((english_cardinal(value).into_iter().map(Token::Word).collect(), used))
}
/// Plural of the last word of a round year: ninety → nineties, hundred → hundreds.
fn pluralize_decade(word: &str) -> String {
match word.strip_suffix('y') {
Some(stem) => format!("{stem}ies"),
None => format!("{word}s"),
}
}
fn has_leading_zero_str(digits: &str) -> bool {
digits.len() > 1 && digits.starts_with('0')
}
+27
View File
@@ -0,0 +1,27 @@
use filmcraft_tts_text::{Lang, Token, normalize};
fn spoken(text: &str) -> String {
let words: Vec<String> = normalize(text, Lang::EnUs).unwrap().into_iter().filter_map(|t| if let Token::Word(w) = t { Some(w) } else { None }).collect();
words.join(" ")
}
#[test]
fn bare_year_is_unchanged() {
assert_eq!(spoken("in 1990"), "in nineteen ninety");
}
#[test]
fn decade_reads_as_plural_year() {
assert_eq!(spoken("the 1990s"), "the nineteen nineties");
assert_eq!(spoken("the 2010s."), "the twenty tens");
assert_eq!(spoken("the 1900s"), "the nineteen hundreds");
assert_eq!(spoken("the 1100s"), "the eleven hundreds");
assert_eq!(spoken("the 2090s"), "the twenty nineties");
}
#[test]
fn only_round_years_followed_by_a_lone_s_are_decades() {
assert_ne!(spoken("the 1995s"), "the nineteen ninety fives");
assert!(!spoken("the 1990sx").contains("nineties"));
assert!(!spoken("the 3000s").contains("thousands"));
}
+1
View File
@@ -0,0 +1 @@
mod decades;