Skip to main content

grafeo_common/types/
timestamp.rs

1//! Timestamps for temporal properties.
2//!
3//! Stored as microseconds since Unix epoch - plenty of precision for most uses.
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8
9/// A point in time, stored as microseconds since Unix epoch.
10///
11/// Microsecond precision, covering roughly 290,000 years in each direction
12/// from 1970. Create with [`from_secs()`](Self::from_secs),
13/// [`from_millis()`](Self::from_millis), or [`now()`](Self::now).
14#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
15#[repr(transparent)]
16pub struct Timestamp(i64);
17
18impl Timestamp {
19    /// The Unix epoch (1970-01-01 00:00:00 UTC).
20    pub const EPOCH: Self = Self(0);
21
22    /// The minimum representable timestamp.
23    pub const MIN: Self = Self(i64::MIN);
24
25    /// The maximum representable timestamp.
26    pub const MAX: Self = Self(i64::MAX);
27
28    /// Creates a timestamp from microseconds since the Unix epoch.
29    #[inline]
30    #[must_use]
31    pub const fn from_micros(micros: i64) -> Self {
32        Self(micros)
33    }
34
35    /// Creates a timestamp from milliseconds since the Unix epoch.
36    #[inline]
37    #[must_use]
38    pub const fn from_millis(millis: i64) -> Self {
39        Self(millis * 1000)
40    }
41
42    /// Creates a timestamp from seconds since the Unix epoch.
43    #[inline]
44    #[must_use]
45    pub const fn from_secs(secs: i64) -> Self {
46        Self(secs * 1_000_000)
47    }
48
49    /// Returns the current time as a timestamp.
50    #[must_use]
51    pub fn now() -> Self {
52        let duration = SystemTime::now()
53            .duration_since(UNIX_EPOCH)
54            .unwrap_or(Duration::ZERO);
55        Self::from_micros(duration.as_micros() as i64)
56    }
57
58    /// Returns the timestamp as microseconds since the Unix epoch.
59    #[inline]
60    #[must_use]
61    pub const fn as_micros(&self) -> i64 {
62        self.0
63    }
64
65    /// Returns the timestamp as milliseconds since the Unix epoch.
66    #[inline]
67    #[must_use]
68    pub const fn as_millis(&self) -> i64 {
69        self.0 / 1000
70    }
71
72    /// Returns the timestamp as seconds since the Unix epoch.
73    #[inline]
74    #[must_use]
75    pub const fn as_secs(&self) -> i64 {
76        self.0 / 1_000_000
77    }
78
79    /// Returns the timestamp as a `SystemTime`, if it's within the representable range.
80    #[must_use]
81    pub fn as_system_time(&self) -> Option<SystemTime> {
82        if self.0 >= 0 {
83            Some(UNIX_EPOCH + Duration::from_micros(self.0 as u64))
84        } else {
85            UNIX_EPOCH.checked_sub(Duration::from_micros((-self.0) as u64))
86        }
87    }
88
89    /// Adds a duration to this timestamp.
90    #[must_use]
91    pub const fn add_micros(self, micros: i64) -> Self {
92        Self(self.0.saturating_add(micros))
93    }
94
95    /// Subtracts a duration from this timestamp.
96    #[must_use]
97    pub const fn sub_micros(self, micros: i64) -> Self {
98        Self(self.0.saturating_sub(micros))
99    }
100
101    /// Returns the duration between this timestamp and another.
102    ///
103    /// Returns a positive value if `other` is before `self`, negative otherwise.
104    #[must_use]
105    pub const fn duration_since(self, other: Self) -> i64 {
106        self.0 - other.0
107    }
108}
109
110impl fmt::Debug for Timestamp {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        write!(f, "Timestamp({}μs)", self.0)
113    }
114}
115
116impl fmt::Display for Timestamp {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        // Simple ISO 8601-ish format
119        let secs = self.0 / 1_000_000;
120        let micros = (self.0 % 1_000_000).unsigned_abs();
121
122        // Calculate date/time components (simplified, doesn't handle all edge cases)
123        const SECS_PER_DAY: i64 = 86400;
124        const DAYS_PER_YEAR: i64 = 365;
125
126        let days = secs / SECS_PER_DAY;
127        let time_secs = (secs % SECS_PER_DAY + SECS_PER_DAY) % SECS_PER_DAY;
128
129        let hours = time_secs / 3600;
130        let minutes = (time_secs % 3600) / 60;
131        let seconds = time_secs % 60;
132
133        // Very rough year calculation (ignores leap years for display)
134        let year = 1970 + days / DAYS_PER_YEAR;
135        let day_of_year = days % DAYS_PER_YEAR;
136
137        write!(
138            f,
139            "{:04}-{:03}T{:02}:{:02}:{:02}.{:06}Z",
140            year, day_of_year, hours, minutes, seconds, micros
141        )
142    }
143}
144
145impl From<i64> for Timestamp {
146    fn from(micros: i64) -> Self {
147        Self::from_micros(micros)
148    }
149}
150
151impl From<Timestamp> for i64 {
152    fn from(ts: Timestamp) -> Self {
153        ts.0
154    }
155}
156
157impl TryFrom<SystemTime> for Timestamp {
158    type Error = ();
159
160    fn try_from(time: SystemTime) -> Result<Self, Self::Error> {
161        match time.duration_since(UNIX_EPOCH) {
162            Ok(duration) => Ok(Self::from_micros(duration.as_micros() as i64)),
163            Err(e) => Ok(Self::from_micros(-(e.duration().as_micros() as i64))),
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_timestamp_creation() {
174        let ts = Timestamp::from_secs(1000);
175        assert_eq!(ts.as_secs(), 1000);
176        assert_eq!(ts.as_millis(), 1_000_000);
177        assert_eq!(ts.as_micros(), 1_000_000_000);
178
179        let ts = Timestamp::from_millis(1234);
180        assert_eq!(ts.as_millis(), 1234);
181
182        let ts = Timestamp::from_micros(1_234_567);
183        assert_eq!(ts.as_micros(), 1_234_567);
184    }
185
186    #[test]
187    fn test_timestamp_now() {
188        let ts = Timestamp::now();
189        // Should be after year 2020
190        assert!(ts.as_secs() > 1_577_836_800);
191    }
192
193    #[test]
194    fn test_timestamp_arithmetic() {
195        let ts = Timestamp::from_secs(1000);
196
197        let ts2 = ts.add_micros(1_000_000);
198        assert_eq!(ts2.as_secs(), 1001);
199
200        let ts3 = ts.sub_micros(1_000_000);
201        assert_eq!(ts3.as_secs(), 999);
202
203        assert_eq!(ts2.duration_since(ts), 1_000_000);
204        assert_eq!(ts.duration_since(ts2), -1_000_000);
205    }
206
207    #[test]
208    fn test_timestamp_ordering() {
209        let ts1 = Timestamp::from_secs(100);
210        let ts2 = Timestamp::from_secs(200);
211
212        assert!(ts1 < ts2);
213        assert!(ts2 > ts1);
214        assert_eq!(ts1, Timestamp::from_secs(100));
215    }
216
217    #[test]
218    fn test_timestamp_system_time_conversion() {
219        let now = SystemTime::now();
220        let ts: Timestamp = now.try_into().unwrap();
221        let back = ts.as_system_time().unwrap();
222
223        // Should be within 1 microsecond
224        let diff = back
225            .duration_since(now)
226            .or_else(|e| Ok::<_, ()>(e.duration()))
227            .unwrap();
228        assert!(diff.as_micros() < 2);
229    }
230
231    #[test]
232    fn test_timestamp_epoch() {
233        assert_eq!(Timestamp::EPOCH.as_micros(), 0);
234        assert_eq!(Timestamp::EPOCH.as_secs(), 0);
235    }
236}