icu_calendar/
coptic.rs

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 Coptic calendar.
6//!
7//! ```rust
8//! use icu::calendar::{coptic::Coptic, 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_coptic = Date::new_from_iso(date_iso, Coptic);
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_coptic = DateTime::new_from_iso(datetime_iso, Coptic);
19//!
20//! // `Date` checks
21//! assert_eq!(date_coptic.year().number, 1686);
22//! assert_eq!(date_coptic.month().ordinal, 4);
23//! assert_eq!(date_coptic.day_of_month().0, 24);
24//!
25//! // `DateTime` type
26//! assert_eq!(datetime_coptic.date.year().number, 1686);
27//! assert_eq!(datetime_coptic.date.month().ordinal, 4);
28//! assert_eq!(datetime_coptic.date.day_of_month().0, 24);
29//! assert_eq!(datetime_coptic.time.hour.number(), 13);
30//! assert_eq!(datetime_coptic.time.minute.number(), 1);
31//! assert_eq!(datetime_coptic.time.second.number(), 0);
32//! ```
33
34use crate::any_calendar::AnyCalendarKind;
35use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
36use crate::iso::Iso;
37use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time};
38use calendrical_calculations::helpers::I32CastError;
39use calendrical_calculations::rata_die::RataDie;
40use tinystr::tinystr;
41
42/// The [Coptic Calendar]
43///
44/// The [Coptic calendar] is a solar calendar used by the Coptic Orthodox Church, with twelve normal months
45/// and a thirteenth small epagomenal month.
46///
47/// This type can be used with [`Date`] or [`DateTime`] to represent dates in this calendar.
48///
49/// [Coptic calendar]: https://en.wikipedia.org/wiki/Coptic_calendar
50///
51/// # Era codes
52///
53/// This calendar supports two era codes: `"bd"`, and `"ad"`, corresponding to the Before Diocletian and After Diocletian/Anno Martyrum
54/// eras. 1 A.M. is equivalent to 284 C.E.
55///
56/// # Month codes
57///
58/// This calendar supports 13 solar month codes (`"M01" - "M13"`), with `"M13"` being used for the short epagomenal month
59/// at the end of the year.
60#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
61#[allow(clippy::exhaustive_structs)] // this type is stable
62pub struct Coptic;
63
64/// The inner date type used for representing [`Date`]s of [`Coptic`]. See [`Date`] and [`Coptic`] for more details.
65#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
66pub struct CopticDateInner(pub(crate) ArithmeticDate<Coptic>);
67
68impl CalendarArithmetic for Coptic {
69    type YearInfo = ();
70
71    fn month_days(year: i32, month: u8, _data: ()) -> u8 {
72        if (1..=12).contains(&month) {
73            30
74        } else if month == 13 {
75            if Self::is_leap_year(year, ()) {
76                6
77            } else {
78                5
79            }
80        } else {
81            0
82        }
83    }
84
85    fn months_for_every_year(_: i32, _data: ()) -> u8 {
86        13
87    }
88
89    fn is_leap_year(year: i32, _data: ()) -> bool {
90        year.rem_euclid(4) == 3
91    }
92
93    fn last_month_day_in_year(year: i32, _data: ()) -> (u8, u8) {
94        if Self::is_leap_year(year, ()) {
95            (13, 6)
96        } else {
97            (13, 5)
98        }
99    }
100
101    fn days_in_provided_year(year: i32, _data: ()) -> u16 {
102        if Self::is_leap_year(year, ()) {
103            366
104        } else {
105            365
106        }
107    }
108}
109
110impl Calendar for Coptic {
111    type DateInner = CopticDateInner;
112    fn date_from_codes(
113        &self,
114        era: types::Era,
115        year: i32,
116        month_code: types::MonthCode,
117        day: u8,
118    ) -> Result<Self::DateInner, CalendarError> {
119        let year = if era.0 == tinystr!(16, "ad") {
120            if year <= 0 {
121                return Err(CalendarError::OutOfRange);
122            }
123            year
124        } else if era.0 == tinystr!(16, "bd") {
125            if year <= 0 {
126                return Err(CalendarError::OutOfRange);
127            }
128            1 - year
129        } else {
130            return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
131        };
132
133        ArithmeticDate::new_from_codes(self, year, month_code, day).map(CopticDateInner)
134    }
135    fn date_from_iso(&self, iso: Date<Iso>) -> CopticDateInner {
136        let fixed_iso = Iso::fixed_from_iso(*iso.inner());
137        Self::coptic_from_fixed(fixed_iso)
138    }
139
140    fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
141        let fixed_coptic = Coptic::fixed_from_coptic(date.0);
142        Iso::iso_from_fixed(fixed_coptic)
143    }
144
145    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
146        date.0.months_in_year()
147    }
148
149    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
150        date.0.days_in_year()
151    }
152
153    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
154        date.0.days_in_month()
155    }
156
157    fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {
158        Iso.day_of_week(Coptic.date_to_iso(date).inner())
159    }
160
161    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
162        date.0.offset_date(offset, &());
163    }
164
165    #[allow(clippy::field_reassign_with_default)]
166    fn until(
167        &self,
168        date1: &Self::DateInner,
169        date2: &Self::DateInner,
170        _calendar2: &Self,
171        _largest_unit: DateDurationUnit,
172        _smallest_unit: DateDurationUnit,
173    ) -> DateDuration<Self> {
174        date1.0.until(date2.0, _largest_unit, _smallest_unit)
175    }
176
177    fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
178        year_as_coptic(date.0.year)
179    }
180
181    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
182        Self::is_leap_year(date.0.year, ())
183    }
184
185    fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
186        date.0.month()
187    }
188
189    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
190        date.0.day_of_month()
191    }
192
193    fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
194        let prev_year = date.0.year - 1;
195        let next_year = date.0.year + 1;
196        types::DayOfYearInfo {
197            day_of_year: date.0.day_of_year(),
198            days_in_year: date.0.days_in_year(),
199            prev_year: year_as_coptic(prev_year),
200            days_in_prev_year: Coptic::days_in_year_direct(prev_year),
201            next_year: year_as_coptic(next_year),
202        }
203    }
204
205    fn debug_name(&self) -> &'static str {
206        "Coptic"
207    }
208
209    fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
210        Some(AnyCalendarKind::Coptic)
211    }
212}
213
214impl Coptic {
215    fn fixed_from_coptic(date: ArithmeticDate<Coptic>) -> RataDie {
216        calendrical_calculations::coptic::fixed_from_coptic(date.year, date.month, date.day)
217    }
218
219    pub(crate) fn coptic_from_fixed(date: RataDie) -> CopticDateInner {
220        let (year, month, day) = match calendrical_calculations::coptic::coptic_from_fixed(date) {
221            Err(I32CastError::BelowMin) => return CopticDateInner(ArithmeticDate::min_date()),
222            Err(I32CastError::AboveMax) => return CopticDateInner(ArithmeticDate::max_date()),
223            Ok(ymd) => ymd,
224        };
225
226        CopticDateInner(ArithmeticDate::new_unchecked(year, month, day))
227    }
228
229    fn days_in_year_direct(year: i32) -> u16 {
230        if Coptic::is_leap_year(year, ()) {
231            366
232        } else {
233            365
234        }
235    }
236}
237
238impl Date<Coptic> {
239    /// Construct new Coptic Date.
240    ///
241    /// Negative years are in the B.D. era, starting with 0 = 1 B.D.
242    ///
243    /// ```rust
244    /// use icu::calendar::Date;
245    ///
246    /// let date_coptic = Date::try_new_coptic_date(1686, 5, 6)
247    ///     .expect("Failed to initialize Coptic Date instance.");
248    ///
249    /// assert_eq!(date_coptic.year().number, 1686);
250    /// assert_eq!(date_coptic.month().ordinal, 5);
251    /// assert_eq!(date_coptic.day_of_month().0, 6);
252    /// ```
253    pub fn try_new_coptic_date(
254        year: i32,
255        month: u8,
256        day: u8,
257    ) -> Result<Date<Coptic>, CalendarError> {
258        ArithmeticDate::new_from_ordinals(year, month, day)
259            .map(CopticDateInner)
260            .map(|inner| Date::from_raw(inner, Coptic))
261    }
262}
263
264impl DateTime<Coptic> {
265    /// Construct a new Coptic datetime from integers.
266    ///
267    /// Negative years are in the B.D. era, starting with 0 = 1 B.D.
268    ///
269    /// ```rust
270    /// use icu::calendar::DateTime;
271    ///
272    /// let datetime_coptic =
273    ///     DateTime::try_new_coptic_datetime(1686, 5, 6, 13, 1, 0)
274    ///         .expect("Failed to initialize Coptic DateTime instance.");
275    ///
276    /// assert_eq!(datetime_coptic.date.year().number, 1686);
277    /// assert_eq!(datetime_coptic.date.month().ordinal, 5);
278    /// assert_eq!(datetime_coptic.date.day_of_month().0, 6);
279    /// assert_eq!(datetime_coptic.time.hour.number(), 13);
280    /// assert_eq!(datetime_coptic.time.minute.number(), 1);
281    /// assert_eq!(datetime_coptic.time.second.number(), 0);
282    /// ```
283    pub fn try_new_coptic_datetime(
284        year: i32,
285        month: u8,
286        day: u8,
287        hour: u8,
288        minute: u8,
289        second: u8,
290    ) -> Result<DateTime<Coptic>, CalendarError> {
291        Ok(DateTime {
292            date: Date::try_new_coptic_date(year, month, day)?,
293            time: Time::try_new(hour, minute, second, 0)?,
294        })
295    }
296}
297
298fn year_as_coptic(year: i32) -> types::FormattableYear {
299    if year > 0 {
300        types::FormattableYear {
301            era: types::Era(tinystr!(16, "ad")),
302            number: year,
303            cyclic: None,
304            related_iso: None,
305        }
306    } else {
307        types::FormattableYear {
308            era: types::Era(tinystr!(16, "bd")),
309            number: 1 - year,
310            cyclic: None,
311            related_iso: None,
312        }
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    #[test]
320    fn test_coptic_regression() {
321        // https://github.com/unicode-org/icu4x/issues/2254
322        let iso_date = Date::try_new_iso_date(-100, 3, 3).unwrap();
323        let coptic = iso_date.to_calendar(Coptic);
324        let recovered_iso = coptic.to_iso();
325        assert_eq!(iso_date, recovered_iso);
326    }
327}