Coverage Report

Created: 2025-08-13 21:02

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.70k
    pub fn ides_of_month(self) -> u8 {
40
        //LISTING 3.8 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
41
4.70k
        match self {
42
458
            RomanMonth::July => 15,
43
499
            RomanMonth::March => 15,
44
421
            RomanMonth::May => 15,
45
373
            RomanMonth::October => 15,
46
2.95k
            _ => 13,
47
        }
48
4.70k
    }
49
50
2.35k
    pub fn nones_of_month(self) -> u8 {
51
        //LISTING 3.9 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
52
2.35k
        self.ides_of_month() - 8
53
2.35k
    }
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
2
    pub fn year(self) -> NonZero<i32> {
74
2
        self.year
75
2
    }
76
77
514
    pub fn month(self) -> RomanMonth {
78
514
        self.month
79
514
    }
80
81
2
    pub fn event(self) -> RomanMonthlyEvent {
82
2
        self.event
83
2
    }
84
85
2
    pub fn count(self) -> NonZero<u8> {
86
2
        self.count
87
2
    }
88
89
2
    pub fn leap(self) -> bool {
90
2
        self.leap
91
2
    }
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_JULIAN131
{
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
256
    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
256
        let a_year = year.get();
110
256
        if YEAR_ROME_FOUNDED_JULIAN <= a_year && 
a_year <= -1131
{
111
2
            NonZero::new(a_year - YEAR_ROME_FOUNDED_JULIAN + 1).expect("Checked by if")
112
        } else {
113
254
            NonZero::new(a_year - YEAR_ROME_FOUNDED_JULIAN).expect("Checked by if")
114
        }
115
256
    }
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
4
            Some(Ordering::Equal)
122
2.04k
        } else if self.year != other.year {
123
1.15k
            self.year.partial_cmp(&other.year)
124
889
        } else if self.month != other.month {
125
772
            self.month.partial_cmp(&other.month)
126
117
        } else if self.event != other.event {
127
60
            self.event.partial_cmp(&other.event)
128
57
        } else if self.count != other.count {
129
56
            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.05k
    fn from_fixed(date: Fixed) -> Roman {
141
        //LISTING 3.11 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
142
2.05k
        let j = Julian::from_fixed(date);
143
2.05k
        let j_cdate = j.to_common_date();
144
2.05k
        let month = (j_cdate.month as i64).adjusted_remainder(12) as u8;
145
2.05k
        let year = j_cdate.year;
146
2.05k
        let day = j_cdate.day;
147
2.05k
        let month1 = (month as i64 + 1).adjusted_remainder(12) as u8;
148
2.05k
        let year1 = if month1 != 1 {
149
1.86k
            year
150
194
        } else if year != -1 {
151
194
            year + 1
152
        } else {
153
0
            1
154
        };
155
2.05k
        let month_r = RomanMonth::from_u8(month).expect("Kept in range by adjusted_remainder");
156
2.05k
        let month1_r = RomanMonth::from_u8(month1).expect("Kept in range by adjusted_remainder");
157
2.05k
        let kalends1 = Roman {
158
2.05k
            year: NonZero::new(year1).expect("From Julian date"),
159
2.05k
            month: month1_r,
160
2.05k
            event: RomanMonthlyEvent::Kalends,
161
2.05k
            count: NonZero::new(1).expect("1 != 0"),
162
2.05k
            leap: false,
163
2.05k
        }
164
2.05k
        .to_fixed()
165
2.05k
        .get_day_i();
166
2.05k
        if day == 1 {
167
66
            Roman {
168
66
                year: NonZero::new(year).expect("From Julian date"),
169
66
                month: month_r,
170
66
                event: RomanMonthlyEvent::Kalends,
171
66
                count: NonZero::new(1).expect("1 != 0"),
172
66
                leap: false,
173
66
            }
174
1.99k
        } else if day <= month_r.nones_of_month() {
175
329
            Roman {
176
329
                year: NonZero::new(year).expect("From Julian date"),
177
329
                month: month_r,
178
329
                event: RomanMonthlyEvent::Nones,
179
329
                count: NonZero::new(month_r.nones_of_month() - day + 1).expect("Checked in if"),
180
329
                leap: false,
181
329
            }
182
1.66k
        } else if day <= month_r.ides_of_month() {
183
599
            Roman {
184
599
                year: NonZero::new(year).expect("From Julian date"),
185
599
                month: month_r,
186
599
                event: RomanMonthlyEvent::Ides,
187
599
                count: NonZero::new(month_r.ides_of_month() - day + 1).expect("Checked in if"),
188
599
                leap: false,
189
599
            }
190
1.06k
        } else if month_r != RomanMonth::February || 
!Julian::is_leap(year)79
{
191
1.03k
            Roman {
192
1.03k
                year: NonZero::new(year1).expect("From Julian date"),
193
1.03k
                month: month1_r,
194
1.03k
                event: RomanMonthlyEvent::Kalends,
195
1.03k
                count: NonZero::new(((kalends1 - date.get_day_i()) + 1) as u8)
196
1.03k
                    .expect("kalends1 > date"),
197
1.03k
                leap: false,
198
1.03k
            }
199
24
        } else if day < 25 {
200
17
            Roman {
201
17
                year: NonZero::new(year).expect("From Julian date"),
202
17
                month: RomanMonth::March,
203
17
                event: RomanMonthlyEvent::Kalends,
204
17
                count: NonZero::new((30 - day) as u8).expect("day < 25 < 30"),
205
17
                leap: false,
206
17
            }
207
        } else {
208
7
            Roman {
209
7
                year: NonZero::new(year).expect("From Julian date"),
210
7
                month: RomanMonth::March,
211
7
                event: RomanMonthlyEvent::Kalends,
212
7
                count: NonZero::new((31 - day) as u8).expect("days in February < 31"),
213
7
                leap: day == 25,
214
7
            }
215
        }
216
2.05k
    }
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.19k
            RomanMonthlyEvent::Kalends => 1,
224
36
            RomanMonthlyEvent::Nones => self.month.nones_of_month(),
225
85
            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
605
            && self.month == RomanMonth::March
235
42
            && self.event == RomanMonthlyEvent::Kalends
236
39
            && self.count.get() <= 16
237
39
            && self.count.get() >= 6;
238
2.31k
        let lp0 = if do_lp { 
00
} else { 1 };
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
}