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 tt_to_tcg(&self, delta: TimeDelta) -> TimeDelta {
76 tt_to_tcg(delta)
77 }
78
79 fn tcg_to_tt(&self, delta: TimeDelta) -> TimeDelta {
81 tcg_to_tt(delta)
82 }
83
84 fn tdb_to_tcb(&self, delta: TimeDelta) -> TimeDelta {
86 tdb_to_tcb(delta)
87 }
88
89 fn tcb_to_tdb(&self, delta: TimeDelta) -> TimeDelta {
91 tcb_to_tdb(delta)
92 }
93
94 fn tt_to_tdb(&self, delta: TimeDelta) -> TimeDelta {
96 tt_to_tdb(delta)
97 }
98
99 fn tdb_to_tt(&self, delta: TimeDelta) -> TimeDelta {
101 tdb_to_tt(delta)
102 }
103}
104
105#[derive(Debug, Clone, Copy, Default)]
107pub struct DefaultOffsetProvider;
108
109impl LeapSecondsProvider for DefaultOffsetProvider {}
110
111impl OffsetProvider for DefaultOffsetProvider {
112 type Error = Infallible;
113
114 fn tai_to_ut1(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error> {
115 let Some(_) = delta.seconds() else {
116 return Ok(TimeDelta::ZERO);
117 };
118 let tai = Time::from_delta(Tai, delta);
119 Ok(DefaultLeapSecondsProvider.delta_tai_utc(tai))
120 }
121
122 fn ut1_to_tai(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error> {
123 let Ok(utc) = Utc::from_delta(delta) else {
124 return Ok(TimeDelta::ZERO);
125 };
126 Ok(DefaultLeapSecondsProvider.delta_utc_tai(utc))
127 }
128}
129
130pub const D_TAI_TT: TimeDelta = TimeDelta::builder().seconds(32).milliseconds(184).build();
134
135const J77_TT: TimeDelta = TimeDelta::builder()
139 .seconds(-725803167)
140 .milliseconds(816)
141 .build();
142
143const LG: f64 = 6.969290134e-10;
145
146const INV_LG: f64 = LG / (1.0 - LG);
148
149fn tt_to_tcg(delta: TimeDelta) -> TimeDelta {
150 INV_LG * (delta - J77_TT)
151}
152
153fn tcg_to_tt(delta: TimeDelta) -> TimeDelta {
154 -LG * (delta - J77_TT)
155}
156
157const LB: f64 = 1.550519768e-8;
161
162const INV_LB: f64 = LB / (1.0 - LB);
164
165const ONE_MINUS_LB_INV: f64 = 1.0 / (1.0 - LB);
167
168const TDB_0: TimeDelta = TimeDelta::builder()
171 .seconds(0)
172 .microseconds(65)
173 .nanoseconds(500)
174 .negative()
175 .build();
176
177const TCB_77: TimeDelta = TDB_0.add_const(J77_TT.mul_const(LB));
178
179fn tdb_to_tcb(delta: TimeDelta) -> TimeDelta {
180 INV_LB * delta - ONE_MINUS_LB_INV * TCB_77
181}
182
183fn tcb_to_tdb(delta: TimeDelta) -> TimeDelta {
184 TCB_77 - LB * delta
185}
186
187const K: f64 = 1.657e-3;
190const EB: f64 = 1.671e-2;
191const M_0: f64 = 6.239996;
192const M_1: f64 = 1.99096871e-7;
193
194fn tt_to_tdb(delta: TimeDelta) -> TimeDelta {
195 let tt = delta.to_seconds().to_f64();
196 let g = M_0 + M_1 * tt;
197 TimeDelta::from_seconds_f64(K * (g + EB * g.sin()).sin())
198}
199
200fn tdb_to_tt(delta: TimeDelta) -> TimeDelta {
201 let tdb = delta.to_seconds().to_f64();
202 let mut offset = 0.0;
203 for _ in 1..3 {
204 let g = M_0 + M_1 * (tdb + offset);
205 offset = -K * (g + EB * g.sin()).sin();
206 }
207 TimeDelta::from_seconds_f64(offset)
208}
209
210fn two_step_offset<P, T1, T2, T3>(
213 provider: &P,
214 origin: T1,
215 via: T2,
216 target: T3,
217 delta: TimeDelta,
218) -> TimeDelta
219where
220 T1: TimeScale,
221 T2: TimeScale + Copy,
222 T3: TimeScale,
223 P: Offset<T1, T2> + Offset<T2, T3>,
224{
225 let mut offset = provider.offset(origin, via, delta);
226 offset += provider.offset(via, target, delta + offset);
227 offset
228}
229
230#[cfg(test)]
231mod tests {
232 use lox_test_utils::assert_approx_eq;
233 use rstest::rstest;
234
235 use super::*;
236 use crate::offsets::TryOffset;
237 use crate::time_scales::DynTimeScale;
238 use crate::{DynTime, calendar_dates::Date, deltas::ToDelta, time_of_day::TimeOfDay};
239
240 const DEFAULT_TOL: f64 = 1e-7;
241 const TCB_TOL: f64 = 1e-4;
242
243 #[rstest]
249 #[case::tai_tai("TAI", "TAI", 0.0, None)]
250 #[case::tai_tcb("TAI", "TCB", 55.66851419888016, Some(TCB_TOL))]
251 #[case::tai_tcg("TAI", "TCG", 33.239589335894145, None)]
252 #[case::tai_tdb("TAI", "TDB", 32.183882324981056, None)]
253 #[case::tai_tt("TAI", "TT", 32.184, None)]
254 #[case::tcb_tai("TCB", "TAI", -55.668513317090046, Some(TCB_TOL))]
255 #[case::tcb_tcb("TCB", "TCB", 0.0, Some(TCB_TOL))]
256 #[case::tcb_tcg("TCB", "TCG", -22.4289240199929, Some(TCB_TOL))]
257 #[case::tcb_tdb("TCB", "TDB", -23.484631010747805, Some(TCB_TOL))]
258 #[case::tcb_tt("TCB", "TT", -23.484513317090048, Some(TCB_TOL))]
259 #[case::tcg_tai("TCG", "TAI", -33.23958931272851, None)]
260 #[case::tcg_tcb("TCG", "TCB", 22.428924359636042, Some(TCB_TOL))]
261 #[case::tcg_tcg("TCG", "TCG", 0.0, None)]
262 #[case::tcg_tdb("TCG", "TDB", -1.0557069988766656, None)]
263 #[case::tcg_tt("TCG", "TT", -1.0555893127285145, None)]
264 #[case::tdb_tai("TDB", "TAI", -32.18388231420531, None)]
265 #[case::tdb_tcb("TDB", "TCB", 23.48463137488165, Some(TCB_TOL))]
266 #[case::tdb_tcg("TDB", "TCG", 1.0557069992589518, None)]
267 #[case::tdb_tdb("TDB", "TDB", 0.0, None)]
268 #[case::tdb_tt("TDB", "TT", 1.176857946845189E-4, None)]
269 #[case::tt_tai("TT", "TAI", -32.184, None)]
270 #[case::tt_tcb("TT", "TCB", 23.484513689085105, Some(TCB_TOL))]
271 #[case::tt_tcg("TT", "TCG", 1.055589313464182, None)]
272 #[case::tt_tdb("TT", "TDB", -1.1768579472004603E-4, None)]
273 #[case::tt_tt("TT", "TT", 0.0, None)]
274 fn test_dyn_time_scale_offsets_new(
275 #[case] scale1: &str,
276 #[case] scale2: &str,
277 #[case] exp: f64,
278 #[case] tol: Option<f64>,
279 ) {
280 let provider = &DefaultOffsetProvider;
281 let scale1: DynTimeScale = scale1.parse().unwrap();
282 let scale2: DynTimeScale = scale2.parse().unwrap();
283 let date = Date::new(2024, 12, 30).unwrap();
284 let time = TimeOfDay::from_hms(10, 27, 13.145).unwrap();
285 let dt = DynTime::from_date_and_time(scale1, date, time)
286 .unwrap()
287 .to_delta();
288 let act = provider
289 .try_offset(scale1, scale2, dt)
290 .unwrap()
291 .to_seconds()
292 .to_f64();
293 assert_approx_eq!(act, exp, atol <= tol.unwrap_or(DEFAULT_TOL));
294 }
295
296 #[rstest]
298 #[case::tt_tcg_tt("TT", "TCG", 1e-15)]
299 #[case::tcg_tt_tcg("TCG", "TT", 1e-15)]
300 #[case::tdb_tcb_tdb("TDB", "TCB", 1e-14)]
301 #[case::tcb_tdb_tcb("TCB", "TDB", 1e-14)]
302 fn test_time_scale_roundtrip(#[case] scale1: &str, #[case] scale2: &str, #[case] tol: f64) {
303 let provider = &DefaultOffsetProvider;
304 let scale1: DynTimeScale = scale1.parse().unwrap();
305 let scale2: DynTimeScale = scale2.parse().unwrap();
306 let date = Date::new(2024, 12, 30).unwrap();
307 let time = TimeOfDay::from_hms(10, 27, 13.145).unwrap();
308 let original_delta = DynTime::from_date_and_time(scale1, date, time)
309 .unwrap()
310 .to_delta();
311
312 let offset1 = provider.try_offset(scale1, scale2, original_delta).unwrap();
314 let intermediate_delta = original_delta + offset1;
315
316 let offset2 = provider
318 .try_offset(scale2, scale1, intermediate_delta)
319 .unwrap();
320 let final_delta = intermediate_delta + offset2;
321
322 let diff = (final_delta - original_delta).to_seconds().to_f64().abs();
323 assert!(
324 diff < tol,
325 "Round-trip conversion {} -> {} -> {} failed: difference = {:.2e} seconds, tolerance = {:.2e} seconds",
326 scale1,
327 scale2,
328 scale1,
329 diff,
330 tol
331 );
332 }
333
334 #[test]
335 fn test_offset_constants() {
336 let tdb_0 = TDB_0.to_seconds();
337 assert!((tdb_0.to_f64() - (-6.55e-5)).abs() < 1e-15);
338
339 let j77 = J77_TT.to_seconds();
340 assert_eq!(j77.hi, -725803168.0);
343 assert!((j77.lo - 0.184).abs() < 1e-15);
344 assert!((j77.to_f64() - (-725803167.816)).abs() < 1e-9);
346 }
347}