spacetimedb_sats/
timestamp.rs1use 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)]
8pub 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 pub fn to_micros_since_unix_epoch(self) -> i64 {
36 self.__timestamp_micros_since_unix_epoch__
37 }
38
39 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 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 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 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 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 pub fn duration_since(self, earlier: Timestamp) -> Option<Duration> {
116 self.time_duration_since(earlier)?.to_duration().ok()
117 }
118
119 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}