spacetimedb_sats/
timestamp.rs

1use crate::{de::Deserialize, impl_st, ser::Serialize, time_duration::TimeDuration, AlgebraicType};
2use std::fmt;
3use std::ops::Add;
4use std::time::{Duration, SystemTime};
5
6#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)]
7#[sats(crate = crate)]
8/// A point in time, measured in microseconds since the Unix epoch.
9pub struct Timestamp {
10    __timestamp_micros_since_unix_epoch__: i64,
11}
12
13impl_st!([] Timestamp, AlgebraicType::timestamp());
14
15impl Timestamp {
16    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
17    pub fn now() -> Self {
18        Self::from_system_time(SystemTime::now())
19    }
20
21    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
22    #[deprecated = "Timestamp::now() is stubbed and will panic. Read the `.timestamp` field of a `ReducerContext` instead."]
23    pub fn now() -> Self {
24        unimplemented!()
25    }
26
27    pub const UNIX_EPOCH: Self = Self {
28        __timestamp_micros_since_unix_epoch__: 0,
29    };
30
31    /// Get the number of microseconds `self` is offset from [`Self::UNIX_EPOCH`].
32    ///
33    /// A positive value means a time after the Unix epoch,
34    /// and a negative value means a time before.
35    pub fn to_micros_since_unix_epoch(self) -> i64 {
36        self.__timestamp_micros_since_unix_epoch__
37    }
38
39    /// Construct a [`Timestamp`] which is `micros` microseconds offset from [`Self::UNIX_EPOCH`].
40    ///
41    /// A positive value means a time after the Unix epoch,
42    /// and a negative value means a time before.
43    pub fn from_micros_since_unix_epoch(micros: i64) -> Self {
44        Self {
45            __timestamp_micros_since_unix_epoch__: micros,
46        }
47    }
48
49    pub fn from_time_duration_since_unix_epoch(time_duration: TimeDuration) -> Self {
50        Self::from_micros_since_unix_epoch(time_duration.to_micros())
51    }
52
53    pub fn to_time_duration_since_unix_epoch(self) -> TimeDuration {
54        TimeDuration::from_micros(self.to_micros_since_unix_epoch())
55    }
56
57    /// Returns `Err(duration_before_unix_epoch)` if `self` is before `Self::UNIX_EPOCH`.
58    pub fn to_duration_since_unix_epoch(self) -> Result<Duration, Duration> {
59        let micros = self.to_micros_since_unix_epoch();
60        if micros >= 0 {
61            Ok(Duration::from_micros(micros as u64))
62        } else {
63            Err(Duration::from_micros((-micros) as u64))
64        }
65    }
66
67    /// Return a [`Timestamp`] which is [`Timestamp::UNIX_EPOCH`] plus `duration`.
68    ///
69    /// Panics if `duration.as_micros` overflows an `i64`
70    pub fn from_duration_since_unix_epoch(duration: Duration) -> Self {
71        Self::from_micros_since_unix_epoch(
72            duration
73                .as_micros()
74                .try_into()
75                .expect("Duration since Unix epoch overflows i64 microseconds"),
76        )
77    }
78
79    /// Convert `self` into a [`SystemTime`] which refers to approximately the same point in time.
80    ///
81    /// This conversion may lose precision, as [`SystemTime`]'s prevision varies depending on platform.
82    /// E.g. Unix targets have microsecond precision, but Windows only 100-microsecond precision.
83    ///
84    /// This conversion may panic if `self` is out of bounds for [`SystemTime`].
85    /// We are not aware of any platforms for which [`SystemTime`] offers a smaller range than [`Timestamp`],
86    /// but such a platform may exist.
87    pub fn to_system_time(self) -> SystemTime {
88        match self.to_duration_since_unix_epoch() {
89            Ok(positive) => SystemTime::UNIX_EPOCH
90                .checked_add(positive)
91                .expect("Timestamp with i64 microseconds since Unix epoch overflows SystemTime"),
92            Err(negative) => SystemTime::UNIX_EPOCH
93                .checked_sub(negative)
94                .expect("Timestamp with i64 microseconds before Unix epoch overflows SystemTime"),
95        }
96    }
97
98    /// Convert a [`SystemTime`] into a [`Timestamp`] which refers to approximately the same point in time.
99    ///
100    /// This conversion may panic if `system_time` is out of bounds for [`Duration`].
101    /// [`SystemTime`]'s range is larger than [`Timestamp`] on both Unix and Windows targets,
102    /// so times in the far past or far future may panic.
103    /// [`Timestamp`]'s range is approximately 292 years before and after the Unix epoch.
104    pub fn from_system_time(system_time: SystemTime) -> Self {
105        let duration = system_time
106            .duration_since(SystemTime::UNIX_EPOCH)
107            .expect("SystemTime predates the Unix epoch");
108        Self::from_duration_since_unix_epoch(duration)
109    }
110
111    /// Returns the [`Duration`] delta between `self` and `earlier`, if `earlier` predates `self`.
112    ///
113    /// Returns `None` if `earlier` is strictly greater than `self`,
114    /// or if the difference between `earlier` and `self` overflows an `i64`.
115    pub fn duration_since(self, earlier: Timestamp) -> Option<Duration> {
116        self.time_duration_since(earlier)?.to_duration().ok()
117    }
118
119    /// Returns the [`TimeDuration`] delta between `self` and `earlier`.
120    ///
121    /// The result may be negative if `earlier` is actually later than `self`.
122    ///
123    /// Returns `None` if the subtraction overflows or underflows `i64` microseconds.
124    pub fn time_duration_since(self, earlier: Timestamp) -> Option<TimeDuration> {
125        let delta = self
126            .to_micros_since_unix_epoch()
127            .checked_sub(earlier.to_micros_since_unix_epoch())?;
128        Some(TimeDuration::from_micros(delta))
129    }
130}
131
132impl Add<TimeDuration> for Timestamp {
133    type Output = Self;
134
135    fn add(self, other: TimeDuration) -> Self::Output {
136        Timestamp::from_micros_since_unix_epoch(self.to_micros_since_unix_epoch() + other.to_micros())
137    }
138}
139
140pub(crate) const MICROSECONDS_PER_SECOND: i64 = 1_000_000;
141
142impl std::fmt::Display for Timestamp {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        let micros = self.to_micros_since_unix_epoch();
145        let sign = if micros < 0 { "-" } else { "" };
146        let pos = micros.abs();
147        let secs = pos / MICROSECONDS_PER_SECOND;
148        let micros_remaining = pos % MICROSECONDS_PER_SECOND;
149
150        write!(f, "{sign}{secs}.{micros_remaining:06}",)
151    }
152}
153
154impl From<SystemTime> for Timestamp {
155    fn from(system_time: SystemTime) -> Self {
156        Self::from_system_time(system_time)
157    }
158}
159
160impl From<Timestamp> for SystemTime {
161    fn from(timestamp: Timestamp) -> Self {
162        timestamp.to_system_time()
163    }
164}
165
166#[cfg(test)]
167mod test {
168    use super::*;
169    use crate::GroundSpacetimeType;
170    use proptest::prelude::*;
171
172    fn round_to_micros(st: SystemTime) -> SystemTime {
173        let duration = st.duration_since(SystemTime::UNIX_EPOCH).unwrap();
174        let micros = duration.as_micros();
175        SystemTime::UNIX_EPOCH + Duration::from_micros(micros as _)
176    }
177
178    #[test]
179    fn timestamp_type_matches() {
180        assert_eq!(AlgebraicType::timestamp(), Timestamp::get_type());
181        assert!(Timestamp::get_type().is_timestamp());
182        assert!(Timestamp::get_type().is_special());
183    }
184
185    #[test]
186    fn round_trip_systemtime_through_timestamp() {
187        let now = round_to_micros(SystemTime::now());
188        let timestamp = Timestamp::from(now);
189        let now_prime = SystemTime::from(timestamp);
190        assert_eq!(now, now_prime);
191    }
192
193    proptest! {
194        #[test]
195        fn round_trip_timestamp_through_systemtime(micros in any::<i64>().prop_map(|n| n.abs())) {
196            let timestamp = Timestamp::from_micros_since_unix_epoch(micros);
197            let system_time = SystemTime::from(timestamp);
198            let timestamp_prime = Timestamp::from(system_time);
199            prop_assert_eq!(timestamp_prime, timestamp);
200            prop_assert_eq!(timestamp_prime.to_micros_since_unix_epoch(), micros);
201        }
202
203        #[test]
204        fn add_duration(since_epoch in any::<i64>().prop_map(|n| n.abs()), duration in any::<i64>()) {
205            prop_assume!(since_epoch.checked_add(duration).is_some());
206
207            let timestamp = Timestamp::from_micros_since_unix_epoch(since_epoch);
208            let time_duration = TimeDuration::from_micros(duration);
209            let result = timestamp + time_duration;
210            prop_assert_eq!(result.to_micros_since_unix_epoch(), since_epoch + duration);
211        }
212    }
213}