1pub fn fuzzy_matches(query: &str, candidate: &str) -> bool {
9 fuzzy_subsequence_score(query, candidate).is_some()
10}
11
12pub fn fuzzy_subsequence_score(query: &str, candidate: &str) -> Option<usize> {
15 let query = query.trim();
16 if query.is_empty() {
17 return Some(0);
18 }
19
20 let query_chars: Vec<char> = query.to_lowercase().chars().collect();
21 let candidate_lower: Vec<char> = candidate.to_lowercase().chars().collect();
22
23 let mut score = 0usize;
24 let mut qi = 0usize;
25 let mut last_match: Option<usize> = None;
26
27 for (ci, &c) in candidate_lower.iter().enumerate() {
28 if qi < query_chars.len() && c == query_chars[qi] {
29 score += 1;
30 if let Some(last) = last_match {
31 if ci == last + 1 {
32 score += 2;
33 }
34 }
35 if ci == 0 || candidate_lower.get(ci.saturating_sub(1)) == Some(&' ') {
36 score += 3;
37 }
38 last_match = Some(ci);
39 qi += 1;
40 }
41 }
42
43 if qi == query_chars.len() {
44 Some(score)
45 } else {
46 None
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 #[test]
55 fn empty_query_matches_all() {
56 assert!(fuzzy_matches("", "Anything"));
57 assert_eq!(fuzzy_subsequence_score("", "Anything"), Some(0));
58 }
59
60 #[test]
61 fn subsequence_match() {
62 assert!(fuzzy_matches("fb", "Foo Bar"));
63 assert!(!fuzzy_matches("bf", "Foo Bar"));
64 }
65
66 #[test]
67 fn consecutive_bonus_ranks_higher() {
68 let compact = fuzzy_subsequence_score("aa", "aaa").unwrap();
69 let spread = fuzzy_subsequence_score("aa", "aba").unwrap();
70 assert!(compact > spread);
71 }
72}