/home/a220/proj/radnelac/src/calendar/positivist.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 | | // Calendier Positiviste Page 52-53 |
6 | | use crate::calendar::gregorian::Gregorian; |
7 | | use crate::calendar::prelude::CommonDate; |
8 | | use crate::calendar::prelude::HasLeapYears; |
9 | | use crate::calendar::prelude::Perennial; |
10 | | use crate::calendar::prelude::Quarter; |
11 | | use crate::calendar::prelude::ToFromCommonDate; |
12 | | use crate::calendar::prelude::ToFromOrdinalDate; |
13 | | use crate::calendar::AllowYearZero; |
14 | | use crate::calendar::CalendarMoment; |
15 | | use crate::calendar::HasIntercalaryDays; |
16 | | use crate::calendar::OrdinalDate; |
17 | | use crate::common::error::CalendarError; |
18 | | use crate::common::math::TermNum; |
19 | | use crate::day_count::BoundedDayCount; |
20 | | use crate::day_count::CalculatedBounds; |
21 | | use crate::day_count::Epoch; |
22 | | use crate::day_count::Fixed; |
23 | | use crate::day_count::FromFixed; |
24 | | use crate::day_count::ToFixed; |
25 | | use crate::day_cycle::Weekday; |
26 | | use std::num::NonZero; |
27 | | |
28 | | #[allow(unused_imports)] //FromPrimitive is needed for derive |
29 | | use num_traits::FromPrimitive; |
30 | | |
31 | | const POSITIVIST_YEAR_OFFSET: i32 = 1789 - 1; |
32 | | const NON_MONTH: u8 = 14; |
33 | | |
34 | | /// Represents a month of the Positivist Calendar |
35 | | /// |
36 | | /// The Positivist months are named after famous historical figures. |
37 | | /// |
38 | | /// Note that the complementary days at the end of the Positivist calendar year have no |
39 | | /// month and thus are not represented by PositivistMonth. When representing an |
40 | | /// arbitrary day in the Positivist calendar, use an `Option<PositivistMonth>` for the |
41 | | /// the month field. |
42 | | /// |
43 | | /// See page 19 of "Calendier Positiviste" for more details. |
44 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)] |
45 | | pub 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 | | /// Represents a complementary day of the Positivist Calendar |
62 | | /// |
63 | | /// These are not part of any week or month. |
64 | | /// See page 8 of "Calendier Positiviste" for more details. |
65 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)] |
66 | | pub enum PositivistComplementaryDay { |
67 | | /// In leap years of the Positivist calendar, this is the second-last day of the year. |
68 | | /// In common years of the Positivist calendar, this is the last day of the year. |
69 | | FestivalOfTheDead = 1, |
70 | | /// In leap years of the Positivist calendar, this is the last day of the year. |
71 | | FestivalOfHolyWomen, |
72 | | } |
73 | | |
74 | | /// Represents a date in the Positivist calendar |
75 | | /// |
76 | | /// ## Further reading |
77 | | /// + [Positivists.org](http://positivists.org/calendar.html) |
78 | | /// + ["Calendrier Positiviste" by August Comte](https://gallica.bnf.fr/ark:/12148/bpt6k21868f/f42.planchecontact) |
79 | | /// + ["The Positivist Calendar" by Henry Edger](https://books.google.ca/books?id=S_BRAAAAMAAJ) |
80 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] |
81 | | pub struct Positivist(CommonDate); |
82 | | |
83 | | impl AllowYearZero for Positivist {} |
84 | | |
85 | | impl ToFromOrdinalDate for Positivist { |
86 | 1.28k | fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> { |
87 | 1.28k | let ord_g = OrdinalDate { |
88 | 1.28k | year: ord.year + POSITIVIST_YEAR_OFFSET, |
89 | 1.28k | day_of_year: ord.day_of_year, |
90 | 1.28k | }; |
91 | 1.28k | Gregorian::valid_ordinal(ord_g) |
92 | 1.28k | } |
93 | | |
94 | 512 | fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate { |
95 | 512 | let ord_g = Gregorian::ordinal_from_fixed(fixed_date); |
96 | 512 | OrdinalDate { |
97 | 512 | year: ord_g.year - POSITIVIST_YEAR_OFFSET, |
98 | 512 | day_of_year: ord_g.day_of_year, |
99 | 512 | } |
100 | 512 | } |
101 | | |
102 | 9.02k | fn to_ordinal(self) -> OrdinalDate { |
103 | 9.02k | let offset_m = ((self.0.month as i64) - 1) * 28; |
104 | 9.02k | let doy = (offset_m as u16) + (self.0.day as u16); |
105 | 9.02k | OrdinalDate { |
106 | 9.02k | year: self.0.year, |
107 | 9.02k | day_of_year: doy, |
108 | 9.02k | } |
109 | 9.02k | } |
110 | | |
111 | 8.71k | fn from_ordinal_unchecked(ord: OrdinalDate) -> Self { |
112 | 8.71k | let year = ord.year; |
113 | 8.71k | let month = (((ord.day_of_year - 1) as i64).div_euclid(28) + 1) as u8; |
114 | 8.71k | let day = (ord.day_of_year as i64).adjusted_remainder(28) as u8; |
115 | 8.71k | debug_assert!(day > 0 && day < 29); |
116 | 8.71k | Positivist(CommonDate::new(year, month, day)) |
117 | 8.71k | } |
118 | | } |
119 | | |
120 | | impl HasIntercalaryDays<PositivistComplementaryDay> for Positivist { |
121 | | // Calendier Positiviste Page 8 |
122 | 256 | fn complementary(self) -> Option<PositivistComplementaryDay> { |
123 | 256 | if self.0.month == NON_MONTH { |
124 | 2 | PositivistComplementaryDay::from_u8(self.0.day) |
125 | | } else { |
126 | 254 | None |
127 | | } |
128 | 256 | } |
129 | | |
130 | 2.70k | fn complementary_count(p_year: i32) -> u8 { |
131 | 2.70k | if Positivist::is_leap(p_year) { |
132 | 987 | 2 |
133 | | } else { |
134 | 1.71k | 1 |
135 | | } |
136 | 2.70k | } |
137 | | } |
138 | | |
139 | | impl Perennial<PositivistMonth, Weekday> for Positivist { |
140 | | // Calendier Positiviste Page 23-30 |
141 | 2.30k | fn weekday(self) -> Option<Weekday> { |
142 | 2.30k | if self.0.month == NON_MONTH { |
143 | 8 | None |
144 | | } else { |
145 | 2.29k | Weekday::from_i64((self.0.day as i64).modulus(7)) |
146 | | } |
147 | 2.30k | } |
148 | | |
149 | 2.04k | fn days_per_week() -> u8 { |
150 | 2.04k | 7 |
151 | 2.04k | } |
152 | | |
153 | 2.04k | fn weeks_per_month() -> u8 { |
154 | 2.04k | 4 |
155 | 2.04k | } |
156 | | } |
157 | | |
158 | | impl HasLeapYears for Positivist { |
159 | | // Not sure about the source for this... |
160 | 3.47k | fn is_leap(p_year: i32) -> bool { |
161 | 3.47k | Gregorian::is_leap(POSITIVIST_YEAR_OFFSET + p_year) |
162 | 3.47k | } |
163 | | } |
164 | | |
165 | | impl CalculatedBounds for Positivist {} |
166 | | |
167 | | impl Epoch for Positivist { |
168 | 0 | fn epoch() -> Fixed { |
169 | 0 | Gregorian::try_year_start(POSITIVIST_YEAR_OFFSET) |
170 | 0 | .expect("Year known to be valid") |
171 | 0 | .to_fixed() |
172 | 0 | } |
173 | | } |
174 | | |
175 | | impl FromFixed for Positivist { |
176 | 8.45k | fn from_fixed(date: Fixed) -> Positivist { |
177 | 8.45k | let ord_g = Gregorian::ordinal_from_fixed(date); |
178 | 8.45k | let ord = OrdinalDate { |
179 | 8.45k | year: ord_g.year - POSITIVIST_YEAR_OFFSET, |
180 | 8.45k | day_of_year: ord_g.day_of_year, |
181 | 8.45k | }; |
182 | 8.45k | Self::from_ordinal_unchecked(ord) |
183 | 8.45k | } |
184 | | } |
185 | | |
186 | | impl ToFixed for Positivist { |
187 | 7.23k | fn to_fixed(self) -> Fixed { |
188 | 7.23k | let y = self.0.year + POSITIVIST_YEAR_OFFSET; |
189 | 7.23k | let offset_y = Gregorian::try_year_start(y) |
190 | 7.23k | .expect("Year known to be valid") |
191 | 7.23k | .to_fixed() |
192 | 7.23k | .get_day_i() |
193 | 7.23k | - 1; |
194 | 7.23k | let doy = self.to_ordinal().day_of_year as i64; |
195 | 7.23k | Fixed::cast_new(offset_y + doy) |
196 | 7.23k | } |
197 | | } |
198 | | |
199 | | impl ToFromCommonDate<PositivistMonth> for Positivist { |
200 | 11.5k | fn to_common_date(self) -> CommonDate { |
201 | 11.5k | self.0 |
202 | 11.5k | } |
203 | | |
204 | 11.4k | fn from_common_date_unchecked(date: CommonDate) -> Self { |
205 | 11.4k | debug_assert!(Self::valid_ymd(date).is_ok()); |
206 | 11.4k | Self(date) |
207 | 11.4k | } |
208 | | |
209 | 24.4k | fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> { |
210 | 24.4k | if date.month < 1 || date.month > NON_MONTH24.2k { |
211 | 768 | Err(CalendarError::InvalidMonth) |
212 | 23.7k | } else if date.day < 1 { |
213 | 256 | Err(CalendarError::InvalidDay) |
214 | 23.4k | } else if date.month < NON_MONTH && date.day > 2820.9k { |
215 | 256 | Err(CalendarError::InvalidDay) |
216 | 23.1k | } else if date.month == NON_MONTH && date.day2.45k > Positivist::complementary_count(date.year) { |
217 | 0 | Err(CalendarError::InvalidDay) |
218 | | } else { |
219 | 23.1k | Ok(()) |
220 | | } |
221 | 24.4k | } |
222 | | |
223 | 256 | fn year_end_date(year: i32) -> CommonDate { |
224 | 256 | CommonDate::new(year, NON_MONTH, Positivist::complementary_count(year)) |
225 | 256 | } |
226 | | } |
227 | | |
228 | | impl Quarter for Positivist { |
229 | 2.81k | fn quarter(self) -> NonZero<u8> { |
230 | 2.81k | match self.try_month() { |
231 | 291 | Some(PositivistMonth::Bichat) | None => NonZero::new(4 as u8).unwrap(), |
232 | 2.52k | Some(m) => NonZero::new((((m as u8) - 1) / 3) + 1).expect("(m-1)/3 > -1"), |
233 | | } |
234 | 2.81k | } |
235 | | } |
236 | | |
237 | | /// Represents a date *and time* in the Positivist Calendar |
238 | | pub type PositivistMoment = CalendarMoment<Positivist>; |
239 | | |
240 | | #[cfg(test)] |
241 | | mod tests { |
242 | | use super::*; |
243 | | |
244 | | #[test] |
245 | 1 | fn example_from_text() { |
246 | | //The Positivist Calendar, page 37 |
247 | 1 | let dg = Gregorian::try_from_common_date(CommonDate::new(1855, 1, 1)).unwrap(); |
248 | 1 | let dp = Positivist::try_from_common_date(CommonDate::new(67, 1, 1)).unwrap(); |
249 | 1 | let fg = dg.to_fixed(); |
250 | 1 | let fp = dp.to_fixed(); |
251 | 1 | assert_eq!(fg, fp); |
252 | 1 | } |
253 | | } |