1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! Fuzzy matching engine for intelligent suggestion filtering
use super::suggestions::RealtimeSuggestion;
/// Fuzzy matching engine using modified Levenshtein distance
pub struct FuzzyMatcher {
pub threshold: f32,
}
impl FuzzyMatcher {
/// Create a new fuzzy matcher with given threshold
pub fn new(threshold: f32) -> Self {
Self { threshold }
}
/// Calculate fuzzy match score between pattern and text
pub fn score(&self, pattern: &str, text: &str) -> f32 {
if pattern.is_empty() {
return 1.0;
}
let pattern = pattern.to_lowercase();
let text = text.to_lowercase();
// Exact match or substring gets highest score
if text.contains(&pattern) {
if text.len() == 0 {
return 1.0; // Avoid division by zero
}
return 1.0 - (pattern.len() as f32 / text.len() as f32) * 0.1;
}
// Character sequence matching
let mut score = 0.0;
let mut last_index = 0;
let mut consecutive_bonus = 0.0;
for ch in pattern.chars() {
if let Some(index) = text[last_index..].find(ch) {
// Use saturating_add to prevent overflow
let actual_index = last_index.saturating_add(index);
// Guard against index going out of bounds
if actual_index >= text.len() {
break;
}
// Bonus for consecutive characters
if actual_index == last_index {
consecutive_bonus += 0.1;
} else {
consecutive_bonus = 0.0;
}
// Bonus for start of word (with bounds check)
let start_bonus = if actual_index == 0
|| (actual_index > 0
&& text
.chars()
.nth(actual_index - 1)
.map_or(false, |c| !c.is_alphanumeric()))
{
0.2
} else {
0.0
};
score += 1.0 + consecutive_bonus + start_bonus;
// Use saturating_add to prevent overflow when moving to next position
last_index = actual_index.saturating_add(1);
} else {
return 0.0; // Character not found
}
}
// Normalize score
let max_score = pattern.len() as f32 * 1.3;
let normalized = score / max_score;
// Penalty for length difference - avoid division by zero
let length_penalty = if text.len() == 0 {
0.0
} else {
(text.len() as f32 - pattern.len() as f32).abs() / text.len() as f32 * 0.2
};
(normalized - length_penalty).max(0.0)
}
/// Get character indices that match the pattern
pub fn get_match_indices(&self, pattern: &str, text: &str) -> Vec<usize> {
let mut indices = Vec::new();
let pattern = pattern.to_lowercase();
let text = text.to_lowercase();
let mut last_index = 0;
for ch in pattern.chars() {
if let Some(index) = text[last_index..].find(ch) {
indices.push(last_index + index);
last_index += index + 1;
}
}
indices
}
/// Filter and score a list of candidates
pub fn filter_suggestions(
&self,
pattern: &str,
candidates: &[(String, String, String)],
) -> Vec<RealtimeSuggestion> {
let mut suggestions: Vec<RealtimeSuggestion> = candidates
.iter()
.filter_map(|(text, description, category)| {
let score = self.score(pattern, text);
if score >= self.threshold {
Some(
RealtimeSuggestion::new(
text.clone(),
description.clone(),
category.clone(),
)
.with_score(score, self.get_match_indices(pattern, text)),
)
} else {
None
}
})
.collect();
// Sort by score (highest first)
suggestions.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
suggestions
}
}