Coverage Report

Created: 2025-10-19 21:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}