1use core::{
57 fmt,
58 marker::PhantomData,
59 ops::{Add, AddAssign, Sub, SubAssign},
60};
61
62use crate::{
63 gps_to_utc, utc_to_gps, DisplayStyle, Duration, Glonass, GnssTimeError, Gps, LeapSeconds,
64 LeapSecondsProvider, OffsetToTai, Tai, TimeScale, Utc,
65};
66
67#[derive(Copy, Clone, Eq, PartialEq, Hash)]
85#[must_use = "Time<S> is a value type; ignoring it has no effect"]
86pub struct Time<S: TimeScale> {
87 nanos: u64,
88 _scale: PhantomData<S>,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
96pub struct DurationParts {
97 pub seconds: u64,
99
100 pub nanos: u32,
102}
103
104impl<S: TimeScale> Time<S> {
105 pub const EPOCH: Self = Time {
110 nanos: 0,
111 _scale: PhantomData,
112 };
113
114 pub const MIN: Self = Self::EPOCH;
116
117 pub const MAX: Self = Time {
134 nanos: u64::MAX,
135 _scale: PhantomData,
136 };
137
138 pub const NANOS_PER_YEAR: u64 = 365 * 24 * 3_600 * 1_000_000_000;
150
151 #[inline(always)]
153 pub const fn from_nanos(nanos: u64) -> Self {
154 Time {
155 nanos,
156 _scale: PhantomData,
157 }
158 }
159
160 #[inline]
165 pub const fn from_seconds(secs: u64) -> Self {
166 match secs.checked_mul(1_000_000_000) {
167 Some(n) => Time::from_nanos(n),
168 None => panic!("Time::from_seconds overflow"),
169 }
170 }
171
172 #[inline]
174 #[must_use = "returns None on overflow; check the result"]
175 pub const fn checked_from_seconds(secs: u64) -> Option<Self> {
176 match secs.checked_mul(1_000_000_000) {
177 Some(n) => Some(Time::from_nanos(n)),
178 None => None,
179 }
180 }
181}
182
183impl<S: TimeScale> Time<S> {
184 #[inline(always)]
186 #[must_use]
187 pub const fn as_nanos(self) -> u64 {
188 self.nanos
189 }
190
191 #[inline]
193 #[must_use]
194 pub const fn as_seconds(self) -> u64 {
195 self.nanos / 1_000_000_000
196 }
197
198 #[inline]
201 #[must_use]
202 pub fn as_seconds_f64(self) -> f64 {
203 self.nanos as f64 / 1_000_000_000.0
204 }
205}
206
207impl<S: TimeScale> Time<S> {
208 pub fn to_tai(self) -> Result<Time<Tai>, GnssTimeError> {
213 match S::OFFSET_TO_TAI {
214 OffsetToTai::Fixed(offset) => {
215 let nanos = (self.nanos as i128) + (offset as i128);
216
217 if nanos < 0 || nanos > u64::MAX as i128 {
218 return Err(GnssTimeError::Overflow);
219 }
220
221 Ok(Time::from_nanos(nanos as u64))
222 }
223 OffsetToTai::Contextual => Err(GnssTimeError::LeapSecondsRequired),
224 }
225 }
226
227 pub fn from_tai(tai: Time<Tai>) -> Result<Self, GnssTimeError> {
229 match S::OFFSET_TO_TAI {
230 OffsetToTai::Fixed(offset) => {
231 let nanos = (tai.as_nanos() as i128) - (offset as i128);
232
233 if nanos < 0 || nanos > u64::MAX as i128 {
234 return Err(GnssTimeError::Overflow);
235 }
236
237 Ok(Time::from_nanos(nanos as u64))
238 }
239 OffsetToTai::Contextual => Err(GnssTimeError::LeapSecondsRequired),
240 }
241 }
242
243 pub fn try_convert<T: TimeScale>(self) -> Result<Time<T>, GnssTimeError> {
247 let tai = self.to_tai()?;
248
249 Time::<T>::from_tai(tai)
250 }
251}
252
253impl<S: TimeScale> Time<S> {
254 #[inline]
256 #[must_use = "returns None on overflow; check the result"]
257 pub fn checked_add(
258 self,
259 d: Duration,
260 ) -> Option<Self> {
261 let result = (self.nanos as i128) + (d.as_nanos() as i128);
262
263 if result < 0 || result > u64::MAX as i128 {
264 return None;
265 };
266
267 Some(Time::from_nanos(result as u64))
268 }
269
270 #[inline]
272 #[must_use = "returns None on underflow; check the result"]
273 pub fn checked_sub_duration(
274 self,
275 d: Duration,
276 ) -> Option<Self> {
277 let result = (self.nanos as i128) - (d.as_nanos() as i128);
278
279 if result < 0 || result > u64::MAX as i128 {
280 return None;
281 }
282
283 Some(Time::from_nanos(result as u64))
284 }
285
286 #[inline]
288 #[must_use = "saturating_add returns a new Time<S>; the original is unchanged"]
289 pub fn saturating_add(
290 self,
291 d: Duration,
292 ) -> Self {
293 self.checked_add(d).unwrap_or(if d.is_negative() {
294 Time::EPOCH
295 } else {
296 Time::MAX
297 })
298 }
299
300 #[inline]
302 #[must_use = "saturating_sub_duration returns a new Time<S>; the original is unchanged"]
303 pub fn saturating_sub_duration(
304 self,
305 d: Duration,
306 ) -> Self {
307 self.checked_sub_duration(d).unwrap_or(if d.is_negative() {
308 Time::MAX
309 } else {
310 Time::EPOCH
311 })
312 }
313
314 #[inline]
316 pub fn try_add(
317 self,
318 d: Duration,
319 ) -> Result<Self, GnssTimeError> {
320 self.checked_add(d).ok_or(GnssTimeError::Overflow)
321 }
322
323 #[inline]
325 pub fn try_sub_duration(
326 self,
327 d: Duration,
328 ) -> Result<Self, GnssTimeError> {
329 self.checked_sub_duration(d).ok_or(GnssTimeError::Overflow)
330 }
331}
332
333impl<S: TimeScale> Time<S> {
334 #[inline]
336 #[must_use = "returns None on overflow; check the result"]
337 pub const fn checked_elapsed(
338 self,
339 earlier: Time<S>,
340 ) -> Option<Duration> {
341 let diff = (self.nanos as i128) - (earlier.nanos as i128);
342
343 if diff > i64::MAX as i128 || diff < i64::MIN as i128 {
344 return None;
345 }
346
347 Some(Duration::from_nanos(diff as i64))
348 }
349}
350
351impl<S: TimeScale> Add<Duration> for Time<S> {
352 type Output = Time<S>;
353
354 #[inline]
355 fn add(
356 self,
357 rhs: Duration,
358 ) -> Time<S> {
359 self.checked_add(rhs)
360 .expect("Time<S> + Duration overflowed")
361 }
362}
363
364impl<S: TimeScale> AddAssign<Duration> for Time<S> {
365 #[inline]
366 fn add_assign(
367 &mut self,
368 rhs: Duration,
369 ) {
370 *self = *self + rhs
371 }
372}
373
374impl<S: TimeScale> Sub<Duration> for Time<S> {
375 type Output = Time<S>;
376
377 #[inline]
378 fn sub(
379 self,
380 rhs: Duration,
381 ) -> Self::Output {
382 self.checked_sub_duration(rhs)
383 .expect("Time<S> - Duration underflowed")
384 }
385}
386
387impl<S: TimeScale> SubAssign<Duration> for Time<S> {
388 #[inline]
389 fn sub_assign(
390 &mut self,
391 rhs: Duration,
392 ) {
393 *self = *self - rhs;
394 }
395}
396
397impl<S: TimeScale> Sub<Time<S>> for Time<S> {
398 type Output = Duration;
399
400 #[inline]
401 fn sub(
402 self,
403 rhs: Time<S>,
404 ) -> Self::Output {
405 self.checked_elapsed(rhs)
406 .expect("Time<S> - Time<S> overflowed i64")
407 }
408}
409
410impl DurationParts {
411 pub const NANOS_PER_SECOND: u32 = 1_000_000_000;
413
414 #[inline]
433 pub const fn new(
434 seconds: u64,
435 nanos: u32,
436 ) -> Result<Self, GnssTimeError> {
437 if nanos >= Self::NANOS_PER_SECOND {
438 return Err(GnssTimeError::InvalidInput(
439 "nanos must be in [0, 1_000_000_000]",
440 ));
441 }
442
443 Ok(Self { seconds, nanos })
444 }
445
446 #[inline]
461 #[must_use]
462 pub const fn as_nanos(self) -> u128 {
463 (self.seconds as u128) * Self::NANOS_PER_SECOND as u128 + self.nanos as u128
464 }
465}
466
467impl Time<Glonass> {
468 pub fn from_day_tod(
477 day: u32,
478 tod: DurationParts,
479 ) -> Result<Self, GnssTimeError> {
480 if tod.seconds >= 86_400 {
481 return Err(GnssTimeError::InvalidInput(
482 "tod.seconds must be in [0, 86_400)",
483 ));
484 }
485
486 if tod.nanos >= DurationParts::NANOS_PER_SECOND {
487 return Err(GnssTimeError::InvalidInput(
488 "tod.nanos must be in [0, 1_000_000_000)",
489 ));
490 }
491
492 let day_ns = (day as u64)
493 .checked_mul(86_400_000_000_000)
494 .ok_or(GnssTimeError::Overflow)?;
495
496 let tod_ns = tod
497 .seconds
498 .checked_mul(1_000_000_000)
499 .ok_or(GnssTimeError::Overflow)?
500 .checked_add(tod.nanos as u64)
501 .ok_or(GnssTimeError::Overflow)?;
502
503 let total = day_ns.checked_add(tod_ns).ok_or(GnssTimeError::Overflow)?;
504 Ok(Time::from_nanos(total))
505 }
506
507 #[inline]
509 #[must_use]
510 pub const fn day(self) -> u32 {
511 (self.nanos / 86_400_000_000_000u64) as u32
512 }
513
514 #[inline]
516 #[must_use]
517 pub const fn tod_seconds(self) -> u32 {
518 ((self.nanos % 86_400_000_000_000u64) / 1_000_000_000u64) as u32
519 }
520
521 #[inline]
523 #[must_use]
524 pub const fn sub_second_nanos(self) -> u32 {
525 (self.nanos % 1_000_000_000u64) as u32
526 }
527
528 #[inline]
583 #[must_use]
584 pub const fn day_of_week(self) -> u8 {
585 (self.day() % 7) as u8 + 1
587 }
588
589 #[inline]
591 #[must_use]
592 pub const fn is_weekend(self) -> bool {
593 let d = self.day_of_week();
594
595 d == 6 || d == 7
596 }
597}
598
599impl Time<Gps> {
600 pub fn from_week_tow(
605 week: u16,
606 tow: DurationParts,
607 ) -> Result<Self, GnssTimeError> {
608 if tow.seconds >= 604_800 {
609 return Err(GnssTimeError::InvalidInput(
610 "tow.seconds must be in [0, 604_800)",
611 ));
612 }
613
614 if tow.nanos >= DurationParts::NANOS_PER_SECOND {
615 return Err(GnssTimeError::InvalidInput(
616 "tow.nanos must be in [0, 1_000_000_000)",
617 ));
618 }
619
620 let week_ns = (week as u64)
621 .checked_mul(604_800_000_000_000)
622 .ok_or(GnssTimeError::Overflow)?;
623
624 let tow_ns = tow
625 .seconds
626 .checked_mul(1_000_000_000)
627 .ok_or(GnssTimeError::Overflow)?
628 .checked_add(tow.nanos as u64)
629 .ok_or(GnssTimeError::Overflow)?;
630
631 let total = week_ns.checked_add(tow_ns).ok_or(GnssTimeError::Overflow)?;
632 Ok(Time::from_nanos(total))
633 }
634
635 pub fn to_utc(self) -> Result<Time<Utc>, GnssTimeError> {
645 gps_to_utc(self, LeapSeconds::builtin())
646 }
647
648 pub fn to_utc_with<P: LeapSecondsProvider>(
654 self,
655 ls: &P,
656 ) -> Result<Time<Utc>, GnssTimeError> {
657 gps_to_utc(self, ls)
658 }
659
660 #[inline]
662 #[must_use]
663 pub const fn week(self) -> u32 {
664 (self.nanos / 604_800_000_000_000u64) as u32
665 }
666
667 #[inline]
669 #[must_use]
670 pub const fn tow_seconds(self) -> u32 {
671 ((self.nanos % 604_800_000_000_000u64) / 1_000_000_000u64) as u32
672 }
673
674 #[inline]
676 #[must_use]
677 pub const fn sub_second_nanos(self) -> u32 {
678 (self.nanos % 1_000_000_000u64) as u32
679 }
680}
681
682impl Time<Utc> {
683 pub fn to_gps(self) -> Result<Time<Gps>, GnssTimeError> {
689 utc_to_gps(self, LeapSeconds::builtin())
690 }
691
692 pub fn to_gps_with<P: LeapSecondsProvider>(
697 self,
698 ls: &P,
699 ) -> Result<Time<Gps>, GnssTimeError> {
700 utc_to_gps(self, ls)
701 }
702}
703
704impl<S: TimeScale> PartialOrd for Time<S> {
705 #[inline]
706 fn partial_cmp(
707 &self,
708 other: &Self,
709 ) -> Option<core::cmp::Ordering> {
710 Some(self.cmp(other))
711 }
712}
713
714impl<S: TimeScale> Ord for Time<S> {
715 #[inline]
716 fn cmp(
717 &self,
718 other: &Self,
719 ) -> core::cmp::Ordering {
720 self.nanos.cmp(&other.nanos)
721 }
722}
723
724impl<S: TimeScale> fmt::Debug for Time<S> {
725 fn fmt(
726 &self,
727 f: &mut fmt::Formatter<'_>,
728 ) -> fmt::Result {
729 write!(f, "Time<{}>({}ns)", S::NAME, self.nanos)
730 }
731}
732
733impl<S: TimeScale> fmt::Display for Time<S> {
734 fn fmt(
742 &self,
743 f: &mut fmt::Formatter<'_>,
744 ) -> fmt::Result {
745 match S::DISPLAY_STYLE {
746 DisplayStyle::WeekTow => {
747 const WEEK_NS: u64 = 604_800_000_000_000;
748 let week = self.nanos / WEEK_NS;
749 let tow_ns = self.nanos % WEEK_NS;
750 let tow_s = tow_ns / 1_000_000_000;
751 let tow_ms = (tow_ns % 1_000_000_000) / 1_000_000;
752
753 write!(f, "{} {}:{:06}.{:03}", S::NAME, week, tow_s, tow_ms)
754 }
755 DisplayStyle::DayTod => {
756 const DAY_NS: u64 = 86_400_000_000_000;
757 let day = self.nanos / DAY_NS;
758 let tod_ns = self.nanos % DAY_NS;
759 let tod_s = tod_ns / 1_000_000_000;
760 let tod_ms = (tod_ns % 1_000_000_000) / 1_000_000;
761
762 write!(f, "{} {}:{:05}.{:03}", S::NAME, day, tod_s, tod_ms)
763 }
764 DisplayStyle::Simple => {
765 let secs = self.nanos / 1_000_000_000;
766 let ns_rem = self.nanos % 1_000_000_000;
767
768 write!(f, "{} +{}s {}ns", S::NAME, secs, ns_rem)
769 }
770 }
771 }
772}
773
774#[cfg(feature = "defmt")]
777impl<S: TimeScale> defmt::Format for Time<S> {
778 fn format(
779 &self,
780 f: defmt::Formatter,
781 ) {
782 match S::DISPLAY_STYLE {
783 DisplayStyle::WeekTow => {
784 const WEEK_NS: u64 = 604_800_000_000_000;
785 let week = self.nanos / WEEK_NS;
786 let tow_ns = self.nanos % WEEK_NS;
787 let tow_s = tow_ns / 1_000_000_000;
788 let tow_ms = (tow_ns % 1_000_000_000) / 1_000_000;
789
790 defmt::write!(f, "{} {}:{:06}.{:03}", S::NAME, week, tow_s, tow_ms)
791 }
792 DisplayStyle::DayTod => {
793 const DAY_NS: u64 = 86_400_000_000_000;
794 let day = self.nanos / DAY_NS;
795 let tod_ns = self.nanos % DAY_NS;
796 let tod_s = tod_ns / 1_000_000_000;
797 let tod_ms = (tod_ns % 1_000_000_000) / 1_000_000;
798
799 defmt::write!(f, "{} {}:{:05}.{:03}", S::NAME, day, tod_s, tod_ms)
800 }
801 DisplayStyle::Simple => {
802 let secs = self.nanos / 1_000_000_000;
803 let ns_rem = self.nanos % 1_000_000_000;
804
805 defmt::write!(f, "{} +{}s {}ns", S::NAME, secs, ns_rem)
806 }
807 }
808 }
809}
810
811#[cfg(test)]
816mod tests {
817 #[allow(unused_imports)]
818 use std::format;
819 #[allow(unused_imports)]
820 use std::string::ToString;
821 #[allow(unused_imports)]
822 use std::vec;
823
824 use super::*;
825 use crate::scale::{Beidou, Galileo, Glonass, Gps, Tai, Utc};
826
827 #[test]
828 fn test_size_equals_u64() {
829 assert_eq!(core::mem::size_of::<Time<Gps>>(), 8);
830 assert_eq!(core::mem::size_of::<Time<Glonass>>(), 8);
831 assert_eq!(core::mem::size_of::<Time<Galileo>>(), 8);
832 assert_eq!(core::mem::size_of::<Time<Beidou>>(), 8);
833 assert_eq!(core::mem::size_of::<Time<Utc>>(), 8);
834 assert_eq!(core::mem::size_of::<Time<Tai>>(), 8);
835 }
836
837 #[test]
838 fn test_epoch_is_zero() {
839 assert_eq!(Time::<Gps>::EPOCH.as_nanos(), 0);
840 }
841
842 #[test]
843 fn test_from_week_tow_zero() {
844 let t = Time::<Gps>::from_week_tow(
845 0,
846 DurationParts {
847 seconds: 0,
848 nanos: 0,
849 },
850 )
851 .unwrap();
852
853 assert_eq!(t, Time::<Gps>::EPOCH);
854 }
855
856 #[test]
857 fn test_from_week_tow_roundtrip() {
858 let t = Time::<Gps>::from_week_tow(
859 2345,
860 DurationParts {
861 seconds: 432_000,
862 nanos: 0,
863 },
864 )
865 .unwrap();
866
867 assert_eq!(t.week(), 2345);
868 assert_eq!(t.tow_seconds(), 432_000);
869 assert_eq!(t.sub_second_nanos(), 0);
870 }
871
872 #[test]
873 fn test_from_week_tow_with_fractional() {
874 let t = Time::<Gps>::from_week_tow(
875 2300,
876 DurationParts {
877 seconds: 3661,
878 nanos: 500_000_000,
879 },
880 )
881 .unwrap();
882
883 assert_eq!(t.week(), 2300);
884 assert_eq!(t.tow_seconds(), 3661);
885 assert_eq!(t.sub_second_nanos(), 500_000_000);
886 }
887
888 #[test]
889 fn test_from_week_tow_invalid() {
890 assert!(matches!(
891 Time::<Gps>::from_week_tow(
892 0,
893 DurationParts {
894 seconds: 604_800,
895 nanos: 0
896 }
897 ),
898 Err(GnssTimeError::InvalidInput(_))
899 ));
900 }
901
902 #[test]
903 fn test_from_day_tod_zero() {
904 let t = Time::<Glonass>::from_day_tod(
905 0,
906 DurationParts {
907 seconds: 0,
908 nanos: 0,
909 },
910 )
911 .unwrap();
912
913 assert_eq!(t, Time::<Glonass>::EPOCH);
914 }
915
916 #[test]
917 fn test_from_day_tod_roundtrip() {
918 let t = Time::<Glonass>::from_day_tod(
919 10_512,
920 DurationParts {
921 seconds: 43_200,
922 nanos: 0,
923 },
924 )
925 .unwrap();
926
927 assert_eq!(t.day(), 10_512);
928 assert_eq!(t.tod_seconds(), 43_200);
929 }
930
931 #[test]
932 fn test_from_day_tod_invalid() {
933 assert!(matches!(
934 Time::<Glonass>::from_day_tod(
935 0,
936 DurationParts {
937 seconds: 86_400,
938 nanos: 0
939 }
940 ),
941 Err(GnssTimeError::InvalidInput(_))
942 ));
943 }
944
945 #[test]
946 fn test_add_positive_duration() {
947 let t = Time::<Gps>::from_seconds(100);
948
949 assert_eq!((t + Duration::from_seconds(50)).as_seconds(), 150);
950 }
951
952 #[test]
953 fn test_add_negative_duration_moves_back() {
954 let t = Time::<Gps>::from_seconds(100);
955
956 assert_eq!((t + Duration::from_nanos(-50_000_000_000)).as_seconds(), 50);
957 }
958
959 #[test]
960 fn test_roundtrip_add_sub() {
961 let t = Time::<Galileo>::from_seconds(1_000_000);
962 let d = Duration::from_seconds(12_345);
963
964 assert_eq!(t + d - d, t);
965 }
966
967 #[test]
968 fn test_sub_times_positive() {
969 let a = Time::<Gps>::from_seconds(200);
970 let b = Time::<Gps>::from_seconds(100);
971
972 assert_eq!((a - b).as_seconds(), 100);
973 }
974
975 #[test]
976 fn test_sub_times_negative() {
977 let a = Time::<Gps>::from_seconds(100);
978 let b = Time::<Gps>::from_seconds(200);
979
980 assert_eq!((a - b).as_seconds(), -100);
981 }
982
983 #[test]
984 fn test_sub_same_is_zero() {
985 let t = Time::<Gps>::from_seconds(42);
986
987 assert!((t - t).is_zero());
988 }
989
990 #[test]
991 #[should_panic]
992 fn test_add_overflow_panics() {
993 let _ = Time::<Gps>::MAX + Duration::ONE_NANOSECOND;
994 }
995
996 #[test]
997 fn test_checked_add_overflow() {
998 assert!(Time::<Gps>::MAX
999 .checked_add(Duration::ONE_NANOSECOND)
1000 .is_none());
1001 }
1002
1003 #[test]
1004 fn test_checked_sub_underflow() {
1005 assert!(Time::<Gps>::EPOCH
1006 .checked_sub_duration(Duration::ONE_NANOSECOND)
1007 .is_none());
1008 }
1009
1010 #[test]
1011 fn test_saturating_add_clamps() {
1012 assert_eq!(
1013 Time::<Gps>::MAX.saturating_add(Duration::from_seconds(1)),
1014 Time::<Gps>::MAX
1015 );
1016 }
1017
1018 #[test]
1019 fn test_gps_to_tai_adds_19s() {
1020 let gps = Time::<Gps>::from_seconds(100);
1021 let tai = gps.to_tai().unwrap();
1022
1023 assert_eq!(tai.as_seconds(), 119);
1024 }
1025
1026 #[test]
1027 fn test_tai_to_gps_subtracts_19s() {
1028 let tai = Time::<Tai>::from_seconds(119);
1029 let gps = Time::<Gps>::from_tai(tai).unwrap();
1030
1031 assert_eq!(gps.as_seconds(), 100);
1032 }
1033
1034 #[test]
1035 fn test_roundtrip_via_tai() {
1036 let original = Time::<Gps>::from_seconds(5_000_000);
1037 let back = Time::<Gps>::from_tai(original.to_tai().unwrap()).unwrap();
1038
1039 assert_eq!(original, back);
1040 }
1041
1042 #[test]
1043 fn test_gps_galileo_identity_via_tai() {
1044 let gps = Time::<Gps>::from_seconds(12_345);
1046 let gal = gps.try_convert::<Galileo>().unwrap();
1047
1048 assert_eq!(gps.as_nanos(), gal.as_nanos());
1049 }
1050
1051 #[test]
1052 fn test_gps_to_beidou_via_tai() {
1053 let gps = Time::<Gps>::from_seconds(100);
1055 let bdt = gps.try_convert::<Beidou>().unwrap();
1056
1057 assert_eq!(bdt.as_seconds(), 86);
1058 }
1059
1060 #[test]
1061 fn test_contextual_scale_to_tai_fails() {
1062 let glo = Time::<Glonass>::from_seconds(100);
1063
1064 assert!(matches!(
1065 glo.to_tai(),
1066 Err(GnssTimeError::LeapSecondsRequired)
1067 ));
1068 }
1069
1070 #[test]
1071 fn test_tai_to_contextual_fails() {
1072 let tai = Time::<Tai>::from_seconds(100);
1073
1074 assert!(matches!(
1075 Time::<Utc>::from_tai(tai),
1076 Err(GnssTimeError::LeapSecondsRequired)
1077 ));
1078 }
1079
1080 #[test]
1081 fn test_to_tai_overflow() {
1082 let t = Time::<Gps>::from_nanos(u64::MAX);
1083
1084 assert!(matches!(t.to_tai(), Err(GnssTimeError::Overflow)));
1085 }
1086
1087 #[test]
1088 fn test_from_tai_underflow() {
1089 let tai = Time::<Tai>::from_nanos(0);
1091
1092 assert!(matches!(
1093 Time::<Gps>::from_tai(tai),
1094 Err(GnssTimeError::Overflow)
1095 ));
1096 }
1097
1098 #[test]
1099 fn test_gps_display_week_tow_format() {
1100 let t = Time::<Gps>::from_week_tow(
1101 2345,
1102 DurationParts {
1103 seconds: 432_000,
1104 nanos: 0,
1105 },
1106 )
1107 .unwrap();
1108
1109 assert_eq!(t.to_string(), "GPS 2345:432000.000");
1110 }
1111
1112 #[test]
1113 fn test_gps_display_epoch_is_week_0() {
1114 let s = Time::<Gps>::EPOCH.to_string();
1115
1116 assert_eq!(s, "GPS 0:000000.000");
1117 }
1118
1119 #[test]
1120 fn test_gps_display_tow_zero_padded() {
1121 let t = Time::<Gps>::from_week_tow(
1123 1,
1124 DurationParts {
1125 seconds: 1,
1126 nanos: 0,
1127 },
1128 )
1129 .unwrap();
1130
1131 assert_eq!(t.to_string(), "GPS 1:000001.000");
1132 }
1133
1134 #[test]
1135 fn test_gps_display_with_millis() {
1136 let t = Time::<Gps>::from_week_tow(
1137 100,
1138 DurationParts {
1139 seconds: 0,
1140 nanos: 500_000_000,
1141 },
1142 )
1143 .unwrap();
1144
1145 assert_eq!(t.to_string(), "GPS 100:000000.500");
1146 }
1147
1148 #[test]
1149 fn test_glonass_display_day_tod_format() {
1150 let t = Time::<Glonass>::from_day_tod(
1151 10_512,
1152 DurationParts {
1153 seconds: 43_200,
1154 nanos: 0,
1155 },
1156 )
1157 .unwrap();
1158
1159 assert_eq!(t.to_string(), "GLO 10512:43200.000");
1160 }
1161
1162 #[test]
1163 fn test_glonass_display_epoch() {
1164 let s = Time::<Glonass>::EPOCH.to_string();
1165
1166 assert_eq!(s, "GLO 0:00000.000");
1167 }
1168
1169 #[test]
1170 fn test_galileo_display_week_format() {
1171 let s = Time::<Galileo>::EPOCH.to_string();
1172
1173 assert!(s.starts_with("GAL "));
1174 assert!(s.contains(':'));
1175 }
1176
1177 #[test]
1178 fn test_tai_display_simple_format() {
1179 let t = Time::<Tai>::from_seconds(1_000_000_000);
1180 let s = t.to_string();
1181
1182 assert!(s.starts_with("TAI +"));
1183 assert!(s.contains("1000000000s"));
1184 }
1185
1186 #[test]
1187 fn test_utc_display_simple_format() {
1188 let s = Time::<Utc>::EPOCH.to_string();
1189
1190 assert!(s.starts_with("UTC +"));
1191 }
1192
1193 #[test]
1194 fn test_debug_shows_scale_and_nanos() {
1195 let t = Time::<Glonass>::from_nanos(777);
1196 let s = format!("{t:?}");
1197
1198 assert!(s.contains("GLO") && s.contains("777"));
1199 }
1200
1201 #[test]
1202 fn test_ordering() {
1203 let t0 = Time::<Gps>::from_seconds(0);
1204 let t1 = Time::<Gps>::from_seconds(1);
1205 let t2 = Time::<Gps>::from_seconds(2);
1206 let mut v = vec![t2, t0, t1];
1207
1208 v.sort();
1209
1210 assert_eq!(v, vec![t0, t1, t2]);
1211 }
1212
1213 #[test]
1214 fn test_glonass_day_accessor() {
1215 let t = Time::<Glonass>::from_day_tod(
1216 42,
1217 DurationParts {
1218 seconds: 3600,
1219 nanos: 0,
1220 },
1221 )
1222 .unwrap();
1223
1224 assert_eq!(t.day(), 42);
1225 assert_eq!(t.tod_seconds(), 3600);
1226 }
1227
1228 #[test]
1229 fn test_time_max_behavior() {
1230 let max = Time::<Gps>::MAX;
1231 let one_ns = Duration::ONE_NANOSECOND;
1232
1233 assert!(max.checked_add(one_ns).is_none());
1235
1236 assert_eq!(max.saturating_add(one_ns), max);
1238
1239 assert!(max.try_add(one_ns).is_err());
1241 }
1242
1243 #[test]
1244 fn test_max_is_u64_max() {
1245 assert_eq!(Time::<Gps>::MAX.as_nanos(), u64::MAX);
1246 assert_eq!(Time::<Glonass>::MAX.as_nanos(), u64::MAX);
1247 assert_eq!(Time::<Galileo>::MAX.as_nanos(), u64::MAX);
1248 assert_eq!(Time::<Beidou>::MAX.as_nanos(), u64::MAX);
1249 assert_eq!(Time::<Tai>::MAX.as_nanos(), u64::MAX);
1250 assert_eq!(Time::<Utc>::MAX.as_nanos(), u64::MAX);
1251 }
1252
1253 #[test]
1254 fn test_nanos_per_year_is_correct() {
1255 let expected: u64 = 365 * 24 * 3_600 * 1_000_000_000;
1256
1257 assert_eq!(Time::<Gps>::NANOS_PER_YEAR, expected);
1258 }
1259
1260 #[test]
1261 fn test_max_covers_at_least_500_years() {
1262 let years = Time::<Gps>::MAX.as_nanos() / Time::<Gps>::NANOS_PER_YEAR;
1263
1264 assert!(
1265 years >= 500,
1266 "MAX should cover at least 500 years, got {years}"
1267 );
1268 }
1269
1270 #[test]
1271 fn test_checked_add_one_ns_before_max_succeeds() {
1272 let t = Time::<Gps>::from_nanos(u64::MAX - 1);
1273 let result = t.checked_add(Duration::from_nanos(1));
1274
1275 assert_eq!(result, Some(Time::<Gps>::MAX));
1276 }
1277
1278 #[test]
1279 fn test_checked_add_at_max_overflows() {
1280 assert!(Time::<Gps>::MAX
1281 .checked_add(Duration::from_nanos(1))
1282 .is_none());
1283 }
1284
1285 #[test]
1286 fn test_checked_add_large_positive_overflows() {
1287 let t = Time::<Gps>::from_nanos(u64::MAX - 100);
1288
1289 assert!(t.checked_add(Duration::from_seconds(1)).is_none());
1290 }
1291
1292 #[test]
1293 fn test_checked_sub_one_ns_after_epoch_succeeds() {
1294 let t = Time::<Gps>::from_nanos(1);
1295 let result = t.checked_sub_duration(Duration::from_nanos(1));
1296
1297 assert_eq!(result, Some(Time::<Gps>::EPOCH));
1298 }
1299
1300 #[test]
1301 fn test_checked_sub_at_epoch_underflows() {
1302 assert!(Time::<Gps>::EPOCH
1303 .checked_sub_duration(Duration::from_nanos(1))
1304 .is_none());
1305 }
1306
1307 #[test]
1308 fn test_checked_sub_large_amount_underflows() {
1309 let t = Time::<Gps>::from_nanos(50);
1310
1311 assert!(t.checked_sub_duration(Duration::from_seconds(1)).is_none());
1312 }
1313
1314 #[test]
1315 fn test_saturating_add_clamps_at_max() {
1316 assert_eq!(
1317 Time::<Gps>::MAX.saturating_add(Duration::from_nanos(1)),
1318 Time::<Gps>::MAX
1319 );
1320 assert_eq!(
1321 Time::<Gps>::MAX.saturating_add(Duration::from_seconds(9999)),
1322 Time::<Gps>::MAX
1323 );
1324 }
1325
1326 #[test]
1327 fn test_saturating_add_negative_clamps_at_epoch() {
1328 assert_eq!(
1329 Time::<Gps>::EPOCH.saturating_add(Duration::from_nanos(-1)),
1330 Time::<Gps>::EPOCH
1331 );
1332 }
1333
1334 #[test]
1335 fn test_saturating_add_normal_value_works() {
1336 let t = Time::<Gps>::from_seconds(100);
1337
1338 assert_eq!(
1339 t.saturating_add(Duration::from_seconds(50)),
1340 Time::<Gps>::from_seconds(150)
1341 );
1342 }
1343
1344 #[test]
1345 fn test_saturating_sub_clamps_at_epoch() {
1346 assert_eq!(
1347 Time::<Gps>::EPOCH.saturating_sub_duration(Duration::from_nanos(1)),
1348 Time::<Gps>::EPOCH
1349 );
1350 }
1351
1352 #[test]
1353 fn test_saturating_sub_normal_value_works() {
1354 let t = Time::<Gps>::from_seconds(100);
1355
1356 assert_eq!(
1357 t.saturating_sub_duration(Duration::from_seconds(30)),
1358 Time::<Gps>::from_seconds(70)
1359 );
1360 }
1361
1362 #[test]
1363 fn test_try_add_overflow_returns_err() {
1364 let result = Time::<Gps>::MAX.try_add(Duration::from_nanos(1));
1365
1366 assert!(matches!(result, Err(GnssTimeError::Overflow)));
1367 }
1368
1369 #[test]
1370 fn test_try_sub_duration_underflow_returns_err() {
1371 let result = Time::<Gps>::EPOCH.try_sub_duration(Duration::from_nanos(1));
1372
1373 assert!(matches!(result, Err(GnssTimeError::Overflow)));
1374 }
1375
1376 #[test]
1377 fn test_try_add_valid_value_works() {
1378 let t = Time::<Gps>::from_seconds(1_000);
1379 let result = t.try_add(Duration::from_seconds(500)).unwrap();
1380
1381 assert_eq!(result.as_seconds(), 1_500);
1382 }
1383
1384 #[test]
1385 #[should_panic]
1386 fn test_add_operator_panics_at_max() {
1387 let _ = Time::<Gps>::MAX + Duration::from_nanos(1);
1388 }
1389
1390 #[test]
1391 #[should_panic]
1392 fn test_sub_operator_panics_at_epoch() {
1393 let _ = Time::<Gps>::EPOCH - Duration::from_nanos(1);
1394 }
1395
1396 #[test]
1397 fn test_checked_elapsed_zero_gives_zero_duration() {
1398 let t = Time::<Gps>::from_seconds(1_000);
1399 assert_eq!(t.checked_elapsed(t), Some(Duration::ZERO));
1400 }
1401
1402 #[test]
1403 fn test_checked_elapsed_overflows_when_gap_exceeds_i64() {
1404 let result = Time::<Gps>::MAX.checked_elapsed(Time::<Gps>::EPOCH);
1407
1408 assert!(result.is_none(), "gap exceeds i64::MAX so must return None");
1409 }
1410
1411 #[test]
1412 fn test_checked_elapsed_within_i64_range_works() {
1413 let a = Time::<Gps>::from_seconds(1_000_000);
1414 let b = Time::<Gps>::from_seconds(500_000);
1415 let elapsed = a.checked_elapsed(b).unwrap();
1416
1417 assert_eq!(elapsed.as_seconds(), 500_000);
1418 }
1419}