celestial_time/scales/conversions/
ut1_tai.rs1use super::ToUT1;
56use crate::julian::JulianDate;
57use crate::scales::{TAI, TT, UT1};
58use crate::TimeResult;
59use celestial_core::constants::SECONDS_PER_DAY_F64;
60
61impl ToUT1 for UT1 {
62 fn to_ut1(&self) -> TimeResult<UT1> {
63 Ok(*self)
64 }
65}
66
67pub trait ToUT1WithOffset {
75 fn to_ut1_with_offset(&self, ut1_tai_offset_seconds: f64) -> TimeResult<UT1>;
80}
81
82pub trait ToTAIWithOffset {
87 fn to_tai_with_offset(&self, ut1_tai_offset_seconds: f64) -> TimeResult<TAI>;
92}
93
94impl ToTAIWithOffset for UT1 {
95 fn to_tai_with_offset(&self, ut1_tai_offset_seconds: f64) -> TimeResult<TAI> {
96 let ut1_jd = self.to_julian_date();
97 let offset_days = ut1_tai_offset_seconds / SECONDS_PER_DAY_F64;
98
99 let (tai_jd1, tai_jd2) = if ut1_jd.jd1().abs() > ut1_jd.jd2().abs() {
102 (ut1_jd.jd1(), ut1_jd.jd2() - offset_days)
103 } else {
104 (ut1_jd.jd1() - offset_days, ut1_jd.jd2())
105 };
106
107 Ok(TAI::from_julian_date(JulianDate::new(tai_jd1, tai_jd2)))
108 }
109}
110
111impl ToUT1WithOffset for TAI {
112 fn to_ut1_with_offset(&self, ut1_tai_offset_seconds: f64) -> TimeResult<UT1> {
113 let tai_jd = self.to_julian_date();
114 let offset_days = ut1_tai_offset_seconds / SECONDS_PER_DAY_F64;
115
116 let (ut1_jd1, ut1_jd2) = if tai_jd.jd1().abs() > tai_jd.jd2().abs() {
119 (tai_jd.jd1(), tai_jd.jd2() + offset_days)
120 } else {
121 (tai_jd.jd1() + offset_days, tai_jd.jd2())
122 };
123
124 Ok(UT1::from_julian_date(JulianDate::new(ut1_jd1, ut1_jd2)))
125 }
126}
127
128pub trait ToTTWithDeltaT {
145 fn to_tt_with_delta_t(&self, delta_t_seconds: f64) -> TimeResult<TT>;
149}
150
151pub trait ToUT1WithDeltaT {
155 fn to_ut1_with_delta_t(&self, delta_t_seconds: f64) -> TimeResult<UT1>;
159}
160
161impl ToTTWithDeltaT for UT1 {
162 fn to_tt_with_delta_t(&self, delta_t_seconds: f64) -> TimeResult<TT> {
163 let ut1_jd = self.to_julian_date();
164 let delta_t_days = delta_t_seconds / SECONDS_PER_DAY_F64;
165
166 let (tt_jd1, tt_jd2) = if ut1_jd.jd1().abs() > ut1_jd.jd2().abs() {
169 (ut1_jd.jd1(), ut1_jd.jd2() + delta_t_days)
170 } else {
171 (ut1_jd.jd1() + delta_t_days, ut1_jd.jd2())
172 };
173
174 Ok(TT::from_julian_date(JulianDate::new(tt_jd1, tt_jd2)))
175 }
176}
177
178impl ToUT1WithDeltaT for TT {
179 fn to_ut1_with_delta_t(&self, delta_t_seconds: f64) -> TimeResult<UT1> {
180 let tt_jd = self.to_julian_date();
181 let delta_t_days = delta_t_seconds / SECONDS_PER_DAY_F64;
182
183 let (ut1_jd1, ut1_jd2) = if tt_jd.jd1().abs() > tt_jd.jd2().abs() {
186 (tt_jd.jd1(), tt_jd.jd2() - delta_t_days)
187 } else {
188 (tt_jd.jd1() - delta_t_days, tt_jd.jd2())
189 };
190
191 Ok(UT1::from_julian_date(JulianDate::new(ut1_jd1, ut1_jd2)))
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use celestial_core::constants::J2000_JD;
199
200 #[test]
201 fn test_ut1_identity_conversion() {
202 let ut1 = UT1::from_julian_date(JulianDate::new(J2000_JD, 0.999999999999999));
203 let identity_ut1 = ut1.to_ut1().unwrap();
204
205 assert_eq!(
206 ut1.to_julian_date().jd1(),
207 identity_ut1.to_julian_date().jd1(),
208 "UT1 identity conversion should preserve JD1 exactly"
209 );
210 assert_eq!(
211 ut1.to_julian_date().jd2(),
212 identity_ut1.to_julian_date().jd2(),
213 "UT1 identity conversion should preserve JD2 exactly"
214 );
215 }
216
217 #[test]
218 fn test_ut1_tai_offset_applied_correctly() {
219 let test_dates = [
220 (J2000_JD, "J2000.0"),
221 (2455197.5, "2010-01-01"),
222 (2459580.5, "2022-01-01"),
223 ];
224 let ut1_tai_offset = -32.3;
225 let delta_t = 69.0;
226
227 for (jd, description) in test_dates {
228 let ut1 = UT1::from_julian_date(JulianDate::new(jd, 0.0));
230 let tai = ut1.to_tai_with_offset(ut1_tai_offset).unwrap();
231
232 let ut1_jd = ut1.to_julian_date();
233 let tai_jd = tai.to_julian_date();
234
235 let offset_days = (tai_jd.jd1() - ut1_jd.jd1()) + (tai_jd.jd2() - ut1_jd.jd2());
236 let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
237
238 assert_eq!(
239 offset_seconds, -ut1_tai_offset,
240 "{}: UT1->TAI offset must be exactly {} seconds",
241 description, -ut1_tai_offset
242 );
243
244 let tai = TAI::from_julian_date(JulianDate::new(jd, 0.0));
246 let ut1 = tai.to_ut1_with_offset(ut1_tai_offset).unwrap();
247
248 let tai_jd = tai.to_julian_date();
249 let ut1_jd = ut1.to_julian_date();
250
251 let offset_days = (tai_jd.jd1() - ut1_jd.jd1()) + (tai_jd.jd2() - ut1_jd.jd2());
252 let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
253
254 assert_eq!(
255 offset_seconds, -ut1_tai_offset,
256 "{}: TAI->UT1 means TAI is {} seconds ahead",
257 description, -ut1_tai_offset
258 );
259
260 let ut1 = UT1::from_julian_date(JulianDate::new(jd, 0.0));
262 let tt = ut1.to_tt_with_delta_t(delta_t).unwrap();
263
264 let ut1_jd = ut1.to_julian_date();
265 let tt_jd = tt.to_julian_date();
266
267 let offset_days = (tt_jd.jd1() - ut1_jd.jd1()) + (tt_jd.jd2() - ut1_jd.jd2());
268 let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
269
270 assert_eq!(
271 offset_seconds, delta_t,
272 "{}: UT1->TT offset must be exactly {} seconds",
273 description, delta_t
274 );
275
276 let tt = TT::from_julian_date(JulianDate::new(jd, 0.0));
278 let ut1 = tt.to_ut1_with_delta_t(delta_t).unwrap();
279
280 let tt_jd = tt.to_julian_date();
281 let ut1_jd = ut1.to_julian_date();
282
283 let offset_days = (tt_jd.jd1() - ut1_jd.jd1()) + (tt_jd.jd2() - ut1_jd.jd2());
284 let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
285
286 assert_eq!(
287 offset_seconds, delta_t,
288 "{}: TT->UT1 means TT is {} seconds ahead",
289 description, delta_t
290 );
291 }
292 }
293
294 #[test]
295 fn test_ut1_tai_round_trip_precision() {
296 const TOLERANCE_DAYS: f64 = 1e-14;
299
300 let test_jd2_values = [0.0, 0.5, 0.123456789012345, -0.123456789012345, 0.987654321];
301 let test_offsets = [-32.0, -31.8, -32.5, -33.1, -30.9];
302
303 for jd2 in test_jd2_values {
304 for &offset in &test_offsets {
305 let original_ut1 = UT1::from_julian_date(JulianDate::new(J2000_JD, jd2));
307 let tai = original_ut1.to_tai_with_offset(offset).unwrap();
308 let round_trip_ut1 = tai.to_ut1_with_offset(offset).unwrap();
309
310 assert_eq!(
311 original_ut1.to_julian_date().jd1(),
312 round_trip_ut1.to_julian_date().jd1(),
313 "UT1->TAI->UT1 JD1 must be exact for jd2={}, offset={}",
314 jd2,
315 offset
316 );
317 let jd2_diff = (original_ut1.to_julian_date().jd2()
318 - round_trip_ut1.to_julian_date().jd2())
319 .abs();
320 assert!(
321 jd2_diff <= TOLERANCE_DAYS,
322 "UT1->TAI->UT1 JD2 diff {} exceeds tolerance {} for jd2={}, offset={}",
323 jd2_diff,
324 TOLERANCE_DAYS,
325 jd2,
326 offset
327 );
328
329 let original_tai = TAI::from_julian_date(JulianDate::new(J2000_JD, jd2));
331 let ut1 = original_tai.to_ut1_with_offset(offset).unwrap();
332 let round_trip_tai = ut1.to_tai_with_offset(offset).unwrap();
333
334 assert_eq!(
335 original_tai.to_julian_date().jd1(),
336 round_trip_tai.to_julian_date().jd1(),
337 "TAI->UT1->TAI JD1 must be exact for jd2={}, offset={}",
338 jd2,
339 offset
340 );
341 let jd2_diff = (original_tai.to_julian_date().jd2()
342 - round_trip_tai.to_julian_date().jd2())
343 .abs();
344 assert!(
345 jd2_diff <= TOLERANCE_DAYS,
346 "TAI->UT1->TAI JD2 diff {} exceeds tolerance {} for jd2={}, offset={}",
347 jd2_diff,
348 TOLERANCE_DAYS,
349 jd2,
350 offset
351 );
352 }
353 }
354
355 let alt_ut1 = UT1::from_julian_date(JulianDate::new(0.5, J2000_JD));
357 let alt_tai = alt_ut1.to_tai_with_offset(-32.0).unwrap();
358 let alt_round_trip = alt_tai.to_ut1_with_offset(-32.0).unwrap();
359
360 assert_eq!(
361 alt_ut1.to_julian_date().jd1(),
362 alt_round_trip.to_julian_date().jd1(),
363 "Alternate split UT1->TAI->UT1 JD1 must be exact"
364 );
365 let jd2_diff =
366 (alt_ut1.to_julian_date().jd2() - alt_round_trip.to_julian_date().jd2()).abs();
367 assert!(
368 jd2_diff <= TOLERANCE_DAYS,
369 "Alternate split UT1->TAI->UT1 JD2 diff {} exceeds tolerance {}",
370 jd2_diff,
371 TOLERANCE_DAYS
372 );
373 }
374
375 #[test]
376 fn test_ut1_tt_round_trip_precision() {
377 const TOLERANCE_DAYS: f64 = 1e-14;
380
381 let test_jd2_values = [0.0, 0.5, 0.123456789012345, -0.123456789012345, 0.987654321];
382 let test_delta_t_values = [63.8, 69.0, 70.5, 65.2];
383
384 for jd2 in test_jd2_values {
385 for &delta_t in &test_delta_t_values {
386 let original_ut1 = UT1::from_julian_date(JulianDate::new(J2000_JD, jd2));
388 let tt = original_ut1.to_tt_with_delta_t(delta_t).unwrap();
389 let round_trip_ut1 = tt.to_ut1_with_delta_t(delta_t).unwrap();
390
391 assert_eq!(
392 original_ut1.to_julian_date().jd1(),
393 round_trip_ut1.to_julian_date().jd1(),
394 "UT1->TT->UT1 JD1 must be exact for jd2={}, delta_t={}",
395 jd2,
396 delta_t
397 );
398 let jd2_diff = (original_ut1.to_julian_date().jd2()
399 - round_trip_ut1.to_julian_date().jd2())
400 .abs();
401 assert!(
402 jd2_diff <= TOLERANCE_DAYS,
403 "UT1->TT->UT1 JD2 diff {} exceeds tolerance {} for jd2={}, delta_t={}",
404 jd2_diff,
405 TOLERANCE_DAYS,
406 jd2,
407 delta_t
408 );
409
410 let original_tt = TT::from_julian_date(JulianDate::new(J2000_JD, jd2));
412 let ut1 = original_tt.to_ut1_with_delta_t(delta_t).unwrap();
413 let round_trip_tt = ut1.to_tt_with_delta_t(delta_t).unwrap();
414
415 assert_eq!(
416 original_tt.to_julian_date().jd1(),
417 round_trip_tt.to_julian_date().jd1(),
418 "TT->UT1->TT JD1 must be exact for jd2={}, delta_t={}",
419 jd2,
420 delta_t
421 );
422 let jd2_diff = (original_tt.to_julian_date().jd2()
423 - round_trip_tt.to_julian_date().jd2())
424 .abs();
425 assert!(
426 jd2_diff <= TOLERANCE_DAYS,
427 "TT->UT1->TT JD2 diff {} exceeds tolerance {} for jd2={}, delta_t={}",
428 jd2_diff,
429 TOLERANCE_DAYS,
430 jd2,
431 delta_t
432 );
433 }
434 }
435
436 let alt_ut1 = UT1::from_julian_date(JulianDate::new(0.5, J2000_JD));
438 let alt_tt = alt_ut1.to_tt_with_delta_t(69.0).unwrap();
439 let alt_round_trip = alt_tt.to_ut1_with_delta_t(69.0).unwrap();
440
441 assert_eq!(
442 alt_ut1.to_julian_date().jd1(),
443 alt_round_trip.to_julian_date().jd1(),
444 "Alternate split UT1->TT->UT1 JD1 must be exact"
445 );
446 let jd2_diff =
447 (alt_ut1.to_julian_date().jd2() - alt_round_trip.to_julian_date().jd2()).abs();
448 assert!(
449 jd2_diff <= TOLERANCE_DAYS,
450 "Alternate split UT1->TT->UT1 JD2 diff {} exceeds tolerance {}",
451 jd2_diff,
452 TOLERANCE_DAYS
453 );
454 }
455}