twine_models/support/schedule/
step_schedule.rs1mod step;
2
3use std::{fmt::Debug, ops::Range};
4
5use thiserror::Error;
6
7pub use step::{EmptyRangeError, Step};
8
9const LINEAR_SEARCH_THRESHOLD: usize = 32;
18
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
45pub struct StepSchedule<T, V> {
46 steps: Vec<Step<T, V>>,
47}
48
49#[derive(Debug, Error)]
57#[error("steps overlap: {existing:?} and {incoming:?}")]
58pub struct OverlappingStepsError<T: Debug + Ord> {
59 pub existing: Range<T>,
60 pub incoming: Range<T>,
61}
62
63impl<T: Debug + Clone + Ord, V> StepSchedule<T, V> {
64 pub fn new<I>(steps: I) -> Result<Self, OverlappingStepsError<T>>
75 where
76 I: IntoIterator<Item = Step<T, V>>,
77 {
78 let mut steps: Vec<_> = steps.into_iter().collect();
79 steps.sort_by(|a, b| a.start().cmp(b.start()));
80
81 if let Some(index) = steps.windows(2).position(|pair| pair[0].overlaps(&pair[1])) {
82 return Err(OverlappingStepsError {
83 existing: steps[index].range().clone(),
84 incoming: steps[index + 1].range().clone(),
85 });
86 }
87
88 Ok(StepSchedule { steps })
89 }
90
91 pub fn try_push(&mut self, step: Step<T, V>) -> Result<(), OverlappingStepsError<T>> {
98 let index = self.steps.partition_point(|s| s.start() < step.start());
99
100 let overlaps_left = index.checked_sub(1).and_then(|i| {
106 let left = &self.steps[i];
107 left.overlaps(&step).then_some(left)
108 });
109
110 let overlaps_right = self.steps.get(index).filter(|s| s.overlaps(&step));
111
112 if let Some(existing_step) = overlaps_left.or(overlaps_right) {
113 return Err(OverlappingStepsError {
114 existing: existing_step.range().clone(),
115 incoming: step.range().clone(),
116 });
117 }
118
119 self.steps.insert(index, step);
120 Ok(())
121 }
122
123 #[must_use]
125 pub fn steps(&self) -> &[Step<T, V>] {
126 &self.steps
127 }
128
129 pub fn value_at(&self, time: &T) -> Option<&V> {
154 if self.steps.len() < LINEAR_SEARCH_THRESHOLD {
155 self.steps.iter().find_map(|step| step.value_at(time))
156 } else {
157 self.steps
158 .binary_search_by(|step| step.cmp_to_time(time))
159 .ok()
160 .map(|index| self.steps[index].value())
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn new_succeeds_with_non_overlapping_steps() {
171 let schedule = StepSchedule::new([
172 Step::new(10..20, "a").unwrap(),
173 Step::new(20..30, "b").unwrap(),
174 ])
175 .unwrap();
176
177 assert_eq!(schedule.steps().len(), 2);
178 assert_eq!(schedule.value_at(&15), Some(&"a"));
179 assert_eq!(schedule.value_at(&25), Some(&"b"));
180 }
181
182 #[test]
183 fn new_rejects_overlapping_steps() {
184 let result = StepSchedule::new([
185 Step::new(0..10, "a").unwrap(),
186 Step::new(9..15, "b").unwrap(),
187 ]);
188
189 assert!(result.is_err());
190 }
191
192 #[test]
193 fn steps_are_sorted_by_start_time() {
194 let schedule = StepSchedule::new([
195 Step::new(20..30, "later").unwrap(),
196 Step::new(0..10, "early").unwrap(),
197 ])
198 .unwrap();
199
200 let steps = schedule.steps();
201 assert_eq!(steps[0].start(), &0);
202 assert_eq!(steps[1].start(), &20);
203 }
204
205 #[test]
206 fn try_push_inserts_steps_correctly() {
207 let starts = |s: &StepSchedule<_, _>| {
208 s.steps()
209 .iter()
210 .map(Step::start)
211 .copied()
212 .collect::<Vec<_>>()
213 };
214
215 let mut schedule = StepSchedule::new([
216 Step::new(0..10, "a").unwrap(),
217 Step::new(20..30, "c").unwrap(),
218 ])
219 .unwrap();
220
221 schedule.try_push(Step::new(10..20, "b").unwrap()).unwrap();
222 assert_eq!(starts(&schedule), vec![0, 10, 20]);
223 assert_eq!(schedule.value_at(&15), Some(&"b"));
224
225 schedule
226 .try_push(Step::new(-10..-5, "start").unwrap())
227 .unwrap();
228 assert_eq!(starts(&schedule), vec![-10, 0, 10, 20]);
229
230 schedule
231 .try_push(Step::new(30..40, "end").unwrap())
232 .unwrap();
233 assert_eq!(starts(&schedule), vec![-10, 0, 10, 20, 30]);
234
235 assert_eq!(
236 schedule
237 .steps()
238 .iter()
239 .map(Step::value)
240 .copied()
241 .collect::<Vec<_>>(),
242 vec!["start", "a", "b", "c", "end"],
243 );
244 }
245
246 #[test]
247 fn try_push_rejects_overlapping_step() {
248 let mut schedule = StepSchedule::new([
249 (0..10, "a").try_into().unwrap(),
250 (20..30, "b").try_into().unwrap(),
251 ])
252 .unwrap();
253
254 assert!(
255 schedule
256 .try_push((5..15, "overlaps a").try_into().unwrap())
257 .is_err()
258 );
259
260 assert!(
261 schedule
262 .try_push((25..35, "overlaps b").try_into().unwrap())
263 .is_err()
264 );
265
266 assert!(
267 schedule
268 .try_push((5..25, "overlaps both").try_into().unwrap())
269 .is_err()
270 );
271 }
272
273 #[test]
274 fn value_at_handles_empty_schedule() {
275 let schedule: StepSchedule<i32, &str> = StepSchedule::default();
276 assert_eq!(schedule.value_at(&5), None);
277 }
278
279 #[test]
280 fn value_at_handles_exact_start_and_end_bounds() {
281 let step = Step::new(5..10, "only").unwrap();
282 let schedule = StepSchedule::new(std::iter::once(step)).unwrap();
283
284 assert_eq!(schedule.value_at(&4), None);
285 assert_eq!(schedule.value_at(&5), Some(&"only"));
286 assert_eq!(schedule.value_at(&9), Some(&"only"));
287 assert_eq!(schedule.value_at(&10), None);
288 }
289
290 #[test]
291 fn value_at_works_for_large_schedules() {
292 let steps = (0..100).map(|i| Step::new(i * 10..(i + 1) * 10, i).unwrap());
293 let schedule = StepSchedule::new(steps).unwrap();
294
295 assert_eq!(schedule.value_at(&0), Some(&0));
296 assert_eq!(schedule.value_at(&42), Some(&4));
297 assert_eq!(schedule.value_at(&999), Some(&99));
298 assert_eq!(schedule.value_at(&1000), None);
299 }
300}