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