/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::HasEpagemonae; |
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 | | /// ## Introduction |
77 | | /// |
78 | | /// The Positivist calendar was proposed by August Comte. It was part of his project of |
79 | | /// creating a "Religion of Humanity". The months, weeks and days of the Positivist |
80 | | /// calendar are named after people who made a positive (as judged by Comte) contributions |
81 | | /// to society. |
82 | | /// |
83 | | /// ## Basic structure |
84 | | /// |
85 | | /// From *The Positivist Calendar* by Henry Edger: |
86 | | /// > The Positivist year, beginning and ending with the Christian year, is divided into |
87 | | /// > thirteen months, and an additional day in bisextile years, following that. To these |
88 | | /// > two days no name, either weekly or monthly, is attached, being sufficiently |
89 | | /// > designated by the corresponding festivals. The Calendar therefore becomes perpetual. |
90 | | /// > Every year, and each month in the year, begins with a Monday, while the Sundays fall |
91 | | /// > on the 7th, 14th 21st and 28th days of all the months alike. |
92 | | /// |
93 | | /// The "bisextile" (leap) years must occur at the same time as Gregorian leap years based |
94 | | /// on the above definition. This further implies a leap year rule similar to the Gregorian, |
95 | | /// but offset by 1788 years. |
96 | | /// |
97 | | /// ## Epoch |
98 | | /// |
99 | | /// The years are numbered based on the French Revolution. The first day of the first year |
100 | | /// of the Positivist calendar occurs on 1 January 1789 Common Era of the Gregorian calendar. |
101 | | /// |
102 | | /// When using this epoch, years are named "of the Great Revolution" or "of the Great Crisis". |
103 | | /// For example, 1855 Common Era in the Gregorian calendar is 67 of the Great Revolution in the |
104 | | /// Positivist calendar. |
105 | | /// |
106 | | /// ## Representation and Examples |
107 | | /// |
108 | | /// ### Months |
109 | | /// |
110 | | /// The months are represented in this crate as [`PositivistMonth`]. |
111 | | /// |
112 | | /// ``` |
113 | | /// use radnelac::calendar::*; |
114 | | /// use radnelac::day_count::*; |
115 | | /// |
116 | | /// let c_1_1 = CommonDate::new(67, 1, 1); |
117 | | /// let a_1_1 = Positivist::try_from_common_date(c_1_1).unwrap(); |
118 | | /// assert_eq!(a_1_1.try_month().unwrap(), PositivistMonth::Moses); |
119 | | /// ``` |
120 | | /// |
121 | | /// ### Weekdays |
122 | | /// |
123 | | /// The days of the Positivist week are not always the same as the days of the common week. |
124 | | /// |
125 | | /// ``` |
126 | | /// use radnelac::calendar::*; |
127 | | /// use radnelac::day_count::*; |
128 | | /// use radnelac::day_cycle::*; |
129 | | /// |
130 | | /// let c = CommonDate::new(66, 13, 28); |
131 | | /// let p = Positivist::try_from_common_date(c).unwrap(); |
132 | | /// assert_eq!(p.weekday().unwrap(), Weekday::Sunday); //Positivist week |
133 | | /// assert_eq!(p.convert::<Weekday>(), Weekday::Saturday); //Common week |
134 | | /// ``` |
135 | | /// |
136 | | /// ### Festivals Ending the Year |
137 | | /// |
138 | | /// The epagomenal festival days at the end of a Positivist year are represented as |
139 | | /// [`PositivistComplementaryDay`]. When converting to and from a [`CommonDate`](crate::calendar::CommonDate), |
140 | | /// the epagomenal days are treated as a 14th month. |
141 | | /// |
142 | | /// ``` |
143 | | /// use radnelac::calendar::*; |
144 | | /// use radnelac::day_count::*; |
145 | | /// |
146 | | /// let c = CommonDate::new(67, 14, 1); |
147 | | /// let a = Positivist::try_from_common_date(c).unwrap(); |
148 | | /// assert!(a.try_month().is_none()); |
149 | | /// assert_eq!(a.epagomenae().unwrap(), PositivistComplementaryDay::FestivalOfTheDead); |
150 | | /// ``` |
151 | | /// |
152 | | /// ## Further reading |
153 | | /// + [Positivists.org](http://positivists.org/calendar.html) |
154 | | /// + [*Calendrier Positiviste* by August Comte](https://gallica.bnf.fr/ark:/12148/bpt6k21868f/f42.planchecontact) |
155 | | /// + [*The Positivist Calendar* by Henry Edger](https://books.google.ca/books?id=S_BRAAAAMAAJ) |
156 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] |
157 | | pub struct Positivist(CommonDate); |
158 | | |
159 | | impl AllowYearZero for Positivist {} |
160 | | |
161 | | impl ToFromOrdinalDate for Positivist { |
162 | 1.28k | fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> { |
163 | 1.28k | let ord_g = OrdinalDate { |
164 | 1.28k | year: ord.year + POSITIVIST_YEAR_OFFSET, |
165 | 1.28k | day_of_year: ord.day_of_year, |
166 | 1.28k | }; |
167 | 1.28k | Gregorian::valid_ordinal(ord_g) |
168 | 1.28k | } |
169 | | |
170 | 512 | fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate { |
171 | 512 | let ord_g = Gregorian::ordinal_from_fixed(fixed_date); |
172 | 512 | OrdinalDate { |
173 | 512 | year: ord_g.year - POSITIVIST_YEAR_OFFSET, |
174 | 512 | day_of_year: ord_g.day_of_year, |
175 | 512 | } |
176 | 512 | } |
177 | | |
178 | 9.02k | fn to_ordinal(self) -> OrdinalDate { |
179 | 9.02k | let offset_m = ((self.0.month as i64) - 1) * 28; |
180 | 9.02k | let doy = (offset_m as u16) + (self.0.day as u16); |
181 | 9.02k | OrdinalDate { |
182 | 9.02k | year: self.0.year, |
183 | 9.02k | day_of_year: doy, |
184 | 9.02k | } |
185 | 9.02k | } |
186 | | |
187 | 8.71k | fn from_ordinal_unchecked(ord: OrdinalDate) -> Self { |
188 | 8.71k | let year = ord.year; |
189 | 8.71k | let month = (((ord.day_of_year - 1) as i64).div_euclid(28) + 1) as u8; |
190 | 8.71k | let day = (ord.day_of_year as i64).adjusted_remainder(28) as u8; |
191 | 8.71k | debug_assert!(day > 0 && day < 29); |
192 | 8.71k | Positivist(CommonDate::new(year, month, day)) |
193 | 8.71k | } |
194 | | } |
195 | | |
196 | | impl HasEpagemonae<PositivistComplementaryDay> for Positivist { |
197 | | // Calendier Positiviste Page 8 |
198 | 256 | fn epagomenae(self) -> Option<PositivistComplementaryDay> { |
199 | 256 | if self.0.month == NON_MONTH { |
200 | 2 | PositivistComplementaryDay::from_u8(self.0.day) |
201 | | } else { |
202 | 254 | None |
203 | | } |
204 | 256 | } |
205 | | |
206 | 2.63k | fn epagomenae_count(p_year: i32) -> u8 { |
207 | 2.63k | if Positivist::is_leap(p_year) { |
208 | 853 | 2 |
209 | | } else { |
210 | 1.77k | 1 |
211 | | } |
212 | 2.63k | } |
213 | | } |
214 | | |
215 | | impl Perennial<PositivistMonth, Weekday> for Positivist { |
216 | | // Calendier Positiviste Page 23-30 |
217 | 4.86k | fn weekday(self) -> Option<Weekday> { |
218 | 4.86k | if self.0.month == NON_MONTH { |
219 | 5 | None |
220 | | } else { |
221 | 4.85k | Weekday::from_i64((self.0.day as i64).modulus(7)) |
222 | | } |
223 | 4.86k | } |
224 | | |
225 | 4.60k | fn days_per_week() -> u8 { |
226 | 4.60k | 7 |
227 | 4.60k | } |
228 | | |
229 | 4.60k | fn weeks_per_month() -> u8 { |
230 | 4.60k | 4 |
231 | 4.60k | } |
232 | | } |
233 | | |
234 | | impl HasLeapYears for Positivist { |
235 | | // Not sure about the source for this... |
236 | 3.39k | fn is_leap(p_year: i32) -> bool { |
237 | 3.39k | Gregorian::is_leap(POSITIVIST_YEAR_OFFSET + p_year) |
238 | 3.39k | } |
239 | | } |
240 | | |
241 | | impl CalculatedBounds for Positivist {} |
242 | | |
243 | | impl Epoch for Positivist { |
244 | 0 | fn epoch() -> Fixed { |
245 | 0 | Gregorian::try_year_start(POSITIVIST_YEAR_OFFSET) |
246 | 0 | .expect("Year known to be valid") |
247 | 0 | .to_fixed() |
248 | 0 | } |
249 | | } |
250 | | |
251 | | impl FromFixed for Positivist { |
252 | 8.45k | fn from_fixed(date: Fixed) -> Positivist { |
253 | 8.45k | let ord_g = Gregorian::ordinal_from_fixed(date); |
254 | 8.45k | let ord = OrdinalDate { |
255 | 8.45k | year: ord_g.year - POSITIVIST_YEAR_OFFSET, |
256 | 8.45k | day_of_year: ord_g.day_of_year, |
257 | 8.45k | }; |
258 | 8.45k | Self::from_ordinal_unchecked(ord) |
259 | 8.45k | } |
260 | | } |
261 | | |
262 | | impl ToFixed for Positivist { |
263 | 7.23k | fn to_fixed(self) -> Fixed { |
264 | 7.23k | let y = self.0.year + POSITIVIST_YEAR_OFFSET; |
265 | 7.23k | let offset_y = Gregorian::try_year_start(y) |
266 | 7.23k | .expect("Year known to be valid") |
267 | 7.23k | .to_fixed() |
268 | 7.23k | .get_day_i() |
269 | 7.23k | - 1; |
270 | 7.23k | let doy = self.to_ordinal().day_of_year as i64; |
271 | 7.23k | Fixed::cast_new(offset_y + doy) |
272 | 7.23k | } |
273 | | } |
274 | | |
275 | | impl ToFromCommonDate<PositivistMonth> for Positivist { |
276 | 13.7k | fn to_common_date(self) -> CommonDate { |
277 | 13.7k | self.0 |
278 | 13.7k | } |
279 | | |
280 | 11.1k | fn from_common_date_unchecked(date: CommonDate) -> Self { |
281 | 11.1k | debug_assert!(Self::valid_ymd(date).is_ok()); |
282 | 11.1k | Self(date) |
283 | 11.1k | } |
284 | | |
285 | 23.8k | fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> { |
286 | 23.8k | if date.month < 1 || date.month > NON_MONTH23.6k { |
287 | 768 | Err(CalendarError::InvalidMonth) |
288 | 23.1k | } else if date.day < 1 { |
289 | 256 | Err(CalendarError::InvalidDay) |
290 | 22.8k | } else if date.month < NON_MONTH && date.day > 2820.4k { |
291 | 256 | Err(CalendarError::InvalidDay) |
292 | 22.6k | } else if date.month == NON_MONTH && date.day2.37k > Positivist::epagomenae_count(date.year) { |
293 | 0 | Err(CalendarError::InvalidDay) |
294 | | } else { |
295 | 22.6k | Ok(()) |
296 | | } |
297 | 23.8k | } |
298 | | |
299 | 256 | fn year_end_date(year: i32) -> CommonDate { |
300 | 256 | CommonDate::new(year, NON_MONTH, Positivist::epagomenae_count(year)) |
301 | 256 | } |
302 | | |
303 | 0 | fn month_length(_year: i32, _month: PositivistMonth) -> u8 { |
304 | 0 | 28 |
305 | 0 | } |
306 | | } |
307 | | |
308 | | impl Quarter for Positivist { |
309 | 2.56k | fn quarter(self) -> NonZero<u8> { |
310 | 2.56k | match self.try_week_of_year() { |
311 | 1 | None => NonZero::new(4).unwrap(), |
312 | 2.55k | Some(w) => NonZero::new((w - 1) / 13 + 1).expect("w > 0"), |
313 | | } |
314 | 2.56k | } |
315 | | } |
316 | | |
317 | | /// Represents a date *and time* in the Positivist Calendar |
318 | | pub type PositivistMoment = CalendarMoment<Positivist>; |
319 | | |
320 | | #[cfg(test)] |
321 | | mod tests { |
322 | | use super::*; |
323 | | |
324 | | #[test] |
325 | 1 | fn epoch() { |
326 | 1 | let dg = Gregorian::try_from_common_date(CommonDate::new(1789, 1, 1)).unwrap(); |
327 | 1 | let dp = Positivist::try_from_common_date(CommonDate::new(1, 1, 1)).unwrap(); |
328 | 1 | let fg = dg.to_fixed(); |
329 | 1 | let fp = dp.to_fixed(); |
330 | 1 | assert_eq!(fg, fp); |
331 | 1 | } |
332 | | |
333 | | #[test] |
334 | 1 | fn example_from_text() { |
335 | | //The Positivist Calendar, page 37 |
336 | 1 | let dg = Gregorian::try_from_common_date(CommonDate::new(1855, 1, 1)).unwrap(); |
337 | 1 | let dp = Positivist::try_from_common_date(CommonDate::new(67, 1, 1)).unwrap(); |
338 | 1 | let fg = dg.to_fixed(); |
339 | 1 | let fp = dp.to_fixed(); |
340 | 1 | assert_eq!(fg, fp); |
341 | 1 | } |
342 | | } |