/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 | | } |