diff --git a/Cargo.toml b/Cargo.toml index 6d5f7b1..a83c55b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ tracing = { version = "0.1.19", features = ["log"] } tracing-futures = "0.2.4" pin-project = "0.4.23" im = "15.0.0" +chrono = { version = "0.4.15" } [dependencies.tracing-subscriber] version = "0.2.11" diff --git a/src/main.rs b/src/main.rs index 5cf125c..c0a6700 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod migrations; mod model; mod option_future; mod program_runner; +mod schedule; mod section_interface; mod section_runner; #[cfg(test)] diff --git a/src/schedule.rs b/src/schedule.rs new file mode 100644 index 0000000..3991407 --- /dev/null +++ b/src/schedule.rs @@ -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; +/// A set of days of week (for [Schedule](struct.Schedule.html)) +pub type WeekdaySet = Vec; + +/// 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(&self, reference: &DateTime) -> Option> { + 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(mut date: Date, weekday: Weekday) -> Date { + 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(times: T, weekdays: W, from: DateTimeBound, to: DateTimeBound) -> Schedule + where + T: IntoIterator, + W: IntoIterator, + { + 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(&self, reference: &DateTime) -> Option> { + 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> = 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> { + 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>)> = 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, Weekday, Date)> = 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, Option>)> = 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, + expected_result: Option>, + } + impl Case { + fn new( + schedule: Schedule, + ref_time: DateTime, + expected_result: Option>, + ) -> 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 = 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); + } + } +}