Coverage Report

Created: 2025-10-19 21:01

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
/// Represents a date in the proleptic Gregorian calendar
51
///
52
/// ## Introduction
53
///
54
/// The Gregorian calendar is the calendar system used in most countries in the world today.
55
/// It was originally designed by Aloysius Lilius. It officially replaced the Julian calendar
56
/// in October 1582 in the Papal States, as part of a decree by Pope Gregory XIII.
57
///
58
/// ### Proleptic Modification
59
///
60
/// According to Wikipedia:
61
/// > The proleptic Gregorian calendar is produced by extending the Gregorian
62
/// > calendar backward to the dates preceding its official introduction in 1582.
63
///
64
/// This means there are no "skipped days" at the point where the Gregorian
65
/// calendar was introduced.
66
///
67
/// The Gregorian reform was implemented at different times in different countries.
68
/// For consistency with historical dates before the Gregorian reform, applications
69
/// should probably use the Julian calendar.
70
///
71
/// ### Year 0
72
///
73
/// Additionally, year 0 is considered valid for this implementation of the proleptic
74
/// Gregorian calendar.
75
///
76
/// ## Basic Structure
77
///
78
/// Years are divided into 12 months. Every month has either 30 or 31 days except for the
79
/// second month, February. February has 28 days in a common year and 29 days in a leap year.
80
///
81
/// Leap years occur on every year divisible by 400, and additionally on every year divisible
82
/// by 4 but not divisible by 100.
83
///
84
/// ## Epoch
85
///
86
/// The first day of the first year of the proleptic Gregorian calendar differs slightly from
87
/// that of the Julian calendar. This is one of the side effects of using a proleptic calendar
88
/// as mentioned in the "Proleptic Modification" section.
89
///
90
/// This crate uses the term "Common Era" (abbreviated "CE") specifically for the proleptic
91
/// Gregorian calendar epoch. The term "Anno Domini" (abbreviated "AD") is used for the Julian
92
/// calendar instead.
93
///
94
/// ## Representation and Examples
95
///
96
/// ### Months
97
///
98
/// The months are represented in this crate as [`GregorianMonth`].
99
///
100
/// ```
101
/// use radnelac::calendar::*;
102
/// use radnelac::day_count::*;
103
///
104
/// let c_1_1 = CommonDate::new(2025, 1, 1);
105
/// let a_1_1 = Gregorian::try_from_common_date(c_1_1).unwrap();
106
/// assert_eq!(a_1_1.month(), GregorianMonth::January);
107
/// ```
108
///
109
/// ### Conversion from Julian
110
///
111
/// For historical dates, it is often necessary to convert from the Julian system.
112
///
113
/// ```
114
/// use radnelac::calendar::*;
115
/// use radnelac::day_count::*;
116
///
117
/// let j = Julian::try_new(1752, JulianMonth::September, 3).unwrap();
118
/// let g = j.convert::<Gregorian>();
119
/// assert_eq!(g, Gregorian::try_new(1752, GregorianMonth::September, 14).unwrap());
120
/// ```
121
///
122
/// ## Inconsistencies with Other Implementations
123
///
124
/// Some other tools might use non-proleptic Gregorian calendars. See the "Proleptic
125
/// Modification" section for details.
126
///
127
/// For example, the UNIX `cal` command uses a non-proleptic Gregorian calendar by default.
128
/// The default settings assume 3 September 1752 was the date of the Gregorian reform (this
129
/// was the date used in the British Empire). Thus, some days of September 1752 are skipped:
130
///
131
/// ```bash
132
/// $ cal September 1752
133
///   September 1752   
134
/// Su Mo Tu We Th Fr Sa
135
///        1  2 14 15 16
136
/// 17 18 19 20 21 22 23
137
/// 24 25 26 27 28 29 30
138
/// ```
139
///
140
/// To imitate such behaviour in this crate, callers must explicitly switch between the
141
/// Julian and the Gregorian calendar. See the "Conversion from Julian" section for an example.
142
///
143
/// Additionally, other tools might not allow year 0 for the Gregorian calendar.
144
///
145
/// ## Further reading
146
/// + Wikipedia
147
///   + [Gregorian calendar](https://en.wikipedia.org/wiki/Gregorian_calendar)
148
///   + [Proleptic Gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar)
149
/// + [OpenGroup `cal`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cal.html)
150
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
151
pub struct Gregorian(CommonDate);
152
153
impl Gregorian {
154
133k
    pub fn prior_elapsed_days(year: i32) -> i64 {
155
        //LISTING 2.17 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
156
        //These are the terms of the sum which do not rely on the month or day.
157
        //LISTING PriorElapsedDays (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
158
133k
        let year = year as i64;
159
133k
        let offset_e = Gregorian::epoch().get_day_i() - 1;
160
133k
        let offset_y = 365 * (year - 1);
161
133k
        let offset_leap =
162
133k
            (year - 1).div_euclid(4) - (year - 1).div_euclid(100) + (year - 1).div_euclid(400);
163
133k
        offset_e + offset_y + offset_leap
164
133k
    }
165
}
166
167
impl AllowYearZero for Gregorian {}
168
169
impl ToFromOrdinalDate for Gregorian {
170
5.12k
    fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
171
5.12k
        let correction = if Gregorian::is_leap(ord.year) { 
11.18k
} else {
03.93k
};
172
5.12k
        if ord.day_of_year > 0 && 
ord.day_of_year <= (365 + correction)4.09k
{
173
2.27k
            Ok(())
174
        } else {
175
2.84k
            Err(CalendarError::InvalidDayOfYear)
176
        }
177
5.12k
    }
178
179
91.4k
    fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
180
        //LISTING 2.21-2.22 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
181
91.4k
        let date = fixed_date.get_day_i();
182
91.4k
        let epoch = Gregorian::epoch().get_day_i();
183
91.4k
        let d0 = date - epoch;
184
91.4k
        let n400 = d0.div_euclid((400 * 365) + 100 - 3);
185
91.4k
        let d1 = d0.modulus((400 * 365) + 100 - 3);
186
91.4k
        let n100 = d1.div_euclid((365 * 100) + 25 - 1);
187
91.4k
        let d2 = d1.modulus((365 * 100) + 25 - 1);
188
91.4k
        let n4 = d2.div_euclid(365 * 4 + 1);
189
91.4k
        let d3 = d2.modulus(365 * 4 + 1);
190
91.4k
        let n1 = d3.div_euclid(365);
191
91.4k
        let year = (400 * n400) + (100 * n100) + (4 * n4) + n1;
192
91.4k
        if n100 == 4 || 
n1 == 491.4k
{
193
48
            OrdinalDate {
194
48
                year: year as i32,
195
48
                day_of_year: 366,
196
48
            }
197
        } else {
198
91.3k
            OrdinalDate {
199
91.3k
                year: (year + 1) as i32,
200
91.3k
                day_of_year: (d3.modulus(365) + 1) as u16,
201
91.3k
            }
202
        }
203
91.4k
    }
204
205
247k
    fn to_ordinal(self) -> OrdinalDate {
206
        //LISTING 2.17 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
207
        //These are the terms of the sum which rely on the month or day
208
247k
        let month = self.0.month as i64;
209
247k
        let day = self.0.day as i64;
210
247k
        let offset_m = ((367 * month) - 362).div_euclid(12);
211
247k
        let offset_x = if month <= 2 {
212
49.3k
            0
213
198k
        } else if Gregorian::is_leap(self.0.year) {
214
98.5k
            -1
215
        } else {
216
99.5k
            -2
217
        };
218
247k
        let offset_d = day;
219
247k
        OrdinalDate {
220
247k
            year: self.0.year,
221
247k
            day_of_year: (offset_m + offset_x + offset_d) as u16,
222
247k
        }
223
247k
    }
224
225
55.5k
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
226
        //LISTING 2.23 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
227
        //Modified to use ordinal day counts instead of day counts from the epoch
228
55.5k
        let year = ord.year;
229
55.5k
        let prior_days: i32 = (ord.day_of_year as i32) - 1; //Modification
230
55.5k
        let ord_march1 = Gregorian(CommonDate::new(year, 3, 1)).to_ordinal(); //Modification
231
55.5k
        let correction: i32 = if ord < ord_march1 {
232
            //Modification
233
21.7k
            0
234
33.7k
        } else if Gregorian::is_leap(year) {
235
3.54k
            1
236
        } else {
237
30.2k
            2
238
        };
239
55.5k
        let month = (12 * (prior_days + correction) + 373).div_euclid(367) as u8;
240
55.5k
        let ord_month = Gregorian(CommonDate::new(year, month, 1)).to_ordinal(); //Modification
241
55.5k
        let day = ((ord.day_of_year - ord_month.day_of_year) as u8) + 1; //Modification
242
55.5k
        debug_assert!(day > 0);
243
55.5k
        Gregorian(CommonDate { year, month, day })
244
55.5k
    }
245
}
246
247
impl HasLeapYears for Gregorian {
248
312k
    fn is_leap(g_year: i32) -> bool {
249
        //LISTING 2.16 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
250
312k
        let m4 = g_year.modulus(4);
251
312k
        let m400 = g_year.modulus(400);
252
312k
        m4 == 0 && 
m400 != 100124k
&&
m400 != 200123k
&&
m400 != 300123k
253
312k
    }
254
}
255
256
impl CalculatedBounds for Gregorian {}
257
258
impl Epoch for Gregorian {
259
298k
    fn epoch() -> Fixed {
260
298k
        RataDie::new(GREGORIAN_EPOCH_RD as f64).to_fixed()
261
298k
    }
262
}
263
264
impl FromFixed for Gregorian {
265
55.0k
    fn from_fixed(date: Fixed) -> Gregorian {
266
55.0k
        let ord = Gregorian::ordinal_from_fixed(date);
267
55.0k
        Gregorian::from_ordinal_unchecked(ord)
268
55.0k
    }
269
}
270
271
impl ToFixed for Gregorian {
272
133k
    fn to_fixed(self) -> Fixed {
273
        //LISTING 2.17 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
274
        //Method is split compared to the original
275
133k
        let offset_prior = Gregorian::prior_elapsed_days(self.0.year);
276
133k
        let ord = self.to_ordinal().day_of_year as i64;
277
133k
        Fixed::cast_new(offset_prior + ord)
278
133k
    }
279
}
280
281
impl ToFromCommonDate<GregorianMonth> for Gregorian {
282
59.8k
    fn to_common_date(self) -> CommonDate {
283
59.8k
        self.0
284
59.8k
    }
285
286
137k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
287
137k
        debug_assert!(Self::valid_ymd(date).is_ok());
288
137k
        Self(date)
289
137k
    }
290
291
291k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
292
291k
        let month_opt = GregorianMonth::from_u8(date.month);
293
291k
        if month_opt.is_none() {
294
1.53k
            Err(CalendarError::InvalidMonth)
295
289k
        } else if date.day < 1 {
296
512
            Err(CalendarError::InvalidDay)
297
289k
        } else if date.day > Self::month_length(date.year, month_opt.unwrap()) {
298
512
            Err(CalendarError::InvalidDay)
299
        } else {
300
288k
            Ok(())
301
        }
302
291k
    }
303
304
632
    fn year_end_date(year: i32) -> CommonDate {
305
        //LISTING 2.19 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
306
632
        let m = GregorianMonth::December;
307
632
        CommonDate::new(year, m as u8, Self::month_length(year, m))
308
632
    }
309
310
499k
    fn month_length(year: i32, month: GregorianMonth) -> u8 {
311
        //LISTING ?? SECTION 2.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
312
        //TODO: use listing 2.1 here?
313
499k
        match month {
314
59.4k
            GregorianMonth::January => 31,
315
            GregorianMonth::February => {
316
8.57k
                if Gregorian::is_leap(year) {
317
2.04k
                    29
318
                } else {
319
6.52k
                    28
320
                }
321
            }
322
9.04k
            GregorianMonth::March => 31,
323
8.14k
            GregorianMonth::April => 30,
324
7.13k
            GregorianMonth::May => 31,
325
8.15k
            GregorianMonth::June => 30,
326
25.1k
            GregorianMonth::July => 31,
327
186k
            GregorianMonth::August => 31,
328
128k
            GregorianMonth::September => 30,
329
8.43k
            GregorianMonth::October => 31,
330
6.45k
            GregorianMonth::November => 30,
331
44.0k
            GregorianMonth::December => 31,
332
        }
333
499k
    }
334
}
335
336
impl Quarter for Gregorian {
337
2.56k
    fn quarter(self) -> NonZero<u8> {
338
2.56k
        NonZero::new(((self.to_common_date().month - 1) / 3) + 1).expect("(m-1)/3 > -1")
339
2.56k
    }
340
}
341
342
impl GuaranteedMonth<GregorianMonth> for Gregorian {}
343
impl CommonWeekOfYear<GregorianMonth> for Gregorian {}
344
345
/// Represents a date *and time* in the Gregorian Calendar
346
pub type GregorianMoment = CalendarMoment<Gregorian>;
347
348
#[cfg(test)]
349
mod tests {
350
    use super::*;
351
    use crate::day_count::FIXED_MAX;
352
    use crate::day_count::FIXED_MIN;
353
    use crate::day_cycle::Weekday;
354
    use proptest::proptest;
355
    use std::num::NonZero;
356
357
    #[test]
358
1
    fn us_canada_labor_day() {
359
1
        let lbd = Gregorian::try_from_common_date(CommonDate {
360
1
            year: 2024,
361
1
            month: 9,
362
1
            day: 2,
363
1
        })
364
1
        .unwrap();
365
1
        let start = Gregorian::try_from_common_date(CommonDate {
366
1
            year: 2024,
367
1
            month: 9,
368
1
            day: 1,
369
1
        })
370
1
        .unwrap();
371
1
        let finish = start.nth_kday(NonZero::new(1).unwrap(), Weekday::Monday);
372
1
        assert_eq!(lbd, Gregorian::from_fixed(finish));
373
1
    }
374
375
    #[test]
376
1
    fn us_memorial_day() {
377
1
        let mmd = Gregorian::try_from_common_date(CommonDate::new(2024, 5, 27)).unwrap();
378
1
        let start = Gregorian::try_from_common_date(CommonDate::new(2024, 6, 1)).unwrap();
379
1
        let finish = start.nth_kday(NonZero::new(-1).unwrap(), Weekday::Monday);
380
1
        assert_eq!(mmd, Gregorian::from_fixed(finish));
381
1
    }
382
383
    #[test]
384
1
    fn prior_elapsed_days() {
385
        // https://kalendis.free.nf/Symmetry454-Arithmetic.pdf
386
1
        let count = Gregorian::prior_elapsed_days(2009);
387
1
        assert_eq!(count, 733407);
388
1
    }
389
390
    #[test]
391
1
    fn ordinal_from_common() {
392
        // https://kalendis.free.nf/Symmetry454-Arithmetic.pdf
393
1
        let g = Gregorian::try_from_common_date(CommonDate::new(2009, 7, 14)).unwrap();
394
1
        let ord = g.to_ordinal();
395
1
        assert_eq!(ord.day_of_year, 195);
396
1
    }
397
398
    #[test]
399
1
    fn notable_days() {
400
1
        let dlist = [
401
1
            //Calendrical Calculations Table 1.2
402
1
            (CommonDate::new(-4713, 11, 24), -1721425), //Julian Day epoch
403
1
            (CommonDate::new(-3760, 9, 7), -1373427),   //Hebrew epoch
404
1
            (CommonDate::new(-3113, 8, 11), -1137142),  //Mayan epoch
405
1
            (CommonDate::new(-3101, 1, 23), -1132959),  //Hindu epoch (Kali Yuga)
406
1
            (CommonDate::new(-2636, 2, 15), -963099),   //Chinese epoch
407
1
            //(CommonDate::new(-1638, 3, 3), -598573),    //Samaritan epoch ... is it correct?
408
1
            (CommonDate::new(-746, 2, 18), -272787), //Egyptian epoch (Nabonassar era)
409
1
            (CommonDate::new(-310, 3, 29), -113502), //Babylonian epoch???????
410
1
            (CommonDate::new(-127, 12, 7), -46410),  //Tibetan epoch
411
1
            (CommonDate::new(0, 12, 30), -1),        // Julian calendar epoch
412
1
            (CommonDate::new(1, 1, 1), 1),           //Gregorian/ISO/Rata Die epoch
413
1
            (CommonDate::new(1, 2, 6), 37),          //Akan epoch
414
1
            (CommonDate::new(8, 8, 27), 2796),       //Ethiopic epoch
415
1
            (CommonDate::new(284, 8, 29), 103605),   //Coptic epoch
416
1
            (CommonDate::new(552, 7, 13), 201443),   //Armenian epoch
417
1
            (CommonDate::new(622, 3, 22), 226896),   //Persian epoch
418
1
            (CommonDate::new(622, 7, 19), 227015),   //Islamic epoch
419
1
            (CommonDate::new(632, 6, 19), 230638),   //Zoroastrian epoch?????
420
1
            (CommonDate::new(1792, 9, 22), 654415),  //French Revolutionary epoch
421
1
            (CommonDate::new(1844, 3, 21), 673222),  //Bahai epoch
422
1
            (CommonDate::new(1858, 11, 17), 678576), //Modified Julian Day epoch
423
1
            (CommonDate::new(1970, 1, 1), 719163),   //Unix epoch
424
1
            //Days which can be calculated by hand, or are at least easy to reason about
425
1
            (CommonDate::new(1, 1, 2), 2),
426
1
            (CommonDate::new(1, 1, 31), 31),
427
1
            (CommonDate::new(400, 12, 31), 146097),
428
1
            (CommonDate::new(800, 12, 31), 146097 * 2),
429
1
            (CommonDate::new(1200, 12, 31), 146097 * 3),
430
1
            (CommonDate::new(1600, 12, 31), 146097 * 4),
431
1
            (CommonDate::new(2000, 12, 31), 146097 * 5),
432
1
            (CommonDate::new(2003, 12, 31), (146097 * 5) + (365 * 3)),
433
1
        ];
434
435
30
        for 
pair29
in dlist {
436
29
            let d = Gregorian::try_from_common_date(pair.0).unwrap().to_fixed();
437
29
            assert_eq!(d.get_day_i(), pair.1);
438
        }
439
1
    }
440
441
    proptest! {
442
        #[test]
443
        fn cycle_146097(t in FIXED_MIN..(FIXED_MAX-146097.0), w in 1..55) {
444
            let f_start = Fixed::new(t);
445
            let f_end = Fixed::new(t + 146097.0);
446
            let g_start = Gregorian::from_fixed(f_start);
447
            let g_end = Gregorian::from_fixed(f_end);
448
            assert_eq!(g_start.year() + 400, g_end.year());
449
            assert_eq!(g_start.month(), g_end.month());
450
            assert_eq!(g_start.day(), g_start.day());
451
452
            let w = NonZero::new(w as i16).unwrap();
453
            let start_sum_kday = Fixed::new(g_start.nth_kday(w, Weekday::Sunday).get() + 146097.0);
454
            let end_kday = g_end.nth_kday(w, Weekday::Sunday);
455
            assert_eq!(start_sum_kday, end_kday);
456
        }
457
    }
458
}