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/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::Gregorian;
14
use crate::calendar::OrdinalDate;
15
use crate::calendar::ToFromOrdinalDate;
16
use crate::common::error::CalendarError;
17
use crate::common::math::TermNum;
18
use crate::day_count::BoundedDayCount;
19
use crate::day_count::CalculatedBounds;
20
use crate::day_count::Epoch;
21
use crate::day_count::Fixed;
22
use crate::day_count::FromFixed;
23
use crate::day_count::RataDie;
24
use crate::day_count::ToFixed;
25
use std::num::NonZero;
26
27
#[allow(unused_imports)] //FromPrimitive is needed for derive
28
use num_traits::FromPrimitive;
29
30
/// Represents a month in the Julian calendar
31
pub type JulianMonth = GregorianMonth;
32
33
//LISTING 3.2 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
34
//Instead of explicitly converting from Gregorian, just use the known Rata Die value.
35
const JULIAN_EPOCH_RD: i32 = -1;
36
37
/// Represents a date in the proleptic Julian calendar
38
///
39
/// ## Introduction
40
///
41
/// The Julian calendar is used by the Eastern Orthodox Church. Historically, it was used
42
/// by the Roman Empire, many later European states, and the colonies of those states.
43
///
44
/// The calendar is named after Julius Caesar who decreed that the calendar be used in the
45
/// Roman Empire, replacing the calendar used in the late Roman Republic. Caesar may have
46
/// been assisted by Sosigenes of Alexandria, according to Pliny. He may have also been
47
/// assisted by Marcus Flavius, according to Macrobius.
48
///
49
/// Over the past 500 years, the Julian calendar has been almost entirely replaced by the
50
/// Gregorian calendar.
51
///
52
/// ### Proleptic Modification
53
///
54
/// During the initial adoption of the Julian calendar in 45 Before Christ (BC), leap years
55
/// were every 3 years instead of every 4 years. According to Macrobius, this error was
56
/// introduced by Roman priests and had to be corrected by Augustus in 8 Anno Domini (AD).
57
/// (See the "Epoch" section for more details about BC, AD and AUC epoch labels).
58
///
59
/// According to Wikipedia:
60
/// > The proleptic Julian calendar is produced by extending the Julian calendar backwards
61
/// > to dates preceding AD 8 when the quadrennial leap year stabilized.
62
///
63
/// This crate implements a proleptic Julian calendar, and so does **not** change the leap year
64
/// rules for dates before 8 AD.
65
///
66
/// ### Year 0
67
///
68
/// Year 0 is **not** supported for this implementation of the Julian calendar.
69
/// The year before 1 is -1.
70
///
71
/// ## Basic Structure
72
///
73
/// Years are divided into 12 months. Every month has either 30 or 31 days except for the
74
/// second month, February. February has 28 days in a common year and 29 days in a leap year.
75
///
76
/// Leap years occur on every positive year divisible by 4, and every negative year before
77
/// a year divisible by 4.
78
///
79
/// (See [`Roman`](crate::calendar::Roman) for Roman names of days).
80
///
81
/// ## Epoch
82
///
83
/// Years are numbered based on an estimate of the date of birth of Jesus Christ. The estimate
84
/// was devised by Dionysius Exiguus 525 years after the birth supposedly happened.
85
///
86
/// The first year of the Julian calendar is called 1 Anno Domini (abbreviated "AD"), and the
87
/// year before that is called 1 Before Christ (abbreviated "BC").
88
///
89
/// ### Alternative Epochs
90
///
91
/// Before 525 AD (and for centuries after 525 AD) there were other epochs used with the Julian
92
/// calendar. One such epoch is "Ab urbe condita" (abbreviated "AUC"), based on the date of the
93
/// founding of Rome - see [`Roman`](crate::calendar::Roman) for more details.
94
///
95
/// Another method of identifying years was to name the consuls who held office that year. Regnal
96
/// years were also used in Roman Egypt and the Byzantine Empire.
97
///
98
/// ## Representation and Examples
99
///
100
/// The months are represented in this crate as [`JulianMonth`].
101
///
102
/// ```
103
/// use radnelac::calendar::*;
104
/// use radnelac::day_count::*;
105
///
106
/// let c_1_1 = CommonDate::new(2025, 1, 1);
107
/// let a_1_1 = Julian::try_from_common_date(c_1_1).unwrap();
108
/// assert_eq!(a_1_1.month(), JulianMonth::January);
109
/// ```
110
///
111
/// ### Conversion to Gregorian
112
///
113
/// For historical dates, it is often necessary to convert to the Gregorian system.
114
///
115
/// ```
116
/// use radnelac::calendar::*;
117
/// use radnelac::day_count::*;
118
///
119
/// let j = Julian::try_new(1752, JulianMonth::September, 3).unwrap();
120
/// let g = j.convert::<Gregorian>();
121
/// assert_eq!(g, Gregorian::try_new(1752, GregorianMonth::September, 14).unwrap());
122
/// ```
123
///
124
/// ## Inconsistencies with Other Implementations
125
///
126
/// Other systems may use non-proleptic Julian calendars. They might also allow year 0 for the
127
/// Julian calendar.
128
///
129
/// ## Further reading
130
/// + Wikipedia
131
///   + [Julian calendar](https://en.wikipedia.org/wiki/Julian_calendar)
132
///   + [Proleptic Julian calendar](https://en.m.wikipedia.org/wiki/Proleptic_Julian_calendar)
133
///   + [Ab urbe condita](https://en.m.wikipedia.org/wiki/Ab_urbe_condita)
134
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
135
pub struct Julian(CommonDate);
136
137
impl Julian {
138
0
    pub fn nz_year(self) -> NonZero<i32> {
139
0
        NonZero::new(self.0.year).expect("Will not be assigned zero")
140
0
    }
141
142
116k
    pub fn prior_elapsed_days(year: i32) -> i64 {
143
        //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
144
        //These are the terms which do not rely on the day or month
145
116k
        let y = if year < 0 { 
year + 113.6k
} else {
year102k
} as i64;
146
116k
        let offset_e = Julian::epoch().get_day_i() - 1;
147
116k
        let offset_y = 365 * (y - 1);
148
116k
        let offset_leap = (y - 1).div_euclid(4);
149
116k
        offset_e + offset_y + offset_leap
150
116k
    }
151
}
152
153
impl ToFromOrdinalDate for Julian {
154
1.28k
    fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
155
1.28k
        let correction = if Julian::is_leap(ord.year) { 
1356
} else {
0924
};
156
1.28k
        if ord.day_of_year > 0 && 
ord.day_of_year <= (365 + correction)1.02k
{
157
586
            Ok(())
158
        } else {
159
694
            Err(CalendarError::InvalidDayOfYear)
160
        }
161
1.28k
    }
162
163
14.8k
    fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
164
        //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
165
        //These are the calculations except for correction, month and day
166
14.8k
        let date = fixed_date.get_day_i();
167
14.8k
        let epoch = Julian::epoch().get_day_i();
168
14.8k
        let approx = ((4 * (date - epoch)) + 1464).div_euclid(1461);
169
14.8k
        let year = if approx <= 0 { 
approx - 17.51k
} else {
approx7.36k
} as i32;
170
14.8k
        let year_start = Julian(CommonDate::new(year, 1, 1)).to_fixed().get_day_i();
171
14.8k
        let prior_days = (date - year_start) as u16;
172
14.8k
        OrdinalDate {
173
14.8k
            year: year,
174
14.8k
            day_of_year: prior_days + 1,
175
14.8k
        }
176
14.8k
    }
177
178
146k
    fn to_ordinal(self) -> OrdinalDate {
179
        //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
180
        //These are the terms which rely on the day or month
181
146k
        let year = self.0.year;
182
146k
        let month = self.0.month as i64;
183
146k
        let day = self.0.day as i64;
184
146k
        let offset_m = ((367 * month) - 362).div_euclid(12);
185
146k
        let offset_x = if month <= 2 {
186
20.2k
            0
187
126k
        } else if Julian::is_leap(year) {
188
100k
            -1
189
        } else {
190
25.8k
            -2
191
        };
192
146k
        let offset_d = day;
193
194
146k
        OrdinalDate {
195
146k
            year: year,
196
146k
            day_of_year: (offset_m + offset_x + offset_d) as u16,
197
146k
        }
198
146k
    }
199
200
14.1k
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
201
        //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
202
        //These are the calculations for correction, month and day
203
14.1k
        let year = ord.year;
204
14.1k
        let prior_days = ord.day_of_year - 1;
205
14.1k
        let march1 = Julian(CommonDate::new(year, 3, 1)).to_ordinal(); //Modification
206
14.1k
        let correction = if ord < march1 {
207
1.47k
            0
208
12.6k
        } else if Julian::is_leap(year) {
209
4.19k
            1
210
        } else {
211
8.44k
            2
212
        };
213
14.1k
        let month = (12 * (prior_days + correction) + 373).div_euclid(367) as u8;
214
14.1k
        let month_start = Julian(CommonDate::new(year, month, 1)).to_ordinal();
215
14.1k
        let day = ((ord.day_of_year - month_start.day_of_year) as u8) + 1;
216
14.1k
        debug_assert!(day > 0);
217
14.1k
        Julian(CommonDate { year, month, day })
218
14.1k
    }
219
}
220
221
impl HasLeapYears for Julian {
222
145k
    fn is_leap(j_year: i32) -> bool {
223
        //LISTING 3.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
224
145k
        let m4 = j_year.modulus(4);
225
145k
        if j_year > 0 {
226
117k
            m4 == 0
227
        } else {
228
27.9k
            m4 == 3
229
        }
230
145k
    }
231
}
232
233
impl CalculatedBounds for Julian {}
234
235
impl Epoch for Julian {
236
131k
    fn epoch() -> Fixed {
237
131k
        RataDie::new(JULIAN_EPOCH_RD as f64).to_fixed()
238
131k
    }
239
}
240
241
impl FromFixed for Julian {
242
13.8k
    fn from_fixed(fixed_date: Fixed) -> Julian {
243
        //LISTING 3.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
244
        //Split compared to original
245
13.8k
        let ord = Self::ordinal_from_fixed(fixed_date);
246
13.8k
        Self::from_ordinal_unchecked(ord)
247
13.8k
    }
248
}
249
250
impl ToFixed for Julian {
251
116k
    fn to_fixed(self) -> Fixed {
252
        //LISTING 3.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
253
        //Split compared to original
254
116k
        let offset_prior = Julian::prior_elapsed_days(self.0.year);
255
116k
        let ord = self.to_ordinal();
256
116k
        Fixed::cast_new(offset_prior + (ord.day_of_year as i64))
257
116k
    }
258
}
259
260
impl ToFromCommonDate<JulianMonth> for Julian {
261
18.1k
    fn to_common_date(self) -> CommonDate {
262
18.1k
        self.0
263
18.1k
    }
264
265
104k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
266
104k
        debug_assert!(Self::valid_ymd(date).is_ok());
267
104k
        Self(date)
268
104k
    }
269
270
211k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
271
211k
        let month_opt = JulianMonth::from_u8(date.month);
272
211k
        if month_opt.is_none() {
273
768
            Err(CalendarError::InvalidMonth)
274
210k
        } else if date.day < 1 {
275
256
            Err(CalendarError::InvalidDay)
276
210k
        } else if date.day > Julian::month_length(date.year, month_opt.unwrap()) {
277
256
            Err(CalendarError::InvalidDay)
278
210k
        } else if date.year == 0 {
279
258
            Err(CalendarError::InvalidYear)
280
        } else {
281
209k
            Ok(())
282
        }
283
211k
    }
284
285
260
    fn year_end_date(year: i32) -> CommonDate {
286
260
        let m = JulianMonth::December;
287
260
        CommonDate::new(year, m as u8, Julian::month_length(year, m))
288
260
    }
289
290
210k
    fn month_length(year: i32, month: JulianMonth) -> u8 {
291
        //LISTING ?? SECTION 2.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
292
        //TODO: use listing 2.1 here?
293
210k
        match month {
294
            JulianMonth::February => {
295
1.71k
                if Julian::is_leap(year) {
296
443
                    29
297
                } else {
298
1.27k
                    28
299
                }
300
            }
301
209k
            _ => Gregorian::month_length(year, month),
302
        }
303
210k
    }
304
}
305
306
impl Quarter for Julian {
307
2.56k
    fn quarter(self) -> NonZero<u8> {
308
2.56k
        NonZero::new(((self.to_common_date().month - 1) / 3) + 1).expect("(m-1)/3 > -1")
309
2.56k
    }
310
}
311
312
impl GuaranteedMonth<JulianMonth> for Julian {}
313
impl CommonWeekOfYear<JulianMonth> for Julian {}
314
315
/// Represents a date *and time* in the Julian Calendar
316
pub type JulianMoment = CalendarMoment<Julian>;
317
318
#[cfg(test)]
319
mod tests {
320
    use super::*;
321
    use crate::calendar::gregorian::Gregorian;
322
    use proptest::proptest;
323
324
    #[test]
325
1
    fn julian_gregorian_conversion() {
326
1
        let gap_list = [
327
1
            // Official dates of adopting the Gregorian calendar
328
1
            // Governments would declare that certain days would be skipped
329
1
            // The table below lists Julian dates and the Gregorian dates of the next day.
330
1
            // https://en.wikipedia.org/wiki/Adoption_of_the_Gregorian_calendar
331
1
            // https://en.wikipedia.org/wiki/List_of_adoption_dates_of_the_Gregorian_calendar_by_country
332
1
            (CommonDate::new(1582, 10, 4), CommonDate::new(1582, 10, 15)), //Papal States, Spain, Portugal
333
1
            (CommonDate::new(1582, 12, 9), CommonDate::new(1582, 12, 20)), //France
334
1
            (CommonDate::new(1582, 12, 14), CommonDate::new(1582, 12, 25)), //"Flanders" (Belgium), Netherlands
335
1
            (CommonDate::new(1582, 12, 20), CommonDate::new(1582, 12, 31)), //"Southern Netherlands" (Belgium), Luxembourg
336
1
            (CommonDate::new(1582, 12, 31), CommonDate::new(1583, 1, 11)),  //"Aachen" (Germany)
337
1
            (CommonDate::new(1583, 1, 1), CommonDate::new(1583, 1, 12)), //"Holland" (Netherlands)
338
1
            (CommonDate::new(1583, 2, 10), CommonDate::new(1583, 2, 21)), //"Salzburg" (Austria), "Liege" (Belgium)
339
1
            (CommonDate::new(1583, 2, 13), CommonDate::new(1583, 2, 24)), //"Kaufbeuren" (Germany)
340
1
            (CommonDate::new(1583, 2, 14), CommonDate::new(1583, 2, 25)), //"Ellwangen" (Germany)
341
1
            (CommonDate::new(1583, 3, 1), CommonDate::new(1583, 3, 12)), //"Groningen" (Netherlands)
342
1
            (CommonDate::new(1583, 10, 4), CommonDate::new(1583, 10, 15)), //"Tyrol" (Austria)
343
1
            (CommonDate::new(1583, 10, 5), CommonDate::new(1583, 10, 16)), //"Bavaria" (Germany)
344
1
            (CommonDate::new(1583, 10, 13), CommonDate::new(1583, 10, 24)), //"Austrian Upper Alsace" (France)
345
1
            (CommonDate::new(1583, 10, 20), CommonDate::new(1583, 10, 31)), //"Lower Austria" (Austria)
346
1
            (CommonDate::new(1583, 11, 2), CommonDate::new(1583, 11, 13)),  //"Cologne" (Germany)
347
1
            (CommonDate::new(1583, 11, 11), CommonDate::new(1583, 11, 22)), //"Mainz" (Germany)
348
1
            (CommonDate::new(1632, 12, 14), CommonDate::new(1632, 12, 25)), //"Hildesheim" (Germany)
349
1
            (CommonDate::new(1700, 2, 18), CommonDate::new(1700, 3, 1)), //"Denmark-Norway" (Denmark, Norway)
350
1
            (CommonDate::new(1753, 2, 17), CommonDate::new(1753, 3, 1)), //Sweden (partial?)
351
1
            (CommonDate::new(1752, 9, 2), CommonDate::new(1752, 9, 14)), //British Empire (United Kingdom, Ireland, Canada, United States)
352
1
            (CommonDate::new(1753, 2, 17), CommonDate::new(1753, 3, 1)), //Sweden
353
1
            (CommonDate::new(1912, 11, 14), CommonDate::new(1912, 11, 28)), //Albania
354
1
            (CommonDate::new(1916, 3, 31), CommonDate::new(1916, 4, 14)), //Bulgaria
355
1
            (CommonDate::new(1918, 1, 31), CommonDate::new(1918, 2, 14)), //Soviet Union (Russia, etc.)
356
1
            (CommonDate::new(1918, 2, 15), CommonDate::new(1918, 3, 1)),  //Estonia, Ukraine
357
1
            (CommonDate::new(1918, 4, 17), CommonDate::new(1918, 5, 1)), //"Transcaucasian Democratic Federative Republic"
358
1
            (CommonDate::new(1919, 1, 14), CommonDate::new(1919, 1, 28)), //Yugoslavia
359
1
            (CommonDate::new(1919, 3, 31), CommonDate::new(1919, 4, 14)), //Romania
360
1
            (CommonDate::new(1923, 2, 15), CommonDate::new(1923, 3, 1)), //Greece
361
1
        ];
362
363
30
        for 
pair29
in gap_list {
364
29
            let dj = Julian::try_from_common_date(pair.0).unwrap().to_fixed();
365
29
            let dg = Gregorian::try_from_common_date(pair.1).unwrap().to_fixed();
366
29
            assert_eq!(dj.get_day_i() + 1, dg.get_day_i());
367
        }
368
1
    }
369
370
    #[test]
371
1
    fn cross_epoch() {
372
1
        let new_years_eve = Julian::try_year_end(-1).unwrap().to_fixed();
373
1
        let new_years_day = Julian::try_year_start(1).unwrap().to_fixed();
374
1
        assert_eq!(new_years_day.get_day_i(), new_years_eve.get_day_i() + 1);
375
1
        assert!(Julian::try_year_start(0).is_err());
376
1
        assert!(Julian::try_year_end(0).is_err());
377
1
    }
378
379
    proptest! {
380
        #[test]
381
        fn invalid_year_0(month in 1..12, day in 1..28) {
382
            let c = CommonDate::new(0, month as u8, day as u8);
383
            assert!(Julian::try_from_common_date(c).is_err())
384
        }
385
    }
386
}