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