/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.66k | pub fn ides_of_month(self) -> u8 { |
40 | | //LISTING 3.8 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
41 | 4.66k | match self { |
42 | 428 | RomanMonth::July => 15, |
43 | 399 | RomanMonth::March => 15, |
44 | 418 | RomanMonth::May => 15, |
45 | 376 | RomanMonth::October => 15, |
46 | 3.04k | _ => 13, |
47 | | } |
48 | 4.66k | } |
49 | | |
50 | 2.36k | pub fn nones_of_month(self) -> u8 { |
51 | | //LISTING 3.9 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
52 | 2.36k | self.ides_of_month() - 8 |
53 | 2.36k | } |
54 | | } |
55 | | |
56 | | /// Represents a date in the Roman calendar after the Julian reform |
57 | | /// |
58 | | /// This is essentially an alternative system for naming Julian dates. |
59 | | /// |
60 | | /// ## Year 0 |
61 | | /// |
62 | | /// Year 0 is **not** supported because they are not supported in the Julian calendar. |
63 | | #[derive(Debug, PartialEq, Clone, Copy)] |
64 | | pub struct Roman { |
65 | | year: NonZero<i32>, |
66 | | month: RomanMonth, |
67 | | event: RomanMonthlyEvent, |
68 | | count: NonZero<u8>, |
69 | | leap: bool, |
70 | | } |
71 | | |
72 | | impl Roman { |
73 | 7 | pub fn year(self) -> NonZero<i32> { |
74 | 7 | self.year |
75 | 7 | } |
76 | | |
77 | 519 | pub fn month(self) -> RomanMonth { |
78 | 519 | self.month |
79 | 519 | } |
80 | | |
81 | 7 | pub fn event(self) -> RomanMonthlyEvent { |
82 | 7 | self.event |
83 | 7 | } |
84 | | |
85 | 13 | pub fn count(self) -> NonZero<u8> { |
86 | 13 | self.count |
87 | 13 | } |
88 | | |
89 | 6 | pub fn leap(self) -> bool { |
90 | 6 | self.leap |
91 | 6 | } |
92 | | |
93 | | /// Converts from BC/AD year to AUC year |
94 | 256 | pub fn julian_year_from_auc(year: NonZero<i32>) -> NonZero<i32> { |
95 | | //LISTING 3.13 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
96 | | //Modified to use NonZero |
97 | 256 | let j_year = year.get(); |
98 | 256 | if j_year >= 1 && j_year <= -YEAR_ROME_FOUNDED_JULIAN130 { |
99 | 2 | NonZero::new(j_year + YEAR_ROME_FOUNDED_JULIAN - 1).expect("Checked by if") |
100 | | } else { |
101 | 254 | NonZero::new(j_year + YEAR_ROME_FOUNDED_JULIAN).expect("Checked by if") |
102 | | } |
103 | 256 | } |
104 | | |
105 | | /// Converts from AUC year to BC/AD year |
106 | 261 | pub fn auc_year_from_julian(year: NonZero<i32>) -> NonZero<i32> { |
107 | | //LISTING 3.14 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
108 | | //Modified to use NonZero |
109 | 261 | let a_year = year.get(); |
110 | 261 | if YEAR_ROME_FOUNDED_JULIAN <= a_year && a_year <= -1135 { |
111 | 5 | NonZero::new(a_year - YEAR_ROME_FOUNDED_JULIAN + 1).expect("Checked by if") |
112 | | } else { |
113 | 256 | NonZero::new(a_year - YEAR_ROME_FOUNDED_JULIAN).expect("Checked by if") |
114 | | } |
115 | 261 | } |
116 | | } |
117 | | |
118 | | impl PartialOrd for Roman { |
119 | 2.05k | fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
120 | 2.05k | if self == other { |
121 | 0 | Some(Ordering::Equal) |
122 | 2.05k | } else if self.year != other.year { |
123 | 1.22k | self.year.partial_cmp(&other.year) |
124 | 825 | } else if self.month != other.month { |
125 | 716 | self.month.partial_cmp(&other.month) |
126 | 109 | } else if self.event != other.event { |
127 | 60 | self.event.partial_cmp(&other.event) |
128 | 49 | } else if self.count != other.count { |
129 | 48 | other.count.partial_cmp(&self.count) //Intentionally reversed, "count" decreases with time |
130 | | } else { |
131 | | // "the second sixth day before the kalends of March" |
132 | 1 | (self.leap as u8).partial_cmp(&(other.leap as u8)) |
133 | | } |
134 | 2.05k | } |
135 | | } |
136 | | |
137 | | impl CalculatedBounds for Roman {} |
138 | | |
139 | | impl FromFixed for Roman { |
140 | 2.06k | fn from_fixed(date: Fixed) -> Roman { |
141 | | //LISTING 3.11 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
142 | 2.06k | let j = Julian::from_fixed(date); |
143 | 2.06k | let j_cdate = j.to_common_date(); |
144 | 2.06k | let month = (j_cdate.month as i64).adjusted_remainder(12) as u8; |
145 | 2.06k | let year = j_cdate.year; |
146 | 2.06k | let day = j_cdate.day; |
147 | 2.06k | let month1 = (month as i64 + 1).adjusted_remainder(12) as u8; |
148 | 2.06k | let year1 = if month1 != 1 { |
149 | 1.88k | year |
150 | 179 | } else if year != -1 { |
151 | 179 | year + 1 |
152 | | } else { |
153 | 0 | 1 |
154 | | }; |
155 | 2.06k | let month_r = RomanMonth::from_u8(month).expect("Kept in range by adjusted_remainder"); |
156 | 2.06k | let month1_r = RomanMonth::from_u8(month1).expect("Kept in range by adjusted_remainder"); |
157 | 2.06k | let kalends1 = Roman { |
158 | 2.06k | year: NonZero::new(year1).expect("From Julian date"), |
159 | 2.06k | month: month1_r, |
160 | 2.06k | event: RomanMonthlyEvent::Kalends, |
161 | 2.06k | count: NonZero::new(1).expect("1 != 0"), |
162 | 2.06k | leap: false, |
163 | 2.06k | } |
164 | 2.06k | .to_fixed() |
165 | 2.06k | .get_day_i(); |
166 | 2.06k | if day == 1 { |
167 | 55 | Roman { |
168 | 55 | year: NonZero::new(year).expect("From Julian date"), |
169 | 55 | month: month_r, |
170 | 55 | event: RomanMonthlyEvent::Kalends, |
171 | 55 | count: NonZero::new(1).expect("1 != 0"), |
172 | 55 | leap: false, |
173 | 55 | } |
174 | 2.00k | } else if day <= month_r.nones_of_month() { |
175 | 316 | Roman { |
176 | 316 | year: NonZero::new(year).expect("From Julian date"), |
177 | 316 | month: month_r, |
178 | 316 | event: RomanMonthlyEvent::Nones, |
179 | 316 | count: NonZero::new(month_r.nones_of_month() - day + 1).expect("Checked in if"), |
180 | 316 | leap: false, |
181 | 316 | } |
182 | 1.69k | } else if day <= month_r.ides_of_month() { |
183 | 534 | Roman { |
184 | 534 | year: NonZero::new(year).expect("From Julian date"), |
185 | 534 | month: month_r, |
186 | 534 | event: RomanMonthlyEvent::Ides, |
187 | 534 | count: NonZero::new(month_r.ides_of_month() - day + 1).expect("Checked in if"), |
188 | 534 | leap: false, |
189 | 534 | } |
190 | 1.15k | } else if month_r != RomanMonth::February || !Julian::is_leap(year)102 { |
191 | 1.13k | Roman { |
192 | 1.13k | year: NonZero::new(year1).expect("From Julian date"), |
193 | 1.13k | month: month1_r, |
194 | 1.13k | event: RomanMonthlyEvent::Kalends, |
195 | 1.13k | count: NonZero::new(((kalends1 - date.get_day_i()) + 1) as u8) |
196 | 1.13k | .expect("kalends1 > date"), |
197 | 1.13k | leap: false, |
198 | 1.13k | } |
199 | 27 | } else if day < 25 { |
200 | 19 | Roman { |
201 | 19 | year: NonZero::new(year).expect("From Julian date"), |
202 | 19 | month: RomanMonth::March, |
203 | 19 | event: RomanMonthlyEvent::Kalends, |
204 | 19 | count: NonZero::new((30 - day) as u8).expect("day < 25 < 30"), |
205 | 19 | leap: false, |
206 | 19 | } |
207 | | } else { |
208 | 8 | Roman { |
209 | 8 | year: NonZero::new(year).expect("From Julian date"), |
210 | 8 | month: RomanMonth::March, |
211 | 8 | event: RomanMonthlyEvent::Kalends, |
212 | 8 | count: NonZero::new((31 - day) as u8).expect("days in February < 31"), |
213 | 8 | leap: day == 25, |
214 | 8 | } |
215 | | } |
216 | 2.06k | } |
217 | | } |
218 | | |
219 | | impl ToFixed for Roman { |
220 | 2.31k | fn to_fixed(self) -> Fixed { |
221 | | //LISTING 3.10 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
222 | 2.31k | let jld = match self.event { |
223 | 2.20k | RomanMonthlyEvent::Kalends => 1, |
224 | 40 | RomanMonthlyEvent::Nones => self.month.nones_of_month(), |
225 | 74 | RomanMonthlyEvent::Ides => self.month.ides_of_month(), |
226 | | }; |
227 | 2.31k | let jlc = CommonDate::new(self.year.get(), self.month as u8, jld); |
228 | 2.31k | let j = Julian::try_from_common_date(jlc) |
229 | 2.31k | .expect("Month/day in range") |
230 | 2.31k | .to_fixed() |
231 | 2.31k | .get_day_i(); |
232 | 2.31k | let c = self.count.get() as i64; |
233 | 2.31k | let do_lp = Julian::is_leap(self.year.get()) |
234 | 623 | && self.month == RomanMonth::March |
235 | 51 | && self.event == RomanMonthlyEvent::Kalends |
236 | 48 | && self.count.get() <= 16 |
237 | 48 | && self.count.get() >= 6; |
238 | 2.31k | let lp0 = if do_lp { 02 } else { 12.31k }; |
239 | 2.31k | let lp1 = if self.leap { 10 } else { 0 }; |
240 | 2.31k | Fixed::cast_new(j - c + lp0 + lp1) |
241 | 2.31k | } |
242 | | } |
243 | | |
244 | | impl Quarter for Roman { |
245 | 512 | fn quarter(self) -> NonZero<u8> { |
246 | 512 | NonZero::new((((self.month() as u8) - 1) / 3) + 1).expect("m/4 > -1") |
247 | 512 | } |
248 | | } |
249 | | |
250 | | #[cfg(test)] |
251 | | mod tests { |
252 | | use super::*; |
253 | | use crate::calendar::prelude::ToFromCommonDate; |
254 | | use proptest::prop_assume; |
255 | | use proptest::proptest; |
256 | | |
257 | | #[test] |
258 | 1 | fn second_sixth_day_before_kalends_of_march() { |
259 | 1 | let j24 = Julian::try_from_common_date(CommonDate::new(4, 2, 24)).unwrap(); |
260 | 1 | let j25 = Julian::try_from_common_date(CommonDate::new(4, 2, 25)).unwrap(); |
261 | 1 | let f24 = j24.to_fixed(); |
262 | 1 | let f25 = j25.to_fixed(); |
263 | 1 | let r24 = Roman::from_fixed(f24); |
264 | 1 | let r25 = Roman::from_fixed(f25); |
265 | 1 | assert_eq!(r24.year(), r25.year()); |
266 | 1 | assert_eq!(r24.month(), r25.month()); |
267 | 1 | assert_eq!(r24.event(), r25.event()); |
268 | 1 | assert_eq!(r24.count(), r25.count()); |
269 | 1 | assert!(!r24.leap() && r25.leap()); |
270 | 1 | assert!(r24 < r25); |
271 | 1 | } |
272 | | |
273 | | #[test] |
274 | 1 | fn ides_of_march() { |
275 | 1 | let j = Julian::try_from_common_date(CommonDate::new(-44, 3, 15)).unwrap(); |
276 | 1 | let f = j.to_fixed(); |
277 | 1 | let r = Roman::from_fixed(f); |
278 | 1 | assert_eq!(r.event, RomanMonthlyEvent::Ides); |
279 | 1 | assert_eq!(r.month, RomanMonth::March); |
280 | 1 | assert_eq!(r.count.get(), 1); |
281 | 1 | } |
282 | | |
283 | | proptest! { |
284 | | #[test] |
285 | | fn auc_roundtrip(t in i16::MIN..i16::MAX) { |
286 | | prop_assume!(t != 0); |
287 | | assert_eq!(t as i32, Roman::julian_year_from_auc(Roman::auc_year_from_julian(NonZero::new(t as i32).unwrap())).get()); |
288 | | } |
289 | | } |
290 | | } |