Skip to main content

deep_time/
time_range.rs

1use crate::{Dt, Scale};
2
3/// Builder type that enables the ergonomic `start.every(step)` syntax.
4///
5/// This struct is created by [`Dt::every`] and is used to
6/// construct a [`TimeRange`] via either `.until(end)` (inclusive) or
7/// `.up_to(end)` (exclusive).
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[cfg_attr(feature = "js", derive(tsify::Tsify))]
10#[derive(Clone, Debug)]
11pub struct Every {
12    pub(crate) start: Dt,
13    pub(crate) step: Dt,
14}
15
16impl Dt {
17    /// Starts building an evenly-spaced time range.
18    ///
19    /// This method returns an [`Every`] builder that can be chained with
20    /// `.until(end)` or `.up_to(end)` to create a [`TimeRange`] iterator.
21    ///
22    /// # Example
23    ///
24    /// ```
25    /// use deep_time::{Dt, Scale};
26    ///
27    /// let start = Dt::from_ymd(2000, 1, 1);
28    /// let end = Dt::from_ymd(2000, 1, 2);
29    /// let step = Dt::from_hr(1, Scale::TAI);
30    ///
31    /// for timestamp in start.every(step).to_including(end) {
32    ///     println!("{:?}", timestamp.to_ymdhms(Scale::TAI));
33    /// }
34    /// ```
35    #[inline]
36    pub const fn every(self, step: Dt) -> Every {
37        Every { start: self, step }
38    }
39
40    /// Creates an **exclusive** evenly-spaced range from `self` to `end`.
41    ///
42    /// Equivalent to `self.every(step).up_to(end)`.
43    #[inline]
44    pub const fn range(self, end: Dt, step: Dt) -> TimeRange {
45        TimeRange::exclusive(self, end, step)
46    }
47
48    /// Creates a range stepping by whole seconds.
49    #[inline]
50    pub const fn every_sec(self) -> Every {
51        self.every(Dt::from_sec(1, Scale::TAI))
52    }
53
54    /// Creates a range stepping by whole minutes.
55    #[inline]
56    pub const fn every_min(self) -> Every {
57        self.every(Dt::from_min(1, Scale::TAI))
58    }
59
60    /// Creates a range stepping by whole hours.
61    #[inline]
62    pub const fn every_hr(self) -> Every {
63        self.every(Dt::from_hr(1, Scale::TAI))
64    }
65
66    /// Creates a range stepping by whole days.
67    #[inline]
68    pub const fn every_day(self) -> Every {
69        self.every(Dt::from_hr(24, Scale::TAI))
70    }
71
72    /// Returns the next `n` points **after** `self` (exclusive of `self`)
73    /// at the given step.
74    ///
75    /// This is a convenient way to get future points without including the start.
76    #[inline]
77    pub fn next_n(self, n: usize, step: Dt) -> impl ExactSizeIterator<Item = Dt> {
78        (self + step).for_n_steps(n, step)
79    }
80
81    /// Returns an iterator yielding exactly `n` evenly spaced points
82    /// starting from `self`.
83    ///
84    /// This is a convenient one-liner for the common "next N steps" pattern.
85    #[inline]
86    pub fn for_n_steps(self, n: usize, step: Dt) -> impl ExactSizeIterator<Item = Dt> {
87        let end = self + step * (n as i64);
88        TimeRange::exclusive(self, end, step).take(n)
89    }
90}
91
92impl Every {
93    /// Creates an **inclusive** time range (`start ... end`).
94    ///
95    /// The resulting iterator will yield `end` as the final element
96    /// (provided `end` is reachable from `start` with the given step).
97    #[inline]
98    pub fn to_including(self, end: Dt) -> TimeRange {
99        TimeRange::new(self.start, end, self.step, true)
100    }
101
102    /// Creates an **exclusive** time range (`start ... end`).
103    ///
104    /// The resulting iterator will **not** yield `end`.
105    #[inline]
106    pub fn to_excluding(self, end: Dt) -> TimeRange {
107        TimeRange::new(self.start, end, self.step, false)
108    }
109}
110
111/// An iterator over evenly spaced [`Dt`] values.
112///
113/// `TimeRange` is the time-domain equivalent of `std::iter::StepBy` or
114/// NumPy's `linspace` / `arange`. It supports both forward and backward
115/// iteration and implements [`ExactSizeIterator`].
116///
117/// # Construction
118///
119///
120/// ```
121/// use deep_time::{Dt, Scale, TimeRange};
122///
123/// let start = Dt::from_ymd(2000, 1, 1);
124/// let end = Dt::from_ymd(2000, 1, 2);
125/// let step = Dt::from_hr(1, Scale::TAI);
126///
127/// for timestamp in start.every(step).to_including(end) {
128///     println!("{:?}", timestamp.to_ymdhms(Scale::TAI));
129/// }
130///
131/// // Or use the explicit constructors:
132/// TimeRange::inclusive(start, end, step);
133/// TimeRange::exclusive(start, end, step);
134/// ```
135///
136/// # Iteration Behavior
137///
138/// - Zero step is handled gracefully (yields at most one element).
139/// - Negative steps are supported for reverse iteration.
140/// - The iterator is **lazy** and evaluates in constant time per step.
141/// - Implements [`DoubleEndedIterator`] and [`ExactSizeIterator`].
142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143#[cfg_attr(feature = "js", derive(tsify::Tsify))]
144#[derive(Clone, Copy, Debug, PartialEq)]
145pub struct TimeRange {
146    pub(crate) start: Dt,
147    pub(crate) current: Dt,
148    pub(crate) end: Dt,
149    pub(crate) step: Dt,
150    pub(crate) inclusive: bool,
151    pub(crate) finished: bool,
152}
153
154impl TimeRange {
155    /// Creates an **inclusive** evenly-spaced time range.
156    ///
157    /// The iterator will yield `end` if it is exactly reachable.
158    #[inline]
159    pub const fn inclusive(start: Dt, end: Dt, step: Dt) -> Self {
160        Self::new(start, end, step, true)
161    }
162
163    /// Creates an **exclusive** evenly-spaced time range.
164    ///
165    /// The iterator will **not** yield `end`.
166    #[inline]
167    pub const fn exclusive(start: Dt, end: Dt, step: Dt) -> Self {
168        Self::new(start, end, step, false)
169    }
170
171    /// Internal constructor.
172    #[inline]
173    pub const fn new(start: Dt, end: Dt, step: Dt, inclusive: bool) -> Self {
174        Self {
175            start,
176            current: start,
177            end,
178            step,
179            inclusive,
180            finished: false,
181        }
182    }
183}
184
185impl Iterator for TimeRange {
186    type Item = Dt;
187
188    /// Advances the iterator and returns the next [`Dt`].
189    ///
190    /// Returns `None` once the range has been exhausted.
191    fn next(&mut self) -> Option<Self::Item> {
192        if self.finished {
193            return None;
194        }
195
196        if self.step.is_zero() {
197            self.finished = true;
198            if self.start == self.end && self.inclusive {
199                return Some(self.start);
200            }
201            return None;
202        }
203
204        let item = self.current;
205
206        let to_end = self.current.to_diff_raw(self.end);
207        let step_positive = self.step.is_positive();
208
209        let beyond_end = if step_positive {
210            to_end > Dt::ZERO
211        } else {
212            to_end < Dt::ZERO
213        };
214
215        if beyond_end {
216            self.finished = true;
217            return None;
218        }
219
220        // Exclusive ranges must not yield `end` even when it is exactly reachable
221        if !self.inclusive && self.current == self.end {
222            self.finished = true;
223            return None;
224        }
225
226        self.current += self.step;
227        Some(item)
228    }
229
230    fn size_hint(&self) -> (usize, Option<usize>) {
231        let len = self.len();
232        (len, Some(len))
233    }
234}
235
236impl DoubleEndedIterator for TimeRange {
237    /// Returns the next element from the back of the range.
238    ///
239    /// This allows `TimeRange` to be used with `.rev()` and in
240    /// double-ended iteration contexts.
241    fn next_back(&mut self) -> Option<Self::Item> {
242        if self.finished {
243            return None;
244        }
245
246        let mut rev = *self;
247        rev.step = rev.step.neg();
248
249        let item = rev.next();
250
251        if item.is_some() {
252            self.current = rev.current;
253        }
254
255        item
256    }
257}
258
259impl ExactSizeIterator for TimeRange {
260    /// Returns the exact number of elements this iterator will yield.
261    ///
262    /// This is computed in constant time without iterating.
263    fn len(&self) -> usize {
264        if self.finished {
265            return 0;
266        }
267
268        if self.step.is_zero() {
269            return if self.current == self.end && self.inclusive {
270                1
271            } else {
272                0
273            };
274        }
275
276        // Mirror the yield decision from next()
277        let to_end = self.current.to_diff_raw(self.end);
278        let step_positive = self.step.is_positive();
279
280        let beyond_end = if step_positive {
281            to_end > Dt::ZERO
282        } else {
283            to_end < Dt::ZERO
284        };
285
286        if beyond_end {
287            return 0;
288        }
289
290        if !self.inclusive && self.current == self.end {
291            return 0;
292        }
293
294        // current is yieldable → compute remaining points
295        let diff = self.end.to_diff_raw(self.current);
296        let intervals = diff.abs_div_floor(self.step);
297
298        if self.inclusive {
299            intervals.saturating_add(1)
300        } else {
301            // For exclusive:
302            // - If we would land exactly on `end` after `intervals` steps → exclude it
303            // - Otherwise include the extra point
304            if intervals == 0 {
305                1
306            } else {
307                let reached = self.current + (self.step * (intervals as i64));
308                if reached == self.end {
309                    intervals
310                } else {
311                    intervals.saturating_add(1)
312                }
313            }
314        }
315    }
316}