harper_core/spell/
mod.rs

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