Skip to main content

lox_time/
intervals.rs

1// SPDX-FileCopyrightText: 2025 Helge Eichhorn <git@helgeeichhorn.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use std::{
6    cmp::{max, min},
7    fmt::Display,
8    ops::{Add, Sub},
9};
10
11use lox_core::time::deltas::TimeDelta;
12use lox_test_utils::approx_eq::ApproxEq;
13use lox_test_utils::approx_eq::results::ApproxEqResults;
14
15use crate::{
16    Time,
17    offsets::{DefaultOffsetProvider, Offset},
18    time_scales::{Tai, TimeScale},
19    utc::{Utc, transformations::ToUtc},
20};
21
22/// A half-open interval `[start, end)`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct Interval<T> {
26    start: T,
27    end: T,
28}
29
30impl<T: ApproxEq + std::fmt::Debug> ApproxEq for Interval<T> {
31    fn approx_eq(&self, rhs: &Self, atol: f64, rtol: f64) -> ApproxEqResults {
32        let mut results = ApproxEqResults::new();
33        results.merge("start", self.start.approx_eq(&rhs.start, atol, rtol));
34        results.merge("end", self.end.approx_eq(&rhs.end, atol, rtol));
35        results
36    }
37}
38
39impl<T> Interval<T> {
40    /// Creates a new interval from `start` to `end`.
41    pub fn new(start: T, end: T) -> Self {
42        Interval { start, end }
43    }
44
45    /// Returns the start of the interval.
46    pub fn start(&self) -> T
47    where
48        T: Copy,
49    {
50        self.start
51    }
52
53    /// Returns the end of the interval.
54    pub fn end(&self) -> T
55    where
56        T: Copy,
57    {
58        self.end
59    }
60
61    /// Returns the duration of the interval as a [`TimeDelta`].
62    pub fn duration(&self) -> TimeDelta
63    where
64        T: Sub<Output = TimeDelta> + Copy,
65    {
66        self.end - self.start
67    }
68
69    /// Returns `true` if the interval is empty (`start >= end`).
70    pub fn is_empty(&self) -> bool
71    where
72        T: Ord,
73    {
74        self.start >= self.end
75    }
76
77    /// Returns `true` if `time` falls within `[start, end)`.
78    pub fn contains_time(&self, time: T) -> bool
79    where
80        T: Ord,
81    {
82        self.start <= time && time < self.end
83    }
84
85    /// Returns the intersection of `self` and `other`.
86    pub fn intersect(&self, other: Self) -> Self
87    where
88        T: Ord + Copy,
89    {
90        Interval {
91            start: max(self.start, other.start),
92            end: min(self.end, other.end),
93        }
94    }
95
96    /// Returns `true` if `self` and `other` overlap.
97    pub fn overlaps(&self, other: Self) -> bool
98    where
99        T: Ord + Copy,
100    {
101        !self.intersect(other).is_empty()
102    }
103
104    /// Returns an iterator of evenly-spaced points from start to end (inclusive)
105    /// with the given step size.
106    ///
107    /// The step sign is automatically adjusted to match the interval direction:
108    /// forward if `start <= end`, backward if `start > end`.
109    ///
110    /// # Panics
111    ///
112    /// Panics if `step` is zero.
113    pub fn step_by(&self, step: TimeDelta) -> IntervalStepIter<T>
114    where
115        T: Copy + Add<TimeDelta, Output = T> + PartialOrd,
116    {
117        assert!(
118            step.is_positive() || step.is_negative(),
119            "step must be non-zero"
120        );
121        let forward = self.start <= self.end;
122        let step = if forward == step.is_positive() {
123            step
124        } else {
125            -step
126        };
127        IntervalStepIter {
128            current: self.start,
129            end: self.end,
130            step,
131            forward,
132        }
133    }
134
135    /// Returns `n` evenly-spaced points from start to end (inclusive).
136    ///
137    /// Panics if `n < 2`.
138    pub fn linspace(&self, n: usize) -> Vec<T>
139    where
140        T: Copy + Add<TimeDelta, Output = T> + Sub<Output = TimeDelta>,
141    {
142        assert!(n >= 2, "linspace requires at least 2 points");
143        let duration = self.end - self.start;
144        let step_secs = duration.to_seconds().to_f64() / (n - 1) as f64;
145        (0..n)
146            .map(|i| self.start + TimeDelta::from_seconds_f64(step_secs * i as f64))
147            .collect()
148    }
149
150    /// True if self fully contains other.
151    pub fn contains(&self, other: &Self) -> bool
152    where
153        T: Ord,
154    {
155        self.start <= other.start && self.end >= other.end
156    }
157}
158
159/// Iterator that steps through an interval with a fixed time step.
160pub struct IntervalStepIter<T> {
161    current: T,
162    end: T,
163    step: TimeDelta,
164    forward: bool,
165}
166
167impl<T> Iterator for IntervalStepIter<T>
168where
169    T: Copy + Add<TimeDelta, Output = T> + PartialOrd,
170{
171    type Item = T;
172
173    fn next(&mut self) -> Option<Self::Item> {
174        let done = if self.forward {
175            self.current > self.end
176        } else {
177            self.current < self.end
178        };
179        if done {
180            return None;
181        }
182        let value = self.current;
183        self.current = self.current + self.step;
184        Some(value)
185    }
186}
187
188/// Intersect two sorted lists of intervals.
189pub fn intersect_intervals<T: Ord + Copy>(
190    a: &[Interval<T>],
191    b: &[Interval<T>],
192) -> Vec<Interval<T>> {
193    let mut result = Vec::new();
194    let mut i = 0;
195    let mut j = 0;
196    while i < a.len() && j < b.len() {
197        let inter = a[i].intersect(b[j]);
198        if !inter.is_empty() {
199            result.push(inter);
200        }
201        // Advance the interval with the smaller end
202        if a[i].end <= b[j].end {
203            i += 1;
204        } else {
205            j += 1;
206        }
207    }
208    result
209}
210
211/// Union two sorted lists of intervals (merge overlapping/adjacent).
212pub fn union_intervals<T: Ord + Copy>(a: &[Interval<T>], b: &[Interval<T>]) -> Vec<Interval<T>> {
213    // Merge the two sorted lists
214    let mut all = Vec::with_capacity(a.len() + b.len());
215    let mut i = 0;
216    let mut j = 0;
217    while i < a.len() && j < b.len() {
218        if a[i].start <= b[j].start {
219            all.push(a[i]);
220            i += 1;
221        } else {
222            all.push(b[j]);
223            j += 1;
224        }
225    }
226    all.extend_from_slice(&a[i..]);
227    all.extend_from_slice(&b[j..]);
228
229    merge_intervals(all)
230}
231
232/// Complement intervals within a bounding interval.
233pub fn complement_intervals<T: Ord + Copy>(
234    intervals: &[Interval<T>],
235    bound: Interval<T>,
236) -> Vec<Interval<T>> {
237    let mut result = Vec::new();
238    let mut cursor = bound.start;
239    for iv in intervals {
240        if iv.start > cursor {
241            let gap = Interval::new(cursor, iv.start);
242            if !gap.is_empty() {
243                result.push(gap);
244            }
245        }
246        if iv.end > cursor {
247            cursor = iv.end;
248        }
249    }
250    if cursor < bound.end {
251        result.push(Interval::new(cursor, bound.end));
252    }
253    result
254}
255
256fn merge_intervals<T: Ord + Copy>(sorted: Vec<Interval<T>>) -> Vec<Interval<T>> {
257    let mut result: Vec<Interval<T>> = Vec::new();
258    for iv in sorted {
259        if iv.is_empty() {
260            continue;
261        }
262        if let Some(last) = result.last_mut()
263            && iv.start <= last.end
264        {
265            last.end = max(last.end, iv.end);
266            continue;
267        }
268        result.push(iv);
269    }
270    result
271}
272
273/// An interval of [`TimeDelta`] values.
274pub type TimeDeltaInterval = Interval<TimeDelta>;
275
276impl TimeDeltaInterval {
277    /// Converts this delta-based interval to a [`TimeInterval`] in the given time scale.
278    pub fn to_scale<T: TimeScale + Copy>(&self, scale: T) -> TimeInterval<T> {
279        Interval {
280            start: Time::from_delta(scale, self.start),
281            end: Time::from_delta(scale, self.end),
282        }
283    }
284}
285
286/// An interval of [`Time`] values in a given time scale.
287pub type TimeInterval<T> = Interval<Time<T>>;
288
289impl<T> TimeInterval<T>
290where
291    T: ToUtc + TimeScale + Copy,
292    DefaultOffsetProvider: Offset<T, Tai>,
293{
294    /// Converts this time interval to a [`UtcInterval`].
295    pub fn to_utc(&self) -> UtcInterval {
296        Interval {
297            start: self.start.to_utc(),
298            end: self.end.to_utc(),
299        }
300    }
301}
302
303/// An interval of [`Utc`] values.
304pub type UtcInterval = Interval<Utc>;
305
306impl UtcInterval {
307    /// Converts this UTC interval to a [`TimeInterval`] in TAI.
308    pub fn to_time(&self) -> TimeInterval<Tai> {
309        Interval {
310            start: self.start.to_time(),
311            end: self.end.to_time(),
312        }
313    }
314}
315
316impl<T> Display for Interval<T>
317where
318    T: Display,
319{
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        self.start.fmt(f)?;
322        write!(f, " – ")?;
323        self.end.fmt(f)
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use crate::{time, time_scales::Tai};
330
331    use super::*;
332
333    #[test]
334    fn test_time_interval() {
335        let t0 = time!(Tai, 2025, 11, 6).unwrap();
336        let t1 = time!(Tai, 2025, 11, 6, 1).unwrap();
337        let i = TimeInterval::new(t0, t1);
338        assert_eq!(i.start(), t0);
339        assert_eq!(i.end(), t1);
340        assert_eq!(i.duration(), TimeDelta::from_hours(1));
341        assert_eq!(
342            format!("{}", i),
343            "2025-11-06T00:00:00.000 TAI – 2025-11-06T01:00:00.000 TAI"
344        );
345    }
346
347    #[test]
348    fn test_step_by() {
349        let t0 = time!(Tai, 2025, 11, 6).unwrap();
350        let t1 = time!(Tai, 2025, 11, 6, 1).unwrap();
351        let interval = TimeInterval::new(t0, t1);
352        let step = TimeDelta::from_minutes(20);
353        let times: Vec<_> = interval.step_by(step).collect();
354        assert_eq!(times.len(), 4); // 0, 20, 40, 60 minutes
355        assert_eq!(times[0], t0);
356        assert_eq!(times[3], t1);
357    }
358
359    #[test]
360    fn test_step_by_non_exact() {
361        let t0 = time!(Tai, 2025, 11, 6).unwrap();
362        let t1 = t0 + TimeDelta::from_minutes(50);
363        let interval = TimeInterval::new(t0, t1);
364        let step = TimeDelta::from_minutes(20);
365        let times: Vec<_> = interval.step_by(step).collect();
366        assert_eq!(times.len(), 3); // 0, 20, 40 minutes (60 exceeds end)
367    }
368
369    #[test]
370    fn test_linspace() {
371        let t0 = time!(Tai, 2025, 11, 6).unwrap();
372        let t1 = time!(Tai, 2025, 11, 6, 1).unwrap();
373        let interval = TimeInterval::new(t0, t1);
374        let times = interval.linspace(5);
375        assert_eq!(times.len(), 5);
376        assert_eq!(times[0], t0);
377        assert_eq!(times[4], t1);
378        // Equal spacing: 15 minutes apart
379        let dt = TimeDelta::from_minutes(15);
380        assert_eq!(times[1], t0 + dt);
381        assert_eq!(times[2], t0 + dt + dt);
382    }
383
384    #[test]
385    fn test_timedelta_interval_step_by() {
386        let td0 = TimeDelta::default();
387        let td1 = TimeDelta::from_minutes(60);
388        let interval = TimeDeltaInterval::new(td0, td1);
389        let step = TimeDelta::from_minutes(20);
390        let times: Vec<_> = interval.step_by(step).collect();
391        assert_eq!(times.len(), 4);
392    }
393
394    #[test]
395    fn test_step_by_backward() {
396        let t0 = time!(Tai, 2025, 11, 6).unwrap();
397        let t1 = time!(Tai, 2025, 11, 6, 1).unwrap();
398        // Interval goes backward: start > end
399        let interval = TimeInterval::new(t1, t0);
400        let step = TimeDelta::from_minutes(20);
401        let times: Vec<_> = interval.step_by(step).collect();
402        assert_eq!(times.len(), 4); // 60, 40, 20, 0 minutes
403        assert_eq!(times[0], t1);
404        assert_eq!(times[3], t0);
405        // Monotonically decreasing
406        for w in times.windows(2) {
407            assert!(w[0] > w[1]);
408        }
409    }
410
411    #[test]
412    fn test_step_by_backward_auto_negates_step() {
413        let t0 = time!(Tai, 2025, 11, 6).unwrap();
414        let t1 = time!(Tai, 2025, 11, 6, 1).unwrap();
415        // Backward interval with an already-negative step — should still work
416        let interval = TimeInterval::new(t1, t0);
417        let step = -TimeDelta::from_minutes(20);
418        let times: Vec<_> = interval.step_by(step).collect();
419        assert_eq!(times.len(), 4);
420        assert_eq!(times[0], t1);
421        assert_eq!(times[3], t0);
422    }
423
424    #[test]
425    #[should_panic(expected = "step must be non-zero")]
426    fn test_step_by_zero_panics() {
427        let t0 = time!(Tai, 2025, 11, 6).unwrap();
428        let t1 = time!(Tai, 2025, 11, 6, 1).unwrap();
429        let interval = TimeInterval::new(t0, t1);
430        let _ = interval.step_by(TimeDelta::default()).collect::<Vec<_>>();
431    }
432
433    #[test]
434    fn test_contains() {
435        let outer = Interval::new(0, 10);
436        let inner = Interval::new(2, 8);
437        assert!(outer.contains(&inner));
438        assert!(!inner.contains(&outer));
439    }
440
441    #[test]
442    fn test_intersect_intervals() {
443        let a = vec![Interval::new(0, 5), Interval::new(10, 15)];
444        let b = vec![Interval::new(3, 12)];
445        let result = intersect_intervals(&a, &b);
446        assert_eq!(result, vec![Interval::new(3, 5), Interval::new(10, 12)]);
447    }
448
449    #[test]
450    fn test_intersect_intervals_no_overlap() {
451        let a = vec![Interval::new(0, 3)];
452        let b = vec![Interval::new(5, 8)];
453        let result = intersect_intervals(&a, &b);
454        assert!(result.is_empty());
455    }
456
457    #[test]
458    fn test_union_intervals() {
459        let a = vec![Interval::new(0, 5)];
460        let b = vec![Interval::new(3, 8)];
461        let result = union_intervals(&a, &b);
462        assert_eq!(result, vec![Interval::new(0, 8)]);
463    }
464
465    #[test]
466    fn test_union_intervals_disjoint() {
467        let a = vec![Interval::new(0, 3)];
468        let b = vec![Interval::new(5, 8)];
469        let result = union_intervals(&a, &b);
470        assert_eq!(result, vec![Interval::new(0, 3), Interval::new(5, 8)]);
471    }
472
473    #[test]
474    fn test_complement_intervals() {
475        let intervals = vec![Interval::new(2, 4), Interval::new(6, 8)];
476        let bound = Interval::new(0, 10);
477        let result = complement_intervals(&intervals, bound);
478        assert_eq!(
479            result,
480            vec![
481                Interval::new(0, 2),
482                Interval::new(4, 6),
483                Interval::new(8, 10),
484            ]
485        );
486    }
487
488    #[test]
489    fn test_complement_intervals_full_coverage() {
490        let intervals = vec![Interval::new(0, 10)];
491        let bound = Interval::new(0, 10);
492        let result = complement_intervals(&intervals, bound);
493        assert!(result.is_empty());
494    }
495}