use chrono::{DateTime, Datelike, Duration, TimeZone, Timelike, Utc};
use std::{collections::BTreeSet, convert::TryFrom, error::Error, fmt, num};
#[derive(Debug)]
pub enum ParseError {
InvalidCron,
InvalidRange,
InvalidValue,
ParseIntError(num::ParseIntError),
TryFromIntError(num::TryFromIntError),
}
enum Dow {
Sun = 0,
Mon = 1,
Tue = 2,
Wed = 3,
Thu = 4,
Fri = 5,
Sat = 6,
}
impl TryFrom<&str> for Dow {
type Error = ();
fn try_from(val: &str) -> Result<Self, Self::Error> {
match &*val.to_uppercase() {
"SUN" => Ok(Dow::Sun),
"MON" => Ok(Dow::Mon),
"TUE" => Ok(Dow::Tue),
"WED" => Ok(Dow::Wed),
"THU" => Ok(Dow::Thu),
"FRI" => Ok(Dow::Fri),
"SAT" => Ok(Dow::Sat),
_ => Err(()),
}
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ParseError::InvalidCron => write!(f, "invalid cron"),
ParseError::InvalidRange => write!(f, "invalid input"),
ParseError::InvalidValue => write!(f, "invalid value"),
ParseError::ParseIntError(ref err) => err.fmt(f),
ParseError::TryFromIntError(ref err) => err.fmt(f),
}
}
}
impl Error for ParseError {}
impl From<num::ParseIntError> for ParseError {
fn from(err: num::ParseIntError) -> Self {
ParseError::ParseIntError(err)
}
}
impl From<num::TryFromIntError> for ParseError {
fn from(err: num::TryFromIntError) -> Self {
ParseError::TryFromIntError(err)
}
}
pub fn parse(cron: &str, dt: DateTime<Utc>) -> Result<DateTime<Utc>, ParseError> {
let mut next = dt + Duration::minutes(1);
let fields: Vec<&str> = cron.split_whitespace().collect();
if fields.len() > 5 {
return Err(ParseError::InvalidCron);
}
next = Utc
.ymd(next.year(), next.month(), next.day())
.and_hms(next.hour(), next.minute(), 0);
loop {
if next.year() - dt.year() > 4 {
return Err(ParseError::InvalidCron);
}
let month = parse_field(fields[3], 1, 12)?;
if !month.contains(&next.month()) {
if next.month() == 12 {
next = Utc.ymd(next.year() + 1, 1, 1).and_hms(0, 0, 0);
} else {
next = Utc.ymd(next.year(), next.month() + 1, 1).and_hms(0, 0, 0);
}
continue;
}
let dom = parse_field(fields[2], 1, 31)?;
if !dom.contains(&next.day()) {
next = next + Duration::days(1);
next = Utc
.ymd(next.year(), next.month(), next.day())
.and_hms(0, 0, 0);
continue;
}
let hour = parse_field(fields[1], 0, 23)?;
if !hour.contains(&next.hour()) {
next = next + Duration::hours(1);
next = Utc
.ymd(next.year(), next.month(), next.day())
.and_hms(next.hour(), 0, 0);
continue;
}
let minute = parse_field(fields[0], 0, 59)?;
if !minute.contains(&next.minute()) {
next = next + Duration::minutes(1);
continue;
}
let dow = parse_field(fields[4], 0, 6)?;
if !dow.contains(&next.weekday().num_days_from_sunday()) {
next = next + Duration::days(1);
continue;
}
break;
}
Ok(next)
}
pub fn parse_field(field: &str, min: u32, max: u32) -> Result<BTreeSet<u32>, ParseError> {
let mut values = BTreeSet::<u32>::new();
let fields: Vec<&str> = field.split(',').filter(|s| !s.is_empty()).collect();
let dow = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
for field in fields.into_iter() {
match field {
day if dow.contains(&field.to_uppercase().as_str()) => {
values.insert(Dow::try_from(day).unwrap() as u32);
}
"*" => {
for i in min..=max {
values.insert(i);
}
}
f if field.starts_with("*/") => {
let f: u32 = f.trim_start_matches("*/").parse()?;
if f > max {
return Err(ParseError::InvalidValue);
}
let step = usize::try_from(f)?;
for i in (min..=max).step_by(step).collect::<Vec<u32>>() {
values.insert(i);
}
}
f if f.contains('-') => {
let tmp_fields: Vec<&str> = f.split('-').collect();
if tmp_fields.len() != 2 {
return Err(ParseError::InvalidRange);
}
let mut fields: Vec<u32> = Vec::new();
if dow.contains(&tmp_fields[0].to_uppercase().as_str()) {
fields.push(Dow::try_from(tmp_fields[0]).unwrap() as u32);
} else {
fields.push(tmp_fields[0].parse::<u32>()?);
};
if dow.contains(&tmp_fields[1].to_uppercase().as_str()) {
fields.push(Dow::try_from(tmp_fields[1]).unwrap() as u32);
} else {
fields.push(tmp_fields[1].parse::<u32>()?);
}
if fields[0] > fields[1] || fields[1] > max {
return Err(ParseError::InvalidRange);
}
for i in (fields[0]..=fields[1]).collect::<Vec<u32>>() {
values.insert(i);
}
}
_ => {
let f = field.parse::<u32>()?;
if f > max {
return Err(ParseError::InvalidValue);
}
values.insert(f);
}
}
}
Ok(values)
}