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/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
}