use std::collections::HashMap;
pub struct FuzzyMatcher {
symbols: HashMap<SymbolType, Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SymbolType {
Variable,
Function,
Type,
Module,
Field,
Method,
}
impl FuzzyMatcher {
pub fn new() -> Self {
Self {
symbols: HashMap::new(),
}
}
pub fn add_symbol(&mut self, symbol_type: SymbolType, name: String) {
self.symbols.entry(symbol_type).or_default().push(name);
}
pub fn add_symbols(&mut self, symbol_type: SymbolType, names: Vec<String>) {
let entry = self.symbols.entry(symbol_type).or_default();
entry.extend(names);
}
pub fn find_best_match(&self, symbol_type: SymbolType, typo: &str) -> Option<(String, usize)> {
let symbols = self.symbols.get(&symbol_type)?;
let mut best_match: Option<(String, usize)> = None;
for symbol in symbols {
let distance = levenshtein_distance(typo, symbol);
let max_distance = std::cmp::min(3, std::cmp::max(typo.len(), symbol.len()) * 3 / 10);
if distance <= max_distance {
if let Some((_, best_distance)) = best_match {
if distance < best_distance {
best_match = Some((symbol.clone(), distance));
}
} else {
best_match = Some((symbol.clone(), distance));
}
}
}
best_match
}
pub fn find_suggestions(
&self,
symbol_type: SymbolType,
typo: &str,
max_suggestions: usize,
) -> Vec<(String, usize)> {
let symbols = match self.symbols.get(&symbol_type) {
Some(s) => s,
None => return Vec::new(),
};
let mut matches: Vec<(String, usize)> = Vec::new();
for symbol in symbols {
let distance = levenshtein_distance(typo, symbol);
let max_distance = std::cmp::min(3, std::cmp::max(typo.len(), symbol.len()) * 3 / 10);
if distance <= max_distance {
matches.push((symbol.clone(), distance));
}
}
matches.sort_by_key(|(_, distance)| *distance);
matches.truncate(max_suggestions);
matches
}
pub fn has_symbol(&self, symbol_type: SymbolType, name: &str) -> bool {
self.symbols
.get(&symbol_type)
.map(|symbols| symbols.iter().any(|s| s == name))
.unwrap_or(false)
}
pub fn get_symbols(&self, symbol_type: SymbolType) -> Vec<String> {
self.symbols.get(&symbol_type).cloned().unwrap_or_default()
}
pub fn clear(&mut self) {
self.symbols.clear();
}
pub fn stats(&self) -> FuzzyMatcherStats {
let mut total_symbols = 0;
let mut by_type: HashMap<SymbolType, usize> = HashMap::new();
for (symbol_type, symbols) in &self.symbols {
let count = symbols.len();
total_symbols += count;
by_type.insert(*symbol_type, count);
}
FuzzyMatcherStats {
total_symbols,
by_type,
}
}
}
impl Default for FuzzyMatcher {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FuzzyMatcherStats {
pub total_symbols: usize,
pub by_type: HashMap<SymbolType, usize>,
}
pub fn levenshtein_distance(s1: &str, s2: &str) -> usize {
let len1 = s1.len();
let len2 = s2.len();
if len1 == 0 {
return len2;
}
if len2 == 0 {
return len1;
}
if s1 == s2 {
return 0;
}
let (s1, s2, len1, len2) = if len1 > len2 {
(s2, s1, len2, len1)
} else {
(s1, s2, len1, len2)
};
let s1_chars: Vec<char> = s1.chars().collect();
let s2_chars: Vec<char> = s2.chars().collect();
let mut prev_row: Vec<usize> = (0..=len1).collect();
let mut curr_row: Vec<usize> = vec![0; len1 + 1];
for i in 1..=len2 {
curr_row[0] = i;
for j in 1..=len1 {
let cost = if s2_chars[i - 1] == s1_chars[j - 1] {
0
} else {
1
};
curr_row[j] = std::cmp::min(
std::cmp::min(
curr_row[j - 1] + 1, prev_row[j] + 1, ),
prev_row[j - 1] + cost, );
}
std::mem::swap(&mut prev_row, &mut curr_row);
}
prev_row[len1]
}
pub fn similarity_ratio(s1: &str, s2: &str) -> f64 {
let max_len = std::cmp::max(s1.len(), s2.len());
if max_len == 0 {
return 1.0;
}
let distance = levenshtein_distance(s1, s2);
1.0 - (distance as f64 / max_len as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_levenshtein_distance() {
assert_eq!(levenshtein_distance("", ""), 0);
assert_eq!(levenshtein_distance("abc", "abc"), 0);
assert_eq!(levenshtein_distance("", "abc"), 3);
assert_eq!(levenshtein_distance("abc", ""), 3);
assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
assert_eq!(levenshtein_distance("saturday", "sunday"), 3);
assert_eq!(levenshtein_distance("abc", "def"), 3);
}
#[test]
fn test_similarity_ratio() {
assert_eq!(similarity_ratio("abc", "abc"), 1.0);
assert!((similarity_ratio("kitten", "sitting") - 0.571).abs() < 0.01);
assert_eq!(similarity_ratio("", ""), 1.0);
}
#[test]
fn test_fuzzy_matcher_basic() {
let mut matcher = FuzzyMatcher::new();
matcher.add_symbol(SymbolType::Variable, "count".to_string());
matcher.add_symbol(SymbolType::Variable, "counter".to_string());
matcher.add_symbol(SymbolType::Variable, "index".to_string());
assert!(matcher.has_symbol(SymbolType::Variable, "count"));
let result = matcher.find_best_match(SymbolType::Variable, "cont");
assert!(result.is_some());
let (suggestion, distance) = result.unwrap();
assert_eq!(suggestion, "count");
assert_eq!(distance, 1); }
#[test]
fn test_fuzzy_matcher_suggestions() {
let mut matcher = FuzzyMatcher::new();
matcher.add_symbol(SymbolType::Function, "print".to_string());
matcher.add_symbol(SymbolType::Function, "println".to_string());
matcher.add_symbol(SymbolType::Function, "printf".to_string());
let suggestions = matcher.find_suggestions(SymbolType::Function, "pring", 3);
assert!(!suggestions.is_empty());
assert_eq!(suggestions[0].0, "print"); }
#[test]
fn test_fuzzy_matcher_no_match() {
let mut matcher = FuzzyMatcher::new();
matcher.add_symbol(SymbolType::Variable, "foo".to_string());
let result = matcher.find_best_match(SymbolType::Variable, "completely_different");
assert!(result.is_none());
}
#[test]
fn test_fuzzy_matcher_stats() {
let mut matcher = FuzzyMatcher::new();
matcher.add_symbol(SymbolType::Variable, "x".to_string());
matcher.add_symbol(SymbolType::Variable, "y".to_string());
matcher.add_symbol(SymbolType::Function, "foo".to_string());
let stats = matcher.stats();
assert_eq!(stats.total_symbols, 3);
assert_eq!(*stats.by_type.get(&SymbolType::Variable).unwrap(), 2);
assert_eq!(*stats.by_type.get(&SymbolType::Function).unwrap(), 1);
}
}