cargo/util/
edit_distance.rs1use std::{cmp, mem};
2
3pub fn edit_distance(a: &str, b: &str, limit: usize) -> Option<usize> {
9 let a = a.to_lowercase();
15 let b = b.to_lowercase();
16
17 let mut a = &a.chars().collect::<Vec<_>>()[..];
18 let mut b = &b.chars().collect::<Vec<_>>()[..];
19
20 if a.len() < b.len() {
22 mem::swap(&mut a, &mut b);
23 }
24
25 let min_dist = a.len() - b.len();
26 if min_dist > limit {
28 return None;
29 }
30
31 while let Some(((b_char, b_rest), (a_char, a_rest))) = b.split_first().zip(a.split_first()) {
33 if a_char != b_char {
34 break;
35 }
36 a = a_rest;
37 b = b_rest;
38 }
39 while let Some(((b_char, b_rest), (a_char, a_rest))) = b.split_last().zip(a.split_last()) {
41 if a_char != b_char {
42 break;
43 }
44 a = a_rest;
45 b = b_rest;
46 }
47
48 if b.len() == 0 {
51 return Some(min_dist);
52 }
53
54 let mut prev_prev = vec![usize::MAX; b.len() + 1];
55 let mut prev = (0..=b.len()).collect::<Vec<_>>();
56 let mut current = vec![0; b.len() + 1];
57
58 for i in 1..=a.len() {
60 current[0] = i;
61 let a_idx = i - 1;
62
63 for j in 1..=b.len() {
65 let b_idx = j - 1;
66
67 let substitution_cost = if a[a_idx] == b[b_idx] { 0 } else { 1 };
69
70 current[j] = cmp::min(
71 prev[j] + 1,
73 cmp::min(
74 current[j - 1] + 1,
76 prev[j - 1] + substitution_cost,
78 ),
79 );
80
81 if (i > 1) && (j > 1) && (a[a_idx] == b[b_idx - 1]) && (a[a_idx - 1] == b[b_idx]) {
82 current[j] = cmp::min(current[j], prev_prev[j - 2] + 1);
84 }
85 }
86
87 [prev_prev, prev, current] = [prev, current, prev_prev];
89 }
90
91 let distance = prev[b.len()];
93 (distance <= limit).then_some(distance)
94}
95
96pub fn closest<'a, T>(
99 choice: &str,
100 iter: impl Iterator<Item = T>,
101 key: impl Fn(&T) -> &'a str,
102) -> Option<T> {
103 iter.filter_map(|e| Some((edit_distance(choice, key(&e), 3)?, e)))
106 .min_by_key(|t| t.0)
107 .map(|t| t.1)
108}
109
110pub fn closest_msg<'a, T>(
113 choice: &str,
114 iter: impl Iterator<Item = T>,
115 key: impl Fn(&T) -> &'a str,
116 kind: &str,
117) -> String {
118 match closest(choice, iter, &key) {
119 Some(e) => format!(
120 "\n\nhelp: a {kind} with a similar name exists: `{}`",
121 key(&e)
122 ),
123 None => String::new(),
124 }
125}
126
127#[test]
128fn test_edit_distance() {
129 use std::char::{from_u32, MAX};
130 for c in (0u32..MAX as u32)
132 .filter_map(from_u32)
133 .map(|i| i.to_string())
134 {
135 assert_eq!(edit_distance(&c, &c, usize::MAX), Some(0));
136 }
137
138 let a = "\nMäry häd ä little lämb\n\nLittle lämb\n";
139 let b = "\nMary häd ä little lämb\n\nLittle lämb\n";
140 let c = "Mary häd ä little lämb\n\nLittle lämb\n";
141 assert_eq!(edit_distance(a, b, usize::MAX), Some(1));
142 assert_eq!(edit_distance(b, a, usize::MAX), Some(1));
143 assert_eq!(edit_distance(a, c, usize::MAX), Some(2));
144 assert_eq!(edit_distance(c, a, usize::MAX), Some(2));
145 assert_eq!(edit_distance(b, c, usize::MAX), Some(1));
146 assert_eq!(edit_distance(c, b, usize::MAX), Some(1));
147}