curds_cron/
expression.rs

1use super::*;
2
3/// A representation of a Cron Expression.
4#[derive(Debug)]
5pub struct CronExpression {
6    fields: [CronField; Self::FIELD_COUNT],
7}
8
9impl Display for CronExpression {
10    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 
11        write!(formatter, "{} {} {} {} {}", 
12            &self.fields[0],
13            &self.fields[1],
14            &self.fields[2],
15            &self.fields[3],
16            &self.fields[4])?;
17        Ok(())
18    }
19}
20
21impl FromStr for CronExpression {
22    type Err = CronParsingError;
23
24    fn from_str(string: &str) -> Result<Self, Self::Err> { 
25        CronExpression::parse::<CurdsCronParser>(string)
26    }
27}
28
29impl CronExpression {
30    const FIELD_COUNT : usize = 5;
31    const FIELD_DELIMITER : char = ' ';
32
33    fn parse<TFieldParser>(expression: &str) -> Result<CronExpression, CronParsingError>
34    where TFieldParser : CronFieldParser {
35        let parts: Vec<&str> = expression
36            .split(Self::FIELD_DELIMITER)
37            .filter(|part| part.len() > 0)
38            .collect();
39        if parts.len() != Self::FIELD_COUNT {
40            return Err(CronParsingError::FieldCount { 
41                expression: expression.to_owned(),
42                parts: parts.len()
43            });
44        }
45        Ok(CronExpression { 
46            fields : [   
47                TFieldParser::parse(CronDatePart::Minutes, parts[0])?,
48                TFieldParser::parse(CronDatePart::Hours, parts[1])?,
49                TFieldParser::parse(CronDatePart::DayOfMonth, parts[2])?,
50                TFieldParser::parse(CronDatePart::Month, parts[3])?,
51                TFieldParser::parse(CronDatePart::DayOfWeek, parts[4])?,
52            ]
53        })
54    }
55
56    /// Test whether the current CronExpression is a match for a given DateTime.
57    /// ```
58    /// use chrono::{DateTime, Utc};
59    /// use curds_cron::CronExpression;
60    /// let test_expression = "25 * * * *".parse::<CronExpression>()?;
61    /// assert_eq!(false, test_expression.is_match(&"2021-04-01T00:00:00Z".parse::<DateTime<Utc>>()?));
62    /// assert_eq!(true, test_expression.is_match(&"2021-04-01T00:25:00Z".parse::<DateTime<Utc>>()?));
63    /// # Ok::<(), Box<dyn std::error::Error>>(())
64    /// ```
65    pub fn is_match<Tz>(&self, datetime: &DateTime<Tz>) -> bool 
66    where Tz: TimeZone {
67        for field in &self.fields {
68            if !field.is_match(datetime) {
69                return false;
70            }
71        }
72        true
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn displays_fields() {
82        let test_object = CronExpression {
83            fields: [
84                CronField::new(CronDatePart::Hours, vec![CronValue::Any]),
85                CronField::new(CronDatePart::Hours, vec![CronValue::Any]),
86                CronField::new(CronDatePart::Hours, vec![CronValue::Any]),
87                CronField::new(CronDatePart::Hours, vec![CronValue::Any]),
88                CronField::new(CronDatePart::Hours, vec![CronValue::Any]),
89            ],
90        };
91
92        assert_eq!("* * * * *", &format!("{}", test_object));
93    }
94
95    #[test]
96    fn too_long_expression_fails() {
97        CronExpression::parse::<CurdsCronParser>("* * * * * *")
98            .expect_err("Expected a long expression to fail");
99    }
100
101    #[test]
102    fn too_short_expression_fails() {
103        CronExpression::parse::<CurdsCronParser>("* * * *")
104            .expect_err("Expected a short expression to fail");
105    }
106
107    macro_rules! expect_parsing {
108        ($context:expr, $sequence:expr => ($($expected_part:expr, $expected_value:expr),+)) => {
109            $(
110                $context
111                    .expect()
112                    .with(predicate::eq($expected_part), predicate::eq($expected_value))
113                    .times(1)
114                    .in_sequence(&mut $sequence)
115                    .returning(|_, _| {
116                        Ok(CronField::new($expected_part, Vec::<CronValue>::new()))
117                    });
118            )+
119        };
120    }
121
122    #[test]
123    fn parses_correctly_with_parser() -> Result<(), CronParsingError> {
124        let mut sequence = Sequence::new();
125        let context = MockCronFieldParser::parse_context();
126        expect_parsing! { context, sequence =>
127            (
128                CronDatePart::Minutes, "Minutes",
129                CronDatePart::Hours, "Hours",
130                CronDatePart::DayOfMonth, "DayOfMonth",
131                CronDatePart::Month, "Month",
132                CronDatePart::DayOfWeek, "DayOfWeek"
133            )
134        }
135
136        CronExpression::parse::<MockCronFieldParser>("Minutes Hours DayOfMonth Month DayOfWeek")?;
137        Ok(())
138    }
139
140    fn false_field() -> CronField { 
141        CronField::new(CronDatePart::Minutes, vec![CronValue::Single(99)]) 
142    }
143    fn true_field() -> CronField { 
144        CronField::new(CronDatePart::Minutes, vec![CronValue::Any])
145    }
146
147    #[test]
148    fn any_nonmatch_field_returns_false() {
149        let expression = CronExpression {
150            fields: [true_field(), false_field(), true_field(), true_field(), true_field()]
151        };
152
153        assert_eq!(false, expression.is_match(&Utc::now()));
154    }
155
156    #[test]
157    fn all_fields_match_returns_true() {
158        let expression = CronExpression {
159            fields: [true_field(), true_field(), true_field(), true_field(), true_field()]
160        };
161
162        assert_eq!(true, expression.is_match(&Utc::now()));
163    }
164}