Skip to main content

harper_core/expr/
spelled_number_expr.rs

1use crate::expr::LongestMatchOf;
2use crate::patterns::{WhitespacePattern, WordSet};
3use crate::{Span, Token};
4
5use super::{Expr, SequenceExpr};
6
7/// Matches spelled-out numbers from one to ninety-nine
8#[derive(Default)]
9pub struct SpelledNumberExpr;
10
11impl Expr for SpelledNumberExpr {
12    fn run(&self, cursor: usize, tokens: &[Token], source: &[char]) -> Option<Span<Token>> {
13        if tokens.is_empty() {
14            return None;
15        }
16
17        // The numbers that can be in the 2nd position of a compound number.
18        // A subset of the standalone numbers since we can't say "twenty zero" or "twenty eleven"
19        // "Zero" and "ten" don't belong: twenty-one ✅ twenty-zero ❌ twenty-ten ❌
20        let units = &[
21            "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
22        ];
23
24        // These can't make a compound with `tens` but they can stand alone
25        let teens = &[
26            "ten",
27            "eleven",
28            "twelve",
29            "thirteen",
30            "fourteen",
31            "fifteen",
32            "sixteen",
33            "seventeen",
34            "eighteen",
35            "nineteen",
36        ];
37
38        // These can make a compound with the part_2 standalones above.
39        // "Ten" and "hundred" don't belong: twenty-one ✅ ten-one ❌ hundred-one ❌
40        let tens = &[
41            "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
42        ];
43
44        let single_words = WordSet::new(
45            &units
46                .iter()
47                .chain(teens.iter())
48                .chain(tens.iter())
49                .copied()
50                .chain(std::iter::once("zero"))
51                .collect::<Vec<&str>>(),
52        );
53
54        let tens_units_compounds = SequenceExpr::word_set(tens)
55            .then_any_of([
56                Box::new(|t: &Token, _s: &[char]| t.kind.is_hyphen()) as Box<dyn Expr>,
57                Box::new(WhitespacePattern),
58            ])
59            .then_word_set(units);
60
61        let expr = LongestMatchOf::new([
62            Box::new(single_words) as Box<dyn Expr>,
63            Box::new(tens_units_compounds),
64        ]);
65
66        expr.run(cursor, tokens, source)
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::SpelledNumberExpr;
73    use crate::Document;
74    use crate::expr::ExprExt;
75    use crate::linting::tests::SpanVecExt;
76
77    #[test]
78    fn matches_single_digit() {
79        let doc = Document::new_markdown_default_curated("one two three");
80        let matches = SpelledNumberExpr.iter_matches_in_doc(&doc);
81        assert_eq!(matches.count(), 3);
82    }
83
84    #[test]
85    fn matches_teens() {
86        let doc = Document::new_markdown_default_curated("ten eleven twelve");
87        let matches = SpelledNumberExpr.iter_matches_in_doc(&doc);
88        assert_eq!(matches.count(), 3);
89    }
90
91    #[test]
92    fn matches_tens() {
93        let doc = Document::new_markdown_default_curated("twenty thirty forty");
94        let matches = SpelledNumberExpr.iter_matches_in_doc(&doc);
95        assert_eq!(matches.count(), 3);
96    }
97
98    #[test]
99    fn matches_compound_numbers() {
100        let doc = Document::new_markdown_default_curated("twenty-one thirty-two");
101        let matches = SpelledNumberExpr
102            .iter_matches_in_doc(&doc)
103            .collect::<Vec<_>>();
104
105        // Debug output
106        println!("Found {} matches:", matches.len());
107        for m in &matches {
108            let text: String = doc.get_tokens()[m.start..m.end]
109                .iter()
110                .map(|t| doc.get_span_content_str(&t.span))
111                .collect();
112            println!("- '{text}' (span: {m:?})");
113        }
114
115        assert_eq!(matches.len(), 2);
116    }
117
118    #[test]
119    fn deep_thought() {
120        let doc = Document::new_markdown_default_curated(
121            "the answer to the ultimate question of life, the universe, and everything is forty-two",
122        );
123        let matches = SpelledNumberExpr
124            .iter_matches_in_doc(&doc)
125            .collect::<Vec<_>>();
126
127        dbg!(&matches);
128        dbg!(matches.to_strings(&doc));
129
130        assert_eq!(matches.to_strings(&doc), vec!["forty-two"]);
131    }
132
133    #[test]
134    fn jacksons() {
135        let doc = Document::new_markdown_default_curated(
136            "A, B, C It's easy as one, two, three. Or simple as Do-Re-Mi",
137        );
138        let matches = SpelledNumberExpr
139            .iter_matches_in_doc(&doc)
140            .collect::<Vec<_>>();
141
142        assert_eq!(matches.to_strings(&doc), vec!["one", "two", "three"]);
143    }
144
145    #[test]
146    fn orwell() {
147        let doc = Document::new_markdown_default_curated("Nineteen Eighty-Four");
148        let matches = SpelledNumberExpr
149            .iter_matches_in_doc(&doc)
150            .collect::<Vec<_>>();
151
152        assert_eq!(matches.to_strings(&doc), vec!["Nineteen", "Eighty-Four"]);
153    }
154
155    #[test]
156    fn get_smart() {
157        let doc = Document::new_markdown_default_curated(
158            "Maxwell Smart was Agent Eighty-Six, but who was Agent Ninety-Nine?",
159        );
160        let matches = SpelledNumberExpr
161            .iter_matches_in_doc(&doc)
162            .collect::<Vec<_>>();
163
164        assert_eq!(matches.to_strings(&doc), vec!["Eighty-Six", "Ninety-Nine"]);
165    }
166
167    #[test]
168    fn hyphens_or_spaces() {
169        let doc = Document::new_markdown_default_curated(
170            "twenty-one, thirty two, forty-three, fifty four, sixty-five, seventy six, eighty-seven, ninety eight",
171        );
172        let matches = SpelledNumberExpr
173            .iter_matches_in_doc(&doc)
174            .collect::<Vec<_>>();
175
176        assert_eq!(
177            matches.to_strings(&doc),
178            vec![
179                "twenty-one",
180                "thirty two",
181                "forty-three",
182                "fifty four",
183                "sixty-five",
184                "seventy six",
185                "eighty-seven",
186                "ninety eight",
187            ]
188        );
189    }
190
191    #[test]
192    fn waiting_since() {
193        let doc = Document::new_markdown_default_curated("I have been waiting since two hours.");
194        let matches = SpelledNumberExpr
195            .iter_matches_in_doc(&doc)
196            .collect::<Vec<_>>();
197
198        assert_eq!(matches.to_strings(&doc), vec!["two"]);
199    }
200}