Coverage Report

Created: 2025-11-24 06:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/icu_calendar-1.5.2/src/ethiopian.rs
Line
Count
Source
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
5
//! This module contains types and implementations for the Ethiopian calendar.
6
//!
7
//! ```rust
8
//! use icu::calendar::{ethiopian::Ethiopian, Date, DateTime};
9
//!
10
//! // `Date` type
11
//! let date_iso = Date::try_new_iso_date(1970, 1, 2)
12
//!     .expect("Failed to initialize ISO Date instance.");
13
//! let date_ethiopian = Date::new_from_iso(date_iso, Ethiopian::new());
14
//!
15
//! // `DateTime` type
16
//! let datetime_iso = DateTime::try_new_iso_datetime(1970, 1, 2, 13, 1, 0)
17
//!     .expect("Failed to initialize ISO DateTime instance.");
18
//! let datetime_ethiopian =
19
//!     DateTime::new_from_iso(datetime_iso, Ethiopian::new());
20
//!
21
//! // `Date` checks
22
//! assert_eq!(date_ethiopian.year().number, 1962);
23
//! assert_eq!(date_ethiopian.month().ordinal, 4);
24
//! assert_eq!(date_ethiopian.day_of_month().0, 24);
25
//!
26
//! // `DateTime` type
27
//! assert_eq!(datetime_ethiopian.date.year().number, 1962);
28
//! assert_eq!(datetime_ethiopian.date.month().ordinal, 4);
29
//! assert_eq!(datetime_ethiopian.date.day_of_month().0, 24);
30
//! assert_eq!(datetime_ethiopian.time.hour.number(), 13);
31
//! assert_eq!(datetime_ethiopian.time.minute.number(), 1);
32
//! assert_eq!(datetime_ethiopian.time.second.number(), 0);
33
//! ```
34
35
use crate::any_calendar::AnyCalendarKind;
36
use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
37
use crate::iso::Iso;
38
use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time};
39
use calendrical_calculations::helpers::I32CastError;
40
use calendrical_calculations::rata_die::RataDie;
41
use tinystr::tinystr;
42
43
/// The number of years the Amete Alem epoch precedes the Amete Mihret epoch
44
const AMETE_ALEM_OFFSET: i32 = 5500;
45
46
/// Which era style the ethiopian calendar uses
47
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
48
#[non_exhaustive]
49
pub enum EthiopianEraStyle {
50
    /// Use an era scheme of pre- and post- Incarnation eras,
51
    /// anchored at the date of the Incarnation of Jesus in this calendar
52
    AmeteMihret,
53
    /// Use an era scheme of the Anno Mundi era, anchored at the date of Creation
54
    /// in this calendar
55
    AmeteAlem,
56
}
57
58
/// The [Ethiopian Calendar]
59
///
60
/// The [Ethiopian calendar] is a solar calendar used by the Coptic Orthodox Church, with twelve normal months
61
/// and a thirteenth small epagomenal month.
62
///
63
/// This type can be used with [`Date`] or [`DateTime`] to represent dates in this calendar.
64
///
65
/// It can be constructed in two modes: using the Amete Alem era scheme, or the Amete Mihret era scheme (the default),
66
/// see [`EthiopianEraStyle`] for more info.
67
///
68
/// [Ethiopian calendar]: https://en.wikipedia.org/wiki/Ethiopian_calendar
69
///
70
/// # Era codes
71
///
72
/// This calendar supports three era codes, based on what mode it is in. In the Amete Mihret scheme it has
73
/// the `"incar"` and `"pre-incar"` eras, 1 Incarnation is 9 CE. In the Amete Alem scheme, it instead has a single era,
74
/// `"mundi`, where 1 Anno Mundi is 5493 BCE. Dates before that use negative year numbers.
75
///
76
/// # Month codes
77
///
78
/// This calendar supports 13 solar month codes (`"M01" - "M13"`), with `"M13"` being used for the short epagomenal month
79
/// at the end of the year.
80
// The bool specifies whether dates should be in the Amete Alem era scheme
81
#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
82
pub struct Ethiopian(pub(crate) bool);
83
84
/// The inner date type used for representing [`Date`]s of [`Ethiopian`]. See [`Date`] and [`Ethiopian`] for more details.
85
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
86
pub struct EthiopianDateInner(ArithmeticDate<Ethiopian>);
87
88
impl CalendarArithmetic for Ethiopian {
89
    type YearInfo = ();
90
91
0
    fn month_days(year: i32, month: u8, _data: ()) -> u8 {
92
0
        if (1..=12).contains(&month) {
93
0
            30
94
0
        } else if month == 13 {
95
0
            if Self::is_leap_year(year, ()) {
96
0
                6
97
            } else {
98
0
                5
99
            }
100
        } else {
101
0
            0
102
        }
103
0
    }
104
105
0
    fn months_for_every_year(_: i32, _data: ()) -> u8 {
106
0
        13
107
0
    }
108
109
0
    fn is_leap_year(year: i32, _data: ()) -> bool {
110
0
        year.rem_euclid(4) == 3
111
0
    }
112
113
0
    fn last_month_day_in_year(year: i32, _data: ()) -> (u8, u8) {
114
0
        if Self::is_leap_year(year, ()) {
115
0
            (13, 6)
116
        } else {
117
0
            (13, 5)
118
        }
119
0
    }
120
121
0
    fn days_in_provided_year(year: i32, _data: ()) -> u16 {
122
0
        if Self::is_leap_year(year, ()) {
123
0
            366
124
        } else {
125
0
            365
126
        }
127
0
    }
128
}
129
130
impl Calendar for Ethiopian {
131
    type DateInner = EthiopianDateInner;
132
0
    fn date_from_codes(
133
0
        &self,
134
0
        era: types::Era,
135
0
        year: i32,
136
0
        month_code: types::MonthCode,
137
0
        day: u8,
138
0
    ) -> Result<Self::DateInner, CalendarError> {
139
0
        let year = if era.0 == tinystr!(16, "incar") {
140
0
            if year <= 0 {
141
0
                return Err(CalendarError::OutOfRange);
142
0
            }
143
0
            year
144
0
        } else if era.0 == tinystr!(16, "pre-incar") {
145
0
            if year <= 0 {
146
0
                return Err(CalendarError::OutOfRange);
147
0
            }
148
0
            1 - year
149
0
        } else if era.0 == tinystr!(16, "mundi") {
150
0
            year - AMETE_ALEM_OFFSET
151
        } else {
152
0
            return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
153
        };
154
155
0
        ArithmeticDate::new_from_codes(self, year, month_code, day).map(EthiopianDateInner)
156
0
    }
157
0
    fn date_from_iso(&self, iso: Date<Iso>) -> EthiopianDateInner {
158
0
        let fixed_iso = Iso::fixed_from_iso(*iso.inner());
159
0
        Self::ethiopian_from_fixed(fixed_iso)
160
0
    }
161
162
0
    fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
163
0
        let fixed_ethiopian = Ethiopian::fixed_from_ethiopian(date.0);
164
0
        Iso::iso_from_fixed(fixed_ethiopian)
165
0
    }
166
167
0
    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
168
0
        date.0.months_in_year()
169
0
    }
170
171
0
    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
172
0
        date.0.days_in_year()
173
0
    }
174
175
0
    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
176
0
        date.0.days_in_month()
177
0
    }
178
179
0
    fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {
180
0
        Iso.day_of_week(self.date_to_iso(date).inner())
181
0
    }
182
183
0
    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
184
0
        date.0.offset_date(offset, &());
185
0
    }
186
187
    #[allow(clippy::field_reassign_with_default)]
188
0
    fn until(
189
0
        &self,
190
0
        date1: &Self::DateInner,
191
0
        date2: &Self::DateInner,
192
0
        _calendar2: &Self,
193
0
        _largest_unit: DateDurationUnit,
194
0
        _smallest_unit: DateDurationUnit,
195
0
    ) -> DateDuration<Self> {
196
0
        date1.0.until(date2.0, _largest_unit, _smallest_unit)
197
0
    }
198
199
0
    fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
200
0
        Self::year_as_ethiopian(date.0.year, self.0)
201
0
    }
202
203
0
    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
204
0
        Self::is_leap_year(date.0.year, ())
205
0
    }
206
207
0
    fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
208
0
        date.0.month()
209
0
    }
210
211
0
    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
212
0
        date.0.day_of_month()
213
0
    }
214
215
0
    fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
216
0
        let prev_year = date.0.year - 1;
217
0
        let next_year = date.0.year + 1;
218
0
        types::DayOfYearInfo {
219
0
            day_of_year: date.0.day_of_year(),
220
0
            days_in_year: date.0.days_in_year(),
221
0
            prev_year: Self::year_as_ethiopian(prev_year, self.0),
222
0
            days_in_prev_year: Ethiopian::days_in_year_direct(prev_year),
223
0
            next_year: Self::year_as_ethiopian(next_year, self.0),
224
0
        }
225
0
    }
226
227
0
    fn debug_name(&self) -> &'static str {
228
0
        "Ethiopian"
229
0
    }
230
231
0
    fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
232
0
        if self.0 {
233
0
            Some(AnyCalendarKind::EthiopianAmeteAlem)
234
        } else {
235
0
            Some(AnyCalendarKind::Ethiopian)
236
        }
237
0
    }
238
}
239
240
impl Ethiopian {
241
    /// Construct a new Ethiopian Calendar for the Amete Mihret era naming scheme
242
0
    pub const fn new() -> Self {
243
0
        Self(false)
244
0
    }
245
    /// Construct a new Ethiopian Calendar with a value specifying whether or not it is Amete Alem
246
0
    pub const fn new_with_era_style(era_style: EthiopianEraStyle) -> Self {
247
0
        Self(matches!(era_style, EthiopianEraStyle::AmeteAlem))
248
0
    }
249
    /// Set whether or not this uses the Amete Alem era scheme
250
0
    pub fn set_era_style(&mut self, era_style: EthiopianEraStyle) {
251
0
        self.0 = era_style == EthiopianEraStyle::AmeteAlem
252
0
    }
253
254
    /// Returns whether this has the Amete Alem era
255
0
    pub fn era_style(&self) -> EthiopianEraStyle {
256
0
        if self.0 {
257
0
            EthiopianEraStyle::AmeteAlem
258
        } else {
259
0
            EthiopianEraStyle::AmeteMihret
260
        }
261
0
    }
262
263
0
    fn fixed_from_ethiopian(date: ArithmeticDate<Ethiopian>) -> RataDie {
264
0
        calendrical_calculations::ethiopian::fixed_from_ethiopian(date.year, date.month, date.day)
265
0
    }
266
267
0
    fn ethiopian_from_fixed(date: RataDie) -> EthiopianDateInner {
268
0
        let (year, month, day) =
269
0
            match calendrical_calculations::ethiopian::ethiopian_from_fixed(date) {
270
                Err(I32CastError::BelowMin) => {
271
0
                    return EthiopianDateInner(ArithmeticDate::min_date())
272
                }
273
                Err(I32CastError::AboveMax) => {
274
0
                    return EthiopianDateInner(ArithmeticDate::max_date())
275
                }
276
0
                Ok(ymd) => ymd,
277
            };
278
0
        EthiopianDateInner(ArithmeticDate::new_unchecked(year, month, day))
279
0
    }
280
281
0
    fn days_in_year_direct(year: i32) -> u16 {
282
0
        if Ethiopian::is_leap_year(year, ()) {
283
0
            366
284
        } else {
285
0
            365
286
        }
287
0
    }
288
289
0
    fn year_as_ethiopian(year: i32, amete_alem: bool) -> types::FormattableYear {
290
0
        if amete_alem {
291
0
            types::FormattableYear {
292
0
                era: types::Era(tinystr!(16, "mundi")),
293
0
                number: year + AMETE_ALEM_OFFSET,
294
0
                cyclic: None,
295
0
                related_iso: None,
296
0
            }
297
0
        } else if year > 0 {
298
0
            types::FormattableYear {
299
0
                era: types::Era(tinystr!(16, "incar")),
300
0
                number: year,
301
0
                cyclic: None,
302
0
                related_iso: None,
303
0
            }
304
        } else {
305
0
            types::FormattableYear {
306
0
                era: types::Era(tinystr!(16, "pre-incar")),
307
0
                number: 1 - year,
308
0
                cyclic: None,
309
0
                related_iso: None,
310
0
            }
311
        }
312
0
    }
313
}
314
315
impl Date<Ethiopian> {
316
    /// Construct new Ethiopian Date.
317
    ///
318
    /// For the Amete Mihret era style, negative years work with
319
    /// year 0 as 1 pre-Incarnation, year -1 as 2 pre-Incarnation,
320
    /// and so on.
321
    ///
322
    /// ```rust
323
    /// use icu::calendar::ethiopian::EthiopianEraStyle;
324
    /// use icu::calendar::Date;
325
    ///
326
    /// let date_ethiopian = Date::try_new_ethiopian_date(
327
    ///     EthiopianEraStyle::AmeteMihret,
328
    ///     2014,
329
    ///     8,
330
    ///     25,
331
    /// )
332
    /// .expect("Failed to initialize Ethopic Date instance.");
333
    ///
334
    /// assert_eq!(date_ethiopian.year().number, 2014);
335
    /// assert_eq!(date_ethiopian.month().ordinal, 8);
336
    /// assert_eq!(date_ethiopian.day_of_month().0, 25);
337
    /// ```
338
0
    pub fn try_new_ethiopian_date(
339
0
        era_style: EthiopianEraStyle,
340
0
        mut year: i32,
341
0
        month: u8,
342
0
        day: u8,
343
0
    ) -> Result<Date<Ethiopian>, CalendarError> {
344
0
        if era_style == EthiopianEraStyle::AmeteAlem {
345
0
            year -= AMETE_ALEM_OFFSET;
346
0
        }
347
0
        ArithmeticDate::new_from_ordinals(year, month, day)
348
0
            .map(EthiopianDateInner)
349
0
            .map(|inner| Date::from_raw(inner, Ethiopian::new_with_era_style(era_style)))
350
0
    }
351
}
352
353
impl DateTime<Ethiopian> {
354
    /// Construct a new Ethiopian datetime from integers.
355
    ///
356
    /// For the Amete Mihret era style, negative years work with
357
    /// year 0 as 1 pre-Incarnation, year -1 as 2 pre-Incarnation,
358
    /// and so on.
359
    ///
360
    /// ```rust
361
    /// use icu::calendar::ethiopian::EthiopianEraStyle;
362
    /// use icu::calendar::DateTime;
363
    ///
364
    /// let datetime_ethiopian = DateTime::try_new_ethiopian_datetime(
365
    ///     EthiopianEraStyle::AmeteMihret,
366
    ///     2014,
367
    ///     8,
368
    ///     25,
369
    ///     13,
370
    ///     1,
371
    ///     0,
372
    /// )
373
    /// .expect("Failed to initialize Ethiopian DateTime instance.");
374
    ///
375
    /// assert_eq!(datetime_ethiopian.date.year().number, 2014);
376
    /// assert_eq!(datetime_ethiopian.date.month().ordinal, 8);
377
    /// assert_eq!(datetime_ethiopian.date.day_of_month().0, 25);
378
    /// assert_eq!(datetime_ethiopian.time.hour.number(), 13);
379
    /// assert_eq!(datetime_ethiopian.time.minute.number(), 1);
380
    /// assert_eq!(datetime_ethiopian.time.second.number(), 0);
381
    /// ```
382
0
    pub fn try_new_ethiopian_datetime(
383
0
        era_style: EthiopianEraStyle,
384
0
        year: i32,
385
0
        month: u8,
386
0
        day: u8,
387
0
        hour: u8,
388
0
        minute: u8,
389
0
        second: u8,
390
0
    ) -> Result<DateTime<Ethiopian>, CalendarError> {
391
        Ok(DateTime {
392
0
            date: Date::try_new_ethiopian_date(era_style, year, month, day)?,
393
0
            time: Time::try_new(hour, minute, second, 0)?,
394
        })
395
0
    }
396
}
397
398
#[cfg(test)]
399
mod test {
400
    use super::*;
401
402
    #[test]
403
    fn test_leap_year() {
404
        // 11th September 2023 in gregorian is 6/13/2015 in ethiopian
405
        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
        // https://github.com/unicode-org/icu4x/issues/2254
430
        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
}