utctimestamp/
lib.rs

1//! Simple & fast UTC time types.
2//!
3//! While [chrono](https://crates.io/crates/chrono) is great for dealing with time
4//! in most cases, its 96-bit integer design can be costly when processing and storing
5//! large amounts of timestamp data.
6//!
7//! This lib solves this problem by providing very simple UTC timestamps that can be
8//! converted from and into their corresponding chrono counterpart using Rust's
9//! `From` and `Into` traits. chrono is then used for all things that aren't expected
10//! to occur in big batches, such as formatting and displaying the timestamps.
11
12use core::{fmt, ops};
13
14#[cfg(feature = "serde-support")]
15use serde::{Deserialize, Serialize};
16
17// ============================================================================================== //
18// [UTC timestamp]                                                                                //
19// ============================================================================================== //
20
21/// Represents a dumb but fast UTC timestamp.
22#[repr(transparent)]
23#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
24#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
25pub struct UtcTimeStamp(i64);
26
27/// Display timestamp using chrono.
28impl fmt::Display for UtcTimeStamp {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        chrono::DateTime::<chrono::Utc>::from(*self).fmt(f)
31    }
32}
33
34impl fmt::Debug for UtcTimeStamp {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(f, "UtcTimeStamp({})", self.0)
37    }
38}
39
40/// Create a dumb timestamp from a chrono date time object.
41impl From<chrono::DateTime<chrono::Utc>> for UtcTimeStamp {
42    fn from(other: chrono::DateTime<chrono::Utc>) -> Self {
43        Self(other.timestamp_millis())
44    }
45}
46
47/// Create a chrono date time object from a dumb timestamp.
48impl From<UtcTimeStamp> for chrono::DateTime<chrono::Utc> {
49    fn from(other: UtcTimeStamp) -> Self {
50        let sec = other.0 / 1000;
51        let ns = (other.0 % 1000 * 1_000_000) as u32;
52        let naive = chrono::NaiveDateTime::from_timestamp(sec, ns);
53        chrono::DateTime::<chrono::Utc>::from_utc(naive, chrono::Utc)
54    }
55}
56
57impl UtcTimeStamp {
58    /// Initialize a timestamp with 0, `1970-01-01 00:00:00 UTC`.
59    #[inline]
60    pub const fn zero() -> Self {
61        UtcTimeStamp(0)
62    }
63
64    /// Initialize a timestamp using the current local time converted to UTC.
65    pub fn now() -> Self {
66        chrono::Utc::now().into()
67    }
68
69    /// Explicit conversion from `i64`.
70    #[inline]
71    pub const fn from_milliseconds(int: i64) -> Self {
72        UtcTimeStamp(int)
73    }
74
75    /// Explicit conversion from `i64` seconds.
76    #[inline]
77    pub const fn from_seconds(int: i64) -> Self {
78        UtcTimeStamp(int * 1000)
79    }
80
81    /// Explicit conversion to `i64`.
82    #[inline]
83    pub const fn as_milliseconds(self) -> i64 {
84        self.0
85    }
86
87    /// Align a timestamp to a given frequency.
88    pub const fn align_to(self, freq: TimeDelta) -> UtcTimeStamp {
89        self.align_to_anchored(UtcTimeStamp::zero(), freq)
90    }
91
92    /// Align a timestamp to a given frequency, with a time anchor.
93    pub const fn align_to_anchored(self, anchor: UtcTimeStamp, freq: TimeDelta) -> UtcTimeStamp {
94        UtcTimeStamp((self.0 - anchor.0) / freq.0 * freq.0 + anchor.0)
95    }
96
97    /// Check whether the timestamp is 0 (`1970-01-01 00:00:00 UTC`).
98    #[inline]
99    pub const fn is_zero(self) -> bool {
100        self.0 == 0
101    }
102}
103
104/// Calculate the timestamp advanced by a timedelta.
105impl ops::Add<TimeDelta> for UtcTimeStamp {
106    type Output = UtcTimeStamp;
107
108    fn add(self, rhs: TimeDelta) -> Self::Output {
109        UtcTimeStamp(self.0 + rhs.0)
110    }
111}
112
113impl ops::AddAssign<TimeDelta> for UtcTimeStamp {
114    fn add_assign(&mut self, rhs: TimeDelta) {
115        *self = *self + rhs;
116    }
117}
118
119/// Calculate the timestamp lessened by a timedelta.
120impl ops::Sub<TimeDelta> for UtcTimeStamp {
121    type Output = UtcTimeStamp;
122
123    fn sub(self, rhs: TimeDelta) -> Self::Output {
124        UtcTimeStamp(self.0 - rhs.0)
125    }
126}
127
128impl ops::SubAssign<TimeDelta> for UtcTimeStamp {
129    fn sub_assign(&mut self, rhs: TimeDelta) {
130        *self = *self - rhs;
131    }
132}
133
134/// Calculate signed timedelta between two timestamps.
135impl ops::Sub<UtcTimeStamp> for UtcTimeStamp {
136    type Output = TimeDelta;
137
138    fn sub(self, rhs: UtcTimeStamp) -> Self::Output {
139        TimeDelta(self.0 - rhs.0)
140    }
141}
142
143// /// How far away is the timestamp from being aligned to the given timedelta?
144// impl ops::Rem<TimeDelta> for UtcTimeStamp {
145//     type Output = TimeDelta;
146//
147//     fn rem(self, rhs: TimeDelta) -> Self::Output {
148//         TimeDelta(self.0 % rhs.0)
149//     }
150// }
151
152// ============================================================================================== //
153// [TimeDelta]                                                                                    //
154// ============================================================================================== //
155
156/// Millisecond precision time delta.
157#[repr(transparent)]
158#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
159#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
160pub struct TimeDelta(i64);
161
162/// Display timedelta using chrono.
163impl fmt::Display for TimeDelta {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        chrono::Duration::from(*self).fmt(f)
166    }
167}
168
169/// Create a simple timedelta from a chrono duration.
170impl From<chrono::Duration> for TimeDelta {
171    fn from(other: chrono::Duration) -> Self {
172        Self(other.num_milliseconds())
173    }
174}
175
176/// Create a chrono duration from a simple timedelta.
177impl From<TimeDelta> for chrono::Duration {
178    fn from(other: TimeDelta) -> Self {
179        chrono::Duration::milliseconds(other.0)
180    }
181}
182
183impl ops::Add<TimeDelta> for TimeDelta {
184    type Output = TimeDelta;
185
186    fn add(self, rhs: TimeDelta) -> Self::Output {
187        TimeDelta(self.0 + rhs.0)
188    }
189}
190
191impl ops::Sub<TimeDelta> for TimeDelta {
192    type Output = TimeDelta;
193
194    fn sub(self, rhs: TimeDelta) -> Self::Output {
195        TimeDelta(self.0 - rhs.0)
196    }
197}
198
199/// Multiply the delta to be n times as long.
200impl ops::Mul<i64> for TimeDelta {
201    type Output = TimeDelta;
202
203    fn mul(self, rhs: i64) -> Self::Output {
204        TimeDelta(self.0 * rhs)
205    }
206}
207
208/// Shorten the delta by a given factor. Integer div.
209impl ops::Div<i64> for TimeDelta {
210    type Output = TimeDelta;
211
212    fn div(self, rhs: i64) -> Self::Output {
213        TimeDelta(self.0 / rhs)
214    }
215}
216
217/// How many times does the timestamp fit into another?
218impl ops::Div<TimeDelta> for TimeDelta {
219    type Output = i64;
220
221    fn div(self, rhs: TimeDelta) -> Self::Output {
222        self.0 / rhs.0
223    }
224}
225
226/// How far away is the delta from being aligned to another delta?
227impl ops::Rem<TimeDelta> for TimeDelta {
228    type Output = TimeDelta;
229
230    fn rem(self, rhs: TimeDelta) -> Self::Output {
231        TimeDelta(self.0 % rhs.0)
232    }
233}
234
235/// Explicit conversion from and to `i64`.
236impl TimeDelta {
237    #[inline]
238    pub const fn zero() -> Self {
239        TimeDelta(0)
240    }
241
242    #[inline]
243    pub const fn from_hours(int: i64) -> Self {
244        TimeDelta::from_minutes(int * 60)
245    }
246
247    #[inline]
248    pub const fn from_minutes(int: i64) -> Self {
249        TimeDelta::from_seconds(int * 60)
250    }
251
252    #[inline]
253    pub const fn from_seconds(int: i64) -> Self {
254        TimeDelta(int * 1000)
255    }
256
257    #[inline]
258    pub const fn from_milliseconds(int: i64) -> Self {
259        TimeDelta(int)
260    }
261
262    #[inline]
263    pub const fn as_milliseconds(self) -> i64 {
264        self.0
265    }
266
267    /// Check whether the timedelta is 0.
268    #[inline]
269    pub const fn is_zero(self) -> bool {
270        self.0 == 0
271    }
272
273    /// Returns `true` if the timedelta is positive and
274    /// `false` if it is zero or negative.
275    #[inline]
276    pub const fn is_positive(self) -> bool {
277        self.0 > 0
278    }
279
280    /// Returns `true` if the timedelta is negative and
281    /// `false` if it is zero or positive.
282    #[inline]
283    pub const fn is_negative(self) -> bool {
284        self.0 < 0
285    }
286}
287
288// ============================================================================================== //
289// [TimeRange]                                                                                    //
290// ============================================================================================== //
291
292/// An iterator looping over dates given a time delta as step.
293///
294/// The range is either right open or right closed depending on the
295/// constructor chosen, but always left closed.
296///
297/// Examples:
298///
299/// ```
300/// use utctimestamp::TimeRange;
301/// use chrono::{offset::TimeZone, Duration, Utc};
302///
303/// let start = Utc.ymd(2019, 4, 14).and_hms(0, 0, 0);
304/// let end = Utc.ymd(2019, 4, 16).and_hms(0, 0, 0);
305/// let step = Duration::hours(12);
306/// let tr: Vec<_> = TimeRange::right_closed(start, end, step).collect();
307///
308/// assert_eq!(tr, vec![
309///     Utc.ymd(2019, 4, 14).and_hms(0, 0, 0).into(),
310///     Utc.ymd(2019, 4, 14).and_hms(12, 0, 0).into(),
311///     Utc.ymd(2019, 4, 15).and_hms(0, 0, 0).into(),
312///     Utc.ymd(2019, 4, 15).and_hms(12, 0, 0).into(),
313///     Utc.ymd(2019, 4, 16).and_hms(0, 0, 0).into(),
314/// ]);
315/// ```
316#[derive(Debug)]
317pub struct TimeRange {
318    cur: UtcTimeStamp,
319    end: UtcTimeStamp,
320    step: TimeDelta,
321    right_closed: bool,
322}
323
324impl TimeRange {
325    /// Create a time range that includes the end date.
326    pub fn right_closed(
327        start: impl Into<UtcTimeStamp>,
328        end: impl Into<UtcTimeStamp>,
329        step: impl Into<TimeDelta>,
330    ) -> Self {
331        TimeRange {
332            cur: start.into(),
333            end: end.into(),
334            step: step.into(),
335            right_closed: true,
336        }
337    }
338
339    /// Create a time range that excludes the end date.
340    pub fn right_open(
341        start: impl Into<UtcTimeStamp>,
342        end: impl Into<UtcTimeStamp>,
343        step: impl Into<TimeDelta>,
344    ) -> Self {
345        TimeRange {
346            cur: start.into(),
347            end: end.into(),
348            step: step.into(),
349            right_closed: false,
350        }
351    }
352}
353
354impl Iterator for TimeRange {
355    type Item = UtcTimeStamp;
356
357    fn next(&mut self) -> Option<Self::Item> {
358        let exhausted = if self.right_closed {
359            self.cur > self.end
360        } else {
361            self.cur >= self.end
362        };
363
364        if exhausted {
365            None
366        } else {
367            let cur = self.cur;
368            self.cur += self.step;
369            Some(cur)
370        }
371    }
372}
373
374// ============================================================================================== //
375// [Tests]                                                                                        //
376// ============================================================================================== //
377
378#[cfg(test)]
379mod tests {
380    use crate::*;
381    use chrono::{offset::TimeZone, Duration, Utc};
382
383    #[test]
384    fn open_time_range() {
385        let start = Utc.ymd(2019, 4, 14).and_hms(0, 0, 0);
386        let end = Utc.ymd(2019, 4, 16).and_hms(0, 0, 0);
387        let step = Duration::hours(12);
388        let tr: Vec<_> = Iterator::collect(TimeRange::right_closed(start, end, step));
389        assert_eq!(tr, vec![
390            Utc.ymd(2019, 4, 14).and_hms(0, 0, 0).into(),
391            Utc.ymd(2019, 4, 14).and_hms(12, 0, 0).into(),
392            Utc.ymd(2019, 4, 15).and_hms(0, 0, 0).into(),
393            Utc.ymd(2019, 4, 15).and_hms(12, 0, 0).into(),
394            Utc.ymd(2019, 4, 16).and_hms(0, 0, 0).into(),
395        ]);
396    }
397
398    #[test]
399    fn timestamp_and_delta_vs_chrono() {
400        let c_dt = Utc.ymd(2019, 3, 13).and_hms(16, 14, 9);
401        let c_td = Duration::milliseconds(123456);
402
403        let my_dt = UtcTimeStamp::from(c_dt.clone());
404        let my_td = TimeDelta::from_milliseconds(123456);
405        assert_eq!(TimeDelta::from(c_td.clone()), my_td);
406
407        let c_result = c_dt + c_td * 555;
408        let my_result = my_dt + my_td * 555;
409        assert_eq!(UtcTimeStamp::from(c_result.clone()), my_result);
410    }
411
412    #[test]
413    fn timestamp_ord_eq() {
414        let ts1: UtcTimeStamp = UtcTimeStamp::from_milliseconds(111);
415        let ts2: UtcTimeStamp = UtcTimeStamp::from_milliseconds(222);
416        let ts3: UtcTimeStamp = UtcTimeStamp::from_milliseconds(222);
417
418        assert!(ts1 < ts2);
419        assert!(ts2 > ts1);
420        assert!(ts1 <= ts2);
421        assert!(ts2 >= ts3);
422        assert!(ts2 <= ts3);
423        assert!(ts2 >= ts3);
424        assert_eq!(ts2, ts3);
425        assert_ne!(ts1, ts3);
426    }
427
428    #[test]
429    fn align_to_anchored() {
430        let day = Utc.ymd(2020, 9, 28);
431        let ts: UtcTimeStamp = day.and_hms(19, 32, 51).into();
432
433        assert_eq!(
434            ts.align_to_anchored(day.and_hms(0, 0, 0).into(), TimeDelta::from_seconds(60 * 5)),
435            day.and_hms(19, 30, 0).into(),
436        );
437
438        assert_eq!(
439            ts.align_to_anchored(
440                day.and_hms(9 /* irrelevant */, 1, 3).into(),
441                TimeDelta::from_seconds(60 * 5)
442            ),
443            day.and_hms(19, 31, 3).into(),
444        );
445    }
446
447    #[test]
448    fn align_to_anchored_eq() {
449        let day = Utc.ymd(2020, 1, 1);
450        let anchor: UtcTimeStamp = day.and_hms(0, 0, 0).into();
451        let freq = TimeDelta::from_seconds(5 * 60);
452
453        let ts1: UtcTimeStamp = day.and_hms(12, 1, 11).into();
454        let ts2: UtcTimeStamp = day.and_hms(12, 4, 11).into();
455        assert_eq!(
456            ts1.align_to_anchored(anchor, freq),
457            ts2.align_to_anchored(anchor, freq),
458        );
459    }
460}
461
462// ============================================================================================== //