#[derive(Clone, Debug, PartialEq)]
pub struct FuzzyMatch {
pub score: i32,
pub indices: Vec<usize>,
}
impl FuzzyMatch {
pub fn new(score: i32, indices: Vec<usize>) -> Self {
Self { score, indices }
}
}
pub fn fuzzy_match(pattern: &str, target: &str) -> Option<FuzzyMatch> {
if pattern.is_empty() {
return Some(FuzzyMatch::new(0, vec![]));
}
let pattern_lower: Vec<char> = pattern.to_lowercase().chars().collect();
let pattern_chars: Vec<char> = pattern.chars().collect();
let target_chars: Vec<char> = target.chars().collect();
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
let mut indices = Vec::with_capacity(pattern_lower.len());
let mut score = 0i32;
let mut pattern_idx = 0;
let mut prev_match_idx: Option<usize> = None;
for (i, &ch) in target_lower.iter().enumerate() {
if pattern_idx < pattern_lower.len() && ch == pattern_lower[pattern_idx] {
indices.push(i);
score += 1;
if target_chars[i] == pattern_chars.get(pattern_idx).copied().unwrap_or(' ') {
score += 1;
}
if let Some(prev) = prev_match_idx {
if i == prev + 1 {
score += 3;
}
}
if i == 0 {
score += 2; } else {
let prev_char = target_chars[i - 1];
if !prev_char.is_alphanumeric()
|| (prev_char.is_lowercase() && target_chars[i].is_uppercase())
{
score += 2; }
}
prev_match_idx = Some(i);
pattern_idx += 1;
}
}
if pattern_idx == pattern_lower.len() {
Some(FuzzyMatch::new(score, indices))
} else {
None
}
}
pub fn fuzzy_match_threshold(pattern: &str, target: &str, min_score: i32) -> Option<FuzzyMatch> {
fuzzy_match(pattern, target).filter(|m| m.score >= min_score)
}
pub fn fuzzy_score(pattern: &str, target: &str) -> i32 {
fuzzy_match(pattern, target).map(|m| m.score).unwrap_or(0)
}
pub fn fuzzy_matches(pattern: &str, target: &str) -> bool {
fuzzy_match(pattern, target).is_some()
}
pub fn fuzzy_filter<'a, T: AsRef<str>>(pattern: &str, items: &'a [T]) -> Vec<(&'a T, FuzzyMatch)> {
let mut matches: Vec<_> = items
.iter()
.filter_map(|item| fuzzy_match(pattern, item.as_ref()).map(|m| (item, m)))
.collect();
matches.sort_by(|a, b| b.1.score.cmp(&a.1.score));
matches
}
pub fn fuzzy_filter_simple<'a, T: AsRef<str>>(pattern: &str, items: &'a [T]) -> Vec<&'a T> {
fuzzy_filter(pattern, items)
.into_iter()
.map(|(item, _)| item)
.collect()
}
#[derive(Clone, Debug)]
pub struct FuzzyMatcher {
pattern: Vec<char>,
original_chars: Vec<char>,
min_score: i32,
}
impl FuzzyMatcher {
pub fn new(pattern: &str) -> Self {
Self {
pattern: pattern.to_lowercase().chars().collect(),
original_chars: pattern.chars().collect(),
min_score: 0,
}
}
pub fn min_score(mut self, score: i32) -> Self {
self.min_score = score;
self
}
pub fn is_empty(&self) -> bool {
self.pattern.is_empty()
}
pub fn match_str(&self, target: &str) -> Option<FuzzyMatch> {
if self.pattern.is_empty() {
return Some(FuzzyMatch::new(0, vec![]));
}
let target_chars: Vec<char> = target.chars().collect();
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
let mut indices = Vec::with_capacity(self.pattern.len());
let mut score = 0i32;
let mut pattern_idx = 0;
let mut prev_match_idx: Option<usize> = None;
for (i, &ch) in target_lower.iter().enumerate() {
if pattern_idx < self.pattern.len() && ch == self.pattern[pattern_idx] {
indices.push(i);
score += 1;
if let Some(orig_ch) = self.original_chars.get(pattern_idx).copied() {
if target_chars[i] == orig_ch {
score += 1;
}
}
if let Some(prev) = prev_match_idx {
if i == prev + 1 {
score += 3;
}
}
if i == 0 {
score += 2;
} else {
let prev_char = target_chars[i - 1];
if !prev_char.is_alphanumeric()
|| (prev_char.is_lowercase() && target_chars[i].is_uppercase())
{
score += 2;
}
}
prev_match_idx = Some(i);
pattern_idx += 1;
}
}
if pattern_idx == self.pattern.len() && score >= self.min_score {
Some(FuzzyMatch::new(score, indices))
} else {
None
}
}
pub fn filter<'a, T: AsRef<str>>(&self, items: &'a [T]) -> Vec<(&'a T, FuzzyMatch)> {
let mut matches: Vec<_> = items
.iter()
.filter_map(|item| self.match_str(item.as_ref()).map(|m| (item, m)))
.collect();
matches.sort_by(|a, b| b.1.score.cmp(&a.1.score));
matches
}
}