1use 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#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
49#[allow(clippy::exhaustive_structs)] pub struct Coptic;
51
52#[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 let anno_martyrum_year = if ordinal_month < 4 || (ordinal_month == 4 && day <= 22) {
126 1689
127 } else if ordinal_month == 13 && day >= 6 {
130 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 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 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 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}