pleiades_types/
time_range.rs1use core::fmt;
4
5use crate::time::Instant;
6
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct TimeRange {
11 pub start: Option<Instant>,
13 pub end: Option<Instant>,
15}
16
17impl TimeRange {
18 pub const fn new(start: Option<Instant>, end: Option<Instant>) -> Self {
20 Self { start, end }
21 }
22
23 pub fn contains(&self, instant: Instant) -> bool {
30 let after_start = self.start.is_none_or(|start| {
31 same_scale_and_jd(instant, start)
32 && instant.julian_day.days() >= start.julian_day.days()
33 });
34 let before_end = self.end.is_none_or(|end| {
35 same_scale_and_jd(instant, end) && instant.julian_day.days() <= end.julian_day.days()
36 });
37 after_start && before_end
38 }
39
40 pub fn validate(self) -> Result<(), TimeRangeValidationError> {
47 if let Some(start) = self.start {
48 if !start.julian_day.days().is_finite() {
49 return Err(TimeRangeValidationError::non_finite_bound("start", start));
50 }
51 }
52 if let Some(end) = self.end {
53 if !end.julian_day.days().is_finite() {
54 return Err(TimeRangeValidationError::non_finite_bound("end", end));
55 }
56 }
57 if let (Some(start), Some(end)) = (self.start, self.end) {
58 if start.scale != end.scale {
59 return Err(TimeRangeValidationError::scale_mismatch(start, end));
60 }
61 if start.julian_day.days() > end.julian_day.days() {
62 return Err(TimeRangeValidationError::out_of_order(start, end));
63 }
64 }
65
66 Ok(())
67 }
68
69 pub fn summary_line(&self) -> String {
71 match (self.start, self.end) {
72 (Some(start), Some(end)) => format!(
73 "{} → {}",
74 format_time_range_instant(start),
75 format_time_range_instant(end)
76 ),
77 (Some(start), None) => format!("from {}", format_time_range_instant(start)),
78 (None, Some(end)) => format!("through {}", format_time_range_instant(end)),
79 (None, None) => "unbounded".to_string(),
80 }
81 }
82}
83
84impl fmt::Display for TimeRange {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.write_str(&self.summary_line())
87 }
88}
89
90pub(crate) fn format_time_range_instant(instant: Instant) -> String {
91 format!("JD {:.1} ({})", instant.julian_day.days(), instant.scale)
92}
93
94pub(crate) fn same_scale_and_jd(a: Instant, b: Instant) -> bool {
95 a.scale == b.scale && a.julian_day.days().is_finite() && b.julian_day.days().is_finite()
96}
97
98#[derive(Clone, Copy, Debug, PartialEq)]
100pub enum TimeRangeValidationError {
101 NonFiniteBound {
103 bound: &'static str,
105 instant: Instant,
107 },
108 ScaleMismatch {
110 start: Instant,
112 end: Instant,
114 },
115 OutOfOrder {
117 start: Instant,
119 end: Instant,
121 },
122}
123
124impl TimeRangeValidationError {
125 pub(crate) const fn non_finite_bound(bound: &'static str, instant: Instant) -> Self {
126 Self::NonFiniteBound { bound, instant }
127 }
128
129 pub(crate) const fn scale_mismatch(start: Instant, end: Instant) -> Self {
130 Self::ScaleMismatch { start, end }
131 }
132
133 pub(crate) const fn out_of_order(start: Instant, end: Instant) -> Self {
134 Self::OutOfOrder { start, end }
135 }
136
137 pub fn summary_line(&self) -> String {
139 match self {
140 Self::NonFiniteBound { bound, instant } => format!(
141 "time range bound `{bound}` must be finite: {}",
142 format_time_range_instant(*instant)
143 ),
144 Self::ScaleMismatch { start, end } => format!(
145 "time range bounds must use the same time scale: start={}; end={}",
146 format_time_range_instant(*start),
147 format_time_range_instant(*end)
148 ),
149 Self::OutOfOrder { start, end } => format!(
150 "time range end must not precede the start: start={}; end={}",
151 format_time_range_instant(*start),
152 format_time_range_instant(*end)
153 ),
154 }
155 }
156}
157
158impl fmt::Display for TimeRangeValidationError {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 f.write_str(&self.summary_line())
161 }
162}
163
164impl std::error::Error for TimeRangeValidationError {}