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/// ## 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)]
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    // Calendier Positiviste Page 8
122    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    // Calendier Positiviste Page 23-30
141    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    // Not sure about the source for this...
160    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
241/// Represents a date *and time* in the Positivist Calendar
242pub type PositivistMoment = CalendarMoment<Positivist>;
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn example_from_text() {
250        //The Positivist Calendar, page 37
251        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}