/home/a220/proj/radnelac/src/calendar/coptic.rs
Line | Count | Source |
1 | | // This Source Code Form is subject to the terms of the Mozilla Public |
2 | | // License, v. 2.0. If a copy of the MPL was not distributed with this |
3 | | // file, You can obtain one at https://mozilla.org/MPL/2.0/. |
4 | | |
5 | | use crate::calendar::julian::Julian; |
6 | | use crate::calendar::prelude::CommonDate; |
7 | | use crate::calendar::prelude::CommonWeekOfYear; |
8 | | use crate::calendar::prelude::GuaranteedMonth; |
9 | | use crate::calendar::prelude::HasLeapYears; |
10 | | use crate::calendar::prelude::Quarter; |
11 | | use crate::calendar::prelude::ToFromCommonDate; |
12 | | use crate::calendar::AllowYearZero; |
13 | | use crate::calendar::CalendarMoment; |
14 | | use crate::calendar::OrdinalDate; |
15 | | use crate::calendar::ToFromOrdinalDate; |
16 | | use crate::common::error::CalendarError; |
17 | | use crate::common::math::TermNum; |
18 | | use crate::day_count::BoundedDayCount; |
19 | | use crate::day_count::CalculatedBounds; |
20 | | use crate::day_count::Epoch; |
21 | | use crate::day_count::Fixed; |
22 | | use crate::day_count::FromFixed; |
23 | | use crate::day_count::ToFixed; |
24 | | #[allow(unused_imports)] //FromPrimitive is needed for derive |
25 | | use num_traits::FromPrimitive; |
26 | | use std::num::NonZero; |
27 | | |
28 | | //TODO: Coptic weekdays |
29 | | |
30 | | //LISTING 4.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
31 | | const COPTIC_EPOCH_JULIAN: CommonDate = CommonDate { |
32 | | year: 284, |
33 | | month: 8, |
34 | | day: 29, |
35 | | }; |
36 | | |
37 | | /// Represents a month in the Coptic Calendar |
38 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)] |
39 | | pub enum CopticMonth { |
40 | | //LISTING ?? SECTION 4.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
41 | | Thoout = 1, |
42 | | Paope, |
43 | | Athor, |
44 | | Koiak, |
45 | | Tobe, |
46 | | Meshir, |
47 | | Paremotep, |
48 | | Parmoute, |
49 | | Pashons, |
50 | | Paone, |
51 | | Epep, |
52 | | Mesore, |
53 | | Epagomene, |
54 | | } |
55 | | |
56 | | /// Represents a date in the Coptic calendar |
57 | | /// |
58 | | /// ## Introduction |
59 | | /// |
60 | | /// The Coptic calendar (also called the Alexandrian calendar or the Calendar of Martyrs) |
61 | | /// is used by the Coptic Orthodox Church and the Coptic Catholic Church. Historically it |
62 | | /// was also used for fiscal purposes in Egypt. |
63 | | /// |
64 | | /// ## Basic Structure |
65 | | /// |
66 | | /// Years are divided into 13 months. The first 12 months have 30 days each. The final month |
67 | | /// has 5 days in a common year and 6 days in a leap year. |
68 | | /// |
69 | | /// There is 1 leap year every four years. This leap year occurs *before* the Julian leap year. |
70 | | /// In other words, if a given year is divisible by 4, the year *before* was a leap year. |
71 | | /// |
72 | | /// ## Epoch |
73 | | /// |
74 | | /// Years are numbered from the start of the reign of the Roman Emperor Diocletian. Each |
75 | | /// individual year starts on the Feast of Neyrouz. Thus the first year of the Coptic calendar |
76 | | /// began on 29 August 284 AD of the Julian calendar. |
77 | | /// |
78 | | /// This epoch is called the Era of the Martyrs. |
79 | | /// |
80 | | /// ## Representation and Examples |
81 | | /// |
82 | | /// The months are represented in this crate as [`CopticMonth`]. |
83 | | /// |
84 | | /// ``` |
85 | | /// use radnelac::calendar::*; |
86 | | /// use radnelac::day_count::*; |
87 | | /// |
88 | | /// let c_1_1 = CommonDate::new(1741, 1, 1); |
89 | | /// let a_1_1 = Coptic::try_from_common_date(c_1_1).unwrap(); |
90 | | /// assert_eq!(a_1_1.month(), CopticMonth::Thoout); |
91 | | /// let c_12_30 = CommonDate::new(1741, 12, 30); |
92 | | /// let a_12_30 = Coptic::try_from_common_date(c_12_30).unwrap(); |
93 | | /// assert_eq!(a_12_30.month(), CopticMonth::Mesore); |
94 | | /// ``` |
95 | | /// |
96 | | /// The start of the Era of Martyrs can be read programatically. |
97 | | /// |
98 | | /// ``` |
99 | | /// use radnelac::calendar::*; |
100 | | /// use radnelac::day_count::*; |
101 | | /// |
102 | | /// let e = Coptic::epoch(); |
103 | | /// let j = Julian::from_fixed(e); |
104 | | /// let c = Coptic::from_fixed(e); |
105 | | /// assert_eq!(j.year(), 284); |
106 | | /// assert_eq!(j.month(), JulianMonth::August); |
107 | | /// assert_eq!(j.day(), 29); |
108 | | /// assert_eq!(c.year(), 1); |
109 | | /// assert_eq!(c.month(), CopticMonth::Thoout); |
110 | | /// assert_eq!(c.day(), 1); |
111 | | /// ``` |
112 | | /// |
113 | | /// ## Further reading |
114 | | /// + Wikipedia |
115 | | /// + [Coptic Calendar](https://en.wikipedia.org/wiki/Coptic_calendar) |
116 | | /// + [Era of the Martyrs](https://en.wikipedia.org/wiki/Era_of_the_Martyrs) |
117 | | /// + [Coptic Orthodox Church](https://copticorthodox.church/en/coptic-church/coptic-history/) |
118 | | /// + [*The Coptic Christian Heritage* by Lois M. Farag](https://www.google.ca/books/edition/The_Coptic_Christian_Heritage/dYK3AQAAQBAJ) |
119 | | /// + [*A Handbook for Travellers in Lower and_Upper Egypt*](https://www.google.ca/books/edition/A_Handbook_for_Travellers_in_Lower_and_U/CnhJYhBzMmgC?hl=en&gbpv=1) |
120 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] |
121 | | pub struct Coptic(CommonDate); |
122 | | |
123 | | impl AllowYearZero for Coptic {} |
124 | | |
125 | | impl ToFromOrdinalDate for Coptic { |
126 | 1.28k | fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> { |
127 | 1.28k | let correction = if Coptic::is_leap(ord.year) { 1328 } else { 0952 }; |
128 | 1.28k | if ord.day_of_year > 0 && ord.day_of_year <= (365 + correction)1.02k { |
129 | 574 | Ok(()) |
130 | | } else { |
131 | 706 | Err(CalendarError::InvalidDayOfYear) |
132 | | } |
133 | 1.28k | } |
134 | | |
135 | 35.3k | fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate { |
136 | | //LISTING 4.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
137 | | //Missing the day term and parts of the month term |
138 | 35.3k | let date = fixed_date.get_day_i(); |
139 | 35.3k | let epoch = Coptic::epoch().get_day_i(); |
140 | 35.3k | let year = (4 * (date - epoch) + 1463).div_euclid(1461) as i32; |
141 | 35.3k | let year_start = Coptic::to_fixed(Coptic(CommonDate::new(year, 1, 1))); |
142 | 35.3k | let doy = ((date - year_start.get_day_i()) + 1) as u16; |
143 | 35.3k | OrdinalDate { |
144 | 35.3k | year: year, |
145 | 35.3k | day_of_year: doy, |
146 | 35.3k | } |
147 | 35.3k | } |
148 | | |
149 | 99.3k | fn to_ordinal(self) -> OrdinalDate { |
150 | | //LISTING 4.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
151 | | //This is just the terms containing monthand day |
152 | 99.3k | OrdinalDate { |
153 | 99.3k | year: self.0.year, |
154 | 99.3k | day_of_year: (30 * ((self.0.month as u16) - 1) + (self.0.day as u16)), |
155 | 99.3k | } |
156 | 99.3k | } |
157 | | |
158 | 34.8k | fn from_ordinal_unchecked(ord: OrdinalDate) -> Self { |
159 | | //LISTING 4.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
160 | | //Only the month and day terms, modified to use ordinal day count instead of count from epoch |
161 | 34.8k | let month = ((ord.day_of_year - 1).div_euclid(30) + 1) as u8; |
162 | 34.8k | let month_start = Coptic(CommonDate::new(ord.year, month, 1)).to_ordinal(); |
163 | 34.8k | let day = (ord.day_of_year - month_start.day_of_year + 1) as u8; |
164 | 34.8k | Coptic(CommonDate::new(ord.year, month, day)) |
165 | 34.8k | } |
166 | | } |
167 | | |
168 | | impl HasLeapYears for Coptic { |
169 | | //LISTING 4.2 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
170 | 99.5k | fn is_leap(year: i32) -> bool { |
171 | 99.5k | year.modulus(4) == 3 |
172 | 99.5k | } |
173 | | } |
174 | | |
175 | | impl CalculatedBounds for Coptic {} |
176 | | |
177 | | impl Epoch for Coptic { |
178 | 124k | fn epoch() -> Fixed { |
179 | 124k | Julian::try_from_common_date(COPTIC_EPOCH_JULIAN) |
180 | 124k | .expect("Epoch known to be in range.") |
181 | 124k | .to_fixed() |
182 | 124k | } |
183 | | } |
184 | | |
185 | | impl FromFixed for Coptic { |
186 | 34.3k | fn from_fixed(fixed_date: Fixed) -> Coptic { |
187 | | //LISTING 4.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
188 | | //Split compared to original |
189 | 34.3k | let ord = Self::ordinal_from_fixed(fixed_date); |
190 | 34.3k | Self::from_ordinal_unchecked(ord) |
191 | 34.3k | } |
192 | | } |
193 | | |
194 | | impl ToFixed for Coptic { |
195 | 60.4k | fn to_fixed(self) -> Fixed { |
196 | | //LISTING 4.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
197 | | //Split compared to original: the terms containing month and day are in to_ordinal |
198 | 60.4k | let year = self.0.year as i64; |
199 | 60.4k | let epoch = Coptic::epoch().get_day_i(); |
200 | 60.4k | let doy = self.to_ordinal().day_of_year as i64; |
201 | 60.4k | Fixed::cast_new(epoch - 1 + (365 * (year - 1)) + year.div_euclid(4) + doy) |
202 | 60.4k | } |
203 | | } |
204 | | |
205 | | impl ToFromCommonDate<CopticMonth> for Coptic { |
206 | 56.8k | fn to_common_date(self) -> CommonDate { |
207 | 56.8k | self.0 |
208 | 56.8k | } |
209 | | |
210 | 24.1k | fn from_common_date_unchecked(date: CommonDate) -> Self { |
211 | 24.1k | debug_assert!(Self::valid_ymd(date).is_ok()); |
212 | 24.1k | Self(date) |
213 | 24.1k | } |
214 | | |
215 | 49.9k | fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> { |
216 | 49.9k | let month_opt = CopticMonth::from_u8(date.month); |
217 | 49.9k | if month_opt.is_none() { |
218 | 768 | Err(CalendarError::InvalidMonth) |
219 | 49.1k | } else if date.day < 1 { |
220 | 256 | Err(CalendarError::InvalidDay) |
221 | 48.9k | } else if date.day > Self::month_length(date.year, month_opt.unwrap()) { |
222 | 256 | Err(CalendarError::InvalidDay) |
223 | | } else { |
224 | 48.6k | Ok(()) |
225 | | } |
226 | 49.9k | } |
227 | | |
228 | 513 | fn year_end_date(year: i32) -> CommonDate { |
229 | 513 | let m = CopticMonth::Epagomene; |
230 | 513 | CommonDate::new(year, m as u8, Self::month_length(year, m)) |
231 | 513 | } |
232 | | |
233 | 96.7k | fn month_length(year: i32, month: CopticMonth) -> u8 { |
234 | | //LISTING ?? SECTION 4.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
235 | 96.7k | match (month, Coptic::is_leap(year)) { |
236 | 5.11k | (CopticMonth::Epagomene, false) => 5, |
237 | 2.19k | (CopticMonth::Epagomene, true) => 6, |
238 | 89.4k | (_, _) => 30, |
239 | | } |
240 | 96.7k | } |
241 | | } |
242 | | |
243 | | impl Quarter for Coptic { |
244 | 2.81k | fn quarter(self) -> NonZero<u8> { |
245 | 2.81k | if self.month() == CopticMonth::Epagomene { |
246 | 256 | NonZero::new(4 as u8).expect("4 != 0") |
247 | | } else { |
248 | 2.56k | NonZero::new((((self.month() as u8) - 1) / 3) + 1).expect("(m-1)/3 > -1") |
249 | | } |
250 | 2.81k | } |
251 | | } |
252 | | |
253 | | impl GuaranteedMonth<CopticMonth> for Coptic {} |
254 | | impl CommonWeekOfYear<CopticMonth> for Coptic {} |
255 | | |
256 | | /// Represents a date *and time* in the Coptic Calendar |
257 | | pub type CopticMoment = CalendarMoment<Coptic>; |
258 | | |
259 | | #[cfg(test)] |
260 | | mod tests { |
261 | | use super::*; |
262 | | use crate::calendar::julian::JulianMonth; |
263 | | use crate::calendar::Gregorian; |
264 | | use proptest::prop_assume; |
265 | | |
266 | | use proptest::proptest; |
267 | | |
268 | | #[test] |
269 | 1 | fn handbook() { |
270 | | //https://www.google.ca/books/edition/A_Handbook_for_Travellers_in_Lower_and_U/CnhJYhBzMmgC?hl=en&gbpv=1 |
271 | 1 | let c = Coptic::try_from_common_date(CommonDate::new(1604, 1, 1)).unwrap(); |
272 | 1 | let g = c.convert::<Gregorian>(); |
273 | 1 | assert_eq!(g.to_common_date(), CommonDate::new(1887, 9, 11)); |
274 | 1 | } |
275 | | |
276 | | proptest! { |
277 | | #[test] |
278 | | fn julian_leap_ad(x in 1..(i16::MAX/4)) { |
279 | | let jy: i32 = (x * 4) as i32; |
280 | | prop_assume!(Julian::is_leap(jy)); |
281 | | let j = Julian::try_from_common_date(CommonDate::new(jy, 1, 1)).unwrap(); |
282 | | let c = j.convert::<Coptic>(); |
283 | | assert!(!Coptic::is_leap(c.year())); |
284 | | assert!(Coptic::is_leap(c.year() - 1)); |
285 | | } |
286 | | |
287 | | #[test] |
288 | | fn christmas(y in i16::MIN..i16::MAX) { |
289 | | //LISTING 4.9 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
290 | | let c = Coptic::try_from_common_date(CommonDate::new(y as i32, CopticMonth::Koiak as u8, 29))?; |
291 | | let j = c.convert::<Julian>(); |
292 | | assert_eq!(j.month(), JulianMonth::December); |
293 | | assert!(j.day() == 25 || j.day() == 26); |
294 | | } |
295 | | |
296 | | #[test] |
297 | | fn feast_of_neyrouz(y in i16::MIN..i16::MAX) { |
298 | | // https://en.wikipedia.org/wiki/Coptic_calendar |
299 | | let c = Coptic::try_from_common_date(CommonDate::new(y as i32, CopticMonth::Thoout as u8, 1))?; |
300 | | let j = c.convert::<Julian>(); |
301 | | assert_eq!(j.month(), JulianMonth::August); |
302 | | assert!(j.day() == 29 || j.day() == 30); |
303 | | } |
304 | | } |
305 | | } |