use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) fn now_ms() -> u64 {
ms(SystemTime::now())
}
pub(crate) fn ms(time: SystemTime) -> u64 {
time.duration_since(UNIX_EPOCH).map_or(0, |since| {
u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
})
}
pub(crate) fn parse_ms(text: &str) -> Option<u64> {
parse_ns(text).map(|ns| ns / 1_000_000)
}
pub(crate) fn parse_ns(text: &str) -> Option<u64> {
let text = text.strip_suffix('Z')?;
let (date, time) = text.split_once('T')?;
let mut date = date.splitn(3, '-').map(str::parse::<u64>);
let (year, month, day) = (date.next()?.ok()?, date.next()?.ok()?, date.next()?.ok()?);
let (time, fraction) = time.split_once('.').unwrap_or((time, ""));
let mut time = time.splitn(3, ':').map(str::parse::<u64>);
let (hour, minute, second) = (time.next()?.ok()?, time.next()?.ok()?, time.next()?.ok()?);
if !(1970..=9999).contains(&year)
|| !(1..=12).contains(&month)
|| !(1..=31).contains(&day)
|| hour > 23
|| minute > 59
|| second > 60
|| !fraction.chars().all(|c| c.is_ascii_digit())
{
return None;
}
let nanos = format!("{fraction:0<9}")[..9].parse::<u64>().ok()?;
let days = days_from_civil(year, month, day);
let seconds = ((days * 24 + hour) * 60 + minute) * 60 + second;
seconds.checked_mul(1_000_000_000)?.checked_add(nanos)
}
fn days_from_civil(year: u64, month: u64, day: u64) -> u64 {
let year = if month <= 2 { year - 1 } else { year };
let era = year / 400;
let year_of_era = year - era * 400;
let month_index = (month + 9) % 12;
let day_of_year = (153 * month_index + 2) / 5 + day - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era - 719_468
}
#[cfg(test)]
mod tests {
use super::parse_ms;
#[test]
fn github_and_docker_timestamps_parse_to_epoch_milliseconds() {
assert_eq!(parse_ms("1970-01-01T00:00:00Z"), Some(0));
assert_eq!(parse_ms("2026-10-22T19:27:25Z"), Some(1_792_697_245_000));
assert_eq!(
parse_ms("2026-10-22T19:27:25.233122085Z"),
Some(1_792_697_245_233)
);
assert_eq!(parse_ms("2024-02-29T12:00:00.5Z"), Some(1_709_208_000_500));
for invalid in [
"",
"2026-10-22T19:27:25",
"2026-13-01T00:00:00Z",
"yesterday",
] {
assert_eq!(parse_ms(invalid), None, "{invalid}");
}
}
}