use time::{Date, Duration, Weekday};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use std::collections::BTreeSet;
mod calendar_definitions;
pub use calendar_definitions::*;
#[derive(Error, Debug)]
pub enum CalendarError {
#[error("calendar could not been found")]
CalendarNotFound,
#[error("failed to create invalid date")]
OutOfBound(#[from]time::error::ComponentRange),
#[error("try to proceed beyond max date")]
MaxDay,
#[error("try to proceed before min date")]
MinDay,
}
type Result<T> = std::result::Result<T, CalendarError>;
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub enum NthWeek {
First,
Second,
Third,
Fourth,
Last,
SecondLast,
ThirdLast,
FourthLast,
}
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub enum Holiday {
WeekDay(Weekday),
YearlyDay {
month: u8,
day: u8,
first: Option<i32>,
last: Option<i32>,
},
MovableYearlyDay {
month: u8,
day: u8,
first: Option<i32>,
last: Option<i32>,
},
ModifiedMovableYearlyDay {
month: u8,
day: u8,
first: Option<i32>,
last: Option<i32>,
},
SingularDay(Date),
EasterOffset {
offset: i32,
first: Option<i32>,
last: Option<i32>,
},
MonthWeekday {
month: u8,
weekday: Weekday,
nth: NthWeek,
first: Option<i32>,
last: Option<i32>,
},
}
#[derive(Debug, Clone)]
pub struct Calendar {
holidays: BTreeSet<Date>,
weekdays: Vec<Weekday>,
}
fn new_date(year: i32, month: u8, day: u8) -> Result<Date> {
Ok(Date::from_calendar_date(year, month.try_into()?, day)?)
}
impl Calendar {
pub fn calc_calendar(holiday_rules: &[Holiday], start: i32, end: i32) -> Result<Calendar> {
let mut holidays = BTreeSet::new();
let mut weekdays = Vec::new();
for rule in holiday_rules {
match rule {
Holiday::SingularDay(date) => {
let year = date.year();
if year >= start && year <= end {
holidays.insert(*date);
}
}
Holiday::WeekDay(weekday) => {
weekdays.push(*weekday);
}
Holiday::YearlyDay {
month,
day,
first,
last,
} => {
let (first, last) = Self::calc_first_and_last(start, end, first, last);
for year in first..last + 1 {
holidays.insert( new_date(year, *month, *day)?);
}
}
Holiday::MovableYearlyDay {
month,
day,
first,
last,
} => {
let (first, last) = Self::calc_first_and_last(start, end, first, last);
for year in first..last + 1 {
let date = new_date(year, *month, *day)?;
let mut date = match date.weekday() {
Weekday::Saturday => date.next_day().ok_or(CalendarError::MaxDay)?.next_day().ok_or(CalendarError::MaxDay)?,
Weekday::Sunday => date.next_day().ok_or(CalendarError::MaxDay)?,
_ => date,
};
while holidays.get(&date).is_some() {
date = date.next_day().ok_or(CalendarError::MaxDay)?;
}
holidays.insert(date);
}
}
Holiday::ModifiedMovableYearlyDay {
month,
day,
first,
last,
} => {
let (first, last) = Self::calc_first_and_last(start, end, first, last);
for year in first..last + 1 {
let date = new_date(year, *month, *day)?;
let moved_date = match date.weekday() {
Weekday::Saturday => date.previous_day().ok_or(CalendarError::MinDay)?,
Weekday::Sunday => date.next_day().ok_or(CalendarError::MaxDay)?,
_ => date,
};
if moved_date.month() == date.month() {
holidays.insert(moved_date);
} else {
holidays.insert(date);
}
}
}
Holiday::EasterOffset {
offset,
first,
last,
} => {
let (first, last) = Self::calc_first_and_last(start, end, first, last);
for year in first..last + 1 {
let easter = computus::gregorian(year).unwrap();
let easter = new_date(easter.year, easter.month as u8, easter.day as u8)?;
let date = easter.checked_add(Duration::days(*offset as i64)).unwrap();
holidays.insert(date);
}
}
Holiday::MonthWeekday {
month,
weekday,
nth,
first,
last,
} => {
let (first, last) = Self::calc_first_and_last(start, end, first, last);
for year in first..last + 1 {
let day = match nth {
NthWeek::First => 1,
NthWeek::Second => 8,
NthWeek::Third => 15,
NthWeek::Fourth => 22,
NthWeek::Last => last_day_of_month(year, *month),
NthWeek::SecondLast => last_day_of_month(year, *month)-7,
NthWeek::ThirdLast => last_day_of_month(year, *month)-14,
NthWeek::FourthLast => last_day_of_month(year, *month)-21,
};
let mut date = new_date(year, *month, day)?;
while date.weekday() != *weekday {
date = match nth {
NthWeek::Last
| NthWeek::SecondLast
| NthWeek::ThirdLast
| NthWeek::FourthLast => date.previous_day().ok_or(CalendarError::MinDay)?,
_ => date.next_day().ok_or(CalendarError::MaxDay)?,
}
}
holidays.insert(date);
}
}
}
}
Ok(Calendar { holidays, weekdays })
}
pub fn next_bday(&self, date: Date) -> Result<Date> {
let mut date = date.next_day().ok_or(CalendarError::MaxDay)?;
while !self.is_business_day(date) {
date = date.next_day().ok_or(CalendarError::MaxDay)?;
}
Ok(date)
}
pub fn prev_bday(&self, date: Date) -> Result<Date> {
let mut date = date.previous_day().ok_or(CalendarError::MinDay)?;
while !self.is_business_day(date) {
date = date.previous_day().ok_or(CalendarError::MinDay)?;
}
Ok(date)
}
fn calc_first_and_last(
start: i32,
end: i32,
first: &Option<i32>,
last: &Option<i32>,
) -> (i32, i32) {
let first = match first {
Some(year) => std::cmp::max(start, *year),
_ => start,
};
let last = match last {
Some(year) => std::cmp::min(end, *year),
_ => end,
};
(first, last)
}
pub fn is_weekend(&self, day: Date) -> bool {
let weekday = day.weekday();
for w_day in &self.weekdays {
if weekday == *w_day {
return true;
}
}
false
}
pub fn is_holiday(&self, date: Date) -> bool {
self.holidays.get(&date).is_some()
}
pub fn is_business_day(&self, date: Date) -> bool {
!self.is_weekend(date) && !self.is_holiday(date)
}
}
pub trait CalendarProvider {
fn get_calendar(&self, calendar_name: &str) -> Result<&Calendar>;
}
pub fn is_leap_year(year: i32) -> bool {
new_date(year, 2, 29).is_ok()
}
pub fn last_day_of_month(year: i32, month: u8) -> u8 {
if let Ok(date) = new_date(year, month + 1, 1) {
date.previous_day().unwrap().day()
} else {
31
}
}
pub struct SimpleCalendar {
cal: Calendar,
}
impl SimpleCalendar {
pub fn new(cal: &Calendar) -> SimpleCalendar {
SimpleCalendar { cal: cal.clone() }
}
}
impl CalendarProvider for SimpleCalendar {
fn get_calendar(&self, _calendar_name: &str) -> Result<&Calendar> {
Ok(&self.cal)
}
}
impl Default for SimpleCalendar {
fn default() -> SimpleCalendar {
SimpleCalendar {
cal: Calendar::calc_calendar(&[], 2020, 2021).unwrap(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::date;
#[test]
fn fixed_dates_calendar() {
let holidays = vec![
Holiday::SingularDay(date!(2019-11-20)),
Holiday::SingularDay(date!(2019-11-24)),
Holiday::SingularDay(date!(2019-11-25)),
Holiday::WeekDay(Weekday::Saturday),
Holiday::WeekDay(Weekday::Sunday),
];
let cal = Calendar::calc_calendar(&holidays, 2019, 2019).unwrap();
assert_eq!(
false,
cal.is_business_day(date!(2019-11-20))
);
assert_eq!(true, cal.is_business_day(date!(2019-11-21)));
assert_eq!(true, cal.is_business_day(date!(2019-11-22)));
assert_eq!(
false,
cal.is_business_day(date!(2019-11-23))
);
assert_eq!(true, cal.is_weekend(date!(2019-11-23)));
assert_eq!(false, cal.is_holiday(date!(2019-11-23)));
assert_eq!(
false,
cal.is_business_day(date!(2019-11-24))
);
assert_eq!(true, cal.is_weekend(date!(2019-11-24)));
assert_eq!(true, cal.is_holiday(date!(2019-11-24)));
assert_eq!(
false,
cal.is_business_day(date!(2019-11-25))
);
assert_eq!(true, cal.is_business_day(date!(2019-11-26)));
}
#[test]
fn test_yearly_day() {
let holidays = vec![
Holiday::YearlyDay {
month: 11,
day: 1,
first: None,
last: None,
},
Holiday::YearlyDay {
month: 11,
day: 2,
first: Some(2019),
last: None,
},
Holiday::YearlyDay {
month: 11,
day: 3,
first: None,
last: Some(2019),
},
Holiday::YearlyDay {
month: 11,
day: 4,
first: Some(2019),
last: Some(2019),
},
];
let cal = Calendar::calc_calendar(&holidays, 2018, 2020).unwrap();
assert_eq!(true, cal.is_holiday(date!(2018-11-1)));
assert_eq!(true, cal.is_holiday(date!(2019-11-1)));
assert_eq!(true, cal.is_holiday(date!(2020-11-1)));
assert_eq!(false, cal.is_holiday(date!(2018-11-2)));
assert_eq!(true, cal.is_holiday(date!(2019-11-2)));
assert_eq!(true, cal.is_holiday(date!(2020-11-2)));
assert_eq!(true, cal.is_holiday(date!(2018-11-3)));
assert_eq!(true, cal.is_holiday(date!(2019-11-3)));
assert_eq!(false, cal.is_holiday(date!(2020-11-3)));
assert_eq!(false, cal.is_holiday(date!(2018-11-4)));
assert_eq!(true, cal.is_holiday(date!(2019-11-4)));
assert_eq!(false, cal.is_holiday(date!(2020-11-4)));
}
#[test]
fn test_movable_yearly_day() {
let holidays = vec![
Holiday::MovableYearlyDay {
month: 11,
day: 1,
first: None,
last: None,
},
Holiday::MovableYearlyDay {
month: 11,
day: 2,
first: None,
last: None,
},
Holiday::MovableYearlyDay {
month: 11,
day: 10,
first: None,
last: Some(2019),
},
Holiday::MovableYearlyDay {
month: 11,
day: 17,
first: Some(2019),
last: None,
},
Holiday::MovableYearlyDay {
month: 11,
day: 24,
first: Some(2019),
last: Some(2019),
},
];
let cal = Calendar::calc_calendar(&holidays, 2018, 2020).unwrap();
assert_eq!(true, cal.is_holiday(date!(2018-11-1)));
assert_eq!(true, cal.is_holiday(date!(2018-11-2)));
assert_eq!(true, cal.is_holiday(date!(2019-11-1)));
assert_eq!(true, cal.is_holiday(date!(2019-11-4)));
assert_eq!(true, cal.is_holiday(date!(2020-11-2)));
assert_eq!(true, cal.is_holiday(date!(2020-11-3)));
assert_eq!(true, cal.is_holiday(date!(2018-11-12)));
assert_eq!(true, cal.is_holiday(date!(2019-11-11)));
assert_eq!(false, cal.is_holiday(date!(2020-11-10)));
assert_eq!(false, cal.is_holiday(date!(2018-11-19)));
assert_eq!(true, cal.is_holiday(date!(2019-11-18)));
assert_eq!(true, cal.is_holiday(date!(2020-11-17)));
assert_eq!(false, cal.is_holiday(date!(2018-11-26)));
assert_eq!(true, cal.is_holiday(date!(2019-11-25)));
assert_eq!(false, cal.is_holiday(date!(2020-11-24)));
}
#[test]
fn test_easter_offset() {
let holidays = vec![Holiday::EasterOffset {
offset: -2,
first: None,
last: None,
}];
let cal = Calendar::calc_calendar(&holidays, 2019, 2020).unwrap();
assert_eq!(false, cal.is_business_day(date!(2019-4-19)));
assert_eq!(false, cal.is_business_day(date!(2020-4-10)));
}
#[test]
fn test_month_weekday() {
let holidays = vec![
Holiday::MonthWeekday {
month: 11,
weekday: Weekday::Monday,
nth: NthWeek::First,
first: None,
last: None,
},
Holiday::MonthWeekday {
month: 11,
weekday: Weekday::Tuesday,
nth: NthWeek::Second,
first: None,
last: None,
},
Holiday::MonthWeekday {
month: 11,
weekday: Weekday::Wednesday,
nth: NthWeek::Third,
first: None,
last: None,
},
Holiday::MonthWeekday {
month: 11,
weekday: Weekday::Thursday,
nth: NthWeek::Fourth,
first: None,
last: None,
},
Holiday::MonthWeekday {
month: 11,
weekday: Weekday::Friday,
nth: NthWeek::Last,
first: None,
last: None,
},
Holiday::MonthWeekday {
month: 11,
weekday: Weekday::Saturday,
nth: NthWeek::First,
first: None,
last: Some(2018),
},
Holiday::MonthWeekday {
month: 11,
weekday: Weekday::Sunday,
nth: NthWeek::Last,
first: Some(2020),
last: None,
},
];
let cal = Calendar::calc_calendar(&holidays, 2018, 2020).unwrap();
assert_eq!(true, cal.is_holiday(date!(2019-11-4)));
assert_eq!(true, cal.is_holiday(date!(2019-11-12)));
assert_eq!(true, cal.is_holiday(date!(2019-11-20)));
assert_eq!(true, cal.is_holiday(date!(2019-11-28)));
assert_eq!(true, cal.is_holiday(date!(2019-11-29)));
assert_eq!(true, cal.is_holiday(date!(2018-11-3)));
assert_eq!(false, cal.is_holiday(date!(2019-11-2)));
assert_eq!(false, cal.is_holiday(date!(2020-11-7)));
assert_eq!(false, cal.is_holiday(date!(2018-11-25)));
assert_eq!(false, cal.is_holiday(date!(2019-11-24)));
assert_eq!(true, cal.is_holiday(date!(2020-11-29)));
}
#[test]
fn serialize_cal_definition() {
let holidays = vec![
Holiday::MonthWeekday {
month: 11,
weekday: Weekday::Monday,
nth: NthWeek::First,
first: None,
last: None,
},
Holiday::MovableYearlyDay {
month: 11,
day: 1,
first: Some(2016),
last: None,
},
Holiday::YearlyDay {
month: 11,
day: 3,
first: None,
last: Some(2019),
},
Holiday::SingularDay(date!(2019-11-25)),
Holiday::WeekDay(Weekday::Saturday),
Holiday::EasterOffset {
offset: -2,
first: None,
last: None,
},
];
let json = serde_json::to_string_pretty(&holidays).unwrap();
assert_eq!(
json,
r#"[
{
"MonthWeekday": {
"month": 11,
"weekday": 1,
"nth": "First",
"first": null,
"last": null
}
},
{
"MovableYearlyDay": {
"month": 11,
"day": 1,
"first": 2016,
"last": null
}
},
{
"YearlyDay": {
"month": 11,
"day": 3,
"first": null,
"last": 2019
}
},
{
"SingularDay": [
2019,
329
]
},
{
"WeekDay": 6
},
{
"EasterOffset": {
"offset": -2,
"first": null,
"last": null
}
}
]"#
);
let holidays2: Vec<Holiday> = serde_json::from_str(&json).unwrap();
assert_eq!(holidays.len(), holidays2.len());
for i in 0..holidays.len() {
assert_eq!(holidays[i], holidays2[i]);
}
}
#[test]
fn test_modified_movable() {
let holidays = vec![
Holiday::ModifiedMovableYearlyDay {
month: 12,
day: 25,
first: None,
last: None,
},
Holiday::ModifiedMovableYearlyDay {
month: 1,
day: 1,
first: None,
last: None,
},
];
let cal = Calendar::calc_calendar(&holidays, 2020, 2023).unwrap();
assert!(cal.is_holiday(date!(2020-12-25)));
assert!(cal.is_holiday(date!(2021-12-24)));
assert!(cal.is_holiday(date!(2022-12-26)));
assert!(cal.is_holiday(date!(2023-12-25)));
assert!(cal.is_holiday(date!(2020-1-1)));
assert!(cal.is_holiday(date!(2021-1-1)));
assert!(cal.is_holiday(date!(2022-1-1)));
assert!(cal.is_holiday(date!(2023-1-2)));
}
}