From 4c3d5f3138e05b17ef85cb3b85e309ca604eee3c Mon Sep 17 00:00:00 2001 From: ljnsn Date: Sun, 8 Dec 2024 19:10:36 +0100 Subject: [PATCH 01/29] add score alignment struct --- src/distance.rs | 1 + src/distance/common.rs | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/distance/common.rs diff --git a/src/distance.rs b/src/distance.rs index c784f0d..a88ab61 100644 --- a/src/distance.rs +++ b/src/distance.rs @@ -1,3 +1,4 @@ +pub mod common; pub mod damerau_levenshtein; pub mod hamming; pub mod indel; diff --git a/src/distance/common.rs b/src/distance/common.rs new file mode 100644 index 0000000..8353578 --- /dev/null +++ b/src/distance/common.rs @@ -0,0 +1,15 @@ +/** +Tuple like object describing the position of the compared strings in +src and dest. + +It indicates that the score has been calculated between +src[src_start:src_end] and dest[dest_start:dest_end] +*/ +#[derive(PartialEq, Debug)] +pub struct ScoreAlignment { + pub score: f64, + pub src_start: usize, + pub src_end: usize, + pub dest_start: usize, + pub dest_end: usize, +} From 83c1aabe11f377f6422d104e7916e9ef21d652e5 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Mon, 9 Dec 2024 00:34:03 +0100 Subject: [PATCH 02/29] partial_ratio v1 --- src/fuzz.rs | 324 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- 2 files changed, 325 insertions(+), 1 deletion(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index ed738f3..08afe01 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -1,7 +1,9 @@ use crate::common::{NoScoreCutoff, SimilarityCutoff, WithScoreCutoff}; use crate::details::distance::MetricUsize; +use crate::distance::common::ScoreAlignment; use crate::distance::indel; use crate::HashableChar; +use std::collections::HashSet; #[must_use] #[derive(Clone, Copy, Debug)] @@ -149,6 +151,286 @@ where } } +/// Searches for the optimal alignment of the shorter string in the +/// longer string and returns the fuzz.ratio for this alignment. +/// +/// # Example +/// ``` +/// use rapidfuzz::fuzz; +/// /// score is 100.0 +/// let score = fuzz::partial_ratio("this is a test".chars(), "this is a test!".chars()); +/// ``` +/// +pub fn partial_ratio(s1: Iter1, s2: Iter2) -> f64 +where + Iter1: IntoIterator, + Iter1::IntoIter: DoubleEndedIterator + Clone, + Iter2: IntoIterator, + Iter2::IntoIter: DoubleEndedIterator + Clone, + Iter1::Item: PartialEq + HashableChar + Copy, + Iter2::Item: PartialEq + HashableChar + Copy, +{ + ratio_with_args(s1, s2, &Args::default()) +} + +pub fn partial_ratio_with_args( + s1: Iter1, + s2: Iter2, + args: &Args, +) -> CutoffType::Output +where + Iter1: IntoIterator, + Iter1::IntoIter: DoubleEndedIterator + Clone, + Iter2: IntoIterator, + Iter2::IntoIter: DoubleEndedIterator + Clone, + Iter1::Item: PartialEq + HashableChar + Copy, + Iter2::Item: PartialEq + HashableChar + Copy, + CutoffType: SimilarityCutoff, +{ + let s1_iter = s1.into_iter(); + let s2_iter = s2.into_iter(); + + let alignment = partial_ratio_alignment( + s1_iter.clone(), + s1_iter.count(), + s2_iter.clone(), + s2_iter.count(), + args.score_cutoff.cutoff(), + args.score_hint, + ); + + match alignment { + Some(alignment) => alignment.score, + None => 0.0, + } +} + +pub fn partial_ratio_alignment( + s1: Iter1, + len1: usize, + s2: Iter2, + len2: usize, + score_cutoff: Option, + score_hint: Option, +) -> Option +where + Iter1: IntoIterator, + Iter1::IntoIter: DoubleEndedIterator + Clone, + Iter2: IntoIterator, + Iter2::IntoIter: DoubleEndedIterator + Clone, + Iter1::Item: PartialEq + HashableChar + Copy, + Iter2::Item: PartialEq + HashableChar + Copy, +{ + let s1_iter = s1.into_iter(); + let s2_iter = s2.into_iter(); + + let mut score_cutoff = score_cutoff.unwrap_or(0.0); + + let mut res = if len1 <= len2 { + partial_ratio_short_needle( + s1_iter.clone(), + len1, + s2_iter.clone(), + len2, + score_cutoff / 100.0, + score_hint, + ) + } else { + partial_ratio_short_needle( + s2_iter.clone(), + len2, + s1_iter.clone(), + len1, + score_cutoff / 100.0, + score_hint, + ) + }; + if (res.score != 100.0) && (len1 == len2) { + score_cutoff = f64::max(score_cutoff, res.score); + let res2 = if len1 <= len2 { + partial_ratio_short_needle( + s2_iter.clone(), + len2, + s1_iter.clone(), + len1, + score_cutoff / 100.0, + None, + ) + } else { + partial_ratio_short_needle( + s1_iter.clone(), + len1, + s2_iter.clone(), + len2, + score_cutoff / 100.0, + None, + ) + }; + if res2.score > res.score { + res = ScoreAlignment { + score: res2.score, + src_start: res2.dest_start, + src_end: res2.dest_end, + dest_start: res2.src_start, + dest_end: res2.src_end, + }; + } + } + + if res.score < score_cutoff { + return None; + } + + // do we need this? why not just `Some(res)`? + if len1 <= len2 { + return Some(res); + } + + Some(ScoreAlignment { + score: res.score, + src_start: res.dest_start, + src_end: res.dest_end, + dest_start: res.src_start, + dest_end: res.src_end, + }) +} + +/** +implementation of partial_ratio for needles <= 64. assumes s1 is already the +shorter string +*/ +fn partial_ratio_short_needle( + s1: Iter1, + len1: usize, + s2: Iter2, + len2: usize, + mut score_cutoff: f64, + score_hint: Option, +) -> ScoreAlignment +where + Iter1: IntoIterator, + Iter1::IntoIter: DoubleEndedIterator + Clone, + Iter2: IntoIterator, + Iter2::IntoIter: DoubleEndedIterator + Clone, + Iter1::Item: PartialEq + HashableChar + Copy, + Iter2::Item: PartialEq + HashableChar + Copy, +{ + if len1 == 0 { + return ScoreAlignment { + score: 0.0, + src_start: 0, + src_end: 0, + dest_start: 0, + dest_end: 0, + }; + } + + let s1_iter = s1.into_iter(); + let s2_iter = s2.into_iter(); + + let s1_char_set = s1_iter + .clone() + .map(|c| c.hash_char()) + .collect::>(); + + let mut res = ScoreAlignment { + score: 0.0, + src_start: 0, + src_end: len1, + dest_start: 0, + dest_end: len1, + }; + + let indel_comp = indel::IndividualComparator {}; + + for i in 1..len1 { + let substr_last = s2_iter.clone().nth(i - 1).unwrap(); + if !s1_char_set.contains(&substr_last.hash_char()) { + continue; + } + + let ls_ratio = indel_comp._normalized_similarity( + s1_iter.clone(), + len1, + s2_iter.clone().take(i).collect::>().into_iter(), + i, + Some(score_cutoff), + score_hint, + ); + if ls_ratio > res.score { + score_cutoff = ls_ratio; + res.score = ls_ratio; + res.dest_start = 0; + res.dest_end = i; + if res.score == 1.0 { + res.score = 100.0; + return res; + } + } + } + + let window_end = len2 - len1; + for i in 0..window_end { + let substr_last = s2_iter.clone().nth(i + len1 - 1).unwrap(); + if !s1_char_set.contains(&substr_last.hash_char()) { + continue; + } + + let ls_ratio = indel_comp._normalized_similarity( + s1_iter.clone(), + len1, + s2_iter + .clone() + .skip(i) + .take(len1) + .collect::>() + .into_iter(), + len1, + Some(score_cutoff), + score_hint, + ); + if ls_ratio > res.score { + score_cutoff = ls_ratio; + res.score = ls_ratio; + res.dest_start = i; + res.dest_end = i + len1; + if res.score == 1.0 { + res.score = 100.0; + return res; + } + } + } + + for i in window_end..len2 { + let substr_first = s2_iter.clone().nth(i).unwrap(); + if !s1_char_set.contains(&substr_first.hash_char()) { + continue; + } + + let ls_ratio = indel_comp._normalized_similarity( + s1_iter.clone(), + len1, + s2_iter.clone().skip(i).collect::>().into_iter(), + len2 - i, + Some(score_cutoff), + score_hint, + ); + if ls_ratio > res.score { + score_cutoff = ls_ratio; + res.score = ls_ratio; + res.dest_start = i; + res.dest_end = len2; + if res.score == 1.0 { + res.score = 100.0; + return res; + } + } + } + + res.score *= 100.0; + res +} + #[cfg(test)] mod tests { use super::*; @@ -299,4 +581,46 @@ mod tests { ); } } + + #[test] + fn test_partial_ratio_short_needle_identical() { + let s1 = "abcd"; + let s2 = "abcd"; + + let result = partial_ratio_short_needle( + s1.chars(), + s1.chars().count(), + s2.chars(), + s2.chars().count(), + 0.0, + None, + ); + + assert_eq!(result.score, 100.0); + assert_eq!(result.src_start, 0); + assert_eq!(result.src_end, 4); + assert_eq!(result.dest_start, 0); + assert_eq!(result.dest_end, 4); + } + + #[test] + fn test_partial_ratio_short_needle_substring() { + let s1 = "bcd"; + let s2 = "abcde"; + + let result = partial_ratio_short_needle( + s1.chars(), + s1.chars().count(), + s2.chars(), + s2.chars().count(), + 0.0, + None, + ); + + assert_eq!(result.score, 100.0); + assert_eq!(result.src_start, 0); + assert_eq!(result.src_end, 3); + assert_eq!(result.dest_start, 1); + assert_eq!(result.dest_end, 4); + } } diff --git a/src/lib.rs b/src/lib.rs index f43933c..d885f76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,7 @@ pub mod distance; pub mod fuzz; /// Hash value in the range `i64::MIN` - `u64::MAX` -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Hash { UNSIGNED(u64), SIGNED(i64), From f5c87d82d81d64852f2f38e8cf3504e65ce2cb5a Mon Sep 17 00:00:00 2001 From: ljnsn Date: Mon, 9 Dec 2024 00:54:15 +0100 Subject: [PATCH 03/29] use batch comparator for partial ratio impl --- src/fuzz.rs | 82 +++++++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 08afe01..aef3f60 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -195,8 +195,7 @@ where s1_iter.count(), s2_iter.clone(), s2_iter.count(), - args.score_cutoff.cutoff(), - args.score_hint, + args, ); match alignment { @@ -205,13 +204,12 @@ where } } -pub fn partial_ratio_alignment( +pub fn partial_ratio_alignment( s1: Iter1, len1: usize, s2: Iter2, len2: usize, - score_cutoff: Option, - score_hint: Option, + args: &Args, ) -> Option where Iter1: IntoIterator, @@ -220,11 +218,12 @@ where Iter2::IntoIter: DoubleEndedIterator + Clone, Iter1::Item: PartialEq + HashableChar + Copy, Iter2::Item: PartialEq + HashableChar + Copy, + CutoffType: SimilarityCutoff, { let s1_iter = s1.into_iter(); let s2_iter = s2.into_iter(); - let mut score_cutoff = score_cutoff.unwrap_or(0.0); + let mut score_cutoff = args.score_cutoff.cutoff().unwrap_or(0.0); let mut res = if len1 <= len2 { partial_ratio_short_needle( @@ -233,7 +232,7 @@ where s2_iter.clone(), len2, score_cutoff / 100.0, - score_hint, + args.score_hint, ) } else { partial_ratio_short_needle( @@ -242,7 +241,7 @@ where s1_iter.clone(), len1, score_cutoff / 100.0, - score_hint, + args.score_hint, ) }; if (res.score != 100.0) && (len1 == len2) { @@ -254,7 +253,7 @@ where s1_iter.clone(), len1, score_cutoff / 100.0, - None, + args.score_hint, ) } else { partial_ratio_short_needle( @@ -263,7 +262,7 @@ where s2_iter.clone(), len2, score_cutoff / 100.0, - None, + args.score_hint, ) }; if res2.score > res.score { @@ -341,7 +340,7 @@ where dest_end: len1, }; - let indel_comp = indel::IndividualComparator {}; + let indel_comp = indel::BatchComparator::new(s1_iter.clone()); for i in 1..len1 { let substr_last = s2_iter.clone().nth(i - 1).unwrap(); @@ -349,14 +348,15 @@ where continue; } - let ls_ratio = indel_comp._normalized_similarity( - s1_iter.clone(), - len1, - s2_iter.clone().take(i).collect::>().into_iter(), - i, - Some(score_cutoff), - score_hint, - ); + let ls_ratio = indel_comp + .normalized_similarity_with_args( + s2_iter.clone().take(i).collect::>().into_iter(), + &indel::Args { + score_cutoff: WithScoreCutoff(score_cutoff), + score_hint, + }, + ) + .expect("score should be calculated"); if ls_ratio > res.score { score_cutoff = ls_ratio; res.score = ls_ratio; @@ -376,19 +376,20 @@ where continue; } - let ls_ratio = indel_comp._normalized_similarity( - s1_iter.clone(), - len1, - s2_iter - .clone() - .skip(i) - .take(len1) - .collect::>() - .into_iter(), - len1, - Some(score_cutoff), - score_hint, - ); + let ls_ratio = indel_comp + .normalized_similarity_with_args( + s2_iter + .clone() + .skip(i) + .take(len1) + .collect::>() + .into_iter(), + &indel::Args { + score_cutoff: WithScoreCutoff(score_cutoff), + score_hint, + }, + ) + .expect("score should be calculated"); if ls_ratio > res.score { score_cutoff = ls_ratio; res.score = ls_ratio; @@ -407,14 +408,15 @@ where continue; } - let ls_ratio = indel_comp._normalized_similarity( - s1_iter.clone(), - len1, - s2_iter.clone().skip(i).collect::>().into_iter(), - len2 - i, - Some(score_cutoff), - score_hint, - ); + let ls_ratio = indel_comp + .normalized_similarity_with_args( + s2_iter.clone().skip(i).collect::>().into_iter(), + &indel::Args { + score_cutoff: WithScoreCutoff(score_cutoff), + score_hint, + }, + ) + .expect("score should be calculated"); if ls_ratio > res.score { score_cutoff = ls_ratio; res.score = ls_ratio; From e8900f4a4e1e213ab4043fcc01ee68cc59fa4e38 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Mon, 9 Dec 2024 01:11:17 +0100 Subject: [PATCH 04/29] change short needle name --- src/fuzz.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index aef3f60..b75f3d0 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -226,7 +226,7 @@ where let mut score_cutoff = args.score_cutoff.cutoff().unwrap_or(0.0); let mut res = if len1 <= len2 { - partial_ratio_short_needle( + partial_ratio_impl( s1_iter.clone(), len1, s2_iter.clone(), @@ -235,7 +235,7 @@ where args.score_hint, ) } else { - partial_ratio_short_needle( + partial_ratio_impl( s2_iter.clone(), len2, s1_iter.clone(), @@ -247,7 +247,7 @@ where if (res.score != 100.0) && (len1 == len2) { score_cutoff = f64::max(score_cutoff, res.score); let res2 = if len1 <= len2 { - partial_ratio_short_needle( + partial_ratio_impl( s2_iter.clone(), len2, s1_iter.clone(), @@ -256,7 +256,7 @@ where args.score_hint, ) } else { - partial_ratio_short_needle( + partial_ratio_impl( s1_iter.clone(), len1, s2_iter.clone(), @@ -298,7 +298,7 @@ where implementation of partial_ratio for needles <= 64. assumes s1 is already the shorter string */ -fn partial_ratio_short_needle( +fn partial_ratio_impl( s1: Iter1, len1: usize, s2: Iter2, @@ -356,7 +356,7 @@ where score_hint, }, ) - .expect("score should be calculated"); + .expect("none only returned when using score_cutoff.score"); if ls_ratio > res.score { score_cutoff = ls_ratio; res.score = ls_ratio; @@ -389,7 +389,7 @@ where score_hint, }, ) - .expect("score should be calculated"); + .expect("none only returned when using score_cutoff.score"); if ls_ratio > res.score { score_cutoff = ls_ratio; res.score = ls_ratio; @@ -416,7 +416,7 @@ where score_hint, }, ) - .expect("score should be calculated"); + .expect("none only returned when using score_cutoff.score"); if ls_ratio > res.score { score_cutoff = ls_ratio; res.score = ls_ratio; @@ -589,7 +589,7 @@ mod tests { let s1 = "abcd"; let s2 = "abcd"; - let result = partial_ratio_short_needle( + let result = partial_ratio_impl( s1.chars(), s1.chars().count(), s2.chars(), @@ -610,7 +610,7 @@ mod tests { let s1 = "bcd"; let s2 = "abcde"; - let result = partial_ratio_short_needle( + let result = partial_ratio_impl( s1.chars(), s1.chars().count(), s2.chars(), From 5f8aae9635172301739ca7bf8a67244bb3839d9d Mon Sep 17 00:00:00 2001 From: ljnsn Date: Tue, 10 Dec 2024 23:17:56 +0100 Subject: [PATCH 05/29] fix wrong call --- src/fuzz.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index b75f3d0..4724caa 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -170,7 +170,7 @@ where Iter1::Item: PartialEq + HashableChar + Copy, Iter2::Item: PartialEq + HashableChar + Copy, { - ratio_with_args(s1, s2, &Args::default()) + partial_ratio_with_args(s1, s2, &Args::default()) } pub fn partial_ratio_with_args( From 9d65c4be3d4a7a93d248089bdd286c471ba70881 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Tue, 10 Dec 2024 23:23:03 +0100 Subject: [PATCH 06/29] unwrap to zero --- src/fuzz.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 4724caa..8f82bd5 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -356,7 +356,7 @@ where score_hint, }, ) - .expect("none only returned when using score_cutoff.score"); + .unwrap_or(0.0); if ls_ratio > res.score { score_cutoff = ls_ratio; res.score = ls_ratio; @@ -389,7 +389,7 @@ where score_hint, }, ) - .expect("none only returned when using score_cutoff.score"); + .unwrap_or(0.0); if ls_ratio > res.score { score_cutoff = ls_ratio; res.score = ls_ratio; @@ -416,7 +416,7 @@ where score_hint, }, ) - .expect("none only returned when using score_cutoff.score"); + .unwrap_or(0.0); if ls_ratio > res.score { score_cutoff = ls_ratio; res.score = ls_ratio; From e23005a72427d59b9eb4e10b236a04a6f73e74b9 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Tue, 10 Dec 2024 23:23:08 +0100 Subject: [PATCH 07/29] add tests --- src/fuzz.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 8f82bd5..dbb62c0 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -585,7 +585,53 @@ mod tests { } #[test] - fn test_partial_ratio_short_needle_identical() { + fn test_partial_ratio2() { + let s1 = "this is a test"; + let s2 = "this is a test!"; + let result = partial_ratio(s1.chars(), s2.chars()); + assert_eq!(result, 100.0, "Expected 100.0"); + } + + #[test] + fn test_partial_ratio_issue138() { + let s1 = &"a".repeat(65); + let s2 = &format!("a{}{}", char::from_u32(256).unwrap(), "a".repeat(63)); + let result = partial_ratio(s1.chars(), s2.chars()); + assert!( + (result - 99.22481).abs() < 1e-5, + "Expected approximately 99.22481, got {}", + result + ); + } + + #[test] + fn test_partial_ratio_alignment() { + let str1 = "er merkantilismus förderte handle und verkehr mit teils marktkonformen, teils dirigistischen maßnahmen."; + let str2 = "ils marktkonformen, teils dirigistischen maßnahmen. an der schwelle zum 19. jahrhundert entstand ein neu"; + + let alignment = partial_ratio_alignment( + str1.chars(), + str1.chars().count(), + str2.chars(), + str2.chars().count(), + &Args::default(), + ); + + dbg!(&alignment); + + assert!( + (alignment.as_ref().unwrap().score - 66.2337662).abs() < 1e-5, + "Expected 66.2337662, got {}", + alignment.unwrap().score + ); + assert_eq!(alignment.as_ref().unwrap().src_start, 0); + assert_eq!(alignment.as_ref().unwrap().src_end, 103); + assert_eq!(alignment.as_ref().unwrap().dest_start, 0); + assert_eq!(alignment.as_ref().unwrap().dest_end, 51); + } + + #[test] + fn test_partial_ratio_impl_identical() { let s1 = "abcd"; let s2 = "abcd"; @@ -606,7 +652,7 @@ mod tests { } #[test] - fn test_partial_ratio_short_needle_substring() { + fn test_partial_ratio_impl_substring() { let s1 = "bcd"; let s2 = "abcde"; From d98e40acb3947cb3c6a653f61863cdf1b94b6ae3 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Tue, 10 Dec 2024 23:31:39 +0100 Subject: [PATCH 08/29] collect into vec for perf --- src/fuzz.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index dbb62c0..11f0fa6 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -325,7 +325,7 @@ where } let s1_iter = s1.into_iter(); - let s2_iter = s2.into_iter(); + let s2_vec = s2.into_iter().collect::>(); let s1_char_set = s1_iter .clone() @@ -343,14 +343,14 @@ where let indel_comp = indel::BatchComparator::new(s1_iter.clone()); for i in 1..len1 { - let substr_last = s2_iter.clone().nth(i - 1).unwrap(); + let substr_last = &s2_vec[i - 1]; if !s1_char_set.contains(&substr_last.hash_char()) { continue; } let ls_ratio = indel_comp .normalized_similarity_with_args( - s2_iter.clone().take(i).collect::>().into_iter(), + s2_vec[..i].iter().cloned(), &indel::Args { score_cutoff: WithScoreCutoff(score_cutoff), score_hint, @@ -371,19 +371,14 @@ where let window_end = len2 - len1; for i in 0..window_end { - let substr_last = s2_iter.clone().nth(i + len1 - 1).unwrap(); + let substr_last = &s2_vec[i + len1 - 1]; if !s1_char_set.contains(&substr_last.hash_char()) { continue; } let ls_ratio = indel_comp .normalized_similarity_with_args( - s2_iter - .clone() - .skip(i) - .take(len1) - .collect::>() - .into_iter(), + s2_vec[i..i + len1].iter().cloned(), &indel::Args { score_cutoff: WithScoreCutoff(score_cutoff), score_hint, @@ -403,14 +398,14 @@ where } for i in window_end..len2 { - let substr_first = s2_iter.clone().nth(i).unwrap(); + let substr_first = &s2_vec[i]; if !s1_char_set.contains(&substr_first.hash_char()) { continue; } let ls_ratio = indel_comp .normalized_similarity_with_args( - s2_iter.clone().skip(i).collect::>().into_iter(), + s2_vec[i..].iter().cloned(), &indel::Args { score_cutoff: WithScoreCutoff(score_cutoff), score_hint, From 77fb71b12c60f0d30372c1c7dfc321db6d7161a3 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Tue, 10 Dec 2024 23:34:41 +0100 Subject: [PATCH 09/29] remove useless reconstruct --- src/fuzz.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 11f0fa6..243cee7 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -280,18 +280,7 @@ where return None; } - // do we need this? why not just `Some(res)`? - if len1 <= len2 { - return Some(res); - } - - Some(ScoreAlignment { - score: res.score, - src_start: res.dest_start, - src_end: res.dest_end, - dest_start: res.src_start, - dest_end: res.src_end, - }) + Some(res) } /** From b1f61bbbdbc66bd5a324e59fc80ed94c3af0e079 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Tue, 10 Dec 2024 23:43:48 +0100 Subject: [PATCH 10/29] minor niceness improvement --- src/fuzz.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 243cee7..f643a61 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -222,7 +222,6 @@ where { let s1_iter = s1.into_iter(); let s2_iter = s2.into_iter(); - let mut score_cutoff = args.score_cutoff.cutoff().unwrap_or(0.0); let mut res = if len1 <= len2 { @@ -276,11 +275,7 @@ where } } - if res.score < score_cutoff { - return None; - } - - Some(res) + (res.score >= score_cutoff).then_some(res) } /** From 89f60942d3508b576c8dbe52f088ad275c32356b Mon Sep 17 00:00:00 2001 From: ljnsn Date: Wed, 11 Dec 2024 00:27:37 +0100 Subject: [PATCH 11/29] don't multiply score by 100 --- src/fuzz.rs | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index f643a61..7df01e1 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -157,7 +157,7 @@ where /// # Example /// ``` /// use rapidfuzz::fuzz; -/// /// score is 100.0 +/// /// score is 1.0 /// let score = fuzz::partial_ratio("this is a test".chars(), "this is a test!".chars()); /// ``` /// @@ -230,7 +230,7 @@ where len1, s2_iter.clone(), len2, - score_cutoff / 100.0, + score_cutoff, args.score_hint, ) } else { @@ -239,11 +239,11 @@ where len2, s1_iter.clone(), len1, - score_cutoff / 100.0, + score_cutoff, args.score_hint, ) }; - if (res.score != 100.0) && (len1 == len2) { + if (res.score != 1.0) && (len1 == len2) { score_cutoff = f64::max(score_cutoff, res.score); let res2 = if len1 <= len2 { partial_ratio_impl( @@ -251,7 +251,7 @@ where len2, s1_iter.clone(), len1, - score_cutoff / 100.0, + score_cutoff, args.score_hint, ) } else { @@ -260,7 +260,7 @@ where len1, s2_iter.clone(), len2, - score_cutoff / 100.0, + score_cutoff, args.score_hint, ) }; @@ -347,7 +347,6 @@ where res.dest_start = 0; res.dest_end = i; if res.score == 1.0 { - res.score = 100.0; return res; } } @@ -375,7 +374,6 @@ where res.dest_start = i; res.dest_end = i + len1; if res.score == 1.0 { - res.score = 100.0; return res; } } @@ -402,13 +400,11 @@ where res.dest_start = i; res.dest_end = len2; if res.score == 1.0 { - res.score = 100.0; return res; } } } - res.score *= 100.0; res } @@ -568,7 +564,7 @@ mod tests { let s1 = "this is a test"; let s2 = "this is a test!"; let result = partial_ratio(s1.chars(), s2.chars()); - assert_eq!(result, 100.0, "Expected 100.0"); + assert_eq!(result, 1.0, "Expected 1.0"); } #[test] @@ -577,8 +573,8 @@ mod tests { let s2 = &format!("a{}{}", char::from_u32(256).unwrap(), "a".repeat(63)); let result = partial_ratio(s1.chars(), s2.chars()); assert!( - (result - 99.22481).abs() < 1e-5, - "Expected approximately 99.22481, got {}", + (result - 0.9922481).abs() < 1e-5, + "Expected approximately 0.9922481, got {}", result ); } @@ -599,8 +595,8 @@ mod tests { dbg!(&alignment); assert!( - (alignment.as_ref().unwrap().score - 66.2337662).abs() < 1e-5, - "Expected 66.2337662, got {}", + (alignment.as_ref().unwrap().score - 0.662337662).abs() < 1e-5, + "Expected 0.662337662, got {}", alignment.unwrap().score ); assert_eq!(alignment.as_ref().unwrap().src_start, 0); @@ -623,7 +619,7 @@ mod tests { None, ); - assert_eq!(result.score, 100.0); + assert_eq!(result.score, 1.0); assert_eq!(result.src_start, 0); assert_eq!(result.src_end, 4); assert_eq!(result.dest_start, 0); @@ -644,7 +640,7 @@ mod tests { None, ); - assert_eq!(result.score, 100.0); + assert_eq!(result.score, 1.0); assert_eq!(result.src_start, 0); assert_eq!(result.src_end, 3); assert_eq!(result.dest_start, 1); From 158e5790ae322e57ef2c8ef55436c7bc44e62fc3 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Thu, 12 Dec 2024 01:28:55 +0100 Subject: [PATCH 12/29] add alignment output type --- src/common.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/common.rs b/src/common.rs index b934fd6..f5fd72f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,3 +1,4 @@ +use crate::distance::common::ScoreAlignment; use std::fmt::Debug; #[derive(Default, Copy, Clone)] @@ -50,9 +51,11 @@ where T: Copy, { type Output: Copy + Into> + PartialEq + Debug; + type AlignmentOutput: Copy + Into> + PartialEq + Debug; fn cutoff(&self) -> Option; fn score(&self, raw: T) -> Self::Output; + fn alignment(&self, raw: Option) -> Self::AlignmentOutput; } impl SimilarityCutoff for NoScoreCutoff @@ -60,6 +63,7 @@ where T: Copy + PartialEq + Debug, { type Output = T; + type AlignmentOutput = ScoreAlignment; fn cutoff(&self) -> Option { None @@ -68,6 +72,10 @@ where fn score(&self, raw: T) -> Self::Output { raw } + + fn alignment(&self, raw: Option) -> Self::AlignmentOutput { + raw.unwrap() + } } impl SimilarityCutoff for WithScoreCutoff @@ -75,6 +83,7 @@ where T: Copy + PartialOrd + Debug, { type Output = Option; + type AlignmentOutput = Option; fn cutoff(&self) -> Option { Some(self.0) @@ -83,4 +92,8 @@ where fn score(&self, raw: T) -> Self::Output { (raw >= self.0).then_some(raw) } + + fn alignment(&self, raw: Option) -> Self::AlignmentOutput { + raw + } } From ef1e2be7f36ad13b626e305fb56698fa4bce008f Mon Sep 17 00:00:00 2001 From: ljnsn Date: Thu, 12 Dec 2024 01:29:20 +0100 Subject: [PATCH 13/29] derive clone and copy for score alignment --- src/distance/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/distance/common.rs b/src/distance/common.rs index 8353578..007fe66 100644 --- a/src/distance/common.rs +++ b/src/distance/common.rs @@ -5,7 +5,7 @@ src and dest. It indicates that the score has been calculated between src[src_start:src_end] and dest[dest_start:dest_end] */ -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone, Copy)] pub struct ScoreAlignment { pub score: f64, pub src_start: usize, From e352abfa906315af78ccaada302a5a465129a9ab Mon Sep 17 00:00:00 2001 From: ljnsn Date: Thu, 12 Dec 2024 01:29:43 +0100 Subject: [PATCH 14/29] simplify code and improve typing --- src/fuzz.rs | 90 ++++++++++++++++++++++------------------------------- 1 file changed, 37 insertions(+), 53 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 7df01e1..2aa46e8 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -185,7 +185,7 @@ where Iter2::IntoIter: DoubleEndedIterator + Clone, Iter1::Item: PartialEq + HashableChar + Copy, Iter2::Item: PartialEq + HashableChar + Copy, - CutoffType: SimilarityCutoff, + CutoffType: SimilarityCutoff, { let s1_iter = s1.into_iter(); let s2_iter = s2.into_iter(); @@ -198,10 +198,8 @@ where args, ); - match alignment { - Some(alignment) => alignment.score, - None => 0.0, - } + let score = alignment.into().map_or(0.0, |alignment| alignment.score); + args.score_cutoff.score(score) } pub fn partial_ratio_alignment( @@ -210,7 +208,7 @@ pub fn partial_ratio_alignment( s2: Iter2, len2: usize, args: &Args, -) -> Option +) -> CutoffType::AlignmentOutput where Iter1: IntoIterator, Iter1::IntoIter: DoubleEndedIterator + Clone, @@ -218,52 +216,40 @@ where Iter2::IntoIter: DoubleEndedIterator + Clone, Iter1::Item: PartialEq + HashableChar + Copy, Iter2::Item: PartialEq + HashableChar + Copy, - CutoffType: SimilarityCutoff, + CutoffType: SimilarityCutoff, { let s1_iter = s1.into_iter(); let s2_iter = s2.into_iter(); let mut score_cutoff = args.score_cutoff.cutoff().unwrap_or(0.0); - let mut res = if len1 <= len2 { - partial_ratio_impl( - s1_iter.clone(), - len1, - s2_iter.clone(), - len2, - score_cutoff, - args.score_hint, - ) - } else { - partial_ratio_impl( + if len1 > len2 { + let res = partial_ratio_alignment(s2_iter.clone(), len2, s1_iter.clone(), len1, args); + return args.score_cutoff.alignment(res.into().map(|mut alignment| { + std::mem::swap(&mut alignment.src_start, &mut alignment.dest_start); + std::mem::swap(&mut alignment.src_end, &mut alignment.dest_end); + alignment + })); + } + + let mut res = partial_ratio_impl( + s1_iter.clone(), + len1, + s2_iter.clone(), + len2, + score_cutoff, + args.score_hint, + ); + + if (res.score != 1.0) && (len1 == len2) { + score_cutoff = f64::max(score_cutoff, res.score); + let res2 = partial_ratio_impl( s2_iter.clone(), len2, s1_iter.clone(), len1, score_cutoff, args.score_hint, - ) - }; - if (res.score != 1.0) && (len1 == len2) { - score_cutoff = f64::max(score_cutoff, res.score); - let res2 = if len1 <= len2 { - partial_ratio_impl( - s2_iter.clone(), - len2, - s1_iter.clone(), - len1, - score_cutoff, - args.score_hint, - ) - } else { - partial_ratio_impl( - s1_iter.clone(), - len1, - s2_iter.clone(), - len2, - score_cutoff, - args.score_hint, - ) - }; + ); if res2.score > res.score { res = ScoreAlignment { score: res2.score, @@ -275,7 +261,7 @@ where } } - (res.score >= score_cutoff).then_some(res) + args.score_cutoff.alignment(Some(res)) } /** @@ -571,12 +557,10 @@ mod tests { fn test_partial_ratio_issue138() { let s1 = &"a".repeat(65); let s2 = &format!("a{}{}", char::from_u32(256).unwrap(), "a".repeat(63)); + let result = partial_ratio(s1.chars(), s2.chars()); - assert!( - (result - 0.9922481).abs() < 1e-5, - "Expected approximately 0.9922481, got {}", - result - ); + + assert_delta!(Some(0.9922481), Some(result)); } #[test] @@ -595,14 +579,14 @@ mod tests { dbg!(&alignment); assert!( - (alignment.as_ref().unwrap().score - 0.662337662).abs() < 1e-5, + (alignment.score - 0.662337662).abs() < 1e-5, "Expected 0.662337662, got {}", - alignment.unwrap().score + alignment.score ); - assert_eq!(alignment.as_ref().unwrap().src_start, 0); - assert_eq!(alignment.as_ref().unwrap().src_end, 103); - assert_eq!(alignment.as_ref().unwrap().dest_start, 0); - assert_eq!(alignment.as_ref().unwrap().dest_end, 51); + assert_eq!(alignment.src_start, 0); + assert_eq!(alignment.src_end, 103); + assert_eq!(alignment.dest_start, 0); + assert_eq!(alignment.dest_end, 51); } #[test] From 1dee8013f4cb5d6a8e65ddb3da39b267cebc49d2 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Mon, 13 Jan 2025 00:36:47 +0100 Subject: [PATCH 15/29] add test for partial_ratio_with_args returning None --- src/fuzz.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/fuzz.rs b/src/fuzz.rs index 2aa46e8..2d082d6 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -515,6 +515,27 @@ mod tests { ) ); } + + { + let score = partial_ratio(str1.chars(), str2.chars()); + + assert_eq!( + None, + partial_ratio_with_args( + str1.chars(), + str2.chars(), + &Args::default().score_cutoff(score + 0.0001) + ) + ); + assert_delta!( + Some(score), + partial_ratio_with_args( + str1.chars(), + str2.chars(), + &Args::default().score_cutoff(score - 0.0001) + ) + ); + } } // https://github.com/rapidfuzz/RapidFuzz/issues/210 From 2ea22a84db8394dd2609f26e4c8d9875a80e1974 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Mon, 13 Jan 2025 00:41:34 +0100 Subject: [PATCH 16/29] fix comment --- src/fuzz.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 2d082d6..d6a98e5 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -265,8 +265,7 @@ where } /** -implementation of partial_ratio for needles <= 64. assumes s1 is already the -shorter string +implementation of partial_ratio for needles <= 64. assumes len(s1) <= len(s2) */ fn partial_ratio_impl( s1: Iter1, From 42c6bf8785b7575378079f19092cac2a9a37040e Mon Sep 17 00:00:00 2001 From: ljnsn Date: Mon, 13 Jan 2025 00:47:31 +0100 Subject: [PATCH 17/29] pass in charset --- src/fuzz.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index d6a98e5..955afc5 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -2,7 +2,7 @@ use crate::common::{NoScoreCutoff, SimilarityCutoff, WithScoreCutoff}; use crate::details::distance::MetricUsize; use crate::distance::common::ScoreAlignment; use crate::distance::indel; -use crate::HashableChar; +use crate::{Hash, HashableChar}; use std::collections::HashSet; #[must_use] @@ -231,11 +231,17 @@ where })); } + let s1_char_set = s1_iter + .clone() + .map(|c| c.hash_char()) + .collect::>(); + let mut res = partial_ratio_impl( s1_iter.clone(), len1, s2_iter.clone(), len2, + &s1_char_set, score_cutoff, args.score_hint, ); @@ -247,6 +253,7 @@ where len2, s1_iter.clone(), len1, + &s1_char_set, score_cutoff, args.score_hint, ); @@ -272,6 +279,7 @@ fn partial_ratio_impl( len1: usize, s2: Iter2, len2: usize, + s1_char_set: &HashSet, mut score_cutoff: f64, score_hint: Option, ) -> ScoreAlignment @@ -296,11 +304,6 @@ where let s1_iter = s1.into_iter(); let s2_vec = s2.into_iter().collect::>(); - let s1_char_set = s1_iter - .clone() - .map(|c| c.hash_char()) - .collect::>(); - let mut res = ScoreAlignment { score: 0.0, src_start: 0, @@ -619,6 +622,7 @@ mod tests { s1.chars().count(), s2.chars(), s2.chars().count(), + &s1.chars().map(|c| c.hash_char()).collect(), 0.0, None, ); @@ -640,6 +644,7 @@ mod tests { s1.chars().count(), s2.chars(), s2.chars().count(), + &s1.chars().map(|c| c.hash_char()).collect(), 0.0, None, ); From e153709e370f7e31c0b87886a9d55421a64ea9d8 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Mon, 13 Jan 2025 01:17:07 +0100 Subject: [PATCH 18/29] fix: pass in batch comparator --- src/fuzz.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 955afc5..fa3a37e 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -236,24 +236,27 @@ where .map(|c| c.hash_char()) .collect::>(); + let indel_comp = indel::BatchComparator::new(s1_iter.clone()); + let mut res = partial_ratio_impl( - s1_iter.clone(), len1, s2_iter.clone(), len2, &s1_char_set, + indel_comp, score_cutoff, args.score_hint, ); if (res.score != 1.0) && (len1 == len2) { score_cutoff = f64::max(score_cutoff, res.score); + let indel_comp = indel::BatchComparator::new(s2_iter.clone()); let res2 = partial_ratio_impl( - s2_iter.clone(), len2, s1_iter.clone(), len1, &s1_char_set, + indel_comp, score_cutoff, args.score_hint, ); @@ -274,22 +277,20 @@ where /** implementation of partial_ratio for needles <= 64. assumes len(s1) <= len(s2) */ -fn partial_ratio_impl( - s1: Iter1, +fn partial_ratio_impl( len1: usize, s2: Iter2, len2: usize, s1_char_set: &HashSet, + indel_comp: indel::BatchComparator, mut score_cutoff: f64, score_hint: Option, ) -> ScoreAlignment where - Iter1: IntoIterator, - Iter1::IntoIter: DoubleEndedIterator + Clone, Iter2: IntoIterator, Iter2::IntoIter: DoubleEndedIterator + Clone, - Iter1::Item: PartialEq + HashableChar + Copy, - Iter2::Item: PartialEq + HashableChar + Copy, + Elem1: PartialEq + HashableChar + Copy, + Iter2::Item: PartialEq + HashableChar + Copy, { if len1 == 0 { return ScoreAlignment { @@ -301,7 +302,6 @@ where }; } - let s1_iter = s1.into_iter(); let s2_vec = s2.into_iter().collect::>(); let mut res = ScoreAlignment { @@ -312,8 +312,6 @@ where dest_end: len1, }; - let indel_comp = indel::BatchComparator::new(s1_iter.clone()); - for i in 1..len1 { let substr_last = &s2_vec[i - 1]; if !s1_char_set.contains(&substr_last.hash_char()) { @@ -617,12 +615,14 @@ mod tests { let s1 = "abcd"; let s2 = "abcd"; + let indel_comp = indel::BatchComparator::new(s1.chars()); + let result = partial_ratio_impl( - s1.chars(), s1.chars().count(), s2.chars(), s2.chars().count(), &s1.chars().map(|c| c.hash_char()).collect(), + indel_comp, 0.0, None, ); @@ -639,12 +639,14 @@ mod tests { let s1 = "bcd"; let s2 = "abcde"; + let indel_comp = indel::BatchComparator::new(s1.chars()); + let result = partial_ratio_impl( - s1.chars(), s1.chars().count(), s2.chars(), s2.chars().count(), &s1.chars().map(|c| c.hash_char()).collect(), + indel_comp, 0.0, None, ); From d5510ba384ba4e8b3e7d842b42f0065a17128457 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Mon, 13 Jan 2025 01:46:31 +0100 Subject: [PATCH 19/29] add PartialRatioBatchComparator --- src/fuzz.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index fa3a37e..ec033e8 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -239,11 +239,11 @@ where let indel_comp = indel::BatchComparator::new(s1_iter.clone()); let mut res = partial_ratio_impl( + &indel_comp, len1, s2_iter.clone(), len2, &s1_char_set, - indel_comp, score_cutoff, args.score_hint, ); @@ -252,11 +252,11 @@ where score_cutoff = f64::max(score_cutoff, res.score); let indel_comp = indel::BatchComparator::new(s2_iter.clone()); let res2 = partial_ratio_impl( + &indel_comp, len2, s1_iter.clone(), len1, &s1_char_set, - indel_comp, score_cutoff, args.score_hint, ); @@ -278,11 +278,11 @@ where implementation of partial_ratio for needles <= 64. assumes len(s1) <= len(s2) */ fn partial_ratio_impl( + comparator: &indel::BatchComparator, len1: usize, s2: Iter2, len2: usize, s1_char_set: &HashSet, - indel_comp: indel::BatchComparator, mut score_cutoff: f64, score_hint: Option, ) -> ScoreAlignment @@ -318,7 +318,7 @@ where continue; } - let ls_ratio = indel_comp + let ls_ratio = comparator .normalized_similarity_with_args( s2_vec[..i].iter().cloned(), &indel::Args { @@ -345,7 +345,7 @@ where continue; } - let ls_ratio = indel_comp + let ls_ratio = comparator .normalized_similarity_with_args( s2_vec[i..i + len1].iter().cloned(), &indel::Args { @@ -371,7 +371,7 @@ where continue; } - let ls_ratio = indel_comp + let ls_ratio = comparator .normalized_similarity_with_args( s2_vec[i..].iter().cloned(), &indel::Args { @@ -394,6 +394,82 @@ where res } +/// `One x Many` comparisons using `partial_ratio` +/// +/// # Examples +/// +/// ``` +/// use rapidfuzz::fuzz; +/// +/// let scorer = fuzz::PartialRatioBatchComparator::new("this is a test".chars()); +/// /// score is 1.0 +/// let score = scorer.similarity("this is a test!".chars()); +/// ``` +pub struct PartialRatioBatchComparator { + scorer: indel::BatchComparator, +} + +impl PartialRatioBatchComparator +where + Elem1: HashableChar + Clone, +{ + pub fn new(s1: Iter1) -> Self + where + Iter1: IntoIterator, + Iter1::IntoIter: Clone, + { + Self { + scorer: indel::BatchComparator::new(s1), + } + } + + /// Similarity calculated similar to [`partial_ratio`] + pub fn similarity(&self, s2: Iter2) -> f64 + where + Iter2: IntoIterator, + Iter2::IntoIter: DoubleEndedIterator + Clone, + Elem1: PartialEq + HashableChar + Copy, + Iter2::Item: PartialEq + HashableChar + Copy, + { + self.similarity_with_args(s2, &Args::default()) + } + + pub fn similarity_with_args( + &self, + s2: Iter2, + args: &Args, + ) -> CutoffType::Output + where + Iter2: IntoIterator, + Iter2::IntoIter: DoubleEndedIterator + Clone, + Elem1: PartialEq + HashableChar + Copy, + Iter2::Item: PartialEq + HashableChar + Copy, + CutoffType: SimilarityCutoff, + { + let s2_iter = s2.into_iter(); + + let res = partial_ratio_impl( + &self.scorer, + self.scorer.scorer.s1.len(), + s2_iter.clone(), + s2_iter.count(), + &self + .scorer + .scorer + .s1 + .iter() + .map(|c| c.hash_char()) + .collect(), + args.score_cutoff.cutoff().unwrap_or(0.0), + args.score_hint, + ); + + let alignment = args.score_cutoff.alignment(Some(res)); + let score = alignment.into().map_or(0.0, |alignment| alignment.score); + args.score_cutoff.score(score) + } +} + #[cfg(test)] mod tests { use super::*; @@ -618,11 +694,11 @@ mod tests { let indel_comp = indel::BatchComparator::new(s1.chars()); let result = partial_ratio_impl( + &indel_comp, s1.chars().count(), s2.chars(), s2.chars().count(), &s1.chars().map(|c| c.hash_char()).collect(), - indel_comp, 0.0, None, ); @@ -642,11 +718,11 @@ mod tests { let indel_comp = indel::BatchComparator::new(s1.chars()); let result = partial_ratio_impl( + &indel_comp, s1.chars().count(), s2.chars(), s2.chars().count(), &s1.chars().map(|c| c.hash_char()).collect(), - indel_comp, 0.0, None, ); From 01fc88cacf95a9661cbaa171ada050106558b49f Mon Sep 17 00:00:00 2001 From: ljnsn Date: Sun, 26 Jan 2025 19:39:42 +0100 Subject: [PATCH 20/29] tive tests better names --- src/fuzz.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index ec033e8..c354e37 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -524,7 +524,7 @@ mod tests { } #[test] - fn test_partial_ratio() { + fn test_partially_equal() { //assert_delta!(Some(1.0), partial_ratio(S1.chars(), S1.chars(), None, None)); assert_delta!( Some(0.65), @@ -643,7 +643,7 @@ mod tests { } #[test] - fn test_partial_ratio2() { + fn test_partial_ratio() { let s1 = "this is a test"; let s2 = "this is a test!"; let result = partial_ratio(s1.chars(), s2.chars()); From 0cdb9dd6dd4c8efc87e39c5df8c82ac1ddd95964 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Sun, 26 Jan 2025 19:39:52 +0100 Subject: [PATCH 21/29] use assert_delta --- src/fuzz.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index c354e37..85991f1 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -647,7 +647,8 @@ mod tests { let s1 = "this is a test"; let s2 = "this is a test!"; let result = partial_ratio(s1.chars(), s2.chars()); - assert_eq!(result, 1.0, "Expected 1.0"); + + assert_delta!(Some(1.0), Some(result)); } #[test] From d8d45cf03fa0f8ed37bcdd7b0d4978ddc7d75a49 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Sun, 26 Jan 2025 20:17:21 +0100 Subject: [PATCH 22/29] fix: return None if score lower than cutoff --- src/fuzz.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 85991f1..c035a5d 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -271,7 +271,13 @@ where } } - args.score_cutoff.alignment(Some(res)) + let alignment = if res.score < score_cutoff { + None + } else { + Some(res) + }; + + args.score_cutoff.alignment(alignment) } /** From d5daf50041d2d1cb2043d6f02f0d90bb534b40f8 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Sun, 26 Jan 2025 20:17:34 +0100 Subject: [PATCH 23/29] test: add more tests --- src/fuzz.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/fuzz.rs b/src/fuzz.rs index c035a5d..7745ffd 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -657,6 +657,26 @@ mod tests { assert_delta!(Some(1.0), Some(result)); } + #[test] + fn test_partial_ratio_issue76() { + let s1 = "physics 2 vid"; + let s2 = "study physics physics 2 video"; + + let result = partial_ratio(s1.chars(), s2.chars()); + + assert_delta!(Some(1.0), Some(result)) + } + + #[test] + fn test_partial_ratio_issue90() { + let s1 = "ax b"; + let s2 = "a b a c b"; + + let result = partial_ratio(s1.chars(), s2.chars()); + + assert_delta!(Some(0.8571428), Some(result)) + } + #[test] fn test_partial_ratio_issue138() { let s1 = &"a".repeat(65); @@ -669,6 +689,72 @@ mod tests { #[test] fn test_partial_ratio_alignment() { + let s1 = "a certain string"; + let s2 = "certain"; + + let result1 = partial_ratio_alignment( + s2.chars(), + s2.chars().count(), + s1.chars(), + s1.chars().count(), + &Args::default(), + ); + + assert_eq!( + result1, + ScoreAlignment { + score: 1.0, + src_start: 0, + src_end: s2.len(), + dest_start: 2, + dest_end: 2 + s2.len() + } + ); + + let result2 = partial_ratio_alignment( + s1.chars(), + s1.chars().count(), + s2.chars(), + s2.chars().count(), + &Args::default(), + ); + + assert_eq!( + result2, + ScoreAlignment { + score: 1.0, + src_start: 2, + src_end: 2 + s2.len(), + dest_start: 0, + dest_end: s2.len() + } + ); + + // assert_eq!( + // None, + // Some(partial_ratio_alignment( + // None, + // 0, + // "test".chars(), + // "test".chars().count(), + // &Args::default() + // )) + // ); + + assert_eq!( + None, + partial_ratio_alignment( + "test".chars(), + "test".chars().count(), + "tesx".chars(), + "tesx".chars().count(), + &Args::default().score_cutoff(0.9) + ) + ); + } + + #[test] + fn test_partial_ratio_alignment_issue231() { let str1 = "er merkantilismus förderte handle und verkehr mit teils marktkonformen, teils dirigistischen maßnahmen."; let str2 = "ils marktkonformen, teils dirigistischen maßnahmen. an der schwelle zum 19. jahrhundert entstand ein neu"; From ef63376b45e474de17cee5389684aba982b13d88 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Sun, 26 Jan 2025 20:21:05 +0100 Subject: [PATCH 24/29] fix: return 1 for two empty strings --- src/fuzz.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/fuzz.rs b/src/fuzz.rs index 7745ffd..dabbd8c 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -231,6 +231,16 @@ where })); } + if len1 == 0 && len2 == 0 { + return args.score_cutoff.alignment(Some(ScoreAlignment { + score: 1.0, + src_start: 0, + src_end: 0, + dest_start: 0, + dest_end: 0, + })); + } + let s1_char_set = s1_iter .clone() .map(|c| c.hash_char()) @@ -545,6 +555,15 @@ mod tests { Some(1.0), Some(ratio_with_args("".chars(), "".chars(), &Args::default())) ); + + assert_delta!( + Some(1.0), + Some(partial_ratio_with_args( + "".chars(), + "".chars(), + &Args::default() + )) + ); } #[test] From 02ec7e7e389d9d85a1cf5471dc8b4782725cb881 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Sun, 26 Jan 2025 20:24:44 +0100 Subject: [PATCH 25/29] add test for issue 257 --- src/fuzz.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/fuzz.rs b/src/fuzz.rs index dabbd8c..bcf5544 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -706,6 +706,22 @@ mod tests { assert_delta!(Some(0.9922481), Some(result)); } + #[test] + fn test_partial_ratio_issue257() { + let s1 = "aaaaaaaaaaaaaaaaaaaaaaaabacaaaaaaaabaaabaaaaaaaababbbbbbbbbbabbcb"; + let s2 = "aaaaaaaaaaaaaaaaaaaaaaaababaaaaaaaabaaabaaaaaaaababbbbbbbbbbabbcb"; + + let expected = 0.9846153846153847; + + let score = partial_ratio(s1.chars(), s2.chars()); + + assert_delta!(Some(expected), Some(score)); + + let score = partial_ratio(s2.chars(), s1.chars()); + + assert_delta!(Some(expected), Some(score)); + } + #[test] fn test_partial_ratio_alignment() { let s1 = "a certain string"; From f0a679e99d69474dab3ac3d6e3406456b4292560 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Sun, 26 Jan 2025 23:37:03 +0100 Subject: [PATCH 26/29] add test for issue 219 --- src/fuzz.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index bcf5544..222da66 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -706,6 +706,56 @@ mod tests { assert_delta!(Some(0.9922481), Some(result)); } + #[test] + fn test_partian_ratio_issue219() { + let s1 = "\ + TTAGCGCTACCGGTCGCCACCATGGTTTTCTAAGGGGAGGCCGTCATCAAAAGAGTTCATGTAGCACGAAGTCCACCTTTGAAGGATCGATGAATG\ + GCCATGAATTCGAAATCGAGGGGAGGGCGAGAGAGGGCCGGCCTTACGAGGGCACACCCAAACTGCCAAACTGAAAGTGACCAAAGGCGGCCCGTT\ + ACCATTCTCCTGGGACATACTGTAAGTGCATGGCACCACGCTCTATTTCTTAAAAAAAGTGTAGGGTCTGGCGCCCTCGGGGGCGGCTTAGGAAAA\ + GAGGCCTGACCAATTTTTGTCTCTTATAGGTCACCACAGTTCATGTACGGAAGCAGAGCGTTCACGAAGCACCCAGCTGACATCCCGGACTACTAT\ + GACAGAGCTTCCCGGAAGGACTCAAGTGGGAGCGGGTCATGAACTTCGAGGACGGTGGGGCAGTGACTGTGACACAGGACACCAGCCTGAAGATGG\ + AACTCTTATCTACAAAGTAAAGCTAAGAGGAACCAACTTCCCGCCAGATGGGCCCGTTATGCAAAAGAAAACGATGGGGTGGGAAGCTTCTGCAGA\ + GCGCCTTTACCCCGAGGATGGCGTCCTTAAGGGGGATATCAAAATGGCGCTACGCCTTAAGGATGGAGGCAGATATTTGGCAGACTTCAAAACAAC\ + ATTACAAGGCGAAGAAGCCAGTCCAGATGCCTGGAGCTTGCAATGGTAAGCACCTCTGCCTGCCCCGCTAGTTGGGTGTGAGTGGCCCAGGCAGCC\ + GCCTGCATTTAGCTCTAGCCGGGGTACGGGTGCCCCTTGATGCCTGAGGCCTCTCCTGTGGCTGAGGCGACTGGCCCAGAGTCTGGGTCTCCTCGA\ + GGGTGGCCATCTGGCGTCACCTGTCATCTGCCACCTCTGACCCCTGCCTCTCTCCTCACAGTTGACCGGAAGCTCGACATAACGAGTCACAACGAG\ + GACTACACAGTTGTCGAGCAGTACGAACGTTCCGAGGGTCGACACTCAACTGGCAGGATGGATGAGCTTTTACAAAGGGCGGGGGCGGAGGAAGCG\ + GAGGAGGAGGAAGTGGTGGAGGAGGCTCGAAAGGTAAGTATCAGGGTTGCAGCGTTTCTCTGACCTCATATTCCAATGGATGTGTGAGAAGCATAG\ + TGAGATCCGTTTACCCCTTTTGCTCAATTCTCACGTGGCTGTAGTCGTGTTTATAAGTCTGATCGTAATGGCAGCTTGGTCTGCGTGCCTTGAAAT\ + TGTGGCCCCCACATGCATAATAAACGATCCTCTAGCACTACTTTCTGTCGAGCCACCTCAGCGCCCGTACAGTAATGTCTACAGCGCGTCTAACCC\ + GACAAATGCGTTTCTTTCTCTCCTAGAACGAAAGATTACGGATCACAGAAACGTCTCGGAAAGTCCAAATAGAAAGAACGAGAAAGAAGAAAGTGA\ + AGGATCACAAGAGCAACTCGAAAGAAAGAGACATAAGAAGGAACTCAGAAAAGGATGACAAGTATAAAAACAAAGTGAAGAAAAGAGCGAAGAGCA\ + GAGTAGAAGCAAGAGTAAAGAGAAGAAGAGCAAATCGAAGGAAAGGTAAGTGGCTTTCAAGAACATTGGTAAAACGTCATGTGTATTGCGGTTCCA\ + TGCTTACACAAATTCGTTCGCTTGTTTTCAGGGACTCGAAACACAACAGAAACGAAGAGAAGAGAATGAGAAGCAGAAGCAAAGGAAGAGACCATG\ + AAAATGTCAAGGAAAAAGAAAAAACAGTCCGATAGCAAAGGCAAAGACCAGGAGCGGTCTCGGTCGAAGGAAAAATCTAAACAACTTGAATCAAAA\ + TCTAACGAGCATGGTAAGTTCGCGAGACACTAAGTTGATTCTTAGTGTTTAGACGTGAAACTCCCTTGGAAGGTTTAACGAATACTGTTAATATTT\ + TCAGATCACTCAAAATCCAAAAGAACCGACGGGCACAATCCCGGAGCCGTGAATGTGATATAACCAAGGAAGCACAGTTGCAATTCGAGAACAAGA\ + GAAAGAAGCAGAAGTAGAGAGATCGCTCGAGAAGAGTGAGAAGCAGAACACATGATAGAGACAGAAGCCGGTCGAAAGAATACCACCGCTACAGAG\ + AACAAGGTAAGCATGACTACTTGAGTGTAAATACGTTGTGATAGAGATGAAAAACAAAACCGAACATTACTTTGGGTAATAATTAACTTTTTTTTA\ + ATAGAATATCGGGAGAAAGGAAGGTCGAGAAGCAGAGAAAGAAGGACGCCTCAGGAAGAAGCCGTTCGAAAGACAGAAGGAGAAGGAGAAGAGATT\ + CGAAAGTTCAGAGCGTGAAGAGTCTCAATCGCGTAATAAAGACAAGTACGGGAACCAAGAAAGTAAAAGTTCCCACAGGAAGAACTCTGAAGAGCG\ + AGAAAAGTAAAAAAGGGTTTCCTGTTTTTTGCCTATTTTGGGTAAAGGGGTTGATGGAGAAACAGGTGTGTGGACTGCTGAGGAGTGAGTTAGAAT\ + AAATGGTGGTATCACTTCTTCAATGCTACTACAATGGAACAACAGTCGTTACCTGTTTTAAGTTCGTGGCGTCTTATGCTCCGGACAGGGACAGAT\ + AGGCGGTTGACAGAGAGTTAAGATCTAGTACACTGGGTTTCCTAAATGTAAGAATTGGCCCGAATCCGGCCTAATATGCGAACTTTGTGCTACCAA\ + GCGAGCGGGAAGCTAAGGGTGGGGAATTGCGGGTTTAATGGACCATCTCATGAGTCTAGCAGTTAATGTATCCTATCTTCCAAACAGGAATGTATT\ + CGAAAGAGTAGAGACCATAATTCGTCTAACAACTCAAGGAAAAGAAGGCGGAGTAGAGCCGATTCCGAACCCTTTGCTAGGACTAGATAGCACGTG\ + AACCTAGACTGTCTCTGAGACTGCGCCATTACGTCTCGATCAGTAACGATTGCATCGCGAGGCTGTGGATGTAAAACCTCTGCTGACCTTGACTGA\ + CTGAGATACAATGCCTTCAGCAATGCGTGGCAG"; + let s2 = "\ + GTAAGGGTTTCCTGTTTTTTGCCTATTTTGGGTAAAGGGGGGTTGATGGAGAAACAGGTGTGTGGACTGCTGAGGAGTGAGTTAGAATAAATGGTG\ + GTATCACTTCTTCAATGCTACAATGGAACAACAGTCGTTACCTGTTTTAAGTTCGTGGCGTCTTATGCTCCGGACAGGGACAGATAGGCGGTTAGA\ + CAGAGAGTTAAGATCTAGTACACTGGGTTTCCTAAATGTAAAAATTGGCCCGAATCCGGCCTAATATGCGAACTTTGTGCTACCAAGCGAGCGGGA\ + AGCTAAGGGTGGGGAGTGCGGGTTTAATGGACCATCTCGCAGGTCTAGCAGTTAATGTATCCTATCTTCCAAACAG"; + + let expected = 0.975274725; + + let score = partial_ratio(s1.chars(), s2.chars()); + assert_delta!(Some(expected), Some(score)); + + let score = partial_ratio(s2.chars(), s1.chars()); + assert_delta!(Some(expected), Some(score)); + } + #[test] fn test_partial_ratio_issue257() { let s1 = "aaaaaaaaaaaaaaaaaaaaaaaabacaaaaaaaabaaabaaaaaaaababbbbbbbbbbabbcb"; @@ -714,11 +764,9 @@ mod tests { let expected = 0.9846153846153847; let score = partial_ratio(s1.chars(), s2.chars()); - assert_delta!(Some(expected), Some(score)); let score = partial_ratio(s2.chars(), s1.chars()); - assert_delta!(Some(expected), Some(score)); } From 7f8a332d3c0db86e878a9292b38a8b81b89ff237 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Wed, 21 Jan 2026 19:01:34 +0100 Subject: [PATCH 27/29] fix: typo in test name --- src/fuzz.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 222da66..768d0cc 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -707,7 +707,7 @@ mod tests { } #[test] - fn test_partian_ratio_issue219() { + fn test_partial_ratio_issue219() { let s1 = "\ TTAGCGCTACCGGTCGCCACCATGGTTTTCTAAGGGGAGGCCGTCATCAAAAGAGTTCATGTAGCACGAAGTCCACCTTTGAAGGATCGATGAATG\ GCCATGAATTCGAAATCGAGGGGAGGGCGAGAGAGGGCCGGCCTTACGAGGGCACACCCAAACTGCCAAACTGAAAGTGACCAAAGGCGGCCCGTT\ From e06bd33380eb1b0e472c4ea98e338f6c9a1ad2a4 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Wed, 21 Jan 2026 19:01:34 +0100 Subject: [PATCH 28/29] implement optimized partial ratio for short strings --- src/fuzz.rs | 104 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 82 insertions(+), 22 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index 768d0cc..f2d4015 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -3,6 +3,7 @@ use crate::details::distance::MetricUsize; use crate::distance::common::ScoreAlignment; use crate::distance::indel; use crate::{Hash, HashableChar}; +use std::cmp; use std::collections::HashSet; #[must_use] @@ -328,6 +329,9 @@ where dest_end: len1, }; + let len_sum = 2 * len1; + let mut cutoff_dist = (len_sum as f64 * (1.0 - score_cutoff)).ceil() as usize; + for i in 1..len1 { let substr_last = &s2_vec[i - 1]; if !s1_char_set.contains(&substr_last.hash_char()) { @@ -345,6 +349,7 @@ where .unwrap_or(0.0); if ls_ratio > res.score { score_cutoff = ls_ratio; + cutoff_dist = (len_sum as f64 * (1.0 - score_cutoff)).ceil() as usize; res.score = ls_ratio; res.dest_start = 0; res.dest_end = i; @@ -355,33 +360,88 @@ where } let window_end = len2 - len1; - for i in 0..window_end { - let substr_last = &s2_vec[i + len1 - 1]; - if !s1_char_set.contains(&substr_last.hash_char()) { - continue; - } + let mut scores = vec![usize::MAX; window_end + 1]; + let mut windows = vec![(0, window_end)]; + let mut new_windows = Vec::new(); + let mut best_dist = (len_sum as f64 * (1.0 - res.score)).ceil() as usize; + + while !windows.is_empty() { + for (start, end) in windows.drain(..) { + if scores[start] == usize::MAX { + let subseq = &s2_vec[start..start + len1]; + let dist = comparator + .distance_with_args( + subseq.iter().cloned(), + &indel::Args::default().score_cutoff(cutoff_dist), + ) + .unwrap_or(usize::MAX); + scores[start] = dist; + + if dist <= cutoff_dist && dist < best_dist { + best_dist = dist; + cutoff_dist = dist; + res.score = 1.0 - (dist as f64 / len_sum as f64); + res.dest_start = start; + res.dest_end = start + len1; + score_cutoff = res.score; + if dist == 0 { + return res; + } + } + } - let ls_ratio = comparator - .normalized_similarity_with_args( - s2_vec[i..i + len1].iter().cloned(), - &indel::Args { - score_cutoff: WithScoreCutoff(score_cutoff), - score_hint, - }, - ) - .unwrap_or(0.0); - if ls_ratio > res.score { - score_cutoff = ls_ratio; - res.score = ls_ratio; - res.dest_start = i; - res.dest_end = i + len1; - if res.score == 1.0 { - return res; + if scores[end] == usize::MAX { + let subseq = &s2_vec[end..end + len1]; + let dist = comparator + .distance_with_args( + subseq.iter().cloned(), + &indel::Args::default().score_cutoff(cutoff_dist), + ) + .unwrap_or(usize::MAX); + scores[end] = dist; + + if dist <= cutoff_dist && dist < best_dist { + best_dist = dist; + cutoff_dist = dist; + res.score = 1.0 - (dist as f64 / len_sum as f64); + res.dest_start = end; + res.dest_end = end + len1; + score_cutoff = res.score; + if dist == 0 { + return res; + } + } + } + + let cell_diff = end - start; + if cell_diff <= 1 { + continue; + } + + let score_start = scores[start]; + let score_end = scores[end]; + + let min_val = cmp::min(score_start, score_end); + let min_score_is_promising = if score_start == usize::MAX || score_end == usize::MAX { + true + } else { + let known_edits = score_start.abs_diff(score_end); + + // half of the cells that are not needed for known_edits can lead to a better score + let max_score_improvement = (cell_diff - known_edits / 2) / 2 * 2; + min_val <= cutoff_dist + max_score_improvement + }; + + if min_score_is_promising { + let center = cell_diff / 2; + new_windows.push((start, start + center)); + new_windows.push((start + center, end)); } } + std::mem::swap(&mut windows, &mut new_windows); } - for i in window_end..len2 { + for i in window_end + 1..len2 { let substr_first = &s2_vec[i]; if !s1_char_set.contains(&substr_first.hash_char()) { continue; From 81a7df99ca033c3340ffce219df8dbcdae61adb6 Mon Sep 17 00:00:00 2001 From: ljnsn Date: Wed, 21 Jan 2026 19:01:34 +0100 Subject: [PATCH 29/29] fix: simplify and calculate full distance for windows --- src/fuzz.rs | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/fuzz.rs b/src/fuzz.rs index f2d4015..fef9bc1 100644 --- a/src/fuzz.rs +++ b/src/fuzz.rs @@ -369,15 +369,10 @@ where for (start, end) in windows.drain(..) { if scores[start] == usize::MAX { let subseq = &s2_vec[start..start + len1]; - let dist = comparator - .distance_with_args( - subseq.iter().cloned(), - &indel::Args::default().score_cutoff(cutoff_dist), - ) - .unwrap_or(usize::MAX); + let dist = comparator.distance(subseq.iter().cloned()); scores[start] = dist; - if dist <= cutoff_dist && dist < best_dist { + if dist < best_dist { best_dist = dist; cutoff_dist = dist; res.score = 1.0 - (dist as f64 / len_sum as f64); @@ -392,15 +387,10 @@ where if scores[end] == usize::MAX { let subseq = &s2_vec[end..end + len1]; - let dist = comparator - .distance_with_args( - subseq.iter().cloned(), - &indel::Args::default().score_cutoff(cutoff_dist), - ) - .unwrap_or(usize::MAX); + let dist = comparator.distance(subseq.iter().cloned()); scores[end] = dist; - if dist <= cutoff_dist && dist < best_dist { + if dist < best_dist { best_dist = dist; cutoff_dist = dist; res.score = 1.0 - (dist as f64 / len_sum as f64); @@ -422,21 +412,17 @@ where let score_end = scores[end]; let min_val = cmp::min(score_start, score_end); - let min_score_is_promising = if score_start == usize::MAX || score_end == usize::MAX { - true - } else { - let known_edits = score_start.abs_diff(score_end); - - // half of the cells that are not needed for known_edits can lead to a better score - let max_score_improvement = (cell_diff - known_edits / 2) / 2 * 2; - min_val <= cutoff_dist + max_score_improvement - }; + let known_edits = score_start.abs_diff(score_end); - if min_score_is_promising { - let center = cell_diff / 2; - new_windows.push((start, start + center)); - new_windows.push((start + center, end)); + // half of the cells that are not needed for known_edits can lead to a better score + let max_score_improvement = (cell_diff.saturating_sub(known_edits / 2) / 2) * 2; + if min_val > cutoff_dist.saturating_add(max_score_improvement) { + continue; } + + let center = cell_diff / 2; + new_windows.push((start, start + center)); + new_windows.push((start + center, end)); } std::mem::swap(&mut windows, &mut new_windows); }