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 start: Dt,
13 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 start: Dt,
147 current: Dt,
148 end: Dt,
149 step: Dt,
150 inclusive: bool,
151 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 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}
317
318#[cfg(feature = "wire")]
319impl Every {
320 /// Size of the canonical wire representation in bytes (33 bytes).
321 pub const WIRE_SIZE: usize = Dt::WIRE_SIZE + Dt::WIRE_SIZE;
322
323 /// Serializes this `Every` builder into a fixed 33-byte buffer.
324 ///
325 /// The layout is simply the concatenation of `start` (17 bytes) and `step` (16 bytes).
326 pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
327 let mut buf = [0u8; Self::WIRE_SIZE];
328 let start = self.start.to_wire_bytes();
329 let step = self.step.to_wire_bytes();
330 buf[0..17].copy_from_slice(&start);
331 buf[17..33].copy_from_slice(&step);
332 buf
333 }
334
335 /// Deserializes an `Every` builder from exactly 33 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 start = Dt::from_wire_bytes(&bytes[0..17])?;
346 let step = Dt::from_wire_bytes(&bytes[17..33])?;
347 Some(Self { start, step })
348 }
349}
350
351#[cfg(feature = "wire")]
352impl TimeRange {
353 /// Current wire format version.
354 pub const WIRE_VERSION: u8 = 1;
355
356 /// Size of the canonical wire representation in bytes.
357 /// Only the logical definition is stored (runtime state is not serialized).
358 pub const WIRE_SIZE: usize = 1 + 2 * Dt::WIRE_SIZE + Dt::WIRE_SIZE + 1;
359
360 /// Serializes this `TimeRange` into a fixed buffer.
361 ///
362 /// Only the logical definition is stored:
363 /// - `start` + `end` + `step` + `inclusive` flag
364 ///
365 /// Runtime iterator state (`current`, `finished`) is **not** serialized.
366 pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
367 let mut buf = [0u8; Self::WIRE_SIZE];
368 buf[0] = Self::WIRE_VERSION;
369
370 let start = self.start.to_wire_bytes();
371 let end = self.end.to_wire_bytes();
372 let step = self.step.to_wire_bytes();
373
374 let tp_size = Dt::WIRE_SIZE;
375 let span_size = Dt::WIRE_SIZE;
376
377 buf[1..1 + tp_size].copy_from_slice(&start);
378 buf[1 + tp_size..1 + 2 * tp_size].copy_from_slice(&end);
379 buf[1 + 2 * tp_size..1 + 2 * tp_size + span_size].copy_from_slice(&step);
380 buf[1 + 2 * tp_size + span_size] = if self.inclusive { 1 } else { 0 };
381
382 buf
383 }
384
385 /// Deserializes a `TimeRange` from exactly `WIRE_SIZE` bytes.
386 ///
387 /// The iterator is reconstructed in its initial state
388 /// (`current = start`, `finished = false`).
389 ///
390 /// Returns `None` if the version is unknown or any component is invalid.
391 ///
392 /// ## Security
393 ///
394 /// Safe for untrusted input. Fixed size with layered validation
395 /// of all inner types. No runtime iterator state is accepted from the wire.
396 pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
397 if bytes.len() != Self::WIRE_SIZE {
398 return None;
399 }
400
401 if bytes[0] != Self::WIRE_VERSION {
402 return None;
403 }
404
405 let tp_size = Dt::WIRE_SIZE;
406 let span_size = Dt::WIRE_SIZE;
407
408 let start = Dt::from_wire_bytes(&bytes[1..1 + tp_size])?;
409 let end = Dt::from_wire_bytes(&bytes[1 + tp_size..1 + 2 * tp_size])?;
410 let step = Dt::from_wire_bytes(&bytes[1 + 2 * tp_size..1 + 2 * tp_size + span_size])?;
411 let inclusive = bytes[1 + 2 * tp_size + span_size] != 0;
412
413 Some(Self::new(start, end, step, inclusive))
414 }
415}