use error::CrontabError;
use parsing::{ScheduleComponents, parse_cron};
use time::{Tm, now, now_utc};
use times::{adv_month, adv_day, adv_hour, adv_minute};
#[derive(Clone)]
pub struct Crontab {
pub schedule: ScheduleComponents,
}
impl Crontab {
pub fn parse(crontab_schedule: &str) -> Result<Crontab, CrontabError> {
let schedule = parse_cron(crontab_schedule)?;
Ok(Crontab {
schedule: schedule,
})
}
pub fn find_event_after(&self, start_time: &Tm) -> Option<Tm> {
calculate_next_event(&self.schedule, start_time)
}
pub fn find_next_event(&self) -> Option<Tm> {
self.find_event_after(&now())
}
pub fn find_next_event_utc(&self) -> Option<Tm> {
self.find_event_after(&now_utc())
}
}
pub (crate) fn calculate_next_event(times: &ScheduleComponents, time: &Tm)
-> Option<Tm> {
let mut next_time = time.clone();
next_time.tm_sec = 0;
adv_minute(&mut next_time);
loop {
match try_month(times, &mut next_time) {
DateTimeMatch::Missed => continue, DateTimeMatch::ContinueMatching => {}, DateTimeMatch::AnswerFound(upcoming) => return Some(upcoming),
}
match try_day(times, &mut next_time) {
DateTimeMatch::Missed => continue, DateTimeMatch::ContinueMatching => {}, DateTimeMatch::AnswerFound(upcoming) => return Some(upcoming),
}
match try_hour(times, &mut next_time) {
DateTimeMatch::Missed => continue, DateTimeMatch::ContinueMatching => {}, DateTimeMatch::AnswerFound(upcoming) => return Some(upcoming),
}
match try_minute(times, &mut next_time) {
DateTimeMatch::Missed => continue, DateTimeMatch::ContinueMatching => return Some(next_time), DateTimeMatch::AnswerFound(upcoming) => return Some(upcoming),
}
}
}
enum DateTimeMatch {
Missed,
ContinueMatching,
AnswerFound(Tm),
}
fn try_month(times: &ScheduleComponents, time: &mut Tm) -> DateTimeMatch {
let test_month = (time.tm_mon + 1) as u32;
match times.months.binary_search(&test_month) {
Ok(_) => {
DateTimeMatch::ContinueMatching
},
Err(pos) => {
if let Some(month) = times.months.get(pos) {
let mut use_time = time.clone();
use_time.tm_mon = (month - 1) as i32;
use_time.tm_mday = times.days.get(0).unwrap().clone() as i32;
use_time.tm_hour = times.hours.get(0).unwrap().clone() as i32;
use_time.tm_min = times.minutes.get(0).unwrap().clone() as i32;
use_time.tm_sec = 0;
DateTimeMatch::AnswerFound(use_time)
} else {
time.tm_year = time.tm_year + 1;
time.tm_mon = (times.months.get(0).unwrap().clone() - 1) as i32;
time.tm_mday = times.days.get(0).unwrap().clone() as i32;
time.tm_hour = times.hours.get(0).unwrap().clone() as i32;
time.tm_min = times.minutes.get(0).unwrap().clone() as i32;
time.tm_sec = 0;
DateTimeMatch::Missed
}
}
}
}
fn try_day(times: &ScheduleComponents, time: &mut Tm) -> DateTimeMatch {
match times.days.binary_search(&(time.tm_mday as u32)) {
Ok(_) => {
DateTimeMatch::ContinueMatching
},
Err(pos) => {
if let Some(day) = times.days.get(pos) {
let mut use_time = time.clone();
use_time.tm_mday = day.clone() as i32;
use_time.tm_hour = times.hours.get(0).unwrap().clone() as i32;
use_time.tm_min = times.minutes.get(0).unwrap().clone() as i32;
use_time.tm_sec = 0;
DateTimeMatch::AnswerFound(use_time)
} else {
time.tm_mday = 1; time.tm_hour = 0; time.tm_min = 0; time.tm_sec = 0; adv_month(time);
DateTimeMatch::Missed
}
}
}
}
fn try_hour(times: &ScheduleComponents, time: &mut Tm) -> DateTimeMatch {
match times.hours.binary_search(&(time.tm_hour as u32)) {
Ok(_) => {
DateTimeMatch::ContinueMatching
},
Err(pos) => {
if let Some(hour) = times.hours.get(pos) {
let mut use_time = time.clone();
use_time.tm_hour = hour.clone() as i32;
use_time.tm_min = times.minutes.get(0).unwrap().clone() as i32;
use_time.tm_sec = 0;
DateTimeMatch::AnswerFound(use_time)
} else {
time.tm_hour = 0; time.tm_min = 0; time.tm_sec = 0; adv_day(time);
DateTimeMatch::Missed
}
}
}
}
fn try_minute(times: &ScheduleComponents, time: &mut Tm) -> DateTimeMatch {
match times.minutes.binary_search(&(time.tm_min as u32)) {
Ok(_) => {
let mut use_time = time.clone();
use_time.tm_sec = 0; DateTimeMatch::AnswerFound(use_time)
},
Err(pos) => {
if let Some(minute) = times.minutes.get(pos) {
let mut use_time = time.clone();
use_time.tm_min = minute.clone() as i32;
use_time.tm_sec = 0;
DateTimeMatch::AnswerFound(use_time)
} else {
time.tm_min = 0; time.tm_sec = 0; adv_hour(time);
DateTimeMatch::Missed
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crontab::Crontab;
use expectest::prelude::*;
use test_helpers::get_tm;
use test_helpers::normal;
use time::{Timespec, at_utc};
fn parse_times(schedule: &str) -> ScheduleComponents {
let crontab = Crontab::parse(schedule).ok().unwrap();
crontab.schedule
}
#[test]
fn every_minute() {
let times = parse_times("* * * * *");
let tm = get_tm(2001, 1, 1, 12, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2001, 1, 1, 12, 1, 0)));
let tm = get_tm(2001, 1, 1, 12, 30, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2001, 1, 1, 12, 31, 0)));
let tm = get_tm(2001, 1, 1, 12, 59, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2001, 1, 1, 13, 0, 0)));
let tm = get_tm(2001, 1, 1, 23, 59, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2001, 1, 2, 0, 0, 0)));
let tm = get_tm(2001, 1, 31, 23, 59, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2001, 2, 1, 0, 0, 0)));
let tm = get_tm(2001, 1, 1, 12, 0, 1);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2001, 1, 1, 12, 1, 0)));
}
#[test]
fn every_fifteen_minutes() {
let times = parse_times("*/15 * * * *");
let tm = get_tm(2017, 5, 15, 11, 14, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2017, 5, 15, 11, 15, 0)));
let tm = get_tm(2017, 5, 15, 11, 16, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2017, 5, 15, 11, 30, 0)));
let tm = get_tm(2017, 5, 15, 11, 31, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2017, 5, 15, 11, 45, 0)));
let tm = get_tm(2017, 10, 15, 23, 59, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2017, 10, 16, 0, 0, 0)));
let tm = get_tm(2017, 12, 31, 23, 58, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2018, 1, 1, 0, 0, 0)));
let tm = get_tm(2017, 12, 31, 23, 59, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2018, 1, 1, 0, 0, 0)));
}
#[test]
fn precise_date_and_time() {
let times = parse_times("0 0 1 10 *");
let tm = get_tm(2017, 9, 30, 23, 59, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2017, 10, 1, 0, 0, 0)));
let tm = get_tm(2017, 9, 30, 23, 59, 59);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2017, 10, 1, 0, 0, 0)));
let tm = get_tm(2017, 9, 1, 0, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2017, 10, 1, 0, 0, 0)));
let tm = get_tm(2017, 10, 1, 0, 1, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2018, 10, 1, 0, 0, 0)));
let tm = get_tm(2017, 11, 1, 0, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2018, 10, 1, 0, 0, 0)));
let times = parse_times("45 22 13 10 *");
let tm = get_tm(2017, 7, 4, 10, 30, 1);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2017, 10, 13, 22, 45, 0)));
let tm = get_tm(2017, 11, 15, 10, 30, 15);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2018, 10, 13, 22, 45, 0)));
}
#[test]
fn first_of_the_month() {
let times = parse_times("0 0 1 * *");
let tm = get_tm(2004, 1, 1, 0, 1, 59);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2004, 2, 1, 0, 0, 0)));
let tm = get_tm(2004, 1, 1, 12, 59, 59);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2004, 2, 1, 0, 0, 0)));
let tm = get_tm(2004, 1, 15, 0, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2004, 2, 1, 0, 0, 0)));
let tm = get_tm(2004, 12, 15, 0, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2005, 1, 1, 0, 0, 0)));
}
#[test]
fn every_hour_in_january_and_july() {
let times = parse_times("0 * * 1,7 *");
let tm = get_tm(2005, 12, 31, 23, 59, 59);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2006, 1, 1, 0, 0, 0)));
let tm = get_tm(2005, 1, 1, 0, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2005, 1, 1, 1, 0, 0)));
let tm = get_tm(2005, 1, 15, 12, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2005, 1, 15, 13, 0, 0)));
let tm = get_tm(2005, 1, 31, 23, 59, 59);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2005, 7, 1, 0, 0, 0)));
let tm = get_tm(2005, 7, 1, 0, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2005, 7, 1, 1, 0, 0)));
let tm = get_tm(2005, 7, 31, 23, 59, 59);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2006, 1, 1, 0, 0, 0)));
}
#[test]
fn new_years() {
let times = parse_times("0 0 1 1 *");
let tm = get_tm(2007, 12, 31, 23, 59, 59);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2008, 1, 1, 0, 0, 0)));
let tm = get_tm(2007, 1, 1, 0, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2008, 1, 1, 0, 0, 0)));
let tm = get_tm(2007, 1, 1, 0, 5, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2008, 1, 1, 0, 0, 0)));
let tm = get_tm(2007, 1, 1, 1, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2008, 1, 1, 0, 0, 0)));
let tm = get_tm(2007, 1, 2, 0, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2008, 1, 1, 0, 0, 0)));
let tm = get_tm(2007, 7, 1, 0, 0, 0);
let next = calculate_next_event(×, &tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2008, 1, 1, 0, 0, 0)));
}
#[test]
fn spot_check_fields_every_day() {
let times = parse_times("0 0 * * *");
let timespec = Timespec::new(1483228800, 0);
let mut last = at_utc(timespec);
let mut next = last.clone();
let mut expected = last.clone();
expect!(last.tm_year).to(be_equal_to(117));
expect!(next.tm_mon).to(be_equal_to(0));
expect!(last.tm_yday).to(be_equal_to(0));
expect!(next.tm_mday).to(be_equal_to(1)); expect!(last.tm_wday).to(be_equal_to(0));
for _ in 0 .. 365 {
adv_day(&mut expected);
next = calculate_next_event(×, &last).unwrap();
expect!(next.tm_year).to(be_equal_to(expected.tm_year));
expect!(next.tm_mon).to(be_equal_to(expected.tm_mon));
expect!(next.tm_mday).to(be_equal_to(expected.tm_mday));
expect!(next.tm_yday).to(be_equal_to(expected.tm_yday));
expect!(next.tm_wday).to(be_equal_to(expected.tm_wday));
expect!(next.tm_hour).to(be_equal_to(0));
expect!(next.tm_min).to(be_equal_to(0));
expect!(next.tm_sec).to(be_equal_to(0));
adv_day(&mut last);
}
expect!(next.tm_year).to(be_equal_to(118));
expect!(next.tm_mon).to(be_equal_to(0));
expect!(next.tm_yday).to(be_equal_to(0));
expect!(next.tm_mday).to(be_equal_to(1)); expect!(next.tm_wday).to(be_equal_to(1));
for _ in 0 .. (365 * 2) {
adv_day(&mut expected);
next = calculate_next_event(×, &last).unwrap();
expect!(next.tm_year).to(be_equal_to(expected.tm_year));
expect!(next.tm_mon).to(be_equal_to(expected.tm_mon));
expect!(next.tm_mday).to(be_equal_to(expected.tm_mday));
expect!(next.tm_yday).to(be_equal_to(expected.tm_yday));
expect!(next.tm_wday).to(be_equal_to(expected.tm_wday));
expect!(next.tm_hour).to(be_equal_to(0));
expect!(next.tm_min).to(be_equal_to(0));
expect!(next.tm_sec).to(be_equal_to(0));
adv_day(&mut last);
}
expect!(next.tm_year).to(be_equal_to(120));
expect!(next.tm_mon).to(be_equal_to(0));
expect!(next.tm_yday).to(be_equal_to(0));
expect!(next.tm_mday).to(be_equal_to(1)); expect!(next.tm_wday).to(be_equal_to(3));
for _ in 0 .. 366 {
adv_day(&mut expected);
next = calculate_next_event(×, &last).unwrap();
expect!(next.tm_year).to(be_equal_to(expected.tm_year));
expect!(next.tm_mon).to(be_equal_to(expected.tm_mon));
expect!(next.tm_mday).to(be_equal_to(expected.tm_mday));
expect!(next.tm_yday).to(be_equal_to(expected.tm_yday));
expect!(next.tm_wday).to(be_equal_to(expected.tm_wday));
expect!(next.tm_hour).to(be_equal_to(0));
expect!(next.tm_min).to(be_equal_to(0));
expect!(next.tm_sec).to(be_equal_to(0));
adv_day(&mut last);
}
expect!(next.tm_year).to(be_equal_to(121));
expect!(next.tm_mon).to(be_equal_to(0));
expect!(next.tm_yday).to(be_equal_to(0));
expect!(next.tm_mday).to(be_equal_to(1)); expect!(next.tm_wday).to(be_equal_to(5)); }
#[test]
fn crontab_find_event_after() {
let crontab = Crontab::parse("* * * * *").ok().unwrap(); let tm = get_tm(2001, 1, 1, 12, 0, 0);
let next = crontab.find_event_after(&tm).unwrap();
expect!(normal(&next)).to(be_equal_to(get_tm(2001, 1, 1, 12, 1, 0)));
}
#[test]
fn crontab_find_next_event() {
let crontab = Crontab::parse("* * * * *").ok().unwrap(); let current = now();
let next = crontab.find_next_event().unwrap();
let delta = next - current;
expect!(delta.num_seconds()).to(be_greater_or_equal_to(0));
expect!(delta.num_seconds()).to(be_less_than(60));
let crontab = Crontab::parse("0 * * * *").ok().unwrap(); let current = now();
let next = crontab.find_next_event().unwrap();
let delta = next - current;
expect!(delta.num_hours()).to(be_greater_or_equal_to(0));
expect!(delta.num_hours()).to(be_less_than(1));
let crontab = Crontab::parse("0 0 * * *").ok().unwrap(); let current = now();
let next = crontab.find_next_event().unwrap();
let delta = next - current;
expect!(delta.num_hours()).to(be_greater_or_equal_to(0));
expect!(delta.num_hours()).to(be_less_than(24));
}
#[test]
fn crontab_find_next_event_utc() {
let crontab = Crontab::parse("* * * * *").ok().unwrap(); let current = now();
let next = crontab.find_next_event_utc().unwrap();
let delta = next - current;
expect!(delta.num_seconds()).to(be_greater_or_equal_to(0));
expect!(delta.num_seconds()).to(be_less_than(60));
let crontab = Crontab::parse("0 * * * *").ok().unwrap(); let current = now();
let next = crontab.find_next_event_utc().unwrap();
let delta = next - current;
expect!(delta.num_hours()).to(be_greater_or_equal_to(0));
expect!(delta.num_hours()).to(be_less_than(1));
let crontab = Crontab::parse("0 0 * * *").ok().unwrap(); let current = now();
let next = crontab.find_next_event_utc().unwrap();
let delta = next - current;
expect!(delta.num_hours()).to(be_greater_or_equal_to(0));
expect!(delta.num_hours()).to(be_less_than(24));
}
}