spacetimedb_sats/
timestamp.rs1use 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)]
11pub 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 pub fn to_micros_since_unix_epoch(self) -> i64 {
39 self.__timestamp_micros_since_unix_epoch__
40 }
41
42 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 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 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 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 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 pub fn duration_since(self, earlier: Timestamp) -> Option<Duration> {
119 self.time_duration_since(earlier)?.to_duration().ok()
120 }
121
122 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 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 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 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 pub fn checked_add_duration(&self, duration: Duration) -> Option<Self> {
164 self.checked_add(TimeDuration::from_duration(duration))
165 }
166
167 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 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 }
343}