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}