pub fn score(query: &str, candi: &str) -> usize {
if query.is_empty() {
return 0;
}
if !is_subsequence(query, candi) {
return 0;
}
let mut score = 0usize;
let mut candi_chars = candi.char_indices();
let mut last_pos = None;
for qc in query.chars() {
let mut found = false;
while let Some((pos, cc)) = candi_chars.next() {
if cc == qc {
let pos_score = 10usize.saturating_sub(pos);
score += pos_score;
if let Some(lp) = last_pos {
if pos == lp + 1 {
score += score / 10;
}
}
last_pos = Some(pos);
found = true;
break;
}
}
if !found {
return 0;
}
}
if query == candi {
return 100;
}
let max_possible = query.len() * 15;
((score * 100) / max_possible).min(100)
}
fn is_subsequence(query: &str, candi: &str) -> bool {
let mut candi_chars = candi.chars();
for qc in query.chars() {
if candi_chars.find(|cc| *cc == qc).is_none() {
return false;
}
}
true
}
pub fn match_items<'a>(query: &str, items: &[&'a str]) -> Vec<(&'a str, usize)> {
let mut scored: Vec<_> = items
.iter()
.map(|item| (*item, score(query, item)))
.collect();
scored.sort_by(|a, b| b.1.cmp(&a.1));
scored
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_match() {
let query = "xb";
let candidates = [
"eeeeeeeeeeeeeeeeeeeeeeeee",
"cat",
"cp",
"mv",
"rm",
"touch",
"mkdir",
"rmdir",
"grep",
"find",
"xargs",
"cut",
"head",
"tail",
"less",
"more",
"man",
"chmod",
"chown",
"ping",
"curl",
"wget",
"ssh",
"scp",
"ps",
"kill",
"top",
"htop",
"nano",
"vim",
"xbps-install",
"xbps-remove",
"xbps-query",
"sudo",
"doas",
"su",
"env",
"export",
"uname",
"whoami",
"uptime",
"date",
"cal",
"clear",
"tput",
"printf",
"echo",
];
let results = match_items(query, &candidates);
for (item, score) in &results {
println!("{} => score: {}", item, score);
}
}
}