#![forbid(unsafe_code)]
#![warn(
anonymous_parameters,
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
nonstandard_style,
rust_2018_idioms,
single_use_lifetimes,
trivial_casts,
trivial_numeric_casts,
unreachable_pub,
unused_extern_crates,
unused_qualifications,
variant_size_differences
)]
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
fn test_fuzzy_1() {
assert!(simple_fuzzy_match("ftw", "ForrestTheWoods"));
}
#[test]
fn test_fuzzy_2() {
assert!(!simple_fuzzy_match("fwt", "ForrestTheWoods"));
}
#[test]
fn test_fuzzy_3() {
assert!(simple_fuzzy_match("gh", "GitHub"));
}
#[test]
fn test_fuzzy_4() {
assert_eq!(fuzzy_match("otw", "Power Of The Wild"), (true, 161));
}
#[test]
fn test_fuzzy_5() {
assert_eq!(fuzzy_match("otw", "Druid of the Claw"), (true, 131));
}
#[test]
fn test_fuzzy_6() {
assert_eq!(fuzzy_match("otw", "Frostwolf Grunt"), (true, 93));
}
}
const BONUS_ADJACENT: i32 = 15;
const BONUS_WORD: i32 = 30;
const BONUS_FIRST: i32 = 15;
const PENALTY_INCORRECT_CHAR: i32 = -1;
const PENALTY_LEADING: i32 = -5;
const MAX_PENALTY_LEADING: i32 = -15;
const MAX_RECURSION: u32 = 15;
#[must_use = "Pure function with no side-effects"]
pub fn simple_fuzzy_match(pattern: &str, matches: &str) -> bool {
let mut pattern = pattern.chars().map(|c| c.to_ascii_lowercase());
let matches = matches.chars().map(|c| c.to_ascii_lowercase());
let mut current = pattern.next();
for matched in matches {
if current == Some(matched) {
current = pattern.next();
}
}
current.is_none()
}
#[must_use = "Pure function with no side-effects"]
pub fn fuzzy_match(pattern: &str, matches: &str) -> (bool, i32) {
let max_matches: usize = 256;
fuzzy_match_recursive(
pattern,
matches,
None,
&mut Vec::with_capacity(max_matches),
max_matches,
MAX_RECURSION,
0,
)
}
fn fuzzy_match_recursive(
mut pattern: &str,
mut matches: &str,
src_match_list: Option<&[usize]>,
match_list: &mut Vec<usize>,
max_matches: usize,
recursions: u32,
mut index: usize,
) -> (bool, i32) {
let matches_orig = matches;
matches = &matches[index..];
if recursions == 0 {
return (false, 0);
}
if pattern.is_empty() || matches.is_empty() {
return (true, 0);
}
let mut out_score = 0;
let mut recursive_match = false;
let mut best_recursive_matches = Vec::new();
let mut best_recursive_score = 0;
let mut first_match: bool = true;
while let Some((current, matched)) =
pattern.chars().next().zip(matches.chars().next())
{
if current.to_ascii_lowercase() == matched.to_ascii_lowercase() {
if max_matches <= match_list.len() {
return (false, out_score);
}
if let Some(src_match_list) = src_match_list {
if first_match {
match_list.clear();
match_list.extend(src_match_list);
first_match = false;
}
}
let mut recursive_matches = Vec::new();
let (is_matching, score) = fuzzy_match_recursive(
pattern,
matches_orig,
Some(match_list),
&mut recursive_matches,
max_matches,
recursions - 1,
index + 1,
);
if is_matching {
if !recursive_match || score > best_recursive_score {
best_recursive_matches = recursive_matches.clone();
best_recursive_score = score;
}
recursive_match = true;
}
match_list.push(index);
pattern = &pattern[1..];
}
matches = &matches[1..];
index += 1;
}
let did_match = pattern.is_empty();
if did_match {
out_score = 100;
let penalty =
((match_list[0] as i32) * PENALTY_LEADING).max(MAX_PENALTY_LEADING);
out_score += penalty;
println!("Applied penalty of {penalty} - now {out_score}");
println!("{}, {}", matches_orig.len(), match_list.len());
let penalty = PENALTY_INCORRECT_CHAR
* (matches_orig.len() as i32 - match_list.len() as i32);
out_score += penalty;
println!("Applied penalty of {penalty} - now {out_score}");
println!("Match list: {match_list:?}");
for i in 0..match_list.len() {
let curr = match_list[i];
if i > 0 && curr == match_list[i - 1] + 1 {
out_score += BONUS_ADJACENT;
println!("Applied sequential bonus of {BONUS_ADJACENT} - now {out_score}");
}
if curr as u32 > 0 {
let neighbor = matches_orig.chars().nth(curr - 1).unwrap();
let current = matches_orig.chars().nth(curr).unwrap();
if neighbor != neighbor.to_ascii_uppercase()
&& current != current.to_ascii_lowercase()
{
out_score += BONUS_WORD;
println!(
"Applied word bonus of {BONUS_WORD} - now {out_score}"
);
}
if matches!(neighbor, '-' | '_' | ' ') {
out_score += BONUS_WORD;
println!(
"Applied word bonus of {BONUS_WORD} - now {out_score}"
);
}
} else {
out_score += BONUS_FIRST;
println!(
"Applied first bonus of {BONUS_FIRST} - now {out_score}"
);
}
}
return if recursive_match
&& (!did_match || best_recursive_score > out_score)
{
*match_list = best_recursive_matches;
(true, best_recursive_score)
} else if did_match {
(true, out_score)
} else {
(false, out_score)
};
}
(false, out_score)
}