cronos_cron/
schedule.rs

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