use strsim::normalized_damerau_levenshtein;
pub fn find_closest<'a>(target: &str, candidates: &[&'a str]) -> Option<&'a str> {
let target_lower = target.to_lowercase();
let mut best: Option<(&str, f64)> = None;
for candidate in candidates {
let score = normalized_damerau_levenshtein(&target_lower, &candidate.to_lowercase());
if score >= 0.4 {
match best {
Some((_, prev_score)) if score > prev_score => best = Some((candidate, score)),
None => best = Some((candidate, score)),
_ => {}
}
}
}
best.map(|(c, _)| c)
}
pub fn suggest_preset_name(target: &str, candidates: &[&str]) -> String {
match find_closest(target, candidates) {
Some(closest) => {
format!("preset '{}' not found. Did you mean '{}'?", target, closest)
}
None => format!(
"preset '{}' not found. Available: {}",
target,
candidates
.iter()
.take(5)
.copied()
.collect::<Vec<_>>()
.join(", ")
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_closest_exact_match() {
let candidates = vec!["menu-card", "leave-request", "weather-current"];
assert_eq!(find_closest("menu-card", &candidates), Some("menu-card"));
}
#[test]
fn test_find_closest_typo() {
let candidates = vec!["menu-card", "leave-request"];
assert_eq!(find_closest("menu-crd", &candidates), Some("menu-card"));
}
#[test]
fn test_find_closest_no_match() {
let candidates = vec!["menu-card"];
assert_eq!(find_closest("xyz", &candidates), None);
}
#[test]
fn test_suggest_with_match() {
let candidates = vec!["menu-card", "weather-current"];
let msg = suggest_preset_name("menu-crd", &candidates);
assert!(msg.contains("menu-card"));
assert!(msg.contains("Did you mean"));
}
#[test]
fn test_suggest_without_match() {
let candidates = vec!["menu-card", "weather-current"];
let msg = suggest_preset_name("qqq", &candidates);
assert!(msg.contains("Available"));
}
}