harper_core/patterns/
word.rs1use super::SingleTokenPattern;
2
3use crate::{CharString, Token};
4
5#[derive(Clone, Copy, PartialEq, Eq)]
7enum CaseMatchingMode {
8 CaseInsensitive,
10 CaseSensitive,
12 StandardCase,
14}
15
16#[derive(Clone)]
18pub struct Word {
19 word: CharString,
21 case_mode: CaseMatchingMode,
23}
24
25impl Word {
26 pub fn new(word: &'static str) -> Self {
28 Self {
29 word: word.chars().collect(),
30 case_mode: CaseMatchingMode::CaseInsensitive,
31 }
32 }
33
34 pub fn from_chars(word: &[char]) -> Self {
36 Self {
37 word: word.iter().copied().collect(),
38 case_mode: CaseMatchingMode::CaseInsensitive,
39 }
40 }
41
42 pub fn from_char_string(word: CharString) -> Self {
44 Self {
45 word,
46 case_mode: CaseMatchingMode::CaseInsensitive,
47 }
48 }
49
50 pub fn new_exact(word: &'static str) -> Self {
52 Self {
53 word: word.chars().collect(),
54 case_mode: CaseMatchingMode::CaseSensitive,
55 }
56 }
57
58 pub fn new_standard_case(word: &'static str) -> Self {
62 Self {
63 word: word.chars().collect(),
64 case_mode: CaseMatchingMode::StandardCase,
65 }
66 }
67
68 fn is_standard_case(chars: &[char]) -> bool {
73 match chars.len() {
74 0 => false,
75 1 => true,
76 _ => {
77 let (first, rest) = chars.split_at(1);
78 let c0 = first[0];
79 let c0_is_lowercase = c0.is_lowercase();
80
81 let (basis, rest) = if !c0_is_lowercase {
82 (rest[0].is_lowercase(), &rest[1..])
83 } else {
84 (c0_is_lowercase, rest)
85 };
86
87 rest.iter().all(|c| c.is_lowercase() == basis)
88 }
89 }
90 }
91}
92
93impl SingleTokenPattern for Word {
94 fn matches_token(&self, token: &Token, source: &[char]) -> bool {
95 if !token.kind.is_word() {
96 return false;
97 }
98 if token.span.len() != self.word.len() {
99 return false;
100 }
101
102 let chars = token.get_ch(source);
103
104 match self.case_mode {
105 CaseMatchingMode::CaseSensitive => chars == self.word.as_slice(),
106 _ => {
107 if matches!(self.case_mode, CaseMatchingMode::StandardCase)
110 && !Self::is_standard_case(chars)
111 {
112 return false;
113 }
114 chars
115 .iter()
116 .zip(&self.word)
117 .all(|(a, b)| a.eq_ignore_ascii_case(b))
118 }
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use crate::{Document, Span, linting::tests::SpanVecExt, patterns::DocPattern};
126
127 use super::Word;
128
129 #[test]
130 fn fruit() {
131 let doc = Document::new_plain_english_curated("I ate a banana and an apple today.");
132
133 assert_eq!(
134 Word::new("banana").find_all_matches_in_doc(&doc),
135 vec![Span::new(6, 7)]
136 );
137 assert_eq!(
138 Word::new_exact("banana").find_all_matches_in_doc(&doc),
139 vec![Span::new(6, 7)]
140 );
141 }
142
143 #[test]
144 fn fruit_whack_capitalization() {
145 let doc = Document::new_plain_english_curated("I Ate A bAnaNa And aN apPlE today.");
146
147 assert_eq!(
148 Word::new("banana").find_all_matches_in_doc(&doc),
149 vec![Span::new(6, 7)]
150 );
151 assert_eq!(
152 Word::new_exact("banana").find_all_matches_in_doc(&doc),
153 vec![]
154 );
155 }
156
157 #[test]
158 fn standard_case_basic_matches() {
159 let doc =
160 Document::new_plain_english_curated("I ate a banana and BANANA and Banana today.");
161
162 let matches = Word::new_standard_case("banana").find_all_matches_in_doc(&doc);
164 assert_eq!(matches.to_strings(&doc), vec!["banana", "BANANA", "Banana"]);
165 }
166
167 #[test]
168 fn standard_case_rejects_mixed_case() {
169 let doc = Document::new_plain_english_curated("I saw iPhone and iPad and YouTube today.");
170
171 let iphone_matches: Vec<String> = Word::new_standard_case("iphone")
173 .find_all_matches_in_doc(&doc)
174 .to_strings(&doc);
175 let ipad_matches: Vec<String> = Word::new_standard_case("ipad")
176 .find_all_matches_in_doc(&doc)
177 .to_strings(&doc);
178 let youtube_matches: Vec<String> = Word::new_standard_case("youtube")
179 .find_all_matches_in_doc(&doc)
180 .to_strings(&doc);
181
182 assert_eq!(iphone_matches, Vec::<String>::new());
183 assert_eq!(ipad_matches, Vec::<String>::new());
184 assert_eq!(youtube_matches, Vec::<String>::new());
185 }
186
187 #[test]
188 fn standard_case_rejects_pascal_and_camel_case() {
189 let doc = Document::new_plain_english_curated(
190 "I saw BananaTree and bananaTree and BaNaNa today.",
191 );
192
193 let bananatree_matches: Vec<String> = Word::new_standard_case("bananatree")
195 .find_all_matches_in_doc(&doc)
196 .to_strings(&doc);
197 let banana_matches: Vec<String> = Word::new_standard_case("banana")
198 .find_all_matches_in_doc(&doc)
199 .to_strings(&doc);
200
201 assert_eq!(bananatree_matches, Vec::<String>::new());
202 assert_eq!(banana_matches, Vec::<String>::new());
203 }
204
205 #[test]
206 fn standard_case_single_letters() {
207 let doc = Document::new_plain_english_curated("A B C a b c I i");
208
209 let a_matches: Vec<String> = Word::new_standard_case("a")
211 .find_all_matches_in_doc(&doc)
212 .to_strings(&doc);
213 let b_matches: Vec<String> = Word::new_standard_case("b")
214 .find_all_matches_in_doc(&doc)
215 .to_strings(&doc);
216 let i_matches: Vec<String> = Word::new_standard_case("i")
217 .find_all_matches_in_doc(&doc)
218 .to_strings(&doc);
219
220 assert_eq!(a_matches, vec!["A", "a"]);
221 assert_eq!(b_matches, vec!["B", "b"]);
222 assert_eq!(i_matches, vec!["I", "i"]);
223 }
224
225 #[test]
226 fn standard_case_vs_exact_case() {
227 let doc = Document::new_plain_english_curated("I ate banana and BANANA and Banana.");
228
229 let exact_matches: Vec<String> = Word::new_exact("banana")
231 .find_all_matches_in_doc(&doc)
232 .to_strings(&doc);
233 assert_eq!(exact_matches, vec!["banana"]);
234
235 let standard_matches: Vec<String> = Word::new_standard_case("banana")
237 .find_all_matches_in_doc(&doc)
238 .to_strings(&doc);
239 assert_eq!(standard_matches, vec!["banana", "BANANA", "Banana"]);
240 }
241
242 #[test]
243 fn standard_case_edge_cases() {
244 let doc = Document::new_plain_english_curated("A a B b I i.");
245
246 let a_matches: Vec<String> = Word::new_standard_case("a")
248 .find_all_matches_in_doc(&doc)
249 .to_strings(&doc);
250 let i_matches: Vec<String> = Word::new_standard_case("i")
251 .find_all_matches_in_doc(&doc)
252 .to_strings(&doc);
253
254 assert_eq!(a_matches, vec!["A", "a"]);
255 assert_eq!(i_matches, vec!["I", "i"]);
256 }
257
258 #[test]
259 fn standard_case_complex_examples() {
260 let doc = Document::new_plain_english_curated(
261 "The iPhone is made by Apple but the apple is fruit.",
262 );
263
264 let apple_matches: Vec<String> = Word::new_standard_case("apple")
266 .find_all_matches_in_doc(&doc)
267 .to_strings(&doc);
268 assert_eq!(apple_matches, vec!["Apple", "apple"]);
269
270 let iphone_matches: Vec<String> = Word::new_standard_case("iphone")
272 .find_all_matches_in_doc(&doc)
273 .to_strings(&doc);
274 assert_eq!(iphone_matches, Vec::<String>::new());
275 }
276
277 #[test]
278 fn match_all_standard_3_letter_sets() {
279 let doc = Document::new_plain_english_curated("abc abC aBc aBC Abc AbC ABc ABC");
280
281 let matches: Vec<String> = Word::new_standard_case("abc")
282 .find_all_matches_in_doc(&doc)
283 .to_strings(&doc);
284
285 assert_eq!(matches, vec!["abc", "Abc", "ABC"]);
286 }
287}