Coverage Report

Created: 2025-08-13 21:01

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::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
11.3k
    fn to_ordinal(self) -> OrdinalDate {
103
11.3k
        let offset_m = ((self.0.month as i64) - 1) * 28;
104
11.3k
        let doy = (offset_m as u16) + (self.0.day as u16);
105
11.3k
        OrdinalDate {
106
11.3k
            year: self.0.year,
107
11.3k
            day_of_year: doy,
108
11.3k
        }
109
11.3k
    }
110
111
15.6k
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
112
15.6k
        let year = ord.year;
113
15.6k
        let month = (((ord.day_of_year - 1) as i64).div_euclid(28) + 1) as u8;
114
15.6k
        let day = (ord.day_of_year as i64).adjusted_remainder(28) as u8;
115
15.6k
        debug_assert!(day > 0 && day < 29);
116
15.6k
        Positivist(CommonDate::new(year, month, day))
117
15.6k
    }
118
}
119
120
impl HasIntercalaryDays<PositivistComplementaryDay> for Positivist {
121
    // Calendier Positiviste Page 8
122
4.35k
    fn complementary(self) -> Option<PositivistComplementaryDay> {
123
4.35k
        if self.0.month == NON_MONTH {
124
1.03k
            PositivistComplementaryDay::from_u8(self.0.day)
125
        } else {
126
3.32k
            None
127
        }
128
4.35k
    }
129
130
4.75k
    fn complementary_count(p_year: i32) -> u8 {
131
4.75k
        if Positivist::is_leap(p_year) {
132
2.27k
            2
133
        } else {
134
2.47k
            1
135
        }
136
4.75k
    }
137
}
138
139
impl Perennial<PositivistMonth, Weekday> for Positivist {
140
    // Calendier Positiviste Page 23-30
141
9.46k
    fn weekday(self) -> Option<Weekday> {
142
9.46k
        if self.0.month == NON_MONTH {
143
16
            None
144
        } else {
145
9.44k
            Weekday::from_i64((self.0.day as i64).modulus(7))
146
        }
147
9.46k
    }
148
149
2.55k
    fn days_per_week() -> u8 {
150
2.55k
        7
151
2.55k
    }
152
153
2.54k
    fn weeks_per_month() -> u8 {
154
2.54k
        4
155
2.54k
    }
156
}
157
158
impl HasLeapYears for Positivist {
159
    // Not sure about the source for this...
160
5.78k
    fn is_leap(p_year: i32) -> bool {
161
5.78k
        Gregorian::is_leap(POSITIVIST_YEAR_OFFSET + p_year)
162
5.78k
    }
163
}
164
165
impl CalculatedBounds for Positivist {}
166
167
impl Epoch for Positivist {
168
1.53k
    fn epoch() -> Fixed {
169
1.53k
        Gregorian::try_year_start(POSITIVIST_YEAR_OFFSET)
170
1.53k
            .expect("Year known to be valid")
171
1.53k
            .to_fixed()
172
1.53k
    }
173
}
174
175
impl FromFixed for Positivist {
176
15.3k
    fn from_fixed(date: Fixed) -> Positivist {
177
15.3k
        let ord_g = Gregorian::ordinal_from_fixed(date);
178
15.3k
        let ord = OrdinalDate {
179
15.3k
            year: ord_g.year - POSITIVIST_YEAR_OFFSET,
180
15.3k
            day_of_year: ord_g.day_of_year,
181
15.3k
        };
182
15.3k
        Self::from_ordinal_unchecked(ord)
183
15.3k
    }
184
}
185
186
impl ToFixed for Positivist {
187
9.02k
    fn to_fixed(self) -> Fixed {
188
9.02k
        let y = self.0.year + POSITIVIST_YEAR_OFFSET;
189
9.02k
        let offset_y = Gregorian::try_year_start(y)
190
9.02k
            .expect("Year known to be valid")
191
9.02k
            .to_fixed()
192
9.02k
            .get_day_i()
193
9.02k
            - 1;
194
9.02k
        let doy = self.to_ordinal().day_of_year as i64;
195
9.02k
        Fixed::cast_new(offset_y + doy)
196
9.02k
    }
197
}
198
199
impl ToFromCommonDate<PositivistMonth> for Positivist {
200
37.6k
    fn to_common_date(self) -> CommonDate {
201
37.6k
        self.0
202
37.6k
    }
203
204
13.0k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
205
13.0k
        debug_assert!(Self::valid_ymd(date).is_ok());
206
13.0k
        Self(date)
207
13.0k
    }
208
209
27.5k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
210
27.5k
        if date.month < 1 || 
date.month > NON_MONTH27.2k
{
211
768
            Err(CalendarError::InvalidMonth)
212
26.7k
        } else if date.day < 1 {
213
256
            Err(CalendarError::InvalidDay)
214
26.5k
        } else if date.month < NON_MONTH && 
date.day > 2822.0k
{
215
256
            Err(CalendarError::InvalidDay)
216
26.2k
        } else if date.month == NON_MONTH && 
date.day4.49k
> Positivist::complementary_count(date.year) {
217
0
            Err(CalendarError::InvalidDay)
218
        } else {
219
26.2k
            Ok(())
220
        }
221
27.5k
    }
222
223
257
    fn year_end_date(year: i32) -> CommonDate {
224
257
        CommonDate::new(year, NON_MONTH, Positivist::complementary_count(year))
225
257
    }
226
}
227
228
impl Quarter for Positivist {
229
2.81k
    fn quarter(self) -> NonZero<u8> {
230
2.81k
        match self.try_month() {
231
288
            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
}