business_days/
lib.rs

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
16/// Martin Luther King Day is on the 3th Monday of January.
17pub 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
21/// President's Day is on the 3th Monday of February.
22pub fn get_president_day(year: i16) -> Result<Date, Error> {
23    Ok(date(year, 2, 1).nth_weekday_of_month(3, Weekday::Monday)?)
24}
25
26/// Labor Day is on the 1st Monday of September.
27pub fn get_labor_day(year: i16) -> Result<Date, Error> {
28    Ok(date(year, 9, 1).nth_weekday_of_month(1, Weekday::Monday)?)
29}
30
31/// Columbus Day is on the 2nd Monday of October.
32pub fn get_columbus_day(year: i16) -> Result<Date, Error> {
33    Ok(date(year, 10, 1).nth_weekday_of_month(2, Weekday::Monday)?)
34}
35
36/// Thanksgiving Day is on the 4th Thursday of November.
37pub fn get_thanksgiving_day(year: i16) -> Result<Date, Error> {
38    Ok(date(year, 11, 1).nth_weekday_of_month(4, Weekday::Thursday)?)
39}
40
41/// Thanksgiving has a bridge day on the Friday after Thanksgiving.
42pub fn get_thanksgiving_bridge_day(year: i16) -> Result<Date, Error> {
43    Ok(get_thanksgiving_day(year)?.tomorrow()?)
44}
45
46/// New Years Day is on the 1st of January.
47pub fn get_new_years_day(year: i16) -> Result<Date, Error> {
48    Ok(date(year, 1, 1))
49}
50
51/// Juneteenth is on the 19th of June.
52pub fn get_juneteenth(year: i16) -> Result<Date, Error> {
53    Ok(date(year, 6, 19))
54}
55
56/// The Day before Independence Day is on the 3th of July.
57pub fn get_day_before_independence_day(year: i16) -> Result<Date, Error> {
58    Ok(get_independence_day(year)?.yesterday()?)
59}
60
61/// Independence Day is on the 4th of July.
62pub fn get_independence_day(year: i16) -> Result<Date, Error> {
63    Ok(date(year, 7, 4))
64}
65
66/// Veteran's Day is on the 11th of November.
67pub fn get_veterans_day(year: i16) -> Result<Date, Error> {
68    Ok(date(year, 11, 11))
69}
70
71/// Christmas Eve is the day before Christmas Day.
72pub fn get_christmas_eve(year: i16) -> Result<Date, Error> {
73    Ok(get_christmas_day(year)?.yesterday()?)
74}
75
76/// Christmas Day is on the 25th of December.
77pub fn get_christmas_day(year: i16) -> Result<Date, Error> {
78    Ok(date(year, 12, 25))
79}
80
81/// Memorial Day is on the last Monday of May.
82pub 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
92/// Easter Day is calculated using the Computus method.
93pub 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
103/// Good Friday is two days before Easter Day (Sunday).
104pub 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    /// Returns `true` if the holiday is an actual holiday. Returns `false` otherwise.
129    pub fn is_holiday(&self) -> bool {
130        matches!(self, Self::Holiday(..))
131    }
132
133    /// Returns `true` if the holiday is observed. Returns `false` otherwise.
134    pub fn is_observed(&self) -> bool {
135        matches!(self, Self::Observed(..))
136    }
137
138    /// Returns `true` if the day is a half-day. Returns `false` otherwise.
139    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    /// The array of functions used to calculate the date of a holiday for a given year.
146    const HOLIDAYS: &[(HolidayDate, T)];
147
148    /// The array of functions used to calculate the date of a half-day for a given year.
149    const HALF_DAYS: &[(HolidayDate, T)] = &[];
150
151    /// Returns `true` if the given date is on a weekday. Returns `false` otherwise.
152    fn is_weekday(&self, date: Date) -> bool;
153
154    /// Returns `true` if the given date is on a weekend. Returns `false` otherwise.
155    fn is_weekend(&self, date: Date) -> bool {
156        !self.is_weekday(date)
157    }
158
159    /// Returns the next consecutive weekday.
160    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    /// Returns the previous consecutive weekday.
171    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    /// Returns the observance day for a given date on which that date would be observed, i.e. the
182    /// closest weekday to the given date if the date is on a weekend.
183    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    /// Returns whether the given date has a holiday or an observed holiday.
200    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    /// Returns `true` if the given date is a holiday. Returns `false` otherwise.
233    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    /// Returns `true` if the given date is the observance day for a holiday. Returns `false`
240    /// otherwise.
241    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    /// Returns `true` if the given date is a half day. Returns `false` otherwise.
248    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    /// Returns `true` if the given date is a business day, i.e., if the given date is neither a
255    /// holiday nor the observance day for a holiday. Returns `false` otherwise.
256    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    /// Returns the next consecutive business day for the given date.
265    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    /// Returns the previous consecutive business day for the given date.
276    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    /// Returns `true` if the given date is a bridge day, i.e., a day that falls between a holiday
287    /// and the weekend. Returns `false` otherwise.
288    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}