diff --git a/src/calendar.rs b/src/calendar.rs index 5effc2b..a42bc14 100644 --- a/src/calendar.rs +++ b/src/calendar.rs @@ -1,15 +1,12 @@ +use crate::date_range::DateRange; use chrono::naive::NaiveDate; use chrono::{Datelike, Month, Weekday}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; /* - There are a few gaping holes here, and some functional deficiencies: - - - workdays are only calculated within a year. If a workday in a prior - year is bumped to the next year, it won't be considered. - - workday impact is searched forward. If there is a mix of AdjustmentPolicies - then some weird stuff can happen (workdays occur A, B, but end up getting + - offday impact is searched forward. If there is a mix of AdjustmentPolicies + then some weird stuff can happen (offdays occur A, B, but end up getting observed B, A) */ @@ -27,6 +24,13 @@ impl Default for AdjustmentPolicy { } } +fn default_earliest_date() -> NaiveDate { + chrono::naive::MIN_DATE +} +fn default_latest_date() -> NaiveDate { + chrono::naive::MAX_DATE +} + #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(tag = "type")] pub enum DateSpec { @@ -42,10 +46,10 @@ pub enum DateSpec { observed: AdjustmentPolicy, #[serde(default)] description: String, - #[serde(default)] - valid_since: Option, - #[serde(default)] - valid_until: Option, + #[serde(default = "default_earliest_date")] + valid_since: NaiveDate, + #[serde(default = "default_latest_date")] + valid_until: NaiveDate, }, NthDayOccurance { month: Month, @@ -55,24 +59,28 @@ pub enum DateSpec { observed: AdjustmentPolicy, #[serde(default)] description: String, - #[serde(default)] - valid_since: Option, - #[serde(default)] - valid_until: Option, + #[serde(default = "default_earliest_date")] + valid_since: NaiveDate, + #[serde(default = "default_latest_date")] + valid_until: NaiveDate, }, } impl DateSpec { /// Get exact date of event and its policy, if it occured in that year - fn resolve(&self, start: NaiveDate, end: NaiveDate) -> Option<(Vec, AdjustmentPolicy)> { + fn resolve( + &self, + start: NaiveDate, + end: NaiveDate, + ) -> Option<(Vec, AdjustmentPolicy)> { use DateSpec::*; match self { SpecificDate { date, .. } => { - if date < start || end < date { + if *date < start || end < *date { None } else { - Some((vec![*date], AdjustmentPolicy::NoAdjustment)), + Some((vec![*date], AdjustmentPolicy::NoAdjustment)) } } DayOfMonth { @@ -83,18 +91,28 @@ impl DateSpec { observed, .. } => { - if valid_since > end || valid_until < start { + if *valid_since > end || *valid_until < start { None } else { - start = + let s = if *valid_since < start { + start + } else { + *valid_since + } + .year(); - if valid_since.is_some() && valid_since.unwrap().year() > start { - None - } else if valid_until.is_some() && valid_until.unwrap().year() < end { - None - } else { - let date = NaiveDate::from_ymd(year, month.number_from_month(), *day); - Some((date, *observed)) + let e = if *valid_until < end { + *valid_until + } else { + end + } + .year(); + + let mut result = Vec::new(); + for year in s..(e + 1) { + result.push(NaiveDate::from_ymd(year, month.number_from_month(), *day)); + } + Some((result, *observed)) } } NthDayOccurance { @@ -106,39 +124,56 @@ impl DateSpec { observed, .. } => { - if valid_since.is_some() && valid_since.unwrap().year() > year { - None - } else if valid_until.is_some() && valid_until.unwrap().year() < year { + if *valid_since > end || *valid_until < start { None } else { - let month_num = month.number_from_month(); - if *offset < 0i8 { - let mut date = if month_num == 12 { - NaiveDate::from_ymd(year, 12, 31) - } else { - NaiveDate::from_ymd(year, month_num + 1, 1).pred() - }; - while date.weekday() != *dow { - date = date.pred() - } - let mut off = offset + 1; - while off < 0 { - date = date.checked_sub_signed(chrono::Duration::days(7)).unwrap(); - off += 1; - } - Some((date, *observed)) + let s = if *valid_since < start { + start } else { - let mut date = NaiveDate::from_ymd(year, month_num, 1); - while date.weekday() != *dow { - date = date.succ() - } - let mut off = offset - 1; - while off > 0 { - date = date.checked_add_signed(chrono::Duration::days(7)).unwrap(); - off -= 1; - } - Some((date, *observed)) + *valid_since } + .year(); + + let e = if *valid_until < end { + *valid_until + } else { + end + } + .year(); + + let mut result = Vec::new(); + for year in s..(e + 1) { + let month_num = month.number_from_month(); + if *offset < 0i8 { + let mut date = if month_num == 12 { + NaiveDate::from_ymd(year, 12, 31) + } else { + NaiveDate::from_ymd(year, month_num + 1, 1).pred() + }; + while date.weekday() != *dow { + date = date.pred() + } + let mut off = offset + 1; + while off < 0 { + date = date.checked_sub_signed(chrono::Duration::days(7)).unwrap(); + off += 1; + } + result.push(date); + } else { + let mut date = NaiveDate::from_ymd(year, month_num, 1); + while date.weekday() != *dow { + date = date.succ() + } + let mut off = offset - 1; + while off > 0 { + date = date.checked_add_signed(chrono::Duration::days(7)).unwrap(); + off -= 1; + } + result.push(date); + } + } + + Some((result, *observed)) } } } @@ -165,34 +200,39 @@ pub struct Calendar { } impl Calendar { - fn adjust_workdays(&self, workdays: &Vec<(NaiveDate, AdjustmentPolicy)>) -> HashSet { + fn adjust_special_offdays( + &self, + offdays: &Vec<(Vec, AdjustmentPolicy)>, + ) -> HashSet { let mut observed = HashSet::new(); - for (date, policy) in workdays.iter() { - match self.adjust_workday(*date, policy, &observed) { - Some(workday) => { - observed.insert(workday); + for (dates, policy) in offdays.iter() { + for date in dates.iter() { + match self.adjust_special_offday(*date, policy, &observed) { + Some(offday) => { + observed.insert(offday); + } + None => {} } - None => {} } } observed } - /// Adjust a date given existing workdays and adjustment policy - fn adjust_workday( + /// Adjust a date given existing offday and adjustment policy + fn adjust_special_offday( &self, date: NaiveDate, policy: &AdjustmentPolicy, - workdays: &HashSet, + offdays: &HashSet, ) -> Option { let mut actual = date.clone(); println!("Adjusting {:?}", date); let is_blocked = - |x: NaiveDate| -> bool { (!self.dow.contains(&x.weekday())) || workdays.contains(&x) }; + |x: NaiveDate| -> bool { (!self.dow.contains(&x.weekday())) || offdays.contains(&x) }; use AdjustmentPolicy::*; match policy { @@ -210,10 +250,10 @@ impl Calendar { } Closest => { let prev = self - .adjust_workday(date, &AdjustmentPolicy::Prev, workdays) + .adjust_special_offday(date, &AdjustmentPolicy::Prev, offdays) .unwrap(); let next = self - .adjust_workday(date, &AdjustmentPolicy::Next, workdays) + .adjust_special_offday(date, &AdjustmentPolicy::Next, offdays) .unwrap(); if (date - prev) < (next - date) { Some(prev) @@ -231,41 +271,35 @@ impl Calendar { } } - /// Get the set of all workdays in a given year - pub fn get_workdays(&self, date: NaiveDate) -> HashSet { - let workdays: Vec<(NaiveDate, AdjustmentPolicy)> = self + /// Get the set of all offdays in a given year + pub fn get_special_offdays(&self, start: NaiveDate, end: NaiveDate) -> HashSet { + let offdays: Vec<(Vec, AdjustmentPolicy)> = self .exclude .iter() - .map(|x| x.resolve(date.year())) + .map(|x| x.resolve(start, end)) .filter(|x| x.is_some()) .map(|x| x.unwrap()) .collect(); - self.adjust_workdays(&workdays) + self.adjust_special_offdays(&offdays) } - /// Returns true if the given date is a workday / non-business day - fn is_workday(&self, date: NaiveDate) -> bool { - self.get_workdays(date).contains(&date) + /// Returns true if the given date is a offday / non-business day + fn is_offday(&self, date: NaiveDate) -> bool { + !self.dow.contains(&date.weekday()) || self.get_special_offdays(date, date).contains(&date) } - /// Returns the set of valid calendar dates within the specified range + /// Returns the set of non-offday calendar dates within the specified range pub fn date_range(&self, from: NaiveDate, to: NaiveDate) -> Vec { - let mut result = Vec::new(); - let mut cur = from.pred(); - let year = from.year(); - let mut workdays = self.get_workdays(cur); + let offdays = self.get_special_offdays(from, to); - while cur <= to { - cur = cur.succ(); - if cur.year() != year { - workdays = self.get_workdays(cur); - } - if !self.dow.contains(&cur.weekday()) || workdays.contains(&cur) { - continue; - } - result.push(cur); - } - result + // Expand by 2 weeks on each side to allow for adjustments in + // out-of-scope periods to affect in-scope dates + let dr = DateRange::new(from - chrono::Duration::days(14), to.succ()); + + dr.into_iter() + .filter(|x| self.dow.contains(&x.weekday()) && !offdays.contains(x)) + .filter(|x| *x >= from) + .collect() } } @@ -288,28 +322,28 @@ mod tests { day: 25u32, observed: AdjustmentPolicy::Next, description: "Christmas".to_owned(), - valid_since: None, - valid_until: None, + valid_since: default_earliest_date(), + valid_until: default_latest_date(), }, DateSpec::DayOfMonth { month: December, day: 26u32, observed: AdjustmentPolicy::Next, description: "Boxing Day".to_owned(), - valid_since: None, - valid_until: None, + valid_since: default_earliest_date(), + valid_until: default_latest_date(), }, ], inherits: vec![], }; // Christmas falls on a Saturday, observed was Monday - assert!(cal.is_workday(NaiveDate::from_ymd(2021, 12, 27))); + assert!(cal.is_offday(NaiveDate::from_ymd(2021, 12, 27))); // Boxing Day falls on a Sunday, observed is a Tuesday - assert!(cal.is_workday(NaiveDate::from_ymd(2021, 12, 28))); + assert!(cal.is_offday(NaiveDate::from_ymd(2021, 12, 28))); - assert!(!cal.is_workday(NaiveDate::from_ymd(2021, 12, 24))); + assert!(!cal.is_offday(NaiveDate::from_ymd(2021, 12, 24))); } #[test] @@ -327,29 +361,31 @@ mod tests { day: 25u32, observed: AdjustmentPolicy::Next, description: "Christmas".to_owned(), - valid_since: None, - valid_until: None, + valid_since: default_earliest_date(), + valid_until: default_latest_date(), }, DateSpec::DayOfMonth { month: December, day: 26u32, observed: AdjustmentPolicy::Next, description: "Boxing Day".to_owned(), - valid_since: None, - valid_until: None, + valid_since: default_earliest_date(), + valid_until: default_latest_date(), }, DateSpec::DayOfMonth { month: January, day: 1u32, observed: AdjustmentPolicy::Next, description: "New Years Day".to_owned(), - valid_since: None, - valid_until: None, + valid_since: default_earliest_date(), + valid_until: default_latest_date(), }, ], inherits: vec![], }; + assert!(cal.is_offday(NaiveDate::from_ymd(2021, 12, 25))); + let myrange = cal.date_range( NaiveDate::from_ymd(2021, 12, 15), NaiveDate::from_ymd(2022, 01, 15), diff --git a/src/date_range.rs b/src/date_range.rs index 926315d..c2c6c20 100644 --- a/src/date_range.rs +++ b/src/date_range.rs @@ -1,13 +1,5 @@ use chrono::naive::NaiveDate; -/* -pub type DateRange = Range; -fn iter_dates(range: DateRange) -> impl Iterator { - let ndays = (range.end - range.start).num_days(); - (0..ndays).map(move |x| range.start.checked_add_signed(Duration::days(x)).unwrap()) -} -*/ - pub struct DateRange { start: NaiveDate, end: NaiveDate,