1use super::{Time, TimeInstant, TimeScale};
11use chrono::{DateTime, Utc};
12use qtty::Days;
13use std::fmt;
14
15#[cfg(feature = "serde")]
16use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ConversionError {
24 OutOfRange,
26}
27
28impl fmt::Display for ConversionError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 ConversionError::OutOfRange => {
32 write!(f, "time instant out of representable range for target type")
33 }
34 }
35 }
36}
37
38impl std::error::Error for ConversionError {}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum InvalidIntervalError {
43 StartAfterEnd,
48}
49
50impl fmt::Display for InvalidIntervalError {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 InvalidIntervalError::StartAfterEnd => {
54 write!(f, "interval start must not be after end")
55 }
56 }
57 }
58}
59
60impl std::error::Error for InvalidIntervalError {}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum PeriodListError {
65 InvalidInterval {
67 index: usize,
69 },
70 Unsorted {
72 index: usize,
74 },
75 Overlapping {
77 index: usize,
79 },
80}
81
82impl fmt::Display for PeriodListError {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 PeriodListError::InvalidInterval { index } => {
86 write!(f, "interval at index {index} has start > end")
87 }
88 PeriodListError::Unsorted { index } => {
89 write!(f, "interval at index {index} is not sorted by start time")
90 }
91 PeriodListError::Overlapping { index } => {
92 write!(f, "interval at index {index} overlaps with its predecessor")
93 }
94 }
95 }
96}
97
98impl std::error::Error for PeriodListError {}
99
100pub trait PeriodTimeTarget<S: TimeScale> {
105 type Instant: TimeInstant;
106
107 fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError>;
108}
109
110impl<S: TimeScale, T: TimeScale> PeriodTimeTarget<S> for T {
111 type Instant = Time<T>;
112
113 #[inline]
114 fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError> {
115 Ok(value.to::<T>())
116 }
117}
118
119impl<S: TimeScale, T: TimeScale> PeriodTimeTarget<S> for Time<T> {
120 type Instant = Time<T>;
121
122 #[inline]
123 fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError> {
124 Ok(value.to::<T>())
125 }
126}
127
128impl<S: TimeScale> PeriodTimeTarget<S> for DateTime<Utc> {
129 type Instant = DateTime<Utc>;
130
131 #[inline]
132 fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError> {
133 value.to_utc().ok_or(ConversionError::OutOfRange)
134 }
135}
136
137pub trait PeriodUtcTarget {
139 type Instant: TimeInstant;
140
141 fn convert(value: DateTime<Utc>) -> Self::Instant;
142}
143
144impl<S: TimeScale> PeriodUtcTarget for S {
145 type Instant = Time<S>;
146
147 #[inline]
148 fn convert(value: DateTime<Utc>) -> Self::Instant {
149 Time::<S>::from_utc(value)
150 }
151}
152
153impl<S: TimeScale> PeriodUtcTarget for Time<S> {
154 type Instant = Time<S>;
155
156 #[inline]
157 fn convert(value: DateTime<Utc>) -> Self::Instant {
158 Time::<S>::from_utc(value)
159 }
160}
161
162impl PeriodUtcTarget for DateTime<Utc> {
163 type Instant = DateTime<Utc>;
164
165 #[inline]
166 fn convert(value: DateTime<Utc>) -> Self::Instant {
167 value
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq)]
191pub struct Interval<T: TimeInstant> {
192 pub start: T,
193 pub end: T,
194}
195
196pub type Period<S> = Interval<Time<S>>;
201
202pub type UtcPeriod = Interval<DateTime<Utc>>;
204
205impl<T: TimeInstant> Interval<T> {
206 pub fn new(start: T, end: T) -> Self {
228 Interval { start, end }
229 }
230
231 pub fn try_new(start: T, end: T) -> Result<Self, InvalidIntervalError> {
250 if start <= end {
251 Ok(Interval { start, end })
252 } else {
253 Err(InvalidIntervalError::StartAfterEnd)
254 }
255 }
256
257 pub fn duration(&self) -> T::Duration {
274 self.end.difference(&self.start)
275 }
276
277 pub fn intersection(&self, other: &Self) -> Option<Self> {
283 let start = if self.start >= other.start {
284 self.start
285 } else {
286 other.start
287 };
288 let end = if self.end <= other.end {
289 self.end
290 } else {
291 other.end
292 };
293
294 if start < end {
295 Some(Self::new(start, end))
296 } else {
297 None
298 }
299 }
300}
301
302impl<T: TimeInstant + fmt::Display> fmt::Display for Interval<T> {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 write!(f, "{} to {}", self.start, self.end)
306 }
307}
308
309impl<S: TimeScale> Interval<Time<S>> {
310 #[inline]
339 pub fn to<Target>(
340 &self,
341 ) -> Result<Interval<<Target as PeriodTimeTarget<S>>::Instant>, ConversionError>
342 where
343 Target: PeriodTimeTarget<S>,
344 {
345 Ok(Interval::new(
346 Target::convert(self.start)?,
347 Target::convert(self.end)?,
348 ))
349 }
350}
351
352impl<T: TimeInstant<Duration = Days>> Interval<T> {
354 pub fn duration_days(&self) -> Days {
373 self.duration()
374 }
375}
376
377impl Interval<DateTime<Utc>> {
379 #[inline]
386 pub fn to<Target>(&self) -> Interval<<Target as PeriodUtcTarget>::Instant>
387 where
388 Target: PeriodUtcTarget,
389 {
390 Interval::new(Target::convert(self.start), Target::convert(self.end))
391 }
392
393 pub fn duration_days(&self) -> f64 {
397 const NANOS_PER_DAY: f64 = 86_400_000_000_000.0;
398 const SECONDS_PER_DAY: f64 = 86_400.0;
399
400 let duration = self.duration();
401 match duration.num_nanoseconds() {
402 Some(ns) => ns as f64 / NANOS_PER_DAY,
403 None => duration.num_seconds() as f64 / SECONDS_PER_DAY,
405 }
406 }
407
408 pub fn duration_seconds(&self) -> i64 {
410 self.duration().num_seconds()
411 }
412}
413
414#[cfg(feature = "serde")]
419impl Serialize for Interval<crate::ModifiedJulianDate> {
420 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
421 where
422 S: Serializer,
423 {
424 let mut s = serializer.serialize_struct("Period", 2)?;
425 s.serialize_field("start_mjd", &self.start.value())?;
426 s.serialize_field("end_mjd", &self.end.value())?;
427 s.end()
428 }
429}
430
431#[cfg(feature = "serde")]
432impl<'de> Deserialize<'de> for Interval<crate::ModifiedJulianDate> {
433 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
434 where
435 D: Deserializer<'de>,
436 {
437 #[derive(Deserialize)]
438 struct Raw {
439 start_mjd: f64,
440 end_mjd: f64,
441 }
442
443 let raw = Raw::deserialize(deserializer)?;
444 if !raw.start_mjd.is_finite() || !raw.end_mjd.is_finite() {
445 return Err(serde::de::Error::custom(
446 "period MJD values must be finite (not NaN or infinity)",
447 ));
448 }
449 if raw.start_mjd > raw.end_mjd {
450 return Err(serde::de::Error::custom(
451 "period start must not be after end",
452 ));
453 }
454 Ok(Interval::new(
455 crate::ModifiedJulianDate::new(raw.start_mjd),
456 crate::ModifiedJulianDate::new(raw.end_mjd),
457 ))
458 }
459}
460
461#[cfg(feature = "serde")]
463impl Serialize for Interval<crate::JulianDate> {
464 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
465 where
466 S: Serializer,
467 {
468 let mut s = serializer.serialize_struct("Period", 2)?;
469 s.serialize_field("start_jd", &self.start.value())?;
470 s.serialize_field("end_jd", &self.end.value())?;
471 s.end()
472 }
473}
474
475#[cfg(feature = "serde")]
476impl<'de> Deserialize<'de> for Interval<crate::JulianDate> {
477 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
478 where
479 D: Deserializer<'de>,
480 {
481 #[derive(Deserialize)]
482 struct Raw {
483 start_jd: f64,
484 end_jd: f64,
485 }
486
487 let raw = Raw::deserialize(deserializer)?;
488 if !raw.start_jd.is_finite() || !raw.end_jd.is_finite() {
489 return Err(serde::de::Error::custom(
490 "period JD values must be finite (not NaN or infinity)",
491 ));
492 }
493 if raw.start_jd > raw.end_jd {
494 return Err(serde::de::Error::custom(
495 "period start must not be after end",
496 ));
497 }
498 Ok(Interval::new(
499 crate::JulianDate::new(raw.start_jd),
500 crate::JulianDate::new(raw.end_jd),
501 ))
502 }
503}
504
505pub fn complement_within<T: TimeInstant>(
520 outer: Interval<T>,
521 periods: &[Interval<T>],
522) -> Vec<Interval<T>> {
523 let mut gaps = Vec::new();
524 let mut cursor = outer.start;
525 for p in periods {
526 if p.start > cursor {
527 gaps.push(Interval::new(cursor, p.start));
528 }
529 if p.end > cursor {
530 cursor = p.end;
531 }
532 }
533 if cursor < outer.end {
534 gaps.push(Interval::new(cursor, outer.end));
535 }
536 gaps
537}
538
539pub fn intersect_periods<T: TimeInstant>(a: &[Interval<T>], b: &[Interval<T>]) -> Vec<Interval<T>> {
550 let mut result = Vec::new();
551 let (mut i, mut j) = (0, 0);
552 while i < a.len() && j < b.len() {
553 let start = if a[i].start >= b[j].start {
554 a[i].start
555 } else {
556 b[j].start
557 };
558 let end = if a[i].end <= b[j].end {
559 a[i].end
560 } else {
561 b[j].end
562 };
563 if start < end {
564 result.push(Interval::new(start, end));
565 }
566 if a[i].end <= b[j].end {
567 i += 1;
568 } else {
569 j += 1;
570 }
571 }
572 result
573}
574
575pub fn validate_period_list<T: TimeInstant>(
597 periods: &[Interval<T>],
598) -> Result<(), PeriodListError> {
599 for (i, p) in periods.iter().enumerate() {
600 if p.start
601 .partial_cmp(&p.end)
602 .is_none_or(|o| o == std::cmp::Ordering::Greater)
603 {
604 return Err(PeriodListError::InvalidInterval { index: i });
605 }
606 }
607 for i in 1..periods.len() {
608 if periods[i - 1]
609 .start
610 .partial_cmp(&periods[i].start)
611 .is_none_or(|o| o == std::cmp::Ordering::Greater)
612 {
613 return Err(PeriodListError::Unsorted { index: i });
614 }
615 if periods[i - 1].end > periods[i].start {
616 return Err(PeriodListError::Overlapping { index: i });
617 }
618 }
619 Ok(())
620}
621
622pub fn normalize_periods<T: TimeInstant>(periods: &[Interval<T>]) -> Vec<Interval<T>> {
642 if periods.is_empty() {
643 return Vec::new();
644 }
645 let mut sorted: Vec<_> = periods.to_vec();
646 sorted.sort_by(|a, b| {
647 a.start
648 .partial_cmp(&b.start)
649 .unwrap_or(std::cmp::Ordering::Equal)
650 });
651 let mut merged = vec![sorted[0]];
652 for p in &sorted[1..] {
653 let last = merged.last_mut().unwrap();
654 if p.start <= last.end {
655 if p.end > last.end {
657 last.end = p.end;
658 }
659 } else {
660 merged.push(*p);
661 }
662 }
663 merged
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669 use crate::{JulianDate, ModifiedJulianDate, JD, MJD};
670
671 #[test]
672 fn test_try_new_valid() {
673 let p = Interval::try_new(
674 ModifiedJulianDate::new(59000.0),
675 ModifiedJulianDate::new(59001.0),
676 );
677 assert!(p.is_ok());
678 }
679
680 #[test]
681 fn test_try_new_equal_bounds() {
682 let p = Interval::try_new(
683 ModifiedJulianDate::new(59000.0),
684 ModifiedJulianDate::new(59000.0),
685 );
686 assert!(p.is_ok()); }
688
689 #[test]
690 fn test_try_new_invalid() {
691 let p = Interval::try_new(
692 ModifiedJulianDate::new(59001.0),
693 ModifiedJulianDate::new(59000.0),
694 );
695 assert_eq!(p, Err(InvalidIntervalError::StartAfterEnd));
696 }
697
698 #[test]
699 fn test_try_new_nan_rejected() {
700 let p = Interval::try_new(
701 ModifiedJulianDate::new(f64::NAN),
702 ModifiedJulianDate::new(59000.0),
703 );
704 assert!(p.is_err());
705 }
706
707 #[test]
708 fn test_validate_period_list_ok() {
709 let periods = vec![
710 Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
711 Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
712 ];
713 assert!(validate_period_list(&periods).is_ok());
714 }
715
716 #[test]
717 fn test_validate_period_list_unsorted() {
718 let periods = vec![
719 Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
720 Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
721 ];
722 assert_eq!(
723 validate_period_list(&periods),
724 Err(PeriodListError::Unsorted { index: 1 })
725 );
726 }
727
728 #[test]
729 fn test_validate_period_list_overlapping() {
730 let periods = vec![
731 Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(5.0)),
732 Period::new(ModifiedJulianDate::new(3.0), ModifiedJulianDate::new(8.0)),
733 ];
734 assert_eq!(
735 validate_period_list(&periods),
736 Err(PeriodListError::Overlapping { index: 1 })
737 );
738 }
739
740 #[test]
741 fn test_validate_period_list_invalid_interval() {
742 let periods = vec![Period::new(
743 ModifiedJulianDate::new(5.0),
744 ModifiedJulianDate::new(3.0),
745 )];
746 assert_eq!(
747 validate_period_list(&periods),
748 Err(PeriodListError::InvalidInterval { index: 0 })
749 );
750 }
751
752 #[test]
753 fn test_normalize_periods_empty() {
754 let periods: Vec<Period<MJD>> = vec![];
755 assert!(normalize_periods(&periods).is_empty());
756 }
757
758 #[test]
759 fn test_normalize_periods_unsorted_and_overlapping() {
760 let periods = vec![
761 Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
762 Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
763 Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(6.0)),
764 ];
765 let merged = normalize_periods(&periods);
766 assert_eq!(merged.len(), 1);
767 assert_eq!(merged[0].start.quantity(), Days::new(0.0));
768 assert_eq!(merged[0].end.quantity(), Days::new(8.0));
769 }
770
771 #[test]
772 fn test_normalize_periods_disjoint() {
773 let periods = vec![
774 Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(6.0)),
775 Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(2.0)),
776 ];
777 let merged = normalize_periods(&periods);
778 assert_eq!(merged.len(), 2);
779 assert_eq!(merged[0].start.quantity(), Days::new(0.0));
780 assert_eq!(merged[1].start.quantity(), Days::new(5.0));
781 }
782
783 #[test]
784 fn test_period_creation_jd() {
785 let start = JulianDate::new(2451545.0);
786 let end = JulianDate::new(2451546.0);
787 let period = Period::new(start, end);
788
789 assert_eq!(period.start, start);
790 assert_eq!(period.end, end);
791 }
792
793 #[test]
794 fn test_period_scale_conversion_jd_to_mjd() {
795 let period_jd = Period::new(Time::<JD>::new(2_451_545.0), Time::<JD>::new(2_451_546.0));
796 let period_mjd = period_jd.to::<MJD>().unwrap();
797
798 assert!((period_mjd.start.value() - 51_544.5).abs() < 1e-12);
799 assert!((period_mjd.end.value() - 51_545.5).abs() < 1e-12);
800 }
801
802 #[test]
803 fn test_period_scale_conversion_roundtrip() {
804 let original = Period::new(Time::<MJD>::new(59_000.125), Time::<MJD>::new(59_001.75));
805 let roundtrip = original.to::<JD>().unwrap().to::<MJD>().unwrap();
806
807 assert!((roundtrip.start.value() - original.start.value()).abs() < 1e-12);
808 assert!((roundtrip.end.value() - original.end.value()).abs() < 1e-12);
809 }
810
811 #[test]
812 fn test_period_scale_conversion_to_utc() {
813 let start_utc = DateTime::from_timestamp(1_700_000_000, 0).unwrap();
814 let end_utc = DateTime::from_timestamp(1_700_000_600, 0).unwrap();
815 let period_jd = Period::new(
816 Time::<JD>::from_utc(start_utc),
817 Time::<JD>::from_utc(end_utc),
818 );
819
820 let period_utc = period_jd.to::<DateTime<Utc>>().unwrap();
821 let start_delta_ns = period_utc.start.timestamp_nanos_opt().unwrap()
822 - start_utc.timestamp_nanos_opt().unwrap();
823 let end_delta_ns =
824 period_utc.end.timestamp_nanos_opt().unwrap() - end_utc.timestamp_nanos_opt().unwrap();
825 assert!(start_delta_ns.abs() < 10_000);
826 assert!(end_delta_ns.abs() < 10_000);
827 }
828
829 #[test]
830 fn test_period_creation_mjd() {
831 let start = ModifiedJulianDate::new(59000.0);
832 let end = ModifiedJulianDate::new(59001.0);
833 let period = Period::new(start, end);
834
835 assert_eq!(period.start, start);
836 assert_eq!(period.end, end);
837 }
838
839 #[test]
840 fn test_period_duration_jd() {
841 let start = JulianDate::new(2451545.0);
842 let end = JulianDate::new(2451546.5);
843 let period = Period::new(start, end);
844
845 assert_eq!(period.duration_days(), Days::new(1.5));
846 }
847
848 #[test]
849 fn test_period_duration_mjd() {
850 let start = ModifiedJulianDate::new(59000.0);
851 let end = ModifiedJulianDate::new(59001.5);
852 let period = Period::new(start, end);
853
854 assert_eq!(period.duration_days(), Days::new(1.5));
855 }
856
857 #[test]
858 fn test_period_duration_utc() {
859 let start = DateTime::from_timestamp(0, 0).unwrap();
860 let end = DateTime::from_timestamp(86400, 0).unwrap(); let period = Interval::new(start, end);
862
863 assert_eq!(period.duration_days(), 1.0);
864 assert_eq!(period.duration_seconds(), 86400);
865 }
866
867 #[test]
868 fn test_period_duration_utc_subsecond_precision() {
869 let start = DateTime::from_timestamp(0, 0).unwrap();
870 let end = DateTime::from_timestamp(0, 500_000_000).unwrap();
871 let period = Interval::new(start, end);
872
873 let expected_days = 0.5 / 86_400.0;
874 assert!((period.duration_days() - expected_days).abs() < 1e-15);
875 assert_eq!(period.duration_seconds(), 0);
876 }
877
878 #[test]
879 fn test_period_to_conversion() {
880 let mjd_start = ModifiedJulianDate::new(59000.0);
881 let mjd_end = ModifiedJulianDate::new(59001.0);
882 let mjd_period = Period::new(mjd_start, mjd_end);
883
884 let utc_period = mjd_period.to::<DateTime<Utc>>().unwrap();
885
886 let duration_secs = utc_period.duration().num_seconds();
888 assert!(
889 (duration_secs - 86400).abs() <= 1,
890 "Duration was {} seconds",
891 duration_secs
892 );
893
894 let back_to_mjd = utc_period.to::<ModifiedJulianDate>();
896 let start_diff = (back_to_mjd.start.quantity() - mjd_start.quantity())
897 .value()
898 .abs();
899 let end_diff = (back_to_mjd.end.quantity() - mjd_end.quantity())
900 .value()
901 .abs();
902 assert!(start_diff < 1e-6, "Start difference: {}", start_diff);
903 assert!(end_diff < 1e-6, "End difference: {}", end_diff);
904 }
905
906 #[test]
907 fn test_period_display() {
908 let start = ModifiedJulianDate::new(59000.0);
909 let end = ModifiedJulianDate::new(59001.0);
910 let period = Period::new(start, end);
911
912 let display = format!("{}", period);
913 assert!(display.contains("MJD 59000"));
914 assert!(display.contains("MJD 59001"));
915 assert!(display.contains("to"));
916 }
917
918 #[test]
919 fn test_period_intersection_overlap() {
920 let a = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(5.0));
921 let b = Period::new(ModifiedJulianDate::new(3.0), ModifiedJulianDate::new(8.0));
922
923 let overlap = a.intersection(&b).expect("expected overlap");
924 assert_eq!(overlap.start.quantity(), Days::new(3.0));
925 assert_eq!(overlap.end.quantity(), Days::new(5.0));
926 }
927
928 #[test]
929 fn test_period_intersection_disjoint() {
930 let a = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0));
931 let b = Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0));
932
933 assert_eq!(a.intersection(&b), None);
934 }
935
936 #[test]
937 fn test_period_intersection_touching_edges() {
938 let a = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0));
939 let b = Period::new(ModifiedJulianDate::new(3.0), ModifiedJulianDate::new(8.0));
940
941 assert_eq!(a.intersection(&b), None);
942 }
943
944 #[test]
945 fn test_complement_within_gaps() {
946 let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
947 let periods = vec![
948 Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(4.0)),
949 Period::new(ModifiedJulianDate::new(6.0), ModifiedJulianDate::new(8.0)),
950 ];
951 let gaps = complement_within(outer, &periods);
952 assert_eq!(gaps.len(), 3);
953 assert_eq!(gaps[0].start.quantity(), Days::new(0.0));
954 assert_eq!(gaps[0].end.quantity(), Days::new(2.0));
955 assert_eq!(gaps[1].start.quantity(), Days::new(4.0));
956 assert_eq!(gaps[1].end.quantity(), Days::new(6.0));
957 assert_eq!(gaps[2].start.quantity(), Days::new(8.0));
958 assert_eq!(gaps[2].end.quantity(), Days::new(10.0));
959 }
960
961 #[test]
962 fn test_complement_within_empty() {
963 let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
964 let gaps = complement_within(outer, &[]);
965 assert_eq!(gaps.len(), 1);
966 assert_eq!(gaps[0].start.quantity(), Days::new(0.0));
967 assert_eq!(gaps[0].end.quantity(), Days::new(10.0));
968 }
969
970 #[test]
971 fn test_complement_within_full() {
972 let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
973 let periods = vec![Period::new(
974 ModifiedJulianDate::new(0.0),
975 ModifiedJulianDate::new(10.0),
976 )];
977 let gaps = complement_within(outer, &periods);
978 assert!(gaps.is_empty());
979 }
980
981 #[test]
982 fn test_intersect_periods_overlap() {
983 let a = vec![Period::new(
984 ModifiedJulianDate::new(0.0),
985 ModifiedJulianDate::new(5.0),
986 )];
987 let b = vec![Period::new(
988 ModifiedJulianDate::new(3.0),
989 ModifiedJulianDate::new(8.0),
990 )];
991 let overlap = intersect_periods(&a, &b);
992 assert_eq!(overlap.len(), 1);
993 assert_eq!(overlap[0].start.quantity(), Days::new(3.0));
994 assert_eq!(overlap[0].end.quantity(), Days::new(5.0));
995 }
996
997 #[test]
998 fn test_intersect_periods_no_overlap() {
999 let a = vec![Period::new(
1000 ModifiedJulianDate::new(0.0),
1001 ModifiedJulianDate::new(3.0),
1002 )];
1003 let b = vec![Period::new(
1004 ModifiedJulianDate::new(5.0),
1005 ModifiedJulianDate::new(8.0),
1006 )];
1007 let overlap = intersect_periods(&a, &b);
1008 assert!(overlap.is_empty());
1009 }
1010
1011 #[test]
1012 fn test_complement_intersect_roundtrip() {
1013 let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
1015 let above_min = vec![
1016 Period::new(ModifiedJulianDate::new(1.0), ModifiedJulianDate::new(3.0)),
1017 Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(9.0)),
1018 ];
1019 let above_max = vec![
1020 Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(4.0)),
1021 Period::new(ModifiedJulianDate::new(7.0), ModifiedJulianDate::new(8.0)),
1022 ];
1023 let below_max = complement_within(outer, &above_max);
1024 let between = intersect_periods(&above_min, &below_max);
1025 assert_eq!(between.len(), 3);
1030 assert_eq!(between[0].start.quantity(), Days::new(1.0));
1031 assert_eq!(between[0].end.quantity(), Days::new(2.0));
1032 assert_eq!(between[1].start.quantity(), Days::new(5.0));
1033 assert_eq!(between[1].end.quantity(), Days::new(7.0));
1034 assert_eq!(between[2].start.quantity(), Days::new(8.0));
1035 assert_eq!(between[2].end.quantity(), Days::new(9.0));
1036 }
1037
1038 #[test]
1041 fn test_conversion_error_display() {
1042 let err = ConversionError::OutOfRange;
1043 let msg = format!("{err}");
1044 assert!(msg.contains("out of representable range"), "got: {msg}");
1045 }
1046
1047 #[test]
1048 fn test_conversion_error_is_error() {
1049 let err = ConversionError::OutOfRange;
1050 let _: &dyn std::error::Error = &err;
1052 }
1053
1054 #[test]
1055 fn test_invalid_interval_error_display() {
1056 let err = InvalidIntervalError::StartAfterEnd;
1057 let msg = format!("{err}");
1058 assert!(msg.contains("start must not be after end"), "got: {msg}");
1059 }
1060
1061 #[test]
1062 fn test_invalid_interval_error_is_error() {
1063 let err = InvalidIntervalError::StartAfterEnd;
1064 let _: &dyn std::error::Error = &err;
1065 }
1066
1067 #[test]
1068 fn test_period_list_error_invalid_interval_display() {
1069 let e = PeriodListError::InvalidInterval { index: 0 };
1070 let msg = format!("{e}");
1071 assert!(msg.contains("index 0"), "got: {msg}");
1072 }
1073
1074 #[test]
1075 fn test_period_list_error_unsorted_display() {
1076 let e = PeriodListError::Unsorted { index: 2 };
1077 let msg = format!("{e}");
1078 assert!(msg.contains("index 2"), "got: {msg}");
1079 }
1080
1081 #[test]
1082 fn test_period_list_error_overlapping_display() {
1083 let e = PeriodListError::Overlapping { index: 3 };
1084 let msg = format!("{e}");
1085 assert!(msg.contains("index 3"), "got: {msg}");
1086 }
1087
1088 #[test]
1089 fn test_period_list_error_is_error() {
1090 let e = PeriodListError::InvalidInterval { index: 0 };
1091 let _: &dyn std::error::Error = &e;
1092 }
1093
1094 #[test]
1095 fn test_intersection_self_larger_than_other() {
1096 let a = Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(8.0));
1099 let b = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(5.0));
1100 let overlap = a.intersection(&b).expect("should overlap");
1101 assert_eq!(overlap.start.quantity(), Days::new(2.0));
1102 assert_eq!(overlap.end.quantity(), Days::new(5.0));
1103 }
1104
1105 #[test]
1106 fn test_period_time_target_for_time_type() {
1107 let period_jd = Period::new(Time::<JD>::new(2_451_545.0), Time::<JD>::new(2_451_546.0));
1110 let period_mjd: Interval<ModifiedJulianDate> =
1111 period_jd.to::<ModifiedJulianDate>().unwrap();
1112 assert!((period_mjd.start.value() - 51_544.5).abs() < 1e-12);
1113 assert!((period_mjd.end.value() - 51_545.5).abs() < 1e-12);
1114 }
1115
1116 #[test]
1117 fn test_utc_period_to_datetime_utc_identity() {
1118 let start = DateTime::from_timestamp(0, 0).unwrap();
1121 let end = DateTime::from_timestamp(86400, 0).unwrap();
1122 let utc_period = Interval::new(start, end);
1123 let same: Interval<DateTime<Utc>> = utc_period.to::<DateTime<Utc>>();
1124 assert_eq!(same.start, start);
1125 assert_eq!(same.end, end);
1126 }
1127
1128 #[cfg(feature = "serde")]
1129 #[test]
1130 fn test_period_mjd_serde_roundtrip() {
1131 let p = Period::new(
1132 ModifiedJulianDate::new(59000.0),
1133 ModifiedJulianDate::new(59001.0),
1134 );
1135 let json = serde_json::to_string(&p).unwrap();
1136 assert!(json.contains("start_mjd"), "serialized: {json}");
1137 let back: Period<MJD> = serde_json::from_str(&json).unwrap();
1138 assert!((back.start.value() - 59000.0).abs() < 1e-12);
1139 assert!((back.end.value() - 59001.0).abs() < 1e-12);
1140 }
1141
1142 #[cfg(feature = "serde")]
1143 #[test]
1144 fn test_period_mjd_deserialize_start_after_end_rejected() {
1145 let json = r#"{"start_mjd": 59001.0, "end_mjd": 59000.0}"#;
1146 let result: Result<Period<MJD>, _> = serde_json::from_str(json);
1147 assert!(result.is_err());
1148 }
1149
1150 #[cfg(feature = "serde")]
1151 #[test]
1152 fn test_period_jd_serde_roundtrip() {
1153 let p = Period::new(JulianDate::new(2_451_545.0), JulianDate::new(2_451_546.0));
1154 let json = serde_json::to_string(&p).unwrap();
1155 assert!(json.contains("start_jd"), "serialized: {json}");
1156 let back: Period<JD> = serde_json::from_str(&json).unwrap();
1157 assert!((back.start.value() - 2_451_545.0).abs() < 1e-12);
1158 assert!((back.end.value() - 2_451_546.0).abs() < 1e-12);
1159 }
1160
1161 #[cfg(feature = "serde")]
1162 #[test]
1163 fn test_period_jd_deserialize_start_after_end_rejected() {
1164 let json = r#"{"start_jd": 2451546.0, "end_jd": 2451545.0}"#;
1165 let result: Result<Period<JD>, _> = serde_json::from_str(json);
1166 assert!(result.is_err());
1167 }
1168}