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