/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::Gregorian; |
14 | | use crate::calendar::OrdinalDate; |
15 | | use crate::calendar::ToFromOrdinalDate; |
16 | | use crate::common::error::CalendarError; |
17 | | use crate::common::math::TermNum; |
18 | | use crate::day_count::BoundedDayCount; |
19 | | use crate::day_count::CalculatedBounds; |
20 | | use crate::day_count::Epoch; |
21 | | use crate::day_count::Fixed; |
22 | | use crate::day_count::FromFixed; |
23 | | use crate::day_count::RataDie; |
24 | | use crate::day_count::ToFixed; |
25 | | use std::num::NonZero; |
26 | | |
27 | | #[allow(unused_imports)] //FromPrimitive is needed for derive |
28 | | use num_traits::FromPrimitive; |
29 | | |
30 | | /// Represents a month in the Julian calendar |
31 | | pub type JulianMonth = GregorianMonth; |
32 | | |
33 | | //LISTING 3.2 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
34 | | //Instead of explicitly converting from Gregorian, just use the known Rata Die value. |
35 | | const JULIAN_EPOCH_RD: i32 = -1; |
36 | | |
37 | | /// Represents a date in the proleptic Julian calendar |
38 | | /// |
39 | | /// ## Introduction |
40 | | /// |
41 | | /// The Julian calendar is used by the Eastern Orthodox Church. Historically, it was used |
42 | | /// by the Roman Empire, many later European states, and the colonies of those states. |
43 | | /// |
44 | | /// The calendar is named after Julius Caesar who decreed that the calendar be used in the |
45 | | /// Roman Empire, replacing the calendar used in the late Roman Republic. Caesar may have |
46 | | /// been assisted by Sosigenes of Alexandria, according to Pliny. He may have also been |
47 | | /// assisted by Marcus Flavius, according to Macrobius. |
48 | | /// |
49 | | /// Over the past 500 years, the Julian calendar has been almost entirely replaced by the |
50 | | /// Gregorian calendar. |
51 | | /// |
52 | | /// ### Proleptic Modification |
53 | | /// |
54 | | /// During the initial adoption of the Julian calendar in 45 Before Christ (BC), leap years |
55 | | /// were every 3 years instead of every 4 years. According to Macrobius, this error was |
56 | | /// introduced by Roman priests and had to be corrected by Augustus in 8 Anno Domini (AD). |
57 | | /// (See the "Epoch" section for more details about BC, AD and AUC epoch labels). |
58 | | /// |
59 | | /// According to Wikipedia: |
60 | | /// > The proleptic Julian calendar is produced by extending the Julian calendar backwards |
61 | | /// > to dates preceding AD 8 when the quadrennial leap year stabilized. |
62 | | /// |
63 | | /// This crate implements a proleptic Julian calendar, and so does **not** change the leap year |
64 | | /// rules for dates before 8 AD. |
65 | | /// |
66 | | /// ### Year 0 |
67 | | /// |
68 | | /// Year 0 is **not** supported for this implementation of the Julian calendar. |
69 | | /// The year before 1 is -1. |
70 | | /// |
71 | | /// ## Basic Structure |
72 | | /// |
73 | | /// Years are divided into 12 months. Every month has either 30 or 31 days except for the |
74 | | /// second month, February. February has 28 days in a common year and 29 days in a leap year. |
75 | | /// |
76 | | /// Leap years occur on every positive year divisible by 4, and every negative year before |
77 | | /// a year divisible by 4. |
78 | | /// |
79 | | /// (See [`Roman`](crate::calendar::Roman) for Roman names of days). |
80 | | /// |
81 | | /// ## Epoch |
82 | | /// |
83 | | /// Years are numbered based on an estimate of the date of birth of Jesus Christ. The estimate |
84 | | /// was devised by Dionysius Exiguus 525 years after the birth supposedly happened. |
85 | | /// |
86 | | /// The first year of the Julian calendar is called 1 Anno Domini (abbreviated "AD"), and the |
87 | | /// year before that is called 1 Before Christ (abbreviated "BC"). |
88 | | /// |
89 | | /// ### Alternative Epochs |
90 | | /// |
91 | | /// Before 525 AD (and for centuries after 525 AD) there were other epochs used with the Julian |
92 | | /// calendar. One such epoch is "Ab urbe condita" (abbreviated "AUC"), based on the date of the |
93 | | /// founding of Rome - see [`Roman`](crate::calendar::Roman) for more details. |
94 | | /// |
95 | | /// Another method of identifying years was to name the consuls who held office that year. Regnal |
96 | | /// years were also used in Roman Egypt and the Byzantine Empire. |
97 | | /// |
98 | | /// ## Representation and Examples |
99 | | /// |
100 | | /// The months are represented in this crate as [`JulianMonth`]. |
101 | | /// |
102 | | /// ``` |
103 | | /// use radnelac::calendar::*; |
104 | | /// use radnelac::day_count::*; |
105 | | /// |
106 | | /// let c_1_1 = CommonDate::new(2025, 1, 1); |
107 | | /// let a_1_1 = Julian::try_from_common_date(c_1_1).unwrap(); |
108 | | /// assert_eq!(a_1_1.month(), JulianMonth::January); |
109 | | /// ``` |
110 | | /// |
111 | | /// ### Conversion to Gregorian |
112 | | /// |
113 | | /// For historical dates, it is often necessary to convert to the Gregorian system. |
114 | | /// |
115 | | /// ``` |
116 | | /// use radnelac::calendar::*; |
117 | | /// use radnelac::day_count::*; |
118 | | /// |
119 | | /// let j = Julian::try_new(1752, JulianMonth::September, 3).unwrap(); |
120 | | /// let g = j.convert::<Gregorian>(); |
121 | | /// assert_eq!(g, Gregorian::try_new(1752, GregorianMonth::September, 14).unwrap()); |
122 | | /// ``` |
123 | | /// |
124 | | /// ## Inconsistencies with Other Implementations |
125 | | /// |
126 | | /// Other systems may use non-proleptic Julian calendars. They might also allow year 0 for the |
127 | | /// Julian calendar. |
128 | | /// |
129 | | /// ## Further reading |
130 | | /// + Wikipedia |
131 | | /// + [Julian calendar](https://en.wikipedia.org/wiki/Julian_calendar) |
132 | | /// + [Proleptic Julian calendar](https://en.m.wikipedia.org/wiki/Proleptic_Julian_calendar) |
133 | | /// + [Ab urbe condita](https://en.m.wikipedia.org/wiki/Ab_urbe_condita) |
134 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] |
135 | | pub struct Julian(CommonDate); |
136 | | |
137 | | impl Julian { |
138 | 0 | pub fn nz_year(self) -> NonZero<i32> { |
139 | 0 | NonZero::new(self.0.year).expect("Will not be assigned zero") |
140 | 0 | } |
141 | | |
142 | 116k | pub fn prior_elapsed_days(year: i32) -> i64 { |
143 | | //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
144 | | //These are the terms which do not rely on the day or month |
145 | 116k | let y = if year < 0 { year + 113.6k } else { year102k } as i64; |
146 | 116k | let offset_e = Julian::epoch().get_day_i() - 1; |
147 | 116k | let offset_y = 365 * (y - 1); |
148 | 116k | let offset_leap = (y - 1).div_euclid(4); |
149 | 116k | offset_e + offset_y + offset_leap |
150 | 116k | } |
151 | | } |
152 | | |
153 | | impl ToFromOrdinalDate for Julian { |
154 | 1.28k | fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> { |
155 | 1.28k | let correction = if Julian::is_leap(ord.year) { 1356 } else { 0924 }; |
156 | 1.28k | if ord.day_of_year > 0 && ord.day_of_year <= (365 + correction)1.02k { |
157 | 586 | Ok(()) |
158 | | } else { |
159 | 694 | Err(CalendarError::InvalidDayOfYear) |
160 | | } |
161 | 1.28k | } |
162 | | |
163 | 14.8k | fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate { |
164 | | //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
165 | | //These are the calculations except for correction, month and day |
166 | 14.8k | let date = fixed_date.get_day_i(); |
167 | 14.8k | let epoch = Julian::epoch().get_day_i(); |
168 | 14.8k | let approx = ((4 * (date - epoch)) + 1464).div_euclid(1461); |
169 | 14.8k | let year = if approx <= 0 { approx - 17.51k } else { approx7.36k } as i32; |
170 | 14.8k | let year_start = Julian(CommonDate::new(year, 1, 1)).to_fixed().get_day_i(); |
171 | 14.8k | let prior_days = (date - year_start) as u16; |
172 | 14.8k | OrdinalDate { |
173 | 14.8k | year: year, |
174 | 14.8k | day_of_year: prior_days + 1, |
175 | 14.8k | } |
176 | 14.8k | } |
177 | | |
178 | 146k | fn to_ordinal(self) -> OrdinalDate { |
179 | | //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
180 | | //These are the terms which rely on the day or month |
181 | 146k | let year = self.0.year; |
182 | 146k | let month = self.0.month as i64; |
183 | 146k | let day = self.0.day as i64; |
184 | 146k | let offset_m = ((367 * month) - 362).div_euclid(12); |
185 | 146k | let offset_x = if month <= 2 { |
186 | 20.2k | 0 |
187 | 126k | } else if Julian::is_leap(year) { |
188 | 100k | -1 |
189 | | } else { |
190 | 25.8k | -2 |
191 | | }; |
192 | 146k | let offset_d = day; |
193 | | |
194 | 146k | OrdinalDate { |
195 | 146k | year: year, |
196 | 146k | day_of_year: (offset_m + offset_x + offset_d) as u16, |
197 | 146k | } |
198 | 146k | } |
199 | | |
200 | 14.1k | fn from_ordinal_unchecked(ord: OrdinalDate) -> Self { |
201 | | //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
202 | | //These are the calculations for correction, month and day |
203 | 14.1k | let year = ord.year; |
204 | 14.1k | let prior_days = ord.day_of_year - 1; |
205 | 14.1k | let march1 = Julian(CommonDate::new(year, 3, 1)).to_ordinal(); //Modification |
206 | 14.1k | let correction = if ord < march1 { |
207 | 1.47k | 0 |
208 | 12.6k | } else if Julian::is_leap(year) { |
209 | 4.19k | 1 |
210 | | } else { |
211 | 8.44k | 2 |
212 | | }; |
213 | 14.1k | let month = (12 * (prior_days + correction) + 373).div_euclid(367) as u8; |
214 | 14.1k | let month_start = Julian(CommonDate::new(year, month, 1)).to_ordinal(); |
215 | 14.1k | let day = ((ord.day_of_year - month_start.day_of_year) as u8) + 1; |
216 | 14.1k | debug_assert!(day > 0); |
217 | 14.1k | Julian(CommonDate { year, month, day }) |
218 | 14.1k | } |
219 | | } |
220 | | |
221 | | impl HasLeapYears for Julian { |
222 | 145k | fn is_leap(j_year: i32) -> bool { |
223 | | //LISTING 3.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
224 | 145k | let m4 = j_year.modulus(4); |
225 | 145k | if j_year > 0 { |
226 | 117k | m4 == 0 |
227 | | } else { |
228 | 27.9k | m4 == 3 |
229 | | } |
230 | 145k | } |
231 | | } |
232 | | |
233 | | impl CalculatedBounds for Julian {} |
234 | | |
235 | | impl Epoch for Julian { |
236 | 131k | fn epoch() -> Fixed { |
237 | 131k | RataDie::new(JULIAN_EPOCH_RD as f64).to_fixed() |
238 | 131k | } |
239 | | } |
240 | | |
241 | | impl FromFixed for Julian { |
242 | 13.8k | fn from_fixed(fixed_date: Fixed) -> Julian { |
243 | | //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
244 | | //Split compared to original |
245 | 13.8k | let ord = Self::ordinal_from_fixed(fixed_date); |
246 | 13.8k | Self::from_ordinal_unchecked(ord) |
247 | 13.8k | } |
248 | | } |
249 | | |
250 | | impl ToFixed for Julian { |
251 | 116k | fn to_fixed(self) -> Fixed { |
252 | | //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
253 | | //Split compared to original |
254 | 116k | let offset_prior = Julian::prior_elapsed_days(self.0.year); |
255 | 116k | let ord = self.to_ordinal(); |
256 | 116k | Fixed::cast_new(offset_prior + (ord.day_of_year as i64)) |
257 | 116k | } |
258 | | } |
259 | | |
260 | | impl ToFromCommonDate<JulianMonth> for Julian { |
261 | 18.1k | fn to_common_date(self) -> CommonDate { |
262 | 18.1k | self.0 |
263 | 18.1k | } |
264 | | |
265 | 104k | fn from_common_date_unchecked(date: CommonDate) -> Self { |
266 | 104k | debug_assert!(Self::valid_ymd(date).is_ok()); |
267 | 104k | Self(date) |
268 | 104k | } |
269 | | |
270 | 211k | fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> { |
271 | 211k | let month_opt = JulianMonth::from_u8(date.month); |
272 | 211k | if month_opt.is_none() { |
273 | 768 | Err(CalendarError::InvalidMonth) |
274 | 210k | } else if date.day < 1 { |
275 | 256 | Err(CalendarError::InvalidDay) |
276 | 210k | } else if date.day > Julian::month_length(date.year, month_opt.unwrap()) { |
277 | 256 | Err(CalendarError::InvalidDay) |
278 | 210k | } else if date.year == 0 { |
279 | 258 | Err(CalendarError::InvalidYear) |
280 | | } else { |
281 | 209k | Ok(()) |
282 | | } |
283 | 211k | } |
284 | | |
285 | 260 | fn year_end_date(year: i32) -> CommonDate { |
286 | 260 | let m = JulianMonth::December; |
287 | 260 | CommonDate::new(year, m as u8, Julian::month_length(year, m)) |
288 | 260 | } |
289 | | |
290 | 210k | fn month_length(year: i32, month: JulianMonth) -> u8 { |
291 | | //LISTING ?? SECTION 2.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
292 | | //TODO: use listing 2.1 here? |
293 | 210k | match month { |
294 | | JulianMonth::February => { |
295 | 1.71k | if Julian::is_leap(year) { |
296 | 443 | 29 |
297 | | } else { |
298 | 1.27k | 28 |
299 | | } |
300 | | } |
301 | 209k | _ => Gregorian::month_length(year, month), |
302 | | } |
303 | 210k | } |
304 | | } |
305 | | |
306 | | impl Quarter for Julian { |
307 | 2.56k | fn quarter(self) -> NonZero<u8> { |
308 | 2.56k | NonZero::new(((self.to_common_date().month - 1) / 3) + 1).expect("(m-1)/3 > -1") |
309 | 2.56k | } |
310 | | } |
311 | | |
312 | | impl GuaranteedMonth<JulianMonth> for Julian {} |
313 | | impl CommonWeekOfYear<JulianMonth> for Julian {} |
314 | | |
315 | | /// Represents a date *and time* in the Julian Calendar |
316 | | pub type JulianMoment = CalendarMoment<Julian>; |
317 | | |
318 | | #[cfg(test)] |
319 | | mod tests { |
320 | | use super::*; |
321 | | use crate::calendar::gregorian::Gregorian; |
322 | | use proptest::proptest; |
323 | | |
324 | | #[test] |
325 | 1 | fn julian_gregorian_conversion() { |
326 | 1 | let gap_list = [ |
327 | 1 | // Official dates of adopting the Gregorian calendar |
328 | 1 | // Governments would declare that certain days would be skipped |
329 | 1 | // The table below lists Julian dates and the Gregorian dates of the next day. |
330 | 1 | // https://en.wikipedia.org/wiki/Adoption_of_the_Gregorian_calendar |
331 | 1 | // https://en.wikipedia.org/wiki/List_of_adoption_dates_of_the_Gregorian_calendar_by_country |
332 | 1 | (CommonDate::new(1582, 10, 4), CommonDate::new(1582, 10, 15)), //Papal States, Spain, Portugal |
333 | 1 | (CommonDate::new(1582, 12, 9), CommonDate::new(1582, 12, 20)), //France |
334 | 1 | (CommonDate::new(1582, 12, 14), CommonDate::new(1582, 12, 25)), //"Flanders" (Belgium), Netherlands |
335 | 1 | (CommonDate::new(1582, 12, 20), CommonDate::new(1582, 12, 31)), //"Southern Netherlands" (Belgium), Luxembourg |
336 | 1 | (CommonDate::new(1582, 12, 31), CommonDate::new(1583, 1, 11)), //"Aachen" (Germany) |
337 | 1 | (CommonDate::new(1583, 1, 1), CommonDate::new(1583, 1, 12)), //"Holland" (Netherlands) |
338 | 1 | (CommonDate::new(1583, 2, 10), CommonDate::new(1583, 2, 21)), //"Salzburg" (Austria), "Liege" (Belgium) |
339 | 1 | (CommonDate::new(1583, 2, 13), CommonDate::new(1583, 2, 24)), //"Kaufbeuren" (Germany) |
340 | 1 | (CommonDate::new(1583, 2, 14), CommonDate::new(1583, 2, 25)), //"Ellwangen" (Germany) |
341 | 1 | (CommonDate::new(1583, 3, 1), CommonDate::new(1583, 3, 12)), //"Groningen" (Netherlands) |
342 | 1 | (CommonDate::new(1583, 10, 4), CommonDate::new(1583, 10, 15)), //"Tyrol" (Austria) |
343 | 1 | (CommonDate::new(1583, 10, 5), CommonDate::new(1583, 10, 16)), //"Bavaria" (Germany) |
344 | 1 | (CommonDate::new(1583, 10, 13), CommonDate::new(1583, 10, 24)), //"Austrian Upper Alsace" (France) |
345 | 1 | (CommonDate::new(1583, 10, 20), CommonDate::new(1583, 10, 31)), //"Lower Austria" (Austria) |
346 | 1 | (CommonDate::new(1583, 11, 2), CommonDate::new(1583, 11, 13)), //"Cologne" (Germany) |
347 | 1 | (CommonDate::new(1583, 11, 11), CommonDate::new(1583, 11, 22)), //"Mainz" (Germany) |
348 | 1 | (CommonDate::new(1632, 12, 14), CommonDate::new(1632, 12, 25)), //"Hildesheim" (Germany) |
349 | 1 | (CommonDate::new(1700, 2, 18), CommonDate::new(1700, 3, 1)), //"Denmark-Norway" (Denmark, Norway) |
350 | 1 | (CommonDate::new(1753, 2, 17), CommonDate::new(1753, 3, 1)), //Sweden (partial?) |
351 | 1 | (CommonDate::new(1752, 9, 2), CommonDate::new(1752, 9, 14)), //British Empire (United Kingdom, Ireland, Canada, United States) |
352 | 1 | (CommonDate::new(1753, 2, 17), CommonDate::new(1753, 3, 1)), //Sweden |
353 | 1 | (CommonDate::new(1912, 11, 14), CommonDate::new(1912, 11, 28)), //Albania |
354 | 1 | (CommonDate::new(1916, 3, 31), CommonDate::new(1916, 4, 14)), //Bulgaria |
355 | 1 | (CommonDate::new(1918, 1, 31), CommonDate::new(1918, 2, 14)), //Soviet Union (Russia, etc.) |
356 | 1 | (CommonDate::new(1918, 2, 15), CommonDate::new(1918, 3, 1)), //Estonia, Ukraine |
357 | 1 | (CommonDate::new(1918, 4, 17), CommonDate::new(1918, 5, 1)), //"Transcaucasian Democratic Federative Republic" |
358 | 1 | (CommonDate::new(1919, 1, 14), CommonDate::new(1919, 1, 28)), //Yugoslavia |
359 | 1 | (CommonDate::new(1919, 3, 31), CommonDate::new(1919, 4, 14)), //Romania |
360 | 1 | (CommonDate::new(1923, 2, 15), CommonDate::new(1923, 3, 1)), //Greece |
361 | 1 | ]; |
362 | | |
363 | 30 | for pair29 in gap_list { |
364 | 29 | let dj = Julian::try_from_common_date(pair.0).unwrap().to_fixed(); |
365 | 29 | let dg = Gregorian::try_from_common_date(pair.1).unwrap().to_fixed(); |
366 | 29 | assert_eq!(dj.get_day_i() + 1, dg.get_day_i()); |
367 | | } |
368 | 1 | } |
369 | | |
370 | | #[test] |
371 | 1 | fn cross_epoch() { |
372 | 1 | let new_years_eve = Julian::try_year_end(-1).unwrap().to_fixed(); |
373 | 1 | let new_years_day = Julian::try_year_start(1).unwrap().to_fixed(); |
374 | 1 | assert_eq!(new_years_day.get_day_i(), new_years_eve.get_day_i() + 1); |
375 | 1 | assert!(Julian::try_year_start(0).is_err()); |
376 | 1 | assert!(Julian::try_year_end(0).is_err()); |
377 | 1 | } |
378 | | |
379 | | proptest! { |
380 | | #[test] |
381 | | fn invalid_year_0(month in 1..12, day in 1..28) { |
382 | | let c = CommonDate::new(0, month as u8, day as u8); |
383 | | assert!(Julian::try_from_common_date(c).is_err()) |
384 | | } |
385 | | } |
386 | | } |