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
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44enum Dow {
45 Sun = 0,
46 Mon = 1,
47 Tue = 2,
48 Wed = 3,
49 Thu = 4,
50 Fri = 5,
51 Sat = 6,
52}
53
54impl FromStr for Dow {
55 type Err = ();
56
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 match &*s.to_uppercase() {
59 "SUN" => Ok(Self::Sun),
60 "MON" => Ok(Self::Mon),
61 "TUE" => Ok(Self::Tue),
62 "WED" => Ok(Self::Wed),
63 "THU" => Ok(Self::Thu),
64 "FRI" => Ok(Self::Fri),
65 "SAT" => Ok(Self::Sat),
66 _ => Err(()),
67 }
68 }
69}
70
71impl fmt::Display for ParseError {
72 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73 match *self {
74 Self::InvalidCron => write!(f, "invalid cron"),
75 Self::InvalidRange => write!(f, "invalid input"),
76 Self::InvalidValue => write!(f, "invalid value"),
77 Self::ParseIntError(ref err) => err.fmt(f),
78 Self::TryFromIntError(ref err) => err.fmt(f),
79 Self::InvalidTimezone => write!(f, "invalid timezone"),
80 }
81 }
82}
83
84impl Error for ParseError {}
85
86impl From<num::ParseIntError> for ParseError {
87 fn from(err: num::ParseIntError) -> Self {
88 Self::ParseIntError(err)
89 }
90}
91
92impl From<num::TryFromIntError> for ParseError {
93 fn from(err: num::TryFromIntError) -> Self {
94 Self::TryFromIntError(err)
95 }
96}
97
98pub fn parse<TZ: TimeZone>(cron: &str, dt: &DateTime<TZ>) -> Result<DateTime<TZ>, ParseError> {
126 let tz = dt.timezone();
127
128 let fields: Vec<&str> = cron.split_whitespace().collect();
129
130 if fields.len() != 5 {
131 return Err(ParseError::InvalidCron);
132 }
133
134 let mut next = match Utc.from_local_datetime(&dt.naive_local()) {
135 chrono::LocalResult::Single(datetime) => datetime + Duration::minutes(1),
136 chrono::LocalResult::Ambiguous(earlier, _later) => earlier + Duration::minutes(1),
137 chrono::LocalResult::None => return Err(ParseError::InvalidTimezone),
138 };
139
140 next = make_utc_datetime(
141 next.year(),
142 next.month(),
143 next.day(),
144 next.hour(),
145 next.minute(),
146 0,
147 )?;
148
149 let result = loop {
150 if next.year() - dt.year() > 4 {
152 return Err(ParseError::InvalidCron);
153 }
154
155 let month = parse_field(fields[3], 1, 12)?;
157 if !month.contains(&next.month()) {
158 next = make_utc_datetime(
159 if next.month() == 12 {
160 next.year() + 1
161 } else {
162 next.year()
163 },
164 if next.month() == 12 {
165 1
166 } else {
167 next.month() + 1
168 },
169 1,
170 0,
171 0,
172 0,
173 )?;
174 continue;
175 }
176
177 let do_m = parse_field(fields[2], 1, 31)?;
179 if !do_m.contains(&next.day()) {
180 next += Duration::days(1);
181 next = make_utc_datetime(next.year(), next.month(), next.day(), 0, 0, 0)?;
182 continue;
183 }
184
185 let hour = parse_field(fields[1], 0, 23)?;
187 if !hour.contains(&next.hour()) {
188 next += Duration::hours(1);
189 next = make_utc_datetime(next.year(), next.month(), next.day(), next.hour(), 0, 0)?;
190 continue;
191 }
192
193 let minute = parse_field(fields[0], 0, 59)?;
195 if !minute.contains(&next.minute()) {
196 next += Duration::minutes(1);
197 continue;
198 }
199
200 let do_w = parse_field(fields[4], 0, 6)?;
202 if !do_w.contains(&next.weekday().num_days_from_sunday()) {
203 next += Duration::days(1);
204 continue;
205 }
206
207 match tz.from_local_datetime(&next.naive_local()) {
209 chrono::LocalResult::Single(dt) => break dt,
210 chrono::LocalResult::Ambiguous(earlier, _later) => break earlier,
211 chrono::LocalResult::None => {
212 next += Duration::minutes(1);
213 continue;
214 }
215 }
216 };
217
218 Ok(result)
219}
220
221pub fn parse_field(field: &str, min: u32, max: u32) -> Result<BTreeSet<u32>, ParseError> {
278 let mut values = BTreeSet::<u32>::new();
279
280 let fields: Vec<&str> = field.split(',').filter(|s| !s.is_empty()).collect();
282
283 for field in fields {
285 match field {
286 "*" => {
288 for i in min..=max {
289 values.insert(i);
290 }
291 }
292
293 f if f.starts_with("*/") => {
295 let step: u32 = f.trim_start_matches("*/").parse()?;
296
297 if step == 0 || step > max {
298 return Err(ParseError::InvalidValue);
299 }
300
301 for i in (min..=max).step_by(step as usize) {
302 values.insert(i);
303 }
304 }
305
306 f if f.contains('/') => {
308 let tmp_fields: Vec<&str> = f.split('/').collect();
309
310 if tmp_fields.len() != 2 {
311 return Err(ParseError::InvalidRange);
312 }
313
314 let step: u32 = tmp_fields[1].parse()?;
316
317 if step == 0 || step > max {
318 return Err(ParseError::InvalidValue);
319 }
320
321 if tmp_fields[0].contains('-') {
323 let tmp_range: Vec<&str> = tmp_fields[0].split('-').collect();
324
325 if tmp_range.len() != 2 {
326 return Err(ParseError::InvalidRange);
327 }
328
329 let start = parse_cron_value(tmp_range[0], min, max)?;
330
331 let end = parse_cron_value(tmp_range[1], min, max)?;
332
333 if start > end {
334 return Err(ParseError::InvalidRange);
335 }
336
337 for i in (start..=end).step_by(step as usize) {
338 values.insert(i);
339 }
340 } else {
341 let start = parse_cron_value(tmp_fields[0], min, max)?;
342
343 for i in (start..=max).step_by(step as usize) {
344 values.insert(i);
345 }
346 }
347 }
348
349 f if f.contains('-') => {
351 let tmp_fields: Vec<&str> = f.split('-').collect();
352
353 if tmp_fields.len() != 2 {
354 return Err(ParseError::InvalidRange);
355 }
356
357 let start = parse_cron_value(tmp_fields[0], min, max)?;
358
359 let end = parse_cron_value(tmp_fields[1], min, max)?;
360
361 if start > end {
362 return Err(ParseError::InvalidRange);
363 }
364 for i in start..=end {
365 values.insert(i);
366 }
367 }
368
369 _ => {
371 let value = parse_cron_value(field, min, max)?;
372 values.insert(value);
373 }
374 }
375 }
376
377 Ok(values)
378}
379
380fn parse_cron_value(value: &str, min: u32, max: u32) -> Result<u32, ParseError> {
382 if let Ok(dow) = Dow::from_str(value) {
383 Ok(dow as u32)
384 } else {
385 let v: u32 = value.parse()?;
386 if v < min || v > max {
387 return Err(ParseError::InvalidValue);
388 }
389 Ok(v)
390 }
391}
392
393fn make_utc_datetime(
395 year: i32,
396 month: u32,
397 day: u32,
398 hour: u32,
399 minute: u32,
400 second: u32,
401) -> Result<DateTime<Utc>, ParseError> {
402 match Utc.with_ymd_and_hms(year, month, day, hour, minute, second) {
403 chrono::LocalResult::Single(datetime) => Ok(datetime),
404 chrono::LocalResult::Ambiguous(earlier, _later) => Ok(earlier),
405 chrono::LocalResult::None => Err(ParseError::InvalidTimezone),
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_make_utc_datetime_valid() {
415 let result = make_utc_datetime(2024, 1, 15, 10, 30, 45);
417 assert!(result.is_ok());
418 let dt = result.unwrap();
419 assert_eq!(dt.year(), 2024);
420 assert_eq!(dt.month(), 1);
421 assert_eq!(dt.day(), 15);
422 assert_eq!(dt.hour(), 10);
423 assert_eq!(dt.minute(), 30);
424 assert_eq!(dt.second(), 45);
425 }
426
427 #[test]
428 fn test_make_utc_datetime_leap_year() {
429 assert!(make_utc_datetime(2024, 2, 29, 12, 0, 0).is_ok());
431 }
432
433 #[test]
434 fn test_make_utc_datetime_invalid_date() {
435 assert!(make_utc_datetime(2024, 2, 30, 12, 0, 0).is_err());
437
438 assert!(make_utc_datetime(2023, 2, 29, 12, 0, 0).is_err());
440
441 assert!(make_utc_datetime(2024, 4, 31, 12, 0, 0).is_err());
443 }
444
445 #[test]
446 fn test_make_utc_datetime_invalid_time() {
447 assert!(make_utc_datetime(2024, 1, 15, 24, 0, 0).is_err());
449
450 assert!(make_utc_datetime(2024, 1, 15, 12, 60, 0).is_err());
452
453 assert!(make_utc_datetime(2024, 1, 15, 12, 0, 60).is_err());
455 }
456
457 #[test]
458 fn test_make_utc_datetime_invalid_month() {
459 assert!(make_utc_datetime(2024, 0, 15, 12, 0, 0).is_err());
461 assert!(make_utc_datetime(2024, 13, 15, 12, 0, 0).is_err());
462 }
463
464 #[test]
465 fn test_make_utc_datetime_boundary_values() {
466 assert!(make_utc_datetime(2024, 1, 1, 0, 0, 0).is_ok());
468
469 assert!(make_utc_datetime(2024, 1, 1, 23, 59, 59).is_ok());
471
472 assert!(make_utc_datetime(2024, 12, 31, 23, 59, 59).is_ok());
474 }
475
476 #[test]
477 fn test_parse_error_display() {
478 let err = ParseError::InvalidCron;
480 assert_eq!(format!("{}", err), "invalid cron");
481
482 let err = ParseError::InvalidRange;
484 assert_eq!(format!("{}", err), "invalid input");
485
486 let err = ParseError::InvalidValue;
488 assert_eq!(format!("{}", err), "invalid value");
489
490 let parse_int_err = "abc".parse::<u32>().unwrap_err();
492 let err = ParseError::ParseIntError(parse_int_err);
493 assert!(format!("{}", err).contains("invalid digit"));
494
495 let try_from_err = u8::try_from(256u32).unwrap_err();
497 let err = ParseError::TryFromIntError(try_from_err);
498 assert!(format!("{}", err).contains("out of range"));
499
500 let err = ParseError::InvalidTimezone;
502 assert_eq!(format!("{}", err), "invalid timezone");
503 }
504
505 #[test]
506 fn test_parse_error_from_try_from_int_error() {
507 let try_from_err = u8::try_from(256u32).unwrap_err();
509 let parse_err: ParseError = try_from_err.into();
510 assert!(matches!(parse_err, ParseError::TryFromIntError(_)));
511 }
512
513 #[test]
514 fn test_parse_error_implements_error_trait() {
515 let err: Box<dyn Error> = Box::new(ParseError::InvalidCron);
517 assert_eq!(err.to_string(), "invalid cron");
518 }
519}