1use core::fmt;
12
13use alloc::vec::Vec;
14
15use crate::Time;
16
17mod error;
18pub mod series;
19pub use error::{InvalidIntervalError, PeriodListError};
20pub use series::{TimeSeries, TimeSeriesError};
21
22#[inline]
23fn partial_max<T: PartialOrd + Copy>(a: T, b: T) -> T {
24 if a >= b {
25 a
26 } else {
27 b
28 }
29}
30
31#[inline]
32fn partial_min<T: PartialOrd + Copy>(a: T, b: T) -> T {
33 if a <= b {
34 a
35 } else {
36 b
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct Interval<T: Copy + PartialOrd> {
46 pub start: T,
48 pub end: T,
50}
51
52impl<T> fmt::Display for Interval<T>
53where
54 T: Copy + PartialOrd + fmt::Display,
55{
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "[{}, {})", self.start, self.end)
58 }
59}
60
61pub type Period<S> = Interval<Time<S>>;
66
67impl<T: Copy + PartialOrd> Interval<T> {
68 #[inline]
74 pub fn new<S: Into<T>, E: Into<T>>(start: S, end: E) -> Self {
75 Self {
76 start: start.into(),
77 end: end.into(),
78 }
79 }
80
81 #[inline]
85 pub fn try_new<S: Into<T>, E: Into<T>>(start: S, end: E) -> Result<Self, InvalidIntervalError> {
86 let start = start.into();
87 let end = end.into();
88 if start <= end {
89 Ok(Self { start, end })
90 } else {
91 Err(InvalidIntervalError::StartAfterEnd)
92 }
93 }
94
95 #[inline]
100 pub fn intersection(&self, other: &Self) -> Option<Self> {
101 let start = partial_max(self.start, other.start);
102 let end = partial_min(self.end, other.end);
103 if start < end {
104 Some(Self::new(start, end))
105 } else {
106 None
107 }
108 }
109
110 #[inline]
119 pub fn union(&self, other: &Self) -> Vec<Self> {
120 if self.start <= other.end && other.start <= self.end {
121 vec![Self::new(
122 partial_min(self.start, other.start),
123 partial_max(self.end, other.end),
124 )]
125 } else if self.start <= other.start {
126 vec![*self, *other]
127 } else {
128 vec![*other, *self]
129 }
130 }
131
132 #[inline]
140 pub fn complement(&self, periods: &[Self]) -> Vec<Self> {
141 let mut gaps = Vec::with_capacity(periods.len().saturating_add(1));
142 let mut cursor = self.start;
143 for p in periods {
144 if p.end <= cursor {
145 continue;
146 }
147 if p.start >= self.end {
148 break;
149 }
150 if p.start > cursor {
151 gaps.push(Self::new(cursor, p.start));
152 }
153 if p.end >= self.end {
154 return gaps;
155 }
156 cursor = p.end;
157 }
158 if cursor < self.end {
159 gaps.push(Self::new(cursor, self.end));
160 }
161 gaps
162 }
163
164 #[inline]
169 pub fn try_complement(&self, periods: &[Self]) -> Result<Vec<Self>, PeriodListError> {
170 if self
171 .start
172 .partial_cmp(&self.end)
173 .is_none_or(|ordering| ordering == core::cmp::Ordering::Greater)
174 {
175 return Err(PeriodListError::InvalidInterval { index: 0 });
176 }
177 Self::validate(periods)?;
178 Ok(self.complement(periods))
179 }
180
181 pub fn validate(periods: &[Self]) -> Result<(), PeriodListError> {
187 let mut prev: Option<Interval<T>> = None;
188 for (i, period) in periods.iter().copied().enumerate() {
189 if period
190 .start
191 .partial_cmp(&period.end)
192 .is_none_or(|ordering| ordering == core::cmp::Ordering::Greater)
193 {
194 return Err(PeriodListError::InvalidInterval { index: i });
195 }
196 if let Some(previous) = prev {
197 if previous
198 .start
199 .partial_cmp(&period.start)
200 .is_none_or(|ordering| ordering == core::cmp::Ordering::Greater)
201 {
202 return Err(PeriodListError::Unsorted { index: i });
203 }
204 if previous.end > period.start {
205 return Err(PeriodListError::Overlapping { index: i });
206 }
207 }
208 prev = Some(period);
209 }
210 Ok(())
211 }
212
213 pub fn intersect_many(a: &[Self], b: &[Self]) -> Vec<Self> {
215 let mut result = Vec::with_capacity(a.len().min(b.len()));
216 let (mut i, mut j) = (0, 0);
217 while i < a.len() && j < b.len() {
218 let start = partial_max(a[i].start, b[j].start);
219 let end = partial_min(a[i].end, b[j].end);
220 if start < end {
221 result.push(Self::new(start, end));
222 }
223 if a[i].end <= b[j].end {
224 i += 1;
225 } else {
226 j += 1;
227 }
228 }
229 result
230 }
231
232 pub fn try_intersect_many(a: &[Self], b: &[Self]) -> Result<Vec<Self>, PeriodListError> {
236 Self::validate(a)?;
237 Self::validate(b)?;
238 Ok(Self::intersect_many(a, b))
239 }
240
241 pub fn union_many(a: &[Self], b: &[Self]) -> Vec<Self> {
246 let mut combined: Vec<Self> = a.iter().chain(b.iter()).copied().collect();
247 combined.sort_unstable_by(|x, y| {
248 x.start
249 .partial_cmp(&y.start)
250 .unwrap_or(core::cmp::Ordering::Equal)
251 });
252 Self::normalize(&combined)
253 }
254
255 pub fn normalize(periods: &[Self]) -> Vec<Self> {
260 if periods.is_empty() {
261 return Vec::new();
262 }
263 let mut sorted: Vec<_> = periods.to_vec();
264 sorted.sort_unstable_by(|a, b| {
265 a.start
266 .partial_cmp(&b.start)
267 .unwrap_or(core::cmp::Ordering::Equal)
268 });
269 let mut merged = Vec::with_capacity(sorted.len());
270 merged.push(sorted[0]);
271 for period in sorted.into_iter().skip(1) {
272 if let Some(last) = merged.last_mut() {
273 if period.start <= last.end {
274 if period.end > last.end {
275 last.end = period.end;
276 }
277 } else {
278 merged.push(period);
279 }
280 }
281 }
282 merged
283 }
284
285 pub fn length<U>(&self) -> crate::qtty::Quantity<U>
286 where
287 T: core::ops::Sub<Output = crate::qtty::Quantity<U>>,
288 U: crate::qtty::time::TimeUnit,
289 {
290 self.end - self.start
291 }
292
293 #[inline]
295 pub fn duration<U>(&self) -> crate::qtty::Quantity<U>
296 where
297 T: core::ops::Sub<Output = crate::qtty::Quantity<U>>,
298 U: crate::qtty::time::TimeUnit,
299 {
300 self.length()
301 }
302}
303
304#[inline]
326pub fn complement_within<T: Copy + PartialOrd>(
327 outer: Interval<T>,
328 periods: &[Interval<T>],
329) -> Vec<Interval<T>> {
330 outer.complement(periods)
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 #[cfg(feature = "serde")]
337 use crate::format::JulianDate;
338 use crate::format::{ModifiedJulianDate, MJD};
339 use crate::qtty::Day;
340 #[cfg(feature = "serde")]
341 use crate::Time;
342 use crate::TT;
343 #[cfg(feature = "serde")]
344 use serde::de::IntoDeserializer;
345 #[cfg(feature = "serde")]
346 use serde::Deserialize;
347 #[cfg(feature = "serde")]
348 use serde_json::json;
349
350 #[test]
351 fn try_new_rejects_reversed() {
352 assert_eq!(
353 Interval::<f64>::try_new(2.0_f64, 1.0).unwrap_err(),
354 InvalidIntervalError::StartAfterEnd
355 );
356 }
357
358 #[test]
359 fn try_new_rejects_nan() {
360 assert!(Interval::<f64>::try_new(f64::NAN, 0.0).is_err());
361 }
362
363 #[test]
364 fn intersection_half_open() {
365 let a = Interval::<f64>::new(0.0_f64, 10.0);
366 let b = Interval::<f64>::new(10.0, 20.0);
367 assert!(a.intersection(&b).is_none());
368 let c = Interval::<f64>::new(5.0_f64, 15.0);
369 let x = a.intersection(&c).unwrap();
370 assert_eq!(x.start, 5.0);
371 assert_eq!(x.end, 10.0);
372 }
373
374 #[test]
375 fn complement_covers_gaps() {
376 let outer = Interval::<f64>::new(0.0_f64, 10.0);
377 let inside = vec![
378 Interval::<f64>::new(1.0_f64, 2.0),
379 Interval::<f64>::new(4.0, 6.0),
380 ];
381 let gaps = outer.complement(&inside);
382 assert_eq!(gaps.len(), 3);
383 assert_eq!(gaps[0], Interval::<f64>::new(0.0, 1.0));
384 assert_eq!(gaps[1], Interval::<f64>::new(2.0, 4.0));
385 assert_eq!(gaps[2], Interval::<f64>::new(6.0, 10.0));
386 }
387
388 #[test]
389 fn complement_ignores_periods_after_outer_interval() {
390 let outer = Interval::<f64>::new(10.0_f64, 20.0);
391 let inside = vec![Interval::<f64>::new(25.0_f64, 30.0)];
392
393 assert_eq!(
394 outer.complement(&inside),
395 vec![Interval::<f64>::new(10.0, 20.0)]
396 );
397 }
398
399 #[test]
400 fn complement_clips_periods_spanning_outer_end() {
401 let outer = Interval::<f64>::new(10.0_f64, 20.0);
402 let inside = vec![Interval::<f64>::new(12.0_f64, 30.0)];
403
404 assert_eq!(
405 outer.complement(&inside),
406 vec![Interval::<f64>::new(10.0, 12.0)]
407 );
408 }
409
410 #[test]
411 fn intersect_merge() {
412 let a = vec![
413 Interval::<f64>::new(0.0_f64, 5.0),
414 Interval::<f64>::new(10.0, 15.0),
415 ];
416 let b = vec![Interval::<f64>::new(3.0_f64, 12.0)];
417 let ix = Interval::intersect_many(&a, &b);
418 assert_eq!(ix.len(), 2);
419 assert_eq!(ix[0], Interval::<f64>::new(3.0, 5.0));
420 assert_eq!(ix[1], Interval::<f64>::new(10.0, 12.0));
421 }
422
423 #[test]
424 fn normalize_merges_overlap() {
425 let input = vec![
426 Interval::<f64>::new(5.0_f64, 8.0),
427 Interval::<f64>::new(0.0, 3.0),
428 Interval::<f64>::new(2.0, 6.0),
429 ];
430 let merged = Interval::normalize(&input);
431 assert_eq!(merged.len(), 1);
432 assert_eq!(merged[0], Interval::<f64>::new(0.0, 8.0));
433 }
434
435 #[test]
436 fn validate_detects_overlap() {
437 let periods = vec![
438 Interval::<f64>::new(0.0_f64, 5.0),
439 Interval::<f64>::new(3.0, 8.0),
440 ];
441 assert_eq!(
442 Interval::validate(&periods),
443 Err(PeriodListError::Overlapping { index: 1 })
444 );
445 }
446
447 #[test]
448 fn period_accepts_typed_times() {
449 let p = Period::<TT>::new(
450 ModifiedJulianDate::<TT>::new(51_544.5).to_j2000s(),
451 ModifiedJulianDate::<TT>::new(51_545.25).to_j2000s(),
452 );
453 assert_eq!(p.start.to::<MJD>().raw(), Day::new(51_544.5));
454 assert_eq!(p.end.to::<MJD>().raw(), Day::new(51_545.25));
455 }
456
457 #[test]
458 fn period_length_returns_seconds() {
459 let p = Period::<TT>::new(
460 ModifiedJulianDate::<TT>::new(51_544.5).to_j2000s(),
461 ModifiedJulianDate::<TT>::new(51_544.75).to_j2000s(),
462 );
463
464 assert_eq!(p.length(), crate::qtty::Second::new(21_600.0));
465 }
466
467 #[test]
468 fn period_length_returns_days_for_mjd() {
469 let p = Period::<TT>::new(
470 ModifiedJulianDate::<TT>::new(51_544.5),
471 ModifiedJulianDate::<TT>::new(51_545.25),
472 );
473
474 assert_eq!(p.length(), Day::new(0.75));
475 }
476
477 #[test]
478 fn display_invalid_interval_error() {
479 let e = InvalidIntervalError::StartAfterEnd;
480 assert!(e.to_string().contains("start"));
481 }
482
483 #[test]
484 fn display_period_list_errors() {
485 assert!(PeriodListError::InvalidInterval { index: 2 }
486 .to_string()
487 .contains("2"));
488 assert!(PeriodListError::Unsorted { index: 3 }
489 .to_string()
490 .contains("3"));
491 assert!(PeriodListError::Overlapping { index: 4 }
492 .to_string()
493 .contains("4"));
494 }
495
496 #[test]
497 fn normalize_empty_returns_empty() {
498 let result = Interval::<f64>::normalize(&[]);
499 assert!(result.is_empty());
500 }
501
502 #[test]
503 fn validate_detects_invalid_interval() {
504 let periods = vec![Interval::<f64> {
506 start: 5.0,
507 end: 1.0,
508 }];
509 assert_eq!(
510 Interval::validate(&periods),
511 Err(PeriodListError::InvalidInterval { index: 0 })
512 );
513 }
514
515 #[test]
516 fn validate_detects_unsorted() {
517 let periods = vec![
518 Interval::<f64>::new(5.0_f64, 8.0),
519 Interval::<f64>::new(1.0_f64, 4.0),
520 ];
521 assert_eq!(
522 Interval::validate(&periods),
523 Err(PeriodListError::Unsorted { index: 1 })
524 );
525 }
526
527 #[test]
528 fn checked_complement_rejects_invalid_inputs() {
529 let outer = Interval::<f64>::new(0.0_f64, 10.0);
530 let periods = vec![
531 Interval::<f64>::new(5.0_f64, 8.0),
532 Interval::<f64>::new(1.0_f64, 4.0),
533 ];
534 assert_eq!(
535 outer.try_complement(&periods),
536 Err(PeriodListError::Unsorted { index: 1 })
537 );
538
539 let invalid_outer = Interval::<f64>::new(10.0_f64, 0.0);
540 assert_eq!(
541 invalid_outer.try_complement(&[]),
542 Err(PeriodListError::InvalidInterval { index: 0 })
543 );
544 }
545
546 #[test]
547 fn checked_intersection_matches_unchecked_for_valid_inputs() {
548 let a = vec![Interval::<f64>::new(0.0_f64, 5.0)];
549 let b = vec![Interval::<f64>::new(3.0_f64, 7.0)];
550 assert_eq!(
551 Interval::try_intersect_many(&a, &b).unwrap(),
552 Interval::intersect_many(&a, &b)
553 );
554 }
555
556 #[test]
557 fn union_pair_overlapping() {
558 let a = Interval::<f64>::new(0.0_f64, 5.0);
559 let b = Interval::<f64>::new(3.0_f64, 8.0);
560 let u = a.union(&b);
561 assert_eq!(u.len(), 1);
562 assert_eq!(u[0], Interval::<f64>::new(0.0, 8.0));
563 }
564
565 #[test]
566 fn union_pair_disjoint() {
567 let a = Interval::<f64>::new(0.0_f64, 3.0);
568 let b = Interval::<f64>::new(5.0_f64, 8.0);
569 let u = a.union(&b);
570 assert_eq!(u.len(), 2);
571 assert_eq!(u[0], Interval::<f64>::new(0.0, 3.0));
572 assert_eq!(u[1], Interval::<f64>::new(5.0, 8.0));
573 }
574
575 #[test]
576 fn union_many_merges_two_lists() {
577 let a = vec![
578 Interval::<f64>::new(0.0_f64, 3.0),
579 Interval::<f64>::new(7.0, 9.0),
580 ];
581 let b = vec![Interval::<f64>::new(2.0_f64, 5.0)];
582 let u = Interval::union_many(&a, &b);
583 assert_eq!(u.len(), 2);
584 assert_eq!(u[0], Interval::<f64>::new(0.0, 5.0));
585 assert_eq!(u[1], Interval::<f64>::new(7.0, 9.0));
586 }
587
588 #[test]
589 fn display_formats_periods_via_endpoint_display() {
590 let mjd = Period::<TT>::new(
591 ModifiedJulianDate::<TT>::new(51_544.5).to_j2000s(),
592 ModifiedJulianDate::<TT>::new(51_545.25).to_j2000s(),
593 );
594
595 assert!(mjd.to_string().contains("TT"));
596 }
597
598 #[cfg(feature = "serde")]
599 #[test]
600 fn serde_roundtrips_period_shapes() {
601 let mjd = Period::<TT>::new(
602 ModifiedJulianDate::<TT>::new(51_544.5).to_j2000s(),
603 ModifiedJulianDate::<TT>::new(51_545.25).to_j2000s(),
604 );
605 let jd = Period::<TT>::new(
606 JulianDate::<TT>::new(2_451_545.0).to_j2000s(),
607 JulianDate::<TT>::new(2_451_546.0).to_j2000s(),
608 );
609 let native = Period::<TT>::new(
610 Time::<TT>::from_raw_j2000_seconds(crate::qtty::Second::new(100.0)).unwrap(),
611 Time::<TT>::from_raw_j2000_seconds(crate::qtty::Second::new(200.0)).unwrap(),
612 );
613
614 let mjd_json = serde_json::to_value(mjd).unwrap();
615 assert_eq!(serde_json::from_value::<Period<TT>>(mjd_json).unwrap(), mjd);
616
617 let jd_json = serde_json::to_value(jd).unwrap();
618 assert_eq!(serde_json::from_value::<Period<TT>>(jd_json).unwrap(), jd);
619
620 let native_json = serde_json::to_value(native).unwrap();
621 assert_eq!(
622 serde_json::from_value::<Period<TT>>(native_json).unwrap(),
623 native
624 );
625 }
626
627 #[cfg(feature = "serde")]
628 #[test]
629 fn serde_rejects_reversed_periods() {
630 let err = serde_json::from_value::<Period<TT>>(json!({
631 "start": 10.0,
632 "end": 9.0,
633 }))
634 .unwrap_err();
635 assert!(err.to_string().contains("start"));
636 }
637
638 #[cfg(feature = "serde")]
639 #[test]
640 fn serde_rejects_nan_scalar_time_values() {
641 let res: Result<Time<TT>, serde::de::value::Error> =
642 Time::<TT>::deserialize(f64::NAN.into_deserializer());
643 let err = res.expect_err("nan scalar");
644 assert!(
645 err.to_string().contains("NaN"),
646 "unexpected serde error message: {err}"
647 );
648
649 let err = serde_json::from_value::<Period<TT>>(json!({
650 "start": serde_json::Value::Null,
651 "end": 5.0,
652 }))
653 .expect_err("null start");
654 assert!(
655 err.to_string().contains("null"),
656 "unexpected serde error message: {err}"
657 );
658 }
659}