use core::fmt;
use crate::time::Instant;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TimeRange {
pub start: Option<Instant>,
pub end: Option<Instant>,
}
impl TimeRange {
pub const fn new(start: Option<Instant>, end: Option<Instant>) -> Self {
Self { start, end }
}
pub fn contains(&self, instant: Instant) -> bool {
let after_start = self.start.is_none_or(|start| {
same_scale_and_jd(instant, start)
&& instant.julian_day.days() >= start.julian_day.days()
});
let before_end = self.end.is_none_or(|end| {
same_scale_and_jd(instant, end) && instant.julian_day.days() <= end.julian_day.days()
});
after_start && before_end
}
pub fn validate(self) -> Result<(), TimeRangeValidationError> {
if let Some(start) = self.start {
if !start.julian_day.days().is_finite() {
return Err(TimeRangeValidationError::non_finite_bound("start", start));
}
}
if let Some(end) = self.end {
if !end.julian_day.days().is_finite() {
return Err(TimeRangeValidationError::non_finite_bound("end", end));
}
}
if let (Some(start), Some(end)) = (self.start, self.end) {
if start.scale != end.scale {
return Err(TimeRangeValidationError::scale_mismatch(start, end));
}
if start.julian_day.days() > end.julian_day.days() {
return Err(TimeRangeValidationError::out_of_order(start, end));
}
}
Ok(())
}
pub fn summary_line(&self) -> String {
match (self.start, self.end) {
(Some(start), Some(end)) => format!(
"{} → {}",
format_time_range_instant(start),
format_time_range_instant(end)
),
(Some(start), None) => format!("from {}", format_time_range_instant(start)),
(None, Some(end)) => format!("through {}", format_time_range_instant(end)),
(None, None) => "unbounded".to_string(),
}
}
}
impl fmt::Display for TimeRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
pub(crate) fn format_time_range_instant(instant: Instant) -> String {
format!("JD {:.1} ({})", instant.julian_day.days(), instant.scale)
}
pub(crate) fn same_scale_and_jd(a: Instant, b: Instant) -> bool {
a.scale == b.scale && a.julian_day.days().is_finite() && b.julian_day.days().is_finite()
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TimeRangeValidationError {
NonFiniteBound {
bound: &'static str,
instant: Instant,
},
ScaleMismatch {
start: Instant,
end: Instant,
},
OutOfOrder {
start: Instant,
end: Instant,
},
}
impl TimeRangeValidationError {
pub(crate) const fn non_finite_bound(bound: &'static str, instant: Instant) -> Self {
Self::NonFiniteBound { bound, instant }
}
pub(crate) const fn scale_mismatch(start: Instant, end: Instant) -> Self {
Self::ScaleMismatch { start, end }
}
pub(crate) const fn out_of_order(start: Instant, end: Instant) -> Self {
Self::OutOfOrder { start, end }
}
pub fn summary_line(&self) -> String {
match self {
Self::NonFiniteBound { bound, instant } => format!(
"time range bound `{bound}` must be finite: {}",
format_time_range_instant(*instant)
),
Self::ScaleMismatch { start, end } => format!(
"time range bounds must use the same time scale: start={}; end={}",
format_time_range_instant(*start),
format_time_range_instant(*end)
),
Self::OutOfOrder { start, end } => format!(
"time range end must not precede the start: start={}; end={}",
format_time_range_instant(*start),
format_time_range_instant(*end)
),
}
}
}
impl fmt::Display for TimeRangeValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
impl std::error::Error for TimeRangeValidationError {}