1pub const DAY_MS: u64 = 86_400_000;
8
9pub fn civil_from_unix_ms(unix_ms: u64) -> (i64, u32, u32) {
11 let z = (unix_ms / DAY_MS) as i64 + 719_468;
12 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
13 let doe = z - era * 146_097;
14 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
15 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
16 let mp = (5 * doy + 2) / 153;
17 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
18 let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
19 let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };
20 (y, m, d)
21}
22
23pub fn unix_ms_from_civil(y: i64, m: u32, d: u32) -> u64 {
25 let y = if m <= 2 { y - 1 } else { y };
26 let era = if y >= 0 { y } else { y - 399 } / 400;
27 let yoe = y - era * 400;
28 let mp = if m > 2 { m - 3 } else { m + 9 } as i64;
29 let doy = (153 * mp + 2) / 5 + d as i64 - 1;
30 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
31 let days = era * 146_097 + doe - 719_468;
32 if days < 0 { 0 } else { days as u64 * DAY_MS }
33}
34
35pub fn days_in_month(y: i64, m: u32) -> u32 {
36 match m {
37 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
38 4 | 6 | 9 | 11 => 30,
39 _ => {
40 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 { 29 } else { 28 }
41 }
42 }
43}
44
45pub fn iso_date(unix_ms: u64) -> String {
47 let (y, m, d) = civil_from_unix_ms(unix_ms);
48 format!("{y:04}-{m:02}-{d:02}")
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn round_trips_known_dates() {
57 for (y, m, d) in [(1970, 1, 1), (2000, 2, 29), (2026, 9, 3), (2100, 12, 31)] {
58 let ms = unix_ms_from_civil(y, m, d);
59 assert_eq!(civil_from_unix_ms(ms), (y, m, d));
60 assert_eq!(civil_from_unix_ms(ms + DAY_MS - 1), (y, m, d), "end of day stays");
61 }
62 assert_eq!(iso_date(unix_ms_from_civil(2026, 9, 3)), "2026-09-03");
63 }
64
65 #[test]
66 fn refuse_twin_the_day_after_is_not_the_same_day() {
67 let ms = unix_ms_from_civil(2026, 9, 3);
68 assert_ne!(civil_from_unix_ms(ms + DAY_MS), (2026, 9, 3));
69 assert_eq!(iso_date(ms + DAY_MS), "2026-09-04");
70 assert_eq!(days_in_month(2024, 2), 29);
71 assert_eq!(days_in_month(1900, 2), 28);
72 }
73}