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