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