Coverage Report

Created: 2025-08-13 21:02

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