radnelac/calendar/
positivist.rs

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
6use 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)] //FromPrimitive is needed for derive
29use num_traits::FromPrimitive;
30
31const POSITIVIST_YEAR_OFFSET: i32 = 1789 - 1;
32const 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)]
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/// 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)]
66pub 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)]
157pub struct Positivist(CommonDate);
158
159impl AllowYearZero for Positivist {}
160
161impl ToFromOrdinalDate for Positivist {
162    fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
163        let ord_g = OrdinalDate {
164            year: ord.year + POSITIVIST_YEAR_OFFSET,
165            day_of_year: ord.day_of_year,
166        };
167        Gregorian::valid_ordinal(ord_g)
168    }
169
170    fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
171        let ord_g = Gregorian::ordinal_from_fixed(fixed_date);
172        OrdinalDate {
173            year: ord_g.year - POSITIVIST_YEAR_OFFSET,
174            day_of_year: ord_g.day_of_year,
175        }
176    }
177
178    fn to_ordinal(self) -> OrdinalDate {
179        let offset_m = ((self.0.month as i64) - 1) * 28;
180        let doy = (offset_m as u16) + (self.0.day as u16);
181        OrdinalDate {
182            year: self.0.year,
183            day_of_year: doy,
184        }
185    }
186
187    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
188        let year = ord.year;
189        let month = (((ord.day_of_year - 1) as i64).div_euclid(28) + 1) as u8;
190        let day = (ord.day_of_year as i64).adjusted_remainder(28) as u8;
191        debug_assert!(day > 0 && day < 29);
192        Positivist(CommonDate::new(year, month, day))
193    }
194}
195
196impl HasEpagemonae<PositivistComplementaryDay> for Positivist {
197    // Calendier Positiviste Page 8
198    fn epagomenae(self) -> Option<PositivistComplementaryDay> {
199        if self.0.month == NON_MONTH {
200            PositivistComplementaryDay::from_u8(self.0.day)
201        } else {
202            None
203        }
204    }
205
206    fn epagomenae_count(p_year: i32) -> u8 {
207        if Positivist::is_leap(p_year) {
208            2
209        } else {
210            1
211        }
212    }
213}
214
215impl Perennial<PositivistMonth, Weekday> for Positivist {
216    // Calendier Positiviste Page 23-30
217    fn weekday(self) -> Option<Weekday> {
218        if self.0.month == NON_MONTH {
219            None
220        } else {
221            Weekday::from_i64((self.0.day as i64).modulus(7))
222        }
223    }
224
225    fn days_per_week() -> u8 {
226        7
227    }
228
229    fn weeks_per_month() -> u8 {
230        4
231    }
232}
233
234impl HasLeapYears for Positivist {
235    // Not sure about the source for this...
236    fn is_leap(p_year: i32) -> bool {
237        Gregorian::is_leap(POSITIVIST_YEAR_OFFSET + p_year)
238    }
239}
240
241impl CalculatedBounds for Positivist {}
242
243impl Epoch for Positivist {
244    fn epoch() -> Fixed {
245        Gregorian::try_year_start(POSITIVIST_YEAR_OFFSET)
246            .expect("Year known to be valid")
247            .to_fixed()
248    }
249}
250
251impl FromFixed for Positivist {
252    fn from_fixed(date: Fixed) -> Positivist {
253        let ord_g = Gregorian::ordinal_from_fixed(date);
254        let ord = OrdinalDate {
255            year: ord_g.year - POSITIVIST_YEAR_OFFSET,
256            day_of_year: ord_g.day_of_year,
257        };
258        Self::from_ordinal_unchecked(ord)
259    }
260}
261
262impl ToFixed for Positivist {
263    fn to_fixed(self) -> Fixed {
264        let y = self.0.year + POSITIVIST_YEAR_OFFSET;
265        let offset_y = Gregorian::try_year_start(y)
266            .expect("Year known to be valid")
267            .to_fixed()
268            .get_day_i()
269            - 1;
270        let doy = self.to_ordinal().day_of_year as i64;
271        Fixed::cast_new(offset_y + doy)
272    }
273}
274
275impl ToFromCommonDate<PositivistMonth> for Positivist {
276    fn to_common_date(self) -> CommonDate {
277        self.0
278    }
279
280    fn from_common_date_unchecked(date: CommonDate) -> Self {
281        debug_assert!(Self::valid_ymd(date).is_ok());
282        Self(date)
283    }
284
285    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
286        if date.month < 1 || date.month > NON_MONTH {
287            Err(CalendarError::InvalidMonth)
288        } else if date.day < 1 {
289            Err(CalendarError::InvalidDay)
290        } else if date.month < NON_MONTH && date.day > 28 {
291            Err(CalendarError::InvalidDay)
292        } else if date.month == NON_MONTH && date.day > Positivist::epagomenae_count(date.year) {
293            Err(CalendarError::InvalidDay)
294        } else {
295            Ok(())
296        }
297    }
298
299    fn year_end_date(year: i32) -> CommonDate {
300        CommonDate::new(year, NON_MONTH, Positivist::epagomenae_count(year))
301    }
302
303    fn month_length(_year: i32, _month: PositivistMonth) -> u8 {
304        28
305    }
306}
307
308impl Quarter for Positivist {
309    fn quarter(self) -> NonZero<u8> {
310        match self.try_week_of_year() {
311            None => NonZero::new(4).unwrap(),
312            Some(w) => NonZero::new((w - 1) / 13 + 1).expect("w > 0"),
313        }
314    }
315}
316
317/// Represents a date *and time* in the Positivist Calendar
318pub type PositivistMoment = CalendarMoment<Positivist>;
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn epoch() {
326        let dg = Gregorian::try_from_common_date(CommonDate::new(1789, 1, 1)).unwrap();
327        let dp = Positivist::try_from_common_date(CommonDate::new(1, 1, 1)).unwrap();
328        let fg = dg.to_fixed();
329        let fp = dp.to_fixed();
330        assert_eq!(fg, fp);
331    }
332
333    #[test]
334    fn example_from_text() {
335        //The Positivist Calendar, page 37
336        let dg = Gregorian::try_from_common_date(CommonDate::new(1855, 1, 1)).unwrap();
337        let dp = Positivist::try_from_common_date(CommonDate::new(67, 1, 1)).unwrap();
338        let fg = dg.to_fixed();
339        let fp = dp.to_fixed();
340        assert_eq!(fg, fp);
341    }
342}