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
9Two intervals [start_a, end_a) and [start_b, end_b) overlap if:
10- start_a < end_b AND start_b < end_a
11
12This uses half-open intervals: the start is inclusive, the end is exclusive.
13
14# Example
15
16```
17use solverforge_scoring::stream::joiner::{Joiner, overlapping};
18
19#[derive(Clone)]
20struct Shift { start: i64, end: i64 }
21
22let 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)
30assert!(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
36assert!(!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
42assert!(!overlap.matches(
43&Shift { start: 0, end: 5 },
44&Shift { start: 10, end: 15 }
45));
46```
47*/
48pub fn overlapping<T, Fsa, Fea, Fsb, Feb>(
49 start_a: Fsa,
50 end_a: Fea,
51 start_b: Fsb,
52 end_b: Feb,
53) -> OverlappingJoiner<Fsa, Fea, Fsb, Feb, T>
54where
55 T: Ord,
56 Fsa: Send + Sync,
57 Fea: Send + Sync,
58 Fsb: Send + Sync,
59 Feb: Send + Sync,
60{
61 OverlappingJoiner {
62 start_a,
63 end_a,
64 start_b,
65 end_b,
66 _phantom: PhantomData,
67 }
68}
69
70/* Creates a joiner matching when the left row's interval overlaps the right's.
71
72Row-aware form of [`overlapping`]: start/end closures for the left side
73receive the whole borrowed row; the right side sees only the new entity.
74Half-open endpoint semantics are unchanged: `[s, e)` overlaps iff
75`start_row < end_c AND start_c < end_row`. Bound extractors stay
76accessible for interval candidate planning.
77*/
78pub fn overlap_on<T, LS, LE, CS, CE>(
79 start_row: LS,
80 end_row: LE,
81 start_c: CS,
82 end_c: CE,
83) -> OverlappingJoiner<LS, LE, CS, CE, T>
84where
85 T: Ord,
86 LS: Send + Sync,
87 LE: Send + Sync,
88 CS: Send + Sync,
89 CE: Send + Sync,
90{
91 OverlappingJoiner {
92 start_a: start_row,
93 end_a: end_row,
94 start_b: start_c,
95 end_b: end_c,
96 _phantom: PhantomData,
97 }
98}
99
100/* A joiner that matches when two intervals overlap.
101
102Created by the [`overlapping()`] function.
103*/
104pub struct OverlappingJoiner<Fsa, Fea, Fsb, Feb, T> {
105 pub(super) start_a: Fsa,
106 pub(super) end_a: Fea,
107 pub(super) start_b: Fsb,
108 pub(super) end_b: Feb,
109 _phantom: PhantomData<fn() -> T>,
110}
111
112impl<Fsa, Fea, Fsb, Feb, T> OverlappingJoiner<Fsa, Fea, Fsb, Feb, T> {
113 /// Compose without prematurely binding a borrowed row lifetime.
114 pub fn and<J>(self, other: J) -> super::AndJoiner<Self, J> {
115 super::AndJoiner {
116 first: self,
117 second: other,
118 }
119 }
120 /* Consumes the joiner and returns the bound extractors for index planning. */
121 #[inline]
122 pub fn into_bounds(self) -> (Fsa, Fea, Fsb, Feb) {
123 (self.start_a, self.end_a, self.start_b, self.end_b)
124 }
125}
126
127impl<A, B, T, Fsa, Fea, Fsb, Feb> Joiner<A, B> for OverlappingJoiner<Fsa, Fea, Fsb, Feb, T>
128where
129 T: Ord,
130 Fsa: Fn(&A) -> T + Send + Sync,
131 Fea: Fn(&A) -> T + Send + Sync,
132 Fsb: Fn(&B) -> T + Send + Sync,
133 Feb: Fn(&B) -> T + Send + Sync,
134{
135 #[inline]
136 fn matches(&self, a: &A, b: &B) -> bool {
137 let start_a = (self.start_a)(a);
138 let end_a = (self.end_a)(a);
139 let start_b = (self.start_b)(b);
140 let end_b = (self.end_b)(b);
141
142 // Half-open interval overlap: [start_a, end_a) ∩ [start_b, end_b) ≠ ∅
143 // iff start_a < end_b AND start_b < end_a
144 start_a < end_b && start_b < end_a
145 }
146}