#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FuzzyMatch {
pub candidate: String,
pub score: i32,
}
pub fn fuzzy_jump<'a>(
candidates: impl IntoIterator<Item = &'a str>,
query: &str,
) -> Vec<FuzzyMatch> {
let query = query.trim().to_ascii_lowercase();
if query.is_empty() {
return candidates
.into_iter()
.map(|c| FuzzyMatch {
candidate: c.to_string(),
score: 0,
})
.collect();
}
let mut matches: Vec<FuzzyMatch> = candidates
.into_iter()
.filter_map(|candidate| {
fuzzy_score(candidate, &query).map(|score| FuzzyMatch {
candidate: candidate.to_string(),
score,
})
})
.collect();
matches.sort_by(|a, b| {
b.score
.cmp(&a.score)
.then_with(|| a.candidate.cmp(&b.candidate))
});
matches
}
fn fuzzy_score(candidate: &str, query: &str) -> Option<i32> {
let lower = candidate.to_ascii_lowercase();
let candidate_chars: Vec<char> = lower.chars().collect();
let query_chars: Vec<char> = query.chars().collect();
if query_chars.is_empty() {
return Some(0);
}
let mut score: i32 = 0;
let mut qi = 0usize; let mut prev_match: Option<usize> = None;
for (ci, c) in candidate_chars.iter().enumerate() {
if qi < query_chars.len() && *c == query_chars[qi] {
score += 1;
if let Some(prev) = prev_match {
if ci == prev + 1 {
score += 3;
}
} else if ci == 0 {
score += 4;
} else {
let prev_char = candidate_chars[ci - 1];
if prev_char == ' '
|| prev_char == '-'
|| prev_char == '_'
|| prev_char == '.'
|| prev_char.is_uppercase()
{
score += 2;
}
}
prev_match = Some(ci);
qi += 1;
}
}
if qi == query_chars.len() {
Some(score)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subsequence_matches_rank_contiguous_higher() {
let names = ["nginx-ingress-controller", "nagios", "nginxxxxxxxxx"];
let ranked = fuzzy_jump(names, "nginx");
assert_eq!(ranked[0].candidate, "nginx-ingress-controller");
}
#[test]
fn empty_query_matches_all() {
let names = ["a", "b"];
let ranked = fuzzy_jump(names, "");
assert_eq!(ranked.len(), 2);
}
#[test]
fn no_match_returns_empty() {
let names = ["pod-a", "pod-b"];
let ranked = fuzzy_jump(names, "zzz");
assert!(ranked.is_empty());
}
#[test]
fn case_insensitive() {
let names = ["Deployment"];
let ranked = fuzzy_jump(names, "DEP");
assert_eq!(ranked.len(), 1);
}
#[test]
fn camel_case_boundary_boost() {
let names = ["imagePullBackOff", "ImagePullError"];
let ranked = fuzzy_jump(names, "ipbo");
assert_eq!(ranked[0].candidate, "imagePullBackOff");
}
}