calenda_rs/
utilities.rs

1// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2// calenda-rs: A Rust library for global calendars.
3// Copyright (C) 2024 https://github.com/avhz
4//
5// Dual licensed under Apache 2.0 and MIT.
6//
7// See:
8//      - LICENSE-APACHE.md
9//      - LICENSE-MIT.md
10// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11
12//! This module defines general calendar and holiday related functions.
13
14use crate::{calendar::Calendar, constants::EASTER_MONDAYS};
15use time::{
16    util::{days_in_year, days_in_year_month, is_leap_year},
17    Date, Duration, Error, Month, Weekday,
18};
19
20/// Unpacks a `Date` into a tuple in the following form:
21///
22/// ```ignore
23/// (
24///     y,      // Year
25///     m,      // Month (January - December)
26///     d,      // Day of month (1 - 31)
27///     wd,     // Weekday (Monday-Sunday)
28///     yd,     // Day of year (1 - 365)
29///     em,     // Easter Monday
30/// )
31/// ```
32pub fn unpack_date(date: Date, is_orthodox: bool) -> (i32, Month, u8, Weekday, u16, u16) {
33    let y = date.year();
34    let m = date.month();
35    let d = date.day();
36
37    let wd = date.weekday();
38    let yd = date.ordinal();
39
40    let em = easter_monday(y as usize, is_orthodox);
41
42    (y, m, d, wd, yd, em)
43}
44
45/// Returns the Easter Monday for the given year.
46fn easter_monday(year: usize, is_orthodox: bool) -> u16 {
47    EASTER_MONDAYS[usize::from(is_orthodox)][year - 1901]
48}
49
50/// Checks if date is a weekend.
51pub fn is_weekend(date: Date) -> bool {
52    let w = date.weekday();
53
54    w == time::Weekday::Saturday || w == time::Weekday::Sunday
55}
56
57/// Check if the date is a weekday.
58pub fn is_weekday(date: Date) -> bool {
59    !is_weekend(date)
60}
61
62/// Function to get a list of the years in a range of `Dates`.
63pub fn get_years_in_range(start: Date, end: Date) -> Vec<i32> {
64    (start.year()..=end.year()).collect()
65}
66
67/// Function to get the number of days for each year in a range of `Dates`.
68///
69/// ```
70/// use time::{Date, Month};
71/// use calenda_rs::utilities::get_days_in_years_in_range;
72///
73/// let start = Date::from_calendar_date(2023, Month::July, 1).unwrap();
74/// let end = Date::from_calendar_date(2025, Month::January, 1).unwrap();
75///
76/// let days_in_years = get_days_in_years_in_range(start, end);
77///
78/// assert_eq!(days_in_years, vec![365, 366, 365]);
79/// ```
80pub fn get_days_in_years_in_range(start: Date, end: Date) -> Vec<u16> {
81    get_years_in_range(start, end)
82        .iter()
83        .map(|&y| days_in_year(y))
84        .collect()
85}
86
87/// Function to check if a range of years contains a leap year.
88pub fn contains_leap_year(start: Date, end: Date) -> bool {
89    get_years_in_range(start, end)
90        .iter()
91        .any(|&y| is_leap_year(y))
92}
93
94/// Function to get the number of leap years in a range of `Dates`.
95///
96/// ```
97/// use time::{Date, Month};
98/// use calenda_rs::utilities::leap_year_count;
99///
100/// let start = Date::from_calendar_date(2023, Month::July, 1).unwrap();
101/// let end = Date::from_calendar_date(2025, Month::January, 1).unwrap();
102///
103/// let number_of_leap_years = leap_year_count(start, end);
104///
105/// assert_eq!(number_of_leap_years, 1);
106pub fn leap_year_count(start: Date, end: Date) -> i64 {
107    get_years_in_range(start, end)
108        .iter()
109        .filter(|&y| is_leap_year(*y))
110        .collect::<Vec<&i32>>()
111        .len() as i64
112}
113
114/// Function to check if date is the last day of February.
115pub fn is_last_day_of_february(date: Date) -> bool {
116    let last_day_of_feb_non_leap =
117        date.month() == Month::February && date.day() == 28 && !is_leap_year(date.year());
118    let last_day_of_feb_leap =
119        date.month() == Month::February && date.day() == 29 && is_leap_year(date.year());
120
121    last_day_of_feb_non_leap || last_day_of_feb_leap
122}
123
124/// Function to get the next business day for a given date and calendar.
125pub fn next_business_day<C: Calendar>(date: Date, calendar: &C) -> Date {
126    let mut new_date = date;
127
128    while !calendar.is_business_day(new_date) {
129        new_date = new_date.next_day().unwrap();
130    }
131
132    new_date
133}
134
135/// Function to get the previous business day for a given date and calendar.
136pub fn previous_business_day<C: Calendar>(date: Date, calendar: &C) -> Date {
137    let mut new_date = date;
138
139    while !calendar.is_business_day(new_date) {
140        new_date = new_date.previous_day().unwrap();
141    }
142
143    new_date
144}
145
146/// Function to generate a sequence of dates from a start date, end date.
147pub fn date_sequence(start: Date, end: Date) -> Vec<Date> {
148    let mut dates = Vec::with_capacity((end - start).whole_days() as usize);
149    let mut current_date = start;
150
151    while current_date <= end {
152        dates.push(current_date);
153        current_date = current_date + Duration::days(1);
154    }
155
156    dates
157}
158
159/// Function to get the first day of the month.
160pub fn get_first_day_of_month(year: i32, month: Month) -> Result<Weekday, Error> {
161    Ok(Date::from_calendar_date(year, month, 1)?.weekday())
162}
163
164/// Function to get the last day of the month.
165pub fn get_last_day_of_month(year: i32, month: Month) -> Result<Weekday, Error> {
166    let date = Date::from_calendar_date(year, month, 1)?;
167
168    let last_day = date + Duration::days(days_in_year_month(year, month) as i64);
169
170    Ok(last_day.weekday())
171}
172
173/// Function to get the date of the first Monday of the month.
174pub fn get_first_monday_of_month(year: i32, month: Month) -> Result<Date, Error> {
175    let first_day_date = Date::from_calendar_date(year, month, 1)?;
176
177    match first_day_date.weekday() {
178        Weekday::Monday => Ok(first_day_date),
179        _ => Ok(first_day_date.next_occurrence(Weekday::Monday)),
180    }
181}
182
183/// Function to get the date of the first Tuesday of the month.
184pub fn get_first_tuesday_of_month(year: i32, month: Month) -> Result<Date, Error> {
185    let first_day_date = Date::from_calendar_date(year, month, 1)?;
186
187    match first_day_date.weekday() {
188        Weekday::Tuesday => Ok(first_day_date),
189        _ => Ok(first_day_date.next_occurrence(Weekday::Tuesday)),
190    }
191}
192
193/// Function to get the date of the first Wednesday of the month.
194pub fn get_first_wednesday_of_month(year: i32, month: Month) -> Result<Date, Error> {
195    let first_day_date = Date::from_calendar_date(year, month, 1)?;
196
197    match first_day_date.weekday() {
198        Weekday::Wednesday => Ok(first_day_date),
199        _ => Ok(first_day_date.next_occurrence(Weekday::Wednesday)),
200    }
201}
202
203/// Function to get the date of the first Thursday of the month.
204pub fn get_first_thursday_of_month(year: i32, month: Month) -> Result<Date, Error> {
205    let first_day_date = Date::from_calendar_date(year, month, 1)?;
206
207    match first_day_date.weekday() {
208        Weekday::Thursday => Ok(first_day_date),
209        _ => Ok(first_day_date.next_occurrence(Weekday::Thursday)),
210    }
211}
212
213/// Function to get the date of the first Friday of the month.
214pub fn get_first_friday_of_month(year: i32, month: Month) -> Result<Date, Error> {
215    let first_day_date = Date::from_calendar_date(year, month, 1)?;
216
217    match first_day_date.weekday() {
218        Weekday::Friday => Ok(first_day_date),
219        _ => Ok(first_day_date.next_occurrence(Weekday::Friday)),
220    }
221}
222
223/// Function to get the date of the first Saturday of the month.
224pub fn get_first_saturday_of_month(year: i32, month: Month) -> Result<Date, Error> {
225    let first_day_date = Date::from_calendar_date(year, month, 1)?;
226
227    match first_day_date.weekday() {
228        Weekday::Saturday => Ok(first_day_date),
229        _ => Ok(first_day_date.next_occurrence(Weekday::Saturday)),
230    }
231}
232
233/// Function to get the date of the first Sunday of the month.
234pub fn get_first_sunday_of_month(year: i32, month: Month) -> Result<Date, Error> {
235    let first_day_date = Date::from_calendar_date(year, month, 1)?;
236
237    match first_day_date.weekday() {
238        Weekday::Sunday => Ok(first_day_date),
239        _ => Ok(first_day_date.next_occurrence(Weekday::Sunday)),
240    }
241}
242
243/// Function to get the date of the last Monday of the month.
244pub fn get_last_monday_of_month(year: i32, month: Month) -> Result<Date, Error> {
245    let days_in_month = days_in_year_month(year, month);
246    let last_day_date = Date::from_calendar_date(year, month, days_in_month)?;
247
248    match last_day_date.weekday() {
249        Weekday::Monday => Ok(last_day_date),
250        _ => Ok(last_day_date.prev_occurrence(Weekday::Monday)),
251    }
252}
253
254/// Function to get the date of the last Tuesday of the month.
255pub fn get_last_tuesday_of_month(year: i32, month: Month) -> Result<Date, Error> {
256    let days_in_month = days_in_year_month(year, month);
257    let last_day_date = Date::from_calendar_date(year, month, days_in_month)?;
258
259    match last_day_date.weekday() {
260        Weekday::Tuesday => Ok(last_day_date),
261        _ => Ok(last_day_date.prev_occurrence(Weekday::Tuesday)),
262    }
263}
264
265/// Function to get the date of the last Wednesday of the month.
266pub fn get_last_wednesday_of_month(year: i32, month: Month) -> Result<Date, Error> {
267    let days_in_month = days_in_year_month(year, month);
268    let last_day_date = Date::from_calendar_date(year, month, days_in_month)?;
269
270    match last_day_date.weekday() {
271        Weekday::Wednesday => Ok(last_day_date),
272        _ => Ok(last_day_date.prev_occurrence(Weekday::Wednesday)),
273    }
274}
275
276/// Function to get the date of the last Thursday of the month.
277pub fn get_last_thursday_of_month(year: i32, month: Month) -> Result<Date, Error> {
278    let days_in_month = days_in_year_month(year, month);
279    let last_day_date = Date::from_calendar_date(year, month, days_in_month)?;
280
281    match last_day_date.weekday() {
282        Weekday::Thursday => Ok(last_day_date),
283        _ => Ok(last_day_date.prev_occurrence(Weekday::Thursday)),
284    }
285}
286
287/// Function to get the date of the last Friday of the month.
288pub fn get_last_friday_of_month(year: i32, month: Month) -> Result<Date, Error> {
289    let days_in_month = days_in_year_month(year, month);
290    let last_day_date = Date::from_calendar_date(year, month, days_in_month)?;
291
292    match last_day_date.weekday() {
293        Weekday::Friday => Ok(last_day_date),
294        _ => Ok(last_day_date.prev_occurrence(Weekday::Friday)),
295    }
296}
297
298/// Function to get the date of the last Saturday of the month.
299pub fn get_last_saturday_of_month(year: i32, month: Month) -> Result<Date, Error> {
300    let days_in_month = days_in_year_month(year, month);
301    let last_day_date = Date::from_calendar_date(year, month, days_in_month)?;
302
303    match last_day_date.weekday() {
304        Weekday::Saturday => Ok(last_day_date),
305        _ => Ok(last_day_date.prev_occurrence(Weekday::Saturday)),
306    }
307}
308
309/// Function to get the date of the last Sunday of the month.
310pub fn get_last_sunday_of_month(year: i32, month: Month) -> Result<Date, Error> {
311    let days_in_month = days_in_year_month(year, month);
312    let last_day_date = Date::from_calendar_date(year, month, days_in_month)?;
313
314    match last_day_date.weekday() {
315        Weekday::Sunday => Ok(last_day_date),
316        _ => Ok(last_day_date.prev_occurrence(Weekday::Sunday)),
317    }
318}
319
320// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
321// UNIT TESTS
322// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
323
324#[cfg(test)]
325mod test_utilities {
326    use super::*;
327
328    #[test]
329    fn test_first_x_day_of_month() {
330        let y = 2024;
331        let m = Month::January;
332
333        assert_eq!(
334            get_first_monday_of_month(y, m).unwrap(),
335            Date::from_calendar_date(y, m, 1).unwrap()
336        );
337        assert_eq!(
338            get_first_tuesday_of_month(y, m).unwrap(),
339            Date::from_calendar_date(y, m, 2).unwrap()
340        );
341        assert_eq!(
342            get_first_wednesday_of_month(y, m).unwrap(),
343            Date::from_calendar_date(y, m, 3).unwrap()
344        );
345        assert_eq!(
346            get_first_thursday_of_month(y, m).unwrap(),
347            Date::from_calendar_date(y, m, 4).unwrap()
348        );
349        assert_eq!(
350            get_first_friday_of_month(y, m).unwrap(),
351            Date::from_calendar_date(y, m, 5).unwrap()
352        );
353        assert_eq!(
354            get_first_saturday_of_month(y, m).unwrap(),
355            Date::from_calendar_date(y, m, 6).unwrap()
356        );
357        assert_eq!(
358            get_first_sunday_of_month(y, m).unwrap(),
359            Date::from_calendar_date(y, m, 7).unwrap()
360        );
361    }
362
363    #[test]
364    fn test_last_x_day_of_month() {
365        let y = 2024;
366        let m = Month::January;
367
368        assert_eq!(
369            get_last_monday_of_month(y, m).unwrap(),
370            Date::from_calendar_date(y, m, 29).unwrap()
371        );
372        assert_eq!(
373            get_last_tuesday_of_month(y, m).unwrap(),
374            Date::from_calendar_date(y, m, 30).unwrap()
375        );
376        assert_eq!(
377            get_last_wednesday_of_month(y, m).unwrap(),
378            Date::from_calendar_date(y, m, 31).unwrap()
379        );
380        assert_eq!(
381            get_last_thursday_of_month(y, m).unwrap(),
382            Date::from_calendar_date(y, m, 25).unwrap()
383        );
384        assert_eq!(
385            get_last_friday_of_month(y, m).unwrap(),
386            Date::from_calendar_date(y, m, 26).unwrap()
387        );
388        assert_eq!(
389            get_last_saturday_of_month(y, m).unwrap(),
390            Date::from_calendar_date(y, m, 27).unwrap()
391        );
392        assert_eq!(
393            get_last_sunday_of_month(y, m).unwrap(),
394            Date::from_calendar_date(y, m, 28).unwrap()
395        );
396    }
397
398    #[test]
399    fn test_get_first_day_of_month() {
400        assert_eq!(
401            get_first_day_of_month(2024, Month::January).unwrap(),
402            Weekday::Monday
403        );
404        assert_eq!(
405            get_first_day_of_month(2024, Month::February).unwrap(),
406            Weekday::Thursday
407        );
408        assert_eq!(
409            get_first_day_of_month(2024, Month::March).unwrap(),
410            Weekday::Friday
411        );
412        assert_eq!(
413            get_first_day_of_month(2024, Month::April).unwrap(),
414            Weekday::Monday
415        );
416        assert_eq!(
417            get_first_day_of_month(2024, Month::May).unwrap(),
418            Weekday::Wednesday
419        );
420        assert_eq!(
421            get_first_day_of_month(2024, Month::June).unwrap(),
422            Weekday::Saturday
423        );
424        assert_eq!(
425            get_first_day_of_month(2024, Month::July).unwrap(),
426            Weekday::Monday
427        );
428        assert_eq!(
429            get_first_day_of_month(2024, Month::August).unwrap(),
430            Weekday::Thursday
431        );
432        assert_eq!(
433            get_first_day_of_month(2024, Month::September).unwrap(),
434            Weekday::Sunday
435        );
436        assert_eq!(
437            get_first_day_of_month(2024, Month::October).unwrap(),
438            Weekday::Tuesday
439        );
440        assert_eq!(
441            get_first_day_of_month(2024, Month::November).unwrap(),
442            Weekday::Friday
443        );
444        assert_eq!(
445            get_first_day_of_month(2024, Month::December).unwrap(),
446            Weekday::Sunday
447        );
448    }
449
450    #[test]
451    fn test_get_first_monday_of_month() {
452        assert_eq!(
453            get_first_monday_of_month(2024, Month::January).unwrap(),
454            Date::from_calendar_date(2024, Month::January, 1).unwrap()
455        );
456        assert_eq!(
457            get_first_monday_of_month(2024, Month::February).unwrap(),
458            Date::from_calendar_date(2024, Month::February, 5).unwrap()
459        );
460        assert_eq!(
461            get_first_monday_of_month(2024, Month::March).unwrap(),
462            Date::from_calendar_date(2024, Month::March, 4).unwrap()
463        );
464        assert_eq!(
465            get_first_monday_of_month(2024, Month::April).unwrap(),
466            Date::from_calendar_date(2024, Month::April, 1).unwrap()
467        );
468        assert_eq!(
469            get_first_monday_of_month(2024, Month::May).unwrap(),
470            Date::from_calendar_date(2024, Month::May, 6).unwrap()
471        );
472        assert_eq!(
473            get_first_monday_of_month(2024, Month::June).unwrap(),
474            Date::from_calendar_date(2024, Month::June, 3).unwrap()
475        );
476        assert_eq!(
477            get_first_monday_of_month(2024, Month::July).unwrap(),
478            Date::from_calendar_date(2024, Month::July, 1).unwrap()
479        );
480        assert_eq!(
481            get_first_monday_of_month(2024, Month::August).unwrap(),
482            Date::from_calendar_date(2024, Month::August, 5).unwrap()
483        );
484        assert_eq!(
485            get_first_monday_of_month(2024, Month::September).unwrap(),
486            Date::from_calendar_date(2024, Month::September, 2).unwrap()
487        );
488        assert_eq!(
489            get_first_monday_of_month(2024, Month::October).unwrap(),
490            Date::from_calendar_date(2024, Month::October, 7).unwrap()
491        );
492        assert_eq!(
493            get_first_monday_of_month(2024, Month::November).unwrap(),
494            Date::from_calendar_date(2024, Month::November, 4).unwrap()
495        );
496        assert_eq!(
497            get_first_monday_of_month(2024, Month::December).unwrap(),
498            Date::from_calendar_date(2024, Month::December, 2).unwrap()
499        );
500    }
501}