Squashed commit of the following:
commit3fd1474140Author: Ian Roddis <tech@kinesin.ca> Date: Wed Dec 8 14:01:28 2021 -0400 Adding license and adjusting readme commit638659a467Author: Ian Roddis <tech@kinesin.ca> Date: Wed Dec 8 13:55:40 2021 -0400 Adding support for generating date ranges on date ranges
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
name = "flexcal"
|
name = "flexcal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license-file = "LICENSE.txt"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|||||||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Kinesin Data Technologies Incorporated
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
165
README.md
165
README.md
@@ -1,27 +1,8 @@
|
|||||||
Introduction
|
Introduction
|
||||||
============
|
============
|
||||||
|
|
||||||
Easier job scheduling, supporting:
|
A flexible calendar structure to support aribtrary and calculated
|
||||||
|
off days.
|
||||||
- Schedules in a particular timezone
|
|
||||||
- Calendars (defining which days are in scope)
|
|
||||||
- Schedules (defining spans of hours in a day)
|
|
||||||
- Jobs
|
|
||||||
- Given a calendar
|
|
||||||
- And a schedule
|
|
||||||
- Define a pattern of when jobs should run
|
|
||||||
- e.g.
|
|
||||||
- minute = "0,15,30,35", hour = "123"
|
|
||||||
- minute = "0", hour = "*"
|
|
||||||
- minute = "5", frequency = "15" // Start at 5 past the hour, and run every 15 minutes from then
|
|
||||||
|
|
||||||
A job will
|
|
||||||
|
|
||||||
Authorization
|
|
||||||
=============
|
|
||||||
|
|
||||||
ezsched relies on PAM authentication. By default, all objects are owned
|
|
||||||
by the user that submitted them.
|
|
||||||
|
|
||||||
Calendars
|
Calendars
|
||||||
=========
|
=========
|
||||||
@@ -43,147 +24,5 @@ Calendars consist of:
|
|||||||
- Optional `description`
|
- Optional `description`
|
||||||
- Optional `observed` attribute that is `Next`, `Prev`, `Closest`, `NoAdjustment` (default)
|
- Optional `observed` attribute that is `Next`, `Prev`, `Closest`, `NoAdjustment` (default)
|
||||||
that is in the day of week mask and not also a holiday
|
that is in the day of week mask and not also a holiday
|
||||||
- A public flag, which if true will make the calendar publicly available
|
|
||||||
- A name, which can contain letters, numbers, dashes, and underscores
|
- A name, which can contain letters, numbers, dashes, and underscores
|
||||||
- They cannot contain dots / periods
|
- They cannot contain dots / periods
|
||||||
|
|
||||||
By default, calendars are referred to by their name, but are scoped by
|
|
||||||
the owning user in the format `user.calendar`.
|
|
||||||
|
|
||||||
Users can refer to their own calendars with just the name, other users
|
|
||||||
must access unowned calendars using the fulle `user.calendar` notation.
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
```
|
|
||||||
/api/v1
|
|
||||||
/calendars
|
|
||||||
GET -- Retrieve list of calendars and descriptions
|
|
||||||
/calendar/:id
|
|
||||||
GET -- Get the current definition
|
|
||||||
POST -- Create a new calendar
|
|
||||||
PATCH -- Update an existing calendar
|
|
||||||
DELETE -- Delete the named calendar
|
|
||||||
/calendar/:id/:date
|
|
||||||
GET -- Returns true if the date is in the calendar
|
|
||||||
/calendar/:id/:from/:to
|
|
||||||
GET -- Return the list of valid dates between :from and :to, inclusive.
|
|
||||||
:from and :to are in the ISO-8601 date format (YYYY-MM-DD)
|
|
||||||
```
|
|
||||||
|
|
||||||
Calendar `:id`s are alphanumeric strings without spaces (e.g. `USHolidays`).
|
|
||||||
|
|
||||||
Encoding
|
|
||||||
--------
|
|
||||||
|
|
||||||
A JSON definition for a calendar looks as follows:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"description": "Long description",
|
|
||||||
"dow_list": ["Mon","Tue","Wed","Thu","Fri", "Sat", "Sun"],
|
|
||||||
"public": true,
|
|
||||||
"exclude": [
|
|
||||||
{
|
|
||||||
"date": "2021-01-01",
|
|
||||||
"description": "New Years Day"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"month": "January",
|
|
||||||
"day": 17,
|
|
||||||
"observed": "closest",
|
|
||||||
"description": "Martin Luther King Day"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"month": "January",
|
|
||||||
"dow": ["Mon"],
|
|
||||||
"offset": "-1",
|
|
||||||
"observed": "closest",
|
|
||||||
"description": "Final Monday Margarita Day"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Inheritance
|
|
||||||
-----------
|
|
||||||
|
|
||||||
Calendars can inherit from other calendars. The result will be a union
|
|
||||||
of the sets of days. For instance, a calendar of vacations could inherit
|
|
||||||
from a calendar of holidays.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"description": "Personal calendar days",
|
|
||||||
"inherits": [
|
|
||||||
"company/USHolidays",
|
|
||||||
"company/CompanyHolidays"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
{
|
|
||||||
"date": "2021-05-01",
|
|
||||||
"description": "My gerbil's birthday"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Job
|
|
||||||
===
|
|
||||||
|
|
||||||
Jobs are tasks that must be run on a schedule. Schedules are defined
|
|
||||||
using a calendar, a start time, and an optional frequency.
|
|
||||||
|
|
||||||
Jobs are identified by a name that can contain letters, numbers, dashes,
|
|
||||||
and underscores. Just like calendars, jobs can be reffered to using a
|
|
||||||
`user.job` notation.
|
|
||||||
|
|
||||||
By default, Jobs are run as the user that submitted them.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"description": "Description of job",
|
|
||||||
"calendar": "US Holidays",
|
|
||||||
"timezone": "Atlantic/Reykjavik",
|
|
||||||
"schedules": [
|
|
||||||
{
|
|
||||||
"start": {
|
|
||||||
"minute": "5",
|
|
||||||
"hour": "*",
|
|
||||||
},
|
|
||||||
"frequency": "15m"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"command": {
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
},
|
|
||||||
"mailto": [
|
|
||||||
"oncall@company.com"
|
|
||||||
],
|
|
||||||
"public_acl": "rwx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `public_acl` is a string that defines how the non-owning users can
|
|
||||||
access the job: `r`ead it, `w`rite its defintiion, or e`x`ecute it.
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
```
|
|
||||||
/api/v1
|
|
||||||
/jobs
|
|
||||||
GET -- Retrieve list of jobs and descriptions
|
|
||||||
/job/:id
|
|
||||||
GET -- Get the current definition
|
|
||||||
POST -- Create a new job
|
|
||||||
PATCH -- Update an existing job
|
|
||||||
DELETE -- Delete the job
|
|
||||||
/job/:id/run
|
|
||||||
GET -- Force a run of the job immediately
|
|
||||||
/job/:id/history
|
|
||||||
GET -- Retrieve the run history and results
|
|
||||||
```
|
|
||||||
|
|||||||
250
src/calendar.rs
250
src/calendar.rs
@@ -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,20 +59,30 @@ 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, year: i32) -> Option<(NaiveDate, AdjustmentPolicy)> {
|
fn resolve(
|
||||||
|
&self,
|
||||||
|
start: NaiveDate,
|
||||||
|
end: NaiveDate,
|
||||||
|
) -> Option<(Vec<NaiveDate>, AdjustmentPolicy)> {
|
||||||
use DateSpec::*;
|
use DateSpec::*;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
SpecificDate { date, .. } => Some((*date, AdjustmentPolicy::NoAdjustment)),
|
SpecificDate { date, .. } => {
|
||||||
|
if *date < start || end < *date {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((vec![*date], AdjustmentPolicy::NoAdjustment))
|
||||||
|
}
|
||||||
|
}
|
||||||
DayOfMonth {
|
DayOfMonth {
|
||||||
month,
|
month,
|
||||||
day,
|
day,
|
||||||
@@ -77,13 +91,28 @@ 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 date = NaiveDate::from_ymd(year, month.number_from_month(), *day);
|
let s = if *valid_since < start {
|
||||||
Some((date, *observed))
|
start
|
||||||
|
} else {
|
||||||
|
*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) {
|
||||||
|
result.push(NaiveDate::from_ymd(year, month.number_from_month(), *day));
|
||||||
|
}
|
||||||
|
Some((result, *observed))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NthDayOccurance {
|
NthDayOccurance {
|
||||||
@@ -95,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,42 +192,45 @@ pub struct Calendar {
|
|||||||
#[serde(default = "default_dow_set")]
|
#[serde(default = "default_dow_set")]
|
||||||
pub dow: HashSet<Weekday>,
|
pub dow: HashSet<Weekday>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub public: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub exclude: Vec<DateSpec>,
|
pub exclude: Vec<DateSpec>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub inherits: Vec<String>,
|
pub inherits: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -199,10 +248,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)
|
||||||
@@ -220,41 +269,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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,35 +313,34 @@ mod tests {
|
|||||||
let cal = Calendar {
|
let cal = Calendar {
|
||||||
description: "Test description".to_owned(),
|
description: "Test description".to_owned(),
|
||||||
dow: HashSet::from([Mon, Tue, Wed, Thu, Fri]),
|
dow: HashSet::from([Mon, Tue, Wed, Thu, Fri]),
|
||||||
public: false,
|
|
||||||
exclude: vec![
|
exclude: vec![
|
||||||
DateSpec::DayOfMonth {
|
DateSpec::DayOfMonth {
|
||||||
month: December,
|
month: December,
|
||||||
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]
|
||||||
@@ -309,36 +351,37 @@ mod tests {
|
|||||||
let cal = Calendar {
|
let cal = Calendar {
|
||||||
description: "Test description".to_owned(),
|
description: "Test description".to_owned(),
|
||||||
dow: HashSet::from([Mon, Tue, Wed, Thu, Fri]),
|
dow: HashSet::from([Mon, Tue, Wed, Thu, Fri]),
|
||||||
public: false,
|
|
||||||
exclude: vec![
|
exclude: vec![
|
||||||
DateSpec::DayOfMonth {
|
DateSpec::DayOfMonth {
|
||||||
month: December,
|
month: December,
|
||||||
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),
|
||||||
@@ -352,7 +395,6 @@ mod tests {
|
|||||||
{
|
{
|
||||||
"description": "Long description",
|
"description": "Long description",
|
||||||
"dow": ["Mon","Tue","Wed","Thu","Fri", "Sat", "Sun"],
|
"dow": ["Mon","Tue","Wed","Thu","Fri", "Sat", "Sun"],
|
||||||
"public": true,
|
|
||||||
"exclude": [
|
"exclude": [
|
||||||
{
|
{
|
||||||
"type": "SpecificDate",
|
"type": "SpecificDate",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user