Coverage Report

Created: 2025-10-19 21:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}