grafeo_common/types/
timestamp.rs1use super::date::civil_from_days;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::time::{Duration as StdDuration, SystemTime, UNIX_EPOCH};
9
10#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
16#[repr(transparent)]
17pub struct Timestamp(i64);
18
19impl Timestamp {
20 pub const EPOCH: Self = Self(0);
22
23 pub const MIN: Self = Self(i64::MIN);
25
26 pub const MAX: Self = Self(i64::MAX);
28
29 #[inline]
31 #[must_use]
32 pub const fn from_micros(micros: i64) -> Self {
33 Self(micros)
34 }
35
36 #[inline]
38 #[must_use]
39 pub const fn from_millis(millis: i64) -> Self {
40 Self(millis * 1000)
41 }
42
43 #[inline]
45 #[must_use]
46 pub const fn from_secs(secs: i64) -> Self {
47 Self(secs * 1_000_000)
48 }
49
50 #[must_use]
52 pub fn now() -> Self {
53 let duration = SystemTime::now()
54 .duration_since(UNIX_EPOCH)
55 .unwrap_or(StdDuration::ZERO);
56 #[allow(clippy::cast_possible_truncation)]
58 Self::from_micros(duration.as_micros() as i64)
59 }
60
61 #[inline]
63 #[must_use]
64 pub const fn as_micros(&self) -> i64 {
65 self.0
66 }
67
68 #[inline]
70 #[must_use]
71 pub const fn as_millis(&self) -> i64 {
72 self.0 / 1000
73 }
74
75 #[inline]
77 #[must_use]
78 pub const fn as_secs(&self) -> i64 {
79 self.0 / 1_000_000
80 }
81
82 #[must_use]
84 pub fn as_system_time(&self) -> Option<SystemTime> {
85 if self.0 >= 0 {
86 #[allow(clippy::cast_sign_loss)]
88 Some(UNIX_EPOCH + StdDuration::from_micros(self.0 as u64))
89 } else {
90 UNIX_EPOCH.checked_sub(StdDuration::from_micros(self.0.unsigned_abs()))
91 }
92 }
93
94 #[must_use]
96 pub const fn add_micros(self, micros: i64) -> Self {
97 Self(self.0.saturating_add(micros))
98 }
99
100 #[must_use]
102 pub const fn sub_micros(self, micros: i64) -> Self {
103 Self(self.0.saturating_sub(micros))
104 }
105
106 #[must_use]
110 pub const fn duration_since(self, other: Self) -> i64 {
111 self.0 - other.0
112 }
113
114 #[must_use]
116 pub fn from_date_time(date: super::Date, time: super::Time) -> Self {
117 let day_micros = date.as_days() as i64 * 86_400_000_000;
118 #[allow(clippy::cast_possible_wrap)]
120 let time_micros = (time.as_nanos() / 1000) as i64;
121 let offset_micros = time.offset_seconds().unwrap_or(0) as i64 * 1_000_000;
123 Self(day_micros + time_micros - offset_micros)
124 }
125
126 #[must_use]
128 pub fn to_date(self) -> super::Date {
129 #[allow(clippy::cast_possible_truncation)]
131 let days = self.0.div_euclid(86_400_000_000) as i32;
132 super::Date::from_days(days)
133 }
134
135 #[must_use]
137 pub fn to_time(self) -> super::Time {
138 let day_nanos = self.0.rem_euclid(86_400_000_000) as u64 * 1000;
139 super::Time::from_nanos(day_nanos).unwrap_or_default()
140 }
141
142 #[must_use]
151 pub fn truncate(self, unit: &str) -> Option<Self> {
152 match unit {
153 "year" => {
154 let date = self.to_date();
155 let jan1 = super::Date::from_ymd(date.year(), 1, 1)?;
156 Some(jan1.to_timestamp())
157 }
158 "month" => {
159 let date = self.to_date();
160 let first = super::Date::from_ymd(date.year(), date.month(), 1)?;
161 Some(first.to_timestamp())
162 }
163 "day" => {
164 let days = self.0.div_euclid(86_400_000_000);
165 Some(Self(days * 86_400_000_000))
166 }
167 "hour" => {
168 let days = self.0.div_euclid(86_400_000_000);
169 let day_micros = self.0.rem_euclid(86_400_000_000);
170 let hours = day_micros / 3_600_000_000;
171 Some(Self(days * 86_400_000_000 + hours * 3_600_000_000))
172 }
173 "minute" => {
174 let days = self.0.div_euclid(86_400_000_000);
175 let day_micros = self.0.rem_euclid(86_400_000_000);
176 let minutes = day_micros / 60_000_000;
177 Some(Self(days * 86_400_000_000 + minutes * 60_000_000))
178 }
179 "second" => {
180 let days = self.0.div_euclid(86_400_000_000);
181 let day_micros = self.0.rem_euclid(86_400_000_000);
182 let seconds = day_micros / 1_000_000;
183 Some(Self(days * 86_400_000_000 + seconds * 1_000_000))
184 }
185 _ => None,
186 }
187 }
188
189 #[must_use]
191 pub fn add_duration(self, dur: &super::Duration) -> Self {
192 let date = self
194 .to_date()
195 .add_duration(&super::Duration::from_months(dur.months()));
196 let time = self.to_time();
197 let base = Self::from_date_time(date, time);
198 let day_micros = dur.days() * 86_400_000_000;
200 let nano_micros = dur.nanos() / 1000;
201 Self(base.0 + day_micros + nano_micros)
202 }
203}
204
205impl fmt::Debug for Timestamp {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 write!(f, "Timestamp({}μs)", self.0)
208 }
209}
210
211impl fmt::Display for Timestamp {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 let micros = self.0;
214 let micro_frac = micros.rem_euclid(1_000_000) as u64;
215
216 #[allow(clippy::cast_possible_truncation)]
218 let total_days = micros.div_euclid(86_400_000_000) as i32;
219 let day_micros = micros.rem_euclid(86_400_000_000);
220 let day_secs = day_micros / 1_000_000;
221
222 let hours = day_secs / 3600;
223 let minutes = (day_secs % 3600) / 60;
224 let seconds = day_secs % 60;
225
226 let (year, month, day) = civil_from_days(total_days);
227
228 write!(
229 f,
230 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
231 year, month, day, hours, minutes, seconds, micro_frac
232 )
233 }
234}
235
236impl From<i64> for Timestamp {
237 fn from(micros: i64) -> Self {
238 Self::from_micros(micros)
239 }
240}
241
242impl From<Timestamp> for i64 {
243 fn from(ts: Timestamp) -> Self {
244 ts.0
245 }
246}
247
248impl TryFrom<SystemTime> for Timestamp {
249 type Error = ();
250
251 fn try_from(time: SystemTime) -> Result<Self, Self::Error> {
252 match time.duration_since(UNIX_EPOCH) {
253 Ok(duration) => {
254 let micros = i64::try_from(duration.as_micros()).map_err(|_| ())?;
255 Ok(Self::from_micros(micros))
256 }
257 Err(e) => {
258 let micros = i64::try_from(e.duration().as_micros()).map_err(|_| ())?;
259 Ok(Self::from_micros(micros.checked_neg().ok_or(())?))
260 }
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_timestamp_creation() {
271 let ts = Timestamp::from_secs(1000);
272 assert_eq!(ts.as_secs(), 1000);
273 assert_eq!(ts.as_millis(), 1_000_000);
274 assert_eq!(ts.as_micros(), 1_000_000_000);
275
276 let ts = Timestamp::from_millis(1234);
277 assert_eq!(ts.as_millis(), 1234);
278
279 let ts = Timestamp::from_micros(1_234_567);
280 assert_eq!(ts.as_micros(), 1_234_567);
281 }
282
283 #[test]
284 #[cfg(not(miri))] fn test_timestamp_now() {
286 let ts = Timestamp::now();
287 assert!(ts.as_secs() > 1_577_836_800);
289 }
290
291 #[test]
292 fn test_timestamp_arithmetic() {
293 let ts = Timestamp::from_secs(1000);
294
295 let ts2 = ts.add_micros(1_000_000);
296 assert_eq!(ts2.as_secs(), 1001);
297
298 let ts3 = ts.sub_micros(1_000_000);
299 assert_eq!(ts3.as_secs(), 999);
300
301 assert_eq!(ts2.duration_since(ts), 1_000_000);
302 assert_eq!(ts.duration_since(ts2), -1_000_000);
303 }
304
305 #[test]
306 fn test_timestamp_ordering() {
307 let ts1 = Timestamp::from_secs(100);
308 let ts2 = Timestamp::from_secs(200);
309
310 assert!(ts1 < ts2);
311 assert!(ts2 > ts1);
312 assert_eq!(ts1, Timestamp::from_secs(100));
313 }
314
315 #[test]
316 #[cfg(not(miri))] fn test_timestamp_system_time_conversion() {
318 let now = SystemTime::now();
319 let ts: Timestamp = now.try_into().unwrap();
320 let back = ts.as_system_time().unwrap();
321
322 let diff = back
324 .duration_since(now)
325 .or_else(|e| Ok::<_, ()>(e.duration()))
326 .unwrap();
327 assert!(diff.as_micros() < 2);
328 }
329
330 #[test]
331 fn test_truncate() {
332 let date = crate::types::Date::from_ymd(2024, 6, 15).unwrap();
334 let time = crate::types::Time::from_hms_nano(14, 30, 45, 123_456_000).unwrap();
335 let ts = Timestamp::from_date_time(date, time);
336
337 let year = ts.truncate("year").unwrap();
338 assert_eq!(year.to_date().to_string(), "2024-01-01");
339 assert_eq!(year.to_time().hour(), 0);
340
341 let month = ts.truncate("month").unwrap();
342 assert_eq!(month.to_date().to_string(), "2024-06-01");
343 assert_eq!(month.to_time().hour(), 0);
344
345 let day = ts.truncate("day").unwrap();
346 assert_eq!(day.to_date().to_string(), "2024-06-15");
347 assert_eq!(day.to_time().hour(), 0);
348
349 let hour = ts.truncate("hour").unwrap();
350 assert_eq!(hour.to_time().hour(), 14);
351 assert_eq!(hour.to_time().minute(), 0);
352
353 let minute = ts.truncate("minute").unwrap();
354 assert_eq!(minute.to_time().hour(), 14);
355 assert_eq!(minute.to_time().minute(), 30);
356 assert_eq!(minute.to_time().second(), 0);
357
358 let second = ts.truncate("second").unwrap();
359 assert_eq!(second.to_time().second(), 45);
360 assert_eq!(second.to_time().nanosecond(), 0);
361
362 assert!(ts.truncate("invalid").is_none());
363 }
364
365 #[test]
366 fn test_timestamp_epoch() {
367 assert_eq!(Timestamp::EPOCH.as_micros(), 0);
368 assert_eq!(Timestamp::EPOCH.as_secs(), 0);
369 }
370
371 #[test]
372 fn test_add_duration_days_and_nanos() {
373 use crate::types::Duration;
374 let ts = Timestamp::from_secs(1_700_000_000); let dur = Duration::from_days(1);
376 let result = ts.add_duration(&dur);
377 assert_eq!(result.as_micros() - ts.as_micros(), 86_400_000_000);
379 }
380
381 #[test]
382 fn test_add_duration_months() {
383 use crate::types::Duration;
384 let ts = Timestamp::from_secs(1_700_000_000); let dur = Duration::from_months(2);
386 let result = ts.add_duration(&dur);
387 let result_date = result.to_date();
388 assert_eq!(result_date.month(), 1);
390 assert_eq!(result_date.year(), 2024);
391 }
392}