spacetimedb_sats/
timestamp.rs

1use anyhow::Context;
2use chrono::DateTime;
3
4use crate::{de::Deserialize, impl_st, ser::Serialize, time_duration::TimeDuration, AlgebraicType, AlgebraicValue};
5use std::fmt;
6use std::ops::{Add, AddAssign, Sub, SubAssign};
7use std::time::{Duration, SystemTime};
8
9#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)]
10#[sats(crate = crate)]
11/// A point in time, measured in microseconds since the Unix epoch.
12pub struct Timestamp {
13    __timestamp_micros_since_unix_epoch__: i64,
14}
15
16impl_st!([] Timestamp, AlgebraicType::timestamp());
17
18impl Timestamp {
19    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
20    pub fn now() -> Self {
21        Self::from_system_time(SystemTime::now())
22    }
23
24    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
25    #[deprecated = "Timestamp::now() is stubbed and will panic. Read the `.timestamp` field of a `ReducerContext` instead."]
26    pub fn now() -> Self {
27        unimplemented!()
28    }
29
30    pub const UNIX_EPOCH: Self = Self {
31        __timestamp_micros_since_unix_epoch__: 0,
32    };
33
34    /// Get the number of microseconds `self` is offset from [`Self::UNIX_EPOCH`].
35    ///
36    /// A positive value means a time after the Unix epoch,
37    /// and a negative value means a time before.
38    pub fn to_micros_since_unix_epoch(self) -> i64 {
39        self.__timestamp_micros_since_unix_epoch__
40    }
41
42    /// Construct a [`Timestamp`] which is `micros` microseconds offset from [`Self::UNIX_EPOCH`].
43    ///
44    /// A positive value means a time after the Unix epoch,
45    /// and a negative value means a time before.
46    pub fn from_micros_since_unix_epoch(micros: i64) -> Self {
47        Self {
48            __timestamp_micros_since_unix_epoch__: micros,
49        }
50    }
51
52    pub fn from_time_duration_since_unix_epoch(time_duration: TimeDuration) -> Self {
53        Self::from_micros_since_unix_epoch(time_duration.to_micros())
54    }
55
56    pub fn to_time_duration_since_unix_epoch(self) -> TimeDuration {
57        TimeDuration::from_micros(self.to_micros_since_unix_epoch())
58    }
59
60    /// Returns `Err(duration_before_unix_epoch)` if `self` is before `Self::UNIX_EPOCH`.
61    pub fn to_duration_since_unix_epoch(self) -> Result<Duration, Duration> {
62        let micros = self.to_micros_since_unix_epoch();
63        if micros >= 0 {
64            Ok(Duration::from_micros(micros as u64))
65        } else {
66            Err(Duration::from_micros((-micros) as u64))
67        }
68    }
69
70    /// Return a [`Timestamp`] which is [`Timestamp::UNIX_EPOCH`] plus `duration`.
71    ///
72    /// Panics if `duration.as_micros` overflows an `i64`
73    pub fn from_duration_since_unix_epoch(duration: Duration) -> Self {
74        Self::from_micros_since_unix_epoch(
75            duration
76                .as_micros()
77                .try_into()
78                .expect("Duration since Unix epoch overflows i64 microseconds"),
79        )
80    }
81
82    /// Convert `self` into a [`SystemTime`] which refers to approximately the same point in time.
83    ///
84    /// This conversion may lose precision, as [`SystemTime`]'s prevision varies depending on platform.
85    /// E.g. Unix targets have microsecond precision, but Windows only 100-microsecond precision.
86    ///
87    /// This conversion may panic if `self` is out of bounds for [`SystemTime`].
88    /// We are not aware of any platforms for which [`SystemTime`] offers a smaller range than [`Timestamp`],
89    /// but such a platform may exist.
90    pub fn to_system_time(self) -> SystemTime {
91        match self.to_duration_since_unix_epoch() {
92            Ok(positive) => SystemTime::UNIX_EPOCH
93                .checked_add(positive)
94                .expect("Timestamp with i64 microseconds since Unix epoch overflows SystemTime"),
95            Err(negative) => SystemTime::UNIX_EPOCH
96                .checked_sub(negative)
97                .expect("Timestamp with i64 microseconds before Unix epoch overflows SystemTime"),
98        }
99    }
100
101    /// Convert a [`SystemTime`] into a [`Timestamp`] which refers to approximately the same point in time.
102    ///
103    /// This conversion may panic if `system_time` is out of bounds for [`Duration`].
104    /// [`SystemTime`]'s range is larger than [`Timestamp`] on both Unix and Windows targets,
105    /// so times in the far past or far future may panic.
106    /// [`Timestamp`]'s range is approximately 292 years before and after the Unix epoch.
107    pub fn from_system_time(system_time: SystemTime) -> Self {
108        let duration = system_time
109            .duration_since(SystemTime::UNIX_EPOCH)
110            .expect("SystemTime predates the Unix epoch");
111        Self::from_duration_since_unix_epoch(duration)
112    }
113
114    /// Returns the [`Duration`] delta between `self` and `earlier`, if `earlier` predates `self`.
115    ///
116    /// Returns `None` if `earlier` is strictly greater than `self`,
117    /// or if the difference between `earlier` and `self` overflows an `i64`.
118    pub fn duration_since(self, earlier: Timestamp) -> Option<Duration> {
119        self.time_duration_since(earlier)?.to_duration().ok()
120    }
121
122    /// Returns the [`TimeDuration`] delta between `self` and `earlier`.
123    ///
124    /// The result may be negative if `earlier` is actually later than `self`.
125    ///
126    /// Returns `None` if the subtraction overflows or underflows `i64` microseconds.
127    pub fn time_duration_since(self, earlier: Timestamp) -> Option<TimeDuration> {
128        let delta = self
129            .to_micros_since_unix_epoch()
130            .checked_sub(earlier.to_micros_since_unix_epoch())?;
131        Some(TimeDuration::from_micros(delta))
132    }
133
134    /// Parses an RFC 3339 formatted timestamp string
135    pub fn parse_from_rfc3339(str: &str) -> anyhow::Result<Timestamp> {
136        DateTime::parse_from_rfc3339(str)
137            .map_err(|err| anyhow::anyhow!(err))
138            .with_context(|| "Invalid timestamp format. Expected RFC 3339 format (e.g. '2025-02-10 15:45:30').")
139            .map(|dt| dt.timestamp_micros())
140            .map(Timestamp::from_micros_since_unix_epoch)
141    }
142
143    /// Returns `Some(t)` where `t` is the time `self + duration` if `t` can be represented as a `Timestamp`,
144    /// i.e. a 64-bit signed number of microseconds before or after the Unix epoch.
145    pub fn checked_add(&self, duration: TimeDuration) -> Option<Self> {
146        self.__timestamp_micros_since_unix_epoch__
147            .checked_add(duration.to_micros())
148            .map(Timestamp::from_micros_since_unix_epoch)
149    }
150
151    /// Returns `Some(t)` where `t` is the time `self - duration` if `t` can be represented as a `Timestamp`,
152    /// i.e. a 64-bit signed number of microseconds before or after the Unix epoch.
153    pub fn checked_sub(&self, duration: TimeDuration) -> Option<Self> {
154        self.__timestamp_micros_since_unix_epoch__
155            .checked_sub(duration.to_micros())
156            .map(Timestamp::from_micros_since_unix_epoch)
157    }
158
159    /// Returns `Some(self + duration)`, or `None` if that value would be out-of-bounds for `Timestamp`.
160    ///
161    /// Converts `duration` into a [`TimeDuration`] before the arithmetic.
162    /// Depending on the target platform's representation of [`Duration`], this may lose precision.
163    pub fn checked_add_duration(&self, duration: Duration) -> Option<Self> {
164        self.checked_add(TimeDuration::from_duration(duration))
165    }
166
167    /// Returns `Some(self - duration)`, or `None` if that value would be out-of-bounds for `Timestamp`.
168    ///
169    /// Converts `duration` into a [`TimeDuration`] before the arithmetic.
170    /// Depending on the target platform's representation of [`Duration`], this may lose precision.
171    pub fn checked_sub_duration(&self, duration: Duration) -> Option<Self> {
172        self.checked_sub(TimeDuration::from_duration(duration))
173    }
174
175    pub fn to_chrono_date_time(&self) -> anyhow::Result<DateTime<chrono::Utc>> {
176        DateTime::from_timestamp_micros(self.to_micros_since_unix_epoch())
177            .ok_or_else(|| anyhow::anyhow!("Timestamp with i64 microseconds since Unix epoch overflows DateTime"))
178            .with_context(|| self.to_micros_since_unix_epoch())
179    }
180
181    /// Returns an RFC 3339 and ISO 8601 date and time string such as `1996-12-19T16:39:57-08:00`.
182    pub fn to_rfc3339(&self) -> anyhow::Result<String> {
183        Ok(self.to_chrono_date_time()?.to_rfc3339())
184    }
185}
186
187impl Add<TimeDuration> for Timestamp {
188    type Output = Self;
189
190    fn add(self, other: TimeDuration) -> Self::Output {
191        self.checked_add(other).unwrap()
192    }
193}
194
195impl Add<Duration> for Timestamp {
196    type Output = Self;
197
198    fn add(self, other: Duration) -> Self::Output {
199        self.checked_add_duration(other).unwrap()
200    }
201}
202
203impl Sub<TimeDuration> for Timestamp {
204    type Output = Self;
205
206    fn sub(self, other: TimeDuration) -> Self::Output {
207        self.checked_sub(other).unwrap()
208    }
209}
210
211impl Sub<Duration> for Timestamp {
212    type Output = Self;
213
214    fn sub(self, other: Duration) -> Self::Output {
215        self.checked_sub_duration(other).unwrap()
216    }
217}
218
219impl AddAssign<TimeDuration> for Timestamp {
220    fn add_assign(&mut self, other: TimeDuration) {
221        *self = *self + other;
222    }
223}
224
225impl AddAssign<Duration> for Timestamp {
226    fn add_assign(&mut self, other: Duration) {
227        *self = *self + other;
228    }
229}
230
231impl SubAssign<TimeDuration> for Timestamp {
232    fn sub_assign(&mut self, rhs: TimeDuration) {
233        *self = *self - rhs;
234    }
235}
236
237impl SubAssign<Duration> for Timestamp {
238    fn sub_assign(&mut self, rhs: Duration) {
239        *self = *self - rhs;
240    }
241}
242
243pub(crate) const MICROSECONDS_PER_SECOND: i64 = 1_000_000;
244
245impl fmt::Display for Timestamp {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write!(f, "{}", self.to_rfc3339().unwrap())
248    }
249}
250
251impl From<SystemTime> for Timestamp {
252    fn from(system_time: SystemTime) -> Self {
253        Self::from_system_time(system_time)
254    }
255}
256
257impl From<Timestamp> for SystemTime {
258    fn from(timestamp: Timestamp) -> Self {
259        timestamp.to_system_time()
260    }
261}
262
263impl From<Timestamp> for AlgebraicValue {
264    fn from(value: Timestamp) -> Self {
265        AlgebraicValue::product([value.to_micros_since_unix_epoch().into()])
266    }
267}
268
269#[cfg(test)]
270mod test {
271    use super::*;
272    use crate::GroundSpacetimeType;
273    use proptest::prelude::*;
274
275    fn round_to_micros(st: SystemTime) -> SystemTime {
276        let duration = st.duration_since(SystemTime::UNIX_EPOCH).unwrap();
277        let micros = duration.as_micros();
278        SystemTime::UNIX_EPOCH + Duration::from_micros(micros as _)
279    }
280
281    #[test]
282    fn timestamp_type_matches() {
283        assert_eq!(AlgebraicType::timestamp(), Timestamp::get_type());
284        assert!(Timestamp::get_type().is_timestamp());
285        assert!(Timestamp::get_type().is_special());
286    }
287
288    #[test]
289    fn round_trip_systemtime_through_timestamp() {
290        let now = round_to_micros(SystemTime::now());
291        let timestamp = Timestamp::from(now);
292        let now_prime = SystemTime::from(timestamp);
293        assert_eq!(now, now_prime);
294    }
295
296    proptest! {
297        #[test]
298        fn round_trip_timestamp_through_systemtime(micros in any::<i64>().prop_map(|n| n.abs())) {
299            let timestamp = Timestamp::from_micros_since_unix_epoch(micros);
300            let system_time = SystemTime::from(timestamp);
301            let timestamp_prime = Timestamp::from(system_time);
302            prop_assert_eq!(timestamp_prime, timestamp);
303            prop_assert_eq!(timestamp_prime.to_micros_since_unix_epoch(), micros);
304        }
305
306        #[test]
307        fn arithmetic_with_timeduration(lhs in any::<i64>(), rhs in any::<i64>()) {
308            let lhs_timestamp = Timestamp::from_micros_since_unix_epoch(lhs);
309            let rhs_time_duration = TimeDuration::from_micros(rhs);
310
311            if let Some(sum) = lhs.checked_add(rhs) {
312                let sum_timestamp = lhs_timestamp.checked_add(rhs_time_duration);
313                prop_assert!(sum_timestamp.is_some());
314                prop_assert_eq!(sum_timestamp.unwrap().to_micros_since_unix_epoch(), sum);
315
316                prop_assert_eq!((lhs_timestamp + rhs_time_duration).to_micros_since_unix_epoch(), sum);
317
318                let mut sum_assign = lhs_timestamp;
319                sum_assign += rhs_time_duration;
320                prop_assert_eq!(sum_assign.to_micros_since_unix_epoch(), sum);
321            } else {
322                prop_assert!(lhs_timestamp.checked_add(rhs_time_duration).is_none());
323            }
324
325            if let Some(diff) = lhs.checked_sub(rhs) {
326                let diff_timestamp = lhs_timestamp.checked_sub(rhs_time_duration);
327                prop_assert!(diff_timestamp.is_some());
328                prop_assert_eq!(diff_timestamp.unwrap().to_micros_since_unix_epoch(), diff);
329
330                prop_assert_eq!((lhs_timestamp - rhs_time_duration).to_micros_since_unix_epoch(), diff);
331
332                let mut diff_assign = lhs_timestamp;
333                diff_assign -= rhs_time_duration;
334                prop_assert_eq!(diff_assign.to_micros_since_unix_epoch(), diff);
335            } else {
336                prop_assert!(lhs_timestamp.checked_sub(rhs_time_duration).is_none());
337            }
338        }
339
340        // TODO: determine what guarantees we provide for arithmetic with `Duration`,
341        // then write tests that we uphold said guarantees.
342    }
343}