//! 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 serde::{Deserialize, Serialize}; 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, PartialEq, Eq)] 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, Serialize, Deserialize)] pub struct Schedule { #[serde( serialize_with = "ser::serialize_times", deserialize_with = "ser::deserialize_times" )] pub times: TimeSet, #[serde( serialize_with = "ser::serialize_weekdays", deserialize_with = "ser::deserialize_weekdays" )] pub weekdays: WeekdaySet, pub from: DateTimeBound, pub to: DateTimeBound, } mod ser { use super::{DateTimeBound, TimeSet, WeekdaySet}; use chrono::{NaiveDate, NaiveTime, Weekday}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::{convert::TryInto, fmt}; #[derive(Default, Debug, Clone, Serialize, Deserialize)] struct TimeOfDay { hour: u32, minute: u32, second: u32, #[serde(default)] millisecond: u32, } impl From<&NaiveTime> for TimeOfDay { fn from(time: &NaiveTime) -> Self { use chrono::Timelike; Self { hour: time.hour(), minute: time.minute(), second: time.second(), millisecond: time.nanosecond() / 1_000_000_u32, } } } #[derive(Copy, Clone, Debug)] struct InvalidTimeOfDay; impl fmt::Display for InvalidTimeOfDay { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { "invalid time of day".fmt(f) } } impl TryInto for TimeOfDay { type Error = InvalidTimeOfDay; fn try_into(self) -> Result { NaiveTime::from_hms_milli_opt(self.hour, self.minute, self.second, self.millisecond) .ok_or(InvalidTimeOfDay) } } #[allow(clippy::ptr_arg)] pub fn serialize_times(times: &TimeSet, serializer: S) -> Result where S: Serializer, { serializer.collect_seq(times.iter().map(TimeOfDay::from)) } pub fn deserialize_times<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { use serde::de::{Error, SeqAccess, Visitor}; struct TimeSetVisitor; impl<'de> Visitor<'de> for TimeSetVisitor { type Value = TimeSet; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a sequence of time of days") } fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de>, { let mut times = TimeSet::with_capacity(seq.size_hint().unwrap_or(0)); while let Some(value) = seq.next_element::()? { let time: NaiveTime = value.try_into().map_err(A::Error::custom)?; times.push(time); } Ok(times) } } deserializer.deserialize_seq(TimeSetVisitor) } #[allow(clippy::ptr_arg)] pub fn serialize_weekdays(weekdays: &WeekdaySet, serializer: S) -> Result where S: Serializer, { let iter = weekdays .iter() .map(|weekday| weekday.num_days_from_sunday()); serializer.collect_seq(iter) } #[derive(Copy, Clone, Debug)] struct InvalidWeekday; impl fmt::Display for InvalidWeekday { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { "weekday out of range 0 to 6".fmt(f) } } fn weekday_from_days_from_sunday(days: u32) -> Result { Ok(match days { 0 => Weekday::Sun, 1 => Weekday::Mon, 2 => Weekday::Tue, 3 => Weekday::Wed, 4 => Weekday::Thu, 5 => Weekday::Fri, 6 => Weekday::Sat, _ => return Err(InvalidWeekday), }) } pub fn deserialize_weekdays<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { use serde::de::{Error, SeqAccess, Visitor}; struct WeekdaySetVisitor; impl<'de> Visitor<'de> for WeekdaySetVisitor { type Value = WeekdaySet; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a sequence of integers representing weekdays") } fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de>, { let mut weekdays = WeekdaySet::with_capacity(seq.size_hint().unwrap_or(0)); while let Some(value) = seq.next_element::()? { let weekday = weekday_from_days_from_sunday(value).map_err(A::Error::custom)?; weekdays.push(weekday); } Ok(weekdays) } } deserializer.deserialize_seq(WeekdaySetVisitor) } #[derive(Clone, Debug, Serialize, Deserialize)] struct DateAndYear { year: i32, month: u32, day: u32, } impl From for DateAndYear { fn from(date: NaiveDate) -> Self { use chrono::Datelike; Self { year: date.year(), month: date.month(), day: date.day(), } } } #[derive(Copy, Clone, Debug)] struct InvalidDateAndYear; impl fmt::Display for InvalidDateAndYear { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { "invalid date or year".fmt(f) } } impl TryInto for DateAndYear { type Error = InvalidDateAndYear; fn try_into(self) -> Result { NaiveDate::from_ymd_opt(self.year, self.month, self.day).ok_or(InvalidDateAndYear) } } impl Serialize for DateTimeBound { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { DateTimeBound::None => serializer.serialize_none(), DateTimeBound::Yearly(date_time) => { // Discard time let mut date_of_year: DateAndYear = date_time.date().into(); // Set year to 0 (since it is yearly) date_of_year.year = 0; date_of_year.serialize(serializer) } DateTimeBound::Definite(date_time) => { // Discard time let date_of_year: DateAndYear = date_time.date().into(); date_of_year.serialize(serializer) } } } } impl<'de> Deserialize<'de> for DateTimeBound { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { use serde::de::Error; let date_of_year: Option = Deserialize::deserialize(deserializer)?; Ok(match date_of_year { Some(date_of_year) => { let year = date_of_year.year; let date: NaiveDate = date_of_year.try_into().map_err(D::Error::custom)?; let date_time = date.and_hms(0, 0, 0); if year == 0 { DateTimeBound::Yearly(date_time) } else { DateTimeBound::Definite(date_time) } } None => DateTimeBound::None, }) } } } /// 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(); 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 } }); 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); } } #[test] fn test_serialize() { let sched = Schedule::new( vec![ NaiveTime::from_hms_milli(0, 0, 0, 0), NaiveTime::from_hms_milli(23, 59, 59, 999), ], every_day(), DateTimeBound::Yearly(NaiveDate::from_ymd(2020, 1, 1).and_hms(0, 0, 0)), DateTimeBound::Definite(NaiveDate::from_ymd(9999, 12, 31).and_hms(23, 59, 59)), ); let ser = serde_json::to_string(&sched).unwrap(); // Weekdays should match the order in `every_day()` but with sunday being 0 assert_eq!( &ser, "{\ \"times\":[\ {\"hour\":0,\"minute\":0,\"second\":0,\"millisecond\":0},\ {\"hour\":23,\"minute\":59,\"second\":59,\"millisecond\":999}\ ],\ \"weekdays\":[1,2,3,4,5,6,0],\ \"from\":{\ \"year\":0,\"month\":1,\"day\":1\ },\ \"to\":{\ \"year\":9999,\"month\":12,\"day\":31\ }\ }" ); let sched_de: Schedule = serde_json::from_str(&ser).unwrap(); assert_eq!(sched.times, sched_de.times); assert_eq!(sched.weekdays, sched_de.weekdays); // This serialization is lossy (year is discarded for yearly) // assert_eq!(sched_de.from, sched.from); assert_eq!( sched_de.from, DateTimeBound::Yearly(NaiveDate::from_ymd(0, 1, 1).and_hms(0, 0, 0)) ); // This serialization is also lossy (time is discarded) // assert_eq!(sched_de.to, sched.to); assert_eq!( sched_de.to, DateTimeBound::Definite(NaiveDate::from_ymd(9999, 12, 31).and_hms(0, 0, 0)) ); } }