Coverage Report

Created: 2025-12-14 07:01

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/coptic.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 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
34
use crate::any_calendar::AnyCalendarKind;
35
use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
36
use crate::iso::Iso;
37
use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time};
38
use calendrical_calculations::helpers::I32CastError;
39
use calendrical_calculations::rata_die::RataDie;
40
use 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
62
pub 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)]
66
pub struct CopticDateInner(pub(crate) ArithmeticDate<Coptic>);
67
68
impl CalendarArithmetic for Coptic {
69
    type YearInfo = ();
70
71
0
    fn month_days(year: i32, month: u8, _data: ()) -> u8 {
72
0
        if (1..=12).contains(&month) {
73
0
            30
74
0
        } else if month == 13 {
75
0
            if Self::is_leap_year(year, ()) {
76
0
                6
77
            } else {
78
0
                5
79
            }
80
        } else {
81
0
            0
82
        }
83
0
    }
84
85
0
    fn months_for_every_year(_: i32, _data: ()) -> u8 {
86
0
        13
87
0
    }
88
89
0
    fn is_leap_year(year: i32, _data: ()) -> bool {
90
0
        year.rem_euclid(4) == 3
91
0
    }
92
93
0
    fn last_month_day_in_year(year: i32, _data: ()) -> (u8, u8) {
94
0
        if Self::is_leap_year(year, ()) {
95
0
            (13, 6)
96
        } else {
97
0
            (13, 5)
98
        }
99
0
    }
100
101
0
    fn days_in_provided_year(year: i32, _data: ()) -> u16 {
102
0
        if Self::is_leap_year(year, ()) {
103
0
            366
104
        } else {
105
0
            365
106
        }
107
0
    }
108
}
109
110
impl Calendar for Coptic {
111
    type DateInner = CopticDateInner;
112
0
    fn date_from_codes(
113
0
        &self,
114
0
        era: types::Era,
115
0
        year: i32,
116
0
        month_code: types::MonthCode,
117
0
        day: u8,
118
0
    ) -> Result<Self::DateInner, CalendarError> {
119
0
        let year = if era.0 == tinystr!(16, "ad") {
120
0
            if year <= 0 {
121
0
                return Err(CalendarError::OutOfRange);
122
0
            }
123
0
            year
124
0
        } else if era.0 == tinystr!(16, "bd") {
125
0
            if year <= 0 {
126
0
                return Err(CalendarError::OutOfRange);
127
0
            }
128
0
            1 - year
129
        } else {
130
0
            return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
131
        };
132
133
0
        ArithmeticDate::new_from_codes(self, year, month_code, day).map(CopticDateInner)
134
0
    }
135
0
    fn date_from_iso(&self, iso: Date<Iso>) -> CopticDateInner {
136
0
        let fixed_iso = Iso::fixed_from_iso(*iso.inner());
137
0
        Self::coptic_from_fixed(fixed_iso)
138
0
    }
139
140
0
    fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
141
0
        let fixed_coptic = Coptic::fixed_from_coptic(date.0);
142
0
        Iso::iso_from_fixed(fixed_coptic)
143
0
    }
144
145
0
    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
146
0
        date.0.months_in_year()
147
0
    }
148
149
0
    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
150
0
        date.0.days_in_year()
151
0
    }
152
153
0
    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
154
0
        date.0.days_in_month()
155
0
    }
156
157
0
    fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {
158
0
        Iso.day_of_week(Coptic.date_to_iso(date).inner())
159
0
    }
160
161
0
    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
162
0
        date.0.offset_date(offset, &());
163
0
    }
164
165
    #[allow(clippy::field_reassign_with_default)]
166
0
    fn until(
167
0
        &self,
168
0
        date1: &Self::DateInner,
169
0
        date2: &Self::DateInner,
170
0
        _calendar2: &Self,
171
0
        _largest_unit: DateDurationUnit,
172
0
        _smallest_unit: DateDurationUnit,
173
0
    ) -> DateDuration<Self> {
174
0
        date1.0.until(date2.0, _largest_unit, _smallest_unit)
175
0
    }
176
177
0
    fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
178
0
        year_as_coptic(date.0.year)
179
0
    }
180
181
0
    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
182
0
        Self::is_leap_year(date.0.year, ())
183
0
    }
184
185
0
    fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
186
0
        date.0.month()
187
0
    }
188
189
0
    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
190
0
        date.0.day_of_month()
191
0
    }
192
193
0
    fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
194
0
        let prev_year = date.0.year - 1;
195
0
        let next_year = date.0.year + 1;
196
0
        types::DayOfYearInfo {
197
0
            day_of_year: date.0.day_of_year(),
198
0
            days_in_year: date.0.days_in_year(),
199
0
            prev_year: year_as_coptic(prev_year),
200
0
            days_in_prev_year: Coptic::days_in_year_direct(prev_year),
201
0
            next_year: year_as_coptic(next_year),
202
0
        }
203
0
    }
204
205
0
    fn debug_name(&self) -> &'static str {
206
0
        "Coptic"
207
0
    }
208
209
0
    fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
210
0
        Some(AnyCalendarKind::Coptic)
211
0
    }
212
}
213
214
impl Coptic {
215
0
    fn fixed_from_coptic(date: ArithmeticDate<Coptic>) -> RataDie {
216
0
        calendrical_calculations::coptic::fixed_from_coptic(date.year, date.month, date.day)
217
0
    }
218
219
0
    pub(crate) fn coptic_from_fixed(date: RataDie) -> CopticDateInner {
220
0
        let (year, month, day) = match calendrical_calculations::coptic::coptic_from_fixed(date) {
221
0
            Err(I32CastError::BelowMin) => return CopticDateInner(ArithmeticDate::min_date()),
222
0
            Err(I32CastError::AboveMax) => return CopticDateInner(ArithmeticDate::max_date()),
223
0
            Ok(ymd) => ymd,
224
        };
225
226
0
        CopticDateInner(ArithmeticDate::new_unchecked(year, month, day))
227
0
    }
228
229
0
    fn days_in_year_direct(year: i32) -> u16 {
230
0
        if Coptic::is_leap_year(year, ()) {
231
0
            366
232
        } else {
233
0
            365
234
        }
235
0
    }
236
}
237
238
impl 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
0
    pub fn try_new_coptic_date(
254
0
        year: i32,
255
0
        month: u8,
256
0
        day: u8,
257
0
    ) -> Result<Date<Coptic>, CalendarError> {
258
0
        ArithmeticDate::new_from_ordinals(year, month, day)
259
0
            .map(CopticDateInner)
260
0
            .map(|inner| Date::from_raw(inner, Coptic))
261
0
    }
262
}
263
264
impl 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
0
    pub fn try_new_coptic_datetime(
284
0
        year: i32,
285
0
        month: u8,
286
0
        day: u8,
287
0
        hour: u8,
288
0
        minute: u8,
289
0
        second: u8,
290
0
    ) -> Result<DateTime<Coptic>, CalendarError> {
291
        Ok(DateTime {
292
0
            date: Date::try_new_coptic_date(year, month, day)?,
293
0
            time: Time::try_new(hour, minute, second, 0)?,
294
        })
295
0
    }
296
}
297
298
0
fn year_as_coptic(year: i32) -> types::FormattableYear {
299
0
    if year > 0 {
300
0
        types::FormattableYear {
301
0
            era: types::Era(tinystr!(16, "ad")),
302
0
            number: year,
303
0
            cyclic: None,
304
0
            related_iso: None,
305
0
        }
306
    } else {
307
0
        types::FormattableYear {
308
0
            era: types::Era(tinystr!(16, "bd")),
309
0
            number: 1 - year,
310
0
            cyclic: None,
311
0
            related_iso: None,
312
0
        }
313
    }
314
0
}
315
316
#[cfg(test)]
317
mod 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
}