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 = "tsify", 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 /// ## Examples
23 ///
24 /// ```rust
25 /// use deep_time::{Dt, Scale};
26 ///
27 /// let start = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
28 /// let end = Dt::from_ymd(2000, 1, 2, Scale::UTC, 0, 0, 0, 0);
29 /// let step = Dt::from_hr(1, Scale::TAI);
30 ///
31 /// for timestamp in start.every(step).to_including(end) {
32 /// println!("{:?}", timestamp.to_ymd());
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/// ```rust
120/// use deep_time::{Dt, Scale, TimeRange};
121///
122/// let start = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
123/// let end = Dt::from_ymd(2000, 1, 2, Scale::UTC, 0, 0, 0, 0);
124/// let step = Dt::from_hr(1, Scale::TAI);
125///
126/// for timestamp in start.every(step).to_including(end) {
127/// println!("{:?}", timestamp.to_ymd());
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 = "tsify", 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) -> TimeRange {
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) -> TimeRange {
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) -> TimeRange {
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}
316
317#[cfg(feature = "wire")]
318impl Every {
319 /// Size of the canonical wire representation in bytes (`2 × Dt::WIRE_SIZE`).
320 pub const WIRE_SIZE: usize = Dt::WIRE_SIZE + Dt::WIRE_SIZE;
321
322 /// Serializes this `Every` builder into a fixed buffer.
323 ///
324 /// The layout is the concatenation of `start` and `step` wire encodings.
325 pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
326 let mut buf = [0u8; Self::WIRE_SIZE];
327 let start = self.start.to_wire_bytes();
328 let step = self.step.to_wire_bytes();
329 let n = Dt::WIRE_SIZE;
330 buf[0..n].copy_from_slice(&start);
331 buf[n..2 * n].copy_from_slice(&step);
332 buf
333 }
334
335 /// Deserializes an `Every` builder from exactly `WIRE_SIZE` bytes.
336 ///
337 /// ## Security
338 ///
339 /// Safe for untrusted input. Fixed size with strict validation
340 /// of the inner `Dt` and `Dt`.
341 pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
342 if bytes.len() != Self::WIRE_SIZE {
343 return None;
344 }
345 let n = Dt::WIRE_SIZE;
346 let start = Dt::from_wire_bytes(&bytes[0..n])?;
347 let step = Dt::from_wire_bytes(&bytes[n..2 * n])?;
348 Some(Self { start, step })
349 }
350}
351
352#[cfg(feature = "wire")]
353impl TimeRange {
354 /// Current wire format version.
355 pub const WIRE_VERSION: u8 = 1;
356
357 /// Size of the canonical wire representation in bytes.
358 /// Only the logical definition is stored (runtime state is not serialized).
359 pub const WIRE_SIZE: usize = 1 + 2 * Dt::WIRE_SIZE + Dt::WIRE_SIZE + 1;
360
361 /// Serializes this `TimeRange` into a fixed buffer.
362 ///
363 /// Only the logical definition is stored:
364 /// - `start` + `end` + `step` + `inclusive` flag
365 ///
366 /// Runtime iterator state (`current`, `finished`) is **not** serialized.
367 pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
368 let mut buf = [0u8; Self::WIRE_SIZE];
369 buf[0] = Self::WIRE_VERSION;
370
371 let start = self.start.to_wire_bytes();
372 let end = self.end.to_wire_bytes();
373 let step = self.step.to_wire_bytes();
374
375 let tp_size = Dt::WIRE_SIZE;
376 let span_size = Dt::WIRE_SIZE;
377
378 buf[1..1 + tp_size].copy_from_slice(&start);
379 buf[1 + tp_size..1 + 2 * tp_size].copy_from_slice(&end);
380 buf[1 + 2 * tp_size..1 + 2 * tp_size + span_size].copy_from_slice(&step);
381 buf[1 + 2 * tp_size + span_size] = if self.inclusive { 1 } else { 0 };
382
383 buf
384 }
385
386 /// Deserializes a `TimeRange` from exactly `WIRE_SIZE` bytes.
387 ///
388 /// The iterator is reconstructed in its initial state
389 /// (`current = start`, `finished = false`).
390 ///
391 /// Returns `None` if the version is unknown or any component is invalid.
392 ///
393 /// ## Security
394 ///
395 /// Safe for untrusted input. Fixed size with layered validation
396 /// of all inner types. No runtime iterator state is accepted from the wire.
397 pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
398 if bytes.len() != Self::WIRE_SIZE {
399 return None;
400 }
401
402 if bytes[0] != Self::WIRE_VERSION {
403 return None;
404 }
405
406 let tp_size = Dt::WIRE_SIZE;
407 let span_size = Dt::WIRE_SIZE;
408
409 let start = Dt::from_wire_bytes(&bytes[1..1 + tp_size])?;
410 let end = Dt::from_wire_bytes(&bytes[1 + tp_size..1 + 2 * tp_size])?;
411 let step = Dt::from_wire_bytes(&bytes[1 + 2 * tp_size..1 + 2 * tp_size + span_size])?;
412 let inclusive = bytes[1 + 2 * tp_size + span_size] != 0;
413
414 Some(Self::new(start, end, step, inclusive))
415 }
416}