icu_calendar/cal/
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::{cal::Ethiopian, Date};
9//!
10//! let date_iso = Date::try_new_iso(1970, 1, 2)
11//!     .expect("Failed to initialize ISO Date instance.");
12//! let date_ethiopian = Date::new_from_iso(date_iso, Ethiopian::new());
13//!
14//! assert_eq!(date_ethiopian.era_year().year, 1962);
15//! assert_eq!(date_ethiopian.month().ordinal, 4);
16//! assert_eq!(date_ethiopian.day_of_month().0, 24);
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 number of years the Amete Alem epoch precedes the Amete Mihret epoch
28const INCARNATION_OFFSET: i32 = 5500;
29
30/// Which era style the ethiopian calendar uses
31#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
32#[non_exhaustive]
33pub enum EthiopianEraStyle {
34    /// Use the Anno Mundi era, anchored at the date of Creation, followed by the
35    /// Incarnation era, anchored at the date of the Incarnation of Jesus
36    AmeteMihret,
37    /// Use the single Anno Mundi era, anchored at the date of Creation
38    AmeteAlem,
39}
40
41/// The [Ethiopian Calendar]
42///
43/// The [Ethiopian calendar] is a solar calendar used by the Coptic Orthodox Church, with twelve normal months
44/// and a thirteenth small epagomenal month.
45///
46/// This type can be used with [`Date`] to represent dates in this calendar.
47///
48/// It can be constructed in two modes: using the Amete Alem era scheme, or the Amete Mihret era scheme (the default),
49/// see [`EthiopianEraStyle`] for more info.
50///
51/// [Ethiopian calendar]: https://en.wikipedia.org/wiki/Ethiopian_calendar
52///
53/// # Era codes
54///
55/// This calendar always uses the `aa` era, where 1 Amete Alem is 5493 BCE. Dates before this era use negative years.
56/// Dates before that use negative year numbers.
57/// In the Amete Mihret scheme it uses the additional `am` era, 1 Amete Mihret is 9 CE.
58///
59/// # Month codes
60///
61/// This calendar supports 13 solar month codes (`"M01" - "M13"`), with `"M13"` being used for the short epagomenal month
62/// at the end of the year.
63// The bool specifies whether dates should be in the Amete Alem era scheme
64#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
65pub struct Ethiopian(pub(crate) bool);
66
67/// The inner date type used for representing [`Date`]s of [`Ethiopian`]. See [`Date`] and [`Ethiopian`] for more details.
68#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
69pub struct EthiopianDateInner(ArithmeticDate<Ethiopian>);
70
71impl CalendarArithmetic for Ethiopian {
72    type YearInfo = i32;
73
74    fn days_in_provided_month(year: i32, month: u8) -> u8 {
75        if (1..=12).contains(&month) {
76            30
77        } else if month == 13 {
78            if Self::provided_year_is_leap(year) {
79                6
80            } else {
81                5
82            }
83        } else {
84            0
85        }
86    }
87
88    fn months_in_provided_year(_: i32) -> u8 {
89        13
90    }
91
92    fn provided_year_is_leap(year: i32) -> bool {
93        year.rem_euclid(4) == 3
94    }
95
96    fn last_month_day_in_provided_year(year: i32) -> (u8, u8) {
97        if Self::provided_year_is_leap(year) {
98            (13, 6)
99        } else {
100            (13, 5)
101        }
102    }
103
104    fn days_in_provided_year(year: i32) -> u16 {
105        if Self::provided_year_is_leap(year) {
106            366
107        } else {
108            365
109        }
110    }
111}
112
113impl crate::cal::scaffold::UnstableSealed for Ethiopian {}
114impl Calendar for Ethiopian {
115    type DateInner = EthiopianDateInner;
116    type Year = types::EraYear;
117    fn from_codes(
118        &self,
119        era: Option<&str>,
120        year: i32,
121        month_code: types::MonthCode,
122        day: u8,
123    ) -> Result<Self::DateInner, DateError> {
124        let year = match (self.era_style(), era) {
125            (EthiopianEraStyle::AmeteMihret, Some("am") | None) => {
126                year_check(year, 1..)? + INCARNATION_OFFSET
127            }
128            (EthiopianEraStyle::AmeteMihret, Some("aa")) => {
129                year_check(year, ..=INCARNATION_OFFSET)?
130            }
131            (EthiopianEraStyle::AmeteAlem, Some("aa") | None) => year,
132            (_, Some(_)) => {
133                return Err(DateError::UnknownEra);
134            }
135        };
136        ArithmeticDate::new_from_codes(self, year, month_code, day).map(EthiopianDateInner)
137    }
138
139    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
140        EthiopianDateInner(
141            match calendrical_calculations::ethiopian::ethiopian_from_fixed(rd) {
142                Err(I32CastError::BelowMin) => ArithmeticDate::min_date(),
143                Err(I32CastError::AboveMax) => ArithmeticDate::max_date(),
144                Ok((year, month, day)) => ArithmeticDate::new_unchecked(
145                    // calendrical calculations returns years in the Incarnation era
146                    year + INCARNATION_OFFSET,
147                    month,
148                    day,
149                ),
150            },
151        )
152    }
153
154    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
155        // calendrical calculations expects years in the Incarnation era
156        calendrical_calculations::ethiopian::fixed_from_ethiopian(
157            date.0.year - INCARNATION_OFFSET,
158            date.0.month,
159            date.0.day,
160        )
161    }
162
163    fn from_iso(&self, iso: IsoDateInner) -> EthiopianDateInner {
164        self.from_rata_die(Iso.to_rata_die(&iso))
165    }
166
167    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
168        Iso.from_rata_die(self.to_rata_die(date))
169    }
170
171    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
172        date.0.months_in_year()
173    }
174
175    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
176        date.0.days_in_year()
177    }
178
179    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
180        date.0.days_in_month()
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_info(&self, date: &Self::DateInner) -> Self::Year {
200        let year = date.0.year;
201        if self.0 || year <= INCARNATION_OFFSET {
202            types::EraYear {
203                era: tinystr!(16, "aa"),
204                era_index: Some(0),
205                year,
206                ambiguity: types::YearAmbiguity::CenturyRequired,
207            }
208        } else {
209            types::EraYear {
210                era: tinystr!(16, "am"),
211                era_index: Some(1),
212                year: year - INCARNATION_OFFSET,
213                ambiguity: types::YearAmbiguity::CenturyRequired,
214            }
215        }
216    }
217
218    fn extended_year(&self, date: &Self::DateInner) -> i32 {
219        let year = date.0.extended_year();
220        if self.0 || year <= INCARNATION_OFFSET {
221            year
222        } else {
223            year - INCARNATION_OFFSET
224        }
225    }
226
227    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
228        Self::provided_year_is_leap(date.0.year)
229    }
230
231    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
232        date.0.month()
233    }
234
235    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
236        date.0.day_of_month()
237    }
238
239    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
240        date.0.day_of_year()
241    }
242
243    fn debug_name(&self) -> &'static str {
244        "Ethiopian"
245    }
246
247    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
248        Some(crate::preferences::CalendarAlgorithm::Ethiopic)
249    }
250}
251
252impl Ethiopian {
253    /// Construct a new Ethiopian Calendar for the Amete Mihret era naming scheme
254    pub const fn new() -> Self {
255        Self(false)
256    }
257    /// Construct a new Ethiopian Calendar with a value specifying whether or not it is Amete Alem
258    pub const fn new_with_era_style(era_style: EthiopianEraStyle) -> Self {
259        Self(matches!(era_style, EthiopianEraStyle::AmeteAlem))
260    }
261
262    /// Returns whether this has the Amete Alem era
263    pub fn era_style(&self) -> EthiopianEraStyle {
264        if self.0 {
265            EthiopianEraStyle::AmeteAlem
266        } else {
267            EthiopianEraStyle::AmeteMihret
268        }
269    }
270}
271
272impl Date<Ethiopian> {
273    /// Construct new Ethiopian Date.
274    ///
275    /// ```rust
276    /// use icu::calendar::cal::EthiopianEraStyle;
277    /// use icu::calendar::Date;
278    ///
279    /// let date_ethiopian =
280    ///     Date::try_new_ethiopian(EthiopianEraStyle::AmeteMihret, 2014, 8, 25)
281    ///         .expect("Failed to initialize Ethopic Date instance.");
282    ///
283    /// assert_eq!(date_ethiopian.era_year().year, 2014);
284    /// assert_eq!(date_ethiopian.month().ordinal, 8);
285    /// assert_eq!(date_ethiopian.day_of_month().0, 25);
286    /// ```
287    pub fn try_new_ethiopian(
288        era_style: EthiopianEraStyle,
289        mut year: i32,
290        month: u8,
291        day: u8,
292    ) -> Result<Date<Ethiopian>, RangeError> {
293        if era_style == EthiopianEraStyle::AmeteAlem {
294            year -= INCARNATION_OFFSET;
295        }
296        ArithmeticDate::new_from_ordinals(year, month, day)
297            .map(EthiopianDateInner)
298            .map(|inner| Date::from_raw(inner, Ethiopian::new_with_era_style(era_style)))
299    }
300}
301
302#[cfg(test)]
303mod test {
304    use super::*;
305
306    #[test]
307    fn test_leap_year() {
308        // 11th September 2023 in gregorian is 6/13/2015 in ethiopian
309        let iso_date = Date::try_new_iso(2023, 9, 11).unwrap();
310        let date_ethiopian = Date::new_from_iso(iso_date, Ethiopian::new());
311        assert_eq!(date_ethiopian.extended_year(), 2015);
312        assert_eq!(date_ethiopian.month().ordinal, 13);
313        assert_eq!(date_ethiopian.day_of_month().0, 6);
314    }
315
316    #[test]
317    fn test_iso_to_ethiopian_conversion_and_back() {
318        let iso_date = Date::try_new_iso(1970, 1, 2).unwrap();
319        let date_ethiopian = Date::new_from_iso(iso_date, Ethiopian::new());
320
321        assert_eq!(date_ethiopian.extended_year(), 1962);
322        assert_eq!(date_ethiopian.month().ordinal, 4);
323        assert_eq!(date_ethiopian.day_of_month().0, 24);
324
325        assert_eq!(
326            date_ethiopian.to_iso(),
327            Date::try_new_iso(1970, 1, 2).unwrap()
328        );
329    }
330
331    #[test]
332    fn test_iso_to_ethiopian_aa_conversion_and_back() {
333        let iso_date = Date::try_new_iso(1970, 1, 2).unwrap();
334        let date_ethiopian = Date::new_from_iso(
335            iso_date,
336            Ethiopian::new_with_era_style(EthiopianEraStyle::AmeteAlem),
337        );
338
339        assert_eq!(date_ethiopian.extended_year(), 7462);
340        assert_eq!(date_ethiopian.month().ordinal, 4);
341        assert_eq!(date_ethiopian.day_of_month().0, 24);
342
343        assert_eq!(
344            date_ethiopian.to_iso(),
345            Date::try_new_iso(1970, 1, 2).unwrap()
346        );
347    }
348
349    #[test]
350    fn test_roundtrip_negative() {
351        // https://github.com/unicode-org/icu4x/issues/2254
352        let iso_date = Date::try_new_iso(-1000, 3, 3).unwrap();
353        let ethiopian = iso_date.to_calendar(Ethiopian::new());
354        let recovered_iso = ethiopian.to_iso();
355        assert_eq!(iso_date, recovered_iso);
356    }
357}