Skip to main content

oxibrain_core/
interval.rs

1//! Interval algebra for the temporal fold (DESIGN §6). All comparisons are plain
2//! integer comparisons on sentinels — no NULL branching (§6.2).
3
4use 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    /// True if this interval covers the given point.
19    pub fn contains(&self, t: Timestamp) -> bool {
20        self.start <= t && t <= self.end
21    }
22}
23
24/// True if two intervals share any point.
25pub fn overlaps(a: &Interval, b: &Interval) -> bool {
26    a.start <= b.end && b.start <= a.end
27}
28
29/// Merge overlapping or adjacent intervals into disjoint, sorted output.
30/// Input is consumed and replaced. Result is sorted by start, disjoint.
31pub 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            // Overlapping or adjacent — extend.
42            if iv.end > last.end {
43                last.end = iv.end;
44            }
45        } else {
46            merged.push(iv);
47        }
48    }
49    *intervals = merged;
50}
51
52/// Subtract a denial interval from affirming intervals.
53/// Returns the pieces of the affirming intervals that remain after removing
54/// the denial's coverage. Result is sorted and disjoint.
55pub 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            // No overlap — keep the whole affirming interval.
60            result.push(*aff);
61            continue;
62        }
63        // Overlap: split into [aff.start, denial.start) and (denial.end, aff.end].
64        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        // If denial fully covers affirming, nothing is kept.
74    }
75    // Result is already sorted because affirming was sorted,
76    // but clip may create pieces out of order — re-sort and merge.
77    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        // Adjacent (5 and 6) should merge since 6 <= 5 is false but 6 <= 5+1...
107        // Actually: merge condition is iv.start <= last.end. 6 <= 5 is false.
108        // So adjacent-but-not-overlapping intervals do NOT merge.
109        // This is correct: [1,5] and [6,10] are disjoint.
110        let mut v = vec![iv(1, 5), iv(6, 10)];
111        merge_overlapping(&mut v);
112        assert_eq!(v.len(), 2); // NOT merged
113    }
114
115    #[test]
116    fn merge_touching() {
117        // Touching: [1,5] and [5,10] — share point 5 → merge.
118        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            // Generate random intervals, merge, check disjoint.
170            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            // Every point in clipped must be in aff but not in denial.
185            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}