/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 | 15.8k | fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate { |
183 | 15.8k | Gregorian::ordinal_from_fixed(fixed_date) |
184 | 15.8k | } |
185 | | |
186 | 11.3k | fn to_ordinal(self) -> OrdinalDate { |
187 | 11.3k | let approx_m = ((self.0.month as i64) - 1) * 28; |
188 | 11.3k | let offset_m = if self.0.month > 6 && Cotsworth::is_leap6.08k (self.0.year6.08k ) { |
189 | 1.53k | approx_m + 1 |
190 | | } else { |
191 | 9.80k | approx_m |
192 | | }; |
193 | 11.3k | let doy = (offset_m as u16) + (self.0.day as u16); |
194 | 11.3k | OrdinalDate { |
195 | 11.3k | year: self.0.year, |
196 | 11.3k | day_of_year: doy, |
197 | 11.3k | } |
198 | 11.3k | } |
199 | | |
200 | 15.6k | fn from_ordinal_unchecked(ord: OrdinalDate) -> Self { |
201 | | const LEAP_DAY_ORD: u16 = (6 * 28) + 1; |
202 | 15.6k | let result = match (ord.day_of_year, Cotsworth::is_leap(ord.year)) { |
203 | 19 | (366, true) => CommonDate::new(ord.year, 13, 29), |
204 | 25 | (365, false) => CommonDate::new(ord.year, 13, 29), |
205 | 25 | (LEAP_DAY_ORD, true) => CommonDate::new(ord.year, 6, 29), |
206 | 15.5k | (doy, is_leap) => { |
207 | 15.5k | let correction = if doy < LEAP_DAY_ORD || !is_leap8.28k { 013.7k } else { 11.84k }; |
208 | 15.5k | let month = ((((doy - correction) - 1) as i64).div_euclid(28) + 1) as u8; |
209 | 15.5k | let day = ((doy - correction) as i64).adjusted_remainder(28) as u8; |
210 | 15.5k | CommonDate::new(ord.year, month, day) |
211 | | } |
212 | | }; |
213 | 15.6k | Cotsworth(result) |
214 | 15.6k | } |
215 | | } |
216 | | |
217 | | impl HasEpagemonae<CotsworthComplementaryDay> for Cotsworth { |
218 | 16.8k | fn epagomenae(self) -> Option<CotsworthComplementaryDay> { |
219 | 16.8k | if self.0.day == 29 && self.0.month == (CotsworthMonth::December as u8)1.05k { |
220 | 542 | Some(CotsworthComplementaryDay::YearDay) |
221 | 16.3k | } else if self.0.day == 29 && self.0.month == (CotsworthMonth::June as u8)517 { |
222 | 517 | Some(CotsworthComplementaryDay::LeapDay) |
223 | | } else { |
224 | 15.8k | None |
225 | | } |
226 | 16.8k | } |
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 | 12.5k | fn weekday(self) -> Option<Weekday> { |
239 | 12.5k | if self.epagomenae().is_some() { |
240 | 33 | None |
241 | | } else { |
242 | 12.4k | Weekday::from_i64(((self.0.day as i64) - 1).modulus(7)) |
243 | | } |
244 | 12.5k | } |
245 | | |
246 | 5.11k | fn days_per_week() -> u8 { |
247 | 5.11k | 7 |
248 | 5.11k | } |
249 | | |
250 | 5.11k | fn weeks_per_month() -> u8 { |
251 | 5.11k | 4 |
252 | 5.11k | } |
253 | | } |
254 | | |
255 | | impl HasLeapYears for Cotsworth { |
256 | 24.1k | fn is_leap(c_year: i32) -> bool { |
257 | 24.1k | Gregorian::is_leap(c_year) |
258 | 24.1k | } |
259 | | } |
260 | | |
261 | | impl CalculatedBounds for Cotsworth {} |
262 | | |
263 | | impl Epoch for Cotsworth { |
264 | 1.53k | fn epoch() -> Fixed { |
265 | 1.53k | Gregorian::epoch() |
266 | 1.53k | } |
267 | | } |
268 | | |
269 | | impl FromFixed for Cotsworth { |
270 | 15.3k | fn from_fixed(fixed_date: Fixed) -> Cotsworth { |
271 | 15.3k | let ord = Self::ordinal_from_fixed(fixed_date); |
272 | 15.3k | Self::from_ordinal_unchecked(ord) |
273 | 15.3k | } |
274 | | } |
275 | | |
276 | | impl ToFixed for Cotsworth { |
277 | 9.03k | fn to_fixed(self) -> Fixed { |
278 | 9.03k | let offset_y = Gregorian::try_year_start(self.0.year) |
279 | 9.03k | .expect("Year known to be valid") |
280 | 9.03k | .to_fixed() |
281 | 9.03k | .get_day_i() |
282 | 9.03k | - 1; |
283 | 9.03k | let ord = self.to_ordinal(); |
284 | 9.03k | Fixed::cast_new(offset_y + (ord.day_of_year as i64)) |
285 | 9.03k | } |
286 | | } |
287 | | |
288 | | impl ToFromCommonDate<CotsworthMonth> for Cotsworth { |
289 | 42.9k | fn to_common_date(self) -> CommonDate { |
290 | 42.9k | self.0 |
291 | 42.9k | } |
292 | | |
293 | 13.0k | fn from_common_date_unchecked(date: CommonDate) -> Self { |
294 | 13.0k | debug_assert!(Self::valid_ymd(date).is_ok()); |
295 | 13.0k | Self(date) |
296 | 13.0k | } |
297 | | |
298 | 27.5k | fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> { |
299 | 27.5k | if date.month < 1 || date.month > 1327.2k { |
300 | 768 | Err(CalendarError::InvalidMonth) |
301 | 26.7k | } else if date.day < 1 || date.day > 2926.5k { |
302 | 512 | Err(CalendarError::InvalidDay) |
303 | 26.2k | } else if date.day == 29 { |
304 | 4.50k | if date.month == 13 || (Cotsworth::is_leap1.43k (date.year1.43k ) && date.month == 61.43k ) { |
305 | 4.50k | Ok(()) |
306 | | } else { |
307 | 0 | Err(CalendarError::InvalidDay) |
308 | | } |
309 | | } else { |
310 | 21.7k | Ok(()) |
311 | | } |
312 | 27.5k | } |
313 | | |
314 | 257 | fn year_end_date(year: i32) -> CommonDate { |
315 | 257 | CommonDate::new(year, CotsworthMonth::December as u8, 29) |
316 | 257 | } |
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 | 0 | (None, Some(CotsworthComplementaryDay::YearDay)) => NonZero::new(4).unwrap(), |
332 | 0 | (None, Some(CotsworthComplementaryDay::LeapDay)) => NonZero::new(2).unwrap(), |
333 | 2.56k | (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>; |