use super::{Time, TimeInstant, TimeScale};
use chrono::{DateTime, Utc};
use qtty::Days;
use std::fmt;
#[cfg(feature = "serde")]
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConversionError {
OutOfRange,
}
impl fmt::Display for ConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConversionError::OutOfRange => {
write!(f, "time instant out of representable range for target type")
}
}
}
}
impl std::error::Error for ConversionError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidIntervalError {
StartAfterEnd,
}
impl fmt::Display for InvalidIntervalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InvalidIntervalError::StartAfterEnd => {
write!(f, "interval start must not be after end")
}
}
}
}
impl std::error::Error for InvalidIntervalError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeriodListError {
InvalidInterval {
index: usize,
},
Unsorted {
index: usize,
},
Overlapping {
index: usize,
},
}
impl fmt::Display for PeriodListError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PeriodListError::InvalidInterval { index } => {
write!(f, "interval at index {index} has start > end")
}
PeriodListError::Unsorted { index } => {
write!(f, "interval at index {index} is not sorted by start time")
}
PeriodListError::Overlapping { index } => {
write!(f, "interval at index {index} overlaps with its predecessor")
}
}
}
}
impl std::error::Error for PeriodListError {}
pub trait PeriodTimeTarget<S: TimeScale> {
type Instant: TimeInstant;
fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError>;
}
impl<S: TimeScale, T: TimeScale> PeriodTimeTarget<S> for T {
type Instant = Time<T>;
#[inline]
fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError> {
Ok(value.to::<T>())
}
}
impl<S: TimeScale, T: TimeScale> PeriodTimeTarget<S> for Time<T> {
type Instant = Time<T>;
#[inline]
fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError> {
Ok(value.to::<T>())
}
}
impl<S: TimeScale> PeriodTimeTarget<S> for DateTime<Utc> {
type Instant = DateTime<Utc>;
#[inline]
fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError> {
value.to_utc().ok_or(ConversionError::OutOfRange)
}
}
pub trait PeriodUtcTarget {
type Instant: TimeInstant;
fn convert(value: DateTime<Utc>) -> Self::Instant;
}
impl<S: TimeScale> PeriodUtcTarget for S {
type Instant = Time<S>;
#[inline]
fn convert(value: DateTime<Utc>) -> Self::Instant {
Time::<S>::from_utc(value)
}
}
impl<S: TimeScale> PeriodUtcTarget for Time<S> {
type Instant = Time<S>;
#[inline]
fn convert(value: DateTime<Utc>) -> Self::Instant {
Time::<S>::from_utc(value)
}
}
impl PeriodUtcTarget for DateTime<Utc> {
type Instant = DateTime<Utc>;
#[inline]
fn convert(value: DateTime<Utc>) -> Self::Instant {
value
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Interval<T: TimeInstant> {
pub start: T,
pub end: T,
}
pub type Period<S> = Interval<Time<S>>;
pub type UtcPeriod = Interval<DateTime<Utc>>;
impl<T: TimeInstant> Interval<T> {
pub fn new(start: T, end: T) -> Self {
Interval { start, end }
}
pub fn try_new(start: T, end: T) -> Result<Self, InvalidIntervalError> {
if start <= end {
Ok(Interval { start, end })
} else {
Err(InvalidIntervalError::StartAfterEnd)
}
}
pub fn duration(&self) -> T::Duration {
self.end.difference(&self.start)
}
pub fn intersection(&self, other: &Self) -> Option<Self> {
let start = if self.start >= other.start {
self.start
} else {
other.start
};
let end = if self.end <= other.end {
self.end
} else {
other.end
};
if start < end {
Some(Self::new(start, end))
} else {
None
}
}
}
impl<T: TimeInstant + fmt::Display> fmt::Display for Interval<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} to {}", self.start, self.end)
}
}
impl<S: TimeScale> Interval<Time<S>> {
#[inline]
pub fn to<Target>(
&self,
) -> Result<Interval<<Target as PeriodTimeTarget<S>>::Instant>, ConversionError>
where
Target: PeriodTimeTarget<S>,
{
Ok(Interval::new(
Target::convert(self.start)?,
Target::convert(self.end)?,
))
}
}
impl<T: TimeInstant<Duration = Days>> Interval<T> {
pub fn duration_days(&self) -> Days {
self.duration()
}
}
impl Interval<DateTime<Utc>> {
#[inline]
pub fn to<Target>(&self) -> Interval<<Target as PeriodUtcTarget>::Instant>
where
Target: PeriodUtcTarget,
{
Interval::new(Target::convert(self.start), Target::convert(self.end))
}
pub fn duration_days(&self) -> f64 {
const NANOS_PER_DAY: f64 = 86_400_000_000_000.0;
const SECONDS_PER_DAY: f64 = 86_400.0;
let duration = self.duration();
match duration.num_nanoseconds() {
Some(ns) => ns as f64 / NANOS_PER_DAY,
None => duration.num_seconds() as f64 / SECONDS_PER_DAY,
}
}
pub fn duration_seconds(&self) -> i64 {
self.duration().num_seconds()
}
}
#[cfg(feature = "serde")]
impl Serialize for Interval<crate::ModifiedJulianDate> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("Period", 2)?;
s.serialize_field("start_mjd", &self.start.value())?;
s.serialize_field("end_mjd", &self.end.value())?;
s.end()
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Interval<crate::ModifiedJulianDate> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Raw {
start_mjd: f64,
end_mjd: f64,
}
let raw = Raw::deserialize(deserializer)?;
if !raw.start_mjd.is_finite() || !raw.end_mjd.is_finite() {
return Err(serde::de::Error::custom(
"period MJD values must be finite (not NaN or infinity)",
));
}
if raw.start_mjd > raw.end_mjd {
return Err(serde::de::Error::custom(
"period start must not be after end",
));
}
Ok(Interval::new(
crate::ModifiedJulianDate::new(raw.start_mjd),
crate::ModifiedJulianDate::new(raw.end_mjd),
))
}
}
#[cfg(feature = "serde")]
impl Serialize for Interval<crate::JulianDate> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("Period", 2)?;
s.serialize_field("start_jd", &self.start.value())?;
s.serialize_field("end_jd", &self.end.value())?;
s.end()
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Interval<crate::JulianDate> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Raw {
start_jd: f64,
end_jd: f64,
}
let raw = Raw::deserialize(deserializer)?;
if !raw.start_jd.is_finite() || !raw.end_jd.is_finite() {
return Err(serde::de::Error::custom(
"period JD values must be finite (not NaN or infinity)",
));
}
if raw.start_jd > raw.end_jd {
return Err(serde::de::Error::custom(
"period start must not be after end",
));
}
Ok(Interval::new(
crate::JulianDate::new(raw.start_jd),
crate::JulianDate::new(raw.end_jd),
))
}
}
pub fn complement_within<T: TimeInstant>(
outer: Interval<T>,
periods: &[Interval<T>],
) -> Vec<Interval<T>> {
let mut gaps = Vec::new();
let mut cursor = outer.start;
for p in periods {
if p.start > cursor {
gaps.push(Interval::new(cursor, p.start));
}
if p.end > cursor {
cursor = p.end;
}
}
if cursor < outer.end {
gaps.push(Interval::new(cursor, outer.end));
}
gaps
}
pub fn intersect_periods<T: TimeInstant>(a: &[Interval<T>], b: &[Interval<T>]) -> Vec<Interval<T>> {
let mut result = Vec::new();
let (mut i, mut j) = (0, 0);
while i < a.len() && j < b.len() {
let start = if a[i].start >= b[j].start {
a[i].start
} else {
b[j].start
};
let end = if a[i].end <= b[j].end {
a[i].end
} else {
b[j].end
};
if start < end {
result.push(Interval::new(start, end));
}
if a[i].end <= b[j].end {
i += 1;
} else {
j += 1;
}
}
result
}
pub fn validate_period_list<T: TimeInstant>(
periods: &[Interval<T>],
) -> Result<(), PeriodListError> {
for (i, p) in periods.iter().enumerate() {
if p.start
.partial_cmp(&p.end)
.is_none_or(|o| o == std::cmp::Ordering::Greater)
{
return Err(PeriodListError::InvalidInterval { index: i });
}
}
for i in 1..periods.len() {
if periods[i - 1]
.start
.partial_cmp(&periods[i].start)
.is_none_or(|o| o == std::cmp::Ordering::Greater)
{
return Err(PeriodListError::Unsorted { index: i });
}
if periods[i - 1].end > periods[i].start {
return Err(PeriodListError::Overlapping { index: i });
}
}
Ok(())
}
pub fn normalize_periods<T: TimeInstant>(periods: &[Interval<T>]) -> Vec<Interval<T>> {
if periods.is_empty() {
return Vec::new();
}
let mut sorted: Vec<_> = periods.to_vec();
sorted.sort_by(|a, b| {
a.start
.partial_cmp(&b.start)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut merged = vec![sorted[0]];
for p in &sorted[1..] {
let last = merged.last_mut().unwrap();
if p.start <= last.end {
if p.end > last.end {
last.end = p.end;
}
} else {
merged.push(*p);
}
}
merged
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{JulianDate, ModifiedJulianDate, JD, MJD};
#[test]
fn test_try_new_valid() {
let p = Interval::try_new(
ModifiedJulianDate::new(59000.0),
ModifiedJulianDate::new(59001.0),
);
assert!(p.is_ok());
}
#[test]
fn test_try_new_equal_bounds() {
let p = Interval::try_new(
ModifiedJulianDate::new(59000.0),
ModifiedJulianDate::new(59000.0),
);
assert!(p.is_ok()); }
#[test]
fn test_try_new_invalid() {
let p = Interval::try_new(
ModifiedJulianDate::new(59001.0),
ModifiedJulianDate::new(59000.0),
);
assert_eq!(p, Err(InvalidIntervalError::StartAfterEnd));
}
#[test]
fn test_try_new_nan_rejected() {
let p = Interval::try_new(
ModifiedJulianDate::new(f64::NAN),
ModifiedJulianDate::new(59000.0),
);
assert!(p.is_err());
}
#[test]
fn test_validate_period_list_ok() {
let periods = vec![
Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
];
assert!(validate_period_list(&periods).is_ok());
}
#[test]
fn test_validate_period_list_unsorted() {
let periods = vec![
Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
];
assert_eq!(
validate_period_list(&periods),
Err(PeriodListError::Unsorted { index: 1 })
);
}
#[test]
fn test_validate_period_list_overlapping() {
let periods = vec![
Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(5.0)),
Period::new(ModifiedJulianDate::new(3.0), ModifiedJulianDate::new(8.0)),
];
assert_eq!(
validate_period_list(&periods),
Err(PeriodListError::Overlapping { index: 1 })
);
}
#[test]
fn test_validate_period_list_invalid_interval() {
let periods = vec![Period::new(
ModifiedJulianDate::new(5.0),
ModifiedJulianDate::new(3.0),
)];
assert_eq!(
validate_period_list(&periods),
Err(PeriodListError::InvalidInterval { index: 0 })
);
}
#[test]
fn test_normalize_periods_empty() {
let periods: Vec<Period<MJD>> = vec![];
assert!(normalize_periods(&periods).is_empty());
}
#[test]
fn test_normalize_periods_unsorted_and_overlapping() {
let periods = vec![
Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(6.0)),
];
let merged = normalize_periods(&periods);
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].start.quantity(), Days::new(0.0));
assert_eq!(merged[0].end.quantity(), Days::new(8.0));
}
#[test]
fn test_normalize_periods_disjoint() {
let periods = vec![
Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(6.0)),
Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(2.0)),
];
let merged = normalize_periods(&periods);
assert_eq!(merged.len(), 2);
assert_eq!(merged[0].start.quantity(), Days::new(0.0));
assert_eq!(merged[1].start.quantity(), Days::new(5.0));
}
#[test]
fn test_period_creation_jd() {
let start = JulianDate::new(2451545.0);
let end = JulianDate::new(2451546.0);
let period = Period::new(start, end);
assert_eq!(period.start, start);
assert_eq!(period.end, end);
}
#[test]
fn test_period_scale_conversion_jd_to_mjd() {
let period_jd = Period::new(Time::<JD>::new(2_451_545.0), Time::<JD>::new(2_451_546.0));
let period_mjd = period_jd.to::<MJD>().unwrap();
assert!((period_mjd.start.value() - 51_544.5).abs() < 1e-12);
assert!((period_mjd.end.value() - 51_545.5).abs() < 1e-12);
}
#[test]
fn test_period_scale_conversion_roundtrip() {
let original = Period::new(Time::<MJD>::new(59_000.125), Time::<MJD>::new(59_001.75));
let roundtrip = original.to::<JD>().unwrap().to::<MJD>().unwrap();
assert!((roundtrip.start.value() - original.start.value()).abs() < 1e-12);
assert!((roundtrip.end.value() - original.end.value()).abs() < 1e-12);
}
#[test]
fn test_period_scale_conversion_to_utc() {
let start_utc = DateTime::from_timestamp(1_700_000_000, 0).unwrap();
let end_utc = DateTime::from_timestamp(1_700_000_600, 0).unwrap();
let period_jd = Period::new(
Time::<JD>::from_utc(start_utc),
Time::<JD>::from_utc(end_utc),
);
let period_utc = period_jd.to::<DateTime<Utc>>().unwrap();
let start_delta_ns = period_utc.start.timestamp_nanos_opt().unwrap()
- start_utc.timestamp_nanos_opt().unwrap();
let end_delta_ns =
period_utc.end.timestamp_nanos_opt().unwrap() - end_utc.timestamp_nanos_opt().unwrap();
assert!(start_delta_ns.abs() < 10_000);
assert!(end_delta_ns.abs() < 10_000);
}
#[test]
fn test_period_creation_mjd() {
let start = ModifiedJulianDate::new(59000.0);
let end = ModifiedJulianDate::new(59001.0);
let period = Period::new(start, end);
assert_eq!(period.start, start);
assert_eq!(period.end, end);
}
#[test]
fn test_period_duration_jd() {
let start = JulianDate::new(2451545.0);
let end = JulianDate::new(2451546.5);
let period = Period::new(start, end);
assert_eq!(period.duration_days(), Days::new(1.5));
}
#[test]
fn test_period_duration_mjd() {
let start = ModifiedJulianDate::new(59000.0);
let end = ModifiedJulianDate::new(59001.5);
let period = Period::new(start, end);
assert_eq!(period.duration_days(), Days::new(1.5));
}
#[test]
fn test_period_duration_utc() {
let start = DateTime::from_timestamp(0, 0).unwrap();
let end = DateTime::from_timestamp(86400, 0).unwrap(); let period = Interval::new(start, end);
assert_eq!(period.duration_days(), 1.0);
assert_eq!(period.duration_seconds(), 86400);
}
#[test]
fn test_period_duration_utc_subsecond_precision() {
let start = DateTime::from_timestamp(0, 0).unwrap();
let end = DateTime::from_timestamp(0, 500_000_000).unwrap();
let period = Interval::new(start, end);
let expected_days = 0.5 / 86_400.0;
assert!((period.duration_days() - expected_days).abs() < 1e-15);
assert_eq!(period.duration_seconds(), 0);
}
#[test]
fn test_period_to_conversion() {
let mjd_start = ModifiedJulianDate::new(59000.0);
let mjd_end = ModifiedJulianDate::new(59001.0);
let mjd_period = Period::new(mjd_start, mjd_end);
let utc_period = mjd_period.to::<DateTime<Utc>>().unwrap();
let duration_secs = utc_period.duration().num_seconds();
assert!(
(duration_secs - 86400).abs() <= 1,
"Duration was {} seconds",
duration_secs
);
let back_to_mjd = utc_period.to::<ModifiedJulianDate>();
let start_diff = (back_to_mjd.start.quantity() - mjd_start.quantity())
.value()
.abs();
let end_diff = (back_to_mjd.end.quantity() - mjd_end.quantity())
.value()
.abs();
assert!(start_diff < 1e-6, "Start difference: {}", start_diff);
assert!(end_diff < 1e-6, "End difference: {}", end_diff);
}
#[test]
fn test_period_display() {
let start = ModifiedJulianDate::new(59000.0);
let end = ModifiedJulianDate::new(59001.0);
let period = Period::new(start, end);
let display = format!("{}", period);
assert!(display.contains("MJD 59000"));
assert!(display.contains("MJD 59001"));
assert!(display.contains("to"));
}
#[test]
fn test_period_intersection_overlap() {
let a = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(5.0));
let b = Period::new(ModifiedJulianDate::new(3.0), ModifiedJulianDate::new(8.0));
let overlap = a.intersection(&b).expect("expected overlap");
assert_eq!(overlap.start.quantity(), Days::new(3.0));
assert_eq!(overlap.end.quantity(), Days::new(5.0));
}
#[test]
fn test_period_intersection_disjoint() {
let a = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0));
let b = Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0));
assert_eq!(a.intersection(&b), None);
}
#[test]
fn test_period_intersection_touching_edges() {
let a = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0));
let b = Period::new(ModifiedJulianDate::new(3.0), ModifiedJulianDate::new(8.0));
assert_eq!(a.intersection(&b), None);
}
#[test]
fn test_complement_within_gaps() {
let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
let periods = vec![
Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(4.0)),
Period::new(ModifiedJulianDate::new(6.0), ModifiedJulianDate::new(8.0)),
];
let gaps = complement_within(outer, &periods);
assert_eq!(gaps.len(), 3);
assert_eq!(gaps[0].start.quantity(), Days::new(0.0));
assert_eq!(gaps[0].end.quantity(), Days::new(2.0));
assert_eq!(gaps[1].start.quantity(), Days::new(4.0));
assert_eq!(gaps[1].end.quantity(), Days::new(6.0));
assert_eq!(gaps[2].start.quantity(), Days::new(8.0));
assert_eq!(gaps[2].end.quantity(), Days::new(10.0));
}
#[test]
fn test_complement_within_empty() {
let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
let gaps = complement_within(outer, &[]);
assert_eq!(gaps.len(), 1);
assert_eq!(gaps[0].start.quantity(), Days::new(0.0));
assert_eq!(gaps[0].end.quantity(), Days::new(10.0));
}
#[test]
fn test_complement_within_full() {
let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
let periods = vec![Period::new(
ModifiedJulianDate::new(0.0),
ModifiedJulianDate::new(10.0),
)];
let gaps = complement_within(outer, &periods);
assert!(gaps.is_empty());
}
#[test]
fn test_intersect_periods_overlap() {
let a = vec![Period::new(
ModifiedJulianDate::new(0.0),
ModifiedJulianDate::new(5.0),
)];
let b = vec![Period::new(
ModifiedJulianDate::new(3.0),
ModifiedJulianDate::new(8.0),
)];
let overlap = intersect_periods(&a, &b);
assert_eq!(overlap.len(), 1);
assert_eq!(overlap[0].start.quantity(), Days::new(3.0));
assert_eq!(overlap[0].end.quantity(), Days::new(5.0));
}
#[test]
fn test_intersect_periods_no_overlap() {
let a = vec![Period::new(
ModifiedJulianDate::new(0.0),
ModifiedJulianDate::new(3.0),
)];
let b = vec![Period::new(
ModifiedJulianDate::new(5.0),
ModifiedJulianDate::new(8.0),
)];
let overlap = intersect_periods(&a, &b);
assert!(overlap.is_empty());
}
#[test]
fn test_complement_intersect_roundtrip() {
let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
let above_min = vec![
Period::new(ModifiedJulianDate::new(1.0), ModifiedJulianDate::new(3.0)),
Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(9.0)),
];
let above_max = vec![
Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(4.0)),
Period::new(ModifiedJulianDate::new(7.0), ModifiedJulianDate::new(8.0)),
];
let below_max = complement_within(outer, &above_max);
let between = intersect_periods(&above_min, &below_max);
assert_eq!(between.len(), 3);
assert_eq!(between[0].start.quantity(), Days::new(1.0));
assert_eq!(between[0].end.quantity(), Days::new(2.0));
assert_eq!(between[1].start.quantity(), Days::new(5.0));
assert_eq!(between[1].end.quantity(), Days::new(7.0));
assert_eq!(between[2].start.quantity(), Days::new(8.0));
assert_eq!(between[2].end.quantity(), Days::new(9.0));
}
#[test]
fn test_conversion_error_display() {
let err = ConversionError::OutOfRange;
let msg = format!("{err}");
assert!(msg.contains("out of representable range"), "got: {msg}");
}
#[test]
fn test_conversion_error_is_error() {
let err = ConversionError::OutOfRange;
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_invalid_interval_error_display() {
let err = InvalidIntervalError::StartAfterEnd;
let msg = format!("{err}");
assert!(msg.contains("start must not be after end"), "got: {msg}");
}
#[test]
fn test_invalid_interval_error_is_error() {
let err = InvalidIntervalError::StartAfterEnd;
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_period_list_error_invalid_interval_display() {
let e = PeriodListError::InvalidInterval { index: 0 };
let msg = format!("{e}");
assert!(msg.contains("index 0"), "got: {msg}");
}
#[test]
fn test_period_list_error_unsorted_display() {
let e = PeriodListError::Unsorted { index: 2 };
let msg = format!("{e}");
assert!(msg.contains("index 2"), "got: {msg}");
}
#[test]
fn test_period_list_error_overlapping_display() {
let e = PeriodListError::Overlapping { index: 3 };
let msg = format!("{e}");
assert!(msg.contains("index 3"), "got: {msg}");
}
#[test]
fn test_period_list_error_is_error() {
let e = PeriodListError::InvalidInterval { index: 0 };
let _: &dyn std::error::Error = &e;
}
#[test]
fn test_intersection_self_larger_than_other() {
let a = Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(8.0));
let b = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(5.0));
let overlap = a.intersection(&b).expect("should overlap");
assert_eq!(overlap.start.quantity(), Days::new(2.0));
assert_eq!(overlap.end.quantity(), Days::new(5.0));
}
#[test]
fn test_period_time_target_for_time_type() {
let period_jd = Period::new(Time::<JD>::new(2_451_545.0), Time::<JD>::new(2_451_546.0));
let period_mjd: Interval<ModifiedJulianDate> =
period_jd.to::<ModifiedJulianDate>().unwrap();
assert!((period_mjd.start.value() - 51_544.5).abs() < 1e-12);
assert!((period_mjd.end.value() - 51_545.5).abs() < 1e-12);
}
#[test]
fn test_utc_period_to_datetime_utc_identity() {
let start = DateTime::from_timestamp(0, 0).unwrap();
let end = DateTime::from_timestamp(86400, 0).unwrap();
let utc_period = Interval::new(start, end);
let same: Interval<DateTime<Utc>> = utc_period.to::<DateTime<Utc>>();
assert_eq!(same.start, start);
assert_eq!(same.end, end);
}
#[cfg(feature = "serde")]
#[test]
fn test_period_mjd_serde_roundtrip() {
let p = Period::new(
ModifiedJulianDate::new(59000.0),
ModifiedJulianDate::new(59001.0),
);
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("start_mjd"), "serialized: {json}");
let back: Period<MJD> = serde_json::from_str(&json).unwrap();
assert!((back.start.value() - 59000.0).abs() < 1e-12);
assert!((back.end.value() - 59001.0).abs() < 1e-12);
}
#[cfg(feature = "serde")]
#[test]
fn test_period_mjd_deserialize_start_after_end_rejected() {
let json = r#"{"start_mjd": 59001.0, "end_mjd": 59000.0}"#;
let result: Result<Period<MJD>, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[cfg(feature = "serde")]
#[test]
fn test_period_jd_serde_roundtrip() {
let p = Period::new(JulianDate::new(2_451_545.0), JulianDate::new(2_451_546.0));
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("start_jd"), "serialized: {json}");
let back: Period<JD> = serde_json::from_str(&json).unwrap();
assert!((back.start.value() - 2_451_545.0).abs() < 1e-12);
assert!((back.end.value() - 2_451_546.0).abs() < 1e-12);
}
#[cfg(feature = "serde")]
#[test]
fn test_period_jd_deserialize_start_after_end_rejected() {
let json = r#"{"start_jd": 2451546.0, "end_jd": 2451545.0}"#;
let result: Result<Period<JD>, _> = serde_json::from_str(json);
assert!(result.is_err());
}
}