1use core::fmt;
12
13use crate::Time;
14
15#[inline]
16fn partial_max<T: PartialOrd + Copy>(a: T, b: T) -> T {
17 if a >= b {
18 a
19 } else {
20 b
21 }
22}
23
24#[inline]
25fn partial_min<T: PartialOrd + Copy>(a: T, b: T) -> T {
26 if a <= b {
27 a
28 } else {
29 b
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum InvalidIntervalError {
36 StartAfterEnd,
38}
39
40impl fmt::Display for InvalidIntervalError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 f.write_str("interval start must not be after end")
43 }
44}
45
46impl std::error::Error for InvalidIntervalError {}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum PeriodListError {
51 InvalidInterval { index: usize },
53 Unsorted { index: usize },
55 Overlapping { index: usize },
57}
58
59impl fmt::Display for PeriodListError {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::InvalidInterval { index } => {
63 write!(f, "interval at index {index} has start > end")
64 }
65 Self::Unsorted { index } => {
66 write!(f, "interval at index {index} is not sorted by start time")
67 }
68 Self::Overlapping { index } => {
69 write!(f, "interval at index {index} overlaps its predecessor")
70 }
71 }
72 }
73}
74
75impl std::error::Error for PeriodListError {}
76
77#[derive(Debug, Clone, Copy, PartialEq)]
82pub struct Interval<T: Copy + PartialOrd> {
83 pub start: T,
85 pub end: T,
87}
88
89impl<T> fmt::Display for Interval<T>
90where
91 T: Copy + PartialOrd + fmt::Display,
92{
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(f, "[{}, {})", self.start, self.end)
95 }
96}
97
98pub type Period<S> = Interval<Time<S>>;
103
104pub type InvalidPeriodError = InvalidIntervalError;
106
107impl<T: Copy + PartialOrd> Interval<T> {
108 #[inline]
114 pub fn new<S: Into<T>, E: Into<T>>(start: S, end: E) -> Self {
115 Self {
116 start: start.into(),
117 end: end.into(),
118 }
119 }
120
121 #[inline]
125 pub fn try_new<S: Into<T>, E: Into<T>>(start: S, end: E) -> Result<Self, InvalidIntervalError> {
126 let start = start.into();
127 let end = end.into();
128 if start <= end {
129 Ok(Self { start, end })
130 } else {
131 Err(InvalidIntervalError::StartAfterEnd)
132 }
133 }
134
135 #[inline]
140 pub fn intersection(&self, other: &Self) -> Option<Self> {
141 let start = partial_max(self.start, other.start);
142 let end = partial_min(self.end, other.end);
143 if start < end {
144 Some(Self::new(start, end))
145 } else {
146 None
147 }
148 }
149
150 #[inline]
159 pub fn union(&self, other: &Self) -> Vec<Self> {
160 if self.start <= other.end && other.start <= self.end {
161 vec![Self::new(
162 partial_min(self.start, other.start),
163 partial_max(self.end, other.end),
164 )]
165 } else if self.start <= other.start {
166 vec![*self, *other]
167 } else {
168 vec![*other, *self]
169 }
170 }
171
172 #[inline]
180 pub fn complement(&self, periods: &[Self]) -> Vec<Self> {
181 let mut gaps = Vec::with_capacity(periods.len().saturating_add(1));
182 let mut cursor = self.start;
183 for p in periods {
184 if p.end <= cursor {
185 continue;
186 }
187 if p.start >= self.end {
188 break;
189 }
190 if p.start > cursor {
191 gaps.push(Self::new(cursor, p.start));
192 }
193 if p.end >= self.end {
194 return gaps;
195 }
196 cursor = p.end;
197 }
198 if cursor < self.end {
199 gaps.push(Self::new(cursor, self.end));
200 }
201 gaps
202 }
203
204 #[inline]
209 pub fn try_complement(&self, periods: &[Self]) -> Result<Vec<Self>, PeriodListError> {
210 if self
211 .start
212 .partial_cmp(&self.end)
213 .is_none_or(|ordering| ordering == core::cmp::Ordering::Greater)
214 {
215 return Err(PeriodListError::InvalidInterval { index: 0 });
216 }
217 Self::validate(periods)?;
218 Ok(self.complement(periods))
219 }
220
221 pub fn validate(periods: &[Self]) -> Result<(), PeriodListError> {
227 let mut prev: Option<Interval<T>> = None;
228 for (i, period) in periods.iter().copied().enumerate() {
229 if period
230 .start
231 .partial_cmp(&period.end)
232 .is_none_or(|ordering| ordering == core::cmp::Ordering::Greater)
233 {
234 return Err(PeriodListError::InvalidInterval { index: i });
235 }
236 if let Some(previous) = prev {
237 if previous
238 .start
239 .partial_cmp(&period.start)
240 .is_none_or(|ordering| ordering == core::cmp::Ordering::Greater)
241 {
242 return Err(PeriodListError::Unsorted { index: i });
243 }
244 if previous.end > period.start {
245 return Err(PeriodListError::Overlapping { index: i });
246 }
247 }
248 prev = Some(period);
249 }
250 Ok(())
251 }
252
253 pub fn intersect_many(a: &[Self], b: &[Self]) -> Vec<Self> {
255 let mut result = Vec::with_capacity(a.len().min(b.len()));
256 let (mut i, mut j) = (0, 0);
257 while i < a.len() && j < b.len() {
258 let start = partial_max(a[i].start, b[j].start);
259 let end = partial_min(a[i].end, b[j].end);
260 if start < end {
261 result.push(Self::new(start, end));
262 }
263 if a[i].end <= b[j].end {
264 i += 1;
265 } else {
266 j += 1;
267 }
268 }
269 result
270 }
271
272 pub fn try_intersect_many(a: &[Self], b: &[Self]) -> Result<Vec<Self>, PeriodListError> {
276 Self::validate(a)?;
277 Self::validate(b)?;
278 Ok(Self::intersect_many(a, b))
279 }
280
281 pub fn union_many(a: &[Self], b: &[Self]) -> Vec<Self> {
286 let mut combined: Vec<Self> = a.iter().chain(b.iter()).copied().collect();
287 combined.sort_unstable_by(|x, y| {
288 x.start
289 .partial_cmp(&y.start)
290 .unwrap_or(core::cmp::Ordering::Equal)
291 });
292 Self::normalize(&combined)
293 }
294
295 pub fn normalize(periods: &[Self]) -> Vec<Self> {
300 if periods.is_empty() {
301 return Vec::new();
302 }
303 let mut sorted: Vec<_> = periods.to_vec();
304 sorted.sort_unstable_by(|a, b| {
305 a.start
306 .partial_cmp(&b.start)
307 .unwrap_or(core::cmp::Ordering::Equal)
308 });
309 let mut merged = Vec::with_capacity(sorted.len());
310 merged.push(sorted[0]);
311 for period in sorted.into_iter().skip(1) {
312 if let Some(last) = merged.last_mut() {
313 if period.start <= last.end {
314 if period.end > last.end {
315 last.end = period.end;
316 }
317 } else {
318 merged.push(period);
319 }
320 }
321 }
322 merged
323 }
324}
325
326#[inline]
349pub fn complement_within<T: Copy + PartialOrd>(
350 outer: Interval<T>,
351 periods: &[Interval<T>],
352) -> Vec<Interval<T>> {
353 outer.complement(periods)
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 #[cfg(feature = "serde")]
360 use crate::representation::JulianDate;
361 use crate::representation::{ModifiedJulianDate, MJD};
362 #[cfg(feature = "serde")]
363 use crate::Time;
364 use crate::TT;
365 use qtty::Day;
366 #[cfg(feature = "serde")]
367 use serde::de::{value, IntoDeserializer};
368 #[cfg(feature = "serde")]
369 use serde::Deserialize;
370 #[cfg(feature = "serde")]
371 use serde_json::json;
372
373 #[test]
374 fn try_new_rejects_reversed() {
375 assert_eq!(
376 Interval::<f64>::try_new(2.0_f64, 1.0).unwrap_err(),
377 InvalidIntervalError::StartAfterEnd
378 );
379 }
380
381 #[test]
382 fn try_new_rejects_nan() {
383 assert!(Interval::<f64>::try_new(f64::NAN, 0.0).is_err());
384 }
385
386 #[test]
387 fn intersection_half_open() {
388 let a = Interval::<f64>::new(0.0_f64, 10.0);
389 let b = Interval::<f64>::new(10.0, 20.0);
390 assert!(a.intersection(&b).is_none());
391 let c = Interval::<f64>::new(5.0_f64, 15.0);
392 let x = a.intersection(&c).unwrap();
393 assert_eq!(x.start, 5.0);
394 assert_eq!(x.end, 10.0);
395 }
396
397 #[test]
398 fn complement_covers_gaps() {
399 let outer = Interval::<f64>::new(0.0_f64, 10.0);
400 let inside = vec![
401 Interval::<f64>::new(1.0_f64, 2.0),
402 Interval::<f64>::new(4.0, 6.0),
403 ];
404 let gaps = outer.complement(&inside);
405 assert_eq!(gaps.len(), 3);
406 assert_eq!(gaps[0], Interval::<f64>::new(0.0, 1.0));
407 assert_eq!(gaps[1], Interval::<f64>::new(2.0, 4.0));
408 assert_eq!(gaps[2], Interval::<f64>::new(6.0, 10.0));
409 }
410
411 #[test]
412 fn complement_ignores_periods_after_outer_interval() {
413 let outer = Interval::<f64>::new(10.0_f64, 20.0);
414 let inside = vec![Interval::<f64>::new(25.0_f64, 30.0)];
415
416 assert_eq!(
417 outer.complement(&inside),
418 vec![Interval::<f64>::new(10.0, 20.0)]
419 );
420 }
421
422 #[test]
423 fn complement_clips_periods_spanning_outer_end() {
424 let outer = Interval::<f64>::new(10.0_f64, 20.0);
425 let inside = vec![Interval::<f64>::new(12.0_f64, 30.0)];
426
427 assert_eq!(
428 outer.complement(&inside),
429 vec![Interval::<f64>::new(10.0, 12.0)]
430 );
431 }
432
433 #[test]
434 fn intersect_merge() {
435 let a = vec![
436 Interval::<f64>::new(0.0_f64, 5.0),
437 Interval::<f64>::new(10.0, 15.0),
438 ];
439 let b = vec![Interval::<f64>::new(3.0_f64, 12.0)];
440 let ix = Interval::intersect_many(&a, &b);
441 assert_eq!(ix.len(), 2);
442 assert_eq!(ix[0], Interval::<f64>::new(3.0, 5.0));
443 assert_eq!(ix[1], Interval::<f64>::new(10.0, 12.0));
444 }
445
446 #[test]
447 fn normalize_merges_overlap() {
448 let input = vec![
449 Interval::<f64>::new(5.0_f64, 8.0),
450 Interval::<f64>::new(0.0, 3.0),
451 Interval::<f64>::new(2.0, 6.0),
452 ];
453 let merged = Interval::normalize(&input);
454 assert_eq!(merged.len(), 1);
455 assert_eq!(merged[0], Interval::<f64>::new(0.0, 8.0));
456 }
457
458 #[test]
459 fn validate_detects_overlap() {
460 let periods = vec![
461 Interval::<f64>::new(0.0_f64, 5.0),
462 Interval::<f64>::new(3.0, 8.0),
463 ];
464 assert_eq!(
465 Interval::validate(&periods),
466 Err(PeriodListError::Overlapping { index: 1 })
467 );
468 }
469
470 #[test]
471 fn period_accepts_typed_times() {
472 let p = Period::<TT>::new(
473 ModifiedJulianDate::<TT>::try_new(Day::new(51_544.5))
474 .unwrap()
475 .to_time(),
476 ModifiedJulianDate::<TT>::try_new(Day::new(51_545.25))
477 .unwrap()
478 .to_time(),
479 );
480 assert_eq!(p.start.to::<MJD>().raw(), Day::new(51_544.5));
481 assert_eq!(p.end.to::<MJD>().raw(), Day::new(51_545.25));
482 }
483
484 #[test]
485 fn display_invalid_interval_error() {
486 let e = InvalidIntervalError::StartAfterEnd;
487 assert!(e.to_string().contains("start"));
488 }
489
490 #[test]
491 fn display_period_list_errors() {
492 assert!(PeriodListError::InvalidInterval { index: 2 }
493 .to_string()
494 .contains("2"));
495 assert!(PeriodListError::Unsorted { index: 3 }
496 .to_string()
497 .contains("3"));
498 assert!(PeriodListError::Overlapping { index: 4 }
499 .to_string()
500 .contains("4"));
501 }
502
503 #[test]
504 fn normalize_empty_returns_empty() {
505 let result = Interval::<f64>::normalize(&[]);
506 assert!(result.is_empty());
507 }
508
509 #[test]
510 fn validate_detects_invalid_interval() {
511 let periods = vec![Interval::<f64> {
513 start: 5.0,
514 end: 1.0,
515 }];
516 assert_eq!(
517 Interval::validate(&periods),
518 Err(PeriodListError::InvalidInterval { index: 0 })
519 );
520 }
521
522 #[test]
523 fn validate_detects_unsorted() {
524 let periods = vec![
525 Interval::<f64>::new(5.0_f64, 8.0),
526 Interval::<f64>::new(1.0_f64, 4.0),
527 ];
528 assert_eq!(
529 Interval::validate(&periods),
530 Err(PeriodListError::Unsorted { index: 1 })
531 );
532 }
533
534 #[test]
535 fn checked_complement_rejects_invalid_inputs() {
536 let outer = Interval::<f64>::new(0.0_f64, 10.0);
537 let periods = vec![
538 Interval::<f64>::new(5.0_f64, 8.0),
539 Interval::<f64>::new(1.0_f64, 4.0),
540 ];
541 assert_eq!(
542 outer.try_complement(&periods),
543 Err(PeriodListError::Unsorted { index: 1 })
544 );
545
546 let invalid_outer = Interval::<f64>::new(10.0_f64, 0.0);
547 assert_eq!(
548 invalid_outer.try_complement(&[]),
549 Err(PeriodListError::InvalidInterval { index: 0 })
550 );
551 }
552
553 #[test]
554 fn checked_intersection_matches_unchecked_for_valid_inputs() {
555 let a = vec![Interval::<f64>::new(0.0_f64, 5.0)];
556 let b = vec![Interval::<f64>::new(3.0_f64, 7.0)];
557 assert_eq!(
558 Interval::try_intersect_many(&a, &b).unwrap(),
559 Interval::intersect_many(&a, &b)
560 );
561 }
562
563 #[test]
564 fn union_pair_overlapping() {
565 let a = Interval::<f64>::new(0.0_f64, 5.0);
566 let b = Interval::<f64>::new(3.0_f64, 8.0);
567 let u = a.union(&b);
568 assert_eq!(u.len(), 1);
569 assert_eq!(u[0], Interval::<f64>::new(0.0, 8.0));
570 }
571
572 #[test]
573 fn union_pair_disjoint() {
574 let a = Interval::<f64>::new(0.0_f64, 3.0);
575 let b = Interval::<f64>::new(5.0_f64, 8.0);
576 let u = a.union(&b);
577 assert_eq!(u.len(), 2);
578 assert_eq!(u[0], Interval::<f64>::new(0.0, 3.0));
579 assert_eq!(u[1], Interval::<f64>::new(5.0, 8.0));
580 }
581
582 #[test]
583 fn union_many_merges_two_lists() {
584 let a = vec![
585 Interval::<f64>::new(0.0_f64, 3.0),
586 Interval::<f64>::new(7.0, 9.0),
587 ];
588 let b = vec![Interval::<f64>::new(2.0_f64, 5.0)];
589 let u = Interval::union_many(&a, &b);
590 assert_eq!(u.len(), 2);
591 assert_eq!(u[0], Interval::<f64>::new(0.0, 5.0));
592 assert_eq!(u[1], Interval::<f64>::new(7.0, 9.0));
593 }
594
595 #[test]
596 fn display_formats_periods_via_endpoint_display() {
597 let mjd = Period::<TT>::new(
598 ModifiedJulianDate::<TT>::try_new(Day::new(51_544.5))
599 .unwrap()
600 .to_time(),
601 ModifiedJulianDate::<TT>::try_new(Day::new(51_545.25))
602 .unwrap()
603 .to_time(),
604 );
605
606 assert!(mjd.to_string().contains("TT"));
607 }
608
609 #[cfg(feature = "serde")]
610 #[test]
611 fn serde_roundtrips_period_shapes() {
612 let mjd = Period::<TT>::new(
613 ModifiedJulianDate::<TT>::try_new(Day::new(51_544.5))
614 .unwrap()
615 .to_time(),
616 ModifiedJulianDate::<TT>::try_new(Day::new(51_545.25))
617 .unwrap()
618 .to_time(),
619 );
620 let jd = Period::<TT>::new(
621 JulianDate::<TT>::try_new(Day::new(2_451_545.0))
622 .unwrap()
623 .to_time(),
624 JulianDate::<TT>::try_new(Day::new(2_451_546.0))
625 .unwrap()
626 .to_time(),
627 );
628 let native = Period::<TT>::new(
629 Time::<TT>::from_raw_j2000_seconds(qtty::Second::new(100.0)).unwrap(),
630 Time::<TT>::from_raw_j2000_seconds(qtty::Second::new(200.0)).unwrap(),
631 );
632
633 assert_eq!(
634 serde_json::to_value(mjd).unwrap(),
635 json!({"start": {"hi": 0.0, "lo": 0.0}, "end": {"hi": 64800.0, "lo": 0.0}})
636 );
637 assert_eq!(
638 serde_json::to_value(native).unwrap(),
639 json!({"start": {"hi": 100.0, "lo": 0.0}, "end": {"hi": 200.0, "lo": 0.0}})
640 );
641
642 assert_eq!(
643 serde_json::from_value::<Period<TT>>(json!({
644 "start": {"hi": 0.0, "lo": 0.0},
645 "end": {"hi": 64800.0, "lo": 0.0}
646 }))
647 .unwrap(),
648 mjd
649 );
650 assert_eq!(
651 serde_json::from_value::<Period<TT>>(json!({
652 "start": {"hi": 0.0, "lo": 0.0},
653 "end": {"hi": 86400.0, "lo": 0.0}
654 }))
655 .unwrap(),
656 jd
657 );
658 assert_eq!(
659 serde_json::from_value::<Period<TT>>(json!({
660 "start": {"hi": 100.0, "lo": 0.0},
661 "end": {"hi": 200.0, "lo": 0.0}
662 }))
663 .unwrap(),
664 native
665 );
666 }
667
668 #[cfg(feature = "serde")]
669 #[test]
670 fn serde_rejects_reversed_periods() {
671 let err = serde_json::from_value::<Period<TT>>(json!({
672 "start": {"hi": 10.0, "lo": 0.0},
673 "end": {"hi": 9.0, "lo": 0.0}
674 }))
675 .unwrap_err();
676 assert!(err.to_string().contains("start"));
677 }
678
679 #[cfg(feature = "serde")]
680 #[test]
681 fn serde_rejects_nonfinite_nested_time_values() {
682 let start = value::MapDeserializer::<_, value::Error>::new(
683 vec![
684 ("hi".into_deserializer(), f64::NAN.into_deserializer()),
685 ("lo".into_deserializer(), 0.0_f64.into_deserializer()),
686 ]
687 .into_iter(),
688 );
689 let end = value::MapDeserializer::<_, value::Error>::new(
690 vec![
691 ("hi".into_deserializer(), 5.0_f64.into_deserializer()),
692 ("lo".into_deserializer(), 0.0_f64.into_deserializer()),
693 ]
694 .into_iter(),
695 );
696 let entries = vec![
697 ("start".into_deserializer(), start),
698 ("end".into_deserializer(), end),
699 ];
700 let deserializer = value::MapDeserializer::<_, value::Error>::new(entries.into_iter());
701 let err = Period::<TT>::deserialize(deserializer).unwrap_err();
702 assert!(err.to_string().contains("finite"));
703 }
704}