icu_calendar/
ethiopian.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 Ethiopian calendar.
6//!
7//! ```rust
8//! use icu::calendar::{ethiopian::Ethiopian, 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_ethiopian = Date::new_from_iso(date_iso, Ethiopian::new());
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_ethiopian =
19//!     DateTime::new_from_iso(datetime_iso, Ethiopian::new());
20//!
21//! // `Date` checks
22//! assert_eq!(date_ethiopian.year().number, 1962);
23//! assert_eq!(date_ethiopian.month().ordinal, 4);
24//! assert_eq!(date_ethiopian.day_of_month().0, 24);
25//!
26//! // `DateTime` type
27//! assert_eq!(datetime_ethiopian.date.year().number, 1962);
28//! assert_eq!(datetime_ethiopian.date.month().ordinal, 4);
29//! assert_eq!(datetime_ethiopian.date.day_of_month().0, 24);
30//! assert_eq!(datetime_ethiopian.time.hour.number(), 13);
31//! assert_eq!(datetime_ethiopian.time.minute.number(), 1);
32//! assert_eq!(datetime_ethiopian.time.second.number(), 0);
33//! ```
34
35use crate::any_calendar::AnyCalendarKind;
36use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
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 number of years the Amete Alem epoch precedes the Amete Mihret epoch
44const AMETE_ALEM_OFFSET: i32 = 5500;
45
46/// Which era style the ethiopian calendar uses
47#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
48#[non_exhaustive]
49pub enum EthiopianEraStyle {
50    /// Use an era scheme of pre- and post- Incarnation eras,
51    /// anchored at the date of the Incarnation of Jesus in this calendar
52    AmeteMihret,
53    /// Use an era scheme of the Anno Mundi era, anchored at the date of Creation
54    /// in this calendar
55    AmeteAlem,
56}
57
58/// The [Ethiopian Calendar]
59///
60/// The [Ethiopian calendar] is a solar calendar used by the Coptic Orthodox Church, with twelve normal months
61/// and a thirteenth small epagomenal month.
62///
63/// This type can be used with [`Date`] or [`DateTime`] to represent dates in this calendar.
64///
65/// It can be constructed in two modes: using the Amete Alem era scheme, or the Amete Mihret era scheme (the default),
66/// see [`EthiopianEraStyle`] for more info.
67///
68/// [Ethiopian calendar]: https://en.wikipedia.org/wiki/Ethiopian_calendar
69///
70/// # Era codes
71///
72/// This calendar supports three era codes, based on what mode it is in. In the Amete Mihret scheme it has
73/// the `"incar"` and `"pre-incar"` eras, 1 Incarnation is 9 CE. In the Amete Alem scheme, it instead has a single era,
74/// `"mundi`, where 1 Anno Mundi is 5493 BCE. Dates before that use negative year numbers.
75///
76/// # Month codes
77///
78/// This calendar supports 13 solar month codes (`"M01" - "M13"`), with `"M13"` being used for the short epagomenal month
79/// at the end of the year.
80// The bool specifies whether dates should be in the Amete Alem era scheme
81#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
82pub struct Ethiopian(pub(crate) bool);
83
84/// The inner date type used for representing [`Date`]s of [`Ethiopian`]. See [`Date`] and [`Ethiopian`] for more details.
85#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
86pub struct EthiopianDateInner(ArithmeticDate<Ethiopian>);
87
88impl CalendarArithmetic for Ethiopian {
89    type YearInfo = ();
90
91    fn month_days(year: i32, month: u8, _data: ()) -> u8 {
92        if (1..=12).contains(&month) {
93            30
94        } else if month == 13 {
95            if Self::is_leap_year(year, ()) {
96                6
97            } else {
98                5
99            }
100        } else {
101            0
102        }
103    }
104
105    fn months_for_every_year(_: i32, _data: ()) -> u8 {
106        13
107    }
108
109    fn is_leap_year(year: i32, _data: ()) -> bool {
110        year.rem_euclid(4) == 3
111    }
112
113    fn last_month_day_in_year(year: i32, _data: ()) -> (u8, u8) {
114        if Self::is_leap_year(year, ()) {
115            (13, 6)
116        } else {
117            (13, 5)
118        }
119    }
120
121    fn days_in_provided_year(year: i32, _data: ()) -> u16 {
122        if Self::is_leap_year(year, ()) {
123            366
124        } else {
125            365
126        }
127    }
128}
129
130impl Calendar for Ethiopian {
131    type DateInner = EthiopianDateInner;
132    fn date_from_codes(
133        &self,
134        era: types::Era,
135        year: i32,
136        month_code: types::MonthCode,
137        day: u8,
138    ) -> Result<Self::DateInner, CalendarError> {
139        let year = if era.0 == tinystr!(16, "incar") {
140            if year <= 0 {
141                return Err(CalendarError::OutOfRange);
142            }
143            year
144        } else if era.0 == tinystr!(16, "pre-incar") {
145            if year <= 0 {
146                return Err(CalendarError::OutOfRange);
147            }
148            1 - year
149        } else if era.0 == tinystr!(16, "mundi") {
150            year - AMETE_ALEM_OFFSET
151        } else {
152            return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
153        };
154
155        ArithmeticDate::new_from_codes(self, year, month_code, day).map(EthiopianDateInner)
156    }
157    fn date_from_iso(&self, iso: Date<Iso>) -> EthiopianDateInner {
158        let fixed_iso = Iso::fixed_from_iso(*iso.inner());
159        Self::ethiopian_from_fixed(fixed_iso)
160    }
161
162    fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
163        let fixed_ethiopian = Ethiopian::fixed_from_ethiopian(date.0);
164        Iso::iso_from_fixed(fixed_ethiopian)
165    }
166
167    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
168        date.0.months_in_year()
169    }
170
171    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
172        date.0.days_in_year()
173    }
174
175    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
176        date.0.days_in_month()
177    }
178
179    fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {
180        Iso.day_of_week(self.date_to_iso(date).inner())
181    }
182
183    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
184        date.0.offset_date(offset, &());
185    }
186
187    #[allow(clippy::field_reassign_with_default)]
188    fn until(
189        &self,
190        date1: &Self::DateInner,
191        date2: &Self::DateInner,
192        _calendar2: &Self,
193        _largest_unit: DateDurationUnit,
194        _smallest_unit: DateDurationUnit,
195    ) -> DateDuration<Self> {
196        date1.0.until(date2.0, _largest_unit, _smallest_unit)
197    }
198
199    fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
200        Self::year_as_ethiopian(date.0.year, self.0)
201    }
202
203    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
204        Self::is_leap_year(date.0.year, ())
205    }
206
207    fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
208        date.0.month()
209    }
210
211    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
212        date.0.day_of_month()
213    }
214
215    fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
216        let prev_year = date.0.year - 1;
217        let next_year = date.0.year + 1;
218        types::DayOfYearInfo {
219            day_of_year: date.0.day_of_year(),
220            days_in_year: date.0.days_in_year(),
221            prev_year: Self::year_as_ethiopian(prev_year, self.0),
222            days_in_prev_year: Ethiopian::days_in_year_direct(prev_year),
223            next_year: Self::year_as_ethiopian(next_year, self.0),
224        }
225    }
226
227    fn debug_name(&self) -> &'static str {
228        "Ethiopian"
229    }
230
231    fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
232        if self.0 {
233            Some(AnyCalendarKind::EthiopianAmeteAlem)
234        } else {
235            Some(AnyCalendarKind::Ethiopian)
236        }
237    }
238}
239
240impl Ethiopian {
241    /// Construct a new Ethiopian Calendar for the Amete Mihret era naming scheme
242    pub const fn new() -> Self {
243        Self(false)
244    }
245    /// Construct a new Ethiopian Calendar with a value specifying whether or not it is Amete Alem
246    pub const fn new_with_era_style(era_style: EthiopianEraStyle) -> Self {
247        Self(matches!(era_style, EthiopianEraStyle::AmeteAlem))
248    }
249    /// Set whether or not this uses the Amete Alem era scheme
250    pub fn set_era_style(&mut self, era_style: EthiopianEraStyle) {
251        self.0 = era_style == EthiopianEraStyle::AmeteAlem
252    }
253
254    /// Returns whether this has the Amete Alem era
255    pub fn era_style(&self) -> EthiopianEraStyle {
256        if self.0 {
257            EthiopianEraStyle::AmeteAlem
258        } else {
259            EthiopianEraStyle::AmeteMihret
260        }
261    }
262
263    fn fixed_from_ethiopian(date: ArithmeticDate<Ethiopian>) -> RataDie {
264        calendrical_calculations::ethiopian::fixed_from_ethiopian(date.year, date.month, date.day)
265    }
266
267    fn ethiopian_from_fixed(date: RataDie) -> EthiopianDateInner {
268        let (year, month, day) =
269            match calendrical_calculations::ethiopian::ethiopian_from_fixed(date) {
270                Err(I32CastError::BelowMin) => {
271                    return EthiopianDateInner(ArithmeticDate::min_date())
272                }
273                Err(I32CastError::AboveMax) => {
274                    return EthiopianDateInner(ArithmeticDate::max_date())
275                }
276                Ok(ymd) => ymd,
277            };
278        EthiopianDateInner(ArithmeticDate::new_unchecked(year, month, day))
279    }
280
281    fn days_in_year_direct(year: i32) -> u16 {
282        if Ethiopian::is_leap_year(year, ()) {
283            366
284        } else {
285            365
286        }
287    }
288
289    fn year_as_ethiopian(year: i32, amete_alem: bool) -> types::FormattableYear {
290        if amete_alem {
291            types::FormattableYear {
292                era: types::Era(tinystr!(16, "mundi")),
293                number: year + AMETE_ALEM_OFFSET,
294                cyclic: None,
295                related_iso: None,
296            }
297        } else if year > 0 {
298            types::FormattableYear {
299                era: types::Era(tinystr!(16, "incar")),
300                number: year,
301                cyclic: None,
302                related_iso: None,
303            }
304        } else {
305            types::FormattableYear {
306                era: types::Era(tinystr!(16, "pre-incar")),
307                number: 1 - year,
308                cyclic: None,
309                related_iso: None,
310            }
311        }
312    }
313}
314
315impl Date<Ethiopian> {
316    /// Construct new Ethiopian Date.
317    ///
318    /// For the Amete Mihret era style, negative years work with
319    /// year 0 as 1 pre-Incarnation, year -1 as 2 pre-Incarnation,
320    /// and so on.
321    ///
322    /// ```rust
323    /// use icu::calendar::ethiopian::EthiopianEraStyle;
324    /// use icu::calendar::Date;
325    ///
326    /// let date_ethiopian = Date::try_new_ethiopian_date(
327    ///     EthiopianEraStyle::AmeteMihret,
328    ///     2014,
329    ///     8,
330    ///     25,
331    /// )
332    /// .expect("Failed to initialize Ethopic Date instance.");
333    ///
334    /// assert_eq!(date_ethiopian.year().number, 2014);
335    /// assert_eq!(date_ethiopian.month().ordinal, 8);
336    /// assert_eq!(date_ethiopian.day_of_month().0, 25);
337    /// ```
338    pub fn try_new_ethiopian_date(
339        era_style: EthiopianEraStyle,
340        mut year: i32,
341        month: u8,
342        day: u8,
343    ) -> Result<Date<Ethiopian>, CalendarError> {
344        if era_style == EthiopianEraStyle::AmeteAlem {
345            year -= AMETE_ALEM_OFFSET;
346        }
347        ArithmeticDate::new_from_ordinals(year, month, day)
348            .map(EthiopianDateInner)
349            .map(|inner| Date::from_raw(inner, Ethiopian::new_with_era_style(era_style)))
350    }
351}
352
353impl DateTime<Ethiopian> {
354    /// Construct a new Ethiopian datetime from integers.
355    ///
356    /// For the Amete Mihret era style, negative years work with
357    /// year 0 as 1 pre-Incarnation, year -1 as 2 pre-Incarnation,
358    /// and so on.
359    ///
360    /// ```rust
361    /// use icu::calendar::ethiopian::EthiopianEraStyle;
362    /// use icu::calendar::DateTime;
363    ///
364    /// let datetime_ethiopian = DateTime::try_new_ethiopian_datetime(
365    ///     EthiopianEraStyle::AmeteMihret,
366    ///     2014,
367    ///     8,
368    ///     25,
369    ///     13,
370    ///     1,
371    ///     0,
372    /// )
373    /// .expect("Failed to initialize Ethiopian DateTime instance.");
374    ///
375    /// assert_eq!(datetime_ethiopian.date.year().number, 2014);
376    /// assert_eq!(datetime_ethiopian.date.month().ordinal, 8);
377    /// assert_eq!(datetime_ethiopian.date.day_of_month().0, 25);
378    /// assert_eq!(datetime_ethiopian.time.hour.number(), 13);
379    /// assert_eq!(datetime_ethiopian.time.minute.number(), 1);
380    /// assert_eq!(datetime_ethiopian.time.second.number(), 0);
381    /// ```
382    pub fn try_new_ethiopian_datetime(
383        era_style: EthiopianEraStyle,
384        year: i32,
385        month: u8,
386        day: u8,
387        hour: u8,
388        minute: u8,
389        second: u8,
390    ) -> Result<DateTime<Ethiopian>, CalendarError> {
391        Ok(DateTime {
392            date: Date::try_new_ethiopian_date(era_style, year, month, day)?,
393            time: Time::try_new(hour, minute, second, 0)?,
394        })
395    }
396}
397
398#[cfg(test)]
399mod test {
400    use super::*;
401
402    #[test]
403    fn test_leap_year() {
404        // 11th September 2023 in gregorian is 6/13/2015 in ethiopian
405        let iso_date = Date::try_new_iso_date(2023, 9, 11).unwrap();
406        let ethiopian_date = Ethiopian::new().date_from_iso(iso_date);
407        assert_eq!(ethiopian_date.0.year, 2015);
408        assert_eq!(ethiopian_date.0.month, 13);
409        assert_eq!(ethiopian_date.0.day, 6);
410    }
411
412    #[test]
413    fn test_iso_to_ethiopian_conversion_and_back() {
414        let iso_date = Date::try_new_iso_date(1970, 1, 2).unwrap();
415        let date_ethiopian = Date::new_from_iso(iso_date, Ethiopian::new());
416
417        assert_eq!(date_ethiopian.inner.0.year, 1962);
418        assert_eq!(date_ethiopian.inner.0.month, 4);
419        assert_eq!(date_ethiopian.inner.0.day, 24);
420
421        assert_eq!(
422            date_ethiopian.to_iso(),
423            Date::try_new_iso_date(1970, 1, 2).unwrap()
424        );
425    }
426
427    #[test]
428    fn test_roundtrip_negative() {
429        // https://github.com/unicode-org/icu4x/issues/2254
430        let iso_date = Date::try_new_iso_date(-1000, 3, 3).unwrap();
431        let ethiopian = iso_date.to_calendar(Ethiopian::new());
432        let recovered_iso = ethiopian.to_iso();
433        assert_eq!(iso_date, recovered_iso);
434    }
435}