1use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::error::{Error, Result};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum Timestamp {
23 At(SystemTime),
25 Text(String),
28}
29
30impl From<SystemTime> for Timestamp {
31 fn from(time: SystemTime) -> Self {
32 Self::At(time)
33 }
34}
35
36impl From<String> for Timestamp {
37 fn from(text: String) -> Self {
38 Self::Text(text)
39 }
40}
41
42impl From<&str> for Timestamp {
43 fn from(text: &str) -> Self {
44 Self::Text(text.to_owned())
45 }
46}
47
48impl Timestamp {
49 pub(crate) fn to_query(&self) -> Result<String> {
51 match self {
52 Self::Text(text) => Ok(text.clone()),
53 Self::At(time) => {
54 let secs = time.duration_since(UNIX_EPOCH).map_err(|_| {
55 Error::InvalidInput("from/to before 1970 is not supported".into())
56 })?;
57 Ok(rfc3339(secs.as_secs()))
58 }
59 }
60 }
61}
62
63fn rfc3339(secs: u64) -> String {
65 let days = (secs / 86_400) as i64;
66 let rem = secs % 86_400;
67 let (y, m, d) = civil_from_days(days);
68 format!(
69 "{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z",
70 rem / 3_600,
71 rem % 3_600 / 60,
72 rem % 60
73 )
74}
75
76fn civil_from_days(days: i64) -> (i64, u32, u32) {
78 let z = days + 719_468;
79 let era = z.div_euclid(146_097);
80 let doe = z.rem_euclid(146_097);
81 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
82 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
83 let mp = (5 * doy + 2) / 153;
84 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
85 let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
86 let y = yoe + era * 400 + i64::from(m <= 2);
87 (y, m, d)
88}
89
90#[cfg(test)]
91mod tests {
92 use std::time::Duration;
93
94 use super::*;
95
96 #[test]
97 fn formats_utc_seconds() {
98 assert_eq!(rfc3339(0), "1970-01-01T00:00:00Z");
99 assert_eq!(rfc3339(951_782_400), "2000-02-29T00:00:00Z");
100 assert_eq!(rfc3339(1_793_093_643), "2026-10-27T09:34:03Z");
101 let at = Timestamp::At(UNIX_EPOCH + Duration::from_millis(1_500));
102 assert_eq!(at.to_query().unwrap(), "1970-01-01T00:00:01Z");
103 assert_eq!(Timestamp::from("x").to_query().unwrap(), "x");
104 }
105}