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