/home/a220/proj/radnelac/src/calendar/armenian.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::egyptian::Egyptian; |
6 | | use crate::calendar::egyptian::EgyptianMonth; |
7 | | use crate::calendar::prelude::CommonDate; |
8 | | use crate::calendar::prelude::CommonWeekOfYear; |
9 | | use crate::calendar::prelude::Quarter; |
10 | | use crate::calendar::prelude::ToFromCommonDate; |
11 | | use crate::calendar::AllowYearZero; |
12 | | use crate::calendar::CalendarMoment; |
13 | | use crate::calendar::HasEpagemonae; |
14 | | use crate::calendar::OrdinalDate; |
15 | | use crate::calendar::ToFromOrdinalDate; |
16 | | use crate::common::error::CalendarError; |
17 | | use crate::day_count::BoundedDayCount; |
18 | | use crate::day_count::CalculatedBounds; |
19 | | use crate::day_count::Epoch; |
20 | | use crate::day_count::Fixed; |
21 | | use crate::day_count::FromFixed; |
22 | | use crate::day_count::RataDie; |
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 | | //LISTING 1.50 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
29 | | const ARMENIAN_EPOCH_RD: i32 = 201443; |
30 | | const NON_MONTH: u8 = 13; |
31 | | |
32 | | /// Represents a month in the Armenian Calendar |
33 | | /// |
34 | | /// Note that the epagomenal days at the end of the Armenian calendar year have no |
35 | | /// month and thus are not represented by ArmenianMonth. When representing an |
36 | | /// arbitrary day in the Armenian calendar, use an [`Option<ArmenianMonth>`] for the |
37 | | /// the month field. |
38 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)] |
39 | | pub enum ArmenianMonth { |
40 | | //LISTING ?? SECTION 1.11 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
41 | | Nawasardi = 1, |
42 | | Hori, |
43 | | Sahmi, |
44 | | Tre, |
45 | | Kaloch, |
46 | | Arach, |
47 | | Mehekani, |
48 | | Areg, |
49 | | Ahekani, |
50 | | Mareri, |
51 | | Margach, |
52 | | Hrotich, |
53 | | } |
54 | | |
55 | | /// Represents a day of month in the Armenian Calendar |
56 | | /// |
57 | | /// The Armenian calendar has name for each day of month instead of a number. |
58 | | /// Note that the epagomenal days at the end of the Armenian calendar year have no |
59 | | /// month therefore they also do not have names. |
60 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)] |
61 | | pub enum ArmenianDaysOfMonth { |
62 | | Areg = 1, |
63 | | Hrand, |
64 | | Aram, |
65 | | Margar, |
66 | | Ahrank, |
67 | | Mazdel, |
68 | | Astlik, |
69 | | Mihr, |
70 | | Jopaber, |
71 | | Murc, |
72 | | Erezhan, |
73 | | Ani, |
74 | | Parkhar, |
75 | | Vanat, |
76 | | Aramazd, |
77 | | Mani, |
78 | | Asak, |
79 | | Masis, |
80 | | Anahit, |
81 | | Aragats, |
82 | | Gorgor, |
83 | | Kordvik, |
84 | | Tsmak, |
85 | | Lusnak, |
86 | | Tsron, |
87 | | Npat, |
88 | | Vahagn, |
89 | | Sim, |
90 | | Varag, |
91 | | Giseravar, |
92 | | } |
93 | | |
94 | | /// Represents a date in the Armenian calendar |
95 | | /// |
96 | | /// ## Introduction |
97 | | /// |
98 | | /// The Armenian calendar was used in Armenia in medieval times. It has a similar structure |
99 | | /// to the [Egyptian calendar](crate::calendar::Egyptian). |
100 | | /// |
101 | | /// ## Basic Structure |
102 | | /// |
103 | | /// Years are always 365 days - there are no leap years. Years are divided into 12 months |
104 | | /// of 30 days each, with an extra 5 epagomenal days. |
105 | | /// |
106 | | /// ## Epoch |
107 | | /// |
108 | | /// The first year of the Armenian calendar began on 11 July 552 AD of the Julian calendar. |
109 | | /// |
110 | | /// ## Representation and Examples |
111 | | /// |
112 | | /// The months are represented in this crate as [`ArmenianMonth`]. |
113 | | /// |
114 | | /// ``` |
115 | | /// use radnelac::calendar::*; |
116 | | /// use radnelac::day_count::*; |
117 | | /// |
118 | | /// let c_1_1 = CommonDate::new(1462, 1, 1); |
119 | | /// let a_1_1 = Armenian::try_from_common_date(c_1_1).unwrap(); |
120 | | /// assert_eq!(a_1_1.try_month().unwrap(), ArmenianMonth::Nawasardi); |
121 | | /// let c_12_30 = CommonDate::new(1462, 12, 30); |
122 | | /// let a_12_30 = Armenian::try_from_common_date(c_12_30).unwrap(); |
123 | | /// assert_eq!(a_12_30.try_month().unwrap(), ArmenianMonth::Hrotich); |
124 | | /// ``` |
125 | | /// |
126 | | /// When converting to and from a [`CommonDate`](crate::calendar::CommonDate), the epagomenal days |
127 | | /// are treated as a 13th month. |
128 | | /// |
129 | | /// ``` |
130 | | /// use radnelac::calendar::*; |
131 | | /// use radnelac::day_count::*; |
132 | | /// |
133 | | /// let c = CommonDate::new(1462, 13, 5); |
134 | | /// let a = Armenian::try_from_common_date(c).unwrap(); |
135 | | /// assert!(a.try_month().is_none()); |
136 | | /// assert!(a.epagomenae().is_some()); |
137 | | /// ``` |
138 | | /// |
139 | | /// The Armenian calendar epoch can be read programatically. |
140 | | /// |
141 | | /// ``` |
142 | | /// use radnelac::calendar::*; |
143 | | /// use radnelac::day_count::*; |
144 | | /// |
145 | | /// let e = Armenian::epoch(); |
146 | | /// let j = Julian::from_fixed(e); |
147 | | /// let a = Armenian::from_fixed(e); |
148 | | /// assert_eq!(j.year(), 552); |
149 | | /// assert_eq!(j.month(), JulianMonth::July); |
150 | | /// assert_eq!(j.day(), 11); |
151 | | /// assert_eq!(a.year(), 1); |
152 | | /// assert_eq!(a.try_month().unwrap(), ArmenianMonth::Nawasardi); |
153 | | /// assert_eq!(a.day(), 1); |
154 | | /// ``` |
155 | | /// |
156 | | /// ## Further reading |
157 | | /// + [Wikipedia](https://en.wikipedia.org/wiki/Armenian_calendar) |
158 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] |
159 | | pub struct Armenian(CommonDate); |
160 | | |
161 | | impl Armenian { |
162 | | /// Returns the day name of month if one exists |
163 | 1.02k | pub fn day_name(self) -> Option<ArmenianDaysOfMonth> { |
164 | 1.02k | if self.0.month == NON_MONTH { |
165 | 512 | None |
166 | | } else { |
167 | 512 | ArmenianDaysOfMonth::from_u8(self.0.day) |
168 | | } |
169 | 1.02k | } |
170 | | } |
171 | | |
172 | | impl AllowYearZero for Armenian {} |
173 | | |
174 | | impl ToFromOrdinalDate for Armenian { |
175 | 1.02k | fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> { |
176 | 1.02k | Egyptian::valid_ordinal(ord) |
177 | 1.02k | } |
178 | | |
179 | 512 | fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate { |
180 | 512 | let f = Fixed::new( |
181 | 512 | fixed_date.get() + Egyptian::epoch().to_day().get() - Armenian::epoch().get(), |
182 | | ); |
183 | 512 | Egyptian::ordinal_from_fixed(f) |
184 | 512 | } |
185 | | |
186 | 1.79k | fn to_ordinal(self) -> OrdinalDate { |
187 | 1.79k | let e = |
188 | 1.79k | Egyptian::try_from_common_date(self.to_common_date()).expect("Same month/day validity"); |
189 | 1.79k | e.to_ordinal() |
190 | 1.79k | } |
191 | | |
192 | 256 | fn from_ordinal_unchecked(ord: OrdinalDate) -> Self { |
193 | 256 | let e = Egyptian::from_ordinal_unchecked(ord); |
194 | 256 | Armenian::try_from_common_date(e.to_common_date()).expect("Same month/day validity") |
195 | 256 | } |
196 | | } |
197 | | |
198 | | impl CalculatedBounds for Armenian {} |
199 | | |
200 | | impl Epoch for Armenian { |
201 | 15.6k | fn epoch() -> Fixed { |
202 | 15.6k | RataDie::new(ARMENIAN_EPOCH_RD as f64).to_fixed() |
203 | 15.6k | } |
204 | | } |
205 | | |
206 | | impl FromFixed for Armenian { |
207 | 10.7k | fn from_fixed(date: Fixed) -> Armenian { |
208 | | //LISTING 1.52 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
209 | 10.7k | let f = Fixed::new(date.get() + Egyptian::epoch().to_day().get() - Armenian::epoch().get()); |
210 | 10.7k | Armenian::try_from_common_date(Egyptian::from_fixed(f).to_common_date()) |
211 | 10.7k | .expect("Same month/day validity") |
212 | 10.7k | } |
213 | | } |
214 | | |
215 | | impl ToFixed for Armenian { |
216 | 3.84k | fn to_fixed(self) -> Fixed { |
217 | | //LISTING 1.51 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
218 | 3.84k | let e = |
219 | 3.84k | Egyptian::try_from_common_date(self.to_common_date()).expect("Same month/day validity"); |
220 | 3.84k | Fixed::new(Armenian::epoch().get() + e.to_fixed().get() - Egyptian::epoch().to_day().get()) |
221 | 3.84k | } |
222 | | } |
223 | | |
224 | | /// The epagomenal days at the end of the Armenian calendar year are represented |
225 | | /// as month 13 when converting to and from a [`CommonDate`]. |
226 | | impl ToFromCommonDate<ArmenianMonth> for Armenian { |
227 | 17.9k | fn to_common_date(self) -> CommonDate { |
228 | 17.9k | self.0 |
229 | 17.9k | } |
230 | | |
231 | 18.4k | fn from_common_date_unchecked(date: CommonDate) -> Self { |
232 | 18.4k | debug_assert!(Self::valid_ymd(date).is_ok()); |
233 | 18.4k | Self(date) |
234 | 18.4k | } |
235 | | |
236 | 38.4k | fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> { |
237 | 38.4k | Egyptian::valid_ymd(date) |
238 | 38.4k | } |
239 | | |
240 | 257 | fn year_end_date(year: i32) -> CommonDate { |
241 | 257 | Egyptian::year_end_date(year) |
242 | 257 | } |
243 | | |
244 | 0 | fn month_length(year: i32, month: ArmenianMonth) -> u8 { |
245 | 0 | let em = EgyptianMonth::from_u8(month as u8).expect("Same number of months"); |
246 | 0 | Egyptian::month_length(year, em) |
247 | 0 | } |
248 | | } |
249 | | |
250 | | impl HasEpagemonae<u8> for Armenian { |
251 | 512 | fn epagomenae(self) -> Option<u8> { |
252 | 512 | if self.0.month == NON_MONTH { |
253 | 256 | Some(self.0.day) |
254 | | } else { |
255 | 256 | None |
256 | | } |
257 | 512 | } |
258 | | |
259 | 0 | fn epagomenae_count(_year: i32) -> u8 { |
260 | 0 | 5 |
261 | 0 | } |
262 | | } |
263 | | |
264 | | impl Quarter for Armenian { |
265 | 2.81k | fn quarter(self) -> NonZero<u8> { |
266 | 2.81k | let m = self.to_common_date().month as u8; |
267 | 2.81k | if m == NON_MONTH { |
268 | 256 | NonZero::new(4 as u8).expect("4 != 0") |
269 | | } else { |
270 | 2.56k | NonZero::new(((m - 1) / 3) + 1).expect("(m - 1) / 3 > -1") |
271 | | } |
272 | 2.81k | } |
273 | | } |
274 | | |
275 | | impl CommonWeekOfYear<ArmenianMonth> for Armenian {} |
276 | | |
277 | | /// Represents a date *and time* in the Armenian Calendar |
278 | | pub type ArmenianMoment = CalendarMoment<Armenian>; |
279 | | |
280 | | #[cfg(test)] |
281 | | mod tests { |
282 | | use super::*; |
283 | | use crate::day_count::FIXED_MAX; |
284 | | use proptest::proptest; |
285 | | const MAX_YEARS: i32 = (FIXED_MAX / 365.25) as i32; |
286 | | proptest! { |
287 | | #[test] |
288 | | fn day_names(y0 in -MAX_YEARS..MAX_YEARS, y1 in -MAX_YEARS..MAX_YEARS, m in 1..12, d in 1..30) { |
289 | | let a0 = Armenian::try_from_common_date(CommonDate::new(y0, m as u8, d as u8)).unwrap(); |
290 | | let a1 = Armenian::try_from_common_date(CommonDate::new(y1, m as u8, d as u8)).unwrap(); |
291 | | assert_eq!(a0.day_name(), a1.day_name()) |
292 | | } |
293 | | |
294 | | #[test] |
295 | | fn day_names_m13(y0 in -MAX_YEARS..MAX_YEARS, y1 in -MAX_YEARS..MAX_YEARS, d in 1..5) { |
296 | | let a0 = Armenian::try_from_common_date(CommonDate::new(y0, 13, d as u8)).unwrap(); |
297 | | let a1 = Armenian::try_from_common_date(CommonDate::new(y1, 13, d as u8)).unwrap(); |
298 | | assert!(a0.day_name().is_none()); |
299 | | assert!(a1.day_name().is_none()); |
300 | | } |
301 | | } |
302 | | } |