/home/a220/proj/radnelac/src/calendar/ethiopic.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::coptic::Coptic; |
6 | | use crate::calendar::julian::Julian; |
7 | | use crate::calendar::prelude::CommonDate; |
8 | | use crate::calendar::prelude::CommonWeekOfYear; |
9 | | use crate::calendar::prelude::GuaranteedMonth; |
10 | | use crate::calendar::prelude::HasLeapYears; |
11 | | use crate::calendar::prelude::Quarter; |
12 | | use crate::calendar::prelude::ToFromCommonDate; |
13 | | use crate::calendar::AllowYearZero; |
14 | | use crate::calendar::CalendarMoment; |
15 | | use crate::calendar::CopticMonth; |
16 | | use crate::calendar::OrdinalDate; |
17 | | use crate::calendar::ToFromOrdinalDate; |
18 | | use crate::common::error::CalendarError; |
19 | | use crate::common::math::TermNum; |
20 | | use crate::day_count::BoundedDayCount; |
21 | | use crate::day_count::CalculatedBounds; |
22 | | use crate::day_count::Epoch; |
23 | | use crate::day_count::Fixed; |
24 | | use crate::day_count::FromFixed; |
25 | | use crate::day_count::ToFixed; |
26 | | #[allow(unused_imports)] //FromPrimitive is needed for derive |
27 | | use num_traits::FromPrimitive; |
28 | | use std::num::NonZero; |
29 | | |
30 | | //TODO: Ethiopic weekdays |
31 | | |
32 | | //LISTING 4.5 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
33 | | const ETHIOPIC_EPOCH_JULIAN: CommonDate = CommonDate { |
34 | | year: 8, |
35 | | month: 8, |
36 | | day: 29, |
37 | | }; |
38 | | |
39 | | /// Represents a month in the Ethiopic Calendar |
40 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)] |
41 | | pub enum EthiopicMonth { |
42 | | //LISTING ?? SECTION 4.2 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
43 | | Maskaram = 1, |
44 | | Teqemt, |
45 | | Hedar, |
46 | | Takhsas, |
47 | | Ter, |
48 | | Yakatit, |
49 | | Magabit, |
50 | | Miyazya, |
51 | | Genbot, |
52 | | Sane, |
53 | | Hamle, |
54 | | Nahase, |
55 | | Paguemen, |
56 | | } |
57 | | |
58 | | /// Represents a date in the Ethiopic calendar |
59 | | /// |
60 | | /// ## Introduction |
61 | | /// |
62 | | /// The Ethiopic calendar (also called the Ge'ez calendar) is the civil calendar in Ethiopia. |
63 | | /// It is similar to the [Coptic calendar](crate::calendar::Coptic). |
64 | | /// |
65 | | /// ## Basic Structure |
66 | | /// |
67 | | /// Years are divided into 13 months. The first 12 months have 30 days each. The final month |
68 | | /// has 5 days in a common year and 6 days in a leap year. |
69 | | /// |
70 | | /// There is 1 leap year every four years. This leap year occurs *before* the Julian leap year. |
71 | | /// In other words, if a given year is divisible by 4, the year *before* was a leap year. |
72 | | /// |
73 | | /// ## Epoch |
74 | | /// |
75 | | /// Years are numbered based on a calculation of the "incarnation" of Jesus - this is a slightly |
76 | | /// different calculation than the one used by the Julian calendar. The first year of the |
77 | | /// Ethiopic calendar began on 29 August 8 AD of the Julian calendar. |
78 | | /// |
79 | | /// This epoch is called the Incarnation Era. |
80 | | /// |
81 | | /// ## Representation and Examples |
82 | | /// |
83 | | /// The months are represented in this crate as [`EthiopicMonth`]. |
84 | | /// |
85 | | /// ``` |
86 | | /// use radnelac::calendar::*; |
87 | | /// use radnelac::day_count::*; |
88 | | /// |
89 | | /// let c_1_1 = CommonDate::new(2017, 1, 1); |
90 | | /// let a_1_1 = Ethiopic::try_from_common_date(c_1_1).unwrap(); |
91 | | /// assert_eq!(a_1_1.month(), EthiopicMonth::Maskaram); |
92 | | /// let c_12_30 = CommonDate::new(2017, 12, 30); |
93 | | /// let a_12_30 = Ethiopic::try_from_common_date(c_12_30).unwrap(); |
94 | | /// assert_eq!(a_12_30.month(), EthiopicMonth::Nahase); |
95 | | /// ``` |
96 | | /// |
97 | | /// The start of the Incarnation Era can be read programatically. |
98 | | /// |
99 | | /// ``` |
100 | | /// use radnelac::calendar::*; |
101 | | /// use radnelac::day_count::*; |
102 | | /// |
103 | | /// let e = Ethiopic::epoch(); |
104 | | /// let j = Julian::from_fixed(e); |
105 | | /// let c = Ethiopic::from_fixed(e); |
106 | | /// assert_eq!(j.year(), 8); |
107 | | /// assert_eq!(j.month(), JulianMonth::August); |
108 | | /// assert_eq!(j.day(), 29); |
109 | | /// assert_eq!(c.year(), 1); |
110 | | /// assert_eq!(c.month(), EthiopicMonth::Maskaram); |
111 | | /// assert_eq!(c.day(), 1); |
112 | | /// ``` |
113 | | /// |
114 | | /// ## Further reading |
115 | | /// + [Wikipedia](https://en.wikipedia.org/wiki/Ethiopic_calendar) |
116 | | /// + [Embassy of Ethiopia, Washington D.C.](https://ethiopianembassy.org/ethiopian-time/) |
117 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] |
118 | | pub struct Ethiopic(CommonDate); |
119 | | |
120 | | impl AllowYearZero for Ethiopic {} |
121 | | |
122 | | impl ToFromOrdinalDate for Ethiopic { |
123 | 1.28k | fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> { |
124 | 1.28k | let correction = if Ethiopic::is_leap(ord.year) { 1298 } else { 0982 }; |
125 | 1.28k | if ord.day_of_year > 0 && ord.day_of_year <= (365 + correction)1.02k { |
126 | 570 | Ok(()) |
127 | | } else { |
128 | 710 | Err(CalendarError::InvalidDayOfYear) |
129 | | } |
130 | 1.28k | } |
131 | | |
132 | 512 | fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate { |
133 | 512 | let f = Fixed::new(fixed_date.get() + Coptic::epoch().get() - Ethiopic::epoch().get()); |
134 | 512 | Coptic::ordinal_from_fixed(f) |
135 | 512 | } |
136 | | |
137 | 1.79k | fn to_ordinal(self) -> OrdinalDate { |
138 | 1.79k | let e = |
139 | 1.79k | Coptic::try_from_common_date(self.to_common_date()).expect("Same month/day validity"); |
140 | 1.79k | e.to_ordinal() |
141 | 1.79k | } |
142 | | |
143 | 256 | fn from_ordinal_unchecked(ord: OrdinalDate) -> Self { |
144 | 256 | let e = Coptic::from_ordinal_unchecked(ord); |
145 | 256 | Ethiopic::try_from_common_date(e.to_common_date()).expect("Same month/day validity") |
146 | 256 | } |
147 | | } |
148 | | |
149 | | impl HasLeapYears for Ethiopic { |
150 | 1.79k | fn is_leap(year: i32) -> bool { |
151 | 1.79k | year.modulus(4) == 3 |
152 | 1.79k | } |
153 | | } |
154 | | |
155 | | impl CalculatedBounds for Ethiopic {} |
156 | | |
157 | | impl Epoch for Ethiopic { |
158 | 15.1k | fn epoch() -> Fixed { |
159 | 15.1k | Julian::try_from_common_date(ETHIOPIC_EPOCH_JULIAN) |
160 | 15.1k | .expect("Epoch known to be in range.") |
161 | 15.1k | .to_fixed() |
162 | 15.1k | } |
163 | | } |
164 | | |
165 | | impl FromFixed for Ethiopic { |
166 | 10.7k | fn from_fixed(date: Fixed) -> Ethiopic { |
167 | | //LISTING 4.7 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
168 | 10.7k | let f = Fixed::new(date.get() + Coptic::epoch().get() - Ethiopic::epoch().get()); |
169 | 10.7k | Ethiopic::try_from_common_date(Coptic::from_fixed(f).to_common_date()) |
170 | 10.7k | .expect("Same month/day validity") |
171 | 10.7k | } |
172 | | } |
173 | | |
174 | | impl ToFixed for Ethiopic { |
175 | 3.58k | fn to_fixed(self) -> Fixed { |
176 | | //LISTING 4.6 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
177 | 3.58k | let e = |
178 | 3.58k | Coptic::try_from_common_date(self.to_common_date()).expect("Same month/day validity"); |
179 | 3.58k | Fixed::new(Ethiopic::epoch().get() + e.to_fixed().get() - Coptic::epoch().get()) |
180 | 3.58k | } |
181 | | } |
182 | | |
183 | | impl ToFromCommonDate<EthiopicMonth> for Ethiopic { |
184 | 20.4k | fn to_common_date(self) -> CommonDate { |
185 | 20.4k | self.0 |
186 | 20.4k | } |
187 | | |
188 | 16.7k | fn from_common_date_unchecked(date: CommonDate) -> Self { |
189 | 16.7k | debug_assert!(Self::valid_ymd(date).is_ok()); |
190 | 16.7k | Self(date) |
191 | 16.7k | } |
192 | | |
193 | 35.0k | fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> { |
194 | 35.0k | let month_opt = EthiopicMonth::from_u8(date.month); |
195 | 35.0k | if month_opt.is_none() { |
196 | 768 | Err(CalendarError::InvalidMonth) |
197 | 34.3k | } else if date.day < 1 { |
198 | 256 | Err(CalendarError::InvalidDay) |
199 | 34.0k | } else if date.day > Ethiopic::month_length(date.year, month_opt.unwrap()) { |
200 | 256 | Err(CalendarError::InvalidDay) |
201 | | } else { |
202 | 33.7k | Ok(()) |
203 | | } |
204 | 35.0k | } |
205 | | |
206 | 256 | fn year_end_date(year: i32) -> CommonDate { |
207 | 256 | Coptic::year_end_date(year) |
208 | 256 | } |
209 | | |
210 | 34.0k | fn month_length(year: i32, month: EthiopicMonth) -> u8 { |
211 | 34.0k | let em = CopticMonth::from_u8(month as u8).expect("Same number of months"); |
212 | 34.0k | Coptic::month_length(year, em) |
213 | 34.0k | } |
214 | | } |
215 | | |
216 | | impl Quarter for Ethiopic { |
217 | 2.81k | fn quarter(self) -> NonZero<u8> { |
218 | 2.81k | if self.month() == EthiopicMonth::Paguemen { |
219 | 268 | NonZero::new(4 as u8).expect("4 != 0") |
220 | | } else { |
221 | 2.54k | NonZero::new((((self.month() as u8) - 1) / 3) + 1).expect("(m-1)/3 > -1") |
222 | | } |
223 | 2.81k | } |
224 | | } |
225 | | |
226 | | impl GuaranteedMonth<EthiopicMonth> for Ethiopic {} |
227 | | impl CommonWeekOfYear<EthiopicMonth> for Ethiopic {} |
228 | | |
229 | | /// Represents a date *and time* in the Ethiopic Calendar |
230 | | pub type EthiopicMoment = CalendarMoment<Ethiopic>; |