pub(crate) fn now_civil() -> (i32, u32, u32, u32, u32, u32) {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
epoch_to_civil(secs)
}
fn epoch_to_civil(epoch_secs: u64) -> (i32, u32, u32, u32, u32, u32) {
let total_secs = epoch_secs;
let day_secs = (total_secs % 86400) as u32;
let hh = day_secs / 3600;
let mm = (day_secs % 3600) / 60;
let ss = day_secs % 60;
let z = (total_secs / 86400) as i64 + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u32; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as i32, m, d, hh, mm, ss)
}
pub fn rfc3339_utc(epoch_ms: u64) -> String {
let (y, m, d, hh, mm, ss) = epoch_to_civil(epoch_ms / 1000);
let ms = epoch_ms % 1000;
format!("{y:04}-{m:02}-{d:02}T{hh:02}:{mm:02}:{ss:02}.{ms:03}Z")
}
pub fn parse_rfc3339_utc(text: &str) -> Option<u64> {
let rest = text
.strip_suffix(['Z', 'z'])
.or_else(|| text.strip_suffix("+00:00"))?;
let (date_time, fraction) = match rest.split_once('.') {
Some((dt, frac)) => (dt, frac),
None => (rest, ""),
};
let secs = parse_date_time(date_time)?;
Some(secs.checked_mul(1000)? + fraction_ms(fraction)?)
}
fn parse_date_time(text: &str) -> Option<u64> {
let b = text.as_bytes();
let shape_ok = b.len() == 19
&& b[4] == b'-'
&& b[7] == b'-'
&& matches!(b[10], b'T' | b't')
&& b[13] == b':'
&& b[16] == b':';
if !shape_ok {
return None;
}
let num = |range: std::ops::Range<usize>| text.get(range)?.parse::<u32>().ok();
let (y, m, d) = (num(0..4)?, num(5..7)?, num(8..10)?);
let (hh, mm, ss) = (num(11..13)?, num(14..16)?, num(17..19)?);
if !(1..=12).contains(&m) || !(1..=31).contains(&d) || hh > 23 || mm > 59 || ss > 60 {
return None;
}
let days = u64::try_from(days_from_civil(y as i64, m, d)).ok()?;
Some(days * 86400 + u64::from(hh * 3600 + mm * 60 + ss))
}
fn fraction_ms(fraction: &str) -> Option<u64> {
if !fraction.bytes().all(|c| c.is_ascii_digit()) {
return None;
}
let ms: String = fraction.chars().chain("000".chars()).take(3).collect();
ms.parse().ok()
}
fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let mp = i64::from((m + 9) % 12);
let doy = (153 * mp + 2) / 5 + i64::from(d) - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe - 719468
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rfc3339_formats_the_epoch() {
assert_eq!(rfc3339_utc(0), "1970-01-01T00:00:00.000Z");
}
#[test]
fn rfc3339_keeps_milliseconds() {
assert_eq!(rfc3339_utc(1_700_000_000_123), "2023-11-14T22:13:20.123Z");
}
#[test]
fn rfc3339_handles_a_leap_day() {
assert_eq!(rfc3339_utc(1_709_251_199_999), "2024-02-29T23:59:59.999Z");
}
#[test]
fn parse_reads_what_rfc3339_utc_writes() {
for ms in [0, 1_700_000_000_123, 1_709_251_199_999] {
assert_eq!(parse_rfc3339_utc(&rfc3339_utc(ms)), Some(ms));
}
}
#[test]
fn parse_takes_any_fraction_or_none() {
assert_eq!(
parse_rfc3339_utc("2023-11-14T22:13:20Z"),
Some(1_700_000_000_000)
);
assert_eq!(
parse_rfc3339_utc("2023-11-14T22:13:20.5Z"),
Some(1_700_000_000_500)
);
assert_eq!(
parse_rfc3339_utc("2023-11-14T22:13:20.123456Z"),
Some(1_700_000_000_123)
);
assert_eq!(
parse_rfc3339_utc("2023-11-14T22:13:20+00:00"),
Some(1_700_000_000_000)
);
}
#[test]
fn parse_refuses_other_text() {
for bad in [
"",
"now",
"2023-11-14",
"2023-11-14T22:13:20",
"2023-13-01T00:00:00Z",
"2023-11-14T22:13:20+02:00",
"1969-12-31T23:59:59Z",
"2023-11-14T25:00:00Z",
] {
assert_eq!(parse_rfc3339_utc(bad), None, "{bad}");
}
}
}