matchr/lib.rs
1/// Scores how well `query` matches the `candi` string.
2///
3/// The score is based on whether `query` is a subsequence of `candi`, with additional weighting:
4/// - Characters matched earlier in `candi` get higher weight.
5/// - Consecutive matched characters earn a small bonus.
6/// - Exact matches yield the highest score (100).
7///
8/// # Arguments
9///
10/// * `query` - The search query string slice.
11/// * `candi` - The candidate string slice to be matched against.
12///
13/// # Returns
14///
15/// A usize score between 0 and 100, higher means better match.
16///
17/// # Examples
18///
19/// ```
20/// let score = matchr::score("fefe", "fefe");
21/// assert_eq!(score, 100);
22/// ```
23pub fn score(query: &str, candi: &str) -> usize {
24 if query.is_empty() {
25 return 0;
26 }
27
28 if !is_subsequence(query, candi) {
29 return 0;
30 }
31
32 let mut score = 0usize;
33 let mut candi_chars = candi.char_indices();
34 let mut last_pos = None;
35
36 for qc in query.chars() {
37 let mut found = false;
38 while let Some((pos, cc)) = candi_chars.next() {
39 if cc == qc {
40 let pos_score = 10usize.saturating_sub(pos);
41 score += pos_score;
42
43 if let Some(lp) = last_pos {
44 if pos == lp + 1 {
45 score += score / 10;
46 }
47 }
48
49 last_pos = Some(pos);
50 found = true;
51 break;
52 }
53 }
54 if !found {
55 return 0;
56 }
57 }
58
59 if query == candi {
60 return 100;
61 }
62 let max_possible = query.len() * 15;
63
64 ((score * 100) / max_possible).min(100)
65}
66
67fn is_subsequence(query: &str, candi: &str) -> bool {
68 let mut candi_chars = candi.chars();
69 for qc in query.chars() {
70 if candi_chars.find(|cc| *cc == qc).is_none() {
71 return false;
72 }
73 }
74 true
75}
76
77/// Matches multiple `items` against the `query` and returns
78/// a sorted vector of tuples containing the item and its match score.
79///
80/// # Arguments
81///
82/// * `query` - The search query string slice.
83/// * `items` - Slice of string slices to be matched.
84///
85/// # Returns
86///
87/// A vector of tuples `(item, score)`, sorted by descending score.
88///
89/// # Examples
90///
91/// ```
92/// let items = ["fefe", "feature", "banana"];
93/// let results = matchr::match_items("fefe", &items);
94/// assert_eq!(results[0].0, "fefe");
95/// ```
96pub fn match_items<'a>(query: &str, items: &[&'a str]) -> Vec<(&'a str, usize)> {
97 let mut scored: Vec<_> = items
98 .iter()
99 .map(|item| (*item, score(query, item)))
100 .collect();
101
102 scored.sort_by(|a, b| b.1.cmp(&a.1));
103 scored
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_basic_match() {
112 let query = "xb";
113 let candidates = [
114 "eeeeeeeeeeeeeeeeeeeeeeeee",
115 "cat",
116 "cp",
117 "mv",
118 "rm",
119 "touch",
120 "mkdir",
121 "rmdir",
122 "grep",
123 "find",
124 "xargs",
125 "cut",
126 "head",
127 "tail",
128 "less",
129 "more",
130 "man",
131 "chmod",
132 "chown",
133 "ping",
134 "curl",
135 "wget",
136 "ssh",
137 "scp",
138 "ps",
139 "kill",
140 "top",
141 "htop",
142 "nano",
143 "vim",
144 "xbps-install",
145 "xbps-remove",
146 "xbps-query",
147 "sudo",
148 "doas",
149 "su",
150 "env",
151 "export",
152 "uname",
153 "whoami",
154 "uptime",
155 "date",
156 "cal",
157 "clear",
158 "tput",
159 "printf",
160 "echo",
161 ];
162
163 let results = match_items(query, &candidates);
164
165 for (item, score) in &results {
166 println!("{} => score: {}", item, score);
167 }
168 }
169}