croner/
iterator.rs

1use crate::{Cron, CronError, Direction};
2use chrono::{DateTime, TimeZone, Duration};
3
4#[derive(Debug, Clone, PartialEq, PartialOrd, Hash)]
5pub struct CronIterator<Tz>
6where
7    Tz: TimeZone,
8{
9    cron: Cron,
10    current_time: DateTime<Tz>,
11    is_first: bool,
12    inclusive: bool,
13    direction: Direction,
14    pending_ambiguous_dt: Option<DateTime<Tz>>,
15}
16
17impl<Tz> CronIterator<Tz>
18where
19    Tz: TimeZone,
20{
21    /// Creates a new `CronIterator`.
22    ///
23    /// # Arguments
24    ///
25    /// * `cron` - The `Cron` schedule instance.
26    /// * `start_time` - The `DateTime` to start iterating from.
27    /// * `inclusive` - Whether the `start_time` should be included in the results if it matches.
28    /// * `direction` - The direction to iterate in (Forward or Backward).
29    pub fn new(
30        cron: Cron,
31        start_time: DateTime<Tz>,
32        inclusive: bool,
33        direction: Direction,
34    ) -> Self {
35        CronIterator {
36            cron,
37            current_time: start_time,
38            is_first: true,
39            inclusive,
40            direction,
41            pending_ambiguous_dt: None,
42        }
43    }
44}
45
46impl<Tz> Iterator for CronIterator<Tz>
47where
48    Tz: TimeZone + Clone + Copy,
49{
50    type Item = DateTime<Tz>;
51
52    fn next(&mut self) -> Option<Self::Item> {
53        // Step 1: Check for and yield a pending ambiguous datetime first.
54        // This handles the second occurrence of a time during DST fallback.
55        if let Some(pending_dt_to_yield) = self.pending_ambiguous_dt.take() {
56            // After yielding the second ambiguous time, advance current_time past it.
57            // Clone pending_dt_to_yield because it's about to be returned,
58            // but we need its value to calculate the next `self.current_time`.
59            self.current_time = pending_dt_to_yield.clone().checked_add_signed(match self.direction { // Fixed E0382: pending_dt_to_yield
60                Direction::Forward => Duration::seconds(1),
61                Direction::Backward => Duration::seconds(-1),
62            }).ok_or(CronError::InvalidTime).ok()?;
63            return Some(pending_dt_to_yield);
64        }
65
66        // Determine if the search should be inclusive based on whether it's the first run.
67        let inclusive_search = if self.is_first {
68            self.is_first = false;
69            self.inclusive
70        } else {
71            false // Subsequent searches are always exclusive of the last actual point in time.
72        };
73
74        let result = self.cron.find_occurrence(&self.current_time, inclusive_search, self.direction);
75
76        match result {
77            Ok((found_time, optional_second_ambiguous_dt)) => {
78                // This `found_time` is the one we will return in this iteration.
79
80                // If there's a second ambiguous datetime (for interval jobs),
81                // store it to be yielded on the *next* call to next().
82                // And importantly, set `self.current_time` to advance *past* this second ambiguous time
83                // so the *next* search for a *new* naive time is correct.
84                if let Some(second_ambiguous_dt) = optional_second_ambiguous_dt {
85                    // Clone second_ambiguous_dt because it's stored in self.pending_ambiguous_dt
86                    // AND used to calculate the next self.current_time.
87                    self.pending_ambiguous_dt = Some(second_ambiguous_dt.clone()); // Fixed E0382: second_ambiguous_dt
88
89                    // Advance `self.current_time` past the latest of the ambiguous pair.
90                    // This ensures the next `find_occurrence` call searches for the next unique naive time.
91                    self.current_time = second_ambiguous_dt.checked_add_signed(match self.direction {
92                        Direction::Forward => Duration::seconds(1),
93                        Direction::Backward => Duration::seconds(-1),
94                    }).ok_or(CronError::InvalidTime).ok()?;
95
96                } else {
97                    // Case: No second ambiguous time (either not an overlap, or fixed-time job).
98                    // Advance `self.current_time` simply past the `found_time`.
99                    // Clone found_time because it's used to calculate the next self.current_time
100                    // AND returned at the end of this block.
101                    self.current_time = found_time.clone().checked_add_signed(match self.direction { // Fixed E0382: found_time
102                        Direction::Forward => Duration::seconds(1),
103                        Direction::Backward => Duration::seconds(-1),
104                    }).ok_or(CronError::InvalidTime).ok()?;
105                }
106
107                // Finally, return the found_time for the current iteration.
108                // This `found_time` is the original value received from `find_occurrence`.
109                Some(found_time)
110            }
111            Err(CronError::TimeSearchLimitExceeded) => None,
112            Err(e) => {
113                eprintln!("CronIterator encountered an error: {e:?}");
114                None
115            }
116        }
117    }
118}