rusty_bubbles/internal/
fuzzy.rs1use std::cmp::Ordering;
8
9#[derive(Debug, Clone)]
11pub struct Match {
12 pub str: String,
14 pub index: usize,
16 pub matched_indexes: Vec<usize>,
18 pub score: i32,
20}
21
22const FIRST_CHAR_MATCH_BONUS: i32 = 10;
23const MATCH_FOLLOWING_SEPARATOR_BONUS: i32 = 20;
24const CAMEL_CASE_MATCH_BONUS: i32 = 20;
25const ADJACENT_MATCH_BONUS: i32 = 5;
26const UNMATCHED_LEADING_CHAR_PENALTY: i32 = -5;
27const MAX_UNMATCHED_LEADING_CHAR_PENALTY: i32 = -15;
28
29const SEPARATORS: [char; 6] = ['/', '-', '_', '.', ' ', '\\'];
30
31pub fn find(pattern: &str, data: &[String]) -> Vec<Match> {
49 let mut matches = find_no_sort(pattern, data);
50 matches.sort_by(|a, b| a.score.cmp(&b.score).reverse());
51 matches
52}
53
54pub fn find_no_sort(pattern: &str, data: &[String]) -> Vec<Match> {
57 if pattern.is_empty() {
58 return vec![];
59 }
60 let runes: Vec<char> = pattern.chars().collect();
61 let mut matches: Vec<Match> = Vec::new();
62 let mut matched_indexes: Option<Vec<usize>> = None;
63 for (i, s) in data.iter().enumerate() {
64 let mut match_ = Match {
65 str: s.clone(),
66 index: i,
67 matched_indexes: matched_indexes
68 .take()
69 .unwrap_or_else(|| Vec::with_capacity(runes.len())),
70 score: 0,
71 };
72 let mut pattern_index = 0usize;
73 let mut best_score = -1i32;
74 let mut matched_index: isize = -1;
75 let mut curr_adjacent_match_bonus = 0i32;
76 let mut last: char = '\0';
77 let mut last_index = 0usize;
78 let chars: Vec<char> = s.chars().collect();
79 let mut j = 0usize;
80 while j < chars.len() {
81 let candidate = chars[j];
82 if let Some(pc) = runes.get(pattern_index).copied() {
83 if equal_fold(candidate, pc) {
84 let mut score = 0i32;
85 if j == 0 {
86 score += FIRST_CHAR_MATCH_BONUS;
87 }
88 if last.is_lowercase() && candidate.is_uppercase() {
89 score += CAMEL_CASE_MATCH_BONUS;
90 }
91 if j != 0 && is_separator(last) {
92 score += MATCH_FOLLOWING_SEPARATOR_BONUS;
93 }
94 if let Some(&last_match) = match_.matched_indexes.last() {
95 let bonus =
96 adjacent_char_bonus(last_index, last_match, curr_adjacent_match_bonus);
97 score += bonus;
98 curr_adjacent_match_bonus += bonus;
102 }
103 if score > best_score {
104 best_score = score;
105 matched_index = j as isize;
106 }
107 }
108 }
109 let nextp = if pattern_index + 1 < runes.len() {
110 Some(runes[pattern_index + 1])
111 } else {
112 None
113 };
114 let nextc = if j + 1 < chars.len() {
115 Some(chars[j + 1])
116 } else {
117 None
118 };
119 if ((nextp.is_some() && nextc.is_some() && equal_fold(nextp.unwrap(), nextc.unwrap()))
124 || nextc.is_none())
125 && matched_index > -1
126 {
127 if match_.matched_indexes.is_empty() {
128 let penalty = matched_index as i32 * UNMATCHED_LEADING_CHAR_PENALTY;
129 best_score += max(penalty, MAX_UNMATCHED_LEADING_CHAR_PENALTY);
130 }
131 match_.score += best_score;
132 match_.matched_indexes.push(matched_index as usize);
133 best_score = -1;
134 pattern_index += 1;
135 }
136 last_index = j;
137 last = candidate;
138 j += 1;
139 }
140 let penalty = match_.matched_indexes.len() as i32 - chars.len() as i32;
142 match_.score += penalty;
143 if match_.matched_indexes.len() == runes.len() {
144 matches.push(match_);
145 matched_indexes = None;
146 } else {
147 matched_indexes = Some(match_.matched_indexes.clone());
148 }
149 }
150 matches
151}
152
153fn equal_fold(tr: char, sr: char) -> bool {
155 if tr == sr {
156 return true;
157 }
158 if tr.to_lowercase().collect::<String>() == sr.to_lowercase().collect::<String>() {
159 return true;
160 }
161 let tr_lower = tr.to_ascii_lowercase();
163 let sr_lower = sr.to_ascii_lowercase();
164 tr_lower == sr_lower && (tr.is_ascii_alphabetic() || sr.is_ascii_alphabetic())
165}
166
167fn adjacent_char_bonus(i: usize, last_match: usize, current_bonus: i32) -> i32 {
168 if last_match == i {
169 return current_bonus * 2 + ADJACENT_MATCH_BONUS;
170 }
171 0
172}
173
174fn is_separator(s: char) -> bool {
175 SEPARATORS.contains(&s)
176}
177
178fn max(x: i32, y: i32) -> i32 {
179 if x > y {
180 x
181 } else {
182 y
183 }
184}
185
186pub fn score_cmp(a: &Match, b: &Match) -> Ordering {
189 b.score.cmp(&a.score)
190}