use super::algorithm::Algorithm;
use std::cmp::Ordering;
const EPSILON: f64 = 1e-9;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PositionF64 {
pub term_index: usize,
pub accumulated_cost: f64,
pub is_special: bool,
}
impl PositionF64 {
#[inline(always)]
pub fn new(term_index: usize, accumulated_cost: f64) -> Self {
debug_assert!(
accumulated_cost >= 0.0,
"Accumulated cost must be non-negative"
);
Self {
term_index,
accumulated_cost,
is_special: false,
}
}
#[inline(always)]
pub fn new_special(term_index: usize, accumulated_cost: f64) -> Self {
debug_assert!(
accumulated_cost >= 0.0,
"Accumulated cost must be non-negative"
);
Self {
term_index,
accumulated_cost,
is_special: true,
}
}
#[inline(always)]
pub fn initial() -> Self {
Self::new(0, 0.0)
}
pub fn subsumes(&self, other: &PositionF64, algorithm: Algorithm, query_length: usize) -> bool {
let i = self.term_index;
let e = self.accumulated_cost;
let s = self.is_special;
let j = other.term_index;
let f = other.accumulated_cost;
let t = other.is_special;
if e > f + EPSILON {
return false;
}
let cost_slack = f - e;
match algorithm {
Algorithm::Standard => {
let index_diff = i.abs_diff(j) as f64;
index_diff <= cost_slack + EPSILON
}
Algorithm::Transposition => {
if s {
if t {
return i == j;
}
let f_as_usize = f.floor() as usize;
return (f_as_usize == query_length) && (i == j);
}
if t {
if !s {
return false;
}
let adjusted_diff = if j < i {
(i.saturating_sub(j).saturating_sub(1)) as f64
} else {
(j.saturating_sub(i) + 1) as f64
};
return adjusted_diff <= cost_slack + EPSILON;
}
let index_diff = i.abs_diff(j) as f64;
index_diff <= cost_slack + EPSILON
}
Algorithm::MergeAndSplit => {
if s != t {
return false;
}
if i > query_length {
return false;
}
if s && i >= query_length && j < query_length {
return false;
}
if e >= f - EPSILON {
return false;
}
i == j
}
}
}
pub fn compare(&self, other: &PositionF64) -> Ordering {
self.term_index
.cmp(&other.term_index)
.then_with(|| self.accumulated_cost.total_cmp(&other.accumulated_cost))
.then_with(|| self.is_special.cmp(&other.is_special))
}
pub fn approx_eq(&self, other: &PositionF64) -> bool {
self.term_index == other.term_index
&& (self.accumulated_cost - other.accumulated_cost).abs() < EPSILON
&& self.is_special == other.is_special
}
}
impl PartialOrd for PositionF64 {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.compare(other))
}
}
impl Ord for PositionF64 {
fn cmp(&self, other: &Self) -> Ordering {
self.compare(other)
}
}
impl Eq for PositionF64 {}
impl std::hash::Hash for PositionF64 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.term_index.hash(state);
self.accumulated_cost.to_bits().hash(state);
self.is_special.hash(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_EPSILON: f64 = 1e-10;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < TEST_EPSILON
}
#[test]
fn test_position_creation() {
let pos = PositionF64::new(5, 2.5);
assert_eq!(pos.term_index, 5);
assert!(approx_eq(pos.accumulated_cost, 2.5));
assert!(!pos.is_special);
}
#[test]
fn test_position_special() {
let pos = PositionF64::new_special(3, 1.5);
assert!(pos.is_special);
assert_eq!(pos.term_index, 3);
assert!(approx_eq(pos.accumulated_cost, 1.5));
}
#[test]
fn test_position_initial() {
let pos = PositionF64::initial();
assert_eq!(pos.term_index, 0);
assert!(approx_eq(pos.accumulated_cost, 0.0));
assert!(!pos.is_special);
}
#[test]
fn test_subsumption_standard_basic() {
let max_distance = 5;
let p1 = PositionF64::new(5, 2.0);
let p2 = PositionF64::new(5, 3.0);
assert!(
p1.subsumes(&p2, Algorithm::Standard, max_distance),
"p1(5, 2.0) should subsume p2(5, 3.0)"
);
let p3 = PositionF64::new(5, 2.0);
let p4 = PositionF64::new(4, 3.0);
assert!(
p3.subsumes(&p4, Algorithm::Standard, max_distance),
"p3(5, 2.0) should subsume p4(4, 3.0)"
);
}
#[test]
fn test_subsumption_standard_float_costs() {
let max_distance = 5;
let p1 = PositionF64::new(3, 1.5);
let p2 = PositionF64::new(4, 2.6);
assert!(
p1.subsumes(&p2, Algorithm::Standard, max_distance),
"p1(3, 1.5) should subsume p2(4, 2.6)"
);
let p3 = PositionF64::new(3, 1.5);
let p4 = PositionF64::new(5, 2.3);
assert!(
!p3.subsumes(&p4, Algorithm::Standard, max_distance),
"p3(3, 1.5) should NOT subsume p4(5, 2.3)"
);
}
#[test]
fn test_subsumption_cannot_subsume_lower_cost() {
let max_distance = 5;
let p1 = PositionF64::new(5, 3.0);
let p2 = PositionF64::new(5, 2.0);
assert!(
!p1.subsumes(&p2, Algorithm::Standard, max_distance),
"Higher cost position cannot subsume lower cost position"
);
}
#[test]
fn test_subsumption_transposition_special() {
let max_distance = 5;
let p1 = PositionF64::new_special(5, 2.0);
let p2 = PositionF64::new_special(5, 3.0);
assert!(
p1.subsumes(&p2, Algorithm::Transposition, max_distance),
"special(5, 2.0) should subsume special(5, 3.0)"
);
let p3 = PositionF64::new_special(5, 2.0);
let p4 = PositionF64::new_special(6, 3.0);
assert!(
!p3.subsumes(&p4, Algorithm::Transposition, max_distance),
"special(5, 2.0) should NOT subsume special(6, 3.0)"
);
let p5 = PositionF64::new(5, 2.0);
let p6 = PositionF64::new_special(4, 3.0);
assert!(
!p5.subsumes(&p6, Algorithm::Transposition, max_distance),
"normal cannot subsume special in transposition"
);
}
#[test]
fn test_subsumption_merge_split() {
let query_length = 5;
let p1 = PositionF64::new_special(5, 2.0);
let p2 = PositionF64::new(5, 3.0);
assert!(
!p1.subsumes(&p2, Algorithm::MergeAndSplit, query_length),
"special cannot subsume non-special in merge-split"
);
let p2a = PositionF64::new(5, 2.0);
let p2b = PositionF64::new_special(4, 3.0);
assert!(
!p2a.subsumes(&p2b, Algorithm::MergeAndSplit, query_length),
"normal cannot subsume special in merge-split"
);
let p2c = PositionF64::new_special(5, 1.0);
let p2d = PositionF64::new_special(4, 2.0);
assert!(
!p2c.subsumes(&p2d, Algorithm::MergeAndSplit, query_length),
"final special cannot subsume non-final special in merge-split"
);
let p3 = PositionF64::new(5, 2.0);
let p4 = PositionF64::new(4, 3.0);
assert!(
!p3.subsumes(&p4, Algorithm::MergeAndSplit, query_length),
"normal(5, 2.0) should NOT subsume normal(4, 3.0)"
);
let p5 = PositionF64::new_special(5, 2.0);
let p6 = PositionF64::new_special(5, 3.0);
assert!(
p5.subsumes(&p6, Algorithm::MergeAndSplit, query_length),
"special(5, 2.0) should subsume special(5, 3.0)"
);
}
#[test]
fn test_position_ordering() {
let p1 = PositionF64::new(3, 1.0);
let p2 = PositionF64::new(3, 2.0);
let p3 = PositionF64::new(4, 1.0);
assert!(p1 < p2); assert!(p1 < p3); assert!(p2 < p3); }
#[test]
fn test_approx_eq() {
let p1 = PositionF64::new(3, 1.5);
let p2 = PositionF64::new(3, 1.5 + 1e-12); let p3 = PositionF64::new(3, 1.6);
assert!(p1.approx_eq(&p2));
assert!(!p1.approx_eq(&p3));
}
}