Skip to main content

uni_btic/
set_ops.rs

1use std::cmp::Ordering;
2
3use crate::btic::{Btic, NEG_INF, POS_INF};
4use crate::certainty::Certainty;
5use crate::granularity::Granularity;
6
7/// Compute the intersection of two intervals.
8///
9/// Returns `[max(a.lo, b.lo), min(a.hi, b.hi))`, or `None` if the intervals
10/// are disjoint (the result would be empty).
11///
12/// Granularity and certainty are inherited per spec §14.3:
13/// - Each result bound inherits metadata from whichever input provided that bound.
14/// - When both inputs have equal bound values, the finer granularity and least
15///   certain certainty are used.
16pub fn intersection(a: &Btic, b: &Btic) -> Option<Btic> {
17    let lo = a.lo().max(b.lo());
18    let hi = a.hi().min(b.hi());
19
20    // Disjoint: no intersection
21    if lo >= hi {
22        return None;
23    }
24
25    let (lo_gran, lo_cert) = pick_bound_meta(a, b, BoundSide::Lo, Ordering::Greater);
26    let (hi_gran, hi_cert) = pick_bound_meta(a, b, BoundSide::Hi, Ordering::Less);
27
28    let meta = build_result_meta(lo, hi, lo_gran, hi_gran, lo_cert, hi_cert);
29    Btic::new(lo, hi, meta).ok()
30}
31
32/// Compute the span (bounding interval) of two intervals.
33///
34/// Returns `[min(a.lo, b.lo), max(a.hi, b.hi))`. Always valid.
35///
36/// Granularity and certainty are inherited per spec §14.3.
37pub fn span(a: &Btic, b: &Btic) -> Btic {
38    let lo = a.lo().min(b.lo());
39    let hi = a.hi().max(b.hi());
40
41    let (lo_gran, lo_cert) = pick_bound_meta(a, b, BoundSide::Lo, Ordering::Less);
42    let (hi_gran, hi_cert) = pick_bound_meta(a, b, BoundSide::Hi, Ordering::Greater);
43
44    let meta = build_result_meta(lo, hi, lo_gran, hi_gran, lo_cert, hi_cert);
45    Btic::new(lo, hi, meta).expect("span of two valid intervals must be valid")
46}
47
48/// Compute the gap between two disjoint intervals.
49///
50/// Returns `[min(a.hi, b.hi), max(a.lo, b.lo))` if the intervals are disjoint
51/// and non-adjacent. Returns `None` if they overlap or are adjacent.
52///
53/// Granularity/certainty: The gap's lo bound comes from whichever interval's `hi`
54/// is smaller; the gap's hi bound comes from whichever interval's `lo` is larger.
55pub fn gap(a: &Btic, b: &Btic) -> Option<Btic> {
56    let gap_lo = a.hi().min(b.hi());
57    let gap_hi = a.lo().max(b.lo());
58
59    // If intervals overlap or are adjacent, there is no gap
60    if gap_lo >= gap_hi {
61        return None;
62    }
63
64    // Gap's lo comes from min(a.hi, b.hi) — the hi side of whichever interval ends first
65    let (lo_gran, lo_cert) = pick_bound_meta(a, b, BoundSide::Hi, Ordering::Less);
66    // Gap's hi comes from max(a.lo, b.lo) — the lo side of whichever interval starts later
67    let (hi_gran, hi_cert) = pick_bound_meta(a, b, BoundSide::Lo, Ordering::Greater);
68
69    let meta = build_result_meta(gap_lo, gap_hi, lo_gran, hi_gran, lo_cert, hi_cert);
70    Btic::new(gap_lo, gap_hi, meta).ok()
71}
72
73// ---------------------------------------------------------------------------
74// Internal helpers
75// ---------------------------------------------------------------------------
76
77#[derive(Clone, Copy)]
78enum BoundSide {
79    Lo,
80    Hi,
81}
82
83/// Extract (granularity, certainty) for a given bound side from a Btic.
84fn bound_meta(btic: &Btic, side: BoundSide) -> (Granularity, Certainty) {
85    match side {
86        BoundSide::Lo => (btic.lo_granularity(), btic.lo_certainty()),
87        BoundSide::Hi => (btic.hi_granularity(), btic.hi_certainty()),
88    }
89}
90
91/// Extract the raw bound value for a given side.
92fn bound_val(btic: &Btic, side: BoundSide) -> i64 {
93    match side {
94        BoundSide::Lo => btic.lo(),
95        BoundSide::Hi => btic.hi(),
96    }
97}
98
99/// Pick metadata for the result bound that comes from the extremal value.
100///
101/// When `pick` is `Greater`, selects metadata from `max(a.bound, b.bound)`.
102/// When `pick` is `Less`, selects metadata from `min(a.bound, b.bound)`.
103/// When both bounds are equal, uses the finer granularity and least certainty.
104///
105/// # Precondition
106/// `pick` must be `Greater` or `Less`; passing `Equal` is a logic error
107/// (the extremal value would be undefined) and will panic.
108fn pick_bound_meta(
109    a: &Btic,
110    b: &Btic,
111    side: BoundSide,
112    pick: std::cmp::Ordering,
113) -> (Granularity, Certainty) {
114    debug_assert_ne!(
115        pick,
116        std::cmp::Ordering::Equal,
117        "pick_bound_meta requires Greater or Less"
118    );
119
120    let va = bound_val(a, side);
121    let vb = bound_val(b, side);
122    let (ga, ca) = bound_meta(a, side);
123    let (gb, cb) = bound_meta(b, side);
124
125    match va.cmp(&vb) {
126        std::cmp::Ordering::Equal => (ga.finer(gb), ca.least_certain(cb)),
127        ord if ord == pick => (ga, ca),
128        _ => (gb, cb),
129    }
130}
131
132/// Build a meta word for a result, respecting INV-6 (sentinel bounds must have zeroed meta).
133fn build_result_meta(
134    lo: i64,
135    hi: i64,
136    lo_gran: Granularity,
137    hi_gran: Granularity,
138    lo_cert: Certainty,
139    hi_cert: Certainty,
140) -> u64 {
141    let (lg, lc) = if lo == NEG_INF {
142        (Granularity::Millisecond, Certainty::Definite)
143    } else {
144        (lo_gran, lo_cert)
145    };
146    let (hg, hc) = if hi == POS_INF {
147        (Granularity::Millisecond, Certainty::Definite)
148    } else {
149        (hi_gran, hi_cert)
150    };
151    Btic::build_meta(lg, hg, lc, hc)
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    fn make(lo: i64, hi: i64) -> Btic {
159        let meta = Btic::build_meta(
160            Granularity::Millisecond,
161            Granularity::Millisecond,
162            Certainty::Definite,
163            Certainty::Definite,
164        );
165        Btic::new(lo, hi, meta).unwrap()
166    }
167
168    fn make_with_gran(lo: i64, hi: i64, lg: Granularity, hg: Granularity) -> Btic {
169        let meta = Btic::build_meta(lg, hg, Certainty::Definite, Certainty::Definite);
170        Btic::new(lo, hi, meta).unwrap()
171    }
172
173    fn make_with_cert(lo: i64, hi: i64, lc: Certainty, hc: Certainty) -> Btic {
174        let meta = Btic::build_meta(Granularity::Millisecond, Granularity::Millisecond, lc, hc);
175        Btic::new(lo, hi, meta).unwrap()
176    }
177
178    // -- intersection --
179
180    #[test]
181    fn intersection_overlapping() {
182        let a = make(100, 300);
183        let b = make(200, 400);
184        let r = intersection(&a, &b).unwrap();
185        assert_eq!(r.lo(), 200);
186        assert_eq!(r.hi(), 300);
187    }
188
189    #[test]
190    fn intersection_contained() {
191        let a = make(100, 400);
192        let b = make(200, 300);
193        let r = intersection(&a, &b).unwrap();
194        assert_eq!(r.lo(), 200);
195        assert_eq!(r.hi(), 300);
196    }
197
198    #[test]
199    fn intersection_identical() {
200        let a = make(100, 200);
201        let r = intersection(&a, &a).unwrap();
202        assert_eq!(r.lo(), 100);
203        assert_eq!(r.hi(), 200);
204    }
205
206    #[test]
207    fn intersection_disjoint() {
208        let a = make(100, 200);
209        let b = make(300, 400);
210        assert!(intersection(&a, &b).is_none());
211    }
212
213    #[test]
214    fn intersection_adjacent_is_none() {
215        let a = make(100, 200);
216        let b = make(200, 300);
217        assert!(intersection(&a, &b).is_none()); // lo >= hi
218    }
219
220    #[test]
221    fn intersection_granularity_inherited() {
222        let a = make_with_gran(100, 300, Granularity::Month, Granularity::Year);
223        let b = make_with_gran(200, 400, Granularity::Day, Granularity::Day);
224        let r = intersection(&a, &b).unwrap();
225        // lo=200 comes from b (larger), so lo_gran = b's lo_gran = Day
226        assert_eq!(r.lo_granularity(), Granularity::Day);
227        // hi=300 comes from a (smaller), so hi_gran = a's hi_gran = Year
228        assert_eq!(r.hi_granularity(), Granularity::Year);
229    }
230
231    #[test]
232    fn intersection_equal_bounds_finer_granularity() {
233        let a = make_with_gran(100, 300, Granularity::Year, Granularity::Year);
234        let b = make_with_gran(100, 300, Granularity::Day, Granularity::Day);
235        let r = intersection(&a, &b).unwrap();
236        // Equal lo: finer = Day (lower code)
237        assert_eq!(r.lo_granularity(), Granularity::Day);
238        assert_eq!(r.hi_granularity(), Granularity::Day);
239    }
240
241    #[test]
242    fn intersection_certainty_inherited() {
243        let a = make_with_cert(100, 300, Certainty::Definite, Certainty::Approximate);
244        let b = make_with_cert(200, 400, Certainty::Uncertain, Certainty::Definite);
245        let r = intersection(&a, &b).unwrap();
246        // lo=200 from b → b's lo_cert = Uncertain
247        assert_eq!(r.lo_certainty(), Certainty::Uncertain);
248        // hi=300 from a → a's hi_cert = Approximate
249        assert_eq!(r.hi_certainty(), Certainty::Approximate);
250    }
251
252    #[test]
253    fn intersection_with_sentinel() {
254        let a = Btic::new(
255            NEG_INF,
256            300,
257            Btic::build_meta(
258                Granularity::Millisecond,
259                Granularity::Day,
260                Certainty::Definite,
261                Certainty::Definite,
262            ),
263        )
264        .unwrap();
265        let b = make(100, 400);
266        let r = intersection(&a, &b).unwrap();
267        assert_eq!(r.lo(), 100);
268        assert_eq!(r.hi(), 300);
269    }
270
271    // -- span --
272
273    #[test]
274    fn span_overlapping() {
275        let a = make(100, 300);
276        let b = make(200, 400);
277        let r = span(&a, &b);
278        assert_eq!(r.lo(), 100);
279        assert_eq!(r.hi(), 400);
280    }
281
282    #[test]
283    fn span_disjoint() {
284        let a = make(100, 200);
285        let b = make(300, 400);
286        let r = span(&a, &b);
287        assert_eq!(r.lo(), 100);
288        assert_eq!(r.hi(), 400);
289    }
290
291    #[test]
292    fn span_contained() {
293        let a = make(100, 400);
294        let b = make(200, 300);
295        let r = span(&a, &b);
296        assert_eq!(r.lo(), 100);
297        assert_eq!(r.hi(), 400);
298    }
299
300    #[test]
301    fn span_identical() {
302        let a = make(100, 200);
303        let r = span(&a, &a);
304        assert_eq!(r.lo(), 100);
305        assert_eq!(r.hi(), 200);
306    }
307
308    #[test]
309    fn span_granularity_inherited() {
310        let a = make_with_gran(100, 300, Granularity::Month, Granularity::Year);
311        let b = make_with_gran(200, 400, Granularity::Day, Granularity::Day);
312        let r = span(&a, &b);
313        // lo=100 comes from a (smaller), so lo_gran = a's lo_gran = Month
314        assert_eq!(r.lo_granularity(), Granularity::Month);
315        // hi=400 comes from b (larger), so hi_gran = b's hi_gran = Day
316        assert_eq!(r.hi_granularity(), Granularity::Day);
317    }
318
319    #[test]
320    fn span_with_sentinel() {
321        let a = Btic::new(
322            NEG_INF,
323            200,
324            Btic::build_meta(
325                Granularity::Millisecond,
326                Granularity::Day,
327                Certainty::Definite,
328                Certainty::Definite,
329            ),
330        )
331        .unwrap();
332        let b = make(100, 400);
333        let r = span(&a, &b);
334        assert_eq!(r.lo(), NEG_INF);
335        assert_eq!(r.hi(), 400);
336        // Sentinel lo: granularity/certainty must be zeroed per INV-6
337        assert_eq!(r.lo_granularity(), Granularity::Millisecond);
338        assert_eq!(r.lo_certainty(), Certainty::Definite);
339    }
340
341    // -- gap --
342
343    #[test]
344    fn gap_disjoint_with_space() {
345        let a = make(100, 200);
346        let b = make(300, 400);
347        let r = gap(&a, &b).unwrap();
348        assert_eq!(r.lo(), 200); // min(a.hi, b.hi) = 200
349        assert_eq!(r.hi(), 300); // max(a.lo, b.lo) = 300
350    }
351
352    #[test]
353    fn gap_disjoint_reversed() {
354        let a = make(300, 400);
355        let b = make(100, 200);
356        let r = gap(&a, &b).unwrap();
357        assert_eq!(r.lo(), 200);
358        assert_eq!(r.hi(), 300);
359    }
360
361    #[test]
362    fn gap_overlapping_returns_none() {
363        let a = make(100, 300);
364        let b = make(200, 400);
365        assert!(gap(&a, &b).is_none());
366    }
367
368    #[test]
369    fn gap_adjacent_returns_none() {
370        let a = make(100, 200);
371        let b = make(200, 300);
372        assert!(gap(&a, &b).is_none());
373    }
374
375    #[test]
376    fn gap_contained_returns_none() {
377        let a = make(100, 400);
378        let b = make(200, 300);
379        assert!(gap(&a, &b).is_none());
380    }
381
382    #[test]
383    fn gap_granularity_inherited() {
384        let a = make_with_gran(100, 200, Granularity::Month, Granularity::Year);
385        let b = make_with_gran(300, 400, Granularity::Day, Granularity::Day);
386        let r = gap(&a, &b).unwrap();
387        // gap lo=200 comes from min(a.hi=200, b.hi=400) = a.hi → a's hi_gran = Year
388        assert_eq!(r.lo_granularity(), Granularity::Year);
389        // gap hi=300 comes from max(a.lo=100, b.lo=300) = b.lo → b's lo_gran = Day
390        assert_eq!(r.hi_granularity(), Granularity::Day);
391    }
392}