/home/a220/proj/radnelac/src/calendar/prelude.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::common::error::CalendarError; |
6 | | use crate::day_count::BoundedDayCount; |
7 | | use crate::day_count::EffectiveBound; |
8 | | use crate::day_count::Fixed; |
9 | | use crate::day_count::ToFixed; |
10 | | use crate::day_cycle::OnOrBefore; |
11 | | use crate::day_cycle::Weekday; |
12 | | use num_traits::FromPrimitive; |
13 | | use num_traits::ToPrimitive; |
14 | | use std::num::NonZero; |
15 | | |
16 | | /// Calendar systems with year 0 |
17 | | pub trait AllowYearZero {} |
18 | | |
19 | | /// Calendar systems with leap years |
20 | | pub trait HasLeapYears { |
21 | | /// [`true`] if a the given year is a leap year. |
22 | | fn is_leap(year: i32) -> bool; |
23 | | } |
24 | | |
25 | | /// Represents a combination of numeric year, month and day |
26 | | /// |
27 | | /// This is not specific to any particular calendar system. |
28 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] |
29 | | pub struct CommonDate { |
30 | | pub year: i32, |
31 | | pub month: u8, |
32 | | pub day: u8, |
33 | | } |
34 | | |
35 | | impl CommonDate { |
36 | | /// Create a `CommonDate` |
37 | 558k | pub fn new(year: i32, month: u8, day: u8) -> CommonDate { |
38 | 558k | CommonDate { year, month, day } |
39 | 558k | } |
40 | | } |
41 | | |
42 | | /// Calendar systems in which a date can be represented by a year, month and day |
43 | | pub trait ToFromCommonDate<T: FromPrimitive>: Sized + EffectiveBound { |
44 | | /// Convert calendar date to a year, month and day |
45 | | fn to_common_date(self) -> CommonDate; |
46 | | /// Convert a year, month and day into a calendar date without checking validity |
47 | | /// |
48 | | /// In almost all cases, [`try_from_common_date`](ToFromCommonDate::try_from_common_date) is preferred. |
49 | | fn from_common_date_unchecked(d: CommonDate) -> Self; |
50 | | /// Returns error if the year, month or day is invalid |
51 | | fn valid_ymd(d: CommonDate) -> Result<(), CalendarError>; |
52 | | /// Start of the year as a numeric year, month and day |
53 | 35.5k | fn year_start_date(year: i32) -> CommonDate { |
54 | | //LISTING 2.18 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
55 | | //Modified to be generalized across many calendar systems |
56 | 35.5k | CommonDate::new(year, 1, 1) |
57 | 35.5k | } |
58 | | /// End of the year as a numeric year, month and day |
59 | | fn year_end_date(year: i32) -> CommonDate; |
60 | | /// Length of month in a given year |
61 | | fn month_length(year: i32, month: T) -> u8; |
62 | | |
63 | | /// [`true`] if the year, month and day is within the supported range of time. |
64 | | /// |
65 | | /// This does not check the validity of the date. |
66 | 40.0k | fn in_effective_bounds(d: CommonDate) -> bool { |
67 | 40.0k | let min = Self::effective_min().to_common_date(); |
68 | 40.0k | let max = Self::effective_max().to_common_date(); |
69 | 40.0k | d >= min && d <= max |
70 | 40.0k | } |
71 | | |
72 | | /// Attempt to create a date in a specific calendar from a [`CommonDate`] |
73 | 413k | fn try_from_common_date(d: CommonDate) -> Result<Self, CalendarError> { |
74 | 413k | match Self::valid_ymd(d) { |
75 | 20.4k | Err(e) => Err(e), |
76 | 393k | Ok(_) => Ok(Self::from_common_date_unchecked(d)), |
77 | | } |
78 | 413k | } |
79 | | |
80 | | /// Attempt to create a date in a specific calendar at the start of a specific year |
81 | | /// |
82 | | /// This may return an error if the year is 0, and the implementor does not support |
83 | | /// year 0. |
84 | 35.8k | fn try_year_start(year: i32) -> Result<Self, CalendarError> { |
85 | | //Might be invalid for calendars without year 0 |
86 | 35.8k | let d = Self::year_start_date(year); |
87 | 35.8k | debug_assert!(Self::in_effective_bounds(d), "year_start: {}"0 , year); |
88 | 35.8k | Self::try_from_common_date(d) |
89 | 35.8k | } |
90 | | |
91 | | /// Attempt to create a date in a specific calendar at the end of a specific year |
92 | | /// |
93 | | /// This may return an error if the year is 0, and the implementor does not support |
94 | | /// year 0. |
95 | 4.21k | fn try_year_end(year: i32) -> Result<Self, CalendarError> { |
96 | | //Might be invalid for calendars without year 0 |
97 | 4.21k | let d = Self::year_end_date(year); |
98 | 4.21k | debug_assert!(Self::in_effective_bounds(d)); |
99 | 4.21k | Self::try_from_common_date(d) |
100 | 4.21k | } |
101 | | |
102 | 16.4k | fn day(self) -> u8 { |
103 | 16.4k | self.to_common_date().day |
104 | 16.4k | } |
105 | | |
106 | | /// Attempt to return the month |
107 | | /// |
108 | | /// In some calendars, certain dates are not associated with a month, which is why |
109 | | /// this function returns an [`Option`]. |
110 | | /// |
111 | | /// Callers using a calendar which guarantees that every date has a month |
112 | | /// should use [`GuaranteedMonth::month`]. |
113 | 66.8k | fn try_month(self) -> Option<T> { |
114 | 66.8k | T::from_u8(self.to_common_date().month) |
115 | 66.8k | } |
116 | | |
117 | 17.9k | fn year(self) -> i32 { |
118 | 17.9k | self.to_common_date().year |
119 | 17.9k | } |
120 | | } |
121 | | |
122 | | /// Calendar systems in which dates which are guaranteed to have a month |
123 | | pub trait GuaranteedMonth<T: FromPrimitive + ToPrimitive>: ToFromCommonDate<T> { |
124 | 21.9k | fn month(self) -> T { |
125 | 21.9k | self.try_month().expect("Month is guaranteed") |
126 | 21.9k | } |
127 | | |
128 | | /// Attempt to a date in a specific calendar system |
129 | 0 | fn try_new(year: i32, month: T, day: u8) -> Result<Self, CalendarError> { |
130 | 0 | let m = month.to_u8().expect("Month is correct type"); |
131 | 0 | Self::try_from_common_date(CommonDate::new(year, m, day)) |
132 | 0 | } |
133 | | } |
134 | | |
135 | | /// Calendar systems which have epagomenae |
136 | | /// |
137 | | /// "Epagomenae" are also known as "intercalary days", "blank days" or "monthless days".[^1][^2] |
138 | | /// |
139 | | /// [^1]: <https://en.wikipedia.org/wiki/Intercalary_month_(Egypt)> |
140 | | /// [^2]: <https://en.wikipedia.org/wiki/Intercalation_(timekeeping)> |
141 | | pub trait HasEpagemonae<T: FromPrimitive + ToPrimitive> { |
142 | | fn epagomenae(self) -> Option<T>; |
143 | | fn epagomenae_count(year: i32) -> u8; |
144 | | } |
145 | | |
146 | | /// Calendar systems which are perennial |
147 | | pub trait Perennial<S, T>: ToFromCommonDate<S> |
148 | | where |
149 | | S: FromPrimitive + ToPrimitive, |
150 | | T: FromPrimitive + ToPrimitive, |
151 | | { |
152 | | fn weekday(self) -> Option<T>; |
153 | | fn days_per_week() -> u8; |
154 | | fn weeks_per_month() -> u8; |
155 | | |
156 | 15.3k | fn try_week_of_year(self) -> Option<u8> { |
157 | 15.3k | match (self.weekday(), self.try_month()) { |
158 | 15.2k | (Some(_), Some(month)) => { |
159 | 15.2k | let d = self.day() as i64; |
160 | 15.2k | let m = month.to_i64().expect("Guaranteed in range"); |
161 | 15.2k | let dpw = Self::days_per_week() as i64; |
162 | 15.2k | let wpm = Self::weeks_per_month() as i64; |
163 | 15.2k | let dpm = dpw * wpm; |
164 | 15.2k | Some(((((m - 1) * dpm) + d - 1) / dpw + 1) as u8) |
165 | | } |
166 | 70 | (_, _) => None, |
167 | | } |
168 | 15.3k | } |
169 | | } |
170 | | |
171 | | /// Calendar systems in which a year can be divided into quarters |
172 | | /// |
173 | | /// The quarters may not have exactly the same number of days. |
174 | | pub trait Quarter { |
175 | | /// Calculate the quarter associated with a particular date. |
176 | | /// |
177 | | /// This must be with the range [1..4] inclusive. |
178 | | fn quarter(self) -> NonZero<u8>; |
179 | | } |
180 | | |
181 | | /// Calendar systems in which a week of year can be calculated for a date |
182 | | pub trait CommonWeekOfYear<T: FromPrimitive>: ToFromCommonDate<T> + ToFixed { |
183 | | /// Calculate the week of year for a particular date. |
184 | 16.8k | fn week_of_year(self) -> u8 { |
185 | 16.8k | let today = self.to_fixed(); |
186 | 16.8k | let start = Self::try_year_start(self.year()) |
187 | 16.8k | .expect("Year known to be valid") |
188 | 16.8k | .to_fixed(); |
189 | 16.8k | let diff = today.get_day_i() - start.get_day_i(); |
190 | 16.8k | (diff.div_euclid(7) + 1) as u8 |
191 | 16.8k | } |
192 | | |
193 | | /// Find the nth occurence of a given day of the week |
194 | 16.6k | fn nth_kday(self, nz: NonZero<i16>, k: Weekday) -> Fixed { |
195 | | //LISTING 2.33 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
196 | | //Arguments swapped from the original, generalized to arbitrary calendar systems |
197 | 16.6k | let n = nz.get(); |
198 | 16.6k | let result = if n > 0 { |
199 | 16.6k | k.before(self.to_fixed()).get_day_i() + (7 * n as i64) |
200 | | } else { |
201 | 1 | k.after(self.to_fixed()).get_day_i() + (7 * n as i64) |
202 | | }; |
203 | 16.6k | Fixed::cast_new(result) |
204 | 16.6k | } |
205 | | |
206 | | //TODO: first_kday (listing 2.34) |
207 | | //TODO: last_kday (listing 2.34) |
208 | | } |
209 | | |
210 | | /// Represents a numeric year and day of year. |
211 | | /// |
212 | | /// This is not specific to any particular calendar system. |
213 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] |
214 | | pub struct OrdinalDate { |
215 | | pub year: i32, |
216 | | pub day_of_year: u16, |
217 | | } |
218 | | |
219 | | /// Calendar systems in which a date can be represented by a year and day of year |
220 | | pub trait ToFromOrdinalDate: Sized { |
221 | | /// Check if the year and day of year is valid for a particular calendar system |
222 | | fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError>; |
223 | | /// Calculate the year and day of year from a [`Fixed`]. |
224 | | fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate; |
225 | | /// Calculate the year and day of year from a calendar date. |
226 | | fn to_ordinal(self) -> OrdinalDate; |
227 | | /// Convert a year and day of year into a calendar date without checking validity |
228 | | /// |
229 | | /// In almost all cases, [`try_from_ordinal`](ToFromOrdinalDate::try_from_ordinal) is preferred. |
230 | | fn from_ordinal_unchecked(ord: OrdinalDate) -> Self; |
231 | | /// Attempt to create a date in a specific calendar from an [`OrdinalDate`] |
232 | 4.09k | fn try_from_ordinal(ord: OrdinalDate) -> Result<Self, CalendarError> { |
233 | 4.09k | match Self::valid_ordinal(ord) { |
234 | 0 | Err(e) => Err(e), |
235 | 4.09k | Ok(_) => Ok(Self::from_ordinal_unchecked(ord)), |
236 | | } |
237 | 4.09k | } |
238 | | } |