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}