icu_calendar/cal/
julian.rs

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::{cal::Julian, Date};
9//!
10//! let date_iso = Date::try_new_iso(1970, 1, 2)
11//!     .expect("Failed to initialize ISO Date instance.");
12//! let date_julian = Date::new_from_iso(date_iso, Julian);
13//!
14//! assert_eq!(date_julian.era_year().year, 1969);
15//! assert_eq!(date_julian.month().ordinal, 12);
16//! assert_eq!(date_julian.day_of_month().0, 20);
17//! ```
18
19use crate::cal::iso::{Iso, IsoDateInner};
20use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
21use crate::error::{year_check, DateError};
22use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
23use calendrical_calculations::helpers::I32CastError;
24use calendrical_calculations::rata_die::RataDie;
25use tinystr::tinystr;
26
27/// The [Julian Calendar]
28///
29/// The [Julian calendar] is a solar calendar that was used commonly historically, with twelve months.
30///
31/// This type can be used with [`Date`] to represent dates in this calendar.
32///
33/// [Julian calendar]: https://en.wikipedia.org/wiki/Julian_calendar
34///
35/// # Era codes
36///
37/// This calendar uses two era codes: `bce` (alias `bc`), and `ce` (alias `ad`), corresponding to the BCE and CE eras.
38///
39/// # Month codes
40///
41/// This calendar supports 12 solar month codes (`"M01" - "M12"`)
42#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
43#[allow(clippy::exhaustive_structs)] // this type is stable
44pub struct Julian;
45
46/// The inner date type used for representing [`Date`]s of [`Julian`]. See [`Date`] and [`Julian`] for more details.
47#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
48// The inner date type used for representing Date<Julian>
49pub struct JulianDateInner(pub(crate) ArithmeticDate<Julian>);
50
51impl CalendarArithmetic for Julian {
52    type YearInfo = i32;
53
54    fn days_in_provided_month(year: i32, month: u8) -> u8 {
55        match month {
56            4 | 6 | 9 | 11 => 30,
57            2 if Self::provided_year_is_leap(year) => 29,
58            2 => 28,
59            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
60            _ => 0,
61        }
62    }
63
64    fn months_in_provided_year(_: i32) -> u8 {
65        12
66    }
67
68    fn provided_year_is_leap(year: i32) -> bool {
69        calendrical_calculations::julian::is_leap_year(year)
70    }
71
72    fn last_month_day_in_provided_year(_year: i32) -> (u8, u8) {
73        (12, 31)
74    }
75
76    fn days_in_provided_year(year: i32) -> u16 {
77        if Self::provided_year_is_leap(year) {
78            366
79        } else {
80            365
81        }
82    }
83}
84
85impl crate::cal::scaffold::UnstableSealed for Julian {}
86impl Calendar for Julian {
87    type DateInner = JulianDateInner;
88    type Year = types::EraYear;
89
90    fn from_codes(
91        &self,
92        era: Option<&str>,
93        year: i32,
94        month_code: types::MonthCode,
95        day: u8,
96    ) -> Result<Self::DateInner, DateError> {
97        let year = match era {
98            Some("ce" | "ad") | None => year_check(year, 1..)?,
99            Some("bce" | "bc") => 1 - year_check(year, 1..)?,
100            Some(_) => return Err(DateError::UnknownEra),
101        };
102
103        ArithmeticDate::new_from_codes(self, year, month_code, day).map(JulianDateInner)
104    }
105
106    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
107        JulianDateInner(
108            match calendrical_calculations::julian::julian_from_fixed(rd) {
109                Err(I32CastError::BelowMin) => ArithmeticDate::min_date(),
110                Err(I32CastError::AboveMax) => ArithmeticDate::max_date(),
111                Ok((year, month, day)) => ArithmeticDate::new_unchecked(year, month, day),
112            },
113        )
114    }
115
116    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
117        calendrical_calculations::julian::fixed_from_julian(date.0.year, date.0.month, date.0.day)
118    }
119
120    fn from_iso(&self, iso: IsoDateInner) -> JulianDateInner {
121        self.from_rata_die(Iso.to_rata_die(&iso))
122    }
123
124    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
125        Iso.from_rata_die(self.to_rata_die(date))
126    }
127
128    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
129        date.0.months_in_year()
130    }
131
132    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
133        date.0.days_in_year()
134    }
135
136    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
137        date.0.days_in_month()
138    }
139
140    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
141        date.0.offset_date(offset, &());
142    }
143
144    #[allow(clippy::field_reassign_with_default)]
145    fn until(
146        &self,
147        date1: &Self::DateInner,
148        date2: &Self::DateInner,
149        _calendar2: &Self,
150        _largest_unit: DateDurationUnit,
151        _smallest_unit: DateDurationUnit,
152    ) -> DateDuration<Self> {
153        date1.0.until(date2.0, _largest_unit, _smallest_unit)
154    }
155
156    /// The calendar-specific year represented by `date`
157    /// Julian has the same era scheme as Gregorian
158    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
159        let extended_year = self.extended_year(date);
160        if extended_year > 0 {
161            types::EraYear {
162                era: tinystr!(16, "ce"),
163                era_index: Some(1),
164                year: extended_year,
165                ambiguity: types::YearAmbiguity::CenturyRequired,
166            }
167        } else {
168            types::EraYear {
169                era: tinystr!(16, "bce"),
170                era_index: Some(0),
171                year: 1_i32.saturating_sub(extended_year),
172                ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
173            }
174        }
175    }
176
177    fn extended_year(&self, date: &Self::DateInner) -> i32 {
178        date.0.extended_year()
179    }
180
181    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
182        Self::provided_year_is_leap(date.0.year)
183    }
184
185    /// The calendar-specific month represented by `date`
186    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
187        date.0.month()
188    }
189
190    /// The calendar-specific day-of-month represented by `date`
191    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
192        date.0.day_of_month()
193    }
194
195    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
196        date.0.day_of_year()
197    }
198
199    fn debug_name(&self) -> &'static str {
200        "Julian"
201    }
202
203    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
204        None
205    }
206}
207
208impl Julian {
209    /// Construct a new Julian Calendar
210    pub fn new() -> Self {
211        Self
212    }
213}
214
215impl Date<Julian> {
216    /// Construct new Julian Date.
217    ///
218    /// Years are arithmetic, meaning there is a year 0. Zero and negative years are in BC, with year 0 = 1 BC
219    ///
220    /// ```rust
221    /// use icu::calendar::Date;
222    ///
223    /// let date_julian = Date::try_new_julian(1969, 12, 20)
224    ///     .expect("Failed to initialize Julian Date instance.");
225    ///
226    /// assert_eq!(date_julian.era_year().year, 1969);
227    /// assert_eq!(date_julian.month().ordinal, 12);
228    /// assert_eq!(date_julian.day_of_month().0, 20);
229    /// ```
230    pub fn try_new_julian(year: i32, month: u8, day: u8) -> Result<Date<Julian>, RangeError> {
231        ArithmeticDate::new_from_ordinals(year, month, day)
232            .map(JulianDateInner)
233            .map(|inner| Date::from_raw(inner, Julian))
234    }
235}
236
237#[cfg(test)]
238mod test {
239    use super::*;
240
241    #[test]
242    fn test_day_iso_to_julian() {
243        // March 1st 200 is same on both calendars
244        let iso_date = Date::try_new_iso(200, 3, 1).unwrap();
245        let julian_date = Date::new_from_iso(iso_date, Julian).inner;
246        assert_eq!(julian_date.0.year, 200);
247        assert_eq!(julian_date.0.month, 3);
248        assert_eq!(julian_date.0.day, 1);
249
250        // Feb 28th, 200 (iso) = Feb 29th, 200 (julian)
251        let iso_date = Date::try_new_iso(200, 2, 28).unwrap();
252        let julian_date = Date::new_from_iso(iso_date, Julian).inner;
253        assert_eq!(julian_date.0.year, 200);
254        assert_eq!(julian_date.0.month, 2);
255        assert_eq!(julian_date.0.day, 29);
256
257        // March 1st 400 (iso) = Feb 29th, 400 (julian)
258        let iso_date = Date::try_new_iso(400, 3, 1).unwrap();
259        let julian_date = Date::new_from_iso(iso_date, Julian).inner;
260        assert_eq!(julian_date.0.year, 400);
261        assert_eq!(julian_date.0.month, 2);
262        assert_eq!(julian_date.0.day, 29);
263
264        // Jan 1st, 2022 (iso) = Dec 19, 2021 (julian)
265        let iso_date = Date::try_new_iso(2022, 1, 1).unwrap();
266        let julian_date = Date::new_from_iso(iso_date, Julian).inner;
267        assert_eq!(julian_date.0.year, 2021);
268        assert_eq!(julian_date.0.month, 12);
269        assert_eq!(julian_date.0.day, 19);
270    }
271
272    #[test]
273    fn test_day_julian_to_iso() {
274        // March 1st 200 is same on both calendars
275        let julian_date = Date::try_new_julian(200, 3, 1).unwrap();
276        let iso_date = julian_date.to_iso();
277        let iso_expected_date = Date::try_new_iso(200, 3, 1).unwrap();
278        assert_eq!(iso_date, iso_expected_date);
279
280        // Feb 28th, 200 (iso) = Feb 29th, 200 (julian)
281        let julian_date = Date::try_new_julian(200, 2, 29).unwrap();
282        let iso_date = julian_date.to_iso();
283        let iso_expected_date = Date::try_new_iso(200, 2, 28).unwrap();
284        assert_eq!(iso_date, iso_expected_date);
285
286        // March 1st 400 (iso) = Feb 29th, 400 (julian)
287        let julian_date = Date::try_new_julian(400, 2, 29).unwrap();
288        let iso_date = julian_date.to_iso();
289        let iso_expected_date = Date::try_new_iso(400, 3, 1).unwrap();
290        assert_eq!(iso_date, iso_expected_date);
291
292        // Jan 1st, 2022 (iso) = Dec 19, 2021 (julian)
293        let julian_date = Date::try_new_julian(2021, 12, 19).unwrap();
294        let iso_date = julian_date.to_iso();
295        let iso_expected_date = Date::try_new_iso(2022, 1, 1).unwrap();
296        assert_eq!(iso_date, iso_expected_date);
297
298        // March 1st, 2022 (iso) = Feb 16, 2022 (julian)
299        let julian_date = Date::try_new_julian(2022, 2, 16).unwrap();
300        let iso_date = julian_date.to_iso();
301        let iso_expected_date = Date::try_new_iso(2022, 3, 1).unwrap();
302        assert_eq!(iso_date, iso_expected_date);
303    }
304
305    #[test]
306    fn test_roundtrip_negative() {
307        // https://github.com/unicode-org/icu4x/issues/2254
308        let iso_date = Date::try_new_iso(-1000, 3, 3).unwrap();
309        let julian = iso_date.to_calendar(Julian::new());
310        let recovered_iso = julian.to_iso();
311        assert_eq!(iso_date, recovered_iso);
312    }
313
314    #[test]
315    fn test_julian_near_era_change() {
316        // Tests that the Julian calendar gives the correct expected
317        // day, month, and year for positive years (CE)
318
319        #[derive(Debug)]
320        struct TestCase {
321            rd: i64,
322            iso_year: i32,
323            iso_month: u8,
324            iso_day: u8,
325            expected_year: i32,
326            expected_era: &'static str,
327            expected_month: u8,
328            expected_day: u8,
329        }
330
331        let cases = [
332            TestCase {
333                rd: 1,
334                iso_year: 1,
335                iso_month: 1,
336                iso_day: 1,
337                expected_year: 1,
338                expected_era: "ce",
339                expected_month: 1,
340                expected_day: 3,
341            },
342            TestCase {
343                rd: 0,
344                iso_year: 0,
345                iso_month: 12,
346                iso_day: 31,
347                expected_year: 1,
348                expected_era: "ce",
349                expected_month: 1,
350                expected_day: 2,
351            },
352            TestCase {
353                rd: -1,
354                iso_year: 0,
355                iso_month: 12,
356                iso_day: 30,
357                expected_year: 1,
358                expected_era: "ce",
359                expected_month: 1,
360                expected_day: 1,
361            },
362            TestCase {
363                rd: -2,
364                iso_year: 0,
365                iso_month: 12,
366                iso_day: 29,
367                expected_year: 1,
368                expected_era: "bce",
369                expected_month: 12,
370                expected_day: 31,
371            },
372            TestCase {
373                rd: -3,
374                iso_year: 0,
375                iso_month: 12,
376                iso_day: 28,
377                expected_year: 1,
378                expected_era: "bce",
379                expected_month: 12,
380                expected_day: 30,
381            },
382            TestCase {
383                rd: -367,
384                iso_year: -1,
385                iso_month: 12,
386                iso_day: 30,
387                expected_year: 1,
388                expected_era: "bce",
389                expected_month: 1,
390                expected_day: 1,
391            },
392            TestCase {
393                rd: -368,
394                iso_year: -1,
395                iso_month: 12,
396                iso_day: 29,
397                expected_year: 2,
398                expected_era: "bce",
399                expected_month: 12,
400                expected_day: 31,
401            },
402            TestCase {
403                rd: -1462,
404                iso_year: -4,
405                iso_month: 12,
406                iso_day: 30,
407                expected_year: 4,
408                expected_era: "bce",
409                expected_month: 1,
410                expected_day: 1,
411            },
412            TestCase {
413                rd: -1463,
414                iso_year: -4,
415                iso_month: 12,
416                iso_day: 29,
417                expected_year: 5,
418                expected_era: "bce",
419                expected_month: 12,
420                expected_day: 31,
421            },
422        ];
423
424        for case in cases {
425            let iso_from_rd = Date::from_rata_die(RataDie::new(case.rd), crate::Iso);
426            let julian_from_rd = Date::from_rata_die(RataDie::new(case.rd), Julian);
427            assert_eq!(julian_from_rd.era_year().year, case.expected_year,
428                "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
429            assert_eq!(julian_from_rd.era_year().era, case.expected_era,
430                "Failed era check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
431            assert_eq!(julian_from_rd.month().ordinal, case.expected_month,
432                "Failed month check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
433            assert_eq!(julian_from_rd.day_of_month().0, case.expected_day,
434                "Failed day check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
435
436            let iso_date_man = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day)
437                .expect("Failed to initialize ISO date for {case:?}");
438            let julian_date_man = Date::new_from_iso(iso_date_man, Julian);
439            assert_eq!(iso_from_rd, iso_date_man,
440                "ISO from RD not equal to ISO generated from manually-input ymd\nCase: {case:?}\nRD: {iso_from_rd:?}\nMan: {iso_date_man:?}");
441            assert_eq!(julian_from_rd, julian_date_man,
442                "Julian from RD not equal to Julian generated from manually-input ymd\nCase: {case:?}\nRD: {julian_from_rd:?}\nMan: {julian_date_man:?}");
443        }
444    }
445
446    #[test]
447    fn test_julian_rd_date_conversion() {
448        // Tests that converting from RD to Julian then
449        // back to RD yields the same RD
450        for i in -10000..=10000 {
451            let rd = RataDie::new(i);
452            let julian = Date::from_rata_die(rd, Julian);
453            let new_rd = julian.to_rata_die();
454            assert_eq!(rd, new_rd);
455        }
456    }
457
458    #[test]
459    fn test_julian_directionality() {
460        // Tests that for a large range of RDs, if a RD
461        // is less than another, the corresponding YMD should also be less
462        // than the other, without exception.
463        for i in -100..=100 {
464            for j in -100..=100 {
465                let julian_i = Date::from_rata_die(RataDie::new(i), Julian);
466                let julian_j = Date::from_rata_die(RataDie::new(j), Julian);
467
468                assert_eq!(
469                    i.cmp(&j),
470                    julian_i.inner.0.cmp(&julian_j.inner.0),
471                    "Julian directionality inconsistent with directionality for i: {i}, j: {j}"
472                );
473            }
474        }
475    }
476
477    #[test]
478    fn test_hebrew_epoch() {
479        assert_eq!(
480            calendrical_calculations::julian::fixed_from_julian_book_version(-3761, 10, 7),
481            RataDie::new(-1373427)
482        );
483    }
484
485    #[test]
486    fn test_julian_leap_years() {
487        assert!(Julian::provided_year_is_leap(4));
488        assert!(Julian::provided_year_is_leap(0));
489        assert!(Julian::provided_year_is_leap(-4));
490
491        Date::try_new_julian(2020, 2, 29).unwrap();
492    }
493}