/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.60k | pub fn ides_of_month(self) -> u8 { |
40 | | //LISTING 3.8 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
41 | 4.60k | match self { |
42 | 330 | RomanMonth::July => 15, |
43 | 409 | RomanMonth::March => 15, |
44 | 441 | RomanMonth::May => 15, |
45 | 362 | RomanMonth::October => 15, |
46 | 3.06k | _ => 13, |
47 | | } |
48 | 4.60k | } |
49 | | |
50 | 2.29k | pub fn nones_of_month(self) -> u8 { |
51 | | //LISTING 3.9 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
52 | 2.29k | self.ides_of_month() - 8 |
53 | 2.29k | } |
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 | 2 | pub fn year(self) -> NonZero<i32> { |
94 | 2 | self.year |
95 | 2 | } |
96 | | |
97 | 514 | pub fn month(self) -> RomanMonth { |
98 | 514 | self.month |
99 | 514 | } |
100 | | |
101 | 2 | pub fn event(self) -> RomanMonthlyEvent { |
102 | 2 | self.event |
103 | 2 | } |
104 | | |
105 | 2 | pub fn count(self) -> NonZero<u8> { |
106 | 2 | self.count |
107 | 2 | } |
108 | | |
109 | 2 | pub fn leap(self) -> bool { |
110 | 2 | self.leap |
111 | 2 | } |
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_JULIAN144 { |
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 | 256 | 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 | 256 | let y = year.get(); |
130 | 256 | if YEAR_ROME_FOUNDED_JULIAN <= y && y <= -1144 { |
131 | 4 | NonZero::new(y - YEAR_ROME_FOUNDED_JULIAN + 1).expect("Checked by if") |
132 | | } else { |
133 | 252 | NonZero::new(y - YEAR_ROME_FOUNDED_JULIAN).expect("Checked by if") |
134 | | } |
135 | 256 | } |
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 | 4 | Some(Ordering::Equal) |
142 | 2.04k | } else if self.year != other.year { |
143 | 1.20k | self.year.partial_cmp(&other.year) |
144 | 841 | } else if self.month != other.month { |
145 | 680 | self.month.partial_cmp(&other.month) |
146 | 161 | } else if self.event != other.event { |
147 | 76 | self.event.partial_cmp(&other.event) |
148 | 85 | } else if self.count != other.count { |
149 | 84 | 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.05k | fn from_fixed(date: Fixed) -> Roman { |
161 | | //LISTING 3.11 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
162 | 2.05k | let j = Julian::from_fixed(date); |
163 | 2.05k | let j_cdate = j.to_common_date(); |
164 | 2.05k | let month = (j_cdate.month as i64).adjusted_remainder(12) as u8; |
165 | 2.05k | let year = j_cdate.year; |
166 | 2.05k | let day = j_cdate.day; |
167 | 2.05k | let month1 = (month as i64 + 1).adjusted_remainder(12) as u8; |
168 | 2.05k | let year1 = if month1 != 1 { |
169 | 1.89k | year |
170 | 163 | } else if year != -1 { |
171 | 163 | year + 1 |
172 | | } else { |
173 | 0 | 1 |
174 | | }; |
175 | 2.05k | let month_r = RomanMonth::from_u8(month).expect("Kept in range by adjusted_remainder"); |
176 | 2.05k | let month1_r = RomanMonth::from_u8(month1).expect("Kept in range by adjusted_remainder"); |
177 | 2.05k | let kalends1 = Roman { |
178 | 2.05k | year: NonZero::new(year1).expect("From Julian date"), |
179 | 2.05k | month: month1_r, |
180 | 2.05k | event: RomanMonthlyEvent::Kalends, |
181 | 2.05k | count: NonZero::new(1).expect("1 != 0"), |
182 | 2.05k | leap: false, |
183 | 2.05k | } |
184 | 2.05k | .to_fixed() |
185 | 2.05k | .get_day_i(); |
186 | 2.05k | if day == 1 { |
187 | 73 | Roman { |
188 | 73 | year: NonZero::new(year).expect("From Julian date"), |
189 | 73 | month: month_r, |
190 | 73 | event: RomanMonthlyEvent::Kalends, |
191 | 73 | count: NonZero::new(1).expect("1 != 0"), |
192 | 73 | leap: false, |
193 | 73 | } |
194 | 1.98k | } else if day <= month_r.nones_of_month() { |
195 | 286 | Roman { |
196 | 286 | year: NonZero::new(year).expect("From Julian date"), |
197 | 286 | month: month_r, |
198 | 286 | event: RomanMonthlyEvent::Nones, |
199 | 286 | count: NonZero::new(month_r.nones_of_month() - day + 1).expect("Checked in if"), |
200 | 286 | leap: false, |
201 | 286 | } |
202 | 1.69k | } else if day <= month_r.ides_of_month() { |
203 | 543 | Roman { |
204 | 543 | year: NonZero::new(year).expect("From Julian date"), |
205 | 543 | month: month_r, |
206 | 543 | event: RomanMonthlyEvent::Ides, |
207 | 543 | count: NonZero::new(month_r.ides_of_month() - day + 1).expect("Checked in if"), |
208 | 543 | leap: false, |
209 | 543 | } |
210 | 1.15k | } else if month_r != RomanMonth::February || !Julian::is_leap(year)74 { |
211 | 1.12k | Roman { |
212 | 1.12k | year: NonZero::new(year1).expect("From Julian date"), |
213 | 1.12k | month: month1_r, |
214 | 1.12k | event: RomanMonthlyEvent::Kalends, |
215 | 1.12k | count: NonZero::new(((kalends1 - date.get_day_i()) + 1) as u8) |
216 | 1.12k | .expect("kalends1 > date"), |
217 | 1.12k | leap: false, |
218 | 1.12k | } |
219 | 26 | } else if day < 25 { |
220 | 17 | Roman { |
221 | 17 | year: NonZero::new(year).expect("From Julian date"), |
222 | 17 | month: RomanMonth::March, |
223 | 17 | event: RomanMonthlyEvent::Kalends, |
224 | 17 | count: NonZero::new((30 - day) as u8).expect("day < 25 < 30"), |
225 | 17 | leap: false, |
226 | 17 | } |
227 | | } else { |
228 | 9 | Roman { |
229 | 9 | year: NonZero::new(year).expect("From Julian date"), |
230 | 9 | month: RomanMonth::March, |
231 | 9 | event: RomanMonthlyEvent::Kalends, |
232 | 9 | count: NonZero::new((31 - day) as u8).expect("days in February < 31"), |
233 | 9 | leap: day == 25, |
234 | 9 | } |
235 | | } |
236 | 2.05k | } |
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.21k | RomanMonthlyEvent::Kalends => 1, |
244 | 25 | RomanMonthlyEvent::Nones => self.month.nones_of_month(), |
245 | 69 | 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 | 587 | && self.month == RomanMonth::March |
255 | 58 | && self.event == RomanMonthlyEvent::Kalends |
256 | 57 | && self.count.get() <= 16 |
257 | 57 | && 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 | | } |