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 pub fn to_relative_string(self, now: Self) -> String {
191 const MINUTE: u64 = 60;
192 const HOUR: u64 = 60 * MINUTE;
193 const DAY: u64 = 24 * HOUR;
194 const WEEK: u64 = 7 * DAY;
195 const YEAR: u64 = 365 * DAY;
196
197 fn fmt_1_unit(count: u64, unit: &str) -> String {
199 let plural = if count == 1 { "" } else { "s" };
200 format!("{count} {unit}{plural}")
201 }
202
203 fn fmt_2_units(
206 count: u64,
207 unit: &str,
208 rem: u64,
209 rem_unit: &str,
210 ) -> String {
211 let major = fmt_1_unit(count, unit);
212 match rem {
213 0 => major,
214 _ => format!("{major} and {}", fmt_1_unit(rem, rem_unit)),
215 }
216 }
217
218 let elapsed = now.checked_duration_since(self);
219 let secs = match elapsed {
220 Some(elapsed) => elapsed.as_secs(),
221 None => self.saturating_duration_since(now).as_secs(),
222 };
223
224 let magnitude = match secs {
227 0 => return "just now".to_owned(),
228 s if s < MINUTE => fmt_1_unit(s, "second"),
229 s if s < HOUR => fmt_1_unit(s / MINUTE, "minute"),
230 s if s < DAY => fmt_1_unit(s / HOUR, "hour"),
231 s if s < WEEK => fmt_1_unit(s / DAY, "day"),
232 s if s < YEAR =>
233 fmt_2_units(s / WEEK, "week", (s % WEEK) / DAY, "day"),
234 s => fmt_2_units(s / YEAR, "year", (s % YEAR) / WEEK, "week"),
235 };
236
237 match elapsed {
238 Some(_) => format!("{magnitude} ago"),
239 None => format!("in {magnitude}"),
240 }
241 }
242
243 #[cfg(test)]
245 fn floor_secs(self) -> Self {
246 let rem = self.0 % 1000;
247 Self(self.0 - rem)
248 }
249}
250
251impl From<TimestampMs> for Duration {
252 #[inline]
253 fn from(t: TimestampMs) -> Self {
254 t.to_duration()
255 }
256}
257
258impl From<TimestampMs> for SystemTime {
259 #[inline]
260 fn from(t: TimestampMs) -> Self {
261 t.to_system_time()
262 }
263}
264
265impl TryFrom<SystemTime> for TimestampMs {
269 type Error = Error;
270 fn try_from(system_time: SystemTime) -> Result<Self, Self::Error> {
271 Self::from_system_time(system_time)
272 }
273}
274
275impl TryFrom<Duration> for TimestampMs {
280 type Error = Error;
281 fn try_from(dur_since_epoch: Duration) -> Result<Self, Self::Error> {
282 Self::from_duration(dur_since_epoch)
283 }
284}
285
286impl TryFrom<i64> for TimestampMs {
289 type Error = Error;
290 #[inline]
291 fn try_from(ms: i64) -> Result<Self, Self::Error> {
292 if ms >= Self::MIN.0 {
293 Ok(Self(ms))
294 } else {
295 Err(Error::Negative)
296 }
297 }
298}
299
300impl FromStr for TimestampMs {
301 type Err = Error;
302 fn from_str(s: &str) -> Result<Self, Self::Err> {
303 Self::try_from(i64::from_str(s)?)
304 }
305}
306
307impl Display for TimestampMs {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 i64::fmt(&self.0, f)
310 }
311}
312
313impl<'de> de::Deserialize<'de> for TimestampMs {
314 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
315 where
316 D: de::Deserializer<'de>,
317 {
318 i64::deserialize(deserializer)
319 .and_then(|x| Self::try_from(x).map_err(de::Error::custom))
320 }
321}
322
323#[cfg(any(test, feature = "test-utils"))]
324mod arbitrary_impl {
325 use proptest::{
326 arbitrary::Arbitrary,
327 strategy::{BoxedStrategy, Strategy},
328 };
329
330 use super::*;
331
332 impl Arbitrary for TimestampMs {
333 type Parameters = ();
334 type Strategy = BoxedStrategy<Self>;
335 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
336 (Self::MIN.0..Self::MAX.0).prop_map(Self).boxed()
337 }
338 }
339}
340
341#[cfg(test)]
342mod test {
343 use proptest::{prop_assert_eq, proptest};
344
345 use super::*;
346 use crate::test_utils::roundtrip;
347
348 #[test]
349 fn timestamp_roundtrip() {
350 roundtrip::fromstr_display_roundtrip_proptest::<TimestampMs>();
351 roundtrip::json_string_roundtrip_proptest::<TimestampMs>();
352 }
353
354 #[test]
355 fn deserialize_enforces_nonnegative() {
356 assert_eq!(serde_json::from_str::<TimestampMs>("42").unwrap().0, 42);
358 assert_eq!(serde_json::from_str::<TimestampMs>("0").unwrap().0, 0);
359 assert!(serde_json::from_str::<TimestampMs>("-42").is_err());
360 }
361
362 fn assert_conversion_roundtrips(t: TimestampMs) {
364 let floored = t.floor_secs();
366 assert_eq!(TimestampMs::from_secs(floored.to_secs()), Ok(floored));
367 if let Ok(secs) = u32::try_from(floored.to_secs()) {
368 assert_eq!(TimestampMs::from_secs_u32(secs), floored);
369 }
370
371 assert_eq!(TimestampMs::from_millis(t.to_millis()), Ok(t));
373 assert_eq!(TimestampMs::try_from(t.to_i64()), Ok(t));
374
375 assert_eq!(TimestampMs::from_duration(t.to_duration()), Ok(t));
377 assert_eq!(TimestampMs::try_from(t.to_duration()), Ok(t));
378
379 assert_eq!(TimestampMs::from_system_time(t.to_system_time()), Ok(t));
381 assert_eq!(TimestampMs::try_from(t.to_system_time()), Ok(t));
382 }
383
384 #[test]
385 fn timestamp_conversions_roundtrip() {
386 assert_conversion_roundtrips(TimestampMs::MIN);
387 assert_conversion_roundtrips(TimestampMs::MAX);
388
389 proptest!(|(t: TimestampMs)| assert_conversion_roundtrips(t));
390 }
391
392 #[test]
393 fn timestamp_diff() {
394 proptest!(|(ts1: TimestampMs, ts2: TimestampMs)| {
395 let (lesser, greater) = if ts1 <= ts2 {
397 (ts1, ts2)
398 } else {
399 (ts2, ts1)
400 };
401
402 let diff =
403 Duration::from_millis(greater.to_millis() - lesser.to_millis());
404
405 let added = lesser.checked_add(diff).unwrap();
406 prop_assert_eq!(added, greater);
407
408 let subtracted = greater.checked_sub(diff).unwrap();
409 prop_assert_eq!(subtracted, lesser);
410 })
411 }
412
413 #[test]
414 fn timestamp_saturating_ops() {
415 proptest!(|(ts: TimestampMs)| {
416 prop_assert_eq!(
417 ts.saturating_add(TimestampMs::MAX.to_duration()),
418 TimestampMs::MAX
419 );
420 prop_assert_eq!(
421 ts.saturating_sub(TimestampMs::MAX.to_duration()),
422 TimestampMs::MIN
423 );
424 prop_assert_eq!(
425 ts.saturating_add(TimestampMs::MIN.to_duration()),
426 ts
427 );
428 prop_assert_eq!(
429 ts.saturating_sub(TimestampMs::MIN.to_duration()),
430 ts
431 );
432 })
433 }
434
435 #[test]
436 fn timestamp_duration_since() {
437 proptest!(|(ts1: TimestampMs, ts2: TimestampMs)| {
438 let (lesser, greater) =
439 if ts1 <= ts2 { (ts1, ts2) } else { (ts2, ts1) };
440 let diff =
441 Duration::from_millis(greater.to_millis() - lesser.to_millis());
442
443 prop_assert_eq!(greater.checked_duration_since(lesser), Some(diff));
445 prop_assert_eq!(greater.saturating_duration_since(lesser), diff);
446
447 let expected_checked = (lesser == greater).then_some(Duration::ZERO);
450 prop_assert_eq!(
451 lesser.checked_duration_since(greater),
452 expected_checked
453 );
454 prop_assert_eq!(
455 lesser.saturating_duration_since(greater),
456 Duration::ZERO
457 );
458 })
459 }
460
461 #[test]
462 fn timestamp_elapsed() {
463 assert!(TimestampMs::MIN.checked_elapsed().is_some());
466 assert!(TimestampMs::MIN.saturating_elapsed() > Duration::ZERO);
467
468 assert_eq!(TimestampMs::MAX.checked_elapsed(), None);
469 assert_eq!(TimestampMs::MAX.saturating_elapsed(), Duration::ZERO);
470 }
471
472 #[test]
474 fn relative_string_sanity() {
475 const MINUTE: u64 = 60;
476 const HOUR: u64 = 60 * MINUTE;
477 const DAY: u64 = 24 * HOUR;
478 const WEEK: u64 = 7 * DAY;
479 const YEAR: u64 = 365 * DAY;
480
481 let cases = [
483 (0, "just now", "just now"),
484 (1, "1 second ago", "in 1 second"),
485 (59, "59 seconds ago", "in 59 seconds"),
486 (MINUTE, "1 minute ago", "in 1 minute"),
487 (5 * MINUTE + 30, "5 minutes ago", "in 5 minutes"),
488 (HOUR, "1 hour ago", "in 1 hour"),
489 (3 * HOUR + 59 * MINUTE, "3 hours ago", "in 3 hours"),
490 (DAY + 12 * HOUR, "1 day ago", "in 1 day"),
491 (6 * DAY, "6 days ago", "in 6 days"),
492 (WEEK, "1 week ago", "in 1 week"),
493 (WEEK + DAY, "1 week and 1 day ago", "in 1 week and 1 day"),
494 (
495 3 * WEEK + 2 * DAY,
496 "3 weeks and 2 days ago",
497 "in 3 weeks and 2 days",
498 ),
499 (
500 51 * WEEK + 6 * DAY,
501 "51 weeks and 6 days ago",
502 "in 51 weeks and 6 days",
503 ),
504 (YEAR, "1 year ago", "in 1 year"),
505 (
506 YEAR + 5 * WEEK,
507 "1 year and 5 weeks ago",
508 "in 1 year and 5 weeks",
509 ),
510 (
512 2 * YEAR + WEEK + 3 * DAY,
513 "2 years and 1 week ago",
514 "in 2 years and 1 week",
515 ),
516 (
517 143 * WEEK,
518 "2 years and 38 weeks ago",
519 "in 2 years and 38 weeks",
520 ),
521 ];
522
523 let now = TimestampMs::from_secs(100 * YEAR).unwrap();
524 for (secs, past, future) in cases {
525 let secs = Duration::from_secs(secs);
526 let past_ts = now.saturating_sub(secs);
527 let future_ts = now.saturating_add(secs);
528
529 assert_eq!(past_ts.to_relative_string(now), past);
530 assert_eq!(future_ts.to_relative_string(now), future);
531 }
532 }
533}