spacetimedb_sats/
timestamp.rs

1use anyhow::Context;
2use chrono::DateTime;
3
4use crate::{de::Deserialize, impl_st, ser::Serialize, time_duration::TimeDuration, AlgebraicType};
5use std::fmt;
6use std::ops::Add;
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 formated timestamp string
135    pub fn parse_from_str(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
144impl Add<TimeDuration> for Timestamp {
145    type Output = Self;
146
147    fn add(self, other: TimeDuration) -> Self::Output {
148        Timestamp::from_micros_since_unix_epoch(self.to_micros_since_unix_epoch() + other.to_micros())
149    }
150}
151
152pub(crate) const MICROSECONDS_PER_SECOND: i64 = 1_000_000;
153
154impl std::fmt::Display for Timestamp {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        let micros = self.to_micros_since_unix_epoch();
157        let sign = if micros < 0 { "-" } else { "" };
158        let pos = micros.abs();
159        let secs = pos / MICROSECONDS_PER_SECOND;
160        let micros_remaining = pos % MICROSECONDS_PER_SECOND;
161
162        write!(f, "{sign}{secs}.{micros_remaining:06}",)
163    }
164}
165
166impl From<SystemTime> for Timestamp {
167    fn from(system_time: SystemTime) -> Self {
168        Self::from_system_time(system_time)
169    }
170}
171
172impl From<Timestamp> for SystemTime {
173    fn from(timestamp: Timestamp) -> Self {
174        timestamp.to_system_time()
175    }
176}
177
178#[cfg(test)]
179mod test {
180    use super::*;
181    use crate::GroundSpacetimeType;
182    use proptest::prelude::*;
183
184    fn round_to_micros(st: SystemTime) -> SystemTime {
185        let duration = st.duration_since(SystemTime::UNIX_EPOCH).unwrap();
186        let micros = duration.as_micros();
187        SystemTime::UNIX_EPOCH + Duration::from_micros(micros as _)
188    }
189
190    #[test]
191    fn timestamp_type_matches() {
192        assert_eq!(AlgebraicType::timestamp(), Timestamp::get_type());
193        assert!(Timestamp::get_type().is_timestamp());
194        assert!(Timestamp::get_type().is_special());
195    }
196
197    #[test]
198    fn round_trip_systemtime_through_timestamp() {
199        let now = round_to_micros(SystemTime::now());
200        let timestamp = Timestamp::from(now);
201        let now_prime = SystemTime::from(timestamp);
202        assert_eq!(now, now_prime);
203    }
204
205    proptest! {
206        #[test]
207        fn round_trip_timestamp_through_systemtime(micros in any::<i64>().prop_map(|n| n.abs())) {
208            let timestamp = Timestamp::from_micros_since_unix_epoch(micros);
209            let system_time = SystemTime::from(timestamp);
210            let timestamp_prime = Timestamp::from(system_time);
211            prop_assert_eq!(timestamp_prime, timestamp);
212            prop_assert_eq!(timestamp_prime.to_micros_since_unix_epoch(), micros);
213        }
214
215        #[test]
216        fn add_duration(since_epoch in any::<i64>().prop_map(|n| n.abs()), duration in any::<i64>()) {
217            prop_assume!(since_epoch.checked_add(duration).is_some());
218
219            let timestamp = Timestamp::from_micros_since_unix_epoch(since_epoch);
220            let time_duration = TimeDuration::from_micros(duration);
221            let result = timestamp + time_duration;
222            prop_assert_eq!(result.to_micros_since_unix_epoch(), since_epoch + duration);
223        }
224    }
225}