harper_core/spell/
mod.rs

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