use std::cell::RefCell;
use frizbee::{Config, Matcher};
use crate::CaseMatching;
use crate::fuzzy_matcher::{FuzzyMatcher, MatchIndices, ScoreType};
thread_local! {
static LOCAL_MATCHER: RefCell<Option<Matcher>> = const { RefCell::new(None) };
}
pub struct FrizbeeMatcher {
config: Config,
}
impl Default for FrizbeeMatcher {
fn default() -> Self {
Self {
config: Config {
casing: CaseMatching::Respect.into(),
..Config::default()
},
}
}
}
impl FrizbeeMatcher {
#[must_use]
pub fn max_typos(mut self, typos: Option<usize>) -> Self {
self.config.max_typos = Some(typos.map_or(0, |x| u16::try_from(x).unwrap_or(u16::MAX)));
self
}
#[must_use]
pub fn case(mut self, case: CaseMatching) -> Self {
self.config.casing = case.into();
self
}
fn with_matcher<R>(&self, pattern: &str, f: impl FnOnce(&mut Matcher) -> R) -> R {
LOCAL_MATCHER.with(|cell| {
let mut slot = cell.borrow_mut();
if slot.as_ref().is_none() {
*slot = Some(Matcher::new("", &self.config));
}
let matcher = slot.as_mut().unwrap();
matcher.set_config(self.config.clone());
matcher.set_needle(pattern);
f(matcher)
})
}
}
impl FuzzyMatcher for FrizbeeMatcher {
fn fuzzy_indices(&self, choice: &str, pattern: &str) -> Option<(ScoreType, MatchIndices)> {
self.with_matcher(pattern, |m| {
m.match_one_indices(choice, 0).map(|mut hit| {
hit.indices.reverse();
(hit.score.into(), hit.indices)
})
})
}
fn fuzzy_match(&self, choice: &str, pattern: &str) -> Option<i64> {
self.with_matcher(pattern, |m| m.match_one(choice, 0).map(|hit| hit.score.into()))
}
}
impl From<CaseMatching> for frizbee::CaseMatching {
fn from(case: CaseMatching) -> Self {
match case {
CaseMatching::Respect => frizbee::CaseMatching::Respect,
CaseMatching::Ignore => frizbee::CaseMatching::Ignore,
CaseMatching::Smart => frizbee::CaseMatching::Smart,
}
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
use super::*;
use crate::fuzzy_matcher::FuzzyMatcher;
#[test]
fn matches_subsequence() {
let m = FrizbeeMatcher::default();
assert!(m.fuzzy_match("foobar", "foo").is_some());
assert!(m.fuzzy_indices("foobar", "foo").is_some());
}
#[test]
fn respect_case_variant() {
let m = FrizbeeMatcher::default().case(CaseMatching::Respect);
assert!(m.fuzzy_indices("FooBar", "Foo").is_some());
}
#[test]
fn smart_case_variant() {
let m = FrizbeeMatcher::default().case(CaseMatching::Smart);
assert!(m.fuzzy_indices("FooBar", "Foo").is_some());
assert!(m.fuzzy_indices("foobar", "foo").is_some());
}
#[test]
fn ignore_case_variant() {
let m = FrizbeeMatcher::default().case(CaseMatching::Ignore);
assert!(m.fuzzy_match("FOOBAR", "foo").is_some());
}
#[test]
fn max_typos_tolerates_mismatch() {
let m = FrizbeeMatcher::default().max_typos(Some(1));
assert!(m.fuzzy_match("foobar", "fxo").is_some());
}
#[test]
fn fuzzy_indices_ignore_case() {
let m = FrizbeeMatcher::default().case(CaseMatching::Ignore);
assert!(m.fuzzy_indices("FOOBAR", "foo").is_some());
}
#[test]
fn fuzzy_indices_no_match_returns_none() {
let m = FrizbeeMatcher::default();
assert!(m.fuzzy_indices("foobar", "zzz").is_none());
}
#[test]
fn fuzzy_match_respect_and_smart_case() {
let respect = FrizbeeMatcher::default().case(CaseMatching::Respect);
assert!(respect.fuzzy_match("FooBar", "Foo").is_some());
let smart = FrizbeeMatcher::default().case(CaseMatching::Smart);
assert!(smart.fuzzy_match("FooBar", "Foo").is_some());
assert!(smart.fuzzy_match("foobar", "foo").is_some());
}
}