1pub const FILETIME_UNIX_EPOCH: i64 = 116_444_736_000_000_000;
9const TICKS_PER_SECOND: i64 = 10_000_000;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct Civil {
14 pub y: i32,
16 pub m: u32,
18 pub d: u32,
20}
21
22impl Civil {
23 pub const fn next_day(self) -> Self {
25 civil_from_days(days_from_civil(self) + 1)
26 }
27
28 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 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
70pub 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
81pub 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
100pub trait DateResolver {
102 fn filetime_at_midnight(&self, c: Civil) -> i64;
105}
106
107pub 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#[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 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}