Coverage Report

Created: 2025-08-13 21:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/a220/proj/radnelac/src/calendar/gregorian.rs
Line
Count
Source
1
// This Source Code Form is subject to the terms of the Mozilla Public
2
// License, v. 2.0. If a copy of the MPL was not distributed with this
3
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5
use crate::calendar::prelude::CommonDate;
6
use crate::calendar::prelude::CommonWeekOfYear;
7
use crate::calendar::prelude::GuaranteedMonth;
8
use crate::calendar::prelude::HasLeapYears;
9
use crate::calendar::prelude::OrdinalDate;
10
use crate::calendar::prelude::Quarter;
11
use crate::calendar::prelude::ToFromCommonDate;
12
use crate::calendar::AllowYearZero;
13
use crate::calendar::CalendarMoment;
14
use crate::calendar::ToFromOrdinalDate;
15
use crate::common::error::CalendarError;
16
use crate::common::math::TermNum;
17
use crate::day_count::BoundedDayCount;
18
use crate::day_count::CalculatedBounds;
19
use crate::day_count::Epoch;
20
use crate::day_count::Fixed;
21
use crate::day_count::FromFixed;
22
use crate::day_count::RataDie;
23
use crate::day_count::ToFixed;
24
use std::num::NonZero;
25
26
#[allow(unused_imports)] //FromPrimitive is needed for derive
27
use num_traits::FromPrimitive;
28
29
//LISTING 2.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
30
const GREGORIAN_EPOCH_RD: i32 = 1;
31
32
/// Represents a month in the Gregorian calendar
33
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
34
pub enum GregorianMonth {
35
    //LISTING 2.4-2.15 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
36
    January = 1,
37
    February,
38
    March,
39
    April,
40
    May,
41
    June,
42
    July,
43
    August,
44
    September,
45
    October,
46
    November,
47
    December,
48
}
49
50
impl GregorianMonth {
51
499k
    pub fn length(self, leap: bool) -> u8 {
52
        //LISTING ?? SECTION 2.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
53
        //TODO: use listing 2.1 here?
54
499k
        match self {
55
58.7k
            GregorianMonth::January => 31,
56
            GregorianMonth::February => {
57
10.2k
                if leap {
58
2.56k
                    29
59
                } else {
60
7.64k
                    28
61
                }
62
            }
63
9.27k
            GregorianMonth::March => 31,
64
8.03k
            GregorianMonth::April => 30,
65
6.88k
            GregorianMonth::May => 31,
66
8.17k
            GregorianMonth::June => 30,
67
25.1k
            GregorianMonth::July => 31,
68
185k
            GregorianMonth::August => 31,
69
129k
            GregorianMonth::September => 30,
70
8.14k
            GregorianMonth::October => 31,
71
6.62k
            GregorianMonth::November => 30,
72
44.2k
            GregorianMonth::December => 31,
73
        }
74
499k
    }
75
}
76
77
/// Represents a date in the proleptic Gregorian calendar
78
///
79
/// According to Wikipedia:
80
/// > The proleptic Gregorian calendar is produced by extending the Gregorian
81
/// > calendar backward to the dates preceding its official introduction in 1582.
82
///
83
/// This means there are no "skipped days" at the point where the Gregorian
84
/// calendar was introduced. Additionally, this means that year 0 is considered
85
/// valid for this implementation of the Gregorian calendar.
86
///
87
/// The Gregorian reform was implemented at different times in different countries.
88
/// For consistency with historical dates before the Gregorian reform, applications
89
/// should probably use the Julian calendar.
90
///
91
/// ## Further reading
92
/// + [Wikipedia](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar)
93
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
94
pub struct Gregorian(CommonDate);
95
96
impl Gregorian {
97
133k
    pub fn prior_elapsed_days(year: i32) -> i64 {
98
        //LISTING 2.17 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
99
        //These are the terms of the sum which do not rely on the month or day.
100
        //LISTING PriorElapsedDays (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
101
133k
        let year = year as i64;
102
133k
        let offset_e = Gregorian::epoch().get_day_i() - 1;
103
133k
        let offset_y = 365 * (year - 1);
104
133k
        let offset_leap =
105
133k
            (year - 1).div_euclid(4) - (year - 1).div_euclid(100) + (year - 1).div_euclid(400);
106
133k
        offset_e + offset_y + offset_leap
107
133k
    }
108
}
109
110
impl AllowYearZero for Gregorian {}
111
112
impl ToFromOrdinalDate for Gregorian {
113
5.12k
    fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
114
5.12k
        let correction = if Gregorian::is_leap(ord.year) { 
11.34k
} else {
03.77k
};
115
5.12k
        if ord.day_of_year > 0 && 
ord.day_of_year <= (365 + correction)4.09k
{
116
2.32k
            Ok(())
117
        } else {
118
2.79k
            Err(CalendarError::InvalidDayOfYear)
119
        }
120
5.12k
    }
121
122
91.3k
    fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
123
        //LISTING 2.21-2.22 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
124
91.3k
        let date = fixed_date.get_day_i();
125
91.3k
        let epoch = Gregorian::epoch().get_day_i();
126
91.3k
        let d0 = date - epoch;
127
91.3k
        let n400 = d0.div_euclid((400 * 365) + 100 - 3);
128
91.3k
        let d1 = d0.modulus((400 * 365) + 100 - 3);
129
91.3k
        let n100 = d1.div_euclid((365 * 100) + 25 - 1);
130
91.3k
        let d2 = d1.modulus((365 * 100) + 25 - 1);
131
91.3k
        let n4 = d2.div_euclid(365 * 4 + 1);
132
91.3k
        let d3 = d2.modulus(365 * 4 + 1);
133
91.3k
        let n1 = d3.div_euclid(365);
134
91.3k
        let year = (400 * n400) + (100 * n100) + (4 * n4) + n1;
135
91.3k
        if n100 == 4 || 
n1 == 491.3k
{
136
43
            OrdinalDate {
137
43
                year: year as i32,
138
43
                day_of_year: 366,
139
43
            }
140
        } else {
141
91.3k
            OrdinalDate {
142
91.3k
                year: (year + 1) as i32,
143
91.3k
                day_of_year: (d3.modulus(365) + 1) as u16,
144
91.3k
            }
145
        }
146
91.3k
    }
147
148
247k
    fn to_ordinal(self) -> OrdinalDate {
149
        //LISTING 2.17 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
150
        //These are the terms of the sum which rely on the month or day
151
247k
        let month = self.0.month as i64;
152
247k
        let day = self.0.day as i64;
153
247k
        let offset_m = ((367 * month) - 362).div_euclid(12);
154
247k
        let offset_x = if month <= 2 {
155
49.4k
            0
156
197k
        } else if Gregorian::is_leap(self.0.year) {
157
98.5k
            -1
158
        } else {
159
99.3k
            -2
160
        };
161
247k
        let offset_d = day;
162
247k
        OrdinalDate {
163
247k
            year: self.0.year,
164
247k
            day_of_year: (offset_m + offset_x + offset_d) as u16,
165
247k
        }
166
247k
    }
167
168
55.5k
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
169
        //LISTING 2.23 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
170
        //Modified to use ordinal day counts instead of day counts from the epoch
171
55.5k
        let year = ord.year;
172
55.5k
        let prior_days: i32 = (ord.day_of_year as i32) - 1; //Modification
173
55.5k
        let ord_march1 = Gregorian(CommonDate::new(year, 3, 1)).to_ordinal(); //Modification
174
55.5k
        let correction: i32 = if ord < ord_march1 {
175
            //Modification
176
21.8k
            0
177
33.6k
        } else if Gregorian::is_leap(year) {
178
3.31k
            1
179
        } else {
180
30.3k
            2
181
        };
182
55.5k
        let month = (12 * (prior_days + correction) + 373).div_euclid(367) as u8;
183
55.5k
        let ord_month = Gregorian(CommonDate::new(year, month, 1)).to_ordinal(); //Modification
184
55.5k
        let day = ((ord.day_of_year - ord_month.day_of_year) as u8) + 1; //Modification
185
55.5k
        debug_assert!(day > 0);
186
55.5k
        Gregorian(CommonDate { year, month, day })
187
55.5k
    }
188
}
189
190
impl HasLeapYears for Gregorian {
191
593k
    fn is_leap(g_year: i32) -> bool {
192
        //LISTING 2.16 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
193
593k
        let m4 = g_year.modulus(4);
194
593k
        let m400 = g_year.modulus(400);
195
593k
        m4 == 0 && 
m400 != 100285k
&&
m400 != 200284k
&&
m400 != 300282k
196
593k
    }
197
}
198
199
impl CalculatedBounds for Gregorian {}
200
201
impl Epoch for Gregorian {
202
298k
    fn epoch() -> Fixed {
203
298k
        RataDie::new(GREGORIAN_EPOCH_RD as f64).to_fixed()
204
298k
    }
205
}
206
207
impl FromFixed for Gregorian {
208
54.9k
    fn from_fixed(date: Fixed) -> Gregorian {
209
54.9k
        let ord = Gregorian::ordinal_from_fixed(date);
210
54.9k
        Gregorian::from_ordinal_unchecked(ord)
211
54.9k
    }
212
}
213
214
impl ToFixed for Gregorian {
215
133k
    fn to_fixed(self) -> Fixed {
216
        //LISTING 2.17 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
217
        //Method is split compared to the original
218
133k
        let offset_prior = Gregorian::prior_elapsed_days(self.0.year);
219
133k
        let ord = self.to_ordinal().day_of_year as i64;
220
133k
        Fixed::cast_new(offset_prior + ord)
221
133k
    }
222
}
223
224
impl ToFromCommonDate<GregorianMonth> for Gregorian {
225
60.0k
    fn to_common_date(self) -> CommonDate {
226
60.0k
        self.0
227
60.0k
    }
228
229
137k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
230
137k
        debug_assert!(Self::valid_ymd(date).is_ok());
231
137k
        Self(date)
232
137k
    }
233
234
291k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
235
291k
        let month_opt = GregorianMonth::from_u8(date.month);
236
291k
        if month_opt.is_none() {
237
1.53k
            Err(CalendarError::InvalidMonth)
238
289k
        } else if date.day < 1 {
239
512
            Err(CalendarError::InvalidDay)
240
289k
        } else if date.day > month_opt.unwrap().length(Gregorian::is_leap(date.year)) {
241
512
            Err(CalendarError::InvalidDay)
242
        } else {
243
288k
            Ok(())
244
        }
245
291k
    }
246
247
627
    fn year_end_date(year: i32) -> CommonDate {
248
        //LISTING 2.19 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
249
627
        let m = GregorianMonth::December;
250
627
        CommonDate::new(year, m as u8, m.length(Gregorian::is_leap(year)))
251
627
    }
252
}
253
254
impl Quarter for Gregorian {
255
2.56k
    fn quarter(self) -> NonZero<u8> {
256
2.56k
        NonZero::new(((self.to_common_date().month - 1) / 3) + 1).expect("(m-1)/3 > -1")
257
2.56k
    }
258
}
259
260
impl GuaranteedMonth<GregorianMonth> for Gregorian {}
261
impl CommonWeekOfYear<GregorianMonth> for Gregorian {}
262
263
/// Represents a date *and time* in the Gregorian Calendar
264
pub type GregorianMoment = CalendarMoment<Gregorian>;
265
266
#[cfg(test)]
267
mod tests {
268
    use super::*;
269
    use crate::day_count::FIXED_MAX;
270
    use crate::day_count::FIXED_MIN;
271
    use crate::day_cycle::Weekday;
272
    use proptest::proptest;
273
    use std::num::NonZero;
274
275
    #[test]
276
1
    fn us_canada_labor_day() {
277
1
        let lbd = Gregorian::try_from_common_date(CommonDate {
278
1
            year: 2024,
279
1
            month: 9,
280
1
            day: 2,
281
1
        })
282
1
        .unwrap();
283
1
        let start = Gregorian::try_from_common_date(CommonDate {
284
1
            year: 2024,
285
1
            month: 9,
286
1
            day: 1,
287
1
        })
288
1
        .unwrap();
289
1
        let finish = start.nth_kday(NonZero::new(1).unwrap(), Weekday::Monday);
290
1
        assert_eq!(lbd, Gregorian::from_fixed(finish));
291
1
    }
292
293
    #[test]
294
1
    fn us_memorial_day() {
295
1
        let mmd = Gregorian::try_from_common_date(CommonDate::new(2024, 5, 27)).unwrap();
296
1
        let start = Gregorian::try_from_common_date(CommonDate::new(2024, 6, 1)).unwrap();
297
1
        let finish = start.nth_kday(NonZero::new(-1).unwrap(), Weekday::Monday);
298
1
        assert_eq!(mmd, Gregorian::from_fixed(finish));
299
1
    }
300
301
    #[test]
302
1
    fn prior_elapsed_days() {
303
        // https://kalendis.free.nf/Symmetry454-Arithmetic.pdf
304
1
        let count = Gregorian::prior_elapsed_days(2009);
305
1
        assert_eq!(count, 733407);
306
1
    }
307
308
    #[test]
309
1
    fn ordinal_from_common() {
310
        // https://kalendis.free.nf/Symmetry454-Arithmetic.pdf
311
1
        let g = Gregorian::try_from_common_date(CommonDate::new(2009, 7, 14)).unwrap();
312
1
        let ord = g.to_ordinal();
313
1
        assert_eq!(ord.day_of_year, 195);
314
1
    }
315
316
    #[test]
317
1
    fn notable_days() {
318
1
        let dlist = [
319
1
            //Calendrical Calculations Table 1.2
320
1
            (CommonDate::new(-4713, 11, 24), -1721425), //Julian Day epoch
321
1
            (CommonDate::new(-3760, 9, 7), -1373427),   //Hebrew epoch
322
1
            (CommonDate::new(-3113, 8, 11), -1137142),  //Mayan epoch
323
1
            (CommonDate::new(-3101, 1, 23), -1132959),  //Hindu epoch (Kali Yuga)
324
1
            (CommonDate::new(-2636, 2, 15), -963099),   //Chinese epoch
325
1
            //(CommonDate::new(-1638, 3, 3), -598573),    //Samaritan epoch ... is it correct?
326
1
            (CommonDate::new(-746, 2, 18), -272787), //Egyptian epoch (Nabonassar era)
327
1
            (CommonDate::new(-310, 3, 29), -113502), //Babylonian epoch???????
328
1
            (CommonDate::new(-127, 12, 7), -46410),  //Tibetan epoch
329
1
            (CommonDate::new(0, 12, 30), -1),        // Julian calendar epoch
330
1
            (CommonDate::new(1, 1, 1), 1),           //Gregorian/ISO/Rata Die epoch
331
1
            (CommonDate::new(1, 2, 6), 37),          //Akan epoch
332
1
            (CommonDate::new(8, 8, 27), 2796),       //Ethiopic epoch
333
1
            (CommonDate::new(284, 8, 29), 103605),   //Coptic epoch
334
1
            (CommonDate::new(552, 7, 13), 201443),   //Armenian epoch
335
1
            (CommonDate::new(622, 3, 22), 226896),   //Persian epoch
336
1
            (CommonDate::new(622, 7, 19), 227015),   //Islamic epoch
337
1
            (CommonDate::new(632, 6, 19), 230638),   //Zoroastrian epoch?????
338
1
            (CommonDate::new(1792, 9, 22), 654415),  //French Revolutionary epoch
339
1
            (CommonDate::new(1844, 3, 21), 673222),  //Bahai epoch
340
1
            (CommonDate::new(1858, 11, 17), 678576), //Modified Julian Day epoch
341
1
            (CommonDate::new(1970, 1, 1), 719163),   //Unix epoch
342
1
            //Days which can be calculated by hand, or are at least easy to reason about
343
1
            (CommonDate::new(1, 1, 2), 2),
344
1
            (CommonDate::new(1, 1, 31), 31),
345
1
            (CommonDate::new(400, 12, 31), 146097),
346
1
            (CommonDate::new(800, 12, 31), 146097 * 2),
347
1
            (CommonDate::new(1200, 12, 31), 146097 * 3),
348
1
            (CommonDate::new(1600, 12, 31), 146097 * 4),
349
1
            (CommonDate::new(2000, 12, 31), 146097 * 5),
350
1
            (CommonDate::new(2003, 12, 31), (146097 * 5) + (365 * 3)),
351
1
        ];
352
353
30
        for 
pair29
in dlist {
354
29
            let d = Gregorian::try_from_common_date(pair.0).unwrap().to_fixed();
355
29
            assert_eq!(d.get_day_i(), pair.1);
356
        }
357
1
    }
358
359
    proptest! {
360
        #[test]
361
        fn cycle_146097(t in FIXED_MIN..(FIXED_MAX-146097.0), w in 1..55) {
362
            let f_start = Fixed::new(t);
363
            let f_end = Fixed::new(t + 146097.0);
364
            let g_start = Gregorian::from_fixed(f_start);
365
            let g_end = Gregorian::from_fixed(f_end);
366
            assert_eq!(g_start.year() + 400, g_end.year());
367
            assert_eq!(g_start.month(), g_end.month());
368
            assert_eq!(g_start.day(), g_start.day());
369
370
            let w = NonZero::new(w as i16).unwrap();
371
            let start_sum_kday = Fixed::new(g_start.nth_kday(w, Weekday::Sunday).get() + 146097.0);
372
            let end_kday = g_end.nth_kday(w, Weekday::Sunday);
373
            assert_eq!(start_sum_kday, end_kday);
374
        }
375
    }
376
}