1pub mod errors;
91pub mod parser;
92pub mod describe;
93
94mod component;
95mod iterator;
96mod pattern;
97
98#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
100pub enum Direction {
101 Forward,
102 Backward,
103}
104#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug)]
105pub enum TimeComponent {
106 Second = 1,
107 Minute,
108 Hour,
109 Day,
110 Month,
111 Year
112}
113
114#[derive(Debug, PartialEq, Eq)]
117pub enum JobType {
118 FixedTime,
119 IntervalWildcard,
120}
121
122use errors::CronError;
123pub use iterator::CronIterator;
124use parser::CronParser;
125use pattern::CronPattern;
126use std::str::FromStr;
127
128use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, TimeZone, Timelike};
129
130#[cfg(feature = "serde")]
131use core::fmt;
132#[cfg(feature = "serde")]
133use serde::{
134 de::{self, Visitor},
135 Deserialize, Serialize, Serializer,
136};
137
138pub const YEAR_UPPER_LIMIT: i32 = 5000;
142
143pub const YEAR_LOWER_LIMIT: i32 = 1;
148
149#[derive(Debug, Clone, PartialEq, PartialOrd, Hash)]
152pub struct Cron {
153 pub pattern: CronPattern, }
155
156impl FromStr for Cron {
157 type Err = CronError;
158
159 fn from_str(s: &str) -> Result<Self, Self::Err> {
160 CronParser::new().parse(s)
161 }
162}
163
164impl Cron {
165 pub fn is_time_matching<Tz: TimeZone>(&self, time: &DateTime<Tz>) -> Result<bool, CronError> {
212 let naive_time = time.naive_local();
213 Ok(self.pattern.second_match(naive_time.second())?
214 && self.pattern.minute_match(naive_time.minute())?
215 && self.pattern.hour_match(naive_time.hour())?
216 && self
217 .pattern
218 .day_match(naive_time.year(), naive_time.month(), naive_time.day())?
219 && self.pattern.month_match(naive_time.month())?
220 && self.pattern.year_match(naive_time.year())?) }
222
223 pub fn find_next_occurrence<Tz: TimeZone>(
273 &self,
274 start_time: &DateTime<Tz>,
275 inclusive: bool,
276 ) -> Result<DateTime<Tz>, CronError> {
277 self.find_occurrence(start_time, inclusive, Direction::Forward)
278 .map(|(dt, _)| dt)
279 }
280
281 pub fn find_previous_occurrence<Tz: TimeZone>(
283 &self,
284 start_time: &DateTime<Tz>,
285 inclusive: bool,
286 ) -> Result<DateTime<Tz>, CronError> {
287 self.find_occurrence(start_time, inclusive, Direction::Backward)
288 .map(|(dt, _)| dt) }
290
291 fn find_occurrence<Tz: TimeZone>(
294 &self,
295 start_time: &DateTime<Tz>,
296 inclusive: bool,
297 direction: Direction,
298 ) -> Result<(DateTime<Tz>, Option<DateTime<Tz>>), CronError> {
299 let mut naive_time = start_time.naive_local();
300 let timezone = start_time.timezone();
301 let job_type = self.determine_job_type();
302
303 let initial_adjusted_naive_time = if !inclusive {
304 let adjustment = match direction {
305 Direction::Forward => Duration::seconds(1),
306 Direction::Backward => Duration::seconds(-1),
307 };
308 naive_time
309 .checked_add_signed(adjustment)
310 .ok_or(CronError::InvalidTime)?
311 } else {
312 naive_time
313 };
314
315 naive_time = initial_adjusted_naive_time;
316
317 let mut iterations = 0;
318 const MAX_SEARCH_ITERATIONS: u32 = 366 * 24 * 60 * 60;
319
320 loop {
321 iterations += 1;
322 if iterations > MAX_SEARCH_ITERATIONS {
323 return Err(CronError::TimeSearchLimitExceeded);
324 }
325
326 let mut changed_component_in_this_pass = false;
327
328 changed_component_in_this_pass |= self.find_matching_date_component(&mut naive_time, direction, TimeComponent::Year)?;
329 if !changed_component_in_this_pass {
330 changed_component_in_this_pass |= self.find_matching_date_component(&mut naive_time, direction, TimeComponent::Month)?;
331 }
332 if !changed_component_in_this_pass {
333 changed_component_in_this_pass |= self.find_matching_date_component(&mut naive_time, direction, TimeComponent::Day)?;
334 }
335
336 if changed_component_in_this_pass {
337 match direction {
338 Direction::Forward => naive_time = naive_time.with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap(),
339 Direction::Backward => naive_time = naive_time.with_hour(23).unwrap().with_minute(59).unwrap().with_second(59).unwrap(),
340 }
341 }
342
343 let mut time_component_adjusted_in_this_pass = false;
344 time_component_adjusted_in_this_pass |= self.find_matching_granular_component(&mut naive_time, direction, TimeComponent::Hour)?;
345 if !time_component_adjusted_in_this_pass {
346 time_component_adjusted_in_this_pass |= self.find_matching_granular_component(&mut naive_time, direction, TimeComponent::Minute)?;
347 }
348 if !time_component_adjusted_in_this_pass {
349 self.find_matching_granular_component(&mut naive_time, direction, TimeComponent::Second)?;
350 }
351
352 match from_naive(naive_time, &timezone) {
353 chrono::LocalResult::Single(dt) => {
354 if self.is_time_matching(&dt)? {
355 return Ok((dt, None)); }
357 naive_time = naive_time.checked_add_signed(match direction {
358 Direction::Forward => Duration::seconds(1),
359 Direction::Backward => Duration::seconds(-1),
360 }).ok_or(CronError::InvalidTime)?;
361 }
362 chrono::LocalResult::Ambiguous(_dt1, _dt2) => {
363 let first_occurrence_dt = timezone.from_local_datetime(&naive_time).earliest().unwrap();
365 let second_occurrence_dt = timezone.from_local_datetime(&naive_time).latest().unwrap();
366
367 if job_type == JobType::FixedTime {
368 if self.is_time_matching(&first_occurrence_dt)? {
370 return Ok((first_occurrence_dt, None)); }
372 naive_time = naive_time.checked_add_signed(match direction {
376 Direction::Forward => Duration::seconds(1),
377 Direction::Backward => Duration::seconds(-1),
378 }).ok_or(CronError::InvalidTime)?;
379
380 } else { let mut primary_match = None;
383 let mut secondary_match = None;
384
385 if self.is_time_matching(&first_occurrence_dt)? {
386 primary_match = Some(first_occurrence_dt);
387 }
388 if self.is_time_matching(&second_occurrence_dt)? {
389 secondary_match = Some(second_occurrence_dt);
390 }
391
392 if let Some(p_match) = primary_match {
393 return Ok((p_match, secondary_match)); } else if let Some(s_match) = secondary_match {
395 return Ok((s_match, None)); }
398 naive_time = naive_time.checked_add_signed(match direction {
400 Direction::Forward => Duration::seconds(1),
401 Direction::Backward => Duration::seconds(-1),
402 }).ok_or(CronError::InvalidTime)?;
403 }
404 }
405 chrono::LocalResult::None => {
406 if job_type == JobType::FixedTime {
408 let mut temp_naive = naive_time;
412 let mut gap_adjust_count = 0;
413 const MAX_GAP_SEARCH_SECONDS: u32 = 3600 * 2; let resolved_dt_after_gap: DateTime<Tz>;
416
417 loop {
418 temp_naive = temp_naive.checked_add_signed(match direction {
419 Direction::Forward => Duration::seconds(1),
420 Direction::Backward => Duration::seconds(-1),
421 }).ok_or(CronError::InvalidTime)?;
422 gap_adjust_count += 1;
423
424 let local_result = from_naive(temp_naive, &timezone);
426
427 if let chrono::LocalResult::Single(dt) = local_result {
428 resolved_dt_after_gap = dt;
429 break;
430 } else if let chrono::LocalResult::Ambiguous(dt1, _) = local_result {
431 resolved_dt_after_gap = dt1;
433 break;
434 }
435 if gap_adjust_count > MAX_GAP_SEARCH_SECONDS {
437 return Err(CronError::TimeSearchLimitExceeded);
438 }
439 }
440
441 if self.pattern.day_match(resolved_dt_after_gap.year(), resolved_dt_after_gap.month(), resolved_dt_after_gap.day())? &&
446 self.pattern.month_match(resolved_dt_after_gap.month())? &&
447 self.pattern.year_match(resolved_dt_after_gap.year())? {
448 return Ok((resolved_dt_after_gap, None));
450 } else {
451 naive_time = temp_naive;
456 continue;
457 }
458 } else { naive_time = naive_time.checked_add_signed(match direction {
461 Direction::Forward => Duration::seconds(1),
462 Direction::Backward => Duration::seconds(-1),
463 }).ok_or(CronError::InvalidTime)?;
464 }
465 }
466 }
467 }
468 }
469
470 pub fn iter_from<Tz: TimeZone>(
483 &self,
484 start_from: DateTime<Tz>,
485 direction: Direction,
486 ) -> CronIterator<Tz> {
487 CronIterator::new(self.clone(), start_from, true, direction)
488 }
489
490 pub fn iter_after<Tz: TimeZone>(&self, start_after: DateTime<Tz>) -> CronIterator<Tz> {
500 CronIterator::new(self.clone(), start_after, false, Direction::Forward)
501 }
502
503 pub fn iter_before<Tz: TimeZone>(&self, start_before: DateTime<Tz>) -> CronIterator<Tz> {
513 CronIterator::new(self.clone(), start_before, false, Direction::Backward)
514 }
515
516 pub fn describe(&self) -> String {
531 self.pattern.describe()
532 }
533
534 pub fn describe_lang<L: crate::describe::Language>(&self, lang: L) -> String {
540 self.pattern.describe_lang(lang)
541 }
542
543 pub fn determine_job_type(&self) -> JobType {
547 let is_seconds_fixed = self.pattern.seconds.step == 1
548 && !self.pattern.seconds.from_wildcard
549 && self.pattern.seconds.get_set_values(component::ALL_BIT).len() == 1;
550 let is_minutes_fixed = self.pattern.minutes.step == 1
551 && !self.pattern.minutes.from_wildcard
552 && self.pattern.minutes.get_set_values(component::ALL_BIT).len() == 1;
553 let is_hours_fixed = self.pattern.hours.step == 1
554 && !self.pattern.hours.from_wildcard
555 && self.pattern.hours.get_set_values(component::ALL_BIT).len() == 1;
556
557 if is_seconds_fixed && is_minutes_fixed && is_hours_fixed {
558 JobType::FixedTime
559 } else {
560 JobType::IntervalWildcard
561 }
562 }
563
564 fn set_time_component(
568 current_time: &mut NaiveDateTime,
569 component: TimeComponent,
570 value: u32,
571 direction: Direction,
572 ) -> Result<(), CronError> {
573 let mut new_time = *current_time;
574
575 new_time = match component {
576 TimeComponent::Second => new_time.with_second(value).ok_or(CronError::InvalidTime)?,
577 TimeComponent::Minute => new_time.with_minute(value).ok_or(CronError::InvalidTime)?,
578 TimeComponent::Hour => new_time.with_hour(value).ok_or(CronError::InvalidTime)?,
579 _ => return Err(CronError::InvalidTime),
580 };
581
582 match direction {
583 Direction::Forward => {
584 if component >= TimeComponent::Hour {
585 new_time = new_time.with_minute(0).unwrap();
586 }
587 if component >= TimeComponent::Minute {
588 new_time = new_time.with_second(0).unwrap();
589 }
590 }
591 Direction::Backward => {
592 if component >= TimeComponent::Hour {
593 new_time = new_time.with_minute(59).unwrap();
594 }
595 if component >= TimeComponent::Minute {
596 new_time = new_time.with_second(59).unwrap();
597 }
598 }
599 }
600
601 *current_time = new_time;
602 Ok(())
603 }
604
605 fn adjust_time_component(
607 current_time: &mut NaiveDateTime,
608 component: TimeComponent,
609 direction: Direction,
610 ) -> Result<(), CronError> {
611 match direction {
613 Direction::Forward => {
614 if current_time.year() >= YEAR_UPPER_LIMIT {
615 return Err(CronError::TimeSearchLimitExceeded);
616 }
617 }
618 Direction::Backward => {
619 if current_time.year() <= YEAR_LOWER_LIMIT {
620 return Err(CronError::TimeSearchLimitExceeded);
621 }
622 }
623 }
624 match direction {
625 Direction::Forward => {
626 let duration = match component {
627 TimeComponent::Year => {
628 let next_year = current_time.year() + 1;
629 *current_time = NaiveDate::from_ymd_opt(next_year, 1, 1)
630 .ok_or(CronError::InvalidDate)?
631 .and_hms_opt(0, 0, 0)
632 .ok_or(CronError::InvalidTime)?;
633 return Ok(());
634 }
635 TimeComponent::Minute => Duration::minutes(1),
636 TimeComponent::Hour => Duration::hours(1),
637 TimeComponent::Day => Duration::days(1),
638 TimeComponent::Month => {
639 let mut year = current_time.year();
640 let mut month = current_time.month() + 1;
641 if month > 12 {
642 year += 1;
643 month = 1;
644 }
645 *current_time = NaiveDate::from_ymd_opt(year, month, 1)
646 .ok_or(CronError::InvalidDate)?
647 .and_hms_opt(0, 0, 0)
648 .ok_or(CronError::InvalidTime)?;
649 return Ok(());
650 }
651 _ => return Err(CronError::InvalidTime),
652 };
653 *current_time = current_time
654 .checked_add_signed(duration)
655 .ok_or(CronError::InvalidTime)?;
656 if component >= TimeComponent::Day {
657 *current_time = current_time.with_hour(0).unwrap();
658 }
659 if component >= TimeComponent::Hour {
660 *current_time = current_time.with_minute(0).unwrap();
661 }
662 if component >= TimeComponent::Minute {
663 *current_time = current_time.with_second(0).unwrap();
664 }
665 }
666 Direction::Backward => {
667 let duration = match component {
668 TimeComponent::Year => { let prev_year = current_time.year() - 1;
670 *current_time = NaiveDate::from_ymd_opt(prev_year, 12, 31)
671 .ok_or(CronError::InvalidDate)?
672 .and_hms_opt(23, 59, 59)
673 .ok_or(CronError::InvalidTime)?;
674 return Ok(());
675 }
676 TimeComponent::Minute => Duration::minutes(1),
677 TimeComponent::Hour => Duration::hours(1),
678 TimeComponent::Day => Duration::days(1),
679 TimeComponent::Month => {
680 let next_month_first_day =
681 NaiveDate::from_ymd_opt(current_time.year(), current_time.month(), 1)
682 .ok_or(CronError::InvalidDate)?;
683 *current_time = (next_month_first_day - Duration::days(1))
684 .and_hms_opt(23, 59, 59)
685 .ok_or(CronError::InvalidTime)?;
686 return Ok(());
687 }
688 _ => return Err(CronError::InvalidTime),
689 };
690 *current_time = current_time
691 .checked_sub_signed(duration)
692 .ok_or(CronError::InvalidTime)?;
693 if component >= TimeComponent::Day {
694 *current_time = current_time.with_hour(23).unwrap();
695 }
696 if component >= TimeComponent::Hour {
697 *current_time = current_time.with_minute(59).unwrap();
698 }
699 if component >= TimeComponent::Minute {
700 *current_time = current_time.with_second(59).unwrap();
701 }
702 }
703 }
704 Ok(())
705 }
706 fn find_matching_date_component(
707 &self,
708 current_time: &mut NaiveDateTime,
709 direction: Direction,
710 component: TimeComponent,
711 ) -> Result<bool, CronError> {
712 let mut changed = false;
713 while !(match component {
715 TimeComponent::Year => self.pattern.year_match(current_time.year()), TimeComponent::Month => self.pattern.month_match(current_time.month()),
717 TimeComponent::Day => self.pattern.day_match(
718 current_time.year(),
719 current_time.month(),
720 current_time.day(),
721 ),
722 _ => Ok(true), })? {
724 Self::adjust_time_component(current_time, component, direction)?;
725 changed = true;
726 }
727 Ok(changed)
728 }
729
730 fn find_matching_granular_component(
732 &self,
733 current_time: &mut NaiveDateTime,
734 direction: Direction,
735 component: TimeComponent,
736 ) -> Result<bool, CronError> {
737 let mut changed = false;
738 let (current_value, next_larger_component) = match component {
739 TimeComponent::Hour => (current_time.hour(), TimeComponent::Day),
740 TimeComponent::Minute => (current_time.minute(), TimeComponent::Hour),
741 TimeComponent::Second => (current_time.second(), TimeComponent::Minute),
742 _ => return Err(CronError::InvalidTime),
743 };
744
745 let match_result =
746 self.pattern
747 .find_match_in_component(current_value, component, direction)?;
748
749 match match_result {
750 Some(match_value) => {
751 if match_value != current_value {
752 Self::set_time_component(current_time, component, match_value, direction)?;
753 }
754 }
755 None => {
756 Self::adjust_time_component(current_time, next_larger_component, direction)?;
757 changed = true;
758 }
759 }
760 Ok(changed)
761 }
762
763 pub fn as_str(&self) -> &str {
764 self.pattern.as_str()
765 }
766}
767
768impl std::fmt::Display for Cron {
769 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
770 write!(f, "{}", self.pattern)
771 }
772}
773
774#[cfg(feature = "serde")]
775impl Serialize for Cron {
776 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
777 where
778 S: Serializer,
779 {
780 serializer.serialize_str(self.pattern.as_str())
781 }
782}
783
784#[cfg(feature = "serde")]
785impl<'de> Deserialize<'de> for Cron {
786 fn deserialize<D>(deserializer: D) -> Result<Cron, D::Error>
787 where
788 D: de::Deserializer<'de>,
789 {
790 struct CronVisitor;
791
792 impl Visitor<'_> for CronVisitor {
793 type Value = Cron;
794
795 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
796 formatter.write_str("a valid cron pattern")
797 }
798
799 fn visit_str<E>(self, value: &str) -> Result<Cron, E>
800 where
801 E: de::Error,
802 {
803 Cron::from_str(value).map_err(de::Error::custom)
804 }
805 }
806
807 deserializer.deserialize_str(CronVisitor)
808 }
809}
810
811pub fn from_naive<Tz: TimeZone>(
813 naive_time: NaiveDateTime,
814 timezone: &Tz,
815) -> chrono::LocalResult<DateTime<Tz>> {
816 timezone.from_local_datetime(&naive_time)
817}
818
819#[cfg(test)]
820mod tests {
821 use std::hash::{DefaultHasher, Hash, Hasher as _};
822
823 use crate::parser::Seconds;
824
825 use super::*;
826 use chrono::{Local, TimeZone};
827 use chrono_tz::Tz;
828
829 use rstest::rstest;
830 #[cfg(feature = "serde")]
831 use serde_test::{assert_de_tokens_error, assert_tokens, Token};
832 #[test]
833 fn test_is_time_matching() -> Result<(), CronError> {
834 let cron = Cron::from_str("0 9 1 1 *")?;
836 let time_matching = Local.with_ymd_and_hms(2023, 1, 1, 9, 0, 0).unwrap();
837 let time_not_matching = Local.with_ymd_and_hms(2023, 1, 1, 10, 0, 0).unwrap();
838
839 assert!(cron.is_time_matching(&time_matching)?);
840 assert!(!cron.is_time_matching(&time_not_matching)?);
841
842 Ok(())
843 }
844
845 #[test]
846 fn test_last_day_of_february_non_leap_year() -> Result<(), CronError> {
847 let cron = Cron::from_str("0 9 L 2 *")?;
849
850 let time_matching = Local.with_ymd_and_hms(2023, 2, 28, 9, 0, 0).unwrap();
852 let time_not_matching = Local.with_ymd_and_hms(2023, 2, 28, 10, 0, 0).unwrap();
853 let time_not_matching_2 = Local.with_ymd_and_hms(2023, 2, 27, 9, 0, 0).unwrap();
854
855 assert!(cron.is_time_matching(&time_matching)?);
856 assert!(!cron.is_time_matching(&time_not_matching)?);
857 assert!(!cron.is_time_matching(&time_not_matching_2)?);
858
859 Ok(())
860 }
861
862 #[test]
863 fn test_last_day_of_february_leap_year() -> Result<(), CronError> {
864 let cron = Cron::from_str("0 9 L 2 *")?;
866
867 let time_matching = Local.with_ymd_and_hms(2024, 2, 29, 9, 0, 0).unwrap();
869 let time_not_matching = Local.with_ymd_and_hms(2024, 2, 29, 10, 0, 0).unwrap();
870 let time_not_matching_2 = Local.with_ymd_and_hms(2024, 2, 28, 9, 0, 0).unwrap();
871
872 assert!(cron.is_time_matching(&time_matching)?);
873 assert!(!cron.is_time_matching(&time_not_matching)?);
874 assert!(!cron.is_time_matching(&time_not_matching_2)?);
875
876 Ok(())
877 }
878
879 #[test]
880 fn test_last_friday_of_year() -> Result<(), CronError> {
881 let cron = Cron::from_str("0 0 * * FRI#L")?;
883
884 let time_matching = Local.with_ymd_and_hms(2023, 12, 29, 0, 0, 0).unwrap();
886
887 assert!(cron.is_time_matching(&time_matching)?);
888
889 Ok(())
890 }
891
892 #[test]
893 fn test_last_friday_of_year_alternative_alpha_syntax() -> Result<(), CronError> {
894 let cron = Cron::from_str("0 0 * * FRIl")?;
896
897 let time_matching = Local.with_ymd_and_hms(2023, 12, 29, 0, 0, 0).unwrap();
899
900 assert!(cron.is_time_matching(&time_matching)?);
901
902 Ok(())
903 }
904
905 #[test]
906 fn test_last_friday_of_year_alternative_number_syntax() -> Result<(), CronError> {
907 let cron = Cron::from_str("0 0 * * 5L")?;
909
910 let time_matching = Local.with_ymd_and_hms(2023, 12, 29, 0, 0, 0).unwrap();
912
913 assert!(cron.is_time_matching(&time_matching)?);
914
915 Ok(())
916 }
917
918 #[test]
919 fn test_find_next_occurrence() -> Result<(), CronError> {
920 let cron = CronParser::builder()
922 .seconds(Seconds::Optional)
923 .build()
924 .parse("* * * * * *")?;
925
926 let start_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 0, 29).unwrap();
928 let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
930
931 let expected_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 0, 30).unwrap();
933 assert_eq!(next_occurrence, expected_time);
934
935 Ok(())
936 }
937
938 #[test]
939 fn test_find_next_minute() -> Result<(), CronError> {
940 let cron = Cron::from_str("* * * * *")?;
941
942 let start_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 0, 29).unwrap();
944 let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
946
947 let expected_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 1, 0).unwrap();
949 assert_eq!(next_occurrence, expected_time);
950
951 Ok(())
952 }
953
954 #[test]
955 fn test_wrap_month_and_year() -> Result<(), CronError> {
956 let cron = CronParser::builder()
958 .seconds(Seconds::Optional)
959 .build()
960 .parse("0 0 15 * * *")?;
961
962 let start_time = Local.with_ymd_and_hms(2023, 12, 31, 16, 0, 0).unwrap();
964 let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
966
967 let expected_time = Local.with_ymd_and_hms(2024, 1, 1, 15, 0, 0).unwrap();
969 assert_eq!(next_occurrence, expected_time);
970
971 Ok(())
972 }
973
974 #[test]
975 fn test_weekday_pattern_correct_weekdays() -> Result<(), CronError> {
976 let schedule = CronParser::builder()
977 .seconds(Seconds::Optional)
978 .build()
979 .parse("0 0 0 * * 5,6")?;
980 let start_time = Local
981 .with_ymd_and_hms(2022, 2, 17, 0, 0, 0)
982 .single()
983 .unwrap();
984 let mut next_runs = Vec::new();
985
986 for next in schedule.iter_after(start_time).take(6) {
987 next_runs.push(next);
988 }
989
990 assert_eq!(next_runs[0].year(), 2022);
991 assert_eq!(next_runs[0].month(), 2);
992 assert_eq!(next_runs[0].day(), 18);
993
994 assert_eq!(next_runs[1].day(), 19);
995 assert_eq!(next_runs[2].day(), 25);
996 assert_eq!(next_runs[3].day(), 26);
997
998 assert_eq!(next_runs[4].month(), 3);
999 assert_eq!(next_runs[4].day(), 4);
1000 assert_eq!(next_runs[5].day(), 5);
1001
1002 Ok(())
1003 }
1004
1005 #[test]
1006 fn test_weekday_pattern_combined_with_day_of_month() -> Result<(), CronError> {
1007 let schedule = CronParser::builder()
1008 .seconds(Seconds::Optional)
1009 .build()
1010 .parse("59 59 23 2 * 6")?;
1011 let start_time = Local
1012 .with_ymd_and_hms(2022, 1, 31, 0, 0, 0)
1013 .single()
1014 .unwrap();
1015 let mut next_runs = Vec::new();
1016
1017 for next in schedule.iter_after(start_time).take(6) {
1018 next_runs.push(next);
1019 }
1020
1021 assert_eq!(next_runs[0].year(), 2022);
1022 assert_eq!(next_runs[0].month(), 2);
1023 assert_eq!(next_runs[0].day(), 2);
1024
1025 assert_eq!(next_runs[1].month(), 2);
1026 assert_eq!(next_runs[1].day(), 5);
1027
1028 assert_eq!(next_runs[2].month(), 2);
1029 assert_eq!(next_runs[2].day(), 12);
1030
1031 assert_eq!(next_runs[3].month(), 2);
1032 assert_eq!(next_runs[3].day(), 19);
1033
1034 assert_eq!(next_runs[4].month(), 2);
1035 assert_eq!(next_runs[4].day(), 26);
1036
1037 assert_eq!(next_runs[5].month(), 3);
1038 assert_eq!(next_runs[5].day(), 2);
1039
1040 Ok(())
1041 }
1042
1043 #[test]
1044 fn test_weekday_pattern_alone() -> Result<(), CronError> {
1045 let schedule = Cron::from_str("15 9 * * mon")?;
1046 let start_time = Local
1047 .with_ymd_and_hms(2022, 2, 28, 23, 59, 0)
1048 .single()
1049 .unwrap();
1050 let mut next_runs = Vec::new();
1051
1052 for next in schedule.iter_after(start_time).take(3) {
1053 next_runs.push(next);
1054 }
1055
1056 assert_eq!(next_runs[0].year(), 2022);
1057 assert_eq!(next_runs[0].month(), 3);
1058 assert_eq!(next_runs[0].day(), 7);
1059 assert_eq!(next_runs[0].hour(), 9);
1060 assert_eq!(next_runs[0].minute(), 15);
1061
1062 assert_eq!(next_runs[1].day(), 14);
1063 assert_eq!(next_runs[1].hour(), 9);
1064 assert_eq!(next_runs[1].minute(), 15);
1065
1066 assert_eq!(next_runs[2].day(), 21);
1067 assert_eq!(next_runs[2].hour(), 9);
1068 assert_eq!(next_runs[2].minute(), 15);
1069
1070 Ok(())
1071 }
1072
1073 #[test]
1074 fn test_cron_expression_13w_wed() -> Result<(), CronError> {
1075 let cron = Cron::from_str("0 0 13W * WED")?;
1077
1078 let start_date = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1080
1081 let expected_dates = [
1083 Local.with_ymd_and_hms(2024, 1, 3, 0, 0, 0).unwrap(),
1084 Local.with_ymd_and_hms(2024, 1, 10, 0, 0, 0).unwrap(),
1085 Local.with_ymd_and_hms(2024, 1, 12, 0, 0, 0).unwrap(),
1086 Local.with_ymd_and_hms(2024, 1, 17, 0, 0, 0).unwrap(),
1087 Local.with_ymd_and_hms(2024, 1, 24, 0, 0, 0).unwrap(),
1088 ];
1089
1090 for (idx, current_date) in cron
1092 .clone()
1093 .iter_from(start_date, Direction::Forward)
1094 .take(5)
1095 .enumerate()
1096 {
1097 assert_eq!(expected_dates[idx], current_date);
1098 }
1099
1100 Ok(())
1101 }
1102
1103 #[test]
1104 fn test_cron_expression_31dec_fri() -> Result<(), CronError> {
1105 let cron = CronParser::builder()
1107 .seconds(Seconds::Required)
1108 .dom_and_dow(true)
1109 .build()
1110 .parse("0 0 0 31 12 FRI")?;
1111
1112 let start_date = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1114
1115 let expected_dates = [
1117 Local.with_ymd_and_hms(2027, 12, 31, 0, 0, 0).unwrap(),
1118 Local.with_ymd_and_hms(2032, 12, 31, 0, 0, 0).unwrap(),
1119 Local.with_ymd_and_hms(2038, 12, 31, 0, 0, 0).unwrap(),
1120 Local.with_ymd_and_hms(2049, 12, 31, 0, 0, 0).unwrap(),
1121 Local.with_ymd_and_hms(2055, 12, 31, 0, 0, 0).unwrap(),
1122 ];
1123
1124 for (idx, current_date) in cron
1126 .clone()
1127 .iter_from(start_date, Direction::Forward)
1128 .take(5)
1129 .enumerate()
1130 {
1131 assert_eq!(expected_dates[idx], current_date);
1132 }
1133
1134 Ok(())
1135 }
1136
1137 #[test]
1138 fn test_cron_parse_invalid_expressions() {
1139 let invalid_expressions = vec![
1140 "* * *",
1141 "invalid",
1142 "123",
1143 "0 0 * * * * * *",
1144 "* * * *",
1145 "* 60 * * * *",
1146 "-1 59 * * * *",
1147 "1- 59 * * * *",
1148 "0 0 0 5L * *",
1149 "0 0 0 5#L * *",
1150 ];
1151 for expr in invalid_expressions {
1152 assert!(CronParser::builder()
1153 .seconds(Seconds::Optional)
1154 .build()
1155 .parse(expr)
1156 .is_err());
1157 }
1158 }
1159
1160 #[test]
1161 fn test_cron_parse_valid_expressions() {
1162 let valid_expressions = vec![
1163 "* * * * *",
1164 "0 0 * * *",
1165 "*/10 * * * *",
1166 "0 0 1 1 *",
1167 "0 12 * * MON",
1168 "0 0 * * 1",
1169 "0 0 1 1,7 * ",
1170 "00 00 01 * SUN ",
1171 "0 0 1-7 * SUN",
1172 "5-10/2 * * * *",
1173 "0 0-23/2 * * *",
1174 "0 12 15-21 * 1-FRI",
1175 "0 0 29 2 *",
1176 "0 0 31 * *",
1177 "*/15 9-17 * * MON-FRI",
1178 "0 12 * JAN-JUN *",
1179 "0 0 1,15,L * SUN#L",
1180 "0 0 2,1 1-6/2 *",
1181 "0 0 5,L * 5L",
1182 "0 0 5,L * 7#2",
1183 ];
1184 for expr in valid_expressions {
1185 assert!(Cron::from_str(expr).is_ok());
1186 }
1187 }
1188
1189 #[test]
1190 fn test_is_time_matching_different_time_zones() -> Result<(), CronError> {
1191 use chrono::FixedOffset;
1192
1193 let cron = Cron::from_str("0 12 * * *")?;
1194 let time_east_matching = FixedOffset::east_opt(3600)
1195 .expect("Success")
1196 .with_ymd_and_hms(2023, 1, 1, 12, 0, 0)
1197 .unwrap(); let time_west_matching = FixedOffset::west_opt(3600)
1199 .expect("Success")
1200 .with_ymd_and_hms(2023, 1, 1, 12, 0, 0)
1201 .unwrap(); assert!(cron.is_time_matching(&time_east_matching)?);
1204 assert!(cron.is_time_matching(&time_west_matching)?);
1205
1206 Ok(())
1207 }
1208
1209 #[test]
1210 fn test_find_next_occurrence_edge_case_inclusive() -> Result<(), CronError> {
1211 let cron = CronParser::builder()
1212 .seconds(Seconds::Required)
1213 .build()
1214 .parse("59 59 23 * * *")?;
1215 let start_time = Local.with_ymd_and_hms(2023, 3, 14, 23, 59, 59).unwrap();
1216 let next_occurrence = cron.find_next_occurrence(&start_time, true)?;
1217 let expected_time = Local.with_ymd_and_hms(2023, 3, 14, 23, 59, 59).unwrap();
1218 assert_eq!(next_occurrence, expected_time);
1219 Ok(())
1220 }
1221
1222 #[test]
1223 fn test_find_next_occurrence_edge_case_exclusive() -> Result<(), CronError> {
1224 let cron = CronParser::builder()
1225 .seconds(Seconds::Optional)
1226 .build()
1227 .parse("59 59 23 * * *")?;
1228 let start_time = Local.with_ymd_and_hms(2023, 3, 14, 23, 59, 59).unwrap();
1229 let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
1230 let expected_time = Local.with_ymd_and_hms(2023, 3, 15, 23, 59, 59).unwrap();
1231 assert_eq!(next_occurrence, expected_time);
1232 Ok(())
1233 }
1234
1235 #[test]
1236 fn test_cron_iterator_large_time_jumps() -> Result<(), CronError> {
1237 let cron = Cron::from_str("0 0 * * *")?;
1238 let start_time = Local.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
1239 let mut iterator = cron.iter_after(start_time);
1240 let next_run = iterator.nth(365 * 5 + 1); let expected_time = Local.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1242 assert_eq!(next_run, Some(expected_time));
1243 Ok(())
1244 }
1245
1246 #[test]
1247 fn test_handling_different_month_lengths() -> Result<(), CronError> {
1248 let cron = Cron::from_str("0 0 L * *")?; let feb_non_leap_year = Local.with_ymd_and_hms(2023, 2, 1, 0, 0, 0).unwrap();
1250 let feb_leap_year = Local.with_ymd_and_hms(2024, 2, 1, 0, 0, 0).unwrap();
1251 let april = Local.with_ymd_and_hms(2023, 4, 1, 0, 0, 0).unwrap();
1252
1253 assert_eq!(
1254 cron.find_next_occurrence(&feb_non_leap_year, false)?,
1255 Local.with_ymd_and_hms(2023, 2, 28, 0, 0, 0).unwrap()
1256 );
1257 assert_eq!(
1258 cron.find_next_occurrence(&feb_leap_year, false)?,
1259 Local.with_ymd_and_hms(2024, 2, 29, 0, 0, 0).unwrap()
1260 );
1261 assert_eq!(
1262 cron.find_next_occurrence(&april, false)?,
1263 Local.with_ymd_and_hms(2023, 4, 30, 0, 0, 0).unwrap()
1264 );
1265
1266 Ok(())
1267 }
1268
1269 #[test]
1270 fn test_cron_iterator_non_standard_intervals() -> Result<(), CronError> {
1271 let cron = CronParser::builder()
1272 .seconds(Seconds::Optional)
1273 .build()
1274 .parse("*/29 */13 * * * *")?;
1275 let start_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap();
1276 let mut iterator = cron.iter_after(start_time);
1277 let first_run = iterator.next().unwrap();
1278 let second_run = iterator.next().unwrap();
1279
1280 assert_eq!(first_run.hour() % 13, 0);
1281 assert_eq!(first_run.minute() % 29, 0);
1282 assert_eq!(second_run.hour() % 13, 0);
1283 assert_eq!(second_run.minute() % 29, 0);
1284
1285 Ok(())
1286 }
1287
1288 #[test]
1289 fn test_cron_iterator_non_standard_intervals_with_offset() -> Result<(), CronError> {
1290 let cron = Cron::from_str("7/29 2/13 * * *")?;
1291 let start_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap();
1292 let mut iterator = cron.iter_after(start_time);
1293
1294 let first_run = iterator.next().unwrap();
1295 assert_eq!(first_run.hour(), 2);
1297 assert_eq!(first_run.minute(), 7);
1298
1299 let second_run = iterator.next().unwrap();
1300 assert_eq!(second_run.hour(), 2);
1302 assert_eq!(second_run.minute(), 36);
1303
1304 Ok(())
1305 }
1306
1307 #[test]
1309 fn test_unusual_cron_expression_end_month_start_month_mon() -> Result<(), CronError> {
1310 use chrono::TimeZone;
1311
1312 let cron = Cron::from_str("0 0 */31,1-7 */1 MON")?;
1314
1315 let start_date = Local.with_ymd_and_hms(2023, 12, 24, 0, 0, 0).unwrap();
1317
1318 let expected_dates = vec![
1320 Local.with_ymd_and_hms(2023, 12, 25, 0, 0, 0).unwrap(),
1321 Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1322 Local.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
1323 Local.with_ymd_and_hms(2024, 1, 3, 0, 0, 0).unwrap(),
1324 Local.with_ymd_and_hms(2024, 1, 4, 0, 0, 0).unwrap(),
1325 Local.with_ymd_and_hms(2024, 1, 5, 0, 0, 0).unwrap(),
1326 Local.with_ymd_and_hms(2024, 1, 6, 0, 0, 0).unwrap(),
1327 Local.with_ymd_and_hms(2024, 1, 7, 0, 0, 0).unwrap(),
1328 Local.with_ymd_and_hms(2024, 1, 8, 0, 0, 0).unwrap(),
1329 Local.with_ymd_and_hms(2024, 1, 15, 0, 0, 0).unwrap(),
1330 Local.with_ymd_and_hms(2024, 1, 22, 0, 0, 0).unwrap(),
1331 Local.with_ymd_and_hms(2024, 1, 29, 0, 0, 0).unwrap(),
1332 Local.with_ymd_and_hms(2024, 2, 1, 0, 0, 0).unwrap(),
1333 ];
1334
1335 let mut idx = 0;
1337 for current_date in cron
1338 .iter_from(start_date, Direction::Forward)
1339 .take(expected_dates.len())
1340 {
1341 assert_eq!(expected_dates[idx], current_date);
1342 idx += 1;
1343 }
1344
1345 assert_eq!(idx, 13);
1346
1347 Ok(())
1348 }
1349
1350 #[test]
1352 fn test_unusual_cron_expression_end_month_start_month_mon_dom_and_dow() -> Result<(), CronError>
1353 {
1354 use chrono::TimeZone;
1355
1356 let cron = CronParser::builder()
1358 .seconds(Seconds::Optional) .dom_and_dow(true)
1360 .build()
1361 .parse("0 0 */31,1-7 */1 MON")?;
1362
1363 let start_date = Local.with_ymd_and_hms(2023, 12, 24, 0, 0, 0).unwrap();
1365
1366 let expected_dates = [
1368 Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1369 Local.with_ymd_and_hms(2024, 2, 5, 0, 0, 0).unwrap(),
1370 Local.with_ymd_and_hms(2024, 3, 4, 0, 0, 0).unwrap(),
1371 ];
1372
1373 let mut idx = 0;
1375 for current_date in cron
1376 .iter_from(start_date, Direction::Forward)
1377 .take(expected_dates.len())
1378 {
1379 assert_eq!(expected_dates[idx], current_date);
1380 idx += 1;
1381 }
1382
1383 assert_eq!(idx, 3);
1384
1385 Ok(())
1386 }
1387
1388 #[test]
1389 fn test_cron_expression_29feb_march_fri() -> Result<(), CronError> {
1390 use chrono::TimeZone;
1391
1392 let cron = CronParser::builder()
1394 .seconds(Seconds::Optional) .dom_and_dow(true)
1396 .build()
1397 .parse("0 0 29 2-3 FRI")?;
1398
1399 let start_date = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1401
1402 let expected_dates = [
1404 Local.with_ymd_and_hms(2024, 3, 29, 0, 0, 0).unwrap(),
1405 Local.with_ymd_and_hms(2030, 3, 29, 0, 0, 0).unwrap(),
1406 Local.with_ymd_and_hms(2036, 2, 29, 0, 0, 0).unwrap(),
1407 Local.with_ymd_and_hms(2041, 3, 29, 0, 0, 0).unwrap(),
1408 Local.with_ymd_and_hms(2047, 3, 29, 0, 0, 0).unwrap(),
1409 ];
1410
1411 let mut idx = 0;
1413 for current_date in cron.iter_from(start_date, Direction::Forward).take(5) {
1414 assert_eq!(expected_dates[idx], current_date);
1415 idx += 1;
1416 }
1417
1418 assert_eq!(idx, 5);
1419
1420 Ok(())
1421 }
1422
1423 #[test]
1424 fn test_cron_expression_second_sunday_using_seven() -> Result<(), CronError> {
1425 use chrono::TimeZone;
1426
1427 let cron = CronParser::builder()
1429 .seconds(Seconds::Optional)
1430 .build()
1431 .parse("0 0 0 * * 7#2")?;
1432
1433 let start_date = Local.with_ymd_and_hms(2024, 10, 1, 0, 0, 0).unwrap();
1435
1436 let expected_dates = [
1438 Local.with_ymd_and_hms(2024, 10, 13, 0, 0, 0).unwrap(),
1439 Local.with_ymd_and_hms(2024, 11, 10, 0, 0, 0).unwrap(),
1440 Local.with_ymd_and_hms(2024, 12, 8, 0, 0, 0).unwrap(),
1441 Local.with_ymd_and_hms(2025, 1, 12, 0, 0, 0).unwrap(),
1442 Local.with_ymd_and_hms(2025, 2, 9, 0, 0, 0).unwrap(),
1443 ];
1444
1445 let mut idx = 0;
1447 for current_date in cron.iter_from(start_date, Direction::Forward).take(5) {
1448 assert_eq!(expected_dates[idx], current_date);
1449 idx += 1;
1450 }
1451
1452 assert_eq!(idx, 5);
1453
1454 Ok(())
1455 }
1456
1457 #[test]
1458 fn test_specific_and_wildcard_entries() -> Result<(), CronError> {
1459 let cron = Cron::from_str("15 */2 * 3,5 FRI")?;
1460 let matching_time = Local.with_ymd_and_hms(2023, 3, 3, 2, 15, 0).unwrap();
1461 let non_matching_time = Local.with_ymd_and_hms(2023, 3, 3, 3, 15, 0).unwrap();
1462
1463 assert!(cron.is_time_matching(&matching_time)?);
1464 assert!(!cron.is_time_matching(&non_matching_time)?);
1465
1466 Ok(())
1467 }
1468
1469 #[test]
1470 fn test_month_weekday_edge_cases() -> Result<(), CronError> {
1471 let cron = Cron::from_str("0 0 * 2-3 SUN")?;
1472
1473 let matching_time = Local.with_ymd_and_hms(2023, 2, 5, 0, 0, 0).unwrap();
1474 let non_matching_time = Local.with_ymd_and_hms(2023, 2, 5, 0, 0, 1).unwrap();
1475
1476 assert!(cron.is_time_matching(&matching_time)?);
1477 assert!(!cron.is_time_matching(&non_matching_time)?);
1478
1479 Ok(())
1480 }
1481
1482 #[test]
1483 fn test_leap_year() -> Result<(), CronError> {
1484 let cron = Cron::from_str("0 0 29 2 *")?;
1485 let leap_year_matching = Local.with_ymd_and_hms(2024, 2, 29, 0, 0, 0).unwrap();
1486
1487 assert!(cron.is_time_matching(&leap_year_matching)?);
1488
1489 Ok(())
1490 }
1491
1492 #[test]
1493 fn test_tabs_for_separator() -> Result<(), CronError> {
1494 let cron = Cron::from_str("0 0 29 2 *")?;
1495 let leap_year_matching = Local.with_ymd_and_hms(2024, 2, 29, 0, 0, 0).unwrap();
1496
1497 assert!(cron.is_time_matching(&leap_year_matching)?);
1498
1499 Ok(())
1500 }
1501
1502 #[test]
1503 fn test_mixed_separators() -> Result<(), CronError> {
1504 let cron = Cron::from_str("0 0 29 2 *")?;
1505 let leap_year_matching = Local.with_ymd_and_hms(2024, 2, 29, 0, 0, 0).unwrap();
1506
1507 assert!(cron.is_time_matching(&leap_year_matching)?);
1508
1509 Ok(())
1510 }
1511
1512 #[test]
1513 fn test_mixed_leading_separators() -> Result<(), CronError> {
1514 let cron = Cron::from_str(" 0 0 29 2 *")?;
1515 let leap_year_matching = Local.with_ymd_and_hms(2024, 2, 29, 0, 0, 0).unwrap();
1516
1517 assert!(cron.is_time_matching(&leap_year_matching)?);
1518
1519 Ok(())
1520 }
1521
1522 #[test]
1523 fn test_mixed_tailing_separators() -> Result<(), CronError> {
1524 let cron = Cron::from_str("0 0 29 2 * ")?;
1525 let leap_year_matching = Local.with_ymd_and_hms(2024, 2, 29, 0, 0, 0).unwrap();
1526
1527 assert!(cron.is_time_matching(&leap_year_matching)?);
1528
1529 Ok(())
1530 }
1531
1532 #[test]
1533 fn test_time_overflow() -> Result<(), CronError> {
1534 let cron_match = CronParser::builder()
1535 .seconds(Seconds::Optional)
1536 .build()
1537 .parse("59 59 23 31 12 *")?;
1538 let cron_next = CronParser::builder()
1539 .seconds(Seconds::Optional)
1540 .build()
1541 .parse("0 0 0 1 1 *")?;
1542 let time_matching = Local.with_ymd_and_hms(2023, 12, 31, 23, 59, 59).unwrap();
1543 let next_day = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1544 let next_match = Local.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap();
1545
1546 let is_matching = cron_match.is_time_matching(&time_matching)?;
1547 let next_occurrence = cron_next.find_next_occurrence(&time_matching, false)?;
1548 let next_match_occurrence = cron_match.find_next_occurrence(&time_matching, false)?;
1549
1550 assert!(is_matching);
1551 assert_eq!(next_occurrence, next_day);
1552 assert_eq!(next_match_occurrence, next_match);
1553
1554 Ok(())
1555 }
1556
1557 #[test]
1558 fn test_yearly_recurrence() -> Result<(), CronError> {
1559 let cron = Cron::from_str("0 0 1 1 *")?;
1560 let matching_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap();
1561 let non_matching_time = Local.with_ymd_and_hms(2023, 1, 2, 0, 0, 0).unwrap();
1562
1563 assert!(cron.is_time_matching(&matching_time)?);
1564 assert!(!cron.is_time_matching(&non_matching_time)?);
1565
1566 Ok(())
1567 }
1568
1569 fn calculate_hash<T: Hash>(t: &T) -> u64 {
1571 let mut s = DefaultHasher::new();
1572 t.hash(&mut s);
1573 s.finish()
1574 }
1575
1576 #[rstest]
1577 #[case("@hourly", "@daily", false)]
1579 #[case("@daily", "@weekly", false)]
1580 #[case("@weekly", "@monthly", false)]
1581 #[case("@monthly", "@yearly", false)]
1582 #[case("* * * * *", "@hourly", false)]
1583 #[case("@annually", "@yearly", true)]
1584 #[case("* * * * * *", "* * * * *", false)]
1586 #[case("0 12 * * *", "30 0 12 * * *", false)]
1587 #[case("0 0 * * * *", "@hourly", true)]
1588 #[case("5 * * * * *", "10 * * * * *", false)]
1590 #[case("15 * * * *", "45 * * * *", false)]
1591 #[case("* * 8 * *", "* * 18 * *", false)]
1592 #[case("* * * 1 *", "* * * 6 *", false)]
1593 #[case("* * * JAN *", "* * * JUL *", false)]
1594 #[case("* * * * 0", "* * * * 3", false)]
1595 #[case("* * * * SUN", "* * * * WED", false)]
1596 #[case("* * * * 7", "* * * * 1", false)]
1597 #[case("0-29 * * * *", "30-59 * * * *", false)]
1599 #[case("* * 1-11 * *", "* * 12-23 * *", false)]
1600 #[case("* * * JAN-JUN *", "* * * JUL-DEC *", false)]
1601 #[case("* * * * MON-WED", "* * * * THU-SAT", false)]
1602 #[case("* * * * *", "0-5 * * * *", false)]
1603 #[case("*/15 * * * *", "*/30 * * * *", false)]
1605 #[case("0/10 * * * *", "5/10 * * * *", false)]
1606 #[case("* * 1-10/2 * *", "* * 1-10/3 * *", false)]
1607 #[case("* * * * *", "*/2 * * * *", false)]
1608 #[case("0,10,20 * * * *", "30,40,50 * * * *", false)]
1610 #[case("* * * * MON,WED,FRI", "* * * * TUE,THU,SAT", false)]
1611 #[case("* * * ? * ?", "* * * * * *", true)]
1613 #[case("@monthly", "0 0 1 * *", true)]
1614 #[case("* * * * 1,3,5", "* * * * MON,WED,FRI", true)]
1615 #[case("* * * mar *", "* * * 3 *", true)]
1616 #[case("0 0 * * 1", "0 0 15 * *", false)]
1618 #[case("0 0 1 * *", "0 0 1 * 1", false)]
1619 #[case("* * 1 * *", "* * L * *", false)]
1621 #[case("* * L FEB *", "* * L MAR *", false)]
1622 #[case("* * * * 1#L", "* * * * 2#L", false)]
1623 #[case("* * * * 4#L", "* * * * FRI#L", false)]
1624 #[case("* * 1W * *", "* * 1 * *", false)]
1626 #[case("* * 15W * *", "* * 16W * *", false)]
1627 #[case("* * * * 1#2", "* * * * 1#1", false)]
1629 #[case("* * * * TUE#4", "* * * * TUE#2", false)]
1630 #[case("* * * * 5#1", "* * * * FRI#1", true)]
1631 #[case("* * * * MON#1", "* * * * TUE#1", false)]
1632 #[case("0 10 * * MON#2", "0 10 1-7 * MON", false)]
1634 #[case("*/10 8-10 * JAN,DEC 1-5", "0 12 * * 6", false)]
1635 fn test_comparison_and_hash(
1636 #[case] pattern_1: &str,
1637 #[case] pattern_2: &str,
1638 #[case] equal: bool,
1639 ) {
1640 use crate::parser::Seconds;
1641
1642 eprintln!("Parsing {pattern_1}");
1643 let cron_1 = Cron::from_str(pattern_1).unwrap_or_else(|err| {
1644 eprintln!(
1645 "Initial parse attempt failed ({err}). Trying again but with allowed seconds."
1646 );
1647 CronParser::builder()
1648 .seconds(Seconds::Required)
1649 .build()
1650 .parse(pattern_1)
1651 .unwrap()
1652 });
1653
1654 eprintln!("Parsing {pattern_2}");
1655 let cron_2 = Cron::from_str(pattern_2).unwrap_or_else(|err| {
1656 eprintln!(
1657 "Initial parse attempt failed ({err}). Trying again but with allowed seconds."
1658 );
1659 CronParser::builder()
1660 .seconds(Seconds::Required)
1661 .build()
1662 .parse(pattern_2)
1663 .unwrap()
1664 });
1665
1666 assert_eq!(
1667 cron_1 == cron_2,
1668 equal,
1669 "Equality relation between both patterns is not {equal}. {cron_1} != {cron_2}."
1670 );
1671 assert_eq!(
1672 calculate_hash(&cron_1) == calculate_hash(&cron_2),
1673 equal,
1674 "Hashes don't respect quality relation"
1675 );
1676
1677 if !equal {
1678 assert!(
1679 cron_1 > cron_2,
1680 "Ordering between first an second pattern is wrong"
1681 );
1682 }
1683
1684 #[expect(clippy::eq_op, reason = "Want to check Eq is correctly implemented")]
1685 {
1686 assert!(
1687 cron_1 == cron_1,
1688 "Eq implementation is incorrect for first patter"
1689 );
1690 assert!(
1691 cron_2 == cron_2,
1692 "Eq implementation is incorrect for second patter"
1693 );
1694 }
1695 }
1696
1697 #[rstest]
1700 #[case("0 0 1-7 * 1", "0 0 * * 1#1")]
1701 #[case("0 0 8-14 * MON", "0 0 * * MON#2")]
1702 #[should_panic(expected = "Patterns are not equal")]
1703 fn failed_equality(#[case] pattern_1: &str, #[case] pattern_2: &str) {
1704 let cron_1 = Cron::from_str(pattern_1).unwrap();
1705 let cron_2 = Cron::from_str(pattern_2).unwrap();
1706 assert!(cron_1 == cron_2, "Patterns are not equal");
1707 }
1708
1709 #[cfg(feature = "serde")]
1710 #[test]
1711 fn test_serde_tokens() {
1712 let cron = Cron::from_str("0 0 * * *").expect("should be valid pattern");
1713 assert_tokens(&cron.to_string(), &[Token::Str("0 0 * * *")]);
1714 }
1715
1716 #[cfg(feature = "serde")]
1717 #[test]
1718 fn test_shorthand_serde_tokens() {
1719 let expressions = [
1720 ("@daily", "0 0 * * *"),
1721 ("0 12 * * MON", "0 12 * * 1"),
1722 ("*/15 9-17 * * MON-FRI", "*/15 9-17 * * 1-5"),
1723 ];
1724 for (shorthand, expected) in expressions.iter() {
1725 let cron = Cron::from_str(shorthand).expect("should be valid pattern");
1726 assert_tokens(&cron.to_string(), &[Token::Str(expected)]);
1727 }
1728 }
1729
1730 #[cfg(feature = "serde")]
1731 #[test]
1732 fn test_invalid_serde_tokens() {
1733 assert_de_tokens_error::<Cron>(
1734 &[Token::Str("Invalid cron pattern")],
1735 "Invalid pattern: Pattern must have between 5 and 7 fields."
1736 );
1737 }
1738
1739 #[test]
1740 fn test_find_previous_occurrence() -> Result<(), CronError> {
1741 let cron = Cron::from_str("* * * * *")?;
1742 let start_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 1, 30).unwrap();
1743 let prev_occurrence = cron.find_previous_occurrence(&start_time, false)?;
1744 let expected_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 1, 0).unwrap();
1745 assert_eq!(prev_occurrence, expected_time);
1746 Ok(())
1747 }
1748
1749 #[test]
1750 fn test_find_previous_occurrence_inclusive() -> Result<(), CronError> {
1751 let cron = Cron::from_str("* * * * *")?;
1752 let start_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 1, 0).unwrap();
1753 let prev_occurrence = cron.find_previous_occurrence(&start_time, true)?;
1754 assert_eq!(prev_occurrence, start_time);
1755 Ok(())
1756 }
1757
1758 #[test]
1759 fn test_wrap_year_backwards() -> Result<(), CronError> {
1760 let cron = Cron::from_str("0 0 1 1 *")?; let start_time = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 1).unwrap();
1762 let prev_occurrence = cron.find_previous_occurrence(&start_time, false)?;
1763 let expected_time = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1764 assert_eq!(prev_occurrence, expected_time);
1765
1766 let start_time_2 = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1767 let prev_occurrence_2 = cron.find_previous_occurrence(&start_time_2, false)?;
1768 let expected_time_2 = Local.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap();
1769 assert_eq!(prev_occurrence_2, expected_time_2);
1770 Ok(())
1771 }
1772
1773 #[test]
1774 fn test_find_occurrence_at_min_year_limit() -> Result<(), CronError> {
1775 let cron = Cron::from_str("0 0 1 1 *")?;
1777
1778 let start_time = Local
1780 .with_ymd_and_hms(YEAR_LOWER_LIMIT, 1, 1, 0, 0, 1)
1781 .unwrap();
1782
1783 let prev_occurrence = cron.find_previous_occurrence(&start_time, false)?;
1785 let expected_time = Local
1786 .with_ymd_and_hms(YEAR_LOWER_LIMIT, 1, 1, 0, 0, 0)
1787 .unwrap();
1788 assert_eq!(prev_occurrence, expected_time);
1789
1790 let result = cron.find_previous_occurrence(&expected_time, false);
1792 assert!(matches!(result, Err(CronError::TimeSearchLimitExceeded)));
1793
1794 Ok(())
1795 }
1796
1797 #[test]
1798 fn test_find_occurrence_at_max_year_limit() -> Result<(), CronError> {
1799 let cron = Cron::from_str("0 0 1 1 *")?;
1801
1802 let start_time = Local
1804 .with_ymd_and_hms(YEAR_UPPER_LIMIT - 1, 12, 31, 23, 59, 59)
1805 .unwrap();
1806
1807 let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
1808 let expected_time = Local
1809 .with_ymd_and_hms(YEAR_UPPER_LIMIT, 1, 1, 0, 0, 0)
1810 .unwrap();
1811 assert_eq!(next_occurrence, expected_time);
1812
1813 let result = cron.find_next_occurrence(&expected_time, false);
1815 assert!(matches!(result, Err(CronError::TimeSearchLimitExceeded)));
1816
1817 Ok(())
1818 }
1819
1820 #[test]
1821 fn test_weekday_for_historical_date_1831() -> Result<(), CronError> {
1822 let cron = Cron::from_str("0 0 * * SUN")?;
1824
1825 let matching_sunday = Local.with_ymd_and_hms(1831, 6, 5, 0, 0, 0).unwrap();
1827
1828 let non_matching_monday = Local.with_ymd_and_hms(1831, 6, 6, 0, 0, 0).unwrap();
1830
1831 assert!(
1833 cron.is_time_matching(&matching_sunday)?,
1834 "Should match on Sunday, June 5, 1831"
1835 );
1836 assert!(
1837 !cron.is_time_matching(&non_matching_monday)?,
1838 "Should not match on Monday, June 6, 1831"
1839 );
1840
1841 Ok(())
1842 }
1843
1844
1845 #[test]
1846 fn test_find_next_occurrence_with_year_range_outside_start() {
1847 let cron = Cron::from_str("0 0 0 1 1 * 2080-2085").unwrap();
1848
1849 let start_time = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1850
1851 let next_occurrence = cron.find_next_occurrence(&start_time, false).unwrap();
1852 let expected_time = Local.with_ymd_and_hms(2080, 1, 1, 0, 0, 0).unwrap();
1853
1854 assert_eq!(next_occurrence, expected_time, "Iterator should jump forward to the correct year.");
1855 }
1856
1857 #[test]
1858 fn test_find_previous_occurrence_with_year_range_outside_start() {
1859 let cron = Cron::from_str("0 0 0 1 1 * 2030-2035").unwrap();
1860
1861 let start_time = Local.with_ymd_and_hms(2050, 1, 1, 0, 0, 0).unwrap();
1862
1863 let prev_occurrence = cron.find_previous_occurrence(&start_time, false).unwrap();
1864 let expected_time = Local.with_ymd_and_hms(2035, 1, 1, 0, 0, 0).unwrap();
1865
1866 assert_eq!(prev_occurrence, expected_time, "Iteratorn should jump backwards to the correct year.");
1867 }
1868
1869 #[test]
1871 fn test_dst_gap_fixed_time_job() -> Result<(), CronError> {
1872 let timezone: Tz = "Europe/Stockholm".parse().unwrap();
1875
1876 let cron = Cron::from_str("0 30 2 * * *")?; let start_time = timezone.with_ymd_and_hms(2025, 3, 30, 1, 59, 59).unwrap(); let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
1882
1883 let expected_time = timezone.with_ymd_and_hms(2025, 3, 30, 3, 0, 0).unwrap();
1886 assert_eq!(next_occurrence, expected_time, "Fixed-time job in DST gap should execute on the next valid occurrence of its pattern.");
1887 Ok(())
1888 }
1889
1890 #[test]
1891 fn test_dst_gap_interval_wildcard_job_minute() -> Result<(), CronError> {
1892 let timezone: Tz = "Europe/Stockholm".parse().unwrap();
1895
1896 let cron = Cron::from_str("0 */5 * * * *")?; let start_time = timezone.with_ymd_and_hms(2025, 3, 30, 1, 59, 59).unwrap(); let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
1902 let expected_time = timezone.with_ymd_and_hms(2025, 3, 30, 3, 0, 0).unwrap();
1905
1906 assert_eq!(next_occurrence, expected_time, "Interval job in DST gap should skip the gap and resume relative to new wall time.");
1907 Ok(())
1908 }
1909
1910 #[test]
1911 fn test_dst_gap_interval_wildcard_job_second() -> Result<(), CronError> {
1912 let timezone: Tz = "Europe/Stockholm".parse().unwrap();
1915
1916 let cron = Cron::from_str("* * * * * *")?; let start_time = timezone.with_ymd_and_hms(2025, 3, 30, 1, 59, 59).unwrap(); let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
1921 let expected_time = timezone.with_ymd_and_hms(2025, 3, 30, 3, 0, 0).unwrap();
1924
1925 assert_eq!(next_occurrence, expected_time, "Every second job in DST gap should jump to the first valid second after the gap.");
1926 Ok(())
1927 }
1928
1929 #[test]
1932 fn test_dst_overlap_fixed_time_job() -> Result<(), CronError> {
1933 let timezone: Tz = "Europe/Stockholm".parse().unwrap();
1938
1939 let cron = Cron::from_str("0 30 2 * * *")?; let start_time = timezone.with_ymd_and_hms(2025, 10, 26, 1, 59, 59).unwrap(); let first_occurrence = cron.find_next_occurrence(&start_time, false)?;
1946 let expected_first_time = timezone.with_ymd_and_hms(2025, 10, 26, 2, 30, 0).earliest().unwrap(); assert_eq!(first_occurrence, expected_first_time, "Fixed-time job in DST overlap should run at first occurrence.");
1948
1949 let _next_search_start = timezone.with_ymd_and_hms(2025, 10, 26, 2, 59, 59).earliest().unwrap(); let next_search_start_after_overlap = timezone.with_ymd_and_hms(2025, 10, 26, 3, 0, 0).unwrap(); let next_occurrence_after_overlap = cron.find_next_occurrence(&next_search_start_after_overlap, false)?;
1959 let expected_next_day = timezone.with_ymd_and_hms(2025, 10, 27, 2, 30, 0).unwrap(); assert_eq!(next_occurrence_after_overlap, expected_next_day, "Fixed-time job should not re-run during the repeated hour.");
1962 Ok(())
1963 }
1964
1965 #[test]
1966 fn test_dst_overlap_interval_wildcard_job() -> Result<(), CronError> {
1967 let timezone: Tz = "Europe/Stockholm".parse().unwrap();
1970
1971 let cron = Cron::from_str("0 * * * * *")?; let start_time = timezone.with_ymd_and_hms(2025, 10, 26, 1, 59, 59).unwrap(); let mut occurrences = Vec::new();
1977 let mut iter = cron.iter_after(start_time);
1978
1979 for _ in 0..120 {
1985 if let Some(time) = iter.next() {
1986 occurrences.push(time);
1987 } else {
1988 break;
1989 }
1990 }
1991
1992 assert_eq!(occurrences.len(), 120, "Interval job in DST overlap should run for both occurrences of each minute.");
1993
1994 for m in 0..60 { let naive_time_m_00 = chrono::NaiveDateTime::new(
1997 chrono::NaiveDate::from_ymd_opt(2025, 10, 26).unwrap(),
1998 chrono::NaiveTime::from_hms_opt(2, m, 0).unwrap(),
1999 );
2000 let ambiguous_m_00 = timezone.from_local_datetime(&naive_time_m_00);
2001
2002 assert_eq!(
2004 occurrences[(2 * m) as usize], ambiguous_m_00.earliest().unwrap(),
2006 "Minute {m}: CEST occurrence mismatch"
2007 );
2008
2009 assert_eq!(
2011 occurrences[(2 * m + 1) as usize], ambiguous_m_00.latest().unwrap(),
2013 "Minute {m}: CET occurrence mismatch"
2014 );
2015 }
2016
2017 Ok(())
2018 }
2019
2020 #[test]
2021 fn test_dst_overlap_interval_wildcard_job_hour_step() -> Result<(), CronError> {
2022 let timezone: Tz = "Europe/Stockholm".parse().unwrap();
2025
2026 let cron = Cron::from_str("0 0 */2 * * *")?; let start_time = timezone.with_ymd_and_hms(2025, 10, 26, 0, 0, 0).unwrap(); let mut iter = cron.iter_from(start_time, Direction::Forward);
2031
2032 let first_run = iter.next().unwrap(); let second_run = iter.next().unwrap(); let third_run = iter.next().unwrap(); let fourth_run = iter.next().unwrap(); let naive_time_2_00 = chrono::NaiveDateTime::new(chrono::NaiveDate::from_ymd_opt(2025, 10, 26).unwrap(), chrono::NaiveTime::from_hms_opt(2, 0, 0).unwrap());
2044 let ambiguous_2_00 = timezone.from_local_datetime(&naive_time_2_00);
2045
2046 assert_eq!(first_run, timezone.with_ymd_and_hms(2025, 10, 26, 0, 0, 0).unwrap());
2047 assert_eq!(second_run, ambiguous_2_00.earliest().unwrap()); assert_eq!(third_run, ambiguous_2_00.latest().unwrap()); assert_eq!(fourth_run, timezone.with_ymd_and_hms(2025, 10, 26, 4, 0, 0).unwrap()); Ok(())
2052 }
2053
2054
2055}