Coverage Report

Created: 2025-08-13 21:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/a220/proj/radnelac/src/calendar/julian.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::gregorian::GregorianMonth;
6
use crate::calendar::prelude::CommonDate;
7
use crate::calendar::prelude::CommonWeekOfYear;
8
use crate::calendar::prelude::GuaranteedMonth;
9
use crate::calendar::prelude::HasLeapYears;
10
use crate::calendar::prelude::Quarter;
11
use crate::calendar::prelude::ToFromCommonDate;
12
use crate::calendar::CalendarMoment;
13
use crate::calendar::OrdinalDate;
14
use crate::calendar::ToFromOrdinalDate;
15
use crate::common::error::CalendarError;
16
use crate::common::math::TermNum;
17
use crate::day_count::BoundedDayCount;
18
use crate::day_count::CalculatedBounds;
19
use crate::day_count::Epoch;
20
use crate::day_count::Fixed;
21
use crate::day_count::FromFixed;
22
use crate::day_count::RataDie;
23
use crate::day_count::ToFixed;
24
use std::num::NonZero;
25
26
#[allow(unused_imports)] //FromPrimitive is needed for derive
27
use num_traits::FromPrimitive;
28
29
/// Represents a month in the Julian calendar
30
pub type JulianMonth = GregorianMonth;
31
32
//LISTING 3.2 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
33
//Instead of explicitly converting from Gregorian, just use the known Rata Die value.
34
const JULIAN_EPOCH_RD: i32 = -1;
35
36
/// Represents a date in the Julian calendar
37
///
38
/// ## Year 0
39
///
40
/// Year 0 is **not** supported for this calendar. The year before 1 AD is 1 BC.
41
///
42
/// ## Further reading
43
/// + [Wikipedia](https://en.wikipedia.org/wiki/Julian_calendar)
44
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
45
pub struct Julian(CommonDate);
46
47
impl Julian {
48
0
    pub fn nz_year(self) -> NonZero<i32> {
49
0
        NonZero::new(self.0.year).expect("Will not be assigned zero")
50
0
    }
51
52
192k
    pub fn prior_elapsed_days(year: i32) -> i64 {
53
        //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
54
        //These are the terms which do not rely on the day or month
55
192k
        let y = if year < 0 { 
year + 117.7k
} else {
year174k
} as i64;
56
192k
        let offset_e = Julian::epoch().get_day_i() - 1;
57
192k
        let offset_y = 365 * (y - 1);
58
192k
        let offset_leap = (y - 1).div_euclid(4);
59
192k
        offset_e + offset_y + offset_leap
60
192k
    }
61
}
62
63
impl ToFromOrdinalDate for Julian {
64
1.28k
    fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
65
1.28k
        let correction = if Julian::is_leap(ord.year) { 
1338
} else {
0942
};
66
1.28k
        if ord.day_of_year > 0 && 
ord.day_of_year <= (365 + correction)1.02k
{
67
581
            Ok(())
68
        } else {
69
699
            Err(CalendarError::InvalidDayOfYear)
70
        }
71
1.28k
    }
72
73
21.5k
    fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
74
        //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
75
        //These are the calculations except for correction, month and day
76
21.5k
        let date = fixed_date.get_day_i();
77
21.5k
        let epoch = Julian::epoch().get_day_i();
78
21.5k
        let approx = ((4 * (date - epoch)) + 1464).div_euclid(1461);
79
21.5k
        let year = if approx <= 0 { 
approx - 19.56k
} else {
approx11.9k
} as i32;
80
21.5k
        let year_start = Julian(CommonDate::new(year, 1, 1)).to_fixed().get_day_i();
81
21.5k
        let prior_days = (date - year_start) as u16;
82
21.5k
        OrdinalDate {
83
21.5k
            year: year,
84
21.5k
            day_of_year: prior_days + 1,
85
21.5k
        }
86
21.5k
    }
87
88
236k
    fn to_ordinal(self) -> OrdinalDate {
89
        //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
90
        //These are the terms which rely on the day or month
91
236k
        let year = self.0.year;
92
236k
        let month = self.0.month as i64;
93
236k
        let day = self.0.day as i64;
94
236k
        let offset_m = ((367 * month) - 362).div_euclid(12);
95
236k
        let offset_x = if month <= 2 {
96
28.7k
            0
97
207k
        } else if Julian::is_leap(year) {
98
169k
            -1
99
        } else {
100
38.3k
            -2
101
        };
102
236k
        let offset_d = day;
103
104
236k
        OrdinalDate {
105
236k
            year: year,
106
236k
            day_of_year: (offset_m + offset_x + offset_d) as u16,
107
236k
        }
108
236k
    }
109
110
20.7k
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
111
        //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
112
        //These are the calculations for correction, month and day
113
20.7k
        let year = ord.year;
114
20.7k
        let prior_days = ord.day_of_year - 1;
115
20.7k
        let march1 = Julian(CommonDate::new(year, 3, 1)).to_ordinal(); //Modification
116
20.7k
        let correction = if ord < march1 {
117
2.52k
            0
118
18.2k
        } else if Julian::is_leap(year) {
119
5.84k
            1
120
        } else {
121
12.4k
            2
122
        };
123
20.7k
        let month = (12 * (prior_days + correction) + 373).div_euclid(367) as u8;
124
20.7k
        let month_start = Julian(CommonDate::new(year, month, 1)).to_ordinal();
125
20.7k
        let day = ((ord.day_of_year - month_start.day_of_year) as u8) + 1;
126
20.7k
        debug_assert!(day > 0);
127
20.7k
        Julian(CommonDate { year, month, day })
128
20.7k
    }
129
}
130
131
impl HasLeapYears for Julian {
132
567k
    fn is_leap(j_year: i32) -> bool {
133
        //LISTING 3.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
134
567k
        let m4 = j_year.modulus(4);
135
567k
        if j_year > 0 {
136
517k
            m4 == 0
137
        } else {
138
49.9k
            m4 == 3
139
        }
140
567k
    }
141
}
142
143
impl CalculatedBounds for Julian {}
144
145
impl Epoch for Julian {
146
215k
    fn epoch() -> Fixed {
147
215k
        RataDie::new(JULIAN_EPOCH_RD as f64).to_fixed()
148
215k
    }
149
}
150
151
impl FromFixed for Julian {
152
20.5k
    fn from_fixed(fixed_date: Fixed) -> Julian {
153
        //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
154
        //Split compared to original
155
20.5k
        let ord = Self::ordinal_from_fixed(fixed_date);
156
20.5k
        Self::from_ordinal_unchecked(ord)
157
20.5k
    }
158
}
159
160
impl ToFixed for Julian {
161
192k
    fn to_fixed(self) -> Fixed {
162
        //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
163
        //Split compared to original
164
192k
        let offset_prior = Julian::prior_elapsed_days(self.0.year);
165
192k
        let ord = self.to_ordinal();
166
192k
        Fixed::cast_new(offset_prior + (ord.day_of_year as i64))
167
192k
    }
168
}
169
170
impl ToFromCommonDate<JulianMonth> for Julian {
171
44.0k
    fn to_common_date(self) -> CommonDate {
172
44.0k
        self.0
173
44.0k
    }
174
175
168k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
176
168k
        debug_assert!(Self::valid_ymd(date).is_ok());
177
168k
        Self(date)
178
168k
    }
179
180
338k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
181
338k
        let month_opt = JulianMonth::from_u8(date.month);
182
338k
        if month_opt.is_none() {
183
768
            Err(CalendarError::InvalidMonth)
184
337k
        } else if date.day < 1 {
185
256
            Err(CalendarError::InvalidDay)
186
337k
        } else if date.day > month_opt.unwrap().length(Julian::is_leap(date.year)) {
187
256
            Err(CalendarError::InvalidDay)
188
337k
        } else if date.year == 0 {
189
258
            Err(CalendarError::InvalidYear)
190
        } else {
191
336k
            Ok(())
192
        }
193
338k
    }
194
195
259
    fn year_end_date(year: i32) -> CommonDate {
196
259
        let m = JulianMonth::December;
197
259
        CommonDate::new(year, m as u8, m.length(Julian::is_leap(year)))
198
259
    }
199
}
200
201
impl Quarter for Julian {
202
2.56k
    fn quarter(self) -> NonZero<u8> {
203
2.56k
        NonZero::new(((self.to_common_date().month - 1) / 3) + 1).expect("(m-1)/3 > -1")
204
2.56k
    }
205
}
206
207
impl GuaranteedMonth<JulianMonth> for Julian {}
208
impl CommonWeekOfYear<JulianMonth> for Julian {}
209
210
/// Represents a date *and time* in the Julian Calendar
211
pub type JulianMoment = CalendarMoment<Julian>;
212
213
#[cfg(test)]
214
mod tests {
215
    use super::*;
216
    use crate::calendar::gregorian::Gregorian;
217
    use proptest::proptest;
218
219
    #[test]
220
1
    fn julian_gregorian_conversion() {
221
1
        let gap_list = [
222
1
            // Official dates of adopting the Gregorian calendar
223
1
            // Governments would declare that certain days would be skipped
224
1
            // The table below lists Julian dates and the Gregorian dates of the next day.
225
1
            // https://en.wikipedia.org/wiki/Adoption_of_the_Gregorian_calendar
226
1
            // https://en.wikipedia.org/wiki/List_of_adoption_dates_of_the_Gregorian_calendar_by_country
227
1
            (CommonDate::new(1582, 10, 4), CommonDate::new(1582, 10, 15)), //Papal States, Spain, Portugal
228
1
            (CommonDate::new(1582, 12, 9), CommonDate::new(1582, 12, 20)), //France
229
1
            (CommonDate::new(1582, 12, 14), CommonDate::new(1582, 12, 25)), //"Flanders" (Belgium), Netherlands
230
1
            (CommonDate::new(1582, 12, 20), CommonDate::new(1582, 12, 31)), //"Southern Netherlands" (Belgium), Luxembourg
231
1
            (CommonDate::new(1582, 12, 31), CommonDate::new(1583, 1, 11)),  //"Aachen" (Germany)
232
1
            (CommonDate::new(1583, 1, 1), CommonDate::new(1583, 1, 12)), //"Holland" (Netherlands)
233
1
            (CommonDate::new(1583, 2, 10), CommonDate::new(1583, 2, 21)), //"Salzburg" (Austria), "Liege" (Belgium)
234
1
            (CommonDate::new(1583, 2, 13), CommonDate::new(1583, 2, 24)), //"Kaufbeuren" (Germany)
235
1
            (CommonDate::new(1583, 2, 14), CommonDate::new(1583, 2, 25)), //"Ellwangen" (Germany)
236
1
            (CommonDate::new(1583, 3, 1), CommonDate::new(1583, 3, 12)), //"Groningen" (Netherlands)
237
1
            (CommonDate::new(1583, 10, 4), CommonDate::new(1583, 10, 15)), //"Tyrol" (Austria)
238
1
            (CommonDate::new(1583, 10, 5), CommonDate::new(1583, 10, 16)), //"Bavaria" (Germany)
239
1
            (CommonDate::new(1583, 10, 13), CommonDate::new(1583, 10, 24)), //"Austrian Upper Alsace" (France)
240
1
            (CommonDate::new(1583, 10, 20), CommonDate::new(1583, 10, 31)), //"Lower Austria" (Austria)
241
1
            (CommonDate::new(1583, 11, 2), CommonDate::new(1583, 11, 13)),  //"Cologne" (Germany)
242
1
            (CommonDate::new(1583, 11, 11), CommonDate::new(1583, 11, 22)), //"Mainz" (Germany)
243
1
            (CommonDate::new(1632, 12, 14), CommonDate::new(1632, 12, 25)), //"Hildesheim" (Germany)
244
1
            (CommonDate::new(1700, 2, 18), CommonDate::new(1700, 3, 1)), //"Denmark-Norway" (Denmark, Norway)
245
1
            (CommonDate::new(1753, 2, 17), CommonDate::new(1753, 3, 1)), //Sweden (partial?)
246
1
            (CommonDate::new(1752, 9, 2), CommonDate::new(1752, 9, 14)), //British Empire (United Kingdom, Ireland, Canada, United States)
247
1
            (CommonDate::new(1753, 2, 17), CommonDate::new(1753, 3, 1)), //Sweden
248
1
            (CommonDate::new(1912, 11, 14), CommonDate::new(1912, 11, 28)), //Albania
249
1
            (CommonDate::new(1916, 3, 31), CommonDate::new(1916, 4, 14)), //Bulgaria
250
1
            (CommonDate::new(1918, 1, 31), CommonDate::new(1918, 2, 14)), //Soviet Union (Russia, etc.)
251
1
            (CommonDate::new(1918, 2, 15), CommonDate::new(1918, 3, 1)),  //Estonia, Ukraine
252
1
            (CommonDate::new(1918, 4, 17), CommonDate::new(1918, 5, 1)), //"Transcaucasian Democratic Federative Republic"
253
1
            (CommonDate::new(1919, 1, 14), CommonDate::new(1919, 1, 28)), //Yugoslavia
254
1
            (CommonDate::new(1919, 3, 31), CommonDate::new(1919, 4, 14)), //Romania
255
1
            (CommonDate::new(1923, 2, 15), CommonDate::new(1923, 3, 1)), //Greece
256
1
        ];
257
258
30
        for 
pair29
in gap_list {
259
29
            let dj = Julian::try_from_common_date(pair.0).unwrap().to_fixed();
260
29
            let dg = Gregorian::try_from_common_date(pair.1).unwrap().to_fixed();
261
29
            assert_eq!(dj.get_day_i() + 1, dg.get_day_i());
262
        }
263
1
    }
264
265
    #[test]
266
1
    fn cross_epoch() {
267
1
        let new_years_eve = Julian::try_year_end(-1).unwrap().to_fixed();
268
1
        let new_years_day = Julian::try_year_start(1).unwrap().to_fixed();
269
1
        assert_eq!(new_years_day.get_day_i(), new_years_eve.get_day_i() + 1);
270
1
        assert!(Julian::try_year_start(0).is_err());
271
1
        assert!(Julian::try_year_end(0).is_err());
272
1
    }
273
274
    proptest! {
275
        #[test]
276
        fn invalid_year_0(month in 1..12, day in 1..28) {
277
            let c = CommonDate::new(0, month as u8, day as u8);
278
            assert!(Julian::try_from_common_date(c).is_err())
279
        }
280
    }
281
}