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" | ||||
| pin-project = "0.4.23" | ||||
| im = "15.0.0" | ||||
| chrono = { version = "0.4.15" } | ||||
| 
 | ||||
| [dependencies.tracing-subscriber] | ||||
| version = "0.2.11" | ||||
|  | ||||
| @ -9,6 +9,7 @@ mod migrations; | ||||
| mod model; | ||||
| mod option_future; | ||||
| mod program_runner; | ||||
| mod schedule; | ||||
| mod section_interface; | ||||
| mod section_runner; | ||||
| #[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