Skip to main content

deep_time/dt/
conversions.rs

1use crate::historical_sofa::historical_sofa_offset_for_non_adjusted;
2use crate::{
3    ClockModel, Drift, Dt, LB_DEN, LB_NUM, LG_DEN, LG_NUM, Scale, TAI_SEC_AT_1972,
4    TCG_TCB_REF_ATTOS_SINCE_J2000, TDB0_ATTOS, TT_TAI_OFFSET, tdb_minus_tt,
5};
6
7impl Dt {
8    #[inline]
9    pub const fn from_dt(dt: Dt, scale: Scale) -> Dt {
10        Self::from(dt.sec, dt.attos, scale)
11    }
12
13    #[inline]
14    pub const fn from_attos_since(attos: i128, reference: Dt) -> Self {
15        reference.add(Dt::from_attos(attos, Scale::TAI))
16    }
17
18    #[inline]
19    pub const fn to_scale_and_then_diff(&self, scale: Scale, epoch: Dt) -> Dt {
20        self.to_internal(scale).to_diff_raw(epoch)
21    }
22
23    #[inline]
24    pub const fn from_diff_and_scale(diff: Dt, epoch: Dt, current: Scale) -> Self {
25        Dt::from_dt(epoch.add(diff), current)
26    }
27
28    /// Creates a TAI [`Dt`], converting from another scale.
29    pub const fn from(sec: i64, attos: u64, scale: Scale) -> Dt {
30        let raw = Dt::new(sec, attos);
31        match scale {
32            Scale::TAI | Scale::Custom | Scale::UT1 => raw,
33            Scale::TT => raw.sub(TT_TAI_OFFSET),
34            Scale::UTC => raw.add(Dt {
35                sec: raw.leap_seconds(true).offset,
36                attos: 0,
37            }),
38            Scale::UTCSpice => {
39                let tai = raw.add(Dt {
40                    sec: raw.leap_seconds(true).offset,
41                    attos: 0,
42                });
43                if sec < TAI_SEC_AT_1972 - 10 {
44                    tai.add(Dt::from_sec(9, Scale::TAI))
45                } else {
46                    tai
47                }
48            }
49            Scale::UTCSofa => {
50                let tai = raw.add(Dt {
51                    sec: raw.leap_seconds(true).offset,
52                    attos: 0,
53                });
54                if let Some(offset) = historical_sofa_offset_for_non_adjusted(&raw) {
55                    tai.add(Dt::from_sec_f(offset))
56                } else {
57                    tai
58                }
59            }
60            Scale::GPS | Scale::QZSS | Scale::GST => raw.add(Dt::SEC_19),
61            Scale::BDT => raw.add(Dt::SEC_33),
62            Scale::TDB | Scale::ET => Self::tdb_to_tai(raw),
63            Scale::TCG => {
64                let tt = Self::tcg_to_tt(raw);
65                tt.sub(TT_TAI_OFFSET)
66            }
67            Scale::TCB => {
68                let tdb = Self::tcb_to_tdb(raw);
69                Self::tdb_to_tai(tdb)
70            }
71            Scale::LTC => {
72                let tt = Self::ltc_to_tt(raw);
73                tt.sub(TT_TAI_OFFSET)
74            }
75            Scale::TCL => Self::tcl_to_tai(raw),
76        }
77    }
78
79    pub(crate) const fn to_internal(&self, scale: Scale) -> Dt {
80        match scale {
81            Scale::TAI | Scale::Custom | Scale::UT1 => *self,
82            Scale::TT => self.add(TT_TAI_OFFSET),
83            Scale::UTC => self.sub(Dt {
84                sec: self.leap_seconds(false).offset,
85                attos: 0,
86            }),
87            Scale::UTCSpice => {
88                let spice = self.sub(Dt {
89                    sec: self.leap_seconds(false).offset,
90                    attos: 0,
91                });
92                if self.sec < TAI_SEC_AT_1972 {
93                    spice.sub(Dt::from_sec_f(f!(9.0)))
94                } else {
95                    spice
96                }
97            }
98            Scale::UTCSofa => {
99                let sofa = self.sub(Dt {
100                    sec: self.leap_seconds(false).offset,
101                    attos: 0,
102                });
103                if let Some(offset) = historical_sofa_offset_for_non_adjusted(&self) {
104                    sofa.sub(Dt::from_sec_f(offset))
105                } else {
106                    sofa
107                }
108            }
109            Scale::GPS | Scale::QZSS | Scale::GST => self.sub(Dt::SEC_19),
110            Scale::BDT => self.sub(Dt::SEC_33),
111            Scale::TDB | Scale::ET => Self::tai_to_tdb(*self),
112            Scale::TCG => Self::tai_to_tcg(*self),
113            Scale::TCB => Self::tai_to_tcb(*self),
114            Scale::LTC => {
115                let tt = self.add(TT_TAI_OFFSET);
116                Self::tt_to_ltc(tt)
117            }
118            Scale::TCL => Self::tai_to_tcl(*self),
119        }
120    }
121
122    #[inline]
123    pub const fn to_tai(&self, current: Scale) -> Dt {
124        Self::from(self.sec, self.attos, current)
125    }
126
127    #[inline]
128    pub const fn to(&self, current: Scale, target: Scale) -> Dt {
129        if !current.eq(target) {
130            Self::from(self.sec, self.attos, current).to_internal(target)
131        } else {
132            *self
133        }
134    }
135
136    /// Converts this instant to any other [`Scale`] while applying an exact quadratic relativistic
137    /// or clock-drift correction defined by a [`Drift`] model relative to a reference instant.
138    #[inline]
139    pub const fn convert_using_drift(self, reference: Self, drift: Drift) -> Self {
140        let span = self.to_diff_raw(reference);
141        let correction = drift.time_diff_after(&span);
142        self.add(correction)
143    }
144
145    /// Performs the inverse conversion of [`Self::convert_using_drift`], recovering the original proper
146    /// time on the source clock scale.
147    ///
148    /// A fixed-point iteration (at most 16 steps) is used to solve the implicit equation. For the common
149    /// case of a pure constant offset the function returns immediately without iteration.
150    pub const fn convert_back_using_drift(self, reference: Self, drift: Drift) -> Self {
151        if drift.rate().is_zero() && drift.accel().is_zero() {
152            return self.sub(*drift.constant());
153        }
154        let mut guess = self;
155        let mut i = 0u32;
156        while i < 16 {
157            let span = guess.to_diff_raw(reference);
158            let correction = drift.time_diff_after(&span);
159            guess = self.sub(correction);
160            i += 1;
161        }
162        guess
163    }
164
165    /// Converts this instant using a self-describing [`ClockModel`].
166    #[inline]
167    pub const fn convert_using_model(self, model: ClockModel) -> Self {
168        self.convert_using_drift(model.reference, model.drift)
169    }
170
171    /// Performs the inverse conversion of [`Self::convert_using_model`].
172    #[inline]
173    pub const fn convert_back_using_model(self, model: ClockModel) -> Self {
174        self.convert_back_using_drift(model.reference, model.drift)
175    }
176
177    pub const fn tai_to_tdb(tai: Self) -> Self {
178        let tt = tai.add(TT_TAI_OFFSET);
179        let correction = tdb_minus_tt(tt.to_sec_f());
180        tt.add(Dt::from_sec_f(correction))
181    }
182
183    pub const fn tdb_to_tai(tdb: Self) -> Self {
184        // Linear-rate + constant initial guess (dominant part of the forward transformation)
185        let elapsed = Self::to_attos_since_tcg_tcb_epoch(tdb);
186        let linear_span = Self::mul_lb(elapsed); // LB * elapsed
187        let mut tt = tdb
188            .sub(Dt::from_attos(linear_span, Scale::TAI))
189            .sub(Dt::from_attos(TDB0_ATTOS, Scale::TAI));
190
191        // Fixed-point iteration: TT_{n+1} = TDB − P(TT_n)
192        let mut i = 0u32;
193        while i < 8 {
194            let p = tdb_minus_tt(tt.to_sec_f());
195            let new_tt = tdb.sub(Dt::from_sec_f(p));
196
197            // Early exit when change is smaller than ~1 atto-second
198            let delta = new_tt.to_diff_raw(tt);
199            if delta.sec == 0 && delta.attos < 1 {
200                tt = new_tt;
201                break;
202            }
203
204            tt = new_tt;
205            i += 1;
206        }
207
208        tt.sub(TT_TAI_OFFSET)
209    }
210
211    pub(crate) const fn tai_to_tcg(tai: Self) -> Self {
212        let tt = tai.add(TT_TAI_OFFSET);
213        Self::tt_to_tcg(tt)
214    }
215
216    pub(crate) const fn tai_to_tcb(tai: Self) -> Self {
217        let tdb = Self::tai_to_tdb(tai);
218        Self::tdb_to_tcb(tdb)
219    }
220
221    /// Exact integer helper: elapsed attoseconds since the TCG/TCB reference epoch (1977-01-01.0 TAI),
222    /// using only the numerical `sec`/`attos` of the supplied `Dt` (scale is ignored).
223    #[inline]
224    pub(crate) const fn to_attos_since_tcg_tcb_epoch(numerical: Self) -> i128 {
225        numerical.to_attos() - TCG_TCB_REF_ATTOS_SINCE_J2000
226    }
227
228    /// Exact fixed-point multiplication: `attos * num / den` (handles negative values safely, no overflow for library time range).
229    pub(crate) const fn mul_rate(attos: i128, num: i128, den: i128) -> i128 {
230        if attos == 0 {
231            return 0;
232        }
233        let sign = if attos < 0 { -1i128 } else { 1i128 };
234        let a = if attos < 0 { -attos } else { attos };
235        let q = a / den;
236        let r = a % den;
237        sign * (q * num + (r * num) / den)
238    }
239
240    #[inline]
241    pub(crate) const fn mul_lg(attos: i128) -> i128 {
242        Self::mul_rate(attos, LG_NUM, LG_DEN)
243    }
244
245    #[inline]
246    pub(crate) const fn mul_lb(attos: i128) -> i128 {
247        Self::mul_rate(attos, LB_NUM, LB_DEN)
248    }
249
250    pub(crate) const fn tt_to_tcg(tt: Self) -> Self {
251        let elapsed = Self::to_attos_since_tcg_tcb_epoch(tt);
252        let span_attos = Self::mul_lg(elapsed);
253        tt.add(Dt::from_attos(span_attos, Scale::TAI))
254    }
255
256    pub(crate) const fn tcg_to_tt(tcg: Self) -> Self {
257        let elapsed_cg = Self::to_attos_since_tcg_tcb_epoch(tcg);
258        let span_attos = Self::mul_rate(elapsed_cg, LG_NUM, LG_DEN + LG_NUM);
259        tcg.sub(Dt::from_attos(span_attos, Scale::TAI))
260    }
261
262    pub(crate) const fn tcb_to_tdb(tcb: Self) -> Self {
263        let elapsed_cg = Self::to_attos_since_tcg_tcb_epoch(tcb);
264        let span_attos = Self::mul_rate(elapsed_cg, LB_NUM, LB_DEN + LB_NUM);
265        tcb.sub(Dt::from_attos(span_attos, Scale::TAI))
266            .sub(Dt::from_attos(TDB0_ATTOS, Scale::TAI))
267    }
268
269    pub(crate) const fn tdb_to_tcb(tdb: Self) -> Self {
270        let elapsed = Self::to_attos_since_tcg_tcb_epoch(tdb);
271        let span_attos = Self::mul_lb(elapsed);
272        tdb.add(Dt::from_attos(span_attos, Scale::TAI))
273            .add(Dt::from_attos(TDB0_ATTOS, Scale::TAI))
274    }
275}