1use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
10pub enum DateSystem {
11 #[default]
12 D1900,
13 D1904,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
18pub struct DateTime {
19 pub year: i32,
20 pub month: u32,
21 pub day: u32,
22 pub hour: u32,
23 pub minute: u32,
24 pub second: u32,
25 pub milli: u32,
27 pub weekday: u32,
29}
30
31pub fn days_from_civil(y: i32, m: u32, d: u32) -> i64 {
38 let (cycles, year) = (i128::from(i64::from(y).div_euclid(400)), i64::from(y).rem_euclid(400));
39 i32::try_from(year)
40 .ok()
41 .and_then(|year| chrono::NaiveDate::from_ymd_opt(year, m, d))
42 .and_then(|date| {
43 let days = i128::from(chrono::Datelike::num_days_from_ce(&date)) - 719_163 + cycles * 146_097;
44 i64::try_from(days).ok()
45 })
46 .unwrap_or(0)
47}
48
49pub fn unix_now_ms() -> Option<f64> {
52 #[cfg(not(target_arch = "wasm32"))]
53 {
54 Some(chrono::Utc::now().timestamp_millis() as f64)
55 }
56 #[cfg(target_arch = "wasm32")]
57 {
58 Some(js_sys::Date::now())
59 }
60}
61
62pub fn serial_from_unix_ms(ms: f64) -> f64 {
64 25569.0 + ms / 86_400_000.0
65}
66
67pub fn serial_in_system(sys: DateSystem, serial_1900: f64) -> f64 {
69 match sys {
70 DateSystem::D1900 => serial_1900,
71 DateSystem::D1904 => serial_1900 - 1462.0,
72 }
73}
74
75pub fn civil_from_days(z: i64) -> (i32, u32, u32) {
83 use chrono::Datelike;
84 let (cycles, rest) = (z.div_euclid(146_097), z.rem_euclid(146_097));
85 match i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt) {
86 Some(d) => {
87 let y = i128::from(d.year()) + i128::from(cycles) * 400;
88 (i32::try_from(y).unwrap_or(if y < 0 { i32::MIN } else { i32::MAX }), d.month(), d.day())
89 }
90 None => (1970, 1, 1),
92 }
93}
94
95const UNIX_1899_12_30: i64 = -25569; const UNIX_1904_01_01: i64 = -24107;
97
98pub fn is_leap(y: i32) -> bool {
100 let y = i64::from(y).rem_euclid(400) as i32;
103 chrono::NaiveDate::from_ymd_opt(y, 2, 29).is_some()
104}
105
106pub fn days_in_month(y: i32, m: u32) -> u32 {
109 if !(1..=12).contains(&m) {
113 return 0;
114 }
115 let y = i64::from(y).rem_euclid(400) as i32;
116 let first = |y: i32, m: u32| chrono::NaiveDate::from_ymd_opt(y, m, 1);
117 match (first(y, m), if m == 12 { first(y + 1, 1) } else { first(y, m + 1) }) {
118 (Some(a), Some(b)) => u32::try_from((b - a).num_days()).unwrap_or(0),
119 _ => 0,
120 }
121}
122
123pub fn days_in_month_in(sys: DateSystem, y: i32, m: u32) -> u32 {
126 if sys == DateSystem::D1900 && y == 1900 && m == 2 { 29 } else { days_in_month(y, m) }
127}
128
129pub fn max_serial(sys: DateSystem) -> i64 {
131 match sys {
132 DateSystem::D1900 => 2_958_465,
133 DateSystem::D1904 => 2_958_465 - 1462,
134 }
135}
136
137pub fn serial_from_ymd(sys: DateSystem, year: i64, month: i64, day: i64) -> Option<f64> {
142 let y = if (0..1900).contains(&year) { year + 1900 } else { year };
144 let m0 = month.checked_sub(1)?;
145 let y = y.checked_add(m0.div_euclid(12))?;
146 let m = m0.rem_euclid(12) + 1;
147 if !(1900..=9999).contains(&y) && sys == DateSystem::D1900 || !(1904..=9999).contains(&y) && sys == DateSystem::D1904 {
148 return None;
149 }
150 let first = days_from_civil(y as i32, m as u32, 1);
151 let first = match sys {
153 DateSystem::D1900 => first - UNIX_1899_12_30 - i64::from(y == 1900 && m <= 2),
156 DateSystem::D1904 => first - UNIX_1904_01_01,
157 };
158 let serial = first.checked_add(day)?.checked_sub(1)?;
159 if !(0..=max_serial(sys)).contains(&serial) {
160 return None;
161 }
162 Some(serial as f64)
163}
164
165pub fn datetime_from_serial(sys: DateSystem, serial: f64) -> Option<DateTime> {
167 if !serial.is_finite() || !(0.0..max_serial(sys) as f64 + 1.0).contains(&serial) {
168 return None;
169 }
170 let mut day = serial.floor() as i64;
171 let mut frac_ms = ((serial - serial.floor()) * 86_400_000.0).round() as i64;
172 if frac_ms >= 86_400_000 {
173 frac_ms -= 86_400_000;
174 day += 1;
175 if day > max_serial(sys) {
176 return None;
177 }
178 }
179 let (y, m, d, weekday) = match sys {
180 DateSystem::D1900 => {
181 let wd = ((day + 6) % 7) as u32;
183 if day == 60 {
184 (1900, 2, 29, wd)
185 } else if day == 0 {
186 (1900, 1, 0, wd)
187 } else {
188 let real = if day < 60 { day + 1 } else { day };
189 let (y, m, d) = civil_from_days(real + UNIX_1899_12_30);
190 (y, m, d, wd)
191 }
192 }
193 DateSystem::D1904 => {
194 let unix = day + UNIX_1904_01_01;
195 let (y, m, d) = civil_from_days(unix);
196 (y, m, d, ((unix + 4).rem_euclid(7)) as u32)
197 }
198 };
199 let secs = frac_ms / 1000;
200 Some(DateTime {
201 year: y,
202 month: m,
203 day: d,
204 hour: (secs / 3600) as u32,
205 minute: ((secs / 60) % 60) as u32,
206 second: (secs % 60) as u32,
207 milli: (frac_ms % 1000) as u32,
208 weekday,
209 })
210}
211
212pub fn time_fraction(h: f64, m: f64, s: f64) -> f64 {
214 (h * 3600.0 + m * 60.0 + s) / 86400.0
215}
216
217pub const MONTHS: [&str; 12] =
218 ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
219pub const WEEKDAYS: [&str; 7] = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn serial_rounding_into_next_day_past_max_is_none() {
227 for sys in [DateSystem::D1900, DateSystem::D1904] {
228 let serial = max_serial(sys) as f64 + 0.9999999995;
229 assert_eq!(datetime_from_serial(sys, serial), None);
230 }
231 }
232
233 #[test]
234 fn leap_and_month_length_outside_chrono_range() {
235 assert!(is_leap(2000));
237 assert!(!is_leap(1900));
238 assert!(is_leap(2024));
239 assert!(!is_leap(2023));
240 assert!(is_leap(400_000)); assert!(!is_leap(300_100)); assert!(is_leap(i32::MAX - 3)); assert!(!is_leap(i32::MAX)); assert_eq!(days_in_month(400_000, 2), 29);
247 assert_eq!(days_in_month(300_100, 2), 28);
248 assert_eq!(days_in_month(400_000, 1), 31);
249 assert_eq!(days_in_month(400_000, 4), 30);
250 assert_eq!(days_in_month(2024, 2), 29);
251 assert_eq!(days_in_month(2023, 2), 28);
252 assert_eq!(days_in_month(2023, 0), 0);
253 assert_eq!(days_in_month(2023, 13), 0);
254 assert_eq!(days_in_month(i32::MAX, 12), 31);
256 assert_eq!(days_in_month(i32::MAX, 2), 28); }
258
259 #[test]
260 fn civil_conversions_exact_beyond_chrono_range() {
261 assert_eq!(days_from_civil(1970, 1, 1), 0);
262 assert_eq!(civil_from_days(0), (1970, 1, 1));
263 for (y, m, d, z) in [
266 (262_144i32, 1u32, 1u32, 95_026_602i64),
267 (300_000, 1, 1, 108_853_222),
268 (400_000, 2, 29, 145_377_531),
269 (-262_145, 3, 1, -96_465_964),
270 (-300_000, 6, 15, -110_292_112),
271 ] {
272 assert_eq!(days_from_civil(y, m, d), z, "{y}-{m}-{d}");
273 assert_eq!(civil_from_days(z), (y, m, d), "{z}");
274 }
275 let _ = (days_from_civil(i32::MAX, 12, 31), days_from_civil(i32::MIN, 1, 1), civil_from_days(i64::MAX), civil_from_days(i64::MIN));
277 }
278
279 #[test]
280 fn serials_unchanged_through_cycle_conversion() {
281 let s = DateSystem::D1900;
284 assert_eq!(serial_from_ymd(s, 1900, 2, 29), Some(60.0)); assert_eq!(serial_from_ymd(s, 1900, 3, 1), Some(61.0));
286 assert_eq!(serial_from_ymd(s, 9999, 12, 31), Some(2958465.0));
287 assert_eq!(days_in_month_in(s, 1900, 2), 29);
288 let s4 = DateSystem::D1904;
289 assert_eq!(serial_from_ymd(s4, 1904, 1, 1), Some(0.0)); assert_eq!(serial_from_ymd(s4, 1904, 2, 29), Some(59.0)); assert_eq!(serial_from_ymd(s4, 1900, 2, 29), None); assert_eq!(days_in_month_in(s4, 1900, 2), 28);
293 }
294
295 #[test]
296 fn serial_from_unix_time() {
297 assert_eq!(serial_from_unix_ms(0.0), 25569.0);
298 assert_eq!(serial_from_unix_ms(1_791_633_600_000.0), 46305.5);
300 assert!(unix_now_ms().is_some_and(|ms| serial_from_unix_ms(ms) > 46305.0));
301 }
302
303 #[test]
304 fn known_serials() {
305 let s = DateSystem::D1900;
306 assert_eq!(serial_from_ymd(s, 1900, 1, 1), Some(1.0));
307 assert_eq!(serial_from_ymd(s, 1900, 2, 28), Some(59.0));
308 assert_eq!(serial_from_ymd(s, 1900, 3, 1), Some(61.0));
309 assert_eq!(serial_from_ymd(s, 2000, 1, 1), Some(36526.0));
310 assert_eq!(serial_from_ymd(s, 2026, 10, 7), Some(46302.0));
311 assert_eq!(serial_from_ymd(s, 9999, 12, 31), Some(2958465.0));
312 assert_eq!(serial_from_ymd(DateSystem::D1904, 9999, 12, 31), Some(2957003.0));
313 assert_eq!(serial_from_ymd(DateSystem::D1904, 9999, 12, 32), None);
314 assert!(datetime_from_serial(DateSystem::D1904, 2957003.5).is_some());
315 assert!(datetime_from_serial(DateSystem::D1904, 2957004.0).is_none());
316 assert_eq!(serial_from_ymd(s, 2020, 13, 1), serial_from_ymd(s, 2021, 1, 1));
317 assert_eq!(serial_from_ymd(s, 2020, 3, 0), serial_from_ymd(s, 2020, 2, 29));
318 assert_eq!(serial_from_ymd(s, 120, 1, 1), serial_from_ymd(s, 2020, 1, 1));
319 assert_eq!(serial_from_ymd(DateSystem::D1904, 1904, 1, 1), Some(0.0));
320 assert_eq!(serial_from_ymd(DateSystem::D1904, 2000, 1, 1), Some(35064.0));
321 }
322
323 #[test]
324 fn fictitious_1900_02_29() -> Result<(), std::io::Error> {
325 let s = DateSystem::D1900;
328 assert_eq!(serial_from_ymd(s, 1900, 2, 29), Some(60.0));
329 assert_eq!(serial_from_ymd(s, 1900, 3, 0), Some(60.0));
330 assert_eq!(serial_from_ymd(s, 1900, 2, 30), Some(61.0));
331 assert_eq!(serial_from_ymd(s, 1900, 1, 60), Some(60.0));
332 assert_eq!(serial_from_ymd(s, 1900, 1, 61), Some(61.0));
333 assert_eq!(serial_from_ymd(s, 1900, 1, 400), Some(400.0));
334 assert_eq!(serial_from_ymd(s, 1900, 4, -30), Some(61.0));
335 assert_eq!(serial_from_ymd(s, 1900, 4, -31), Some(60.0));
336 assert_eq!(serial_from_ymd(s, 1900, 3, -1), Some(59.0));
337 assert_eq!(serial_from_ymd(s, 1901, -10, 29), Some(60.0));
338 assert_eq!(serial_from_ymd(s, 1900, 1, 0), Some(0.0));
339 assert_eq!(serial_from_ymd(s, 1900, 1, -1), None);
340 assert_eq!(serial_from_ymd(s, 1900, 0, 1), None);
341 assert_eq!(serial_from_ymd(s, 1901, 2, 29), serial_from_ymd(s, 1901, 3, 1));
343 assert_eq!(serial_from_ymd(s, 1904, 2, 29), Some(1521.0));
344 for serial in 0..=366 {
346 let d = datetime_from_serial(s, serial as f64).ok_or_else(|| std::io::Error::other("serial should convert"))?;
347 assert_eq!(serial_from_ymd(s, d.year as i64, d.month as i64, d.day as i64), Some(serial as f64), "{d:?}");
348 }
349 assert_eq!(days_in_month_in(s, 1900, 2), 29);
350 assert_eq!(days_in_month_in(s, 1901, 2), 28);
351 assert_eq!(days_in_month_in(DateSystem::D1904, 1900, 2), 28);
352 let s = DateSystem::D1904;
354 assert_eq!(serial_from_ymd(s, 1904, 2, 29), Some(59.0));
355 assert_eq!(serial_from_ymd(s, 1904, 3, 0), Some(59.0));
356 assert_eq!(serial_from_ymd(s, 1904, 3, 1), Some(60.0));
357 assert_eq!(serial_from_ymd(s, 1904, 1, 0), None);
358 assert_eq!(serial_from_ymd(s, 1900, 2, 29), None);
359 Ok(())
360 }
361
362 #[test]
363 fn hostile_month_and_day() {
364 let s = DateSystem::D1900;
365 assert_eq!(serial_from_ymd(s, 2000, i64::MIN, 1), None);
366 assert_eq!(serial_from_ymd(s, 2000, i64::MAX, 1), None);
367 assert_eq!(serial_from_ymd(s, 2000, 1, i64::MAX), None);
368 assert_eq!(serial_from_ymd(s, 2000, 1, i64::MIN), None);
369 assert_eq!(serial_from_ymd(s, i64::MAX, 1, 1), None);
370 }
371
372 #[test]
373 fn roundtrip() -> Result<(), std::io::Error> {
374 let s = DateSystem::D1900;
375 for serial in [1.0, 59.0, 60.0, 61.0, 36526.0, 46302.0, 2958465.0] {
376 let d = datetime_from_serial(s, serial).ok_or_else(|| std::io::Error::other("serial should convert"))?;
377 assert_eq!(serial_from_ymd(s, d.year as i64, d.month as i64, d.day as i64), Some(serial));
378 }
379 let d = datetime_from_serial(s, 60.0).ok_or_else(|| std::io::Error::other("leap-day serial should convert"))?;
380 assert_eq!((d.year, d.month, d.day), (1900, 2, 29));
381 let d = datetime_from_serial(s, 46302.75).ok_or_else(|| std::io::Error::other("time serial should convert"))?;
382 assert_eq!((d.hour, d.minute, d.weekday), (18, 0, 3)); let d = datetime_from_serial(s, 1.0).ok_or_else(|| std::io::Error::other("first serial should convert"))?;
384 assert_eq!(d.weekday, 0); Ok(())
386 }
387}