Skip to main content

fmf_core\query/
dates.rs

1//! Civil-date ↔ FILETIME conversion for `dm:` filters.
2//!
3//! `dm:` bounds are interpreted in the *local* time zone
4//! (docs/ARCHITECTURE.md C-4). The conversion is injected via
5//! [`DateResolver`] so the parser/compiler stay pure and tests can use UTC.
6
7/// FILETIME ticks (100 ns since 1601-01-01) at the Unix epoch (1970-01-01).
8pub const FILETIME_UNIX_EPOCH: i64 = 116_444_736_000_000_000;
9const TICKS_PER_SECOND: i64 = 10_000_000;
10
11/// A proleptic Gregorian calendar date with no time-of-day or zone.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct Civil {
14    /// Year (full, e.g. 2026).
15    pub y: i32,
16    /// Month, 1..=12.
17    pub m: u32,
18    /// Day of month, 1..=31.
19    pub d: u32,
20}
21
22impl Civil {
23    /// The calendar date one day after this one (handles month/leap rollover).
24    pub const fn next_day(self) -> Self {
25        civil_from_days(days_from_civil(self) + 1)
26    }
27
28    /// The first day of the month following this date's month.
29    pub const fn first_of_next_month(self) -> Self {
30        if self.m == 12 {
31            Self {
32                y: self.y + 1,
33                m: 1,
34                d: 1,
35            }
36        } else {
37            Self {
38                y: self.y,
39                m: self.m + 1,
40                d: 1,
41            }
42        }
43    }
44
45    /// True if the date is a real calendar date within FILETIME's range
46    /// (year 1601..=9999, valid month, day within the month's length).
47    pub fn is_valid(self) -> bool {
48        if !(1601..=9999).contains(&self.y) || !(1..=12).contains(&self.m) {
49            return false;
50        }
51        self.d >= 1 && self.d <= days_in_month(self.y, self.m)
52    }
53}
54
55const fn days_in_month(y: i32, m: u32) -> u32 {
56    match m {
57        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
58        4 | 6 | 9 | 11 => 30,
59        2 => {
60            if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 {
61                29
62            } else {
63                28
64            }
65        }
66        _ => 0,
67    }
68}
69
70/// Days since 1970-01-01 (Howard Hinnant's `days_from_civil`).
71pub const fn days_from_civil(c: Civil) -> i64 {
72    let y = if c.m <= 2 { c.y - 1 } else { c.y } as i64;
73    let era = if y >= 0 { y } else { y - 399 } / 400;
74    let yoe = y - era * 400;
75    let mp = (c.m as i64 + 9) % 12;
76    let doy = (153 * mp + 2) / 5 + c.d as i64 - 1;
77    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
78    era * 146_097 + doe - 719_468
79}
80
81/// Inverse of [`days_from_civil`]: the civil date for a day count since
82/// 1970-01-01 (Howard Hinnant's `civil_from_days`).
83pub const fn civil_from_days(days: i64) -> Civil {
84    let z = days + 719_468;
85    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
86    let doe = z - era * 146_097;
87    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
88    let y = yoe + era * 400;
89    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
90    let mp = (5 * doy + 2) / 153;
91    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
92    let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
93    Civil {
94        y: (if m <= 2 { y + 1 } else { y }) as i32,
95        m,
96        d,
97    }
98}
99
100/// Converts a civil date (midnight) to FILETIME ticks.
101pub trait DateResolver {
102    /// FILETIME ticks (100 ns since 1601-01-01) for midnight at the start of
103    /// the given civil date, in this resolver's time zone.
104    fn filetime_at_midnight(&self, c: Civil) -> i64;
105}
106
107/// Pure UTC resolver — deterministic, used by unit tests.
108pub struct UtcResolver;
109
110impl DateResolver for UtcResolver {
111    fn filetime_at_midnight(&self, c: Civil) -> i64 {
112        FILETIME_UNIX_EPOCH + days_from_civil(c) * 86_400 * TICKS_PER_SECOND
113    }
114}
115
116/// Local-time-zone resolver backed by the Windows time-zone/DST rules.
117#[cfg(windows)]
118pub struct WindowsLocalResolver;
119
120#[cfg(windows)]
121impl DateResolver for WindowsLocalResolver {
122    fn filetime_at_midnight(&self, c: Civil) -> i64 {
123        use windows_sys::Win32::Foundation::{FILETIME, SYSTEMTIME};
124        use windows_sys::Win32::System::Time::{
125            SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime,
126        };
127
128        unsafe {
129            let local = SYSTEMTIME {
130                wYear: c.y as u16,
131                wMonth: c.m as u16,
132                wDayOfWeek: 0,
133                wDay: c.d as u16,
134                wHour: 0,
135                wMinute: 0,
136                wSecond: 0,
137                wMilliseconds: 0,
138            };
139            let mut utc: SYSTEMTIME = std::mem::zeroed();
140            let mut ft: FILETIME = std::mem::zeroed();
141            if TzSpecificLocalTimeToSystemTime(std::ptr::null(), &raw const local, &raw mut utc)
142                != 0
143                && SystemTimeToFileTime(&raw const utc, &raw mut ft) != 0
144            {
145                ((ft.dwHighDateTime as i64) << 32) | ft.dwLowDateTime as i64
146            } else {
147                // Out-of-range dates: fall back to UTC math rather than failing
148                // the whole query.
149                UtcResolver.filetime_at_midnight(c)
150            }
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn civil_days_roundtrip() {
161        for (y, m, d) in [(1970, 1, 1), (2000, 2, 29), (2026, 6, 10), (1999, 12, 31)] {
162            let c = Civil { y, m, d };
163            assert_eq!(civil_from_days(days_from_civil(c)), c);
164        }
165        assert_eq!(
166            days_from_civil(Civil {
167                y: 1970,
168                m: 1,
169                d: 1
170            }),
171            0
172        );
173    }
174
175    #[test]
176    fn next_day_handles_month_and_leap() {
177        assert_eq!(
178            Civil {
179                y: 2024,
180                m: 2,
181                d: 28
182            }
183            .next_day(),
184            Civil {
185                y: 2024,
186                m: 2,
187                d: 29
188            }
189        );
190        assert_eq!(
191            Civil {
192                y: 2023,
193                m: 12,
194                d: 31
195            }
196            .next_day(),
197            Civil {
198                y: 2024,
199                m: 1,
200                d: 1
201            }
202        );
203    }
204
205    #[test]
206    fn utc_resolver_epoch() {
207        assert_eq!(
208            UtcResolver.filetime_at_midnight(Civil {
209                y: 1970,
210                m: 1,
211                d: 1
212            }),
213            FILETIME_UNIX_EPOCH
214        );
215    }
216
217    #[test]
218    fn validity() {
219        assert!(
220            Civil {
221                y: 2024,
222                m: 2,
223                d: 29
224            }
225            .is_valid()
226        );
227        assert!(
228            !Civil {
229                y: 2023,
230                m: 2,
231                d: 29
232            }
233            .is_valid()
234        );
235        assert!(
236            !Civil {
237                y: 2023,
238                m: 13,
239                d: 1
240            }
241            .is_valid()
242        );
243    }
244}