solverforge_scoring/stream/joiner/
overlapping.rs

1//! Overlapping joiner for interval overlap detection.
2
3use std::marker::PhantomData;
4
5use super::Joiner;
6
7/// Creates a joiner that matches when two intervals overlap.
8///
9/// Two intervals [start_a, end_a) and [start_b, end_b) overlap if:
10/// - start_a < end_b AND start_b < end_a
11///
12/// This uses half-open intervals: the start is inclusive, the end is exclusive.
13///
14/// # Example
15///
16/// ```
17/// use solverforge_scoring::stream::joiner::{Joiner, overlapping};
18///
19/// #[derive(Clone)]
20/// struct Shift { start: i64, end: i64 }
21///
22/// let overlap = overlapping(
23///     |s: &Shift| s.start,
24///     |s: &Shift| s.end,
25///     |s: &Shift| s.start,
26///     |s: &Shift| s.end
27/// );
28///
29/// // Overlapping shifts: [0, 10) and [5, 15) overlap at [5, 10)
30/// assert!(overlap.matches(
31///     &Shift { start: 0, end: 10 },
32///     &Shift { start: 5, end: 15 }
33/// ));
34///
35/// // Non-overlapping shifts: [0, 10) and [10, 20) touch but don't overlap
36/// assert!(!overlap.matches(
37///     &Shift { start: 0, end: 10 },
38///     &Shift { start: 10, end: 20 }
39/// ));
40///
41/// // Non-overlapping shifts: [0, 5) and [10, 15) are disjoint
42/// assert!(!overlap.matches(
43///     &Shift { start: 0, end: 5 },
44///     &Shift { start: 10, end: 15 }
45/// ));
46/// ```
47pub fn overlapping<A, B, T, Fsa, Fea, Fsb, Feb>(
48    start_a: Fsa,
49    end_a: Fea,
50    start_b: Fsb,
51    end_b: Feb,
52) -> OverlappingJoiner<Fsa, Fea, Fsb, Feb, T>
53where
54    T: Ord,
55    Fsa: Fn(&A) -> T + Send + Sync,
56    Fea: Fn(&A) -> T + Send + Sync,
57    Fsb: Fn(&B) -> T + Send + Sync,
58    Feb: Fn(&B) -> T + Send + Sync,
59{
60    OverlappingJoiner {
61        start_a,
62        end_a,
63        start_b,
64        end_b,
65        _phantom: PhantomData,
66    }
67}
68
69/// A joiner that matches when two intervals overlap.
70///
71/// Created by the [`overlapping()`] function.
72pub struct OverlappingJoiner<Fsa, Fea, Fsb, Feb, T> {
73    start_a: Fsa,
74    end_a: Fea,
75    start_b: Fsb,
76    end_b: Feb,
77    _phantom: PhantomData<fn() -> T>,
78}
79
80impl<A, B, T, Fsa, Fea, Fsb, Feb> Joiner<A, B> for OverlappingJoiner<Fsa, Fea, Fsb, Feb, T>
81where
82    T: Ord,
83    Fsa: Fn(&A) -> T + Send + Sync,
84    Fea: Fn(&A) -> T + Send + Sync,
85    Fsb: Fn(&B) -> T + Send + Sync,
86    Feb: Fn(&B) -> T + Send + Sync,
87{
88    #[inline]
89    fn matches(&self, a: &A, b: &B) -> bool {
90        let start_a = (self.start_a)(a);
91        let end_a = (self.end_a)(a);
92        let start_b = (self.start_b)(b);
93        let end_b = (self.end_b)(b);
94
95        // Half-open interval overlap: [start_a, end_a) ∩ [start_b, end_b) ≠ ∅
96        // iff start_a < end_b AND start_b < end_a
97        start_a < end_b && start_b < end_a
98    }
99}