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. An empty range holds none, so it
70    /// intersects nothing; ranges that only touch share none either.
71    #[must_use]
72    pub const fn intersects(self, other: Self) -> bool {
73        self.intersection(other).is_some()
74    }
75    /// Their non-empty intersection.
76    #[must_use]
77    pub const fn intersection(self, other: Self) -> Option<Self> {
78        let start = if self.start.0 > other.start.0 {
79            self.start
80        } else {
81            other.start
82        };
83        let end = if self.end.0 < other.end.0 {
84            self.end
85        } else {
86            other.end
87        };
88        if start.0 < end.0 {
89            Some(Self { start, end })
90        } else {
91            None
92        }
93    }
94}
95#[cfg(test)]
96mod tests {
97    use super::*;
98    #[test]
99    fn half_open() {
100        let range = TimeRange::new(MediaTime(1), MediaTime(2)).unwrap();
101        assert!(range.contains(MediaTime(1)));
102        assert!(!range.contains(MediaTime(2)));
103    }
104
105    #[test]
106    fn ranges_intersect_where_they_share_an_instant() {
107        let range = |start, end| TimeRange::new(MediaTime(start), MediaTime(end)).unwrap();
108        // An empty range holds no instant, so it meets nothing, even inside another.
109        assert!(!range(5, 5).intersects(range(0, 10)));
110        assert!(!range(0, 10).intersects(range(5, 5)));
111        assert!(!range(0, 0).intersects(range(0, 10)));
112        assert!(!range(5, 5).intersects(range(5, 5)));
113        // Ranges that only touch share no instant.
114        assert!(!range(0, 5).intersects(range(5, 10)));
115        assert!(range(0, 6).intersects(range(5, 10)));
116        assert!(range(2, 3).intersects(range(0, 10)));
117        let ranges = (-3..4)
118            .flat_map(|start| (start..4).map(move |end| range(start, end)))
119            .collect::<Vec<_>>();
120        for &a in &ranges {
121            for &b in &ranges {
122                let shared = (-3..4).any(|t| a.contains(MediaTime(t)) && b.contains(MediaTime(t)));
123                assert_eq!(a.intersects(b), shared, "{a:?} {b:?}");
124                assert_eq!(a.intersection(b).is_some(), shared, "{a:?} {b:?}");
125            }
126        }
127    }
128}