diff --git a/crates/genie-core/src/tools/number_words.rs b/crates/genie-core/src/tools/number_words.rs index 8754c6a1..dc00911e 100644 --- a/crates/genie-core/src/tools/number_words.rs +++ b/crates/genie-core/src/tools/number_words.rs @@ -7,15 +7,25 @@ enum CardinalWord { } pub(crate) fn parse_spoken_number(tokens: &[&str], start: usize) -> Option<(u64, usize)> { - if let Some(Ok(value)) = tokens.get(start).map(|token| token.parse::()) { - return Some((value, start + 1)); - } - let mut total: u64 = 0; let mut group: u64 = 0; let mut index = start; let mut matched = false; + // A digit token ("5") immediately followed by a spelled magnitude + // ("hundred"/"thousand") must scale by it: "5 hundred" == 500, "3 thousand" + // == 3000. The old fast path returned the bare digit and silently dropped + // the magnitude ("5 hundred" -> 5). A digit with no trailing magnitude still + // returns immediately, exactly as before. + if let Some(Ok(value)) = tokens.get(start).map(|token| token.parse::()) { + if !matches!(tokens.get(start + 1).copied(), Some("hundred" | "thousand")) { + return Some((value, start + 1)); + } + group = value; + index = start + 1; + matched = true; + } + while let Some(&token) = tokens.get(index) { if matched && token == "and" @@ -107,3 +117,36 @@ fn cardinal_word(token: &str) -> Option { _ => return None, }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn digit_before_spelled_magnitude_scales() { + // "5 hundred" is 500; the old digit fast path returned 5 and silently + // dropped the magnitude word, so calculator/duration paths were wrong. + assert_eq!(parse_spoken_number(&["5", "hundred"], 0), Some((500, 2))); + assert_eq!(parse_spoken_number(&["3", "thousand"], 0), Some((3000, 2))); + } + + #[test] + fn digit_magnitude_continues_like_spelled_numbers() { + let tokens = ["2", "thousand", "five", "hundred"]; + assert_eq!(parse_spoken_number(&tokens, 0), Some((2500, 4))); + let tokens = ["5", "hundred", "and", "twenty"]; + assert_eq!(parse_spoken_number(&tokens, 0), Some((520, 4))); + } + + #[test] + fn bare_digit_without_magnitude_is_unchanged() { + assert_eq!(parse_spoken_number(&["5", "minutes"], 0), Some((5, 1))); + assert_eq!(parse_spoken_number(&["42"], 0), Some((42, 1))); + } + + #[test] + fn spelled_magnitudes_still_parse() { + assert_eq!(parse_spoken_number(&["five", "hundred"], 0), Some((500, 2))); + assert_eq!(parse_spoken_number(&["hundred"], 0), Some((100, 1))); + } +}