sablier_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                            if let Some(second) =
87                                self.fields.seconds.ordinals().range(second_range).next()
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                            //TODO Rework it
214                            if let Some(second) = self
215                                .fields
216                                .seconds
217                                .ordinals()
218                                .range(second_range)
219                                .rev()
220                                .cloned()
221                                .next()
222                            {
223                                let timezone = before.timezone();
224                                let candidate = timezone
225                                    .with_ymd_and_hms(
226                                        year as i32,
227                                        month,
228                                        day_of_month,
229                                        hour,
230                                        minute,
231                                        second,
232                                    )
233                                    .unwrap();
234                                if !self
235                                    .fields
236                                    .days_of_week
237                                    .ordinals()
238                                    .contains(&candidate.weekday().number_from_sunday())
239                                {
240                                    continue 'day_loop;
241                                }
242                                return Some(candidate);
243                            }
244                            query.reset_minute();
245                        } // End of minutes range
246                        query.reset_hour();
247                    } // End of hours range
248                    query.reset_day_of_month();
249                } // End of Day of Month range
250                query.reset_month();
251            } // End of Month range
252        }
253
254        // We ran out of dates to try.
255        None
256    }
257
258    // /// Provides an iterator which will return each DateTime that matches the schedule starting with
259    // /// the current time if applicable.
260    // pub fn upcoming<Z>(&self, timezone: Z) -> ScheduleIterator<'_, Z>
261    // where
262    //     Z: TimeZone,
263    // {
264    //     self.after(&timezone.from_utc_datetime(&Utc::now().naive_utc()))
265    // }
266
267    /// Like the `upcoming` method, but allows you to specify a start time other than the present.
268    pub fn after<Z>(&self, after: &DateTime<Z>) -> ScheduleIterator<'_, Z>
269    where
270        Z: TimeZone,
271    {
272        ScheduleIterator::new(self, after)
273    }
274
275    pub fn includes<Z>(&self, date_time: DateTime<Z>) -> bool
276    where
277        Z: TimeZone,
278    {
279        self.fields.years.includes(date_time.year() as Ordinal)
280            && self.fields.months.includes(date_time.month() as Ordinal)
281            && self
282                .fields
283                .days_of_week
284                .includes(date_time.weekday().number_from_sunday())
285            && self
286                .fields
287                .days_of_month
288                .includes(date_time.day() as Ordinal)
289            && self.fields.hours.includes(date_time.hour() as Ordinal)
290            && self.fields.minutes.includes(date_time.minute() as Ordinal)
291            && self.fields.seconds.includes(date_time.second() as Ordinal)
292    }
293
294    /// Returns a [TimeUnitSpec](trait.TimeUnitSpec.html) describing the years included
295    /// in this [Schedule](struct.Schedule.html).
296    pub fn years(&self) -> &impl TimeUnitSpec {
297        &self.fields.years
298    }
299
300    /// Returns a [TimeUnitSpec](trait.TimeUnitSpec.html) describing the months of the year included
301    /// in this [Schedule](struct.Schedule.html).
302    pub fn months(&self) -> &impl TimeUnitSpec {
303        &self.fields.months
304    }
305
306    /// Returns a [TimeUnitSpec](trait.TimeUnitSpec.html) describing the days of the month included
307    /// in this [Schedule](struct.Schedule.html).
308    pub fn days_of_month(&self) -> &impl TimeUnitSpec {
309        &self.fields.days_of_month
310    }
311
312    /// Returns a [TimeUnitSpec](trait.TimeUnitSpec.html) describing the days of the week included
313    /// in this [Schedule](struct.Schedule.html).
314    pub fn days_of_week(&self) -> &impl TimeUnitSpec {
315        &self.fields.days_of_week
316    }
317
318    /// Returns a [TimeUnitSpec](trait.TimeUnitSpec.html) describing the hours of the day included
319    /// in this [Schedule](struct.Schedule.html).
320    pub fn hours(&self) -> &impl TimeUnitSpec {
321        &self.fields.hours
322    }
323
324    /// Returns a [TimeUnitSpec](trait.TimeUnitSpec.html) describing the minutes of the hour included
325    /// in this [Schedule](struct.Schedule.html).
326    pub fn minutes(&self) -> &impl TimeUnitSpec {
327        &self.fields.minutes
328    }
329
330    /// Returns a [TimeUnitSpec](trait.TimeUnitSpec.html) describing the seconds of the minute included
331    /// in this [Schedule](struct.Schedule.html).
332    pub fn seconds(&self) -> &impl TimeUnitSpec {
333        &self.fields.seconds
334    }
335
336    pub fn timeunitspec_eq(&self, other: &Schedule) -> bool {
337        self.fields == other.fields
338    }
339}
340
341impl Display for Schedule {
342    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
343        write!(f, "{}", self.source)
344    }
345}
346
347impl PartialEq for Schedule {
348    fn eq(&self, other: &Schedule) -> bool {
349        self.source == other.source
350    }
351}
352
353#[derive(Clone, Debug, PartialEq, Eq)]
354pub struct ScheduleFields {
355    years: Years,
356    days_of_week: DaysOfWeek,
357    months: Months,
358    days_of_month: DaysOfMonth,
359    hours: Hours,
360    minutes: Minutes,
361    seconds: Seconds,
362}
363
364impl ScheduleFields {
365    pub(crate) fn new(
366        seconds: Seconds,
367        minutes: Minutes,
368        hours: Hours,
369        days_of_month: DaysOfMonth,
370        months: Months,
371        days_of_week: DaysOfWeek,
372        years: Years,
373    ) -> ScheduleFields {
374        ScheduleFields {
375            years,
376            days_of_week,
377            months,
378            days_of_month,
379            hours,
380            minutes,
381            seconds,
382        }
383    }
384}
385
386pub struct ScheduleIterator<'a, Z>
387where
388    Z: TimeZone,
389{
390    is_done: bool,
391    schedule: &'a Schedule,
392    previous_datetime: DateTime<Z>,
393}
394//TODO: Cutoff datetime?
395
396impl<'a, Z> ScheduleIterator<'a, Z>
397where
398    Z: TimeZone,
399{
400    fn new(schedule: &'a Schedule, starting_datetime: &DateTime<Z>) -> ScheduleIterator<'a, Z> {
401        ScheduleIterator {
402            is_done: false,
403            schedule,
404            previous_datetime: starting_datetime.clone(),
405        }
406    }
407}
408
409impl<'a, Z> Iterator for ScheduleIterator<'a, Z>
410where
411    Z: TimeZone,
412{
413    type Item = DateTime<Z>;
414
415    fn next(&mut self) -> Option<DateTime<Z>> {
416        if self.is_done {
417            return None;
418        }
419        if let Some(next_datetime) = self.schedule.next_after(&self.previous_datetime) {
420            self.previous_datetime = next_datetime.clone();
421            Some(next_datetime)
422        } else {
423            self.is_done = true;
424            None
425        }
426    }
427}
428
429impl<'a, Z> DoubleEndedIterator for ScheduleIterator<'a, Z>
430where
431    Z: TimeZone,
432{
433    fn next_back(&mut self) -> Option<Self::Item> {
434        if self.is_done {
435            return None;
436        }
437
438        if let Some(prev_datetime) = self.schedule.prev_before(&self.previous_datetime) {
439            self.previous_datetime = prev_datetime.clone();
440            Some(prev_datetime)
441        } else {
442            self.is_done = true;
443            None
444        }
445    }
446}
447
448fn is_leap_year(year: Ordinal) -> bool {
449    let by_four = year % 4 == 0;
450    let by_hundred = year % 100 == 0;
451    let by_four_hundred = year % 400 == 0;
452    by_four && ((!by_hundred) || by_four_hundred)
453}
454
455fn days_in_month(month: Ordinal, year: Ordinal) -> u32 {
456    let is_leap_year = is_leap_year(year);
457    match month {
458        9 | 4 | 6 | 11 => 30,
459        2 if is_leap_year => 29,
460        2 => 28,
461        _ => 31,
462    }
463}
464
465#[cfg(test)]
466mod test {
467    use super::*;
468    use std::str::FromStr;
469
470    // #[test]
471    // fn test_next_and_prev_from() {
472    //     let expression = "0 5,13,40-42 17 1 Jan *";
473    //     let schedule = Schedule::from_str(expression).unwrap();
474
475    //     let next = schedule.next_after(&Utc::now());
476    //     println!("NEXT AFTER for {} {:?}", expression, next);
477    //     assert!(next.is_some());
478
479    //     let next2 = schedule.next_after(&next.unwrap());
480    //     println!("NEXT2 AFTER for {} {:?}", expression, next2);
481    //     assert!(next2.is_some());
482
483    //     let prev = schedule.prev_from(&next2.unwrap());
484    //     println!("PREV FROM for {} {:?}", expression, prev);
485    //     assert!(prev.is_some());
486    //     assert_eq!(prev, next);
487    // }
488
489    // #[test]
490    // fn test_prev_from() {
491    //     let expression = "0 5,13,40-42 17 1 Jan *";
492    //     let schedule = Schedule::from_str(expression).unwrap();
493    //     let prev = schedule.prev_from(&Utc::now());
494    //     println!("PREV FROM for {} {:?}", expression, prev);
495    //     assert!(prev.is_some());
496    // }
497
498    // #[test]
499    // fn test_next_after() {
500    //     let expression = "0 5,13,40-42 17 1 Jan *";
501    //     let schedule = Schedule::from_str(expression).unwrap();
502    //     let next = schedule.next_after(&Utc::now());
503    //     println!("NEXT AFTER for {} {:?}", expression, next);
504    //     assert!(next.is_some());
505    // }
506
507    // #[test]
508    // fn test_upcoming_utc() {
509    //     let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs";
510    //     let schedule = Schedule::from_str(expression).unwrap();
511    //     let mut upcoming = schedule.upcoming(Utc);
512    //     let next1 = upcoming.next();
513    //     assert!(next1.is_some());
514    //     let next2 = upcoming.next();
515    //     assert!(next2.is_some());
516    //     let next3 = upcoming.next();
517    //     assert!(next3.is_some());
518    //     println!("Upcoming 1 for {} {:?}", expression, next1);
519    //     println!("Upcoming 2 for {} {:?}", expression, next2);
520    //     println!("Upcoming 3 for {} {:?}", expression, next3);
521    // }
522
523    // #[test]
524    // fn test_upcoming_rev_utc() {
525    //     let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs";
526    //     let schedule = Schedule::from_str(expression).unwrap();
527    //     let mut upcoming = schedule.upcoming(Utc).rev();
528    //     let prev1 = upcoming.next();
529    //     assert!(prev1.is_some());
530    //     let prev2 = upcoming.next();
531    //     assert!(prev2.is_some());
532    //     let prev3 = upcoming.next();
533    //     assert!(prev3.is_some());
534    //     println!("Prev Upcoming 1 for {} {:?}", expression, prev1);
535    //     println!("Prev Upcoming 2 for {} {:?}", expression, prev2);
536    //     println!("Prev Upcoming 3 for {} {:?}", expression, prev3);
537    // }
538
539    // #[test]
540    // fn test_upcoming_local() {
541    //     use chrono::Local;
542    //     let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs";
543    //     let schedule = Schedule::from_str(expression).unwrap();
544    //     let mut upcoming = schedule.upcoming(Local);
545    //     let next1 = upcoming.next();
546    //     assert!(next1.is_some());
547    //     let next2 = upcoming.next();
548    //     assert!(next2.is_some());
549    //     let next3 = upcoming.next();
550    //     assert!(next3.is_some());
551    //     println!("Upcoming 1 for {} {:?}", expression, next1);
552    //     println!("Upcoming 2 for {} {:?}", expression, next2);
553    //     println!("Upcoming 3 for {} {:?}", expression, next3);
554    // }
555
556    #[test]
557    fn test_schedule_to_string() {
558        let expression = "* 1,2,3 * * * *";
559        let schedule: Schedule = Schedule::from_str(expression).unwrap();
560        let result = String::from(schedule);
561        assert_eq!(expression, result);
562    }
563
564    #[test]
565    fn test_display_schedule() {
566        use std::fmt::Write;
567        let expression = "@monthly";
568        let schedule = Schedule::from_str(expression).unwrap();
569        let mut result = String::new();
570        write!(result, "{}", schedule).unwrap();
571        assert_eq!(expression, result);
572    }
573
574    #[test]
575    fn test_valid_from_str() {
576        let schedule = Schedule::from_str("0 0,30 0,6,12,18 1,15 Jan-March Thurs");
577        schedule.unwrap();
578    }
579
580    #[test]
581    fn test_invalid_from_str() {
582        let schedule = Schedule::from_str("cheesecake 0,30 0,6,12,18 1,15 Jan-March Thurs");
583        assert!(schedule.is_err());
584    }
585
586    #[test]
587    fn test_time_unit_spec_equality() {
588        let schedule_1 = Schedule::from_str("@weekly").unwrap();
589        let schedule_2 = Schedule::from_str("0 0 0 * * 1 *").unwrap();
590        let schedule_3 = Schedule::from_str("0 0 0 * * 1-7 *").unwrap();
591        let schedule_4 = Schedule::from_str("0 0 0 * * * *").unwrap();
592        assert_ne!(schedule_1, schedule_2);
593        assert!(schedule_1.timeunitspec_eq(&schedule_2));
594        assert!(schedule_3.timeunitspec_eq(&schedule_4));
595    }
596}