Adding support for generating date ranges on date ranges
This commit is contained in:
244
src/calendar.rs
244
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<NaiveDate>,
|
||||
#[serde(default)]
|
||||
valid_until: Option<NaiveDate>,
|
||||
#[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<NaiveDate>,
|
||||
#[serde(default)]
|
||||
valid_until: Option<NaiveDate>,
|
||||
#[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<NaiveDate>, AdjustmentPolicy)> {
|
||||
fn resolve(
|
||||
&self,
|
||||
start: NaiveDate,
|
||||
end: NaiveDate,
|
||||
) -> Option<(Vec<NaiveDate>, 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<NaiveDate> {
|
||||
fn adjust_special_offdays(
|
||||
&self,
|
||||
offdays: &Vec<(Vec<NaiveDate>, AdjustmentPolicy)>,
|
||||
) -> HashSet<NaiveDate> {
|
||||
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<NaiveDate>,
|
||||
offdays: &HashSet<NaiveDate>,
|
||||
) -> Option<NaiveDate> {
|
||||
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<NaiveDate> {
|
||||
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<NaiveDate> {
|
||||
let offdays: Vec<(Vec<NaiveDate>, 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<NaiveDate> {
|
||||
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),
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
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 {
|
||||
start: NaiveDate,
|
||||
end: NaiveDate,
|
||||
|
||||
Reference in New Issue
Block a user