1use 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#[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 pub const MAX: Self = Resolution(NonZeroU64::MAX);
21 #[must_use]
23 pub fn as_timestamp(self) -> Timestamp {
24 Timestamp(self.0.get())
25 }
26
27 pub fn as_idx(self) -> Result<u32, TryFromIntError> {
29 self.try_into()
30 }
31
32 #[must_use]
34 #[allow(clippy::cast_precision_loss)]
35 pub fn as_f64(self) -> f64 {
36 self.0.get() as f64
37 }
38
39 pub fn secs(s: u64) -> Result<Self, ResolutionError> {
41 NonZeroU64::new(s)
42 .map(Resolution)
43 .ok_or(ResolutionError::ZeroResolution)
44 }
45
46 #[must_use]
48 pub fn as_u64(self) -> u64 {
49 self.0.get()
50 }
51
52 #[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#[derive(Debug, thiserror::Error, Clone, Copy)]
110pub enum ResolutionError {
111 #[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#[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#[derive(Debug, thiserror::Error)]
245pub enum IndexError {
246 #[error("idx too large: {0}")]
248 IdxTooLarge(#[from] std::num::TryFromIntError),
249}
250
251impl Timestamp {
252 pub const MAX: Timestamp = Timestamp(u64::MAX);
254
255 pub const MIN: Timestamp = Timestamp(u64::MIN);
257
258 #[must_use]
260 pub fn is_multiple_of(self, other: Resolution) -> bool {
261 self.0.is_multiple_of(other.0.get())
262 }
263
264 pub fn as_idx(self) -> Result<u32, IndexError> {
266 self.try_into().map_err(IndexError::IdxTooLarge)
267 }
268
269 #[must_use]
271 pub fn new(s: u64) -> Self {
272 Self(s)
273 }
274
275 #[allow(clippy::self_named_constructors)] #[must_use]
278 pub fn secs(s: u64) -> Self {
279 Self(s)
280 }
281
282 #[must_use]
284 pub fn mins(m: u64) -> Self {
285 Self::secs(m * 60)
286 }
287
288 #[must_use]
290 pub fn hours(h: u64) -> Self {
291 Self::mins(h * 60)
292 }
293
294 #[must_use]
296 pub fn days(d: u64) -> Self {
297 Self::hours(d * 24)
298 }
299
300 #[must_use]
302 pub fn weeks(w: u64) -> Self {
303 Self::days(w * 7)
304 }
305
306 #[must_use]
308 pub fn as_secs(&self) -> u64 {
309 self.0
310 }
311
312 #[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 #[must_use]
325 pub fn align_down(self, resolution: Resolution) -> Self {
326 Timestamp((self.0 / resolution.as_u64()) * resolution.as_u64())
327 }
328
329 #[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 pub fn range(start: Self, end: Self) -> impl Iterator<Item = Self> {
337 (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 #[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#[derive(Debug, thiserror::Error)]
477pub enum TimerangeError {
478 #[error("end is before start, start: {start}, end: {end}")]
480 EndBeforeStart {
481 start: Timestamp,
483 end: Timestamp,
485 },
486}
487
488#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
492pub struct Timerange {
493 start: Timestamp,
495 end: Timestamp,
497}
498
499impl ops::Div<u128> for Timerange {
502 type Output = Resolution;
503
504 fn div(self, rhs: u128) -> Self::Output {
505 Resolution::secs(
506 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 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 #[must_use]
525 pub fn start(&self) -> Timestamp {
526 self.start
527 }
528
529 #[must_use]
531 pub fn end(&self) -> Timestamp {
532 self.end
533 }
534
535 #[must_use]
537 pub fn duration(&self) -> u64 {
538 self.end.0 - self.start.0
539 }
540
541 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 #[must_use]
552 pub fn is_overlapping(&self, other: &Timerange) -> bool {
553 (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; }
587
588 let start = self.rolling_start;
589 let end = cmp::min(self.rolling_start + self.step.as_u64(), self.end);
590
591 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}