1use 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
18const AMETE_MIHRET_OFFSET: i32 = -276;
20
21const AMETE_ALEM_OFFSET: i32 = -5776;
23
24#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
26#[non_exhaustive]
27pub enum EthiopianEraStyle {
28 AmeteMihret,
31 AmeteAlem,
33}
34
35#[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)] #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
83pub struct EthiopianDateInner(CopticDateInner);
84
85impl DateFieldsResolver for Ethiopian {
86 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 pub const fn new() -> Self {
275 Self(EthiopianEraStyle::AmeteMihret)
276 }
277
278 pub const fn new_with_era_style(era_style: EthiopianEraStyle) -> Self {
280 Self(era_style)
281 }
282
283 pub fn era_style(&self) -> EthiopianEraStyle {
285 self.0
286 }
287}
288
289impl Date<Ethiopian> {
290 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 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 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}