Skip to main content

pma_core/
dates.rs

1//! Calendar days as integers: days since 1970-01-01, in UTC.
2//!
3//! UTC rather than local time keeps the standard library sufficient. A due
4//! date therefore turns at UTC midnight, a few hours off local midnight.
5
6use std::time::{SystemTime, UNIX_EPOCH};
7
8/// Seconds since the Unix epoch.
9pub fn now() -> i64 {
10    SystemTime::now()
11        .duration_since(UNIX_EPOCH)
12        .map_or(0, |d| d.as_secs() as i64)
13}
14
15/// The day containing a Unix timestamp.
16pub fn day(unix: i64) -> i64 {
17    unix.div_euclid(86_400)
18}
19
20/// Parses `YYYY-MM-DD`, rejecting dates that do not exist.
21pub fn parse(s: &str) -> Option<i64> {
22    let b = s.as_bytes();
23    if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
24        return None;
25    }
26    let num = |r: std::ops::Range<usize>| -> Option<i64> {
27        let t = &s[r];
28        t.bytes()
29            .all(|c| c.is_ascii_digit())
30            .then(|| t.parse().ok())?
31    };
32    let (y, m, d) = (num(0..4)?, num(5..7)?, num(8..10)?);
33    let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
34    let days = match m {
35        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
36        4 | 6 | 9 | 11 => 30,
37        2 if leap => 29,
38        2 => 28,
39        _ => return None,
40    };
41    (1..=days).contains(&d).then(|| from_civil(y, m, d))
42}
43
44/// `YYYY-MM-DD` for a day number.
45/// Algorithm from https://howardhinnant.github.io/date_algorithms.html#civil_from_days
46pub fn format(day: i64) -> String {
47    let z = day + 719_468;
48    let era = z.div_euclid(146_097);
49    let doe = z - era * 146_097;
50    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
51    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
52    let mp = (5 * doy + 2) / 153;
53    let d = doy - (153 * mp + 2) / 5 + 1;
54    let m = if mp < 10 { mp + 3 } else { mp - 9 };
55    let y = yoe + era * 400 + i64::from(m <= 2);
56    format!("{y:04}-{m:02}-{d:02}")
57}
58
59/// Days from 1970-01-01 to a proleptic Gregorian date.
60/// Algorithm from https://howardhinnant.github.io/date_algorithms.html#days_from_civil
61fn from_civil(y: i64, m: i64, d: i64) -> i64 {
62    let y = if m <= 2 { y - 1 } else { y };
63    let era = y.div_euclid(400);
64    let yoe = y - era * 400;
65    let doy = (153 * ((m + 9) % 12) + 2) / 5 + d - 1;
66    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
67    era * 146_097 + doe - 719_468
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn known_days() {
76        for (s, want) in [
77            ("1970-01-01", 0),
78            ("1969-12-31", -1),
79            ("2000-03-01", 11017),
80            ("2026-09-14", 20710),
81            ("2028-02-29", 21243),
82            ("2100-03-01", 47541),
83        ] {
84            assert_eq!(parse(s), Some(want), "{s}");
85            assert_eq!(format(want), s);
86        }
87    }
88
89    #[test]
90    fn rejects_impossible_and_malformed_dates() {
91        for s in [
92            "2026-02-29",
93            "2100-02-29",
94            "2026-13-01",
95            "2026-00-10",
96            "2026-04-31",
97            "2026-1-01",
98            "2026-+1-01",
99            "20260101",
100            "2026/01/01",
101            "",
102        ] {
103            assert_eq!(parse(s), None, "{s}");
104        }
105    }
106
107    #[test]
108    fn day_of_timestamp() {
109        assert_eq!(day(0), 0);
110        assert_eq!(day(86_399), 0);
111        assert_eq!(day(-1), -1);
112        assert_eq!(day(20710 * 86_400 + 5), 20710);
113    }
114}