1
2use chrono::offset::TimeZone;
3use chrono::{DateTime, Datelike, Timelike};
4use std::ops::Bound::{Included, Unbounded};
5use std::fmt::{Display, Formatter, Result as FmtResult};
6
7use crate::time_unit::*;
8use crate::ordinal::*;
9use crate::queries::*;
10
11impl From<Schedule> for String {
12 fn from(schedule: Schedule) -> String {
13 schedule.source
14 }
15}
16
17#[derive(Clone, Debug, Eq)]
18pub struct Schedule {
19 source: String,
20 fields: ScheduleFields,
21}
22
23impl Schedule {
24 pub(crate) fn new(
25 source: String,
26 fields: ScheduleFields,
27 ) -> Schedule {
28 Schedule {
29 source,
30 fields,
31 }
32 }
33
34 fn next_after<Z>(&self, after: &DateTime<Z>) -> Option<DateTime<Z>>
35 where
36 Z: TimeZone,
37 {
38 let mut query = NextAfterQuery::from(after);
39 for year in self
40 .fields
41 .years
42 .ordinals()
43 .range((Included(query.year_lower_bound()), Unbounded))
44 .cloned()
45 {
46 let month_start = query.month_lower_bound();
47 if !self.fields.months.ordinals().contains(&month_start) {
48 query.reset_month();
49 }
50 let month_range = (Included(month_start), Included(Months::inclusive_max()));
51 for month in self.fields.months.ordinals().range(month_range).cloned() {
52 let day_of_month_start = query.day_of_month_lower_bound();
53 if !self.fields.days_of_month.ordinals().contains(&day_of_month_start) {
54 query.reset_day_of_month();
55 }
56 let day_of_month_end = days_in_month(month, year);
57 let day_of_month_range = (Included(day_of_month_start), Included(day_of_month_end));
58
59 'day_loop: for day_of_month in self
60 .fields
61 .days_of_month
62 .ordinals()
63 .range(day_of_month_range)
64 .cloned()
65 {
66 let hour_start = query.hour_lower_bound();
67 if !self.fields.hours.ordinals().contains(&hour_start) {
68 query.reset_hour();
69 }
70 let hour_range = (Included(hour_start), Included(Hours::inclusive_max()));
71
72 for hour in self.fields.hours.ordinals().range(hour_range).cloned() {
73 let minute_start = query.minute_lower_bound();
74 if !self.fields.minutes.ordinals().contains(&minute_start) {
75 query.reset_minute();
76 }
77 let minute_range =
78 (Included(minute_start), Included(Minutes::inclusive_max()));
79
80 for minute in self.fields.minutes.ordinals().range(minute_range).cloned() {
81 let second_start = query.second_lower_bound();
82 if !self.fields.seconds.ordinals().contains(&second_start) {
83 query.reset_second();
84 }
85 let second_range =
86 (Included(second_start), Included(Seconds::inclusive_max()));
87
88 for second in self.fields.seconds.ordinals().range(second_range).cloned() {
89 let timezone = after.timezone();
90 let candidate = if let Some(candidate) = timezone
91 .ymd(year as i32, month, day_of_month)
92 .and_hms_opt(hour, minute, second)
93 {
94 candidate
95 } else {
96 continue;
97 };
98 if !self
99 .fields
100 .days_of_week
101 .ordinals()
102 .contains(&candidate.weekday().number_from_sunday())
103 {
104 continue 'day_loop;
105 }
106 return Some(candidate);
107 }
108 query.reset_minute();
109 } query.reset_hour();
111 } query.reset_day_of_month();
113 } query.reset_month();
115 } }
117
118 None
120 }
121
122 fn prev_from<Z>(&self, before: &DateTime<Z>) -> Option<DateTime<Z>>
123 where
124 Z: TimeZone,
125 {
126 let mut query = PrevFromQuery::from(before);
127 for year in self
128 .fields
129 .years
130 .ordinals()
131 .range((Unbounded, Included(query.year_upper_bound())))
132 .rev()
133 .cloned()
134 {
135 let month_start = query.month_upper_bound();
136
137 if !self.fields.months.ordinals().contains(&month_start) {
138 query.reset_month();
139 }
140 let month_range = (Included(Months::inclusive_min()), Included(month_start));
141
142 for month in self.fields.months.ordinals().range(month_range).rev().cloned() {
143 let day_of_month_end = query.day_of_month_upper_bound();
144 if !self.fields.days_of_month.ordinals().contains(&day_of_month_end) {
145 query.reset_day_of_month();
146 }
147
148 let day_of_month_end = days_in_month(month, year).min(day_of_month_end);
149
150 let day_of_month_range = (
151 Included(DaysOfMonth::inclusive_min()),
152 Included(day_of_month_end),
153 );
154
155 'day_loop: for day_of_month in self
156 .fields
157 .days_of_month
158 .ordinals()
159 .range(day_of_month_range)
160 .rev()
161 .cloned()
162 {
163 let hour_start = query.hour_upper_bound();
164 if !self.fields.hours.ordinals().contains(&hour_start) {
165 query.reset_hour();
166 }
167 let hour_range = (Included(Hours::inclusive_min()), Included(hour_start));
168
169 for hour in self.fields.hours.ordinals().range(hour_range).rev().cloned() {
170 let minute_start = query.minute_upper_bound();
171 if !self.fields.minutes.ordinals().contains(&minute_start) {
172 query.reset_minute();
173 }
174 let minute_range =
175 (Included(Minutes::inclusive_min()), Included(minute_start));
176
177 for minute in self.fields.minutes.ordinals().range(minute_range).rev().cloned() {
178 let second_start = query.second_upper_bound();
179 if !self.fields.seconds.ordinals().contains(&second_start) {
180 query.reset_second();
181 }
182 let second_range =
183 (Included(Seconds::inclusive_min()), Included(second_start));
184
185 for second in self.fields.seconds.ordinals().range(second_range).rev().cloned()
186 {
187 let timezone = before.timezone();
188 let candidate = if let Some(candidate) = timezone
189 .ymd(year as i32, month, day_of_month)
190 .and_hms_opt(hour, minute, second)
191 {
192 candidate
193 } else {
194 continue;
195 };
196 if !self
197 .fields
198 .days_of_week
199 .ordinals()
200 .contains(&candidate.weekday().number_from_sunday())
201 {
202 continue 'day_loop;
203 }
204 return Some(candidate);
205 }
206 query.reset_minute();
207 } query.reset_hour();
209 } query.reset_day_of_month();
211 } query.reset_month();
213 } }
215
216 None
218 }
219
220 pub fn after<Z>(&self, after: &DateTime<Z>) -> ScheduleIterator<'_, Z>
231 where
232 Z: TimeZone,
233 {
234 ScheduleIterator::new(self, after)
235 }
236
237 pub fn includes<Z>(&self, date_time: DateTime<Z>) -> bool
238 where
239 Z: TimeZone,
240 {
241 self.fields.years.includes(date_time.year() as Ordinal) &&
242 self.fields.months.includes(date_time.month() as Ordinal) &&
243 self.fields.days_of_week.includes(date_time.weekday().number_from_sunday()) &&
244 self.fields.days_of_month.includes(date_time.day() as Ordinal) &&
245 self.fields.hours.includes(date_time.hour() as Ordinal) &&
246 self.fields.minutes.includes(date_time.minute() as Ordinal) &&
247 self.fields.seconds.includes(date_time.second() as Ordinal)
248 }
249
250 pub fn years(&self) -> &impl TimeUnitSpec {
253 &self.fields.years
254 }
255
256 pub fn months(&self) -> &impl TimeUnitSpec {
259 &self.fields.months
260 }
261
262 pub fn days_of_month(&self) -> &impl TimeUnitSpec {
265 &self.fields.days_of_month
266 }
267
268 pub fn days_of_week(&self) -> &impl TimeUnitSpec {
271 &self.fields.days_of_week
272 }
273
274 pub fn hours(&self) -> &impl TimeUnitSpec {
277 &self.fields.hours
278 }
279
280 pub fn minutes(&self) -> &impl TimeUnitSpec {
283 &self.fields.minutes
284 }
285
286 pub fn seconds(&self) -> &impl TimeUnitSpec {
289 &self.fields.seconds
290 }
291
292 pub fn timeunitspec_eq(&self, other: &Schedule) -> bool {
293 self.fields == other.fields
294 }
295}
296
297impl Display for Schedule {
298 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
299 write!(f, "{}", self.source)
300 }
301}
302
303impl PartialEq for Schedule {
304 fn eq(&self, other: &Schedule) -> bool {
305 self.source == other.source
306 }
307}
308
309#[derive(Clone, Debug, PartialEq, Eq)]
310pub struct ScheduleFields {
311 years: Years,
312 days_of_week: DaysOfWeek,
313 months: Months,
314 days_of_month: DaysOfMonth,
315 hours: Hours,
316 minutes: Minutes,
317 seconds: Seconds,
318}
319
320impl ScheduleFields {
321 pub(crate) fn new(
322 seconds: Seconds,
323 minutes: Minutes,
324 hours: Hours,
325 days_of_month: DaysOfMonth,
326 months: Months,
327 days_of_week: DaysOfWeek,
328 years: Years,
329 ) -> ScheduleFields {
330 ScheduleFields {
331 years,
332 days_of_week,
333 months,
334 days_of_month,
335 hours,
336 minutes,
337 seconds,
338 }
339 }
340}
341
342pub struct ScheduleIterator<'a, Z>
343where
344 Z: TimeZone,
345{
346 is_done: bool,
347 schedule: &'a Schedule,
348 previous_datetime: DateTime<Z>,
349}
350impl<'a, Z> ScheduleIterator<'a, Z>
353where
354 Z: TimeZone,
355{
356 fn new(schedule: &'a Schedule, starting_datetime: &DateTime<Z>) -> ScheduleIterator<'a, Z> {
357 ScheduleIterator {
358 is_done: false,
359 schedule,
360 previous_datetime: starting_datetime.clone(),
361 }
362 }
363}
364
365impl<'a, Z> Iterator for ScheduleIterator<'a, Z>
366where
367 Z: TimeZone,
368{
369 type Item = DateTime<Z>;
370
371 fn next(&mut self) -> Option<DateTime<Z>> {
372 if self.is_done {
373 return None;
374 }
375 if let Some(next_datetime) = self.schedule.next_after(&self.previous_datetime) {
376 self.previous_datetime = next_datetime.clone();
377 Some(next_datetime)
378 } else {
379 self.is_done = true;
380 None
381 }
382 }
383}
384
385impl<'a, Z> DoubleEndedIterator for ScheduleIterator<'a, Z>
386where
387 Z: TimeZone,
388{
389 fn next_back(&mut self) -> Option<Self::Item> {
390 if self.is_done {
391 return None;
392 }
393
394 if let Some(prev_datetime) = self.schedule.prev_from(&self.previous_datetime) {
395 self.previous_datetime = prev_datetime.clone();
396 Some(prev_datetime)
397 } else {
398 self.is_done = true;
399 None
400 }
401 }
402}
403
404fn is_leap_year(year: Ordinal) -> bool {
405 let by_four = year % 4 == 0;
406 let by_hundred = year % 100 == 0;
407 let by_four_hundred = year % 400 == 0;
408 by_four && ((!by_hundred) || by_four_hundred)
409}
410
411fn days_in_month(month: Ordinal, year: Ordinal) -> u32 {
412 let is_leap_year = is_leap_year(year);
413 match month {
414 9 | 4 | 6 | 11 => 30,
415 2 if is_leap_year => 29,
416 2 => 28,
417 _ => 31,
418 }
419}
420
421#[cfg(test)]
422mod test {
423 use super::*;
424 use std::str::{FromStr};
425
426 #[test]
513 fn test_schedule_to_string() {
514 let expression = "* 1,2,3 * * * *";
515 let schedule: Schedule = Schedule::from_str(expression).unwrap();
516 let result = String::from(schedule);
517 assert_eq!(expression, result);
518 }
519
520 #[test]
521 fn test_display_schedule() {
522 use std::fmt::Write;
523 let expression = "@monthly";
524 let schedule = Schedule::from_str(expression).unwrap();
525 let mut result = String::new();
526 write!(result, "{}", schedule).unwrap();
527 assert_eq!(expression, result);
528 }
529
530 #[test]
531 fn test_valid_from_str() {
532 let schedule = Schedule::from_str("0 0,30 0,6,12,18 1,15 Jan-March Thurs");
533 schedule.unwrap();
534 }
535
536 #[test]
537 fn test_invalid_from_str() {
538 let schedule = Schedule::from_str("cheesecake 0,30 0,6,12,18 1,15 Jan-March Thurs");
539 assert!(schedule.is_err());
540 }
541
542 #[test]
543 fn test_no_panic_on_nonexistent_time_after() {
544 use chrono::offset::TimeZone;
545 use chrono_tz::Tz;
546
547 let schedule_tz: Tz = "Europe/London".parse().unwrap();
548 let dt = schedule_tz
549 .ymd(2019, 10, 27)
550 .and_hms(0, 3, 29)
551 .checked_add_signed(chrono::Duration::hours(1)) .unwrap();
553 let schedule = Schedule::from_str("* * * * * Sat,Sun *").unwrap();
554 let next = schedule.after(&dt).next().unwrap();
555 assert!(next > dt); }
557
558 #[test]
559 fn test_no_panic_on_nonexistent_time_before() {
560 use chrono::offset::TimeZone;
561 use chrono_tz::Tz;
562
563 let schedule_tz: Tz = "Europe/London".parse().unwrap();
564 let dt = schedule_tz
565 .ymd(2019, 10, 27)
566 .and_hms(0, 3, 29)
567 .checked_add_signed(chrono::Duration::hours(1)) .unwrap();
569 let schedule = Schedule::from_str("* * * * * Sat,Sun *").unwrap();
570 let prev = schedule.after(&dt).rev().next().unwrap();
571 assert!(prev < dt); }
573
574 #[test]
575 fn test_time_unit_spec_equality() {
576 let schedule_1 = Schedule::from_str("@weekly").unwrap();
577 let schedule_2 = Schedule::from_str("0 0 0 * * 1 *").unwrap();
578 let schedule_3 = Schedule::from_str("0 0 0 * * 1-7 *").unwrap();
579 let schedule_4 = Schedule::from_str("0 0 0 * * * *").unwrap();
580 assert_ne!(schedule_1, schedule_2);
581 assert!(schedule_1.timeunitspec_eq(&schedule_2));
582 assert!(schedule_3.timeunitspec_eq(&schedule_4));
583 }
584}