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    /// Truncates this timestamp to the given unit.
135    ///
136    /// - `"year"`: truncates to midnight on January 1st
137    /// - `"month"`: truncates to midnight on the 1st of the month
138    /// - `"day"`: truncates to midnight (zeroes time component)
139    /// - `"hour"`: zeroes minutes, seconds, microseconds
140    /// - `"minute"`: zeroes seconds, microseconds
141    /// - `"second"`: zeroes microseconds
142    #[must_use]
143    pub fn truncate(self, unit: &str) -> Option<Self> {
144        match unit {
145            "year" => {
146                let date = self.to_date();
147                let jan1 = super::Date::from_ymd(date.year(), 1, 1)?;
148                Some(jan1.to_timestamp())
149            }
150            "month" => {
151                let date = self.to_date();
152                let first = super::Date::from_ymd(date.year(), date.month(), 1)?;
153                Some(first.to_timestamp())
154            }
155            "day" => {
156                let days = self.0.div_euclid(86_400_000_000);
157                Some(Self(days * 86_400_000_000))
158            }
159            "hour" => {
160                let days = self.0.div_euclid(86_400_000_000);
161                let day_micros = self.0.rem_euclid(86_400_000_000);
162                let hours = day_micros / 3_600_000_000;
163                Some(Self(days * 86_400_000_000 + hours * 3_600_000_000))
164            }
165            "minute" => {
166                let days = self.0.div_euclid(86_400_000_000);
167                let day_micros = self.0.rem_euclid(86_400_000_000);
168                let minutes = day_micros / 60_000_000;
169                Some(Self(days * 86_400_000_000 + minutes * 60_000_000))
170            }
171            "second" => {
172                let days = self.0.div_euclid(86_400_000_000);
173                let day_micros = self.0.rem_euclid(86_400_000_000);
174                let seconds = day_micros / 1_000_000;
175                Some(Self(days * 86_400_000_000 + seconds * 1_000_000))
176            }
177            _ => None,
178        }
179    }
180
181    /// Adds a temporal duration to this timestamp.
182    #[must_use]
183    pub fn add_duration(self, dur: &super::Duration) -> Self {
184        // Add months via date arithmetic
185        let date = self
186            .to_date()
187            .add_duration(&super::Duration::from_months(dur.months()));
188        let time = self.to_time();
189        let base = Self::from_date_time(date, time);
190        // Add days and nanos directly
191        let day_micros = dur.days() * 86_400_000_000;
192        let nano_micros = dur.nanos() / 1000;
193        Self(base.0 + day_micros + nano_micros)
194    }
195}
196
197impl fmt::Debug for Timestamp {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        write!(f, "Timestamp({}μs)", self.0)
200    }
201}
202
203impl fmt::Display for Timestamp {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        let micros = self.0;
206        let micro_frac = micros.rem_euclid(1_000_000) as u64;
207
208        let total_days = micros.div_euclid(86_400_000_000) as i32;
209        let day_micros = micros.rem_euclid(86_400_000_000);
210        let day_secs = day_micros / 1_000_000;
211
212        let hours = day_secs / 3600;
213        let minutes = (day_secs % 3600) / 60;
214        let seconds = day_secs % 60;
215
216        let (year, month, day) = civil_from_days(total_days);
217
218        write!(
219            f,
220            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
221            year, month, day, hours, minutes, seconds, micro_frac
222        )
223    }
224}
225
226impl From<i64> for Timestamp {
227    fn from(micros: i64) -> Self {
228        Self::from_micros(micros)
229    }
230}
231
232impl From<Timestamp> for i64 {
233    fn from(ts: Timestamp) -> Self {
234        ts.0
235    }
236}
237
238impl TryFrom<SystemTime> for Timestamp {
239    type Error = ();
240
241    fn try_from(time: SystemTime) -> Result<Self, Self::Error> {
242        match time.duration_since(UNIX_EPOCH) {
243            Ok(duration) => Ok(Self::from_micros(duration.as_micros() as i64)),
244            Err(e) => Ok(Self::from_micros(-(e.duration().as_micros() as i64))),
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_timestamp_creation() {
255        let ts = Timestamp::from_secs(1000);
256        assert_eq!(ts.as_secs(), 1000);
257        assert_eq!(ts.as_millis(), 1_000_000);
258        assert_eq!(ts.as_micros(), 1_000_000_000);
259
260        let ts = Timestamp::from_millis(1234);
261        assert_eq!(ts.as_millis(), 1234);
262
263        let ts = Timestamp::from_micros(1_234_567);
264        assert_eq!(ts.as_micros(), 1_234_567);
265    }
266
267    #[test]
268    #[cfg(not(miri))] // SystemTime::now() requires clock_gettime, blocked by Miri isolation
269    fn test_timestamp_now() {
270        let ts = Timestamp::now();
271        // Should be after year 2020
272        assert!(ts.as_secs() > 1_577_836_800);
273    }
274
275    #[test]
276    fn test_timestamp_arithmetic() {
277        let ts = Timestamp::from_secs(1000);
278
279        let ts2 = ts.add_micros(1_000_000);
280        assert_eq!(ts2.as_secs(), 1001);
281
282        let ts3 = ts.sub_micros(1_000_000);
283        assert_eq!(ts3.as_secs(), 999);
284
285        assert_eq!(ts2.duration_since(ts), 1_000_000);
286        assert_eq!(ts.duration_since(ts2), -1_000_000);
287    }
288
289    #[test]
290    fn test_timestamp_ordering() {
291        let ts1 = Timestamp::from_secs(100);
292        let ts2 = Timestamp::from_secs(200);
293
294        assert!(ts1 < ts2);
295        assert!(ts2 > ts1);
296        assert_eq!(ts1, Timestamp::from_secs(100));
297    }
298
299    #[test]
300    #[cfg(not(miri))] // SystemTime::now() requires clock_gettime, blocked by Miri isolation
301    fn test_timestamp_system_time_conversion() {
302        let now = SystemTime::now();
303        let ts: Timestamp = now.try_into().unwrap();
304        let back = ts.as_system_time().unwrap();
305
306        // Should be within 1 microsecond
307        let diff = back
308            .duration_since(now)
309            .or_else(|e| Ok::<_, ()>(e.duration()))
310            .unwrap();
311        assert!(diff.as_micros() < 2);
312    }
313
314    #[test]
315    fn test_truncate() {
316        // 2024-06-15T14:30:45.123456Z
317        let date = crate::types::Date::from_ymd(2024, 6, 15).unwrap();
318        let time = crate::types::Time::from_hms_nano(14, 30, 45, 123_456_000).unwrap();
319        let ts = Timestamp::from_date_time(date, time);
320
321        let year = ts.truncate("year").unwrap();
322        assert_eq!(year.to_date().to_string(), "2024-01-01");
323        assert_eq!(year.to_time().hour(), 0);
324
325        let month = ts.truncate("month").unwrap();
326        assert_eq!(month.to_date().to_string(), "2024-06-01");
327        assert_eq!(month.to_time().hour(), 0);
328
329        let day = ts.truncate("day").unwrap();
330        assert_eq!(day.to_date().to_string(), "2024-06-15");
331        assert_eq!(day.to_time().hour(), 0);
332
333        let hour = ts.truncate("hour").unwrap();
334        assert_eq!(hour.to_time().hour(), 14);
335        assert_eq!(hour.to_time().minute(), 0);
336
337        let minute = ts.truncate("minute").unwrap();
338        assert_eq!(minute.to_time().hour(), 14);
339        assert_eq!(minute.to_time().minute(), 30);
340        assert_eq!(minute.to_time().second(), 0);
341
342        let second = ts.truncate("second").unwrap();
343        assert_eq!(second.to_time().second(), 45);
344        assert_eq!(second.to_time().nanosecond(), 0);
345
346        assert!(ts.truncate("invalid").is_none());
347    }
348
349    #[test]
350    fn test_timestamp_epoch() {
351        assert_eq!(Timestamp::EPOCH.as_micros(), 0);
352        assert_eq!(Timestamp::EPOCH.as_secs(), 0);
353    }
354
355    #[test]
356    fn test_add_duration_days_and_nanos() {
357        use crate::types::Duration;
358        let ts = Timestamp::from_secs(1_700_000_000); // 2023-11-14T22:13:20Z
359        let dur = Duration::from_days(1);
360        let result = ts.add_duration(&dur);
361        // Adding 1 day = 86400 seconds = 86_400_000_000 microseconds
362        assert_eq!(result.as_micros() - ts.as_micros(), 86_400_000_000);
363    }
364
365    #[test]
366    fn test_add_duration_months() {
367        use crate::types::Duration;
368        let ts = Timestamp::from_secs(1_700_000_000); // 2023-11-14
369        let dur = Duration::from_months(2);
370        let result = ts.add_duration(&dur);
371        let result_date = result.to_date();
372        // Nov + 2 months = January (next year)
373        assert_eq!(result_date.month(), 1);
374        assert_eq!(result_date.year(), 2024);
375    }
376}