cron-expr 0.1.0

Cron expression parsing and evaluation
Documentation
use std::collections::HashSet;
use std::fmt;
use std::fmt::Formatter;

use crate::error::TimeUnitError;
use crate::error::TimeUnitErrorKind::{InvalidRange, InvalidRangeEnd, InvalidRangeStart,
    InvalidStep, InvalidValue, OutOfBounds};

#[derive(Debug)]
pub enum TimeUnitKind {
    Minute,
    Hour,
    MonthDay,
    Month,
    WeekDay
}

impl fmt::Display for TimeUnitKind {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        use TimeUnitKind::*;

        f.write_str(match self {
            Minute => "minute",
            Hour => "hour",
            MonthDay => "month day",
            Month => "month",
            WeekDay => "week day"
        })
    }
}

#[derive(Debug)]
pub enum TimeUnitValue {
    Wildcard,
    ValidValues(HashSet<u32>)
}

impl TimeUnitValue {
    pub fn includes(&self, value: u32) -> bool {
        use TimeUnitValue::*;

        match self {
            Wildcard => true,
            ValidValues(values) => values.contains(&value)
        }
    }

    pub fn is_wildcard(&self) -> bool {
        if let TimeUnitValue::Wildcard = self {
            true
        } else {
            false
        }
    }
}

pub trait TimeUnit {
    fn kind() -> TimeUnitKind;

    fn new(value: TimeUnitValue) -> Self where Self: Sized;

    fn parse_value(input: &str) -> Result<u32, TimeUnitError> {
        let value: u32 = input.parse().map_err(|_| InvalidValue.err(input))?;
        if value < Self::min_value() || value > Self::max_value() {
            return Err(OutOfBounds.err(input));
        }
        Ok(value)
    }

    fn parse(input: &str) -> Result<Self, TimeUnitError> where Self: Sized {
        if input == "*" {
            return Ok(Self::new(TimeUnitValue::Wildcard))
        }

        let values: HashSet<u32> = input.split(",")
            .map(|item| match item {
                _ if item.contains("-") => {
                    let split: Vec<&str> = item.split("-").collect();
                    if split.len() != 2 {
                        return Err(InvalidRange.err(item))
                    }

                    let start = Self::parse_value(split[0]).map_err(|err|
                        InvalidRangeStart(Box::new(err)).err(item))?;
                    let end = Self::parse_value(split[1]).map_err(|err|
                        InvalidRangeEnd(Box::new(err)).err(item))?;
                    Ok((start..=end).collect())
                }
                _ if item.starts_with("*/") => {
                    if item.len() < 3 {
                        return Err(InvalidStep.err(item));
                    }

                    let step = Self::parse_value(&item[2..])
                        .map_err(|_| InvalidStep.err(item))?;
                    Ok((Self::min_value()..=Self::max_value()).step_by(step as usize).collect())
                }
                _ => Ok(vec![Self::parse_value(item)?])
            })
            .collect::<Result<Vec<Vec<u32>>, TimeUnitError>>()?
            .into_iter()
            .flatten()
            .collect();

        Ok(Self::new(TimeUnitValue::ValidValues(values)))
    }

    fn min_value() -> u32;

    fn max_value() -> u32;

    fn value(&self) -> &TimeUnitValue;
}

macro_rules! define_time_unit {
    ($name:ident, $min:expr, $max:expr) => {
        #[derive(Debug)]
        pub struct $name {
            value: TimeUnitValue
        }

        impl TimeUnit for $name {
            fn kind() -> TimeUnitKind {
                TimeUnitKind::$name
            }

            fn new(value: TimeUnitValue) -> Self where Self: Sized {
                $name {
                    value
                }
            }

            fn min_value() -> u32 {
                $min
            }

            fn max_value() -> u32 {
                $max
            }

            fn value(&self) -> &TimeUnitValue {
                &self.value
            }
        }
    }
}

define_time_unit!(Minute, 0, 59);
define_time_unit!(Hour, 0, 23);
define_time_unit!(MonthDay, 1, 31);
define_time_unit!(Month, 1, 12);
define_time_unit!(WeekDay, 1, 7);