cron_lingo/
schedule.rs

1use crate::error::*;
2use crate::parse::parse;
3use crate::types::*;
4use std::iter::Iterator;
5use std::str::FromStr;
6use time::{Duration, OffsetDateTime, PrimitiveDateTime, UtcOffset};
7
8/// A schedule that is built from an expression and can be iterated
9/// in order to compute the next date(s) that match the specification.
10#[derive(Debug, PartialEq, Clone)]
11pub struct Schedule(ParsedSchedule);
12
13impl Schedule {
14    #[allow(dead_code)]
15    pub fn iter(&self) -> Result<ScheduleIter, Error> {
16        let Schedule(schedule) = self;
17        let iter = ScheduleIter {
18            schedule: schedule.clone(),
19            current: OffsetDateTime::now_local().map_err(Error::IndeterminateOffset)?,
20            skip_outdated: true,
21            offset: None,
22        };
23        Ok(iter)
24    }
25}
26
27impl FromStr for Schedule {
28    type Err = Error;
29
30    /// Attempt to create a new `Schedule` object from an expression.
31    ///
32    /// ```rust
33    /// use cron_lingo::Schedule;
34    /// use std::str::FromStr;
35    ///
36    /// let expr = "at 6 AM on Mondays and Thursdays in even weeks";
37    /// assert!(Schedule::from_str(expr).is_ok());
38    /// ```
39    fn from_str(expression: &str) -> Result<Self, Self::Err> {
40        Ok(Schedule(parse(expression)?))
41    }
42}
43
44impl std::ops::Add<Schedule> for Schedule {
45    type Output = MultiSchedule;
46
47    fn add(self, other: Self) -> Self::Output {
48        let Schedule(first) = self;
49        let Schedule(second) = other;
50        MultiSchedule(vec![first, second])
51    }
52}
53
54/// A wrapper around `Schedule` that keeps track of state during iteration.
55#[derive(Clone)]
56pub struct ScheduleIter {
57    schedule: ParsedSchedule,
58    current: OffsetDateTime,
59    skip_outdated: bool,
60    offset: Option<UtcOffset>,
61}
62
63impl ScheduleIter {
64    /// By default the `next` method will not return a date that is
65    /// in the past but compute the next future data. This method
66    /// allows to change the iterators default behaviour.
67    pub fn skip_outdated(mut self, skip: bool) -> ScheduleIter {
68        self.skip_outdated = skip;
69        self
70    }
71
72    /// By default the iterator returns dates in the current local
73    /// offset taken from the system. This method allows to change
74    /// the iteration behaviour to compute dates in another offset.
75    pub fn assume_offset(mut self, offset: UtcOffset) -> ScheduleIter {
76        self.offset = Some(offset);
77        self
78    }
79
80    /// Compute dates in the current local offset. This is also the
81    /// default behaviour and can be used to revert changes to this
82    /// behaviour that were made using `assume_offset`.
83    pub fn use_local_offset(mut self) -> ScheduleIter {
84        self.offset = None;
85        self
86    }
87}
88
89impl Iterator for ScheduleIter {
90    type Item = Result<OffsetDateTime, Error>;
91
92    fn next(&mut self) -> Option<Self::Item> {
93        if let Some(offset) = self.offset {
94            self.current = self.current.to_offset(offset);
95        }
96
97        if self.skip_outdated {
98            let mut now = match OffsetDateTime::now_local().map_err(Error::IndeterminateOffset) {
99                Ok(n) => n,
100                Err(e) => return Some(Err(e)),
101            };
102
103            if let Some(offset) = self.offset {
104                now = now.to_offset(offset);
105            }
106
107            if now > self.current {
108                self.current = now;
109            }
110        }
111
112        // Create every possible combination of dates for each
113        // ParsedSchedule and add them to a vector.
114        let candidates: Vec<OffsetDateTime> = compute_dates(self.current, &self.schedule);
115
116        // Iterate the vector of dates and find the next date
117        // by subtracting the current date from each element
118        // in the vector. Return the date that results in the
119        // lowest delta.
120        let next_date = candidates
121            .iter()
122            .min_by_key(|d| **d - self.current)
123            .unwrap();
124
125        self.current = *next_date;
126
127        Some(Ok(*next_date))
128    }
129}
130
131/// A combination of multiple schedules that can be iterated in order
132/// to compute the next date(s) that match the set of specifications. By
133/// default the computation is based on the current system time, meaning
134/// the iterator will never return a date in the past.
135#[derive(Debug, PartialEq, Clone)]
136pub struct MultiSchedule(Vec<ParsedSchedule>);
137
138impl MultiSchedule {
139    #[allow(dead_code)]
140    pub fn iter(&self) -> Result<MultiScheduleIter, Error> {
141        let MultiSchedule(schedules) = self;
142        let iter = MultiScheduleIter {
143            schedules: &schedules,
144            current: OffsetDateTime::now_local().map_err(Error::IndeterminateOffset)?,
145            skip_outdated: true,
146            offset: None,
147        };
148        Ok(iter)
149    }
150}
151
152impl From<Schedule> for MultiSchedule {
153    fn from(schedule: Schedule) -> Self {
154        let Schedule(schedule) = schedule;
155        MultiSchedule(vec![schedule])
156    }
157}
158
159impl std::ops::Add<Schedule> for MultiSchedule {
160    type Output = Self;
161
162    fn add(self, other: Schedule) -> Self {
163        let MultiSchedule(mut schedules) = self;
164        let Schedule(schedule) = other;
165        schedules.push(schedule);
166        MultiSchedule(schedules)
167    }
168}
169
170impl std::ops::Add<MultiSchedule> for MultiSchedule {
171    type Output = Self;
172
173    fn add(self, other: MultiSchedule) -> Self {
174        let MultiSchedule(mut schedules) = self;
175        let MultiSchedule(mut other_schedules) = other;
176        schedules.append(&mut other_schedules);
177        MultiSchedule(schedules)
178    }
179}
180
181impl std::ops::AddAssign<Schedule> for MultiSchedule {
182    fn add_assign(&mut self, other: Schedule) {
183        let MultiSchedule(schedules) = self;
184        let Schedule(schedule) = other;
185        schedules.push(schedule);
186    }
187}
188
189impl std::ops::AddAssign<MultiSchedule> for MultiSchedule {
190    fn add_assign(&mut self, other: MultiSchedule) {
191        let MultiSchedule(schedules) = self;
192        let MultiSchedule(mut other_schedules) = other;
193        schedules.append(&mut other_schedules);
194    }
195}
196
197/// A wrapper around `MultiSchedule` that keeps track of state during iteration.
198#[derive(Clone)]
199pub struct MultiScheduleIter<'a> {
200    schedules: &'a [ParsedSchedule],
201    current: OffsetDateTime,
202    skip_outdated: bool,
203    offset: Option<UtcOffset>,
204}
205
206impl<'a> MultiScheduleIter<'a> {
207    /// By default the `next` method will not return a date that is
208    /// in the past but compute the next future data. This method
209    /// allows to change the iterators default behaviour.
210    pub fn skip_outdated(mut self, skip: bool) -> MultiScheduleIter<'a> {
211        self.skip_outdated = skip;
212        self
213    }
214    /// By default the iterator returns dates in the current local
215    /// offset taken from the system. This method allows to change
216    /// the iteration behaviour to compute dates in another offset.
217    pub fn assume_offset(mut self, offset: UtcOffset) -> MultiScheduleIter<'a> {
218        self.offset = Some(offset);
219        self
220    }
221
222    /// Compute dates in the current local offset. This is also the
223    /// default behaviour and can be used to revert changes to this
224    /// behaviour that were made using `assume_offset`.
225    pub fn use_local_offset(mut self) -> MultiScheduleIter<'a> {
226        self.offset = None;
227        self
228    }
229}
230
231impl<'a> Iterator for MultiScheduleIter<'a> {
232    type Item = Result<OffsetDateTime, Error>;
233
234    fn next(&mut self) -> Option<Self::Item> {
235        if let Some(offset) = self.offset {
236            self.current = self.current.to_offset(offset);
237        }
238
239        if self.skip_outdated {
240            let mut now = match OffsetDateTime::now_local().map_err(Error::IndeterminateOffset) {
241                Ok(n) => n,
242                Err(e) => return Some(Err(e)),
243            };
244
245            if let Some(offset) = self.offset {
246                now = now.to_offset(offset);
247            }
248
249            if now > self.current {
250                self.current = now;
251            }
252        }
253
254        // Create every possible combination of dates for each
255        // ParsedSchedule and add them to a vector.
256        let mut candidates: Vec<OffsetDateTime> = vec![];
257
258        for schedule in self.schedules {
259            candidates.append(&mut compute_dates(self.current, schedule));
260        }
261
262        // Iterate the vector of dates and find the next date
263        // by subtracting the current date from each element
264        // in the vector. Return the date that results in the
265        // lowest delta.
266        let next_date = candidates
267            .iter()
268            .min_by_key(|d| **d - self.current)
269            .unwrap();
270
271        self.current = *next_date;
272
273        Some(Ok(*next_date))
274    }
275}
276
277// Returns a selection of possible next dates according to the rules in a ParsedSchedule.
278fn compute_dates(base: OffsetDateTime, spec: &ParsedSchedule) -> Vec<OffsetDateTime> {
279    let mut candidates = vec![];
280    let today = base.date();
281    let offset = base.offset();
282
283    // For each specified time ...
284    for time in &spec.times {
285        // ... create an OffsetDateTime object for each upcoming weekday ...
286        for i in 0..=6 {
287            let mut date =
288                PrimitiveDateTime::new(today + Duration::days(i), *time).assume_offset(offset);
289
290            if date <= base {
291                date += Duration::weeks(1);
292            }
293
294            candidates.push(date);
295        }
296    }
297
298    // ... remove all objects that match none of the desired weekdays (if any)
299    // and increment the remaining dates according to the optional WeekdayModifier
300    // and WeekVariant.
301    if let Some(ref days) = spec.days {
302        let weeks = spec.weeks;
303
304        candidates = candidates
305            .into_iter()
306            .filter(|c| days.iter().any(|x| x.0 == c.weekday()))
307            .collect();
308
309        for candidate in &mut candidates {
310            let day_modifier = days.iter().find(|x| x.0 == candidate.weekday()).unwrap().1;
311
312            while !check_date_validity(candidate, day_modifier, weeks) {
313                *candidate += Duration::weeks(1);
314            }
315        }
316    }
317
318    // ... and return the filtered date candidates of this ParsedSchedule.
319    candidates
320}
321
322// Takes a date and checks its bounds according to optional WeekdayModifiers
323// and/or WeekVariants. Returns false if the date does not match the specified rules.
324fn check_date_validity(
325    date: &OffsetDateTime,
326    weekday_mod: Option<WeekdayModifier>,
327    week_mod: Option<WeekVariant>,
328) -> bool {
329    let is_correct_day = match weekday_mod {
330        Some(modifier) => {
331            let day = date.day();
332
333            match modifier {
334                WeekdayModifier::First => day <= 7,
335                WeekdayModifier::Second => day > 7 && day <= 14,
336                WeekdayModifier::Third => day > 14 && day <= 21,
337                WeekdayModifier::Fourth => day > 21 && day <= 28,
338                WeekdayModifier::Last => date.month() != (*date + Duration::weeks(1)).month(),
339            }
340        }
341        None => true,
342    };
343
344    let is_correct_week = match week_mod {
345        Some(modifier) => {
346            let week = date.date().iso_week();
347
348            match modifier {
349                WeekVariant::Even => week % 2 == 0,
350                WeekVariant::Odd => week % 2 != 0,
351            }
352        }
353        None => true,
354    };
355
356    is_correct_day && is_correct_week
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use time::macros::{datetime, offset, time};
363    use time::Weekday;
364
365    #[test]
366    fn test_compute_dates_1() {
367        let base = datetime!(2021-06-04 13:38:00 UTC);
368        let spec = ParsedSchedule {
369            times: vec![time!(12:00:00), time!(18:00:00)],
370            days: None,
371            weeks: None,
372        };
373        let result = vec![
374            datetime!(2021-06-11 12:00:00 UTC),
375            datetime!(2021-06-05 12:00:00 UTC),
376            datetime!(2021-06-06 12:00:00 UTC),
377            datetime!(2021-06-07 12:00:00 UTC),
378            datetime!(2021-06-08 12:00:00 UTC),
379            datetime!(2021-06-09 12:00:00 UTC),
380            datetime!(2021-06-10 12:00:00 UTC),
381            datetime!(2021-06-04 18:00:00 UTC),
382            datetime!(2021-06-05 18:00:00 UTC),
383            datetime!(2021-06-06 18:00:00 UTC),
384            datetime!(2021-06-07 18:00:00 UTC),
385            datetime!(2021-06-08 18:00:00 UTC),
386            datetime!(2021-06-09 18:00:00 UTC),
387            datetime!(2021-06-10 18:00:00 UTC),
388        ];
389        assert_eq!(compute_dates(base, &spec), result);
390    }
391
392    #[test]
393    fn test_compute_dates_2() {
394        let base = datetime!(2021-06-04 13:38:00 UTC);
395        let spec = ParsedSchedule {
396            times: vec![time!(18:00:00)],
397            days: Some(vec![(Weekday::Monday, None), (Weekday::Thursday, None)]),
398            weeks: None,
399        };
400        let result = vec![
401            datetime!(2021-06-07 18:00:00 UTC),
402            datetime!(2021-06-10 18:00:00 UTC),
403        ];
404        assert_eq!(compute_dates(base, &spec), result);
405    }
406
407    #[test]
408    fn test_compute_dates_3() {
409        let base = datetime!(2021-06-04 13:38:00 UTC);
410        let spec = ParsedSchedule {
411            times: vec![time!(18:00:00)],
412            days: Some(vec![
413                (Weekday::Monday, Some(WeekdayModifier::Second)),
414                (Weekday::Thursday, None),
415            ]),
416            weeks: None,
417        };
418        let result = vec![
419            datetime!(2021-06-14 18:00:00 UTC),
420            datetime!(2021-06-10 18:00:00 UTC),
421        ];
422        assert_eq!(compute_dates(base, &spec), result);
423    }
424
425    #[test]
426    fn test_compute_dates_4() {
427        let base = datetime!(2021-06-04 13:38:00 UTC);
428        let spec = ParsedSchedule {
429            times: vec![time!(12:00:00), time!(18:00:00)],
430            days: Some(vec![
431                (Weekday::Friday, Some(WeekdayModifier::First)),
432                (Weekday::Thursday, None),
433            ]),
434            weeks: None,
435        };
436        let result = vec![
437            datetime!(2021-07-02 12:00:00 UTC),
438            datetime!(2021-06-10 12:00:00 UTC),
439            datetime!(2021-06-04 18:00:00 UTC),
440            datetime!(2021-06-10 18:00:00 UTC),
441        ];
442        assert_eq!(compute_dates(base, &spec), result);
443    }
444
445    #[test]
446    fn test_compute_dates_5() {
447        let base = datetime!(2021-06-12 13:38:00 UTC);
448        let spec = ParsedSchedule {
449            times: vec![time!(06:00:00), time!(12:00:00), time!(18:00:00)],
450            days: Some(vec![
451                (Weekday::Friday, Some(WeekdayModifier::First)),
452                (Weekday::Thursday, None),
453                (Weekday::Monday, Some(WeekdayModifier::Third)),
454            ]),
455            weeks: None,
456        };
457        let result = vec![
458            datetime!(2021-06-21 06:00:00 UTC),
459            datetime!(2021-06-17 06:00:00 UTC),
460            datetime!(2021-07-02 06:00:00 UTC),
461            datetime!(2021-06-21 12:00:00 UTC),
462            datetime!(2021-06-17 12:00:00 UTC),
463            datetime!(2021-07-02 12:00:00 UTC),
464            datetime!(2021-06-21 18:00:00 UTC),
465            datetime!(2021-06-17 18:00:00 UTC),
466            datetime!(2021-07-02 18:00:00 UTC),
467        ];
468
469        assert_eq!(compute_dates(base, &spec), result);
470    }
471
472    #[test]
473    fn test_schedule_iteration_1() {
474        let iterator = ScheduleIter {
475            current: datetime!(2021-06-09 13:00:00 UTC),
476            schedule: ParsedSchedule {
477                times: vec![time!(01:00:00)],
478                days: None,
479                weeks: None,
480            },
481            skip_outdated: false,
482            offset: None,
483        };
484
485        let result = vec![
486            Ok(datetime!(2021-06-10 01:00:00 UTC)),
487            Ok(datetime!(2021-06-11 01:00:00 UTC)),
488            Ok(datetime!(2021-06-12 01:00:00 UTC)),
489        ];
490
491        assert_eq!(
492            iterator
493                .take(3)
494                .collect::<Vec<Result<OffsetDateTime, Error>>>(),
495            result
496        );
497    }
498
499    #[test]
500    fn test_schedule_iteration_2() {
501        let iterator = ScheduleIter {
502            current: datetime!(2021-06-09 13:00:00 UTC),
503            schedule: ParsedSchedule {
504                times: vec![time!(13:00:00)],
505                days: Some(vec![(Weekday::Monday, None)]),
506                weeks: None,
507            },
508            skip_outdated: false,
509            offset: None,
510        };
511
512        let result = vec![
513            Ok(datetime!(2021-06-14 13:00:00 UTC)),
514            Ok(datetime!(2021-06-21 13:00:00 UTC)),
515            Ok(datetime!(2021-06-28 13:00:00 UTC)),
516        ];
517
518        assert_eq!(
519            iterator
520                .take(3)
521                .collect::<Vec<Result<OffsetDateTime, Error>>>(),
522            result
523        );
524    }
525
526    #[test]
527    fn test_schedule_iteration_3() {
528        let iterator = ScheduleIter {
529            current: datetime!(2021-06-09 13:00:00 UTC),
530            schedule: ParsedSchedule {
531                times: vec![time!(06:00:00), time!(13:00:00)],
532                days: Some(vec![
533                    (Weekday::Monday, Some(WeekdayModifier::Third)),
534                    (Weekday::Thursday, None),
535                ]),
536                weeks: None,
537            },
538            skip_outdated: false,
539            offset: None,
540        };
541
542        let result = vec![
543            Ok(datetime!(2021-06-10 06:00:00 UTC)),
544            Ok(datetime!(2021-06-10 13:00:00 UTC)),
545            Ok(datetime!(2021-06-17 06:00:00 UTC)),
546            Ok(datetime!(2021-06-17 13:00:00 UTC)),
547            Ok(datetime!(2021-06-21 06:00:00 UTC)),
548            Ok(datetime!(2021-06-21 13:00:00 UTC)),
549            Ok(datetime!(2021-06-24 06:00:00 UTC)),
550            Ok(datetime!(2021-06-24 13:00:00 UTC)),
551        ];
552
553        assert_eq!(
554            iterator
555                .take(8)
556                .collect::<Vec<Result<OffsetDateTime, Error>>>(),
557            result
558        );
559    }
560
561    #[test]
562    fn test_schedule_iteration_4() {
563        let iterator = ScheduleIter {
564            current: datetime!(2021-06-09 13:00:00 UTC),
565            schedule: ParsedSchedule {
566                times: vec![time!(06:00:00), time!(13:00:00)],
567                days: Some(vec![
568                    (Weekday::Monday, Some(WeekdayModifier::Third)),
569                    (Weekday::Thursday, None),
570                ]),
571                weeks: None,
572            },
573            skip_outdated: false,
574            offset: Some(offset!(+3)),
575        };
576
577        let result = vec![
578            Ok(datetime!(2021-06-10 06:00:00 +3)),
579            Ok(datetime!(2021-06-10 13:00:00 +3)),
580            Ok(datetime!(2021-06-17 06:00:00 +3)),
581            Ok(datetime!(2021-06-17 13:00:00 +3)),
582            Ok(datetime!(2021-06-21 06:00:00 +3)),
583            Ok(datetime!(2021-06-21 13:00:00 +3)),
584            Ok(datetime!(2021-06-24 06:00:00 +3)),
585            Ok(datetime!(2021-06-24 13:00:00 +3)),
586        ];
587
588        assert_eq!(
589            iterator
590                .take(8)
591                .collect::<Vec<Result<OffsetDateTime, Error>>>(),
592            result
593        );
594    }
595
596    #[test]
597    fn test_schedule_iteration_5() {
598        let iterator = MultiScheduleIter {
599            current: datetime!(2021-06-09 13:00:00 UTC),
600            schedules: &vec![
601                ParsedSchedule {
602                    times: vec![time!(06:00:00), time!(13:00:00)],
603                    days: Some(vec![
604                        (Weekday::Monday, Some(WeekdayModifier::Third)),
605                        (Weekday::Thursday, None),
606                    ]),
607                    weeks: None,
608                },
609                ParsedSchedule {
610                    times: vec![time!(18:00:00)],
611                    days: Some(vec![(Weekday::Saturday, Some(WeekdayModifier::Fourth))]),
612                    weeks: Some(WeekVariant::Odd),
613                },
614            ],
615            skip_outdated: false,
616            offset: None,
617        };
618
619        let result = vec![
620            Ok(datetime!(2021-06-10 06:00:00 UTC)),
621            Ok(datetime!(2021-06-10 13:00:00 UTC)),
622            Ok(datetime!(2021-06-17 06:00:00 UTC)),
623            Ok(datetime!(2021-06-17 13:00:00 UTC)),
624            Ok(datetime!(2021-06-21 06:00:00 UTC)),
625            Ok(datetime!(2021-06-21 13:00:00 UTC)),
626            Ok(datetime!(2021-06-24 06:00:00 UTC)),
627            Ok(datetime!(2021-06-24 13:00:00 UTC)),
628            Ok(datetime!(2021-06-26 18:00:00 UTC)),
629            Ok(datetime!(2021-07-01 06:00:00 UTC)),
630            Ok(datetime!(2021-07-01 13:00:00 UTC)),
631        ];
632
633        assert_eq!(
634            iterator
635                .take(11)
636                .collect::<Vec<Result<OffsetDateTime, Error>>>(),
637            result
638        );
639    }
640
641    #[test]
642    fn test_schedule_iteration_6() {
643        let iterator = MultiScheduleIter {
644            current: datetime!(2021-06-18 13:00:00 UTC),
645            schedules: &vec![
646                ParsedSchedule {
647                    times: vec![time!(06:00:00), time!(18:00:00)],
648                    days: Some(vec![
649                        (Weekday::Monday, Some(WeekdayModifier::Last)),
650                        (Weekday::Thursday, None),
651                    ]),
652                    weeks: None,
653                },
654                ParsedSchedule {
655                    times: vec![time!(18:00:00)],
656                    days: Some(vec![(Weekday::Saturday, Some(WeekdayModifier::Fourth))]),
657                    weeks: None,
658                },
659            ],
660            skip_outdated: false,
661            offset: None,
662        };
663
664        let result = vec![
665            Ok(datetime!(2021-06-24 06:00:00 UTC)),
666            Ok(datetime!(2021-06-24 18:00:00 UTC)),
667            Ok(datetime!(2021-06-26 18:00:00 UTC)),
668            Ok(datetime!(2021-06-28 06:00:00 UTC)),
669            Ok(datetime!(2021-06-28 18:00:00 UTC)),
670            Ok(datetime!(2021-07-01 06:00:00 UTC)),
671            Ok(datetime!(2021-07-01 18:00:00 UTC)),
672            Ok(datetime!(2021-07-08 06:00:00 UTC)),
673            Ok(datetime!(2021-07-08 18:00:00 UTC)),
674            Ok(datetime!(2021-07-15 06:00:00 UTC)),
675            Ok(datetime!(2021-07-15 18:00:00 UTC)),
676            Ok(datetime!(2021-07-22 06:00:00 UTC)),
677            Ok(datetime!(2021-07-22 18:00:00 UTC)),
678            Ok(datetime!(2021-07-24 18:00:00 UTC)),
679            Ok(datetime!(2021-07-26 06:00:00 UTC)),
680            Ok(datetime!(2021-07-26 18:00:00 UTC)),
681            Ok(datetime!(2021-07-29 06:00:00 UTC)),
682            Ok(datetime!(2021-07-29 18:00:00 UTC)),
683        ];
684
685        assert_eq!(
686            iterator
687                .take(18)
688                .collect::<Vec<Result<OffsetDateTime, Error>>>(),
689            result
690        );
691    }
692
693    #[test]
694    fn test_schedule_iteration_7() {
695        let iterator = MultiScheduleIter {
696            current: datetime!(2021-06-18 13:00:00 UTC),
697            schedules: &vec![
698                ParsedSchedule {
699                    times: vec![time!(06:00:00), time!(18:00:00)],
700                    days: Some(vec![
701                        (Weekday::Monday, Some(WeekdayModifier::Last)),
702                        (Weekday::Thursday, None),
703                    ]),
704                    weeks: None,
705                },
706                ParsedSchedule {
707                    times: vec![time!(18:00:00)],
708                    days: Some(vec![(Weekday::Saturday, Some(WeekdayModifier::Fourth))]),
709                    weeks: None,
710                },
711            ],
712            skip_outdated: false,
713            offset: Some(offset!(+2:30)),
714        };
715
716        let result = vec![
717            Ok(datetime!(2021-06-24 06:00:00 +2:30)),
718            Ok(datetime!(2021-06-24 18:00:00 +2:30)),
719            Ok(datetime!(2021-06-26 18:00:00 +2:30)),
720            Ok(datetime!(2021-06-28 06:00:00 +2:30)),
721            Ok(datetime!(2021-06-28 18:00:00 +2:30)),
722            Ok(datetime!(2021-07-01 06:00:00 +2:30)),
723            Ok(datetime!(2021-07-01 18:00:00 +2:30)),
724        ];
725
726        assert_eq!(
727            iterator
728                .take(7)
729                .collect::<Vec<Result<OffsetDateTime, Error>>>(),
730            result
731        );
732    }
733
734    #[test]
735    fn test_add_two_schedules() {
736        let sched1 = Schedule(ParsedSchedule {
737            times: vec![time!(06:00:00), time!(13:00:00)],
738            days: Some(vec![
739                (Weekday::Monday, Some(WeekdayModifier::Third)),
740                (Weekday::Thursday, None),
741            ]),
742            weeks: None,
743        });
744
745        let sched2 = Schedule(ParsedSchedule {
746            times: vec![time!(18:00:00)],
747            days: Some(vec![(Weekday::Saturday, Some(WeekdayModifier::Fourth))]),
748            weeks: Some(WeekVariant::Odd),
749        });
750
751        let multi_sched = MultiSchedule(vec![
752            ParsedSchedule {
753                times: vec![time!(06:00:00), time!(13:00:00)],
754                days: Some(vec![
755                    (Weekday::Monday, Some(WeekdayModifier::Third)),
756                    (Weekday::Thursday, None),
757                ]),
758                weeks: None,
759            },
760            ParsedSchedule {
761                times: vec![time!(18:00:00)],
762                days: Some(vec![(Weekday::Saturday, Some(WeekdayModifier::Fourth))]),
763                weeks: Some(WeekVariant::Odd),
764            },
765        ]);
766
767        assert_eq!(multi_sched, sched1 + sched2)
768    }
769}