Struggling with generics in a cron parser

I'm writing a parser for cron expressions but I'm struggling with making the parsing logic generic over the different time units (minutes, hours, days of month, month, days of week). This also make me wonder if I'm using the right abstraction. I don't know what parts of the code are relevant to make recommendations on that so I'll just post everything. Ik hope it's not too much.

use std::str::FromStr;

const MINUTES_MIN: u8 = 0;
const MINUTES_MAX: u8 = 59;
const HOURS_MIN: u8 = 0;
const HOURS_MAX: u8 = 23;
const DAYS_OF_MONTH_MIN: u8 = 1;
const DAYS_OF_MONTH_MAX: u8 = 31;

pub trait TimeUnit {
    fn min() -> Self;
    fn max() -> Self;
}

#[derive(Debug)]
pub struct Minute(u8);

impl TimeUnit for Minute {
    fn min() -> Self {
        Self(MINUTES_MIN)
    }

    fn max() -> Self {
        Self(MINUTES_MAX)
    }
}

impl FromStr for Minute {
    type Err = ParseScheduleError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let minute = s.parse::<u8>().map_err(|_| ParseScheduleError)?;
        if minute >= MINUTES_MIN && minute <= MINUTES_MAX {
            Ok(Minute(minute))
        } else {
            Err(ParseScheduleError)
        }
    }
}

pub struct Hour(u8);

impl TimeUnit for Hour {
    fn min() -> Self {
        Self(HOURS_MIN)
    }

    fn max() -> Self {
        Self(HOURS_MAX)
    }
}

impl FromStr for Hour {
    type Err = ParseScheduleError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let minute = s.parse::<u8>().map_err(|_| ParseScheduleError)?;
        if minute >= HOURS_MIN && minute <= HOURS_MAX {
            Ok(Hour(minute))
        } else {
            Err(ParseScheduleError)
        }
    }
}

#[derive(Debug)]
pub struct DayOfMonth(u8);

impl TimeUnit for DayOfMonth {
    fn min() -> Self {
        Self(DAYS_OF_MONTH_MIN)
    }

    fn max() -> Self {
        Self(DAYS_OF_MONTH_MIN)
    }
}

impl FromStr for DayOfMonth {
    type Err = ParseScheduleError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let day = s.parse::<u8>().map_err(|_| ParseScheduleError)?;
        if day >= DAYS_OF_MONTH_MIN && day <= DAYS_OF_MONTH_MAX {
            Ok(DayOfMonth(day))
        } else {
            Err(ParseScheduleError)
        }
    }
}

#[derive(Debug)]
pub enum Month {
    January,
    February,
    March,
    April,
    May,
    June,
    July,
    August,
    September,
    October,
    November,
    December,
}

impl TimeUnit for Month {
    fn min() -> Self {
        Self::January
    }

    fn max() -> Self {
        Self::December
    }
}

impl FromStr for Month {
    type Err = ParseScheduleError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use Month::*;
        match s.to_uppercase().as_ref() {
            "JAN" | "1" => Ok(January),
            "FEB" | "2" => Ok(February),
            "MAR" | "3" => Ok(March),
            "APR" | "4" => Ok(April),
            "MAY" | "5" => Ok(May),
            "JUN" | "6" => Ok(June),
            "JUL" | "7" => Ok(July),
            "AUG" | "8" => Ok(August),
            "SEP" | "9" => Ok(September),
            "OCT" | "10" => Ok(October),
            "NOV" | "11" => Ok(November),
            "DEC" | "12" => Ok(December),
            _ => Err(ParseScheduleError),
        }
    }
}

#[derive(Debug)]
pub enum DayOfWeek {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

impl TimeUnit for DayOfWeek {
    fn min() -> Self {
        Self::Sunday
    }

    fn max() -> Self {
        Self::Saturday
    }
}

impl FromStr for DayOfWeek {
    type Err = ParseScheduleError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use DayOfWeek::*;
        match s.to_uppercase().as_ref() {
            "MON" | "1" => Ok(Monday),
            "TUE" | "2" => Ok(Tuesday),
            "WED" | "3" => Ok(Wednesday),
            "THU" | "4" => Ok(Thursday),
            "FRI" | "5" => Ok(Friday),
            "SAT" | "6" => Ok(Saturday),
            "SUN" | "0" => Ok(Sunday),
            _ => Err(ParseScheduleError),
        }
    }
}

pub struct ParseScheduleError;

pub struct Schedule {
    minutes: Field<Minute>,
    hours: Field<Hour>,
    days_of_month: Field<DayOfMonth>,
    months: Field<Month>,
    days_of_week: Field<DayOfWeek>,
}

#[derive(Debug, PartialEq)]
struct Field<T: TimeUnit> {
    entries: Vec<Entry<T>>,
}

enum FieldType {
    Minutes,
    Hours,
    DaysOfMonth,
    Month,
    DaysOfWeek,
}

#[derive(Debug, PartialEq)]
enum Entry<T: TimeUnit> {
    All,
    SingleValue(T),
    Range(T, T),
    Step(Step<T>),
}

#[derive(Debug, PartialEq)]
enum Step<T: TimeUnit> {
    FullRange { by: u32 },
    LimitedRange { from: T, through: T, by: u32 },
}

impl FromStr for Schedule {
    type Err = ParseScheduleError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let fields = s.split_whitespace().collect::<Vec<_>>();

        if fields.len() != 5 {
            return Err(ParseScheduleError);
        }

        let minutes = parse_field::<Minute>(fields[0], &FieldType::Minutes)?;

        todo!()
    }
}

fn parse_field<T: TimeUnit>(
    value: &str,
    field_type: &FieldType,
) -> Result<Field<T>, ParseScheduleError> {
    let entries = value
        .split(',')
        .map(|range| {
            if range == "*" {
                Ok(Entry::All)
            } else if range.contains("/") {
                Ok(Entry::Step(parse_step(range, field_type)?))
            } else if range.contains("-") {
                let (from, through) = parse_range(range, field_type)?;
                Ok(Entry::Range(from, through))
            } else {
                Ok(Entry::SingleValue(parse_single_value(range, field_type)?))
            }
        })
        .collect::<Result<_, ParseScheduleError>>()?;
    Ok(Field { entries })
}

fn parse_step<T: TimeUnit>(
    value: &str,
    field_type: &FieldType,
) -> Result<Step<T>, ParseScheduleError> {
    let mut step_iter = value.split('/');
    let step_range = step_iter.next().unwrap();
    let step_by = step_iter
        .next()
        .unwrap()
        .parse::<u32>()
        .map_err(|_| ParseScheduleError)?;

    if step_range == "*" {
        Ok(Step::FullRange { by: step_by })
    } else if step_range.contains("-") {
        let (from, through) = parse_range(step_range, field_type)?;
        Ok(Step::LimitedRange {
            from,
            through,
            by: step_by,
        })
    } else {
        Err(ParseScheduleError)
    }
}

fn parse_range<T: TimeUnit>(
    value: &str,
    field_type: &FieldType,
) -> Result<(T, T), ParseScheduleError> {
    let (from, through) = value.split_once('-').ok_or(ParseScheduleError)?;

    let from = parse_single_value(from, field_type)?;
    let through = parse_single_value(through, field_type)?;
    Ok((from, through))
}

fn parse_single_value<T: TimeUnit>(
    value: &str,
    field_type: &FieldType,
) -> Result<T, ParseScheduleError> {
    match field_type {
        FieldType::Minutes => Ok(Minute::from_str(value)?),
        FieldType::Hours => Ok(Hour::from_str(value)?),
        FieldType::DaysOfMonth => Ok(DayOfMonth::from_str(value)?),
        FieldType::Month => Ok(Month::from_str(value)?),
        FieldType::DaysOfWeek => Ok(DayOfWeek::from_str(value)?),
    }
}

My questions are:

  1. As you can see I'm passing generic type T: TimeUnit through all the functions but once I get down to parse_single_value I need to know which type I need to parse. AFAIK it's not possible to match on the concrete type of the generic parameter so I'm also passing along a FieldType type enum but this looks rather ugly to me. Is there another way to do this?
  2. What should the return type of parse_single_value be to fix the compiler error? Do I need to use a trait object here? In case it's relevant, the goal is to compile this to WASM and run it in the browser so I'm willing to accept a small runtime performance cost to limit the binary size.

Any pointers would be appreciated :slight_smile:

I don't know how they do it, but you could look at the source code of the cron crate for inspiration. While reading your topic I immediately thought that Cron expressions must be representable by a LR(1) grammar. Maybe you want to check out lalrpop, which I find a very handy tool for creating parsers for small languages like Cron expressions in Rust.

Your parse_single_value() is just FromStr::from_str() itself. There's no need for a field type enum or a trait object. The whole point of generics is that you can call trait methods on generic types without having to know the concrete type.

Also, don't unnecessarily constrain type definitions with generic bounds. Those should go on impls only, unless you have a very good reason to do so.

This compiles.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.