use crate::error::{DbError, Result};
use std::time::{Duration, SystemTime};
pub const TIMESTAMP_LEN: usize = 27;
pub const OPEN_SENTINEL: &str = "9999-12-31T23:59:59.999999Z";
pub const CANONICAL_TS_GLOB: &str =
"'[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9]Z'";
pub fn is_canonical(s: &str) -> bool {
let b = s.as_bytes();
if b.len() != TIMESTAMP_LEN {
return false;
}
const DIGITS: [usize; 20] = [
0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18, 20, 21, 22, 23, 24, 25,
];
const SEPS: [(usize, u8); 7] = [
(4, b'-'),
(7, b'-'),
(10, b'T'),
(13, b':'),
(16, b':'),
(19, b'.'),
(26, b'Z'),
];
DIGITS.iter().all(|&i| b[i].is_ascii_digit()) && SEPS.iter().all(|&(i, c)| b[i] == c)
}
pub fn normalize(s: &str) -> Result<String> {
if is_canonical(s) {
return Ok(s.to_string());
}
if s.len() == 20 && s.ends_with('Z') {
let widened = format!("{}.000000Z", &s[..19]);
if is_canonical(&widened) {
return Ok(widened);
}
}
Err(DbError::InvalidTimestamp {
value: s.to_string(),
reason: "expected YYYY-MM-DDTHH:MM:SS.ffffffZ".to_string(),
})
}
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
let y = y - if m <= 2 { 1 } else { 0 };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400; let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146097 + doe - 719468
}
fn civil_from_days(z: i64) -> (i64, i64, i64) {
let z = z + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe + 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 }; (y + if m <= 2 { 1 } else { 0 }, m, d)
}
fn days_in_month(year: i64, month: i64) -> i64 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 => 29,
2 => 28,
_ => 0,
}
}
pub fn parse(s: &str) -> Result<SystemTime> {
let canon = normalize(s)?;
let b = canon.as_bytes();
let num = |lo: usize, hi: usize| -> i64 {
canon[lo..hi]
.parse::<i64>()
.expect("is_canonical checked digits")
};
let (year, month, day) = (num(0, 4), num(5, 7), num(8, 10));
let (hour, min, sec) = (num(11, 13), num(14, 16), num(17, 19));
let micros = num(20, 26);
debug_assert_eq!(b[26], b'Z');
let bad = |why: &str| DbError::InvalidTimestamp {
value: s.to_string(),
reason: why.to_string(),
};
if !(1..=12).contains(&month) {
return Err(bad("month out of range"));
}
if day < 1 || day > days_in_month(year, month) {
return Err(bad("day out of range for month"));
}
if hour > 23 || min > 59 || sec > 59 {
return Err(bad("time component out of range"));
}
let secs = days_from_civil(year, month, day) * 86_400 + hour * 3600 + min * 60 + sec;
let offset = Duration::new(secs.unsigned_abs(), micros as u32 * 1_000);
let t = if secs >= 0 {
SystemTime::UNIX_EPOCH.checked_add(offset)
} else {
SystemTime::UNIX_EPOCH.checked_sub(offset)
};
t.ok_or_else(|| bad("not representable as a SystemTime on this platform"))
}
pub fn format(st: SystemTime) -> String {
let d = st
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let secs = d.as_secs() as i64;
let micros = d.subsec_micros();
let days = secs.div_euclid(86_400);
let tod = secs.rem_euclid(86_400);
let (y, m, dd) = civil_from_days(days);
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
y,
m,
dd,
tod / 3600,
(tod % 3600) / 60,
tod % 60,
micros
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sentinel_is_canonical_and_maximal() {
assert!(is_canonical(OPEN_SENTINEL));
assert_eq!(OPEN_SENTINEL.len(), TIMESTAMP_LEN);
assert!("2026-01-01T00:00:00.000000Z" < OPEN_SENTINEL);
assert!("9999-12-31T23:59:59.999998Z" < OPEN_SENTINEL);
}
#[test]
fn canonical_form_orders_lexicographically() {
let a = normalize("2026-01-01T00:00:00Z").unwrap();
let b = "2026-01-01T00:00:00.000000Z";
assert_eq!(a, b);
assert!(a.as_str() <= b);
let mut stamps = [
"2026-01-01T00:00:01.000000Z",
"2026-01-01T00:00:00.000001Z",
"2026-01-01T00:00:00.000000Z",
"2025-12-31T23:59:59.999999Z",
];
stamps.sort_unstable();
assert_eq!(stamps[0], "2025-12-31T23:59:59.999999Z");
assert_eq!(stamps[3], "2026-01-01T00:00:01.000000Z");
}
#[test]
fn normalize_widens_seconds_and_rejects_everything_else() {
assert_eq!(
normalize("2026-01-01T00:00:00Z").unwrap(),
"2026-01-01T00:00:00.000000Z"
);
assert_eq!(normalize(OPEN_SENTINEL).unwrap(), OPEN_SENTINEL);
for bad in [
"2026-01-01T00:00:00", "2026-01-01T00:00:00+01:00", "2026-01-01T00:00:00.000Z", "2026-01-01T00:00:00.000000000Z", "2026-01-01 00:00:00.000000Z", "",
] {
assert!(normalize(bad).is_err(), "should reject {bad:?}");
}
}
#[test]
fn format_reports_the_real_date() {
assert_eq!(
format(SystemTime::UNIX_EPOCH),
"1970-01-01T00:00:00.000000Z"
);
assert_eq!(
format(SystemTime::UNIX_EPOCH + Duration::from_secs(86_400)),
"1970-01-02T00:00:00.000000Z"
);
assert_eq!(
format(SystemTime::UNIX_EPOCH + Duration::from_secs(951_868_800)),
"2000-03-01T00:00:00.000000Z"
);
}
#[test]
fn parse_format_roundtrip() {
for s in [
"1970-01-01T00:00:00.000000Z",
"2000-02-29T12:34:56.654321Z",
"2026-07-28T09:15:00.000001Z",
OPEN_SENTINEL,
] {
assert_eq!(format(parse(s).unwrap()), s, "roundtrip failed for {s}");
}
}
#[test]
fn parse_validates_the_calendar_not_just_the_shape() {
for bad in [
"2026-02-30T00:00:00.000000Z", "2026-13-01T00:00:00.000000Z", "2026-00-01T00:00:00.000000Z",
"2025-02-29T00:00:00.000000Z", "2026-01-01T24:00:00.000000Z",
"2026-01-01T00:60:00.000000Z",
"2026-01-01T00:00:60.000000Z", ] {
assert!(parse(bad).is_err(), "should reject {bad:?}");
}
assert!(parse("2024-02-29T00:00:00.000000Z").is_ok());
}
}