Coverage Report

Created: 2025-10-19 21:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
11.3k
    fn to_ordinal(self) -> OrdinalDate {
179
11.3k
        let offset_m = ((self.0.month as i64) - 1) * 28;
180
11.3k
        let doy = (offset_m as u16) + (self.0.day as u16);
181
11.3k
        OrdinalDate {
182
11.3k
            year: self.0.year,
183
11.3k
            day_of_year: doy,
184
11.3k
        }
185
11.3k
    }
186
187
15.6k
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
188
15.6k
        let year = ord.year;
189
15.6k
        let month = (((ord.day_of_year - 1) as i64).div_euclid(28) + 1) as u8;
190
15.6k
        let day = (ord.day_of_year as i64).adjusted_remainder(28) as u8;
191
15.6k
        debug_assert!(day > 0 && day < 29);
192
15.6k
        Positivist(CommonDate::new(year, month, day))
193
15.6k
    }
194
}
195
196
impl HasEpagemonae<PositivistComplementaryDay> for Positivist {
197
    // Calendier Positiviste Page 8
198
4.36k
    fn epagomenae(self) -> Option<PositivistComplementaryDay> {
199
4.36k
        if self.0.month == NON_MONTH {
200
1.05k
            PositivistComplementaryDay::from_u8(self.0.day)
201
        } else {
202
3.31k
            None
203
        }
204
4.36k
    }
205
206
4.74k
    fn epagomenae_count(p_year: i32) -> u8 {
207
4.74k
        if Positivist::is_leap(p_year) {
208
2.28k
            2
209
        } else {
210
2.46k
            1
211
        }
212
4.74k
    }
213
}
214
215
impl Perennial<PositivistMonth, Weekday> for Positivist {
216
    // Calendier Positiviste Page 23-30
217
12.0k
    fn weekday(self) -> Option<Weekday> {
218
12.0k
        if self.0.month == NON_MONTH {
219
12
            None
220
        } else {
221
11.9k
            Weekday::from_i64((self.0.day as i64).modulus(7))
222
        }
223
12.0k
    }
224
225
5.11k
    fn days_per_week() -> u8 {
226
5.11k
        7
227
5.11k
    }
228
229
5.11k
    fn weeks_per_month() -> u8 {
230
5.11k
        4
231
5.11k
    }
232
}
233
234
impl HasLeapYears for Positivist {
235
    // Not sure about the source for this...
236
5.77k
    fn is_leap(p_year: i32) -> bool {
237
5.77k
        Gregorian::is_leap(POSITIVIST_YEAR_OFFSET + p_year)
238
5.77k
    }
239
}
240
241
impl CalculatedBounds for Positivist {}
242
243
impl Epoch for Positivist {
244
1.53k
    fn epoch() -> Fixed {
245
1.53k
        Gregorian::try_year_start(POSITIVIST_YEAR_OFFSET)
246
1.53k
            .expect("Year known to be valid")
247
1.53k
            .to_fixed()
248
1.53k
    }
249
}
250
251
impl FromFixed for Positivist {
252
15.3k
    fn from_fixed(date: Fixed) -> Positivist {
253
15.3k
        let ord_g = Gregorian::ordinal_from_fixed(date);
254
15.3k
        let ord = OrdinalDate {
255
15.3k
            year: ord_g.year - POSITIVIST_YEAR_OFFSET,
256
15.3k
            day_of_year: ord_g.day_of_year,
257
15.3k
        };
258
15.3k
        Self::from_ordinal_unchecked(ord)
259
15.3k
    }
260
}
261
262
impl ToFixed for Positivist {
263
9.02k
    fn to_fixed(self) -> Fixed {
264
9.02k
        let y = self.0.year + POSITIVIST_YEAR_OFFSET;
265
9.02k
        let offset_y = Gregorian::try_year_start(y)
266
9.02k
            .expect("Year known to be valid")
267
9.02k
            .to_fixed()
268
9.02k
            .get_day_i()
269
9.02k
            - 1;
270
9.02k
        let doy = self.to_ordinal().day_of_year as i64;
271
9.02k
        Fixed::cast_new(offset_y + doy)
272
9.02k
    }
273
}
274
275
impl ToFromCommonDate<PositivistMonth> for Positivist {
276
39.9k
    fn to_common_date(self) -> CommonDate {
277
39.9k
        self.0
278
39.9k
    }
279
280
12.7k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
281
12.7k
        debug_assert!(Self::valid_ymd(date).is_ok());
282
12.7k
        Self(date)
283
12.7k
    }
284
285
27.0k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
286
27.0k
        if date.month < 1 || 
date.month > NON_MONTH26.7k
{
287
768
            Err(CalendarError::InvalidMonth)
288
26.2k
        } else if date.day < 1 {
289
256
            Err(CalendarError::InvalidDay)
290
26.0k
        } else if date.month < NON_MONTH && 
date.day > 2821.5k
{
291
256
            Err(CalendarError::InvalidDay)
292
25.7k
        } else if date.month == NON_MONTH && 
date.day4.49k
> Positivist::epagomenae_count(date.year) {
293
0
            Err(CalendarError::InvalidDay)
294
        } else {
295
25.7k
            Ok(())
296
        }
297
27.0k
    }
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
3
            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
}