harper_core/expr/
spelled_number_expr.rs1use crate::expr::LongestMatchOf;
2use crate::patterns::{WhitespacePattern, WordSet};
3use crate::{Span, Token};
4
5use super::{Expr, SequenceExpr};
6
7#[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 let units = &[
21 "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
22 ];
23
24 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 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 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}