1use chrono::{offset::Utc, TimeZone};
6use std::time::SystemTime;
7
8use super::Schedule;
9use crate::error::ScheduleError;
10
11mod parsing;
12mod time_units;
13use parsing::{parse_longhand, parse_shorthand, CronParsingError, Shorthand};
14use time_units::{Hours, Minutes, MonthDays, Months, TimeUnitField, WeekDays};
15
16pub const MAX_YEAR: Ordinal = 2100;
18
19type Ordinal = u32;
21
22#[derive(Debug)]
53pub struct CronSchedule<Z>
54where
55 Z: TimeZone,
56{
57 minutes: Minutes,
58 hours: Hours,
59 month_days: MonthDays,
60 months: Months,
61 week_days: WeekDays,
62 time_zone: Z,
63}
64
65impl<Z> Schedule for CronSchedule<Z>
66where
67 Z: TimeZone,
68{
69 fn next_call_at(&self, _last_run_at: Option<SystemTime>) -> Option<SystemTime> {
70 let now = self.time_zone.from_utc_datetime(&Utc::now().naive_utc());
71 self.next(now).map(SystemTime::from)
72 }
73}
74
75impl CronSchedule<Utc> {
76 pub fn new(
83 minutes: Vec<Ordinal>,
84 hours: Vec<Ordinal>,
85 month_days: Vec<Ordinal>,
86 months: Vec<Ordinal>,
87 week_days: Vec<Ordinal>,
88 ) -> Result<Self, ScheduleError> {
89 Self::new_with_time_zone(minutes, hours, month_days, months, week_days, Utc)
90 }
91
92 pub fn from_string(schedule: &str) -> Result<Self, ScheduleError> {
117 Self::from_string_with_time_zone(schedule, Utc)
118 }
119}
120
121impl<Z> CronSchedule<Z>
122where
123 Z: TimeZone,
124{
125 pub fn new_with_time_zone(
132 mut minutes: Vec<Ordinal>,
133 mut hours: Vec<Ordinal>,
134 mut month_days: Vec<Ordinal>,
135 mut months: Vec<Ordinal>,
136 mut week_days: Vec<Ordinal>,
137 time_zone: Z,
138 ) -> Result<Self, ScheduleError> {
139 minutes.sort_unstable();
140 minutes.dedup();
141 hours.sort_unstable();
142 hours.dedup();
143 month_days.sort_unstable();
144 month_days.dedup();
145 months.sort_unstable();
146 months.dedup();
147 week_days.sort_unstable();
148 week_days.dedup();
149
150 Self::validate(&minutes, &hours, &month_days, &months, &week_days)?;
151
152 Ok(Self {
153 minutes: Minutes::from_vec(minutes),
154 hours: Hours::from_vec(hours),
155 month_days: MonthDays::from_vec(month_days),
156 months: Months::from_vec(months),
157 week_days: WeekDays::from_vec(week_days),
158 time_zone,
159 })
160 }
161
162 pub fn from_string_with_time_zone(schedule: &str, time_zone: Z) -> Result<Self, ScheduleError> {
187 if schedule.starts_with('@') {
188 Self::from_shorthand(schedule, time_zone)
189 } else {
190 Self::from_longhand(schedule, time_zone)
191 }
192 }
193
194 fn validate(
197 minutes: &[Ordinal],
198 hours: &[Ordinal],
199 month_days: &[Ordinal],
200 months: &[Ordinal],
201 week_days: &[Ordinal],
202 ) -> Result<(), ScheduleError> {
203 use ScheduleError::CronScheduleError;
204
205 if minutes.is_empty() {
206 return Err(CronScheduleError("Minutes were not set".to_string()));
207 }
208 if *minutes.first().unwrap() < Minutes::inclusive_min() {
209 return Err(CronScheduleError(format!(
210 "Minutes cannot be less than {}",
211 Minutes::inclusive_min()
212 )));
213 }
214 if *minutes.last().unwrap() > Minutes::inclusive_max() {
215 return Err(CronScheduleError(format!(
216 "Minutes cannot be more than {}",
217 Minutes::inclusive_max()
218 )));
219 }
220
221 if hours.is_empty() {
222 return Err(CronScheduleError("Hours were not set".to_string()));
223 }
224 if *hours.first().unwrap() < Hours::inclusive_min() {
225 return Err(CronScheduleError(format!(
226 "Hours cannot be less than {}",
227 Hours::inclusive_min()
228 )));
229 }
230 if *hours.last().unwrap() > Hours::inclusive_max() {
231 return Err(CronScheduleError(format!(
232 "Hours cannot be more than {}",
233 Hours::inclusive_max()
234 )));
235 }
236
237 if month_days.is_empty() {
238 return Err(CronScheduleError("Month days were not set".to_string()));
239 }
240 if *month_days.first().unwrap() < MonthDays::inclusive_min() {
241 return Err(CronScheduleError(format!(
242 "Month days cannot be less than {}",
243 MonthDays::inclusive_min()
244 )));
245 }
246 if *month_days.last().unwrap() > MonthDays::inclusive_max() {
247 return Err(CronScheduleError(format!(
248 "Month days cannot be more than {}",
249 MonthDays::inclusive_max()
250 )));
251 }
252
253 if months.is_empty() {
254 return Err(CronScheduleError("Months were not set".to_string()));
255 }
256 if *months.first().unwrap() < Months::inclusive_min() {
257 return Err(CronScheduleError(format!(
258 "Months cannot be less than {}",
259 Months::inclusive_min()
260 )));
261 }
262 if *months.last().unwrap() > Months::inclusive_max() {
263 return Err(CronScheduleError(format!(
264 "Months cannot be more than {}",
265 Months::inclusive_max()
266 )));
267 }
268
269 if week_days.is_empty() {
270 return Err(CronScheduleError("Week days were not set".to_string()));
271 }
272 if *week_days.first().unwrap() < WeekDays::inclusive_min() {
273 return Err(CronScheduleError(format!(
274 "Week days cannot be less than {}",
275 WeekDays::inclusive_min()
276 )));
277 }
278 if *week_days.last().unwrap() > WeekDays::inclusive_max() {
279 return Err(CronScheduleError(format!(
280 "Week days cannot be more than {}",
281 WeekDays::inclusive_max()
282 )));
283 }
284
285 Ok(())
286 }
287
288 fn from_shorthand(schedule: &str, time_zone: Z) -> Result<Self, ScheduleError> {
289 use Shorthand::*;
290 match parse_shorthand(schedule)? {
291 Yearly => Ok(Self {
292 minutes: Minutes::List(vec![0]),
293 hours: Hours::List(vec![0]),
294 month_days: MonthDays::List(vec![1]),
295 week_days: WeekDays::All,
296 months: Months::List(vec![1]),
297 time_zone,
298 }),
299 Monthly => Ok(Self {
300 minutes: Minutes::List(vec![0]),
301 hours: Hours::List(vec![0]),
302 month_days: MonthDays::List(vec![1]),
303 week_days: WeekDays::All,
304 months: Months::All,
305 time_zone,
306 }),
307 Weekly => Ok(Self {
308 minutes: Minutes::List(vec![0]),
309 hours: Hours::List(vec![0]),
310 month_days: MonthDays::All,
311 week_days: WeekDays::List(vec![1]),
312 months: Months::All,
313 time_zone,
314 }),
315 Daily => Ok(Self {
316 minutes: Minutes::List(vec![0]),
317 hours: Hours::List(vec![0]),
318 month_days: MonthDays::All,
319 week_days: WeekDays::All,
320 months: Months::All,
321 time_zone,
322 }),
323 Hourly => Ok(Self {
324 minutes: Minutes::List(vec![0]),
325 hours: Hours::All,
326 month_days: MonthDays::All,
327 week_days: WeekDays::All,
328 months: Months::All,
329 time_zone,
330 }),
331 }
332 }
333
334 fn from_longhand(schedule: &str, time_zone: Z) -> Result<Self, ScheduleError> {
335 let components: Vec<_> = schedule.split_whitespace().collect();
336 if components.len() != 5 {
337 Err(ScheduleError::CronScheduleError(format!(
338 "'{schedule}' is not a valid cron schedule: invalid number of elements"
339 )))
340 } else {
341 let minutes = parse_longhand::<Minutes>(components[0])?;
342 let hours = parse_longhand::<Hours>(components[1])?;
343 let month_days = parse_longhand::<MonthDays>(components[2])?;
344 let months = parse_longhand::<Months>(components[3])?;
345 let week_days = parse_longhand::<WeekDays>(components[4])?;
346
347 CronSchedule::new_with_time_zone(
348 minutes, hours, month_days, months, week_days, time_zone,
349 )
350 }
351 }
352
353 fn next<Tz>(&self, now: chrono::DateTime<Tz>) -> Option<chrono::DateTime<Tz>>
383 where
384 Tz: chrono::TimeZone,
385 {
386 use chrono::{Datelike, Timelike};
387
388 let current_minute = now.minute();
389 let current_hour = now.hour();
390 let current_month_day = now.day();
391 let current_month = now.month();
392 let current_year = now.year() as Ordinal;
393 assert!(current_year <= MAX_YEAR);
394
395 let mut overflow = false;
396 for year in current_year..MAX_YEAR {
397 let month_start = if overflow { 1 } else { current_month };
398 for month in self.months.open_range(month_start) {
399 if month > current_month {
400 overflow = true;
401 }
402 let month_day_start = if overflow { 1 } else { current_month_day };
403 let num_days_in_month = days_in_month(month, year);
404 'day_loop: for month_day in self
405 .month_days
406 .bounded_range(month_day_start, num_days_in_month)
407 {
408 if month_day > current_month_day {
409 overflow = true;
410 }
411 let hour_target = if overflow { 0 } else { current_hour };
412 for hour in self.hours.open_range(hour_target) {
413 if hour > current_hour {
414 overflow = true;
415 }
416 let minute_target = if overflow { 0 } else { current_minute + 1 };
417 for minute in self.minutes.open_range(minute_target) {
418 let timezone = now.timezone();
420 if let chrono::offset::LocalResult::Single(candidate) = timezone
421 .with_ymd_and_hms(year as i32, month, month_day, hour, minute, 0)
422 {
423 if !self
425 .week_days
426 .contains(candidate.weekday().num_days_from_sunday())
427 {
428 continue 'day_loop;
431 }
432
433 return Some(candidate);
434 }
435 }
436 overflow = true;
437 }
438 overflow = true;
439 }
440 overflow = true;
441 }
442 overflow = true;
443 }
444
445 None
446 }
447}
448
449fn is_leap_year(year: Ordinal) -> bool {
450 let by_four = year % 4 == 0;
451 let by_hundred = year % 100 == 0;
452 let by_four_hundred = year % 400 == 0;
453 by_four && ((!by_hundred) || by_four_hundred)
454}
455
456fn days_in_month(month: Ordinal, year: Ordinal) -> Ordinal {
457 let is_leap_year = is_leap_year(year);
458 match month {
459 4 | 6 | 9 | 11 => 30,
460 2 if is_leap_year => 29,
461 2 => 28,
462 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
463 x => panic!(
464 "{} is not a valid value for a month (it must be between 1 and 12)",
465 x
466 ),
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473 use chrono::{DateTime, NaiveDateTime};
474
475 fn make_utc_date(s: &str) -> DateTime<Utc> {
476 DateTime::<Utc>::from_naive_utc_and_offset(
477 NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S %z").unwrap(),
478 Utc,
479 )
480 }
481
482 #[test]
483 fn test_cron_next() {
484 let date = make_utc_date("2020-10-19 20:30:00 +0000");
485 let cron_schedule = CronSchedule::from_string("* * * * *").unwrap();
486 let expected_date = make_utc_date("2020-10-19 20:31:00 +0000");
487 assert_eq!(Some(expected_date), cron_schedule.next(date));
488
489 let date = make_utc_date("2020-10-19 20:30:00 +0000");
490 let cron_schedule = CronSchedule::from_string("31 20 * * *").unwrap();
491 let expected_date = make_utc_date("2020-10-19 20:31:00 +0000");
492 assert_eq!(Some(expected_date), cron_schedule.next(date));
493
494 let date = make_utc_date("2020-10-19 20:30:00 +0000");
495 let cron_schedule = CronSchedule::from_string("31 14 4 11 *").unwrap();
496 let expected_date = make_utc_date("2020-11-04 14:31:00 +0000");
497 assert_eq!(Some(expected_date), cron_schedule.next(date));
498
499 let date = make_utc_date("2020-10-19 20:29:23 +0000");
500 let cron_schedule = CronSchedule::from_string("*/5 9-18 1 * 6,0").unwrap();
501 let expected_date = make_utc_date("2020-11-01 09:00:00 +0000");
502 assert_eq!(Some(expected_date), cron_schedule.next(date));
503
504 let date = make_utc_date("2020-10-19 20:29:23 +0000");
505 let cron_schedule = CronSchedule::from_string("3 12 29-31 1-6 2-4").unwrap();
506 let expected_date = make_utc_date("2021-03-30 12:03:00 +0000");
507 assert_eq!(Some(expected_date), cron_schedule.next(date));
508
509 let date = make_utc_date("2020-10-19 20:29:23 +0000");
510 let cron_schedule = CronSchedule::from_string("* * 30 2 *").unwrap();
511 assert_eq!(None, cron_schedule.next(date));
512 }
513
514 #[test]
515 fn test_cron_next_with_date_time() {
516 let date =
517 chrono::DateTime::parse_from_str("2020-10-19 20:29:23 +0112", "%Y-%m-%d %H:%M:%S %z")
518 .unwrap();
519 let time_zone = chrono::offset::FixedOffset::east_opt(3600 + 600 + 120).unwrap();
520 let cron_schedule =
521 CronSchedule::from_string_with_time_zone("3 12 29-31 1-6 2-4", time_zone).unwrap();
522 let expected_date =
523 chrono::DateTime::parse_from_str("2021-03-30 12:03:00 +0112", "%Y-%m-%d %H:%M:%S %z")
524 .unwrap();
525 assert_eq!(Some(expected_date), cron_schedule.next(date));
526 }
527
528 fn cron_schedule_equal<Z: TimeZone>(
529 schedule: &CronSchedule<Z>,
530 minutes: &[Ordinal],
531 hours: &[Ordinal],
532 month_days: &[Ordinal],
533 months: &[Ordinal],
534 week_days: &[Ordinal],
535 ) -> bool {
536 let minutes_equal = match &schedule.minutes {
537 Minutes::All => minutes == (1..=60).collect::<Vec<_>>(),
538 Minutes::List(vec) => minutes == vec,
539 };
540 let hours_equal = match &schedule.hours {
541 Hours::All => hours == (0..=23).collect::<Vec<_>>(),
542 Hours::List(vec) => hours == vec,
543 };
544 let month_days_equal = match &schedule.month_days {
545 MonthDays::All => month_days == (1..=31).collect::<Vec<_>>(),
546 MonthDays::List(vec) => month_days == vec,
547 };
548 let months_equal = match &schedule.months {
549 Months::All => months == (1..=12).collect::<Vec<_>>(),
550 Months::List(vec) => months == vec,
551 };
552 let week_days_equal = match &schedule.week_days {
553 WeekDays::All => week_days == (0..=6).collect::<Vec<_>>(),
554 WeekDays::List(vec) => week_days == vec,
555 };
556
557 minutes_equal && hours_equal && month_days_equal && months_equal && week_days_equal
558 }
559
560 #[test]
561 fn test_from_string() -> Result<(), ScheduleError> {
562 let schedule = CronSchedule::from_string("2 12 8 1 *")?;
563 assert!(cron_schedule_equal(
564 &schedule,
565 &[2],
566 &[12],
567 &[8],
568 &[1],
569 &(0..=6).collect::<Vec<_>>(),
570 ));
571
572 let schedule = CronSchedule::from_string("@yearly")?;
573 assert!(cron_schedule_equal(
574 &schedule,
575 &[0],
576 &[0],
577 &[1],
578 &[1],
579 &(0..=6).collect::<Vec<_>>(),
580 ));
581 let schedule = CronSchedule::from_string("@monthly")?;
582 assert!(cron_schedule_equal(
583 &schedule,
584 &[0],
585 &[0],
586 &[1],
587 &(1..=12).collect::<Vec<_>>(),
588 &(0..=6).collect::<Vec<_>>(),
589 ));
590 let schedule = CronSchedule::from_string("@weekly")?;
591 assert!(cron_schedule_equal(
592 &schedule,
593 &[0],
594 &[0],
595 &(1..=31).collect::<Vec<_>>(),
596 &(1..=12).collect::<Vec<_>>(),
597 &[1],
598 ));
599 let schedule = CronSchedule::from_string("@daily")?;
600 assert!(cron_schedule_equal(
601 &schedule,
602 &[0],
603 &[0],
604 &(1..=31).collect::<Vec<_>>(),
605 &(1..=12).collect::<Vec<_>>(),
606 &(0..=6).collect::<Vec<_>>(),
607 ));
608 let schedule = CronSchedule::from_string("@hourly")?;
609 assert!(cron_schedule_equal(
610 &schedule,
611 &[0],
612 &(0..=23).collect::<Vec<_>>(),
613 &(1..=31).collect::<Vec<_>>(),
614 &(1..=12).collect::<Vec<_>>(),
615 &(0..=6).collect::<Vec<_>>(),
616 ));
617
618 Ok(())
619 }
620}