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 || 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 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}