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]
153 pub fn try_from_raw_j2000_seconds_split(
154 hi: Second,
155 lo: Second,
156 ) -> Result<Self, ConversionError> {
157 Self::try_new(hi, lo)
158 }
159
160 #[inline]
162 #[cfg(test)]
163 pub(crate) fn from_raw_j2000_seconds_split(
164 hi: Second,
165 lo: Second,
166 ) -> Result<Self, ConversionError> {
167 Self::try_new(hi, lo)
168 }
169
170 #[inline]
172 pub(crate) fn raw_j2000_seconds(self) -> Second {
173 self.total_seconds()
174 }
175}
176
177impl<S: Scale> Time<S> {
178 #[allow(private_bounds)]
180 #[inline]
181 pub fn to<T>(self) -> T::Output
182 where
183 T: InfallibleConversionTarget<S>,
184 {
185 T::convert(self)
186 }
187
188 #[allow(private_bounds)]
190 #[inline]
191 pub fn try_to<T>(self) -> Result<T::Output, ConversionError>
192 where
193 T: ConversionTarget<S>,
194 {
195 T::try_convert(self)
196 }
197
198 #[allow(private_bounds)]
200 #[inline]
201 pub fn to_with<T>(self, ctx: &TimeContext) -> Result<T::Output, ConversionError>
202 where
203 T: ContextConversionTarget<S>,
204 {
205 T::convert_with(self, ctx)
206 }
207
208 #[allow(private_bounds)]
211 #[inline]
212 pub fn to_scale<S2: Scale>(self) -> Time<S2>
213 where
214 S: InfallibleScaleConvert<S2>,
215 {
216 let (hi, lo) = self.split_seconds();
217 let (new_hi, new_lo) = <S as InfallibleScaleConvert<S2>>::convert(hi, lo);
218 Time::new_unchecked(new_hi, new_lo)
219 }
220
221 #[allow(private_bounds)]
223 #[inline]
224 pub fn to_scale_with<S2: Scale>(self, ctx: &TimeContext) -> Result<Time<S2>, ConversionError>
225 where
226 S: ContextScaleConvert<S2>,
227 {
228 let (hi, lo) = self.split_seconds();
229 let (new_hi, new_lo) = <S as ContextScaleConvert<S2>>::convert_with(hi, lo, ctx)?;
230 Ok(Time::new_unchecked(new_hi, new_lo))
231 }
232}
233
234impl<S: CoordinateScale> core::ops::Sub for Time<S> {
235 type Output = Second;
236
237 #[inline]
238 fn sub(self, rhs: Self) -> Second {
239 self.instant - rhs.instant
240 }
241}
242
243impl<S: CoordinateScale> core::ops::Add<Second> for Time<S> {
244 type Output = Self;
245
246 #[inline]
247 fn add(self, rhs: Second) -> Self {
248 Self {
249 instant: self.instant + rhs,
250 }
251 }
252}
253
254impl<S: CoordinateScale> core::ops::Sub<Second> for Time<S> {
255 type Output = Self;
256
257 #[inline]
258 fn sub(self, rhs: Second) -> Self {
259 Self {
260 instant: self.instant - rhs,
261 }
262 }
263}
264
265impl<S: CoordinateScale> core::ops::AddAssign<Second> for Time<S> {
266 #[inline]
267 fn add_assign(&mut self, rhs: Second) {
268 *self = *self + rhs;
269 }
270}
271
272impl<S: CoordinateScale> core::ops::SubAssign<Second> for Time<S> {
273 #[inline]
274 fn sub_assign(&mut self, rhs: Second) {
275 *self = *self - rhs;
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::super::encoding::{j2000_seconds_to_mjd, mjd_to_j2000_seconds};
282 use super::super::scale::{TAI, TCG, TDB, TT, UTC};
283 use super::*;
284
285 #[test]
286 fn normalized_constructor_keeps_sum() {
287 let time = Time::<TT>::from_raw_j2000_seconds_split(Second::new(1.0e9), Second::new(0.25))
288 .unwrap();
289 assert!((time.raw_j2000_seconds() - Second::new(1.0e9 + 0.25)).abs() < Second::new(1e-6));
290 }
291
292 #[test]
293 fn tt_tai_round_trip_exact_offset() {
294 let tt = Time::<TT>::from_raw_j2000_seconds(Second::new(0.0)).unwrap();
295 let tai = tt.to_scale::<TAI>();
296 let roundtrip = tai.to_scale::<TT>();
297 assert!(
298 (tt.raw_j2000_seconds() - roundtrip.raw_j2000_seconds()).abs() < Second::new(1e-12)
299 );
300 assert!((tai.raw_j2000_seconds() - Second::new(-32.184)).abs() < Second::new(1e-12));
301 }
302
303 #[test]
304 fn tt_tdb_round_trip_model_error() {
305 let tt = Time::<TT>::from_raw_j2000_seconds(Second::new(1_000_000.0)).unwrap();
306 let tdb = tt.to_scale::<TDB>();
307 let tt2 = tdb.to_scale::<TT>();
308 assert!((tt.raw_j2000_seconds() - tt2.raw_j2000_seconds()).abs() < Second::new(1e-6));
309 }
310
311 #[test]
312 fn tt_tcg_offset_is_finite() {
313 let tt = Time::<TT>::from_raw_j2000_seconds(qtty::Day::new(1.0).to::<qtty::unit::Second>())
314 .unwrap();
315 let tcg = tt.to_scale::<TCG>();
316 assert!(tcg.raw_j2000_seconds().is_finite());
317 }
318
319 #[test]
320 fn utc_exposes_raw_axis_helpers_and_arithmetic() {
321 let utc =
322 Time::<UTC>::from_raw_j2000_seconds(mjd_to_j2000_seconds(qtty::Day::new(51_544.5)))
323 .unwrap();
324 let shifted = utc + Second::new(10.0);
325 assert_eq!(
326 j2000_seconds_to_mjd(utc.raw_j2000_seconds()),
327 qtty::Day::new(51_544.5)
328 );
329 assert!((shifted - utc - Second::new(10.0)).abs() < Second::new(1e-12));
330 }
331
332 #[test]
333 #[allow(clippy::clone_on_copy)]
334 fn scale_axis_debug_and_time_formatting_are_stable() {
335 let axis = ScaleAxis::<TT>(PhantomData);
336 assert_eq!(format!("{axis:?}"), "ScaleAxis(\"TT\")");
337
338 let time = Time::<TT>::from_raw_j2000_seconds(Second::new(1.25)).unwrap();
339 let cloned = time.clone();
340 assert_eq!(cloned, time);
341 assert!(format!("{time:?}").starts_with("Time<TT>("));
342 assert_eq!(format!("{time}"), "TT 1.250000000 s");
343 }
344
345 #[test]
346 fn time_partial_order_and_assign_arithmetic() {
347 let start = Time::<TT>::from_raw_j2000_seconds(Second::new(10.0)).unwrap();
348 let mut shifted = start;
349
350 shifted += Second::new(3.0);
351 assert!(shifted > start);
352 assert_eq!(shifted - start, Second::new(3.0));
353
354 shifted -= Second::new(1.25);
355 assert_eq!(shifted - start, Second::new(1.75));
356 assert_eq!(shifted - Second::new(1.75), start);
357 }
358
359 #[test]
360 fn raw_j2000_constructor_rejects_nonfinite() {
361 assert!(matches!(
362 Time::<TT>::from_raw_j2000_seconds(Second::new(f64::NAN)),
363 Err(ConversionError::NonFinite)
364 ));
365 }
366}