[go: up one dir, main page]

croner/
lib.rs

1//! # Croner
2//!
3//! Croner is a fully-featured, lightweight, and efficient Rust library designed for parsing and evaluating cron patterns.
4//!
5//! ## Features
6//! - Parses a wide range of cron expressions, including extended formats.
7//! - Generates human-readable descriptions of cron patterns.
8//! - Evaluates cron patterns to calculate upcoming and previous execution times.
9//! - Supports time zone-aware scheduling.
10//! - Offers granularity up to seconds for precise task scheduling.
11//! - Compatible with the `chrono` library for dealing with date and time in Rust.
12//!
13//! ## Crate Features
14//! - `serde`: Enables [`serde::Serialize`](https://docs.rs/serde/1/serde/trait.Serialize.html) and
15//!   [`serde::Deserialize`](https://docs.rs/serde/1/serde/trait.Deserialize.html) implementations for
16//!   [`Cron`](struct.Cron.html). This feature is disabled by default.
17//!
18//! ## Example
19//! The following example demonstrates how to use Croner to parse a cron expression and find the next and previous occurrences.
20//!
21//! ```rust
22//! use std::str::FromStr as _;
23//!
24//! use chrono::Utc;
25//! use croner::Cron;
26//!
27//! // Parse a cron expression to find occurrences at 00:00 on Friday
28//! let cron = Cron::from_str("0 0 * * FRI").expect("Successful parsing");
29//! let now = Utc::now();
30//!
31//! // Get the next occurrence from the current time
32//! let next = cron.find_next_occurrence(&now, false).unwrap();
33//!
34//! // Get the previous occurrence from the current time
35//! let previous = cron.find_previous_occurrence(&now, false).unwrap();
36//!
37//! println!(
38//!     "Pattern \"{}\" will match next at {}",
39//!     cron.pattern.to_string(),
40//!     next
41//! );
42//!
43//! println!(
44//!     "Pattern \"{}\" matched previously at {}",
45//!     cron.pattern.to_string(),
46//!     previous
47//! );
48//! ```
49//!
50//! In this example, `Cron::from_str("0 0 * * FRI")` creates a new Cron instance for the pattern that represents every Friday at midnight. The `find_next_occurrence` method calculates the next time this pattern will be true from the current moment.
51//!
52//! The `false` argument in `find_next_occurrence` specifies that the current time is not included in the calculation, ensuring that only future occurrences are considered.
53//!
54//! ## Describing a Pattern
55//! Croner can also generate a human-readable, English description of a cron pattern. This is highly useful for displaying schedule information in a UI or for debugging complex patterns.
56//!
57//! The .describe() method returns a String detailing what the schedule means.
58//!
59//! ## Getting Started
60//! To start using Croner, add it to your project's `Cargo.toml` and follow the examples to integrate cron pattern parsing and scheduling into your application.
61//!
62//! ## Pattern
63//!
64//! The expressions used by Croner are very similar to those of Vixie Cron, but with
65//! a few additions as outlined below:
66//!
67//! ```javascript
68//! // ┌──────────────── (optional) second (0 - 59)
69//! // │ ┌────────────── minute (0 - 59)
70//! // │ │ ┌──────────── hour (0 - 23)
71//! // │ │ │ ┌────────── day of month (1 - 31)
72//! // │ │ │ │ ┌──────── month (1 - 12, JAN-DEC)
73//! // │ │ │ │ │ ┌────── day of week (0 - 6, SUN-Mon)
74//! // │ │ │ │ │ │       (0 to 6 are Sunday to Saturday; 7 is Sunday, the same as 0)
75//! // │ │ │ │ │ │
76//! // * * * * * *
77//! ```
78//!
79//! | Field        | Required | Allowed values    | Allowed special characters | Remarks                                                                                         |
80//! |--------------|----------|-------------------|----------------------------|-------------------------------------------------------------------------------------------------|
81//! | Seconds      | Optional | 0-59              | * , - / ?                  |                                                                                                 |
82//! | Minutes      | Yes      | 0-59              | * , - / ?                  |                                                                                                 |
83//! | Hours        | Yes      | 0-23              | * , - / ?                  |                                                                                                 |
84//! | Day of Month | Yes      | 1-31              | * , - / ? L W              |                                                                                                 |
85//! | Month        | Yes      | 1-12 or JAN-DEC   | * , - / ?                  |                                                                                                 |
86//! | Day of Week  | Yes      | 0-7 or SUN-MON    | * , - / ? # L              | 0 to 6 are Sunday to Saturday, 7 is Sunday, the same as 0. '#' is used to specify the nth weekday |
87//!
88//! For more information, refer to the full [README](https://github.com/hexagon/croner-rust).
89
90pub mod errors;
91pub mod parser;
92pub mod describe;
93
94mod component;
95mod iterator;
96mod pattern;
97
98// Enum to specify the direction of time search
99#[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/// Categorizes a cron pattern as either a Fixed-Time Job or an Interval/Wildcard Job.
115/// This is used to apply specific Daylight Saving Time (DST) transition rules.
116#[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
138/// Safeguard to prevent infinite loops when searching for future
139/// occurrences of a cron pattern that may never match. It ensures that the search
140/// function will eventually terminate and return an error instead of running indefinitely.
141pub const YEAR_UPPER_LIMIT: i32 = 5000;
142
143/// Sets the lower year limit to 1 AD/CE.
144/// This is a pragmatic choice to avoid the complexities of year 0 (1 BCE) and pre-CE
145/// dates, which involve different calendar systems and are outside the scope of a
146/// modern scheduling library.
147pub const YEAR_LOWER_LIMIT: i32 = 1;
148
149// The Cron struct represents a cron schedule and provides methods to parse cron strings,
150// check if a datetime matches the cron pattern, and find the next occurrence.
151#[derive(Debug, Clone, PartialEq, PartialOrd, Hash)]
152pub struct Cron {
153    pub pattern: CronPattern, // Parsed cron pattern
154}
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    /// Evaluates if a given `DateTime` matches the cron pattern.
166    ///
167    /// The function checks each cron field (seconds, minutes, hours, day of month, month and 
168    /// year) against the provided `DateTime` to determine if it aligns with the cron pattern. 
169    /// Each field is checked for a match, and all fields must match for the entire pattern 
170    /// to be considered a match.
171    ///
172    /// # Parameters
173    ///
174    /// - `time`: A reference to the `DateTime<Tz>` to be checked against the cron pattern.
175    ///
176    /// # Returns
177    ///
178    /// - `Ok(bool)`: `true` if `time` matches the cron pattern, `false` otherwise.
179    /// - `Err(CronError)`: An error if there is a problem checking any of the pattern fields
180    ///   against the provided `DateTime`.
181    ///
182    /// # Errors
183    ///
184    /// This method may return `CronError` if an error occurs during the evaluation of the
185    /// cron pattern fields. Errors can occur due to invalid bit operations or invalid dates.
186    ///
187    /// # Examples
188    ///
189    /// ```
190    /// use std::str::FromStr as _;
191    ///
192    /// use croner::Cron;
193    /// use chrono::Utc;
194    ///
195    /// // Parse cron expression
196    /// let cron: Cron = Cron::from_str("* * * * *").expect("Couldn't parse cron string");
197    ///
198    /// // Compare to time now
199    /// let time = Utc::now();
200    /// let matches_all = cron.is_time_matching(&time).unwrap();
201    ///
202    /// // Output results
203    /// println!("Time is: {}", time);
204    /// println!(
205    ///     "Pattern \"{}\" does {} time {}",
206    ///     cron.pattern.to_string(),
207    ///     if matches_all { "match" } else { "not match" },
208    ///     time
209    /// );
210    /// ```
211    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())?) // Add year match check
221    }
222
223    /// Finds the next occurrence of a scheduled time that matches the cron pattern.
224    /// starting from a given `start_time`. If `inclusive` is `true`, the search includes the
225    /// `start_time`; otherwise, it starts from the next second.
226    ///
227    /// This method performs a search through time, beginning at `start_time`, to find the
228    /// next date and time that aligns with the cron pattern defined within the `Cron` instance.
229    /// The search respects cron fields (seconds, minutes, hours, day of month, month, day of week)
230    /// and iterates through time until a match is found or an error occurs.
231    ///
232    /// # Parameters
233    ///
234    /// - `start_time`: A reference to a `DateTime<Tz>` indicating the start time for the search.
235    /// - `inclusive`: A `bool` that specifies whether the search should include `start_time` itself.
236    ///
237    /// # Returns
238    ///
239    /// - `Ok(DateTime<Tz>)`: The next occurrence that matches the cron pattern.
240    /// - `Err(CronError)`: An error if the next occurrence cannot be found within a reasonable
241    ///   limit, if any of the date/time manipulations result in an invalid date, or if the
242    ///   cron pattern match fails.
243    ///
244    /// # Errors
245    ///
246    /// - `CronError::InvalidTime`: If the start time provided is invalid or adjustments to the
247    ///   time result in an invalid date/time.
248    /// - `CronError::TimeSearchLimitExceeded`: If the search exceeds a reasonable time limit.
249    ///   This prevents infinite loops in case of patterns that cannot be matched.
250    /// - Other errors as defined by the `CronError` enum may occur if the pattern match fails
251    ///   at any stage of the search.
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// use chrono::Utc;
257    /// use croner::{Cron, parser::{Seconds, CronParser}};
258    ///
259    /// // Parse cron expression
260    /// let cron: Cron = CronParser::builder().seconds(Seconds::Required).build().parse("0 18 * * * 5").expect("Success");
261    ///
262    /// // Get next match
263    /// let time = Utc::now();
264    /// let next = cron.find_next_occurrence(&time, false).unwrap();
265    ///
266    /// println!(
267    ///     "Pattern \"{}\" will match next time at {}",
268    ///     cron.pattern.to_string(),
269    ///     next
270    /// );
271    /// ```
272    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    /// Finds the previous occurrence of a scheduled time that matches the cron pattern.
282    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) // Take only the first element (DateTime<Tz>)
289    }
290
291    /// The main generic search function.
292    /// Returns (found_datetime, optional_second_ambiguous_datetime_if_any)
293    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)); // Single match, no second ambiguous time
356                    }
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                    // DST Overlap (Fall Back)
364                    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                        // Fixed-Time Job: Execute only once, at its first occurrence (earliest in the ambiguous pair).
369                        if self.is_time_matching(&first_occurrence_dt)? {
370                            return Ok((first_occurrence_dt, None)); // Return only the first, no second for fixed jobs.
371                        }
372                        // If fixed time doesn't match first_occurrence_dt, it means this particular naive_time
373                        // doesn't match the fixed pattern's exact time (e.g., cron is "0 0 2 *" and naive is 02:30:00).
374                        // So, we just advance to the next second and continue the loop.
375                        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 { // Interval/Wildcard Job
381                        // Interval/Wildcard Job: Execute for each occurrence that matches.
382                        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)); // Return first, and potentially the second.
394                        } else if let Some(s_match) = secondary_match {
395                            // Only the second occurrence matched, return it as primary.
396                            return Ok((s_match, None)); // No secondary from this point.
397                        }
398                        // If neither matched the pattern for this ambiguous naive_time, advance and continue.
399                        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                    // DST Gap (Spring Forward)
407                    if job_type == JobType::FixedTime {
408                        // For fixed-time jobs that fall into a gap, we want them to "snap" to the first valid time after the gap.
409                        // Find the very first valid NaiveDateTime after the current `naive_time`
410                        // that can be successfully converted to a DateTime<Tz>.
411                        let mut temp_naive = naive_time;
412                        let mut gap_adjust_count = 0;
413                        const MAX_GAP_SEARCH_SECONDS: u32 = 3600 * 2; // Max 2 hours for a typical gap
414
415                        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                            // Try to resolve this `temp_naive` into a real DateTime.
425                            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                                // If it resolves to ambiguous (unlikely right at a gap boundary for Single), take the earliest.
432                                resolved_dt_after_gap = dt1; 
433                                break;
434                            }
435                            // Keep looping if still None or search limit exceeded
436                            if gap_adjust_count > MAX_GAP_SEARCH_SECONDS {
437                                return Err(CronError::TimeSearchLimitExceeded);
438                            }
439                        }
440
441                        // `resolved_dt_after_gap` is now the first valid wall-clock time after the gap.
442                        // For a fixed-time job that fell into the gap, this is the time it should run.
443                        // We must ensure that its date components (year, month, day, day of week) still match the pattern.
444                        // We do NOT check the original fixed hour/minute/second from the pattern, as they were "missing".
445                        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                            // No need to update naive_time here
449                            return Ok((resolved_dt_after_gap, None)); 
450                        } else {
451                            // If even the date components of this post-gap time do not match the pattern,
452                            // then the fixed job's *date* itself was not the one containing the gap.
453                            // In this case, we simply advance `naive_time` past the gap
454                            // and let the main loop continue searching for the next matching date.
455                            naive_time = temp_naive;
456                            continue;
457                        }
458                    } else { // Interval/Wildcard Job in DST Gap
459                        // Existing logic: simply advance by one second/minute
460                        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    /// Creates a `CronIterator` starting from the specified time.
471    ///
472    /// The search can be performed forwards or backwards in time.
473    ///
474    /// # Arguments
475    ///
476    /// * `start_from` - A `DateTime<Tz>` that represents the starting point for the iterator.
477    /// * `direction` - A `Direction` to specify the search direction.
478    ///
479    /// # Returns
480    ///
481    /// Returns a `CronIterator<Tz>` that can be used to iterate over scheduled times.
482    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    /// Creates a `CronIterator` starting after the specified time, in forward direction.
491    ///
492    /// # Arguments
493    ///
494    /// * `start_after` - A `DateTime<Tz>` that represents the starting point for the iterator.
495    ///
496    /// # Returns
497    ///
498    /// Returns a `CronIterator<Tz>` that can be used to iterate over scheduled times.
499    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    /// Creates a `CronIterator` starting before the specified time, in backwards direction.
504    ///
505    /// # Arguments
506    ///
507    /// * `start_before` - A `DateTime<Tz>` that represents the starting point for the iterator.
508    ///
509    /// # Returns
510    ///
511    /// Returns a `CronIterator<Tz>` that can be used to iterate over scheduled times.
512    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    /// Returns a human-readable description of the cron pattern.
517    ///
518    /// This method provides a best-effort English description of the cron schedule.
519    /// Note: The cron instance must be parsed successfully before calling this method.
520    ///
521    /// # Example
522    /// ```
523    /// use croner::Cron;
524    /// use std::str::FromStr as _;
525    ///
526    /// let cron = Cron::from_str("0 12 * * MON-FRI").unwrap();
527    /// println!("{}", cron.describe());
528    /// // Output: At on minute 0, at hour 12, on Monday,Tuesday,Wednesday,Thursday,Friday.
529    /// ```
530    pub fn describe(&self) -> String {
531        self.pattern.describe()
532    }
533
534    /// Returns a human-readable description using a provided language provider.
535    ///
536    /// # Arguments
537    ///
538    /// * `lang` - An object that implements the `Language` trait.
539    pub fn describe_lang<L: crate::describe::Language>(&self, lang: L) -> String {
540        self.pattern.describe_lang(lang)
541    }
542  
543    /// Determines if the cron pattern represents a Fixed-Time Job or an Interval/Wildcard Job.
544    /// A Fixed-Time Job has fixed (non-wildcard, non-stepped, single-value) Seconds, Minute,
545    /// and Hour fields. Otherwise, it's an Interval/Wildcard Job.
546    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    // TIME MANIPULATION FUNCTIONS
565
566    /// Sets a time component and resets lower-order ones based on direction.
567    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    /// Adjusts a time component up or down, resetting lower-order ones.
606    fn adjust_time_component(
607        current_time: &mut NaiveDateTime,
608        component: TimeComponent,
609        direction: Direction,
610    ) -> Result<(), CronError> {
611        // Check for limits
612        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 => { // Tillagd logik för år
669                        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        // Loop until the component matches the pattern
714        while !(match component {
715            TimeComponent::Year => self.pattern.year_match(current_time.year()), // Tillagd
716            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), // Should not happen for other components, but this is safe
723        })? {
724            Self::adjust_time_component(current_time, component, direction)?;
725            changed = true;
726        }
727        Ok(changed)
728    }
729
730    /// Consolidated helper for time-based components (Hour, Minute, Second).
731    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
811// Convert `NaiveDateTime` back to `DateTime<Tz>`
812pub 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        // This pattern is meant to match first second of 9 am on the first day of January.
835        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        // This pattern is meant to match every second of 9 am on the last day of February in a non-leap year.
848        let cron = Cron::from_str("0 9 L 2 *")?;
849
850        // February 28th, 2023 is the last day of February in a non-leap year.
851        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        // This pattern is meant to match every second of 9 am on the last day of February in a leap year.
865        let cron = Cron::from_str("0 9 L 2 *")?;
866
867        // February 29th, 2024 is the last day of February in a leap year.
868        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        // This pattern is meant to match 0:00:00 last friday of current year
882        let cron = Cron::from_str("0 0 * * FRI#L")?;
883
884        // February 29th, 2024 is the last day of February in a leap year.
885        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        // This pattern is meant to match 0:00:00 last friday of current year
895        let cron = Cron::from_str("0 0 * * FRIl")?;
896
897        // February 29th, 2024 is the last day of February in a leap year.
898        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        // This pattern is meant to match 0:00:00 last friday of current year
908        let cron = Cron::from_str("0 0 * * 5L")?;
909
910        // February 29th, 2024 is the last day of February in a leap year.
911        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        // This pattern is meant to match every minute at 30 seconds past the minute.
921        let cron = CronParser::builder()
922            .seconds(Seconds::Optional)
923            .build()
924            .parse("* * * * * *")?;
925
926        // Set the start time to a known value.
927        let start_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 0, 29).unwrap();
928        // Calculate the next occurrence from the start time.
929        let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
930
931        // Verify that the next occurrence is at the expected time.
932        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        // Set the start time to a known value.
943        let start_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 0, 29).unwrap();
944        // Calculate the next occurrence from the start time.
945        let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
946
947        // Verify that the next occurrence is at the expected time.
948        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        // This pattern is meant to match every minute at 30 seconds past the minute.
957        let cron = CronParser::builder()
958            .seconds(Seconds::Optional)
959            .build()
960            .parse("0 0 15 * * *")?;
961
962        // Set the start time to a known value.
963        let start_time = Local.with_ymd_and_hms(2023, 12, 31, 16, 0, 0).unwrap();
964        // Calculate the next occurrence from the start time.
965        let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
966
967        // Verify that the next occurrence is at the expected time.
968        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        // Parse the cron expression
1076        let cron = Cron::from_str("0 0 13W * WED")?;
1077
1078        // Define the start date for the test
1079        let start_date = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1080
1081        // Define the expected matching dates
1082        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        // Iterate over the expected dates, checking each one
1091        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        // Parse the cron expression
1106        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        // Define the start date for the test
1113        let start_date = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1114
1115        // Define the expected matching dates
1116        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        // Iterate over the expected dates, checking each one
1125        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(); // UTC+1
1198        let time_west_matching = FixedOffset::west_opt(3600)
1199            .expect("Success")
1200            .with_ymd_and_hms(2023, 1, 1, 12, 0, 0)
1201            .unwrap(); // UTC-1
1202
1203        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); // Jump 5 years ahead
1241        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 * *")?; // Last day of the month
1249        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        // Expect the first run to be at 02:07 (2 hours and 7 minutes after midnight)
1296        assert_eq!(first_run.hour(), 2);
1297        assert_eq!(first_run.minute(), 7);
1298
1299        let second_run = iterator.next().unwrap();
1300        // Expect the second run to be at 02:36 (29 minutes after the first run)
1301        assert_eq!(second_run.hour(), 2);
1302        assert_eq!(second_run.minute(), 36);
1303
1304        Ok(())
1305    }
1306
1307    // Unusual cron pattern found online, perfect for testing
1308    #[test]
1309    fn test_unusual_cron_expression_end_month_start_month_mon() -> Result<(), CronError> {
1310        use chrono::TimeZone;
1311
1312        // Parse the cron expression with specified options
1313        let cron = Cron::from_str("0 0 */31,1-7 */1 MON")?;
1314
1315        // Define the start date for the test
1316        let start_date = Local.with_ymd_and_hms(2023, 12, 24, 0, 0, 0).unwrap();
1317
1318        // Define the expected matching dates
1319        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        // Iterate over the expected dates, checking each one
1336        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    // Unusual cron pattern found online, perfect for testing, with dom_and_dow
1351    #[test]
1352    fn test_unusual_cron_expression_end_month_start_month_mon_dom_and_dow() -> Result<(), CronError>
1353    {
1354        use chrono::TimeZone;
1355
1356        // Parse the cron expression with specified options
1357        let cron = CronParser::builder()
1358            .seconds(Seconds::Optional) // Just to differ as much from the non dom-and-dow test
1359            .dom_and_dow(true)
1360            .build()
1361            .parse("0 0 */31,1-7 */1 MON")?;
1362
1363        // Define the start date for the test
1364        let start_date = Local.with_ymd_and_hms(2023, 12, 24, 0, 0, 0).unwrap();
1365
1366        // Define the expected matching dates
1367        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        // Iterate over the expected dates, checking each one
1374        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        // Parse the cron expression with specified options
1393        let cron = CronParser::builder()
1394            .seconds(Seconds::Optional) // Just to differ as much from the non dom-and-dow test
1395            .dom_and_dow(true)
1396            .build()
1397            .parse("0 0 29 2-3 FRI")?;
1398
1399        // Define the start date for the test
1400        let start_date = Local.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1401
1402        // Define the expected matching dates
1403        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        // Iterate over the expected dates, checking each one
1412        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        // Parse the cron expression with specified options
1428        let cron = CronParser::builder()
1429            .seconds(Seconds::Optional)
1430            .build()
1431            .parse("0 0 0 * * 7#2")?;
1432
1433        // Define the start date for the test
1434        let start_date = Local.with_ymd_and_hms(2024, 10, 1, 0, 0, 0).unwrap();
1435
1436        // Define the expected matching dates
1437        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        // Iterate over the expected dates, checking each one
1446        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    /// Utility function used in hashing test
1570    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    // Frequency & Nicknames
1578    #[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    // Optional Seconds Field (5 vs 6 fields)
1585    #[case("* * * * * *", "* * * * *", false)]
1586    #[case("0 12 * * *", "30 0 12 * * *", false)]
1587    #[case("0 0 * * * *", "@hourly", true)]
1588    // Field Specificity (Earlier vs. Later)
1589    #[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    // Ranges (`-`)
1598    #[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    // Steps (`/`)
1604    #[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    // Lists (`,`)
1609    #[case("0,10,20 * * * *", "30,40,50 * * * *", false)]
1610    #[case("* * * * MON,WED,FRI", "* * * * TUE,THU,SAT", false)]
1611    // Equivalency & Wildcards
1612    #[case("* * * ? * ?", "* * * * * *", true)]
1613    #[case("@monthly", "0 0 1 * *", true)]
1614    #[case("* * * * 1,3,5", "* * * * MON,WED,FRI", true)]
1615    #[case("* * * mar *", "* * * 3 *", true)]
1616    // Day-of-Month vs. Day-of-Week
1617    #[case("0 0 * * 1", "0 0 15 * *", false)]
1618    #[case("0 0 1 * *", "0 0 1 * 1", false)]
1619    // Special Character `L` (Last)
1620    #[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    // Special Character `W` (Weekday)
1625    #[case("* * 1W * *", "* * 1 * *", false)]
1626    #[case("* * 15W * *", "* * 16W * *", false)]
1627    // Special Character `#` (Nth Weekday)
1628    #[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    // Complex Combinations
1633    #[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    /// KNOWN BUG: these patterns are technically identical but the current
1698    /// `PartialEq` implementation doesn't respect that.
1699    #[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 *")?; // Jan 1st, 00:00
1761        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        // This pattern matches at midnight on January 1st every year.
1776        let cron = Cron::from_str("0 0 1 1 *")?;
1777
1778        // Start the search just after midnight on the first day of the minimum allowed year.
1779        let start_time = Local
1780            .with_ymd_and_hms(YEAR_LOWER_LIMIT, 1, 1, 0, 0, 1)
1781            .unwrap();
1782
1783        // Find the previous occurrence, which should be exactly at the start of the minimum year.
1784        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        // Searching past the limit will return TimeSearchLimitExceeded.
1791        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        // This pattern matches at midnight on January 1st every year.
1800        let cron = Cron::from_str("0 0 1 1 *")?;
1801
1802        // Start the search late in the year just before the upper limit.
1803        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        // Any search beyond the maximum year limit should fail.
1814        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        // This pattern should match at midnight every Sunday.
1823        let cron = Cron::from_str("0 0 * * SUN")?;
1824
1825        // June 5, 1831 was a Sunday.
1826        let matching_sunday = Local.with_ymd_and_hms(1831, 6, 5, 0, 0, 0).unwrap();
1827
1828        // June 6, 1831 was a Monday.
1829        let non_matching_monday = Local.with_ymd_and_hms(1831, 6, 6, 0, 0, 0).unwrap();
1830
1831        // Verify that the Sunday matches and the Monday does not.
1832        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    // --- DST Gap (Spring Forward) Tests ---
1870    #[test]
1871    fn test_dst_gap_fixed_time_job() -> Result<(), CronError> {
1872        // Europe/Stockholm: 2025-03-30 02:00:00 (CET) -> 03:00:00 (CEST)
1873        // The hour 02:00-02:59:59 does not exist.
1874        let timezone: Tz = "Europe/Stockholm".parse().unwrap();
1875
1876        // Fixed-Time Job: Scheduled for 02:30:00, which falls in the gap.
1877        // According to spec: Should execute at the first valid second/minute immediately following the gap (03:00:00).
1878        let cron = Cron::from_str("0 30 2 * * *")?; // 02:30:00
1879        let start_time = timezone.with_ymd_and_hms(2025, 3, 30, 1, 59, 59).unwrap(); // Just before the gap
1880
1881        let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
1882        
1883        // The hour 02:00-02:59:59 does not exist.
1884        // According to spec: Should execute at the first valid second/minute immediately following the gap (03:00:00).
1885        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        // Europe/Stockholm: 2025-03-30 02:00:00 (CET) -> 03:00:00 (CEST)
1893        // The hour 02:00-02:59:59 does not exist.
1894        let timezone: Tz = "Europe/Stockholm".parse().unwrap();
1895
1896        // Interval/Wildcard Job: Every 5 minutes, scheduled at 02:05, 02:10, etc.
1897        // These should be skipped. Next run should be relative to new wall clock time.
1898        let cron = Cron::from_str("0 */5 * * * *")?; // Every 5 minutes
1899        let start_time = timezone.with_ymd_and_hms(2025, 3, 30, 1, 59, 59).unwrap(); // Just before the gap
1900
1901        let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
1902        // After 01:59:59, clock jumps to 03:00:00.
1903        // The next 5-minute interval after 03:00:00 is 03:00:00 itself (03:00 is a multiple of 5).
1904        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        // Europe/Stockholm: 2025-03-30 02:00:00 (CET) -> 03:00:00 (CEST)
1913        // The hour 02:00-02:59:59 does not exist.
1914        let timezone: Tz = "Europe/Stockholm".parse().unwrap();
1915
1916        // Interval/Wildcard Job: Every second
1917        let cron = Cron::from_str("* * * * * *")?; // Every second
1918        let start_time = timezone.with_ymd_and_hms(2025, 3, 30, 1, 59, 59).unwrap(); // Just before the gap
1919
1920        let next_occurrence = cron.find_next_occurrence(&start_time, false)?;
1921        // After 01:59:59, clock jumps to 03:00:00.
1922        // The next second is 03:00:00.
1923        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    // --- DST Overlap (Fall Back) Tests ---
1930
1931    #[test]
1932    fn test_dst_overlap_fixed_time_job() -> Result<(), CronError> {
1933        // Europe/Stockholm: 2025-10-26 03:00:00 (CEST) -> 02:00:00 (CET)
1934        // The hour 02:00-02:59:59 occurs twice.
1935        // First occurrence: 02:00:00-02:59:59 CEST
1936        // Second occurrence: 02:00:00-02:59:59 CET (after fallback from 03:00 CEST)
1937        let timezone: Tz = "Europe/Stockholm".parse().unwrap();
1938
1939        // Fixed-Time Job: Scheduled for 02:30:00.
1940        // Should execute only once, at its first occurrence (CEST).
1941        let cron = Cron::from_str("0 30 2 * * *")?; // 02:30:00
1942        let start_time = timezone.with_ymd_and_hms(2025, 10, 26, 1, 59, 59).unwrap(); // Just before the repeated hour
1943
1944        // First expected run: 02:30:00 CEST
1945        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(); // This is 02:30 CEST
1947        assert_eq!(first_occurrence, expected_first_time, "Fixed-time job in DST overlap should run at first occurrence.");
1948
1949        // Check that it does NOT run again for the second occurrence of 02:30:00 (CET)
1950        // Start search just after the first occurrence of 02:30:00 CEST.
1951        // The naive_time 02:30:00 is ambiguous, so after `first_occurrence`, the next naive_time is 02:30:01.
1952        // We need to advance past the entire ambiguous period.
1953        let _next_search_start = timezone.with_ymd_and_hms(2025, 10, 26, 2, 59, 59).earliest().unwrap(); // End of first 2am hour (CEST)
1954        let next_search_start_after_overlap = timezone.with_ymd_and_hms(2025, 10, 26, 3, 0, 0).unwrap(); // Start of the *second* 2am hour (CET)
1955        
1956        // Find the next occurrence after the *entire* ambiguous period.
1957        // The next 02:30:00 will be on the next day.
1958        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(); // Next day at 02:30 CET
1960        
1961        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        // Europe/Stockholm: 2025-10-26 03:00:00 (CEST) -> 02:00:00 (CET)
1968        // The hour 02:00-02:59:59 occurs twice.
1969        let timezone: Tz = "Europe/Stockholm".parse().unwrap();
1970
1971        // Interval/Wildcard Job: Every minute
1972        // Should execute for both occurrences of each minute in the repeated hour.
1973        let cron = Cron::from_str("0 * * * * *")?; // Every minute at 0 seconds
1974        let start_time = timezone.with_ymd_and_hms(2025, 10, 26, 1, 59, 59).unwrap(); // Just before the repeated hour
1975
1976        let mut occurrences = Vec::new();
1977        let mut iter = cron.iter_after(start_time);
1978
1979        // Collect occurrences for the repeated hour (02:00:00 to 02:59:00 twice)
1980        // We expect two entries for each minute from 02:00 to 02:59.
1981        // The loop should find the 02:00:00 CEST, then 02:01:00 CEST... 02:59:00 CEST,
1982        // then 02:00:00 CET, then 02:01:00 CET... 02:59:00 CET.
1983        // So, 60 minutes * 2 occurrences = 120 entries.
1984        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        // Verify occurrences for each minute
1995        for m in 0..60 { // m is u32
1996            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 CEST occurrence (earliest)
2003            assert_eq!(
2004                occurrences[(2 * m) as usize], // <-- CAST TO usize HERE
2005                ambiguous_m_00.earliest().unwrap(),
2006                "Minute {m}: CEST occurrence mismatch"
2007            );
2008
2009            // Assert CET occurrence (latest)
2010            assert_eq!(
2011                occurrences[(2 * m + 1) as usize], // <-- CAST TO usize HERE
2012                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        // Europe/Stockholm: 2025-10-26 03:00:00 (CEST) -> 02:00:00 (CET)
2023        // The hour 02:00-02:59:59 occurs twice.
2024        let timezone: Tz = "Europe/Stockholm".parse().unwrap();
2025
2026        // Interval/Wildcard Job: Every 2 hours, at 0 minutes and 0 seconds
2027        let cron = Cron::from_str("0 0 */2 * * *")?; // Every 2 hours
2028        let start_time = timezone.with_ymd_and_hms(2025, 10, 26, 0, 0, 0).unwrap(); // Start at midnight
2029
2030        let mut iter = cron.iter_from(start_time, Direction::Forward);
2031
2032        // Expected sequence:
2033        // 00:00:00 (CEST)
2034        // 02:00:00 (CEST) - first occurrence of 2 AM
2035        // 02:00:00 (CET) - the second occurrence of 2 AM
2036        // 04:00:00 (CET) - next 2-hour interval after the second 2 AM
2037
2038        let first_run = iter.next().unwrap(); // 00:00:00 CEST
2039        let second_run = iter.next().unwrap(); // 02:00:00 CEST
2040        let third_run = iter.next().unwrap();  // 02:00:00 CET (the second occurrence of 2 AM)
2041        let fourth_run = iter.next().unwrap(); // 04:00:00 CET (next 2-hour interval after the second 2 AM)
2042
2043        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()); // First 2 AM (CEST)
2048        assert_eq!(third_run, ambiguous_2_00.latest().unwrap()); // Second 2 AM (CET)
2049        assert_eq!(fourth_run, timezone.with_ymd_and_hms(2025, 10, 26, 4, 0, 0).unwrap()); // 4 AM CET - this is not ambiguous, so earlier() is fine
2050
2051        Ok(())
2052    }
2053
2054
2055}