alibi_core/utils/datetime/
mod.rs1mod parse;
4use chrono::{DateTime, Datelike, Duration, NaiveDate, SecondsFormat, Utc};
5pub use parse::parse_date_millis;
6use serde::Serializer;
7
8pub fn serialize<S: Serializer>(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error> {
17 serializer.serialize_str(&value.to_rfc3339_opts(SecondsFormat::Millis, true))
18}
19
20pub fn serialize_optional<S: Serializer>(
26 value: &Option<DateTime<Utc>>,
27 serializer: S,
28) -> Result<S::Ok, S::Error> {
29 match value {
30 Some(value) => {
31 serializer.serialize_some(&value.to_rfc3339_opts(SecondsFormat::Millis, true))
32 }
33 None => serializer.serialize_none(),
34 }
35}
36
37#[must_use]
44pub fn normalize_json_date(value: &str) -> Option<String> {
45 let bytes = value.as_bytes();
46 if bytes.len() < 20
47 || !bytes.iter().enumerate().all(|(index, byte)| match index {
48 4 | 7 => *byte == b'-',
49 10 => *byte == b'T',
50 13 | 16 => *byte == b':',
51 19 if bytes.len() > 20 => *byte == b'.',
52 index if index == bytes.len() - 1 => *byte == b'Z',
53 _ => byte.is_ascii_digit(),
54 })
55 {
56 return None;
57 }
58 let year = value.get(..4)?.parse().ok()?;
59 let month = value.get(5..7)?.parse().ok()?;
60 let day: u32 = value.get(8..10)?.parse().ok()?;
61 let hour: u32 = value.get(11..13)?.parse().ok()?;
62 let minute: u32 = value.get(14..16)?.parse().ok()?;
63 let second: u32 = value.get(17..19)?.parse().ok()?;
64 let fraction = if bytes.len() == 20 {
65 ""
66 } else {
67 let fraction = value.get(20..bytes.len() - 1)?;
68 if fraction.is_empty() {
69 return None;
70 }
71 fraction
72 };
73 if !(1..=31).contains(&day)
74 || hour > 24
75 || minute > 59
76 || second > 59
77 || (hour == 24
78 && (minute != 0 || second != 0 || fraction.bytes().any(|digit| digit != b'0')))
79 {
80 return None;
81 }
82 let milliseconds = fraction
83 .bytes()
84 .chain(std::iter::repeat(b'0'))
85 .take(3)
86 .fold(0u32, |value, digit| value * 10 + u32::from(digit - b'0'));
87 let offset = i64::from(day - 1) * 86_400_000
88 + i64::from(hour) * 3_600_000
89 + i64::from(minute) * 60_000
90 + i64::from(second) * 1_000
91 + i64::from(milliseconds);
92 let date = NaiveDate::from_ymd_opt(year, month, 1)?
93 .and_hms_opt(0, 0, 0)?
94 .checked_add_signed(Duration::milliseconds(offset))?;
95 let year = if date.year() > 9999 {
96 format!("+{:06}", date.year())
97 } else {
98 format!("{:04}", date.year())
99 };
100 Some(format!("{year}{}", date.format("-%m-%dT%H:%M:%S%.3fZ")))
101}
102
103#[must_use]
106pub fn json_date_millis(millis: i64) -> String {
107 let days = millis.div_euclid(86_400_000);
108 let shifted = days + 719_468;
109 let era = shifted.div_euclid(146_097);
110 let day_of_era = shifted - era * 146_097;
111 let year_of_era =
112 (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
113 let mut year = year_of_era + era * 400;
114 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
115 let month_index = (5 * day_of_year + 2) / 153;
116 let day = day_of_year - (153 * month_index + 2) / 5 + 1;
117 let month = month_index + if month_index < 10 { 3 } else { -9 };
118 year += i64::from(month <= 2);
119 let year = if (0..=9999).contains(&year) {
120 format!("{year:04}")
121 } else {
122 format!("{year:+07}")
123 };
124 let time = millis.rem_euclid(86_400_000);
125 format!(
126 "{year}-{month:02}-{day:02}T{:02}:{:02}:{:02}.{:03}Z",
127 time / 3_600_000,
128 time / 60_000 % 60,
129 time / 1000 % 60,
130 time % 1000
131 )
132}
133
134#[cfg(test)]
136mod tests {
137 use super::*;
138 use serde::{Deserialize, Serialize};
139
140 #[derive(Serialize, Deserialize)]
141 struct Timestamps {
142 #[serde(serialize_with = "super::serialize")]
143 created_at: DateTime<Utc>,
144 #[serde(serialize_with = "super::serialize_optional")]
145 expires_at: Option<DateTime<Utc>>,
146 }
147
148 #[test]
149 fn wire_precision_is_milliseconds_for_every_source_precision() {
150 for (input, expected) in [
151 ("2026-09-30T12:00:00Z", "2026-09-30T12:00:00.000Z"),
152 ("2026-09-30T12:00:00.123Z", "2026-09-30T12:00:00.123Z"),
153 ("2026-09-30T12:00:00.123456Z", "2026-09-30T12:00:00.123Z"),
154 ("2026-09-30T12:00:00.123456789Z", "2026-09-30T12:00:00.123Z"),
155 ] {
156 let value: DateTime<Utc> = input.parse().expect("valid timestamp");
157 let encoded = serde_json::to_value(Timestamps {
158 created_at: value,
159 expires_at: Some(value),
160 })
161 .expect("serialize timestamps");
162 assert_eq!(
163 (*(encoded)
164 .get("created_at")
165 .unwrap_or(&serde_json::Value::Null)),
166 expected
167 );
168 assert_eq!(
169 (*(encoded)
170 .get("expires_at")
171 .unwrap_or(&serde_json::Value::Null)),
172 expected
173 );
174 let decoded: Timestamps =
175 serde_json::from_value(encoded).expect("deserialize timestamps");
176 assert_eq!(
177 decoded.created_at.timestamp_millis(),
178 value.timestamp_millis()
179 );
180 assert_eq!(
181 decoded.expires_at.map(|date| date.timestamp_millis()),
182 Some(value.timestamp_millis())
183 );
184 }
185 }
186
187 #[test]
188 fn optional_null_remains_null_and_input_keeps_full_precision() {
189 let value: Timestamps = serde_json::from_value(
190 serde_json::json!({"created_at":"2026-09-30T12:00:00.123456789Z","expires_at":null}),
191 )
192 .expect("deserialize timestamps");
193 assert_eq!(value.created_at.timestamp_subsec_nanos(), 123_456_789);
194 let encoded = serde_json::to_value(value).expect("serialize timestamps");
195 assert_eq!(
196 (*(encoded)
197 .get("created_at")
198 .unwrap_or(&serde_json::Value::Null)),
199 "2026-09-30T12:00:00.123Z"
200 );
201 assert_eq!(
202 (*(encoded)
203 .get("expires_at")
204 .unwrap_or(&serde_json::Value::Null)),
205 serde_json::Value::Null
206 );
207 }
208}
209