Skip to main content

u_numflow/collections/
interval_set.rs

1//! Sets of half-open intervals on an ordered line.
2//!
3//! An [`IntervalSet`] is a finite union of half-open intervals `[start, end)`,
4//! stored in **normal form**: sorted by start, pairwise disjoint, and with no
5//! two pieces touching (`[1, 3) ∪ [3, 5)` is stored as `[1, 5)`). Every
6//! operation takes normal-form inputs and returns a normal-form result, so the
7//! measure of a set is always the plain sum of its pieces — overlapping input
8//! can never be counted twice.
9//!
10//! # Algorithm
11//!
12//! Construction sorts the pieces and merges in one sweep. Union, intersection
13//! and difference of two normal-form sets are linear two-pointer merges.
14//!
15//! # Complexity
16//!
17//! | Operation | Cost |
18//! |---|---|
19//! | [`IntervalSet::from_intervals`] | O(n log n) |
20//! | [`union`](IntervalSet::union) · [`intersection`](IntervalSet::intersection) · [`difference`](IntervalSet::difference) | O(n + m) |
21//! | [`clip`](IntervalSet::clip) · [`measure`](IntervalSet::measure) | O(n) |
22//! | [`contains`](IntervalSet::contains) | O(log n) |
23//!
24//! # References
25//!
26//! - de Berg, Cheong, van Kreveld & Overmars (2008), *Computational Geometry*,
27//!   3rd ed., §10.1 (interval sets and their sweep-based operations).
28
29use std::cmp::Ordering;
30use std::fmt;
31use std::ops::Add;
32
33/// A coordinate type an [`IntervalSet`] can be built over.
34///
35/// Implemented for `f32`, `f64` and the primitive integers. The associated
36/// [`Length`](IntervalBound::Length) is the type of a distance between two
37/// coordinates: the coordinate type itself for floats, and the unsigned type
38/// of the same width for integers — the same choice as the standard library's
39/// `i64::abs_diff`, so that the length of `[i64::MIN, i64::MAX)` is
40/// representable. Because the pieces of a set are disjoint, the total measure
41/// of any set is at most the distance between the type's extremes and never
42/// overflows `Length`.
43pub trait IntervalBound: Copy + PartialOrd + fmt::Debug {
44    /// The type of a distance between two coordinates.
45    type Length: Copy + PartialOrd + Default + Add<Output = Self::Length> + fmt::Debug;
46
47    /// Whether the value can bound an interval (false for NaN and ±∞).
48    fn is_admissible(self) -> bool;
49
50    /// The distance from `start` to `end`. Callers guarantee `start <= end`.
51    fn distance(start: Self, end: Self) -> Self::Length;
52}
53
54macro_rules! float_bound {
55    ($($t:ty),*) => {$(
56        impl IntervalBound for $t {
57            type Length = $t;
58            fn is_admissible(self) -> bool {
59                self.is_finite()
60            }
61            fn distance(start: Self, end: Self) -> Self::Length {
62                end - start
63            }
64        }
65    )*};
66}
67
68macro_rules! int_bound {
69    ($($t:ty => $len:ty),*) => {$(
70        impl IntervalBound for $t {
71            type Length = $len;
72            fn is_admissible(self) -> bool {
73                true
74            }
75            fn distance(start: Self, end: Self) -> Self::Length {
76                end.abs_diff(start)
77            }
78        }
79    )*};
80}
81
82float_bound!(f32, f64);
83int_bound!(
84    i8 => u8, i16 => u16, i32 => u32, i64 => u64, i128 => u128, isize => usize,
85    u8 => u8, u16 => u16, u32 => u32, u64 => u64, u128 => u128, usize => usize
86);
87
88/// Why an interval was refused.
89///
90/// `index` is the interval's position in the input of
91/// [`IntervalSet::from_intervals`]; operations that take a single interval
92/// ([`IntervalSet::insert`], [`IntervalSet::clip`]) report `0`.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum IntervalError {
95    /// `start > end`. A reversed interval is refused rather than swapped: the
96    /// caller's data says something it did not mean, and guessing which bound
97    /// is wrong would hide that.
98    Reversed {
99        /// Position of the interval in the input.
100        index: usize,
101    },
102    /// A bound is NaN or infinite.
103    NotFinite {
104        /// Position of the interval in the input.
105        index: usize,
106    },
107}
108
109impl fmt::Display for IntervalError {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            IntervalError::Reversed { index } => {
113                write!(f, "interval {index} has start > end")
114            }
115            IntervalError::NotFinite { index } => {
116                write!(f, "interval {index} has a NaN or infinite bound")
117            }
118        }
119    }
120}
121
122impl std::error::Error for IntervalError {}
123
124fn check<T: IntervalBound>(index: usize, start: T, end: T) -> Result<(), IntervalError> {
125    if !start.is_admissible() || !end.is_admissible() {
126        return Err(IntervalError::NotFinite { index });
127    }
128    if start > end {
129        return Err(IntervalError::Reversed { index });
130    }
131    Ok(())
132}
133
134/// Total order on admissible bounds (NaN is refused before it gets here).
135fn cmp<T: IntervalBound>(a: T, b: T) -> Ordering {
136    a.partial_cmp(&b)
137        .expect("IntervalSet bounds are admissible, so comparable")
138}
139
140fn max<T: IntervalBound>(a: T, b: T) -> T {
141    if b > a {
142        b
143    } else {
144        a
145    }
146}
147
148fn min<T: IntervalBound>(a: T, b: T) -> T {
149    if b < a {
150        b
151    } else {
152        a
153    }
154}
155
156/// A finite union of half-open intervals `[start, end)`, kept in normal form.
157///
158/// Empty intervals (`start == end`) contribute nothing and are dropped.
159///
160/// # Examples
161///
162/// Up-time of an asset in a reporting window, where planned and unplanned
163/// stops overlap:
164///
165/// ```
166/// use u_numflow::collections::IntervalSet;
167///
168/// let planned = IntervalSet::from_intervals([(0.0, 24.0)]).unwrap();
169/// let unplanned = IntervalSet::from_intervals([(10.0, 30.0)]).unwrap();
170/// let window = IntervalSet::from_intervals([(0.0, 168.0)]).unwrap();
171///
172/// let down = planned.union(&unplanned); // [0, 30): the 14 h overlap counts once
173/// assert_eq!(down.measure(), 30.0);
174/// assert_eq!(window.difference(&down).measure(), 138.0);
175/// ```
176#[derive(Debug, Clone, PartialEq)]
177pub struct IntervalSet<T: IntervalBound> {
178    pieces: Vec<(T, T)>,
179}
180
181impl<T: IntervalBound> Default for IntervalSet<T> {
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187impl<T: IntervalBound> IntervalSet<T> {
188    /// The empty set.
189    pub fn new() -> Self {
190        Self { pieces: Vec::new() }
191    }
192
193    /// Builds the union of `(start, end)` pairs, each read as `[start, end)`.
194    ///
195    /// Overlapping and touching pieces are merged; empty ones are dropped.
196    ///
197    /// # Errors
198    /// [`IntervalError::Reversed`] for a pair with `start > end`,
199    /// [`IntervalError::NotFinite`] for a NaN or infinite bound — with the
200    /// pair's position in `intervals`.
201    pub fn from_intervals<I>(intervals: I) -> Result<Self, IntervalError>
202    where
203        I: IntoIterator<Item = (T, T)>,
204    {
205        let mut pieces = Vec::new();
206        for (index, (start, end)) in intervals.into_iter().enumerate() {
207            check(index, start, end)?;
208            if start < end {
209                pieces.push((start, end));
210            }
211        }
212        pieces.sort_by(|a, b| cmp(a.0, b.0));
213        Ok(Self {
214            pieces: merge_sorted(pieces),
215        })
216    }
217
218    /// Adds `[start, end)` to the set.
219    ///
220    /// # Errors
221    /// As [`from_intervals`](Self::from_intervals), with `index` 0.
222    pub fn insert(&mut self, start: T, end: T) -> Result<(), IntervalError> {
223        check(0, start, end)?;
224        if start < end {
225            *self = self.union(&Self {
226                pieces: vec![(start, end)],
227            });
228        }
229        Ok(())
230    }
231
232    /// The normal-form pieces, in increasing order.
233    pub fn iter(&self) -> impl Iterator<Item = (T, T)> + '_ {
234        self.pieces.iter().copied()
235    }
236
237    /// The normal-form pieces as a slice.
238    pub fn as_slice(&self) -> &[(T, T)] {
239        &self.pieces
240    }
241
242    /// Number of disjoint pieces.
243    pub fn len(&self) -> usize {
244        self.pieces.len()
245    }
246
247    /// Whether the set contains no point.
248    pub fn is_empty(&self) -> bool {
249        self.pieces.is_empty()
250    }
251
252    /// Total length of the set — each point counted once.
253    pub fn measure(&self) -> T::Length {
254        self.pieces
255            .iter()
256            .fold(T::Length::default(), |acc, &(s, e)| acc + T::distance(s, e))
257    }
258
259    /// Whether `t` lies in the set (`start <= t < end` for some piece).
260    pub fn contains(&self, t: T) -> bool {
261        if !t.is_admissible() {
262            return false;
263        }
264        // First piece whose end is beyond t; t is inside iff that piece starts at or before t.
265        let i = self.pieces.partition_point(|&(_, e)| e <= t);
266        self.pieces.get(i).is_some_and(|&(s, _)| s <= t)
267    }
268
269    /// Points in either set.
270    pub fn union(&self, other: &Self) -> Self {
271        let mut all = Vec::with_capacity(self.pieces.len() + other.pieces.len());
272        let (mut i, mut j) = (0, 0);
273        while i < self.pieces.len() || j < other.pieces.len() {
274            let take_self = match (self.pieces.get(i), other.pieces.get(j)) {
275                (Some(a), Some(b)) => a.0 <= b.0,
276                (Some(_), None) => true,
277                _ => false,
278            };
279            if take_self {
280                all.push(self.pieces[i]);
281                i += 1;
282            } else {
283                all.push(other.pieces[j]);
284                j += 1;
285            }
286        }
287        Self {
288            pieces: merge_sorted(all),
289        }
290    }
291
292    /// Points in both sets.
293    pub fn intersection(&self, other: &Self) -> Self {
294        let mut out = Vec::new();
295        let (mut i, mut j) = (0, 0);
296        while let (Some(&(a0, a1)), Some(&(b0, b1))) = (self.pieces.get(i), other.pieces.get(j)) {
297            let s = max(a0, b0);
298            let e = min(a1, b1);
299            if s < e {
300                out.push((s, e));
301            }
302            if a1 < b1 {
303                i += 1;
304            } else {
305                j += 1;
306            }
307        }
308        // Pieces of each input are disjoint and non-touching, so the overlaps are too.
309        Self { pieces: out }
310    }
311
312    /// Points in `self` but not in `other`.
313    pub fn difference(&self, other: &Self) -> Self {
314        let mut out = Vec::new();
315        let mut j = 0;
316        for &(a0, a1) in &self.pieces {
317            let mut cur = a0;
318            // Skip subtrahend pieces that end at or before this piece starts.
319            while other.pieces.get(j).is_some_and(|&(_, b1)| b1 <= cur) {
320                j += 1;
321            }
322            let mut k = j;
323            while let Some(&(b0, b1)) = other.pieces.get(k) {
324                if b0 >= a1 {
325                    break;
326                }
327                if b0 > cur {
328                    out.push((cur, b0));
329                }
330                cur = max(cur, b1);
331                if cur >= a1 {
332                    break;
333                }
334                k += 1;
335            }
336            if cur < a1 {
337                out.push((cur, a1));
338            }
339            // Every piece before `k` ended inside this minuend piece, so cannot reach the
340            // next one; piece `k` itself (if any) may, so the next scan starts there.
341            j = k;
342        }
343        Self { pieces: out }
344    }
345
346    /// The part of the set inside `[start, end)`.
347    ///
348    /// # Errors
349    /// As [`from_intervals`](Self::from_intervals), with `index` 0.
350    pub fn clip(&self, start: T, end: T) -> Result<Self, IntervalError> {
351        check(0, start, end)?;
352        if start >= end {
353            return Ok(Self::new());
354        }
355        Ok(self.intersection(&Self {
356            pieces: vec![(start, end)],
357        }))
358    }
359}
360
361/// Merges a start-sorted list of non-empty pieces into normal form.
362fn merge_sorted<T: IntervalBound>(sorted: Vec<(T, T)>) -> Vec<(T, T)> {
363    let mut out: Vec<(T, T)> = Vec::with_capacity(sorted.len());
364    for (s, e) in sorted {
365        match out.last_mut() {
366            Some(last) if s <= last.1 => last.1 = max(last.1, e),
367            _ => out.push((s, e)),
368        }
369    }
370    out
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use proptest::prelude::*;
377
378    fn set(v: &[(i64, i64)]) -> IntervalSet<i64> {
379        IntervalSet::from_intervals(v.iter().copied()).expect("valid test intervals")
380    }
381
382    #[test]
383    fn normalises_overlap_touch_and_empty() {
384        let s = set(&[(5, 7), (1, 3), (3, 4), (2, 2), (6, 9), (11, 12)]);
385        assert_eq!(s.as_slice(), &[(1, 4), (5, 9), (11, 12)]);
386        assert_eq!(s.measure(), 8);
387        assert_eq!(s.len(), 3);
388    }
389
390    #[test]
391    fn refuses_reversed_and_non_finite_with_position() {
392        assert_eq!(
393            IntervalSet::from_intervals([(0, 1), (3, 2)]),
394            Err(IntervalError::Reversed { index: 1 })
395        );
396        assert_eq!(
397            IntervalSet::from_intervals([(0.0, 1.0), (f64::NAN, 2.0)]),
398            Err(IntervalError::NotFinite { index: 1 })
399        );
400        assert_eq!(
401            IntervalSet::from_intervals([(0.0, f64::INFINITY)]),
402            Err(IntervalError::NotFinite { index: 0 })
403        );
404        let mut s = IntervalSet::new();
405        assert_eq!(s.insert(2, 1), Err(IntervalError::Reversed { index: 0 }));
406        assert_eq!(
407            set(&[(0, 5)]).clip(4, 3),
408            Err(IntervalError::Reversed { index: 0 })
409        );
410    }
411
412    #[test]
413    fn set_operations() {
414        let a = set(&[(0, 10), (20, 30)]);
415        let b = set(&[(5, 25)]);
416        assert_eq!(a.union(&b).as_slice(), &[(0, 30)]);
417        assert_eq!(a.intersection(&b).as_slice(), &[(5, 10), (20, 25)]);
418        assert_eq!(a.difference(&b).as_slice(), &[(0, 5), (25, 30)]);
419        assert_eq!(b.difference(&a).as_slice(), &[(10, 20)]);
420        assert_eq!(a.clip(8, 22).unwrap().as_slice(), &[(8, 10), (20, 22)]);
421        assert!(a.clip(3, 3).unwrap().is_empty());
422    }
423
424    #[test]
425    fn difference_where_one_subtrahend_covers_several_pieces() {
426        let a = set(&[(0, 2), (3, 5), (6, 8), (9, 12)]);
427        let b = set(&[(1, 7), (10, 11)]);
428        assert_eq!(
429            a.difference(&b).as_slice(),
430            &[(0, 1), (7, 8), (9, 10), (11, 12)]
431        );
432    }
433
434    #[test]
435    fn contains_is_half_open() {
436        let s = set(&[(1, 3), (5, 6)]);
437        assert!(!s.contains(0));
438        assert!(s.contains(1));
439        assert!(s.contains(2));
440        assert!(!s.contains(3));
441        assert!(s.contains(5));
442        assert!(!s.contains(6));
443        assert!(!IntervalSet::<f64>::new().contains(0.0));
444        assert!(!IntervalSet::from_intervals([(0.0, 1.0)])
445            .unwrap()
446            .contains(f64::NAN));
447    }
448
449    #[test]
450    fn integer_measure_spans_the_whole_type() {
451        let s = set(&[(i64::MIN, 0), (1, i64::MAX)]);
452        assert_eq!(s.measure(), u64::MAX - 1);
453    }
454
455    /// Reported case: 24 h planned + 20 h unplanned stops overlapping 14 h in a
456    /// 168 h week give 30 h down (not 44 h).
457    #[test]
458    fn overlapping_categories_count_once() {
459        let planned = IntervalSet::from_intervals([(0.0, 24.0)]).unwrap();
460        let unplanned = IntervalSet::from_intervals([(10.0, 30.0)]).unwrap();
461        let week = IntervalSet::from_intervals([(0.0, 168.0)]).unwrap();
462        let up = week.difference(&planned.union(&unplanned));
463        assert_eq!(up.measure(), 138.0);
464    }
465
466    fn arb_pieces() -> impl Strategy<Value = Vec<(i64, i64)>> {
467        prop::collection::vec((-50i64..50, 0i64..20), 0..12)
468            .prop_map(|v| v.into_iter().map(|(s, l)| (s, s + l)).collect())
469    }
470
471    /// Membership of every integer point in [-60, 80) — the reference model.
472    fn points(s: &IntervalSet<i64>) -> Vec<bool> {
473        (-60..80).map(|t| s.contains(t)).collect()
474    }
475
476    fn is_normal(s: &IntervalSet<i64>) -> bool {
477        s.as_slice().iter().all(|&(a, b)| a < b) && s.as_slice().windows(2).all(|w| w[0].1 < w[1].0)
478    }
479
480    proptest! {
481        #[test]
482        fn operations_agree_with_pointwise_model(a in arb_pieces(), b in arb_pieces()) {
483            let (sa, sb) = (set(&a), set(&b));
484            let (pa, pb) = (points(&sa), points(&sb));
485            for (op, expect) in [
486                (sa.union(&sb), pa.iter().zip(&pb).map(|(x, y)| *x || *y).collect::<Vec<_>>()),
487                (sa.intersection(&sb), pa.iter().zip(&pb).map(|(x, y)| *x && *y).collect()),
488                (sa.difference(&sb), pa.iter().zip(&pb).map(|(x, y)| *x && !*y).collect()),
489            ] {
490                prop_assert!(is_normal(&op));
491                prop_assert_eq!(points(&op), expect.clone());
492                // Unit-spaced integer model: measure = number of covered points.
493                prop_assert_eq!(op.measure() as usize, expect.iter().filter(|x| **x).count());
494            }
495        }
496
497        #[test]
498        fn measure_is_inclusion_exclusion(a in arb_pieces(), b in arb_pieces()) {
499            let (sa, sb) = (set(&a), set(&b));
500            prop_assert_eq!(
501                sa.union(&sb).measure() + sa.intersection(&sb).measure(),
502                sa.measure() + sb.measure()
503            );
504            prop_assert_eq!(
505                sa.difference(&sb).measure() + sa.intersection(&sb).measure(),
506                sa.measure()
507            );
508        }
509
510        #[test]
511        fn insert_equals_union(a in arb_pieces(), s in -50i64..50, l in 0i64..20) {
512            let mut x = set(&a);
513            x.insert(s, s + l).unwrap();
514            prop_assert_eq!(x, set(&a).union(&set(&[(s, s + l)])));
515        }
516    }
517}