1use std::{
2 fmt::{self, Display},
3 str::FromStr,
4 time::{Duration, SystemTime, UNIX_EPOCH},
5};
6
7use serde::{Serialize, de};
8
9pub struct DisplayMs(pub Duration);
14
15impl Display for DisplayMs {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 let ms = self.0.as_secs_f64() * 1000.0;
18 write!(f, "{ms:.3}ms")
19 }
20}
21
22#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
31#[derive(Serialize)]
32pub struct TimestampMs(i64);
33
34#[derive(Debug, Eq, PartialEq, thiserror::Error)]
36pub enum Error {
37 #[error("timestamp value is negative")]
38 Negative,
39
40 #[error("timestamp is more than 292 million years past epoch")]
41 TooLarge,
42
43 #[error("timestamp is before January 1st, 1970")]
44 BeforeEpoch,
45
46 #[error("failed to parse timestamp: {0}")]
47 Parse(#[from] std::num::ParseIntError),
48}
49
50impl TimestampMs {
51 pub const MIN: Self = TimestampMs(0);
52 pub const MAX: Self = TimestampMs(i64::MAX);
53
54 pub fn now() -> Self {
58 Self::try_from(SystemTime::now()).unwrap()
59 }
60
61 #[inline]
63 pub fn to_i64(self) -> i64 {
64 self.0
65 }
66
67 #[inline]
69 pub fn to_u64(self) -> u64 {
70 debug_assert!(self.0 >= 0);
71 self.0 as u64
72 }
73
74 pub fn from_secs(secs: u64) -> Result<Self, Error> {
76 Self::try_from(Duration::from_secs(secs))
77 }
78
79 pub fn from_secs_u32(secs: u32) -> Self {
81 Self(i64::from(secs) * 1000)
82 }
83
84 pub fn from_millis(millis: u64) -> Result<Self, Error> {
86 Self::try_from(Duration::from_millis(millis))
87 }
88
89 pub fn from_duration(dur_since_epoch: Duration) -> Result<Self, Error> {
91 i64::try_from(dur_since_epoch.as_millis())
92 .map(Self)
93 .map_err(|_| Error::TooLarge)
94 }
95
96 pub fn from_system_time(system_time: SystemTime) -> Result<Self, Error> {
98 let duration = system_time
99 .duration_since(UNIX_EPOCH)
100 .map_err(|_| Error::BeforeEpoch)?;
101 Self::try_from(duration)
102 }
103
104 #[cfg(any(test, feature = "test-utils"))]
106 pub fn from_u8(i: u8) -> Self {
107 Self(i64::from(i))
108 }
109
110 #[inline]
112 pub fn to_millis(self) -> u64 {
113 self.to_u64()
114 }
115
116 #[inline]
118 pub fn to_secs(self) -> u64 {
119 Duration::from_millis(self.to_millis()).as_secs()
120 }
121
122 #[inline]
124 pub fn to_duration(self) -> Duration {
125 Duration::from_millis(self.to_millis())
126 }
127
128 #[inline]
130 pub fn to_system_time(self) -> SystemTime {
131 UNIX_EPOCH + self.to_duration()
133 }
134
135 pub fn checked_add(self, duration: Duration) -> Option<Self> {
136 let dur_ms = i64::try_from(duration.as_millis()).ok()?;
137 let added = self.0.checked_add(dur_ms)?;
138 Self::try_from(added).ok()
139 }
140
141 pub fn checked_sub(self, duration: Duration) -> Option<Self> {
142 let dur_ms = i64::try_from(duration.as_millis()).ok()?;
143 let subtracted = self.0.checked_sub(dur_ms)?;
144 Self::try_from(subtracted).ok()
145 }
146
147 pub fn saturating_add(self, duration: Duration) -> Self {
148 self.checked_add(duration).unwrap_or(Self::MAX)
149 }
150
151 pub fn saturating_sub(self, duration: Duration) -> Self {
152 self.checked_sub(duration).unwrap_or(Self::MIN)
153 }
154
155 pub fn checked_duration_since(self, earlier: Self) -> Option<Duration> {
158 let dur_ms = self.to_u64().checked_sub(earlier.to_u64())?;
159 Some(Duration::from_millis(dur_ms))
160 }
161
162 pub fn saturating_duration_since(self, earlier: Self) -> Duration {
165 let dur_ms = self.to_u64().saturating_sub(earlier.to_u64());
166 Duration::from_millis(dur_ms)
167 }
168
169 #[inline]
172 pub fn checked_elapsed(self) -> Option<Duration> {
173 Self::now().checked_duration_since(self)
174 }
175
176 #[inline]
179 pub fn saturating_elapsed(self) -> Duration {
180 Self::now().saturating_duration_since(self)
181 }
182
183 #[cfg(test)]
185 fn floor_secs(self) -> Self {
186 let rem = self.0 % 1000;
187 Self(self.0 - rem)
188 }
189}
190
191impl From<TimestampMs> for Duration {
192 #[inline]
193 fn from(t: TimestampMs) -> Self {
194 t.to_duration()
195 }
196}
197
198impl From<TimestampMs> for SystemTime {
199 #[inline]
200 fn from(t: TimestampMs) -> Self {
201 t.to_system_time()
202 }
203}
204
205impl TryFrom<SystemTime> for TimestampMs {
209 type Error = Error;
210 fn try_from(system_time: SystemTime) -> Result<Self, Self::Error> {
211 Self::from_system_time(system_time)
212 }
213}
214
215impl TryFrom<Duration> for TimestampMs {
220 type Error = Error;
221 fn try_from(dur_since_epoch: Duration) -> Result<Self, Self::Error> {
222 Self::from_duration(dur_since_epoch)
223 }
224}
225
226impl TryFrom<i64> for TimestampMs {
229 type Error = Error;
230 #[inline]
231 fn try_from(ms: i64) -> Result<Self, Self::Error> {
232 if ms >= Self::MIN.0 {
233 Ok(Self(ms))
234 } else {
235 Err(Error::Negative)
236 }
237 }
238}
239
240impl FromStr for TimestampMs {
241 type Err = Error;
242 fn from_str(s: &str) -> Result<Self, Self::Err> {
243 Self::try_from(i64::from_str(s)?)
244 }
245}
246
247impl Display for TimestampMs {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 i64::fmt(&self.0, f)
250 }
251}
252
253impl<'de> de::Deserialize<'de> for TimestampMs {
254 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
255 where
256 D: de::Deserializer<'de>,
257 {
258 i64::deserialize(deserializer)
259 .and_then(|x| Self::try_from(x).map_err(de::Error::custom))
260 }
261}
262
263#[cfg(any(test, feature = "test-utils"))]
264mod arbitrary_impl {
265 use proptest::{
266 arbitrary::Arbitrary,
267 strategy::{BoxedStrategy, Strategy},
268 };
269
270 use super::*;
271
272 impl Arbitrary for TimestampMs {
273 type Parameters = ();
274 type Strategy = BoxedStrategy<Self>;
275 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
276 (Self::MIN.0..Self::MAX.0).prop_map(Self).boxed()
277 }
278 }
279}
280
281#[cfg(test)]
282mod test {
283 use proptest::{prop_assert_eq, proptest};
284
285 use super::*;
286 use crate::test_utils::roundtrip;
287
288 #[test]
289 fn timestamp_roundtrip() {
290 roundtrip::fromstr_display_roundtrip_proptest::<TimestampMs>();
291 roundtrip::json_string_roundtrip_proptest::<TimestampMs>();
292 }
293
294 #[test]
295 fn deserialize_enforces_nonnegative() {
296 assert_eq!(serde_json::from_str::<TimestampMs>("42").unwrap().0, 42);
298 assert_eq!(serde_json::from_str::<TimestampMs>("0").unwrap().0, 0);
299 assert!(serde_json::from_str::<TimestampMs>("-42").is_err());
300 }
301
302 fn assert_conversion_roundtrips(t: TimestampMs) {
304 let floored = t.floor_secs();
306 assert_eq!(TimestampMs::from_secs(floored.to_secs()), Ok(floored));
307 if let Ok(secs) = u32::try_from(floored.to_secs()) {
308 assert_eq!(TimestampMs::from_secs_u32(secs), floored);
309 }
310
311 assert_eq!(TimestampMs::from_millis(t.to_millis()), Ok(t));
313 assert_eq!(TimestampMs::try_from(t.to_i64()), Ok(t));
314
315 assert_eq!(TimestampMs::from_duration(t.to_duration()), Ok(t));
317 assert_eq!(TimestampMs::try_from(t.to_duration()), Ok(t));
318
319 assert_eq!(TimestampMs::from_system_time(t.to_system_time()), Ok(t));
321 assert_eq!(TimestampMs::try_from(t.to_system_time()), Ok(t));
322 }
323
324 #[test]
325 fn timestamp_conversions_roundtrip() {
326 assert_conversion_roundtrips(TimestampMs::MIN);
327 assert_conversion_roundtrips(TimestampMs::MAX);
328
329 proptest!(|(t: TimestampMs)| assert_conversion_roundtrips(t));
330 }
331
332 #[test]
333 fn timestamp_diff() {
334 proptest!(|(ts1: TimestampMs, ts2: TimestampMs)| {
335 let (lesser, greater) = if ts1 <= ts2 {
337 (ts1, ts2)
338 } else {
339 (ts2, ts1)
340 };
341
342 let diff =
343 Duration::from_millis(greater.to_millis() - lesser.to_millis());
344
345 let added = lesser.checked_add(diff).unwrap();
346 prop_assert_eq!(added, greater);
347
348 let subtracted = greater.checked_sub(diff).unwrap();
349 prop_assert_eq!(subtracted, lesser);
350 })
351 }
352
353 #[test]
354 fn timestamp_saturating_ops() {
355 proptest!(|(ts: TimestampMs)| {
356 prop_assert_eq!(
357 ts.saturating_add(TimestampMs::MAX.to_duration()),
358 TimestampMs::MAX
359 );
360 prop_assert_eq!(
361 ts.saturating_sub(TimestampMs::MAX.to_duration()),
362 TimestampMs::MIN
363 );
364 prop_assert_eq!(
365 ts.saturating_add(TimestampMs::MIN.to_duration()),
366 ts
367 );
368 prop_assert_eq!(
369 ts.saturating_sub(TimestampMs::MIN.to_duration()),
370 ts
371 );
372 })
373 }
374
375 #[test]
376 fn timestamp_duration_since() {
377 proptest!(|(ts1: TimestampMs, ts2: TimestampMs)| {
378 let (lesser, greater) =
379 if ts1 <= ts2 { (ts1, ts2) } else { (ts2, ts1) };
380 let diff =
381 Duration::from_millis(greater.to_millis() - lesser.to_millis());
382
383 prop_assert_eq!(greater.checked_duration_since(lesser), Some(diff));
385 prop_assert_eq!(greater.saturating_duration_since(lesser), diff);
386
387 let expected_checked = (lesser == greater).then_some(Duration::ZERO);
390 prop_assert_eq!(
391 lesser.checked_duration_since(greater),
392 expected_checked
393 );
394 prop_assert_eq!(
395 lesser.saturating_duration_since(greater),
396 Duration::ZERO
397 );
398 })
399 }
400
401 #[test]
402 fn timestamp_elapsed() {
403 assert!(TimestampMs::MIN.checked_elapsed().is_some());
406 assert!(TimestampMs::MIN.saturating_elapsed() > Duration::ZERO);
407
408 assert_eq!(TimestampMs::MAX.checked_elapsed(), None);
409 assert_eq!(TimestampMs::MAX.saturating_elapsed(), Duration::ZERO);
410 }
411}