use chrono::{DateTime, Utc};
pub fn trunc_ms(dt: DateTime<Utc>) -> DateTime<Utc> {
DateTime::from_timestamp_millis(dt.timestamp_millis()).unwrap_or(dt)
}
pub fn fmt_rfc3339_ms(dt: DateTime<Utc>) -> String {
dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
}
pub fn fmt_rfc3339_canonical(dt: DateTime<Utc>) -> String {
if dt.timestamp_subsec_nanos() % 1_000_000 == 0 {
fmt_rfc3339_ms(dt)
} else {
dt.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true)
}
}
pub mod canonical_rfc3339 {
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(dt: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&super::fmt_rfc3339_canonical(*dt))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<DateTime<Utc>, D::Error> {
DateTime::<Utc>::deserialize(d)
}
}
pub mod canonical_rfc3339_option {
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(dt: &Option<DateTime<Utc>>, s: S) -> Result<S::Ok, S::Error> {
match dt {
Some(dt) => s.serialize_str(&super::fmt_rfc3339_canonical(*dt)),
None => s.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<DateTime<Utc>>, D::Error> {
Option::<DateTime<Utc>>::deserialize(d)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_keeps_zero_millis() {
let dt: DateTime<Utc> = "2026-12-31T23:59:59.000Z".parse().unwrap();
assert_eq!(fmt_rfc3339_canonical(dt), "2026-12-31T23:59:59.000Z");
let dt: DateTime<Utc> = "2026-12-31T23:59:59Z".parse().unwrap();
assert_eq!(fmt_rfc3339_canonical(dt), "2026-12-31T23:59:59.000Z");
}
#[test]
fn canonical_is_lossless_above_millis() {
for s in [
"2026-04-16T10:30:15.123Z",
"2026-04-16T10:30:15.123456Z",
"2026-04-16T10:30:15.123456789Z",
] {
let dt: DateTime<Utc> = s.parse().unwrap();
assert_eq!(fmt_rfc3339_canonical(dt), s);
}
}
#[test]
fn ms_truncation_drops_sub_ms() {
let dt = DateTime::from_timestamp_nanos(1_700_000_000_123_456_789);
let truncated = trunc_ms(dt);
assert_eq!(truncated.timestamp_subsec_nanos() % 1_000_000, 0);
}
#[test]
fn fmt_emits_canonical_form() {
let dt = DateTime::from_timestamp_millis(1_700_000_000_123).unwrap();
let s = fmt_rfc3339_ms(dt);
assert!(s.ends_with("Z"), "got {s}");
assert!(s.contains(".123"), "got {s}");
}
}