use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result};
pub fn utc_now_sequence(count: u64) -> Result<Vec<String>> {
let now = now_secs()?;
Ok((0..count).map(|i| format_utc(now + i)).collect())
}
pub fn utc_now_after(latest: Option<&str>) -> Result<String> {
let now = now_secs()?;
let next = latest
.and_then(parse_utc)
.map_or(now, |latest| now.max(latest + 1));
Ok(format_utc(next))
}
fn now_secs() -> Result<u64> {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("system clock is set before 1970")?
.as_secs())
}
fn parse_utc(text: &str) -> Option<u64> {
if text.len() != 14 || !text.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let field = |range: std::ops::Range<usize>| text[range].parse::<u64>().ok();
let (year, month, day) = (field(0..4)?, field(4..6)?, field(6..8)?);
let (hour, minute, second) = (field(8..10)?, field(10..12)?, field(12..14)?);
if year < 1970 || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let days = days_from_civil(year, month, day);
Some(days * 86_400 + hour * 3600 + minute * 60 + second)
}
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 yoe = year % 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 format_utc(unix_secs: u64) -> String {
let (days, rem) = (unix_secs / 86_400, unix_secs % 86_400);
let (year, month, day) = civil_from_days(days);
format!(
"{year:04}{month:02}{day:02}{:02}{:02}{:02}",
rem / 3600,
rem % 3600 / 60,
rem % 60
)
}
fn civil_from_days(days: u64) -> (u64, u64, u64) {
let z = days + 719_468;
let era = z / 146_097;
let doe = z % 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
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 = yoe + era * 400 + u64::from(month <= 2);
(year, month, day)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats_known_instants() {
assert_eq!(format_utc(0), "19700101000000");
assert_eq!(format_utc(951_782_400), "20000229000000"); assert_eq!(format_utc(1_700_000_000), "20231114221320");
assert_eq!(format_utc(1_791_741_599), "20261011175959");
}
#[test]
fn parses_what_it_formats() {
for secs in [0, 951_782_400, 1_700_000_000, 1_791_741_599] {
assert_eq!(parse_utc(&format_utc(secs)), Some(secs));
}
assert_eq!(parse_utc("2026101616"), None);
assert_eq!(parse_utc("2026x016162020"), None);
}
#[test]
fn stays_after_latest() {
let future = "29991231235959";
assert_eq!(utc_now_after(Some(future)).unwrap(), "30000101000000");
let now = utc_now_after(Some("19700101000000")).unwrap();
assert!(now.as_str() > "2026");
assert!(utc_now_after(None).unwrap().as_str() > "2026");
}
#[test]
fn sequences_are_strictly_increasing() {
let seq = utc_now_sequence(3).unwrap();
assert_eq!(seq.len(), 3);
assert!(seq[0] < seq[1] && seq[1] < seq[2]);
}
#[test]
fn now_has_fourteen_digits() {
let now = utc_now_after(None).unwrap();
assert_eq!(now.len(), 14);
assert!(now.chars().all(|c| c.is_ascii_digit()));
}
}