1use chrono::{DateTime, Datelike, Duration, TimeZone, Timelike, Utc};
31use std::{collections::BTreeSet, error::Error, fmt, num, str::FromStr};
32
33#[derive(Debug)]
34pub enum ParseError {
35 InvalidCron,
36 InvalidRange,
37 InvalidValue,
38 ParseIntError(num::ParseIntError),
39 TryFromIntError(num::TryFromIntError),
40 InvalidTimezone,
41}
42
43enum Dow {
44 Sun = 0,
45 Mon = 1,
46 Tue = 2,
47 Wed = 3,
48 Thu = 4,
49 Fri = 5,
50 Sat = 6,
51}
52
53impl FromStr for Dow {
54 type Err = ();
55
56 fn from_str(s: &str) -> Result<Self, Self::Err> {
57 match &*s.to_uppercase() {
58 "SUN" => Ok(Self::Sun),
59 "MON" => Ok(Self::Mon),
60 "TUE" => Ok(Self::Tue),
61 "WED" => Ok(Self::Wed),
62 "THU" => Ok(Self::Thu),
63 "FRI" => Ok(Self::Fri),
64 "SAT" => Ok(Self::Sat),
65 _ => Err(()),
66 }
67 }
68}
69
70impl fmt::Display for ParseError {
71 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
72 match *self {
73 Self::InvalidCron => write!(f, "invalid cron"),
74 Self::InvalidRange => write!(f, "invalid input"),
75 Self::InvalidValue => write!(f, "invalid value"),
76 Self::ParseIntError(ref err) => err.fmt(f),
77 Self::TryFromIntError(ref err) => err.fmt(f),
78 Self::InvalidTimezone => write!(f, "invalid timezone"),
79 }
80 }
81}
82
83impl Error for ParseError {}
84
85impl From<num::ParseIntError> for ParseError {
86 #[must_use]
87 fn from(err: num::ParseIntError) -> Self {
88 Self::ParseIntError(err)
89 }
90}
91
92impl From<num::TryFromIntError> for ParseError {
93 #[must_use]
94 fn from(err: num::TryFromIntError) -> Self {
95 Self::TryFromIntError(err)
96 }
97}
98
99pub fn parse<TZ: TimeZone>(cron: &str, dt: &DateTime<TZ>) -> Result<DateTime<TZ>, ParseError> {
127 let tz = dt.timezone();
128
129 let fields: Vec<&str> = cron.split_whitespace().collect();
130
131 if fields.len() != 5 {
132 return Err(ParseError::InvalidCron);
133 }
134
135 let mut next = match Utc.from_local_datetime(&dt.naive_local()) {
136 chrono::LocalResult::Single(datetime) => datetime + Duration::minutes(1),
137 chrono::LocalResult::Ambiguous(earlier, _later) => earlier + Duration::minutes(1),
138 chrono::LocalResult::None => return Err(ParseError::InvalidTimezone),
139 };
140
141 next = match Utc.with_ymd_and_hms(
142 next.year(),
143 next.month(),
144 next.day(),
145 next.hour(),
146 next.minute(),
147 0,
148 ) {
149 chrono::LocalResult::Single(datetime) => datetime,
150 chrono::LocalResult::Ambiguous(earlier, _later) => earlier,
151 chrono::LocalResult::None => return Err(ParseError::InvalidTimezone),
152 };
153
154 let result = loop {
155 if next.year() - dt.year() > 4 {
157 return Err(ParseError::InvalidCron);
158 }
159
160 let month = parse_field(fields[3], 1, 12)?;
162 if !month.contains(&next.month()) {
163 next = match Utc.with_ymd_and_hms(
164 if next.month() == 12 {
165 next.year() + 1
166 } else {
167 next.year()
168 },
169 if next.month() == 12 {
170 1
171 } else {
172 next.month() + 1
173 },
174 1,
175 0,
176 0,
177 0,
178 ) {
179 chrono::LocalResult::Single(datetime) => datetime,
180 chrono::LocalResult::Ambiguous(earlier, _later) => earlier,
181 chrono::LocalResult::None => return Err(ParseError::InvalidTimezone),
182 };
183 continue;
184 }
185
186 let do_m = parse_field(fields[2], 1, 31)?;
188 if !do_m.contains(&next.day()) {
189 next += Duration::days(1);
190 next = match Utc.with_ymd_and_hms(next.year(), next.month(), next.day(), 0, 0, 0) {
191 chrono::LocalResult::Single(datetime) => datetime,
192 chrono::LocalResult::Ambiguous(earlier, _later) => earlier,
193 chrono::LocalResult::None => return Err(ParseError::InvalidTimezone),
194 };
195 continue;
196 }
197
198 let hour = parse_field(fields[1], 0, 23)?;
200 if !hour.contains(&next.hour()) {
201 next += Duration::hours(1);
202 next = match Utc.with_ymd_and_hms(
203 next.year(),
204 next.month(),
205 next.day(),
206 next.hour(),
207 0,
208 0,
209 ) {
210 chrono::LocalResult::Single(datetime) => datetime,
211 chrono::LocalResult::Ambiguous(earlier, _later) => earlier,
212 chrono::LocalResult::None => return Err(ParseError::InvalidTimezone),
213 };
214 continue;
215 }
216
217 let minute = parse_field(fields[0], 0, 59)?;
219 if !minute.contains(&next.minute()) {
220 next += Duration::minutes(1);
221 continue;
222 }
223
224 let do_w = parse_field(fields[4], 0, 6)?;
226 if !do_w.contains(&next.weekday().num_days_from_sunday()) {
227 next += Duration::days(1);
228 continue;
229 }
230
231 match tz.from_local_datetime(&next.naive_local()) {
233 chrono::LocalResult::Single(dt) => break dt,
234 chrono::LocalResult::Ambiguous(earlier, _later) => break earlier,
235 chrono::LocalResult::None => {
236 next += Duration::minutes(1);
237 continue;
238 }
239 }
240 };
241
242 Ok(result)
243}
244
245pub fn parse_field(field: &str, min: u32, max: u32) -> Result<BTreeSet<u32>, ParseError> {
302 let mut values = BTreeSet::<u32>::new();
303
304 let fields: Vec<&str> = field.split(',').filter(|s| !s.is_empty()).collect();
306
307 for field in fields {
309 match field {
310 "*" => {
312 for i in min..=max {
313 values.insert(i);
314 }
315 }
316
317 f if f.starts_with("*/") => {
319 let step: u32 = f.trim_start_matches("*/").parse()?;
320
321 if step == 0 || step > max {
322 return Err(ParseError::InvalidValue);
323 }
324
325 for i in (min..=max).step_by(step as usize) {
326 values.insert(i);
327 }
328 }
329
330 f if f.contains('/') => {
332 let tmp_fields: Vec<&str> = f.split('/').collect();
333
334 if tmp_fields.len() != 2 {
335 return Err(ParseError::InvalidRange);
336 }
337
338 let step: u32 = tmp_fields[1].parse()?;
340
341 if step == 0 || step > max {
342 return Err(ParseError::InvalidValue);
343 }
344
345 if tmp_fields[0].contains('-') {
347 let tmp_range: Vec<&str> = tmp_fields[0].split('-').collect();
348
349 if tmp_range.len() != 2 {
350 return Err(ParseError::InvalidRange);
351 }
352
353 let start = parse_cron_value(tmp_range[0], min, max)?;
354
355 let end = parse_cron_value(tmp_range[1], min, max)?;
356
357 if start > end {
358 return Err(ParseError::InvalidRange);
359 }
360
361 for i in (start..=end).step_by(step as usize) {
362 values.insert(i);
363 }
364 } else {
365 let start = parse_cron_value(tmp_fields[0], min, max)?;
366
367 for i in (start..=max).step_by(step as usize) {
368 values.insert(i);
369 }
370 }
371 }
372
373 f if f.contains('-') => {
375 let tmp_fields: Vec<&str> = f.split('-').collect();
376
377 if tmp_fields.len() != 2 {
378 return Err(ParseError::InvalidRange);
379 }
380
381 let start = parse_cron_value(tmp_fields[0], min, max)?;
382
383 let end = parse_cron_value(tmp_fields[1], min, max)?;
384
385 if start > end {
386 return Err(ParseError::InvalidRange);
387 }
388 for i in start..=end {
389 values.insert(i);
390 }
391 }
392
393 _ => {
395 let value = parse_cron_value(field, min, max)?;
396 values.insert(value);
397 }
398 }
399 }
400
401 Ok(values)
402}
403
404fn parse_cron_value(value: &str, min: u32, max: u32) -> Result<u32, ParseError> {
406 if let Ok(dow) = Dow::from_str(value) {
407 Ok(dow as u32)
408 } else {
409 let v: u32 = value.parse()?;
410 if v < min || v > max {
411 return Err(ParseError::InvalidValue);
412 }
413 Ok(v)
414 }
415}