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