pub fn closest_match<'a, I>(target: &str, candidates: I) -> Option<&'a str>
where
I: IntoIterator<Item = &'a str>,
{
let threshold = target.len().div_ceil(3).max(1);
let mut best: Option<(&str, usize)> = None;
for cand in candidates {
if cand == target {
continue;
}
let d = levenshtein(target, cand);
if d <= threshold && best.is_none_or(|(_, bd)| d < bd) {
best = Some((cand, d));
}
}
best.map(|(s, _)| s)
}
fn levenshtein(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
if a.is_empty() {
return b.len();
}
if b.is_empty() {
return a.len();
}
let mut prev: Vec<usize> = (0..=b.len()).collect();
let mut curr: Vec<usize> = vec![0; b.len() + 1];
for i in 1..=a.len() {
curr[0] = i;
for j in 1..=b.len() {
let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
curr[j] = (prev[j] + 1) .min(curr[j - 1] + 1) .min(prev[j - 1] + cost); }
std::mem::swap(&mut prev, &mut curr);
}
prev[b.len()]
}