1use core::marker::PhantomData;
7
8use super::context::TimeContext;
9use super::error::ConversionError;
10use super::scale::conversion::{ContextScaleConvert, InfallibleScaleConvert};
11use super::scale::{CoordinateScale, Scale};
12use super::target::{ContextConversionTarget, ConversionTarget, InfallibleConversionTarget};
13use affn::algebra::{Space, SplitPoint1};
14use qtty::unit::Second as SecondUnit;
15use qtty::Second;
16
17#[inline]
18fn is_finite_pair(hi: f64, lo: f64) -> bool {
19 hi.is_finite() && lo.is_finite()
20}
21
22#[derive(Copy, Clone)]
23pub(crate) struct ScaleAxis<S: Scale>(PhantomData<fn() -> S>);
24
25impl<S: Scale> core::fmt::Debug for ScaleAxis<S> {
26 #[inline]
27 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
28 f.debug_tuple("ScaleAxis").field(&S::NAME).finish()
29 }
30}
31
32impl<S: Scale> Space for ScaleAxis<S> {}
33
34pub struct Time<S: Scale> {
59 instant: SplitPoint1<ScaleAxis<S>, SecondUnit>,
60}
61
62impl<S: Scale> Copy for Time<S> {}
63impl<S: Scale> Clone for Time<S> {
64 #[inline]
65 fn clone(&self) -> Self {
66 *self
67 }
68}
69
70impl<S: Scale> PartialEq for Time<S> {
71 #[inline]
72 fn eq(&self, other: &Self) -> bool {
73 self.split_seconds() == other.split_seconds()
74 }
75}
76
77impl<S: Scale> PartialOrd for Time<S> {
78 #[inline]
79 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
80 let (self_hi, self_lo) = self.split_seconds();
81 let (other_hi, other_lo) = other.split_seconds();
82 match self_hi.partial_cmp(&other_hi) {
83 Some(core::cmp::Ordering::Equal) => self_lo.partial_cmp(&other_lo),
84 ordering => ordering,
85 }
86 }
87}
88
89impl<S: Scale> core::fmt::Debug for Time<S> {
90 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91 let (hi, lo) = self.split_seconds();
92 write!(f, "Time<{}>({:.17e}, {:.17e})", S::NAME, hi, lo)
93 }
94}
95
96impl<S: Scale> core::fmt::Display for Time<S> {
97 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
98 write!(f, "{} {:.9}", S::NAME, self.total_seconds())
99 }
100}
101
102impl<S: Scale> Time<S> {
103 #[inline]
104 pub(crate) fn new_unchecked(hi: Second, lo: Second) -> Self {
105 debug_assert!(hi.is_finite());
106 debug_assert!(lo.is_finite());
107 let instant = SplitPoint1::new(hi, lo);
108 let (hi, lo) = instant.coordinate().pair();
109 debug_assert!(hi.is_finite());
110 debug_assert!(lo.is_finite());
111 Self { instant }
112 }
113
114 #[inline]
115 pub(crate) fn try_new(hi: Second, lo: Second) -> Result<Self, ConversionError> {
116 if is_finite_pair(hi.value(), lo.value()) {
117 Ok(Self::new_unchecked(hi, lo))
118 } else {
119 Err(ConversionError::NonFinite)
120 }
121 }
122
123 #[inline]
124 pub(crate) fn split_seconds(self) -> (Second, Second) {
125 self.instant.coordinate().pair()
126 }
127
128 #[inline]
129 pub(crate) fn total_seconds(self) -> Second {
130 self.instant.coordinate().total()
131 }
132
133 #[inline]
135 pub fn raw_seconds_pair(self) -> (Second, Second) {
136 self.split_seconds()
137 }
138}
139
140impl<S: CoordinateScale> Time<S> {
141 #[inline]
143 pub(crate) fn from_raw_j2000_seconds(seconds: Second) -> Result<Self, ConversionError> {
144 Self::try_new(seconds, Second::new(0.0))
145 }
146
147 #[inline]
149 #[cfg(test)]
150 pub(crate) fn from_raw_j2000_seconds_split(
151 hi: Second,
152 lo: Second,
153 ) -> Result<Self, ConversionError> {
154 Self::try_new(hi, lo)
155 }
156
157 #[inline]
159 pub(crate) fn raw_j2000_seconds(self) -> Second {
160 self.total_seconds()
161 }
162}
163
164impl<S: Scale> Time<S> {
165 #[allow(private_bounds)]
167 #[inline]
168 pub fn to<T>(self) -> T::Output
169 where
170 T: InfallibleConversionTarget<S>,
171 {
172 T::convert(self)
173 }
174
175 #[allow(private_bounds)]
177 #[inline]
178 pub fn try_to<T>(self) -> Result<T::Output, ConversionError>
179 where
180 T: ConversionTarget<S>,
181 {
182 T::try_convert(self)
183 }
184
185 #[allow(private_bounds)]
187 #[inline]
188 pub fn to_with<T>(self, ctx: &TimeContext) -> Result<T::Output, ConversionError>
189 where
190 T: ContextConversionTarget<S>,
191 {
192 T::convert_with(self, ctx)
193 }
194
195 #[allow(private_bounds)]
198 #[inline]
199 pub fn to_scale<S2: Scale>(self) -> Time<S2>
200 where
201 S: InfallibleScaleConvert<S2>,
202 {
203 let (hi, lo) = self.split_seconds();
204 let (new_hi, new_lo) = <S as InfallibleScaleConvert<S2>>::convert(hi, lo);
205 Time::new_unchecked(new_hi, new_lo)
206 }
207
208 #[allow(private_bounds)]
210 #[inline]
211 pub fn to_scale_with<S2: Scale>(self, ctx: &TimeContext) -> Result<Time<S2>, ConversionError>
212 where
213 S: ContextScaleConvert<S2>,
214 {
215 let (hi, lo) = self.split_seconds();
216 let (new_hi, new_lo) = <S as ContextScaleConvert<S2>>::convert_with(hi, lo, ctx)?;
217 Ok(Time::new_unchecked(new_hi, new_lo))
218 }
219}
220
221impl<S: CoordinateScale> core::ops::Sub for Time<S> {
222 type Output = Second;
223
224 #[inline]
225 fn sub(self, rhs: Self) -> Second {
226 self.instant - rhs.instant
227 }
228}
229
230impl<S: CoordinateScale> core::ops::Add<Second> for Time<S> {
231 type Output = Self;
232
233 #[inline]
234 fn add(self, rhs: Second) -> Self {
235 Self {
236 instant: self.instant + rhs,
237 }
238 }
239}
240
241impl<S: CoordinateScale> core::ops::Sub<Second> for Time<S> {
242 type Output = Self;
243
244 #[inline]
245 fn sub(self, rhs: Second) -> Self {
246 Self {
247 instant: self.instant - rhs,
248 }
249 }
250}
251
252impl<S: CoordinateScale> core::ops::AddAssign<Second> for Time<S> {
253 #[inline]
254 fn add_assign(&mut self, rhs: Second) {
255 *self = *self + rhs;
256 }
257}
258
259impl<S: CoordinateScale> core::ops::SubAssign<Second> for Time<S> {
260 #[inline]
261 fn sub_assign(&mut self, rhs: Second) {
262 *self = *self - rhs;
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::super::encoding::{j2000_seconds_to_mjd, mjd_to_j2000_seconds};
269 use super::super::scale::{TAI, TCG, TDB, TT, UTC};
270 use super::*;
271
272 #[test]
273 fn normalized_constructor_keeps_sum() {
274 let time = Time::<TT>::from_raw_j2000_seconds_split(Second::new(1.0e9), Second::new(0.25))
275 .unwrap();
276 assert!((time.raw_j2000_seconds() - Second::new(1.0e9 + 0.25)).abs() < Second::new(1e-6));
277 }
278
279 #[test]
280 fn tt_tai_round_trip_exact_offset() {
281 let tt = Time::<TT>::from_raw_j2000_seconds(Second::new(0.0)).unwrap();
282 let tai = tt.to_scale::<TAI>();
283 let roundtrip = tai.to_scale::<TT>();
284 assert!(
285 (tt.raw_j2000_seconds() - roundtrip.raw_j2000_seconds()).abs() < Second::new(1e-12)
286 );
287 assert!((tai.raw_j2000_seconds() - Second::new(-32.184)).abs() < Second::new(1e-12));
288 }
289
290 #[test]
291 fn tt_tdb_round_trip_model_error() {
292 let tt = Time::<TT>::from_raw_j2000_seconds(Second::new(1_000_000.0)).unwrap();
293 let tdb = tt.to_scale::<TDB>();
294 let tt2 = tdb.to_scale::<TT>();
295 assert!((tt.raw_j2000_seconds() - tt2.raw_j2000_seconds()).abs() < Second::new(1e-6));
296 }
297
298 #[test]
299 fn tt_tcg_offset_is_finite() {
300 let tt = Time::<TT>::from_raw_j2000_seconds(qtty::Day::new(1.0).to::<qtty::unit::Second>())
301 .unwrap();
302 let tcg = tt.to_scale::<TCG>();
303 assert!(tcg.raw_j2000_seconds().is_finite());
304 }
305
306 #[test]
307 fn utc_exposes_raw_axis_helpers_and_arithmetic() {
308 let utc =
309 Time::<UTC>::from_raw_j2000_seconds(mjd_to_j2000_seconds(qtty::Day::new(51_544.5)))
310 .unwrap();
311 let shifted = utc + Second::new(10.0);
312 assert_eq!(
313 j2000_seconds_to_mjd(utc.raw_j2000_seconds()),
314 qtty::Day::new(51_544.5)
315 );
316 assert!((shifted - utc - Second::new(10.0)).abs() < Second::new(1e-12));
317 }
318
319 #[test]
320 #[allow(clippy::clone_on_copy)]
321 fn scale_axis_debug_and_time_formatting_are_stable() {
322 let axis = ScaleAxis::<TT>(PhantomData);
323 assert_eq!(format!("{axis:?}"), "ScaleAxis(\"TT\")");
324
325 let time = Time::<TT>::from_raw_j2000_seconds(Second::new(1.25)).unwrap();
326 let cloned = time.clone();
327 assert_eq!(cloned, time);
328 assert!(format!("{time:?}").starts_with("Time<TT>("));
329 assert_eq!(format!("{time}"), "TT 1.250000000 s");
330 }
331
332 #[test]
333 fn time_partial_order_and_assign_arithmetic() {
334 let start = Time::<TT>::from_raw_j2000_seconds(Second::new(10.0)).unwrap();
335 let mut shifted = start;
336
337 shifted += Second::new(3.0);
338 assert!(shifted > start);
339 assert_eq!(shifted - start, Second::new(3.0));
340
341 shifted -= Second::new(1.25);
342 assert_eq!(shifted - start, Second::new(1.75));
343 assert_eq!(shifted - Second::new(1.75), start);
344 }
345
346 #[test]
347 fn raw_j2000_constructor_rejects_nonfinite() {
348 assert!(matches!(
349 Time::<TT>::from_raw_j2000_seconds(Second::new(f64::NAN)),
350 Err(ConversionError::NonFinite)
351 ));
352 }
353}