1use core::convert::Infallible;
6
7use lox_core::math::float::sin;
8
9use crate::Time;
10use crate::deltas::TimeDelta;
11use crate::time_scales::{ContinuousTimeScale, Tai};
12use crate::utc::Utc;
13use crate::utc::leap_seconds::{DefaultLeapSecondsProvider, LeapSecondsProvider};
14
15mod impls;
16
17pub trait TryOffset<Origin, Target>
19where
20 Origin: ContinuousTimeScale,
21 Target: ContinuousTimeScale,
22{
23 type Error: core::error::Error + Send + Sync + 'static;
25
26 fn try_offset(
28 &self,
29 origin: Origin,
30 target: Target,
31 delta: TimeDelta,
32 ) -> Result<TimeDelta, Self::Error>;
33}
34
35pub trait Offset<Origin, Target>
37where
38 Origin: ContinuousTimeScale,
39 Target: ContinuousTimeScale,
40{
41 fn offset(&self, origin: Origin, target: Target, delta: TimeDelta) -> TimeDelta;
43}
44
45impl<T, Origin, Target> Offset<Origin, Target> for T
46where
47 Origin: ContinuousTimeScale,
48 Target: ContinuousTimeScale,
49 T: TryOffset<Origin, Target, Error = Infallible>,
50{
51 fn offset(&self, origin: Origin, target: Target, delta: TimeDelta) -> TimeDelta {
52 self.try_offset(origin, target, delta).unwrap()
53 }
54}
55
56pub trait OffsetProvider {
58 type Error: core::error::Error + Send + Sync + 'static;
60
61 fn tai_to_ut1(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error>;
63 fn ut1_to_tai(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error>;
65
66 fn tai_to_tt(&self) -> TimeDelta {
68 D_TAI_TT
69 }
70
71 fn tt_to_tai(&self) -> TimeDelta {
73 -D_TAI_TT
74 }
75
76 fn tai_to_gps(&self) -> TimeDelta {
78 D_TAI_GPS
79 }
80
81 fn gps_to_tai(&self) -> TimeDelta {
83 -D_TAI_GPS
84 }
85
86 fn tt_to_tcg(&self, delta: TimeDelta) -> TimeDelta {
88 tt_to_tcg(delta)
89 }
90
91 fn tcg_to_tt(&self, delta: TimeDelta) -> TimeDelta {
93 tcg_to_tt(delta)
94 }
95
96 fn tdb_to_tcb(&self, delta: TimeDelta) -> TimeDelta {
98 tdb_to_tcb(delta)
99 }
100
101 fn tcb_to_tdb(&self, delta: TimeDelta) -> TimeDelta {
103 tcb_to_tdb(delta)
104 }
105
106 fn tt_to_tdb(&self, delta: TimeDelta) -> TimeDelta {
108 tt_to_tdb(delta)
109 }
110
111 fn tdb_to_tt(&self, delta: TimeDelta) -> TimeDelta {
113 tdb_to_tt(delta)
114 }
115}
116
117#[derive(Debug, Clone, Copy, Default)]
119pub struct DefaultOffsetProvider;
120
121impl LeapSecondsProvider for DefaultOffsetProvider {}
122
123impl OffsetProvider for DefaultOffsetProvider {
124 type Error = Infallible;
125
126 fn tai_to_ut1(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error> {
127 let tai = Time::from_delta(Tai, delta);
131 Ok(-DefaultLeapSecondsProvider.delta_tai_utc(tai))
132 }
133
134 fn ut1_to_tai(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error> {
135 let utc = Utc::from_delta(delta);
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 * sin(g + EB * sin(g)))
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 * sin(g + EB * sin(g));
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: ContinuousTimeScale,
240 T2: ContinuousTimeScale + Copy,
241 T3: ContinuousTimeScale,
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_approx::assert_approx_eq;
252 use rstest::rstest;
253
254 use super::*;
255 use crate::offsets::TryOffset;
256 use crate::time_scales::TimeScale;
257 use crate::{Time, calendar_dates::Date, deltas::ToDelta, time_of_day::TimeOfDay};
258
259 #[test]
264 fn test_default_provider_tai_to_ut1_sign() {
265 use crate::time_scales::Ut1;
266 use crate::utc::Utc;
267
268 let utc: Utc = "2022-02-01T00:00:00.000".parse().unwrap();
269 let tai = utc.to_time();
270 let offset = DefaultOffsetProvider
271 .try_offset(Tai, Ut1, tai.to_delta())
272 .unwrap();
273 assert_approx_eq!(offset.to_seconds().to_f64(), -37.0, rtol <= 1e-9);
274
275 let ut1 = tai.to_scale(Ut1);
277 assert_approx_eq!(
278 ut1.to_delta().to_seconds().to_f64(),
279 utc.to_delta().to_seconds().to_f64(),
280 atol <= 1e-9
281 );
282 }
283
284 #[test]
285 fn test_default_provider_ut1_tai_round_trip() {
286 use crate::time_scales::Ut1;
287 use crate::utc::Utc;
288
289 let tai = "2022-02-01T00:00:00.000".parse::<Utc>().unwrap().to_time();
290 let back = tai.to_scale(Ut1).to_scale(Tai);
291 assert_eq!(tai.to_delta(), back.to_delta());
292 }
293
294 const DEFAULT_TOL: f64 = 1e-7;
295 const TCB_TOL: f64 = 1e-4;
296
297 #[rstest]
303 #[case::tai_tai("TAI", "TAI", 0.0, None)]
304 #[case::tai_tcb("TAI", "TCB", 55.66851419888016, Some(TCB_TOL))]
305 #[case::tai_tcg("TAI", "TCG", 33.239589335894145, None)]
306 #[case::tai_tdb("TAI", "TDB", 32.183882324981056, None)]
307 #[case::tai_tt("TAI", "TT", 32.184, None)]
308 #[case::tcb_tai("TCB", "TAI", -55.668513317090046, Some(TCB_TOL))]
309 #[case::tcb_tcb("TCB", "TCB", 0.0, Some(TCB_TOL))]
310 #[case::tcb_tcg("TCB", "TCG", -22.4289240199929, Some(TCB_TOL))]
311 #[case::tcb_tdb("TCB", "TDB", -23.484631010747805, Some(TCB_TOL))]
312 #[case::tcb_tt("TCB", "TT", -23.484513317090048, Some(TCB_TOL))]
313 #[case::tcg_tai("TCG", "TAI", -33.23958931272851, None)]
314 #[case::tcg_tcb("TCG", "TCB", 22.428924359636042, Some(TCB_TOL))]
315 #[case::tcg_tcg("TCG", "TCG", 0.0, None)]
316 #[case::tcg_tdb("TCG", "TDB", -1.0557069988766656, None)]
317 #[case::tcg_tt("TCG", "TT", -1.0555893127285145, None)]
318 #[case::tdb_tai("TDB", "TAI", -32.18388231420531, None)]
319 #[case::tdb_tcb("TDB", "TCB", 23.48463137488165, Some(TCB_TOL))]
320 #[case::tdb_tcg("TDB", "TCG", 1.0557069992589518, None)]
321 #[case::tdb_tdb("TDB", "TDB", 0.0, None)]
322 #[case::tdb_tt("TDB", "TT", 1.176857946845189E-4, None)]
323 #[case::tt_tai("TT", "TAI", -32.184, None)]
324 #[case::tt_tcb("TT", "TCB", 23.484513689085105, Some(TCB_TOL))]
325 #[case::tt_tcg("TT", "TCG", 1.055589313464182, None)]
326 #[case::tt_tdb("TT", "TDB", -1.1768579472004603E-4, None)]
327 #[case::tt_tt("TT", "TT", 0.0, None)]
328 fn test_dynamic_time_scale_offsets_new(
329 #[case] scale1: &str,
330 #[case] scale2: &str,
331 #[case] exp: f64,
332 #[case] tol: Option<f64>,
333 ) {
334 let provider = &DefaultOffsetProvider;
335 let scale1: TimeScale = scale1.parse().unwrap();
336 let scale2: TimeScale = scale2.parse().unwrap();
337 let date = Date::new(2024, 12, 30).unwrap();
338 let time = TimeOfDay::from_hms(10, 27, 13.145).unwrap();
339 let dt = Time::from_date_and_time(scale1, date, time)
340 .unwrap()
341 .to_delta();
342 let act = provider
343 .try_offset(scale1, scale2, dt)
344 .unwrap()
345 .to_seconds()
346 .to_f64();
347 assert_approx_eq!(act, exp, atol <= tol.unwrap_or(DEFAULT_TOL));
348 }
349
350 #[rstest]
352 #[case::tt_tcg_tt("TT", "TCG", 1e-15)]
353 #[case::tcg_tt_tcg("TCG", "TT", 1e-15)]
354 #[case::tdb_tcb_tdb("TDB", "TCB", 1e-14)]
355 #[case::tcb_tdb_tcb("TCB", "TDB", 1e-14)]
356 fn test_time_scale_roundtrip(#[case] scale1: &str, #[case] scale2: &str, #[case] tol: f64) {
357 let provider = &DefaultOffsetProvider;
358 let scale1: TimeScale = scale1.parse().unwrap();
359 let scale2: TimeScale = scale2.parse().unwrap();
360 let date = Date::new(2024, 12, 30).unwrap();
361 let time = TimeOfDay::from_hms(10, 27, 13.145).unwrap();
362 let original_delta = Time::from_date_and_time(scale1, date, time)
363 .unwrap()
364 .to_delta();
365
366 let offset1 = provider.try_offset(scale1, scale2, original_delta).unwrap();
368 let intermediate_delta = original_delta + offset1;
369
370 let offset2 = provider
372 .try_offset(scale2, scale1, intermediate_delta)
373 .unwrap();
374 let final_delta = intermediate_delta + offset2;
375
376 let diff = (final_delta - original_delta).to_seconds().to_f64().abs();
377 assert!(
378 diff < tol,
379 "Round-trip conversion {} -> {} -> {} failed: difference = {:.2e} seconds, tolerance = {:.2e} seconds",
380 scale1,
381 scale2,
382 scale1,
383 diff,
384 tol
385 );
386 }
387
388 #[test]
389 fn test_offset_constants() {
390 let tdb_0 = TDB_0.to_seconds();
391 assert!((tdb_0.to_f64() - (-6.55e-5)).abs() < 1e-15);
392
393 let j77 = J77_TT.to_seconds();
394 assert_eq!(j77.hi, -725803168.0);
397 assert!((j77.lo - 0.184).abs() < 1e-15);
398 assert!((j77.to_f64() - (-725803167.816)).abs() < 1e-9);
400 }
401}