radnelac/calendar/
positivist.rs1use crate::calendar::gregorian::Gregorian;
7use crate::calendar::prelude::CommonDate;
8use crate::calendar::prelude::HasLeapYears;
9use crate::calendar::prelude::Perennial;
10use crate::calendar::prelude::Quarter;
11use crate::calendar::prelude::ToFromCommonDate;
12use crate::calendar::prelude::ToFromOrdinalDate;
13use crate::calendar::AllowYearZero;
14use crate::calendar::CalendarMoment;
15use crate::calendar::HasEpagemonae;
16use crate::calendar::OrdinalDate;
17use crate::common::error::CalendarError;
18use crate::common::math::TermNum;
19use crate::day_count::BoundedDayCount;
20use crate::day_count::CalculatedBounds;
21use crate::day_count::Epoch;
22use crate::day_count::Fixed;
23use crate::day_count::FromFixed;
24use crate::day_count::ToFixed;
25use crate::day_cycle::Weekday;
26use std::num::NonZero;
27
28#[allow(unused_imports)] use num_traits::FromPrimitive;
30
31const POSITIVIST_YEAR_OFFSET: i32 = 1789 - 1;
32const NON_MONTH: u8 = 14;
33
34#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
45pub enum PositivistMonth {
46 Moses = 1,
47 Homer,
48 Aristotle,
49 Archimedes,
50 Caesar,
51 SaintPaul,
52 Charlemagne,
53 Dante,
54 Gutenburg,
55 Shakespeare,
56 Descartes,
57 Frederick,
58 Bichat,
59}
60
61#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
66pub enum PositivistComplementaryDay {
67 FestivalOfTheDead = 1,
70 FestivalOfHolyWomen,
72}
73
74#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
81pub struct Positivist(CommonDate);
82
83impl AllowYearZero for Positivist {}
84
85impl ToFromOrdinalDate for Positivist {
86 fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
87 let ord_g = OrdinalDate {
88 year: ord.year + POSITIVIST_YEAR_OFFSET,
89 day_of_year: ord.day_of_year,
90 };
91 Gregorian::valid_ordinal(ord_g)
92 }
93
94 fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
95 let ord_g = Gregorian::ordinal_from_fixed(fixed_date);
96 OrdinalDate {
97 year: ord_g.year - POSITIVIST_YEAR_OFFSET,
98 day_of_year: ord_g.day_of_year,
99 }
100 }
101
102 fn to_ordinal(self) -> OrdinalDate {
103 let offset_m = ((self.0.month as i64) - 1) * 28;
104 let doy = (offset_m as u16) + (self.0.day as u16);
105 OrdinalDate {
106 year: self.0.year,
107 day_of_year: doy,
108 }
109 }
110
111 fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
112 let year = ord.year;
113 let month = (((ord.day_of_year - 1) as i64).div_euclid(28) + 1) as u8;
114 let day = (ord.day_of_year as i64).adjusted_remainder(28) as u8;
115 debug_assert!(day > 0 && day < 29);
116 Positivist(CommonDate::new(year, month, day))
117 }
118}
119
120impl HasEpagemonae<PositivistComplementaryDay> for Positivist {
121 fn epagomenae(self) -> Option<PositivistComplementaryDay> {
123 if self.0.month == NON_MONTH {
124 PositivistComplementaryDay::from_u8(self.0.day)
125 } else {
126 None
127 }
128 }
129
130 fn epagomenae_count(p_year: i32) -> u8 {
131 if Positivist::is_leap(p_year) {
132 2
133 } else {
134 1
135 }
136 }
137}
138
139impl Perennial<PositivistMonth, Weekday> for Positivist {
140 fn weekday(self) -> Option<Weekday> {
142 if self.0.month == NON_MONTH {
143 None
144 } else {
145 Weekday::from_i64((self.0.day as i64).modulus(7))
146 }
147 }
148
149 fn days_per_week() -> u8 {
150 7
151 }
152
153 fn weeks_per_month() -> u8 {
154 4
155 }
156}
157
158impl HasLeapYears for Positivist {
159 fn is_leap(p_year: i32) -> bool {
161 Gregorian::is_leap(POSITIVIST_YEAR_OFFSET + p_year)
162 }
163}
164
165impl CalculatedBounds for Positivist {}
166
167impl Epoch for Positivist {
168 fn epoch() -> Fixed {
169 Gregorian::try_year_start(POSITIVIST_YEAR_OFFSET)
170 .expect("Year known to be valid")
171 .to_fixed()
172 }
173}
174
175impl FromFixed for Positivist {
176 fn from_fixed(date: Fixed) -> Positivist {
177 let ord_g = Gregorian::ordinal_from_fixed(date);
178 let ord = OrdinalDate {
179 year: ord_g.year - POSITIVIST_YEAR_OFFSET,
180 day_of_year: ord_g.day_of_year,
181 };
182 Self::from_ordinal_unchecked(ord)
183 }
184}
185
186impl ToFixed for Positivist {
187 fn to_fixed(self) -> Fixed {
188 let y = self.0.year + POSITIVIST_YEAR_OFFSET;
189 let offset_y = Gregorian::try_year_start(y)
190 .expect("Year known to be valid")
191 .to_fixed()
192 .get_day_i()
193 - 1;
194 let doy = self.to_ordinal().day_of_year as i64;
195 Fixed::cast_new(offset_y + doy)
196 }
197}
198
199impl ToFromCommonDate<PositivistMonth> for Positivist {
200 fn to_common_date(self) -> CommonDate {
201 self.0
202 }
203
204 fn from_common_date_unchecked(date: CommonDate) -> Self {
205 debug_assert!(Self::valid_ymd(date).is_ok());
206 Self(date)
207 }
208
209 fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
210 if date.month < 1 || date.month > NON_MONTH {
211 Err(CalendarError::InvalidMonth)
212 } else if date.day < 1 {
213 Err(CalendarError::InvalidDay)
214 } else if date.month < NON_MONTH && date.day > 28 {
215 Err(CalendarError::InvalidDay)
216 } else if date.month == NON_MONTH && date.day > Positivist::epagomenae_count(date.year) {
217 Err(CalendarError::InvalidDay)
218 } else {
219 Ok(())
220 }
221 }
222
223 fn year_end_date(year: i32) -> CommonDate {
224 CommonDate::new(year, NON_MONTH, Positivist::epagomenae_count(year))
225 }
226
227 fn month_length(_year: i32, _month: PositivistMonth) -> u8 {
228 28
229 }
230}
231
232impl Quarter for Positivist {
233 fn quarter(self) -> NonZero<u8> {
234 match self.try_month() {
235 Some(PositivistMonth::Bichat) | None => NonZero::new(4 as u8).unwrap(),
236 Some(m) => NonZero::new((((m as u8) - 1) / 3) + 1).expect("(m-1)/3 > -1"),
237 }
238 }
239}
240
241pub type PositivistMoment = CalendarMoment<Positivist>;
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn example_from_text() {
250 let dg = Gregorian::try_from_common_date(CommonDate::new(1855, 1, 1)).unwrap();
252 let dp = Positivist::try_from_common_date(CommonDate::new(67, 1, 1)).unwrap();
253 let fg = dg.to_fixed();
254 let fp = dp.to_fixed();
255 assert_eq!(fg, fp);
256 }
257}