mod parse;
use chrono::{DateTime, Datelike, Duration, NaiveDate, SecondsFormat, Utc};
pub use parse::parse_date_millis;
use serde::Serializer;
pub fn serialize<S: Serializer>(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&value.to_rfc3339_opts(SecondsFormat::Millis, true))
}
pub fn serialize_optional<S: Serializer>(
value: &Option<DateTime<Utc>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(value) => {
serializer.serialize_some(&value.to_rfc3339_opts(SecondsFormat::Millis, true))
}
None => serializer.serialize_none(),
}
}
#[must_use]
pub fn normalize_json_date(value: &str) -> Option<String> {
let bytes = value.as_bytes();
if bytes.len() < 20
|| !bytes.iter().enumerate().all(|(index, byte)| match index {
4 | 7 => *byte == b'-',
10 => *byte == b'T',
13 | 16 => *byte == b':',
19 if bytes.len() > 20 => *byte == b'.',
index if index == bytes.len() - 1 => *byte == b'Z',
_ => byte.is_ascii_digit(),
})
{
return None;
}
let year = value.get(..4)?.parse().ok()?;
let month = value.get(5..7)?.parse().ok()?;
let day: u32 = value.get(8..10)?.parse().ok()?;
let hour: u32 = value.get(11..13)?.parse().ok()?;
let minute: u32 = value.get(14..16)?.parse().ok()?;
let second: u32 = value.get(17..19)?.parse().ok()?;
let fraction = if bytes.len() == 20 {
""
} else {
let fraction = value.get(20..bytes.len() - 1)?;
if fraction.is_empty() {
return None;
}
fraction
};
if !(1..=31).contains(&day)
|| hour > 24
|| minute > 59
|| second > 59
|| (hour == 24
&& (minute != 0 || second != 0 || fraction.bytes().any(|digit| digit != b'0')))
{
return None;
}
let milliseconds = fraction
.bytes()
.chain(std::iter::repeat(b'0'))
.take(3)
.fold(0u32, |value, digit| value * 10 + u32::from(digit - b'0'));
let offset = i64::from(day - 1) * 86_400_000
+ i64::from(hour) * 3_600_000
+ i64::from(minute) * 60_000
+ i64::from(second) * 1_000
+ i64::from(milliseconds);
let date = NaiveDate::from_ymd_opt(year, month, 1)?
.and_hms_opt(0, 0, 0)?
.checked_add_signed(Duration::milliseconds(offset))?;
let year = if date.year() > 9999 {
format!("+{:06}", date.year())
} else {
format!("{:04}", date.year())
};
Some(format!("{year}{}", date.format("-%m-%dT%H:%M:%S%.3fZ")))
}
#[must_use]
pub fn json_date_millis(millis: i64) -> String {
let days = millis.div_euclid(86_400_000);
let shifted = days + 719_468;
let era = shifted.div_euclid(146_097);
let day_of_era = shifted - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let mut year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let month_index = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * month_index + 2) / 5 + 1;
let month = month_index + if month_index < 10 { 3 } else { -9 };
year += i64::from(month <= 2);
let year = if (0..=9999).contains(&year) {
format!("{year:04}")
} else {
format!("{year:+07}")
};
let time = millis.rem_euclid(86_400_000);
format!(
"{year}-{month:02}-{day:02}T{:02}:{:02}:{:02}.{:03}Z",
time / 3_600_000,
time / 60_000 % 60,
time / 1000 % 60,
time % 1000
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Timestamps {
#[serde(serialize_with = "super::serialize")]
created_at: DateTime<Utc>,
#[serde(serialize_with = "super::serialize_optional")]
expires_at: Option<DateTime<Utc>>,
}
#[test]
fn wire_precision_is_milliseconds_for_every_source_precision() {
for (input, expected) in [
("2026-09-30T12:00:00Z", "2026-09-30T12:00:00.000Z"),
("2026-09-30T12:00:00.123Z", "2026-09-30T12:00:00.123Z"),
("2026-09-30T12:00:00.123456Z", "2026-09-30T12:00:00.123Z"),
("2026-09-30T12:00:00.123456789Z", "2026-09-30T12:00:00.123Z"),
] {
let value: DateTime<Utc> = input.parse().expect("valid timestamp");
let encoded = serde_json::to_value(Timestamps {
created_at: value,
expires_at: Some(value),
})
.expect("serialize timestamps");
assert_eq!(
(*(encoded)
.get("created_at")
.unwrap_or(&serde_json::Value::Null)),
expected
);
assert_eq!(
(*(encoded)
.get("expires_at")
.unwrap_or(&serde_json::Value::Null)),
expected
);
let decoded: Timestamps =
serde_json::from_value(encoded).expect("deserialize timestamps");
assert_eq!(
decoded.created_at.timestamp_millis(),
value.timestamp_millis()
);
assert_eq!(
decoded.expires_at.map(|date| date.timestamp_millis()),
Some(value.timestamp_millis())
);
}
}
#[test]
fn optional_null_remains_null_and_input_keeps_full_precision() {
let value: Timestamps = serde_json::from_value(
serde_json::json!({"created_at":"2026-09-30T12:00:00.123456789Z","expires_at":null}),
)
.expect("deserialize timestamps");
assert_eq!(value.created_at.timestamp_subsec_nanos(), 123_456_789);
let encoded = serde_json::to_value(value).expect("serialize timestamps");
assert_eq!(
(*(encoded)
.get("created_at")
.unwrap_or(&serde_json::Value::Null)),
"2026-09-30T12:00:00.123Z"
);
assert_eq!(
(*(encoded)
.get("expires_at")
.unwrap_or(&serde_json::Value::Null)),
serde_json::Value::Null
);
}
}