1#[derive(Clone, Debug, PartialEq)]
21pub struct FuzzyMatch {
22 pub score: i32,
24 pub indices: Vec<usize>,
26}
27
28impl FuzzyMatch {
29 pub fn new(score: i32, indices: Vec<usize>) -> Self {
31 Self { score, indices }
32 }
33}
34
35pub fn fuzzy_match(pattern: &str, target: &str) -> Option<FuzzyMatch> {
61 if pattern.is_empty() {
62 return Some(FuzzyMatch::new(0, vec![]));
63 }
64
65 let pattern_lower: Vec<char> = pattern.to_lowercase().chars().collect();
66 let pattern_chars: Vec<char> = pattern.chars().collect();
67 let target_chars: Vec<char> = target.chars().collect();
68 let target_lower: Vec<char> = target.to_lowercase().chars().collect();
69
70 let mut indices = Vec::with_capacity(pattern_lower.len());
71 let mut score = 0i32;
72 let mut pattern_idx = 0;
73 let mut prev_match_idx: Option<usize> = None;
74
75 for (i, &ch) in target_lower.iter().enumerate() {
76 if pattern_idx < pattern_lower.len() && ch == pattern_lower[pattern_idx] {
77 indices.push(i);
78
79 score += 1;
81
82 if target_chars[i] == pattern_chars.get(pattern_idx).copied().unwrap_or(' ') {
84 score += 1;
85 }
86
87 if let Some(prev) = prev_match_idx {
89 if i == prev + 1 {
90 score += 3;
91 }
92 }
93
94 if i == 0 {
96 score += 2; } else {
98 let prev_char = target_chars[i - 1];
99 if !prev_char.is_alphanumeric()
100 || (prev_char.is_lowercase() && target_chars[i].is_uppercase())
101 {
102 score += 2; }
104 }
105
106 prev_match_idx = Some(i);
107 pattern_idx += 1;
108 }
109 }
110
111 if pattern_idx == pattern_lower.len() {
112 Some(FuzzyMatch::new(score, indices))
113 } else {
114 None
115 }
116}
117
118pub fn fuzzy_match_threshold(pattern: &str, target: &str, min_score: i32) -> Option<FuzzyMatch> {
120 fuzzy_match(pattern, target).filter(|m| m.score >= min_score)
121}
122
123pub fn fuzzy_score(pattern: &str, target: &str) -> i32 {
125 fuzzy_match(pattern, target).map(|m| m.score).unwrap_or(0)
126}
127
128pub fn fuzzy_matches(pattern: &str, target: &str) -> bool {
130 fuzzy_match(pattern, target).is_some()
131}
132
133pub fn fuzzy_filter<'a, T: AsRef<str>>(pattern: &str, items: &'a [T]) -> Vec<(&'a T, FuzzyMatch)> {
147 let mut matches: Vec<_> = items
148 .iter()
149 .filter_map(|item| fuzzy_match(pattern, item.as_ref()).map(|m| (item, m)))
150 .collect();
151
152 matches.sort_by(|a, b| b.1.score.cmp(&a.1.score));
153 matches
154}
155
156pub fn fuzzy_filter_simple<'a, T: AsRef<str>>(pattern: &str, items: &'a [T]) -> Vec<&'a T> {
158 fuzzy_filter(pattern, items)
159 .into_iter()
160 .map(|(item, _)| item)
161 .collect()
162}
163
164#[derive(Clone, Debug)]
166pub struct FuzzyMatcher {
167 pattern: Vec<char>,
169 original_chars: Vec<char>,
171 min_score: i32,
173}
174
175impl FuzzyMatcher {
176 pub fn new(pattern: &str) -> Self {
178 Self {
179 pattern: pattern.to_lowercase().chars().collect(),
180 original_chars: pattern.chars().collect(),
181 min_score: 0,
182 }
183 }
184
185 pub fn min_score(mut self, score: i32) -> Self {
187 self.min_score = score;
188 self
189 }
190
191 pub fn is_empty(&self) -> bool {
193 self.pattern.is_empty()
194 }
195
196 pub fn match_str(&self, target: &str) -> Option<FuzzyMatch> {
198 if self.pattern.is_empty() {
199 return Some(FuzzyMatch::new(0, vec![]));
200 }
201
202 let target_chars: Vec<char> = target.chars().collect();
203 let target_lower: Vec<char> = target.to_lowercase().chars().collect();
204
205 let mut indices = Vec::with_capacity(self.pattern.len());
206 let mut score = 0i32;
207 let mut pattern_idx = 0;
208 let mut prev_match_idx: Option<usize> = None;
209
210 for (i, &ch) in target_lower.iter().enumerate() {
211 if pattern_idx < self.pattern.len() && ch == self.pattern[pattern_idx] {
212 indices.push(i);
213 score += 1;
214
215 if let Some(orig_ch) = self.original_chars.get(pattern_idx).copied() {
217 if target_chars[i] == orig_ch {
218 score += 1;
219 }
220 }
221
222 if let Some(prev) = prev_match_idx {
224 if i == prev + 1 {
225 score += 3;
226 }
227 }
228
229 if i == 0 {
231 score += 2;
232 } else {
233 let prev_char = target_chars[i - 1];
234 if !prev_char.is_alphanumeric()
235 || (prev_char.is_lowercase() && target_chars[i].is_uppercase())
236 {
237 score += 2;
238 }
239 }
240
241 prev_match_idx = Some(i);
242 pattern_idx += 1;
243 }
244 }
245
246 if pattern_idx == self.pattern.len() && score >= self.min_score {
247 Some(FuzzyMatch::new(score, indices))
248 } else {
249 None
250 }
251 }
252
253 pub fn filter<'a, T: AsRef<str>>(&self, items: &'a [T]) -> Vec<(&'a T, FuzzyMatch)> {
255 let mut matches: Vec<_> = items
256 .iter()
257 .filter_map(|item| self.match_str(item.as_ref()).map(|m| (item, m)))
258 .collect();
259
260 matches.sort_by(|a, b| b.1.score.cmp(&a.1.score));
261 matches
262 }
263}