1use crate::astro::constants::time::{BDT_MINUS_TAI_S, GPST_MINUS_TAI_S};
14use crate::astro::constants::time::{
15 DAYS_PER_JULIAN_CENTURY, J2000_JD, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
16 TT_MINUS_TAI_S,
17};
18use crate::astro::data::iers::{Ut1Entry, UT1_DATA};
19use crate::astro::time::civil;
20use crate::astro::time::eop::{
21 check_ut1_coverage, CoverageError, LeapSecondTable, TimeScaleInputErrorKind, Ut1Provenance,
22 Validated, ValidityMode,
23};
24use crate::astro::time::model::TimeScale;
25use crate::validate::{self, FieldError};
26
27const ROUND_1E7: f64 = 10_000_000.0;
28
29pub const GLONASST_MINUS_UTC_S: f64 = 3.0 * SECONDS_PER_HOUR;
34
35pub const TT_TCG_RATE_L_G: f64 = 6.969290134e-10;
37
38pub const TDB_TCB_RATE_L_B: f64 = 1.550519768e-8;
40
41pub const TDB_TCB_OFFSET_TDB0_S: f64 = -6.55e-5;
43
44pub const TCG_TCB_REFERENCE_JD: f64 = 2_443_144.500_372_5;
46
47#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct TimeScales {
55 pub jd_whole: f64,
57 pub ut1_fraction: f64,
59 pub tt_fraction: f64,
61 pub tdb_fraction: f64,
63 pub jd_ut1: f64,
65 pub jd_tt: f64,
67 pub jd_tdb: f64,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq)]
73pub struct LeapSecondEntry {
74 pub mjd: i32,
76 pub tai_utc: f64,
78}
79
80#[derive(Debug, Clone, Copy)]
82pub struct TimeTables<'a> {
83 pub leap_seconds: &'a [LeapSecondEntry],
85 pub ut1_utc: &'a [Ut1Entry],
87}
88
89impl<'a> TimeTables<'a> {
90 pub fn new(
92 leap_seconds: &'a [LeapSecondEntry],
93 ut1_utc: &'a [Ut1Entry],
94 ) -> Result<Self, CoverageError> {
95 validate_time_tables(leap_seconds, ut1_utc)?;
96 Ok(Self {
97 leap_seconds,
98 ut1_utc,
99 })
100 }
101
102 #[must_use]
104 pub fn leap_second_table(&self) -> LeapSecondTable {
105 leap_second_table_for(self.leap_seconds, "caller-supplied leap-second table")
106 }
107
108 #[must_use]
110 pub fn ut1_coverage(&self) -> Ut1Provenance {
111 ut1_coverage_for(
112 self.ut1_utc,
113 self.leap_seconds,
114 "caller-supplied UT1-UTC table",
115 )
116 }
117}
118
119impl TimeTables<'static> {
120 #[must_use]
122 pub fn embedded() -> Self {
123 Self {
124 leap_seconds: LEAP_SECONDS,
125 ut1_utc: &UT1_DATA,
126 }
127 }
128}
129
130static LEAP_SECONDS: &[LeapSecondEntry] = &[
131 LeapSecondEntry {
132 mjd: 41317,
133 tai_utc: 10.0,
134 },
135 LeapSecondEntry {
136 mjd: 41499,
137 tai_utc: 11.0,
138 },
139 LeapSecondEntry {
140 mjd: 41683,
141 tai_utc: 12.0,
142 },
143 LeapSecondEntry {
144 mjd: 42048,
145 tai_utc: 13.0,
146 },
147 LeapSecondEntry {
148 mjd: 42413,
149 tai_utc: 14.0,
150 },
151 LeapSecondEntry {
152 mjd: 42778,
153 tai_utc: 15.0,
154 },
155 LeapSecondEntry {
156 mjd: 43144,
157 tai_utc: 16.0,
158 },
159 LeapSecondEntry {
160 mjd: 43509,
161 tai_utc: 17.0,
162 },
163 LeapSecondEntry {
164 mjd: 43874,
165 tai_utc: 18.0,
166 },
167 LeapSecondEntry {
168 mjd: 44239,
169 tai_utc: 19.0,
170 },
171 LeapSecondEntry {
172 mjd: 44786,
173 tai_utc: 20.0,
174 },
175 LeapSecondEntry {
176 mjd: 45151,
177 tai_utc: 21.0,
178 },
179 LeapSecondEntry {
180 mjd: 45516,
181 tai_utc: 22.0,
182 },
183 LeapSecondEntry {
184 mjd: 46247,
185 tai_utc: 23.0,
186 },
187 LeapSecondEntry {
188 mjd: 47161,
189 tai_utc: 24.0,
190 },
191 LeapSecondEntry {
192 mjd: 47892,
193 tai_utc: 25.0,
194 },
195 LeapSecondEntry {
196 mjd: 48257,
197 tai_utc: 26.0,
198 },
199 LeapSecondEntry {
200 mjd: 48804,
201 tai_utc: 27.0,
202 },
203 LeapSecondEntry {
204 mjd: 49169,
205 tai_utc: 28.0,
206 },
207 LeapSecondEntry {
208 mjd: 49534,
209 tai_utc: 29.0,
210 },
211 LeapSecondEntry {
212 mjd: 50083,
213 tai_utc: 30.0,
214 },
215 LeapSecondEntry {
216 mjd: 50448,
217 tai_utc: 31.0,
218 },
219 LeapSecondEntry {
220 mjd: 50813,
221 tai_utc: 32.0,
222 },
223 LeapSecondEntry {
224 mjd: 53736,
225 tai_utc: 33.0,
226 },
227 LeapSecondEntry {
228 mjd: 54832,
229 tai_utc: 34.0,
230 },
231 LeapSecondEntry {
232 mjd: 56109,
233 tai_utc: 35.0,
234 },
235 LeapSecondEntry {
236 mjd: 57204,
237 tai_utc: 36.0,
238 },
239 LeapSecondEntry {
240 mjd: 57754,
241 tai_utc: 37.0,
242 },
243];
244
245struct RubberSecondEntry {
249 start_mjd: i32,
251 base: f64,
253 ref_mjd: f64,
255 rate: f64,
257}
258
259static RUBBER_SECONDS: &[RubberSecondEntry] = &[
265 RubberSecondEntry {
266 start_mjd: 37300,
267 base: 1.4228180,
268 ref_mjd: 37300.0,
269 rate: 0.001296,
270 },
271 RubberSecondEntry {
272 start_mjd: 37512,
273 base: 1.3728180,
274 ref_mjd: 37300.0,
275 rate: 0.001296,
276 },
277 RubberSecondEntry {
278 start_mjd: 37665,
279 base: 1.8458580,
280 ref_mjd: 37665.0,
281 rate: 0.0011232,
282 },
283 RubberSecondEntry {
284 start_mjd: 38334,
285 base: 1.9458580,
286 ref_mjd: 37665.0,
287 rate: 0.0011232,
288 },
289 RubberSecondEntry {
290 start_mjd: 38395,
291 base: 3.2401300,
292 ref_mjd: 38761.0,
293 rate: 0.001296,
294 },
295 RubberSecondEntry {
296 start_mjd: 38486,
297 base: 3.3401300,
298 ref_mjd: 38761.0,
299 rate: 0.001296,
300 },
301 RubberSecondEntry {
302 start_mjd: 38639,
303 base: 3.4401300,
304 ref_mjd: 38761.0,
305 rate: 0.001296,
306 },
307 RubberSecondEntry {
308 start_mjd: 38761,
309 base: 3.5401300,
310 ref_mjd: 38761.0,
311 rate: 0.001296,
312 },
313 RubberSecondEntry {
314 start_mjd: 38820,
315 base: 3.6401300,
316 ref_mjd: 38761.0,
317 rate: 0.001296,
318 },
319 RubberSecondEntry {
320 start_mjd: 38942,
321 base: 3.7401300,
322 ref_mjd: 38761.0,
323 rate: 0.001296,
324 },
325 RubberSecondEntry {
326 start_mjd: 39004,
327 base: 3.8401300,
328 ref_mjd: 38761.0,
329 rate: 0.001296,
330 },
331 RubberSecondEntry {
332 start_mjd: 39126,
333 base: 4.3131700,
334 ref_mjd: 39126.0,
335 rate: 0.002592,
336 },
337 RubberSecondEntry {
338 start_mjd: 39887,
339 base: 4.2131700,
340 ref_mjd: 39126.0,
341 rate: 0.002592,
342 },
343];
344
345impl TimeScales {
346 pub fn from_utc(
350 year: i32,
351 month: i32,
352 day: i32,
353 hour: i32,
354 minute: i32,
355 second: f64,
356 ) -> Result<Self, CoverageError> {
357 validate_utc_input_embedded(year, month, day, hour, minute, second)?;
358 Ok(Self::from_utc_unchecked(
359 year, month, day, hour, minute, second,
360 ))
361 }
362
363 pub fn from_utc_with_tables(
366 year: i32,
367 month: i32,
368 day: i32,
369 hour: i32,
370 minute: i32,
371 second: f64,
372 tables: TimeTables<'_>,
373 ) -> Result<Self, CoverageError> {
374 validate_time_tables(tables.leap_seconds, tables.ut1_utc)?;
375 validate_utc_input_with_table(year, month, day, hour, minute, second, tables.leap_seconds)?;
376 let scales =
377 Self::from_utc_with_tables_unchecked(year, month, day, hour, minute, second, tables)?;
378 let prov = tables.ut1_coverage();
379 check_ut1_coverage(&prov, scales.jd_tt, ValidityMode::Strict)?;
380 Ok(scales)
381 }
382
383 fn from_utc_unchecked(
386 year: i32,
387 month: i32,
388 day: i32,
389 hour: i32,
390 minute: i32,
391 second: f64,
392 ) -> Self {
393 let jd_day = julian_day_number(year, month, day);
394 let jd1 = jd_day as f64 - 0.5;
395 let utc_seconds_of_day =
396 hour as f64 * SECONDS_PER_HOUR + minute as f64 * SECONDS_PER_MINUTE + second;
397 let leap_lookup_second = if second >= 60.0 { 59.0 } else { second };
398 let jd2 = (leap_lookup_second
399 + minute as f64 * SECONDS_PER_MINUTE
400 + hour as f64 * SECONDS_PER_HOUR)
401 / SECONDS_PER_DAY;
402 let jd_utc_total = jd1 + jd2;
403
404 let leap_seconds = find_leap_seconds(jd_utc_total);
405 let utc_seconds_at_midnight = jd1 * SECONDS_PER_DAY;
406
407 let utc_whole_seconds = utc_seconds_of_day.trunc();
408 let utc_subsecond = utc_seconds_of_day.fract();
409
410 let tai_seconds = utc_seconds_at_midnight + leap_seconds + utc_whole_seconds;
412 let jd_whole = (tai_seconds / SECONDS_PER_DAY).floor();
413 let tai_fraction =
414 (tai_seconds - jd_whole * SECONDS_PER_DAY + utc_subsecond) / SECONDS_PER_DAY;
415 let tt_offset_days = TT_MINUS_TAI_S / SECONDS_PER_DAY;
416
417 let tt_fraction = tai_fraction + tt_offset_days;
418 let jd_tt = jd_whole + tt_fraction;
419
420 let delta_t = interpolate_delta_t(jd_tt);
421 let ut1_fraction = tt_fraction - delta_t / SECONDS_PER_DAY;
422 let jd_ut1 = jd_whole + ut1_fraction;
423
424 let t = (jd_whole - J2000_JD + tt_fraction) / DAYS_PER_JULIAN_CENTURY;
425 let tdb_minus_tt_seconds = 0.001657 * (628.3076 * t + 6.2401).sin()
426 + 0.000022 * (575.3385 * t + 4.2970).sin()
427 + 0.000014 * (1256.6152 * t + 6.1969).sin()
428 + 0.000005 * (606.9777 * t + 4.0212).sin()
429 + 0.000005 * (52.9691 * t + 0.4444).sin()
430 + 0.000002 * (21.3299 * t + 5.5431).sin()
431 + 0.000010 * t * (628.3076 * t + 4.2490).sin();
432
433 let tdb_fraction = tt_fraction + tdb_minus_tt_seconds / SECONDS_PER_DAY;
434 let jd_tdb = jd_whole + tdb_fraction;
435
436 Self {
437 jd_whole,
438 ut1_fraction,
439 tt_fraction,
440 tdb_fraction,
441 jd_ut1,
442 jd_tt,
443 jd_tdb,
444 }
445 }
446
447 fn from_utc_with_tables_unchecked(
449 year: i32,
450 month: i32,
451 day: i32,
452 hour: i32,
453 minute: i32,
454 second: f64,
455 tables: TimeTables<'_>,
456 ) -> Result<Self, CoverageError> {
457 let jd_day = julian_day_number(year, month, day);
458 let jd1 = jd_day as f64 - 0.5;
459 let utc_seconds_of_day =
460 hour as f64 * SECONDS_PER_HOUR + minute as f64 * SECONDS_PER_MINUTE + second;
461 let leap_lookup_second = if second >= 60.0 { 59.0 } else { second };
462 let jd2 = (leap_lookup_second
463 + minute as f64 * SECONDS_PER_MINUTE
464 + hour as f64 * SECONDS_PER_HOUR)
465 / SECONDS_PER_DAY;
466 let jd_utc_total = jd1 + jd2;
467
468 let leap_seconds = find_leap_seconds_in_table_checked(jd_utc_total, tables.leap_seconds)?;
469 let utc_seconds_at_midnight = jd1 * SECONDS_PER_DAY;
470
471 let utc_whole_seconds = utc_seconds_of_day.trunc();
472 let utc_subsecond = utc_seconds_of_day.fract();
473
474 let tai_seconds = utc_seconds_at_midnight + leap_seconds + utc_whole_seconds;
475 let jd_whole = (tai_seconds / SECONDS_PER_DAY).floor();
476 let tai_fraction =
477 (tai_seconds - jd_whole * SECONDS_PER_DAY + utc_subsecond) / SECONDS_PER_DAY;
478 let tt_offset_days = TT_MINUS_TAI_S / SECONDS_PER_DAY;
479
480 let tt_fraction = tai_fraction + tt_offset_days;
481 let jd_tt = jd_whole + tt_fraction;
482
483 let delta_t = interpolate_delta_t_with_table(jd_tt, tables.ut1_utc, tables.leap_seconds)?;
484 let ut1_fraction = tt_fraction - delta_t / SECONDS_PER_DAY;
485 let jd_ut1 = jd_whole + ut1_fraction;
486
487 let t = (jd_whole - J2000_JD + tt_fraction) / DAYS_PER_JULIAN_CENTURY;
488 let tdb_minus_tt_seconds = 0.001657 * (628.3076 * t + 6.2401).sin()
489 + 0.000022 * (575.3385 * t + 4.2970).sin()
490 + 0.000014 * (1256.6152 * t + 6.1969).sin()
491 + 0.000005 * (606.9777 * t + 4.0212).sin()
492 + 0.000005 * (52.9691 * t + 0.4444).sin()
493 + 0.000002 * (21.3299 * t + 5.5431).sin()
494 + 0.000010 * t * (628.3076 * t + 4.2490).sin();
495
496 let tdb_fraction = tt_fraction + tdb_minus_tt_seconds / SECONDS_PER_DAY;
497 let jd_tdb = jd_whole + tdb_fraction;
498
499 Ok(Self {
500 jd_whole,
501 ut1_fraction,
502 tt_fraction,
503 tdb_fraction,
504 jd_ut1,
505 jd_tt,
506 jd_tdb,
507 })
508 }
509
510 pub fn from_utc_validated(
529 year: i32,
530 month: i32,
531 day: i32,
532 hour: i32,
533 minute: i32,
534 second: f64,
535 mode: ValidityMode,
536 ) -> Result<Validated<Self>, CoverageError> {
537 let scales = Self::from_utc(year, month, day, hour, minute, second)?;
539 let prov = ut1_coverage();
543 let degraded = check_ut1_coverage(&prov, scales.jd_tt, mode)?;
544 Ok(Validated {
545 value: scales,
546 degraded,
547 })
548 }
549
550 #[allow(clippy::too_many_arguments)]
552 pub fn from_utc_validated_with_tables(
553 year: i32,
554 month: i32,
555 day: i32,
556 hour: i32,
557 minute: i32,
558 second: f64,
559 mode: ValidityMode,
560 tables: TimeTables<'_>,
561 ) -> Result<Validated<Self>, CoverageError> {
562 validate_time_tables(tables.leap_seconds, tables.ut1_utc)?;
563 validate_utc_input_with_table(year, month, day, hour, minute, second, tables.leap_seconds)?;
564 let scales =
565 Self::from_utc_with_tables_unchecked(year, month, day, hour, minute, second, tables)?;
566 let prov = tables.ut1_coverage();
567 let degraded = check_ut1_coverage(&prov, scales.jd_tt, mode)?;
568 Ok(Validated {
569 value: scales,
570 degraded,
571 })
572 }
573
574 pub fn from_scale(
583 scale: TimeScale,
584 year: i32,
585 month: i32,
586 day: i32,
587 hour: i32,
588 minute: i32,
589 second: f64,
590 ) -> Result<Self, CoverageError> {
591 let cal = ScaleCal {
592 year,
593 month,
594 day,
595 hour,
596 minute,
597 second,
598 };
599 validate_scale_input_embedded(scale, cal)?;
600 let utc = scale_calendar_to_utc(scale, cal, LEAP_SECONDS);
601 Self::from_utc(
602 utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second,
603 )
604 }
605
606 #[allow(clippy::too_many_arguments)]
609 pub fn from_scale_with_tables(
610 scale: TimeScale,
611 year: i32,
612 month: i32,
613 day: i32,
614 hour: i32,
615 minute: i32,
616 second: f64,
617 tables: TimeTables<'_>,
618 ) -> Result<Self, CoverageError> {
619 validate_time_tables(tables.leap_seconds, tables.ut1_utc)?;
620 let cal = ScaleCal {
621 year,
622 month,
623 day,
624 hour,
625 minute,
626 second,
627 };
628 validate_scale_input_with_table(scale, cal, tables.leap_seconds)?;
629 let utc = scale_calendar_to_utc_with_table(scale, cal, tables.leap_seconds)?;
630 Self::from_utc_with_tables(
631 utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second, tables,
632 )
633 }
634
635 #[must_use]
637 pub fn jd_tcg(&self) -> f64 {
638 tt_to_tcg_jd(self.jd_tt)
639 }
640
641 #[must_use]
643 pub fn tcg_fraction(&self) -> f64 {
644 tcg_fraction_from_tt_split(self.jd_whole, self.tt_fraction)
645 }
646
647 #[must_use]
649 pub fn jd_tcb(&self) -> f64 {
650 tdb_to_tcb_jd(self.jd_tdb)
651 }
652
653 #[must_use]
655 pub fn tcb_fraction(&self) -> f64 {
656 tcb_fraction_from_tdb_split(self.jd_whole, self.tdb_fraction)
657 }
658}
659
660#[derive(Debug, Clone, Copy, PartialEq)]
662struct ScaleCal {
663 year: i32,
664 month: i32,
665 day: i32,
666 hour: i32,
667 minute: i32,
668 second: f64,
669}
670
671fn scale_calendar_to_utc(
679 scale: TimeScale,
680 cal: ScaleCal,
681 leap_seconds: &[LeapSecondEntry],
682) -> ScaleCal {
683 match scale {
684 TimeScale::Utc => cal,
685 TimeScale::Glonasst => normalize_calendar_seconds(cal, cal.second - GLONASST_MINUS_UTC_S),
686 TimeScale::Tcg => {
687 let tcg_jd = continuous_calendar_jd(cal);
688 coordinate_calendar_to_utc(
689 cal,
690 tcg_to_tt_jd(tcg_jd) - tcg_jd,
691 TimeScale::Tt,
692 leap_seconds,
693 )
694 }
695 TimeScale::Tdb => {
696 let tdb_jd = continuous_calendar_jd(cal);
697 coordinate_calendar_to_utc(
698 cal,
699 tdb_to_tt_jd_for_tdb_input(tdb_jd) - tdb_jd,
700 TimeScale::Tt,
701 leap_seconds,
702 )
703 }
704 TimeScale::Tcb => {
705 let tcb_jd = continuous_calendar_jd(cal);
706 let tdb_jd = tcb_to_tdb_jd(tcb_jd);
707 let tt_jd = tdb_to_tt_jd_for_tdb_input(tdb_jd);
708 coordinate_calendar_to_utc(cal, tt_jd - tcb_jd, TimeScale::Tt, leap_seconds)
709 }
710 _ => {
711 let tai = normalize_calendar_seconds(cal, cal.second + tai_minus_scale_seconds(scale));
712 tai_calendar_to_utc(tai, leap_seconds)
713 }
714 }
715}
716
717fn scale_calendar_to_utc_with_table(
718 scale: TimeScale,
719 cal: ScaleCal,
720 leap_seconds: &[LeapSecondEntry],
721) -> Result<ScaleCal, CoverageError> {
722 match scale {
723 TimeScale::Utc => Ok(cal),
724 TimeScale::Glonasst => Ok(normalize_calendar_seconds(
725 cal,
726 cal.second - GLONASST_MINUS_UTC_S,
727 )),
728 TimeScale::Tcg => {
729 let tcg_jd = continuous_calendar_jd(cal);
730 coordinate_calendar_to_utc_with_table(
731 cal,
732 tcg_to_tt_jd(tcg_jd) - tcg_jd,
733 TimeScale::Tt,
734 leap_seconds,
735 )
736 }
737 TimeScale::Tdb => {
738 let tdb_jd = continuous_calendar_jd(cal);
739 coordinate_calendar_to_utc_with_table(
740 cal,
741 tdb_to_tt_jd_for_tdb_input(tdb_jd) - tdb_jd,
742 TimeScale::Tt,
743 leap_seconds,
744 )
745 }
746 TimeScale::Tcb => {
747 let tcb_jd = continuous_calendar_jd(cal);
748 let tdb_jd = tcb_to_tdb_jd(tcb_jd);
749 let tt_jd = tdb_to_tt_jd_for_tdb_input(tdb_jd);
750 coordinate_calendar_to_utc_with_table(cal, tt_jd - tcb_jd, TimeScale::Tt, leap_seconds)
751 }
752 _ => {
753 let tai = normalize_calendar_seconds(cal, cal.second + tai_minus_scale_seconds(scale));
754 tai_calendar_to_utc_with_table(tai, leap_seconds)
755 }
756 }
757}
758
759fn coordinate_calendar_to_utc(
760 cal: ScaleCal,
761 target_minus_source_days: f64,
762 target_scale: TimeScale,
763 leap_seconds: &[LeapSecondEntry],
764) -> ScaleCal {
765 let target =
766 normalize_calendar_seconds(cal, cal.second + target_minus_source_days * SECONDS_PER_DAY);
767 let tai = normalize_calendar_seconds(
768 target,
769 target.second + tai_minus_scale_seconds(target_scale),
770 );
771 tai_calendar_to_utc(tai, leap_seconds)
772}
773
774fn coordinate_calendar_to_utc_with_table(
775 cal: ScaleCal,
776 target_minus_source_days: f64,
777 target_scale: TimeScale,
778 leap_seconds: &[LeapSecondEntry],
779) -> Result<ScaleCal, CoverageError> {
780 let target =
781 normalize_calendar_seconds(cal, cal.second + target_minus_source_days * SECONDS_PER_DAY);
782 let tai = normalize_calendar_seconds(
783 target,
784 target.second + tai_minus_scale_seconds(target_scale),
785 );
786 tai_calendar_to_utc_with_table(tai, leap_seconds)
787}
788
789fn continuous_calendar_jd(cal: ScaleCal) -> f64 {
790 let jd1 = julian_day_number(cal.year, cal.month, cal.day) as f64 - 0.5;
791 jd1 + seconds_of_day(cal) / SECONDS_PER_DAY
792}
793
794fn tai_minus_scale_seconds(scale: TimeScale) -> f64 {
795 match scale {
796 TimeScale::Utc | TimeScale::Glonasst | TimeScale::Tcg | TimeScale::Tdb | TimeScale::Tcb => {
799 0.0
800 }
801 TimeScale::Tai => 0.0,
802 TimeScale::Tt => -TT_MINUS_TAI_S,
803 TimeScale::Gpst | TimeScale::Gst | TimeScale::Qzsst => GPST_MINUS_TAI_S,
805 TimeScale::Bdt => BDT_MINUS_TAI_S,
806 }
807}
808
809fn tai_calendar_to_utc(tai: ScaleCal, leap_seconds: &[LeapSecondEntry]) -> ScaleCal {
810 if let Some(utc) = positive_leap_second_utc_label(tai, leap_seconds) {
811 return utc;
812 }
813
814 let mut leap = leap_seconds_at_utc_label(tai, leap_seconds);
815 let mut utc = normalize_calendar_seconds(tai, tai.second - leap);
816 for _ in 0..3 {
817 let next_leap = leap_seconds_at_utc_label(utc, leap_seconds);
818 if next_leap == leap {
819 return utc;
820 }
821 leap = next_leap;
822 utc = normalize_calendar_seconds(tai, tai.second - leap);
823 }
824 utc
825}
826
827fn tai_calendar_to_utc_with_table(
828 tai: ScaleCal,
829 leap_seconds: &[LeapSecondEntry],
830) -> Result<ScaleCal, CoverageError> {
831 if let Some(utc) = positive_leap_second_utc_label_with_table(tai, leap_seconds)? {
832 return Ok(utc);
833 }
834
835 let mut leap = leap_seconds_at_utc_label_checked(tai, leap_seconds)?;
836 let mut utc = normalize_calendar_seconds(tai, tai.second - leap);
837 for _ in 0..3 {
838 let next_leap = leap_seconds_at_utc_label_checked(utc, leap_seconds)?;
839 if next_leap == leap {
840 return Ok(utc);
841 }
842 leap = next_leap;
843 utc = normalize_calendar_seconds(tai, tai.second - leap);
844 }
845 Ok(utc)
846}
847
848fn positive_leap_second_utc_label(
849 tai: ScaleCal,
850 leap_seconds: &[LeapSecondEntry],
851) -> Option<ScaleCal> {
852 let tai_sod = seconds_of_day(tai);
853 let utc_midnight = ScaleCal {
854 year: tai.year,
855 month: tai.month,
856 day: tai.day,
857 hour: 0,
858 minute: 0,
859 second: 0.0,
860 };
861 let previous_second = normalize_calendar_seconds(utc_midnight, -1.0);
862 let old_leap = leap_seconds_at_utc_label(previous_second, leap_seconds);
863 let new_leap = leap_seconds_at_utc_label(utc_midnight, leap_seconds);
864 if new_leap <= old_leap || !(old_leap..new_leap).contains(&tai_sod) {
865 return None;
866 }
867
868 let mut utc = previous_second;
869 utc.second = 60.0 + (tai_sod - old_leap);
870 Some(utc)
871}
872
873fn positive_leap_second_utc_label_with_table(
874 tai: ScaleCal,
875 leap_seconds: &[LeapSecondEntry],
876) -> Result<Option<ScaleCal>, CoverageError> {
877 let tai_sod = seconds_of_day(tai);
878 let utc_midnight = ScaleCal {
879 year: tai.year,
880 month: tai.month,
881 day: tai.day,
882 hour: 0,
883 minute: 0,
884 second: 0.0,
885 };
886 let previous_second = normalize_calendar_seconds(utc_midnight, -1.0);
887 let Ok(old_leap) = leap_seconds_at_utc_label_checked(previous_second, leap_seconds) else {
888 return Ok(None);
889 };
890 let new_leap = leap_seconds_at_utc_label_checked(utc_midnight, leap_seconds)?;
891 if new_leap <= old_leap || !(old_leap..new_leap).contains(&tai_sod) {
892 return Ok(None);
893 }
894
895 let mut utc = previous_second;
896 utc.second = 60.0 + (tai_sod - old_leap);
897 Ok(Some(utc))
898}
899
900fn leap_seconds_at_utc_label(cal: ScaleCal, leap_seconds: &[LeapSecondEntry]) -> f64 {
901 let jd1 = julian_day_number(cal.year, cal.month, cal.day) as f64 - 0.5;
902 let lookup_second = if cal.second >= 60.0 { 59.0 } else { cal.second };
903 let jd2 = (cal.hour as f64 * SECONDS_PER_HOUR
904 + cal.minute as f64 * SECONDS_PER_MINUTE
905 + lookup_second)
906 / SECONDS_PER_DAY;
907 find_leap_seconds_in_table(jd1 + jd2, leap_seconds)
908}
909
910fn leap_seconds_at_utc_label_checked(
911 cal: ScaleCal,
912 leap_seconds: &[LeapSecondEntry],
913) -> Result<f64, CoverageError> {
914 let jd1 = julian_day_number(cal.year, cal.month, cal.day) as f64 - 0.5;
915 let lookup_second = if cal.second >= 60.0 { 59.0 } else { cal.second };
916 let jd2 = (cal.hour as f64 * SECONDS_PER_HOUR
917 + cal.minute as f64 * SECONDS_PER_MINUTE
918 + lookup_second)
919 / SECONDS_PER_DAY;
920 find_leap_seconds_in_table_checked(jd1 + jd2, leap_seconds)
921}
922
923fn seconds_of_day(cal: ScaleCal) -> f64 {
924 cal.hour as f64 * SECONDS_PER_HOUR + cal.minute as f64 * SECONDS_PER_MINUTE + cal.second
925}
926
927fn tdb_to_tt_jd_for_tdb_input(jd_tdb: f64) -> f64 {
928 let mut jd_tt = jd_tdb;
929 for _ in 0..4 {
930 jd_tt = jd_tdb - tdb_minus_tt_seconds_at_tt_jd(jd_tt) / SECONDS_PER_DAY;
931 }
932 jd_tt
933}
934
935fn tdb_minus_tt_seconds_at_tt_jd(jd_tt: f64) -> f64 {
936 let t = (jd_tt - J2000_JD) / DAYS_PER_JULIAN_CENTURY;
937 0.001657 * (628.3076 * t + 6.2401).sin()
938 + 0.000022 * (575.3385 * t + 4.2970).sin()
939 + 0.000014 * (1256.6152 * t + 6.1969).sin()
940 + 0.000005 * (606.9777 * t + 4.0212).sin()
941 + 0.000005 * (52.9691 * t + 0.4444).sin()
942 + 0.000002 * (21.3299 * t + 5.5431).sin()
943 + 0.000010 * t * (628.3076 * t + 4.2490).sin()
944}
945
946fn normalize_calendar_seconds(mut cal: ScaleCal, second: f64) -> ScaleCal {
947 if !second.is_finite() {
951 cal.second = second;
952 return cal;
953 }
954 cal.second = second;
955 while cal.second < 0.0 {
956 cal.second += 60.0;
957 cal.minute -= 1;
958 }
959 while cal.second >= 60.0 {
960 cal.second -= 60.0;
961 cal.minute += 1;
962 }
963 while cal.minute < 0 {
964 cal.minute += 60;
965 cal.hour -= 1;
966 }
967 while cal.minute > 59 {
968 cal.minute -= 60;
969 cal.hour += 1;
970 }
971 while cal.hour < 0 {
972 cal.hour += 24;
973 cal.day -= 1;
974 }
975 while cal.hour > 23 {
976 cal.hour -= 24;
977 cal.day += 1;
978 }
979 while cal.day < 1 {
980 cal.month -= 1;
981 if cal.month < 1 {
982 cal.month = 12;
983 cal.year -= 1;
984 }
985 cal.day = civil::days_in_month(i64::from(cal.year), i64::from(cal.month)) as i32;
986 }
987 loop {
988 let month_days = civil::days_in_month(i64::from(cal.year), i64::from(cal.month)) as i32;
989 if cal.day <= month_days {
990 break;
991 }
992 cal.day -= month_days;
993 cal.month += 1;
994 if cal.month > 12 {
995 cal.month = 1;
996 cal.year += 1;
997 }
998 }
999 cal
1000}
1001
1002pub(crate) fn is_positive_leap_second_label(
1003 year: i32,
1004 month: i32,
1005 day: i32,
1006 hour: i32,
1007 minute: i32,
1008) -> bool {
1009 is_positive_leap_second_label_with_table(year, month, day, hour, minute, LEAP_SECONDS)
1010}
1011
1012fn is_positive_leap_second_label_with_table(
1013 year: i32,
1014 month: i32,
1015 day: i32,
1016 hour: i32,
1017 minute: i32,
1018 leap_seconds: &[LeapSecondEntry],
1019) -> bool {
1020 if hour != 23 || minute != 59 {
1021 return false;
1022 }
1023 let jd1 = julian_day_number(year, month, day) as f64 - 0.5;
1024 find_leap_seconds_in_table(jd1 + 1.0, leap_seconds)
1025 > find_leap_seconds_in_table(jd1, leap_seconds)
1026}
1027
1028fn is_positive_leap_second_label_with_table_checked(
1029 year: i32,
1030 month: i32,
1031 day: i32,
1032 hour: i32,
1033 minute: i32,
1034 leap_seconds: &[LeapSecondEntry],
1035) -> Result<bool, CoverageError> {
1036 if hour != 23 || minute != 59 {
1037 return Ok(false);
1038 }
1039 let jd1 = julian_day_number(year, month, day) as f64 - 0.5;
1040 Ok(find_leap_seconds_in_table_checked(jd1 + 1.0, leap_seconds)?
1041 > find_leap_seconds_in_table_checked(jd1, leap_seconds)?)
1042}
1043
1044impl From<&FieldError> for TimeScaleInputErrorKind {
1045 fn from(error: &FieldError) -> Self {
1046 match error {
1047 FieldError::Missing { .. } => Self::Missing,
1048 FieldError::NonFinite { .. } => Self::NonFinite,
1049 FieldError::NotPositive { .. } => Self::NotPositive,
1050 FieldError::Negative { .. } => Self::Negative,
1051 FieldError::OutOfRange { .. } => Self::OutOfRange,
1052 FieldError::FloatParse { .. } => Self::FloatParse,
1053 FieldError::IntParse { .. } => Self::IntParse,
1054 FieldError::InvalidCivilDate { .. } => Self::InvalidCivilDate,
1055 FieldError::InvalidCivilTime { .. } => Self::InvalidCivilTime,
1056 }
1057 }
1058}
1059
1060fn map_time_scale_field_error(error: FieldError) -> CoverageError {
1061 CoverageError::InvalidInput {
1062 field: error.field(),
1063 kind: TimeScaleInputErrorKind::from(&error),
1064 }
1065}
1066
1067fn validate_utc_input_embedded(
1068 year: i32,
1069 month: i32,
1070 day: i32,
1071 hour: i32,
1072 minute: i32,
1073 second: f64,
1074) -> Result<(), CoverageError> {
1075 validate_utc_civil_input(year, month, day, hour, minute, second)?;
1076 if second >= 60.0
1077 && !is_positive_leap_second_label_with_table(year, month, day, hour, minute, LEAP_SECONDS)
1078 {
1079 return Err(CoverageError::InvalidInput {
1080 field: "civil datetime",
1081 kind: TimeScaleInputErrorKind::InvalidCivilTime,
1082 });
1083 }
1084 Ok(())
1085}
1086
1087fn validate_utc_input_with_table(
1088 year: i32,
1089 month: i32,
1090 day: i32,
1091 hour: i32,
1092 minute: i32,
1093 second: f64,
1094 leap_seconds: &[LeapSecondEntry],
1095) -> Result<(), CoverageError> {
1096 validate_utc_civil_input(year, month, day, hour, minute, second)?;
1097 ensure_leap_table_covers_calendar(year, month, day, hour, minute, second, leap_seconds)?;
1098 if second >= 60.0
1099 && !is_positive_leap_second_label_with_table_checked(
1100 year,
1101 month,
1102 day,
1103 hour,
1104 minute,
1105 leap_seconds,
1106 )?
1107 {
1108 return Err(CoverageError::InvalidInput {
1109 field: "civil datetime",
1110 kind: TimeScaleInputErrorKind::InvalidCivilTime,
1111 });
1112 }
1113 Ok(())
1114}
1115
1116fn validate_utc_civil_input(
1117 year: i32,
1118 month: i32,
1119 day: i32,
1120 hour: i32,
1121 minute: i32,
1122 second: f64,
1123) -> Result<(), CoverageError> {
1124 validate::finite(second, "second").map_err(map_time_scale_field_error)?;
1125 validate::civil_datetime_with_second_policy(
1126 i64::from(year),
1127 i64::from(month),
1128 i64::from(day),
1129 i64::from(hour),
1130 i64::from(minute),
1131 second,
1132 validate::CivilSecondPolicy::UtcLike,
1133 )
1134 .map_err(map_time_scale_field_error)?;
1135 Ok(())
1136}
1137
1138fn ensure_leap_table_covers_calendar(
1139 year: i32,
1140 month: i32,
1141 day: i32,
1142 hour: i32,
1143 minute: i32,
1144 second: f64,
1145 leap_seconds: &[LeapSecondEntry],
1146) -> Result<(), CoverageError> {
1147 let jd1 = julian_day_number(year, month, day) as f64 - 0.5;
1148 let lookup_second = if second >= 60.0 { 59.0 } else { second };
1149 let jd2 = (hour as f64 * SECONDS_PER_HOUR + minute as f64 * SECONDS_PER_MINUTE + lookup_second)
1150 / SECONDS_PER_DAY;
1151 find_leap_seconds_in_table_checked(jd1 + jd2, leap_seconds).map(|_| ())
1152}
1153
1154fn validate_time_tables(
1155 leap_seconds: &[LeapSecondEntry],
1156 ut1_utc: &[Ut1Entry],
1157) -> Result<(), CoverageError> {
1158 validate_leap_seconds_table(leap_seconds)?;
1159 validate_ut1_table(ut1_utc)?;
1160 if effective_ut1_utc_rows(ut1_utc, leap_seconds).len() < 2 {
1161 return Err(table_error("ut1_utc", TimeScaleInputErrorKind::Missing));
1162 }
1163 Ok(())
1164}
1165
1166fn validate_leap_seconds_table(leap_seconds: &[LeapSecondEntry]) -> Result<(), CoverageError> {
1167 if leap_seconds.is_empty() {
1168 return Err(table_error(
1169 "leap_seconds",
1170 TimeScaleInputErrorKind::Missing,
1171 ));
1172 }
1173 let mut previous_mjd = leap_seconds[0].mjd;
1174 if !leap_seconds[0].tai_utc.is_finite() {
1175 return Err(table_error(
1176 "leap_seconds",
1177 TimeScaleInputErrorKind::NonFinite,
1178 ));
1179 }
1180 for entry in &leap_seconds[1..] {
1181 if entry.mjd <= previous_mjd {
1182 return Err(table_error(
1183 "leap_seconds",
1184 TimeScaleInputErrorKind::OutOfRange,
1185 ));
1186 }
1187 if !entry.tai_utc.is_finite() {
1188 return Err(table_error(
1189 "leap_seconds",
1190 TimeScaleInputErrorKind::NonFinite,
1191 ));
1192 }
1193 previous_mjd = entry.mjd;
1194 }
1195 Ok(())
1196}
1197
1198fn validate_ut1_table(ut1_utc: &[Ut1Entry]) -> Result<(), CoverageError> {
1199 if ut1_utc.len() < 2 {
1200 return Err(table_error("ut1_utc", TimeScaleInputErrorKind::Missing));
1201 }
1202 let mut previous_mjd = ut1_utc[0].mjd;
1203 if !ut1_utc[0].ut1_utc.is_finite() {
1204 return Err(table_error("ut1_utc", TimeScaleInputErrorKind::NonFinite));
1205 }
1206 for entry in &ut1_utc[1..] {
1207 if entry.mjd <= previous_mjd {
1208 return Err(table_error("ut1_utc", TimeScaleInputErrorKind::OutOfRange));
1209 }
1210 if !entry.ut1_utc.is_finite() {
1211 return Err(table_error("ut1_utc", TimeScaleInputErrorKind::NonFinite));
1212 }
1213 previous_mjd = entry.mjd;
1214 }
1215 Ok(())
1216}
1217
1218fn effective_ut1_utc_rows<'a>(
1219 ut1_utc: &'a [Ut1Entry],
1220 leap_seconds: &[LeapSecondEntry],
1221) -> &'a [Ut1Entry] {
1222 debug_assert!(!leap_seconds.is_empty());
1223 let first_covered_mjd = leap_seconds[0].mjd;
1224 let first = ut1_utc
1225 .iter()
1226 .position(|entry| entry.mjd >= first_covered_mjd)
1227 .unwrap_or(ut1_utc.len());
1228 &ut1_utc[first..]
1229}
1230
1231fn table_error(field: &'static str, kind: TimeScaleInputErrorKind) -> CoverageError {
1232 CoverageError::InvalidInput { field, kind }
1233}
1234
1235fn validate_scale_input_embedded(scale: TimeScale, cal: ScaleCal) -> Result<(), CoverageError> {
1236 if scale == TimeScale::Utc {
1237 return validate_utc_input_embedded(
1238 cal.year, cal.month, cal.day, cal.hour, cal.minute, cal.second,
1239 );
1240 }
1241 validate_continuous_scale_input(cal)
1242}
1243
1244fn validate_scale_input_with_table(
1245 scale: TimeScale,
1246 cal: ScaleCal,
1247 leap_seconds: &[LeapSecondEntry],
1248) -> Result<(), CoverageError> {
1249 if scale == TimeScale::Utc {
1250 return validate_utc_input_with_table(
1251 cal.year,
1252 cal.month,
1253 cal.day,
1254 cal.hour,
1255 cal.minute,
1256 cal.second,
1257 leap_seconds,
1258 );
1259 }
1260 validate_continuous_scale_input(cal)?;
1261 if is_utc_based(scale) {
1262 let utc = scale_calendar_to_utc_with_table(scale, cal, leap_seconds)?;
1263 ensure_leap_table_covers_calendar(
1264 utc.year,
1265 utc.month,
1266 utc.day,
1267 utc.hour,
1268 utc.minute,
1269 utc.second,
1270 leap_seconds,
1271 )?;
1272 }
1273 Ok(())
1274}
1275
1276fn validate_continuous_scale_input(cal: ScaleCal) -> Result<(), CoverageError> {
1277 validate::finite(cal.second, "second").map_err(map_time_scale_field_error)?;
1278 validate::civil_datetime_with_second_policy(
1279 i64::from(cal.year),
1280 i64::from(cal.month),
1281 i64::from(cal.day),
1282 i64::from(cal.hour),
1283 i64::from(cal.minute),
1284 cal.second,
1285 validate::CivilSecondPolicy::Continuous,
1286 )
1287 .map_err(map_time_scale_field_error)
1288 .map(|_| ())
1289}
1290
1291#[must_use]
1293pub fn tt_to_tcg_jd(jd_tt: f64) -> f64 {
1294 TCG_TCB_REFERENCE_JD + (jd_tt - TCG_TCB_REFERENCE_JD) / (1.0 - TT_TCG_RATE_L_G)
1295}
1296
1297#[must_use]
1299pub fn tcg_to_tt_jd(jd_tcg: f64) -> f64 {
1300 TCG_TCB_REFERENCE_JD + (jd_tcg - TCG_TCB_REFERENCE_JD) * (1.0 - TT_TCG_RATE_L_G)
1301}
1302
1303#[must_use]
1305pub fn tdb_to_tcb_jd(jd_tdb: f64) -> f64 {
1306 TCG_TCB_REFERENCE_JD
1307 + (((jd_tdb - TCG_TCB_REFERENCE_JD) * SECONDS_PER_DAY - TDB_TCB_OFFSET_TDB0_S)
1308 / (1.0 - TDB_TCB_RATE_L_B))
1309 / SECONDS_PER_DAY
1310}
1311
1312#[must_use]
1314pub fn tcb_to_tdb_jd(jd_tcb: f64) -> f64 {
1315 TCG_TCB_REFERENCE_JD
1316 + (((jd_tcb - TCG_TCB_REFERENCE_JD) * SECONDS_PER_DAY) * (1.0 - TDB_TCB_RATE_L_B)
1317 + TDB_TCB_OFFSET_TDB0_S)
1318 / SECONDS_PER_DAY
1319}
1320
1321fn tcg_fraction_from_tt_split(jd_whole: f64, tt_fraction: f64) -> f64 {
1322 let elapsed_tt_days = (jd_whole - TCG_TCB_REFERENCE_JD) + tt_fraction;
1323 tt_fraction + elapsed_tt_days * TT_TCG_RATE_L_G / (1.0 - TT_TCG_RATE_L_G)
1324}
1325
1326fn tcb_fraction_from_tdb_split(jd_whole: f64, tdb_fraction: f64) -> f64 {
1327 let elapsed_tdb_days = (jd_whole - TCG_TCB_REFERENCE_JD) + tdb_fraction;
1328 tdb_fraction
1329 + (elapsed_tdb_days * TDB_TCB_RATE_L_B - TDB_TCB_OFFSET_TDB0_S / SECONDS_PER_DAY)
1330 / (1.0 - TDB_TCB_RATE_L_B)
1331}
1332
1333pub fn julian_day_number(year: i32, month: i32, day: i32) -> i64 {
1335 let year = i64::from(year);
1336 let month = i64::from(month);
1337 let day = i64::from(day);
1338 let janfeb = month <= 2;
1339 let g = year + 4716 - if janfeb { 1 } else { 0 };
1340 let f = (month + 9) % 12;
1341 let e = 1461 * g / 4 + day - 1402;
1342 let j = e + (153 * f + 2) / 5;
1343 j + 38 - ((g + 184) / 100) * 3 / 4
1344}
1345
1346pub fn find_leap_seconds(jd_utc: f64) -> f64 {
1372 if !jd_utc.is_finite() {
1373 return f64::NAN;
1374 }
1375 let mjd = (jd_utc - 2400000.5) as i32;
1376 if mjd >= LEAP_SECONDS[0].mjd {
1377 let mut ls = 10.0;
1379 for entry in LEAP_SECONDS {
1380 if mjd >= entry.mjd {
1381 ls = entry.tai_utc;
1382 } else {
1383 break;
1384 }
1385 }
1386 return ls;
1387 }
1388 rubber_tai_minus_utc(jd_utc)
1389}
1390
1391fn find_leap_seconds_in_table(jd_utc: f64, leap_seconds: &[LeapSecondEntry]) -> f64 {
1392 debug_assert!(!leap_seconds.is_empty());
1393 if !jd_utc.is_finite() {
1394 return f64::NAN;
1395 }
1396 let mjd = (jd_utc - 2400000.5) as i32;
1397 if mjd >= leap_seconds[0].mjd {
1398 let mut ls = leap_seconds[0].tai_utc;
1399 for entry in leap_seconds {
1400 if mjd >= entry.mjd {
1401 ls = entry.tai_utc;
1402 } else {
1403 break;
1404 }
1405 }
1406 return ls;
1407 }
1408 rubber_tai_minus_utc(jd_utc)
1409}
1410
1411fn find_leap_seconds_in_table_checked(
1412 jd_utc: f64,
1413 leap_seconds: &[LeapSecondEntry],
1414) -> Result<f64, CoverageError> {
1415 debug_assert!(!leap_seconds.is_empty());
1416 if !jd_utc.is_finite() {
1417 return Err(table_error(
1418 "leap_seconds",
1419 TimeScaleInputErrorKind::NonFinite,
1420 ));
1421 }
1422 let mjd = (jd_utc - 2400000.5) as i32;
1423 if mjd < leap_seconds[0].mjd {
1424 return Err(table_error(
1425 "leap_seconds",
1426 TimeScaleInputErrorKind::OutOfRange,
1427 ));
1428 }
1429
1430 let mut ls = leap_seconds[0].tai_utc;
1431 for entry in leap_seconds {
1432 if mjd >= entry.mjd {
1433 ls = entry.tai_utc;
1434 } else {
1435 break;
1436 }
1437 }
1438 Ok(ls)
1439}
1440
1441pub fn tai_utc_offset_s(jd_utc: f64) -> f64 {
1455 find_leap_seconds(jd_utc)
1456}
1457
1458pub fn gps_utc_offset_s(jd_utc: f64) -> f64 {
1472 find_leap_seconds(jd_utc) - GPST_MINUS_TAI_S
1473}
1474
1475fn rubber_tai_minus_utc(jd_utc: f64) -> f64 {
1482 let mjd = jd_utc - 2400000.5;
1483 let first = &RUBBER_SECONDS[0];
1484 if !mjd.is_finite() {
1485 return f64::NAN;
1486 }
1487 if mjd < first.start_mjd as f64 {
1489 return first.base;
1490 }
1491 let mut selected = first;
1492 for entry in RUBBER_SECONDS {
1493 if mjd >= entry.start_mjd as f64 {
1494 selected = entry;
1495 } else {
1496 break;
1497 }
1498 }
1499 selected.base + (mjd - selected.ref_mjd) * selected.rate
1500}
1501
1502#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
1504pub enum TimeOffsetError {
1505 #[error(
1509 "time-scale {0} is UTC-based; its offset is epoch-dependent, use timescale_offset_at_s"
1510 )]
1511 EpochRequired(&'static str),
1512 #[error("time-scale {0} has no fixed/constant offset; resolve it through TimeScales")]
1515 Unsupported(&'static str),
1516 #[error("utc_jd must be finite to resolve leap seconds for scale {0}")]
1518 NonFiniteEpoch(&'static str),
1519}
1520
1521#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1529#[repr(u8)]
1530pub enum TimeOffsetErrorCode {
1531 EpochRequired = 1,
1533 Unsupported = 2,
1535 NonFiniteEpoch = 3,
1537}
1538
1539impl TimeOffsetError {
1540 #[must_use]
1546 pub fn code(&self) -> TimeOffsetErrorCode {
1547 match self {
1548 Self::EpochRequired(_) => TimeOffsetErrorCode::EpochRequired,
1549 Self::Unsupported(_) => TimeOffsetErrorCode::Unsupported,
1550 Self::NonFiniteEpoch(_) => TimeOffsetErrorCode::NonFiniteEpoch,
1551 }
1552 }
1553}
1554
1555fn is_utc_based(scale: TimeScale) -> bool {
1557 matches!(scale, TimeScale::Utc | TimeScale::Glonasst)
1558}
1559
1560fn scale_minus_tai_s(scale: TimeScale, utc_jd: f64) -> Result<f64, TimeOffsetError> {
1567 let leap = |s: TimeScale| -> Result<f64, TimeOffsetError> {
1568 if !utc_jd.is_finite() {
1569 return Err(TimeOffsetError::NonFiniteEpoch(s.abbrev()));
1570 }
1571 Ok(find_leap_seconds(utc_jd))
1572 };
1573 Ok(match scale {
1574 TimeScale::Tai => 0.0,
1575 TimeScale::Tt => TT_MINUS_TAI_S,
1577 TimeScale::Gpst => -GPST_MINUS_TAI_S,
1579 TimeScale::Gst => -GPST_MINUS_TAI_S,
1583 TimeScale::Qzsst => -GPST_MINUS_TAI_S,
1586 TimeScale::Bdt => -BDT_MINUS_TAI_S,
1589 TimeScale::Utc => -leap(scale)?,
1591 TimeScale::Glonasst => -leap(scale)? + GLONASST_MINUS_UTC_S,
1593 TimeScale::Tcg | TimeScale::Tdb | TimeScale::Tcb => {
1594 return Err(TimeOffsetError::Unsupported(scale.abbrev()));
1595 }
1596 })
1597}
1598
1599pub fn timescale_offset_s(from: TimeScale, to: TimeScale) -> Result<f64, TimeOffsetError> {
1620 for scale in [from, to] {
1621 if is_utc_based(scale) {
1622 return Err(TimeOffsetError::EpochRequired(scale.abbrev()));
1623 }
1624 }
1625 timescale_offset_at_s(from, to, f64::NAN)
1627}
1628
1629pub fn timescale_offset_at_s(
1641 from: TimeScale,
1642 to: TimeScale,
1643 utc_jd: f64,
1644) -> Result<f64, TimeOffsetError> {
1645 Ok(scale_minus_tai_s(to, utc_jd)? - scale_minus_tai_s(from, utc_jd)?)
1646}
1647
1648pub fn leap_second_table() -> LeapSecondTable {
1653 leap_second_table_for(
1654 LEAP_SECONDS,
1655 "IERS Bulletin C (TAI-UTC), bundled in sidereon-core",
1656 )
1657}
1658
1659fn leap_second_table_for(
1660 leap_seconds: &[LeapSecondEntry],
1661 source: &'static str,
1662) -> LeapSecondTable {
1663 LeapSecondTable {
1664 source,
1665 first_mjd: leap_seconds.first().map(|e| e.mjd).unwrap_or(0),
1666 last_mjd: leap_seconds.last().map(|e| e.mjd).unwrap_or(0),
1667 entries: leap_seconds.len(),
1668 }
1669}
1670
1671fn interpolate_delta_t(jd_tt: f64) -> f64 {
1672 use std::sync::LazyLock;
1674
1675 struct DeltaTRow {
1676 jd_tt: f64,
1677 delta_t: f64,
1678 }
1679
1680 static TABLE: LazyLock<Vec<DeltaTRow>> = LazyLock::new(|| {
1681 UT1_DATA
1682 .iter()
1683 .map(|entry| {
1684 let jd_utc = entry.mjd as f64 + 2400000.5;
1685 let leap_seconds = find_leap_seconds(jd_utc);
1686 let tt_minus_utc = leap_seconds + TT_MINUS_TAI_S;
1687 let delta_t = ((tt_minus_utc - entry.ut1_utc) * ROUND_1E7).round() / ROUND_1E7;
1688 DeltaTRow {
1689 jd_tt: jd_utc + tt_minus_utc / SECONDS_PER_DAY,
1690 delta_t,
1691 }
1692 })
1693 .collect()
1694 });
1695
1696 match TABLE.binary_search_by(|row| row.jd_tt.partial_cmp(&jd_tt).unwrap()) {
1698 Ok(i) => TABLE[i].delta_t,
1699 Err(0) => TABLE[0].delta_t,
1700 Err(i) if i >= TABLE.len() => TABLE.last().unwrap().delta_t,
1701 Err(i) => {
1702 let p1 = &TABLE[i - 1];
1703 let p2 = &TABLE[i];
1704 p1.delta_t + (jd_tt - p1.jd_tt) * (p2.delta_t - p1.delta_t) / (p2.jd_tt - p1.jd_tt)
1705 }
1706 }
1707}
1708
1709fn interpolate_delta_t_with_table(
1710 jd_tt: f64,
1711 ut1_utc: &[Ut1Entry],
1712 leap_seconds: &[LeapSecondEntry],
1713) -> Result<f64, CoverageError> {
1714 struct DeltaTRow {
1715 jd_tt: f64,
1716 delta_t: f64,
1717 }
1718
1719 let table: Vec<DeltaTRow> = effective_ut1_utc_rows(ut1_utc, leap_seconds)
1720 .iter()
1721 .map(|entry| {
1722 let jd_utc = entry.mjd as f64 + 2400000.5;
1723 let leap_seconds = find_leap_seconds_in_table_checked(jd_utc, leap_seconds)?;
1724 let tt_minus_utc = leap_seconds + TT_MINUS_TAI_S;
1725 let delta_t = ((tt_minus_utc - entry.ut1_utc) * ROUND_1E7).round() / ROUND_1E7;
1726 Ok(DeltaTRow {
1727 jd_tt: jd_utc + tt_minus_utc / SECONDS_PER_DAY,
1728 delta_t,
1729 })
1730 })
1731 .collect::<Result<_, CoverageError>>()?;
1732
1733 let delta_t = match table.binary_search_by(|row| row.jd_tt.partial_cmp(&jd_tt).unwrap()) {
1734 Ok(i) => table[i].delta_t,
1735 Err(0) => table[0].delta_t,
1736 Err(i) if i >= table.len() => table.last().unwrap().delta_t,
1737 Err(i) => {
1738 let p1 = &table[i - 1];
1739 let p2 = &table[i];
1740 p1.delta_t + (jd_tt - p1.jd_tt) * (p2.delta_t - p1.delta_t) / (p2.jd_tt - p1.jd_tt)
1741 }
1742 };
1743 Ok(delta_t)
1744}
1745
1746pub fn ut1_coverage() -> Ut1Provenance {
1753 ut1_coverage_for(
1754 &UT1_DATA,
1755 LEAP_SECONDS,
1756 "IERS Earth Orientation Parameters (UT1-UTC), bundled",
1757 )
1758}
1759
1760fn ut1_coverage_for(
1761 ut1_utc: &[Ut1Entry],
1762 leap_seconds: &[LeapSecondEntry],
1763 source: &'static str,
1764) -> Ut1Provenance {
1765 let ut1_utc = effective_ut1_utc_rows(ut1_utc, leap_seconds);
1766 let first = ut1_utc.first();
1767 let last = ut1_utc.last();
1768 let to_jd_tt = |mjd: i32| -> f64 {
1769 let jd_utc = mjd as f64 + 2400000.5;
1770 let tt_minus_utc = find_leap_seconds_in_table_checked(jd_utc, leap_seconds)
1771 .expect("effective UT1 rows are covered by leap table")
1772 + TT_MINUS_TAI_S;
1773 jd_utc + tt_minus_utc / SECONDS_PER_DAY
1774 };
1775 Ut1Provenance {
1776 source,
1777 first_mjd: first.map(|e| e.mjd).unwrap_or(0),
1778 last_mjd: last.map(|e| e.mjd).unwrap_or(0),
1779 first_jd_tt: first.map(|e| to_jd_tt(e.mjd)).unwrap_or(0.0),
1780 last_jd_tt: last.map(|e| to_jd_tt(e.mjd)).unwrap_or(0.0),
1781 entries: ut1_utc.len(),
1782 }
1783}
1784
1785#[cfg(test)]
1786mod tests {
1787 use super::*;
1788
1789 #[test]
1790 fn julian_day_number_widens_extreme_inputs_before_arithmetic() {
1791 let _ = julian_day_number(i32::MIN, i32::MAX, i32::MAX);
1792 let _ = julian_day_number(i32::MAX, i32::MIN, i32::MIN);
1793 }
1794
1795 fn utc_jd(year: i32, month: i32, day: i32, hour: i32, minute: i32, second: f64) -> f64 {
1799 let jd1 = julian_day_number(year, month, day) as f64 - 0.5;
1800 let sod = hour as f64 * SECONDS_PER_HOUR + minute as f64 * SECONDS_PER_MINUTE + second;
1801 jd1 + sod / SECONDS_PER_DAY
1802 }
1803
1804 fn positive_ulp_distance(a: f64, b: f64) -> u64 {
1805 debug_assert!(a.is_sign_positive());
1806 debug_assert!(b.is_sign_positive());
1807 a.to_bits().abs_diff(b.to_bits())
1808 }
1809
1810 #[test]
1813 fn iau_tcg_tcb_constants_are_exact_f64_bits() {
1814 assert_eq!(TT_TCG_RATE_L_G.to_bits(), 0x3e07_f240_9f5d_dc8f);
1815 assert_eq!(TDB_TCB_RATE_L_B.to_bits(), 0x3e50_a609_49f9_cf0c);
1816 assert_eq!(TDB_TCB_OFFSET_TDB0_S.to_bits(), 0xbf11_2ba1_6e7a_311f);
1817 assert_eq!(TCG_TCB_REFERENCE_JD.to_bits(), 0x4142_a3c4_400c_34c2);
1818 assert_eq!(TT_TCG_RATE_L_G, 6.969290134e-10);
1819 assert_eq!(TDB_TCB_RATE_L_B, 1.550519768e-8);
1820 assert_eq!(TDB_TCB_OFFSET_TDB0_S, -6.55e-5);
1821 assert_eq!(TCG_TCB_REFERENCE_JD, 2_443_144.500_372_5);
1822 }
1823
1824 #[test]
1825 fn tcg_tcb_linear_conversions_round_trip_to_one_ulp() {
1826 let cases = [
1827 TCG_TCB_REFERENCE_JD,
1828 J2000_JD,
1829 2_460_000.5,
1830 2_443_145.5,
1831 2_443_144.500_372_5 + 1_000.0,
1832 2_460_123.456_789,
1833 2_400_014.327_160_368,
1834 2_400_015.561_728_258,
1835 ];
1836 for jd in cases {
1837 let tcg = tt_to_tcg_jd(jd);
1838 let tt_back = tcg_to_tt_jd(tcg);
1839 assert!(
1840 positive_ulp_distance(tt_back, jd) <= 1,
1841 "TT->TCG->TT round trip at JD {jd}: got {tt_back}"
1842 );
1843
1844 let tcb = tdb_to_tcb_jd(jd);
1845 let tdb_back = tcb_to_tdb_jd(tcb);
1846 assert!(
1847 positive_ulp_distance(tdb_back, jd) <= 1,
1848 "TDB->TCB->TDB round trip at JD {jd}: got {tdb_back}"
1849 );
1850 }
1851 }
1852
1853 #[test]
1854 fn tcg_reference_epoch_is_synchronized_and_tcb_carries_tdb0_before_jd_rounding() {
1855 assert_eq!(
1856 tt_to_tcg_jd(TCG_TCB_REFERENCE_JD).to_bits(),
1857 TCG_TCB_REFERENCE_JD.to_bits()
1858 );
1859 assert_eq!(
1860 tcg_to_tt_jd(TCG_TCB_REFERENCE_JD).to_bits(),
1861 TCG_TCB_REFERENCE_JD.to_bits()
1862 );
1863
1864 let tdb_offset_at_reference_s = (TCG_TCB_REFERENCE_JD - TCG_TCB_REFERENCE_JD)
1865 * SECONDS_PER_DAY
1866 * (1.0 - TDB_TCB_RATE_L_B)
1867 + TDB_TCB_OFFSET_TDB0_S;
1868 assert_eq!(
1869 tdb_offset_at_reference_s.to_bits(),
1870 TDB_TCB_OFFSET_TDB0_S.to_bits()
1871 );
1872
1873 let tdb_at_tcb_reference = tcb_to_tdb_jd(TCG_TCB_REFERENCE_JD);
1874 let rounded_full_jd = TCG_TCB_REFERENCE_JD + TDB_TCB_OFFSET_TDB0_S / SECONDS_PER_DAY;
1875 assert_eq!(tdb_at_tcb_reference.to_bits(), rounded_full_jd.to_bits());
1876 assert_eq!(tdb_at_tcb_reference.to_bits(), 0x4142_a3c4_400c_34c0);
1877 assert_eq!(
1878 tdb_to_tcb_jd(tdb_at_tcb_reference).to_bits(),
1879 TCG_TCB_REFERENCE_JD.to_bits()
1880 );
1881 }
1882
1883 #[test]
1884 fn tai_defining_epoch_maps_tt_and_tcg_to_reference_jd() {
1885 let scales = TimeScales::from_scale(TimeScale::Tai, 1977, 1, 1, 0, 0, 0.0)
1886 .expect("valid TAI reference instant");
1887 assert_eq!(scales.jd_tt.to_bits(), TCG_TCB_REFERENCE_JD.to_bits());
1888 assert_eq!(scales.jd_tcg().to_bits(), TCG_TCB_REFERENCE_JD.to_bits());
1889 }
1890
1891 #[test]
1892 fn tcb_calendar_reference_input_resolves_to_tt_reference_jd() {
1893 let scales = TimeScales::from_scale(TimeScale::Tcb, 1977, 1, 1, 0, 0, 32.184)
1894 .expect("valid TCB reference instant");
1895 assert_eq!(scales.jd_tt.to_bits(), TCG_TCB_REFERENCE_JD.to_bits());
1896 assert_eq!(scales.jd_tcg().to_bits(), TCG_TCB_REFERENCE_JD.to_bits());
1897 }
1898
1899 #[test]
1900 fn tdb_calendar_input_uses_periodic_tdb_tt_inverse() {
1901 let tdb_jd = J2000_JD;
1902 let tt_jd = tdb_to_tt_jd_for_tdb_input(tdb_jd);
1903 let reconstructed_tdb = tt_jd + tdb_minus_tt_seconds_at_tt_jd(tt_jd) / SECONDS_PER_DAY;
1904 assert_eq!(reconstructed_tdb.to_bits(), tdb_jd.to_bits());
1905
1906 let scales = TimeScales::from_scale(TimeScale::Tdb, 2000, 1, 1, 12, 0, 0.0)
1907 .expect("valid TDB input");
1908 assert_eq!(scales.jd_tdb.to_bits(), tdb_jd.to_bits());
1909 }
1910
1911 #[test]
1912 fn tcg_tcb_fractions_use_split_affine_relations() {
1913 let scales = TimeScales::from_utc(2000, 1, 1, 12, 0, 0.0).expect("valid UTC instant");
1914
1915 assert_eq!(scales.tcg_fraction().to_bits(), 0x3f48_88c2_8751_43f2);
1916 assert_ne!(
1917 scales.tcg_fraction().to_bits(),
1918 (scales.jd_tcg() - scales.jd_whole).to_bits()
1919 );
1920
1921 assert_eq!(scales.tcb_fraction().to_bits(), 0x3f4c_9c46_0494_33ba);
1922 assert_ne!(
1923 scales.tcb_fraction().to_bits(),
1924 (scales.jd_tcb() - scales.jd_whole).to_bits()
1925 );
1926 }
1927
1928 #[test]
1929 fn from_scale_rejects_continuous_scale_leap_second_labels_before_normalizing() {
1930 let expected = Err(CoverageError::InvalidInput {
1931 field: "civil datetime",
1932 kind: TimeScaleInputErrorKind::InvalidCivilTime,
1933 });
1934 for scale in [
1935 TimeScale::Tai,
1936 TimeScale::Tt,
1937 TimeScale::Tcg,
1938 TimeScale::Tdb,
1939 TimeScale::Tcb,
1940 TimeScale::Gpst,
1941 TimeScale::Gst,
1942 TimeScale::Bdt,
1943 TimeScale::Qzsst,
1944 ] {
1945 assert_eq!(
1946 TimeScales::from_scale(scale, 2017, 1, 1, 0, 0, 60.0),
1947 expected
1948 );
1949 }
1950 assert!(TimeScales::from_scale(TimeScale::Utc, 2016, 12, 31, 23, 59, 60.0).is_ok());
1951
1952 let tables = TimeTables::embedded();
1953 assert_eq!(
1954 TimeScales::from_scale_with_tables(TimeScale::Tcb, 2017, 1, 1, 0, 0, 60.0, tables),
1955 expected
1956 );
1957 }
1958
1959 #[test]
1960 fn embedded_tables_path_is_bit_identical() {
1961 let tables = TimeTables::embedded();
1962 for (year, month, day, hour, minute, second) in [
1963 (1973, 1, 2, 0, 0, 0.0),
1964 (2000, 1, 1, 12, 0, 0.0),
1965 (2016, 12, 31, 23, 59, 60.0),
1966 (2026, 6, 1, 0, 0, 0.0),
1967 ] {
1968 let embedded = TimeScales::from_utc(year, month, day, hour, minute, second)
1969 .expect("embedded UTC conversion");
1970 let via_tables =
1971 TimeScales::from_utc_with_tables(year, month, day, hour, minute, second, tables)
1972 .expect("table UTC conversion");
1973 assert_eq!(via_tables, embedded);
1974 }
1975 }
1976
1977 #[test]
1978 fn caller_leap_table_future_step_shifts_tt_by_exactly_one_second() {
1979 let mut leap_seconds = LEAP_SECONDS.to_vec();
1980 let last = leap_seconds.last().expect("embedded leap table");
1981 leap_seconds.push(LeapSecondEntry {
1982 mjd: 61041,
1983 tai_utc: last.tai_utc + 1.0,
1984 });
1985 let tables = TimeTables::new(&leap_seconds, &UT1_DATA).expect("valid override tables");
1986
1987 let embedded =
1988 TimeScales::from_utc(2026, 1, 2, 0, 0, 0.0).expect("embedded UTC conversion");
1989 let override_scales = TimeScales::from_utc_with_tables(2026, 1, 2, 0, 0, 0.0, tables)
1990 .expect("override UTC conversion");
1991 assert_eq!(
1992 override_scales.jd_whole.to_bits(),
1993 embedded.jd_whole.to_bits()
1994 );
1995 assert_eq!(
1996 override_scales.tt_fraction.to_bits(),
1997 (embedded.tt_fraction + 1.0 / SECONDS_PER_DAY).to_bits()
1998 );
1999
2000 let before_embedded =
2001 TimeScales::from_utc(2025, 12, 1, 0, 0, 0.0).expect("embedded UTC conversion");
2002 let before_override = TimeScales::from_utc_with_tables(2025, 12, 1, 0, 0, 0.0, tables)
2003 .expect("override UTC conversion");
2004 assert_eq!(before_override, before_embedded);
2005 }
2006
2007 #[test]
2008 fn caller_leap_table_must_cover_queried_epoch() {
2009 let leap_seconds = [LeapSecondEntry {
2010 mjd: 61041,
2011 tai_utc: 38.0,
2012 }];
2013 let tables = TimeTables::new(&leap_seconds, &UT1_DATA).expect("valid future tables");
2014
2015 let err = TimeScales::from_utc_with_tables(2025, 12, 31, 0, 0, 0.0, tables)
2016 .expect_err("caller leap table must cover the query epoch");
2017 assert_eq!(
2018 err,
2019 CoverageError::InvalidInput {
2020 field: "leap_seconds",
2021 kind: TimeScaleInputErrorKind::OutOfRange
2022 }
2023 );
2024
2025 let err = TimeScales::from_utc_validated_with_tables(
2026 2025,
2027 12,
2028 31,
2029 0,
2030 0,
2031 0.0,
2032 ValidityMode::Permissive,
2033 tables,
2034 )
2035 .expect_err("permissive mode still requires leap table coverage");
2036 assert_eq!(
2037 err,
2038 CoverageError::InvalidInput {
2039 field: "leap_seconds",
2040 kind: TimeScaleInputErrorKind::OutOfRange
2041 }
2042 );
2043 }
2044
2045 #[test]
2046 fn caller_ut1_table_uses_validated_coverage_modes() {
2047 let ut1_utc = [
2048 Ut1Entry {
2049 mjd: 61041,
2050 ut1_utc: 0.0,
2051 },
2052 Ut1Entry {
2053 mjd: 61042,
2054 ut1_utc: 0.0,
2055 },
2056 ];
2057 let tables = TimeTables::new(LEAP_SECONDS, &ut1_utc).expect("valid short UT1 table");
2058
2059 let strict = TimeScales::from_utc_with_tables(2025, 12, 31, 0, 0, 0.0, tables)
2060 .expect_err("strict caller-table path must reject before UT1 coverage");
2061 assert_eq!(
2062 strict,
2063 CoverageError::OutsideCoverage(crate::astro::time::eop::DegradeReason::BeforeCoverage)
2064 );
2065
2066 let permissive = TimeScales::from_utc_validated_with_tables(
2067 2025,
2068 12,
2069 31,
2070 0,
2071 0,
2072 0.0,
2073 ValidityMode::Permissive,
2074 tables,
2075 )
2076 .expect("permissive caller-table path returns degraded value");
2077 assert_eq!(
2078 permissive.degraded,
2079 Some(crate::astro::time::eop::DegradeReason::BeforeCoverage)
2080 );
2081
2082 let strict_after = TimeScales::from_utc_validated_with_tables(
2083 2026,
2084 1,
2085 3,
2086 0,
2087 0,
2088 0.0,
2089 ValidityMode::Strict,
2090 tables,
2091 )
2092 .expect_err("strict caller-table path must reject after UT1 coverage");
2093 assert_eq!(
2094 strict_after,
2095 CoverageError::OutsideCoverage(crate::astro::time::eop::DegradeReason::AfterCoverage)
2096 );
2097 }
2098
2099 #[test]
2100 fn caller_tables_reject_short_or_malformed_inputs() {
2101 assert_eq!(
2102 TimeTables::new(&[], &UT1_DATA).expect_err("empty leap table"),
2103 CoverageError::InvalidInput {
2104 field: "leap_seconds",
2105 kind: TimeScaleInputErrorKind::Missing
2106 }
2107 );
2108 let unsorted = [
2109 LeapSecondEntry {
2110 mjd: 41317,
2111 tai_utc: 10.0,
2112 },
2113 LeapSecondEntry {
2114 mjd: 41317,
2115 tai_utc: 11.0,
2116 },
2117 ];
2118 assert_eq!(
2119 TimeTables::new(&unsorted, &UT1_DATA).expect_err("unsorted leap table"),
2120 CoverageError::InvalidInput {
2121 field: "leap_seconds",
2122 kind: TimeScaleInputErrorKind::OutOfRange
2123 }
2124 );
2125 assert_eq!(
2126 TimeTables::new(LEAP_SECONDS, &[]).expect_err("short UT1 table"),
2127 CoverageError::InvalidInput {
2128 field: "ut1_utc",
2129 kind: TimeScaleInputErrorKind::Missing
2130 }
2131 );
2132 }
2133
2134 #[test]
2137 fn tai_minus_utc_pre_1972_matches_published_table() {
2138 let cases = [
2141 (1961, 1, 1, 1.4228180), (1965, 1, 1, 3.5401300), (1968, 2, 1, 6.1856820), (1971, 1, 1, 8.9461620), ];
2147 for (y, m, d, want) in cases {
2148 let jd = utc_jd(y, m, d, 0, 0, 0.0);
2149 let got = find_leap_seconds(jd);
2150 assert!(
2151 (got - want).abs() < 1.0e-7,
2152 "TAI-UTC at {y}-{m:02}-{d:02}: got {got}, want {want}"
2153 );
2154 }
2155 }
2156
2157 #[test]
2158 fn tai_minus_utc_pre_1972_is_continuous_within_a_segment() {
2159 let midnight = find_leap_seconds(utc_jd(1969, 6, 1, 0, 0, 0.0));
2162 let noon = find_leap_seconds(utc_jd(1969, 6, 1, 12, 0, 0.0));
2163 assert!(
2164 (noon - midnight - 0.5 * 0.002592).abs() < 1.0e-9,
2165 "rubber-second drift over half a day must equal 0.5*rate"
2166 );
2167 }
2168
2169 #[test]
2170 fn tai_minus_utc_steps_to_ten_at_1972_and_post_1972_unchanged() {
2171 let pre = find_leap_seconds(utc_jd(1971, 12, 31, 0, 0, 0.0));
2173 assert!((pre - 9.8896500).abs() < 1.0e-7, "1971-12-31 TAI-UTC");
2174 assert_eq!(find_leap_seconds(utc_jd(1972, 1, 1, 0, 0, 0.0)), 10.0);
2176 assert_eq!(find_leap_seconds(utc_jd(1980, 1, 1, 0, 0, 0.0)), 19.0);
2178 assert_eq!(find_leap_seconds(utc_jd(2017, 1, 1, 0, 0, 0.0)), 37.0);
2179 }
2180
2181 #[test]
2182 fn tai_utc_and_gps_utc_offsets_match_iers_and_is_gps_200() {
2183 let jd_2017 = utc_jd(2017, 1, 1, 0, 0, 0.0);
2186 assert_eq!(tai_utc_offset_s(jd_2017), 37.0);
2187 assert_eq!(gps_utc_offset_s(jd_2017), 18.0);
2188 assert_eq!(tai_utc_offset_s(jd_2017) - gps_utc_offset_s(jd_2017), 19.0);
2189
2190 for (y, m, d) in [(1980, 1, 1), (2000, 1, 1), (2009, 1, 1), (2017, 1, 1)] {
2193 let jd = utc_jd(y, m, d, 0, 0, 0.0);
2194 assert_eq!(
2195 tai_utc_offset_s(jd).to_bits(),
2196 find_leap_seconds(jd).to_bits()
2197 );
2198 assert_eq!(gps_utc_offset_s(jd), find_leap_seconds(jd) - 19.0);
2199 }
2200
2201 assert_eq!(
2204 gps_utc_offset_s(jd_2017),
2205 timescale_offset_at_s(TimeScale::Utc, TimeScale::Gpst, jd_2017)
2206 .expect("leap-aware offset")
2207 );
2208 }
2209
2210 #[test]
2211 fn tai_minus_utc_pre_1961_clamps_to_first_segment_and_nonfinite_is_nan() {
2212 assert_eq!(find_leap_seconds(utc_jd(1958, 1, 1, 0, 0, 0.0)), 1.4228180);
2214 assert!(find_leap_seconds(f64::NAN).is_nan());
2215 assert!(find_leap_seconds(f64::INFINITY).is_nan());
2216 }
2217
2218 #[test]
2225 fn offset_gpst_to_bdt_is_minus_14s() {
2226 let want = f64::from_bits(0xc02c_0000_0000_0000);
2228 assert_eq!(
2229 timescale_offset_s(TimeScale::Gpst, TimeScale::Bdt).expect("fixed offset"),
2230 want
2231 );
2232 assert_eq!(want, -14.0);
2233 }
2234
2235 #[test]
2236 fn offset_bdt_to_gpst_is_plus_14s() {
2237 assert_eq!(
2238 timescale_offset_s(TimeScale::Bdt, TimeScale::Gpst).expect("fixed offset"),
2239 14.0
2240 );
2241 }
2242
2243 #[test]
2244 fn offset_gpst_to_gst_is_nominal_zero() {
2245 assert_eq!(
2248 timescale_offset_s(TimeScale::Gpst, TimeScale::Gst).expect("fixed offset"),
2249 0.0
2250 );
2251 }
2252
2253 #[test]
2254 fn offset_gpst_to_qzsst_is_nominal_zero() {
2255 assert_eq!(
2257 timescale_offset_s(TimeScale::Gpst, TimeScale::Qzsst).expect("fixed offset"),
2258 0.0
2259 );
2260 assert_eq!(
2261 timescale_offset_s(TimeScale::Gst, TimeScale::Qzsst).expect("fixed offset"),
2262 0.0
2263 );
2264 }
2265
2266 #[test]
2267 fn offset_tai_to_tt_is_32_184s() {
2268 let want = f64::from_bits(0x4040_178d_4fdf_3b64);
2270 assert_eq!(
2271 timescale_offset_s(TimeScale::Tai, TimeScale::Tt).expect("fixed offset"),
2272 want
2273 );
2274 assert_eq!(want, 32.184);
2275 }
2276
2277 #[test]
2278 fn offset_gpst_to_tt_is_51_184s() {
2279 let want = f64::from_bits(0x4049_978d_4fdf_3b64);
2281 assert_eq!(
2282 timescale_offset_s(TimeScale::Gpst, TimeScale::Tt).expect("fixed offset"),
2283 want
2284 );
2285 assert_eq!(want, 51.184);
2286 }
2287
2288 #[test]
2289 fn offset_gpst_to_tai_is_plus_19s() {
2290 assert_eq!(
2292 timescale_offset_s(TimeScale::Gpst, TimeScale::Tai).expect("fixed offset"),
2293 19.0
2294 );
2295 }
2296
2297 #[test]
2298 fn fixed_offsets_are_antisymmetric_for_atomic_pairs() {
2299 let atomic = [
2300 TimeScale::Tai,
2301 TimeScale::Tt,
2302 TimeScale::Gpst,
2303 TimeScale::Gst,
2304 TimeScale::Qzsst,
2305 TimeScale::Bdt,
2306 ];
2307 for &a in &atomic {
2308 for &b in &atomic {
2309 let ab = timescale_offset_s(a, b).expect("fixed offset");
2310 let ba = timescale_offset_s(b, a).expect("fixed offset");
2311 assert_eq!(ab, -ba, "offset({a:?},{b:?}) must be -offset({b:?},{a:?})");
2312 }
2313 }
2314 }
2315
2316 #[test]
2319 fn fixed_offset_requires_epoch_for_utc_based_scales() {
2320 assert_eq!(
2321 timescale_offset_s(TimeScale::Gpst, TimeScale::Utc),
2322 Err(TimeOffsetError::EpochRequired("UTC"))
2323 );
2324 assert_eq!(
2325 timescale_offset_s(TimeScale::Glonasst, TimeScale::Gpst),
2326 Err(TimeOffsetError::EpochRequired("GLONASST"))
2327 );
2328 }
2329
2330 #[test]
2331 fn tdb_has_no_fixed_offset() {
2332 assert_eq!(
2333 timescale_offset_s(TimeScale::Gpst, TimeScale::Tdb),
2334 Err(TimeOffsetError::Unsupported("TDB"))
2335 );
2336 assert_eq!(
2337 timescale_offset_at_s(TimeScale::Tt, TimeScale::Tdb, 2_451_545.0),
2338 Err(TimeOffsetError::Unsupported("TDB"))
2339 );
2340 }
2341
2342 #[test]
2343 fn leap_aware_offset_rejects_non_finite_epoch() {
2344 assert_eq!(
2345 timescale_offset_at_s(TimeScale::Gpst, TimeScale::Utc, f64::NAN),
2346 Err(TimeOffsetError::NonFiniteEpoch("UTC"))
2347 );
2348 assert_eq!(
2349 timescale_offset_at_s(TimeScale::Glonasst, TimeScale::Gpst, f64::INFINITY),
2350 Err(TimeOffsetError::NonFiniteEpoch("GLONASST"))
2351 );
2352 }
2353
2354 #[test]
2355 fn error_code_maps_each_variant_to_stable_discriminant() {
2356 assert_eq!(
2357 TimeOffsetError::EpochRequired("UTC").code(),
2358 TimeOffsetErrorCode::EpochRequired
2359 );
2360 assert_eq!(
2361 TimeOffsetError::Unsupported("TDB").code(),
2362 TimeOffsetErrorCode::Unsupported
2363 );
2364 assert_eq!(
2365 TimeOffsetError::NonFiniteEpoch("UTC").code(),
2366 TimeOffsetErrorCode::NonFiniteEpoch
2367 );
2368 assert_eq!(TimeOffsetErrorCode::EpochRequired as u8, 1);
2371 assert_eq!(TimeOffsetErrorCode::Unsupported as u8, 2);
2372 assert_eq!(TimeOffsetErrorCode::NonFiniteEpoch as u8, 3);
2373 assert_eq!(
2375 TimeOffsetError::EpochRequired("GLONASST").code() as u8,
2376 TimeOffsetError::EpochRequired("UTC").code() as u8
2377 );
2378 }
2379
2380 #[test]
2381 fn leap_aware_offset_ignores_epoch_for_atomic_pairs() {
2382 assert_eq!(
2384 timescale_offset_at_s(TimeScale::Gpst, TimeScale::Bdt, f64::NAN)
2385 .expect("atomic pair ignores epoch"),
2386 -14.0
2387 );
2388 }
2389
2390 #[test]
2399 fn offset_utc_gpst_matches_rtklib_2017() {
2400 let jd = utc_jd(2017, 1, 1, 0, 0, 0.0);
2401 let want = f64::from_bits(0x4032_0000_0000_0000);
2403 assert_eq!(
2404 timescale_offset_at_s(TimeScale::Utc, TimeScale::Gpst, jd).expect("leap-aware offset"),
2405 want
2406 );
2407 assert_eq!(want, 18.0);
2408 assert_eq!(
2410 timescale_offset_at_s(TimeScale::Gpst, TimeScale::Utc, jd).expect("leap-aware offset"),
2411 -18.0
2412 );
2413 }
2414
2415 #[test]
2416 fn offset_glonasst_gpst_matches_rtklib_2017() {
2417 let jd = utc_jd(2017, 1, 1, 0, 0, 0.0);
2418 let want = f64::from_bits(0x40c5_0f00_0000_0000);
2420 assert_eq!(
2421 timescale_offset_at_s(TimeScale::Gpst, TimeScale::Glonasst, jd)
2422 .expect("leap-aware offset"),
2423 want
2424 );
2425 assert_eq!(want, 10782.0);
2426 }
2427
2428 #[test]
2429 fn offset_glonasst_gpst_at_j2000_matches_rtklib() {
2430 let jd = utc_jd(2000, 1, 1, 12, 0, 0.0);
2431 assert_eq!(
2433 timescale_offset_at_s(TimeScale::Gpst, TimeScale::Glonasst, jd)
2434 .expect("leap-aware offset"),
2435 10787.0
2436 );
2437 }
2438
2439 #[test]
2443 fn glonasst_offset_steps_across_2017_leap_second() {
2444 let before = utc_jd(2016, 12, 31, 23, 59, 59.0);
2445 let after = utc_jd(2017, 1, 1, 0, 0, 0.0);
2446
2447 let off_before = timescale_offset_at_s(TimeScale::Gpst, TimeScale::Glonasst, before)
2448 .expect("leap-aware offset");
2449 let off_after = timescale_offset_at_s(TimeScale::Gpst, TimeScale::Glonasst, after)
2450 .expect("leap-aware offset");
2451
2452 assert_eq!(off_before, f64::from_bits(0x40c5_0f80_0000_0000)); assert_eq!(off_after, f64::from_bits(0x40c5_0f00_0000_0000)); assert_eq!(off_before, 10783.0);
2455 assert_eq!(off_after, 10782.0);
2456 assert_eq!(off_before - off_after, 1.0);
2459
2460 assert_eq!(
2462 timescale_offset_at_s(TimeScale::Utc, TimeScale::Gpst, before)
2463 .expect("leap-aware offset"),
2464 17.0
2465 );
2466 assert_eq!(
2467 timescale_offset_at_s(TimeScale::Utc, TimeScale::Gpst, after)
2468 .expect("leap-aware offset"),
2469 18.0
2470 );
2471 }
2472
2473 #[test]
2478 fn leap_aware_offset_agrees_with_timescales_path() {
2479 let jd = utc_jd(2020, 6, 15, 0, 0, 0.0);
2481 let gpst_minus_utc =
2482 timescale_offset_at_s(TimeScale::Utc, TimeScale::Gpst, jd).expect("leap-aware offset");
2483 assert_eq!(gpst_minus_utc, 18.0);
2485 }
2486}