Skip to main content

dpm_model/calendar/working/
arithmetic.rs

1//! Queries on compiled working spans, exact in integer milliseconds.
2//!
3//! Every forward operation has a backward partner that never undershoots it: `sub(add(t, h), h)`
4//! and `add_late(sub(t, h), h)` are at or after `t`. Schedule passes rely on this, so a latest
5//! time computed backwards is never earlier than the earliest time computed forwards.
6
7use super::{BeyondCalendar, WorkingTime, hours, millis};
8
9impl WorkingTime {
10    fn at(&self, t: f64) -> Result<i64, BeyondCalendar> {
11        let t = millis(t).ok_or(BeyondCalendar)?;
12        if t < self.window.0 || t > self.window.1 {
13            return Err(BeyondCalendar);
14        }
15        Ok(t)
16    }
17
18    fn span(&self, index: usize) -> Result<((i64, i64), i64), BeyondCalendar> {
19        match (self.spans.get(index), self.before.get(index)) {
20            (Some(span), Some(before)) => Ok((*span, *before)),
21            _ => Err(BeyondCalendar),
22        }
23    }
24
25    /// Working milliseconds between the window start and `t`, and the last span starting at or
26    /// before `t`.
27    fn locate(&self, t: i64) -> Result<(i64, Option<(i64, i64)>), BeyondCalendar> {
28        let Some(index) = self.starts.through(t).checked_sub(1) else {
29            return Ok((0, None));
30        };
31        let ((start, end), before) = self.span(index)?;
32        Ok((before + t.min(end) - start, Some((start, end))))
33    }
34
35    /// Working milliseconds between the window start and `t`.
36    fn worked(&self, t: i64) -> Result<i64, BeyondCalendar> {
37        Ok(self.locate(t)?.0)
38    }
39
40    /// The instant inside span `index` at which `target` working milliseconds have passed.
41    fn instant(&self, index: usize, target: i64) -> Result<i64, BeyondCalendar> {
42        let ((start, _), before) = self.span(index)?;
43        let at = start + (target - before);
44        if at > self.window.1 {
45            return Err(BeyondCalendar);
46        }
47        Ok(at)
48    }
49
50    /// Earliest time by which `hours` of working time after `t` have passed.
51    pub fn add(&self, t: f64, hours_of_work: f64) -> Result<f64, BeyondCalendar> {
52        if self.always {
53            return Ok(t + hours_of_work.max(0.0));
54        }
55        let from = self.at(t)?;
56        let amount = millis(hours_of_work).ok_or(BeyondCalendar)?;
57        if amount <= 0 {
58            return Ok(t);
59        }
60        let (worked, span) = self.locate(from)?;
61        // Work that ends inside the span it starts in needs no second lookup.
62        if let Some((start, end)) = span
63            && from >= start
64            && from < end
65            && from + amount <= end
66        {
67            return Ok(hours(from + amount));
68        }
69        let target = worked + amount;
70        let end = self.instant(self.through.below(target), target)?;
71        Ok(hours(end.max(from)))
72    }
73
74    /// Latest time at which `hours` of working time after `t` have passed: past a span's end it
75    /// is the next span's start, the same working instant.
76    pub fn add_late(&self, t: f64, hours_of_work: f64) -> Result<f64, BeyondCalendar> {
77        if self.always {
78            return Ok(t + hours_of_work.max(0.0));
79        }
80        let from = self.at(t)?;
81        let amount = millis(hours_of_work).ok_or(BeyondCalendar)?.max(0);
82        let target = self.worked(from)? + amount;
83        let end = self.instant(self.through.through(target), target)?;
84        Ok(hours(end.max(from)))
85    }
86
87    /// Latest time from which `hours` of working time reach `t`.
88    pub fn sub(&self, t: f64, hours_of_work: f64) -> Result<f64, BeyondCalendar> {
89        if self.always {
90            return Ok(t - hours_of_work.max(0.0));
91        }
92        let to = self.at(t)?;
93        let amount = millis(hours_of_work).ok_or(BeyondCalendar)?;
94        if amount <= 0 {
95            return Ok(t);
96        }
97        let (worked, span) = self.locate(to)?;
98        // Work that starts inside the span it ends in needs no second lookup.
99        if let Some((start, end)) = span
100            && to > start
101            && to <= end
102            && to - amount > start
103        {
104            return Ok(hours(to - amount));
105        }
106        let target = worked - amount;
107        if target < 0 {
108            return Err(BeyondCalendar);
109        }
110        let start = self.instant(self.through.through(target), target)?;
111        Ok(hours(start.min(to)))
112    }
113
114    /// A signed lag forwards: `add` for a non-negative amount and `sub` for a negative one.
115    pub fn shift(&self, t: f64, hours_of_work: f64) -> Result<f64, BeyondCalendar> {
116        if hours_of_work >= 0.0 {
117            self.add(t, hours_of_work)
118        } else {
119            self.sub(t, -hours_of_work)
120        }
121    }
122
123    /// The latest time whose forward `shift` by `hours` is at most `t`.
124    pub fn unshift(&self, t: f64, hours_of_work: f64) -> Result<f64, BeyondCalendar> {
125        if hours_of_work >= 0.0 {
126            self.sub(t, hours_of_work)
127        } else {
128            self.add_late(t, -hours_of_work)
129        }
130    }
131
132    /// Earliest working moment at or after `t`; span ends count as working.
133    pub fn align(&self, t: f64) -> Result<f64, BeyondCalendar> {
134        if self.always {
135            return Ok(t);
136        }
137        let at = self.at(t)?;
138        let ((start, _), _) = self.span(self.ends.below(at))?;
139        Ok(if start > at { hours(start) } else { t })
140    }
141
142    /// Earliest moment at or after `t` that working time follows; a span's end moves to the next
143    /// span, so work starting there is placed where it actually begins.
144    pub fn next_working(&self, t: f64) -> Result<f64, BeyondCalendar> {
145        if self.always {
146            return Ok(t);
147        }
148        let at = self.at(t)?;
149        let ((start, _), _) = self.span(self.ends.through(at))?;
150        Ok(if start > at { hours(start) } else { t })
151    }
152
153    /// Where work of `hours` starting no earlier than `t` runs: its start, the first moment that
154    /// working time follows, and its end, the earliest time the hours have passed. Equal to
155    /// `next_working` then `add`, with one lookup when the work fits in one span.
156    pub fn place(&self, t: f64, hours_of_work: f64) -> Result<(f64, f64), BeyondCalendar> {
157        if self.always {
158            return Ok((t, t + hours_of_work.max(0.0)));
159        }
160        let amount = millis(hours_of_work).ok_or(BeyondCalendar)?;
161        if amount <= 0 {
162            return Ok((t, t));
163        }
164        let at = self.at(t)?;
165        let index = self.ends.through(at);
166        let ((start, end), before) = self.span(index)?;
167        let begin = start.max(at);
168        let starts = if begin > at { hours(begin) } else { t };
169        if begin + amount <= end {
170            return Ok((starts, hours(begin + amount)));
171        }
172        let target = before + (begin - start) + amount;
173        let finish = self.instant(self.through.below(target), target)?;
174        Ok((starts, hours(finish.max(begin))))
175    }
176
177    /// The latest start from which `hours` of work end by `align_back(t)`: equal to `align_back`
178    /// then `sub`, with one lookup when the work fits in one span.
179    pub fn retreat(&self, t: f64, hours_of_work: f64) -> Result<f64, BeyondCalendar> {
180        if self.always {
181            return Ok(t - hours_of_work.max(0.0));
182        }
183        let at = self.at(t)?;
184        let index = self
185            .starts
186            .through(at)
187            .checked_sub(1)
188            .ok_or(BeyondCalendar)?;
189        let ((start, end), before) = self.span(index)?;
190        let to = at.min(end);
191        let amount = millis(hours_of_work).ok_or(BeyondCalendar)?;
192        if amount <= 0 {
193            return Ok(if end < at { hours(end) } else { t });
194        }
195        if to - amount > start {
196            return Ok(hours(to - amount));
197        }
198        let target = before + (to - start) - amount;
199        if target < 0 {
200            return Err(BeyondCalendar);
201        }
202        let begin = self.instant(self.through.through(target), target)?;
203        Ok(hours(begin.min(to)))
204    }
205
206    /// Latest moment at or before `t` whose `align` is not after `t`.
207    pub fn align_back(&self, t: f64) -> Result<f64, BeyondCalendar> {
208        if self.always {
209            return Ok(t);
210        }
211        let at = self.at(t)?;
212        let index = self
213            .starts
214            .through(at)
215            .checked_sub(1)
216            .ok_or(BeyondCalendar)?;
217        let ((_, end), _) = self.span(index)?;
218        Ok(if end < at { hours(end) } else { t })
219    }
220
221    /// Working hours between `a` and a later `b`.
222    pub fn between(&self, a: f64, b: f64) -> Result<f64, BeyondCalendar> {
223        if self.always {
224            return Ok((b - a).max(0.0));
225        }
226        let worked = self.worked(self.at(b)?)? - self.worked(self.at(a)?)?;
227        Ok(hours(worked.max(0)))
228    }
229}