Coverage Report

Created: 2025-10-13 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/icu_calendar-1.5.2/src/julian.rs
Line
Count
Source
1
// This file is part of ICU4X. For terms of use, please see the file
2
// called LICENSE at the top level of the ICU4X source tree
3
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5
//! This module contains types and implementations for the Julian calendar.
6
//!
7
//! ```rust
8
//! use icu::calendar::{julian::Julian, Date, DateTime};
9
//!
10
//! // `Date` type
11
//! let date_iso = Date::try_new_iso_date(1970, 1, 2)
12
//!     .expect("Failed to initialize ISO Date instance.");
13
//! let date_julian = Date::new_from_iso(date_iso, Julian);
14
//!
15
//! // `DateTime` type
16
//! let datetime_iso = DateTime::try_new_iso_datetime(1970, 1, 2, 13, 1, 0)
17
//!     .expect("Failed to initialize ISO DateTime instance.");
18
//! let datetime_julian = DateTime::new_from_iso(datetime_iso, Julian);
19
//!
20
//! // `Date` checks
21
//! assert_eq!(date_julian.year().number, 1969);
22
//! assert_eq!(date_julian.month().ordinal, 12);
23
//! assert_eq!(date_julian.day_of_month().0, 20);
24
//!
25
//! // `DateTime` type
26
//! assert_eq!(datetime_julian.date.year().number, 1969);
27
//! assert_eq!(datetime_julian.date.month().ordinal, 12);
28
//! assert_eq!(datetime_julian.date.day_of_month().0, 20);
29
//! assert_eq!(datetime_julian.time.hour.number(), 13);
30
//! assert_eq!(datetime_julian.time.minute.number(), 1);
31
//! assert_eq!(datetime_julian.time.second.number(), 0);
32
//! ```
33
34
use crate::any_calendar::AnyCalendarKind;
35
use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
36
use crate::gregorian::year_as_gregorian;
37
use crate::iso::Iso;
38
use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time};
39
use calendrical_calculations::helpers::I32CastError;
40
use calendrical_calculations::rata_die::RataDie;
41
use tinystr::tinystr;
42
43
/// The [Julian Calendar]
44
///
45
/// The [Julian calendar] is a solar calendar that was used commonly historically, with twelve months.
46
///
47
/// This type can be used with [`Date`] or [`DateTime`] to represent dates in this calendar.
48
///
49
/// [Julian calendar]: https://en.wikipedia.org/wiki/Julian_calendar
50
///
51
/// # Era codes
52
///
53
/// This calendar supports two era codes: `"bce"`, and `"ce"`, corresponding to the BCE/BC and CE/AD eras
54
///
55
/// # Month codes
56
///
57
/// This calendar supports 12 solar month codes (`"M01" - "M12"`)
58
#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
59
#[allow(clippy::exhaustive_structs)] // this type is stable
60
pub struct Julian;
61
62
/// The inner date type used for representing [`Date`]s of [`Julian`]. See [`Date`] and [`Julian`] for more details.
63
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
64
// The inner date type used for representing Date<Julian>
65
pub struct JulianDateInner(pub(crate) ArithmeticDate<Julian>);
66
67
impl CalendarArithmetic for Julian {
68
    type YearInfo = ();
69
70
0
    fn month_days(year: i32, month: u8, _data: ()) -> u8 {
71
0
        match month {
72
0
            4 | 6 | 9 | 11 => 30,
73
0
            2 if Self::is_leap_year(year, ()) => 29,
74
0
            2 => 28,
75
0
            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
76
0
            _ => 0,
77
        }
78
0
    }
79
80
0
    fn months_for_every_year(_: i32, _data: ()) -> u8 {
81
0
        12
82
0
    }
83
84
0
    fn is_leap_year(year: i32, _data: ()) -> bool {
85
0
        calendrical_calculations::julian::is_leap_year(year)
86
0
    }
87
88
0
    fn last_month_day_in_year(_year: i32, _data: ()) -> (u8, u8) {
89
0
        (12, 31)
90
0
    }
91
92
0
    fn days_in_provided_year(year: i32, _data: ()) -> u16 {
93
0
        if Self::is_leap_year(year, ()) {
94
0
            366
95
        } else {
96
0
            365
97
        }
98
0
    }
99
}
100
101
impl Calendar for Julian {
102
    type DateInner = JulianDateInner;
103
0
    fn date_from_codes(
104
0
        &self,
105
0
        era: types::Era,
106
0
        year: i32,
107
0
        month_code: types::MonthCode,
108
0
        day: u8,
109
0
    ) -> Result<Self::DateInner, CalendarError> {
110
0
        let year = if era.0 == tinystr!(16, "ce") {
111
0
            if year <= 0 {
112
0
                return Err(CalendarError::OutOfRange);
113
0
            }
114
0
            year
115
0
        } else if era.0 == tinystr!(16, "bce") {
116
0
            if year <= 0 {
117
0
                return Err(CalendarError::OutOfRange);
118
0
            }
119
0
            1 - year
120
        } else {
121
0
            return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
122
        };
123
124
0
        ArithmeticDate::new_from_codes(self, year, month_code, day).map(JulianDateInner)
125
0
    }
126
0
    fn date_from_iso(&self, iso: Date<Iso>) -> JulianDateInner {
127
0
        let fixed_iso = Iso::fixed_from_iso(*iso.inner());
128
0
        Self::julian_from_fixed(fixed_iso)
129
0
    }
130
131
0
    fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
132
0
        let fixed_julian = Julian::fixed_from_julian(date.0);
133
0
        Iso::iso_from_fixed(fixed_julian)
134
0
    }
135
136
0
    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
137
0
        date.0.months_in_year()
138
0
    }
139
140
0
    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
141
0
        date.0.days_in_year()
142
0
    }
143
144
0
    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
145
0
        date.0.days_in_month()
146
0
    }
147
148
0
    fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {
149
0
        Iso.day_of_week(Julian.date_to_iso(date).inner())
150
0
    }
151
152
0
    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
153
0
        date.0.offset_date(offset, &());
154
0
    }
155
156
    #[allow(clippy::field_reassign_with_default)]
157
0
    fn until(
158
0
        &self,
159
0
        date1: &Self::DateInner,
160
0
        date2: &Self::DateInner,
161
0
        _calendar2: &Self,
162
0
        _largest_unit: DateDurationUnit,
163
0
        _smallest_unit: DateDurationUnit,
164
0
    ) -> DateDuration<Self> {
165
0
        date1.0.until(date2.0, _largest_unit, _smallest_unit)
166
0
    }
167
168
    /// The calendar-specific year represented by `date`
169
    /// Julian has the same era scheme as Gregorian
170
0
    fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
171
0
        year_as_gregorian(date.0.year)
172
0
    }
173
174
0
    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
175
0
        Self::is_leap_year(date.0.year, ())
176
0
    }
177
178
    /// The calendar-specific month represented by `date`
179
0
    fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
180
0
        date.0.month()
181
0
    }
182
183
    /// The calendar-specific day-of-month represented by `date`
184
0
    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
185
0
        date.0.day_of_month()
186
0
    }
187
188
0
    fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
189
0
        let prev_year = date.0.year - 1;
190
0
        let next_year = date.0.year + 1;
191
0
        types::DayOfYearInfo {
192
0
            day_of_year: date.0.day_of_year(),
193
0
            days_in_year: date.0.days_in_year(),
194
0
            prev_year: crate::gregorian::year_as_gregorian(prev_year),
195
0
            days_in_prev_year: Julian::days_in_year_direct(prev_year),
196
0
            next_year: crate::gregorian::year_as_gregorian(next_year),
197
0
        }
198
0
    }
199
200
0
    fn debug_name(&self) -> &'static str {
201
0
        "Julian"
202
0
    }
203
204
0
    fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
205
0
        None
206
0
    }
207
}
208
209
impl Julian {
210
    /// Construct a new Julian Calendar
211
0
    pub fn new() -> Self {
212
0
        Self
213
0
    }
214
215
    // "Fixed" is a day count representation of calendars staring from Jan 1st of year 1 of the Georgian Calendar.
216
0
    pub(crate) const fn fixed_from_julian(date: ArithmeticDate<Julian>) -> RataDie {
217
0
        calendrical_calculations::julian::fixed_from_julian(date.year, date.month, date.day)
218
0
    }
219
220
    /// Convenience function so we can call days_in_year without
221
    /// needing to construct a full ArithmeticDate
222
0
    fn days_in_year_direct(year: i32) -> u16 {
223
0
        if Julian::is_leap_year(year, ()) {
224
0
            366
225
        } else {
226
0
            365
227
        }
228
0
    }
229
230
0
    fn julian_from_fixed(date: RataDie) -> JulianDateInner {
231
0
        let (year, month, day) = match calendrical_calculations::julian::julian_from_fixed(date) {
232
0
            Err(I32CastError::BelowMin) => return JulianDateInner(ArithmeticDate::min_date()),
233
0
            Err(I32CastError::AboveMax) => return JulianDateInner(ArithmeticDate::max_date()),
234
0
            Ok(ymd) => ymd,
235
        };
236
0
        JulianDateInner(ArithmeticDate::new_unchecked(year, month, day))
237
0
    }
238
}
239
240
impl Date<Julian> {
241
    /// Construct new Julian Date.
242
    ///
243
    /// Years are arithmetic, meaning there is a year 0. Zero and negative years are in BC, with year 0 = 1 BC
244
    ///
245
    /// ```rust
246
    /// use icu::calendar::Date;
247
    ///
248
    /// let date_julian = Date::try_new_julian_date(1969, 12, 20)
249
    ///     .expect("Failed to initialize Julian Date instance.");
250
    ///
251
    /// assert_eq!(date_julian.year().number, 1969);
252
    /// assert_eq!(date_julian.month().ordinal, 12);
253
    /// assert_eq!(date_julian.day_of_month().0, 20);
254
    /// ```
255
0
    pub fn try_new_julian_date(
256
0
        year: i32,
257
0
        month: u8,
258
0
        day: u8,
259
0
    ) -> Result<Date<Julian>, CalendarError> {
260
0
        ArithmeticDate::new_from_ordinals(year, month, day)
261
0
            .map(JulianDateInner)
262
0
            .map(|inner| Date::from_raw(inner, Julian))
263
0
    }
264
}
265
266
impl DateTime<Julian> {
267
    /// Construct a new Julian datetime from integers.
268
    ///
269
    /// Years are arithmetic, meaning there is a year 0. Zero and negative years are in BC, with year 0 = 1 BC
270
    ///
271
    /// ```rust
272
    /// use icu::calendar::DateTime;
273
    ///
274
    /// let datetime_julian =
275
    ///     DateTime::try_new_julian_datetime(1969, 12, 20, 13, 1, 0)
276
    ///         .expect("Failed to initialize Julian DateTime instance.");
277
    ///
278
    /// assert_eq!(datetime_julian.date.year().number, 1969);
279
    /// assert_eq!(datetime_julian.date.month().ordinal, 12);
280
    /// assert_eq!(datetime_julian.date.day_of_month().0, 20);
281
    /// assert_eq!(datetime_julian.time.hour.number(), 13);
282
    /// assert_eq!(datetime_julian.time.minute.number(), 1);
283
    /// assert_eq!(datetime_julian.time.second.number(), 0);
284
    /// ```
285
0
    pub fn try_new_julian_datetime(
286
0
        year: i32,
287
0
        month: u8,
288
0
        day: u8,
289
0
        hour: u8,
290
0
        minute: u8,
291
0
        second: u8,
292
0
    ) -> Result<DateTime<Julian>, CalendarError> {
293
        Ok(DateTime {
294
0
            date: Date::try_new_julian_date(year, month, day)?,
295
0
            time: Time::try_new(hour, minute, second, 0)?,
296
        })
297
0
    }
298
}
299
300
#[cfg(test)]
301
mod test {
302
    use super::*;
303
    use types::Era;
304
305
    #[test]
306
    fn test_day_iso_to_julian() {
307
        // March 1st 200 is same on both calendars
308
        let iso_date = Date::try_new_iso_date(200, 3, 1).unwrap();
309
        let julian_date = Julian.date_from_iso(iso_date);
310
        assert_eq!(julian_date.0.year, 200);
311
        assert_eq!(julian_date.0.month, 3);
312
        assert_eq!(julian_date.0.day, 1);
313
314
        // Feb 28th, 200 (iso) = Feb 29th, 200 (julian)
315
        let iso_date = Date::try_new_iso_date(200, 2, 28).unwrap();
316
        let julian_date = Julian.date_from_iso(iso_date);
317
        assert_eq!(julian_date.0.year, 200);
318
        assert_eq!(julian_date.0.month, 2);
319
        assert_eq!(julian_date.0.day, 29);
320
321
        // March 1st 400 (iso) = Feb 29th, 400 (julian)
322
        let iso_date = Date::try_new_iso_date(400, 3, 1).unwrap();
323
        let julian_date = Julian.date_from_iso(iso_date);
324
        assert_eq!(julian_date.0.year, 400);
325
        assert_eq!(julian_date.0.month, 2);
326
        assert_eq!(julian_date.0.day, 29);
327
328
        // Jan 1st, 2022 (iso) = Dec 19, 2021 (julian)
329
        let iso_date = Date::try_new_iso_date(2022, 1, 1).unwrap();
330
        let julian_date = Julian.date_from_iso(iso_date);
331
        assert_eq!(julian_date.0.year, 2021);
332
        assert_eq!(julian_date.0.month, 12);
333
        assert_eq!(julian_date.0.day, 19);
334
    }
335
336
    #[test]
337
    fn test_day_julian_to_iso() {
338
        // March 1st 200 is same on both calendars
339
        let julian_date = Date::try_new_julian_date(200, 3, 1).unwrap();
340
        let iso_date = Julian.date_to_iso(julian_date.inner());
341
        let iso_expected_date = Date::try_new_iso_date(200, 3, 1).unwrap();
342
        assert_eq!(iso_date, iso_expected_date);
343
344
        // Feb 28th, 200 (iso) = Feb 29th, 200 (julian)
345
        let julian_date = Date::try_new_julian_date(200, 2, 29).unwrap();
346
        let iso_date = Julian.date_to_iso(julian_date.inner());
347
        let iso_expected_date = Date::try_new_iso_date(200, 2, 28).unwrap();
348
        assert_eq!(iso_date, iso_expected_date);
349
350
        // March 1st 400 (iso) = Feb 29th, 400 (julian)
351
        let julian_date = Date::try_new_julian_date(400, 2, 29).unwrap();
352
        let iso_date = Julian.date_to_iso(julian_date.inner());
353
        let iso_expected_date = Date::try_new_iso_date(400, 3, 1).unwrap();
354
        assert_eq!(iso_date, iso_expected_date);
355
356
        // Jan 1st, 2022 (iso) = Dec 19, 2021 (julian)
357
        let julian_date = Date::try_new_julian_date(2021, 12, 19).unwrap();
358
        let iso_date = Julian.date_to_iso(julian_date.inner());
359
        let iso_expected_date = Date::try_new_iso_date(2022, 1, 1).unwrap();
360
        assert_eq!(iso_date, iso_expected_date);
361
362
        // March 1st, 2022 (iso) = Feb 16, 2022 (julian)
363
        let julian_date = Date::try_new_julian_date(2022, 2, 16).unwrap();
364
        let iso_date = Julian.date_to_iso(julian_date.inner());
365
        let iso_expected_date = Date::try_new_iso_date(2022, 3, 1).unwrap();
366
        assert_eq!(iso_date, iso_expected_date);
367
    }
368
369
    #[test]
370
    fn test_roundtrip_negative() {
371
        // https://github.com/unicode-org/icu4x/issues/2254
372
        let iso_date = Date::try_new_iso_date(-1000, 3, 3).unwrap();
373
        let julian = iso_date.to_calendar(Julian::new());
374
        let recovered_iso = julian.to_iso();
375
        assert_eq!(iso_date, recovered_iso);
376
    }
377
378
    #[test]
379
    fn test_julian_near_era_change() {
380
        // Tests that the Julian calendar gives the correct expected
381
        // day, month, and year for positive years (CE)
382
383
        #[derive(Debug)]
384
        struct TestCase {
385
            fixed_date: i64,
386
            iso_year: i32,
387
            iso_month: u8,
388
            iso_day: u8,
389
            expected_year: i32,
390
            expected_era: Era,
391
            expected_month: u32,
392
            expected_day: u32,
393
        }
394
395
        let cases = [
396
            TestCase {
397
                fixed_date: 1,
398
                iso_year: 1,
399
                iso_month: 1,
400
                iso_day: 1,
401
                expected_year: 1,
402
                expected_era: Era(tinystr!(16, "ce")),
403
                expected_month: 1,
404
                expected_day: 3,
405
            },
406
            TestCase {
407
                fixed_date: 0,
408
                iso_year: 0,
409
                iso_month: 12,
410
                iso_day: 31,
411
                expected_year: 1,
412
                expected_era: Era(tinystr!(16, "ce")),
413
                expected_month: 1,
414
                expected_day: 2,
415
            },
416
            TestCase {
417
                fixed_date: -1,
418
                iso_year: 0,
419
                iso_month: 12,
420
                iso_day: 30,
421
                expected_year: 1,
422
                expected_era: Era(tinystr!(16, "ce")),
423
                expected_month: 1,
424
                expected_day: 1,
425
            },
426
            TestCase {
427
                fixed_date: -2,
428
                iso_year: 0,
429
                iso_month: 12,
430
                iso_day: 29,
431
                expected_year: 1,
432
                expected_era: Era(tinystr!(16, "bce")),
433
                expected_month: 12,
434
                expected_day: 31,
435
            },
436
            TestCase {
437
                fixed_date: -3,
438
                iso_year: 0,
439
                iso_month: 12,
440
                iso_day: 28,
441
                expected_year: 1,
442
                expected_era: Era(tinystr!(16, "bce")),
443
                expected_month: 12,
444
                expected_day: 30,
445
            },
446
            TestCase {
447
                fixed_date: -367,
448
                iso_year: -1,
449
                iso_month: 12,
450
                iso_day: 30,
451
                expected_year: 1,
452
                expected_era: Era(tinystr!(16, "bce")),
453
                expected_month: 1,
454
                expected_day: 1,
455
            },
456
            TestCase {
457
                fixed_date: -368,
458
                iso_year: -1,
459
                iso_month: 12,
460
                iso_day: 29,
461
                expected_year: 2,
462
                expected_era: Era(tinystr!(16, "bce")),
463
                expected_month: 12,
464
                expected_day: 31,
465
            },
466
            TestCase {
467
                fixed_date: -1462,
468
                iso_year: -4,
469
                iso_month: 12,
470
                iso_day: 30,
471
                expected_year: 4,
472
                expected_era: Era(tinystr!(16, "bce")),
473
                expected_month: 1,
474
                expected_day: 1,
475
            },
476
            TestCase {
477
                fixed_date: -1463,
478
                iso_year: -4,
479
                iso_month: 12,
480
                iso_day: 29,
481
                expected_year: 5,
482
                expected_era: Era(tinystr!(16, "bce")),
483
                expected_month: 12,
484
                expected_day: 31,
485
            },
486
        ];
487
488
        for case in cases {
489
            let iso_from_fixed: Date<Iso> = Iso::iso_from_fixed(RataDie::new(case.fixed_date));
490
            let julian_from_fixed: Date<Julian> = Date::new_from_iso(iso_from_fixed, Julian);
491
            assert_eq!(julian_from_fixed.year().number, case.expected_year,
492
                "Failed year check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nJulian: {julian_from_fixed:?}");
493
            assert_eq!(julian_from_fixed.year().era, case.expected_era,
494
                "Failed era check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nJulian: {julian_from_fixed:?}");
495
            assert_eq!(julian_from_fixed.month().ordinal, case.expected_month,
496
                "Failed month check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nJulian: {julian_from_fixed:?}");
497
            assert_eq!(julian_from_fixed.day_of_month().0, case.expected_day,
498
                "Failed day check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nJulian: {julian_from_fixed:?}");
499
500
            let iso_date_man: Date<Iso> =
501
                Date::try_new_iso_date(case.iso_year, case.iso_month, case.iso_day)
502
                    .expect("Failed to initialize ISO date for {case:?}");
503
            let julian_date_man: Date<Julian> = Date::new_from_iso(iso_date_man, Julian);
504
            assert_eq!(iso_from_fixed, iso_date_man,
505
                "ISO from fixed not equal to ISO generated from manually-input ymd\nCase: {case:?}\nFixed: {iso_from_fixed:?}\nMan: {iso_date_man:?}");
506
            assert_eq!(julian_from_fixed, julian_date_man,
507
                "Julian from fixed not equal to Julian generated from manually-input ymd\nCase: {case:?}\nFixed: {julian_from_fixed:?}\nMan: {julian_date_man:?}");
508
        }
509
    }
510
511
    #[test]
512
    fn test_julian_fixed_date_conversion() {
513
        // Tests that converting from fixed date to Julian then
514
        // back to fixed date yields the same fixed date
515
        for i in -10000..=10000 {
516
            let fixed = RataDie::new(i);
517
            let julian = Julian::julian_from_fixed(fixed);
518
            let new_fixed = Julian::fixed_from_julian(julian.0);
519
            assert_eq!(fixed, new_fixed);
520
        }
521
    }
522
523
    #[test]
524
    fn test_julian_directionality() {
525
        // Tests that for a large range of fixed dates, if a fixed date
526
        // is less than another, the corresponding YMD should also be less
527
        // than the other, without exception.
528
        for i in -100..=100 {
529
            for j in -100..=100 {
530
                let julian_i = Julian::julian_from_fixed(RataDie::new(i)).0;
531
                let julian_j = Julian::julian_from_fixed(RataDie::new(j)).0;
532
533
                assert_eq!(
534
                    i.cmp(&j),
535
                    julian_i.cmp(&julian_j),
536
                    "Julian directionality inconsistent with directionality for i: {i}, j: {j}"
537
                );
538
            }
539
        }
540
    }
541
542
    #[test]
543
    fn test_hebrew_epoch() {
544
        assert_eq!(
545
            calendrical_calculations::julian::fixed_from_julian_book_version(-3761, 10, 7),
546
            RataDie::new(-1373427)
547
        );
548
    }
549
550
    #[test]
551
    fn test_julian_leap_years() {
552
        assert!(Julian::is_leap_year(4, ()));
553
        assert!(Julian::is_leap_year(0, ()));
554
        assert!(Julian::is_leap_year(-4, ()));
555
556
        Date::try_new_julian_date(2020, 2, 29).unwrap();
557
    }
558
}