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#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::representation::{JulianDate, ModifiedJulianDate, MJD};
330 use crate::{Time, TT};
331 use qtty::Day;
332 #[cfg(feature = "serde")]
333 use serde::de::{value, IntoDeserializer};
334 #[cfg(feature = "serde")]
335 use serde::Deserialize;
336 #[cfg(feature = "serde")]
337 use serde_json::json;
338
339 #[test]
340 fn try_new_rejects_reversed() {
341 assert_eq!(
342 Interval::<f64>::try_new(2.0_f64, 1.0).unwrap_err(),
343 InvalidIntervalError::StartAfterEnd
344 );
345 }
346
347 #[test]
348 fn try_new_rejects_nan() {
349 assert!(Interval::<f64>::try_new(f64::NAN, 0.0).is_err());
350 }
351
352 #[test]
353 fn intersection_half_open() {
354 let a = Interval::<f64>::new(0.0_f64, 10.0);
355 let b = Interval::<f64>::new(10.0, 20.0);
356 assert!(a.intersection(&b).is_none());
357 let c = Interval::<f64>::new(5.0_f64, 15.0);
358 let x = a.intersection(&c).unwrap();
359 assert_eq!(x.start, 5.0);
360 assert_eq!(x.end, 10.0);
361 }
362
363 #[test]
364 fn complement_covers_gaps() {
365 let outer = Interval::<f64>::new(0.0_f64, 10.0);
366 let inside = vec![
367 Interval::<f64>::new(1.0_f64, 2.0),
368 Interval::<f64>::new(4.0, 6.0),
369 ];
370 let gaps = outer.complement(&inside);
371 assert_eq!(gaps.len(), 3);
372 assert_eq!(gaps[0], Interval::<f64>::new(0.0, 1.0));
373 assert_eq!(gaps[1], Interval::<f64>::new(2.0, 4.0));
374 assert_eq!(gaps[2], Interval::<f64>::new(6.0, 10.0));
375 }
376
377 #[test]
378 fn complement_ignores_periods_after_outer_interval() {
379 let outer = Interval::<f64>::new(10.0_f64, 20.0);
380 let inside = vec![Interval::<f64>::new(25.0_f64, 30.0)];
381
382 assert_eq!(
383 outer.complement(&inside),
384 vec![Interval::<f64>::new(10.0, 20.0)]
385 );
386 }
387
388 #[test]
389 fn complement_clips_periods_spanning_outer_end() {
390 let outer = Interval::<f64>::new(10.0_f64, 20.0);
391 let inside = vec![Interval::<f64>::new(12.0_f64, 30.0)];
392
393 assert_eq!(
394 outer.complement(&inside),
395 vec![Interval::<f64>::new(10.0, 12.0)]
396 );
397 }
398
399 #[test]
400 fn intersect_merge() {
401 let a = vec![
402 Interval::<f64>::new(0.0_f64, 5.0),
403 Interval::<f64>::new(10.0, 15.0),
404 ];
405 let b = vec![Interval::<f64>::new(3.0_f64, 12.0)];
406 let ix = Interval::intersect_many(&a, &b);
407 assert_eq!(ix.len(), 2);
408 assert_eq!(ix[0], Interval::<f64>::new(3.0, 5.0));
409 assert_eq!(ix[1], Interval::<f64>::new(10.0, 12.0));
410 }
411
412 #[test]
413 fn normalize_merges_overlap() {
414 let input = vec![
415 Interval::<f64>::new(5.0_f64, 8.0),
416 Interval::<f64>::new(0.0, 3.0),
417 Interval::<f64>::new(2.0, 6.0),
418 ];
419 let merged = Interval::normalize(&input);
420 assert_eq!(merged.len(), 1);
421 assert_eq!(merged[0], Interval::<f64>::new(0.0, 8.0));
422 }
423
424 #[test]
425 fn validate_detects_overlap() {
426 let periods = vec![
427 Interval::<f64>::new(0.0_f64, 5.0),
428 Interval::<f64>::new(3.0, 8.0),
429 ];
430 assert_eq!(
431 Interval::validate(&periods),
432 Err(PeriodListError::Overlapping { index: 1 })
433 );
434 }
435
436 #[test]
437 fn period_accepts_typed_times() {
438 let p = Period::<TT>::new(
439 ModifiedJulianDate::<TT>::try_new(Day::new(51_544.5))
440 .unwrap()
441 .to_time(),
442 ModifiedJulianDate::<TT>::try_new(Day::new(51_545.25))
443 .unwrap()
444 .to_time(),
445 );
446 assert_eq!(p.start.to::<MJD>().raw(), Day::new(51_544.5));
447 assert_eq!(p.end.to::<MJD>().raw(), Day::new(51_545.25));
448 }
449
450 #[test]
451 fn display_invalid_interval_error() {
452 let e = InvalidIntervalError::StartAfterEnd;
453 assert!(e.to_string().contains("start"));
454 }
455
456 #[test]
457 fn display_period_list_errors() {
458 assert!(PeriodListError::InvalidInterval { index: 2 }
459 .to_string()
460 .contains("2"));
461 assert!(PeriodListError::Unsorted { index: 3 }
462 .to_string()
463 .contains("3"));
464 assert!(PeriodListError::Overlapping { index: 4 }
465 .to_string()
466 .contains("4"));
467 }
468
469 #[test]
470 fn normalize_empty_returns_empty() {
471 let result = Interval::<f64>::normalize(&[]);
472 assert!(result.is_empty());
473 }
474
475 #[test]
476 fn validate_detects_invalid_interval() {
477 let periods = vec![Interval::<f64> {
479 start: 5.0,
480 end: 1.0,
481 }];
482 assert_eq!(
483 Interval::validate(&periods),
484 Err(PeriodListError::InvalidInterval { index: 0 })
485 );
486 }
487
488 #[test]
489 fn validate_detects_unsorted() {
490 let periods = vec![
491 Interval::<f64>::new(5.0_f64, 8.0),
492 Interval::<f64>::new(1.0_f64, 4.0),
493 ];
494 assert_eq!(
495 Interval::validate(&periods),
496 Err(PeriodListError::Unsorted { index: 1 })
497 );
498 }
499
500 #[test]
501 fn checked_complement_rejects_invalid_inputs() {
502 let outer = Interval::<f64>::new(0.0_f64, 10.0);
503 let periods = vec![
504 Interval::<f64>::new(5.0_f64, 8.0),
505 Interval::<f64>::new(1.0_f64, 4.0),
506 ];
507 assert_eq!(
508 outer.try_complement(&periods),
509 Err(PeriodListError::Unsorted { index: 1 })
510 );
511
512 let invalid_outer = Interval::<f64>::new(10.0_f64, 0.0);
513 assert_eq!(
514 invalid_outer.try_complement(&[]),
515 Err(PeriodListError::InvalidInterval { index: 0 })
516 );
517 }
518
519 #[test]
520 fn checked_intersection_matches_unchecked_for_valid_inputs() {
521 let a = vec![Interval::<f64>::new(0.0_f64, 5.0)];
522 let b = vec![Interval::<f64>::new(3.0_f64, 7.0)];
523 assert_eq!(
524 Interval::try_intersect_many(&a, &b).unwrap(),
525 Interval::intersect_many(&a, &b)
526 );
527 }
528
529 #[test]
530 fn union_pair_overlapping() {
531 let a = Interval::<f64>::new(0.0_f64, 5.0);
532 let b = Interval::<f64>::new(3.0_f64, 8.0);
533 let u = a.union(&b);
534 assert_eq!(u.len(), 1);
535 assert_eq!(u[0], Interval::<f64>::new(0.0, 8.0));
536 }
537
538 #[test]
539 fn union_pair_disjoint() {
540 let a = Interval::<f64>::new(0.0_f64, 3.0);
541 let b = Interval::<f64>::new(5.0_f64, 8.0);
542 let u = a.union(&b);
543 assert_eq!(u.len(), 2);
544 assert_eq!(u[0], Interval::<f64>::new(0.0, 3.0));
545 assert_eq!(u[1], Interval::<f64>::new(5.0, 8.0));
546 }
547
548 #[test]
549 fn union_many_merges_two_lists() {
550 let a = vec![
551 Interval::<f64>::new(0.0_f64, 3.0),
552 Interval::<f64>::new(7.0, 9.0),
553 ];
554 let b = vec![Interval::<f64>::new(2.0_f64, 5.0)];
555 let u = Interval::union_many(&a, &b);
556 assert_eq!(u.len(), 2);
557 assert_eq!(u[0], Interval::<f64>::new(0.0, 5.0));
558 assert_eq!(u[1], Interval::<f64>::new(7.0, 9.0));
559 }
560
561 #[test]
562 fn display_formats_periods_via_endpoint_display() {
563 let mjd = Period::<TT>::new(
564 ModifiedJulianDate::<TT>::try_new(Day::new(51_544.5))
565 .unwrap()
566 .to_time(),
567 ModifiedJulianDate::<TT>::try_new(Day::new(51_545.25))
568 .unwrap()
569 .to_time(),
570 );
571
572 assert!(mjd.to_string().contains("TT"));
573 }
574
575 #[cfg(feature = "serde")]
576 #[test]
577 fn serde_roundtrips_period_shapes() {
578 let mjd = Period::<TT>::new(
579 ModifiedJulianDate::<TT>::try_new(Day::new(51_544.5))
580 .unwrap()
581 .to_time(),
582 ModifiedJulianDate::<TT>::try_new(Day::new(51_545.25))
583 .unwrap()
584 .to_time(),
585 );
586 let jd = Period::<TT>::new(
587 JulianDate::<TT>::try_new(Day::new(2_451_545.0))
588 .unwrap()
589 .to_time(),
590 JulianDate::<TT>::try_new(Day::new(2_451_546.0))
591 .unwrap()
592 .to_time(),
593 );
594 let native = Period::<TT>::new(
595 Time::<TT>::from_raw_j2000_seconds(qtty::Second::new(100.0)).unwrap(),
596 Time::<TT>::from_raw_j2000_seconds(qtty::Second::new(200.0)).unwrap(),
597 );
598
599 assert_eq!(
600 serde_json::to_value(mjd).unwrap(),
601 json!({"start": {"hi": 0.0, "lo": 0.0}, "end": {"hi": 64800.0, "lo": 0.0}})
602 );
603 assert_eq!(
604 serde_json::to_value(native).unwrap(),
605 json!({"start": {"hi": 100.0, "lo": 0.0}, "end": {"hi": 200.0, "lo": 0.0}})
606 );
607
608 assert_eq!(
609 serde_json::from_value::<Period<TT>>(json!({
610 "start": {"hi": 0.0, "lo": 0.0},
611 "end": {"hi": 64800.0, "lo": 0.0}
612 }))
613 .unwrap(),
614 mjd
615 );
616 assert_eq!(
617 serde_json::from_value::<Period<TT>>(json!({
618 "start": {"hi": 0.0, "lo": 0.0},
619 "end": {"hi": 86400.0, "lo": 0.0}
620 }))
621 .unwrap(),
622 jd
623 );
624 assert_eq!(
625 serde_json::from_value::<Period<TT>>(json!({
626 "start": {"hi": 100.0, "lo": 0.0},
627 "end": {"hi": 200.0, "lo": 0.0}
628 }))
629 .unwrap(),
630 native
631 );
632 }
633
634 #[cfg(feature = "serde")]
635 #[test]
636 fn serde_rejects_reversed_periods() {
637 let err = serde_json::from_value::<Period<TT>>(json!({
638 "start": {"hi": 10.0, "lo": 0.0},
639 "end": {"hi": 9.0, "lo": 0.0}
640 }))
641 .unwrap_err();
642 assert!(err.to_string().contains("start"));
643 }
644
645 #[cfg(feature = "serde")]
646 #[test]
647 fn serde_rejects_nonfinite_nested_time_values() {
648 let start = value::MapDeserializer::<_, value::Error>::new(
649 vec![
650 ("hi".into_deserializer(), f64::NAN.into_deserializer()),
651 ("lo".into_deserializer(), 0.0_f64.into_deserializer()),
652 ]
653 .into_iter(),
654 );
655 let end = value::MapDeserializer::<_, value::Error>::new(
656 vec![
657 ("hi".into_deserializer(), 5.0_f64.into_deserializer()),
658 ("lo".into_deserializer(), 0.0_f64.into_deserializer()),
659 ]
660 .into_iter(),
661 );
662 let entries = vec![
663 ("start".into_deserializer(), start),
664 ("end".into_deserializer(), end),
665 ];
666 let deserializer = value::MapDeserializer::<_, value::Error>::new(entries.into_iter());
667 let err = Period::<TT>::deserialize(deserializer).unwrap_err();
668 assert!(err.to_string().contains("finite"));
669 }
670}