holochain_timestamp/
lib.rs

1//! A microsecond-precision UTC timestamp for use in Holochain's actions.
2#![deny(missing_docs)]
3
4#[allow(missing_docs)]
5mod error;
6#[cfg(feature = "now")]
7mod human;
8
9pub use crate::error::{TimestampError, TimestampResult};
10#[cfg(feature = "now")]
11pub(crate) use chrono_ext::*;
12use core::ops::{Add, Sub};
13#[cfg(feature = "now")]
14pub use human::*;
15use serde::{Deserialize, Serialize};
16use std::convert::{TryFrom, TryInto};
17
18#[cfg(feature = "now")]
19mod chrono_ext;
20
21/// One million
22pub(crate) const MM: i64 = 1_000_000;
23
24/// A microsecond-precision UTC timestamp for use in Holochain's actions.
25///
26/// It is assumed to be untrustworthy:
27/// it may contain times offset from the UNIX epoch with the full +/- i64 range.
28/// Most of these times are *not* representable by a `chrono::DateTime<Utc>`
29/// (which limits itself to a +/- i32 offset in days from Jan 1, 0AD and from 1970AD).
30///
31/// Also, most differences between two Timestamps are *not*
32/// representable by either a `chrono::Duration` (which limits itself to +/- i64 microseconds), *nor*
33/// by `core::time::Duration` (which limits itself to +'ve u64 seconds).  Many constructions of these
34/// chrono and core::time types will panic!, so painful measures must be taken to avoid this outcome
35/// -- it is not acceptable for our core Holochain algorithms to panic when accessing DHT Action
36/// information committed by other random Holochain nodes!
37///
38/// Timestamp implements `Serialize` and `Display` as rfc3339 time strings (if possible).
39///
40/// Supports +/- `chrono::Duration` directly.  There is no `Timestamp::now()` method, since this is not
41/// supported by WASM; however, `holochain_types` provides a `Timestamp::now()` method.
42#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
43#[cfg_attr(not(feature = "now"), derive(Debug))]
44pub struct Timestamp(
45    /// Microseconds from UNIX Epoch, positive or negative
46    pub i64,
47);
48
49/// Timestamp +/- Into<core::time::Duration>: Anything that can be converted into a
50/// core::time::Duration can be used as an overflow-checked offset (unsigned) for a Timestamp.  A
51/// core::time::Duration allows only +'ve offsets
52impl<D: Into<core::time::Duration>> Add<D> for Timestamp {
53    type Output = TimestampResult<Timestamp>;
54
55    fn add(self, rhs: D) -> Self::Output {
56        self.checked_add(&rhs.into())
57            .ok_or(TimestampError::Overflow)
58    }
59}
60
61impl<D: Into<core::time::Duration>> Add<D> for &Timestamp {
62    type Output = TimestampResult<Timestamp>;
63
64    fn add(self, rhs: D) -> Self::Output {
65        self.to_owned() + rhs
66    }
67}
68
69/// Timestamp - core::time::Duration.
70impl<D: Into<core::time::Duration>> Sub<D> for Timestamp {
71    type Output = TimestampResult<Timestamp>;
72
73    fn sub(self, rhs: D) -> Self::Output {
74        self.checked_sub(&rhs.into())
75            .ok_or(TimestampError::Overflow)
76    }
77}
78
79impl<D: Into<core::time::Duration>> Sub<D> for &Timestamp {
80    type Output = TimestampResult<Timestamp>;
81
82    fn sub(self, rhs: D) -> Self::Output {
83        self.to_owned() - rhs
84    }
85}
86
87impl Timestamp {
88    /// The Timestamp corresponding to the UNIX epoch
89    pub const ZERO: Timestamp = Timestamp(0);
90    /// The smallest possible Timestamp
91    pub const MIN: Timestamp = Timestamp(i64::MIN);
92    /// The largest possible Timestamp
93    pub const MAX: Timestamp = Timestamp(i64::MAX);
94    /// Jan 1, 2022, 12:00:00 AM UTC
95    pub const HOLOCHAIN_EPOCH: Timestamp = Timestamp(1640995200000000);
96
97    /// Largest possible Timestamp.
98    pub fn max() -> Timestamp {
99        Timestamp(i64::MAX)
100    }
101
102    /// Construct from microseconds
103    pub fn from_micros(micros: i64) -> Self {
104        Self(micros)
105    }
106
107    /// Access time as microseconds since UNIX epoch
108    pub fn as_micros(&self) -> i64 {
109        self.0
110    }
111
112    /// Access time as milliseconds since UNIX epoch
113    pub fn as_millis(&self) -> i64 {
114        self.0 / 1000
115    }
116
117    /// Access seconds since UNIX epoch plus nanosecond offset
118    pub fn as_seconds_and_nanos(&self) -> (i64, u32) {
119        let secs = self.0 / MM;
120        let nsecs = (self.0 % 1_000_000) * 1000;
121        (secs, nsecs as u32)
122    }
123
124    /// Add unsigned core::time::Duration{ secs: u64, nanos: u32 } to a Timestamp.  See:
125    /// <https://doc.rust-lang.org/src/core/time.rs.html#53-56>
126    pub fn checked_add(&self, rhs: &core::time::Duration) -> Option<Timestamp> {
127        let micros = rhs.as_micros();
128        if micros <= i64::MAX as u128 {
129            Some(Self(self.0.checked_add(micros as i64)?))
130        } else {
131            None
132        }
133    }
134
135    /// Sub unsigned core::time::Duration{ secs: u64, nanos: u32 } from a Timestamp.
136    pub fn checked_sub(&self, rhs: &core::time::Duration) -> Option<Timestamp> {
137        let micros = rhs.as_micros();
138        if micros <= i64::MAX as u128 {
139            Some(Self(self.0.checked_sub(micros as i64)?))
140        } else {
141            None
142        }
143    }
144
145    /// Add a duration, clamping to MAX if overflow
146    pub fn saturating_add(&self, rhs: &core::time::Duration) -> Timestamp {
147        self.checked_add(rhs).unwrap_or(Self::MAX)
148    }
149
150    /// Subtract a duration, clamping to MIN if overflow
151    pub fn saturating_sub(&self, rhs: &core::time::Duration) -> Timestamp {
152        self.checked_sub(rhs).unwrap_or(Self::MIN)
153    }
154
155    /// Create a [`Timestamp`] from a [`core::time::Duration`] saturating at i64::MAX.
156    pub fn saturating_from_dur(duration: &core::time::Duration) -> Self {
157        Timestamp(std::cmp::min(duration.as_micros(), i64::MAX as u128) as i64)
158    }
159}
160
161impl TryFrom<core::time::Duration> for Timestamp {
162    type Error = error::TimestampError;
163
164    fn try_from(value: core::time::Duration) -> Result<Self, Self::Error> {
165        Ok(Timestamp(
166            value
167                .as_micros()
168                .try_into()
169                .map_err(|_| error::TimestampError::Overflow)?,
170        ))
171    }
172}
173
174#[cfg(feature = "sqlite")]
175impl rusqlite::ToSql for Timestamp {
176    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
177        Ok(rusqlite::types::ToSqlOutput::Owned(self.0.into()))
178    }
179}
180
181#[cfg(feature = "sqlite")]
182impl rusqlite::types::FromSql for Timestamp {
183    fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
184        match value {
185            // NB: if you have a NULLable Timestamp field in a DB, use `Option<Timestamp>`.
186            //     otherwise, you'll get an InvalidType error, because we don't handle null
187            //     values here.
188            rusqlite::types::ValueRef::Integer(i) => Ok(Self::from_micros(i)),
189            _ => Err(rusqlite::types::FromSqlError::InvalidType),
190        }
191    }
192}
193
194/// It's an interval bounded by timestamps that are not infinite.
195#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
196pub struct InclusiveTimestampInterval {
197    start: Timestamp,
198    end: Timestamp,
199}
200
201impl InclusiveTimestampInterval {
202    /// Try to make the interval but fail if it ends before it starts.
203    pub fn try_new(start: Timestamp, end: Timestamp) -> TimestampResult<Self> {
204        if start > end {
205            Err(TimestampError::OutOfOrder)
206        } else {
207            Ok(Self { start, end })
208        }
209    }
210
211    /// Accessor for start timestamp.
212    pub fn start(&self) -> Timestamp {
213        self.start
214    }
215
216    /// Accessor for end timestamp.
217    pub fn end(&self) -> Timestamp {
218        self.end
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use std::convert::TryInto;
226
227    const TEST_TS: &str = "2020-05-05T19:16:04.266431Z";
228
229    #[test]
230    fn timestamp_distance() {
231        // Obtaining an ordering of timestamps and their difference / distance is subtle and error
232        // prone.  It is easy to get panics when converting Timestamp to chrono::Datetime<Utc> and
233        // chrono::Duration, both of which have strict range limits.  Since we cannot generally
234        // trust code that produces Timestamps, it has no intrinsic range limits.
235        let t1 = Timestamp(i64::MAX); // invalid secs for DateTime
236        let d1: TimestampResult<chrono::DateTime<chrono::Utc>> = t1.try_into();
237        assert_eq!(d1, Err(TimestampError::Overflow));
238
239        let t2 = Timestamp(0) + core::time::Duration::new(0, 1000);
240        assert_eq!(t2, Ok(Timestamp(1)));
241    }
242
243    #[test]
244    fn micros_roundtrip() {
245        for t in [Timestamp(1234567890), Timestamp(987654321)] {
246            let micros = t.clone().as_micros();
247            let r = Timestamp::from_micros(micros);
248            assert_eq!(t.0, r.0);
249            assert_eq!(t, r);
250        }
251    }
252
253    #[test]
254    fn test_timestamp_serialization() {
255        use holochain_serialized_bytes::prelude::*;
256        let t: Timestamp = TEST_TS.try_into().unwrap();
257        let (secs, nsecs) = t.as_seconds_and_nanos();
258        assert_eq!(secs, 1588706164);
259        assert_eq!(nsecs, 266431000);
260        assert_eq!(TEST_TS, &t.to_string());
261
262        #[derive(Debug, serde::Serialize, serde::Deserialize, SerializedBytes)]
263        struct S(Timestamp);
264        let s = S(t);
265        let sb = SerializedBytes::try_from(s).unwrap();
266        let s: S = sb.try_into().unwrap();
267        let t = s.0;
268        assert_eq!(TEST_TS, &t.to_string());
269    }
270
271    #[test]
272    fn test_timestamp_alternate_forms() {
273        use holochain_serialized_bytes::prelude::*;
274
275        decode::<_, Timestamp>(&encode(&(0u64)).unwrap()).unwrap();
276        decode::<_, Timestamp>(&encode(&(i64::MAX as u64)).unwrap()).unwrap();
277        assert!(decode::<_, Timestamp>(&encode(&(i64::MAX as u64 + 1)).unwrap()).is_err());
278    }
279
280    #[test]
281    fn inclusive_timestamp_interval_test_new() {
282        // valids.
283        for (start, end) in [(0, 0), (-1, 0), (0, 1), (i64::MIN, i64::MAX)] {
284            InclusiveTimestampInterval::try_new(Timestamp(start), Timestamp(end)).unwrap();
285        }
286
287        // invalids.
288        for (start, end) in [(0, -1), (1, 0), (i64::MAX, i64::MIN)] {
289            assert!(
290                super::InclusiveTimestampInterval::try_new(Timestamp(start), Timestamp(end))
291                    .is_err()
292            );
293        }
294    }
295}