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