use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) fn now_unix() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs() as i64)
.unwrap_or(0)
}
pub(crate) fn now_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as i64)
.unwrap_or(0)
}
pub fn parse_rfc3339(text: &str) -> Option<i64> {
let text = text.trim();
let bytes = text.as_bytes();
if bytes.len() < 20 {
return None;
}
let digits = |from: usize, to: usize| -> Option<i64> {
let slice = text.get(from..to)?;
if !slice.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
slice.parse().ok()
};
let year = digits(0, 4)?;
let month = digits(5, 7)?;
let day = digits(8, 10)?;
let hour = digits(11, 13)?;
let minute = digits(14, 16)?;
let second = digits(17, 19)?;
if bytes[4] != b'-' || bytes[7] != b'-' || bytes[10] != b'T' && bytes[10] != b't' {
return None;
}
if bytes[13] != b':' || bytes[16] != b':' {
return None;
}
let mut index = 19;
if bytes.get(index) == Some(&b'.') {
index += 1;
let start = index;
while bytes.get(index).is_some_and(u8::is_ascii_digit) {
index += 1;
}
if index == start {
return None;
}
}
let offset_seconds = match bytes.get(index) {
Some(b'Z') | Some(b'z') if index + 1 == bytes.len() => 0,
Some(sign @ (b'+' | b'-')) if index + 6 == bytes.len() && bytes[index + 3] == b':' => {
let oh = digits(index + 1, index + 3)?;
let om = digits(index + 4, index + 6)?;
if oh > 23 || om > 59 {
return None;
}
let total = oh * 3600 + om * 60;
if *sign == b'+' {
total
} else {
-total
}
}
_ => return None,
};
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
if hour > 23 || minute > 59 || second > 60 {
return None;
}
let days = days_from_civil(year, month, day);
Some(days * 86_400 + hour * 3600 + minute * 60 + second - offset_seconds)
}
pub fn format_rfc3339(unix: i64) -> String {
let days = unix.div_euclid(86_400);
let rest = unix.rem_euclid(86_400);
let (year, month, day) = civil_from_days(days);
format!(
"{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z",
rest / 3600,
(rest % 3600) / 60,
rest % 60
)
}
pub(crate) fn format_amz_date(unix: i64) -> (String, String) {
let days = unix.div_euclid(86_400);
let rest = unix.rem_euclid(86_400);
let (year, month, day) = civil_from_days(days);
let date = format!("{year:04}{month:02}{day:02}");
let stamp = format!(
"{date}T{:02}{:02}{:02}Z",
rest / 3600,
(rest % 3600) / 60,
rest % 60
);
(stamp, date)
}
pub(crate) fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
let y = if month <= 2 { year - 1 } else { year };
let era = y.div_euclid(400);
let yoe = y - era * 400;
let mp = (month + 9) % 12;
let doy = (153 * mp + 2) / 5 + day - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
fn civil_from_days(days: i64) -> (i64, i64, i64) {
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if month <= 2 { y + 1 } else { y };
(year, month, day)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_and_formats_the_vector_timestamps() {
for text in [
"2026-09-03T13:00:00Z",
"2026-09-03T18:00:00Z",
"1970-01-01T00:00:00Z",
] {
let unix = parse_rfc3339(text).unwrap();
assert_eq!(format_rfc3339(unix), text);
}
assert_eq!(parse_rfc3339("1970-01-01T00:00:00Z"), Some(0));
assert_eq!(parse_rfc3339("2000-03-01T00:00:00Z"), Some(951_868_800));
let unix = parse_rfc3339("2015-08-30T12:36:00Z").unwrap();
assert_eq!(
format_amz_date(unix),
("20150830T123600Z".to_string(), "20150830".to_string())
);
}
#[test]
fn offsets_and_fractions_normalize_to_utc() {
assert_eq!(
parse_rfc3339("2026-09-03T15:00:00+02:00"),
parse_rfc3339("2026-09-03T13:00:00Z")
);
assert_eq!(
parse_rfc3339("2026-09-03T13:00:00.250Z"),
parse_rfc3339("2026-09-03T13:00:00Z")
);
assert_eq!(parse_rfc3339("2026-09-03T13:00:00"), None);
assert_eq!(parse_rfc3339("2026-13-03T13:00:00Z"), None);
assert_eq!(parse_rfc3339("not a date"), None);
}
}