Skip to main content

lexe_common/
time.rs

1use std::{
2    fmt::{self, Display},
3    str::FromStr,
4    time::{Duration, SystemTime, UNIX_EPOCH},
5};
6
7use serde::{Serialize, de};
8
9/// [`Display`]s a [`Duration`] in ms with 3 decimal places, e.g. "123.456ms".
10///
11/// Useful to log elapsed times in a consistent unit, as [`Duration`]'s default
12/// implementation will go with seconds, ms, nanos etc depending on the value.
13pub struct DisplayMs(pub Duration);
14
15impl Display for DisplayMs {
16    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17        let ms = self.0.as_secs_f64() * 1000.0;
18        write!(f, "{ms:.3}ms")
19    }
20}
21
22/// A timestamp in milliseconds since the UNIX epoch (January 1, 1970).
23/// Serialized as a non-negative integer.
24//
25// - Internally represented by a non-negative [`i64`] to ease interoperability
26//   with some platforms we use which don't support unsigned ints well (Postgres
27//   and Dart/Flutter).
28// - Can represent any time from January 1st, 1970 00:00:00.000 UTC to roughly
29//   292 million years in the future.
30#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
31#[derive(Serialize)]
32pub struct TimestampMs(i64);
33
34/// Errors that can occur when attempting to construct a [`TimestampMs`].
35#[derive(Debug, Eq, PartialEq, thiserror::Error)]
36pub enum Error {
37    #[error("timestamp value is negative")]
38    Negative,
39
40    #[error("timestamp is more than 292 million years past epoch")]
41    TooLarge,
42
43    #[error("timestamp is before January 1st, 1970")]
44    BeforeEpoch,
45
46    #[error("failed to parse timestamp: {0}")]
47    Parse(#[from] std::num::ParseIntError),
48}
49
50impl TimestampMs {
51    pub const MIN: Self = TimestampMs(0);
52    pub const MAX: Self = TimestampMs(i64::MAX);
53
54    /// Creates a new [`TimestampMs`] from the current [`SystemTime`].
55    ///
56    /// Panics if the current time is not within bounds.
57    pub fn now() -> Self {
58        Self::try_from(SystemTime::now()).unwrap()
59    }
60
61    /// Get this unix timestamp as an [`i64`] in milliseconds from unix epoch.
62    #[inline]
63    pub fn to_i64(self) -> i64 {
64        self.0
65    }
66
67    /// Get this unix timestamp as a [`u64`] in milliseconds from unix epoch.
68    #[inline]
69    pub fn to_u64(self) -> u64 {
70        debug_assert!(self.0 >= 0);
71        self.0 as u64
72    }
73
74    /// Construct [`TimestampMs`] from seconds since Unix epoch.
75    pub fn from_secs(secs: u64) -> Result<Self, Error> {
76        Self::try_from(Duration::from_secs(secs))
77    }
78
79    /// Infallibly construct [`TimestampMs`] from seconds since Unix epoch.
80    pub fn from_secs_u32(secs: u32) -> Self {
81        Self(i64::from(secs) * 1000)
82    }
83
84    /// Construct [`TimestampMs`] from milliseconds since Unix epoch.
85    pub fn from_millis(millis: u64) -> Result<Self, Error> {
86        Self::try_from(Duration::from_millis(millis))
87    }
88
89    /// Construct [`TimestampMs`] from [`Duration`] since Unix epoch.
90    pub fn from_duration(dur_since_epoch: Duration) -> Result<Self, Error> {
91        i64::try_from(dur_since_epoch.as_millis())
92            .map(Self)
93            .map_err(|_| Error::TooLarge)
94    }
95
96    /// Construct [`TimestampMs`] from a [`SystemTime`].
97    pub fn from_system_time(system_time: SystemTime) -> Result<Self, Error> {
98        let duration = system_time
99            .duration_since(UNIX_EPOCH)
100            .map_err(|_| Error::BeforeEpoch)?;
101        Self::try_from(duration)
102    }
103
104    /// Quickly create a dummy timestamp which can be used in tests.
105    #[cfg(any(test, feature = "test-utils"))]
106    pub fn from_u8(i: u8) -> Self {
107        Self(i64::from(i))
108    }
109
110    /// Get this unix timestamp as a [`u64`] in milliseconds from unix epoch.
111    #[inline]
112    pub fn to_millis(self) -> u64 {
113        self.to_u64()
114    }
115
116    /// Get this unix timestamp as a [`u64`] in seconds from unix epoch.
117    #[inline]
118    pub fn to_secs(self) -> u64 {
119        Duration::from_millis(self.to_millis()).as_secs()
120    }
121
122    /// Get this unix timestamp as a [`Duration`] from the unix epoch.
123    #[inline]
124    pub fn to_duration(self) -> Duration {
125        Duration::from_millis(self.to_millis())
126    }
127
128    /// Get this unix timestamp as a [`SystemTime`].
129    #[inline]
130    pub fn to_system_time(self) -> SystemTime {
131        // This add is infallible -- it doesn't panic even with Self::MAX.
132        UNIX_EPOCH + self.to_duration()
133    }
134
135    pub fn checked_add(self, duration: Duration) -> Option<Self> {
136        let dur_ms = i64::try_from(duration.as_millis()).ok()?;
137        let added = self.0.checked_add(dur_ms)?;
138        Self::try_from(added).ok()
139    }
140
141    pub fn checked_sub(self, duration: Duration) -> Option<Self> {
142        let dur_ms = i64::try_from(duration.as_millis()).ok()?;
143        let subtracted = self.0.checked_sub(dur_ms)?;
144        Self::try_from(subtracted).ok()
145    }
146
147    pub fn saturating_add(self, duration: Duration) -> Self {
148        self.checked_add(duration).unwrap_or(Self::MAX)
149    }
150
151    pub fn saturating_sub(self, duration: Duration) -> Self {
152        self.checked_sub(duration).unwrap_or(Self::MIN)
153    }
154
155    /// Returns the absolute difference two timestamps as a [`Duration`].
156    #[inline]
157    pub fn absolute_diff(self, other: Self) -> Duration {
158        Duration::from_millis(self.0.abs_diff(other.0))
159    }
160
161    /// Floors the timestamp to the most recent second.
162    #[cfg(test)]
163    fn floor_secs(self) -> Self {
164        let rem = self.0 % 1000;
165        Self(self.0 - rem)
166    }
167}
168
169impl From<TimestampMs> for Duration {
170    #[inline]
171    fn from(t: TimestampMs) -> Self {
172        t.to_duration()
173    }
174}
175
176impl From<TimestampMs> for SystemTime {
177    #[inline]
178    fn from(t: TimestampMs) -> Self {
179        t.to_system_time()
180    }
181}
182
183/// Attempts to convert a [`SystemTime`] into a [`TimestampMs`].
184///
185/// Returns an error if the [`SystemTime`] is not within bounds.
186impl TryFrom<SystemTime> for TimestampMs {
187    type Error = Error;
188    fn try_from(system_time: SystemTime) -> Result<Self, Self::Error> {
189        Self::from_system_time(system_time)
190    }
191}
192
193/// Attempts to convert a [`Duration`] since the UNIX epoch into a
194/// [`TimestampMs`].
195///
196/// Returns an error if the [`Duration`] is too large.
197impl TryFrom<Duration> for TimestampMs {
198    type Error = Error;
199    fn try_from(dur_since_epoch: Duration) -> Result<Self, Self::Error> {
200        Self::from_duration(dur_since_epoch)
201    }
202}
203
204/// Attempt to convert an [`i64`] in milliseconds since unix epoch into a
205/// [`TimestampMs`].
206impl TryFrom<i64> for TimestampMs {
207    type Error = Error;
208    #[inline]
209    fn try_from(ms: i64) -> Result<Self, Self::Error> {
210        if ms >= Self::MIN.0 {
211            Ok(Self(ms))
212        } else {
213            Err(Error::Negative)
214        }
215    }
216}
217
218impl FromStr for TimestampMs {
219    type Err = Error;
220    fn from_str(s: &str) -> Result<Self, Self::Err> {
221        Self::try_from(i64::from_str(s)?)
222    }
223}
224
225impl Display for TimestampMs {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        i64::fmt(&self.0, f)
228    }
229}
230
231impl<'de> de::Deserialize<'de> for TimestampMs {
232    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233    where
234        D: de::Deserializer<'de>,
235    {
236        i64::deserialize(deserializer)
237            .and_then(|x| Self::try_from(x).map_err(de::Error::custom))
238    }
239}
240
241#[cfg(any(test, feature = "test-utils"))]
242mod arbitrary_impl {
243    use proptest::{
244        arbitrary::Arbitrary,
245        strategy::{BoxedStrategy, Strategy},
246    };
247
248    use super::*;
249
250    impl Arbitrary for TimestampMs {
251        type Parameters = ();
252        type Strategy = BoxedStrategy<Self>;
253        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
254            (Self::MIN.0..Self::MAX.0).prop_map(Self).boxed()
255        }
256    }
257}
258
259#[cfg(test)]
260mod test {
261    use proptest::{prop_assert_eq, proptest};
262
263    use super::*;
264    use crate::test_utils::roundtrip;
265
266    #[test]
267    fn timestamp_roundtrip() {
268        roundtrip::fromstr_display_roundtrip_proptest::<TimestampMs>();
269        roundtrip::json_string_roundtrip_proptest::<TimestampMs>();
270    }
271
272    #[test]
273    fn deserialize_enforces_nonnegative() {
274        // We deserialize from JSON numbers; note that it is NOT e.g. "\"42\""
275        assert_eq!(serde_json::from_str::<TimestampMs>("42").unwrap().0, 42);
276        assert_eq!(serde_json::from_str::<TimestampMs>("0").unwrap().0, 0);
277        assert!(serde_json::from_str::<TimestampMs>("-42").is_err());
278    }
279
280    // Value conversions should roundtrip.
281    fn assert_conversion_roundtrips(t: TimestampMs) {
282        // Seconds
283        let floored = t.floor_secs();
284        assert_eq!(TimestampMs::from_secs(floored.to_secs()), Ok(floored));
285        if let Ok(secs) = u32::try_from(floored.to_secs()) {
286            assert_eq!(TimestampMs::from_secs_u32(secs), floored);
287        }
288
289        // Milliseconds
290        assert_eq!(TimestampMs::from_millis(t.to_millis()), Ok(t));
291        assert_eq!(TimestampMs::try_from(t.to_i64()), Ok(t));
292
293        // Duration
294        assert_eq!(TimestampMs::from_duration(t.to_duration()), Ok(t));
295        assert_eq!(TimestampMs::try_from(t.to_duration()), Ok(t));
296
297        // SystemTime
298        assert_eq!(TimestampMs::from_system_time(t.to_system_time()), Ok(t));
299        assert_eq!(TimestampMs::try_from(t.to_system_time()), Ok(t));
300    }
301
302    #[test]
303    fn timestamp_conversions_roundtrip() {
304        assert_conversion_roundtrips(TimestampMs::MIN);
305        assert_conversion_roundtrips(TimestampMs::MAX);
306
307        proptest!(|(t: TimestampMs)| assert_conversion_roundtrips(t));
308    }
309
310    #[test]
311    fn timestamp_diff() {
312        proptest!(|(ts1: TimestampMs, ts2: TimestampMs)| {
313            // Determine which timestamp is lesser/greater
314            let (lesser, greater) = if ts1 <= ts2 {
315                (ts1, ts2)
316            } else {
317                (ts2, ts1)
318            };
319
320            let diff =
321                Duration::from_millis(greater.to_millis() - lesser.to_millis());
322
323            let added = lesser.checked_add(diff).unwrap();
324            prop_assert_eq!(added, greater);
325
326            let subtracted = greater.checked_sub(diff).unwrap();
327            prop_assert_eq!(subtracted, lesser);
328        })
329    }
330
331    #[test]
332    fn timestamp_saturating_ops() {
333        proptest!(|(ts: TimestampMs)| {
334            prop_assert_eq!(
335                ts.saturating_add(TimestampMs::MAX.to_duration()),
336                TimestampMs::MAX
337            );
338            prop_assert_eq!(
339                ts.saturating_sub(TimestampMs::MAX.to_duration()),
340                TimestampMs::MIN
341            );
342            prop_assert_eq!(
343                ts.saturating_add(TimestampMs::MIN.to_duration()),
344                ts
345            );
346            prop_assert_eq!(
347                ts.saturating_sub(TimestampMs::MIN.to_duration()),
348                ts
349            );
350        })
351    }
352}