use rapidfuzz::fuzz;
use super::{FuzzyMode, SearchFieldMode};
pub(crate) fn matches_field(norm_value: &str, norm_token: &str, mode: SearchFieldMode) -> bool {
match mode {
SearchFieldMode::Exact => norm_value == norm_token,
SearchFieldMode::Prefix => norm_value.starts_with(norm_token),
SearchFieldMode::Contains => norm_value.contains(norm_token),
}
}
pub(crate) fn fuzzy_score(
norm_value: &str,
norm_token: &str,
threshold: i64,
fuzzy_mode: FuzzyMode,
) -> i64 {
let score = match fuzzy_mode {
FuzzyMode::TokenSort => token_sort_ratio(norm_value, norm_token),
_ => partial_ratio(norm_value, norm_token),
};
if score >= threshold {
score
} else {
0
}
}
fn ratio(a: &str, b: &str) -> i64 {
if a.is_empty() && b.is_empty() {
return 100;
}
(fuzz::ratio(a.chars(), b.chars()) * 100.0) as i64
}
fn partial_ratio(a: &str, b: &str) -> i64 {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let (short, long) = if a_chars.len() <= b_chars.len() {
(&a_chars, &b_chars)
} else {
(&b_chars, &a_chars)
};
if short.is_empty() {
return i64::from(long.is_empty()) * 100;
}
let short_str: String = short.iter().collect();
let mut best = 0;
for window in long.windows(short.len()) {
let candidate: String = window.iter().collect();
best = best.max(ratio(&short_str, &candidate));
if best == 100 {
break;
}
}
best
}
fn token_sort_ratio(a: &str, b: &str) -> i64 {
ratio(&sorted_tokens(a), &sorted_tokens(b))
}
fn sorted_tokens(s: &str) -> String {
let mut tokens: Vec<&str> = s.split_whitespace().collect();
tokens.sort_unstable();
tokens.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn modes() {
assert!(matches_field("hello", "hello", SearchFieldMode::Exact));
assert!(!matches_field(
"hello world",
"hello",
SearchFieldMode::Exact
));
assert!(matches_field("hello", "hel", SearchFieldMode::Prefix));
assert!(matches_field("hello", "ell", SearchFieldMode::Contains));
assert!(!matches_field("hello", "xyz", SearchFieldMode::Contains));
}
#[test]
fn fuzzy_threshold_gate() {
assert_eq!(fuzzy_score("hello", "ell", 75, FuzzyMode::Fuzzy), 100);
assert_eq!(fuzzy_score("hello", "xyz", 75, FuzzyMode::Fuzzy), 0);
}
#[test]
fn partial_ratio_finds_substring() {
assert_eq!(partial_ratio("alice johnson", "alice"), 100);
assert_eq!(partial_ratio("alice", "alice johnson"), 100);
}
#[test]
fn token_sort_is_word_order_agnostic() {
assert_eq!(token_sort_ratio("johnson alice", "alice johnson"), 100);
assert_eq!(
fuzzy_score("johnson alice", "alice johnson", 90, FuzzyMode::TokenSort),
100
);
}
#[test]
fn dissimilar_scores_below_threshold() {
assert_eq!(fuzzy_score("zebra", "alice", 75, FuzzyMode::Fuzzy), 0);
}
}