Skip to main content

mpl_lang/
time.rs

1//! Time relateded types for `MPL`
2use std::{
3    cmp,
4    num::{NonZeroU32, NonZeroU64, TryFromIntError},
5    ops::{self, Add, AddAssign, Div, Mul, Rem, Sub},
6    time::Duration,
7};
8
9use chrono::{DateTime, Utc};
10
11/// Resolution
12#[derive(
13    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, serde::Serialize,
14)]
15#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
16pub struct Resolution(NonZeroU64);
17
18impl Resolution {
19    /// The largest value that can be represented
20    pub const MAX: Self = Resolution(NonZeroU64::MAX);
21    /// Translates the resolution to a `Timestamp`.
22    #[must_use]
23    pub fn as_timestamp(self) -> Timestamp {
24        Timestamp(self.0.get())
25    }
26
27    /// trues to convert the secs into a u32 index
28    pub fn as_idx(self) -> Result<u32, TryFromIntError> {
29        self.try_into()
30    }
31
32    /// translates the resolution into a float
33    #[must_use]
34    #[allow(clippy::cast_precision_loss)]
35    pub fn as_f64(self) -> f64 {
36        self.0.get() as f64
37    }
38
39    /// Creates a new `Resolution` from a number of seconds.
40    pub fn secs(s: u64) -> Result<Self, ResolutionError> {
41        NonZeroU64::new(s)
42            .map(Resolution)
43            .ok_or(ResolutionError::ZeroResolution)
44    }
45
46    /// The resolution in seconds as u64.
47    #[must_use]
48    pub fn as_u64(self) -> u64 {
49        self.0.get()
50    }
51
52    /// Aligns this resolution up to match the given `Resolution`.
53    #[must_use]
54    pub fn align_up_to(&self, align_up_to: Resolution) -> Self {
55        Self(self.0.div_ceil(align_up_to.0).saturating_mul(align_up_to.0))
56    }
57}
58
59impl PartialEq<u64> for Resolution {
60    fn eq(&self, other: &u64) -> bool {
61        self.0.get() == *other
62    }
63}
64impl TryInto<u32> for Resolution {
65    type Error = TryFromIntError;
66
67    fn try_into(self) -> Result<u32, Self::Error> {
68        self.0.get().try_into()
69    }
70}
71
72impl Default for Resolution {
73    fn default() -> Self {
74        Self(NonZeroU64::MIN)
75    }
76}
77
78impl Rem for Resolution {
79    type Output = u64;
80
81    fn rem(self, rhs: Self) -> Self::Output {
82        self.0.get() % rhs.0.get()
83    }
84}
85
86impl Div for Resolution {
87    type Output = u64;
88
89    fn div(self, rhs: Self) -> u64 {
90        self.0.get() / rhs.0.get()
91    }
92}
93
94impl Div<Resolution> for Timestamp {
95    type Output = Timestamp;
96
97    fn div(self, rhs: Resolution) -> Timestamp {
98        Timestamp(self.0 / rhs.0.get())
99    }
100}
101
102impl std::fmt::Display for Resolution {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(f, "{}s", self.0.get())
105    }
106}
107
108/// Errors when dealing with resolutions.
109#[derive(Debug, thiserror::Error, Clone, Copy)]
110pub enum ResolutionError {
111    /// The resolution cannot be zero.
112    #[error("resolution cannot be zero")]
113    ZeroResolution,
114}
115
116impl TryInto<i64> for Resolution {
117    type Error = TryFromIntError;
118
119    fn try_into(self) -> Result<i64, Self::Error> {
120        self.0.get().try_into()
121    }
122}
123
124impl PartialEq<Timestamp> for Resolution {
125    fn eq(&self, other: &Timestamp) -> bool {
126        self.0.get() == other.0
127    }
128}
129
130impl PartialOrd<Timestamp> for Resolution {
131    fn partial_cmp(&self, other: &Timestamp) -> Option<std::cmp::Ordering> {
132        Some(self.0.get().cmp(&other.0))
133    }
134}
135
136impl Mul<NonZeroU32> for Resolution {
137    type Output = Self;
138
139    fn mul(self, rhs: NonZeroU32) -> Self::Output {
140        Resolution(self.0.saturating_mul(NonZeroU64::from(rhs)))
141    }
142}
143
144impl PartialEq<Resolution> for Timestamp {
145    fn eq(&self, other: &Resolution) -> bool {
146        self.0 == other.0.get()
147    }
148}
149
150impl PartialOrd<Resolution> for Timestamp {
151    fn partial_cmp(&self, other: &Resolution) -> Option<std::cmp::Ordering> {
152        Some(self.0.cmp(&other.0.get()))
153    }
154}
155
156impl From<Resolution> for NonZeroU64 {
157    fn from(value: Resolution) -> Self {
158        value.0
159    }
160}
161
162impl TryFrom<Resolution> for NonZeroU32 {
163    type Error = TryFromIntError;
164
165    fn try_from(value: Resolution) -> Result<Self, Self::Error> {
166        value.0.try_into()
167    }
168}
169
170/// Timestamp
171
172#[derive(
173    Debug,
174    Clone,
175    Copy,
176    PartialEq,
177    Eq,
178    PartialOrd,
179    Ord,
180    Default,
181    serde::Deserialize,
182    serde::Serialize,
183)]
184#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
185pub struct Timestamp(pub u64);
186
187impl From<Timestamp> for DateTime<Utc> {
188    #[allow(clippy::cast_possible_wrap)]
189    fn from(value: Timestamp) -> Self {
190        DateTime::<Utc>::from_timestamp(value.0 as i64, 0).unwrap_or_default()
191    }
192}
193
194impl TryFrom<Timestamp> for i64 {
195    type Error = TryFromIntError;
196
197    #[allow(clippy::cast_possible_wrap)]
198    fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
199        value.0.try_into()
200    }
201}
202
203impl Add<usize> for Timestamp {
204    type Output = Timestamp;
205
206    fn add(self, rhs: usize) -> Self::Output {
207        Timestamp(self.0 + rhs as u64)
208    }
209}
210
211impl Add<u64> for Timestamp {
212    type Output = Timestamp;
213
214    fn add(self, rhs: u64) -> Self::Output {
215        Timestamp(self.0 + rhs)
216    }
217}
218
219impl Sub<Timestamp> for u64 {
220    type Output = u64;
221
222    fn sub(self, rhs: Timestamp) -> Self::Output {
223        self - rhs.0
224    }
225}
226
227impl Mul<Resolution> for Timestamp {
228    type Output = Timestamp;
229
230    fn mul(self, rhs: Resolution) -> Self::Output {
231        Timestamp(self.0 * rhs.0.get())
232    }
233}
234
235impl Rem<Resolution> for Timestamp {
236    type Output = Timestamp;
237
238    fn rem(self, rhs: Resolution) -> Self::Output {
239        Timestamp(self.0 % rhs.0.get())
240    }
241}
242
243/// Errors from converting a `Timestamp` to an index.
244#[derive(Debug, thiserror::Error)]
245pub enum IndexError {
246    /// Index is too large and cannot be converted to u32.
247    #[error("idx too large: {0}")]
248    IdxTooLarge(#[from] std::num::TryFromIntError),
249}
250
251impl Timestamp {
252    /// The largest value that can be represented
253    pub const MAX: Timestamp = Timestamp(u64::MAX);
254
255    /// The smallest value that can be represented
256    pub const MIN: Timestamp = Timestamp(u64::MIN);
257
258    /// Checks if the timestamp is a multiple of the given resolution.
259    #[must_use]
260    pub fn is_multiple_of(self, other: Resolution) -> bool {
261        self.0.is_multiple_of(other.0.get())
262    }
263
264    /// trues to convert the secs into a u32 index
265    pub fn as_idx(self) -> Result<u32, IndexError> {
266        self.try_into().map_err(IndexError::IdxTooLarge)
267    }
268
269    /// Creates a new `Timestamp` from a number of seconds.
270    #[must_use]
271    pub fn new(s: u64) -> Self {
272        Self(s)
273    }
274
275    #[allow(clippy::self_named_constructors)] // we want to be explicit
276    /// Creates a new `Timestamp` from a number of seconds.
277    #[must_use]
278    pub fn secs(s: u64) -> Self {
279        Self(s)
280    }
281
282    /// Creates a new `Timestamp` from a number of minutes.
283    #[must_use]
284    pub fn mins(m: u64) -> Self {
285        Self::secs(m * 60)
286    }
287
288    /// Creates a new `Timestamp` from a number of hours.
289    #[must_use]
290    pub fn hours(h: u64) -> Self {
291        Self::mins(h * 60)
292    }
293
294    /// Creates a new `Timestamp` from a number of days.
295    #[must_use]
296    pub fn days(d: u64) -> Self {
297        Self::hours(d * 24)
298    }
299
300    /// Creates a new `Timestamp` from a number of weeks.
301    #[must_use]
302    pub fn weeks(w: u64) -> Self {
303        Self::days(w * 7)
304    }
305
306    /// Returns seconds as u64
307    #[must_use]
308    pub fn as_secs(&self) -> u64 {
309        self.0
310    }
311
312    /// Returns the difference between two timestamps as a `Resolution`.
313    /// Returns None if the timestamps are equal.
314    #[must_use]
315    pub fn diff(self, other: Self) -> Option<Resolution> {
316        if self.0 < other.0 {
317            Some(Resolution((other.0 - self.0).try_into().ok()?))
318        } else {
319            Some(Resolution((self.0 - other.0).try_into().ok()?))
320        }
321    }
322
323    /// Aligns the timestamp down to a given resolution.
324    #[must_use]
325    pub fn align_down(self, resolution: Resolution) -> Self {
326        Timestamp((self.0 / resolution.as_u64()) * resolution.as_u64())
327    }
328
329    /// Aligns the timestamp up to a given resolution.
330    #[must_use]
331    pub fn align_up(self, resolution: Resolution) -> Self {
332        Timestamp(self.0.div_ceil(resolution.as_u64()) * resolution.as_u64())
333    }
334
335    /// Get a range between two timestamps (works like start..end).
336    pub fn range(start: Self, end: Self) -> impl Iterator<Item = Self> {
337        // TODO(arne): Once iter::step is stabilized, implement that and delete
338        //             this function.
339        (start.0..end.0).map(Timestamp)
340    }
341}
342
343impl std::ops::Rem for Timestamp {
344    type Output = Self;
345
346    fn rem(self, rhs: Self) -> Self::Output {
347        Timestamp(self.0 % rhs.0)
348    }
349}
350
351impl From<DateTime<Utc>> for Timestamp {
352    fn from(d: DateTime<Utc>) -> Self {
353        Self(u64::try_from(d.timestamp()).unwrap_or_default())
354    }
355}
356impl PartialEq<u64> for Timestamp {
357    fn eq(&self, other: &u64) -> bool {
358        self.0 == *other
359    }
360}
361
362impl std::fmt::Display for Timestamp {
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        write!(f, "{}s", self.0)
365    }
366}
367
368impl Add for Timestamp {
369    type Output = Self;
370
371    fn add(self, rhs: Self) -> Self::Output {
372        Timestamp(self.0 + rhs.0)
373    }
374}
375
376impl Add<Resolution> for Timestamp {
377    type Output = Self;
378
379    fn add(self, rhs: Resolution) -> Self::Output {
380        Timestamp(self.0 + rhs.0.get())
381    }
382}
383
384impl Add<u32> for Timestamp {
385    type Output = Self;
386
387    fn add(self, rhs: u32) -> Self::Output {
388        Timestamp(self.0 + u64::from(rhs))
389    }
390}
391
392impl AddAssign<Resolution> for Timestamp {
393    fn add_assign(&mut self, rhs: Resolution) {
394        self.0 += rhs.0.get();
395    }
396}
397
398impl TryFrom<Timestamp> for u32 {
399    type Error = TryFromIntError;
400
401    fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
402        value.0.try_into()
403    }
404}
405impl TryFrom<i64> for Timestamp {
406    type Error = TryFromIntError;
407
408    fn try_from(value: i64) -> Result<Self, Self::Error> {
409        Ok(Timestamp(value.try_into()?))
410    }
411}
412impl From<u32> for Timestamp {
413    fn from(value: u32) -> Self {
414        Timestamp(u64::from(value))
415    }
416}
417
418impl Mul<usize> for Timestamp {
419    type Output = Self;
420
421    fn mul(self, rhs: usize) -> Self::Output {
422        Timestamp(self.0 * rhs as u64)
423    }
424}
425
426impl Mul<u32> for Timestamp {
427    type Output = Self;
428
429    fn mul(self, rhs: u32) -> Self::Output {
430        Timestamp(self.0 * u64::from(rhs))
431    }
432}
433
434impl Sub for Timestamp {
435    type Output = Self;
436
437    fn sub(self, rhs: Self) -> Self::Output {
438        Timestamp(self.0 - rhs.0)
439    }
440}
441impl Timestamp {
442    /// Saturating subtraction of two timestamps.
443    #[must_use]
444    pub fn saturating_sub(self, rhs: Self) -> Self {
445        Timestamp(self.0.saturating_sub(rhs.0))
446    }
447}
448
449impl Div for Timestamp {
450    type Output = Self;
451
452    fn div(self, rhs: Self) -> Self::Output {
453        Timestamp(self.0 / rhs.0)
454    }
455}
456
457impl AddAssign for Timestamp {
458    fn add_assign(&mut self, rhs: Self) {
459        self.0 += rhs.0;
460    }
461}
462
463impl AddAssign<u32> for Timestamp {
464    fn add_assign(&mut self, rhs: u32) {
465        self.0 += u64::from(rhs);
466    }
467}
468
469impl From<Duration> for Timestamp {
470    fn from(d: Duration) -> Self {
471        Self(d.as_secs())
472    }
473}
474
475/// Returned from methods of `Timerange`.
476#[derive(Debug, thiserror::Error)]
477pub enum TimerangeError {
478    /// Returned from `Timerange::new` if end is before start.
479    #[error("end is before start, start: {start}, end: {end}")]
480    EndBeforeStart {
481        /// The given start.
482        start: Timestamp,
483        /// The given end.
484        end: Timestamp,
485    },
486}
487
488/// A (half-open) date range  bounded inclusively below and exclusively above.
489///
490/// We cannot use `std::ops::Range` because the chunking logic is experimental.
491#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
492pub struct Timerange {
493    /// The lower bound of the range (inclusive).
494    start: Timestamp,
495    /// The upper bound of the range (exclusive).
496    end: Timestamp,
497}
498
499/// divides a time range giving a resolution as a result
500/// the minimum resolution is 1 second
501impl ops::Div<u128> for Timerange {
502    type Output = Resolution;
503
504    fn div(self, rhs: u128) -> Self::Output {
505        Resolution::secs(
506            // we can never overflow as end-start is a u64 (Timestamp is a u64)
507            u64::try_from(u128::from(self.end.0 - self.start.0) / rhs).unwrap_or(u64::MAX),
508        )
509        .unwrap_or_default()
510    }
511}
512
513impl Timerange {
514    /// Create a new `Timerange`.
515    pub fn new(start: Timestamp, end: Timestamp) -> Result<Timerange, TimerangeError> {
516        if start > end {
517            return Err(TimerangeError::EndBeforeStart { start, end });
518        }
519
520        Ok(Self { start, end })
521    }
522
523    /// Return the start value.
524    #[must_use]
525    pub fn start(&self) -> Timestamp {
526        self.start
527    }
528
529    /// Return the end value.
530    #[must_use]
531    pub fn end(&self) -> Timestamp {
532        self.end
533    }
534
535    /// Return the duration of the range.
536    #[must_use]
537    pub fn duration(&self) -> u64 {
538        self.end.0 - self.start.0
539    }
540
541    /// Iterate over chunks of at most `chunk_size`.
542    pub fn split_by_resolution(&self, resolution: Resolution) -> impl Iterator<Item = Timerange> {
543        TimerangeIterator {
544            rolling_start: self.start,
545            end: self.end,
546            step: resolution,
547        }
548    }
549
550    /// Are the ranges overlapping?
551    #[must_use]
552    pub fn is_overlapping(&self, other: &Timerange) -> bool {
553        // TODO(arne): switch to `std::ops::Range::is_overlapping`,
554        // when it's stable
555        //
556        // the general idea is that there are two conditions where
557        // ranges _don't_ overlap:
558        //
559        // condition a: [ other ] [ self ]
560        // true if self.start > other.end
561        //
562        // condition b: [ self ] [ other]
563        // true if other.start > self.end
564        //
565        // this means it overlaps if:
566        // !(self.start > other.end) && !(other.start > self.end)
567        //
568        // if we inverse this boolean logic, we get the same logic
569        // that `std::ops::Range::is_overlapping` does:
570        (self.start < other.end) && (other.start < self.end)
571    }
572}
573
574struct TimerangeIterator {
575    rolling_start: Timestamp,
576    end: Timestamp,
577    step: Resolution,
578}
579
580impl Iterator for TimerangeIterator {
581    type Item = Timerange;
582
583    fn next(&mut self) -> Option<Self::Item> {
584        if self.rolling_start >= self.end {
585            return None; // we're done iterating
586        }
587
588        let start = self.rolling_start;
589        let end = cmp::min(self.rolling_start + self.step.as_u64(), self.end);
590
591        // move rolling start to the next window, but add one second so there's
592        // no overlap
593        self.rolling_start = Timestamp(self.rolling_start.0 + self.step.as_u64());
594
595        Timerange::new(start, end).ok()
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602    use test_case::test_case;
603
604    #[test]
605    fn test_timestamp_add() {
606        let t1 = Timestamp::from(10);
607        let t2 = Timestamp::from(5);
608        let result = t1 + t2;
609        assert_eq!(result, Timestamp::from(15));
610    }
611
612    #[test]
613    fn test_timestamp_sub() {
614        let t1 = Timestamp::from(10);
615        let t2 = Timestamp::from(5);
616        let result = t1 - t2;
617        assert_eq!(result, Timestamp::from(5));
618    }
619
620    #[test]
621    fn test_timestamp_mul() {
622        let t = Timestamp::from(10);
623        let result = t * Resolution::secs(2).expect("2 is zero");
624        assert_eq!(result, Timestamp::from(20));
625    }
626
627    #[test]
628    fn test_timestamp_div() {
629        let t1 = Timestamp::from(10);
630        let t2 = Timestamp::from(5);
631        let result = t1 / t2;
632        assert_eq!(result, Timestamp::from(2));
633    }
634
635    #[test]
636    fn test_timestamp_add_assign() {
637        let mut t = Timestamp::from(10);
638        t += Timestamp::from(5);
639        assert_eq!(t, Timestamp::from(15));
640    }
641
642    #[test]
643    fn test_timestamp_add_assign_u32() {
644        let mut t = Timestamp::from(10);
645        t += 5;
646        assert_eq!(t, Timestamp::from(15));
647    }
648
649    #[test]
650    fn test_timestamp_from_duration() {
651        let duration = Duration::from_secs(10);
652        let timestamp = Timestamp::from(duration);
653        assert_eq!(timestamp, Timestamp::from(10));
654    }
655
656    #[test]
657    fn test_align_down() {
658        let t = Timestamp::from(10);
659        let result = t.align_down(Resolution::secs(3).expect("3 is zero"));
660        assert_eq!(result, Timestamp::from(9));
661    }
662
663    #[test]
664    fn test_align_up() {
665        let t = Timestamp::from(10);
666        let result = t.align_up(Resolution::secs(3).expect("3 is zero"));
667        assert_eq!(result, Timestamp::from(12));
668    }
669
670    #[test_case("already aligned", 6, 3, 6)]
671    #[test_case("other > self", 3, 5, 5)]
672    #[test_case("align up", 11, 4, 12)]
673    #[test_case("million", 1_230_978, 10, 1_230_980)]
674    fn align_up_to(
675        name: &str,
676        res: u64,
677        align_up_to: u64,
678        expected: u64,
679    ) -> Result<(), Box<dyn std::error::Error>> {
680        let expected = Resolution::secs(expected)?;
681        let res = Resolution::secs(res)?;
682        let align_up_to = Resolution::secs(align_up_to)?;
683
684        assert_eq!(expected, res.align_up_to(align_up_to), "{name}",);
685        Ok(())
686    }
687}