pub fn edit_distance(a: &str, b: &str) -> usize {
let b: Vec<char> = b.chars().collect();
let mut prev: Vec<usize> = (0..=b.len()).collect();
let mut cur = vec![0usize; b.len() + 1];
for (i, ca) in a.chars().enumerate() {
cur[0] = i + 1;
for (j, &cb) in b.iter().enumerate() {
let sub = prev[j] + usize::from(ca != cb);
cur[j + 1] = sub.min(prev[j + 1] + 1).min(cur[j] + 1);
}
std::mem::swap(&mut prev, &mut cur);
}
prev[b.len()]
}
pub fn nearest<'a>(
input: &str,
candidates: impl IntoIterator<Item = &'a str>,
k: usize,
) -> Vec<&'a str> {
let mut near: Vec<(usize, usize, &str)> = candidates
.into_iter()
.enumerate()
.map(|(i, c)| (edit_distance(input, c), i, c))
.filter(|&(d, _, _)| d <= 2)
.collect();
near.sort_by_key(|&(d, i, _)| (d, i));
near.into_iter().take(k).map(|(_, _, c)| c).collect()
}
pub fn did_you_mean<S: AsRef<str>>(candidates: &[S]) -> String {
if candidates.is_empty() {
return String::new();
}
let quoted: Vec<String> = candidates
.iter()
.map(|c| format!("'{}'", c.as_ref()))
.collect();
format!("; did you mean {}?", quoted.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn distance_counts_edits() {
assert_eq!(edit_distance("kitten", "sitting"), 3);
assert_eq!(edit_distance("m9", "m8"), 1);
assert_eq!(edit_distance("same", "same"), 0);
}
#[test]
fn nearest_ranks_and_caps_at_a_typo() {
let cands = ["portrait", "landscape", "a0"];
assert_eq!(nearest("portrai", cands, 2), ["portrait"]);
assert!(nearest("zzzzzz", cands, 2).is_empty());
}
#[test]
fn nearest_breaks_ties_by_candidate_order() {
let cands = ["a0", "a1", "a2"];
assert_eq!(nearest("a9", cands, 1), ["a0"]);
}
#[test]
fn clause_is_empty_without_candidates() {
assert_eq!(did_you_mean::<&str>(&[]), "");
assert_eq!(did_you_mean(&["a", "b"]), "; did you mean 'a', 'b'?");
}
}