pub fn levenshtein_distance(a: &str, b: &str) -> usize {
let a_len = a.len();
let b_len = b.len();
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}
let mut matrix = vec![vec![0; b_len + 1]; a_len + 1];
#[allow(clippy::needless_range_loop)]
for i in 0..=a_len {
matrix[i][0] = i;
}
#[allow(clippy::needless_range_loop)]
for j in 0..=b_len {
matrix[0][j] = j;
}
for (i, a_char) in a.chars().enumerate() {
for (j, b_char) in b.chars().enumerate() {
let cost = if a_char == b_char { 0 } else { 1 };
matrix[i + 1][j + 1] = std::cmp::min(
std::cmp::min(
matrix[i][j + 1] + 1, matrix[i + 1][j] + 1, ),
matrix[i][j] + cost, );
}
}
matrix[a_len][b_len]
}
pub fn find_closest_match<'a>(
target: &str,
candidates: &[&'a str],
threshold: usize,
) -> Option<(&'a str, usize)> {
let mut best_match: Option<(&'a str, usize)> = None;
for &candidate in candidates {
let distance = levenshtein_distance(target, candidate);
if distance <= threshold {
match best_match {
None => best_match = Some((candidate, distance)),
Some((_, best_distance)) if distance < best_distance => {
best_match = Some((candidate, distance));
}
_ => {}
}
}
}
best_match
}
pub fn suggest(unknown: &str, candidates: &[&str], threshold: usize) -> String {
if let Some((suggestion, distance)) = find_closest_match(unknown, candidates, threshold) {
format!(" Did you mean '{}'? (distance: {})", suggestion, distance)
} else {
String::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_levenshtein_distance_identical() {
assert_eq!(levenshtein_distance("hello", "hello"), 0);
}
#[test]
fn test_levenshtein_distance_one_substitution() {
assert_eq!(levenshtein_distance("on_click", "on_clik"), 1);
}
#[test]
fn test_levenshtein_distance_one_insertion() {
assert_eq!(levenshtein_distance("click", "clicck"), 1);
}
#[test]
fn test_levenshtein_distance_one_deletion() {
assert_eq!(levenshtein_distance("button", "buton"), 1);
}
#[test]
fn test_levenshtein_distance_multiple_edits() {
assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
}
#[test]
fn test_levenshtein_distance_empty_strings() {
assert_eq!(levenshtein_distance("", ""), 0);
assert_eq!(levenshtein_distance("hello", ""), 5);
assert_eq!(levenshtein_distance("", "world"), 5);
}
#[test]
fn test_find_closest_match_within_threshold() {
let candidates = vec!["on_click", "on_press", "on_change"];
let result = find_closest_match("on_clik", &candidates, 3);
assert_eq!(result, Some(("on_click", 1)));
}
#[test]
fn test_find_closest_match_exceeds_threshold() {
let candidates = vec!["on_click", "on_press"];
let result = find_closest_match("completely_different", &candidates, 3);
assert_eq!(result, None);
}
#[test]
fn test_find_closest_match_multiple_candidates() {
let candidates = vec!["increment", "decrement", "increment_by"];
let result = find_closest_match("incremnt", &candidates, 3);
assert_eq!(result, Some(("increment", 1)));
}
#[test]
fn test_suggest_with_match() {
let candidates = vec!["on_click", "on_press"];
let result = suggest("on_clik", &candidates, 3);
assert_eq!(result, " Did you mean 'on_click'? (distance: 1)");
}
#[test]
fn test_suggest_without_match() {
let candidates = vec!["on_click", "on_press"];
let result = suggest("completely_different", &candidates, 3);
assert_eq!(result, "");
}
#[test]
fn test_suggest_threshold_boundary() {
let candidates = vec!["on_click"];
let result = suggest("on_cxxxk", &candidates, 3);
assert!(result.contains("on_click"));
let result = suggest("on_cxxxk", &candidates, 2);
assert_eq!(result, "");
}
}