use std::cmp::Ordering;
pub const NO_NEIGHBOR: f32 = f32::MAX;
pub fn catmull_rom_interpolate(y1: f32, y2: f32, y3: f32, y4: f32, t: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
let tg2 = if y1 == NO_NEIGHBOR {
y3 - y2
} else {
(y3 - y1) * 0.5
};
let tg3 = if y4 == NO_NEIGHBOR {
y3 - y2
} else {
(y4 - y2) * 0.5
};
(2.0 * t3 - 3.0 * t2 + 1.0) * y2
+ (t3 - 2.0 * t2 + t) * tg2
+ (-2.0 * t3 + 3.0 * t2) * y3
+ (t3 - t2) * tg3
}
pub struct FuzzyStrCmp {
pattern_words: Vec<String>,
match_all_words: bool,
}
impl FuzzyStrCmp {
pub fn new(pattern: &str, all_words: bool) -> Self {
let mut pattern_words = Vec::new();
split(pattern, &mut pattern_words);
Self {
pattern_words,
match_all_words: all_words,
}
}
pub fn compare(&self, target: &str) -> i32 {
let mut match_words = Vec::new();
split(target, &mut match_words);
if match_words.is_empty() || self.pattern_words.is_empty() {
return 0;
}
let mut mi: usize = 0;
let mut score: i32 = 0;
for pattern_str in &self.pattern_words {
let old_mi = mi;
let mut found_match = false;
while mi < match_words.len() {
match pattern_str.as_str().cmp(match_words[mi].as_str()) {
Ordering::Equal => {
score += 1;
found_match = true;
break;
}
Ordering::Less => {
if self.match_all_words {
return 0;
}
break;
}
Ordering::Greater => mi += 1,
}
}
if self.match_all_words {
if !found_match {
return 0;
}
mi += 1;
} else if found_match {
mi += 1;
} else {
mi = old_mi;
}
}
(score * 200) / (self.pattern_words.len() + match_words.len()) as i32
}
}
pub fn fuzzy_str_cmp(pattern: &str, target: &str) -> i32 {
FuzzyStrCmp::new(pattern, false).compare(target)
}
fn split(str: &str, dest: &mut Vec<String>) {
let bytes = str.as_bytes();
let mut i = 0;
while i < bytes.len() {
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
if i >= bytes.len() {
break;
}
let word_start = i;
let first = bytes[i];
i += 1;
let mut strip_suffix = 0;
if first.is_ascii_digit() {
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
i += 1;
}
if i - word_start >= 2 && bytes[i - 2] == b'.' && bytes[i - 1] == b'0' {
strip_suffix = 2;
}
} else if is_ascii_punct(first) {
while i < bytes.len() && is_ascii_punct(bytes[i]) {
i += 1;
}
} else {
while i < bytes.len()
&& !bytes[i].is_ascii_whitespace()
&& !bytes[i].is_ascii_digit()
&& !is_ascii_punct(bytes[i])
{
i += 1;
}
}
if i - word_start == 1
&& (is_ascii_punct(first) || first.eq_ignore_ascii_case(&b'f'))
&& first != b'*'
&& first != b'+'
{
continue;
}
let raw = &str[word_start..(i - strip_suffix)];
let folded = raw.to_lowercase();
let pos = dest.binary_search(&folded).unwrap_or_else(|p| p);
dest.insert(pos, folded);
}
}
fn is_ascii_punct(b: u8) -> bool {
b.is_ascii_graphic() && !b.is_ascii_alphanumeric()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_basic() {
let mut words = Vec::new();
split("Nikkor 18mm f/4 DX", &mut words);
assert_eq!(words, vec!["18", "4", "dx", "mm", "nikkor"]);
}
#[test]
fn split_strips_dot_zero_on_digit_run() {
let mut words = Vec::new();
split("Nikkor 18mm f/4.0 DX", &mut words);
assert_eq!(words, vec!["18", "4", "dx", "mm", "nikkor"]);
}
#[test]
fn split_keeps_star_and_plus() {
let mut words = Vec::new();
split("EF 50 *", &mut words);
assert!(words.contains(&"*".to_string()));
let mut words = Vec::new();
split("X 50 +", &mut words);
assert!(words.contains(&"+".to_string()));
}
#[test]
fn empty_pattern_returns_zero() {
let cmp = FuzzyStrCmp::new("", true);
assert_eq!(cmp.compare("anything"), 0);
}
#[test]
fn empty_target_returns_zero() {
let cmp = FuzzyStrCmp::new("anything", true);
assert_eq!(cmp.compare(""), 0);
}
#[test]
fn perfect_match_scores_100() {
let cmp = FuzzyStrCmp::new("Nikkor 18mm f/4 DX", true);
assert_eq!(cmp.compare("Nikkor 18mm f/4 DX"), 100);
}
}