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