1pub const SECONDS_PER_DAY: i64 = 86_400;
4pub const MILLIS_PER_DAY: i64 = 86_400_000;
5
6pub const EPOCH_1900_TO_1970_SECONDS: i64 = 2_208_988_800;
8pub const EPOCH_1900_TO_1970_MILLIS: i64 = EPOCH_1900_TO_1970_SECONDS * 1000;
9
10pub const LILIAN_ZERO: i64 = days_from_civil(1582, 10, 14);
12pub const LAST_LILIAN: i64 = days_from_civil(9999, 12, 31) - LILIAN_ZERO;
14
15pub const fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
17 let (y, m) = if month <= 2 { (year - 1, month + 9) } else { (year, month - 3) };
18 let era = y.div_euclid(400);
19 let yoe = y.rem_euclid(400);
20 let doe = yoe * 365 + yoe / 4 - yoe / 100 + (153 * m + 2) / 5 + day - 1;
21 era * 146_097 + doe - 719_468
22}
23
24pub fn is_leap(year: i64) -> bool {
25 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
26}
27
28pub fn days_in_month(year: i64, month: u32) -> u32 {
29 match month {
30 2 if is_leap(year) => 29,
31 2 => 28,
32 4 | 6 | 9 | 11 => 30,
33 _ => 31,
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct Civil {
40 pub year: i64,
41 pub month: u32,
42 pub day: u32,
43 pub hour: u32,
44 pub minute: u32,
45 pub second: u32,
46 pub day_of_year: u32,
48 pub weekday: u32,
49}
50
51impl Civil {
52 pub fn cics_weekday(&self) -> i64 {
54 i64::from(self.weekday % 7)
55 }
56}
57
58pub fn civil(seconds: i64) -> Civil {
60 let days = seconds.div_euclid(SECONDS_PER_DAY);
61 let secs = seconds.rem_euclid(SECONDS_PER_DAY) as u32;
62 let z = days + 719_468;
63 let era = z.div_euclid(146_097);
64 let doe = z.rem_euclid(146_097);
65 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
66 let doy_march = doe - (365 * yoe + yoe / 4 - yoe / 100);
67 let mp = (5 * doy_march + 2) / 153;
68 let day = (doy_march - (153 * mp + 2) / 5 + 1) as u32;
69 let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
70 let year = yoe + era * 400 + i64::from(month <= 2);
71 let before = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334][month as usize - 1] + u32::from(is_leap(year) && month > 2);
72 Civil {
73 year,
74 month,
75 day,
76 hour: secs / 3600,
77 minute: secs / 60 % 60,
78 second: secs % 60,
79 day_of_year: before + day,
80 weekday: ((days + 3).rem_euclid(7) + 1) as u32,
81 }
82}
83
84pub fn lilian(year: i64, month: u32, day: u32) -> i64 {
85 days_from_civil(year, i64::from(month), i64::from(day)) - LILIAN_ZERO
86}
87
88pub fn weekday(lilian: i64) -> u32 {
90 ((lilian + 4).rem_euclid(7) + 1) as u32
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn civil_dates() {
99 let epoch = civil(0);
100 assert_eq!((epoch.year, epoch.month, epoch.day, epoch.day_of_year, epoch.weekday), (1970, 1, 1, 1, 4));
101 let c = civil(1_790_510_400);
102 assert_eq!((c.year, c.month, c.day, c.day_of_year, c.weekday), (2026, 9, 27, 270, 7));
103 assert_eq!((c.hour, c.minute, c.second), (12, 0, 0));
104 assert_eq!(civil(951_782_400).day_of_year, 60);
105 }
106
107 #[test]
108 fn days_and_civil_round_trip() {
109 for (y, m, d) in [(1582, 10, 15), (1900, 3, 1), (1970, 1, 1), (2000, 2, 29), (9999, 12, 31)] {
110 let days = days_from_civil(y, i64::from(m), i64::from(d));
111 let c = civil(days * SECONDS_PER_DAY);
112 assert_eq!((c.year, c.month, c.day), (y, m, d));
113 }
114 assert_eq!(days_from_civil(1970, 1, 1), 0);
115 }
116
117 #[test]
118 fn leap_years() {
119 assert!(!is_leap(1900));
120 assert!(is_leap(2000));
121 assert!(is_leap(2024));
122 assert_eq!((days_in_month(1900, 2), days_in_month(2024, 2), days_in_month(2023, 4)), (28, 29, 30));
123 }
124
125 #[test]
126 fn lilian_day_one_is_15_october_1582() {
127 assert_eq!(lilian(1582, 10, 15), 1);
128 assert_eq!(LAST_LILIAN, lilian(9999, 12, 31));
129 }
130
131 #[test]
132 fn weekday_conventions() {
133 assert_eq!(civil(1_790_510_400).cics_weekday(), 0);
134 assert_eq!(civil(0).cics_weekday(), 4);
135 assert_eq!(weekday(1), 6);
136 }
137
138 #[test]
139 fn epoch_offset_is_seventy_years() {
140 assert_eq!(EPOCH_1900_TO_1970_SECONDS, -days_from_civil(1900, 1, 1) * SECONDS_PER_DAY);
141 }
142}