pub fn levenshtein_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 find_similar<'a>(
candidates: impl IntoIterator<Item = &'a str>,
target: &str,
max_distance: usize,
) -> Vec<&'a str> {
let mut results: Vec<(&str, usize)> = candidates
.into_iter()
.filter_map(|candidate| {
let distance = levenshtein_distance(candidate, target);
if distance <= max_distance && distance > 0 {
Some((candidate, distance))
} else {
None
}
})
.collect();
results.sort_by_key(|(_, d)| *d);
results.into_iter().map(|(s, _)| s).collect()
}
pub fn reasonable_max_distance(target: &str) -> usize {
let len = target.len();
if len <= 2 {
1
} else if len <= 5 {
2
} else {
3
}
}
pub fn format_suggestion(similar: &[&str]) -> Option<String> {
match similar.len() {
0 => None,
1 => Some(format!("Did you mean '{}'?", similar[0])),
2 => Some(format!(
"Did you mean '{}' or '{}'?",
similar[0], similar[1]
)),
_ => Some(format!(
"Did you mean '{}', '{}', or '{}'?",
similar[0], similar[1], similar[2]
)),
}
}
pub fn suggest_variable(
undefined_name: &str,
available_names: impl IntoIterator<Item = impl AsRef<str>>,
) -> Option<String> {
let available: Vec<String> = available_names
.into_iter()
.map(|s| s.as_ref().to_string())
.collect();
let max_dist = reasonable_max_distance(undefined_name);
let similar = find_similar(
available.iter().map(|s| s.as_str()),
undefined_name,
max_dist,
);
format_suggestion(&similar)
}
pub fn suggest_function(
undefined_name: &str,
available_functions: impl IntoIterator<Item = impl AsRef<str>>,
) -> Option<String> {
let available: Vec<String> = available_functions
.into_iter()
.map(|s| s.as_ref().to_string())
.collect();
let max_dist = reasonable_max_distance(undefined_name);
let similar = find_similar(
available.iter().map(|s| s.as_str()),
undefined_name,
max_dist,
);
format_suggestion(&similar)
}
pub fn suggest_property(
unknown_prop: &str,
available_props: impl IntoIterator<Item = impl AsRef<str>>,
) -> Option<String> {
let available: Vec<String> = available_props
.into_iter()
.map(|s| s.as_ref().to_string())
.collect();
let max_dist = reasonable_max_distance(unknown_prop);
let similar = find_similar(available.iter().map(|s| s.as_str()), unknown_prop, max_dist);
format_suggestion(&similar)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_levenshtein_same_string() {
assert_eq!(levenshtein_distance("hello", "hello"), 0);
}
#[test]
fn test_levenshtein_empty_strings() {
assert_eq!(levenshtein_distance("", ""), 0);
assert_eq!(levenshtein_distance("hello", ""), 5);
assert_eq!(levenshtein_distance("", "world"), 5);
}
#[test]
fn test_levenshtein_single_edit() {
assert_eq!(levenshtein_distance("hello", "hallo"), 1);
assert_eq!(levenshtein_distance("hello", "helloo"), 1);
assert_eq!(levenshtein_distance("hello", "helo"), 1);
}
#[test]
fn test_levenshtein_multiple_edits() {
assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
assert_eq!(levenshtein_distance("saturday", "sunday"), 3);
}
#[test]
fn test_find_similar() {
let candidates = vec!["count", "counter", "amount", "account", "mount"];
let similar = find_similar(candidates.iter().copied(), "cont", 2);
assert!(similar.contains(&"count"));
}
#[test]
fn test_find_similar_no_matches() {
let candidates = vec!["apple", "banana", "cherry"];
let similar = find_similar(candidates.iter().copied(), "xyz", 2);
assert!(similar.is_empty());
}
#[test]
fn test_format_suggestion_single() {
let similar = vec!["count"];
assert_eq!(
format_suggestion(&similar),
Some("Did you mean 'count'?".to_string())
);
}
#[test]
fn test_format_suggestion_multiple() {
let similar = vec!["count", "counter"];
assert_eq!(
format_suggestion(&similar),
Some("Did you mean 'count' or 'counter'?".to_string())
);
}
#[test]
fn test_suggest_variable() {
let available = vec!["count", "counter", "total"];
let suggestion = suggest_variable("cont", available);
assert!(suggestion.is_some());
assert!(suggestion.unwrap().contains("count"));
}
#[test]
fn test_reasonable_max_distance() {
assert_eq!(reasonable_max_distance("a"), 1);
assert_eq!(reasonable_max_distance("ab"), 1);
assert_eq!(reasonable_max_distance("abc"), 2);
assert_eq!(reasonable_max_distance("hello"), 2);
assert_eq!(reasonable_max_distance("variable"), 3);
}
}