Skip to main content

deep_time/
time_range.rs

1//! High-precision evenly-spaced `Dt` iterator (the "linspace" for time).
2
3use crate::{Dt, Scale};
4
5/// Builder type that enables the ergonomic `start.every(step)` syntax.
6///
7/// This struct is created by [`Dt::every`] and is used to
8/// construct a [`TimeRange`] via either `.until(end)` (inclusive) or
9/// `.up_to(end)` (exclusive).
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11#[cfg_attr(feature = "js", derive(tsify::Tsify))]
12#[derive(Clone, Debug)]
13pub struct Every {
14    start: Dt,
15    step: Dt,
16}
17
18impl Dt {
19    /// Starts building an evenly-spaced time range.
20    ///
21    /// This method returns an [`Every`] builder that can be chained with
22    /// `.until(end)` or `.up_to(end)` to create a [`TimeRange`] iterator.
23    ///
24    /// # Example
25    ///
26    /// ```ignore
27    /// let start = Dt::from_gregorian(2025, 1, 1, 0, 0, 0, 0, Scale::TAI);
28    /// let step = Dt::from_hours(1);
29    ///
30    /// // Inclusive range: yields 25 points (including both start and end)
31    /// for t in start.every(step).until(end) { ... }
32    ///
33    /// // Exclusive range: yields 24 points
34    /// for t in start.every(step).up_to(end) { ... }
35    /// ```
36    #[inline]
37    pub const fn every(self, step: Dt) -> Every {
38        Every { start: self, step }
39    }
40
41    /// Creates an **inclusive** evenly-spaced range from `self` to `end`.
42    ///
43    /// Equivalent to `self.every(step).until(end)`.
44    #[inline]
45    pub const fn range_to(self, end: Dt, step: Dt) -> TimeRange {
46        TimeRange::inclusive(self, end, step)
47    }
48
49    /// Creates an **exclusive** evenly-spaced range from `self` to `end`.
50    ///
51    /// Equivalent to `self.every(step).up_to(end)`.
52    #[inline]
53    pub const fn range_until(self, end: Dt, step: Dt) -> TimeRange {
54        TimeRange::exclusive(self, end, step)
55    }
56
57    /// Creates a range stepping by whole seconds.
58    #[inline]
59    pub const fn every_second(self) -> Every {
60        self.every(Dt::from_sec(1, Scale::TAI))
61    }
62
63    /// Creates a range stepping by whole minutes.
64    #[inline]
65    pub const fn every_minute(self) -> Every {
66        self.every(Dt::from_min(1, Scale::TAI))
67    }
68
69    /// Creates a range stepping by whole hours.
70    #[inline]
71    pub const fn every_hour(self) -> Every {
72        self.every(Dt::from_hr(1, Scale::TAI))
73    }
74
75    /// Creates a range stepping by whole days.
76    #[inline]
77    pub const fn every_day(self) -> Every {
78        self.every(Dt::from_hr(24, Scale::TAI))
79    }
80
81    /// Returns the next `n` points **after** `self` (exclusive of `self`)
82    /// at the given step.
83    ///
84    /// This is a convenient way to get future points without including the start.
85    #[inline]
86    pub fn next_n(self, n: usize, step: Dt) -> impl Iterator<Item = Dt> {
87        (self + step).for_n_steps(n, step)
88    }
89
90    /// Returns an iterator yielding exactly `n` evenly spaced points
91    /// starting from `self`.
92    ///
93    /// This is a convenient one-liner for the common "next N steps" pattern.
94    #[inline]
95    pub fn for_n_steps(self, n: usize, step: Dt) -> impl Iterator<Item = Dt> {
96        // We create an exclusive range long enough for n steps, then limit it
97        let end = self + step * (n as i64);
98        TimeRange::exclusive(self, end, step).take(n)
99    }
100}
101
102impl Every {
103    /// Creates an **inclusive** time range (`start ... end`).
104    ///
105    /// The resulting iterator will yield `end` as the final element
106    /// (provided `end` is reachable from `start` with the given step).
107    #[inline]
108    pub fn until(self, end: Dt) -> TimeRange {
109        TimeRange::new(self.start, end, self.step, true)
110    }
111
112    /// Creates an **exclusive** time range (`start ... end`).
113    ///
114    /// The resulting iterator will **not** yield `end`.
115    #[inline]
116    pub fn up_to(self, end: Dt) -> TimeRange {
117        TimeRange::new(self.start, end, self.step, false)
118    }
119
120    /// Creates a **descending** inclusive range.
121    ///
122    /// Example: `start.every(-1.hour()).down_to(earlier_time)`
123    #[inline]
124    pub fn down_to(self, end: Dt) -> TimeRange {
125        TimeRange::new(self.start, end, self.step, true)
126    }
127}
128
129#[cfg(feature = "wire")]
130impl Every {
131    /// Size of the canonical wire representation in bytes (33 bytes).
132    pub const WIRE_SIZE: usize = Dt::WIRE_SIZE + Dt::WIRE_SIZE;
133
134    /// Serializes this `Every` builder into a fixed 33-byte buffer.
135    ///
136    /// The layout is simply the concatenation of `start` (17 bytes) and `step` (16 bytes).
137    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
138        let mut buf = [0u8; Self::WIRE_SIZE];
139        let start = self.start.to_wire_bytes();
140        let step = self.step.to_wire_bytes();
141        buf[0..17].copy_from_slice(&start);
142        buf[17..33].copy_from_slice(&step);
143        buf
144    }
145
146    /// Deserializes an `Every` builder from exactly 33 bytes.
147    ///
148    /// ## Security
149    ///
150    /// Safe for untrusted input. Fixed size with strict validation
151    /// of the inner `Dt` and `Dt`.
152    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
153        if bytes.len() != Self::WIRE_SIZE {
154            return None;
155        }
156        let start = Dt::from_wire_bytes(&bytes[0..17])?;
157        let step = Dt::from_wire_bytes(&bytes[17..33])?;
158        Some(Self { start, step })
159    }
160}
161
162/// An iterator over evenly spaced [`Dt`] values.
163///
164/// `TimeRange` is the time-domain equivalent of `std::iter::StepBy` or
165/// NumPy's `linspace` / `arange`. It supports both forward and backward
166/// iteration and implements [`ExactSizeIterator`].
167///
168/// # Construction
169///
170/// Prefer the ergonomic builder syntax:
171///
172/// ```ignore
173/// start.every(step).until(end)   // inclusive
174/// start.every(step).up_to(end)   // exclusive
175/// ```
176///
177/// Or use the explicit constructors:
178///
179/// ```ignore
180/// TimeRange::inclusive(start, end, step)
181/// TimeRange::exclusive(start, end, step)
182/// ```
183///
184/// # Iteration Behavior
185///
186/// - Zero step is handled gracefully (yields at most one element).
187/// - Negative steps are supported for reverse iteration.
188/// - The iterator is **lazy** and evaluates in constant time per step.
189/// - Implements [`DoubleEndedIterator`] and [`ExactSizeIterator`].
190#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
191#[cfg_attr(feature = "js", derive(tsify::Tsify))]
192#[derive(Clone, Copy, Debug, PartialEq)]
193pub struct TimeRange {
194    start: Dt,
195    current: Dt,
196    end: Dt,
197    step: Dt,
198    inclusive: bool,
199    finished: bool,
200}
201
202impl TimeRange {
203    /// Creates an **inclusive** evenly-spaced time range.
204    ///
205    /// The iterator will yield `end` if it is exactly reachable.
206    #[inline]
207    pub const fn inclusive(start: Dt, end: Dt, step: Dt) -> Self {
208        Self::new(start, end, step, true)
209    }
210
211    /// Creates an **exclusive** evenly-spaced time range.
212    ///
213    /// The iterator will **not** yield `end`.
214    #[inline]
215    pub const fn exclusive(start: Dt, end: Dt, step: Dt) -> Self {
216        Self::new(start, end, step, false)
217    }
218
219    /// Internal constructor.
220    #[inline]
221    const fn new(start: Dt, end: Dt, step: Dt, inclusive: bool) -> Self {
222        Self {
223            start,
224            current: start,
225            end,
226            step,
227            inclusive,
228            finished: false,
229        }
230    }
231}
232
233impl Iterator for TimeRange {
234    type Item = Dt;
235
236    /// Advances the iterator and returns the next [`Dt`].
237    ///
238    /// Returns `None` once the range has been exhausted.
239    fn next(&mut self) -> Option<Self::Item> {
240        if self.finished {
241            return None;
242        }
243
244        let item = self.current;
245
246        let to_end = self.current.to_diff_raw(self.end);
247        let passed = if self.step.is_zero() {
248            true
249        } else if self.step.sec > 0 || (self.step.sec == 0 && self.step.attos > 0) {
250            to_end > Dt::ZERO
251        } else {
252            to_end < Dt::ZERO
253        };
254
255        if passed {
256            self.finished = true;
257            if self.inclusive && self.current == self.end {
258                return Some(item);
259            }
260            return None;
261        }
262
263        self.current += self.step;
264        Some(item)
265    }
266}
267
268impl DoubleEndedIterator for TimeRange {
269    /// Returns the next element from the back of the range.
270    ///
271    /// This allows `TimeRange` to be used with `.rev()` and in
272    /// double-ended iteration contexts.
273    fn next_back(&mut self) -> Option<Self::Item> {
274        if self.finished {
275            return None;
276        }
277
278        let mut rev = *self;
279        rev.step = rev.step.neg();
280
281        let item = rev.next();
282
283        if item.is_some() {
284            self.current = rev.current;
285        }
286
287        item
288    }
289}
290
291impl ExactSizeIterator for TimeRange {
292    /// Returns the exact number of elements this iterator will yield.
293    ///
294    /// This is computed in constant time without iterating.
295    fn len(&self) -> usize {
296        if self.step.is_zero() {
297            return if self.start == self.end && self.inclusive {
298                1
299            } else {
300                0
301            };
302        }
303
304        let total = self.end.to_diff_raw(self.start);
305        let steps = total.abs_div_floor(self.step);
306
307        if self.inclusive {
308            steps.saturating_add(1)
309        } else {
310            steps
311        }
312    }
313}
314
315#[cfg(feature = "wire")]
316impl TimeRange {
317    /// Current wire format version.
318    pub const WIRE_VERSION: u8 = 1;
319
320    /// Size of the canonical wire representation in bytes.
321    /// Only the logical definition is stored (runtime state is not serialized).
322    pub const WIRE_SIZE: usize = 1 + 2 * Dt::WIRE_SIZE + Dt::WIRE_SIZE + 1;
323
324    /// Serializes this `TimeRange` into a fixed buffer.
325    ///
326    /// Only the logical definition is stored:
327    /// - `start` + `end` + `step` + `inclusive` flag
328    ///
329    /// Runtime iterator state (`current`, `finished`) is **not** serialized.
330    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
331        let mut buf = [0u8; Self::WIRE_SIZE];
332        buf[0] = Self::WIRE_VERSION;
333
334        let start = self.start.to_wire_bytes();
335        let end = self.end.to_wire_bytes();
336        let step = self.step.to_wire_bytes();
337
338        let tp_size = Dt::WIRE_SIZE;
339        let span_size = Dt::WIRE_SIZE;
340
341        buf[1..1 + tp_size].copy_from_slice(&start);
342        buf[1 + tp_size..1 + 2 * tp_size].copy_from_slice(&end);
343        buf[1 + 2 * tp_size..1 + 2 * tp_size + span_size].copy_from_slice(&step);
344        buf[1 + 2 * tp_size + span_size] = if self.inclusive { 1 } else { 0 };
345
346        buf
347    }
348
349    /// Deserializes a `TimeRange` from exactly `WIRE_SIZE` bytes.
350    ///
351    /// The iterator is reconstructed in its initial state
352    /// (`current = start`, `finished = false`).
353    ///
354    /// Returns `None` if the version is unknown or any component is invalid.
355    ///
356    /// ## Security
357    ///
358    /// Safe for untrusted input. Fixed size with layered validation
359    /// of all inner types. No runtime iterator state is accepted from the wire.
360    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
361        if bytes.len() != Self::WIRE_SIZE {
362            return None;
363        }
364
365        if bytes[0] != Self::WIRE_VERSION {
366            return None;
367        }
368
369        let tp_size = Dt::WIRE_SIZE;
370        let span_size = Dt::WIRE_SIZE;
371
372        let start = Dt::from_wire_bytes(&bytes[1..1 + tp_size])?;
373        let end = Dt::from_wire_bytes(&bytes[1 + tp_size..1 + 2 * tp_size])?;
374        let step = Dt::from_wire_bytes(&bytes[1 + 2 * tp_size..1 + 2 * tp_size + span_size])?;
375        let inclusive = bytes[1 + 2 * tp_size + span_size] != 0;
376
377        Some(Self::new(start, end, step, inclusive))
378    }
379}