use nucleo_matcher::{
Config, Matcher,
pattern::{CaseMatching, Normalization, Pattern},
};
#[must_use]
pub fn fuzzy_rank(query: &str, mut candidates: Vec<String>, limit: Option<usize>) -> Vec<String> {
if query.is_empty() {
candidates.sort();
if let Some(n) = limit {
candidates.truncate(n);
}
return candidates;
}
let mut matcher = Matcher::new(Config::DEFAULT);
let ranked = Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart)
.match_list(candidates, &mut matcher);
let mut out: Vec<String> = ranked.into_iter().map(|(name, _score)| name).collect();
if let Some(n) = limit {
out.truncate(n);
}
out
}
#[cfg(test)]
mod test {
use super::fuzzy_rank;
fn keys() -> Vec<String> {
vec![
"aws-prod-key".to_string(),
"aws-staging".to_string(),
"github-token".to_string(),
"gitlab-hook".to_string(),
]
}
#[test]
fn empty_query_lists_all_sorted() {
let ranked = fuzzy_rank("", keys(), None);
assert_eq!(
ranked,
vec![
"aws-prod-key".to_string(),
"aws-staging".to_string(),
"github-token".to_string(),
"gitlab-hook".to_string(),
]
);
}
#[test]
fn contiguous_match_ranks_above_scattered_subsequence() {
let candidates = vec!["a-b-c-key".to_string(), "abc-key".to_string()];
let ranked = fuzzy_rank("abc", candidates, None);
assert_eq!(ranked.first().map(String::as_str), Some("abc-key"));
assert!(ranked.iter().any(|k| k == "a-b-c-key"));
}
#[test]
fn case_insensitive_match() {
let ranked = fuzzy_rank("AWS", keys(), None);
assert!(ranked.iter().any(|k| k == "aws-prod-key"));
assert!(ranked.iter().any(|k| k == "aws-staging"));
}
#[test]
fn limit_truncates_results() {
let ranked = fuzzy_rank("aws", keys(), Some(1));
assert_eq!(ranked.len(), 1);
}
#[test]
fn empty_query_with_limit_truncates_sorted() {
let ranked = fuzzy_rank("", keys(), Some(2));
assert_eq!(
ranked,
vec!["aws-prod-key".to_string(), "aws-staging".to_string()]
);
}
#[test]
fn no_match_returns_empty() {
let ranked = fuzzy_rank("zzzznope", keys(), None);
assert!(ranked.is_empty());
}
}