harper_core/spell/
mod.rs

1use crate::{CharString, CharStringExt, WordMetadata};
2
3pub use self::dictionary::Dictionary;
4pub use self::fst_dictionary::FstDictionary;
5pub use self::merged_dictionary::MergedDictionary;
6pub use self::mutable_dictionary::MutableDictionary;
7pub use self::word_id::WordId;
8
9mod dictionary;
10mod fst_dictionary;
11mod merged_dictionary;
12mod mutable_dictionary;
13mod rune;
14mod word_id;
15mod word_map;
16
17#[derive(PartialEq, Debug, Hash, Eq)]
18pub struct FuzzyMatchResult<'a> {
19    pub word: &'a [char],
20    pub edit_distance: u8,
21    pub metadata: &'a WordMetadata,
22}
23
24impl PartialOrd for FuzzyMatchResult<'_> {
25    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
26        self.edit_distance.partial_cmp(&other.edit_distance)
27    }
28}
29
30/// Returns whether the two words are the same, expect that one is written
31/// with 'ou' and the other with 'o'.
32///
33/// E.g. "color" and "colour"
34pub(crate) fn is_ou_misspelling(a: &[char], b: &[char]) -> bool {
35    if a.len().abs_diff(b.len()) != 1 {
36        return false;
37    }
38
39    let mut a_iter = a.iter();
40    let mut b_iter = b.iter();
41
42    loop {
43        match (
44            a_iter.next().map(char::to_ascii_lowercase),
45            b_iter.next().map(char::to_ascii_lowercase),
46        ) {
47            (Some('o'), Some('o')) => {
48                let mut a_next = a_iter.next().map(char::to_ascii_lowercase);
49                let mut b_next = b_iter.next().map(char::to_ascii_lowercase);
50                if a_next != b_next {
51                    if a_next == Some('u') {
52                        a_next = a_iter.next().map(char::to_ascii_lowercase);
53                    } else if b_next == Some('u') {
54                        b_next = b_iter.next().map(char::to_ascii_lowercase);
55                    }
56
57                    if a_next != b_next {
58                        return false;
59                    }
60                }
61            }
62            (Some(a_char), Some(b_char)) => {
63                if !a_char.eq_ignore_ascii_case(&b_char) {
64                    return false;
65                }
66            }
67            (None, None) => return true,
68            _ => return false,
69        }
70    }
71}
72
73/// Returns whether the two words are the same, expect for a single confusion of:
74///
75/// - `s` and `z`. E.g."realize" and "realise"
76/// - `s` and `c`. E.g. "defense" and "defence"
77/// - `k` and `c`. E.g. "skepticism" and "scepticism"
78pub(crate) fn is_cksz_misspelling(a: &[char], b: &[char]) -> bool {
79    if a.len() != b.len() {
80        return false;
81    }
82    if a.is_empty() {
83        return true;
84    }
85
86    // the first character must be the same
87    if !a[0].eq_ignore_ascii_case(&b[0]) {
88        return false;
89    }
90
91    let mut found = false;
92    for (a_char, b_char) in a.iter().copied().zip(b.iter().copied()) {
93        let a_char = a_char.to_ascii_lowercase();
94        let b_char = b_char.to_ascii_lowercase();
95
96        if a_char != b_char {
97            if (a_char == 's' && b_char == 'z')
98                || (a_char == 'z' && b_char == 's')
99                || (a_char == 's' && b_char == 'c')
100                || (a_char == 'c' && b_char == 's')
101                || (a_char == 'k' && b_char == 'c')
102                || (a_char == 'c' && b_char == 'k')
103            {
104                if found {
105                    return false;
106                }
107                found = true;
108            } else {
109                return false;
110            }
111        }
112    }
113
114    found
115}
116
117/// Returns whether the two words are the same, expect that one is written
118/// with '-er' and the other with '-re'.
119///
120/// E.g. "meter" and "metre"
121pub(crate) fn is_er_misspelling(a: &[char], b: &[char]) -> bool {
122    if a.len() != b.len() || a.len() <= 4 {
123        return false;
124    }
125
126    let len = a.len();
127    let a_suffix = [&a[len - 2], &a[len - 1]].map(char::to_ascii_lowercase);
128    let b_suffix = [&b[len - 2], &b[len - 1]].map(char::to_ascii_lowercase);
129
130    if a_suffix == ['r', 'e'] && b_suffix == ['e', 'r']
131        || a_suffix == ['e', 'r'] && b_suffix == ['r', 'e']
132    {
133        return a[0..len - 2]
134            .iter()
135            .copied()
136            .zip(b[0..len - 2].iter().copied())
137            .all(|(a_char, b_char)| a_char.eq_ignore_ascii_case(&b_char));
138    }
139
140    false
141}
142
143/// Returns whether the two words are the same, expect that one is written
144/// with 'll' and the other with 'l'.
145///
146/// E.g. "traveller" and "traveler"
147pub(crate) fn is_ll_misspelling(a: &[char], b: &[char]) -> bool {
148    if a.len().abs_diff(b.len()) != 1 {
149        return false;
150    }
151
152    let mut a_iter = a.iter();
153    let mut b_iter = b.iter();
154
155    loop {
156        match (
157            a_iter.next().map(char::to_ascii_lowercase),
158            b_iter.next().map(char::to_ascii_lowercase),
159        ) {
160            (Some('l'), Some('l')) => {
161                let mut a_next = a_iter.next().map(char::to_ascii_lowercase);
162                let mut b_next = b_iter.next().map(char::to_ascii_lowercase);
163                if a_next != b_next {
164                    if a_next == Some('l') {
165                        a_next = a_iter.next().map(char::to_ascii_lowercase);
166                    } else if b_next == Some('l') {
167                        b_next = b_iter.next().map(char::to_ascii_lowercase);
168                    }
169
170                    if a_next != b_next {
171                        return false;
172                    }
173                }
174            }
175            (Some(a_char), Some(b_char)) => {
176                if !a_char.eq_ignore_ascii_case(&b_char) {
177                    return false;
178                }
179            }
180            (None, None) => return true,
181            _ => return false,
182        }
183    }
184}
185
186/// Returns whether the two words are the same, except that one is written
187/// with 'ay' and the other with 'ey'.
188///
189/// E.g. "gray" and "grey"
190pub(crate) fn is_ay_ey_misspelling(a: &[char], b: &[char]) -> bool {
191    if a.len() != b.len() {
192        return false;
193    }
194
195    let mut found_ay_ey = false;
196    let mut a_iter = a.iter();
197    let mut b_iter = b.iter();
198
199    while let (Some(&a_char), Some(&b_char)) = (a_iter.next(), b_iter.next()) {
200        if a_char.eq_ignore_ascii_case(&b_char) {
201            continue;
202        }
203
204        // Check for 'a'/'e' difference
205        if (a_char.eq_ignore_ascii_case(&'a') && b_char.eq_ignore_ascii_case(&'e'))
206            || (a_char.eq_ignore_ascii_case(&'e') && b_char.eq_ignore_ascii_case(&'a'))
207        {
208            // Check if next character is 'y' for both
209            if let (Some(&a_next), Some(&b_next)) = (a_iter.next(), b_iter.next()) {
210                if a_next.eq_ignore_ascii_case(&'y') && b_next.eq_ignore_ascii_case(&'y') {
211                    if found_ay_ey {
212                        return false; // More than one ay/ey difference
213                    }
214                    found_ay_ey = true;
215                    continue;
216                }
217            }
218        }
219        return false; // Non-ay/ey difference found
220    }
221
222    if !found_ay_ey {
223        return false;
224    }
225    found_ay_ey
226}
227
228/// Returns whether the two words are the same, except that one is written
229/// with 'ei' and the other with 'ie'.
230///
231/// E.g. "recieved" instead of "received", "cheif" instead of "chief"
232pub(crate) fn is_ei_ie_misspelling(a: &[char], b: &[char]) -> bool {
233    if a.len() != b.len() {
234        return false;
235    }
236    let mut found_ei_ie = false;
237    let mut a_iter = a.iter();
238    let mut b_iter = b.iter();
239
240    while let (Some(&a_char), Some(&b_char)) = (a_iter.next(), b_iter.next()) {
241        if a_char.eq_ignore_ascii_case(&b_char) {
242            continue;
243        }
244
245        // Check for 'e' vs 'i' in first position
246        if a_char.eq_ignore_ascii_case(&'e') && b_char.eq_ignore_ascii_case(&'i') {
247            if let (Some(&a_next), Some(&b_next)) = (a_iter.next(), b_iter.next()) {
248                // Next chars must be 'i' and 'e' respectively
249                if a_next.eq_ignore_ascii_case(&'i') && b_next.eq_ignore_ascii_case(&'e') {
250                    if found_ei_ie {
251                        return false; // More than one ei/ie difference
252                    }
253                    found_ei_ie = true;
254                    continue;
255                }
256            }
257        }
258        // Check for 'i' vs 'e' in first position
259        else if a_char.eq_ignore_ascii_case(&'i') && b_char.eq_ignore_ascii_case(&'e') {
260            if let (Some(&a_next), Some(&b_next)) = (a_iter.next(), b_iter.next()) {
261                // Next chars must be 'e' and 'i' respectively
262                if a_next.eq_ignore_ascii_case(&'e') && b_next.eq_ignore_ascii_case(&'i') {
263                    if found_ei_ie {
264                        return false; // More than one ei/ie difference
265                    }
266                    found_ei_ie = true;
267                    continue;
268                }
269            }
270        }
271        return false;
272    }
273    found_ei_ie
274}
275
276/// Scores a possible spelling suggestion based on possible relevance to the user.
277///
278/// Lower = better.
279fn score_suggestion(misspelled_word: &[char], sug: &FuzzyMatchResult) -> i32 {
280    if misspelled_word.is_empty() || sug.word.is_empty() {
281        return i32::MAX;
282    }
283
284    let mut score = sug.edit_distance as i32 * 10;
285
286    // People are much less likely to mistype the first letter.
287    if misspelled_word
288        .first()
289        .unwrap()
290        .eq_ignore_ascii_case(sug.word.first().unwrap())
291    {
292        score -= 10;
293    }
294
295    // If the original word is plural, the correct one probably is too.
296    if *misspelled_word.last().unwrap() == 's' && *sug.word.last().unwrap() == 's' {
297        score -= 5;
298    }
299
300    // Boost common words.
301    if sug.metadata.common {
302        score -= 5;
303    }
304
305    // For turning words into contractions.
306    if sug.word.iter().filter(|c| **c == '\'').count() == 1 {
307        score -= 5;
308    }
309
310    // Detect dialect-specific variations
311    if sug.edit_distance == 1
312        && (is_cksz_misspelling(misspelled_word, sug.word)
313            || is_ou_misspelling(misspelled_word, sug.word)
314            || is_ll_misspelling(misspelled_word, sug.word)
315            || is_ay_ey_misspelling(misspelled_word, sug.word))
316    {
317        score -= 6;
318    }
319    if sug.edit_distance == 2 {
320        if is_ei_ie_misspelling(misspelled_word, sug.word) {
321            score -= 11;
322        }
323        if is_er_misspelling(misspelled_word, sug.word) {
324            score -= 15;
325        }
326    }
327
328    score
329}
330
331/// Order the suggestions to be shown to the user.
332fn order_suggestions<'b>(
333    misspelled_word: &[char],
334    mut matches: Vec<FuzzyMatchResult<'b>>,
335) -> Vec<&'b [char]> {
336    matches.sort_by_key(|v| score_suggestion(misspelled_word, v));
337
338    matches.into_iter().map(|v| v.word).collect()
339}
340
341/// Get the closest matches in the provided [`Dictionary`] and rank them
342/// Implementation is left up to the underlying dictionary.
343pub fn suggest_correct_spelling<'a>(
344    misspelled_word: &[char],
345    result_limit: usize,
346    max_edit_dist: u8,
347    dictionary: &'a impl Dictionary,
348) -> Vec<&'a [char]> {
349    let matches: Vec<FuzzyMatchResult> = dictionary
350        .fuzzy_match(misspelled_word, max_edit_dist, result_limit)
351        .into_iter()
352        .collect();
353
354    order_suggestions(misspelled_word, matches)
355}
356
357/// Convenience function over [`suggest_correct_spelling`] that does conversions
358/// for you.
359pub fn suggest_correct_spelling_str(
360    misspelled_word: impl Into<String>,
361    result_limit: usize,
362    max_edit_dist: u8,
363    dictionary: &impl Dictionary,
364) -> Vec<String> {
365    let chars: CharString = misspelled_word.into().chars().collect();
366    suggest_correct_spelling(&chars, result_limit, max_edit_dist, dictionary)
367        .into_iter()
368        .map(|a| a.to_string())
369        .collect()
370}
371
372#[cfg(test)]
373mod tests {
374    use itertools::Itertools;
375
376    use crate::{
377        CharStringExt, Dialect,
378        linting::{
379            SpellCheck,
380            tests::{assert_suggestion_result, assert_top3_suggestion_result},
381        },
382    };
383
384    use super::{FstDictionary, suggest_correct_spelling_str};
385
386    const RESULT_LIMIT: usize = 100;
387    const MAX_EDIT_DIST: u8 = 2;
388
389    #[test]
390    fn normalizes_weve() {
391        let word = ['w', 'e', '’', 'v', 'e'];
392        let norm = word.normalized();
393
394        assert_eq!(norm.clone(), vec!['w', 'e', '\'', 'v', 'e'])
395    }
396
397    #[test]
398    fn punctation_no_duplicates() {
399        let results = suggest_correct_spelling_str(
400            "punctation",
401            RESULT_LIMIT,
402            MAX_EDIT_DIST,
403            &FstDictionary::curated(),
404        );
405
406        assert!(results.iter().all_unique())
407    }
408
409    #[test]
410    fn youre_contraction() {
411        assert_suggests_correction("youre", "you're");
412    }
413
414    #[test]
415    fn thats_contraction() {
416        assert_suggests_correction("thats", "that's");
417    }
418
419    #[test]
420    fn weve_contraction() {
421        assert_suggests_correction("weve", "we've");
422    }
423
424    #[test]
425    fn this_correction() {
426        assert_suggests_correction("ths", "this");
427    }
428
429    #[test]
430    fn issue_624_no_duplicates() {
431        let results = suggest_correct_spelling_str(
432            "Semantical",
433            RESULT_LIMIT,
434            MAX_EDIT_DIST,
435            &FstDictionary::curated(),
436        );
437
438        dbg!(&results);
439
440        assert!(results.iter().all_unique())
441    }
442
443    #[test]
444    fn issue_182() {
445        assert_suggests_correction("Im", "I'm");
446    }
447
448    #[test]
449    fn fst_spellcheck_hvllo() {
450        let results = suggest_correct_spelling_str(
451            "hvllo",
452            RESULT_LIMIT,
453            MAX_EDIT_DIST,
454            &FstDictionary::curated(),
455        );
456
457        dbg!(&results);
458
459        assert!(results.iter().take(3).contains(&"hello".to_string()));
460    }
461
462    /// Assert that the default suggestion settings result in a specific word
463    /// being in the top three results for a given misspelling.
464    #[track_caller]
465    fn assert_suggests_correction(misspelled_word: &str, correct: &str) {
466        let results = suggest_correct_spelling_str(
467            misspelled_word,
468            RESULT_LIMIT,
469            MAX_EDIT_DIST,
470            &FstDictionary::curated(),
471        );
472
473        dbg!(&results);
474
475        assert!(results.iter().take(3).contains(&correct.to_string()));
476    }
477
478    #[test]
479    fn spellcheck_hvllo() {
480        assert_suggests_correction("hvllo", "hello");
481    }
482
483    #[test]
484    fn spellcheck_aout() {
485        assert_suggests_correction("aout", "about");
486    }
487
488    #[test]
489    fn spellchecking_is_deterministic() {
490        let results1 = suggest_correct_spelling_str(
491            "hello",
492            RESULT_LIMIT,
493            MAX_EDIT_DIST,
494            &FstDictionary::curated(),
495        );
496        let results2 = suggest_correct_spelling_str(
497            "hello",
498            RESULT_LIMIT,
499            MAX_EDIT_DIST,
500            &FstDictionary::curated(),
501        );
502        let results3 = suggest_correct_spelling_str(
503            "hello",
504            RESULT_LIMIT,
505            MAX_EDIT_DIST,
506            &FstDictionary::curated(),
507        );
508
509        assert_eq!(results1, results2);
510        assert_eq!(results1, results3);
511    }
512
513    #[test]
514    fn adviced_correction() {
515        assert_suggests_correction("adviced", "advised");
516    }
517
518    #[test]
519    fn aknowledged_correction() {
520        assert_suggests_correction("aknowledged", "acknowledged");
521    }
522
523    #[test]
524    fn alcaholic_correction() {
525        assert_suggests_correction("alcaholic", "alcoholic");
526    }
527
528    #[test]
529    fn slaves_correction() {
530        assert_suggests_correction("Slaves", "Slavs");
531    }
532
533    #[test]
534    fn conciousness_correction() {
535        assert_suggests_correction("conciousness", "consciousness");
536    }
537
538    // Tests for dialect-specific misspelling patterns
539
540    // is_ou_misspelling
541    #[test]
542    fn suggest_color_for_colour_lowercase() {
543        assert_suggestion_result(
544            "colour",
545            SpellCheck::new(FstDictionary::curated(), Dialect::American),
546            "color",
547        );
548    }
549
550    #[test]
551    fn suggest_colour_for_color_lowercase() {
552        assert_suggestion_result(
553            "color",
554            SpellCheck::new(FstDictionary::curated(), Dialect::British),
555            "colour",
556        );
557    }
558
559    // titlecase
560    #[test]
561    fn suggest_color_for_colour_titlecase() {
562        assert_suggestion_result(
563            "Colour",
564            SpellCheck::new(FstDictionary::curated(), Dialect::American),
565            "Color",
566        );
567    }
568
569    #[test]
570    #[ignore = "known failure due to bug"]
571    fn suggest_colour_for_color_titlecase() {
572        assert_suggestion_result(
573            "Color",
574            SpellCheck::new(FstDictionary::curated(), Dialect::British),
575            "Colour",
576        );
577    }
578
579    // all-caps
580    #[test]
581    #[ignore = "known failure due to bug"]
582    fn suggest_color_for_colour_all_caps() {
583        assert_suggestion_result(
584            "COLOUR",
585            SpellCheck::new(FstDictionary::curated(), Dialect::American),
586            "COLOR",
587        );
588    }
589
590    #[test]
591    #[ignore = "known failure due to bug"]
592    fn suggest_colour_for_color_all_caps() {
593        assert_suggestion_result(
594            "COLOR",
595            SpellCheck::new(FstDictionary::curated(), Dialect::British),
596            "COLOUR",
597        );
598    }
599
600    // is_cksz_misspelling
601
602    // s/z as in realise/realize
603    #[test]
604    fn suggest_realise_for_realize() {
605        assert_suggestion_result(
606            "realize",
607            SpellCheck::new(FstDictionary::curated(), Dialect::British),
608            "realise",
609        );
610    }
611
612    #[test]
613    fn suggest_realize_for_realise() {
614        assert_suggestion_result(
615            "realise",
616            SpellCheck::new(FstDictionary::curated(), Dialect::American),
617            "realize",
618        );
619    }
620
621    #[test]
622    fn suggest_realise_for_realize_titlecase() {
623        assert_suggestion_result(
624            "Realize",
625            SpellCheck::new(FstDictionary::curated(), Dialect::British),
626            "Realise",
627        );
628    }
629
630    #[test]
631    #[ignore = "known failure due to bug"]
632    fn suggest_realize_for_realise_titlecase() {
633        assert_suggestion_result(
634            "Realise",
635            SpellCheck::new(FstDictionary::curated(), Dialect::American),
636            "Realize",
637        );
638    }
639
640    #[test]
641    #[ignore = "known failure due to bug"]
642    fn suggest_realise_for_realize_all_caps() {
643        assert_suggestion_result(
644            "REALIZE",
645            SpellCheck::new(FstDictionary::curated(), Dialect::British),
646            "REALISE",
647        );
648    }
649
650    #[test]
651    #[ignore = "known failure due to bug"]
652    fn suggest_realize_for_realise_all_caps() {
653        assert_suggestion_result(
654            "REALISE",
655            SpellCheck::new(FstDictionary::curated(), Dialect::American),
656            "REALIZE",
657        );
658    }
659
660    // s/c as in defense/defence
661    #[test]
662    fn suggest_defence_for_defense() {
663        assert_suggestion_result(
664            "defense",
665            SpellCheck::new(FstDictionary::curated(), Dialect::British),
666            "defence",
667        );
668    }
669
670    #[test]
671    fn suggest_defense_for_defence() {
672        assert_suggestion_result(
673            "defence",
674            SpellCheck::new(FstDictionary::curated(), Dialect::American),
675            "defense",
676        );
677    }
678
679    #[test]
680    fn suggest_defense_for_defence_titlecase() {
681        assert_suggestion_result(
682            "Defense",
683            SpellCheck::new(FstDictionary::curated(), Dialect::British),
684            "Defence",
685        );
686    }
687
688    #[test]
689    fn suggest_defence_for_defense_titlecase() {
690        assert_suggestion_result(
691            "Defence",
692            SpellCheck::new(FstDictionary::curated(), Dialect::American),
693            "Defense",
694        );
695    }
696
697    #[test]
698    #[ignore = "known failure due to bug"]
699    fn suggest_defense_for_defence_all_caps() {
700        assert_suggestion_result(
701            "DEFENSE",
702            SpellCheck::new(FstDictionary::curated(), Dialect::British),
703            "DEFENCE",
704        );
705    }
706
707    #[test]
708    #[ignore = "known failure due to bug"]
709    fn suggest_defence_for_defense_all_caps() {
710        assert_suggestion_result(
711            "DEFENCE",
712            SpellCheck::new(FstDictionary::curated(), Dialect::American),
713            "DEFENSE",
714        );
715    }
716
717    // k/c as in skeptic/sceptic
718    #[test]
719    fn suggest_sceptic_for_skeptic() {
720        assert_suggestion_result(
721            "skeptic",
722            SpellCheck::new(FstDictionary::curated(), Dialect::British),
723            "sceptic",
724        );
725    }
726
727    #[test]
728    fn suggest_skeptic_for_sceptic() {
729        assert_suggestion_result(
730            "sceptic",
731            SpellCheck::new(FstDictionary::curated(), Dialect::American),
732            "skeptic",
733        );
734    }
735
736    #[test]
737    fn suggest_sceptic_for_skeptic_titlecase() {
738        assert_suggestion_result(
739            "Skeptic",
740            SpellCheck::new(FstDictionary::curated(), Dialect::British),
741            "Sceptic",
742        );
743    }
744
745    #[test]
746    #[ignore = "known failure due to bug"]
747    fn suggest_skeptic_for_sceptic_titlecase() {
748        assert_suggestion_result(
749            "Sceptic",
750            SpellCheck::new(FstDictionary::curated(), Dialect::American),
751            "Skeptic",
752        );
753    }
754
755    #[test]
756    #[ignore = "known failure due to bug"]
757    fn suggest_skeptic_for_sceptic_all_caps() {
758        assert_suggestion_result(
759            "SKEPTIC",
760            SpellCheck::new(FstDictionary::curated(), Dialect::British),
761            "SCEPTIC",
762        );
763    }
764
765    #[test]
766    #[ignore = "known failure due to bug"]
767    fn suggest_sceptic_for_skeptic_all_caps() {
768        assert_suggestion_result(
769            "SCEPTIC",
770            SpellCheck::new(FstDictionary::curated(), Dialect::American),
771            "SKEPTIC",
772        );
773    }
774
775    // is_er_misspelling
776    // as in meter/metre
777    #[test]
778    fn suggest_centimeter_for_centimetre() {
779        assert_suggestion_result(
780            "centimetre",
781            SpellCheck::new(FstDictionary::curated(), Dialect::American),
782            "centimeter",
783        );
784    }
785
786    #[test]
787    fn suggest_centimetre_for_centimeter() {
788        assert_suggestion_result(
789            "centimeter",
790            SpellCheck::new(FstDictionary::curated(), Dialect::British),
791            "centimetre",
792        );
793    }
794
795    #[test]
796    fn suggest_centimeter_for_centimetre_titlecase() {
797        assert_suggestion_result(
798            "Centimetre",
799            SpellCheck::new(FstDictionary::curated(), Dialect::American),
800            "Centimeter",
801        );
802    }
803
804    #[test]
805    #[ignore = "known failure due to bug"]
806    fn suggest_centimetre_for_centimeter_titlecase() {
807        assert_suggestion_result(
808            "Centimeter",
809            SpellCheck::new(FstDictionary::curated(), Dialect::British),
810            "Centimetre",
811        );
812    }
813
814    #[test]
815    #[ignore = "known failure due to bug"]
816    fn suggest_centimeter_for_centimetre_all_caps() {
817        assert_suggestion_result(
818            "CENTIMETRE",
819            SpellCheck::new(FstDictionary::curated(), Dialect::American),
820            "CENTIMETER",
821        );
822    }
823
824    #[test]
825    #[ignore = "known failure due to bug"]
826    fn suggest_centimetre_for_centimeter_all_caps() {
827        assert_suggestion_result(
828            "CENTIMETER",
829            SpellCheck::new(FstDictionary::curated(), Dialect::British),
830            "CENTIMETRE",
831        );
832    }
833
834    // is_ll_misspelling
835    // as in traveller/traveler
836    #[test]
837    fn suggest_traveler_for_traveller() {
838        assert_suggestion_result(
839            "traveller",
840            SpellCheck::new(FstDictionary::curated(), Dialect::American),
841            "traveler",
842        );
843    }
844
845    #[test]
846    fn suggest_traveller_for_traveler() {
847        assert_suggestion_result(
848            "traveler",
849            SpellCheck::new(FstDictionary::curated(), Dialect::British),
850            "traveller",
851        );
852    }
853
854    #[test]
855    fn suggest_traveler_for_traveller_titlecase() {
856        assert_suggestion_result(
857            "Traveller",
858            SpellCheck::new(FstDictionary::curated(), Dialect::American),
859            "Traveler",
860        );
861    }
862
863    #[test]
864    #[ignore = "known failure due to bug"]
865    fn suggest_traveller_for_traveler_titlecase() {
866        assert_suggestion_result(
867            "Traveler",
868            SpellCheck::new(FstDictionary::curated(), Dialect::British),
869            "Traveller",
870        );
871    }
872
873    #[test]
874    #[ignore = "known failure due to bug"]
875    fn suggest_traveler_for_traveller_all_caps() {
876        assert_suggestion_result(
877            "TRAVELLER",
878            SpellCheck::new(FstDictionary::curated(), Dialect::American),
879            "TRAVELER",
880        );
881    }
882
883    #[test]
884    #[ignore = "known failure due to bug"]
885    fn suggest_traveller_for_traveler_all_caps() {
886        assert_suggestion_result(
887            "TRAVELER",
888            SpellCheck::new(FstDictionary::curated(), Dialect::British),
889            "TRAVELLER",
890        );
891    }
892
893    // is_ay_ey_misspelling
894    // as in gray/grey
895
896    #[test]
897    fn suggest_grey_for_gray_in_non_american() {
898        assert_suggestion_result(
899            "I've got a gray cat.",
900            SpellCheck::new(FstDictionary::curated(), Dialect::British),
901            "I've got a grey cat.",
902        );
903    }
904
905    #[test]
906    fn suggest_gray_for_grey_in_american() {
907        assert_suggestion_result(
908            "It's a greyscale photo.",
909            SpellCheck::new(FstDictionary::curated(), Dialect::American),
910            "It's a grayscale photo.",
911        );
912    }
913
914    #[test]
915    #[ignore = "known failure due to bug"]
916    fn suggest_grey_for_gray_in_non_american_titlecase() {
917        assert_suggestion_result(
918            "I've Got a Gray Cat.",
919            SpellCheck::new(FstDictionary::curated(), Dialect::British),
920            "I've Got a Grey Cat.",
921        );
922    }
923
924    #[test]
925    fn suggest_gray_for_grey_in_american_titlecase() {
926        assert_suggestion_result(
927            "It's a Greyscale Photo.",
928            SpellCheck::new(FstDictionary::curated(), Dialect::American),
929            "It's a Grayscale Photo.",
930        );
931    }
932
933    #[test]
934    #[ignore = "known failure due to bug"]
935    fn suggest_grey_for_gray_in_non_american_all_caps() {
936        assert_suggestion_result(
937            "GRAY",
938            SpellCheck::new(FstDictionary::curated(), Dialect::British),
939            "GREY",
940        );
941    }
942
943    #[test]
944    #[ignore = "known failure due to bug"]
945    fn suggest_gray_for_grey_in_american_all_caps() {
946        assert_suggestion_result(
947            "GREY",
948            SpellCheck::new(FstDictionary::curated(), Dialect::American),
949            "GRAY",
950        );
951    }
952
953    // Tests for non-dialectal misspelling patterns
954
955    // is_ei_ie_misspelling
956    #[test]
957    fn fix_cheif_and_recieved() {
958        assert_top3_suggestion_result(
959            "The cheif recieved a letter.",
960            SpellCheck::new(FstDictionary::curated(), Dialect::British),
961            "The chief received a letter.",
962        );
963    }
964
965    #[test]
966    #[ignore = "known failure due to bug"]
967    fn fix_cheif_and_recieved_titlecase() {
968        assert_top3_suggestion_result(
969            "The Cheif Recieved a Letter.",
970            SpellCheck::new(FstDictionary::curated(), Dialect::British),
971            "The Chief Received a Letter.",
972        );
973    }
974
975    #[test]
976    #[ignore = "known failure due to bug"]
977    fn fix_cheif_and_recieved_all_caps() {
978        assert_top3_suggestion_result(
979            "THE CHEIF RECIEVED A LETTER.",
980            SpellCheck::new(FstDictionary::curated(), Dialect::British),
981            "THE CHEIF RECEIVED A LETTER.",
982        );
983    }
984}