Skip to main content

allow_core/
date.rs

1use std::fmt;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
5pub struct SimpleDate {
6    pub year: i32,
7    pub month: u32,
8    pub day: u32,
9}
10
11impl SimpleDate {
12    pub fn parse(input: &str) -> Option<Self> {
13        let mut parts = input.trim().split('-');
14        let year = parts.next()?.parse().ok()?;
15        let month = parts.next()?.parse().ok()?;
16        let day = parts.next()?.parse().ok()?;
17        if parts.next().is_some() || !valid_ymd(year, month, day) {
18            return None;
19        }
20        Some(Self { year, month, day })
21    }
22
23    pub fn days_until(self, other: Self) -> i64 {
24        other.days_since_unix_epoch() - self.days_since_unix_epoch()
25    }
26
27    pub fn add_days(self, days: i64) -> Self {
28        Self::from_days_since_unix_epoch(self.days_since_unix_epoch() + days)
29    }
30
31    pub(crate) fn days_since_unix_epoch(self) -> i64 {
32        // Howard Hinnant's civil date algorithm. This keeps lifecycle validation
33        // deterministic without adding a date dependency to the core crate.
34        let mut year = i64::from(self.year);
35        let month = i64::from(self.month);
36        let day = i64::from(self.day);
37        if month <= 2 {
38            year -= 1;
39        }
40        let era = if year >= 0 { year } else { year - 399 } / 400;
41        let year_of_era = year - era * 400;
42        let month_prime = month + if month > 2 { -3 } else { 9 };
43        let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
44        let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
45        era * 146_097 + day_of_era - 719_468
46    }
47
48    pub fn from_days_since_unix_epoch(days: i64) -> Self {
49        // Inverse of the civil date algorithm above.
50        let z = days + 719_468;
51        let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
52        let day_of_era = z - era * 146_097;
53        let year_of_era =
54            (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
55        let mut year = year_of_era + era * 400;
56        let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
57        let month_prime = (5 * day_of_year + 2) / 153;
58        let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
59        let month = month_prime + if month_prime < 10 { 3 } else { -9 };
60        if month <= 2 {
61            year += 1;
62        }
63        Self {
64            year: year as i32,
65            month: month as u32,
66            day: day as u32,
67        }
68    }
69
70    pub fn today_utc_approx() -> Self {
71        let seconds = SystemTime::now()
72            .duration_since(UNIX_EPOCH)
73            .map(|duration| duration.as_secs())
74            .unwrap_or(0);
75        Self::from_days_since_unix_epoch((seconds / 86_400) as i64)
76    }
77
78    /// `true` when `date` has arrived as of `today` — i.e. `date <= today`
79    /// (inclusive of the deadline day itself).
80    ///
81    /// This is the deadline/expiry predicate used for `expires` and drain
82    /// windows. The boundary is inclusive: a deadline dated *today* has passed
83    /// (#2008 — a strict `<` previously enforced expiry one day late, letting
84    /// drain windows and entries live a day past their stated expiry). The
85    /// `is_due_date_str` review predicate uses the same inclusive boundary so
86    /// the two cannot drift.
87    pub fn has_passed_date_str(date: Option<&str>, today: Self) -> bool {
88        date.and_then(Self::parse).is_some_and(|date| date <= today)
89    }
90
91    /// `true` when `date` is due on or before `today` — i.e. `date <= today`
92    /// (inclusive). Used for `review_after`.
93    pub fn is_due_date_str(date: Option<&str>, today: Self) -> bool {
94        date.and_then(Self::parse).is_some_and(|date| date <= today)
95    }
96}
97
98/// Accepted year range for SimpleDate. Dates outside this range are rejected
99/// at parse time so typos like `expires: "99999-01-01"` or `expires: "3026-01-01"`
100/// don't silently make entries immortal (#1827).
101pub const MIN_YEAR: i32 = 1900;
102pub const MAX_YEAR: i32 = 9999;
103
104fn valid_ymd(year: i32, month: u32, day: u32) -> bool {
105    if !(MIN_YEAR..=MAX_YEAR).contains(&year) {
106        return false;
107    }
108    let max_day = match month {
109        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
110        4 | 6 | 9 | 11 => 30,
111        2 if leap_year(year) => 29,
112        2 => 28,
113        _ => return false,
114    };
115    day > 0 && day <= max_day
116}
117
118fn leap_year(year: i32) -> bool {
119    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
120}
121
122impl fmt::Display for SimpleDate {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
125    }
126}