1use jiff::civil::{date, Date, Weekday};
2use thiserror::Error;
3
4#[derive(Debug, Error)]
5pub enum Error {
6 #[error(transparent)]
7 TryFromInt(#[from] std::num::TryFromIntError),
8 #[error("{0}")]
9 Computus(&'static str),
10 #[error(transparent)]
11 Jiff(#[from] jiff::Error),
12}
13
14type HolidayDate = fn(i16) -> Result<Date, Error>;
15
16pub fn get_martin_luther_king_day(year: i16) -> Result<Date, Error> {
18 Ok(date(year, 1, 1).nth_weekday_of_month(3, Weekday::Monday)?)
19}
20
21pub fn get_president_day(year: i16) -> Result<Date, Error> {
23 Ok(date(year, 2, 1).nth_weekday_of_month(3, Weekday::Monday)?)
24}
25
26pub fn get_labor_day(year: i16) -> Result<Date, Error> {
28 Ok(date(year, 9, 1).nth_weekday_of_month(1, Weekday::Monday)?)
29}
30
31pub fn get_columbus_day(year: i16) -> Result<Date, Error> {
33 Ok(date(year, 10, 1).nth_weekday_of_month(2, Weekday::Monday)?)
34}
35
36pub fn get_thanksgiving_day(year: i16) -> Result<Date, Error> {
38 Ok(date(year, 11, 1).nth_weekday_of_month(4, Weekday::Thursday)?)
39}
40
41pub fn get_thanksgiving_bridge_day(year: i16) -> Result<Date, Error> {
43 Ok(get_thanksgiving_day(year)?.tomorrow()?)
44}
45
46pub fn get_new_years_day(year: i16) -> Result<Date, Error> {
48 Ok(date(year, 1, 1))
49}
50
51pub fn get_juneteenth(year: i16) -> Result<Date, Error> {
53 Ok(date(year, 6, 19))
54}
55
56pub fn get_day_before_independence_day(year: i16) -> Result<Date, Error> {
58 Ok(get_independence_day(year)?.yesterday()?)
59}
60
61pub fn get_independence_day(year: i16) -> Result<Date, Error> {
63 Ok(date(year, 7, 4))
64}
65
66pub fn get_veterans_day(year: i16) -> Result<Date, Error> {
68 Ok(date(year, 11, 11))
69}
70
71pub fn get_christmas_eve(year: i16) -> Result<Date, Error> {
73 Ok(get_christmas_day(year)?.yesterday()?)
74}
75
76pub fn get_christmas_day(year: i16) -> Result<Date, Error> {
78 Ok(date(year, 12, 25))
79}
80
81pub fn get_memorial_day(year: i16) -> Result<Date, Error> {
83 let mut date = date(year, 5, 31);
84
85 while date.weekday() != Weekday::Monday {
86 date = date.yesterday()?;
87 }
88
89 Ok(date)
90}
91
92pub fn get_easter(year: i16) -> Result<Date, Error> {
94 let easter = computus::gregorian(year.into()).map_err(Error::Computus)?;
95
96 Ok(date(
97 easter.year.try_into()?,
98 easter.month.try_into()?,
99 easter.day.try_into()?,
100 ))
101}
102
103pub fn get_good_friday(year: i16) -> Result<Date, Error> {
105 get_easter(year)
106 .and_then(|easter| easter.yesterday().map_err(Error::Jiff))
107 .and_then(|saturday| saturday.yesterday().map_err(Error::Jiff))
108}
109
110#[derive(Clone, Debug)]
111pub enum Holiday<T: Clone + std::fmt::Debug + std::fmt::Display> {
112 Holiday(T),
113 Observed(T),
114 HalfDay(T),
115}
116
117impl<T: Clone + std::fmt::Debug + std::fmt::Display> std::fmt::Display for Holiday<T> {
118 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
119 match self {
120 Self::Holiday(holiday) => write!(f, "{holiday}"),
121 Self::Observed(holiday) => write!(f, "{holiday} (Observed)"),
122 Self::HalfDay(holiday) => write!(f, "{holiday}"),
123 }
124 }
125}
126
127impl<T: Clone + std::fmt::Debug + std::fmt::Display> Holiday<T> {
128 pub fn is_holiday(&self) -> bool {
130 matches!(self, Self::Holiday(..))
131 }
132
133 pub fn is_observed(&self) -> bool {
135 matches!(self, Self::Observed(..))
136 }
137
138 pub fn is_half_day(&self) -> bool {
140 matches!(self, Self::HalfDay(..))
141 }
142}
143
144pub trait Calendar<T: Clone + std::fmt::Debug + std::fmt::Display + 'static> {
145 const HOLIDAYS: &[(HolidayDate, T)];
147
148 const HALF_DAYS: &[(HolidayDate, T)] = &[];
150
151 fn is_weekday(&self, date: Date) -> bool;
153
154 fn is_weekend(&self, date: Date) -> bool {
156 !self.is_weekday(date)
157 }
158
159 fn next_weekday(&self, date: Date) -> Result<Date, Error> {
161 let mut next = date.tomorrow()?;
162
163 while self.is_weekend(next) {
164 next = next.tomorrow()?;
165 }
166
167 Ok(next)
168 }
169
170 fn previous_weekday(&self, date: Date) -> Result<Date, Error> {
172 let mut previous = date.yesterday()?;
173
174 while self.is_weekend(previous) {
175 previous = previous.yesterday()?;
176 }
177
178 Ok(previous)
179 }
180
181 fn get_observance_day(&self, date: Date) -> Result<Date, Error> {
184 let mut previous = date;
185 let mut next = date;
186
187 while self.is_weekend(previous) && self.is_weekend(next) {
188 previous = previous.yesterday()?;
189 next = next.tomorrow()?;
190 }
191
192 Ok(if self.is_weekday(previous) {
193 previous
194 } else {
195 next
196 })
197 }
198
199 fn get_holiday(&self, date: Date) -> Option<Holiday<T>> {
201 for (f, result) in Self::HOLIDAYS {
202 let Ok(holiday) = f(date.year()) else {
203 continue;
204 };
205
206 if holiday == date {
207 return Some(Holiday::Holiday(result.clone()));
208 }
209
210 let Ok(holiday) = self.get_observance_day(holiday) else {
211 continue;
212 };
213
214 if holiday == date {
215 return Some(Holiday::Observed(result.clone()));
216 }
217 }
218
219 for (f, result) in Self::HALF_DAYS {
220 let Ok(half_day) = f(date.year()) else {
221 continue;
222 };
223
224 if half_day == date {
225 return Some(Holiday::HalfDay(result.clone()));
226 }
227 }
228
229 None
230 }
231
232 fn is_holiday(&self, date: Date) -> bool {
234 self.get_holiday(date)
235 .map(|holiday| holiday.is_holiday())
236 .unwrap_or(false)
237 }
238
239 fn is_observed_holiday(&self, date: Date) -> bool {
242 self.get_holiday(date)
243 .map(|holiday| holiday.is_observed())
244 .unwrap_or(false)
245 }
246
247 fn is_half_day(&self, date: Date) -> bool {
249 self.get_holiday(date)
250 .map(|holiday| holiday.is_half_day())
251 .unwrap_or(false)
252 }
253
254 fn is_business_day(&self, date: Date) -> bool {
257 self.is_weekday(date)
258 && !self
259 .get_holiday(date)
260 .map(|holiday| holiday.is_holiday() || holiday.is_observed())
261 .unwrap_or(false)
262 }
263
264 fn next_business_day(&self, date: Date) -> Result<Date, Error> {
266 let mut next = date.tomorrow()?;
267
268 while !self.is_business_day(next) {
269 next = next.tomorrow()?;
270 }
271
272 Ok(next)
273 }
274
275 fn previous_business_day(&self, date: Date) -> Result<Date, Error> {
277 let mut previous = date.yesterday()?;
278
279 while !self.is_business_day(previous) {
280 previous = previous.yesterday()?;
281 }
282
283 Ok(previous)
284 }
285
286 fn is_bridge_day(&self, date: Date) -> bool {
289 let is_prev_non_business = date
290 .yesterday()
291 .map(|date| !self.is_business_day(date))
292 .unwrap_or(false);
293 let is_next_non_business = date
294 .tomorrow()
295 .map(|date| !self.is_business_day(date))
296 .unwrap_or(false);
297
298 is_prev_non_business && is_next_non_business
299 }
300}
301
302#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
303pub enum UsHoliday {
304 NewYear,
305 MartinLutherKingDay,
306 PresidentDay,
307 GoodFriday,
308 Easter,
309 DayBeforeIndependenceDay,
310 IndependenceDay,
311 MemorialDay,
312 Juneteenth,
313 LaborDay,
314 ColumbusDay,
315 VeteransDay,
316 Thanksgiving,
317 ThanksgivingBridgeDay,
318 ChristmasEve,
319 Christmas,
320}
321
322impl std::fmt::Display for UsHoliday {
323 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
324 write!(
325 f,
326 "{}",
327 match self {
328 Self::NewYear => "New Year",
329 Self::MartinLutherKingDay => "Martin Luther King Day",
330 Self::PresidentDay => "President's Day",
331 Self::GoodFriday => "Good Friday",
332 Self::Easter => "Easter",
333 Self::DayBeforeIndependenceDay => "Day before Independence Day",
334 Self::IndependenceDay => "Independence Day",
335 Self::MemorialDay => "Memorial Day",
336 Self::Juneteenth => "Juneteenth",
337 Self::LaborDay => "Labor Day",
338 Self::ColumbusDay => "Columbus Day",
339 Self::VeteransDay => "Veterans Day",
340 Self::Thanksgiving => "Thanksgiving",
341 Self::ThanksgivingBridgeDay => "Day after Thanksgiving",
342 Self::ChristmasEve => "Christmas Eve",
343 Self::Christmas => "Christmas",
344 }
345 )
346 }
347}
348
349pub struct UsCalendar;
350
351impl Calendar<UsHoliday> for UsCalendar {
352 const HOLIDAYS: &[(HolidayDate, UsHoliday)] = &[
353 (get_new_years_day, UsHoliday::NewYear),
354 (get_martin_luther_king_day, UsHoliday::MartinLutherKingDay),
355 (get_president_day, UsHoliday::PresidentDay),
356 (get_good_friday, UsHoliday::GoodFriday),
357 (get_easter, UsHoliday::Easter),
358 (get_independence_day, UsHoliday::IndependenceDay),
359 (get_memorial_day, UsHoliday::MemorialDay),
360 (get_juneteenth, UsHoliday::Juneteenth),
361 (get_labor_day, UsHoliday::LaborDay),
362 (get_columbus_day, UsHoliday::ColumbusDay),
363 (get_veterans_day, UsHoliday::VeteransDay),
364 (get_thanksgiving_day, UsHoliday::Thanksgiving),
365 (get_christmas_day, UsHoliday::Christmas),
366 ];
367
368 const HALF_DAYS: &[(HolidayDate, UsHoliday)] = &[
369 (
370 get_day_before_independence_day,
371 UsHoliday::DayBeforeIndependenceDay,
372 ),
373 (
374 get_thanksgiving_bridge_day,
375 UsHoliday::ThanksgivingBridgeDay,
376 ),
377 (get_christmas_eve, UsHoliday::ChristmasEve),
378 ];
379
380 fn is_weekday(&self, date: Date) -> bool {
381 matches!(
382 date.weekday(),
383 Weekday::Monday
384 | Weekday::Tuesday
385 | Weekday::Wednesday
386 | Weekday::Thursday
387 | Weekday::Friday
388 )
389 }
390}