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.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.clone();
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}