1use 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
27const INCARNATION_OFFSET: i32 = 5500;
29
30#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
32#[non_exhaustive]
33pub enum EthiopianEraStyle {
34    AmeteMihret,
37    AmeteAlem,
39}
40
41#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
65pub struct Ethiopian(pub(crate) bool);
66
67#[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                    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::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 {
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    pub const fn new() -> Self {
255        Self(false)
256    }
257    pub const fn new_with_era_style(era_style: EthiopianEraStyle) -> Self {
259        Self(matches!(era_style, EthiopianEraStyle::AmeteAlem))
260    }
261
262    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    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        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        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
358    #[test]
359    fn extended_year() {
360        assert_eq!(
361            Date::new_from_iso(
362                Date::try_new_iso(-5500 + 9, 1, 1).unwrap(),
363                Ethiopian::new_with_era_style(EthiopianEraStyle::AmeteAlem)
364            )
365            .extended_year(),
366            1
367        );
368        assert_eq!(
369            Date::new_from_iso(
370                Date::try_new_iso(9, 1, 1).unwrap(),
371                Ethiopian::new_with_era_style(EthiopianEraStyle::AmeteAlem)
372            )
373            .extended_year(),
374            5501
375        );
376
377        assert_eq!(
378            Date::new_from_iso(
379                Date::try_new_iso(-5500 + 9, 1, 1).unwrap(),
380                Ethiopian::new_with_era_style(EthiopianEraStyle::AmeteMihret)
381            )
382            .extended_year(),
383            -5499
384        );
385        assert_eq!(
386            Date::new_from_iso(
387                Date::try_new_iso(9, 1, 1).unwrap(),
388                Ethiopian::new_with_era_style(EthiopianEraStyle::AmeteMihret)
389            )
390            .extended_year(),
391            1
392        );
393    }
394}