/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 | 4.60k | fn to_ordinal(self) -> OrdinalDate { |
160 | 4.60k | let month = self.0.month as i64; |
161 | 4.60k | let day = self.0.day as i64; |
162 | 4.60k | let offset = ((30 * (month - 1)) + day) as u16; |
163 | 4.60k | OrdinalDate { |
164 | 4.60k | year: self.0.year, |
165 | 4.60k | day_of_year: offset, |
166 | 4.60k | } |
167 | 4.60k | } |
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 | 83.1k | fn epoch() -> Fixed { |
180 | 83.1k | JulianDay::new(NABONASSAR_ERA_JD as f64).to_fixed() |
181 | 83.1k | } |
182 | | } |
183 | | |
184 | | impl FromFixed for Egyptian { |
185 | 34.8k | fn from_fixed(date: Fixed) -> Egyptian { |
186 | | //LISTING 1.49 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
187 | 34.8k | let days = date.get_day_i() - Egyptian::epoch().get_day_i(); |
188 | 34.8k | let year = days.div_euclid(365) + 1; |
189 | 34.8k | let month = days.modulus(365).div_euclid(30) + 1; |
190 | 34.8k | let day = days - (365 * (year - 1)) - (30 * (month - 1)) + 1; |
191 | 34.8k | Egyptian(CommonDate { |
192 | 34.8k | year: year as i32, |
193 | 34.8k | month: month as u8, |
194 | 34.8k | day: day as u8, |
195 | 34.8k | }) |
196 | 34.8k | } |
197 | | } |
198 | | |
199 | | impl ToFixed for Egyptian { |
200 | 18.3k | fn to_fixed(self) -> Fixed { |
201 | | //LISTING 1.47 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
202 | 18.3k | let year = self.0.year as i64; |
203 | 18.3k | let month = self.0.month as i64; |
204 | 18.3k | let day = self.0.day as i64; |
205 | 18.3k | let offset = (365 * (year - 1)) + (30 * (month - 1)) + day - 1; |
206 | 18.3k | Fixed::cast_new(Egyptian::epoch().get_day_i() + offset) |
207 | 18.3k | } |
208 | | } |
209 | | |
210 | | impl ToFromCommonDate<EgyptianMonth> for Egyptian { |
211 | 54.1k | fn to_common_date(self) -> CommonDate { |
212 | 54.1k | self.0 |
213 | 54.1k | } |
214 | | |
215 | 17.9k | fn from_common_date_unchecked(date: CommonDate) -> Self { |
216 | 17.9k | debug_assert!(Self::valid_ymd(date).is_ok()); |
217 | 17.9k | Self(date) |
218 | 17.9k | } |
219 | | |
220 | 88.6k | fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> { |
221 | 88.6k | if date.month < 1 || date.month > NON_MONTH88.0k { |
222 | 1.53k | Err(CalendarError::InvalidMonth) |
223 | 87.0k | } else if date.day < 1 { |
224 | 512 | Err(CalendarError::InvalidDay) |
225 | 86.5k | } else if date.month < NON_MONTH && date.day > 3077.8k { |
226 | 512 | Err(CalendarError::InvalidDay) |
227 | 86.0k | } else if date.month == NON_MONTH && date.day > 58.75k { |
228 | 0 | Err(CalendarError::InvalidDay) |
229 | | } else { |
230 | 86.0k | Ok(()) |
231 | | } |
232 | 88.6k | } |
233 | | |
234 | 515 | fn year_end_date(year: i32) -> CommonDate { |
235 | 515 | CommonDate::new(year, NON_MONTH, 5) |
236 | 515 | } |
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 | 3.58k | fn epagomenae(self) -> Option<EgyptianDaysUponTheYear> { |
245 | 3.58k | if self.0.month == NON_MONTH { |
246 | 304 | EgyptianDaysUponTheYear::from_u8(self.0.day) |
247 | | } else { |
248 | 3.28k | None |
249 | | } |
250 | 3.58k | } |
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 | 257 | 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>; |