chrono_kit/iter/
naive_datetime_iter.rs

1use chrono::{Duration, NaiveDateTime};
2use thiserror::Error;
3
4/// Errors that can occur when creating a datetime iterator
5#[derive(Debug, Error)]
6pub enum NaiveDatetimeIterError {
7    /// Returned when step duration is zero
8    #[error("Step duration cannot be zero")]
9    ZeroStep,
10    /// Returned when start datetime is after end datetime for positive step
11    #[error("Invalid range: start {start} must be before end {end} for positive step")]
12    InvalidRange {
13        start: NaiveDateTime,
14        end: NaiveDateTime,
15    },
16}
17
18/// Iterator that yields datetimes between start and end with given step
19///
20/// Handles both ascending and descending iteration based on step sign.
21pub struct NaiveDatetimeIterator {
22    start: NaiveDateTime,
23    end: NaiveDateTime,
24    step: Duration,
25}
26
27impl NaiveDatetimeIterator {
28    /// Creates a new DatetimeIterator
29    ///
30    /// # Arguments
31    /// * `start` - The starting datetime (inclusive)
32    /// * `end` - The ending datetime (inclusive)
33    /// * `step` - The duration between each step (must be non-zero)
34    ///
35    /// # Errors
36    /// Returns `DatetimeIterError` if:
37    /// - `step` is zero
38    /// - `start` is after `end` for positive step
39    pub fn new(
40        start: NaiveDateTime,
41        end: NaiveDateTime,
42        step: Duration,
43    ) -> Result<Self, NaiveDatetimeIterError> {
44        if step.is_zero() {
45            return Err(NaiveDatetimeIterError::ZeroStep);
46        }
47        if start > end {
48            return Err(NaiveDatetimeIterError::InvalidRange { start, end });
49        }
50        Ok(NaiveDatetimeIterator { start, end, step })
51    }
52
53    fn next_asc(&mut self) -> Option<NaiveDateTime> {
54        if self.start > self.end {
55            return None;
56        }
57
58        let result = self.start;
59        self.start = if self.start < self.end && self.start + self.step > self.end {
60            self.end
61        } else {
62            self.start + self.step
63        };
64
65        Some(result)
66    }
67
68    fn next_desc(&mut self) -> Option<NaiveDateTime> {
69        if self.end < self.start {
70            return None;
71        }
72
73        let result = self.end;
74        self.end = if self.end > self.start && self.end + self.step < self.start {
75            self.start
76        } else {
77            self.end + self.step
78        };
79
80        Some(result)
81    }
82}
83
84impl Iterator for NaiveDatetimeIterator {
85    type Item = NaiveDateTime;
86
87    fn next(&mut self) -> Option<Self::Item> {
88        if self.step > Duration::zero() {
89            self.next_asc()
90        } else {
91            self.next_desc()
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_ascending_iteration() {
102        let start =
103            NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
104        let end =
105            NaiveDateTime::parse_from_str("2023-01-03 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
106        let step = Duration::days(1);
107
108        let mut iter = NaiveDatetimeIterator::new(start, end, step).unwrap();
109        assert_eq!(iter.next(), Some(start));
110        assert_eq!(iter.next(), Some(start + step));
111        assert_eq!(iter.next(), Some(end));
112        assert_eq!(iter.next(), None);
113    }
114
115    #[test]
116    fn test_descending_iteration() {
117        let start =
118            NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
119        let end =
120            NaiveDateTime::parse_from_str("2023-01-03 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
121        let step = Duration::days(-1);
122
123        let mut iter = NaiveDatetimeIterator::new(start, end, step).unwrap();
124        assert_eq!(iter.next(), Some(end));
125        assert_eq!(iter.next(), Some(end + step));
126        assert_eq!(iter.next(), Some(start));
127        assert_eq!(iter.next(), None);
128    }
129
130    #[test]
131    fn test_zero_step_error() {
132        let start =
133            NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
134        let end =
135            NaiveDateTime::parse_from_str("2023-01-03 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
136        let step = Duration::zero();
137
138        let result = NaiveDatetimeIterator::new(start, end, step);
139        assert!(matches!(result, Err(NaiveDatetimeIterError::ZeroStep)));
140    }
141
142    #[test]
143    fn test_invalid_range_error() {
144        let start =
145            NaiveDateTime::parse_from_str("2023-01-03 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
146        let end =
147            NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
148        let step = Duration::days(1);
149
150        let result = NaiveDatetimeIterator::new(start, end, step);
151        assert!(matches!(
152            result,
153            Err(NaiveDatetimeIterError::InvalidRange { .. })
154        ));
155    }
156
157    #[test]
158    fn test_non_integer_step() {
159        let start =
160            NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
161        let end =
162            NaiveDateTime::parse_from_str("2023-01-03 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
163        let step = Duration::days(1);
164
165        let mut iter = NaiveDatetimeIterator::new(start, end, step).unwrap();
166        assert_eq!(iter.next(), Some(start));
167        assert_eq!(iter.next(), Some(start + step));
168        assert_eq!(iter.next(), Some(start + step * 2));
169        assert_eq!(iter.next(), Some(end));
170    }
171}