Adding support for generating date ranges on date ranges

This commit is contained in:
Ian Roddis
2021-12-08 13:55:40 -04:00
parent cadb5782f0
commit 638659a467
2 changed files with 140 additions and 112 deletions

View File

@@ -1,15 +1,12 @@
use crate::date_range::DateRange;
use chrono::naive::NaiveDate; use chrono::naive::NaiveDate;
use chrono::{Datelike, Month, Weekday}; use chrono::{Datelike, Month, Weekday};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::collections::HashSet;
/* /*
There are a few gaping holes here, and some functional deficiencies: - 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
- 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
observed B, A) 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)] #[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum DateSpec { pub enum DateSpec {
@@ -42,10 +46,10 @@ pub enum DateSpec {
observed: AdjustmentPolicy, observed: AdjustmentPolicy,
#[serde(default)] #[serde(default)]
description: String, description: String,
#[serde(default)] #[serde(default = "default_earliest_date")]
valid_since: Option<NaiveDate>, valid_since: NaiveDate,
#[serde(default)] #[serde(default = "default_latest_date")]
valid_until: Option<NaiveDate>, valid_until: NaiveDate,
}, },
NthDayOccurance { NthDayOccurance {
month: Month, month: Month,
@@ -55,24 +59,28 @@ pub enum DateSpec {
observed: AdjustmentPolicy, observed: AdjustmentPolicy,
#[serde(default)] #[serde(default)]
description: String, description: String,
#[serde(default)] #[serde(default = "default_earliest_date")]
valid_since: Option<NaiveDate>, valid_since: NaiveDate,
#[serde(default)] #[serde(default = "default_latest_date")]
valid_until: Option<NaiveDate>, valid_until: NaiveDate,
}, },
} }
impl DateSpec { impl DateSpec {
/// Get exact date of event and its policy, if it occured in that year /// Get exact date of event and its policy, if it occured in that year
fn resolve(&self, start: NaiveDate, end: NaiveDate) -> Option<(Vec<NaiveDate>, AdjustmentPolicy)> { fn resolve(
&self,
start: NaiveDate,
end: NaiveDate,
) -> Option<(Vec<NaiveDate>, AdjustmentPolicy)> {
use DateSpec::*; use DateSpec::*;
match self { match self {
SpecificDate { date, .. } => { SpecificDate { date, .. } => {
if date < start || end < date { if *date < start || end < *date {
None None
} else { } else {
Some((vec![*date], AdjustmentPolicy::NoAdjustment)), Some((vec![*date], AdjustmentPolicy::NoAdjustment))
} }
} }
DayOfMonth { DayOfMonth {
@@ -83,18 +91,28 @@ impl DateSpec {
observed, observed,
.. ..
} => { } => {
if valid_since > end || valid_until < start { if *valid_since > end || *valid_until < start {
None None
} else { } else {
start = let s = if *valid_since < start {
start
} else {
*valid_since
}
.year();
if valid_since.is_some() && valid_since.unwrap().year() > start { let e = if *valid_until < end {
None *valid_until
} else if valid_until.is_some() && valid_until.unwrap().year() < end { } else {
None end
} else { }
let date = NaiveDate::from_ymd(year, month.number_from_month(), *day); .year();
Some((date, *observed))
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 { NthDayOccurance {
@@ -106,39 +124,56 @@ impl DateSpec {
observed, observed,
.. ..
} => { } => {
if valid_since.is_some() && valid_since.unwrap().year() > year { if *valid_since > end || *valid_until < start {
None
} else if valid_until.is_some() && valid_until.unwrap().year() < year {
None None
} else { } else {
let month_num = month.number_from_month(); let s = if *valid_since < start {
if *offset < 0i8 { start
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))
} else { } else {
let mut date = NaiveDate::from_ymd(year, month_num, 1); *valid_since
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))
} }
.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 { impl Calendar {
fn adjust_workdays(&self, workdays: &Vec<(NaiveDate, AdjustmentPolicy)>) -> HashSet<NaiveDate> { fn adjust_special_offdays(
&self,
offdays: &Vec<(Vec<NaiveDate>, AdjustmentPolicy)>,
) -> HashSet<NaiveDate> {
let mut observed = HashSet::new(); let mut observed = HashSet::new();
for (date, policy) in workdays.iter() { for (dates, policy) in offdays.iter() {
match self.adjust_workday(*date, policy, &observed) { for date in dates.iter() {
Some(workday) => { match self.adjust_special_offday(*date, policy, &observed) {
observed.insert(workday); Some(offday) => {
observed.insert(offday);
}
None => {}
} }
None => {}
} }
} }
observed observed
} }
/// Adjust a date given existing workdays and adjustment policy /// Adjust a date given existing offday and adjustment policy
fn adjust_workday( fn adjust_special_offday(
&self, &self,
date: NaiveDate, date: NaiveDate,
policy: &AdjustmentPolicy, policy: &AdjustmentPolicy,
workdays: &HashSet<NaiveDate>, offdays: &HashSet<NaiveDate>,
) -> Option<NaiveDate> { ) -> Option<NaiveDate> {
let mut actual = date.clone(); let mut actual = date.clone();
println!("Adjusting {:?}", date); println!("Adjusting {:?}", date);
let is_blocked = 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::*; use AdjustmentPolicy::*;
match policy { match policy {
@@ -210,10 +250,10 @@ impl Calendar {
} }
Closest => { Closest => {
let prev = self let prev = self
.adjust_workday(date, &AdjustmentPolicy::Prev, workdays) .adjust_special_offday(date, &AdjustmentPolicy::Prev, offdays)
.unwrap(); .unwrap();
let next = self let next = self
.adjust_workday(date, &AdjustmentPolicy::Next, workdays) .adjust_special_offday(date, &AdjustmentPolicy::Next, offdays)
.unwrap(); .unwrap();
if (date - prev) < (next - date) { if (date - prev) < (next - date) {
Some(prev) Some(prev)
@@ -231,41 +271,35 @@ impl Calendar {
} }
} }
/// Get the set of all workdays in a given year /// Get the set of all offdays in a given year
pub fn get_workdays(&self, date: NaiveDate) -> HashSet<NaiveDate> { pub fn get_special_offdays(&self, start: NaiveDate, end: NaiveDate) -> HashSet<NaiveDate> {
let workdays: Vec<(NaiveDate, AdjustmentPolicy)> = self let offdays: Vec<(Vec<NaiveDate>, AdjustmentPolicy)> = self
.exclude .exclude
.iter() .iter()
.map(|x| x.resolve(date.year())) .map(|x| x.resolve(start, end))
.filter(|x| x.is_some()) .filter(|x| x.is_some())
.map(|x| x.unwrap()) .map(|x| x.unwrap())
.collect(); .collect();
self.adjust_workdays(&workdays) self.adjust_special_offdays(&offdays)
} }
/// Returns true if the given date is a workday / non-business day /// Returns true if the given date is a offday / non-business day
fn is_workday(&self, date: NaiveDate) -> bool { fn is_offday(&self, date: NaiveDate) -> bool {
self.get_workdays(date).contains(&date) !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<NaiveDate> { pub fn date_range(&self, from: NaiveDate, to: NaiveDate) -> Vec<NaiveDate> {
let mut result = Vec::new(); let offdays = self.get_special_offdays(from, to);
let mut cur = from.pred();
let year = from.year();
let mut workdays = self.get_workdays(cur);
while cur <= to { // Expand by 2 weeks on each side to allow for adjustments in
cur = cur.succ(); // out-of-scope periods to affect in-scope dates
if cur.year() != year { let dr = DateRange::new(from - chrono::Duration::days(14), to.succ());
workdays = self.get_workdays(cur);
} dr.into_iter()
if !self.dow.contains(&cur.weekday()) || workdays.contains(&cur) { .filter(|x| self.dow.contains(&x.weekday()) && !offdays.contains(x))
continue; .filter(|x| *x >= from)
} .collect()
result.push(cur);
}
result
} }
} }
@@ -288,28 +322,28 @@ mod tests {
day: 25u32, day: 25u32,
observed: AdjustmentPolicy::Next, observed: AdjustmentPolicy::Next,
description: "Christmas".to_owned(), description: "Christmas".to_owned(),
valid_since: None, valid_since: default_earliest_date(),
valid_until: None, valid_until: default_latest_date(),
}, },
DateSpec::DayOfMonth { DateSpec::DayOfMonth {
month: December, month: December,
day: 26u32, day: 26u32,
observed: AdjustmentPolicy::Next, observed: AdjustmentPolicy::Next,
description: "Boxing Day".to_owned(), description: "Boxing Day".to_owned(),
valid_since: None, valid_since: default_earliest_date(),
valid_until: None, valid_until: default_latest_date(),
}, },
], ],
inherits: vec![], inherits: vec![],
}; };
// Christmas falls on a Saturday, observed was Monday // 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 // 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] #[test]
@@ -327,29 +361,31 @@ mod tests {
day: 25u32, day: 25u32,
observed: AdjustmentPolicy::Next, observed: AdjustmentPolicy::Next,
description: "Christmas".to_owned(), description: "Christmas".to_owned(),
valid_since: None, valid_since: default_earliest_date(),
valid_until: None, valid_until: default_latest_date(),
}, },
DateSpec::DayOfMonth { DateSpec::DayOfMonth {
month: December, month: December,
day: 26u32, day: 26u32,
observed: AdjustmentPolicy::Next, observed: AdjustmentPolicy::Next,
description: "Boxing Day".to_owned(), description: "Boxing Day".to_owned(),
valid_since: None, valid_since: default_earliest_date(),
valid_until: None, valid_until: default_latest_date(),
}, },
DateSpec::DayOfMonth { DateSpec::DayOfMonth {
month: January, month: January,
day: 1u32, day: 1u32,
observed: AdjustmentPolicy::Next, observed: AdjustmentPolicy::Next,
description: "New Years Day".to_owned(), description: "New Years Day".to_owned(),
valid_since: None, valid_since: default_earliest_date(),
valid_until: None, valid_until: default_latest_date(),
}, },
], ],
inherits: vec![], inherits: vec![],
}; };
assert!(cal.is_offday(NaiveDate::from_ymd(2021, 12, 25)));
let myrange = cal.date_range( let myrange = cal.date_range(
NaiveDate::from_ymd(2021, 12, 15), NaiveDate::from_ymd(2021, 12, 15),
NaiveDate::from_ymd(2022, 01, 15), NaiveDate::from_ymd(2022, 01, 15),

View File

@@ -1,13 +1,5 @@
use chrono::naive::NaiveDate; use chrono::naive::NaiveDate;
/*
pub type DateRange = Range<NaiveDate>;
fn iter_dates(range: DateRange) -> impl Iterator<Item = NaiveDate> {
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 { pub struct DateRange {
start: NaiveDate, start: NaiveDate,
end: NaiveDate, end: NaiveDate,