use strsim::levenshtein;
pub fn suggest_similar_symbols(
target: &str,
candidates: &[String],
max_distance: usize,
) -> Vec<String> {
if target.is_empty() || candidates.is_empty() {
return Vec::new();
}
let target_first = match target.chars().next() {
Some(c) => c,
None => return Vec::new(),
};
let mut suggestions: Vec<_> = candidates
.iter()
.filter(|symbol| {
symbol.chars().next() == Some(target_first)
})
.map(|symbol| {
let dist = levenshtein(target, symbol);
(symbol.clone(), dist)
})
.filter(|(_, dist)| {
*dist > 0 && *dist <= max_distance
})
.collect();
suggestions.sort_by_key(|(_, dist)| *dist);
suggestions.truncate(5);
suggestions.into_iter().map(|(name, _)| name).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_suggest_similar_symbols_exact_match_excluded() {
let symbols = vec!["foo".to_string()];
let suggestions = suggest_similar_symbols("foo", &symbols, 3);
assert_eq!(suggestions.len(), 0, "Exact match should not be suggested");
}
#[test]
fn test_suggest_similar_single_edit() {
let symbols = vec![
"foo".to_string(),
"foobar".to_string(),
"bar".to_string(),
"baz".to_string(),
];
let suggestions = suggest_similar_symbols("fooo", &symbols, 2);
assert_eq!(suggestions, vec!["foo".to_string()]);
}
#[test]
fn test_suggest_similar_transposition() {
let symbols = vec!["apple".to_string(), "banana".to_string()];
let suggestions = suggest_similar_symbols("appel", &symbols, 2);
assert_eq!(suggestions, vec!["apple".to_string()]);
}
#[test]
fn test_suggest_similar_empty_target() {
let symbols = vec!["foo".to_string()];
let suggestions = suggest_similar_symbols("", &symbols, 3);
assert_eq!(suggestions.len(), 0);
}
#[test]
fn test_suggest_similar_limit_to_five() {
let symbols = vec![
"foo1".to_string(),
"foo2".to_string(),
"foo3".to_string(),
"foo4".to_string(),
"foo5".to_string(),
"foo6".to_string(),
];
let suggestions = suggest_similar_symbols("foox", &symbols, 3);
assert!(suggestions.len() <= 5, "Should limit to 5 suggestions");
}
}