Skip to main content

monetize_embed/
civil.rs

1//! Proleptic Gregorian civil dates from unix milliseconds, pure integer arithmetic
2//! (Howard Hinnant's `days_from_civil` / `civil_from_days`). Here rather than in core
3//! because both sides need it: core to compute a period's end and a reference's date
4//! label, embed to print `plan expired 2026-10-01` in a refusal line. No chrono: the
5//! product should not inherit a date library for one string.
6
7pub const DAY_MS: u64 = 86_400_000;
8
9/// `(year, month, day)` of the UTC day containing `unix_ms`.
10pub 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
23/// Midnight UTC of `y-m-d` in unix milliseconds. Dates before 1970 saturate to 0.
24pub 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
45/// `YYYY-MM-DD` of the UTC day containing `unix_ms`.
46pub 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}