/home/a220/proj/radnelac/src/calendar/julian.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::gregorian::GregorianMonth; |
6 | | use crate::calendar::prelude::CommonDate; |
7 | | use crate::calendar::prelude::CommonWeekOfYear; |
8 | | use crate::calendar::prelude::GuaranteedMonth; |
9 | | use crate::calendar::prelude::HasLeapYears; |
10 | | use crate::calendar::prelude::Quarter; |
11 | | use crate::calendar::prelude::ToFromCommonDate; |
12 | | use crate::calendar::CalendarMoment; |
13 | | use crate::calendar::OrdinalDate; |
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 | | /// Represents a month in the Julian calendar |
30 | | pub type JulianMonth = GregorianMonth; |
31 | | |
32 | | //LISTING 3.2 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
33 | | //Instead of explicitly converting from Gregorian, just use the known Rata Die value. |
34 | | const JULIAN_EPOCH_RD: i32 = -1; |
35 | | |
36 | | /// Represents a date in the Julian calendar |
37 | | /// |
38 | | /// ## Year 0 |
39 | | /// |
40 | | /// Year 0 is **not** supported for this calendar. The year before 1 AD is 1 BC. |
41 | | /// |
42 | | /// ## Further reading |
43 | | /// + [Wikipedia](https://en.wikipedia.org/wiki/Julian_calendar) |
44 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] |
45 | | pub struct Julian(CommonDate); |
46 | | |
47 | | impl Julian { |
48 | 0 | pub fn nz_year(self) -> NonZero<i32> { |
49 | 0 | NonZero::new(self.0.year).expect("Will not be assigned zero") |
50 | 0 | } |
51 | | |
52 | 192k | pub fn prior_elapsed_days(year: i32) -> i64 { |
53 | | //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
54 | | //These are the terms which do not rely on the day or month |
55 | 192k | let y = if year < 0 { year + 117.7k } else { year174k } as i64; |
56 | 192k | let offset_e = Julian::epoch().get_day_i() - 1; |
57 | 192k | let offset_y = 365 * (y - 1); |
58 | 192k | let offset_leap = (y - 1).div_euclid(4); |
59 | 192k | offset_e + offset_y + offset_leap |
60 | 192k | } |
61 | | } |
62 | | |
63 | | impl ToFromOrdinalDate for Julian { |
64 | 1.28k | fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> { |
65 | 1.28k | let correction = if Julian::is_leap(ord.year) { 1338 } else { 0942 }; |
66 | 1.28k | if ord.day_of_year > 0 && ord.day_of_year <= (365 + correction)1.02k { |
67 | 581 | Ok(()) |
68 | | } else { |
69 | 699 | Err(CalendarError::InvalidDayOfYear) |
70 | | } |
71 | 1.28k | } |
72 | | |
73 | 21.5k | fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate { |
74 | | //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
75 | | //These are the calculations except for correction, month and day |
76 | 21.5k | let date = fixed_date.get_day_i(); |
77 | 21.5k | let epoch = Julian::epoch().get_day_i(); |
78 | 21.5k | let approx = ((4 * (date - epoch)) + 1464).div_euclid(1461); |
79 | 21.5k | let year = if approx <= 0 { approx - 19.56k } else { approx11.9k } as i32; |
80 | 21.5k | let year_start = Julian(CommonDate::new(year, 1, 1)).to_fixed().get_day_i(); |
81 | 21.5k | let prior_days = (date - year_start) as u16; |
82 | 21.5k | OrdinalDate { |
83 | 21.5k | year: year, |
84 | 21.5k | day_of_year: prior_days + 1, |
85 | 21.5k | } |
86 | 21.5k | } |
87 | | |
88 | 236k | fn to_ordinal(self) -> OrdinalDate { |
89 | | //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
90 | | //These are the terms which rely on the day or month |
91 | 236k | let year = self.0.year; |
92 | 236k | let month = self.0.month as i64; |
93 | 236k | let day = self.0.day as i64; |
94 | 236k | let offset_m = ((367 * month) - 362).div_euclid(12); |
95 | 236k | let offset_x = if month <= 2 { |
96 | 28.7k | 0 |
97 | 207k | } else if Julian::is_leap(year) { |
98 | 169k | -1 |
99 | | } else { |
100 | 38.3k | -2 |
101 | | }; |
102 | 236k | let offset_d = day; |
103 | | |
104 | 236k | OrdinalDate { |
105 | 236k | year: year, |
106 | 236k | day_of_year: (offset_m + offset_x + offset_d) as u16, |
107 | 236k | } |
108 | 236k | } |
109 | | |
110 | 20.7k | fn from_ordinal_unchecked(ord: OrdinalDate) -> Self { |
111 | | //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
112 | | //These are the calculations for correction, month and day |
113 | 20.7k | let year = ord.year; |
114 | 20.7k | let prior_days = ord.day_of_year - 1; |
115 | 20.7k | let march1 = Julian(CommonDate::new(year, 3, 1)).to_ordinal(); //Modification |
116 | 20.7k | let correction = if ord < march1 { |
117 | 2.52k | 0 |
118 | 18.2k | } else if Julian::is_leap(year) { |
119 | 5.84k | 1 |
120 | | } else { |
121 | 12.4k | 2 |
122 | | }; |
123 | 20.7k | let month = (12 * (prior_days + correction) + 373).div_euclid(367) as u8; |
124 | 20.7k | let month_start = Julian(CommonDate::new(year, month, 1)).to_ordinal(); |
125 | 20.7k | let day = ((ord.day_of_year - month_start.day_of_year) as u8) + 1; |
126 | 20.7k | debug_assert!(day > 0); |
127 | 20.7k | Julian(CommonDate { year, month, day }) |
128 | 20.7k | } |
129 | | } |
130 | | |
131 | | impl HasLeapYears for Julian { |
132 | 567k | fn is_leap(j_year: i32) -> bool { |
133 | | //LISTING 3.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
134 | 567k | let m4 = j_year.modulus(4); |
135 | 567k | if j_year > 0 { |
136 | 517k | m4 == 0 |
137 | | } else { |
138 | 49.9k | m4 == 3 |
139 | | } |
140 | 567k | } |
141 | | } |
142 | | |
143 | | impl CalculatedBounds for Julian {} |
144 | | |
145 | | impl Epoch for Julian { |
146 | 215k | fn epoch() -> Fixed { |
147 | 215k | RataDie::new(JULIAN_EPOCH_RD as f64).to_fixed() |
148 | 215k | } |
149 | | } |
150 | | |
151 | | impl FromFixed for Julian { |
152 | 20.5k | fn from_fixed(fixed_date: Fixed) -> Julian { |
153 | | //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
154 | | //Split compared to original |
155 | 20.5k | let ord = Self::ordinal_from_fixed(fixed_date); |
156 | 20.5k | Self::from_ordinal_unchecked(ord) |
157 | 20.5k | } |
158 | | } |
159 | | |
160 | | impl ToFixed for Julian { |
161 | 192k | fn to_fixed(self) -> Fixed { |
162 | | //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
163 | | //Split compared to original |
164 | 192k | let offset_prior = Julian::prior_elapsed_days(self.0.year); |
165 | 192k | let ord = self.to_ordinal(); |
166 | 192k | Fixed::cast_new(offset_prior + (ord.day_of_year as i64)) |
167 | 192k | } |
168 | | } |
169 | | |
170 | | impl ToFromCommonDate<JulianMonth> for Julian { |
171 | 44.0k | fn to_common_date(self) -> CommonDate { |
172 | 44.0k | self.0 |
173 | 44.0k | } |
174 | | |
175 | 168k | fn from_common_date_unchecked(date: CommonDate) -> Self { |
176 | 168k | debug_assert!(Self::valid_ymd(date).is_ok()); |
177 | 168k | Self(date) |
178 | 168k | } |
179 | | |
180 | 338k | fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> { |
181 | 338k | let month_opt = JulianMonth::from_u8(date.month); |
182 | 338k | if month_opt.is_none() { |
183 | 768 | Err(CalendarError::InvalidMonth) |
184 | 337k | } else if date.day < 1 { |
185 | 256 | Err(CalendarError::InvalidDay) |
186 | 337k | } else if date.day > month_opt.unwrap().length(Julian::is_leap(date.year)) { |
187 | 256 | Err(CalendarError::InvalidDay) |
188 | 337k | } else if date.year == 0 { |
189 | 258 | Err(CalendarError::InvalidYear) |
190 | | } else { |
191 | 336k | Ok(()) |
192 | | } |
193 | 338k | } |
194 | | |
195 | 259 | fn year_end_date(year: i32) -> CommonDate { |
196 | 259 | let m = JulianMonth::December; |
197 | 259 | CommonDate::new(year, m as u8, m.length(Julian::is_leap(year))) |
198 | 259 | } |
199 | | } |
200 | | |
201 | | impl Quarter for Julian { |
202 | 2.56k | fn quarter(self) -> NonZero<u8> { |
203 | 2.56k | NonZero::new(((self.to_common_date().month - 1) / 3) + 1).expect("(m-1)/3 > -1") |
204 | 2.56k | } |
205 | | } |
206 | | |
207 | | impl GuaranteedMonth<JulianMonth> for Julian {} |
208 | | impl CommonWeekOfYear<JulianMonth> for Julian {} |
209 | | |
210 | | /// Represents a date *and time* in the Julian Calendar |
211 | | pub type JulianMoment = CalendarMoment<Julian>; |
212 | | |
213 | | #[cfg(test)] |
214 | | mod tests { |
215 | | use super::*; |
216 | | use crate::calendar::gregorian::Gregorian; |
217 | | use proptest::proptest; |
218 | | |
219 | | #[test] |
220 | 1 | fn julian_gregorian_conversion() { |
221 | 1 | let gap_list = [ |
222 | 1 | // Official dates of adopting the Gregorian calendar |
223 | 1 | // Governments would declare that certain days would be skipped |
224 | 1 | // The table below lists Julian dates and the Gregorian dates of the next day. |
225 | 1 | // https://en.wikipedia.org/wiki/Adoption_of_the_Gregorian_calendar |
226 | 1 | // https://en.wikipedia.org/wiki/List_of_adoption_dates_of_the_Gregorian_calendar_by_country |
227 | 1 | (CommonDate::new(1582, 10, 4), CommonDate::new(1582, 10, 15)), //Papal States, Spain, Portugal |
228 | 1 | (CommonDate::new(1582, 12, 9), CommonDate::new(1582, 12, 20)), //France |
229 | 1 | (CommonDate::new(1582, 12, 14), CommonDate::new(1582, 12, 25)), //"Flanders" (Belgium), Netherlands |
230 | 1 | (CommonDate::new(1582, 12, 20), CommonDate::new(1582, 12, 31)), //"Southern Netherlands" (Belgium), Luxembourg |
231 | 1 | (CommonDate::new(1582, 12, 31), CommonDate::new(1583, 1, 11)), //"Aachen" (Germany) |
232 | 1 | (CommonDate::new(1583, 1, 1), CommonDate::new(1583, 1, 12)), //"Holland" (Netherlands) |
233 | 1 | (CommonDate::new(1583, 2, 10), CommonDate::new(1583, 2, 21)), //"Salzburg" (Austria), "Liege" (Belgium) |
234 | 1 | (CommonDate::new(1583, 2, 13), CommonDate::new(1583, 2, 24)), //"Kaufbeuren" (Germany) |
235 | 1 | (CommonDate::new(1583, 2, 14), CommonDate::new(1583, 2, 25)), //"Ellwangen" (Germany) |
236 | 1 | (CommonDate::new(1583, 3, 1), CommonDate::new(1583, 3, 12)), //"Groningen" (Netherlands) |
237 | 1 | (CommonDate::new(1583, 10, 4), CommonDate::new(1583, 10, 15)), //"Tyrol" (Austria) |
238 | 1 | (CommonDate::new(1583, 10, 5), CommonDate::new(1583, 10, 16)), //"Bavaria" (Germany) |
239 | 1 | (CommonDate::new(1583, 10, 13), CommonDate::new(1583, 10, 24)), //"Austrian Upper Alsace" (France) |
240 | 1 | (CommonDate::new(1583, 10, 20), CommonDate::new(1583, 10, 31)), //"Lower Austria" (Austria) |
241 | 1 | (CommonDate::new(1583, 11, 2), CommonDate::new(1583, 11, 13)), //"Cologne" (Germany) |
242 | 1 | (CommonDate::new(1583, 11, 11), CommonDate::new(1583, 11, 22)), //"Mainz" (Germany) |
243 | 1 | (CommonDate::new(1632, 12, 14), CommonDate::new(1632, 12, 25)), //"Hildesheim" (Germany) |
244 | 1 | (CommonDate::new(1700, 2, 18), CommonDate::new(1700, 3, 1)), //"Denmark-Norway" (Denmark, Norway) |
245 | 1 | (CommonDate::new(1753, 2, 17), CommonDate::new(1753, 3, 1)), //Sweden (partial?) |
246 | 1 | (CommonDate::new(1752, 9, 2), CommonDate::new(1752, 9, 14)), //British Empire (United Kingdom, Ireland, Canada, United States) |
247 | 1 | (CommonDate::new(1753, 2, 17), CommonDate::new(1753, 3, 1)), //Sweden |
248 | 1 | (CommonDate::new(1912, 11, 14), CommonDate::new(1912, 11, 28)), //Albania |
249 | 1 | (CommonDate::new(1916, 3, 31), CommonDate::new(1916, 4, 14)), //Bulgaria |
250 | 1 | (CommonDate::new(1918, 1, 31), CommonDate::new(1918, 2, 14)), //Soviet Union (Russia, etc.) |
251 | 1 | (CommonDate::new(1918, 2, 15), CommonDate::new(1918, 3, 1)), //Estonia, Ukraine |
252 | 1 | (CommonDate::new(1918, 4, 17), CommonDate::new(1918, 5, 1)), //"Transcaucasian Democratic Federative Republic" |
253 | 1 | (CommonDate::new(1919, 1, 14), CommonDate::new(1919, 1, 28)), //Yugoslavia |
254 | 1 | (CommonDate::new(1919, 3, 31), CommonDate::new(1919, 4, 14)), //Romania |
255 | 1 | (CommonDate::new(1923, 2, 15), CommonDate::new(1923, 3, 1)), //Greece |
256 | 1 | ]; |
257 | | |
258 | 30 | for pair29 in gap_list { |
259 | 29 | let dj = Julian::try_from_common_date(pair.0).unwrap().to_fixed(); |
260 | 29 | let dg = Gregorian::try_from_common_date(pair.1).unwrap().to_fixed(); |
261 | 29 | assert_eq!(dj.get_day_i() + 1, dg.get_day_i()); |
262 | | } |
263 | 1 | } |
264 | | |
265 | | #[test] |
266 | 1 | fn cross_epoch() { |
267 | 1 | let new_years_eve = Julian::try_year_end(-1).unwrap().to_fixed(); |
268 | 1 | let new_years_day = Julian::try_year_start(1).unwrap().to_fixed(); |
269 | 1 | assert_eq!(new_years_day.get_day_i(), new_years_eve.get_day_i() + 1); |
270 | 1 | assert!(Julian::try_year_start(0).is_err()); |
271 | 1 | assert!(Julian::try_year_end(0).is_err()); |
272 | 1 | } |
273 | | |
274 | | proptest! { |
275 | | #[test] |
276 | | fn invalid_year_0(month in 1..12, day in 1..28) { |
277 | | let c = CommonDate::new(0, month as u8, day as u8); |
278 | | assert!(Julian::try_from_common_date(c).is_err()) |
279 | | } |
280 | | } |
281 | | } |