use smol_str::SmolStr;
pub fn edit_distance(a: &str, b: &str) -> usize {
let a_len = a.chars().count();
let b_len = b.chars().count();
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}
let mut prev_row: Vec<usize> = (0..=b_len).collect();
let mut curr_row = vec![0; b_len + 1];
for (i, a_char) in a.chars().enumerate() {
curr_row[0] = i + 1;
for (j, b_char) in b.chars().enumerate() {
let cost = if a_char == b_char { 0 } else { 1 };
curr_row[j + 1] = (prev_row[j + 1] + 1) .min(curr_row[j] + 1) .min(prev_row[j] + cost); }
std::mem::swap(&mut prev_row, &mut curr_row);
}
prev_row[b_len]
}
pub fn did_you_mean(target: &str, candidates: &[SmolStr]) -> Option<SmolStr> {
let target_lower = target.to_lowercase();
let mut best_match: Option<SmolStr> = None;
let mut best_distance = usize::MAX;
let threshold = (target.len() / 3).max(3);
for candidate in candidates {
let candidate_lower = candidate.to_lowercase();
let distance = edit_distance(&target_lower, &candidate_lower);
if distance == 0 {
return Some(candidate.clone());
}
if distance < best_distance && distance <= threshold {
best_distance = distance;
best_match = Some(candidate.clone());
}
}
best_match
}
pub fn find_similar_matches(
target: &str,
candidates: &[SmolStr],
max_results: usize,
) -> Vec<SmolStr> {
let target_lower = target.to_lowercase();
let threshold = (target.len() / 3).max(3);
let mut matches: Vec<(SmolStr, usize)> = candidates
.iter()
.map(|c| {
let dist = edit_distance(&target_lower, &c.to_lowercase());
(c.clone(), dist)
})
.filter(|(_, d)| *d <= threshold && *d > 0) .collect();
matches.sort_by(|(a, da), (b, db)| da.cmp(db).then(a.cmp(b)));
matches.truncate(max_results);
matches.into_iter().map(|(s, _)| s).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_edit_distance() {
assert_eq!(edit_distance("", ""), 0);
assert_eq!(edit_distance("a", ""), 1);
assert_eq!(edit_distance("", "a"), 1);
assert_eq!(edit_distance("abc", "abc"), 0);
assert_eq!(edit_distance("abc", "def"), 3);
assert_eq!(edit_distance("kitten", "sitting"), 3);
assert_eq!(edit_distance("sunday", "saturday"), 3);
}
#[test]
fn test_did_you_mean() {
let candidates: Vec<SmolStr> = vec![
"username".into(),
"user_id".into(),
"user_name".into(),
"email".into(),
"password".into(),
];
assert_eq!(did_you_mean("email", &candidates), Some("email".into()));
assert_eq!(
did_you_mean("usrname", &candidates),
Some("username".into())
);
assert_eq!(did_you_mean("xyz", &candidates), None);
assert_eq!(did_you_mean("EMAIL", &candidates), Some("email".into()));
}
#[test]
fn test_find_similar_matches() {
let candidates: Vec<SmolStr> = vec![
"username".into(),
"user_id".into(),
"user_name".into(),
"email".into(),
"password".into(),
];
let similar = find_similar_matches("usrname", &candidates, 3);
assert!(!similar.is_empty());
assert!(similar.contains(&"username".into()));
}
#[test]
fn test_did_you_mean_with_import_names() {
let available_exports: Vec<SmolStr> =
vec!["Foo".into(), "Bar".into(), "Baz".into(), "MyMessage".into()];
assert_eq!(did_you_mean("Baaz", &available_exports), Some("Baz".into()));
assert_eq!(
did_you_mean("MyMesage", &available_exports),
Some("MyMessage".into())
);
assert_eq!(did_you_mean("SomethingElse", &available_exports), None);
}
}