clockwork_cron/
schedule.rs

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