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
5use crate::calendar_arithmetic::ArithmeticDate;
6use crate::calendar_arithmetic::DateFieldsResolver;
7use crate::error::{DateError, DateFromFieldsError, EcmaReferenceYearError, UnknownEraError};
8use crate::options::DateFromFieldsOptions;
9use crate::options::{DateAddOptions, DateDifferenceOptions};
10use crate::types::DateFields;
11use crate::{types, Calendar, Date, RangeError};
12use calendrical_calculations::helpers::I32CastError;
13use calendrical_calculations::rata_die::RataDie;
14use tinystr::tinystr;
15
16/// The [Julian Calendar](https://en.wikipedia.org/wiki/Julian_calendar).
17///
18/// The Julian calendar is a solar calendar that was introduced in the Roman Republic under
19/// Julius Caesar in 45 BCE, and used in Europe and much of the western world until it was
20/// eventually replaced by the more accurate [`Gregorian`](super::Gregorian) calendar.
21///
22/// This implementation extends proleptically for dates before the calendar's creation.
23///
24/// While no country uses the Julian calendar as its civil calendar today, it is still
25/// used by eastern Christian churches to determine lithurgical dates like Christmas and
26/// Easter.
27///
28/// # Era codes
29///
30/// This calendar uses two era codes: `bce` (alias `bc`), and `ce` (alias `ad`), corresponding to the BCE and CE eras.
31///
32/// # Months and days
33///
34/// The 12 months are called January (`M01`, 31 days), February (`M02`, 28 days),
35/// March (`M03`, 31 days), April (`M04`, 30 days), May (`M05`, 31 days), June (`M06`, 30 days),
36/// July (`M07`, 31 days), August (`M08`, 31 days), September (`M09`, 30 days),
37/// October (`M10`, 31 days), November (`M11`, 30 days), December (`M12`, 31 days).
38///
39/// In leap years (years divisible by 4), February gains a 29th day.
40///
41/// Standard years thus have 365 days, and leap years 366.
42///
43/// # Calendar drift
44///
45/// The Julian calendar has an average year length of 365.25, slightly longer than
46/// the mean solar year, so this calendar drifts 1 day in ~128 years with
47/// respect to the seasons. This significant drift was the reason for its replacement
48/// by the Gregorian calendar. The Julian calendar is currently 14 days ahead of the
49/// Gregorian calendar and the solar year.
50///
51/// # Historical accuracy
52///
53/// Historically, a variety of year reckoning schemes have been used with the Julian
54/// calendar, such as Roman consular years, regnal years, [indictions](
55/// https://en.wikipedia.org/wiki/Indiction), [Anno Mundi](
56/// https://en.wikipedia.org/wiki/Anno_Mundi#Byzantine_era), the [Diocletian era](
57/// https://en.wikipedia.org/wiki/Era_of_the_Martyrs), [Anno Domini](
58/// https://en.wikipedia.org/wiki/Anno_Domini), and the (equivalent) [Common era](
59/// https://en.wikipedia.org/wiki/Common_Era).
60/// The latter, which is used today and by this implementation, has been used by
61/// western European authorities since the early middle ages, however some eastern
62/// European countries/churches have not adopted it until fairly recently, or, in
63/// some cases, are still using a different year reckoning scheme.
64///
65/// Also during the middle ages, [some countries](https://en.wikipedia.org/wiki/New_Year#Historical_European_new_year_dates)
66/// used different dates for the first day of the year, ranging from late December to
67/// late March. Care has to be taken when interpreting year numbers with dates in this
68/// range.
69///
70/// The calendar was used [incorrectly](https://en.wikipedia.org/wiki/Julian_calendar#Leap_year_error)
71/// for a while after adoption, so the first year where the months align with this proleptic
72/// implementation is probably 4 CE.
73#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
74#[allow(clippy::exhaustive_structs)] // this type is stable
75pub struct Julian;
76
77/// The inner date type used for representing [`Date`]s of [`Julian`]. See [`Date`] and [`Julian`] for more details.
78#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
79// The inner date type used for representing Date<Julian>
80pub struct JulianDateInner(pub(crate) ArithmeticDate<Julian>);
81
82impl DateFieldsResolver for Julian {
83    type YearInfo = i32;
84
85    fn days_in_provided_month(year: i32, month: u8) -> u8 {
86        match month {
87            4 | 6 | 9 | 11 => 30,
88            2 if calendrical_calculations::julian::is_leap_year(year) => 29,
89            2 => 28,
90            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
91            _ => 0,
92        }
93    }
94
95    fn months_in_provided_year(_: i32) -> u8 {
96        12
97    }
98
99    #[inline]
100    fn year_info_from_era(
101        &self,
102        era: &[u8],
103        era_year: i32,
104    ) -> Result<Self::YearInfo, UnknownEraError> {
105        match era {
106            b"ad" | b"ce" => Ok(era_year),
107            b"bc" | b"bce" => Ok(1 - era_year),
108            _ => Err(UnknownEraError),
109        }
110    }
111
112    #[inline]
113    fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo {
114        extended_year
115    }
116
117    #[inline]
118    fn reference_year_from_month_day(
119        &self,
120        month_code: types::ValidMonthCode,
121        day: u8,
122    ) -> Result<Self::YearInfo, EcmaReferenceYearError> {
123        let (ordinal_month, false) = month_code.to_tuple() else {
124            return Err(EcmaReferenceYearError::MonthCodeNotInCalendar);
125        };
126        // December 31, 1972 occurs on 12th month, 18th day, 1972 Old Style
127        // Note: 1972 is a leap year
128        let julian_year = if ordinal_month < 12 || (ordinal_month == 12 && day <= 18) {
129            1972
130        } else {
131            1971
132        };
133        Ok(julian_year)
134    }
135}
136
137impl crate::cal::scaffold::UnstableSealed for Julian {}
138impl Calendar for Julian {
139    type DateInner = JulianDateInner;
140    type Year = types::EraYear;
141    type DifferenceError = core::convert::Infallible;
142
143    fn from_codes(
144        &self,
145        era: Option<&str>,
146        year: i32,
147        month_code: types::MonthCode,
148        day: u8,
149    ) -> Result<Self::DateInner, DateError> {
150        ArithmeticDate::from_codes(era, year, month_code, day, self).map(JulianDateInner)
151    }
152
153    #[cfg(feature = "unstable")]
154    fn from_fields(
155        &self,
156        fields: DateFields,
157        options: DateFromFieldsOptions,
158    ) -> Result<Self::DateInner, DateFromFieldsError> {
159        ArithmeticDate::from_fields(fields, options, self).map(JulianDateInner)
160    }
161
162    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
163        JulianDateInner(
164            match calendrical_calculations::julian::julian_from_fixed(rd) {
165                Err(I32CastError::BelowMin) => ArithmeticDate::new_unchecked(i32::MIN, 1, 1),
166                Err(I32CastError::AboveMax) => ArithmeticDate::new_unchecked(i32::MAX, 12, 31),
167                Ok((year, month, day)) => ArithmeticDate::new_unchecked(year, month, day),
168            },
169        )
170    }
171
172    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
173        calendrical_calculations::julian::fixed_from_julian(date.0.year, date.0.month, date.0.day)
174    }
175
176    fn has_cheap_iso_conversion(&self) -> bool {
177        false
178    }
179
180    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
181        Self::months_in_provided_year(date.0.year)
182    }
183
184    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
185        if self.is_in_leap_year(date) {
186            366
187        } else {
188            365
189        }
190    }
191
192    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
193        Self::days_in_provided_month(date.0.year, date.0.month)
194    }
195
196    #[cfg(feature = "unstable")]
197    fn add(
198        &self,
199        date: &Self::DateInner,
200        duration: types::DateDuration,
201        options: DateAddOptions,
202    ) -> Result<Self::DateInner, DateError> {
203        date.0.added(duration, self, options).map(JulianDateInner)
204    }
205
206    #[cfg(feature = "unstable")]
207    fn until(
208        &self,
209        date1: &Self::DateInner,
210        date2: &Self::DateInner,
211        options: DateDifferenceOptions,
212    ) -> Result<types::DateDuration, Self::DifferenceError> {
213        Ok(date1.0.until(&date2.0, self, options))
214    }
215
216    /// The calendar-specific year represented by `date`
217    /// Julian has the same era scheme as Gregorian
218    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
219        let extended_year = date.0.year;
220        if extended_year > 0 {
221            types::EraYear {
222                era: tinystr!(16, "ce"),
223                era_index: Some(1),
224                year: extended_year,
225                extended_year,
226                ambiguity: types::YearAmbiguity::CenturyRequired,
227            }
228        } else {
229            types::EraYear {
230                era: tinystr!(16, "bce"),
231                era_index: Some(0),
232                year: 1 - extended_year,
233                extended_year,
234                ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
235            }
236        }
237    }
238
239    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
240        calendrical_calculations::julian::is_leap_year(date.0.year)
241    }
242
243    /// The calendar-specific month represented by `date`
244    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
245        types::MonthInfo::non_lunisolar(date.0.month)
246    }
247
248    /// The calendar-specific day-of-month represented by `date`
249    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
250        types::DayOfMonth(date.0.day)
251    }
252
253    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
254        types::DayOfYear(
255            (1..date.0.month)
256                .map(|m| Self::days_in_provided_month(date.0.year, m) as u16)
257                .sum::<u16>()
258                + date.0.day as u16,
259        )
260    }
261
262    fn debug_name(&self) -> &'static str {
263        "Julian"
264    }
265
266    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
267        None
268    }
269}
270
271impl Julian {
272    /// Construct a new Julian Calendar
273    pub fn new() -> Self {
274        Self
275    }
276
277    /// Returns the date of (Orthodox) Easter in the given year.
278    pub fn easter(year: i32) -> Date<Self> {
279        Date::from_rata_die(calendrical_calculations::julian::easter(year), Self)
280    }
281}
282
283impl Date<Julian> {
284    /// Construct new Julian Date.
285    ///
286    /// Years are arithmetic, meaning there is a year 0. Zero and negative years are in BC, with year 0 = 1 BC
287    ///
288    /// ```rust
289    /// use icu::calendar::Date;
290    ///
291    /// let date_julian = Date::try_new_julian(1969, 12, 20)
292    ///     .expect("Failed to initialize Julian Date instance.");
293    ///
294    /// assert_eq!(date_julian.era_year().year, 1969);
295    /// assert_eq!(date_julian.month().ordinal, 12);
296    /// assert_eq!(date_julian.day_of_month().0, 20);
297    /// ```
298    pub fn try_new_julian(year: i32, month: u8, day: u8) -> Result<Date<Julian>, RangeError> {
299        ArithmeticDate::try_from_ymd(year, month, day)
300            .map(JulianDateInner)
301            .map(|inner| Date::from_raw(inner, Julian))
302    }
303}
304
305#[cfg(test)]
306mod test {
307    use super::*;
308
309    #[test]
310    fn test_day_iso_to_julian() {
311        // March 1st 200 is same on both calendars
312        let iso_date = Date::try_new_iso(200, 3, 1).unwrap();
313        let julian_date = Date::new_from_iso(iso_date, Julian).inner;
314        assert_eq!(julian_date.0.year, 200);
315        assert_eq!(julian_date.0.month, 3);
316        assert_eq!(julian_date.0.day, 1);
317
318        // Feb 28th, 200 (iso) = Feb 29th, 200 (julian)
319        let iso_date = Date::try_new_iso(200, 2, 28).unwrap();
320        let julian_date = Date::new_from_iso(iso_date, Julian).inner;
321        assert_eq!(julian_date.0.year, 200);
322        assert_eq!(julian_date.0.month, 2);
323        assert_eq!(julian_date.0.day, 29);
324
325        // March 1st 400 (iso) = Feb 29th, 400 (julian)
326        let iso_date = Date::try_new_iso(400, 3, 1).unwrap();
327        let julian_date = Date::new_from_iso(iso_date, Julian).inner;
328        assert_eq!(julian_date.0.year, 400);
329        assert_eq!(julian_date.0.month, 2);
330        assert_eq!(julian_date.0.day, 29);
331
332        // Jan 1st, 2022 (iso) = Dec 19, 2021 (julian)
333        let iso_date = Date::try_new_iso(2022, 1, 1).unwrap();
334        let julian_date = Date::new_from_iso(iso_date, Julian).inner;
335        assert_eq!(julian_date.0.year, 2021);
336        assert_eq!(julian_date.0.month, 12);
337        assert_eq!(julian_date.0.day, 19);
338    }
339
340    #[test]
341    fn test_day_julian_to_iso() {
342        // March 1st 200 is same on both calendars
343        let julian_date = Date::try_new_julian(200, 3, 1).unwrap();
344        let iso_date = julian_date.to_iso();
345        let iso_expected_date = Date::try_new_iso(200, 3, 1).unwrap();
346        assert_eq!(iso_date, iso_expected_date);
347
348        // Feb 28th, 200 (iso) = Feb 29th, 200 (julian)
349        let julian_date = Date::try_new_julian(200, 2, 29).unwrap();
350        let iso_date = julian_date.to_iso();
351        let iso_expected_date = Date::try_new_iso(200, 2, 28).unwrap();
352        assert_eq!(iso_date, iso_expected_date);
353
354        // March 1st 400 (iso) = Feb 29th, 400 (julian)
355        let julian_date = Date::try_new_julian(400, 2, 29).unwrap();
356        let iso_date = julian_date.to_iso();
357        let iso_expected_date = Date::try_new_iso(400, 3, 1).unwrap();
358        assert_eq!(iso_date, iso_expected_date);
359
360        // Jan 1st, 2022 (iso) = Dec 19, 2021 (julian)
361        let julian_date = Date::try_new_julian(2021, 12, 19).unwrap();
362        let iso_date = julian_date.to_iso();
363        let iso_expected_date = Date::try_new_iso(2022, 1, 1).unwrap();
364        assert_eq!(iso_date, iso_expected_date);
365
366        // March 1st, 2022 (iso) = Feb 16, 2022 (julian)
367        let julian_date = Date::try_new_julian(2022, 2, 16).unwrap();
368        let iso_date = julian_date.to_iso();
369        let iso_expected_date = Date::try_new_iso(2022, 3, 1).unwrap();
370        assert_eq!(iso_date, iso_expected_date);
371    }
372
373    #[test]
374    fn test_roundtrip_negative() {
375        // https://github.com/unicode-org/icu4x/issues/2254
376        let iso_date = Date::try_new_iso(-1000, 3, 3).unwrap();
377        let julian = iso_date.to_calendar(Julian::new());
378        let recovered_iso = julian.to_iso();
379        assert_eq!(iso_date, recovered_iso);
380    }
381
382    #[test]
383    fn test_julian_near_era_change() {
384        // Tests that the Julian calendar gives the correct expected
385        // day, month, and year for positive years (CE)
386
387        #[derive(Debug)]
388        struct TestCase {
389            rd: i64,
390            iso_year: i32,
391            iso_month: u8,
392            iso_day: u8,
393            expected_year: i32,
394            expected_era: &'static str,
395            expected_month: u8,
396            expected_day: u8,
397        }
398
399        let cases = [
400            TestCase {
401                rd: 1,
402                iso_year: 1,
403                iso_month: 1,
404                iso_day: 1,
405                expected_year: 1,
406                expected_era: "ce",
407                expected_month: 1,
408                expected_day: 3,
409            },
410            TestCase {
411                rd: 0,
412                iso_year: 0,
413                iso_month: 12,
414                iso_day: 31,
415                expected_year: 1,
416                expected_era: "ce",
417                expected_month: 1,
418                expected_day: 2,
419            },
420            TestCase {
421                rd: -1,
422                iso_year: 0,
423                iso_month: 12,
424                iso_day: 30,
425                expected_year: 1,
426                expected_era: "ce",
427                expected_month: 1,
428                expected_day: 1,
429            },
430            TestCase {
431                rd: -2,
432                iso_year: 0,
433                iso_month: 12,
434                iso_day: 29,
435                expected_year: 1,
436                expected_era: "bce",
437                expected_month: 12,
438                expected_day: 31,
439            },
440            TestCase {
441                rd: -3,
442                iso_year: 0,
443                iso_month: 12,
444                iso_day: 28,
445                expected_year: 1,
446                expected_era: "bce",
447                expected_month: 12,
448                expected_day: 30,
449            },
450            TestCase {
451                rd: -367,
452                iso_year: -1,
453                iso_month: 12,
454                iso_day: 30,
455                expected_year: 1,
456                expected_era: "bce",
457                expected_month: 1,
458                expected_day: 1,
459            },
460            TestCase {
461                rd: -368,
462                iso_year: -1,
463                iso_month: 12,
464                iso_day: 29,
465                expected_year: 2,
466                expected_era: "bce",
467                expected_month: 12,
468                expected_day: 31,
469            },
470            TestCase {
471                rd: -1462,
472                iso_year: -4,
473                iso_month: 12,
474                iso_day: 30,
475                expected_year: 4,
476                expected_era: "bce",
477                expected_month: 1,
478                expected_day: 1,
479            },
480            TestCase {
481                rd: -1463,
482                iso_year: -4,
483                iso_month: 12,
484                iso_day: 29,
485                expected_year: 5,
486                expected_era: "bce",
487                expected_month: 12,
488                expected_day: 31,
489            },
490        ];
491
492        for case in cases {
493            let iso_from_rd = Date::from_rata_die(RataDie::new(case.rd), crate::Iso);
494            let julian_from_rd = Date::from_rata_die(RataDie::new(case.rd), Julian);
495            assert_eq!(julian_from_rd.era_year().year, case.expected_year,
496                "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
497            assert_eq!(julian_from_rd.era_year().era, case.expected_era,
498                "Failed era check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
499            assert_eq!(julian_from_rd.month().ordinal, case.expected_month,
500                "Failed month check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
501            assert_eq!(julian_from_rd.day_of_month().0, case.expected_day,
502                "Failed day check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
503
504            let iso_date_man = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day)
505                .expect("Failed to initialize ISO date for {case:?}");
506            let julian_date_man = Date::new_from_iso(iso_date_man, Julian);
507            assert_eq!(iso_from_rd, iso_date_man,
508                "ISO from RD not equal to ISO generated from manually-input ymd\nCase: {case:?}\nRD: {iso_from_rd:?}\nMan: {iso_date_man:?}");
509            assert_eq!(julian_from_rd, julian_date_man,
510                "Julian from RD not equal to Julian generated from manually-input ymd\nCase: {case:?}\nRD: {julian_from_rd:?}\nMan: {julian_date_man:?}");
511        }
512    }
513
514    #[test]
515    fn test_julian_rd_date_conversion() {
516        // Tests that converting from RD to Julian then
517        // back to RD yields the same RD
518        for i in -10000..=10000 {
519            let rd = RataDie::new(i);
520            let julian = Date::from_rata_die(rd, Julian);
521            let new_rd = julian.to_rata_die();
522            assert_eq!(rd, new_rd);
523        }
524    }
525
526    #[test]
527    fn test_julian_directionality() {
528        // Tests that for a large range of RDs, if a RD
529        // is less than another, the corresponding YMD should also be less
530        // than the other, without exception.
531        for i in -100..=100 {
532            for j in -100..=100 {
533                let julian_i = Date::from_rata_die(RataDie::new(i), Julian);
534                let julian_j = Date::from_rata_die(RataDie::new(j), Julian);
535
536                assert_eq!(
537                    i.cmp(&j),
538                    julian_i.inner.0.cmp(&julian_j.inner.0),
539                    "Julian directionality inconsistent with directionality for i: {i}, j: {j}"
540                );
541            }
542        }
543    }
544
545    #[test]
546    fn test_julian_leap_years() {
547        Date::try_new_julian(4, 2, 29).unwrap();
548        Date::try_new_julian(0, 2, 29).unwrap();
549        Date::try_new_julian(-4, 2, 29).unwrap();
550        Date::try_new_julian(2020, 2, 29).unwrap();
551    }
552}