Skip to main content

emergent_client/types/
timestamp.rs

1//! Timestamp newtype for milliseconds since Unix epoch.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7/// A timestamp in milliseconds since Unix epoch.
8///
9/// This provides type safety and clear semantics for timestamp values.
10/// All timestamps in the Emergent system use this format for consistency.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct Timestamp(u64);
14
15impl Timestamp {
16    /// Creates a timestamp from milliseconds since Unix epoch.
17    #[must_use]
18    pub const fn from_millis(millis: u64) -> Self {
19        Self(millis)
20    }
21
22    /// Returns the current system time as a timestamp.
23    ///
24    /// Returns `Timestamp(0)` if system time is before Unix epoch.
25    #[must_use]
26    pub fn now() -> Self {
27        SystemTime::now()
28            .duration_since(UNIX_EPOCH)
29            .map_or(Self(0), |d| {
30                Self(u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
31            })
32    }
33
34    /// Returns the timestamp as milliseconds.
35    #[must_use]
36    pub const fn as_millis(&self) -> u64 {
37        self.0
38    }
39
40    /// Returns the timestamp as seconds (truncated).
41    #[must_use]
42    pub const fn as_secs(&self) -> u64 {
43        self.0 / 1000
44    }
45
46    /// Returns the duration since another timestamp (if this one is later).
47    ///
48    /// Returns `None` if `other` is after `self`.
49    #[must_use]
50    pub const fn duration_since(&self, other: Self) -> Option<u64> {
51        if self.0 >= other.0 {
52            Some(self.0 - other.0)
53        } else {
54            None
55        }
56    }
57
58    /// Adds milliseconds to this timestamp.
59    #[must_use]
60    pub const fn add_millis(&self, millis: u64) -> Self {
61        Self(self.0.saturating_add(millis))
62    }
63
64    /// Subtracts milliseconds from this timestamp.
65    #[must_use]
66    pub const fn sub_millis(&self, millis: u64) -> Self {
67        Self(self.0.saturating_sub(millis))
68    }
69}
70
71impl Default for Timestamp {
72    fn default() -> Self {
73        Self::now()
74    }
75}
76
77impl fmt::Display for Timestamp {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}", self.0)
80    }
81}
82
83impl From<u64> for Timestamp {
84    fn from(millis: u64) -> Self {
85        Self::from_millis(millis)
86    }
87}
88
89impl From<Timestamp> for u64 {
90    fn from(ts: Timestamp) -> Self {
91        ts.0
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn from_millis_creates_timestamp() {
101        let ts = Timestamp::from_millis(1_704_067_200_000);
102        assert_eq!(ts.as_millis(), 1_704_067_200_000);
103    }
104
105    #[test]
106    fn now_creates_current_timestamp() {
107        let ts = Timestamp::now();
108        assert!(ts.as_millis() > 0);
109    }
110
111    #[test]
112    fn as_secs_truncates() {
113        let ts = Timestamp::from_millis(5500);
114        assert_eq!(ts.as_secs(), 5);
115    }
116
117    #[test]
118    fn timestamps_are_ordered() {
119        let ts1 = Timestamp::from_millis(1000);
120        let ts2 = Timestamp::from_millis(2000);
121        assert!(ts1 < ts2);
122    }
123
124    #[test]
125    fn conversion_to_u64() {
126        let ts = Timestamp::from_millis(12345);
127        let millis: u64 = ts.into();
128        assert_eq!(millis, 12345);
129    }
130
131    #[test]
132    fn conversion_from_u64() {
133        let ts: Timestamp = 12345_u64.into();
134        assert_eq!(ts.as_millis(), 12345);
135    }
136
137    #[test]
138    fn duration_since() {
139        let ts1 = Timestamp::from_millis(1000);
140        let ts2 = Timestamp::from_millis(2000);
141        assert_eq!(ts2.duration_since(ts1), Some(1000));
142        assert_eq!(ts1.duration_since(ts2), None);
143    }
144
145    #[test]
146    fn add_millis() {
147        let ts = Timestamp::from_millis(1000);
148        assert_eq!(ts.add_millis(500).as_millis(), 1500);
149    }
150
151    #[test]
152    fn sub_millis() {
153        let ts = Timestamp::from_millis(1000);
154        assert_eq!(ts.sub_millis(500).as_millis(), 500);
155    }
156
157    #[test]
158    fn sub_millis_saturates() {
159        let ts = Timestamp::from_millis(100);
160        assert_eq!(ts.sub_millis(500).as_millis(), 0);
161    }
162
163    #[test]
164    fn serde_roundtrip() -> Result<(), serde_json::Error> {
165        let ts = Timestamp::from_millis(1_704_067_200_000);
166        let json = serde_json::to_string(&ts)?;
167        let restored: Timestamp = serde_json::from_str(&json)?;
168        assert_eq!(ts, restored);
169        Ok(())
170    }
171
172    #[test]
173    fn serde_is_transparent() -> Result<(), serde_json::Error> {
174        let ts = Timestamp::from_millis(12345);
175        let json = serde_json::to_string(&ts)?;
176        assert_eq!(json, "12345");
177        Ok(())
178    }
179}