Skip to main content

alpha2_time/
range.rs

1use crate::{MediaTime, TimeRangeError};
2use serde::{Deserialize, Serialize};
3
4/// A half-open timeline interval `[start, end)`.
5#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
6#[serde(try_from = "Wire")]
7#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
8pub struct TimeRange {
9    start: MediaTime,
10    end: MediaTime,
11}
12#[derive(Deserialize)]
13#[serde(deny_unknown_fields)]
14#[cfg_attr(
15    feature = "schema",
16    derive(schemars::JsonSchema),
17    schemars(rename = "TimeRange")
18)]
19/// A half-open interval `[start, end)` in timeline ticks; `end` may not precede `start`.
20struct Wire {
21    /// Inclusive start.
22    start: MediaTime,
23    /// Exclusive end.
24    end: MediaTime,
25}
26impl TryFrom<Wire> for TimeRange {
27    type Error = TimeRangeError;
28    fn try_from(wire: Wire) -> Result<Self, Self::Error> {
29        Self::new(wire.start, wire.end)
30    }
31}
32impl TimeRange {
33    /// Creates an ordered range.
34    ///
35    /// # Errors
36    /// Returns an error if end precedes start.
37    pub const fn new(start: MediaTime, end: MediaTime) -> Result<Self, TimeRangeError> {
38        if end.0 < start.0 {
39            Err(TimeRangeError::EndBeforeStart)
40        } else {
41            Ok(Self { start, end })
42        }
43    }
44    /// Inclusive start.
45    #[must_use]
46    pub const fn start(self) -> MediaTime {
47        self.start
48    }
49    /// Exclusive end.
50    #[must_use]
51    pub const fn end(self) -> MediaTime {
52        self.end
53    }
54    /// Whether the range is empty.
55    #[must_use]
56    pub const fn is_empty(self) -> bool {
57        self.start.0 == self.end.0
58    }
59    /// Checked duration.
60    #[must_use]
61    pub const fn duration(self) -> Option<MediaTime> {
62        self.end.checked_sub(self.start)
63    }
64    /// Half-open membership.
65    #[must_use]
66    pub const fn contains(self, time: MediaTime) -> bool {
67        self.start.0 <= time.0 && time.0 < self.end.0
68    }
69    /// Whether the ranges share an instant. Empty ranges and ranges that only touch do not.
70    #[must_use]
71    pub const fn intersects(self, other: Self) -> bool {
72        self.intersection(other).is_some()
73    }
74    /// Their non-empty intersection.
75    #[must_use]
76    pub const fn intersection(self, other: Self) -> Option<Self> {
77        let start = if self.start.0 > other.start.0 {
78            self.start
79        } else {
80            other.start
81        };
82        let end = if self.end.0 < other.end.0 {
83            self.end
84        } else {
85            other.end
86        };
87        if start.0 < end.0 {
88            Some(Self { start, end })
89        } else {
90            None
91        }
92    }
93}
94#[cfg(test)]
95mod tests {
96    use super::*;
97    #[test]
98    fn half_open() {
99        let range = TimeRange::new(MediaTime(1), MediaTime(2)).unwrap();
100        assert!(range.contains(MediaTime(1)));
101        assert!(!range.contains(MediaTime(2)));
102    }
103
104    #[test]
105    fn ranges_intersect_where_they_share_an_instant() {
106        let range = |start, end| TimeRange::new(MediaTime(start), MediaTime(end)).unwrap();
107        assert!(!range(5, 5).intersects(range(0, 10)));
108        assert!(!range(0, 10).intersects(range(5, 5)));
109        assert!(!range(0, 0).intersects(range(0, 10)));
110        assert!(!range(5, 5).intersects(range(5, 5)));
111        assert!(!range(0, 5).intersects(range(5, 10)));
112        assert!(range(0, 6).intersects(range(5, 10)));
113        assert!(range(2, 3).intersects(range(0, 10)));
114        let ranges = (-3..4)
115            .flat_map(|start| (start..4).map(move |end| range(start, end)))
116            .collect::<Vec<_>>();
117        for &a in &ranges {
118            for &b in &ranges {
119                let shared = (-3..4).any(|t| a.contains(MediaTime(t)) && b.contains(MediaTime(t)));
120                assert_eq!(a.intersects(b), shared, "{a:?} {b:?}");
121                assert_eq!(a.intersection(b).is_some(), shared, "{a:?} {b:?}");
122            }
123        }
124    }
125}