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 super::date::civil_from_days;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::time::{Duration as StdDuration, SystemTime, UNIX_EPOCH};
9
10/// A point in time, stored as microseconds since Unix epoch.
11///
12/// Microsecond precision, covering roughly 290,000 years in each direction
13/// from 1970. Create with [`from_secs()`](Self::from_secs),
14/// [`from_millis()`](Self::from_millis), or [`now()`](Self::now).
15#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
16#[repr(transparent)]
17pub struct Timestamp(i64);
18
19impl Timestamp {
20    /// The Unix epoch (1970-01-01 00:00:00 UTC).
21    pub const EPOCH: Self = Self(0);
22
23    /// The minimum representable timestamp.
24    pub const MIN: Self = Self(i64::MIN);
25
26    /// The maximum representable timestamp.
27    pub const MAX: Self = Self(i64::MAX);
28
29    /// Creates a timestamp from microseconds since the Unix epoch.
30    #[inline]
31    #[must_use]
32    pub const fn from_micros(micros: i64) -> Self {
33        Self(micros)
34    }
35
36    /// Creates a timestamp from milliseconds since the Unix epoch.
37    #[inline]
38    #[must_use]
39    pub const fn from_millis(millis: i64) -> Self {
40        Self(millis * 1000)
41    }
42
43    /// Creates a timestamp from seconds since the Unix epoch.
44    #[inline]
45    #[must_use]
46    pub const fn from_secs(secs: i64) -> Self {
47        Self(secs * 1_000_000)
48    }
49
50    /// Returns the current time as a timestamp.
51    #[must_use]
52    pub fn now() -> Self {
53        let duration = SystemTime::now()
54            .duration_since(UNIX_EPOCH)
55            .unwrap_or(StdDuration::ZERO);
56        Self::from_micros(duration.as_micros() as i64)
57    }
58
59    /// Returns the timestamp as microseconds since the Unix epoch.
60    #[inline]
61    #[must_use]
62    pub const fn as_micros(&self) -> i64 {
63        self.0
64    }
65
66    /// Returns the timestamp as milliseconds since the Unix epoch.
67    #[inline]
68    #[must_use]
69    pub const fn as_millis(&self) -> i64 {
70        self.0 / 1000
71    }
72
73    /// Returns the timestamp as seconds since the Unix epoch.
74    #[inline]
75    #[must_use]
76    pub const fn as_secs(&self) -> i64 {
77        self.0 / 1_000_000
78    }
79
80    /// Returns the timestamp as a `SystemTime`, if it's within the representable range.
81    #[must_use]
82    pub fn as_system_time(&self) -> Option<SystemTime> {
83        if self.0 >= 0 {
84            Some(UNIX_EPOCH + StdDuration::from_micros(self.0 as u64))
85        } else {
86            UNIX_EPOCH.checked_sub(StdDuration::from_micros((-self.0) as u64))
87        }
88    }
89
90    /// Adds a duration to this timestamp.
91    #[must_use]
92    pub const fn add_micros(self, micros: i64) -> Self {
93        Self(self.0.saturating_add(micros))
94    }
95
96    /// Subtracts a duration from this timestamp.
97    #[must_use]
98    pub const fn sub_micros(self, micros: i64) -> Self {
99        Self(self.0.saturating_sub(micros))
100    }
101
102    /// Returns the duration between this timestamp and another.
103    ///
104    /// Returns a positive value if `other` is before `self`, negative otherwise.
105    #[must_use]
106    pub const fn duration_since(self, other: Self) -> i64 {
107        self.0 - other.0
108    }
109
110    /// Creates a timestamp from a date and time.
111    #[must_use]
112    pub fn from_date_time(date: super::Date, time: super::Time) -> Self {
113        let day_micros = date.as_days() as i64 * 86_400_000_000;
114        let time_micros = (time.as_nanos() / 1000) as i64;
115        // If the time has an offset, subtract it to get UTC
116        let offset_micros = time.offset_seconds().unwrap_or(0) as i64 * 1_000_000;
117        Self(day_micros + time_micros - offset_micros)
118    }
119
120    /// Extracts the date component (UTC).
121    #[must_use]
122    pub fn to_date(self) -> super::Date {
123        let days = self.0.div_euclid(86_400_000_000) as i32;
124        super::Date::from_days(days)
125    }
126
127    /// Extracts the time-of-day component (UTC).
128    #[must_use]
129    pub fn to_time(self) -> super::Time {
130        let day_nanos = self.0.rem_euclid(86_400_000_000) as u64 * 1000;
131        super::Time::from_nanos(day_nanos).unwrap_or_default()
132    }
133
134    /// Adds a temporal duration to this timestamp.
135    #[must_use]
136    pub fn add_duration(self, dur: &super::Duration) -> Self {
137        // Add months via date arithmetic
138        let date = self
139            .to_date()
140            .add_duration(&super::Duration::from_months(dur.months()));
141        let time = self.to_time();
142        let base = Self::from_date_time(date, time);
143        // Add days and nanos directly
144        let day_micros = dur.days() * 86_400_000_000;
145        let nano_micros = dur.nanos() / 1000;
146        Self(base.0 + day_micros + nano_micros)
147    }
148}
149
150impl fmt::Debug for Timestamp {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(f, "Timestamp({}μs)", self.0)
153    }
154}
155
156impl fmt::Display for Timestamp {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        let micros = self.0;
159        let micro_frac = micros.rem_euclid(1_000_000) as u64;
160
161        let total_days = micros.div_euclid(86_400_000_000) as i32;
162        let day_micros = micros.rem_euclid(86_400_000_000);
163        let day_secs = day_micros / 1_000_000;
164
165        let hours = day_secs / 3600;
166        let minutes = (day_secs % 3600) / 60;
167        let seconds = day_secs % 60;
168
169        let (year, month, day) = civil_from_days(total_days);
170
171        write!(
172            f,
173            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
174            year, month, day, hours, minutes, seconds, micro_frac
175        )
176    }
177}
178
179impl From<i64> for Timestamp {
180    fn from(micros: i64) -> Self {
181        Self::from_micros(micros)
182    }
183}
184
185impl From<Timestamp> for i64 {
186    fn from(ts: Timestamp) -> Self {
187        ts.0
188    }
189}
190
191impl TryFrom<SystemTime> for Timestamp {
192    type Error = ();
193
194    fn try_from(time: SystemTime) -> Result<Self, Self::Error> {
195        match time.duration_since(UNIX_EPOCH) {
196            Ok(duration) => Ok(Self::from_micros(duration.as_micros() as i64)),
197            Err(e) => Ok(Self::from_micros(-(e.duration().as_micros() as i64))),
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_timestamp_creation() {
208        let ts = Timestamp::from_secs(1000);
209        assert_eq!(ts.as_secs(), 1000);
210        assert_eq!(ts.as_millis(), 1_000_000);
211        assert_eq!(ts.as_micros(), 1_000_000_000);
212
213        let ts = Timestamp::from_millis(1234);
214        assert_eq!(ts.as_millis(), 1234);
215
216        let ts = Timestamp::from_micros(1_234_567);
217        assert_eq!(ts.as_micros(), 1_234_567);
218    }
219
220    #[test]
221    #[cfg(not(miri))] // SystemTime::now() requires clock_gettime, blocked by Miri isolation
222    fn test_timestamp_now() {
223        let ts = Timestamp::now();
224        // Should be after year 2020
225        assert!(ts.as_secs() > 1_577_836_800);
226    }
227
228    #[test]
229    fn test_timestamp_arithmetic() {
230        let ts = Timestamp::from_secs(1000);
231
232        let ts2 = ts.add_micros(1_000_000);
233        assert_eq!(ts2.as_secs(), 1001);
234
235        let ts3 = ts.sub_micros(1_000_000);
236        assert_eq!(ts3.as_secs(), 999);
237
238        assert_eq!(ts2.duration_since(ts), 1_000_000);
239        assert_eq!(ts.duration_since(ts2), -1_000_000);
240    }
241
242    #[test]
243    fn test_timestamp_ordering() {
244        let ts1 = Timestamp::from_secs(100);
245        let ts2 = Timestamp::from_secs(200);
246
247        assert!(ts1 < ts2);
248        assert!(ts2 > ts1);
249        assert_eq!(ts1, Timestamp::from_secs(100));
250    }
251
252    #[test]
253    #[cfg(not(miri))] // SystemTime::now() requires clock_gettime, blocked by Miri isolation
254    fn test_timestamp_system_time_conversion() {
255        let now = SystemTime::now();
256        let ts: Timestamp = now.try_into().unwrap();
257        let back = ts.as_system_time().unwrap();
258
259        // Should be within 1 microsecond
260        let diff = back
261            .duration_since(now)
262            .or_else(|e| Ok::<_, ()>(e.duration()))
263            .unwrap();
264        assert!(diff.as_micros() < 2);
265    }
266
267    #[test]
268    fn test_timestamp_epoch() {
269        assert_eq!(Timestamp::EPOCH.as_micros(), 0);
270        assert_eq!(Timestamp::EPOCH.as_secs(), 0);
271    }
272}