use crate::format::TimeFormat;
use crate::foundation::duration::{DurationError, ExactDuration};
use crate::model::scale::CoordinateScale;
use crate::model::time::Time;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeSeriesError {
ZeroStep,
EmptyForwardRange,
DurationOverflow,
}
impl core::fmt::Display for TimeSeriesError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::ZeroStep => f.write_str("TimeSeries step must be non-zero"),
Self::EmptyForwardRange => {
f.write_str("TimeSeries::new requires end >= start; use new_with_step with a negative step for descending series")
}
Self::DurationOverflow => {
f.write_str("TimeSeries range exceeds i128 nanosecond capacity")
}
}
}
}
impl std::error::Error for TimeSeriesError {}
impl From<DurationError> for TimeSeriesError {
fn from(_: DurationError) -> Self {
Self::DurationOverflow
}
}
#[derive(Debug, Clone)]
pub struct TimeSeries<S: CoordinateScale, F: TimeFormat = crate::format::J2000s> {
start: Time<S, F>,
#[allow(dead_code)]
span_nanos: i128,
step_nanos: i128,
cursor: u64,
len: u64,
}
impl<S: CoordinateScale, F: TimeFormat> TimeSeries<S, F> {
pub fn new(
start: Time<S, F>,
end: Time<S, F>,
step: ExactDuration,
) -> Result<Self, TimeSeriesError> {
if step.is_zero() {
return Err(TimeSeriesError::ZeroStep);
}
let span = end.diff_exact(start)?;
let span_nanos = span.as_nanos_i128();
let step_nanos = step.as_nanos_i128();
if span_nanos == 0 {
return Ok(Self {
start,
span_nanos: 0,
step_nanos,
cursor: 0,
len: 0,
});
}
if span_nanos.signum() != step_nanos.signum() {
return Err(TimeSeriesError::EmptyForwardRange);
}
let len = {
let span_abs = span_nanos.unsigned_abs();
let step_abs = step_nanos.unsigned_abs();
let q = span_abs / step_abs;
let r = span_abs % step_abs;
if r == 0 {
if q > u64::MAX as u128 {
return Err(TimeSeriesError::DurationOverflow);
}
q as u64
} else {
if q >= u64::MAX as u128 {
return Err(TimeSeriesError::DurationOverflow);
}
(q + 1) as u64
}
};
Ok(Self {
start,
span_nanos,
step_nanos,
cursor: 0,
len,
})
}
pub fn new_with_step(
start: Time<S, F>,
end: Time<S, F>,
step: ExactDuration,
) -> Result<Self, TimeSeriesError> {
Self::new(start, end, step)
}
#[inline]
pub fn remaining(&self) -> u64 {
self.len.saturating_sub(self.cursor)
}
#[inline]
pub fn len_total(&self) -> u64 {
self.len
}
#[inline]
pub fn is_exhausted(&self) -> bool {
self.cursor >= self.len
}
pub fn nth_item(&self, n: u64) -> Option<Time<S, F>> {
if n >= self.len {
return None;
}
let total_nanos = (n as i128).checked_mul(self.step_nanos)?;
self.start
.try_add_exact(ExactDuration::from_nanos(total_nanos))
.ok()
}
}
impl<S: CoordinateScale, F: TimeFormat> Iterator for TimeSeries<S, F> {
type Item = Time<S, F>;
fn next(&mut self) -> Option<Self::Item> {
if self.is_exhausted() {
return None;
}
let item = self.nth_item(self.cursor)?;
self.cursor += 1;
Some(item)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.remaining();
let cap = remaining.min(usize::MAX as u64) as usize;
(cap, Some(cap))
}
fn count(self) -> usize {
self.remaining().min(usize::MAX as u64) as usize
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
self.cursor = self.cursor.saturating_add(n as u64);
self.next()
}
}
impl<S: CoordinateScale, F: TimeFormat> ExactSizeIterator for TimeSeries<S, F> {}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Time, TT};
use qtty::Second;
fn t(s: f64) -> Time<TT> {
Time::<TT>::from_raw_j2000_seconds(Second::new(s)).unwrap()
}
#[test]
fn ten_second_series() {
let s = TimeSeries::new(t(0.0), t(10.0), ExactDuration::SECOND).unwrap();
assert_eq!(s.len_total(), 10);
assert_eq!(s.count(), 10);
}
#[test]
fn zero_step_rejected() {
assert!(matches!(
TimeSeries::new(t(0.0), t(10.0), ExactDuration::ZERO),
Err(TimeSeriesError::ZeroStep)
));
}
#[test]
fn empty_forward_range_rejected() {
assert!(matches!(
TimeSeries::new(t(10.0), t(0.0), ExactDuration::SECOND),
Err(TimeSeriesError::EmptyForwardRange)
));
}
#[test]
fn empty_zero_span_returns_empty() {
let s = TimeSeries::new(t(5.0), t(5.0), ExactDuration::SECOND).unwrap();
assert_eq!(s.len_total(), 0);
assert_eq!(s.count(), 0);
}
#[test]
fn half_open_excludes_endpoint() {
let s = TimeSeries::new(t(0.0), t(3.0), ExactDuration::SECOND).unwrap();
let items: Vec<_> = s.collect();
assert_eq!(items.len(), 3);
let last = items.last().unwrap();
let secs = (last.raw_seconds_pair().0 + last.raw_seconds_pair().1).value();
assert!((secs - 2.0).abs() < 1e-9);
}
#[test]
fn non_dividing_step_yields_ceiling_count() {
let s = TimeSeries::new(t(0.0), t(3.5), ExactDuration::SECOND).unwrap();
assert_eq!(s.len_total(), 4);
}
#[test]
fn nth_item_is_deterministic() {
let s = TimeSeries::new(t(0.0), t(100.0), ExactDuration::SECOND).unwrap();
let got = s.nth_item(50).unwrap();
let secs = (got.raw_seconds_pair().0 + got.raw_seconds_pair().1).value();
assert!((secs - 50.0).abs() < 1e-9);
assert!(s.nth_item(100).is_none());
}
#[test]
fn reverse_step_iterates_downward() {
let s =
TimeSeries::new_with_step(t(10.0), t(0.0), ExactDuration::from_nanos(-1_000_000_000))
.unwrap();
assert_eq!(s.len_total(), 10);
let items: Vec<_> = s.collect();
let first = items.first().unwrap();
let last = items.last().unwrap();
let first_s = (first.raw_seconds_pair().0 + first.raw_seconds_pair().1).value();
let last_s = (last.raw_seconds_pair().0 + last.raw_seconds_pair().1).value();
assert!((first_s - 10.0).abs() < 1e-9);
assert!((last_s - 1.0).abs() < 1e-9);
}
#[test]
fn skip_via_nth() {
let mut s = TimeSeries::new(t(0.0), t(10.0), ExactDuration::SECOND).unwrap();
let third = s.nth(2).unwrap();
let secs = (third.raw_seconds_pair().0 + third.raw_seconds_pair().1).value();
assert!((secs - 2.0).abs() < 1e-9);
}
#[test]
fn no_drift_versus_nth_item() {
let series = TimeSeries::new(t(0.0), t(100.0), ExactDuration::SECOND).unwrap();
let items: Vec<_> = series.collect();
let fresh = TimeSeries::new(t(0.0), t(100.0), ExactDuration::SECOND).unwrap();
for (i, item) in items.iter().enumerate() {
let direct = fresh.nth_item(i as u64).unwrap();
let a = (item.raw_seconds_pair().0 + item.raw_seconds_pair().1).value();
let b = (direct.raw_seconds_pair().0 + direct.raw_seconds_pair().1).value();
assert_eq!(
a, b,
"iterator vs nth_item mismatch at index {i}: {a} vs {b}"
);
}
}
#[test]
fn nth_item_out_of_bounds_is_none() {
let s = TimeSeries::new(t(0.0), t(10.0), ExactDuration::SECOND).unwrap();
assert_eq!(s.len_total(), 10);
assert!(s.nth_item(10).is_none(), "expected None at len boundary");
assert!(s.nth_item(100).is_none(), "expected None well past end");
}
}