Skip to main content

cron/
schedule.rs

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