1pub fn did_you_mean<'a, 'b, I, S>(possibilities: I, input: &'b str) -> Option<String>
2where
3 I: IntoIterator<Item = &'a S>,
4 S: AsRef<str> + 'a + ?Sized,
5{
6 let possibilities: Vec<&str> = possibilities.into_iter().map(|s| s.as_ref()).collect();
7 let suggestion =
8 crate::lev_distance::find_best_match_for_name_with_substrings(&possibilities, input, None)
9 .map(|s| s.to_string());
10 if let Some(suggestion) = &suggestion {
11 if suggestion.len() == 1 && !suggestion.eq_ignore_ascii_case(input) {
12 return None;
13 }
14 }
15 suggestion
16}
17
18#[cfg(test)]
19mod tests {
20
21 use super::did_you_mean;
22
23 #[test]
24 fn did_you_mean_examples() {
25 let all_cases = [
26 (
27 vec!["a", "b"],
28 vec![
29 ("a", Some("a"), ""),
30 ("A", Some("a"), ""),
31 (
32 "c",
33 None,
34 "Not helpful to suggest an arbitrary choice when none are close",
35 ),
36 (
37 "ccccccccccccccccccccccc",
38 None,
39 "Not helpful to suggest an arbitrary choice when none are close",
40 ),
41 ],
42 ),
43 (
44 vec!["OS", "PWD", "PWDPWDPWDPWD"],
45 vec![
46 (
47 "pwd",
48 Some("PWD"),
49 "Exact case insensitive match yields a match",
50 ),
51 (
52 "pwdpwdpwdpwd",
53 Some("PWDPWDPWDPWD"),
54 "Exact case insensitive match yields a match",
55 ),
56 ("PWF", Some("PWD"), "One-letter typo yields a match"),
57 ("pwf", None, "Case difference plus typo yields no match"),
58 (
59 "Xwdpwdpwdpwd",
60 None,
61 "Case difference plus typo yields no match",
62 ),
63 ],
64 ),
65 (
66 vec!["foo", "bar", "baz"],
67 vec![
68 ("fox", Some("foo"), ""),
69 ("FOO", Some("foo"), ""),
70 ("FOX", None, ""),
71 (
72 "ccc",
73 None,
74 "Not helpful to suggest an arbitrary choice when none are close",
75 ),
76 (
77 "zzz",
78 None,
79 "'baz' does share a character, but rustc rule is edit distance must be <= 1/3 of the length of the user input",
80 ),
81 ],
82 ),
83 (
84 vec!["aaaaaa"],
85 vec![
86 (
87 "XXaaaa",
88 Some("aaaaaa"),
89 "Distance of 2 out of 6 chars: close enough to meet rustc's rule",
90 ),
91 (
92 "XXXaaa",
93 None,
94 "Distance of 3 out of 6 chars: not close enough to meet rustc's rule",
95 ),
96 (
97 "XaaaaX",
98 Some("aaaaaa"),
99 "Distance of 2 out of 6 chars: close enough to meet rustc's rule",
100 ),
101 (
102 "XXaaaaXX",
103 None,
104 "Distance of 4 out of 6 chars: not close enough to meet rustc's rule",
105 ),
106 ],
107 ),
108 ];
109 for (possibilities, cases) in all_cases {
110 for (input, expected_suggestion, discussion) in cases {
111 let suggestion = did_you_mean(&possibilities, input);
112 assert_eq!(
113 suggestion.as_deref(),
114 expected_suggestion,
115 "Expected the following reasoning to hold but it did not: '{discussion}'"
116 );
117 }
118 }
119 }
120}