cron_expr/
lib.rs

1use chrono::{Datelike, DateTime, Local, Timelike, TimeZone};
2
3use time_unit::{Hour, Minute, Month, MonthDay, TimeUnit, WeekDay};
4
5use crate::error::CronError;
6use crate::error::CronErrorKind::{InvalidSyntax, InvalidTimeUnit};
7
8mod time_unit;
9mod error;
10
11#[derive(Debug)]
12pub struct CronExpr {
13    minute: Minute,
14    hour: Hour,
15    day_of_month: MonthDay,
16    month: Month,
17    day_of_week: WeekDay
18}
19
20#[cfg(feature = "serialize")]
21impl<'de> serde::Deserialize<'de> for CronExpr {
22    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de> {
23        use std::fmt;
24
25        struct Visitor {}
26
27        impl<'de> serde::de::Visitor<'de> for Visitor {
28            type Value = CronExpr;
29
30            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
31                write!(f, "string")
32            }
33
34            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> where
35                E: serde::de::Error, {
36                CronExpr::parse(v).map_err(|err| E::custom(format!("{}", err)))
37            }
38
39            fn visit_string<E>(self, v: String) -> Result<Self::Value, E> where
40                E: serde::de::Error, {
41                CronExpr::parse(&v).map_err(|err| E::custom(format!("{}", err)))
42            }
43        }
44
45        deserializer.deserialize_string(Visitor {})
46    }
47}
48
49fn next_month<T: TimeZone>(dt: &DateTime<T>) -> DateTime<T> {
50    dt.with_month(dt.month() + 1)
51        .or(dt.with_year(dt.year() + 1)
52            .and_then(|d| d.with_month0(0)))
53        .and_then(|d| d.with_day0(0))
54        .and_then(|d| d.with_hour(0))
55        .and_then(|d| d.with_minute(0))
56        .and_then(|d| d.with_second(0))
57        .and_then(|d| d.with_nanosecond(0))
58        .unwrap()
59}
60
61fn next_day<T: TimeZone>(dt: &DateTime<T>) -> DateTime<T> {
62    dt.with_day(dt.day() + 1)
63        .and_then(|d| d.with_hour(0))
64        .and_then(|d| d.with_minute(0))
65        .and_then(|d| d.with_second(0))
66        .and_then(|d| d.with_nanosecond(0))
67        .unwrap_or(next_month(dt))
68}
69
70fn next_hour<T: TimeZone>(dt: &DateTime<T>) -> DateTime<T> {
71    dt.with_hour(dt.hour() + 1)
72        .and_then(|d| d.with_minute(0))
73        .and_then(|d| d.with_second(0))
74        .and_then(|d| d.with_nanosecond(0))
75        .unwrap_or(next_day(dt))
76}
77
78fn next_minute<T: TimeZone>(dt: &DateTime<T>) -> DateTime<T> {
79    dt.with_minute(dt.minute() + 1)
80        .and_then(|d| d.with_second(0))
81        .and_then(|d| d.with_nanosecond(0))
82        .unwrap_or(next_hour(dt))
83}
84
85impl CronExpr {
86    pub fn parse(input: &str) -> Result<CronExpr, CronError> {
87        let split: Vec<&str> = input.split(" ").collect();
88
89        if split.len() != 5 {
90            return Err(InvalidSyntax.err(input))
91        }
92
93        fn parse_time_unit<T: TimeUnit>(input: &str) -> Result<T, CronError> {
94            T::parse(input).map_err(|err| InvalidTimeUnit(T::kind(), err).err(input))
95        }
96
97        Ok(CronExpr {
98            minute: parse_time_unit(split[0])?,
99            hour: parse_time_unit(split[1])?,
100            day_of_month: parse_time_unit(split[2])?,
101            month: parse_time_unit(split[3])?,
102            day_of_week: parse_time_unit(split[4])?,
103        })
104    }
105
106    pub fn next<T: TimeZone>(&self, after: &DateTime<T>) -> DateTime<T> {
107        let mut next = next_minute(after);
108        loop {
109            if !self.month.value().includes(next.month()) {
110                next = next_month(&next);
111                continue;
112            }
113
114            if !((!self.day_of_month.value().is_wildcard() && self.day_of_month.value().includes(next.day())) ||
115                (!self.day_of_week.value().is_wildcard() && self.day_of_week.value().includes(next.weekday().number_from_monday())) ||
116                (self.day_of_month.value().is_wildcard() && self.day_of_week.value().is_wildcard())) {
117                next = next_day(&next);
118                continue;
119            }
120
121            if !self.hour.value().includes(next.hour()) {
122                next = next_hour(&next);
123                continue;
124            }
125
126            if !self.minute.value().includes(next.minute()) {
127                next = next_minute(&next);
128                continue;
129            }
130
131            break;
132        }
133
134        return next;
135    }
136
137    pub fn next_after_now(&self) -> DateTime<Local> {
138        self.next(&Local::now())
139    }
140}