1use crate::{
41 tables::BUILTIN_TABLE, Beidou, CivilDate, Galileo, Glonass, GnssTimeError, Gps, Tai, Time, Utc,
42};
43
44pub const RUNTIME_CAPACITY: usize = 64;
49
50static BUILTIN_LEAP_SECONDS: LeapSeconds = LeapSeconds {
51 entries: &BUILTIN_TABLE,
52};
53
54const GLONASS_FROM_UTC_EPOCH_NS: i64 = {
59 let to_1996 = CivilDate::new(1972, 1, 1).nanos_until(CivilDate::new(1996, 1, 1));
61
62 to_1996 - 3 * 3_600 * 1_000_000_000_i64
64 };
67
68const _VERIFY_GLONASS_OFFSET: () = {
69 let s = GLONASS_FROM_UTC_EPOCH_NS / 1_000_000_000;
70
71 assert!(
72 s == 757_371_600,
73 "GLONASS -> UTC epoch offset must be 757371600 s"
74 );
75};
76
77const UTC_TO_GPS_EPOCH_NS: i64 = CivilDate::new(1972, 1, 1).nanos_until(CivilDate::new(1980, 1, 6));
83const _VERIFY_UTC_GPS_OFFSET: () = {
86 let s = UTC_TO_GPS_EPOCH_NS / 1_000_000_000;
87
88 assert!(
89 s == 252_892_800,
90 "UTC -> GPS epoch offset must be 252892800 s (2927 days)"
91 );
92};
93
94pub trait LeapSecondsProvider {
116 fn tai_minus_utc_at(
118 &self,
119 tai: Time<Tai>,
120 ) -> i32;
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125#[must_use = "handle the extension error; ignoring it means the table was not updated"]
126#[non_exhaustive]
127pub enum LeapExtendError {
128 NotStrictlyAscending,
131
132 NonUnitIncrement,
135
136 BufferFull,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
147pub struct LeapEntry {
148 pub tai_nanos: u64,
150
151 pub tai_minus_utc: i32,
153}
154
155pub struct LeapSeconds {
187 entries: &'static [LeapEntry], }
189
190#[derive(Debug)]
217pub struct RuntimeLeapSeconds {
218 buf: [LeapEntry; RUNTIME_CAPACITY],
219 len: usize,
220}
221
222impl LeapEntry {
223 #[inline]
231 #[must_use]
232 pub const fn new(
233 tai_nanos: u64,
234 tai_minus_utc: i32,
235 ) -> Self {
236 LeapEntry {
237 tai_nanos,
238 tai_minus_utc,
239 }
240 }
241}
242
243impl LeapSeconds {
244 #[inline]
254 #[must_use]
255 pub fn builtin() -> &'static LeapSeconds {
256 &BUILTIN_LEAP_SECONDS
257 }
258
259 #[inline]
280 #[must_use]
281 pub const fn from_slice(entries: &'static [LeapEntry]) -> Self {
282 Self { entries }
283 }
284
285 #[inline]
291 #[must_use]
292 pub const fn from_table(entries: &'static [LeapEntry]) -> Self {
293 Self { entries }
294 }
295
296 #[inline]
298 #[must_use]
299 pub fn len(&self) -> usize {
300 self.entries.len()
301 }
302
303 #[inline]
305 #[must_use]
306 pub fn is_empty(&self) -> bool {
307 self.entries.is_empty()
308 }
309
310 #[inline]
312 #[must_use]
313 pub fn entries(&self) -> &[LeapEntry] {
314 self.entries
315 }
316
317 #[inline]
337 #[must_use]
338 pub const fn last_update(&self) -> Option<Time<Tai>> {
339 if self.entries.len() <= 1 {
340 return None;
341 }
342
343 let last = &self.entries[self.entries.len() - 1];
344
345 Some(Time::<Tai>::from_nanos(last.tai_nanos))
346 }
347
348 #[inline]
361 #[must_use]
362 pub const fn current_tai_minus_utc(&self) -> i32 {
363 if self.entries.is_empty() {
364 return 19;
365 }
366
367 self.entries[self.entries.len() - 1].tai_minus_utc
368 }
369}
370
371impl RuntimeLeapSeconds {
372 #[inline]
377 #[must_use]
378 pub fn new() -> Self {
379 Self {
380 buf: [LeapEntry::new(0, 0); RUNTIME_CAPACITY],
381 len: 0,
382 }
383 }
384
385 #[must_use]
395 pub fn from_builtin() -> Self {
396 assert!(
397 BUILTIN_TABLE.len() <= RUNTIME_CAPACITY,
398 "BUILTIN_TABLE exceeds RUNTIME_CAPACITY"
399 );
400
401 let mut rt = Self::new();
402
403 for &entry in BUILTIN_TABLE.iter() {
404 rt.buf[rt.len] = entry;
405 rt.len += 1;
406 }
407
408 rt
409 }
410
411 #[inline]
421 pub fn from_slice(entries: &[LeapEntry]) -> Result<Self, LeapExtendError> {
422 if entries.len() > RUNTIME_CAPACITY {
423 return Err(LeapExtendError::BufferFull);
424 }
425
426 let mut rt = Self::new();
427
428 for &entry in entries {
429 rt.buf[rt.len] = entry;
430 rt.len += 1;
431 }
432
433 Ok(rt)
434 }
435
436 pub fn try_extend(
475 &mut self,
476 entry: LeapEntry,
477 ) -> Result<(), LeapExtendError> {
478 if self.len >= RUNTIME_CAPACITY {
481 return Err(LeapExtendError::BufferFull);
482 }
483
484 if self.len > 0 {
486 let last = &self.buf[self.len - 1];
487
488 if entry.tai_nanos <= last.tai_nanos {
491 return Err(LeapExtendError::NotStrictlyAscending);
492 }
493
494 if entry.tai_minus_utc != last.tai_minus_utc + 1 {
497 return Err(LeapExtendError::NonUnitIncrement);
498 }
499 }
500
501 self.buf[self.len] = entry;
502 self.len += 1;
503
504 Ok(())
505 }
506
507 #[inline]
509 #[must_use]
510 pub const fn len(&self) -> usize {
511 self.len
512 }
513
514 #[inline]
516 #[must_use]
517 pub const fn is_empty(&self) -> bool {
518 self.len == 0
519 }
520
521 #[inline]
523 #[must_use]
524 pub fn entries(&self) -> &[LeapEntry] {
525 &self.buf[..self.len]
526 }
527
528 #[inline]
531 #[must_use]
532 pub const fn last_update(&self) -> Option<Time<Tai>> {
533 if self.len <= 1 {
534 return None;
535 }
536
537 Some(Time::<Tai>::from_nanos(self.buf[self.len - 1].tai_nanos))
538 }
539
540 #[inline]
543 #[must_use]
544 pub const fn current_tai_minus_utc(&self) -> i32 {
545 if self.len == 0 {
546 return 19;
547 }
548
549 self.buf[self.len - 1].tai_minus_utc
550 }
551}
552
553impl LeapSecondsProvider for LeapSeconds {
554 fn tai_minus_utc_at(
555 &self,
556 tai: Time<Tai>,
557 ) -> i32 {
558 let nanos = tai.as_nanos();
559 let entries = self.entries;
560
561 if entries.is_empty() {
562 return 19; }
564
565 match entries.binary_search_by_key(&nanos, |e| e.tai_nanos) {
567 Ok(i) => entries[i].tai_minus_utc,
569 Err(0) => entries[0].tai_minus_utc,
571 Err(i) => entries[i - 1].tai_minus_utc,
573 }
574 }
575}
576
577impl LeapSecondsProvider for RuntimeLeapSeconds {
578 fn tai_minus_utc_at(
579 &self,
580 tai: Time<Tai>,
581 ) -> i32 {
582 let entries = self.entries();
583 let nanos = tai.as_nanos();
584
585 if entries.is_empty() {
586 return 19;
587 }
588
589 match entries.binary_search_by_key(&nanos, |e| e.tai_nanos) {
590 Ok(i) => entries[i].tai_minus_utc,
591 Err(0) => entries[0].tai_minus_utc,
592 Err(i) => entries[i - 1].tai_minus_utc,
593 }
594 }
595}
596
597impl<P: LeapSecondsProvider> LeapSecondsProvider for &P {
600 fn tai_minus_utc_at(
601 &self,
602 tai: Time<Tai>,
603 ) -> i32 {
604 (*self).tai_minus_utc_at(tai)
605 }
606}
607
608pub fn glonass_to_utc(glo: Time<Glonass>) -> Result<Time<Utc>, GnssTimeError> {
627 let utc_ns = (glo.as_nanos() as i128) + (GLONASS_FROM_UTC_EPOCH_NS as i128);
628
629 if utc_ns < 0 || utc_ns > u64::MAX as i128 {
630 return Err(GnssTimeError::Overflow);
631 }
632
633 Ok(Time::<Utc>::from_nanos(utc_ns as u64))
634}
635
636pub fn glonass_to_gps<P: LeapSecondsProvider>(
640 glo: Time<Glonass>,
641 ls: &P,
642) -> Result<Time<Gps>, GnssTimeError> {
643 let utc = glonass_to_utc(glo)?;
644
645 utc_to_gps(utc, ls)
646}
647
648pub fn glonass_to_galileo<P: LeapSecondsProvider>(
650 glo: Time<Glonass>,
651 ls: &P,
652) -> Result<Time<Galileo>, GnssTimeError> {
653 let utc = glonass_to_utc(glo)?;
654
655 utc_to_galileo(utc, ls)
656}
657
658pub fn glonass_to_beidou<P: LeapSecondsProvider>(
660 glo: Time<Glonass>,
661 ls: &P,
662) -> Result<Time<Beidou>, GnssTimeError> {
663 let utc = glonass_to_utc(glo)?;
664
665 utc_to_beidou(utc, ls)
666}
667
668pub fn gps_to_utc<P: LeapSecondsProvider>(
699 gps: Time<Gps>,
700 ls: &P,
701) -> Result<Time<Utc>, GnssTimeError> {
702 let tai = gps.to_tai()?;
703 let n = ls.tai_minus_utc_at(tai);
704 let utc_ns = (gps.as_nanos() as i128) - ((n - 19) as i128 * 1_000_000_000_i128)
706 + (UTC_TO_GPS_EPOCH_NS as i128);
707
708 if utc_ns < 0 || utc_ns > u64::MAX as i128 {
709 return Err(GnssTimeError::Overflow);
710 }
711
712 Ok(Time::<Utc>::from_nanos(utc_ns as u64))
713}
714
715pub fn gps_to_glonass<P: LeapSecondsProvider>(
719 gps: Time<Gps>,
720 ls: &P,
721) -> Result<Time<Glonass>, GnssTimeError> {
722 let utc = gps_to_utc(gps, ls)?;
723
724 utc_to_glonass(utc)
725}
726
727pub fn galileo_to_utc<P: LeapSecondsProvider>(
736 gal: Time<Galileo>,
737 ls: &P,
738) -> Result<Time<Utc>, GnssTimeError> {
739 let gps = gal.try_convert::<Gps>()?;
742
743 gps_to_utc(gps, ls)
744}
745
746pub fn galileo_to_glonass<P: LeapSecondsProvider>(
748 gal: Time<Galileo>,
749 ls: &P,
750) -> Result<Time<Glonass>, GnssTimeError> {
751 let utc = galileo_to_utc(gal, ls)?;
752
753 utc_to_glonass(utc)
754}
755
756pub fn beidou_to_utc<P: LeapSecondsProvider>(
765 bdt: Time<Beidou>,
766 ls: &P,
767) -> Result<Time<Utc>, GnssTimeError> {
768 let gps = bdt.try_convert::<Gps>()?;
769
770 gps_to_utc(gps, ls)
771}
772
773pub fn beidou_to_glonass<P: LeapSecondsProvider>(
775 bdt: Time<Beidou>,
776 ls: &P,
777) -> Result<Time<Glonass>, GnssTimeError> {
778 let utc = beidou_to_utc(bdt, ls)?;
779
780 utc_to_glonass(utc)
781}
782
783pub fn utc_to_glonass(utc: Time<Utc>) -> Result<Time<Glonass>, GnssTimeError> {
794 let glo_ns = (utc.as_nanos() as i128) - (GLONASS_FROM_UTC_EPOCH_NS as i128);
795
796 if glo_ns < 0 || glo_ns > u64::MAX as i128 {
797 return Err(GnssTimeError::Overflow);
798 }
799
800 Ok(Time::<Glonass>::from_nanos(glo_ns as u64))
801}
802
803pub fn utc_to_gps<P: LeapSecondsProvider>(
816 utc: Time<Utc>,
817 ls: &P,
818) -> Result<Time<Gps>, GnssTimeError> {
819 let approx_tai_ns =
825 (utc.as_nanos() as i128) - (UTC_TO_GPS_EPOCH_NS as i128) + 19_000_000_000_i128;
826
827 let tai1 = if approx_tai_ns >= 0 && approx_tai_ns <= u64::MAX as i128 {
828 Time::<Tai>::from_nanos(approx_tai_ns as u64)
829 } else {
830 Time::<Tai>::EPOCH
831 };
832
833 let n1 = ls.tai_minus_utc_at(tai1);
834
835 let refined_tai_ns = (utc.as_nanos() as i128) - (UTC_TO_GPS_EPOCH_NS as i128)
837 + (n1 as i128 * 1_000_000_000_i128);
838
839 let tai2 = if refined_tai_ns >= 0 && refined_tai_ns <= u64::MAX as i128 {
840 Time::<Tai>::from_nanos(refined_tai_ns as u64)
841 } else {
842 tai1
843 };
844
845 let n = ls.tai_minus_utc_at(tai2);
846
847 let gps_ns = (utc.as_nanos() as i128) + ((n - 19) as i128 * 1_000_000_000_i128)
848 - (UTC_TO_GPS_EPOCH_NS as i128);
849 if gps_ns < 0 || gps_ns > u64::MAX as i128 {
850 return Err(GnssTimeError::Overflow);
851 }
852
853 Ok(Time::<Gps>::from_nanos(gps_ns as u64))
854}
855
856pub fn utc_to_galileo<P: LeapSecondsProvider>(
858 utc: Time<Utc>,
859 ls: &P,
860) -> Result<Time<Galileo>, GnssTimeError> {
861 let gps = utc_to_gps(utc, ls)?;
862
863 gps.try_convert::<Galileo>()
864}
865
866pub fn utc_to_beidou<P: LeapSecondsProvider>(
868 utc: Time<Utc>,
869 ls: &P,
870) -> Result<Time<Beidou>, GnssTimeError> {
871 let gps = utc_to_gps(utc, ls)?;
872
873 gps.try_convert::<Beidou>()
874}
875
876impl core::fmt::Display for LeapExtendError {
877 fn fmt(
878 &self,
879 f: &mut core::fmt::Formatter<'_>,
880 ) -> core::fmt::Result {
881 match self {
882 LeapExtendError::NotStrictlyAscending => {
883 f.write_str("new entry tai_nanos is not strictly greater than the last entry")
884 }
885 LeapExtendError::NonUnitIncrement => {
886 f.write_str("new entry tai_minus_utc be exactly one more tham the last entry")
887 }
888 LeapExtendError::BufferFull => {
889 f.write_str("runtime leap-second buffer is full; cannot add more entries")
890 }
891 }
892 }
893}
894
895#[cfg(feature = "std")]
896impl std::error::Error for LeapExtendError {}
897
898impl Default for RuntimeLeapSeconds {
899 fn default() -> Self {
900 Self::new()
901 }
902}
903
904#[cfg(test)]
909mod tests {
910 #[allow(unused_imports)]
911 use std::string::ToString;
912
913 use super::*;
914 use crate::{scale::Gps, DurationParts};
915
916 #[test]
917 fn test_utc_to_gps_epoch_offset_is_252892800_seconds() {
918 assert_eq!(UTC_TO_GPS_EPOCH_NS / 1_000_000_000, 252_892_800);
919 }
920
921 #[test]
922 fn test_glonass_epoch_offset_is_757371600_seconds() {
923 assert_eq!(GLONASS_FROM_UTC_EPOCH_NS / 1_000_000_000, 757_371_600);
924 }
925
926 #[test]
927 fn test_builtin_table_length() {
928 assert_eq!(LeapSeconds::builtin().len(), 19);
929 }
930
931 #[test]
932 fn test_utc_to_gps_epoch_offset_is_2927_days() {
933 assert_eq!(UTC_TO_GPS_EPOCH_NS / 1_000_000_000 / 86_400, 2927);
934 }
935
936 #[test]
937 fn test_glonass_epoch_offset_from_utc_epoch_is_correct() {
938 assert_eq!(GLONASS_FROM_UTC_EPOCH_NS / 1_000_000_000, 757_371_600);
941 }
942
943 #[test]
944 fn test_builtin_table_is_sorted() {
945 let entries = LeapSeconds::builtin().entries();
946
947 for w in entries.windows(2) {
948 assert!(
949 w[0].tai_nanos < w[1].tai_nanos,
950 "table not sorted at {:?}",
951 w
952 );
953 }
954 }
955
956 #[test]
957 fn test_builtin_table_starts_with_tai_minus_utc_19() {
958 assert_eq!(LeapSeconds::builtin().entries()[0].tai_minus_utc, 19);
959 }
960
961 #[test]
962 fn test_builtin_table_ends_with_tai_minus_utc_37() {
963 let last = *LeapSeconds::builtin().entries().last().unwrap();
964 assert_eq!(last.tai_minus_utc, 37);
965 }
966
967 #[test]
968 fn test_builtin_table_has_monotone_increasing_tai_minus_utc() {
969 let entries = LeapSeconds::builtin().entries();
970
971 for w in entries.windows(2) {
972 assert_eq!(
973 w[1].tai_minus_utc,
974 w[0].tai_minus_utc + 1,
975 "expected each entry to increment by 1"
976 );
977 }
978 }
979
980 #[test]
986 fn test_builtin_table_matches_iers_bulletin_c() {
987 const GPS_EPOCH_UNIX: u64 = 315_964_800;
988
989 let iers_events: &[(u64, i32)] = &[
991 (362_793_600, 20), (394_329_600, 21), (425_865_600, 22), (489_024_000, 23), (567_993_600, 24), (631_152_000, 25), (662_688_000, 26), (709_948_800, 27), (741_484_800, 28), (773_020_800, 29), (820_454_400, 30), (867_715_200, 31), (915_148_800, 32), (1_136_073_600, 33), (1_230_768_000, 34), (1_341_100_800, 35), (1_435_708_800, 36), (1_483_228_800, 37), ];
1010
1011 let entries = LeapSeconds::builtin().entries();
1012
1013 assert_eq!(entries[0].tai_nanos, 0);
1015 assert_eq!(entries[0].tai_minus_utc, 19);
1016
1017 for (idx, &(unix, expected_n)) in iers_events.iter().enumerate() {
1019 let gps_s = unix - GPS_EPOCH_UNIX;
1020 let expected_threshold = (gps_s + expected_n as u64) * 1_000_000_000;
1021 let entry = &entries[idx + 1];
1022
1023 assert_eq!(
1024 entry.tai_nanos,
1025 expected_threshold,
1026 "threshold mismatch at IERS event {} (unix={})",
1027 idx + 1,
1028 unix
1029 );
1030 assert_eq!(
1031 entry.tai_minus_utc,
1032 expected_n,
1033 "tai_minus_utc mismatch at IERS event {} (unix={})",
1034 idx + 1,
1035 unix
1036 );
1037 }
1038 }
1039
1040 #[test]
1041 fn test_last_update_builtin_is_2017_threshold() {
1042 let last = LeapSeconds::builtin()
1043 .last_update()
1044 .expect("builtin must have last_update");
1045
1046 assert_eq!(last.as_nanos(), 1_167_264_037_000_000_000);
1047 }
1048
1049 #[test]
1050 fn test_last_update_single_entry_is_none() {
1051 static SINGLE: [LeapEntry; 1] = [LeapEntry::new(0, 37)];
1052 let ls = LeapSeconds::from_slice(&SINGLE);
1053
1054 assert!(ls.last_update().is_none());
1055 }
1056
1057 #[test]
1058 fn test_last_update_empty_is_none() {
1059 static EMPTY: [LeapEntry; 0] = [];
1060 let ls = LeapSeconds::from_slice(&EMPTY);
1061
1062 assert!(ls.last_update().is_none());
1063 }
1064
1065 #[test]
1066 fn test_current_tai_minus_utc_builtin_is_37() {
1067 assert_eq!(LeapSeconds::builtin().current_tai_minus_utc(), 37);
1068 }
1069
1070 #[test]
1071 fn test_current_tai_minus_utc_empty_is_fallback_19() {
1072 static EMPTY: [LeapEntry; 0] = [];
1073 let ls = LeapSeconds::from_slice(&EMPTY);
1074
1075 assert_eq!(ls.current_tai_minus_utc(), 19);
1076 }
1077
1078 #[test]
1079 fn test_from_slice_and_from_table_are_equivalent() {
1080 static TABLE: [LeapEntry; 2] = [LeapEntry::new(0, 19), LeapEntry::new(1_000_000, 20)];
1081
1082 let ls_slice = LeapSeconds::from_slice(&TABLE);
1083 let ls_table = LeapSeconds::from_table(&TABLE);
1084
1085 assert_eq!(ls_slice.len(), ls_table.len());
1086 assert_eq!(
1087 ls_slice.entries()[0].tai_nanos,
1088 ls_table.entries()[0].tai_nanos
1089 );
1090 }
1091
1092 #[test]
1093 fn test_lookup_at_tai_zero_returns_19() {
1094 let ls = LeapSeconds::builtin();
1095 assert_eq!(ls.tai_minus_utc_at(Time::<Tai>::EPOCH), 19);
1096 }
1097
1098 #[test]
1099 fn test_lookup_at_max_tai_returns_37() {
1100 let ls = LeapSeconds::builtin();
1101 assert_eq!(ls.tai_minus_utc_at(Time::<Tai>::MAX), 37);
1102 }
1103
1104 #[test]
1105 fn test_lookup_at_max_tai_returns_last_value() {
1106 let ls = LeapSeconds::builtin();
1107
1108 assert_eq!(ls.tai_minus_utc_at(Time::<Tai>::MAX), 37);
1109 }
1110
1111 #[test]
1112 fn test_lookup_at_exact_2017_threshold_returns_37() {
1113 let ls = LeapSeconds::builtin();
1114 let tai = Time::<Tai>::from_nanos(1_167_264_037_000_000_000);
1116
1117 assert_eq!(ls.tai_minus_utc_at(tai), 37);
1118 }
1119
1120 #[test]
1121 fn test_lookup_one_ns_before_2017_threshold_returns_36() {
1122 let ls = LeapSeconds::builtin();
1123 let tai = Time::<Tai>::from_nanos(1_167_264_037_000_000_000 - 1);
1124
1125 assert_eq!(ls.tai_minus_utc_at(tai), 36);
1126 }
1127
1128 #[test]
1129 fn test_lookup_at_1999_threshold_returns_32() {
1130 let ls = LeapSeconds::builtin();
1131 let tai = Time::<Tai>::from_nanos(599_184_032_000_000_000);
1133
1134 assert_eq!(ls.tai_minus_utc_at(tai), 32);
1135 }
1136
1137 #[test]
1138 fn test_lookup_one_ns_before_1999_threshold_returns_31() {
1139 let ls = LeapSeconds::builtin();
1140 let tai = Time::<Tai>::from_nanos(599_184_032_000_000_000 - 1);
1141
1142 assert_eq!(ls.tai_minus_utc_at(tai), 31);
1143 }
1144
1145 #[test]
1146 fn test_gps_utc_gps_roundtrip_at_gps_epoch() {
1147 let ls = LeapSeconds::builtin();
1148 let gps = Time::<Gps>::EPOCH;
1149 let utc = gps_to_utc(gps, &ls).unwrap();
1150 let back = utc_to_gps(utc, &ls).unwrap();
1151
1152 assert_eq!(gps, back);
1153 }
1154
1155 #[test]
1156 fn test_gps_utc_gps_roundtrip_at_2020() {
1157 let ls = LeapSeconds::builtin();
1158 let gps = Time::<Gps>::from_week_tow(
1160 2086,
1161 DurationParts {
1162 seconds: 0,
1163 nanos: 0,
1164 },
1165 )
1166 .unwrap();
1167 let utc = gps_to_utc(gps, &ls).unwrap();
1168 let back = utc_to_gps(utc, &ls).unwrap();
1169
1170 assert_eq!(gps, back);
1171 }
1172
1173 #[test]
1174 fn test_gps_epoch_utc_is_correct_offset_from_utc_epoch() {
1175 let ls = LeapSeconds::builtin();
1176 let utc = gps_to_utc(Time::<Gps>::EPOCH, &ls).unwrap();
1180
1181 assert_eq!(utc.as_nanos(), 252_892_800_000_000_000);
1182 }
1183
1184 #[test]
1191 fn test_gps_minus_utc_is_18s_at_2017_01_01() {
1192 let ls = LeapSeconds::builtin();
1193 let gps_s: u64 = 1_167_264_000 + 18;
1196 let gps = Time::<Gps>::from_seconds(gps_s);
1197 let utc = gps_to_utc(gps, &ls).unwrap();
1198
1199 let expected_utc_ns: u64 = 16_437 * 86_400 * 1_000_000_000;
1201
1202 assert_eq!(utc.as_nanos(), expected_utc_ns);
1203 }
1204
1205 #[test]
1207 fn test_gps_minus_utc_is_13s_at_1999_01_01() {
1208 let ls = LeapSeconds::builtin();
1209 let gps = Time::<Gps>::from_seconds(599_184_013);
1211 let utc = gps_to_utc(gps, &ls).unwrap();
1212
1213 let expected_utc_s: u64 = 9_862 * 86_400;
1218
1219 assert_eq!(utc.as_seconds(), expected_utc_s);
1220 }
1221
1222 #[test]
1226 fn test_leap_second_transition_1999_gps_jumps_by_2s() {
1227 let ls = LeapSeconds::builtin();
1228
1229 let gps_before = Time::<Gps>::from_seconds(599_184_011);
1233
1234 let gps_after = Time::<Gps>::from_seconds(599_184_013);
1238
1239 let utc_before = gps_to_utc(gps_before, &ls).unwrap();
1241 let utc_after = gps_to_utc(gps_after, &ls).unwrap();
1242
1243 let diff = (utc_after - utc_before).as_seconds();
1245
1246 assert_eq!(diff, 1, "GPS jumped 2s but UTC advanced 1s (leap second)");
1247 }
1248
1249 #[test]
1251 fn test_leap_second_transition_2017_gps_jumps_by_2s() {
1252 let ls = LeapSeconds::builtin();
1253 let gps_before = Time::<Gps>::from_seconds(1_167_263_999 + 17);
1256 let gps_after = Time::<Gps>::from_seconds(1_167_264_000 + 18);
1259 let utc_before = gps_to_utc(gps_before, &ls).unwrap();
1260 let utc_after = gps_to_utc(gps_after, &ls).unwrap();
1261 let diff = (utc_after - utc_before).as_seconds();
1262
1263 assert_eq!(diff, 1, "GPS jumped 2s but UTC advanced 1s");
1264 }
1265
1266 #[test]
1267 fn test_glonass_epoch_to_utc_gives_correct_nanos() {
1268 let utc = glonass_to_utc(Time::<Glonass>::EPOCH).unwrap();
1275
1276 assert_eq!(utc.as_nanos(), GLONASS_FROM_UTC_EPOCH_NS as u64);
1277 }
1278
1279 #[test]
1280 fn test_utc_to_glonass_epoch_gives_zero() {
1281 let utc = Time::<Utc>::from_nanos(GLONASS_FROM_UTC_EPOCH_NS as u64);
1282 let glo = utc_to_glonass(utc).unwrap();
1283
1284 assert_eq!(glo, Time::<Glonass>::EPOCH);
1285 }
1286
1287 #[test]
1288 fn test_glonass_utc_glonass_roundtrip() {
1289 let glo = Time::<Glonass>::from_day_tod(
1290 10_000,
1291 DurationParts {
1292 seconds: 43_200,
1293 nanos: 0,
1294 },
1295 )
1296 .unwrap();
1297 let utc = glonass_to_utc(glo).unwrap();
1298 let back = utc_to_glonass(utc).unwrap();
1299
1300 assert_eq!(glo, back);
1301 }
1302
1303 #[test]
1304 fn test_utc_before_glonass_epoch_returns_error() {
1305 let utc = Time::<Utc>::EPOCH;
1308
1309 assert!(matches!(utc_to_glonass(utc), Err(GnssTimeError::Overflow)));
1310 }
1311
1312 #[test]
1313 fn test_glonass_offset_is_exactly_3_hours_less_than_day_boundary() {
1314 let three_hours_ns: i64 = 3 * 3_600 * 1_000_000_000;
1317 let days_ns: i64 = 8766 * 86_400 * 1_000_000_000;
1318
1319 assert_eq!(GLONASS_FROM_UTC_EPOCH_NS, days_ns - three_hours_ns);
1320 }
1321
1322 #[test]
1323 fn test_gps_to_glonass_to_gps_roundtrip() {
1324 let ls = LeapSeconds::builtin();
1325 let gps = Time::<Gps>::from_week_tow(
1327 2100,
1328 DurationParts {
1329 seconds: 86400,
1330 nanos: 0,
1331 },
1332 )
1333 .unwrap();
1334 let glo = gps_to_glonass(gps, &ls).unwrap();
1335 let back = glonass_to_gps(glo, &ls).unwrap();
1336
1337 assert_eq!(gps, back);
1338 }
1339
1340 #[test]
1341 fn test_custom_provider_works() {
1342 struct Always37;
1343
1344 impl LeapSecondsProvider for Always37 {
1345 fn tai_minus_utc_at(
1346 &self,
1347 _: Time<Tai>,
1348 ) -> i32 {
1349 37
1350 }
1351 }
1352
1353 let gps = Time::<Gps>::from_seconds(1_000_000_000);
1354 let utc = gps_to_utc(gps, &Always37).unwrap();
1355 let back = utc_to_gps(utc, &Always37).unwrap();
1356
1357 assert_eq!(gps, back);
1358 }
1359
1360 #[test]
1361 fn test_empty_table_returns_fallback_19() {
1362 static EMPTY: [LeapEntry; 0] = [];
1363
1364 let ls = LeapSeconds::from_table(&EMPTY);
1365
1366 assert_eq!(
1367 ls.tai_minus_utc_at(Time::<Tai>::from_seconds(1_000_000)),
1368 19
1369 );
1370 }
1371
1372 #[test]
1373 fn test_runtime_from_builtin_has_19_entries() {
1374 assert_eq!(RuntimeLeapSeconds::from_builtin().len(), 19);
1375 }
1376
1377 #[test]
1378 fn test_runtime_from_builtin_current_is_37() {
1379 assert_eq!(
1380 RuntimeLeapSeconds::from_builtin().current_tai_minus_utc(),
1381 37
1382 );
1383 }
1384
1385 #[test]
1386 fn test_runtime_try_extend_valid() {
1387 let mut rt = RuntimeLeapSeconds::from_builtin();
1388 rt.try_extend(LeapEntry::new(9_999_999_999_000_000_000, 38))
1389 .unwrap();
1390
1391 assert_eq!(rt.len(), 20);
1392 assert_eq!(rt.current_tai_minus_utc(), 38);
1393 }
1394
1395 #[test]
1396 fn test_runtime_try_extend_last_update_updated() {
1397 let mut rt = RuntimeLeapSeconds::from_builtin();
1398 rt.try_extend(LeapEntry::new(9_999_999_999_000_000_000, 38))
1399 .unwrap();
1400
1401 let last = rt.last_update().unwrap();
1402 assert_eq!(last.as_nanos(), 9_999_999_999_000_000_000);
1403 }
1404
1405 #[test]
1406 fn test_runtime_try_extend_not_ascending_error() {
1407 let mut rt = RuntimeLeapSeconds::from_builtin();
1408 let err = rt
1410 .try_extend(LeapEntry::new(1_167_264_037_000_000_000, 38))
1411 .unwrap_err();
1412
1413 assert_eq!(err, LeapExtendError::NotStrictlyAscending);
1414 }
1415
1416 #[test]
1417 fn test_runtime_try_extend_non_unit_increment_error() {
1418 let mut rt = RuntimeLeapSeconds::from_builtin();
1419 let err = rt
1421 .try_extend(LeapEntry::new(9_999_999_999_000_000_000, 39))
1422 .unwrap_err();
1423
1424 assert_eq!(err, LeapExtendError::NonUnitIncrement);
1425 }
1426
1427 #[test]
1428 fn test_runtime_from_slice_too_large_returns_buffer_full() {
1429 let big: std::vec::Vec<LeapEntry> = (0..RUNTIME_CAPACITY + 1)
1430 .map(|i| LeapEntry::new(i as u64 * 1_000_000_000, 19 + i as i32))
1431 .collect();
1432 let err = RuntimeLeapSeconds::from_slice(&big).unwrap_err();
1433
1434 assert_eq!(err, LeapExtendError::BufferFull);
1435 }
1436
1437 #[test]
1438 fn test_runtime_provider_matches_static_at_all_thresholds() {
1439 let rt = RuntimeLeapSeconds::from_builtin();
1440 let ls = LeapSeconds::builtin();
1441
1442 let test_nanos: &[u64] = &[
1443 0,
1444 46_828_820_000_000_000,
1445 599_184_032_000_000_000,
1446 1_167_264_037_000_000_000,
1447 u64::MAX,
1448 ];
1449
1450 for &nanos in test_nanos {
1451 let tai = Time::<Tai>::from_nanos(nanos);
1452 assert_eq!(
1453 rt.tai_minus_utc_at(tai),
1454 ls.tai_minus_utc_at(tai),
1455 "mismatch at tai_nanos={}",
1456 nanos
1457 );
1458 }
1459 }
1460
1461 #[test]
1462 fn test_runtime_empty_last_update_is_none() {
1463 assert!(RuntimeLeapSeconds::new().last_update().is_none());
1464 }
1465
1466 #[test]
1467 fn test_runtime_single_entry_last_update_is_none() {
1468 let mut rt = RuntimeLeapSeconds::new();
1469 rt.try_extend(LeapEntry::new(0, 19)).unwrap();
1470 assert!(rt.last_update().is_none());
1471 }
1472
1473 #[test]
1474 fn test_gps_utc_gps_roundtrip_with_runtime_table() {
1475 let rt = RuntimeLeapSeconds::from_builtin();
1476 let gps = Time::<Gps>::from_week_tow(
1477 2086,
1478 DurationParts {
1479 seconds: 0,
1480 nanos: 0,
1481 },
1482 )
1483 .unwrap();
1484 let utc = gps_to_utc(gps, &rt).unwrap();
1485 let back = utc_to_gps(utc, &rt).unwrap();
1486
1487 assert_eq!(gps, back);
1488 }
1489
1490 #[test]
1491 fn test_gps_utc_roundtrip_extended_table() {
1492 let mut rt = RuntimeLeapSeconds::from_builtin();
1493 rt.try_extend(LeapEntry::new(9_999_999_999_000_000_000, 38))
1494 .unwrap();
1495
1496 let gps = Time::<Gps>::from_week_tow(
1497 2086,
1498 DurationParts {
1499 seconds: 0,
1500 nanos: 0,
1501 },
1502 )
1503 .unwrap();
1504 let utc = gps_to_utc(gps, &rt).unwrap();
1505 let back = utc_to_gps(utc, &rt).unwrap();
1506
1507 assert_eq!(gps, back);
1508 }
1509
1510 #[test]
1511 fn test_gps_epoch_utc_is_correct() {
1512 let ls = LeapSeconds::builtin();
1513 let utc = gps_to_utc(Time::<Gps>::EPOCH, &ls).unwrap();
1514
1515 assert_eq!(utc.as_nanos(), 252_892_800_000_000_000);
1516 }
1517
1518 #[test]
1519 fn test_custom_provider_roundtrip() {
1520 struct Always37;
1521 impl LeapSecondsProvider for Always37 {
1522 fn tai_minus_utc_at(
1523 &self,
1524 _: Time<Tai>,
1525 ) -> i32 {
1526 37
1527 }
1528 }
1529
1530 let gps = Time::<Gps>::from_seconds(1_000_000_000);
1531 let utc = gps_to_utc(gps, &Always37).unwrap();
1532 let back = utc_to_gps(utc, &Always37).unwrap();
1533
1534 assert_eq!(gps, back);
1535 }
1536}