Add step parsing

master
Nick Krichevsky 2022-01-30 15:14:33 -05:00
parent 3c4d34e484
commit 9916389af9
5 changed files with 90 additions and 73 deletions

View File

@ -6,9 +6,9 @@ mod parse;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CronSpecifier {
Any,
Any(Option<u8>),
Specifically(u8),
Range(u8, u8),
Range(u8, u8, Option<u8>),
Several(Vec<CronSpecifier>),
}
@ -37,59 +37,61 @@ mod tests {
use CronSpecifier::{Any, Range, Several, Specifically};
// simple parse checks
#[test_case("* * * * *", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Any})]
#[test_case("0 4 * * *", &CronEntry{minute: Specifically(0), hour: Specifically(4), day_of_month: Any, month: Any, day_of_week: Any})]
#[test_case("* * * * *", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Any(None)})]
#[test_case("0 4 * * *", &CronEntry{minute: Specifically(0), hour: Specifically(4), day_of_month: Any(None), month: Any(None), day_of_week: Any(None)})]
#[test_case("0 4 10 5 4", &CronEntry{minute: Specifically(0), hour: Specifically(4), day_of_month: Specifically(10), month: Specifically(5), day_of_week: Specifically(4)})]
#[test_case("* * 10 5 *", &CronEntry{minute: Any, hour: Any, day_of_month: Specifically(10), month: Specifically(5), day_of_week: Any})]
#[test_case("* * * * 7", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Specifically(0)}; "sunday can be seven")]
#[test_case("* 4-6 * * *", &CronEntry{minute: Any, hour: Range(4, 6), day_of_month: Any, month: Any, day_of_week: Any})]
#[test_case("* * * * mon-wed", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Range(1, 3)})]
#[test_case("* * * * mon-5", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Range(1, 5)})]
#[test_case("* * * jan-jun *", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Range(1, 6), day_of_week: Any})]
#[test_case("* * * 1,5 *", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Several(vec![Specifically(1), Specifically(5)]), day_of_week: Any})]
#[test_case("* * * * 1,5", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Several(vec![Specifically(1), Specifically(5)])})]
#[test_case("* * * * mon-wed,1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Several(vec![Range(1, 3), Specifically(1)])})]
#[test_case("* * * * mon-wed,1,3-5", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Several(vec![Range(1, 3), Specifically(1), Range(3, 5)])})]
#[test_case("* * 10 5 *", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Specifically(10), month: Specifically(5), day_of_week: Any(None)})]
#[test_case("* * * * 7", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Specifically(0)}; "sunday can be seven")]
#[test_case("* 4-6 * * *", &CronEntry{minute: Any(None), hour: Range(4, 6, None), day_of_month: Any(None), month: Any(None), day_of_week: Any(None)})]
#[test_case("* * * * mon-wed", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Range(1, 3, None)})]
#[test_case("* * * * mon-5", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Range(1, 5, None)})]
#[test_case("* * * jan-jun *", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Range(1, 6, None), day_of_week: Any(None)})]
#[test_case("* * * 1,5 *", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Several(vec![Specifically(1), Specifically(5)]), day_of_week: Any(None)})]
#[test_case("* * * * 1,5", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Several(vec![Specifically(1), Specifically(5)])})]
#[test_case("* * * * mon-wed,1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Several(vec![Range(1, 3, None), Specifically(1)])})]
#[test_case("* * * * mon-wed,1,3-5", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Several(vec![Range(1, 3, None), Specifically(1), Range(3, 5, None)])})]
#[test_case("*/10 * * * *", &CronEntry{minute: Any(Some(10)), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Any(None)})]
#[test_case("* */2 * * SUN", &CronEntry{minute: Any(None), hour: Any(Some(2)), day_of_month: Any(None), month: Any(None), day_of_week: Specifically(0)})]
// days of week
#[test_case("* * * 1 mon", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(1)})]
#[test_case("* * * 2 MON", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(1)})]
#[test_case("* * * 1 tue", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(2)})]
#[test_case("* * * 2 TUE", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(2)})]
#[test_case("* * * 1 wed", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(3)})]
#[test_case("* * * 2 WED", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(3)})]
#[test_case("* * * 1 thu", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(4)})]
#[test_case("* * * 2 THU", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(4)})]
#[test_case("* * * 1 fri", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(5)})]
#[test_case("* * * 2 FRI", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(5)})]
#[test_case("* * * 1 sat", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(6)})]
#[test_case("* * * 2 SAT", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(6)})]
#[test_case("* * * 1 sun", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(0)})]
#[test_case("* * * 2 SUN", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(0)})]
#[test_case("* * * 1 mon", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(1)})]
#[test_case("* * * 2 MON", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(1)})]
#[test_case("* * * 1 tue", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(2)})]
#[test_case("* * * 2 TUE", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(2)})]
#[test_case("* * * 1 wed", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(3)})]
#[test_case("* * * 2 WED", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(3)})]
#[test_case("* * * 1 thu", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(4)})]
#[test_case("* * * 2 THU", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(4)})]
#[test_case("* * * 1 fri", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(5)})]
#[test_case("* * * 2 FRI", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(5)})]
#[test_case("* * * 1 sat", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(6)})]
#[test_case("* * * 2 SAT", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(6)})]
#[test_case("* * * 1 sun", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(0)})]
#[test_case("* * * 2 SUN", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(0)})]
// months
#[test_case("* * * jan 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(0)})]
#[test_case("* * * JAN 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(1)})]
#[test_case("* * * feb 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(0)})]
#[test_case("* * * FEB 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(1)})]
#[test_case("* * * mar 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(3), day_of_week: Specifically(0)})]
#[test_case("* * * MAR 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(3), day_of_week: Specifically(1)})]
#[test_case("* * * apr 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(4), day_of_week: Specifically(0)})]
#[test_case("* * * APR 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(4), day_of_week: Specifically(1)})]
#[test_case("* * * may 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(5), day_of_week: Specifically(0)})]
#[test_case("* * * MAY 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(5), day_of_week: Specifically(1)})]
#[test_case("* * * jun 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(6), day_of_week: Specifically(0)})]
#[test_case("* * * JUN 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(6), day_of_week: Specifically(1)})]
#[test_case("* * * jul 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(7), day_of_week: Specifically(0)})]
#[test_case("* * * JUL 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(7), day_of_week: Specifically(1)})]
#[test_case("* * * aug 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(8), day_of_week: Specifically(0)})]
#[test_case("* * * AUG 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(8), day_of_week: Specifically(1)})]
#[test_case("* * * sep 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(9), day_of_week: Specifically(0)})]
#[test_case("* * * SEP 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(9), day_of_week: Specifically(1)})]
#[test_case("* * * oct 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(10), day_of_week: Specifically(0)})]
#[test_case("* * * OCT 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(10), day_of_week: Specifically(1)})]
#[test_case("* * * nov 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(11), day_of_week: Specifically(0)})]
#[test_case("* * * NOV 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(11), day_of_week: Specifically(1)})]
#[test_case("* * * dec 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(12), day_of_week: Specifically(0)})]
#[test_case("* * * DEC 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(12), day_of_week: Specifically(1)})]
#[test_case("* * * jan 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(0)})]
#[test_case("* * * JAN 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(1)})]
#[test_case("* * * feb 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(0)})]
#[test_case("* * * FEB 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(1)})]
#[test_case("* * * mar 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(3), day_of_week: Specifically(0)})]
#[test_case("* * * MAR 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(3), day_of_week: Specifically(1)})]
#[test_case("* * * apr 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(4), day_of_week: Specifically(0)})]
#[test_case("* * * APR 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(4), day_of_week: Specifically(1)})]
#[test_case("* * * may 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(5), day_of_week: Specifically(0)})]
#[test_case("* * * MAY 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(5), day_of_week: Specifically(1)})]
#[test_case("* * * jun 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(6), day_of_week: Specifically(0)})]
#[test_case("* * * JUN 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(6), day_of_week: Specifically(1)})]
#[test_case("* * * jul 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(7), day_of_week: Specifically(0)})]
#[test_case("* * * JUL 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(7), day_of_week: Specifically(1)})]
#[test_case("* * * aug 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(8), day_of_week: Specifically(0)})]
#[test_case("* * * AUG 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(8), day_of_week: Specifically(1)})]
#[test_case("* * * sep 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(9), day_of_week: Specifically(0)})]
#[test_case("* * * SEP 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(9), day_of_week: Specifically(1)})]
#[test_case("* * * oct 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(10), day_of_week: Specifically(0)})]
#[test_case("* * * OCT 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(10), day_of_week: Specifically(1)})]
#[test_case("* * * nov 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(11), day_of_week: Specifically(0)})]
#[test_case("* * * NOV 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(11), day_of_week: Specifically(1)})]
#[test_case("* * * dec 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(12), day_of_week: Specifically(0)})]
#[test_case("* * * DEC 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(12), day_of_week: Specifically(1)})]
fn test_successful_parse(to_parse: &str, expected: &CronEntry) {
let parse_res = CronEntry::from_str(to_parse);

View File

@ -1,5 +1,5 @@
use itertools::Itertools;
use nom::{branch::alt, character::complete::char, IResult, Parser};
use nom::{branch::alt, character::complete::char, error::FromExternalError, IResult, Parser};
use thiserror::Error;
use crate::{CronEntry, CronSpecifier};
@ -163,7 +163,7 @@ fn list_or_single_specifier<'a, P, E>(
) -> impl FnMut(&'a str) -> IResult<&'a str, CronSpecifier, E>
where
P: Parser<&'a str, CronSpecifier, E> + Copy,
E: nom::error::ParseError<&'a str>,
E: nom::error::ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
{
alt((
compound::several_specifiers(list_element_parser),

View File

@ -3,11 +3,12 @@
use crate::parse::{simple, word};
use crate::CronSpecifier;
use nom::combinator::fail;
use nom::error::FromExternalError;
use nom::multi::separated_list1;
use nom::InputLength;
use nom::{
branch::alt, character::complete::char, sequence::separated_pair, AsChar, IResult, InputIter,
Parser, Slice,
branch::alt, character::complete::char, combinator::opt, sequence::pair,
sequence::separated_pair, AsChar, IResult, InputIter, Parser, Slice,
};
use std::ops::RangeFrom;
@ -101,20 +102,25 @@ where
/// Build a parser for a ranged specifier, with some kind of parser for the left/right of the range.
/// This accepts strings of the form `<left>-<right>`. The left/right parsers must return the
/// raw components of a range, rather than the specifiers themselves
fn build_range_parser<I, E, L, R>(
fn build_range_parser<'a, E, L, R>(
left_parser: L,
right_parser: R,
) -> impl FnMut(I) -> IResult<I, CronSpecifier, E>
) -> impl FnMut(&'a str) -> IResult<&'a str, CronSpecifier, E>
where
I: InputIter + Slice<RangeFrom<usize>>,
<I as InputIter>::Item: AsChar,
L: Parser<I, u8, E> + Copy,
R: Parser<I, u8, E> + Copy,
E: nom::error::ParseError<I>,
L: Parser<&'a str, u8, E> + Copy,
R: Parser<&'a str, u8, E> + Copy,
E: nom::error::ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
{
move |data: I| {
let (remaining, (start, end)) = separated_pair(left_parser, char('-'), right_parser)(data)?;
let specifier = CronSpecifier::Range(start, end);
// I'd much prefer it if I could make this take a generic input, but I backed myself into a corner with
// `parse_number`.
// TODO: make this take a generic input
move |data: &'a str| {
let (remaining, ((start, end), step)) = pair(
separated_pair(left_parser, char('-'), right_parser),
opt(simple::parse_step),
)(data)?;
let specifier = CronSpecifier::Range(start, end, step);
Ok((remaining, specifier))
}

View File

@ -3,8 +3,9 @@
use nom::{
character::complete::{char, digit1},
combinator::map_res,
combinator::{map_res, opt},
error::FromExternalError,
sequence::{pair, preceded},
IResult,
};
@ -27,9 +28,15 @@ where
pub(super) fn parse_star_specifier<'a, E>(chunk: &'a str) -> IResult<&'a str, CronSpecifier, E>
where
E: nom::error::ParseError<&'a str>,
E: nom::error::ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
{
let (remaining, _) = char('*')(chunk)?;
let (remaining, (_, step)) = pair(char('*'), opt(parse_step))(chunk)?;
Ok((remaining, CronSpecifier::Any))
Ok((remaining, CronSpecifier::Any(step)))
}
pub(super) fn parse_step<'a, E>(chunk: &'a str) -> IResult<&'a str, u8, E>
where
E: nom::error::ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError> {
preceded(char('/'), parse_number)(chunk)
}

View File

@ -25,7 +25,7 @@ fn ensure_value_in_range(
construct_error: fn(u8) -> Error,
) -> Result<(), Error> {
match *specifier {
CronSpecifier::Any => Ok(()),
CronSpecifier::Any(_) => Ok(()),
CronSpecifier::Specifically(value) => {
if value >= min && value <= max {
Ok(())
@ -33,7 +33,8 @@ fn ensure_value_in_range(
Err(construct_error(value))
}
}
CronSpecifier::Range(range_min, range_max) => {
// TODO: Handle steps
CronSpecifier::Range(range_min, range_max, _) => {
let min_ok = range_min >= min && range_min <= max;
let max_ok = range_max >= min && range_max <= max;
if max_ok && min_ok {
@ -54,7 +55,8 @@ fn ensure_value_in_range(
fn ensure_range_validity(specifier: &CronSpecifier) -> Result<(), Error> {
match *specifier {
CronSpecifier::Range(min, max) => {
// TODO: Handle steps
CronSpecifier::Range(min, max, _) => {
if min <= max {
Ok(())
} else {