1use 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
43const AMETE_ALEM_OFFSET: i32 = 5500;
45
46#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
48#[non_exhaustive]
49pub enum EthiopianEraStyle {
50 AmeteMihret,
53 AmeteAlem,
56}
57
58#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
82pub struct Ethiopian(pub(crate) bool);
83
84#[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 pub const fn new() -> Self {
243 Self(false)
244 }
245 pub const fn new_with_era_style(era_style: EthiopianEraStyle) -> Self {
247 Self(matches!(era_style, EthiopianEraStyle::AmeteAlem))
248 }
249 pub fn set_era_style(&mut self, era_style: EthiopianEraStyle) {
251 self.0 = era_style == EthiopianEraStyle::AmeteAlem
252 }
253
254 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 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 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 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 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}