Coverage Report

Created: 2025-08-13 21:02

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
impl CopticMonth {
57
37.6k
    pub fn length(self, leap: bool) -> u8 {
58
        //LISTING ?? SECTION 4.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
59
37.6k
        match self {
60
            CopticMonth::Epagomene => {
61
4.41k
                if leap {
62
1.34k
                    6
63
                } else {
64
3.07k
                    5
65
                }
66
            }
67
33.2k
            _ => 30,
68
        }
69
37.6k
    }
70
}
71
72
/// Represents a date in the Coptic calendar
73
///
74
/// ## Further reading
75
/// + [Wikipedia](https://en.wikipedia.org/wiki/Coptic_calendar)
76
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
77
pub struct Coptic(CommonDate);
78
79
impl AllowYearZero for Coptic {}
80
81
impl ToFromOrdinalDate for Coptic {
82
1.28k
    fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
83
1.28k
        let correction = if Coptic::is_leap(ord.year) { 
1336
} else {
0944
};
84
1.28k
        if ord.day_of_year > 0 && 
ord.day_of_year <= (365 + correction)1.02k
{
85
579
            Ok(())
86
        } else {
87
701
            Err(CalendarError::InvalidDayOfYear)
88
        }
89
1.28k
    }
90
91
22.0k
    fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
92
        //LISTING 4.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
93
        //Missing the day term and parts of the month term
94
22.0k
        let date = fixed_date.get_day_i();
95
22.0k
        let epoch = Coptic::epoch().get_day_i();
96
22.0k
        let year = (4 * (date - epoch) + 1463).div_euclid(1461) as i32;
97
22.0k
        let year_start = Coptic::to_fixed(Coptic(CommonDate::new(year, 1, 1)));
98
22.0k
        let doy = ((date - year_start.get_day_i()) + 1) as u16;
99
22.0k
        OrdinalDate {
100
22.0k
            year: year,
101
22.0k
            day_of_year: doy,
102
22.0k
        }
103
22.0k
    }
104
105
60.9k
    fn to_ordinal(self) -> OrdinalDate {
106
        //LISTING 4.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
107
        //This is just the terms containing monthand day
108
60.9k
        OrdinalDate {
109
60.9k
            year: self.0.year,
110
60.9k
            day_of_year: (30 * ((self.0.month as u16) - 1) + (self.0.day as u16)),
111
60.9k
        }
112
60.9k
    }
113
114
21.5k
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
115
        //LISTING 4.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
116
        //Only the month and day terms, modified to use ordinal day count instead of count from epoch
117
21.5k
        let month = ((ord.day_of_year - 1).div_euclid(30) + 1) as u8;
118
21.5k
        let month_start = Coptic(CommonDate::new(ord.year, month, 1)).to_ordinal();
119
21.5k
        let day = (ord.day_of_year - month_start.day_of_year + 1) as u8;
120
21.5k
        Coptic(CommonDate::new(ord.year, month, day))
121
21.5k
    }
122
}
123
124
impl HasLeapYears for Coptic {
125
    //LISTING 4.2 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
126
39.9k
    fn is_leap(year: i32) -> bool {
127
39.9k
        year.modulus(4) == 3
128
39.9k
    }
129
}
130
131
impl CalculatedBounds for Coptic {}
132
133
impl Epoch for Coptic {
134
73.5k
    fn epoch() -> Fixed {
135
73.5k
        Julian::try_from_common_date(COPTIC_EPOCH_JULIAN)
136
73.5k
            .expect("Epoch known to be in range.")
137
73.5k
            .to_fixed()
138
73.5k
    }
139
}
140
141
impl FromFixed for Coptic {
142
21.0k
    fn from_fixed(fixed_date: Fixed) -> Coptic {
143
        //LISTING 4.4 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
144
        //Split compared to original
145
21.0k
        let ord = Self::ordinal_from_fixed(fixed_date);
146
21.0k
        Self::from_ordinal_unchecked(ord)
147
21.0k
    }
148
}
149
150
impl ToFixed for Coptic {
151
36.3k
    fn to_fixed(self) -> Fixed {
152
        //LISTING 4.3 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
153
        //Split compared to original: the terms containing month and day are in to_ordinal
154
36.3k
        let year = self.0.year as i64;
155
36.3k
        let epoch = Coptic::epoch().get_day_i();
156
36.3k
        let doy = self.to_ordinal().day_of_year as i64;
157
36.3k
        Fixed::cast_new(epoch - 1 + (365 * (year - 1)) + year.div_euclid(4) + doy)
158
36.3k
    }
159
}
160
161
impl ToFromCommonDate<CopticMonth> for Coptic {
162
26.1k
    fn to_common_date(self) -> CommonDate {
163
26.1k
        self.0
164
26.1k
    }
165
166
18.3k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
167
18.3k
        debug_assert!(Self::valid_ymd(date).is_ok());
168
18.3k
        Self(date)
169
18.3k
    }
170
171
38.1k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
172
38.1k
        let month_opt = CopticMonth::from_u8(date.month);
173
38.1k
        if month_opt.is_none() {
174
768
            Err(CalendarError::InvalidMonth)
175
37.3k
        } else if date.day < 1 {
176
256
            Err(CalendarError::InvalidDay)
177
37.1k
        } else if date.day > month_opt.unwrap().length(Coptic::is_leap(date.year)) {
178
256
            Err(CalendarError::InvalidDay)
179
        } else {
180
36.8k
            Ok(())
181
        }
182
38.1k
    }
183
184
512
    fn year_end_date(year: i32) -> CommonDate {
185
512
        let m = CopticMonth::Epagomene;
186
512
        CommonDate::new(year, m as u8, m.length(Coptic::is_leap(year)))
187
512
    }
188
}
189
190
impl Quarter for Coptic {
191
2.81k
    fn quarter(self) -> NonZero<u8> {
192
2.81k
        if self.month() == CopticMonth::Epagomene {
193
260
            NonZero::new(4 as u8).expect("4 != 0")
194
        } else {
195
2.55k
            NonZero::new((((self.month() as u8) - 1) / 3) + 1).expect("(m-1)/3 > -1")
196
        }
197
2.81k
    }
198
}
199
200
impl GuaranteedMonth<CopticMonth> for Coptic {}
201
impl CommonWeekOfYear<CopticMonth> for Coptic {}
202
203
/// Represents a date *and time* in the Coptic Calendar
204
pub type CopticMoment = CalendarMoment<Coptic>;
205
206
#[cfg(test)]
207
mod tests {
208
    use super::*;
209
    use crate::calendar::julian::JulianMonth;
210
211
    use proptest::proptest;
212
213
    proptest! {
214
        #[test]
215
        fn christmas(y in i16::MIN..i16::MAX) {
216
            //LISTING 4.9 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
217
            let c = Coptic::try_from_common_date(CommonDate::new(y as i32, CopticMonth::Koiak as u8, 29))?;
218
            let j = c.convert::<Julian>();
219
            assert_eq!(j.month(), JulianMonth::December);
220
            assert!(j.day() == 25 || j.day() == 26);
221
        }
222
223
        #[test]
224
        fn feast_of_neyrouz(y in i16::MIN..i16::MAX) {
225
            // https://en.wikipedia.org/wiki/Coptic_calendar
226
            let c = Coptic::try_from_common_date(CommonDate::new(y as i32, CopticMonth::Thoout as u8, 1))?;
227
            let j = c.convert::<Julian>();
228
            assert_eq!(j.month(), JulianMonth::August);
229
            assert!(j.day() == 29 || j.day() == 30);
230
        }
231
    }
232
}