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
5use crate::cal::coptic::CopticDateInner;
6use crate::cal::Coptic;
7use crate::calendar_arithmetic::{ArithmeticDate, DateFieldsResolver};
8use crate::error::{
9    DateError, DateFromFieldsError, EcmaReferenceYearError, MonthCodeError, UnknownEraError,
10};
11use crate::options::DateFromFieldsOptions;
12use crate::options::{DateAddOptions, DateDifferenceOptions};
13use crate::types::DateFields;
14use crate::{types, Calendar, Date, RangeError};
15use calendrical_calculations::rata_die::RataDie;
16use tinystr::tinystr;
17
18/// The Coptic year of the Amete Mihret epoch
19const AMETE_MIHRET_OFFSET: i32 = -276;
20
21/// The Coptic year of the Amete Alem epoch
22const AMETE_ALEM_OFFSET: i32 = -5776;
23
24/// Which era style the ethiopian calendar uses
25#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
26#[non_exhaustive]
27pub enum EthiopianEraStyle {
28    /// Use the Anno Mundi era, anchored at the date of Creation, followed by the
29    /// Incarnation era, anchored at the date of the Incarnation of Jesus
30    AmeteMihret,
31    /// Use the single Anno Mundi era, anchored at the date of Creation
32    AmeteAlem,
33}
34
35/// The [Ethiopian Calendar](https://en.wikipedia.org/wiki/Ethiopian_calendar)
36///
37/// The Ethiopian calendar is a variant of the [`Coptic`] calendar. It differs
38/// from the Coptic calendar by the names of the months as well as the era.
39///
40/// This implementation can be constructed in two modes: using the Amete Alem era
41/// scheme, or the Amete Mihret era scheme (the default), see [`EthiopianEraStyle`]
42/// for more info.
43///
44/// This implementation extends proleptically for dates before the calendar's creation.
45///
46/// This corresponds to the `"ethiopic"` and `"ethioaa"` [CLDR calendars](https://unicode.org/reports/tr35/#UnicodeCalendarIdentifier),
47/// with `"ethiopic"` being for [`EthiopianEraStyle::AmeteMihret`]
48///
49/// # Era codes
50///
51/// This calendar always uses the `aa` era, where 1 Amete Alem is 5493 BCE. Dates before this era use negative years.
52/// Dates before that use negative year numbers.
53///
54/// In the Amete Mihret scheme it uses the additional `am` era, 1 Amete Mihret is 9 CE.
55///
56/// # Months and days
57///
58/// The 13 months are called Mäskäräm (`M01`, 30 days), Ṭəqəmt (`M02`, 30 days),
59/// Ḫədar (`M03`, 30 days), Taḫśaś (`M04`, 30 days), Ṭərr (`M05`, 30 days), Yäkatit (`M06`, 30 days),
60/// Mägabit (`M07`, 30 days), Miyazya (`M08`, 30 days), Gənbo (`M09`, 30 days),
61/// Säne (`M10`, 30 days), Ḥamle (`M11`, 30 days), Nähase (`M12`, 30 days), Ṗagʷəmen (`M13`, 5 days).
62///
63/// In leap years (years divisible by 4), Ṗagʷəmen gains a 6th day.
64///
65/// Standard years thus have 365 days, and leap years 366.
66///
67/// # Calendar drift
68///
69/// The Ethiopian calendar has the same year lengths and leap year rules as the [`Coptic`] and
70/// [`Julian`](crate::cal::Julian) calendars, so it experiences the same drift of 1 day in ~128
71/// years with respect to the seasons.
72#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
73pub struct Ethiopian(EthiopianEraStyle);
74
75impl Default for Ethiopian {
76    fn default() -> Self {
77        Self(EthiopianEraStyle::AmeteMihret)
78    }
79}
80
81#[allow(missing_docs)] // not actually public
82#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
83pub struct EthiopianDateInner(CopticDateInner);
84
85impl DateFieldsResolver for Ethiopian {
86    // Coptic year
87    type YearInfo = i32;
88
89    fn days_in_provided_month(year: Self::YearInfo, month: u8) -> u8 {
90        Coptic::days_in_provided_month(year, month)
91    }
92
93    fn months_in_provided_year(year: Self::YearInfo) -> u8 {
94        Coptic::months_in_provided_year(year)
95    }
96
97    #[inline]
98    fn year_info_from_era(
99        &self,
100        era: &[u8],
101        era_year: i32,
102    ) -> Result<Self::YearInfo, UnknownEraError> {
103        match (self.era_style(), era) {
104            (EthiopianEraStyle::AmeteMihret, b"am") => Ok(era_year + AMETE_MIHRET_OFFSET),
105            (_, b"aa") => Ok(era_year + AMETE_ALEM_OFFSET),
106            (_, _) => Err(UnknownEraError),
107        }
108    }
109
110    #[inline]
111    fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo {
112        extended_year
113            + if self.0 == EthiopianEraStyle::AmeteMihret {
114                AMETE_MIHRET_OFFSET
115            } else {
116                AMETE_ALEM_OFFSET
117            }
118    }
119
120    #[inline]
121    fn reference_year_from_month_day(
122        &self,
123        month_code: types::ValidMonthCode,
124        day: u8,
125    ) -> Result<Self::YearInfo, EcmaReferenceYearError> {
126        crate::cal::Coptic::reference_year_from_month_day(month_code, day)
127    }
128
129    #[inline]
130    fn ordinal_month_from_code(
131        &self,
132        _year: &Self::YearInfo,
133        month_code: types::ValidMonthCode,
134        _options: DateFromFieldsOptions,
135    ) -> Result<u8, MonthCodeError> {
136        match month_code.to_tuple() {
137            (month_number @ 1..=13, false) => Ok(month_number),
138            _ => Err(MonthCodeError::NotInCalendar),
139        }
140    }
141}
142
143impl crate::cal::scaffold::UnstableSealed for Ethiopian {}
144impl Calendar for Ethiopian {
145    type DateInner = EthiopianDateInner;
146    type Year = <Coptic as Calendar>::Year;
147    type DifferenceError = <Coptic as Calendar>::DifferenceError;
148
149    fn from_codes(
150        &self,
151        era: Option<&str>,
152        year: i32,
153        month_code: types::MonthCode,
154        day: u8,
155    ) -> Result<Self::DateInner, DateError> {
156        ArithmeticDate::from_codes(era, year, month_code, day, self)
157            .map(ArithmeticDate::cast)
158            .map(CopticDateInner)
159            .map(EthiopianDateInner)
160    }
161
162    #[cfg(feature = "unstable")]
163    fn from_fields(
164        &self,
165        fields: DateFields,
166        options: DateFromFieldsOptions,
167    ) -> Result<Self::DateInner, DateFromFieldsError> {
168        ArithmeticDate::from_fields(fields, options, self)
169            .map(ArithmeticDate::cast)
170            .map(CopticDateInner)
171            .map(EthiopianDateInner)
172    }
173
174    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
175        EthiopianDateInner(Coptic.from_rata_die(rd))
176    }
177
178    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
179        Coptic.to_rata_die(&date.0)
180    }
181
182    fn has_cheap_iso_conversion(&self) -> bool {
183        false
184    }
185
186    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
187        Coptic.months_in_year(&date.0)
188    }
189
190    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
191        Coptic.days_in_year(&date.0)
192    }
193
194    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
195        Coptic.days_in_month(&date.0)
196    }
197
198    #[cfg(feature = "unstable")]
199    fn add(
200        &self,
201        date: &Self::DateInner,
202        duration: types::DateDuration,
203        options: DateAddOptions,
204    ) -> Result<Self::DateInner, DateError> {
205        Coptic
206            .add(&date.0, duration, options)
207            .map(EthiopianDateInner)
208    }
209
210    #[cfg(feature = "unstable")]
211    fn until(
212        &self,
213        date1: &Self::DateInner,
214        date2: &Self::DateInner,
215        options: DateDifferenceOptions,
216    ) -> Result<types::DateDuration, Self::DifferenceError> {
217        Coptic.until(&date1.0, &date2.0, options)
218    }
219
220    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
221        let coptic_year = date.0 .0.year;
222        let extended_year = if self.0 == EthiopianEraStyle::AmeteAlem {
223            coptic_year - AMETE_ALEM_OFFSET
224        } else {
225            coptic_year - AMETE_MIHRET_OFFSET
226        };
227
228        if self.0 == EthiopianEraStyle::AmeteAlem || extended_year <= 0 {
229            types::EraYear {
230                era: tinystr!(16, "aa"),
231                era_index: Some(0),
232                year: coptic_year - AMETE_ALEM_OFFSET,
233                extended_year,
234                ambiguity: types::YearAmbiguity::CenturyRequired,
235            }
236        } else {
237            types::EraYear {
238                era: tinystr!(16, "am"),
239                era_index: Some(1),
240                year: coptic_year - AMETE_MIHRET_OFFSET,
241                extended_year,
242                ambiguity: types::YearAmbiguity::CenturyRequired,
243            }
244        }
245    }
246
247    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
248        Coptic.is_in_leap_year(&date.0)
249    }
250
251    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
252        Coptic.month(&date.0)
253    }
254
255    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
256        Coptic.day_of_month(&date.0)
257    }
258
259    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
260        Coptic.day_of_year(&date.0)
261    }
262
263    fn debug_name(&self) -> &'static str {
264        "Ethiopian"
265    }
266
267    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
268        Some(crate::preferences::CalendarAlgorithm::Ethiopic)
269    }
270}
271
272impl Ethiopian {
273    /// Construct a new Ethiopian Calendar for the Amete Mihret era naming scheme
274    pub const fn new() -> Self {
275        Self(EthiopianEraStyle::AmeteMihret)
276    }
277
278    /// Construct a new Ethiopian Calendar with an explicit [`EthiopianEraStyle`].
279    pub const fn new_with_era_style(era_style: EthiopianEraStyle) -> Self {
280        Self(era_style)
281    }
282
283    /// Returns the [`EthiopianEraStyle`] used by this calendar.
284    pub fn era_style(&self) -> EthiopianEraStyle {
285        self.0
286    }
287}
288
289impl Date<Ethiopian> {
290    /// Construct new Ethiopian Date.
291    ///
292    /// ```rust
293    /// use icu::calendar::cal::EthiopianEraStyle;
294    /// use icu::calendar::Date;
295    ///
296    /// let date_ethiopian =
297    ///     Date::try_new_ethiopian(EthiopianEraStyle::AmeteMihret, 2014, 8, 25)
298    ///         .expect("Failed to initialize Ethopic Date instance.");
299    ///
300    /// assert_eq!(date_ethiopian.era_year().year, 2014);
301    /// assert_eq!(date_ethiopian.month().ordinal, 8);
302    /// assert_eq!(date_ethiopian.day_of_month().0, 25);
303    /// ```
304    pub fn try_new_ethiopian(
305        era_style: EthiopianEraStyle,
306        year: i32,
307        month: u8,
308        day: u8,
309    ) -> Result<Date<Ethiopian>, RangeError> {
310        let year = Ethiopian(era_style).year_info_from_extended(year);
311        ArithmeticDate::try_from_ymd(year, month, day)
312            .map(CopticDateInner)
313            .map(EthiopianDateInner)
314            .map(|inner| Date::from_raw(inner, Ethiopian(era_style)))
315    }
316}
317
318#[cfg(test)]
319mod test {
320    use super::*;
321
322    #[test]
323    fn test_leap_year() {
324        // 11th September 2023 in gregorian is 6/13/2015 in ethiopian
325        let iso_date = Date::try_new_iso(2023, 9, 11).unwrap();
326        let date_ethiopian = Date::new_from_iso(iso_date, Ethiopian::new());
327        assert_eq!(date_ethiopian.extended_year(), 2015);
328        assert_eq!(date_ethiopian.month().ordinal, 13);
329        assert_eq!(date_ethiopian.day_of_month().0, 6);
330    }
331
332    #[test]
333    fn test_iso_to_ethiopian_conversion_and_back() {
334        let iso_date = Date::try_new_iso(1970, 1, 2).unwrap();
335        let date_ethiopian = Date::new_from_iso(iso_date, Ethiopian::new());
336
337        assert_eq!(date_ethiopian.extended_year(), 1962);
338        assert_eq!(date_ethiopian.month().ordinal, 4);
339        assert_eq!(date_ethiopian.day_of_month().0, 24);
340
341        assert_eq!(
342            date_ethiopian.to_iso(),
343            Date::try_new_iso(1970, 1, 2).unwrap()
344        );
345    }
346
347    #[test]
348    fn test_iso_to_ethiopian_aa_conversion_and_back() {
349        let iso_date = Date::try_new_iso(1970, 1, 2).unwrap();
350        let date_ethiopian = Date::new_from_iso(iso_date, Ethiopian(EthiopianEraStyle::AmeteAlem));
351
352        assert_eq!(date_ethiopian.extended_year(), 7462);
353        assert_eq!(date_ethiopian.month().ordinal, 4);
354        assert_eq!(date_ethiopian.day_of_month().0, 24);
355
356        assert_eq!(
357            date_ethiopian.to_iso(),
358            Date::try_new_iso(1970, 1, 2).unwrap()
359        );
360    }
361
362    #[test]
363    fn test_roundtrip_negative() {
364        // https://github.com/unicode-org/icu4x/issues/2254
365        let iso_date = Date::try_new_iso(-1000, 3, 3).unwrap();
366        let ethiopian = iso_date.to_calendar(Ethiopian::new());
367        let recovered_iso = ethiopian.to_iso();
368        assert_eq!(iso_date, recovered_iso);
369    }
370
371    #[test]
372    fn extended_year() {
373        assert_eq!(
374            Date::new_from_iso(
375                Date::try_new_iso(-5500 + 9, 1, 1).unwrap(),
376                Ethiopian(EthiopianEraStyle::AmeteAlem)
377            )
378            .extended_year(),
379            1
380        );
381        assert_eq!(
382            Date::new_from_iso(
383                Date::try_new_iso(9, 1, 1).unwrap(),
384                Ethiopian(EthiopianEraStyle::AmeteAlem)
385            )
386            .extended_year(),
387            5501
388        );
389
390        assert_eq!(
391            Date::new_from_iso(
392                Date::try_new_iso(-5500 + 9, 1, 1).unwrap(),
393                Ethiopian(EthiopianEraStyle::AmeteMihret)
394            )
395            .extended_year(),
396            -5499
397        );
398        assert_eq!(
399            Date::new_from_iso(
400                Date::try_new_iso(9, 1, 1).unwrap(),
401                Ethiopian(EthiopianEraStyle::AmeteMihret)
402            )
403            .extended_year(),
404            1
405        );
406    }
407}