Skip to main content

tempoch_core/model/
target.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Conversion-target markers for the unified `Time::to::<T>()` API.
5//!
6//! Format markers (`JD`, `MJD`, `J2000s`, `Unix`, `GPS`) and scale markers
7//! (`TT`, `TAI`, …) implement these traits. The source instant may carry any
8//! format phantom `SrcF`; storage is always the compensated J2000-second pair.
9
10use crate::earth::context::TimeContext;
11use crate::format::markers::{J2000s, Unix, GPS, JD, MJD};
12use crate::format::TimeFormat;
13use crate::foundation::error::ConversionError;
14use crate::foundation::sealed::Sealed;
15use crate::model::scale::conversion::{ContextScaleConvert, InfallibleScaleConvert};
16use crate::model::scale::{CoordinateScale, Scale, TAI, TCB, TCG, TDB, TT, UT1, UTC};
17use crate::model::time::Time;
18
19/// Unified conversion target for `Time<S, F>::try_to::<T>()`.
20#[allow(private_bounds)]
21pub trait ConversionTarget<S: Scale, SrcF: TimeFormat = J2000s>: Sealed {
22    type Output;
23
24    fn try_convert(src: Time<S, SrcF>) -> Result<Self::Output, ConversionError>;
25}
26
27/// Unified infallible conversion target for `Time<S, F>::to::<T>()`.
28#[allow(private_bounds)]
29pub trait InfallibleConversionTarget<S: Scale, SrcF: TimeFormat = J2000s>:
30    ConversionTarget<S, SrcF> + Sealed
31{
32    fn convert(src: Time<S, SrcF>) -> Self::Output;
33}
34
35/// Unified context-backed conversion target for `Time<S, F>::to_with::<T>(&ctx)`.
36#[allow(private_bounds)]
37pub trait ContextConversionTarget<S: Scale, SrcF: TimeFormat = J2000s>: Sealed {
38    type Output;
39
40    fn convert_with(src: Time<S, SrcF>, ctx: &TimeContext)
41        -> Result<Self::Output, ConversionError>;
42}
43
44impl<S: CoordinateScale, SrcF: TimeFormat> ConversionTarget<S, SrcF> for J2000s {
45    type Output = Time<S, J2000s>;
46
47    #[inline]
48    fn try_convert(src: Time<S, SrcF>) -> Result<Self::Output, ConversionError> {
49        Ok(src.reinterpret())
50    }
51}
52
53impl<S: CoordinateScale, SrcF: TimeFormat> InfallibleConversionTarget<S, SrcF> for J2000s {
54    #[inline]
55    fn convert(src: Time<S, SrcF>) -> Self::Output {
56        src.reinterpret()
57    }
58}
59
60impl<S: CoordinateScale, SrcF: TimeFormat> ConversionTarget<S, SrcF> for JD {
61    type Output = Time<S, JD>;
62
63    #[inline]
64    fn try_convert(src: Time<S, SrcF>) -> Result<Self::Output, ConversionError> {
65        Ok(<JD as InfallibleConversionTarget<S, SrcF>>::convert(src))
66    }
67}
68
69impl<S: CoordinateScale, SrcF: TimeFormat> InfallibleConversionTarget<S, SrcF> for JD {
70    #[inline]
71    fn convert(src: Time<S, SrcF>) -> Self::Output {
72        src.reinterpret()
73    }
74}
75
76impl<S: CoordinateScale, SrcF: TimeFormat> ConversionTarget<S, SrcF> for MJD {
77    type Output = Time<S, MJD>;
78
79    #[inline]
80    fn try_convert(src: Time<S, SrcF>) -> Result<Self::Output, ConversionError> {
81        Ok(<MJD as InfallibleConversionTarget<S, SrcF>>::convert(src))
82    }
83}
84
85impl<S: CoordinateScale, SrcF: TimeFormat> InfallibleConversionTarget<S, SrcF> for MJD {
86    #[inline]
87    fn convert(src: Time<S, SrcF>) -> Self::Output {
88        src.reinterpret()
89    }
90}
91
92impl<S1: Scale + InfallibleScaleConvert<S2>, S2: Scale, SrcF: TimeFormat> ConversionTarget<S1, SrcF>
93    for S2
94{
95    type Output = Time<S2, SrcF>;
96
97    #[inline]
98    fn try_convert(src: Time<S1, SrcF>) -> Result<Self::Output, ConversionError> {
99        Ok(<S2 as InfallibleConversionTarget<S1, SrcF>>::convert(src))
100    }
101}
102
103impl<S1: Scale + InfallibleScaleConvert<S2>, S2: Scale, SrcF: TimeFormat>
104    InfallibleConversionTarget<S1, SrcF> for S2
105{
106    #[inline]
107    fn convert(src: Time<S1, SrcF>) -> Self::Output {
108        src.to_scale()
109    }
110}
111
112impl<S1: Scale + ContextScaleConvert<S2>, S2: Scale, SrcF: TimeFormat>
113    ContextConversionTarget<S1, SrcF> for S2
114{
115    type Output = Time<S2, SrcF>;
116
117    #[inline]
118    fn convert_with(
119        src: Time<S1, SrcF>,
120        ctx: &TimeContext,
121    ) -> Result<Self::Output, ConversionError> {
122        src.to_scale_with(ctx)
123    }
124}
125
126/// Implements [`ConversionTarget`] for a scale pair that requires a
127/// [`TimeContext`] (i.e. UT1 conversions), using a fresh default snapshot.
128macro_rules! default_context_scale_target {
129    ($src:ty => $dst:ty) => {
130        impl<SrcF: TimeFormat> ConversionTarget<$src, SrcF> for $dst {
131            type Output = Time<$dst, SrcF>;
132
133            #[inline]
134            fn try_convert(src: Time<$src, SrcF>) -> Result<Self::Output, ConversionError> {
135                src.to_scale_with::<$dst>(&TimeContext::new())
136            }
137        }
138    };
139}
140
141default_context_scale_target!(TT => UT1);
142default_context_scale_target!(TAI => UT1);
143default_context_scale_target!(TDB => UT1);
144default_context_scale_target!(TCG => UT1);
145default_context_scale_target!(TCB => UT1);
146default_context_scale_target!(UTC => UT1);
147default_context_scale_target!(UT1 => TT);
148default_context_scale_target!(UT1 => TAI);
149default_context_scale_target!(UT1 => TDB);
150default_context_scale_target!(UT1 => TCG);
151default_context_scale_target!(UT1 => TCB);
152default_context_scale_target!(UT1 => UTC);
153
154impl<S: Scale + InfallibleScaleConvert<UTC>, SrcF: TimeFormat> ConversionTarget<S, SrcF> for Unix {
155    type Output = Time<UTC, Unix>;
156
157    #[inline]
158    fn try_convert(src: Time<S, SrcF>) -> Result<Self::Output, ConversionError> {
159        let ctx = TimeContext::new();
160        let utc = src.to_scale::<UTC>();
161        utc.to_j2000s().raw_unix_seconds_with(&ctx)?;
162        Ok(utc.reinterpret())
163    }
164}
165
166impl<S: Scale + ContextScaleConvert<UTC>, SrcF: TimeFormat> ContextConversionTarget<S, SrcF>
167    for Unix
168{
169    type Output = Time<UTC, Unix>;
170
171    #[inline]
172    fn convert_with(
173        src: Time<S, SrcF>,
174        ctx: &TimeContext,
175    ) -> Result<Self::Output, ConversionError> {
176        let utc = src.to_scale_with::<UTC>(ctx)?;
177        utc.to_j2000s().raw_unix_seconds_with(ctx)?;
178        Ok(utc.reinterpret())
179    }
180}
181
182impl<S: Scale + InfallibleScaleConvert<TAI>, SrcF: TimeFormat> ConversionTarget<S, SrcF> for GPS {
183    type Output = Time<TAI, GPS>;
184
185    #[inline]
186    fn try_convert(src: Time<S, SrcF>) -> Result<Self::Output, ConversionError> {
187        Ok(<GPS as InfallibleConversionTarget<S, SrcF>>::convert(src))
188    }
189}
190
191impl<S: Scale + InfallibleScaleConvert<TAI>, SrcF: TimeFormat> InfallibleConversionTarget<S, SrcF>
192    for GPS
193{
194    #[inline]
195    fn convert(src: Time<S, SrcF>) -> Self::Output {
196        src.to_scale::<TAI>().reinterpret()
197    }
198}
199
200impl<S: Scale + ContextScaleConvert<TAI>, SrcF: TimeFormat> ContextConversionTarget<S, SrcF>
201    for GPS
202{
203    type Output = Time<TAI, GPS>;
204
205    #[inline]
206    fn convert_with(
207        src: Time<S, SrcF>,
208        ctx: &TimeContext,
209    ) -> Result<Self::Output, ConversionError> {
210        Ok(src.to_scale_with::<TAI>(ctx)?.reinterpret())
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use crate::format::{J2000s, Unix, GPS, JD, MJD};
217    use crate::model::scale::{TAI, TT, UT1, UTC};
218    use qtty::Second;
219
220    #[test]
221    fn scalar_targets_match_coordinate_helpers() {
222        let tt = crate::model::time::Time::<TT>::from_raw_j2000_seconds(Second::new(12_345.678))
223            .unwrap();
224
225        assert_eq!(tt.to::<J2000s>().raw(), tt.raw_j2000_seconds());
226        assert_eq!(
227            tt.to::<JD>().raw(),
228            crate::encoding::j2000_seconds_to_day::<JD>(tt.raw_j2000_seconds())
229        );
230        assert_eq!(
231            tt.to::<MJD>().raw(),
232            crate::encoding::j2000_seconds_to_day::<MJD>(tt.raw_j2000_seconds())
233        );
234    }
235
236    #[test]
237    fn unix_and_gps_targets_use_expected_axes() {
238        let ctx = crate::earth::context::TimeContext::new();
239        let utc = crate::model::time::Time::<UTC>::from_raw_unix_seconds_with(
240            Second::new(946_728_000.0),
241            &ctx,
242        )
243        .unwrap();
244        let unix = utc.try_to::<Unix>().unwrap();
245        let unix_sec = unix.try_raw_with(&ctx).unwrap();
246        assert!(
247            (unix_sec - utc.to_j2000s().raw_unix_seconds_with(&ctx).unwrap()).abs()
248                < Second::new(1e-12)
249        );
250
251        let tai = utc.to::<TAI>();
252        let gps = tai.to::<GPS>();
253        assert!((gps.raw() - tai.to_j2000s().raw_gps_seconds()).abs() < Second::new(1e-12));
254
255        let gps_from_tt = crate::model::time::Time::<TT>::from_raw_j2000_seconds(Second::new(0.0))
256            .unwrap()
257            .to::<GPS>();
258        assert!(gps_from_tt.raw().is_finite());
259    }
260
261    #[test]
262    fn default_context_ut1_routes_are_reachable() {
263        let tt = crate::model::time::Time::<TT>::from_raw_j2000_seconds(Second::new(0.0)).unwrap();
264        let ut1 = tt.try_to::<UT1>().unwrap();
265        let tt_back = ut1.try_to::<TT>().unwrap();
266        let utc_back = ut1.try_to::<UTC>().unwrap();
267
268        assert!(tt_back.raw_j2000_seconds().is_finite());
269        assert!(utc_back.raw_j2000_seconds().is_finite());
270    }
271
272    #[test]
273    fn context_targets_support_ut1_sources() {
274        let tt = crate::model::time::Time::<TT>::from_raw_j2000_seconds(Second::new(0.0)).unwrap();
275        let ctx = crate::earth::context::TimeContext::new();
276        let ut1 = tt.to_with::<UT1>(&ctx).unwrap();
277
278        let unix_sec = ut1
279            .to_with::<Unix>(&ctx)
280            .unwrap()
281            .try_raw_with(&ctx)
282            .unwrap();
283        let gps = ut1.to_with::<GPS>(&ctx).unwrap();
284
285        assert!(unix_sec.is_finite());
286        assert!(gps.raw().is_finite());
287    }
288}