1use crate::format::TimeFormat;
25use crate::foundation::duration::{DurationError, ExactDuration};
26use crate::model::scale::CoordinateScale;
27use crate::model::time::Time;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum TimeSeriesError {
32 ZeroStep,
34 EmptyForwardRange,
38 DurationOverflow,
40}
41
42impl core::fmt::Display for TimeSeriesError {
43 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
44 match self {
45 Self::ZeroStep => f.write_str("TimeSeries step must be non-zero"),
46 Self::EmptyForwardRange => {
47 f.write_str("TimeSeries::new requires end >= start; use new_with_step with a negative step for descending series")
48 }
49 Self::DurationOverflow => {
50 f.write_str("TimeSeries range exceeds i128 nanosecond capacity")
51 }
52 }
53 }
54}
55
56impl std::error::Error for TimeSeriesError {}
57
58impl From<DurationError> for TimeSeriesError {
59 fn from(_: DurationError) -> Self {
60 Self::DurationOverflow
61 }
62}
63
64#[derive(Debug, Clone)]
70pub struct TimeSeries<S: CoordinateScale, F: TimeFormat = crate::format::J2000s> {
71 start: Time<S, F>,
72 #[allow(dead_code)]
73 span_nanos: i128,
76 step_nanos: i128,
77 cursor: u64,
79 len: u64,
81}
82
83impl<S: CoordinateScale, F: TimeFormat> TimeSeries<S, F> {
84 pub fn new(
89 start: Time<S, F>,
90 end: Time<S, F>,
91 step: ExactDuration,
92 ) -> Result<Self, TimeSeriesError> {
93 if step.is_zero() {
94 return Err(TimeSeriesError::ZeroStep);
95 }
96 let span = end.diff_exact(start)?;
97 let span_nanos = span.as_nanos_i128();
98 let step_nanos = step.as_nanos_i128();
99 if span_nanos == 0 {
100 return Ok(Self {
102 start,
103 span_nanos: 0,
104 step_nanos,
105 cursor: 0,
106 len: 0,
107 });
108 }
109 if span_nanos.signum() != step_nanos.signum() {
112 return Err(TimeSeriesError::EmptyForwardRange);
113 }
114 let len = {
121 let span_abs = span_nanos.unsigned_abs();
122 let step_abs = step_nanos.unsigned_abs();
123 let q = span_abs / step_abs;
124 let r = span_abs % step_abs;
125 if r == 0 {
126 if q > u64::MAX as u128 {
127 return Err(TimeSeriesError::DurationOverflow);
128 }
129 q as u64
130 } else {
131 if q >= u64::MAX as u128 {
132 return Err(TimeSeriesError::DurationOverflow);
133 }
134 (q + 1) as u64
135 }
136 };
137 Ok(Self {
138 start,
139 span_nanos,
140 step_nanos,
141 cursor: 0,
142 len,
143 })
144 }
145
146 pub fn new_with_step(
150 start: Time<S, F>,
151 end: Time<S, F>,
152 step: ExactDuration,
153 ) -> Result<Self, TimeSeriesError> {
154 Self::new(start, end, step)
155 }
156
157 #[inline]
159 pub fn remaining(&self) -> u64 {
160 self.len.saturating_sub(self.cursor)
161 }
162
163 #[inline]
165 pub fn len_total(&self) -> u64 {
166 self.len
167 }
168
169 #[inline]
171 pub fn is_exhausted(&self) -> bool {
172 self.cursor >= self.len
173 }
174
175 pub fn nth_item(&self, n: u64) -> Option<Time<S, F>> {
179 if n >= self.len {
180 return None;
181 }
182 let total_nanos = (n as i128).checked_mul(self.step_nanos)?;
183 self.start
184 .try_add_exact(ExactDuration::from_nanos(total_nanos))
185 .ok()
186 }
187}
188
189impl<S: CoordinateScale, F: TimeFormat> Iterator for TimeSeries<S, F> {
190 type Item = Time<S, F>;
191
192 fn next(&mut self) -> Option<Self::Item> {
193 if self.is_exhausted() {
194 return None;
195 }
196 let item = self.nth_item(self.cursor)?;
197 self.cursor += 1;
198 Some(item)
199 }
200
201 fn size_hint(&self) -> (usize, Option<usize>) {
202 let remaining = self.remaining();
203 let cap = remaining.min(usize::MAX as u64) as usize;
204 (cap, Some(cap))
205 }
206
207 fn count(self) -> usize {
208 self.remaining().min(usize::MAX as u64) as usize
209 }
210
211 fn nth(&mut self, n: usize) -> Option<Self::Item> {
212 self.cursor = self.cursor.saturating_add(n as u64);
213 self.next()
214 }
215}
216
217impl<S: CoordinateScale, F: TimeFormat> ExactSizeIterator for TimeSeries<S, F> {}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::{Time, TT};
223 use qtty::Second;
224
225 fn t(s: f64) -> Time<TT> {
226 Time::<TT>::from_raw_j2000_seconds(Second::new(s)).unwrap()
227 }
228
229 #[test]
230 fn ten_second_series() {
231 let s = TimeSeries::new(t(0.0), t(10.0), ExactDuration::SECOND).unwrap();
232 assert_eq!(s.len_total(), 10);
233 assert_eq!(s.count(), 10);
234 }
235
236 #[test]
237 fn zero_step_rejected() {
238 assert!(matches!(
239 TimeSeries::new(t(0.0), t(10.0), ExactDuration::ZERO),
240 Err(TimeSeriesError::ZeroStep)
241 ));
242 }
243
244 #[test]
245 fn empty_forward_range_rejected() {
246 assert!(matches!(
247 TimeSeries::new(t(10.0), t(0.0), ExactDuration::SECOND),
248 Err(TimeSeriesError::EmptyForwardRange)
249 ));
250 }
251
252 #[test]
253 fn empty_zero_span_returns_empty() {
254 let s = TimeSeries::new(t(5.0), t(5.0), ExactDuration::SECOND).unwrap();
255 assert_eq!(s.len_total(), 0);
256 assert_eq!(s.count(), 0);
257 }
258
259 #[test]
260 fn half_open_excludes_endpoint() {
261 let s = TimeSeries::new(t(0.0), t(3.0), ExactDuration::SECOND).unwrap();
262 let items: Vec<_> = s.collect();
263 assert_eq!(items.len(), 3);
264 let last = items.last().unwrap();
266 let secs = (last.raw_seconds_pair().0 + last.raw_seconds_pair().1).value();
267 assert!((secs - 2.0).abs() < 1e-9);
268 }
269
270 #[test]
271 fn non_dividing_step_yields_ceiling_count() {
272 let s = TimeSeries::new(t(0.0), t(3.5), ExactDuration::SECOND).unwrap();
274 assert_eq!(s.len_total(), 4);
275 }
276
277 #[test]
278 fn nth_item_is_deterministic() {
279 let s = TimeSeries::new(t(0.0), t(100.0), ExactDuration::SECOND).unwrap();
280 let got = s.nth_item(50).unwrap();
281 let secs = (got.raw_seconds_pair().0 + got.raw_seconds_pair().1).value();
282 assert!((secs - 50.0).abs() < 1e-9);
283 assert!(s.nth_item(100).is_none());
284 }
285
286 #[test]
287 fn reverse_step_iterates_downward() {
288 let s =
289 TimeSeries::new_with_step(t(10.0), t(0.0), ExactDuration::from_nanos(-1_000_000_000))
290 .unwrap();
291 assert_eq!(s.len_total(), 10);
292 let items: Vec<_> = s.collect();
293 let first = items.first().unwrap();
294 let last = items.last().unwrap();
295 let first_s = (first.raw_seconds_pair().0 + first.raw_seconds_pair().1).value();
296 let last_s = (last.raw_seconds_pair().0 + last.raw_seconds_pair().1).value();
297 assert!((first_s - 10.0).abs() < 1e-9);
298 assert!((last_s - 1.0).abs() < 1e-9);
299 }
300
301 #[test]
302 fn skip_via_nth() {
303 let mut s = TimeSeries::new(t(0.0), t(10.0), ExactDuration::SECOND).unwrap();
304 let third = s.nth(2).unwrap();
305 let secs = (third.raw_seconds_pair().0 + third.raw_seconds_pair().1).value();
306 assert!((secs - 2.0).abs() < 1e-9);
307 }
308
309 #[test]
311 fn no_drift_versus_nth_item() {
312 let series = TimeSeries::new(t(0.0), t(100.0), ExactDuration::SECOND).unwrap();
313 let items: Vec<_> = series.collect();
314 let fresh = TimeSeries::new(t(0.0), t(100.0), ExactDuration::SECOND).unwrap();
315 for (i, item) in items.iter().enumerate() {
316 let direct = fresh.nth_item(i as u64).unwrap();
317 let a = (item.raw_seconds_pair().0 + item.raw_seconds_pair().1).value();
318 let b = (direct.raw_seconds_pair().0 + direct.raw_seconds_pair().1).value();
319 assert_eq!(
320 a, b,
321 "iterator vs nth_item mismatch at index {i}: {a} vs {b}"
322 );
323 }
324 }
325
326 #[test]
328 fn nth_item_out_of_bounds_is_none() {
329 let s = TimeSeries::new(t(0.0), t(10.0), ExactDuration::SECOND).unwrap();
330 assert_eq!(s.len_total(), 10);
331 assert!(s.nth_item(10).is_none(), "expected None at len boundary");
332 assert!(s.nth_item(100).is_none(), "expected None well past end");
333 }
334}