Skip to main content

graphos_common/types/
timestamp.rs

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