1use core::{
70 fmt,
71 marker::PhantomData,
72 ops::{Add, AddAssign, Sub, SubAssign},
73};
74
75use crate::{
76 gps_to_utc, utc_to_gps, CivilDateTime, DisplayStyle, Duration, Glonass, GnssTimeError, Gps,
77 LeapSeconds, LeapSecondsProvider, OffsetToTai, Tai, TimeScale, Utc, UTC_EPOCH_UNIX_OFFSET_NS,
78 UTC_EPOCH_UNIX_OFFSET_S,
79};
80
81#[derive(Copy, Clone, Eq, PartialEq, Hash)]
99#[must_use = "Time<S> is a value type; ignoring it has no effect"]
100pub struct Time<S: TimeScale> {
101 nanos: u64,
102 _scale: PhantomData<S>,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
110pub struct DurationParts {
111 pub seconds: u64,
113
114 pub nanos: u32,
116}
117
118#[cfg(feature = "serde")]
119#[expect(dead_code)]
120struct TimeVisitor<S>(PhantomData<S>);
121
122impl<S: TimeScale> Time<S> {
123 pub const EPOCH: Self = Time {
128 nanos: 0,
129 _scale: PhantomData,
130 };
131
132 pub const MIN: Self = Self::EPOCH;
134
135 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;
152
153 #[inline]
155 pub const fn from_nanos(nanos: u64) -> Self {
156 Time {
157 nanos,
158 _scale: PhantomData,
159 }
160 }
161
162 #[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 #[inline]
187 #[must_use]
188 pub const fn as_nanos(self) -> u64 {
189 self.nanos
190 }
191
192 #[inline]
194 #[must_use]
195 pub const fn as_seconds(self) -> u64 {
196 self.nanos / 1_000_000_000
197 }
198
199 #[inline]
202 #[must_use]
203 pub const fn as_parts(self) -> (u64, u32) {
204 (
205 self.nanos / 1_000_000_000,
206 (self.nanos % 1_000_000_000) as u32,
207 )
208 }
209
210 pub fn to_tai(self) -> Result<Time<Tai>, GnssTimeError> {
221 match S::OFFSET_TO_TAI {
222 OffsetToTai::Fixed(offset) => {
223 let nanos = i128::from(self.nanos) + i128::from(offset);
224
225 if nanos < 0 || nanos > i128::from(u64::MAX) {
226 return Err(GnssTimeError::Overflow);
227 }
228
229 let nanos = u64::try_from(nanos).map_err(|_| GnssTimeError::Overflow)?;
230
231 Ok(Time::from_nanos(nanos))
232 }
233 OffsetToTai::Contextual => Err(GnssTimeError::LeapSecondsRequired),
234 }
235 }
236
237 pub fn from_tai(tai: Time<Tai>) -> Result<Self, GnssTimeError> {
248 match S::OFFSET_TO_TAI {
249 OffsetToTai::Fixed(offset) => {
250 let nanos = i128::from(tai.as_nanos()) - i128::from(offset);
251
252 if nanos < 0 || nanos > i128::from(u64::MAX) {
253 return Err(GnssTimeError::Overflow);
254 }
255
256 let nanos = u64::try_from(nanos).map_err(|_| GnssTimeError::Overflow)?;
257
258 Ok(Time::from_nanos(nanos))
259 }
260 OffsetToTai::Contextual => Err(GnssTimeError::LeapSecondsRequired),
261 }
262 }
263
264 pub fn try_convert<T: TimeScale>(self) -> Result<Time<T>, GnssTimeError> {
276 let tai = self.to_tai()?;
277
278 Time::<T>::from_tai(tai)
279 }
280
281 #[inline]
283 #[must_use = "returns None on overflow; check the result"]
284 pub fn checked_add(
285 self,
286 d: Duration,
287 ) -> Option<Self> {
288 let result = i128::from(self.nanos) + i128::from(d.as_nanos());
289 let nanos = u64::try_from(result).ok()?;
290
291 Some(Time::from_nanos(nanos))
292 }
293
294 #[inline]
296 #[must_use = "returns None on underflow; check the result"]
297 pub fn checked_sub_duration(
298 self,
299 d: Duration,
300 ) -> Option<Self> {
301 let result = i128::from(self.nanos) - i128::from(d.as_nanos());
302 let nanos = u64::try_from(result).ok()?;
303
304 Some(Time::from_nanos(nanos))
305 }
306
307 #[inline]
309 #[must_use = "saturating_add returns a new Time<S>; the original is unchanged"]
310 pub fn saturating_add(
311 self,
312 d: Duration,
313 ) -> Self {
314 self.checked_add(d).unwrap_or(if d.is_negative() {
315 Time::EPOCH
316 } else {
317 Time::MAX
318 })
319 }
320
321 #[inline]
323 #[must_use = "saturating_sub_duration returns a new Time<S>; the original is unchanged"]
324 pub fn saturating_sub_duration(
325 self,
326 d: Duration,
327 ) -> Self {
328 self.checked_sub_duration(d).unwrap_or(if d.is_negative() {
329 Time::MAX
330 } else {
331 Time::EPOCH
332 })
333 }
334
335 #[inline]
346 pub fn try_add(
347 self,
348 d: Duration,
349 ) -> Result<Self, GnssTimeError> {
350 self.checked_add(d).ok_or(GnssTimeError::Overflow)
351 }
352
353 #[inline]
364 pub fn try_sub_duration(
365 self,
366 d: Duration,
367 ) -> Result<Self, GnssTimeError> {
368 self.checked_sub_duration(d).ok_or(GnssTimeError::Overflow)
369 }
370
371 #[inline]
373 #[must_use = "returns None on overflow; check the result"]
374 pub fn checked_elapsed(
375 self,
376 earlier: Time<S>,
377 ) -> Option<Duration> {
378 let diff = i128::from(self.nanos) - i128::from(earlier.nanos);
379
380 let diff = i64::try_from(diff).ok()?;
381 Some(Duration::from_nanos(diff))
382 }
383}
384
385impl<S: TimeScale> Add<Duration> for Time<S> {
386 type Output = Time<S>;
387
388 #[inline]
389 fn add(
390 self,
391 rhs: Duration,
392 ) -> Time<S> {
393 self.checked_add(rhs)
394 .expect("Time<S> + Duration overflowed")
395 }
396}
397
398impl<S: TimeScale> AddAssign<Duration> for Time<S> {
399 #[inline]
400 fn add_assign(
401 &mut self,
402 rhs: Duration,
403 ) {
404 *self = *self + rhs;
405 }
406}
407
408impl<S: TimeScale> Sub<Duration> for Time<S> {
409 type Output = Time<S>;
410
411 #[inline]
412 fn sub(
413 self,
414 rhs: Duration,
415 ) -> Self::Output {
416 self.checked_sub_duration(rhs)
417 .expect("Time<S> - Duration underflowed")
418 }
419}
420
421impl<S: TimeScale> SubAssign<Duration> for Time<S> {
422 #[inline]
423 fn sub_assign(
424 &mut self,
425 rhs: Duration,
426 ) {
427 *self = *self - rhs;
428 }
429}
430
431impl<S: TimeScale> Sub<Time<S>> for Time<S> {
432 type Output = Duration;
433
434 #[inline]
435 fn sub(
436 self,
437 rhs: Time<S>,
438 ) -> Self::Output {
439 self.checked_elapsed(rhs)
440 .expect("Time<S> - Time<S> overflowed i64")
441 }
442}
443
444impl DurationParts {
445 pub const NANOS_PER_SECOND: u32 = 1_000_000_000;
447
448 #[inline]
467 pub const fn new(
468 seconds: u64,
469 nanos: u32,
470 ) -> Result<Self, GnssTimeError> {
471 if nanos >= Self::NANOS_PER_SECOND {
472 return Err(GnssTimeError::InvalidInput(
473 "nanos must be in [0, 1_000_000_000)",
474 ));
475 }
476
477 Ok(Self { seconds, nanos })
478 }
479
480 #[inline]
495 #[must_use]
496 pub const fn as_nanos(self) -> u128 {
497 (self.seconds as u128) * Self::NANOS_PER_SECOND as u128 + self.nanos as u128
498 }
499}
500
501impl Time<Glonass> {
502 pub fn from_day_tod(
511 day: u32,
512 tod: DurationParts,
513 ) -> Result<Self, GnssTimeError> {
514 if tod.seconds >= 86_400 {
515 return Err(GnssTimeError::InvalidInput(
516 "tod.seconds must be in [0, 86_400)",
517 ));
518 }
519 if tod.nanos >= DurationParts::NANOS_PER_SECOND {
520 return Err(GnssTimeError::InvalidInput(
521 "tod.nanos must be in [0, 1_000_000_000)",
522 ));
523 }
524
525 let day_ns = u64::from(day)
526 .checked_mul(86_400_000_000_000)
527 .ok_or(GnssTimeError::Overflow)?;
528 let tod_ns = tod
529 .seconds
530 .checked_mul(1_000_000_000)
531 .ok_or(GnssTimeError::Overflow)?
532 .checked_add(u64::from(tod.nanos))
533 .ok_or(GnssTimeError::Overflow)?;
534 let total = day_ns.checked_add(tod_ns).ok_or(GnssTimeError::Overflow)?;
535
536 Ok(Time::from_nanos(total))
537 }
538
539 #[inline]
541 #[must_use]
542 pub const fn day(self) -> u32 {
543 (self.nanos / 86_400_000_000_000u64) as u32
544 }
545
546 #[inline]
548 #[must_use]
549 pub const fn tod_seconds(self) -> u32 {
550 ((self.nanos % 86_400_000_000_000u64) / 1_000_000_000u64) as u32
551 }
552
553 #[inline]
555 #[must_use]
556 pub const fn sub_second_nanos(self) -> u32 {
557 (self.nanos % 1_000_000_000u64) as u32
558 }
559
560 #[inline]
616 #[must_use]
617 pub const fn day_of_week(self) -> u8 {
618 (self.day() % 7) as u8 + 1
620 }
621
622 #[inline]
624 #[must_use]
625 pub const fn is_weekend(self) -> bool {
626 let d = self.day_of_week();
627
628 d == 6 || d == 7
629 }
630}
631
632impl Time<Gps> {
633 pub fn from_week_tow(
647 week: u16,
648 tow: DurationParts,
649 ) -> Result<Self, GnssTimeError> {
650 if tow.seconds >= 604_800 {
651 return Err(GnssTimeError::InvalidInput(
652 "tow.seconds must be in [0, 604_800)",
653 ));
654 }
655
656 if tow.nanos >= DurationParts::NANOS_PER_SECOND {
657 return Err(GnssTimeError::InvalidInput(
658 "tow.nanos must be in [0, 1_000_000_000)",
659 ));
660 }
661
662 let week_ns = u64::from(week)
663 .checked_mul(604_800_000_000_000)
664 .ok_or(GnssTimeError::Overflow)?;
665
666 let tow_ns = tow
667 .seconds
668 .checked_mul(1_000_000_000)
669 .ok_or(GnssTimeError::Overflow)?
670 .checked_add(u64::from(tow.nanos))
671 .ok_or(GnssTimeError::Overflow)?;
672
673 let total = week_ns.checked_add(tow_ns).ok_or(GnssTimeError::Overflow)?;
674
675 Ok(Time::from_nanos(total))
676 }
677
678 pub fn from_unix_seconds<P: LeapSecondsProvider>(
690 unix_seconds: i64,
691 ls: P,
692 ) -> Result<Self, GnssTimeError> {
693 let utc = Time::<Utc>::from_unix_seconds(unix_seconds)?;
694
695 utc_to_gps(utc, &ls)
696 }
697
698 pub fn as_unix_seconds<P: LeapSecondsProvider>(
716 self,
717 ls: P,
718 ) -> Result<i64, GnssTimeError> {
719 let utc = gps_to_utc(self, &ls)?;
720
721 Ok(utc.as_unix_seconds())
722 }
723
724 pub fn to_utc(self) -> Result<Time<Utc>, GnssTimeError> {
740 gps_to_utc(self, LeapSeconds::builtin())
741 }
742
743 pub fn to_utc_with<P: LeapSecondsProvider>(
757 self,
758 ls: &P,
759 ) -> Result<Time<Utc>, GnssTimeError> {
760 gps_to_utc(self, ls)
761 }
762
763 #[inline]
765 #[must_use]
766 pub const fn week(self) -> u32 {
767 (self.nanos / 604_800_000_000_000u64) as u32
768 }
769
770 #[inline]
772 #[must_use]
773 pub const fn tow_seconds(self) -> u32 {
774 ((self.nanos % 604_800_000_000_000u64) / 1_000_000_000u64) as u32
775 }
776
777 #[inline]
779 #[must_use]
780 pub const fn sub_second_nanos(self) -> u32 {
781 (self.nanos % 1_000_000_000u64) as u32
782 }
783}
784
785impl Time<Utc> {
786 pub fn from_unix_seconds(unix_seconds: i64) -> Result<Self, GnssTimeError> {
815 let utc_s = unix_seconds
817 .checked_sub(UTC_EPOCH_UNIX_OFFSET_S)
818 .ok_or(GnssTimeError::Overflow)?;
819
820 if utc_s < 0 {
821 return Err(GnssTimeError::Overflow);
822 }
823
824 let nanos = u64::try_from(utc_s)
825 .map_err(|_| GnssTimeError::Overflow)?
826 .checked_mul(1_000_000_000)
827 .ok_or(GnssTimeError::Overflow)?;
828
829 Ok(Time::from_nanos(nanos))
830 }
831
832 pub fn from_unix_nanos(unix_nanos: i64) -> Result<Self, GnssTimeError> {
856 let utc_ns = unix_nanos
858 .checked_sub(UTC_EPOCH_UNIX_OFFSET_NS)
859 .ok_or(GnssTimeError::Overflow)?;
860
861 if utc_ns < 0 {
862 return Err(GnssTimeError::Overflow);
863 }
864
865 let nanos = u64::try_from(utc_ns).map_err(|_| GnssTimeError::Overflow)?;
866
867 Ok(Time::from_nanos(nanos))
868 }
869
870 #[inline]
897 #[must_use]
898 pub fn as_unix_seconds(self) -> i64 {
899 let secs = i128::from(self.nanos / 1_000_000_000) + i128::from(UTC_EPOCH_UNIX_OFFSET_S);
900
901 i64::try_from(secs).unwrap_or(i64::MAX)
902 }
903
904 #[inline]
927 #[must_use]
928 pub fn as_unix_nanos(self) -> i64 {
929 i64::try_from(self.nanos).map_or(i64::MAX, |ns| ns.saturating_add(UTC_EPOCH_UNIX_OFFSET_NS))
932 }
933
934 pub fn to_gps(self) -> Result<Time<Gps>, GnssTimeError> {
944 utc_to_gps(self, LeapSeconds::builtin())
945 }
946
947 pub fn to_gps_with<P: LeapSecondsProvider>(
958 self,
959 ls: &P,
960 ) -> Result<Time<Gps>, GnssTimeError> {
961 utc_to_gps(self, ls)
962 }
963
964 #[must_use]
993 pub fn to_civil(self) -> CivilDateTime {
994 CivilDateTime::from_utc_nanos(self.as_nanos()).unwrap()
995 }
996}
997
998impl<S: TimeScale> PartialOrd for Time<S> {
999 #[inline]
1000 fn partial_cmp(
1001 &self,
1002 other: &Self,
1003 ) -> Option<core::cmp::Ordering> {
1004 Some(self.cmp(other))
1005 }
1006}
1007
1008impl<S: TimeScale> Ord for Time<S> {
1009 #[inline]
1010 fn cmp(
1011 &self,
1012 other: &Self,
1013 ) -> core::cmp::Ordering {
1014 self.nanos.cmp(&other.nanos)
1015 }
1016}
1017
1018impl<S: TimeScale> fmt::Debug for Time<S> {
1019 fn fmt(
1020 &self,
1021 f: &mut fmt::Formatter<'_>,
1022 ) -> fmt::Result {
1023 write!(f, "Time<{}>({}ns)", S::NAME, self.nanos)
1024 }
1025}
1026
1027impl<S: TimeScale> fmt::Display for Time<S> {
1028 fn fmt(
1036 &self,
1037 f: &mut fmt::Formatter<'_>,
1038 ) -> fmt::Result {
1039 match S::DISPLAY_STYLE {
1040 DisplayStyle::WeekTow => {
1041 const WEEK_NS: u64 = 604_800_000_000_000;
1042 let week = self.nanos / WEEK_NS;
1043 let tow_ns = self.nanos % WEEK_NS;
1044 let tow_s = tow_ns / 1_000_000_000;
1045 let tow_ms = (tow_ns % 1_000_000_000) / 1_000_000;
1046
1047 write!(f, "{} {}:{:06}.{:03}", S::NAME, week, tow_s, tow_ms)
1048 }
1049 DisplayStyle::DayTod => {
1050 const DAY_NS: u64 = 86_400_000_000_000;
1051 let day = self.nanos / DAY_NS;
1052 let tod_ns = self.nanos % DAY_NS;
1053 let tod_s = tod_ns / 1_000_000_000;
1054 let tod_ms = (tod_ns % 1_000_000_000) / 1_000_000;
1055
1056 write!(f, "{} {}:{:05}.{:03}", S::NAME, day, tod_s, tod_ms)
1057 }
1058 DisplayStyle::Simple => {
1059 let secs = self.nanos / 1_000_000_000;
1060 let ns_rem = self.nanos % 1_000_000_000;
1061
1062 write!(f, "{} +{}s {}ns", S::NAME, secs, ns_rem)
1063 }
1064 }
1065 }
1066}
1067
1068#[cfg(feature = "defmt")]
1071impl<S: TimeScale> defmt::Format for Time<S> {
1072 fn format(
1073 &self,
1074 f: defmt::Formatter,
1075 ) {
1076 match S::DISPLAY_STYLE {
1077 DisplayStyle::WeekTow => {
1078 const WEEK_NS: u64 = 604_800_000_000_000;
1079 let week = self.nanos / WEEK_NS;
1080 let tow_ns = self.nanos % WEEK_NS;
1081 let tow_s = tow_ns / 1_000_000_000;
1082 let tow_ms = (tow_ns % 1_000_000_000) / 1_000_000;
1083
1084 defmt::write!(f, "{} {}:{:06}.{:03}", S::NAME, week, tow_s, tow_ms);
1085 }
1086 DisplayStyle::DayTod => {
1087 const DAY_NS: u64 = 86_400_000_000_000;
1088 let day = self.nanos / DAY_NS;
1089 let tod_ns = self.nanos % DAY_NS;
1090 let tod_s = tod_ns / 1_000_000_000;
1091 let tod_ms = (tod_ns % 1_000_000_000) / 1_000_000;
1092
1093 defmt::write!(f, "{} {}:{:05}.{:03}", S::NAME, day, tod_s, tod_ms);
1094 }
1095 DisplayStyle::Simple => {
1096 let secs = self.nanos / 1_000_000_000;
1097 let ns_rem = self.nanos % 1_000_000_000;
1098
1099 defmt::write!(f, "{} +{}s {}ns", S::NAME, secs, ns_rem);
1100 }
1101 }
1102 }
1103}
1104
1105#[cfg(test)]
1110mod tests {
1111 #[allow(unused_imports)]
1112 use std::format;
1113 #[allow(unused_imports)]
1114 use std::string::ToString;
1115 #[allow(unused_imports)]
1116 use std::vec;
1117
1118 use super::*;
1119 use crate::scale::{Beidou, Galileo, Glonass, Gps, Tai, Utc};
1120
1121 #[test]
1122 fn test_size_equals_u64() {
1123 assert_eq!(core::mem::size_of::<Time<Gps>>(), 8);
1124 assert_eq!(core::mem::size_of::<Time<Glonass>>(), 8);
1125 assert_eq!(core::mem::size_of::<Time<Galileo>>(), 8);
1126 assert_eq!(core::mem::size_of::<Time<Beidou>>(), 8);
1127 assert_eq!(core::mem::size_of::<Time<Utc>>(), 8);
1128 assert_eq!(core::mem::size_of::<Time<Tai>>(), 8);
1129 }
1130
1131 #[test]
1132 fn test_epoch_is_zero() {
1133 assert_eq!(Time::<Gps>::EPOCH.as_nanos(), 0);
1134 assert_eq!(Time::<Utc>::EPOCH.as_nanos(), 0);
1135 }
1136
1137 #[test]
1138 fn test_from_week_tow_zero() {
1139 let t = Time::<Gps>::from_week_tow(
1140 0,
1141 DurationParts {
1142 seconds: 0,
1143 nanos: 0,
1144 },
1145 )
1146 .unwrap();
1147
1148 assert_eq!(t, Time::<Gps>::EPOCH);
1149 }
1150
1151 #[test]
1152 fn test_from_week_tow_roundtrip() {
1153 let t = Time::<Gps>::from_week_tow(
1154 2345,
1155 DurationParts {
1156 seconds: 432_000,
1157 nanos: 0,
1158 },
1159 )
1160 .unwrap();
1161
1162 assert_eq!(t.week(), 2345);
1163 assert_eq!(t.tow_seconds(), 432_000);
1164 assert_eq!(t.sub_second_nanos(), 0);
1165 }
1166
1167 #[test]
1168 fn test_from_week_tow_with_fractional() {
1169 let t = Time::<Gps>::from_week_tow(
1170 2300,
1171 DurationParts {
1172 seconds: 3661,
1173 nanos: 500_000_000,
1174 },
1175 )
1176 .unwrap();
1177
1178 assert_eq!(t.week(), 2300);
1179 assert_eq!(t.tow_seconds(), 3661);
1180 assert_eq!(t.sub_second_nanos(), 500_000_000);
1181 }
1182
1183 #[test]
1184 fn test_from_week_tow_invalid() {
1185 assert!(matches!(
1186 Time::<Gps>::from_week_tow(
1187 0,
1188 DurationParts {
1189 seconds: 604_800,
1190 nanos: 0
1191 }
1192 ),
1193 Err(GnssTimeError::InvalidInput(_))
1194 ));
1195 }
1196
1197 #[test]
1198 fn test_from_day_tod_zero() {
1199 let t = Time::<Glonass>::from_day_tod(
1200 0,
1201 DurationParts {
1202 seconds: 0,
1203 nanos: 0,
1204 },
1205 )
1206 .unwrap();
1207
1208 assert_eq!(t, Time::<Glonass>::EPOCH);
1209 }
1210
1211 #[test]
1212 fn test_from_day_tod_roundtrip() {
1213 let t = Time::<Glonass>::from_day_tod(
1214 10_512,
1215 DurationParts {
1216 seconds: 43_200,
1217 nanos: 0,
1218 },
1219 )
1220 .unwrap();
1221
1222 assert_eq!(t.day(), 10_512);
1223 assert_eq!(t.tod_seconds(), 43_200);
1224 }
1225
1226 #[test]
1227 fn test_from_day_tod_invalid() {
1228 assert!(matches!(
1229 Time::<Glonass>::from_day_tod(
1230 0,
1231 DurationParts {
1232 seconds: 86_400,
1233 nanos: 0
1234 }
1235 ),
1236 Err(GnssTimeError::InvalidInput(_))
1237 ));
1238 }
1239
1240 #[test]
1241 fn test_add_positive_duration() {
1242 let t = Time::<Gps>::from_seconds(100);
1243
1244 assert_eq!((t + Duration::from_seconds(50)).as_seconds(), 150);
1245 }
1246
1247 #[test]
1248 fn test_add_negative_duration_moves_back() {
1249 let t = Time::<Gps>::from_seconds(100);
1250
1251 assert_eq!((t + Duration::from_nanos(-50_000_000_000)).as_seconds(), 50);
1252 }
1253
1254 #[test]
1255 fn test_roundtrip_add_sub() {
1256 let t = Time::<Galileo>::from_seconds(1_000_000);
1257 let d = Duration::from_seconds(12_345);
1258
1259 assert_eq!(t + d - d, t);
1260 }
1261
1262 #[test]
1263 fn test_sub_times_positive() {
1264 let a = Time::<Gps>::from_seconds(200);
1265 let b = Time::<Gps>::from_seconds(100);
1266
1267 assert_eq!((a - b).as_seconds(), 100);
1268 }
1269
1270 #[test]
1271 fn test_sub_times_negative() {
1272 let a = Time::<Gps>::from_seconds(100);
1273 let b = Time::<Gps>::from_seconds(200);
1274
1275 assert_eq!((a - b).as_seconds(), -100);
1276 }
1277
1278 #[test]
1279 fn test_sub_same_is_zero() {
1280 let t = Time::<Gps>::from_seconds(42);
1281
1282 assert!((t - t).is_zero());
1283 }
1284
1285 #[test]
1286 #[should_panic(expected = "Time<S> + Duration overflowed")]
1287 fn test_add_overflow_panics() {
1288 let _ = Time::<Gps>::MAX + Duration::ONE_NANOSECOND;
1289 }
1290
1291 #[test]
1292 fn test_checked_add_overflow() {
1293 assert!(Time::<Gps>::MAX
1294 .checked_add(Duration::ONE_NANOSECOND)
1295 .is_none());
1296 }
1297
1298 #[test]
1299 fn test_checked_sub_underflow() {
1300 assert!(Time::<Gps>::EPOCH
1301 .checked_sub_duration(Duration::ONE_NANOSECOND)
1302 .is_none());
1303 }
1304
1305 #[test]
1306 fn test_saturating_add_clamps() {
1307 assert_eq!(
1308 Time::<Gps>::MAX.saturating_add(Duration::from_seconds(1)),
1309 Time::<Gps>::MAX
1310 );
1311 }
1312
1313 #[test]
1314 fn test_gps_to_tai_adds_19s() {
1315 let gps = Time::<Gps>::from_seconds(100);
1316 let tai = gps.to_tai().unwrap();
1317
1318 assert_eq!(tai.as_seconds(), 119);
1319 }
1320
1321 #[test]
1322 fn test_tai_to_gps_subtracts_19s() {
1323 let tai = Time::<Tai>::from_seconds(119);
1324 let gps = Time::<Gps>::from_tai(tai).unwrap();
1325
1326 assert_eq!(gps.as_seconds(), 100);
1327 }
1328
1329 #[test]
1330 fn test_roundtrip_via_tai() {
1331 let original = Time::<Gps>::from_seconds(5_000_000);
1332 let back = Time::<Gps>::from_tai(original.to_tai().unwrap()).unwrap();
1333
1334 assert_eq!(original, back);
1335 }
1336
1337 #[test]
1338 fn test_gps_galileo_identity_via_tai() {
1339 let gps = Time::<Gps>::from_seconds(12_345);
1341 let gal = gps.try_convert::<Galileo>().unwrap();
1342
1343 assert_eq!(gps.as_nanos(), gal.as_nanos());
1344 }
1345
1346 #[test]
1347 fn test_gps_to_beidou_via_tai() {
1348 let gps = Time::<Gps>::from_seconds(100);
1350 let bdt = gps.try_convert::<Beidou>().unwrap();
1351
1352 assert_eq!(bdt.as_seconds(), 86);
1353 }
1354
1355 #[test]
1356 fn test_contextual_scale_to_tai_fails() {
1357 let glo = Time::<Glonass>::from_seconds(100);
1358
1359 assert!(matches!(
1360 glo.to_tai(),
1361 Err(GnssTimeError::LeapSecondsRequired)
1362 ));
1363 }
1364
1365 #[test]
1366 fn test_tai_to_contextual_fails() {
1367 let tai = Time::<Tai>::from_seconds(100);
1368
1369 assert!(matches!(
1370 Time::<Utc>::from_tai(tai),
1371 Err(GnssTimeError::LeapSecondsRequired)
1372 ));
1373 }
1374
1375 #[test]
1376 fn test_to_tai_overflow() {
1377 let t = Time::<Gps>::from_nanos(u64::MAX);
1378
1379 assert!(matches!(t.to_tai(), Err(GnssTimeError::Overflow)));
1380 }
1381
1382 #[test]
1383 fn test_from_tai_underflow() {
1384 let tai = Time::<Tai>::from_nanos(0);
1386
1387 assert!(matches!(
1388 Time::<Gps>::from_tai(tai),
1389 Err(GnssTimeError::Overflow)
1390 ));
1391 }
1392
1393 #[test]
1394 fn test_gps_display_week_tow_format() {
1395 let t = Time::<Gps>::from_week_tow(
1396 2345,
1397 DurationParts {
1398 seconds: 432_000,
1399 nanos: 0,
1400 },
1401 )
1402 .unwrap();
1403
1404 assert_eq!(t.to_string(), "GPS 2345:432000.000");
1405 }
1406
1407 #[test]
1408 fn test_gps_display_epoch_is_week_0() {
1409 let s = Time::<Gps>::EPOCH.to_string();
1410
1411 assert_eq!(s, "GPS 0:000000.000");
1412 }
1413
1414 #[test]
1415 fn test_gps_display_tow_zero_padded() {
1416 let t = Time::<Gps>::from_week_tow(
1418 1,
1419 DurationParts {
1420 seconds: 1,
1421 nanos: 0,
1422 },
1423 )
1424 .unwrap();
1425
1426 assert_eq!(t.to_string(), "GPS 1:000001.000");
1427 }
1428
1429 #[test]
1430 fn test_gps_display_with_millis() {
1431 let t = Time::<Gps>::from_week_tow(
1432 100,
1433 DurationParts {
1434 seconds: 0,
1435 nanos: 500_000_000,
1436 },
1437 )
1438 .unwrap();
1439
1440 assert_eq!(t.to_string(), "GPS 100:000000.500");
1441 }
1442
1443 #[test]
1444 fn test_glonass_display_day_tod_format() {
1445 let t = Time::<Glonass>::from_day_tod(
1446 10_512,
1447 DurationParts {
1448 seconds: 43_200,
1449 nanos: 0,
1450 },
1451 )
1452 .unwrap();
1453
1454 assert_eq!(t.to_string(), "GLO 10512:43200.000");
1455 }
1456
1457 #[test]
1458 fn test_glonass_display_epoch() {
1459 let s = Time::<Glonass>::EPOCH.to_string();
1460
1461 assert_eq!(s, "GLO 0:00000.000");
1462 }
1463
1464 #[test]
1465 fn test_galileo_display_week_format() {
1466 let s = Time::<Galileo>::EPOCH.to_string();
1467
1468 assert!(s.starts_with("GAL "));
1469 assert!(s.contains(':'));
1470 }
1471
1472 #[test]
1473 fn test_tai_display_simple_format() {
1474 let t = Time::<Tai>::from_seconds(1_000_000_000);
1475 let s = t.to_string();
1476
1477 assert!(s.starts_with("TAI +"));
1478 assert!(s.contains("1000000000s"));
1479 }
1480
1481 #[test]
1482 fn test_utc_display_simple_format() {
1483 let s = Time::<Utc>::EPOCH.to_string();
1484
1485 assert!(s.starts_with("UTC +"));
1486 }
1487
1488 #[test]
1489 fn test_debug_shows_scale_and_nanos() {
1490 let t = Time::<Glonass>::from_nanos(777);
1491 let s = format!("{t:?}");
1492
1493 assert!(s.contains("GLO") && s.contains("777"));
1494 }
1495
1496 #[test]
1497 fn test_ordering() {
1498 let t0 = Time::<Gps>::from_seconds(0);
1499 let t1 = Time::<Gps>::from_seconds(1);
1500 let t2 = Time::<Gps>::from_seconds(2);
1501 let mut v = vec![t2, t0, t1];
1502
1503 v.sort();
1504
1505 assert_eq!(v, vec![t0, t1, t2]);
1506 }
1507
1508 #[test]
1509 fn test_glonass_day_accessor() {
1510 let t = Time::<Glonass>::from_day_tod(
1511 42,
1512 DurationParts {
1513 seconds: 3600,
1514 nanos: 0,
1515 },
1516 )
1517 .unwrap();
1518
1519 assert_eq!(t.day(), 42);
1520 assert_eq!(t.tod_seconds(), 3600);
1521 }
1522
1523 #[test]
1524 fn test_time_max_behavior() {
1525 let max = Time::<Gps>::MAX;
1526 let one_ns = Duration::ONE_NANOSECOND;
1527
1528 assert!(max.checked_add(one_ns).is_none());
1530
1531 assert_eq!(max.saturating_add(one_ns), max);
1533
1534 assert!(max.try_add(one_ns).is_err());
1536 }
1537
1538 #[test]
1539 fn test_max_is_u64_max() {
1540 assert_eq!(Time::<Gps>::MAX.as_nanos(), u64::MAX);
1541 assert_eq!(Time::<Glonass>::MAX.as_nanos(), u64::MAX);
1542 assert_eq!(Time::<Galileo>::MAX.as_nanos(), u64::MAX);
1543 assert_eq!(Time::<Beidou>::MAX.as_nanos(), u64::MAX);
1544 assert_eq!(Time::<Tai>::MAX.as_nanos(), u64::MAX);
1545 assert_eq!(Time::<Utc>::MAX.as_nanos(), u64::MAX);
1546 }
1547
1548 #[test]
1549 fn test_nanos_per_year_is_correct() {
1550 let expected: u64 = 365 * 24 * 3_600 * 1_000_000_000;
1551
1552 assert_eq!(Time::<Gps>::NANOS_PER_YEAR, expected);
1553 }
1554
1555 #[test]
1556 fn test_max_covers_at_least_500_years() {
1557 let years = Time::<Gps>::MAX.as_nanos() / Time::<Gps>::NANOS_PER_YEAR;
1558
1559 assert!(
1560 years >= 500,
1561 "MAX should cover at least 500 years, got {years}"
1562 );
1563 }
1564
1565 #[test]
1566 fn test_checked_add_one_ns_before_max_succeeds() {
1567 let t = Time::<Gps>::from_nanos(u64::MAX - 1);
1568 let result = t.checked_add(Duration::from_nanos(1));
1569
1570 assert_eq!(result, Some(Time::<Gps>::MAX));
1571 }
1572
1573 #[test]
1574 fn test_checked_add_at_max_overflows() {
1575 assert!(Time::<Gps>::MAX
1576 .checked_add(Duration::from_nanos(1))
1577 .is_none());
1578 }
1579
1580 #[test]
1581 fn test_checked_add_large_positive_overflows() {
1582 let t = Time::<Gps>::from_nanos(u64::MAX - 100);
1583
1584 assert!(t.checked_add(Duration::from_seconds(1)).is_none());
1585 }
1586
1587 #[test]
1588 fn test_checked_sub_one_ns_after_epoch_succeeds() {
1589 let t = Time::<Gps>::from_nanos(1);
1590 let result = t.checked_sub_duration(Duration::from_nanos(1));
1591
1592 assert_eq!(result, Some(Time::<Gps>::EPOCH));
1593 }
1594
1595 #[test]
1596 fn test_checked_sub_at_epoch_underflows() {
1597 assert!(Time::<Gps>::EPOCH
1598 .checked_sub_duration(Duration::from_nanos(1))
1599 .is_none());
1600 }
1601
1602 #[test]
1603 fn test_checked_sub_large_amount_underflows() {
1604 let t = Time::<Gps>::from_nanos(50);
1605
1606 assert!(t.checked_sub_duration(Duration::from_seconds(1)).is_none());
1607 }
1608
1609 #[test]
1610 fn test_saturating_add_negative_clamps_at_epoch() {
1611 assert_eq!(
1612 Time::<Gps>::EPOCH.saturating_add(Duration::from_nanos(-1)),
1613 Time::<Gps>::EPOCH
1614 );
1615 }
1616
1617 #[test]
1618 fn test_saturating_add_normal_value_works() {
1619 let t = Time::<Gps>::from_seconds(100);
1620
1621 assert_eq!(
1622 t.saturating_add(Duration::from_seconds(50)),
1623 Time::<Gps>::from_seconds(150)
1624 );
1625 }
1626
1627 #[test]
1628 fn test_saturating_sub_clamps_at_epoch() {
1629 assert_eq!(
1630 Time::<Gps>::EPOCH.saturating_sub_duration(Duration::from_nanos(1)),
1631 Time::<Gps>::EPOCH
1632 );
1633 }
1634
1635 #[test]
1636 fn test_saturating_sub_normal_value_works() {
1637 let t = Time::<Gps>::from_seconds(100);
1638
1639 assert_eq!(
1640 t.saturating_sub_duration(Duration::from_seconds(30)),
1641 Time::<Gps>::from_seconds(70)
1642 );
1643 }
1644
1645 #[test]
1646 fn test_try_add_overflow_returns_err() {
1647 let result = Time::<Gps>::MAX.try_add(Duration::from_nanos(1));
1648
1649 assert!(matches!(result, Err(GnssTimeError::Overflow)));
1650 }
1651
1652 #[test]
1653 fn test_try_add_valid_value_works() {
1654 let t = Time::<Gps>::from_seconds(1_000);
1655 let result = t.try_add(Duration::from_seconds(500)).unwrap();
1656
1657 assert_eq!(result.as_seconds(), 1_500);
1658 }
1659
1660 #[test]
1661 #[should_panic(expected = "Time<S> + Duration overflowed")]
1662 fn test_add_operator_panics_at_max() {
1663 let _ = Time::<Gps>::MAX + Duration::from_nanos(1);
1664 }
1665
1666 #[test]
1667 #[should_panic(expected = "Time<S> - Duration underflowed")]
1668 fn test_sub_operator_panics_at_epoch() {
1669 let _ = Time::<Gps>::EPOCH - Duration::from_nanos(1);
1670 }
1671
1672 #[test]
1673 fn test_checked_elapsed_zero_gives_zero_duration() {
1674 let t = Time::<Gps>::from_seconds(1_000);
1675 assert_eq!(t.checked_elapsed(t), Some(Duration::ZERO));
1676 }
1677
1678 #[test]
1679 fn test_checked_elapsed_overflows_when_gap_exceeds_i64() {
1680 let result = Time::<Gps>::MAX.checked_elapsed(Time::<Gps>::EPOCH);
1683
1684 assert!(result.is_none(), "gap exceeds i64::MAX so must return None");
1685 }
1686
1687 #[test]
1688 fn test_checked_elapsed_within_i64_range_works() {
1689 let a = Time::<Gps>::from_seconds(1_000_000);
1690 let b = Time::<Gps>::from_seconds(500_000);
1691 let elapsed = a.checked_elapsed(b).unwrap();
1692
1693 assert_eq!(elapsed.as_seconds(), 500_000);
1694 }
1695
1696 #[test]
1697 fn test_unix_seconds_roundtrip() {
1698 let unix = 1_600_000_000; let utc = Time::<Utc>::from_unix_seconds(unix).unwrap();
1700
1701 assert_eq!(utc.as_unix_seconds(), unix);
1702 }
1703
1704 #[test]
1705 fn test_unix_nanos_roundtrip() {
1706 let unix_ns = 1_600_000_000_123_456_789;
1707 let utc = Time::<Utc>::from_unix_nanos(unix_ns).unwrap();
1708
1709 assert_eq!(utc.as_unix_nanos(), unix_ns);
1710 }
1711
1712 #[test]
1713 fn test_gps_display_format() {
1714 let t = Time::<Gps>::from_week_tow(
1715 2345,
1716 DurationParts {
1717 seconds: 432_000,
1718 nanos: 0,
1719 },
1720 )
1721 .unwrap();
1722 assert_eq!(t.to_string(), "GPS 2345:432000.000");
1723 }
1724
1725 #[test]
1726 fn test_saturating_add_clamps_at_max() {
1727 assert_eq!(
1728 Time::<Gps>::MAX.saturating_add(Duration::from_nanos(1)),
1729 Time::<Gps>::MAX
1730 );
1731 }
1732
1733 #[test]
1734 fn test_utc_from_unix_seconds_zero_fails() {
1735 assert!(matches!(
1737 Time::<Utc>::from_unix_seconds(0),
1738 Err(GnssTimeError::Overflow)
1739 ));
1740 }
1741
1742 #[test]
1743 fn test_utc_from_unix_seconds_negative_fails() {
1744 assert!(matches!(
1745 Time::<Utc>::from_unix_seconds(-1),
1746 Err(GnssTimeError::Overflow)
1747 ));
1748 }
1749
1750 #[test]
1751 fn test_utc_from_unix_seconds_just_before_utc_epoch_fails() {
1752 assert!(matches!(
1754 Time::<Utc>::from_unix_seconds(63_071_999),
1755 Err(GnssTimeError::Overflow)
1756 ));
1757 }
1758
1759 #[test]
1760 fn test_utc_from_unix_seconds_at_utc_epoch_gives_epoch() {
1761 let utc = Time::<Utc>::from_unix_seconds(63_072_000).unwrap();
1763 assert_eq!(utc, Time::<Utc>::EPOCH);
1764 }
1765
1766 #[test]
1767 fn test_utc_from_unix_seconds_roundtrip() {
1768 let unix_s: i64 = 1_700_000_000; let utc = Time::<Utc>::from_unix_seconds(unix_s).unwrap();
1770 assert_eq!(utc.as_unix_seconds(), unix_s);
1771 }
1772
1773 #[test]
1774 fn test_utc_from_unix_seconds_known_date() {
1775 let unix_s: i64 = 1_704_067_200;
1777 let utc = Time::<Utc>::from_unix_seconds(unix_s).unwrap();
1778 assert_eq!(utc.as_unix_seconds(), unix_s);
1779 }
1780
1781 #[test]
1782 fn test_utc_as_unix_seconds_at_epoch_equals_offset() {
1783 use crate::UTC_EPOCH_UNIX_OFFSET_S;
1784 assert_eq!(
1785 Time::<Utc>::EPOCH.as_unix_seconds(),
1786 UTC_EPOCH_UNIX_OFFSET_S
1787 );
1788 assert_eq!(Time::<Utc>::EPOCH.as_unix_seconds(), 63_072_000);
1789 }
1790
1791 #[test]
1792 fn test_utc_as_unix_seconds_one_second_after_epoch() {
1793 let utc = Time::<Utc>::from_nanos(1_000_000_000); assert_eq!(utc.as_unix_seconds(), 63_072_001);
1795 }
1796
1797 #[test]
1798 fn test_utc_from_unix_nanos_at_utc_epoch() {
1799 use crate::UTC_EPOCH_UNIX_OFFSET_NS;
1800 let utc = Time::<Utc>::from_unix_nanos(UTC_EPOCH_UNIX_OFFSET_NS).unwrap();
1801 assert_eq!(utc, Time::<Utc>::EPOCH);
1802 }
1803
1804 #[test]
1805 fn test_utc_from_unix_nanos_zero_fails() {
1806 assert!(matches!(
1807 Time::<Utc>::from_unix_nanos(0),
1808 Err(GnssTimeError::Overflow)
1809 ));
1810 }
1811
1812 #[test]
1813 fn test_utc_from_unix_nanos_one_ns_before_utc_epoch_fails() {
1814 assert!(matches!(
1815 Time::<Utc>::from_unix_nanos(63_072_000_000_000_000 - 1),
1816 Err(GnssTimeError::Overflow)
1817 ));
1818 }
1819
1820 #[test]
1821 fn test_utc_from_unix_nanos_roundtrip() {
1822 let unix_ns: i64 = 1_700_000_000_123_456_789;
1823 let utc = Time::<Utc>::from_unix_nanos(unix_ns).unwrap();
1824 assert_eq!(utc.as_unix_nanos(), unix_ns);
1825 }
1826
1827 #[test]
1828 fn test_utc_as_unix_nanos_at_epoch() {
1829 use crate::UTC_EPOCH_UNIX_OFFSET_NS;
1830 assert_eq!(Time::<Utc>::EPOCH.as_unix_nanos(), UTC_EPOCH_UNIX_OFFSET_NS);
1831 assert_eq!(Time::<Utc>::EPOCH.as_unix_nanos(), 63_072_000_000_000_000);
1832 }
1833
1834 #[test]
1835 fn test_utc_as_unix_nanos_one_ns_after_epoch() {
1836 let utc = Time::<Utc>::from_nanos(1);
1837 assert_eq!(utc.as_unix_nanos(), 63_072_000_000_000_001);
1838 }
1839
1840 #[test]
1841 fn test_utc_unix_seconds_and_nanos_consistent() {
1842 let unix_s: i64 = 1_600_000_000;
1843 let unix_ns: i64 = unix_s * 1_000_000_000;
1844 let from_s = Time::<Utc>::from_unix_seconds(unix_s).unwrap();
1845 let from_ns = Time::<Utc>::from_unix_nanos(unix_ns).unwrap();
1846 assert_eq!(from_s, from_ns);
1847 }
1848
1849 #[test]
1850 fn test_utc_unix_nanos_sub_second_preserved() {
1851 let unix_ns: i64 = 1_700_000_000_500_000_000; let utc = Time::<Utc>::from_unix_nanos(unix_ns).unwrap();
1853 assert_eq!(utc.as_unix_seconds(), 1_700_000_000);
1855 assert_eq!(utc.as_unix_nanos(), unix_ns);
1857 }
1858
1859 #[test]
1860 fn test_gps_from_unix_seconds_at_gps_epoch() {
1861 let ls = LeapSeconds::builtin();
1862 let gps = Time::<Gps>::from_unix_seconds(315_964_800, ls).unwrap();
1865 assert_eq!(gps, Time::<Gps>::EPOCH);
1866 }
1867
1868 #[test]
1869 fn test_gps_from_unix_seconds_before_utc_epoch_fails() {
1870 let ls = LeapSeconds::builtin();
1871 assert!(Time::<Gps>::from_unix_seconds(0, ls).is_err());
1873 }
1874
1875 #[test]
1876 fn test_gps_as_unix_seconds_at_gps_epoch() {
1877 let ls = LeapSeconds::builtin();
1878 assert_eq!(Time::<Gps>::EPOCH.as_unix_seconds(ls).unwrap(), 315_964_800);
1879 }
1880
1881 #[test]
1882 fn test_gps_unix_seconds_roundtrip() {
1883 let ls = LeapSeconds::builtin();
1884 let unix_s: i64 = 1_577_836_800;
1886 let gps = Time::<Gps>::from_unix_seconds(unix_s, ls).unwrap();
1887 assert_eq!(gps.as_unix_seconds(ls).unwrap(), unix_s);
1888 }
1889
1890 #[test]
1891 fn test_gps_unix_seconds_post_2017() {
1892 let ls = LeapSeconds::builtin();
1893 let unix_s: i64 = 1_672_531_200;
1895 let gps = Time::<Gps>::from_unix_seconds(unix_s, ls).unwrap();
1896 assert_eq!(gps.as_unix_seconds(ls).unwrap(), unix_s);
1897 }
1898
1899 #[test]
1900 fn test_gps_unix_offset_is_18s_post_2017() {
1901 let ls = LeapSeconds::builtin();
1902 let unix_s: i64 = 1_672_531_200; let gps = Time::<Gps>::from_unix_seconds(unix_s, ls).unwrap();
1905 let expected_gps_s = u64::try_from(i128::from(unix_s) - 315_964_800i128 + 18i128).unwrap();
1906 assert_eq!(gps.as_seconds(), expected_gps_s);
1907 }
1908}