icu_calendar/
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::{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
34use crate::any_calendar::AnyCalendarKind;
35use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
36use crate::gregorian::year_as_gregorian;
37use crate::iso::Iso;
38use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time};
39use calendrical_calculations::helpers::I32CastError;
40use calendrical_calculations::rata_die::RataDie;
41use 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
60pub 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>
65pub struct JulianDateInner(pub(crate) ArithmeticDate<Julian>);
66
67impl CalendarArithmetic for Julian {
68    type YearInfo = ();
69
70    fn month_days(year: i32, month: u8, _data: ()) -> u8 {
71        match month {
72            4 | 6 | 9 | 11 => 30,
73            2 if Self::is_leap_year(year, ()) => 29,
74            2 => 28,
75            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
76            _ => 0,
77        }
78    }
79
80    fn months_for_every_year(_: i32, _data: ()) -> u8 {
81        12
82    }
83
84    fn is_leap_year(year: i32, _data: ()) -> bool {
85        calendrical_calculations::julian::is_leap_year(year)
86    }
87
88    fn last_month_day_in_year(_year: i32, _data: ()) -> (u8, u8) {
89        (12, 31)
90    }
91
92    fn days_in_provided_year(year: i32, _data: ()) -> u16 {
93        if Self::is_leap_year(year, ()) {
94            366
95        } else {
96            365
97        }
98    }
99}
100
101impl Calendar for Julian {
102    type DateInner = JulianDateInner;
103    fn date_from_codes(
104        &self,
105        era: types::Era,
106        year: i32,
107        month_code: types::MonthCode,
108        day: u8,
109    ) -> Result<Self::DateInner, CalendarError> {
110        let year = if era.0 == tinystr!(16, "ce") {
111            if year <= 0 {
112                return Err(CalendarError::OutOfRange);
113            }
114            year
115        } else if era.0 == tinystr!(16, "bce") {
116            if year <= 0 {
117                return Err(CalendarError::OutOfRange);
118            }
119            1 - year
120        } else {
121            return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
122        };
123
124        ArithmeticDate::new_from_codes(self, year, month_code, day).map(JulianDateInner)
125    }
126    fn date_from_iso(&self, iso: Date<Iso>) -> JulianDateInner {
127        let fixed_iso = Iso::fixed_from_iso(*iso.inner());
128        Self::julian_from_fixed(fixed_iso)
129    }
130
131    fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
132        let fixed_julian = Julian::fixed_from_julian(date.0);
133        Iso::iso_from_fixed(fixed_julian)
134    }
135
136    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
137        date.0.months_in_year()
138    }
139
140    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
141        date.0.days_in_year()
142    }
143
144    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
145        date.0.days_in_month()
146    }
147
148    fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {
149        Iso.day_of_week(Julian.date_to_iso(date).inner())
150    }
151
152    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
153        date.0.offset_date(offset, &());
154    }
155
156    #[allow(clippy::field_reassign_with_default)]
157    fn until(
158        &self,
159        date1: &Self::DateInner,
160        date2: &Self::DateInner,
161        _calendar2: &Self,
162        _largest_unit: DateDurationUnit,
163        _smallest_unit: DateDurationUnit,
164    ) -> DateDuration<Self> {
165        date1.0.until(date2.0, _largest_unit, _smallest_unit)
166    }
167
168    /// The calendar-specific year represented by `date`
169    /// Julian has the same era scheme as Gregorian
170    fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
171        year_as_gregorian(date.0.year)
172    }
173
174    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
175        Self::is_leap_year(date.0.year, ())
176    }
177
178    /// The calendar-specific month represented by `date`
179    fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
180        date.0.month()
181    }
182
183    /// The calendar-specific day-of-month represented by `date`
184    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
185        date.0.day_of_month()
186    }
187
188    fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
189        let prev_year = date.0.year - 1;
190        let next_year = date.0.year + 1;
191        types::DayOfYearInfo {
192            day_of_year: date.0.day_of_year(),
193            days_in_year: date.0.days_in_year(),
194            prev_year: crate::gregorian::year_as_gregorian(prev_year),
195            days_in_prev_year: Julian::days_in_year_direct(prev_year),
196            next_year: crate::gregorian::year_as_gregorian(next_year),
197        }
198    }
199
200    fn debug_name(&self) -> &'static str {
201        "Julian"
202    }
203
204    fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
205        None
206    }
207}
208
209impl Julian {
210    /// Construct a new Julian Calendar
211    pub fn new() -> Self {
212        Self
213    }
214
215    // "Fixed" is a day count representation of calendars staring from Jan 1st of year 1 of the Georgian Calendar.
216    pub(crate) const fn fixed_from_julian(date: ArithmeticDate<Julian>) -> RataDie {
217        calendrical_calculations::julian::fixed_from_julian(date.year, date.month, date.day)
218    }
219
220    /// Convenience function so we can call days_in_year without
221    /// needing to construct a full ArithmeticDate
222    fn days_in_year_direct(year: i32) -> u16 {
223        if Julian::is_leap_year(year, ()) {
224            366
225        } else {
226            365
227        }
228    }
229
230    fn julian_from_fixed(date: RataDie) -> JulianDateInner {
231        let (year, month, day) = match calendrical_calculations::julian::julian_from_fixed(date) {
232            Err(I32CastError::BelowMin) => return JulianDateInner(ArithmeticDate::min_date()),
233            Err(I32CastError::AboveMax) => return JulianDateInner(ArithmeticDate::max_date()),
234            Ok(ymd) => ymd,
235        };
236        JulianDateInner(ArithmeticDate::new_unchecked(year, month, day))
237    }
238}
239
240impl 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    pub fn try_new_julian_date(
256        year: i32,
257        month: u8,
258        day: u8,
259    ) -> Result<Date<Julian>, CalendarError> {
260        ArithmeticDate::new_from_ordinals(year, month, day)
261            .map(JulianDateInner)
262            .map(|inner| Date::from_raw(inner, Julian))
263    }
264}
265
266impl 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    pub fn try_new_julian_datetime(
286        year: i32,
287        month: u8,
288        day: u8,
289        hour: u8,
290        minute: u8,
291        second: u8,
292    ) -> Result<DateTime<Julian>, CalendarError> {
293        Ok(DateTime {
294            date: Date::try_new_julian_date(year, month, day)?,
295            time: Time::try_new(hour, minute, second, 0)?,
296        })
297    }
298}
299
300#[cfg(test)]
301mod 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}