Skip to main content

twine_models/support/schedule/step_schedule/
step.rs

1use std::{cmp::Ordering, fmt::Debug, ops::Range};
2
3use thiserror::Error;
4
5/// Associates a value with a non-empty, half-open time range.
6///
7/// A `Step` pairs a value with the range `[start, end)`, where `start < end`.
8/// The type `T` must implement [`Ord`] and typically represents time.
9///
10/// Steps are used as building blocks for schedules like [`super::StepSchedule`],
11/// which assign values to non-overlapping intervals.
12///
13/// # Examples
14///
15/// ```
16/// use twine_models::support::schedule::step_schedule::Step;
17///
18/// let step = Step::new(0..10, "active").unwrap();
19/// assert!(step.contains(&5));
20/// assert_eq!(step.value(), &"active");
21///
22/// assert!(Step::new(2..2, "empty").is_err());
23/// ```
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Step<T, V> {
26    range: Range<T>,
27    value: V,
28}
29
30/// Error returned when attempting to create a [`Step`] with an empty range.
31#[derive(Debug, Error)]
32#[error("empty range: start ({start:?}) >= end ({end:?})")]
33pub struct EmptyRangeError<T: Debug> {
34    pub start: T,
35    pub end: T,
36}
37
38impl<T: Debug + Ord, V> Step<T, V> {
39    /// Creates a new `Step` with the given range and value.
40    ///
41    /// # Errors
42    ///
43    /// Returns an [`EmptyRangeError`] if the provided range is empty.
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use twine_models::support::schedule::step_schedule::Step;
49    ///
50    /// let step = Step::new(0..10, 42.0).unwrap();
51    /// assert_eq!(step.start(), &0);
52    /// assert_eq!(step.end(), &10);
53    /// assert_eq!(step.value(), &42.0);
54    ///
55    /// assert!(Step::new(5..1, "invalid range").is_err());
56    /// ```
57    pub fn new(range: Range<T>, value: V) -> Result<Self, EmptyRangeError<T>> {
58        if range.is_empty() {
59            Err(EmptyRangeError {
60                start: range.start,
61                end: range.end,
62            })
63        } else {
64            Ok(Self { range, value })
65        }
66    }
67
68    /// Returns a reference to the range covered by this step.
69    pub fn range(&self) -> &Range<T> {
70        &self.range
71    }
72
73    /// Returns a reference to the value associated with this step.
74    pub fn value(&self) -> &V {
75        &self.value
76    }
77
78    /// Returns a reference to the inclusive start bound of the range.
79    pub fn start(&self) -> &T {
80        &self.range.start
81    }
82
83    /// Returns a reference to the exclusive end bound of the range.
84    pub fn end(&self) -> &T {
85        &self.range.end
86    }
87
88    /// Returns `true` if `time` falls within the range of this step.
89    ///
90    /// Equivalent to `self.range.contains(time)`.
91    pub fn contains(&self, time: &T) -> bool {
92        self.range.contains(time)
93    }
94
95    /// Returns `true` if this step's range overlaps with `other`'s range.
96    ///
97    /// Two steps overlap if their ranges share any values.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use twine_models::support::schedule::step_schedule::Step;
103    ///
104    /// let a = Step::new(0..5, "a").unwrap();
105    /// let b = Step::new(4..8, "b").unwrap();
106    /// let c = Step::new(8..10, "c").unwrap();
107    ///
108    /// assert!(a.overlaps(&b));
109    /// assert!(!b.overlaps(&c));
110    /// ```
111    pub fn overlaps(&self, other: &Self) -> bool {
112        self.range.start < other.range.end && other.range.start < self.range.end
113    }
114
115    /// Returns a reference to the value if `time` is within this step's range.
116    ///
117    /// Returns `None` if `time` is outside the range.
118    pub fn value_at(&self, time: &T) -> Option<&V> {
119        if self.contains(time) {
120            Some(&self.value)
121        } else {
122            None
123        }
124    }
125
126    /// Returns how this step's range relates to a given time value.
127    ///
128    /// - [`Ordering::Less`] if the step ends at or before `time`
129    /// - [`Ordering::Greater`] if the step starts after `time`
130    /// - [`Ordering::Equal`] if `time` is within the step's range
131    ///
132    /// Useful for efficient searching (e.g., with `binary_search_by`).
133    pub fn cmp_to_time(&self, time: &T) -> Ordering {
134        if self.end() <= time {
135            Ordering::Less
136        } else if self.start() > time {
137            Ordering::Greater
138        } else {
139            Ordering::Equal
140        }
141    }
142}
143
144impl<T: Debug + Ord, V> TryFrom<(Range<T>, V)> for Step<T, V> {
145    type Error = EmptyRangeError<T>;
146
147    fn try_from((range, value): (Range<T>, V)) -> Result<Self, Self::Error> {
148        Step::new(range, value)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn step_creation() {
158        let step = Step::new(1..5, "a").unwrap();
159        assert_eq!(step.start(), &1);
160        assert_eq!(step.end(), &5);
161        assert_eq!(step.value(), &"a");
162
163        let err = Step::new(3..3, "x").unwrap_err();
164        assert_eq!(err.start, 3);
165        assert_eq!(err.end, 3);
166    }
167
168    #[test]
169    fn value_at_works() {
170        let step = Step::new(10..20, "middle").unwrap();
171        assert_eq!(step.value_at(&0), None);
172        assert_eq!(step.value_at(&10), Some(&"middle"));
173        assert_eq!(step.value_at(&15), Some(&"middle"));
174        assert_eq!(step.value_at(&20), None);
175        assert_eq!(step.value_at(&25), None);
176    }
177
178    #[test]
179    fn overlaps_works() {
180        let a = Step::new(0..5, "a").unwrap();
181        let b = Step::new(4..8, "b").unwrap();
182        let c = Step::new(8..10, "c").unwrap();
183
184        assert!(a.overlaps(&b));
185        assert!(!b.overlaps(&c));
186    }
187
188    #[test]
189    fn cmp_to_time_works() {
190        let step = Step::new(10..20, "x").unwrap();
191        assert_eq!(step.cmp_to_time(&5), std::cmp::Ordering::Greater);
192        assert_eq!(step.cmp_to_time(&15), std::cmp::Ordering::Equal);
193        assert_eq!(step.cmp_to_time(&25), std::cmp::Ordering::Less);
194    }
195}