/home/a220/proj/radnelac/src/calendar/roman.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::julian::Julian; |
6 | | use crate::calendar::julian::JulianMonth; |
7 | | use crate::calendar::prelude::CommonDate; |
8 | | use crate::calendar::prelude::HasLeapYears; |
9 | | use crate::calendar::prelude::Quarter; |
10 | | use crate::calendar::prelude::ToFromCommonDate; |
11 | | use crate::common::math::TermNum; |
12 | | use crate::day_count::BoundedDayCount; |
13 | | use crate::day_count::CalculatedBounds; |
14 | | use crate::day_count::Fixed; |
15 | | use crate::day_count::FromFixed; |
16 | | use crate::day_count::ToFixed; |
17 | | use std::cmp::Ordering; |
18 | | use std::num::NonZero; |
19 | | |
20 | | #[allow(unused_imports)] //FromPrimitive is needed for derive |
21 | | use num_traits::FromPrimitive; |
22 | | |
23 | | //LISTING 3.12 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
24 | | const YEAR_ROME_FOUNDED_JULIAN: i32 = -753; |
25 | | |
26 | | /// Represents key events in a Roman month |
27 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)] |
28 | | pub enum RomanMonthlyEvent { |
29 | | //LISTING 3.5-3.7 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
30 | | Kalends = 1, |
31 | | Nones, |
32 | | Ides, |
33 | | } |
34 | | |
35 | | /// Represents a month in the Roman calendar after the Julian reform |
36 | | pub type RomanMonth = JulianMonth; |
37 | | |
38 | | impl RomanMonth { |
39 | 4.63k | pub fn ides_of_month(self) -> u8 { |
40 | | //LISTING 3.8 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
41 | 4.63k | match self { |
42 | 345 | RomanMonth::July => 15, |
43 | 327 | RomanMonth::March => 15, |
44 | 375 | RomanMonth::May => 15, |
45 | 439 | RomanMonth::October => 15, |
46 | 3.15k | _ => 13, |
47 | | } |
48 | 4.63k | } |
49 | | |
50 | 2.30k | pub fn nones_of_month(self) -> u8 { |
51 | | //LISTING 3.9 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
52 | 2.30k | self.ides_of_month() - 8 |
53 | 2.30k | } |
54 | | } |
55 | | |
56 | | /// Represents a date in the Roman calendar after the Julian reform |
57 | | /// |
58 | | /// ## Introduction |
59 | | /// |
60 | | /// This struct supports alternative naming schemes for the Julian calendar. |
61 | | /// |
62 | | /// The Romans referred to days using a countdown towards one of 3 events of the year: the |
63 | | /// Kalends, the Nones and the Ides. |
64 | | /// |
65 | | /// ## Epoch |
66 | | /// |
67 | | /// Most functions for Roman naming use the same Anno Domini / Before Christ (AD/BC) epoch as |
68 | | /// the Julian calendar, unless stated otherwise. |
69 | | /// |
70 | | /// The exceptions are functions which convert year numbers between the Anno Domini epoch and |
71 | | /// the Ab Urbe Condita (AUC) epoch, which corresponds to the legendary date of the founding of |
72 | | /// Rome. |
73 | | /// |
74 | | /// The year 1 AUC corresponds to 753 BC. |
75 | | /// |
76 | | /// ### Year 0 |
77 | | /// |
78 | | /// Year 0 is **not** supported because they are not supported in the Julian calendar. |
79 | | /// |
80 | | /// ## Further Reading |
81 | | /// |
82 | | /// + [Wikipedia](https://en.wikipedia.org/wiki/Roman_calendar#Days) |
83 | | #[derive(Debug, PartialEq, Clone, Copy)] |
84 | | pub struct Roman { |
85 | | year: NonZero<i32>, |
86 | | month: RomanMonth, |
87 | | event: RomanMonthlyEvent, |
88 | | count: NonZero<u8>, |
89 | | leap: bool, |
90 | | } |
91 | | |
92 | | impl Roman { |
93 | 7 | pub fn year(self) -> NonZero<i32> { |
94 | 7 | self.year |
95 | 7 | } |
96 | | |
97 | 519 | pub fn month(self) -> RomanMonth { |
98 | 519 | self.month |
99 | 519 | } |
100 | | |
101 | 7 | pub fn event(self) -> RomanMonthlyEvent { |
102 | 7 | self.event |
103 | 7 | } |
104 | | |
105 | 13 | pub fn count(self) -> NonZero<u8> { |
106 | 13 | self.count |
107 | 13 | } |
108 | | |
109 | 6 | pub fn leap(self) -> bool { |
110 | 6 | self.leap |
111 | 6 | } |
112 | | |
113 | | /// Converts from BC/AD year to AUC year |
114 | 256 | pub fn julian_year_from_auc(year: NonZero<i32>) -> NonZero<i32> { |
115 | | //LISTING 3.13 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
116 | | //Modified to use NonZero |
117 | 256 | let y = year.get(); |
118 | 256 | if y >= 1 && y <= -YEAR_ROME_FOUNDED_JULIAN142 { |
119 | 4 | NonZero::new(y + YEAR_ROME_FOUNDED_JULIAN - 1).expect("Checked by if") |
120 | | } else { |
121 | 252 | NonZero::new(y + YEAR_ROME_FOUNDED_JULIAN).expect("Checked by if") |
122 | | } |
123 | 256 | } |
124 | | |
125 | | /// Converts from AUC year to BC/AD year |
126 | 261 | pub fn auc_year_from_julian(year: NonZero<i32>) -> NonZero<i32> { |
127 | | //LISTING 3.14 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
128 | | //Modified to use NonZero |
129 | 261 | let y = year.get(); |
130 | 261 | if YEAR_ROME_FOUNDED_JULIAN <= y && y <= -1147 { |
131 | 7 | NonZero::new(y - YEAR_ROME_FOUNDED_JULIAN + 1).expect("Checked by if") |
132 | | } else { |
133 | 254 | NonZero::new(y - YEAR_ROME_FOUNDED_JULIAN).expect("Checked by if") |
134 | | } |
135 | 261 | } |
136 | | } |
137 | | |
138 | | impl PartialOrd for Roman { |
139 | 2.05k | fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
140 | 2.05k | if self == other { |
141 | 0 | Some(Ordering::Equal) |
142 | 2.05k | } else if self.year != other.year { |
143 | 1.26k | self.year.partial_cmp(&other.year) |
144 | 785 | } else if self.month != other.month { |
145 | 668 | self.month.partial_cmp(&other.month) |
146 | 117 | } else if self.event != other.event { |
147 | 60 | self.event.partial_cmp(&other.event) |
148 | 57 | } else if self.count != other.count { |
149 | 56 | other.count.partial_cmp(&self.count) //Intentionally reversed, "count" decreases with time |
150 | | } else { |
151 | | // "the second sixth day before the kalends of March" |
152 | 1 | (self.leap as u8).partial_cmp(&(other.leap as u8)) |
153 | | } |
154 | 2.05k | } |
155 | | } |
156 | | |
157 | | impl CalculatedBounds for Roman {} |
158 | | |
159 | | impl FromFixed for Roman { |
160 | 2.06k | fn from_fixed(date: Fixed) -> Roman { |
161 | | //LISTING 3.11 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
162 | 2.06k | let j = Julian::from_fixed(date); |
163 | 2.06k | let j_cdate = j.to_common_date(); |
164 | 2.06k | let month = (j_cdate.month as i64).adjusted_remainder(12) as u8; |
165 | 2.06k | let year = j_cdate.year; |
166 | 2.06k | let day = j_cdate.day; |
167 | 2.06k | let month1 = (month as i64 + 1).adjusted_remainder(12) as u8; |
168 | 2.06k | let year1 = if month1 != 1 { |
169 | 1.88k | year |
170 | 175 | } else if year != -1 { |
171 | 175 | year + 1 |
172 | | } else { |
173 | 0 | 1 |
174 | | }; |
175 | 2.06k | let month_r = RomanMonth::from_u8(month).expect("Kept in range by adjusted_remainder"); |
176 | 2.06k | let month1_r = RomanMonth::from_u8(month1).expect("Kept in range by adjusted_remainder"); |
177 | 2.06k | let kalends1 = Roman { |
178 | 2.06k | year: NonZero::new(year1).expect("From Julian date"), |
179 | 2.06k | month: month1_r, |
180 | 2.06k | event: RomanMonthlyEvent::Kalends, |
181 | 2.06k | count: NonZero::new(1).expect("1 != 0"), |
182 | 2.06k | leap: false, |
183 | 2.06k | } |
184 | 2.06k | .to_fixed() |
185 | 2.06k | .get_day_i(); |
186 | 2.06k | if day == 1 { |
187 | 81 | Roman { |
188 | 81 | year: NonZero::new(year).expect("From Julian date"), |
189 | 81 | month: month_r, |
190 | 81 | event: RomanMonthlyEvent::Kalends, |
191 | 81 | count: NonZero::new(1).expect("1 != 0"), |
192 | 81 | leap: false, |
193 | 81 | } |
194 | 1.98k | } else if day <= month_r.nones_of_month() { |
195 | 288 | Roman { |
196 | 288 | year: NonZero::new(year).expect("From Julian date"), |
197 | 288 | month: month_r, |
198 | 288 | event: RomanMonthlyEvent::Nones, |
199 | 288 | count: NonZero::new(month_r.nones_of_month() - day + 1).expect("Checked in if"), |
200 | 288 | leap: false, |
201 | 288 | } |
202 | 1.69k | } else if day <= month_r.ides_of_month() { |
203 | 565 | Roman { |
204 | 565 | year: NonZero::new(year).expect("From Julian date"), |
205 | 565 | month: month_r, |
206 | 565 | event: RomanMonthlyEvent::Ides, |
207 | 565 | count: NonZero::new(month_r.ides_of_month() - day + 1).expect("Checked in if"), |
208 | 565 | leap: false, |
209 | 565 | } |
210 | 1.12k | } else if month_r != RomanMonth::February || !Julian::is_leap(year)85 { |
211 | 1.10k | Roman { |
212 | 1.10k | year: NonZero::new(year1).expect("From Julian date"), |
213 | 1.10k | month: month1_r, |
214 | 1.10k | event: RomanMonthlyEvent::Kalends, |
215 | 1.10k | count: NonZero::new(((kalends1 - date.get_day_i()) + 1) as u8) |
216 | 1.10k | .expect("kalends1 > date"), |
217 | 1.10k | leap: false, |
218 | 1.10k | } |
219 | 19 | } else if day < 25 { |
220 | 15 | Roman { |
221 | 15 | year: NonZero::new(year).expect("From Julian date"), |
222 | 15 | month: RomanMonth::March, |
223 | 15 | event: RomanMonthlyEvent::Kalends, |
224 | 15 | count: NonZero::new((30 - day) as u8).expect("day < 25 < 30"), |
225 | 15 | leap: false, |
226 | 15 | } |
227 | | } else { |
228 | 4 | Roman { |
229 | 4 | year: NonZero::new(year).expect("From Julian date"), |
230 | 4 | month: RomanMonth::March, |
231 | 4 | event: RomanMonthlyEvent::Kalends, |
232 | 4 | count: NonZero::new((31 - day) as u8).expect("days in February < 31"), |
233 | 4 | leap: day == 25, |
234 | 4 | } |
235 | | } |
236 | 2.06k | } |
237 | | } |
238 | | |
239 | | impl ToFixed for Roman { |
240 | 2.31k | fn to_fixed(self) -> Fixed { |
241 | | //LISTING 3.10 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
242 | 2.31k | let jld = match self.event { |
243 | 2.20k | RomanMonthlyEvent::Kalends => 1, |
244 | 36 | RomanMonthlyEvent::Nones => self.month.nones_of_month(), |
245 | 74 | RomanMonthlyEvent::Ides => self.month.ides_of_month(), |
246 | | }; |
247 | 2.31k | let jlc = CommonDate::new(self.year.get(), self.month as u8, jld); |
248 | 2.31k | let j = Julian::try_from_common_date(jlc) |
249 | 2.31k | .expect("Month/day in range") |
250 | 2.31k | .to_fixed() |
251 | 2.31k | .get_day_i(); |
252 | 2.31k | let c = self.count.get() as i64; |
253 | 2.31k | let do_lp = Julian::is_leap(self.year.get()) |
254 | 585 | && self.month == RomanMonth::March |
255 | 31 | && self.event == RomanMonthlyEvent::Kalends |
256 | 31 | && self.count.get() <= 16 |
257 | 31 | && self.count.get() >= 6; |
258 | 2.31k | let lp0 = if do_lp { 01 } else { 12.31k }; |
259 | 2.31k | let lp1 = if self.leap { 10 } else { 0 }; |
260 | 2.31k | Fixed::cast_new(j - c + lp0 + lp1) |
261 | 2.31k | } |
262 | | } |
263 | | |
264 | | impl Quarter for Roman { |
265 | 512 | fn quarter(self) -> NonZero<u8> { |
266 | 512 | NonZero::new((((self.month() as u8) - 1) / 3) + 1).expect("m/4 > -1") |
267 | 512 | } |
268 | | } |
269 | | |
270 | | #[cfg(test)] |
271 | | mod tests { |
272 | | use super::*; |
273 | | use crate::calendar::prelude::ToFromCommonDate; |
274 | | use proptest::prop_assume; |
275 | | use proptest::proptest; |
276 | | |
277 | | #[test] |
278 | 1 | fn second_sixth_day_before_kalends_of_march() { |
279 | 1 | let j24 = Julian::try_from_common_date(CommonDate::new(4, 2, 24)).unwrap(); |
280 | 1 | let j25 = Julian::try_from_common_date(CommonDate::new(4, 2, 25)).unwrap(); |
281 | 1 | let f24 = j24.to_fixed(); |
282 | 1 | let f25 = j25.to_fixed(); |
283 | 1 | let r24 = Roman::from_fixed(f24); |
284 | 1 | let r25 = Roman::from_fixed(f25); |
285 | 1 | assert_eq!(r24.year(), r25.year()); |
286 | 1 | assert_eq!(r24.month(), r25.month()); |
287 | 1 | assert_eq!(r24.event(), r25.event()); |
288 | 1 | assert_eq!(r24.count(), r25.count()); |
289 | 1 | assert!(!r24.leap() && r25.leap()); |
290 | 1 | assert!(r24 < r25); |
291 | 1 | } |
292 | | |
293 | | #[test] |
294 | 1 | fn ides_of_march() { |
295 | 1 | let j = Julian::try_from_common_date(CommonDate::new(-44, 3, 15)).unwrap(); |
296 | 1 | let f = j.to_fixed(); |
297 | 1 | let r = Roman::from_fixed(f); |
298 | 1 | assert_eq!(r.event, RomanMonthlyEvent::Ides); |
299 | 1 | assert_eq!(r.month, RomanMonth::March); |
300 | 1 | assert_eq!(r.count.get(), 1); |
301 | 1 | } |
302 | | |
303 | | proptest! { |
304 | | #[test] |
305 | | fn auc_roundtrip(t in i16::MIN..i16::MAX) { |
306 | | prop_assume!(t != 0); |
307 | | let j_0 = NonZero::new(t as i32).unwrap(); |
308 | | let auc = Roman::auc_year_from_julian(j_0); |
309 | | let j_1 = Roman::julian_year_from_auc(auc); |
310 | | assert_eq!(j_0, j_1); |
311 | | assert!(auc > j_0); |
312 | | } |
313 | | } |
314 | | } |