use std::time::{SystemTime, UNIX_EPOCH};
pub fn now_rfc3339() -> String {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format_unix_secs_rfc3339(secs)
}
pub fn format_unix_secs_rfc3339(secs: u64) -> String {
let (year, month, day, hour, minute, second) = unix_to_ymdhms(secs);
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
}
fn unix_to_ymdhms(secs: u64) -> (i32, u32, u32, u32, u32, u32) {
let days = (secs / 86_400) as i64;
let secs_of_day = (secs % 86_400) as u32;
let hour = secs_of_day / 3600;
let minute = (secs_of_day % 3600) / 60;
let second = secs_of_day % 60;
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 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) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
let year = (y + if m <= 2 { 1 } else { 0 }) as i32;
(year, m, d, hour, minute, second)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn epoch_renders_as_1970_01_01() {
assert_eq!(format_unix_secs_rfc3339(0), "1970-01-01T00:00:00Z");
}
#[test]
fn known_timestamp_2024_01_01() {
assert_eq!(
format_unix_secs_rfc3339(1_704_067_200),
"2024-01-01T00:00:00Z"
);
}
#[test]
fn known_timestamp_with_time_of_day() {
assert_eq!(
format_unix_secs_rfc3339(1_716_552_896),
"2024-05-24T12:14:56Z"
);
}
#[test]
fn leap_year_feb_29() {
assert_eq!(
format_unix_secs_rfc3339(1_709_164_800),
"2024-02-29T00:00:00Z"
);
}
#[test]
fn now_has_z_suffix_and_t_separator() {
let s = now_rfc3339();
assert_eq!(s.len(), 20);
assert_eq!(&s[4..5], "-");
assert_eq!(&s[7..8], "-");
assert_eq!(&s[10..11], "T");
assert_eq!(&s[13..14], ":");
assert_eq!(&s[16..17], ":");
assert!(s.ends_with('Z'));
}
#[test]
fn non_leap_year_feb_to_march_boundary() {
assert_eq!(
format_unix_secs_rfc3339(1_677_628_799),
"2023-02-28T23:59:59Z"
);
assert_eq!(
format_unix_secs_rfc3339(1_677_628_800),
"2023-03-01T00:00:00Z"
);
}
#[test]
fn december_to_january_year_boundary() {
assert_eq!(
format_unix_secs_rfc3339(1_704_067_199),
"2023-12-31T23:59:59Z"
);
assert_eq!(
format_unix_secs_rfc3339(1_704_067_200),
"2024-01-01T00:00:00Z"
);
}
#[test]
fn century_year_2100_is_not_a_leap_year() {
assert_eq!(
format_unix_secs_rfc3339(4_107_542_400),
"2100-03-01T00:00:00Z"
);
}
#[test]
fn four_century_year_2000_is_a_leap_year() {
assert_eq!(
format_unix_secs_rfc3339(951_782_400),
"2000-02-29T00:00:00Z"
);
}
#[test]
fn january_31_to_february_1() {
assert_eq!(
format_unix_secs_rfc3339(1_675_209_599),
"2023-01-31T23:59:59Z"
);
assert_eq!(
format_unix_secs_rfc3339(1_675_209_600),
"2023-02-01T00:00:00Z"
);
}
#[test]
fn march_31_to_april_1() {
assert_eq!(
format_unix_secs_rfc3339(1_680_307_199),
"2023-03-31T23:59:59Z"
);
assert_eq!(
format_unix_secs_rfc3339(1_680_307_200),
"2023-04-01T00:00:00Z"
);
}
#[test]
fn april_30_to_may_1() {
assert_eq!(
format_unix_secs_rfc3339(1_682_899_199),
"2023-04-30T23:59:59Z"
);
assert_eq!(
format_unix_secs_rfc3339(1_682_899_200),
"2023-05-01T00:00:00Z"
);
}
#[test]
fn september_30_to_october_1() {
assert_eq!(
format_unix_secs_rfc3339(1_696_118_399),
"2023-09-30T23:59:59Z"
);
assert_eq!(
format_unix_secs_rfc3339(1_696_118_400),
"2023-10-01T00:00:00Z"
);
}
#[test]
fn far_future_century_years_are_not_leap() {
assert_eq!(
format_unix_secs_rfc3339(7_263_215_999),
"2200-02-28T23:59:59Z"
);
assert_eq!(
format_unix_secs_rfc3339(7_263_216_000),
"2200-03-01T00:00:00Z"
);
assert_eq!(
format_unix_secs_rfc3339(10_418_889_599),
"2300-02-28T23:59:59Z"
);
assert_eq!(
format_unix_secs_rfc3339(10_418_889_600),
"2300-03-01T00:00:00Z"
);
}
#[test]
fn year_2400_is_a_leap_year() {
assert_eq!(
format_unix_secs_rfc3339(13_574_606_400),
"2400-02-29T12:00:00Z"
);
}
#[test]
fn far_future_leap_day_with_time_of_day() {
assert_eq!(
format_unix_secs_rfc3339(8_777_917_815),
"2248-02-29T06:30:15Z"
);
}
#[test]
fn time_of_day_rollovers() {
let cases: &[(u64, &str)] = &[
(1_704_067_259, "2024-01-01T00:00:59Z"), (1_704_067_260, "2024-01-01T00:01:00Z"), (1_704_070_799, "2024-01-01T00:59:59Z"), (1_704_070_800, "2024-01-01T01:00:00Z"), (1_704_110_400, "2024-01-01T12:00:00Z"), ];
for &(secs, expected) in cases {
assert_eq!(format_unix_secs_rfc3339(secs), expected, "secs={secs}");
}
}
#[test]
fn max_u64_input_does_not_panic() {
let result = std::panic::catch_unwind(|| format_unix_secs_rfc3339(u64::MAX));
assert!(result.is_ok(), "u64::MAX must not panic");
let s = result.unwrap();
assert!(s.ends_with('Z'), "output must still end with Z");
}
#[test]
fn now_output_parses_into_plausible_fields() {
let s = now_rfc3339();
let year: u32 = s[0..4].parse().unwrap();
let month: u32 = s[5..7].parse().unwrap();
let day: u32 = s[8..10].parse().unwrap();
let hour: u32 = s[11..13].parse().unwrap();
let minute: u32 = s[14..16].parse().unwrap();
let second: u32 = s[17..19].parse().unwrap();
assert!((1970..3000).contains(&year), "year out of range: {year}");
assert!((1..=12).contains(&month), "month out of range: {month}");
assert!((1..=31).contains(&day), "day out of range: {day}");
assert!(hour < 24);
assert!(minute < 60);
assert!(second < 60);
}
}