compact_time/
lib.rs

1//! 64-bit time value with nanosecond precision.
2//!
3//! Time values range from 1970-01-01T00:00:00.000000000Z to 2554-07-21T23:34:33.709551615Z.
4mod source;
5
6use std::{fmt, time::{SystemTime, UNIX_EPOCH, Duration}, ops::{Add, Sub}};
7
8pub use source::TimeSource;
9
10const NANO: u64 = 1_000_000_000;
11
12/// A time value with nanosecond precision.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[cfg_attr(feature = "minicbor", derive(minicbor::Encode, minicbor::Decode), cbor(transparent))]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
16pub struct Time(u64);
17
18impl Time {
19    /// The minimum time value (1970-01-01T00:00:00.000000000Z).
20    pub const MIN: Self = Time(0);
21
22    /// The maximum time value (2554-07-21T23:34:33.709551615Z).
23    pub const MAX: Self = Time(u64::MAX);
24
25    /// Get the current system time.
26    ///
27    /// # Panics
28    ///
29    /// If the reported system time is less than [`Time::MIN`] or greater than [`Time::MAX`].
30    pub fn now() -> Self {
31        Self::try_now().expect("SystemTime in range")
32    }
33
34    /// Get the current system time.
35    ///
36    /// Fails if the reported system time is less than [`Time::MIN`] or greater than [`Time::MAX`].
37    pub fn try_now() -> Result<Self, OutOfRange> {
38        let t = SystemTime::now();
39        let Ok(d) = t.duration_since(UNIX_EPOCH) else {
40            return Err(OutOfRange(()))
41        };
42        Self::try_from_sn(d.as_secs(), d.subsec_nanos())
43    }
44
45    /// Get the current system time using `CLOCK_REALTIME_COARSE`.
46    ///
47    /// This is faster but less precise than [`Time::now`].
48    ///
49    /// # Panics
50    ///
51    /// If the reported system time is less than [`Time::MIN`] or greater than [`Time::MAX`].
52    #[cfg(feature = "coarse")]
53    pub fn coarse() -> Self {
54        Self::try_coarse().expect("valid time")
55    }
56
57    /// Get the current system time using `CLOCK_REALTIME_COARSE`.
58    ///
59    /// This is faster but less precise than [`Time::now`].
60    ///
61    /// Fails if the reported system time is less than [`Time::MIN`] or greater than [`Time::MAX`].
62    #[cfg(feature = "coarse")]
63    pub fn try_coarse() -> Result<Self, OutOfRange> {
64        use rustix::time::{clock_gettime, ClockId};
65        let t = clock_gettime(ClockId::RealtimeCoarse);
66        if t.tv_sec < 0 || t.tv_nsec < 0 {
67            return Err(OutOfRange(()))
68        }
69        Self::try_from_sn(t.tv_sec as u64, t.tv_nsec as u64)
70    }
71
72    /// Construct a time value from the given seconds.
73    ///
74    /// # Panics
75    ///
76    /// If the given seconds are greater than [`Time::MAX`] seconds.
77    pub fn from_seconds(s: u64) -> Self {
78        Self::try_from_seconds(s).expect("seconds in range")
79    }
80
81    /// Construct a time value from the given seconds.
82    ///
83    /// Fails if the given seconds are greater than [`Time::MAX`] seconds.
84    pub fn try_from_seconds(s: u64) -> Result<Self, OutOfRange> {
85        Self::try_from_sn(s, 0u64)
86    }
87
88    /// Construct a time value from the given nanoseconds.
89    pub const fn from_nanos(n: u64) -> Self {
90        Self(n)
91    }
92
93    /// Seconds since the Unix epoch.
94    pub const fn seconds(self) -> u64 {
95        self.0 / NANO
96    }
97
98    /// Nanoseconds since the Unix epoch.
99    pub const fn nanos(self) -> u64 {
100        self.0
101    }
102
103    /// Subsecond nanoseconds.
104    pub const fn second_nanos(self) -> u64 {
105        self.nanos() - self.seconds() * NANO
106    }
107
108    /// Try to add the given time value.
109    ///
110    /// Fails if the result would be greater than [`Time::MAX`].
111    pub fn try_add(self, t: Time) -> Result<Self, OutOfRange> {
112        let n = self.0.checked_add(t.0).ok_or(OutOfRange(()))?;
113        Ok(Self(n))
114    }
115
116    /// Try to substract the given time value.
117    ///
118    /// Fails if the result would be less than [`Time::MIN`].
119    pub fn try_sub(self, t: Time) -> Result<Self, OutOfRange> {
120        let n = self.0.checked_sub(t.0).ok_or(OutOfRange(()))?;
121        Ok(Self(n))
122    }
123
124    /// Add the given number of nanoseconds.
125    ///
126    /// # Panics
127    ///
128    /// If the result would be greater than [`Time::MAX`]. See
129    /// [`Time::add_nanos_checked`] for an alternative that does
130    /// not panic.
131    pub fn add_nanos(self, n: u64) -> Self {
132        self.add_nanos_checked(n).expect("no overflow")
133    }
134
135    /// Add the given number of seconds.
136    ///
137    /// # Panics
138    ///
139    /// If the result would be greater than [`Time::MAX`]. See
140    /// [`Time::add_seconds_checked`] for an alternative that does
141    /// not panic.
142    pub fn add_seconds(self, s: u64) -> Self {
143        self.add_seconds_checked(s).expect("no overflow")
144    }
145
146    /// Add the given number of minutes.
147    ///
148    /// # Panics
149    ///
150    /// If the result would be greater than [`Time::MAX`]. See
151    /// [`Time::add_minutes_checked`] for an alternative that does
152    /// not panic.
153    pub fn add_minutes(self, m: u64) -> Self {
154        self.add_minutes_checked(m).expect("no overflow")
155    }
156
157    /// Add the given number of hours.
158    ///
159    /// # Panics
160    ///
161    /// If the result would be greater than [`Time::MAX`]. See
162    /// [`Time::add_hours_checked`] for an alternative that does
163    /// not panic.
164    pub fn add_hours(self, h: u64) -> Self {
165        self.add_hours_checked(h).expect("no overflow")
166    }
167
168    /// Add the given number of days.
169    ///
170    /// # Panics
171    ///
172    /// If the result would be greater than [`Time::MAX`]. See
173    /// [`Time::add_days_checked`] for an alternative that does
174    /// not panic.
175    pub fn add_days(self, d: u64) -> Self {
176        self.add_days_checked(d).expect("no overflow")
177    }
178
179    /// Add the given number of nanoseconds.
180    ///
181    /// Fails if the result would be greater than [`Time::MAX`].
182    pub fn add_nanos_checked(self, n: u64) -> Result<Self, OutOfRange> {
183        let n = self.0.checked_add(n).ok_or(OutOfRange::new())?;
184        Ok(Self(n))
185    }
186
187    /// Add the given number of seconds.
188    ///
189    /// Fails if the result would be greater than [`Time::MAX`].
190    pub fn add_seconds_checked(self, s: u64) -> Result<Self, OutOfRange> {
191        self.add_nanos_checked(s.checked_mul(NANO).ok_or(OutOfRange::new())?)
192    }
193
194    /// Add the given number of minutes.
195    ///
196    /// Fails if the result would be greater than [`Time::MAX`].
197    pub fn add_minutes_checked(self, m: u64) -> Result<Self, OutOfRange> {
198        self.add_seconds_checked(m.checked_mul(60).ok_or(OutOfRange::new())?)
199    }
200
201    /// Add the given number of hours.
202    ///
203    /// Fails if the result would be greater than [`Time::MAX`].
204    pub fn add_hours_checked(self, h: u64) -> Result<Self, OutOfRange> {
205        self.add_seconds_checked(h.checked_mul(3600).ok_or(OutOfRange::new())?)
206    }
207
208    /// Add the given number of days.
209    ///
210    /// Fails if the result would be greater than [`Time::MAX`].
211    pub fn add_days_checked(self, d: u64) -> Result<Self, OutOfRange> {
212        self.add_seconds_checked(d.checked_mul(24 * 3600).ok_or(OutOfRange::new())?)
213    }
214
215    /// Substract the given number of nanoseconds.
216    ///
217    /// # Panics
218    ///
219    /// If the result would be less than [`Time::MIN`]. See
220    /// [`Time::sub_nanos_checked`] for an alternative that does
221    /// not panic.
222    pub fn sub_nanos(self, n: u64) -> Self {
223        self.sub_nanos_checked(n).expect("no underflow")
224    }
225
226    /// Substract the given number of seconds.
227    ///
228    /// # Panics
229    ///
230    /// If the result would be less than [`Time::MIN`]. See
231    /// [`Time::sub_seconds_checked`] for an alternative that does
232    /// not panic.
233    pub fn sub_seconds(self, s: u64) -> Self {
234        self.sub_seconds_checked(s).expect("no underflow")
235    }
236
237    /// Substract the given number of minutes.
238    ///
239    /// # Panics
240    ///
241    /// If the result would be less than [`Time::MIN`]. See
242    /// [`Time::sub_minutes_checked`] for an alternative that does
243    /// not panic.
244    pub fn sub_minutes(self, m: u64) -> Self {
245        self.sub_minutes_checked(m).expect("no underflow")
246    }
247
248    /// Substract the given number of hours.
249    ///
250    /// # Panics
251    ///
252    /// If the result would be less than [`Time::MIN`]. See
253    /// [`Time::sub_hours_checked`] for an alternative that does
254    /// not panic.
255    pub fn sub_hours(self, h: u64) -> Self {
256        self.sub_hours_checked(h).expect("no underflow")
257    }
258
259    /// Substract the given number of days.
260    ///
261    /// # Panics
262    ///
263    /// If the result would be less than [`Time::MIN`]. See
264    /// [`Time::sub_days_checked`] for an alternative that does
265    /// not panic.
266    pub fn sub_days(self, d: u64) -> Self {
267        self.sub_days_checked(d).expect("no underflow")
268    }
269
270    /// Substract the given number of nanoseconds.
271    ///
272    /// Fails if the result would be less than [`Time::MIN`].
273    pub fn sub_nanos_checked(self, n: u64) -> Result<Self, OutOfRange> {
274        let n = self.0.checked_sub(n).ok_or(OutOfRange::new())?;
275        Ok(Self(n))
276    }
277
278    /// Substract the given number of seconds.
279    ///
280    /// Fails if the result would be less than [`Time::MIN`].
281    pub fn sub_seconds_checked(self, s: u64) -> Result<Self, OutOfRange> {
282        self.sub_nanos_checked(s.checked_mul(NANO).ok_or(OutOfRange::new())?)
283    }
284
285    /// Substract the given number of minutes.
286    ///
287    /// Fails if the result would be less than [`Time::MIN`].
288    pub fn sub_minutes_checked(self, m: u64) -> Result<Self, OutOfRange> {
289        self.sub_seconds_checked(m.checked_mul(60).ok_or(OutOfRange::new())?)
290    }
291
292    /// Substract the given number of hours.
293    ///
294    /// Fails if the result would be less than [`Time::MIN`].
295    pub fn sub_hours_checked(self, h: u64) -> Result<Self, OutOfRange> {
296        self.sub_seconds_checked(h.checked_mul(3600).ok_or(OutOfRange::new())?)
297    }
298
299    /// Substract the given number of days.
300    ///
301    /// Fails if the result would be less than [`Time::MIN`].
302    pub fn sub_days_checked(self, d: u64) -> Result<Self, OutOfRange> {
303        self.sub_seconds_checked(d.checked_mul(24 * 3600).ok_or(OutOfRange::new())?)
304    }
305
306    /// Convert this time value to the [`std::time::Duration`] since the Unix epoch.
307    pub const fn to_duration(self) -> Duration {
308        let s = self.seconds();
309        let n = self.second_nanos();
310        Duration::new(s, n as u32)
311    }
312
313    /// Convert the delta between this time and the given one into a [`std::time::Duration`].
314    ///
315    /// Returns `None` if the argument is greater that `self`.
316    pub fn duration_since(self, earlier: Self) -> Option<Duration> {
317        self.to_duration().checked_sub(earlier.to_duration())
318    }
319
320    /// Return this time value as a plain integer.
321    pub fn as_u64(self) -> u64 {
322        self.0
323    }
324
325    /// Format this time value as `YYYY-MM-DDThh:mm:ss.nnnnnnnnnZ`.
326    ///
327    /// *Requires feature `"utc"`*.
328    #[cfg(feature = "utc")]
329    pub fn to_utc_string(self) -> Option<String> {
330        let s = self.seconds();
331        let n = self.second_nanos();
332        if let Ok(utc) = tz::UtcDateTime::from_timespec(s as i64, n as u32) {
333            Some(utc.to_string())
334        } else {
335            None
336        }
337    }
338
339    /// Try to construct a time value from seconds and nanoseconds.
340    fn try_from_sn<S: Into<u64>, N: Into<u64>>(s: S, n: N) -> Result<Self, OutOfRange> {
341        let n = s.into()
342            .checked_mul(NANO)
343            .and_then(|x| x.checked_add(n.into()))
344            .ok_or(OutOfRange::new())?;
345        Ok(Self(n))
346    }
347}
348
349impl From<Time> for u64 {
350    fn from(value: Time) -> Self {
351        value.0
352    }
353}
354
355impl From<u64> for Time {
356    fn from(n: u64) -> Self {
357        Self(n)
358    }
359}
360
361/// Error if values go out of range.
362#[derive(Debug)]
363pub struct OutOfRange(());
364
365impl OutOfRange {
366    fn new() -> Self {
367        Self(())
368    }
369}
370
371impl fmt::Display for OutOfRange {
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        f.write_str("out of range")
374    }
375}
376
377impl std::error::Error for OutOfRange {}
378
379impl Add<Duration> for Time {
380    type Output = Self;
381
382    /// Adds the given duration.
383    ///
384    /// # Panics
385    ///
386    /// If the result would be greater than [`Time::MAX`].
387    fn add(self, d: Duration) -> Self::Output {
388        self.add_seconds(d.as_secs())
389            .add_nanos(d.subsec_nanos().into())
390    }
391}
392
393impl Add<Time> for Time {
394    type Output = Self;
395
396    /// Adds the given time.
397    ///
398    /// # Panics
399    ///
400    /// If the result would be greater than [`Time::MAX`].
401    fn add(self, t: Time) -> Self::Output {
402        self.add_nanos(t.nanos())
403    }
404}
405
406impl Sub<Duration> for Time {
407    type Output = Self;
408
409    /// Subtracts the given duration.
410    ///
411    /// # Panics
412    ///
413    /// If the result would be less than [`Time::MIN`].
414    fn sub(self, d: Duration) -> Self::Output {
415        self.sub_seconds(d.as_secs())
416            .sub_nanos(d.subsec_nanos().into())
417    }
418}
419
420impl Sub<Time> for Time {
421    type Output = Self;
422
423    /// Subtracts the given time.
424    ///
425    /// # Panics
426    ///
427    /// If the result would be less than [`Time::MIN`].
428    fn sub(self, t: Time) -> Self::Output {
429        self.sub_nanos(t.nanos())
430    }
431}
432
433impl fmt::Display for Time {
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        let s = self.seconds();
436        let n = self.second_nanos();
437        write!(f, "{s}.{n}")
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use quickcheck::{Arbitrary, Gen, quickcheck, TestResult};
444    use super::{NANO, Time};
445
446    impl Arbitrary for Time {
447        fn arbitrary(g: &mut Gen) -> Self {
448            Time::from(u64::arbitrary(g))
449        }
450    }
451
452    quickcheck! {
453        fn prop_sec_nano_id(s: u64, n: u64) -> TestResult {
454            let n = n % NANO; // subsec nanos
455            if s.checked_mul(NANO).and_then(|s| s.checked_add(n)).is_none() {
456                return TestResult::discard()
457            }
458            let t = Time::try_from_sn(s, n).unwrap();
459            let b = t.seconds() == s && t.second_nanos() == n;
460            TestResult::from_bool(b)
461        }
462
463        fn prop_add_sub_nanos_id(t: Time, n: u64) -> TestResult {
464            let Ok(u) = t.add_nanos_checked(n) else {
465                return TestResult::discard()
466            };
467            TestResult::from_bool(u.sub_nanos(n) == t)
468        }
469
470        fn prop_add_sub_secs_id(t: Time, s: u64) -> TestResult {
471            let Ok(u) = t.add_seconds_checked(s) else {
472                return TestResult::discard()
473            };
474            TestResult::from_bool(u.sub_seconds(s) == t)
475        }
476
477        fn prop_add_sub_mins_id(t: Time, m: u64) -> TestResult {
478            let Ok(u) = t.add_minutes_checked(m) else {
479                return TestResult::discard()
480            };
481            TestResult::from_bool(u.sub_minutes(m) == t)
482        }
483
484        fn prop_add_sub_hours_id(t: Time, h: u64) -> TestResult {
485            let Ok(u) = t.add_hours_checked(h) else {
486                return TestResult::discard()
487            };
488            TestResult::from_bool(u.sub_hours(h) == t)
489        }
490
491        fn prop_add_sub_days_id(t: Time, d: u64) -> TestResult {
492            let Ok(u) = t.add_days_checked(d) else {
493                return TestResult::discard()
494            };
495            TestResult::from_bool(u.sub_days(d) == t)
496        }
497
498        fn prop_second_nanos(t: Time) -> bool {
499            t.nanos() - t.seconds() * NANO == t.second_nanos()
500        }
501
502        fn prop_add_1s_as_nanos(t: Time) -> TestResult {
503            if Time::MAX.try_sub(t).map(|d| d < NANO.into()).unwrap_or(true) {
504                return TestResult::discard()
505            }
506            let u = t.add_nanos(NANO);
507            let b = u.seconds() - t.seconds() == 1 && u.second_nanos() - t.second_nanos() == 0;
508            TestResult::from_bool(b)
509        }
510    }
511
512    #[test]
513    fn max_time() {
514        assert!(Time::MAX.add_nanos_checked(1).is_err());
515        assert!(Time::MAX.add_seconds_checked(1).is_err());
516        assert!(Time::MAX.add_minutes_checked(1).is_err());
517        assert!(Time::MAX.add_hours_checked(1).is_err());
518        assert!(Time::MAX.add_days_checked(1).is_err());
519    }
520
521    #[test]
522    fn min_time() {
523        assert!(Time::MIN.sub_nanos_checked(1).is_err());
524        assert!(Time::MIN.sub_seconds_checked(1).is_err());
525        assert!(Time::MIN.sub_minutes_checked(1).is_err());
526        assert!(Time::MIN.sub_hours_checked(1).is_err());
527        assert!(Time::MIN.sub_days_checked(1).is_err());
528    }
529}