Coverage Report

Created: 2025-08-13 21:02

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