Skip to main content

cron/
schedule.rs

1use std::{
2    fmt::{Display, Formatter, Result as FmtResult},
3    ops::Bound::{Included, Unbounded},
4};
5
6use time::{DateTime, TimeZone};
7
8use crate::{ordinal::*, queries::*, time_unit::*};
9
10impl From<Schedule> for String {
11    fn from(schedule: Schedule) -> String {
12        schedule.source
13    }
14}
15
16#[derive(Clone, Debug, Eq)]
17pub struct Schedule {
18    source: String,
19    fields: ScheduleFields,
20}
21
22impl Schedule {
23    pub(crate) fn new(source: String, fields: ScheduleFields) -> Schedule {
24        Schedule {
25            source,
26            fields,
27        }
28    }
29
30    fn next_after(&self, after: &DateTime) -> Option<DateTime> {
31        let mut query = NextAfterQuery::from(after);
32        for year in self
33            .fields
34            .years
35            .ordinals()
36            .range((Included(query.year_lower_bound()), Unbounded))
37            .cloned()
38        {
39            let month_start = query.month_lower_bound();
40            if !self.fields.months.ordinals().contains(&month_start) {
41                query.reset_month();
42            }
43            let month_range = (Included(month_start), Included(Months::inclusive_max()));
44            for month in self.fields.months.ordinals().range(month_range).cloned() {
45                let day_of_month_start = query.day_of_month_lower_bound();
46                if !self.fields.days_of_month.ordinals().contains(&day_of_month_start) {
47                    query.reset_day_of_month();
48                }
49                let day_of_month_end = days_in_month(month, year);
50                let day_of_month_range =
51                    (Included(day_of_month_start.min(day_of_month_end)), Included(day_of_month_end));
52
53                'day_loop: for day_of_month in self.fields.days_of_month.ordinals().range(day_of_month_range).cloned() {
54                    let hour_start = query.hour_lower_bound();
55                    if !self.fields.hours.ordinals().contains(&hour_start) {
56                        query.reset_hour();
57                    }
58                    let hour_range = (Included(hour_start), Included(Hours::inclusive_max()));
59
60                    for hour in self.fields.hours.ordinals().range(hour_range).cloned() {
61                        let minute_start = query.minute_lower_bound();
62                        if !self.fields.minutes.ordinals().contains(&minute_start) {
63                            query.reset_minute();
64                        }
65                        let minute_range = (Included(minute_start), Included(Minutes::inclusive_max()));
66
67                        for minute in self.fields.minutes.ordinals().range(minute_range).cloned() {
68                            let second_start = query.second_lower_bound();
69                            if !self.fields.seconds.ordinals().contains(&second_start) {
70                                query.reset_second();
71                            }
72                            let second_range = (Included(second_start), Included(Seconds::inclusive_max()));
73
74                            for second in self.fields.seconds.ordinals().range(second_range).cloned() {
75                                let candidate = match DateTime::from_parts(
76                                    year as i32,
77                                    month as u8,
78                                    day_of_month as u8,
79                                    hour as u8,
80                                    minute as u8,
81                                    second as u8,
82                                    0,
83                                    after.timezone(),
84                                ) {
85                                    Ok(candidate) => candidate,
86                                    Err(_) => continue,
87                                };
88                                if !self
89                                    .fields
90                                    .days_of_week
91                                    .ordinals()
92                                    .contains(&((candidate.weekday().number_from_sunday() as Ordinal) + 1))
93                                {
94                                    continue 'day_loop;
95                                }
96                                return Some(candidate);
97                            }
98                            query.reset_minute();
99                        } // End of minutes range
100                        query.reset_hour();
101                    } // End of hours range
102                    query.reset_day_of_month();
103                } // End of Day of Month range
104                query.reset_month();
105            } // End of Month range
106        }
107
108        // We ran out of dates to try.
109        None
110    }
111
112    fn prev_from(&self, before: &DateTime) -> Option<DateTime> {
113        let mut query = PrevFromQuery::from(before);
114        for year in self
115            .fields
116            .years
117            .ordinals()
118            .range((Unbounded, Included(query.year_upper_bound())))
119            .rev()
120            .cloned()
121        {
122            let month_start = query.month_upper_bound();
123
124            if !self.fields.months.ordinals().contains(&month_start) {
125                query.reset_month();
126            }
127            let month_range = (Included(Months::inclusive_min()), Included(month_start));
128
129            for month in self.fields.months.ordinals().range(month_range).rev().cloned() {
130                let day_of_month_end = query.day_of_month_upper_bound();
131                if !self.fields.days_of_month.ordinals().contains(&day_of_month_end) {
132                    query.reset_day_of_month();
133                }
134
135                let day_of_month_end = days_in_month(month, year).min(day_of_month_end);
136
137                let day_of_month_range = (Included(DaysOfMonth::inclusive_min()), Included(day_of_month_end));
138
139                'day_loop: for day_of_month in self
140                    .fields
141                    .days_of_month
142                    .ordinals()
143                    .range(day_of_month_range)
144                    .rev()
145                    .cloned()
146                {
147                    let hour_start = query.hour_upper_bound();
148                    if !self.fields.hours.ordinals().contains(&hour_start) {
149                        query.reset_hour();
150                    }
151                    let hour_range = (Included(Hours::inclusive_min()), Included(hour_start));
152
153                    for hour in self.fields.hours.ordinals().range(hour_range).rev().cloned() {
154                        let minute_start = query.minute_upper_bound();
155                        if !self.fields.minutes.ordinals().contains(&minute_start) {
156                            query.reset_minute();
157                        }
158                        let minute_range = (Included(Minutes::inclusive_min()), Included(minute_start));
159
160                        for minute in self.fields.minutes.ordinals().range(minute_range).rev().cloned() {
161                            let second_start = query.second_upper_bound();
162                            if !self.fields.seconds.ordinals().contains(&second_start) {
163                                query.reset_second();
164                            }
165                            let second_range = (Included(Seconds::inclusive_min()), Included(second_start));
166
167                            for second in self.fields.seconds.ordinals().range(second_range).rev().cloned() {
168                                let candidate = match DateTime::from_parts(
169                                    year as i32,
170                                    month as u8,
171                                    day_of_month as u8,
172                                    hour as u8,
173                                    minute as u8,
174                                    second as u8,
175                                    0,
176                                    before.timezone(),
177                                ) {
178                                    Ok(candidate) => candidate,
179                                    Err(_) => continue,
180                                };
181                                if !self
182                                    .fields
183                                    .days_of_week
184                                    .ordinals()
185                                    .contains(&((candidate.weekday().number_from_sunday() as Ordinal) + 1))
186                                {
187                                    continue 'day_loop;
188                                }
189                                return Some(candidate);
190                            }
191                            query.reset_minute();
192                        } // End of minutes range
193                        query.reset_hour();
194                    } // End of hours range
195                    query.reset_day_of_month();
196                } // End of Day of Month range
197                query.reset_month();
198            } // End of Month range
199        }
200
201        // We ran out of dates to try.
202        None
203    }
204
205    /// Provides an iterator which will return each DateTime that matches the schedule starting with
206    /// the current time if applicable.
207    pub fn upcoming(&self, timezone: TimeZone) -> ScheduleIterator<'_> {
208        self.after(&DateTime::now_in(timezone))
209    }
210
211    /// The same, but with an iterator with a static ownership
212    pub fn upcoming_owned(&self, timezone: TimeZone) -> OwnedScheduleIterator {
213        self.after_owned(DateTime::now_in(timezone))
214    }
215
216    /// Like the `upcoming` method, but allows you to specify a start time other than the present.
217    pub fn after(&self, after: &DateTime) -> ScheduleIterator<'_> {
218        ScheduleIterator::new(self, after)
219    }
220
221    /// The same, but with a static ownership.
222    pub fn after_owned(&self, after: DateTime) -> OwnedScheduleIterator {
223        OwnedScheduleIterator::new(self.clone(), after)
224    }
225
226    pub fn includes(&self, date_time: DateTime) -> bool {
227        self.fields.years.includes(date_time.year() as Ordinal)
228            && self.fields.months.includes(date_time.month() as Ordinal)
229            && self
230                .fields
231                .days_of_week
232                .includes((date_time.weekday().number_from_sunday() as Ordinal) + 1)
233            && self.fields.days_of_month.includes(date_time.day() as Ordinal)
234            && self.fields.hours.includes(date_time.hour() as Ordinal)
235            && self.fields.minutes.includes(date_time.minute() as Ordinal)
236            && self.fields.seconds.includes(date_time.second() as Ordinal)
237    }
238
239    /// Returns a [TimeUnitSpec] describing the years included in this [Schedule].
240    pub fn years(&self) -> &impl TimeUnitSpec {
241        &self.fields.years
242    }
243
244    /// Returns a [TimeUnitSpec] describing the months of the year included in this [Schedule].
245    pub fn months(&self) -> &impl TimeUnitSpec {
246        &self.fields.months
247    }
248
249    /// Returns a [TimeUnitSpec] describing the days of the month included in this [Schedule].
250    pub fn days_of_month(&self) -> &impl TimeUnitSpec {
251        &self.fields.days_of_month
252    }
253
254    /// Returns a [TimeUnitSpec] describing the days of the week included in this [Schedule].
255    pub fn days_of_week(&self) -> &impl TimeUnitSpec {
256        &self.fields.days_of_week
257    }
258
259    /// Returns a [TimeUnitSpec] describing the hours of the day included in this [Schedule].
260    pub fn hours(&self) -> &impl TimeUnitSpec {
261        &self.fields.hours
262    }
263
264    /// Returns a [TimeUnitSpec] describing the minutes of the hour included in this [Schedule].
265    pub fn minutes(&self) -> &impl TimeUnitSpec {
266        &self.fields.minutes
267    }
268
269    /// Returns a [TimeUnitSpec] describing the seconds of the minute included in this [Schedule].
270    pub fn seconds(&self) -> &impl TimeUnitSpec {
271        &self.fields.seconds
272    }
273
274    pub fn timeunitspec_eq(&self, other: &Schedule) -> bool {
275        self.fields == other.fields
276    }
277}
278
279impl Display for Schedule {
280    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
281        write!(f, "{}", self.source)
282    }
283}
284
285impl PartialEq for Schedule {
286    fn eq(&self, other: &Schedule) -> bool {
287        self.source == other.source
288    }
289}
290
291#[derive(Clone, Debug, PartialEq, Eq)]
292pub struct ScheduleFields {
293    years: Years,
294    days_of_week: DaysOfWeek,
295    months: Months,
296    days_of_month: DaysOfMonth,
297    hours: Hours,
298    minutes: Minutes,
299    seconds: Seconds,
300}
301
302impl ScheduleFields {
303    pub(crate) fn new(
304        seconds: Seconds,
305        minutes: Minutes,
306        hours: Hours,
307        days_of_month: DaysOfMonth,
308        months: Months,
309        days_of_week: DaysOfWeek,
310        years: Years,
311    ) -> ScheduleFields {
312        ScheduleFields {
313            years,
314            days_of_week,
315            months,
316            days_of_month,
317            hours,
318            minutes,
319            seconds,
320        }
321    }
322}
323
324pub struct ScheduleIterator<'a> {
325    schedule: &'a Schedule,
326    previous_datetime: Option<DateTime>,
327}
328//TODO: Cutoff datetime?
329
330impl<'a> ScheduleIterator<'a> {
331    fn new(schedule: &'a Schedule, starting_datetime: &DateTime) -> Self {
332        ScheduleIterator {
333            schedule,
334            previous_datetime: Some(*starting_datetime),
335        }
336    }
337}
338
339impl Iterator for ScheduleIterator<'_> {
340    type Item = DateTime;
341
342    fn next(&mut self) -> Option<DateTime> {
343        let previous = self.previous_datetime.take()?;
344
345        if let Some(next) = self.schedule.next_after(&previous) {
346            self.previous_datetime = Some(next);
347            Some(next)
348        } else {
349            None
350        }
351    }
352}
353
354impl DoubleEndedIterator for ScheduleIterator<'_> {
355    fn next_back(&mut self) -> Option<Self::Item> {
356        let previous = self.previous_datetime.take()?;
357
358        if let Some(prev) = self.schedule.prev_from(&previous) {
359            self.previous_datetime = Some(prev);
360            Some(prev)
361        } else {
362            None
363        }
364    }
365}
366
367/// A `ScheduleIterator` with a static lifetime.
368pub struct OwnedScheduleIterator {
369    schedule: Schedule,
370    previous_datetime: Option<DateTime>,
371}
372
373impl OwnedScheduleIterator {
374    pub fn new(schedule: Schedule, starting_datetime: DateTime) -> Self {
375        Self {
376            schedule,
377            previous_datetime: Some(starting_datetime),
378        }
379    }
380}
381
382impl Iterator for OwnedScheduleIterator {
383    type Item = DateTime;
384
385    fn next(&mut self) -> Option<DateTime> {
386        let previous = self.previous_datetime.take()?;
387
388        if let Some(next) = self.schedule.next_after(&previous) {
389            self.previous_datetime = Some(next);
390            Some(next)
391        } else {
392            None
393        }
394    }
395}
396
397impl DoubleEndedIterator for OwnedScheduleIterator {
398    fn next_back(&mut self) -> Option<Self::Item> {
399        let previous = self.previous_datetime.take()?;
400
401        if let Some(prev) = self.schedule.prev_from(&previous) {
402            self.previous_datetime = Some(prev);
403            Some(prev)
404        } else {
405            None
406        }
407    }
408}
409
410fn is_leap_year(year: Ordinal) -> bool {
411    let by_four = year.is_multiple_of(4);
412    let by_hundred = year.is_multiple_of(100);
413    let by_four_hundred = year.is_multiple_of(400);
414    by_four && ((!by_hundred) || by_four_hundred)
415}
416
417fn days_in_month(month: Ordinal, year: Ordinal) -> u32 {
418    let is_leap_year = is_leap_year(year);
419    match month {
420        9 | 4 | 6 | 11 => 30,
421        2 if is_leap_year => 29,
422        2 => 28,
423        _ => 31,
424    }
425}
426
427#[cfg(test)]
428mod test {
429    use std::str::FromStr;
430
431    use super::*;
432
433    #[test]
434    fn test_next_and_prev_from() {
435        let expression = "0 5,13,40-42 17 1 Jan *";
436        let schedule = Schedule::from_str(expression).unwrap();
437
438        let next = schedule.next_after(&DateTime::now_in(TimeZone::UTC));
439        println!("NEXT AFTER for {} {:?}", expression, next);
440        assert!(next.is_some());
441
442        let next2 = schedule.next_after(&next.unwrap());
443        println!("NEXT2 AFTER for {} {:?}", expression, next2);
444        assert!(next2.is_some());
445
446        let prev = schedule.prev_from(&next2.unwrap());
447        println!("PREV FROM for {} {:?}", expression, prev);
448        assert!(prev.is_some());
449        assert_eq!(prev, next);
450    }
451
452    #[test]
453    fn test_prev_from() {
454        let expression = "0 5,13,40-42 17 1 Jan *";
455        let schedule = Schedule::from_str(expression).unwrap();
456        let prev = schedule.prev_from(&DateTime::now_in(TimeZone::UTC));
457        println!("PREV FROM for {} {:?}", expression, prev);
458        assert!(prev.is_some());
459    }
460
461    #[test]
462    fn test_next_after() {
463        let expression = "0 5,13,40-42 17 1 Jan *";
464        let schedule = Schedule::from_str(expression).unwrap();
465        let next = schedule.next_after(&DateTime::now_in(TimeZone::UTC));
466        println!("NEXT AFTER for {} {:?}", expression, next);
467        assert!(next.is_some());
468    }
469
470    #[test]
471    fn test_upcoming_utc() {
472        let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs";
473        let schedule = Schedule::from_str(expression).unwrap();
474        let mut upcoming = schedule.upcoming(TimeZone::UTC);
475        let next1 = upcoming.next();
476        assert!(next1.is_some());
477        let next2 = upcoming.next();
478        assert!(next2.is_some());
479        let next3 = upcoming.next();
480        assert!(next3.is_some());
481        println!("Upcoming 1 for {} {:?}", expression, next1);
482        println!("Upcoming 2 for {} {:?}", expression, next2);
483        println!("Upcoming 3 for {} {:?}", expression, next3);
484    }
485
486    #[test]
487    fn test_upcoming_utc_owned() {
488        let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs";
489        let schedule = Schedule::from_str(expression).unwrap();
490        let mut upcoming = schedule.upcoming_owned(TimeZone::UTC);
491        let next1 = upcoming.next();
492        assert!(next1.is_some());
493        let next2 = upcoming.next();
494        assert!(next2.is_some());
495        let next3 = upcoming.next();
496        assert!(next3.is_some());
497        println!("Upcoming 1 for {} {:?}", expression, next1);
498        println!("Upcoming 2 for {} {:?}", expression, next2);
499        println!("Upcoming 3 for {} {:?}", expression, next3);
500    }
501
502    #[test]
503    fn test_upcoming_rev_utc() {
504        let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs";
505        let schedule = Schedule::from_str(expression).unwrap();
506        let mut upcoming = schedule.upcoming(TimeZone::UTC).rev();
507        let prev1 = upcoming.next();
508        assert!(prev1.is_some());
509        let prev2 = upcoming.next();
510        assert!(prev2.is_some());
511        let prev3 = upcoming.next();
512        assert!(prev3.is_some());
513        println!("Prev Upcoming 1 for {} {:?}", expression, prev1);
514        println!("Prev Upcoming 2 for {} {:?}", expression, prev2);
515        println!("Prev Upcoming 3 for {} {:?}", expression, prev3);
516    }
517
518    #[test]
519    fn test_upcoming_rev_utc_owned() {
520        let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs";
521        let schedule = Schedule::from_str(expression).unwrap();
522        let mut upcoming = schedule.upcoming_owned(TimeZone::UTC).rev();
523        let prev1 = upcoming.next();
524        assert!(prev1.is_some());
525        let prev2 = upcoming.next();
526        assert!(prev2.is_some());
527        let prev3 = upcoming.next();
528        assert!(prev3.is_some());
529        println!("Prev Upcoming 1 for {} {:?}", expression, prev1);
530        println!("Prev Upcoming 2 for {} {:?}", expression, prev2);
531        println!("Prev Upcoming 3 for {} {:?}", expression, prev3);
532    }
533
534    #[test]
535    fn test_upcoming_local() {
536        let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs";
537        let schedule = Schedule::from_str(expression).unwrap();
538        let mut upcoming = schedule.upcoming(TimeZone::system().unwrap_or(TimeZone::UTC));
539        let next1 = upcoming.next();
540        assert!(next1.is_some());
541        let next2 = upcoming.next();
542        assert!(next2.is_some());
543        let next3 = upcoming.next();
544        assert!(next3.is_some());
545        println!("Upcoming 1 for {} {:?}", expression, next1);
546        println!("Upcoming 2 for {} {:?}", expression, next2);
547        println!("Upcoming 3 for {} {:?}", expression, next3);
548    }
549
550    #[test]
551    fn test_schedule_to_string() {
552        let expression = "* 1,2,3 * * * *";
553        let schedule: Schedule = Schedule::from_str(expression).unwrap();
554        let result = String::from(schedule);
555        assert_eq!(expression, result);
556    }
557
558    #[test]
559    fn test_display_schedule() {
560        use std::fmt::Write;
561        let expression = "@monthly";
562        let schedule = Schedule::from_str(expression).unwrap();
563        let mut result = String::new();
564        write!(result, "{}", schedule).unwrap();
565        assert_eq!(expression, result);
566    }
567
568    #[test]
569    fn test_valid_from_str() {
570        let schedule = Schedule::from_str("0 0,30 0,6,12,18 1,15 Jan-March Thurs");
571        schedule.unwrap();
572    }
573
574    #[test]
575    fn test_invalid_from_str() {
576        let schedule = Schedule::from_str("cheesecake 0,30 0,6,12,18 1,15 Jan-March Thurs");
577        assert!(schedule.is_err());
578    }
579
580    // PATCH
581    // #[test]
582    // fn test_no_panic_on_nonexistent_time_after() {
583    //     use chrono::offset::TimeZone;
584    //     use chrono_tz::Tz;
585
586    //     let schedule_tz: Tz = "Europe/London".parse().unwrap();
587    //     let dt = schedule_tz
588    //         .ymd(2019, 10, 27)
589    //         .and_hms(0, 3, 29)
590    //         .checked_add_signed(chrono::Duration::hours(1)) // puts it in the middle of the DST transition
591    //         .unwrap();
592    //     let schedule = Schedule::from_str("* * * * * Sat,Sun *").unwrap();
593    //     let next = schedule.after(&dt).next().unwrap();
594    //     assert!(next > dt); // test is ensuring line above does not panic
595    // }
596
597    // PATCH
598    // #[test]
599    // fn test_no_panic_on_nonexistent_time_before() {
600    //     use chrono::offset::TimeZone;
601    //     use chrono_tz::Tz;
602
603    //     let schedule_tz: Tz = "Europe/London".parse().unwrap();
604    //     let dt = schedule_tz
605    //         .ymd(2019, 10, 27)
606    //         .and_hms(0, 3, 29)
607    //         .checked_add_signed(chrono::Duration::hours(1)) // puts it in the middle of the DST transition
608    //         .unwrap();
609    //     let schedule = Schedule::from_str("* * * * * Sat,Sun *").unwrap();
610    //     let prev = schedule.after(&dt).rev().next().unwrap();
611    //     assert!(prev < dt); // test is ensuring line above does not panic
612    // }
613
614    #[test]
615    fn test_no_panic_on_leap_day_time_after() {
616        let dt = "2024-02-29T10:00:00.000+08:00".parse::<DateTime>().unwrap();
617        let schedule = Schedule::from_str("0 0 0 * * * 2100").unwrap();
618        let next = schedule.after(&dt).next().unwrap();
619        assert!(next > dt); // test is ensuring line above does not panic
620    }
621
622    #[test]
623    fn test_time_unit_spec_equality() {
624        let schedule_1 = Schedule::from_str("@weekly").unwrap();
625        let schedule_2 = Schedule::from_str("0 0 0 * * 1 *").unwrap();
626        let schedule_3 = Schedule::from_str("0 0 0 * * 1-7 *").unwrap();
627        let schedule_4 = Schedule::from_str("0 0 0 * * * *").unwrap();
628        assert_ne!(schedule_1, schedule_2);
629        assert!(schedule_1.timeunitspec_eq(&schedule_2));
630        assert!(schedule_3.timeunitspec_eq(&schedule_4));
631    }
632}