Add Schedule module for schedule support
This commit is contained in:
		
							parent
							
								
									0263a2b782
								
							
						
					
					
						commit
						e2c06f03a5
					
				| @ -16,6 +16,7 @@ tracing = { version = "0.1.19", features = ["log"] } | |||||||
| tracing-futures = "0.2.4" | tracing-futures = "0.2.4" | ||||||
| pin-project = "0.4.23" | pin-project = "0.4.23" | ||||||
| im = "15.0.0" | im = "15.0.0" | ||||||
|  | chrono = { version = "0.4.15" } | ||||||
| 
 | 
 | ||||||
| [dependencies.tracing-subscriber] | [dependencies.tracing-subscriber] | ||||||
| version = "0.2.11" | version = "0.2.11" | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ mod migrations; | |||||||
| mod model; | mod model; | ||||||
| mod option_future; | mod option_future; | ||||||
| mod program_runner; | mod program_runner; | ||||||
|  | mod schedule; | ||||||
| mod section_interface; | mod section_interface; | ||||||
| mod section_runner; | mod section_runner; | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
|  | |||||||
							
								
								
									
										368
									
								
								src/schedule.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										368
									
								
								src/schedule.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user