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/// use deep_time::{Dt, Scale, TimeRange};
121///
122/// let start = Dt::from_ymd(2000, 1, 1);
123/// let end = Dt::from_ymd(2000, 1, 2);
124/// let step = Dt::from_hr(1, Scale::TAI);
125///
126/// for timestamp in start.every(step).to_including(end) {
127///     println!("{:?}", timestamp.to_ymdhms(Scale::TAI));
128/// }
129///
130/// // Or use the explicit constructors:
131/// TimeRange::inclusive(start, end, step);
132/// TimeRange::exclusive(start, end, step);
133/// ```
134///
135/// ## Iteration Behavior
136///
137/// - Zero step is handled gracefully (yields at most one element).
138/// - Negative steps are supported for reverse iteration.
139/// - The iterator is **lazy** and evaluates in constant time per step.
140/// - Implements [`DoubleEndedIterator`] and [`ExactSizeIterator`].
141#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
142#[cfg_attr(feature = "js", derive(tsify::Tsify))]
143#[derive(Clone, Copy, Debug, PartialEq)]
144pub struct TimeRange {
145    pub(crate) start: Dt,
146    pub(crate) current: Dt,
147    pub(crate) end: Dt,
148    pub(crate) step: Dt,
149    pub(crate) inclusive: bool,
150    pub(crate) finished: bool,
151}
152
153impl TimeRange {
154    /// Creates an **inclusive** evenly-spaced time range.
155    ///
156    /// The iterator will yield `end` if it is exactly reachable.
157    #[inline]
158    pub const fn inclusive(start: Dt, end: Dt, step: Dt) -> Self {
159        Self::new(start, end, step, true)
160    }
161
162    /// Creates an **exclusive** evenly-spaced time range.
163    ///
164    /// The iterator will **not** yield `end`.
165    #[inline]
166    pub const fn exclusive(start: Dt, end: Dt, step: Dt) -> Self {
167        Self::new(start, end, step, false)
168    }
169
170    /// Internal constructor.
171    #[inline]
172    pub const fn new(start: Dt, end: Dt, step: Dt, inclusive: bool) -> Self {
173        Self {
174            start,
175            current: start,
176            end,
177            step,
178            inclusive,
179            finished: false,
180        }
181    }
182}
183
184impl Iterator for TimeRange {
185    type Item = Dt;
186
187    /// Advances the iterator and returns the next [`Dt`].
188    ///
189    /// Returns `None` once the range has been exhausted.
190    fn next(&mut self) -> Option<Self::Item> {
191        if self.finished {
192            return None;
193        }
194
195        if self.step.is_zero() {
196            self.finished = true;
197            if self.start == self.end && self.inclusive {
198                return Some(self.start);
199            }
200            return None;
201        }
202
203        let item = self.current;
204
205        let to_end = self.current.to_diff_raw(self.end);
206        let step_positive = self.step.is_positive();
207
208        let beyond_end = if step_positive {
209            to_end > Dt::ZERO
210        } else {
211            to_end < Dt::ZERO
212        };
213
214        if beyond_end {
215            self.finished = true;
216            return None;
217        }
218
219        // Exclusive ranges must not yield `end` even when it is exactly reachable
220        if !self.inclusive && self.current == self.end {
221            self.finished = true;
222            return None;
223        }
224
225        self.current += self.step;
226        Some(item)
227    }
228
229    fn size_hint(&self) -> (usize, Option<usize>) {
230        let len = self.len();
231        (len, Some(len))
232    }
233}
234
235impl DoubleEndedIterator for TimeRange {
236    /// Returns the next element from the back of the range.
237    ///
238    /// This allows `TimeRange` to be used with `.rev()` and in
239    /// double-ended iteration contexts.
240    fn next_back(&mut self) -> Option<Self::Item> {
241        if self.finished {
242            return None;
243        }
244
245        let mut rev = *self;
246        rev.step = rev.step.neg();
247
248        let item = rev.next();
249
250        if item.is_some() {
251            self.current = rev.current;
252        }
253
254        item
255    }
256}
257
258impl ExactSizeIterator for TimeRange {
259    /// Returns the exact number of elements this iterator will yield.
260    ///
261    /// This is computed in constant time without iterating.
262    fn len(&self) -> usize {
263        if self.finished {
264            return 0;
265        }
266
267        if self.step.is_zero() {
268            return if self.current == self.end && self.inclusive {
269                1
270            } else {
271                0
272            };
273        }
274
275        // Mirror the yield decision from next()
276        let to_end = self.current.to_diff_raw(self.end);
277        let step_positive = self.step.is_positive();
278
279        let beyond_end = if step_positive {
280            to_end > Dt::ZERO
281        } else {
282            to_end < Dt::ZERO
283        };
284
285        if beyond_end {
286            return 0;
287        }
288
289        if !self.inclusive && self.current == self.end {
290            return 0;
291        }
292
293        // current is yieldable → compute remaining points
294        let diff = self.end.to_diff_raw(self.current);
295        let intervals = diff.abs_div_floor(self.step);
296
297        if self.inclusive {
298            intervals.saturating_add(1)
299        } else {
300            // For exclusive:
301            // - If we would land exactly on `end` after `intervals` steps → exclude it
302            // - Otherwise include the extra point
303            if intervals == 0 {
304                1
305            } else {
306                let reached = self.current + (self.step * (intervals as i64));
307                if reached == self.end {
308                    intervals
309                } else {
310                    intervals.saturating_add(1)
311                }
312            }
313        }
314    }
315}