cron_clock/
schedule.rs

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