oxibrain_core/
interval.rs1use oxibrain_ports::Timestamp;
5
6#[derive(Debug, Clone, Copy, Eq, PartialEq)]
7pub struct Interval {
8 pub start: Timestamp,
9 pub end: Timestamp,
10}
11
12impl Interval {
13 pub fn new(start: Timestamp, end: Timestamp) -> Self {
14 debug_assert!(start <= end, "interval start must be <= end");
15 Self { start, end }
16 }
17
18 pub fn contains(&self, t: Timestamp) -> bool {
20 self.start <= t && t <= self.end
21 }
22}
23
24pub fn overlaps(a: &Interval, b: &Interval) -> bool {
26 a.start <= b.end && b.start <= a.end
27}
28
29pub fn merge_overlapping(intervals: &mut Vec<Interval>) {
32 if intervals.len() <= 1 {
33 return;
34 }
35 intervals.sort_by_key(|iv| iv.start);
36 let mut merged: Vec<Interval> = Vec::with_capacity(intervals.len());
37 merged.push(intervals[0]);
38 for &iv in &intervals[1..] {
39 let last = merged.last_mut().expect("non-empty");
40 if iv.start <= last.end {
41 if iv.end > last.end {
43 last.end = iv.end;
44 }
45 } else {
46 merged.push(iv);
47 }
48 }
49 *intervals = merged;
50}
51
52pub fn clip(affirming: &[Interval], denial: &Interval) -> Vec<Interval> {
56 let mut result: Vec<Interval> = Vec::new();
57 for aff in affirming {
58 if !overlaps(aff, denial) {
59 result.push(*aff);
61 continue;
62 }
63 if aff.start < denial.start {
65 result.push(Interval::new(
66 aff.start,
67 Timestamp(denial.start.millis() - 1),
68 ));
69 }
70 if denial.end < aff.end {
71 result.push(Interval::new(Timestamp(denial.end.millis() + 1), aff.end));
72 }
73 }
75 merge_overlapping(&mut result);
78 result
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use proptest::prelude::*;
85
86 fn iv(s: i64, e: i64) -> Interval {
87 Interval::new(Timestamp(s), Timestamp(e))
88 }
89
90 #[test]
91 fn merge_disjoint_unchanged() {
92 let mut v = vec![iv(1, 5), iv(10, 15)];
93 merge_overlapping(&mut v);
94 assert_eq!(v, vec![iv(1, 5), iv(10, 15)]);
95 }
96
97 #[test]
98 fn merge_overlapping_test() {
99 let mut v = vec![iv(1, 5), iv(3, 10)];
100 merge_overlapping(&mut v);
101 assert_eq!(v, vec![iv(1, 10)]);
102 }
103
104 #[test]
105 fn merge_adjacent() {
106 let mut v = vec![iv(1, 5), iv(6, 10)];
111 merge_overlapping(&mut v);
112 assert_eq!(v.len(), 2); }
114
115 #[test]
116 fn merge_touching() {
117 let mut v = vec![iv(1, 5), iv(5, 10)];
119 merge_overlapping(&mut v);
120 assert_eq!(v, vec![iv(1, 10)]);
121 }
122
123 #[test]
124 fn clip_no_overlap() {
125 let aff = vec![iv(1, 10)];
126 let result = clip(&aff, &iv(20, 30));
127 assert_eq!(result, vec![iv(1, 10)]);
128 }
129
130 #[test]
131 fn clip_full_cover() {
132 let aff = vec![iv(5, 10)];
133 let result = clip(&aff, &iv(1, 20));
134 assert!(result.is_empty());
135 }
136
137 #[test]
138 fn clip_partial_left() {
139 let aff = vec![iv(1, 10)];
140 let result = clip(&aff, &iv(1, 5));
141 assert_eq!(result, vec![iv(6, 10)]);
142 }
143
144 #[test]
145 fn clip_partial_right() {
146 let aff = vec![iv(1, 10)];
147 let result = clip(&aff, &iv(7, 15));
148 assert_eq!(result, vec![iv(1, 6)]);
149 }
150
151 #[test]
152 fn clip_middle() {
153 let aff = vec![iv(1, 20)];
154 let result = clip(&aff, &iv(8, 12));
155 assert_eq!(result, vec![iv(1, 7), iv(13, 20)]);
156 }
157
158 #[test]
159 fn overlaps_symmetric() {
160 let a = iv(1, 5);
161 let b = iv(3, 10);
162 assert!(overlaps(&a, &b));
163 assert!(overlaps(&b, &a));
164 }
165
166 proptest! {
167 #[test]
168 fn merge_output_is_disjoint(starts in 1i64..100, lens in 1i64..50, count in 2usize..10) {
169 let mut v: Vec<Interval> = (0..count)
171 .map(|i| iv(starts + i as i64 * lens, starts + i as i64 * lens + lens))
172 .collect();
173 merge_overlapping(&mut v);
174 for w in v.windows(2) {
175 prop_assert!(w[0].end < w[1].start, "intervals must be disjoint after merge");
176 }
177 }
178
179 #[test]
180 fn clip_is_subset(aff_start in 1i64..50, aff_len in 1i64..50, d_start in 1i64..100, d_len in 1i64..50) {
181 let aff = vec![iv(aff_start, aff_start + aff_len)];
182 let denial = iv(d_start, d_start + d_len);
183 let clipped = clip(&aff, &denial);
184 for c in &clipped {
186 prop_assert!(c.start >= aff[0].start);
187 prop_assert!(c.end <= aff[0].end);
188 prop_assert!(!overlaps(c, &denial) || c.start == c.end,
189 "clipped interval must not overlap denial");
190 }
191 }
192 }
193}