pub(crate) fn nearest(key: &str, candidates: &[&str]) -> Option<String> {
candidates
.iter()
.map(|cand| (levenshtein(key, cand), *cand))
.filter(|(d, _)| (1..=2).contains(d))
.min_by_key(|(d, _)| *d)
.map(|(_, cand)| cand.to_string())
}
pub(crate) fn nearest_owned(key: &str, candidates: &[String]) -> Option<String> {
candidates
.iter()
.map(|cand| (levenshtein(key, cand), cand))
.filter(|(d, _)| (1..=2).contains(d))
.min_by_key(|(d, _)| *d)
.map(|(_, cand)| cand.clone())
}
pub(crate) fn levenshtein(a: &str, b: &str) -> usize {
let b: Vec<char> = b.chars().collect();
let mut prev: Vec<usize> = (0..=b.len()).collect();
let mut curr = vec![0usize; b.len() + 1];
for (i, ca) in a.chars().enumerate() {
curr[0] = i + 1;
for (j, cb) in b.iter().enumerate() {
let cost = if ca == *cb { 0 } else { 1 };
curr[j + 1] = (prev[j] + cost).min(prev[j + 1] + 1).min(curr[j] + 1);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[b.len()]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nearest_finds_a_close_typo_and_ignores_distant_words() {
assert_eq!(
nearest("recyle_bin", &["recycle_bin", "fixity"]),
Some("recycle_bin".to_string())
);
assert_eq!(nearest("author", &["recycle_bin", "fixity"]), None);
assert_eq!(nearest("fixity", &["fixity"]), None);
}
#[test]
fn nearest_owned_matches_the_slice_form() {
let cands = vec!["public".to_string(), "friends".to_string()];
assert_eq!(
nearest_owned("freinds", &cands),
Some("friends".to_string())
);
assert_eq!(nearest_owned("colleagues", &cands), None);
}
}