Coverage Report

Created: 2025-10-19 21:01

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.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
}