Coverage Report

Created: 2025-10-19 21:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/a220/proj/radnelac/src/calendar/cotsworth.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
use crate::calendar::gregorian::Gregorian;
6
use crate::calendar::prelude::CommonDate;
7
use crate::calendar::prelude::GuaranteedMonth;
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
#[allow(unused_imports)] //FromPrimitive is needed for derive
27
use num_traits::FromPrimitive;
28
use std::num::NonZero;
29
30
/// Represents a month in the Cotsworth Calendar
31
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
32
pub enum CotsworthMonth {
33
    January = 1,
34
    February,
35
    March,
36
    April,
37
    May,
38
    June,
39
    Sol,
40
    July,
41
    August,
42
    September,
43
    October,
44
    November,
45
    December,
46
}
47
48
/// Represents a complementary day of the Cotsworth Calendar
49
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
50
pub enum CotsworthComplementaryDay {
51
    /// The day that ends every year of the Cotsworth Calendar.
52
    /// This is also represented as December 29. It is not part of any week.
53
    YearDay = 1,
54
    /// The extra day added in leap years of the Cotsworth Calendar.
55
    /// This is also represented as June 29. It is not part of any week.
56
    LeapDay,
57
}
58
59
/// Represents a date in the Cotsworth calendar
60
///
61
/// ## Introduction
62
///
63
/// The Cotsworth calendar (also called the International Fixed Calendar, the Eastman plan, or
64
/// the Yearal) was originally designed by Moses Bruine Cotsworth. The supposed benefits compared
65
/// to the Gregorian calendar are that the Cotsworth months all have the same lengths and the
66
/// Cotsworth months always start on the same day of the week, every year.
67
///
68
/// George Eastman instituted the use of the Cotsworth calendar within the Eastman Kodak\
69
/// Company from 1928 to 1989.
70
///
71
/// There was an International Fixed Calendar League advocating for the adoption of the Cotsworth
72
/// calendar from 1923 to 1937.
73
///
74
/// ## Basic structure
75
///
76
/// Years are divided into 13 months. All months have 4 weeks of 7 days each. The first day of
77
/// every month is a Sunday, and the twenty-eighth day of every month is a Saturday.
78
///
79
/// The final month, December, has an extra day which is not part of any week - this is Year Day.
80
///
81
/// During leap years the sixth month, June, also has an extra day which is not part of any week -
82
/// this is Leap Day. The Cotsworth calendar follows the Gregorian leap year rule: if a Gregorian
83
/// year is a leap year, the corresponding Cotsworth year is also a leap year.
84
///
85
/// The start of any given Gregorian year is also the start of the corresponding Cotsworth year.
86
///
87
/// ## Epoch
88
///
89
/// The first year of the Cotsworth calendar is also the first year of the proleptic Gregorian
90
/// calendar.
91
///
92
/// ## Representation and Examples
93
///
94
/// ### Months
95
///
96
/// The months are represented in this crate as [`CotsworthMonth`].
97
///
98
/// ```
99
/// use radnelac::calendar::*;
100
/// use radnelac::day_count::*;
101
///
102
/// let c_1_1 = CommonDate::new(1902, 1, 1);
103
/// let a_1_1 = Cotsworth::try_from_common_date(c_1_1).unwrap();
104
/// assert_eq!(a_1_1.month(), CotsworthMonth::January);
105
/// ```
106
///
107
/// Note that although many month names are shared with [`GregorianMonth`](crate::calendar::GregorianMonth),
108
/// the months of these two calendar systems are represented by distinct enums. This is because:
109
///
110
/// 1. [`CotsworthMonth::Sol`] has no corresponding [`GregorianMonth`](crate::calendar::GregorianMonth)
111
/// 2. Any [`CotsworthMonth`] after [`CotsworthMonth::Sol`] has a different numeric value than
112
///    the corresponding [`GregorianMonth`](crate::calendar::GregorianMonth).
113
///
114
/// ```
115
/// use radnelac::calendar::*;
116
/// use radnelac::day_count::*;
117
///
118
/// assert_eq!(CotsworthMonth::June as u8, GregorianMonth::June as u8);
119
/// assert!(CotsworthMonth::June < CotsworthMonth::Sol && CotsworthMonth::Sol < CotsworthMonth::July);
120
/// assert_ne!(CotsworthMonth::July as u8, GregorianMonth::July as u8);
121
///
122
/// ```
123
///
124
/// ### Weekdays
125
///
126
/// The days of the Cotsworth week are not always the same as the days of the common week.
127
///
128
/// ```
129
/// use radnelac::calendar::*;
130
/// use radnelac::day_count::*;
131
/// use radnelac::day_cycle::*;
132
///
133
/// let c_1_1 = CommonDate::new(2025, 1, 1);
134
/// let a_1_1 = Cotsworth::try_from_common_date(c_1_1).unwrap();
135
/// assert_eq!(a_1_1.weekday().unwrap(), Weekday::Sunday); //Cotsworth week
136
/// assert_eq!(a_1_1.convert::<Weekday>(), Weekday::Wednesday); //Common week
137
/// ```
138
///
139
/// ### Year Day and Leap Day
140
///
141
/// Year Day and Leap Day can be represented using [`CotsworthComplementaryDay`], or as
142
/// December 29 and June 29 respectively.
143
///
144
/// ```
145
/// use radnelac::calendar::*;
146
/// use radnelac::day_count::*;
147
///
148
/// let c_year_day = CommonDate::new(2028, 13, 29);
149
/// let a_year_day = Cotsworth::try_from_common_date(c_year_day).unwrap();
150
/// assert_eq!(a_year_day.month(), CotsworthMonth::December);
151
/// assert_eq!(a_year_day.epagomenae().unwrap(), CotsworthComplementaryDay::YearDay);
152
/// assert!(a_year_day.weekday().is_none());
153
/// let c_leap_day = CommonDate::new(2028, 6, 29);
154
/// let a_leap_day = Cotsworth::try_from_common_date(c_leap_day).unwrap();
155
/// assert_eq!(a_leap_day.month(), CotsworthMonth::June);
156
/// assert_eq!(a_leap_day.epagomenae().unwrap(), CotsworthComplementaryDay::LeapDay);
157
/// assert!(a_leap_day.weekday().is_none());
158
/// ```
159
///
160
/// ## Inconsistencies with Other Implementations
161
///
162
/// In other implementations of the Cotsworth calendar, Leap Day and Year Day may be treated as
163
/// being outside any month. This crate **does not** support that representation - instead
164
/// Leap Day is treated as June 29 and Year Day is treated as December 29.
165
///
166
/// ## Further reading
167
///
168
/// + [Wikipedia](https://en.wikipedia.org/wiki/Cotsworth_calendar)
169
/// + [*The Rational Almanac* by Moses Bruine Cotsworth](https://archive.org/details/rationalalmanact00cotsuoft/mode/2up)
170
/// + [*The Importance of Calendar Reform to the Business World* by George Eastman](https://www.freexenon.com/wp-content/uploads/2018/07/The-Importance-of-Calendar-Reform-to-the-Business-World-George-Eastman.pdf)
171
172
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
173
pub struct Cotsworth(CommonDate);
174
175
impl AllowYearZero for Cotsworth {}
176
177
impl ToFromOrdinalDate for Cotsworth {
178
1.28k
    fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
179
1.28k
        Gregorian::valid_ordinal(ord)
180
1.28k
    }
181
182
8.97k
    fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
183
8.97k
        Gregorian::ordinal_from_fixed(fixed_date)
184
8.97k
    }
185
186
9.01k
    fn to_ordinal(self) -> OrdinalDate {
187
9.01k
        let approx_m = ((self.0.month as i64) - 1) * 28;
188
9.01k
        let offset_m = if self.0.month > 6 && 
Cotsworth::is_leap4.93k
(
self.0.year4.93k
) {
189
1.13k
            approx_m + 1
190
        } else {
191
7.88k
            approx_m
192
        };
193
9.01k
        let doy = (offset_m as u16) + (self.0.day as u16);
194
9.01k
        OrdinalDate {
195
9.01k
            year: self.0.year,
196
9.01k
            day_of_year: doy,
197
9.01k
        }
198
9.01k
    }
199
200
8.71k
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
201
        const LEAP_DAY_ORD: u16 = (6 * 28) + 1;
202
8.71k
        let result = match (ord.day_of_year, Cotsworth::is_leap(ord.year)) {
203
12
            (366, true) => CommonDate::new(ord.year, 13, 29),
204
10
            (365, false) => CommonDate::new(ord.year, 13, 29),
205
4
            (LEAP_DAY_ORD, true) => CommonDate::new(ord.year, 6, 29),
206
8.69k
            (doy, is_leap) => {
207
8.69k
                let correction = if doy < LEAP_DAY_ORD || 
!is_leap4.60k
{
07.64k
} else {
11.04k
};
208
8.69k
                let month = ((((doy - correction) - 1) as i64).div_euclid(28) + 1) as u8;
209
8.69k
                let day = ((doy - correction) as i64).adjusted_remainder(28) as u8;
210
8.69k
                CommonDate::new(ord.year, month, day)
211
            }
212
        };
213
8.71k
        Cotsworth(result)
214
8.71k
    }
215
}
216
217
impl HasEpagemonae<CotsworthComplementaryDay> for Cotsworth {
218
7.68k
    fn epagomenae(self) -> Option<CotsworthComplementaryDay> {
219
7.68k
        if self.0.day == 29 && 
self.0.month == (CotsworthMonth::December as u8)16
{
220
14
            Some(CotsworthComplementaryDay::YearDay)
221
7.66k
        } else if self.0.day == 29 && 
self.0.month == (CotsworthMonth::June as u8)2
{
222
2
            Some(CotsworthComplementaryDay::LeapDay)
223
        } else {
224
7.66k
            None
225
        }
226
7.68k
    }
227
228
0
    fn epagomenae_count(p_year: i32) -> u8 {
229
0
        if Cotsworth::is_leap(p_year) {
230
0
            2
231
        } else {
232
0
            1
233
        }
234
0
    }
235
}
236
237
impl Perennial<CotsworthMonth, Weekday> for Cotsworth {
238
4.86k
    fn weekday(self) -> Option<Weekday> {
239
4.86k
        if self.epagomenae().is_some() {
240
9
            None
241
        } else {
242
4.85k
            Weekday::from_i64(((self.0.day as i64) - 1).modulus(7))
243
        }
244
4.86k
    }
245
246
4.60k
    fn days_per_week() -> u8 {
247
4.60k
        7
248
4.60k
    }
249
250
4.60k
    fn weeks_per_month() -> u8 {
251
4.60k
        4
252
4.60k
    }
253
}
254
255
impl HasLeapYears for Cotsworth {
256
14.7k
    fn is_leap(c_year: i32) -> bool {
257
14.7k
        Gregorian::is_leap(c_year)
258
14.7k
    }
259
}
260
261
impl CalculatedBounds for Cotsworth {}
262
263
impl Epoch for Cotsworth {
264
0
    fn epoch() -> Fixed {
265
0
        Gregorian::epoch()
266
0
    }
267
}
268
269
impl FromFixed for Cotsworth {
270
8.46k
    fn from_fixed(fixed_date: Fixed) -> Cotsworth {
271
8.46k
        let ord = Self::ordinal_from_fixed(fixed_date);
272
8.46k
        Self::from_ordinal_unchecked(ord)
273
8.46k
    }
274
}
275
276
impl ToFixed for Cotsworth {
277
7.22k
    fn to_fixed(self) -> Fixed {
278
7.22k
        let offset_y = Gregorian::try_year_start(self.0.year)
279
7.22k
            .expect("Year known to be valid")
280
7.22k
            .to_fixed()
281
7.22k
            .get_day_i()
282
7.22k
            - 1;
283
7.22k
        let ord = self.to_ordinal();
284
7.22k
        Fixed::cast_new(offset_y + (ord.day_of_year as i64))
285
7.22k
    }
286
}
287
288
impl ToFromCommonDate<CotsworthMonth> for Cotsworth {
289
13.8k
    fn to_common_date(self) -> CommonDate {
290
13.8k
        self.0
291
13.8k
    }
292
293
11.1k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
294
11.1k
        debug_assert!(Self::valid_ymd(date).is_ok());
295
11.1k
        Self(date)
296
11.1k
    }
297
298
23.9k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
299
23.9k
        if date.month < 1 || 
date.month > 1323.6k
{
300
768
            Err(CalendarError::InvalidMonth)
301
23.1k
        } else if date.day < 1 || 
date.day > 2922.9k
{
302
512
            Err(CalendarError::InvalidDay)
303
22.6k
        } else if date.day == 29 {
304
2.42k
            if date.month == 13 || (
Cotsworth::is_leap374
(
date.year374
) &&
date.month == 6374
) {
305
2.42k
                Ok(())
306
            } else {
307
0
                Err(CalendarError::InvalidDay)
308
            }
309
        } else {
310
20.2k
            Ok(())
311
        }
312
23.9k
    }
313
314
256
    fn year_end_date(year: i32) -> CommonDate {
315
256
        CommonDate::new(year, CotsworthMonth::December as u8, 29)
316
256
    }
317
318
0
    fn month_length(year: i32, month: CotsworthMonth) -> u8 {
319
0
        let approx_len = Cotsworth::days_per_week() * Cotsworth::weeks_per_month();
320
0
        match (month, Cotsworth::is_leap(year)) {
321
0
            (CotsworthMonth::June, true) => approx_len + 1,
322
0
            (CotsworthMonth::December, _) => approx_len + 1,
323
0
            (_, _) => approx_len,
324
        }
325
0
    }
326
}
327
328
impl Quarter for Cotsworth {
329
2.56k
    fn quarter(self) -> NonZero<u8> {
330
2.56k
        match (self.try_week_of_year(), self.epagomenae()) {
331
5
            (None, Some(CotsworthComplementaryDay::YearDay)) => NonZero::new(4).unwrap(),
332
1
            (None, Some(CotsworthComplementaryDay::LeapDay)) => NonZero::new(2).unwrap(),
333
2.55k
            (Some(w), None) => NonZero::new((w - 1) / 13 + 1).expect("w > 0"),
334
0
            (_, _) => unreachable!(),
335
        }
336
2.56k
    }
337
}
338
339
impl GuaranteedMonth<CotsworthMonth> for Cotsworth {}
340
341
/// Represents a date *and time* in the Cotsworth Calendar
342
pub type CotsworthMoment = CalendarMoment<Cotsworth>;