Coverage Report

Created: 2025-10-19 21:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/a220/proj/radnelac/src/calendar/egyptian.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::prelude::CommonDate;
6
use crate::calendar::prelude::CommonWeekOfYear;
7
use crate::calendar::prelude::Quarter;
8
use crate::calendar::prelude::ToFromCommonDate;
9
use crate::calendar::AllowYearZero;
10
use crate::calendar::CalendarMoment;
11
use crate::calendar::HasEpagemonae;
12
use crate::calendar::OrdinalDate;
13
use crate::calendar::ToFromOrdinalDate;
14
use crate::common::error::CalendarError;
15
use crate::common::math::TermNum;
16
use crate::day_count::BoundedDayCount;
17
use crate::day_count::CalculatedBounds;
18
use crate::day_count::Epoch;
19
use crate::day_count::Fixed;
20
use crate::day_count::FromFixed;
21
use crate::day_count::JulianDay;
22
use crate::day_count::ToFixed;
23
#[allow(unused_imports)] //FromPrimitive is needed for derive
24
use num_traits::FromPrimitive;
25
use std::num::NonZero;
26
27
//LISTING 1.46 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
28
const NABONASSAR_ERA_JD: i32 = 1448638;
29
const NON_MONTH: u8 = 13;
30
31
/// Represents a month in the Egyptian Calendar
32
///
33
/// Note that the epagomenal days at the end of the Egyptian calendar year have no
34
/// month and thus are not represented by ArmenianMonth. When representing an
35
/// arbitrary day in the Egyptian calendar, use an `Option<EgyptianMonth>` for the
36
/// the month field.
37
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
38
pub enum EgyptianMonth {
39
    //LISTING ?? SECTION 1.11 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
40
    Thoth = 1,
41
    Phaophi,
42
    Athyr,
43
    Choiak,
44
    Tybi,
45
    Mechir,
46
    Phamenoth,
47
    Pharmuthi,
48
    Pachon,
49
    Payni,
50
    Epiphi,
51
    Mesori,
52
}
53
54
/// Represents an epagomenal day at the end of the Egyptian calendar year
55
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
56
pub enum EgyptianDaysUponTheYear {
57
    BirthOfOsiris = 1,
58
    BirthOfHorus,
59
    BirthOfSeth,
60
    BirthOfIsis,
61
    BirthOfNephthys,
62
}
63
64
/// Represents a date in the Egyptian calendar
65
///
66
/// ## Introduction
67
///
68
/// The Egyptian calendar was used in ancient Egypt.
69
///
70
/// ## Basic Structure
71
///
72
/// Years are always 365 days - there are no leap years. Years are divided into 12 months
73
/// of 30 days each, with an extra 5 epagomenal days.
74
///
75
/// ## Epoch
76
///
77
/// This implementation of the Egyptian calendar uses the Nabonassar Era from the *Almagest*
78
/// written by Claudius Ptolomy. The first year of the Nabonassar Era began on 26 Februrary
79
/// 747 BC of the Julian calendar.
80
///
81
/// The *Almagest* was written in Greek, and Nabonassar was a Babylonian king instead of an
82
/// Egyptian. The actual calendar dates in Ancient Egypt used regnal years.
83
///
84
/// ## Representation and Examples
85
///
86
/// The months are represented in this crate as [`EgyptianMonth`].
87
///
88
/// ```
89
/// use radnelac::calendar::*;
90
/// use radnelac::day_count::*;
91
///
92
/// let c_1_1 = CommonDate::new(1462, 1, 1);
93
/// let a_1_1 = Egyptian::try_from_common_date(c_1_1).unwrap();
94
/// assert_eq!(a_1_1.try_month().unwrap(), EgyptianMonth::Thoth);
95
/// let c_12_30 = CommonDate::new(1462, 12, 30);
96
/// let a_12_30 = Egyptian::try_from_common_date(c_12_30).unwrap();
97
/// assert_eq!(a_12_30.try_month().unwrap(), EgyptianMonth::Mesori);
98
/// ```
99
///
100
/// When converting to and from a [`CommonDate`](crate::calendar::CommonDate), the epagomenal days
101
/// are treated as a 13th month.
102
///
103
/// ```
104
/// use radnelac::calendar::*;
105
/// use radnelac::day_count::*;
106
///
107
/// let c = CommonDate::new(1462, 13, 5);
108
/// let a = Egyptian::try_from_common_date(c).unwrap();
109
/// assert!(a.try_month().is_none());
110
/// assert!(a.epagomenae().is_some());
111
/// ```
112
///
113
/// The start of the Nabonassar Era can be read programatically.
114
///
115
/// ```
116
/// use radnelac::calendar::*;
117
/// use radnelac::day_count::*;
118
///
119
/// let e = Egyptian::epoch();
120
/// let j = Julian::from_fixed(e);
121
/// let a = Egyptian::from_fixed(e);
122
/// assert_eq!(j.year(), -747);
123
/// assert_eq!(j.month(), JulianMonth::February);
124
/// assert_eq!(j.day(), 26);
125
/// assert_eq!(a.year(), 1);
126
/// assert_eq!(a.try_month().unwrap(), EgyptianMonth::Thoth);
127
/// assert_eq!(a.day(), 1);
128
/// ```
129
///
130
/// ## Further reading
131
/// + Wikipedia
132
///   + [Egyptian Calendar](https://en.wikipedia.org/wiki/Egyptian_calendar)
133
///   + [Nabonassar](https://en.wikipedia.org/wiki/Nabonassar)
134
///   + [Egyptian chronology](https://en.wikipedia.org/wiki/Egyptian_chronology)
135
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
136
pub struct Egyptian(CommonDate);
137
138
impl AllowYearZero for Egyptian {}
139
140
impl ToFromOrdinalDate for Egyptian {
141
2.04k
    fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
142
2.04k
        if ord.day_of_year < 1 || 
ord.day_of_year > 3651.53k
{
143
1.02k
            Err(CalendarError::InvalidDayOfYear)
144
        } else {
145
1.02k
            Ok(())
146
        }
147
2.04k
    }
148
149
1.02k
    fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
150
1.02k
        let days = fixed_date.get_day_i() - Egyptian::epoch().get_day_i();
151
1.02k
        let year = (days.div_euclid(365) + 1) as i32;
152
1.02k
        let doy = (days.modulus(365) + 1) as u16;
153
1.02k
        OrdinalDate {
154
1.02k
            year: year,
155
1.02k
            day_of_year: doy,
156
1.02k
        }
157
1.02k
    }
158
159
3.58k
    fn to_ordinal(self) -> OrdinalDate {
160
3.58k
        let month = self.0.month as i64;
161
3.58k
        let day = self.0.day as i64;
162
3.58k
        let offset = ((30 * (month - 1)) + day) as u16;
163
3.58k
        OrdinalDate {
164
3.58k
            year: self.0.year,
165
3.58k
            day_of_year: offset,
166
3.58k
        }
167
3.58k
    }
168
169
512
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
170
512
        let month = (ord.day_of_year - 1).div_euclid(30) + 1;
171
512
        let day = ord.day_of_year - (30 * (month - 1));
172
512
        Egyptian(CommonDate::new(ord.year, month as u8, day as u8))
173
512
    }
174
}
175
176
impl CalculatedBounds for Egyptian {}
177
178
impl Epoch for Egyptian {
179
45.8k
    fn epoch() -> Fixed {
180
45.8k
        JulianDay::new(NABONASSAR_ERA_JD as f64).to_fixed()
181
45.8k
    }
182
}
183
184
impl FromFixed for Egyptian {
185
21.5k
    fn from_fixed(date: Fixed) -> Egyptian {
186
        //LISTING 1.49 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
187
21.5k
        let days = date.get_day_i() - Egyptian::epoch().get_day_i();
188
21.5k
        let year = days.div_euclid(365) + 1;
189
21.5k
        let month = days.modulus(365).div_euclid(30) + 1;
190
21.5k
        let day = days - (365 * (year - 1)) - (30 * (month - 1)) + 1;
191
21.5k
        Egyptian(CommonDate {
192
21.5k
            year: year as i32,
193
21.5k
            month: month as u8,
194
21.5k
            day: day as u8,
195
21.5k
        })
196
21.5k
    }
197
}
198
199
impl ToFixed for Egyptian {
200
7.68k
    fn to_fixed(self) -> Fixed {
201
        //LISTING 1.47 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
202
7.68k
        let year = self.0.year as i64;
203
7.68k
        let month = self.0.month as i64;
204
7.68k
        let day = self.0.day as i64;
205
7.68k
        let offset = (365 * (year - 1)) + (30 * (month - 1)) + day - 1;
206
7.68k
        Fixed::cast_new(Egyptian::epoch().get_day_i() + offset)
207
7.68k
    }
208
}
209
210
impl ToFromCommonDate<EgyptianMonth> for Egyptian {
211
23.3k
    fn to_common_date(self) -> CommonDate {
212
23.3k
        self.0
213
23.3k
    }
214
215
12.0k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
216
12.0k
        debug_assert!(Self::valid_ymd(date).is_ok());
217
12.0k
        Self(date)
218
12.0k
    }
219
220
64.0k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
221
64.0k
        if date.month < 1 || 
date.month > NON_MONTH63.5k
{
222
1.53k
            Err(CalendarError::InvalidMonth)
223
62.4k
        } else if date.day < 1 {
224
512
            Err(CalendarError::InvalidDay)
225
61.9k
        } else if date.month < NON_MONTH && 
date.day > 3053.5k
{
226
512
            Err(CalendarError::InvalidDay)
227
61.4k
        } else if date.month == NON_MONTH && 
date.day > 58.43k
{
228
0
            Err(CalendarError::InvalidDay)
229
        } else {
230
61.4k
            Ok(())
231
        }
232
64.0k
    }
233
234
514
    fn year_end_date(year: i32) -> CommonDate {
235
514
        CommonDate::new(year, NON_MONTH, 5)
236
514
    }
237
238
0
    fn month_length(_year: i32, _month: EgyptianMonth) -> u8 {
239
0
        30
240
0
    }
241
}
242
243
impl HasEpagemonae<EgyptianDaysUponTheYear> for Egyptian {
244
512
    fn epagomenae(self) -> Option<EgyptianDaysUponTheYear> {
245
512
        if self.0.month == NON_MONTH {
246
256
            EgyptianDaysUponTheYear::from_u8(self.0.day)
247
        } else {
248
256
            None
249
        }
250
512
    }
251
252
0
    fn epagomenae_count(_year: i32) -> u8 {
253
0
        5
254
0
    }
255
}
256
257
impl Quarter for Egyptian {
258
2.81k
    fn quarter(self) -> NonZero<u8> {
259
2.81k
        let m = self.to_common_date().month as u8;
260
2.81k
        if m == NON_MONTH {
261
264
            NonZero::new(4 as u8).expect("4 != 0")
262
        } else {
263
2.55k
            NonZero::new(((m - 1) / 3) + 1).expect("(m - 1) / 3 > -1")
264
        }
265
2.81k
    }
266
}
267
268
impl CommonWeekOfYear<EgyptianMonth> for Egyptian {}
269
270
/// Represents a date *and time* in the Egyptian Calendar
271
pub type EgyptianMoment = CalendarMoment<Egyptian>;