#[must_use]
pub fn nearest<'a>(name: &str, candidates: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
let budget = (name.chars().count() / 3).max(1);
let mut best: Option<(usize, &'a str)> = None;
for candidate in candidates {
if candidate == name {
continue;
}
let distance = edit_distance(name, candidate);
if distance > budget {
continue;
}
match best {
Some((d, _)) if d <= distance => {}
_ => best = Some((distance, candidate)),
}
}
best.map(|(_, c)| c)
}
#[must_use]
pub fn edit_distance(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 current = vec![0usize; b.len() + 1];
for (i, ca) in a.iter().enumerate() {
current[0] = i + 1;
for (j, cb) in b.iter().enumerate() {
let substitution = prev[j] + usize::from(ca != cb);
let deletion = prev[j + 1] + 1;
let insertion = current[j] + 1;
current[j + 1] = substitution.min(deletion).min(insertion);
}
std::mem::swap(&mut prev, &mut current);
}
prev[b.len()]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_suggests_lines() {
assert_eq!(
nearest("line", ["lines", "sections", "csv", "grid"]),
Some("lines")
);
}
#[test]
fn a_name_with_nothing_near_it_suggests_nothing() {
assert_eq!(nearest("frobnicate", ["lines", "sections", "csv"]), None);
assert_eq!(nearest("abc", ["xyz"]), None);
}
#[test]
fn the_budget_grows_with_the_name() {
assert_eq!(nearest("csw", ["csv"]), Some("csv"));
assert_eq!(nearest("cww", ["csv"]), None);
assert_eq!(nearest("sectionz", ["sections"]), Some("sections"));
assert_eq!(nearest("sektionz", ["sections"]), Some("sections"));
}
#[test]
fn an_exact_match_is_not_a_suggestion() {
assert_eq!(nearest("lines", ["lines"]), None);
}
#[test]
fn the_nearest_wins_and_ties_go_to_the_first() {
assert_eq!(nearest("abc", ["axy", "abx"]), Some("abx"));
assert_eq!(nearest("abc", ["axc", "abx"]), Some("axc"));
}
#[test]
fn distance_counts_characters_not_bytes() {
assert_eq!(edit_distance("é", "e"), 1);
assert_eq!(edit_distance("", "abc"), 3);
assert_eq!(edit_distance("kitten", "sitting"), 3);
}
}