1use std::convert::Infallible;
6
7use crate::Time;
8use crate::deltas::TimeDelta;
9use crate::time_scales::{Tai, TimeScale};
10use crate::utc::Utc;
11use crate::utc::leap_seconds::{DefaultLeapSecondsProvider, LeapSecondsProvider};
12
13mod impls;
14
15pub trait TryOffset<Origin, Target>
17where
18 Origin: TimeScale,
19 Target: TimeScale,
20{
21 type Error: std::error::Error + Send + Sync + 'static;
23
24 fn try_offset(
26 &self,
27 origin: Origin,
28 target: Target,
29 delta: TimeDelta,
30 ) -> Result<TimeDelta, Self::Error>;
31}
32
33pub trait Offset<Origin, Target>
35where
36 Origin: TimeScale,
37 Target: TimeScale,
38{
39 fn offset(&self, origin: Origin, target: Target, delta: TimeDelta) -> TimeDelta;
41}
42
43impl<T, Origin, Target> Offset<Origin, Target> for T
44where
45 Origin: TimeScale,
46 Target: TimeScale,
47 T: TryOffset<Origin, Target, Error = Infallible>,
48{
49 fn offset(&self, origin: Origin, target: Target, delta: TimeDelta) -> TimeDelta {
50 self.try_offset(origin, target, delta).unwrap()
51 }
52}
53
54pub trait OffsetProvider {
56 type Error: std::error::Error + Send + Sync + 'static;
58
59 fn tai_to_ut1(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error>;
61 fn ut1_to_tai(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error>;
63
64 fn tai_to_tt(&self) -> TimeDelta {
66 D_TAI_TT
67 }
68
69 fn tt_to_tai(&self) -> TimeDelta {
71 -D_TAI_TT
72 }
73
74 fn tai_to_gps(&self) -> TimeDelta {
76 D_TAI_GPS
77 }
78
79 fn gps_to_tai(&self) -> TimeDelta {
81 -D_TAI_GPS
82 }
83
84 fn tt_to_tcg(&self, delta: TimeDelta) -> TimeDelta {
86 tt_to_tcg(delta)
87 }
88
89 fn tcg_to_tt(&self, delta: TimeDelta) -> TimeDelta {
91 tcg_to_tt(delta)
92 }
93
94 fn tdb_to_tcb(&self, delta: TimeDelta) -> TimeDelta {
96 tdb_to_tcb(delta)
97 }
98
99 fn tcb_to_tdb(&self, delta: TimeDelta) -> TimeDelta {
101 tcb_to_tdb(delta)
102 }
103
104 fn tt_to_tdb(&self, delta: TimeDelta) -> TimeDelta {
106 tt_to_tdb(delta)
107 }
108
109 fn tdb_to_tt(&self, delta: TimeDelta) -> TimeDelta {
111 tdb_to_tt(delta)
112 }
113}
114
115#[derive(Debug, Clone, Copy, Default)]
117pub struct DefaultOffsetProvider;
118
119impl LeapSecondsProvider for DefaultOffsetProvider {}
120
121impl OffsetProvider for DefaultOffsetProvider {
122 type Error = Infallible;
123
124 fn tai_to_ut1(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error> {
125 let Some(_) = delta.seconds() else {
126 return Ok(TimeDelta::ZERO);
127 };
128 let tai = Time::from_delta(Tai, delta);
129 Ok(DefaultLeapSecondsProvider.delta_tai_utc(tai))
130 }
131
132 fn ut1_to_tai(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error> {
133 let Ok(utc) = Utc::from_delta(delta) else {
134 return Ok(TimeDelta::ZERO);
135 };
136 Ok(DefaultLeapSecondsProvider.delta_utc_tai(utc))
137 }
138}
139
140pub const D_TAI_TT: TimeDelta = TimeDelta::builder().seconds(32).milliseconds(184).build();
144
145pub const D_TAI_GPS: TimeDelta = TimeDelta::builder().seconds(-19).build();
153
154const J77_TT: TimeDelta = TimeDelta::builder()
158 .seconds(-725803167)
159 .milliseconds(816)
160 .build();
161
162const LG: f64 = 6.969290134e-10;
164
165const INV_LG: f64 = LG / (1.0 - LG);
167
168fn tt_to_tcg(delta: TimeDelta) -> TimeDelta {
169 INV_LG * (delta - J77_TT)
170}
171
172fn tcg_to_tt(delta: TimeDelta) -> TimeDelta {
173 -LG * (delta - J77_TT)
174}
175
176const LB: f64 = 1.550519768e-8;
180
181const INV_LB: f64 = LB / (1.0 - LB);
183
184const ONE_MINUS_LB_INV: f64 = 1.0 / (1.0 - LB);
186
187const TDB_0: TimeDelta = TimeDelta::builder()
190 .seconds(0)
191 .microseconds(65)
192 .nanoseconds(500)
193 .negative()
194 .build();
195
196const TCB_77: TimeDelta = TDB_0.add_const(J77_TT.mul_const(LB));
197
198fn tdb_to_tcb(delta: TimeDelta) -> TimeDelta {
199 INV_LB * delta - ONE_MINUS_LB_INV * TCB_77
200}
201
202fn tcb_to_tdb(delta: TimeDelta) -> TimeDelta {
203 TCB_77 - LB * delta
204}
205
206const K: f64 = 1.657e-3;
209const EB: f64 = 1.671e-2;
210const M_0: f64 = 6.239996;
211const M_1: f64 = 1.99096871e-7;
212
213fn tt_to_tdb(delta: TimeDelta) -> TimeDelta {
214 let tt = delta.to_seconds().to_f64();
215 let g = M_0 + M_1 * tt;
216 TimeDelta::from_seconds_f64(K * (g + EB * g.sin()).sin())
217}
218
219fn tdb_to_tt(delta: TimeDelta) -> TimeDelta {
220 let tdb = delta.to_seconds().to_f64();
221 let mut offset = 0.0;
222 for _ in 1..3 {
223 let g = M_0 + M_1 * (tdb + offset);
224 offset = -K * (g + EB * g.sin()).sin();
225 }
226 TimeDelta::from_seconds_f64(offset)
227}
228
229fn two_step_offset<P, T1, T2, T3>(
232 provider: &P,
233 origin: T1,
234 via: T2,
235 target: T3,
236 delta: TimeDelta,
237) -> TimeDelta
238where
239 T1: TimeScale,
240 T2: TimeScale + Copy,
241 T3: TimeScale,
242 P: Offset<T1, T2> + Offset<T2, T3>,
243{
244 let mut offset = provider.offset(origin, via, delta);
245 offset += provider.offset(via, target, delta + offset);
246 offset
247}
248
249#[cfg(test)]
250mod tests {
251 use lox_test_utils::assert_approx_eq;
252 use rstest::rstest;
253
254 use super::*;
255 use crate::offsets::TryOffset;
256 use crate::time_scales::DynTimeScale;
257 use crate::{DynTime, calendar_dates::Date, deltas::ToDelta, time_of_day::TimeOfDay};
258
259 const DEFAULT_TOL: f64 = 1e-7;
260 const TCB_TOL: f64 = 1e-4;
261
262 #[rstest]
268 #[case::tai_tai("TAI", "TAI", 0.0, None)]
269 #[case::tai_tcb("TAI", "TCB", 55.66851419888016, Some(TCB_TOL))]
270 #[case::tai_tcg("TAI", "TCG", 33.239589335894145, None)]
271 #[case::tai_tdb("TAI", "TDB", 32.183882324981056, None)]
272 #[case::tai_tt("TAI", "TT", 32.184, None)]
273 #[case::tcb_tai("TCB", "TAI", -55.668513317090046, Some(TCB_TOL))]
274 #[case::tcb_tcb("TCB", "TCB", 0.0, Some(TCB_TOL))]
275 #[case::tcb_tcg("TCB", "TCG", -22.4289240199929, Some(TCB_TOL))]
276 #[case::tcb_tdb("TCB", "TDB", -23.484631010747805, Some(TCB_TOL))]
277 #[case::tcb_tt("TCB", "TT", -23.484513317090048, Some(TCB_TOL))]
278 #[case::tcg_tai("TCG", "TAI", -33.23958931272851, None)]
279 #[case::tcg_tcb("TCG", "TCB", 22.428924359636042, Some(TCB_TOL))]
280 #[case::tcg_tcg("TCG", "TCG", 0.0, None)]
281 #[case::tcg_tdb("TCG", "TDB", -1.0557069988766656, None)]
282 #[case::tcg_tt("TCG", "TT", -1.0555893127285145, None)]
283 #[case::tdb_tai("TDB", "TAI", -32.18388231420531, None)]
284 #[case::tdb_tcb("TDB", "TCB", 23.48463137488165, Some(TCB_TOL))]
285 #[case::tdb_tcg("TDB", "TCG", 1.0557069992589518, None)]
286 #[case::tdb_tdb("TDB", "TDB", 0.0, None)]
287 #[case::tdb_tt("TDB", "TT", 1.176857946845189E-4, None)]
288 #[case::tt_tai("TT", "TAI", -32.184, None)]
289 #[case::tt_tcb("TT", "TCB", 23.484513689085105, Some(TCB_TOL))]
290 #[case::tt_tcg("TT", "TCG", 1.055589313464182, None)]
291 #[case::tt_tdb("TT", "TDB", -1.1768579472004603E-4, None)]
292 #[case::tt_tt("TT", "TT", 0.0, None)]
293 fn test_dyn_time_scale_offsets_new(
294 #[case] scale1: &str,
295 #[case] scale2: &str,
296 #[case] exp: f64,
297 #[case] tol: Option<f64>,
298 ) {
299 let provider = &DefaultOffsetProvider;
300 let scale1: DynTimeScale = scale1.parse().unwrap();
301 let scale2: DynTimeScale = scale2.parse().unwrap();
302 let date = Date::new(2024, 12, 30).unwrap();
303 let time = TimeOfDay::from_hms(10, 27, 13.145).unwrap();
304 let dt = DynTime::from_date_and_time(scale1, date, time)
305 .unwrap()
306 .to_delta();
307 let act = provider
308 .try_offset(scale1, scale2, dt)
309 .unwrap()
310 .to_seconds()
311 .to_f64();
312 assert_approx_eq!(act, exp, atol <= tol.unwrap_or(DEFAULT_TOL));
313 }
314
315 #[rstest]
317 #[case::tt_tcg_tt("TT", "TCG", 1e-15)]
318 #[case::tcg_tt_tcg("TCG", "TT", 1e-15)]
319 #[case::tdb_tcb_tdb("TDB", "TCB", 1e-14)]
320 #[case::tcb_tdb_tcb("TCB", "TDB", 1e-14)]
321 fn test_time_scale_roundtrip(#[case] scale1: &str, #[case] scale2: &str, #[case] tol: f64) {
322 let provider = &DefaultOffsetProvider;
323 let scale1: DynTimeScale = scale1.parse().unwrap();
324 let scale2: DynTimeScale = scale2.parse().unwrap();
325 let date = Date::new(2024, 12, 30).unwrap();
326 let time = TimeOfDay::from_hms(10, 27, 13.145).unwrap();
327 let original_delta = DynTime::from_date_and_time(scale1, date, time)
328 .unwrap()
329 .to_delta();
330
331 let offset1 = provider.try_offset(scale1, scale2, original_delta).unwrap();
333 let intermediate_delta = original_delta + offset1;
334
335 let offset2 = provider
337 .try_offset(scale2, scale1, intermediate_delta)
338 .unwrap();
339 let final_delta = intermediate_delta + offset2;
340
341 let diff = (final_delta - original_delta).to_seconds().to_f64().abs();
342 assert!(
343 diff < tol,
344 "Round-trip conversion {} -> {} -> {} failed: difference = {:.2e} seconds, tolerance = {:.2e} seconds",
345 scale1,
346 scale2,
347 scale1,
348 diff,
349 tol
350 );
351 }
352
353 #[test]
354 fn test_offset_constants() {
355 let tdb_0 = TDB_0.to_seconds();
356 assert!((tdb_0.to_f64() - (-6.55e-5)).abs() < 1e-15);
357
358 let j77 = J77_TT.to_seconds();
359 assert_eq!(j77.hi, -725803168.0);
362 assert!((j77.lo - 0.184).abs() < 1e-15);
363 assert!((j77.to_f64() - (-725803167.816)).abs() < 1e-9);
365 }
366}