Skip to main content

ironwork_rt/
calendar.rs

1//! The proleptic Gregorian calendar: day counts, civil fields, Lilian days and the epoch offsets.
2
3pub const SECONDS_PER_DAY: i64 = 86_400;
4pub const MILLIS_PER_DAY: i64 = 86_400_000;
5
6/// Seconds from 1900-01-01T00:00:00 to the Unix epoch, the origin of CICS ABSTIME.
7pub 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
10/// Day 0 of the Lilian calendar, 14 October 1582, in days since 1970-01-01.
11pub const LILIAN_ZERO: i64 = days_from_civil(1582, 10, 14);
12/// 31 December 9999.
13pub const LAST_LILIAN: i64 = days_from_civil(9999, 12, 31) - LILIAN_ZERO;
14
15/// Days since 1970-01-01 of a proleptic Gregorian date.
16pub 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/// The calendar fields of a UTC time. `weekday` counts 1 Monday to 7 Sunday.
38#[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    /// 1-based.
47    pub day_of_year: u32,
48    pub weekday: u32,
49}
50
51impl Civil {
52    /// CICS FORMATTIME's DAYOFWEEK: 0 Sunday to 6 Saturday.
53    pub fn cics_weekday(&self) -> i64 {
54        i64::from(self.weekday % 7)
55    }
56}
57
58/// The calendar fields of `seconds` since 1970-01-01T00:00:00 UTC.
59pub 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
88/// LE's day of week: 1 Sunday to 7 Saturday; Lilian day 1 was a Friday.
89pub 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}