use strsim::levenshtein;
#[derive(Debug, Clone)]
pub struct FuzzyHit {
pub query_token: String,
pub matched_token: String,
pub distance: usize,
}
pub fn is_fuzzy_match(query_token: &str, candidate_token: &str) -> bool {
let distance = levenshtein(query_token, candidate_token);
if distance == 0 {
return true; }
let len = query_token.len().min(candidate_token.len());
let max_distance = match len {
0..=4 => 1,
5..=9 => 2,
_ => 3,
};
distance <= max_distance
}
pub fn fuzzy_match_ratio(
query_tokens: &[String],
candidate_tokens: &[String],
) -> (f64, Vec<FuzzyHit>) {
if query_tokens.is_empty() {
return (0.0, Vec::new());
}
let mut hits = Vec::new();
let mut matched_count = 0;
for qt in query_tokens {
let mut best_distance = usize::MAX;
let mut best_candidate = None;
for ct in candidate_tokens {
let distance = levenshtein(qt, ct);
if distance < best_distance {
best_distance = distance;
best_candidate = Some(ct.clone());
}
}
if let Some(candidate) = best_candidate {
if is_fuzzy_match(qt, &candidate) {
matched_count += 1;
hits.push(FuzzyHit {
query_token: qt.clone(),
matched_token: candidate,
distance: best_distance,
});
}
}
}
let ratio = matched_count as f64 / query_tokens.len() as f64;
(ratio, hits)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_threshold_short_token() {
assert!(is_fuzzy_match("fin", "find"));
}
#[test]
fn test_threshold_medium_token() {
assert!(is_fuzzy_match("stauts", "status"));
}
#[test]
fn test_threshold_long_token() {
assert!(is_fuzzy_match("subscrption", "subscription"));
}
#[test]
fn test_threshold_exceeded() {
assert!(!is_fuzzy_match("abc", "xyz"));
}
#[test]
fn test_fuzzy_ratio() {
let query = vec!["fin".to_string(), "pets".to_string()];
let candidate = vec![
"find".to_string(),
"pets".to_string(),
"by".to_string(),
"status".to_string(),
];
let (ratio, hits) = fuzzy_match_ratio(&query, &candidate);
assert!(ratio > 0.0);
assert_eq!(hits.len(), 2); }
#[test]
fn test_exact_match_distance_zero() {
assert!(is_fuzzy_match("status", "status"));
let query = vec!["status".to_string()];
let candidate = vec!["status".to_string()];
let (ratio, hits) = fuzzy_match_ratio(&query, &candidate);
assert_eq!(ratio, 1.0);
assert_eq!(hits[0].distance, 0);
}
#[test]
fn test_empty_query_tokens() {
let (ratio, hits) = fuzzy_match_ratio(&[], &["test".to_string()]);
assert_eq!(ratio, 0.0);
assert!(hits.is_empty());
}
#[test]
fn test_no_match_completely_different() {
assert!(!is_fuzzy_match("hello", "world"));
}
}