pub fn fuzzy_search(key: &str, lst: &[impl AsRef<str>]) -> Option<String> {
fuzzy_search_limited(key, lst, None)
}
pub fn fuzzy_search_limited(
key: &str,
lst: &[impl AsRef<str>],
max_distance: Option<usize>,
) -> Option<String> {
if key.is_empty() || lst.is_empty() {
None
} else {
let t = lst.iter().fold((usize::MAX, ""), |acc, word| {
let e = levenshtein_distance(key, word.as_ref());
if e < acc.0 {
(e, word.as_ref())
} else {
acc
}
});
if let Some(threshold) = max_distance {
(t.0 <= threshold).then_some(t.1.to_owned())
} else {
Some(t.1.to_owned())
}
}
}
fn levenshtein_distance(word1: &str, word2: &str) -> usize {
let w1 = word1.chars().collect::<Vec<_>>();
let w2 = word2.chars().collect::<Vec<_>>();
let word1_length = w1.len() + 1;
let word2_length = w2.len() + 1;
let mut matrix = vec![vec![0; word1_length]; word2_length];
#[expect(clippy::indexing_slicing, reason = "see above comment")]
#[expect(
clippy::needless_range_loop,
reason = "probably clearer written like this than with iterators"
)]
for i in 1..word1_length {
matrix[0][i] = i;
}
#[expect(
clippy::indexing_slicing,
reason = "similar to above, but fixing the column index instead"
)]
#[expect(
clippy::needless_range_loop,
reason = "probably clearer written like this than with iterators"
)]
for j in 1..word2_length {
matrix[j][0] = j;
}
#[expect(
clippy::indexing_slicing,
reason = r#"
`i` and `j` start at 1, so the accesses at `i - 1` and `j - 1` are safe.
`i` and `j` stop before the length of the array in their respective
dimensions, so accesses at `i` and `j` are safe.
"#
)]
for j in 1..word2_length {
for i in 1..word1_length {
let x: usize = if w1[i - 1] == w2[j - 1] {
matrix[j - 1][i - 1]
} else {
1 + std::cmp::min(
std::cmp::min(matrix[j][i - 1], matrix[j - 1][i]),
matrix[j - 1][i - 1],
)
};
matrix[j][i] = x;
}
}
#[expect(
clippy::indexing_slicing,
reason = "Accesses at one less than length in both dimensions. The length in both dimensions is non-zero"
)]
matrix[word2_length - 1][word1_length - 1]
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_match1() {
let word1 = "user::Alice";
let words = vec!["User::Alice", "user::alice", "user", "alice"];
let x = fuzzy_search(word1, &words);
assert_eq!(x, Some("User::Alice".to_owned()));
}
#[test]
fn test_match2() {
let word1 = "princpal";
let words = vec![
"principal",
"Principal",
"principality",
"prince",
"principle",
];
let x = fuzzy_search(word1, &words);
assert_eq!(x, Some("principal".to_owned()));
}
#[test]
fn test_match3() {
let word1 = "prncpal";
let words = vec![
"principal",
"Principal",
"principality",
"prince",
"principle",
];
let x = fuzzy_search(word1, &words);
assert_eq!(x, Some("principal".to_owned()));
}
#[test]
fn test_match4() {
let word1 = "user::\"Alice\"";
let words = vec!["User::\"Alice\"", "user::\"alice\"", "user", "alice"];
let x = fuzzy_search(word1, &words);
assert_eq!(x, Some("User::\"Alice\"".to_owned()));
}
#[test]
fn test_match5() {
let word1 = "";
let words = vec!["User::\"Alice\"", "user::\"alice\"", "user", "alice"];
let x = fuzzy_search(word1, &words);
assert_eq!(x, None); }
#[test]
fn test_match6() {
let word1 = "prncpal";
let words = vec![
"principal",
"Principal",
"principality",
"principal",
"prince",
"principle",
"principal",
];
let x = fuzzy_search(word1, &words);
assert_eq!(x, Some("principal".to_owned()));
}
#[test]
fn test_match7() {
let word1 = "User::\"Alice\"";
let words = vec!["User::\'Alice\'", "user::\"alice\"", "user", "alice"];
let x = fuzzy_search(word1, &words);
assert_eq!(x, Some("User::\'Alice\'".to_owned()));
}
#[test]
fn test_match_empty() {
let word1 = "user::Alice";
let words: Vec<&str> = Vec::new();
let x = fuzzy_search(word1, &words);
assert_eq!(x, None);
}
}