pub fn closest(needle: &str, haystack: &[&str], max_distance: usize) -> Option<String> {
haystack
.iter()
.map(|cand| (*cand, strsim::levenshtein(needle, cand)))
.filter(|(_, d)| *d <= max_distance)
.min_by_key(|(_, d)| *d)
.map(|(cand, _)| cand.to_string())
}
pub fn threshold_for(ident: &str) -> usize {
let len = ident.chars().count();
if len <= 4 {
2
} else {
(len / 3).min(3).max(1)
}
}
pub fn suggest(needle: &str, haystack: &[&str]) -> Option<String> {
closest(needle, haystack, threshold_for(needle))
}
pub fn help_line(suggestion: Option<&str>) -> String {
match suggestion {
Some(s) => format!("\n help: did you mean `{}`?", s),
None => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_close_match() {
let haystack = ["text", "target", "size"];
assert_eq!(suggest("texx", &haystack).as_deref(), Some("text"));
}
#[test]
fn rejects_wildly_different() {
let haystack = ["text", "target", "size"];
assert_eq!(suggest("foo", &haystack), None);
}
#[test]
fn stable_tie_break_returns_first_by_order() {
let haystack = ["text", "size"];
assert_eq!(suggest("tex", &haystack).as_deref(), Some("text"));
}
#[test]
fn threshold_scales_with_len() {
assert_eq!(threshold_for("ab"), 2);
assert_eq!(threshold_for("abcd"), 2);
assert_eq!(threshold_for("abcdef"), 2); assert_eq!(threshold_for("abcdefghi"), 3); assert_eq!(threshold_for("abcdefghijkl"), 3); }
#[test]
fn help_line_formats_suggestion() {
assert_eq!(
help_line(Some("Button")),
"\n help: did you mean `Button`?"
);
assert_eq!(help_line(None), "");
}
}