use nucleo_matcher::{
Config, Matcher, Utf32Str,
pattern::{AtomKind, CaseMatching, Normalization, Pattern},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FuzzyMatch {
pub score: u32,
pub indices: Vec<usize>,
}
pub fn fuzzy_match(pattern: &str, target: &str) -> Option<FuzzyMatch> {
if pattern.is_empty() {
return Some(FuzzyMatch {
score: 0,
indices: vec![],
});
}
let mut matcher = Matcher::new(Config::DEFAULT);
let pat = Pattern::new(
pattern,
CaseMatching::Ignore,
Normalization::Smart,
AtomKind::Fuzzy,
);
let mut buf = Vec::new();
let target_utf32 = Utf32Str::new(target, &mut buf);
let mut indices = Vec::new();
let score = pat.indices(target_utf32, &mut matcher, &mut indices)?;
let indices: Vec<usize> = indices.into_iter().map(|i| i as usize).collect();
Some(FuzzyMatch { score, indices })
}
#[allow(dead_code)]
pub fn fuzzy_match_sorted<'a, T, F>(
pattern: &str,
candidates: impl Iterator<Item = T>,
get_text: F,
) -> Vec<(T, FuzzyMatch)>
where
F: Fn(&T) -> &'a str,
{
let mut results: Vec<_> = candidates
.filter_map(|item| {
let text = get_text(&item);
fuzzy_match(pattern, text).map(|m| (item, m))
})
.collect();
results.sort_by_key(|entry| std::cmp::Reverse(entry.1.score));
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fuzzy_match_basic() {
let m = fuzzy_match("rep", "reprex").unwrap();
assert_eq!(m.indices, vec![0, 1, 2]);
let m = fuzzy_match("sw", "switch").unwrap();
assert_eq!(m.indices, vec![0, 1]);
}
#[test]
fn test_fuzzy_match_non_consecutive() {
let m = fuzzy_match("rst", "restart").unwrap();
assert!(m.indices.len() == 3);
assert!(fuzzy_match("ht", "shell").is_none());
}
#[test]
fn test_fuzzy_match_case_insensitive() {
assert!(fuzzy_match("REP", "reprex").is_some());
assert!(fuzzy_match("rEp", "reprex").is_some());
}
#[test]
fn test_fuzzy_match_no_match() {
assert!(fuzzy_match("xyz", "restart").is_none());
assert!(fuzzy_match("abc", "shell").is_none());
}
#[test]
fn test_fuzzy_match_empty_pattern() {
let m = fuzzy_match("", "restart").unwrap();
assert_eq!(m.indices, Vec::<usize>::new());
assert_eq!(m.score, 0);
}
#[test]
fn test_fuzzy_match_sorted() {
let candidates = vec!["restart", "reprex", "shell", "system"];
let results = fuzzy_match_sorted("r", candidates.into_iter(), |s| s);
assert!(!results.is_empty());
for i in 1..results.len() {
assert!(results[i - 1].1.score >= results[i].1.score);
}
}
#[test]
fn test_fuzzy_match_scoring() {
let prefix = fuzzy_match("re", "reprex").unwrap();
let scattered = fuzzy_match("re", "restart").unwrap();
assert!(prefix.score > 0);
assert!(scattered.score > 0);
}
}