icu_calendar/cal/
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
5use crate::calendar_arithmetic::ArithmeticDate;
6use crate::calendar_arithmetic::DateFieldsResolver;
7use crate::error::{
8    DateError, DateFromFieldsError, EcmaReferenceYearError, MonthCodeError, UnknownEraError,
9};
10use crate::options::DateFromFieldsOptions;
11use crate::options::{DateAddOptions, DateDifferenceOptions};
12use crate::{types, Calendar, Date, RangeError};
13use calendrical_calculations::helpers::I32CastError;
14use calendrical_calculations::rata_die::RataDie;
15use tinystr::tinystr;
16
17/// The [Coptic Calendar](https://en.wikipedia.org/wiki/Coptic_calendar)
18///
19/// The Coptic calendar, also called the Alexandrian Calendar, is a solar calendar that
20/// is influenced by both the ancient Egpytian calendar and the [`Julian`](crate::cal::Julian)
21/// calendar. It was introduced in Egypt under Roman rule in the first century BCE, and
22/// replaced for civil use in 1875, however continues to be used liturgically.
23///
24/// This implementation extends proleptically for dates before the calendar's creation.
25///
26/// This corresponds to the `"coptic"` [CLDR calendar](https://unicode.org/reports/tr35/#UnicodeCalendarIdentifier).
27///
28/// # Era codes
29///
30/// This calendar uses a single code: `am`, corresponding to the After Diocletian/Anno Martyrum
31/// era. 1 A.M. is equivalent to 284 C.E.
32///
33/// # Months and days
34///
35/// The 13 months are called Thout (`M01`, 30 days), Paopi (`M02`, 30 days), Hathor (`M03`, 30 days),
36/// Koiak (`M04`, 30 days), Tobi (`M05`, 30 days), Meshir (`M06`, 30 days), Paremhat (`M07`, 30 days),
37/// Parmouti (`M08`, 30 days), Pashons (`M09`, 30 days), Paoni (`M10`, 30 days), Epip (`M11`, 30 days),
38/// Mesori (`M12`, 30 days), Pi Kogi Enavot (`M13`, 5 days).
39///
40/// In leap years (years divisible by 4), Pi Kogi Enavot gains a 6th day.
41///
42/// Standard years thus have 365 days, and leap years 366.
43///
44/// # Calendar drift
45///
46/// The Coptic calendar has the same year lengths and leap year rules as the Julian calendar,
47/// so it experiences the same drift of 1 day in ~128 years with respect to the seasons.
48#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
49#[allow(clippy::exhaustive_structs)] // this type is stable
50pub struct Coptic;
51
52/// The inner date type used for representing [`Date`]s of [`Coptic`]. See [`Date`] and [`Coptic`] for more details.
53#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
54pub struct CopticDateInner(pub(crate) ArithmeticDate<Coptic>);
55
56impl DateFieldsResolver for Coptic {
57    type YearInfo = i32;
58
59    fn days_in_provided_month(year: i32, month: u8) -> u8 {
60        if (1..=12).contains(&month) {
61            30
62        } else if month == 13 {
63            if year.rem_euclid(4) == 3 {
64                6
65            } else {
66                5
67            }
68        } else {
69            0
70        }
71    }
72
73    fn months_in_provided_year(_: i32) -> u8 {
74        13
75    }
76    #[inline]
77    fn year_info_from_era(
78        &self,
79        era: &[u8],
80        era_year: i32,
81    ) -> Result<Self::YearInfo, UnknownEraError> {
82        match era {
83            b"am" => Ok(era_year),
84            _ => Err(UnknownEraError),
85        }
86    }
87
88    #[inline]
89    fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo {
90        extended_year
91    }
92
93    #[inline]
94    fn reference_year_from_month_day(
95        &self,
96        month_code: types::ValidMonthCode,
97        day: u8,
98    ) -> Result<Self::YearInfo, EcmaReferenceYearError> {
99        Coptic::reference_year_from_month_day(month_code, day)
100    }
101
102    #[inline]
103    fn ordinal_month_from_code(
104        &self,
105        _year: &Self::YearInfo,
106        month_code: types::ValidMonthCode,
107        _options: DateFromFieldsOptions,
108    ) -> Result<u8, MonthCodeError> {
109        match month_code.to_tuple() {
110            (month_number @ 1..=13, false) => Ok(month_number),
111            _ => Err(MonthCodeError::NotInCalendar),
112        }
113    }
114}
115
116impl Coptic {
117    pub(crate) fn reference_year_from_month_day(
118        month_code: types::ValidMonthCode,
119        day: u8,
120    ) -> Result<i32, EcmaReferenceYearError> {
121        let (ordinal_month, false) = month_code.to_tuple() else {
122            return Err(EcmaReferenceYearError::MonthCodeNotInCalendar);
123        };
124        // December 31, 1972 occurs on 4th month, 22nd day, 1689 AM
125        let anno_martyrum_year = if ordinal_month < 4 || (ordinal_month == 4 && day <= 22) {
126            1689
127        // Note: this must be >=6, not just == 6, since we have not yet
128        // applied a potential Overflow::Constrain.
129        } else if ordinal_month == 13 && day >= 6 {
130            // 1687 AM is a leap year
131            1687
132        } else {
133            1688
134        };
135        Ok(anno_martyrum_year)
136    }
137}
138
139impl crate::cal::scaffold::UnstableSealed for Coptic {}
140impl Calendar for Coptic {
141    type DateInner = CopticDateInner;
142    type Year = types::EraYear;
143    type DifferenceError = core::convert::Infallible;
144
145    fn from_codes(
146        &self,
147        era: Option<&str>,
148        year: i32,
149        month_code: types::MonthCode,
150        day: u8,
151    ) -> Result<Self::DateInner, DateError> {
152        ArithmeticDate::from_codes(era, year, month_code, day, self).map(CopticDateInner)
153    }
154
155    #[cfg(feature = "unstable")]
156    fn from_fields(
157        &self,
158        fields: types::DateFields,
159        options: DateFromFieldsOptions,
160    ) -> Result<Self::DateInner, DateFromFieldsError> {
161        ArithmeticDate::from_fields(fields, options, self).map(CopticDateInner)
162    }
163
164    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
165        CopticDateInner(
166            match calendrical_calculations::coptic::coptic_from_fixed(rd) {
167                Err(I32CastError::BelowMin) => ArithmeticDate::new_unchecked(i32::MIN, 1, 1),
168                Err(I32CastError::AboveMax) => ArithmeticDate::new_unchecked(i32::MAX, 13, 6),
169                Ok((year, month, day)) => ArithmeticDate::new_unchecked(year, month, day),
170            },
171        )
172    }
173
174    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
175        calendrical_calculations::coptic::fixed_from_coptic(date.0.year, date.0.month, date.0.day)
176    }
177
178    fn has_cheap_iso_conversion(&self) -> bool {
179        false
180    }
181
182    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
183        Self::months_in_provided_year(date.0.year)
184    }
185
186    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
187        if self.is_in_leap_year(date) {
188            366
189        } else {
190            365
191        }
192    }
193
194    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
195        Self::days_in_provided_month(date.0.year, date.0.month)
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        date.0.added(duration, self, options).map(CopticDateInner)
206    }
207
208    #[cfg(feature = "unstable")]
209    fn until(
210        &self,
211        date1: &Self::DateInner,
212        date2: &Self::DateInner,
213        options: DateDifferenceOptions,
214    ) -> Result<types::DateDuration, Self::DifferenceError> {
215        Ok(date1.0.until(&date2.0, self, options))
216    }
217
218    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
219        let year = date.0.year;
220        types::EraYear {
221            era: tinystr!(16, "am"),
222            era_index: Some(0),
223            year,
224            extended_year: year,
225            ambiguity: types::YearAmbiguity::CenturyRequired,
226        }
227    }
228
229    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
230        date.0.year.rem_euclid(4) == 3
231    }
232
233    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
234        types::MonthInfo::non_lunisolar(date.0.month)
235    }
236
237    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
238        types::DayOfMonth(date.0.day)
239    }
240
241    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
242        types::DayOfYear(30 * (date.0.month as u16 - 1) + date.0.day as u16)
243    }
244
245    fn debug_name(&self) -> &'static str {
246        "Coptic"
247    }
248
249    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
250        Some(crate::preferences::CalendarAlgorithm::Coptic)
251    }
252}
253
254impl Date<Coptic> {
255    /// Construct new Coptic Date.
256    ///
257    /// ```rust
258    /// use icu::calendar::Date;
259    ///
260    /// let date_coptic = Date::try_new_coptic(1686, 5, 6)
261    ///     .expect("Failed to initialize Coptic Date instance.");
262    ///
263    /// assert_eq!(date_coptic.era_year().year, 1686);
264    /// assert_eq!(date_coptic.month().ordinal, 5);
265    /// assert_eq!(date_coptic.day_of_month().0, 6);
266    /// ```
267    pub fn try_new_coptic(year: i32, month: u8, day: u8) -> Result<Date<Coptic>, RangeError> {
268        ArithmeticDate::try_from_ymd(year, month, day)
269            .map(CopticDateInner)
270            .map(|inner| Date::from_raw(inner, Coptic))
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::options::{DateFromFieldsOptions, MissingFieldsStrategy, Overflow};
278    use crate::types::DateFields;
279
280    #[test]
281    fn test_coptic_regression() {
282        // https://github.com/unicode-org/icu4x/issues/2254
283        let iso_date = Date::try_new_iso(-100, 3, 3).unwrap();
284        let coptic = iso_date.to_calendar(Coptic);
285        let recovered_iso = coptic.to_iso();
286        assert_eq!(iso_date, recovered_iso);
287    }
288
289    #[test]
290    fn test_from_fields_monthday_constrain() {
291        // M13-7 is not a real day, however this should resolve to M12-6
292        // with Overflow::Constrain
293        let fields = DateFields {
294            month_code: Some(b"M13"),
295            day: Some(7),
296            ..Default::default()
297        };
298        let options = DateFromFieldsOptions {
299            overflow: Some(Overflow::Constrain),
300            missing_fields_strategy: Some(MissingFieldsStrategy::Ecma),
301            ..Default::default()
302        };
303
304        let date = Date::try_from_fields(fields, options, Coptic).unwrap();
305        assert_eq!(date.day_of_month().0, 6, "Day was successfully constrained");
306    }
307}