cron/
schedule.rs

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