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