Skip to main content

tempoch_core/
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-encoding targets (`JD`, `MJD`, `J2000s`, `Unix`, `GPS`) live in
7//! [`crate::representation`]. This module provides the trait definitions and
8//! scale-to-scale conversion impls.
9
10use crate::context::TimeContext;
11use crate::error::ConversionError;
12use crate::scale::conversion::{ContextScaleConvert, InfallibleScaleConvert};
13use crate::scale::{Scale, TAI, TCB, TCG, TDB, TT, UT1, UTC};
14use crate::sealed::Sealed;
15use crate::time::Time;
16
17/// Unified conversion target for `Time<S>::try_to::<T>()`.
18#[allow(private_bounds)]
19pub trait ConversionTarget<S: Scale>: Sealed {
20    type Output;
21
22    fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError>;
23}
24
25/// Unified infallible conversion target for `Time<S>::to::<T>()`.
26#[allow(private_bounds)]
27pub trait InfallibleConversionTarget<S: Scale>: ConversionTarget<S> + Sealed {
28    fn convert(src: Time<S>) -> Self::Output;
29}
30
31/// Unified context-backed conversion target for `Time<S>::to_with::<T>(&ctx)`.
32#[allow(private_bounds)]
33pub trait ContextConversionTarget<S: Scale>: Sealed {
34    type Output;
35
36    fn convert_with(src: Time<S>, ctx: &TimeContext) -> Result<Self::Output, ConversionError>;
37}
38
39impl<S1, S2> ConversionTarget<S1> for S2
40where
41    S1: Scale + InfallibleScaleConvert<S2>,
42    S2: Scale,
43{
44    type Output = Time<S2>;
45
46    #[inline]
47    fn try_convert(src: Time<S1>) -> Result<Self::Output, ConversionError> {
48        Ok(Self::convert(src))
49    }
50}
51
52impl<S1, S2> InfallibleConversionTarget<S1> for S2
53where
54    S1: Scale + InfallibleScaleConvert<S2>,
55    S2: Scale,
56{
57    #[inline]
58    fn convert(src: Time<S1>) -> Self::Output {
59        src.to_scale()
60    }
61}
62
63impl<S1, S2> ContextConversionTarget<S1> for S2
64where
65    S1: Scale + ContextScaleConvert<S2>,
66    S2: Scale,
67{
68    type Output = Time<S2>;
69
70    #[inline]
71    fn convert_with(src: Time<S1>, ctx: &TimeContext) -> Result<Self::Output, ConversionError> {
72        src.to_scale_with(ctx)
73    }
74}
75
76/// Implements [`ConversionTarget`] for a scale pair that requires a
77/// [`TimeContext`] (i.e. UT1 conversions), using a fresh default snapshot.
78/// For reproducible pipelines, prefer [`ContextConversionTarget`] via
79/// [`Time::to_with`](crate::time::Time::to_with).
80macro_rules! default_context_scale_target {
81    ($src:ty => $dst:ty) => {
82        impl ConversionTarget<$src> for $dst {
83            type Output = Time<$dst>;
84
85            #[inline]
86            fn try_convert(src: Time<$src>) -> Result<Self::Output, ConversionError> {
87                src.to_scale_with::<$dst>(&TimeContext::new())
88            }
89        }
90    };
91}
92
93default_context_scale_target!(TT => UT1);
94default_context_scale_target!(TAI => UT1);
95default_context_scale_target!(TDB => UT1);
96default_context_scale_target!(TCG => UT1);
97default_context_scale_target!(TCB => UT1);
98default_context_scale_target!(UTC => UT1);
99default_context_scale_target!(UT1 => TT);
100default_context_scale_target!(UT1 => TAI);
101default_context_scale_target!(UT1 => TDB);
102default_context_scale_target!(UT1 => TCG);
103default_context_scale_target!(UT1 => TCB);
104default_context_scale_target!(UT1 => UTC);
105
106#[cfg(test)]
107mod tests {
108    use crate::representation::{J2000s, Unix, GPS, JD, MJD};
109    use crate::scale::{TAI, TT, UT1, UTC};
110    use qtty::Second;
111
112    #[test]
113    fn scalar_targets_match_coordinate_helpers() {
114        let tt = crate::time::Time::<TT>::from_raw_j2000_seconds(Second::new(12_345.678)).unwrap();
115
116        assert_eq!(tt.to::<J2000s>().raw(), tt.raw_j2000_seconds());
117        assert_eq!(
118            tt.to::<JD>().raw(),
119            crate::encoding::j2000_seconds_to_jd(tt.raw_j2000_seconds())
120        );
121        assert_eq!(
122            tt.to::<MJD>().raw(),
123            crate::encoding::j2000_seconds_to_mjd(tt.raw_j2000_seconds())
124        );
125    }
126
127    #[test]
128    fn unix_and_gps_targets_use_expected_axes() {
129        let ctx = crate::context::TimeContext::new();
130        let utc =
131            crate::time::Time::<UTC>::from_raw_unix_seconds_with(Second::new(946_728_000.0), &ctx)
132                .unwrap();
133        let unix = utc.try_to::<Unix>().unwrap();
134        assert!((unix.raw() - utc.raw_unix_seconds_with(&ctx).unwrap()).abs() < Second::new(1e-12));
135
136        let tai = utc.to::<TAI>();
137        let gps = tai.to::<GPS>();
138        assert!((gps.raw() - tai.raw_gps_seconds()).abs() < Second::new(1e-12));
139
140        let gps_from_tt = crate::time::Time::<TT>::from_raw_j2000_seconds(Second::new(0.0))
141            .unwrap()
142            .to::<GPS>();
143        assert!(gps_from_tt.raw().is_finite());
144    }
145
146    #[test]
147    fn default_context_ut1_routes_are_reachable() {
148        let tt = crate::time::Time::<TT>::from_raw_j2000_seconds(Second::new(0.0)).unwrap();
149        let ut1 = tt.try_to::<UT1>().unwrap();
150        let tt_back = ut1.try_to::<TT>().unwrap();
151        let utc_back = ut1.try_to::<UTC>().unwrap();
152
153        assert!(tt_back.raw_j2000_seconds().is_finite());
154        assert!(utc_back.raw_j2000_seconds().is_finite());
155    }
156
157    #[test]
158    fn context_targets_support_ut1_sources() {
159        let tt = crate::time::Time::<TT>::from_raw_j2000_seconds(Second::new(0.0)).unwrap();
160        let ctx = crate::context::TimeContext::new();
161        let ut1 = tt.to_with::<UT1>(&ctx).unwrap();
162
163        let unix = ut1.to_with::<Unix>(&ctx).unwrap();
164        let gps = ut1.to_with::<GPS>(&ctx).unwrap();
165
166        assert!(unix.raw().is_finite());
167        assert!(gps.raw().is_finite());
168    }
169}