Alex Mikhalev
4 years ago
3 changed files with 370 additions and 0 deletions
@ -0,0 +1,368 @@
@@ -0,0 +1,368 @@
|
||||
//! Scheduling for events to run at certain intervals in the future
|
||||
|
||||
use chrono::{ |
||||
Date, DateTime, Datelike, Duration as CDuration, Local, NaiveDateTime, NaiveTime, |
||||
TimeZone, Weekday, |
||||
}; |
||||
use std::cmp; |
||||
use std::iter::FromIterator; |
||||
|
||||
/// A set of times of day (for [Schedule](struct.Schedule.html))
|
||||
pub type TimeSet = Vec<NaiveTime>; |
||||
/// A set of days of week (for [Schedule](struct.Schedule.html))
|
||||
pub type WeekdaySet = Vec<Weekday>; |
||||
|
||||
/// Returns a [`WeekdaySet`](type.WeekdaySet.html) of every day of the week
|
||||
#[allow(dead_code)] |
||||
pub fn every_day() -> WeekdaySet { |
||||
WeekdaySet::from_iter( |
||||
[ |
||||
Weekday::Mon, |
||||
Weekday::Tue, |
||||
Weekday::Wed, |
||||
Weekday::Thu, |
||||
Weekday::Fri, |
||||
Weekday::Sat, |
||||
Weekday::Sun, |
||||
] |
||||
.iter() |
||||
.cloned(), |
||||
) |
||||
} |
||||
|
||||
/// Represents the different types of date-time bounds that can be on a schedule
|
||||
#[allow(dead_code)] |
||||
#[derive(Debug, Clone)] |
||||
pub enum DateTimeBound { |
||||
/// There is no bound (ie. the Schedule extends with no limit)
|
||||
None, |
||||
/// There is a bound that repeats every year (ie. the year is set to the current year)
|
||||
Yearly(NaiveDateTime), |
||||
/// There is a definite bound on the schedule
|
||||
Definite(NaiveDateTime), |
||||
} |
||||
|
||||
impl Default for DateTimeBound { |
||||
fn default() -> DateTimeBound { |
||||
DateTimeBound::None |
||||
} |
||||
} |
||||
|
||||
impl DateTimeBound { |
||||
/// Resolves this bound into an optional `DateTime`. If there is no bound or the bound could
|
||||
/// not be resolved, None is returned.
|
||||
///
|
||||
/// `reference` is the reference that is used to resolve a `Yearly` bound.
|
||||
pub fn resolve_from<Tz: TimeZone>(&self, reference: &DateTime<Tz>) -> Option<DateTime<Tz>> { |
||||
match *self { |
||||
DateTimeBound::None => None, |
||||
DateTimeBound::Yearly(date_time) => { |
||||
date_time.with_year(reference.year()).and_then(|date_time| { |
||||
reference |
||||
.timezone() |
||||
.from_local_datetime(&date_time) |
||||
.single() |
||||
}) |
||||
} |
||||
DateTimeBound::Definite(date_time) => reference |
||||
.timezone() |
||||
.from_local_datetime(&date_time) |
||||
.single(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// A schedule that determines when an event will occur.
|
||||
#[derive(Default, Debug, Clone)] |
||||
pub struct Schedule { |
||||
pub times: TimeSet, |
||||
pub weekdays: WeekdaySet, |
||||
pub from: DateTimeBound, |
||||
pub to: DateTimeBound, |
||||
} |
||||
|
||||
/// Gets the next date matching the `weekday` after `date`
|
||||
fn next_weekday<Tz: TimeZone>(mut date: Date<Tz>, weekday: Weekday) -> Date<Tz> { |
||||
while date.weekday() != weekday { |
||||
date = date.succ(); |
||||
} |
||||
date |
||||
} |
||||
|
||||
#[allow(dead_code)] |
||||
impl Schedule { |
||||
/// Creates a new Schedule.
|
||||
///
|
||||
/// `times` is the times of day the event will be run. `weekdays` is the set of days of week
|
||||
/// the event will be run. `from` and `to` are restrictions on the end and beginning of event
|
||||
/// runs, respectively.
|
||||
pub fn new<T, W>(times: T, weekdays: W, from: DateTimeBound, to: DateTimeBound) -> Schedule |
||||
where |
||||
T: IntoIterator<Item = NaiveTime>, |
||||
W: IntoIterator<Item = Weekday>, |
||||
{ |
||||
Schedule { |
||||
times: TimeSet::from_iter(times), |
||||
weekdays: WeekdaySet::from_iter(weekdays), |
||||
from, |
||||
to, |
||||
} |
||||
} |
||||
|
||||
/// Gets the next `DateTime` the event should run after `reference`
|
||||
///
|
||||
/// Returns `None` if the event will never run after `reference` (ie. must be a `from` bound)
|
||||
pub fn next_run_after<Tz: TimeZone>(&self, reference: &DateTime<Tz>) -> Option<DateTime<Tz>> { |
||||
let mut to = self.to.resolve_from(reference); |
||||
let mut from = self.from.resolve_from(reference); |
||||
if let (Some(from), Some(to)) = (&mut from, &mut to) { |
||||
// We must handle the case where yearly bounds cross a year boundary
|
||||
if to < from { |
||||
if reference < to { |
||||
// Still in the bounds overlapping the previous year boundary
|
||||
*from = from.with_year(from.year() - 1).unwrap(); |
||||
} else { |
||||
// Awaiting (or in) next years bounds
|
||||
*to = to.with_year(to.year() + 1).unwrap(); |
||||
} |
||||
} |
||||
} |
||||
let from = match (from, &to) { |
||||
(Some(from), Some(to)) if &from > to => from.with_year(from.year() + 1), |
||||
(from, _) => from, |
||||
}; |
||||
let reference = match &from { |
||||
Some(from) if from > reference => from, |
||||
_ => reference, |
||||
} |
||||
.clone(); |
||||
println!( |
||||
"resolved to: {:?}, from: {:?}, reference: {:?}", |
||||
to, &from, reference |
||||
); |
||||
let mut next_run: Option<DateTime<Tz>> = None; |
||||
for weekday in &self.weekdays { |
||||
for time in &self.times { |
||||
let candidate = next_weekday(reference.date(), *weekday) |
||||
.and_time(*time) |
||||
.map(|date| { |
||||
if date < reference { |
||||
date + CDuration::weeks(1) |
||||
} else { |
||||
date |
||||
} |
||||
}); |
||||
println!("trying date: {:?}", candidate); |
||||
let candidate = match (candidate, &to) { |
||||
(Some(date), Some(to)) if &date > to => None, |
||||
(date, _) => date, |
||||
}; |
||||
next_run = match (next_run, candidate) { |
||||
// return whichever is first if there are 2 candidates
|
||||
(Some(d1), Some(d2)) => Some(cmp::min(d1, d2)), |
||||
// otherwise return whichever isn't None (or None if both are)
|
||||
(o1, o2) => o1.or(o2), |
||||
} |
||||
} |
||||
} |
||||
next_run |
||||
} |
||||
|
||||
/// Gets the next run after the current (local) time
|
||||
///
|
||||
/// See [next_run_after](#method.next_run_after)
|
||||
pub fn next_run_local(&self) -> Option<DateTime<Local>> { |
||||
self.next_run_after(&Local::now()) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod test { |
||||
use super::*; |
||||
use chrono::NaiveDate; |
||||
|
||||
#[test] |
||||
fn test_date_time_bound() { |
||||
use super::DateTimeBound::*; |
||||
|
||||
let cases: Vec<(DateTimeBound, Option<DateTime<Local>>)> = vec![ |
||||
(None, Option::None), |
||||
( |
||||
Definite(NaiveDate::from_ymd(2016, 11, 16).and_hms(10, 30, 0)), |
||||
Some(Local.ymd(2016, 11, 16).and_hms(10, 30, 0)), |
||||
), |
||||
( |
||||
Yearly(NaiveDate::from_ymd(2016, 11, 16).and_hms(10, 30, 0)), |
||||
Some(Local.ymd(2018, 11, 16).and_hms(10, 30, 0)), |
||||
), |
||||
( |
||||
Yearly(NaiveDate::from_ymd(2016, 1, 1).and_hms(0, 0, 0)), |
||||
Some(Local.ymd(2018, 1, 1).and_hms(0, 0, 0)), |
||||
), |
||||
( |
||||
Yearly(NaiveDate::from_ymd(2016, 12, 31).and_hms(23, 59, 59)), |
||||
Some(Local.ymd(2018, 12, 31).and_hms(23, 59, 59)), |
||||
), |
||||
( |
||||
Yearly(NaiveDate::from_ymd(2012, 2, 29).and_hms(0, 0, 0)), |
||||
Option::None, |
||||
), /* leap day */ |
||||
]; |
||||
let from = Local.ymd(2018, 1, 1).and_hms(0, 0, 0); |
||||
|
||||
for (bound, expected_result) in cases { |
||||
let result = bound.resolve_from(&from); |
||||
assert_eq!(result, expected_result); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_next_weekday() { |
||||
use super::next_weekday; |
||||
use chrono::Weekday; |
||||
// (date, weekday, result)
|
||||
let cases: Vec<(Date<Local>, Weekday, Date<Local>)> = vec![ |
||||
( |
||||
Local.ymd(2016, 11, 16), |
||||
Weekday::Wed, |
||||
Local.ymd(2016, 11, 16), |
||||
), |
||||
( |
||||
Local.ymd(2016, 11, 16), |
||||
Weekday::Fri, |
||||
Local.ymd(2016, 11, 18), |
||||
), |
||||
( |
||||
Local.ymd(2016, 11, 16), |
||||
Weekday::Tue, |
||||
Local.ymd(2016, 11, 22), |
||||
), |
||||
(Local.ymd(2016, 12, 30), Weekday::Tue, Local.ymd(2017, 1, 3)), |
||||
( |
||||
Local.ymd(2016, 11, 16), |
||||
Weekday::Tue, |
||||
Local.ymd(2016, 11, 22), |
||||
), |
||||
]; |
||||
|
||||
for (date, weekday, expected_result) in cases { |
||||
let result = next_weekday(date, weekday); |
||||
assert_eq!(result, expected_result); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_next_run_after() { |
||||
use super::{DateTimeBound, Schedule}; |
||||
use chrono::{DateTime, Local, NaiveTime, TimeZone, Weekday}; |
||||
let schedule = Schedule::new( |
||||
vec![NaiveTime::from_hms(10, 30, 0)], |
||||
vec![Weekday::Wed], |
||||
DateTimeBound::None, |
||||
DateTimeBound::None, |
||||
); |
||||
let cases: Vec<(DateTime<Local>, Option<DateTime<Local>>)> = vec![ |
||||
( |
||||
Local.ymd(2016, 11, 14).and_hms(10, 30, 0), |
||||
Some(Local.ymd(2016, 11, 16).and_hms(10, 30, 0)), |
||||
), |
||||
( |
||||
Local.ymd(2016, 11, 16).and_hms(10, 20, 0), |
||||
Some(Local.ymd(2016, 11, 16).and_hms(10, 30, 0)), |
||||
), |
||||
( |
||||
Local.ymd(2016, 11, 16).and_hms(10, 40, 0), |
||||
Some(Local.ymd(2016, 11, 23).and_hms(10, 30, 0)), |
||||
), |
||||
]; |
||||
for (reference, expected_result) in cases { |
||||
let result = schedule.next_run_after(&reference); |
||||
assert_eq!(result, expected_result); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_next_run_after2() { |
||||
use super::{DateTimeBound, Schedule}; |
||||
use chrono::{DateTime, Local, NaiveTime, TimeZone, Weekday}; |
||||
#[derive(Debug)] |
||||
struct Case { |
||||
schedule: Schedule, |
||||
ref_time: DateTime<Local>, |
||||
expected_result: Option<DateTime<Local>>, |
||||
} |
||||
impl Case { |
||||
fn new( |
||||
schedule: Schedule, |
||||
ref_time: DateTime<Local>, |
||||
expected_result: Option<DateTime<Local>>, |
||||
) -> Self { |
||||
Self { |
||||
schedule, |
||||
ref_time, |
||||
expected_result, |
||||
} |
||||
} |
||||
} |
||||
let sched1 = Schedule::new( |
||||
vec![NaiveTime::from_hms(8, 30, 0), NaiveTime::from_hms(20, 0, 0)], |
||||
vec![Weekday::Thu, Weekday::Fri], |
||||
DateTimeBound::None, |
||||
DateTimeBound::None, |
||||
); |
||||
let sched2 = Schedule::new( |
||||
vec![NaiveTime::from_hms(8, 30, 0), NaiveTime::from_hms(20, 0, 0)], |
||||
vec![Weekday::Thu, Weekday::Fri], |
||||
DateTimeBound::Definite(NaiveDate::from_ymd(2016, 5, 30).and_hms(0, 0, 0)), |
||||
DateTimeBound::Definite(NaiveDate::from_ymd(2016, 6, 30).and_hms(0, 0, 0)), |
||||
); |
||||
let sched3 = Schedule::new( |
||||
vec![NaiveTime::from_hms(8, 30, 0), NaiveTime::from_hms(20, 0, 0)], |
||||
every_day(), |
||||
DateTimeBound::Yearly(NaiveDate::from_ymd(0, 12, 15).and_hms(0, 0, 0)), |
||||
DateTimeBound::Yearly(NaiveDate::from_ymd(0, 1, 15).and_hms(0, 0, 0)), |
||||
); |
||||
let cases: Vec<Case> = vec![ |
||||
Case::new( |
||||
sched1.clone(), |
||||
Local.ymd(2016, 5, 16).and_hms(0, 0, 0), |
||||
Some(Local.ymd(2016, 5, 19).and_hms(8, 30, 0)), |
||||
), |
||||
Case::new( |
||||
sched1, |
||||
Local.ymd(2016, 5, 20).and_hms(9, 0, 0), |
||||
Some(Local.ymd(2016, 5, 20).and_hms(20, 0, 0)), |
||||
), |
||||
Case::new( |
||||
sched2.clone(), |
||||
Local.ymd(2016, 6, 1).and_hms(0, 0, 0), |
||||
Some(Local.ymd(2016, 6, 2).and_hms(8, 30, 0)), |
||||
), |
||||
Case::new( |
||||
sched2.clone(), |
||||
Local.ymd(2016, 5, 1).and_hms(0, 0, 0), |
||||
Some(Local.ymd(2016, 6, 2).and_hms(8, 30, 0)), |
||||
), |
||||
Case::new(sched2, Local.ymd(2016, 7, 1).and_hms(0, 0, 0), None), |
||||
Case::new( |
||||
sched3.clone(), |
||||
Local.ymd(2016, 11, 1).and_hms(0, 0, 0), |
||||
Some(Local.ymd(2016, 12, 15).and_hms(8, 30, 0)), |
||||
), |
||||
Case::new( |
||||
sched3.clone(), |
||||
Local.ymd(2017, 1, 1).and_hms(9, 0, 0), |
||||
Some(Local.ymd(2017, 1, 1).and_hms(20, 0, 0)), |
||||
), |
||||
Case::new( |
||||
sched3, |
||||
Local.ymd(2016, 1, 30).and_hms(0, 0, 0), |
||||
Some(Local.ymd(2016, 12, 15).and_hms(8, 30, 0)), |
||||
), |
||||
]; |
||||
for case in cases { |
||||
let result = case.schedule.next_run_after(&case.ref_time); |
||||
assert_eq!(result, case.expected_result, "case failed: {:?}", case); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue