use core::{
fmt,
marker::PhantomData,
ops::{Add, AddAssign, Sub, SubAssign},
};
use crate::{
gps_to_utc, utc_to_gps, DisplayStyle, Duration, Glonass, GnssTimeError, Gps, LeapSeconds,
LeapSecondsProvider, OffsetToTai, Tai, TimeScale, Utc,
};
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
#[must_use = "Time<S> is a value type; ignoring it has no effect"]
pub struct Time<S: TimeScale> {
nanos: u64,
_scale: PhantomData<S>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct DurationParts {
pub seconds: u64,
pub nanos: u32,
}
impl<S: TimeScale> Time<S> {
pub const EPOCH: Self = Time {
nanos: 0,
_scale: PhantomData,
};
pub const MIN: Self = Self::EPOCH;
pub const MAX: Self = Time {
nanos: u64::MAX,
_scale: PhantomData,
};
pub const NANOS_PER_YEAR: u64 = 365 * 24 * 3_600 * 1_000_000_000;
#[inline(always)]
pub const fn from_nanos(nanos: u64) -> Self {
Time {
nanos,
_scale: PhantomData,
}
}
#[inline]
pub const fn from_seconds(secs: u64) -> Self {
match secs.checked_mul(1_000_000_000) {
Some(n) => Time::from_nanos(n),
None => panic!("Time::from_seconds overflow"),
}
}
#[inline]
#[must_use = "returns None on overflow; check the result"]
pub const fn checked_from_seconds(secs: u64) -> Option<Self> {
match secs.checked_mul(1_000_000_000) {
Some(n) => Some(Time::from_nanos(n)),
None => None,
}
}
}
impl<S: TimeScale> Time<S> {
#[inline(always)]
#[must_use]
pub const fn as_nanos(self) -> u64 {
self.nanos
}
#[inline]
#[must_use]
pub const fn as_seconds(self) -> u64 {
self.nanos / 1_000_000_000
}
#[inline]
#[must_use]
pub fn as_seconds_f64(self) -> f64 {
self.nanos as f64 / 1_000_000_000.0
}
}
impl<S: TimeScale> Time<S> {
pub fn to_tai(self) -> Result<Time<Tai>, GnssTimeError> {
match S::OFFSET_TO_TAI {
OffsetToTai::Fixed(offset) => {
let nanos = (self.nanos as i128) + (offset as i128);
if nanos < 0 || nanos > u64::MAX as i128 {
return Err(GnssTimeError::Overflow);
}
Ok(Time::from_nanos(nanos as u64))
}
OffsetToTai::Contextual => Err(GnssTimeError::LeapSecondsRequired),
}
}
pub fn from_tai(tai: Time<Tai>) -> Result<Self, GnssTimeError> {
match S::OFFSET_TO_TAI {
OffsetToTai::Fixed(offset) => {
let nanos = (tai.as_nanos() as i128) - (offset as i128);
if nanos < 0 || nanos > u64::MAX as i128 {
return Err(GnssTimeError::Overflow);
}
Ok(Time::from_nanos(nanos as u64))
}
OffsetToTai::Contextual => Err(GnssTimeError::LeapSecondsRequired),
}
}
pub fn try_convert<T: TimeScale>(self) -> Result<Time<T>, GnssTimeError> {
let tai = self.to_tai()?;
Time::<T>::from_tai(tai)
}
}
impl<S: TimeScale> Time<S> {
#[inline]
#[must_use = "returns None on overflow; check the result"]
pub fn checked_add(
self,
d: Duration,
) -> Option<Self> {
let result = (self.nanos as i128) + (d.as_nanos() as i128);
if result < 0 || result > u64::MAX as i128 {
return None;
};
Some(Time::from_nanos(result as u64))
}
#[inline]
#[must_use = "returns None on underflow; check the result"]
pub fn checked_sub_duration(
self,
d: Duration,
) -> Option<Self> {
let result = (self.nanos as i128) - (d.as_nanos() as i128);
if result < 0 || result > u64::MAX as i128 {
return None;
}
Some(Time::from_nanos(result as u64))
}
#[inline]
#[must_use = "saturating_add returns a new Time<S>; the original is unchanged"]
pub fn saturating_add(
self,
d: Duration,
) -> Self {
self.checked_add(d).unwrap_or(if d.is_negative() {
Time::EPOCH
} else {
Time::MAX
})
}
#[inline]
#[must_use = "saturating_sub_duration returns a new Time<S>; the original is unchanged"]
pub fn saturating_sub_duration(
self,
d: Duration,
) -> Self {
self.checked_sub_duration(d).unwrap_or(if d.is_negative() {
Time::MAX
} else {
Time::EPOCH
})
}
#[inline]
pub fn try_add(
self,
d: Duration,
) -> Result<Self, GnssTimeError> {
self.checked_add(d).ok_or(GnssTimeError::Overflow)
}
#[inline]
pub fn try_sub_duration(
self,
d: Duration,
) -> Result<Self, GnssTimeError> {
self.checked_sub_duration(d).ok_or(GnssTimeError::Overflow)
}
}
impl<S: TimeScale> Time<S> {
#[inline]
#[must_use = "returns None on overflow; check the result"]
pub const fn checked_elapsed(
self,
earlier: Time<S>,
) -> Option<Duration> {
let diff = (self.nanos as i128) - (earlier.nanos as i128);
if diff > i64::MAX as i128 || diff < i64::MIN as i128 {
return None;
}
Some(Duration::from_nanos(diff as i64))
}
}
impl<S: TimeScale> Add<Duration> for Time<S> {
type Output = Time<S>;
#[inline]
fn add(
self,
rhs: Duration,
) -> Time<S> {
self.checked_add(rhs)
.expect("Time<S> + Duration overflowed")
}
}
impl<S: TimeScale> AddAssign<Duration> for Time<S> {
#[inline]
fn add_assign(
&mut self,
rhs: Duration,
) {
*self = *self + rhs
}
}
impl<S: TimeScale> Sub<Duration> for Time<S> {
type Output = Time<S>;
#[inline]
fn sub(
self,
rhs: Duration,
) -> Self::Output {
self.checked_sub_duration(rhs)
.expect("Time<S> - Duration underflowed")
}
}
impl<S: TimeScale> SubAssign<Duration> for Time<S> {
#[inline]
fn sub_assign(
&mut self,
rhs: Duration,
) {
*self = *self - rhs;
}
}
impl<S: TimeScale> Sub<Time<S>> for Time<S> {
type Output = Duration;
#[inline]
fn sub(
self,
rhs: Time<S>,
) -> Self::Output {
self.checked_elapsed(rhs)
.expect("Time<S> - Time<S> overflowed i64")
}
}
impl DurationParts {
pub const NANOS_PER_SECOND: u32 = 1_000_000_000;
#[inline]
pub const fn new(
seconds: u64,
nanos: u32,
) -> Result<Self, GnssTimeError> {
if nanos >= Self::NANOS_PER_SECOND {
return Err(GnssTimeError::InvalidInput(
"nanos must be in [0, 1_000_000_000]",
));
}
Ok(Self { seconds, nanos })
}
#[inline]
#[must_use]
pub const fn as_nanos(self) -> u128 {
(self.seconds as u128) * Self::NANOS_PER_SECOND as u128 + self.nanos as u128
}
}
impl Time<Glonass> {
pub fn from_day_tod(
day: u32,
tod: DurationParts,
) -> Result<Self, GnssTimeError> {
if tod.seconds >= 86_400 {
return Err(GnssTimeError::InvalidInput(
"tod.seconds must be in [0, 86_400)",
));
}
if tod.nanos >= DurationParts::NANOS_PER_SECOND {
return Err(GnssTimeError::InvalidInput(
"tod.nanos must be in [0, 1_000_000_000)",
));
}
let day_ns = (day as u64)
.checked_mul(86_400_000_000_000)
.ok_or(GnssTimeError::Overflow)?;
let tod_ns = tod
.seconds
.checked_mul(1_000_000_000)
.ok_or(GnssTimeError::Overflow)?
.checked_add(tod.nanos as u64)
.ok_or(GnssTimeError::Overflow)?;
let total = day_ns.checked_add(tod_ns).ok_or(GnssTimeError::Overflow)?;
Ok(Time::from_nanos(total))
}
#[inline]
#[must_use]
pub const fn day(self) -> u32 {
(self.nanos / 86_400_000_000_000u64) as u32
}
#[inline]
#[must_use]
pub const fn tod_seconds(self) -> u32 {
((self.nanos % 86_400_000_000_000u64) / 1_000_000_000u64) as u32
}
#[inline]
#[must_use]
pub const fn sub_second_nanos(self) -> u32 {
(self.nanos % 1_000_000_000u64) as u32
}
#[inline]
#[must_use]
pub const fn day_of_week(self) -> u8 {
(self.day() % 7) as u8 + 1
}
#[inline]
#[must_use]
pub const fn is_weekend(self) -> bool {
let d = self.day_of_week();
d == 6 || d == 7
}
}
impl Time<Gps> {
pub fn from_week_tow(
week: u16,
tow: DurationParts,
) -> Result<Self, GnssTimeError> {
if tow.seconds >= 604_800 {
return Err(GnssTimeError::InvalidInput(
"tow.seconds must be in [0, 604_800)",
));
}
if tow.nanos >= DurationParts::NANOS_PER_SECOND {
return Err(GnssTimeError::InvalidInput(
"tow.nanos must be in [0, 1_000_000_000)",
));
}
let week_ns = (week as u64)
.checked_mul(604_800_000_000_000)
.ok_or(GnssTimeError::Overflow)?;
let tow_ns = tow
.seconds
.checked_mul(1_000_000_000)
.ok_or(GnssTimeError::Overflow)?
.checked_add(tow.nanos as u64)
.ok_or(GnssTimeError::Overflow)?;
let total = week_ns.checked_add(tow_ns).ok_or(GnssTimeError::Overflow)?;
Ok(Time::from_nanos(total))
}
pub fn to_utc(self) -> Result<Time<Utc>, GnssTimeError> {
gps_to_utc(self, LeapSeconds::builtin())
}
pub fn to_utc_with<P: LeapSecondsProvider>(
self,
ls: &P,
) -> Result<Time<Utc>, GnssTimeError> {
gps_to_utc(self, ls)
}
#[inline]
#[must_use]
pub const fn week(self) -> u32 {
(self.nanos / 604_800_000_000_000u64) as u32
}
#[inline]
#[must_use]
pub const fn tow_seconds(self) -> u32 {
((self.nanos % 604_800_000_000_000u64) / 1_000_000_000u64) as u32
}
#[inline]
#[must_use]
pub const fn sub_second_nanos(self) -> u32 {
(self.nanos % 1_000_000_000u64) as u32
}
}
impl Time<Utc> {
pub fn to_gps(self) -> Result<Time<Gps>, GnssTimeError> {
utc_to_gps(self, LeapSeconds::builtin())
}
pub fn to_gps_with<P: LeapSecondsProvider>(
self,
ls: &P,
) -> Result<Time<Gps>, GnssTimeError> {
utc_to_gps(self, ls)
}
}
impl<S: TimeScale> PartialOrd for Time<S> {
#[inline]
fn partial_cmp(
&self,
other: &Self,
) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<S: TimeScale> Ord for Time<S> {
#[inline]
fn cmp(
&self,
other: &Self,
) -> core::cmp::Ordering {
self.nanos.cmp(&other.nanos)
}
}
impl<S: TimeScale> fmt::Debug for Time<S> {
fn fmt(
&self,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
write!(f, "Time<{}>({}ns)", S::NAME, self.nanos)
}
}
impl<S: TimeScale> fmt::Display for Time<S> {
fn fmt(
&self,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
match S::DISPLAY_STYLE {
DisplayStyle::WeekTow => {
const WEEK_NS: u64 = 604_800_000_000_000;
let week = self.nanos / WEEK_NS;
let tow_ns = self.nanos % WEEK_NS;
let tow_s = tow_ns / 1_000_000_000;
let tow_ms = (tow_ns % 1_000_000_000) / 1_000_000;
write!(f, "{} {}:{:06}.{:03}", S::NAME, week, tow_s, tow_ms)
}
DisplayStyle::DayTod => {
const DAY_NS: u64 = 86_400_000_000_000;
let day = self.nanos / DAY_NS;
let tod_ns = self.nanos % DAY_NS;
let tod_s = tod_ns / 1_000_000_000;
let tod_ms = (tod_ns % 1_000_000_000) / 1_000_000;
write!(f, "{} {}:{:05}.{:03}", S::NAME, day, tod_s, tod_ms)
}
DisplayStyle::Simple => {
let secs = self.nanos / 1_000_000_000;
let ns_rem = self.nanos % 1_000_000_000;
write!(f, "{} +{}s {}ns", S::NAME, secs, ns_rem)
}
}
}
}
#[cfg(feature = "defmt")]
impl<S: TimeScale> defmt::Format for Time<S> {
fn format(
&self,
f: defmt::Formatter,
) {
match S::DISPLAY_STYLE {
DisplayStyle::WeekTow => {
const WEEK_NS: u64 = 604_800_000_000_000;
let week = self.nanos / WEEK_NS;
let tow_ns = self.nanos % WEEK_NS;
let tow_s = tow_ns / 1_000_000_000;
let tow_ms = (tow_ns % 1_000_000_000) / 1_000_000;
defmt::write!(f, "{} {}:{:06}.{:03}", S::NAME, week, tow_s, tow_ms)
}
DisplayStyle::DayTod => {
const DAY_NS: u64 = 86_400_000_000_000;
let day = self.nanos / DAY_NS;
let tod_ns = self.nanos % DAY_NS;
let tod_s = tod_ns / 1_000_000_000;
let tod_ms = (tod_ns % 1_000_000_000) / 1_000_000;
defmt::write!(f, "{} {}:{:05}.{:03}", S::NAME, day, tod_s, tod_ms)
}
DisplayStyle::Simple => {
let secs = self.nanos / 1_000_000_000;
let ns_rem = self.nanos % 1_000_000_000;
defmt::write!(f, "{} +{}s {}ns", S::NAME, secs, ns_rem)
}
}
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use std::format;
#[allow(unused_imports)]
use std::string::ToString;
#[allow(unused_imports)]
use std::vec;
use super::*;
use crate::scale::{Beidou, Galileo, Glonass, Gps, Tai, Utc};
#[test]
fn test_size_equals_u64() {
assert_eq!(core::mem::size_of::<Time<Gps>>(), 8);
assert_eq!(core::mem::size_of::<Time<Glonass>>(), 8);
assert_eq!(core::mem::size_of::<Time<Galileo>>(), 8);
assert_eq!(core::mem::size_of::<Time<Beidou>>(), 8);
assert_eq!(core::mem::size_of::<Time<Utc>>(), 8);
assert_eq!(core::mem::size_of::<Time<Tai>>(), 8);
}
#[test]
fn test_epoch_is_zero() {
assert_eq!(Time::<Gps>::EPOCH.as_nanos(), 0);
}
#[test]
fn test_from_week_tow_zero() {
let t = Time::<Gps>::from_week_tow(
0,
DurationParts {
seconds: 0,
nanos: 0,
},
)
.unwrap();
assert_eq!(t, Time::<Gps>::EPOCH);
}
#[test]
fn test_from_week_tow_roundtrip() {
let t = Time::<Gps>::from_week_tow(
2345,
DurationParts {
seconds: 432_000,
nanos: 0,
},
)
.unwrap();
assert_eq!(t.week(), 2345);
assert_eq!(t.tow_seconds(), 432_000);
assert_eq!(t.sub_second_nanos(), 0);
}
#[test]
fn test_from_week_tow_with_fractional() {
let t = Time::<Gps>::from_week_tow(
2300,
DurationParts {
seconds: 3661,
nanos: 500_000_000,
},
)
.unwrap();
assert_eq!(t.week(), 2300);
assert_eq!(t.tow_seconds(), 3661);
assert_eq!(t.sub_second_nanos(), 500_000_000);
}
#[test]
fn test_from_week_tow_invalid() {
assert!(matches!(
Time::<Gps>::from_week_tow(
0,
DurationParts {
seconds: 604_800,
nanos: 0
}
),
Err(GnssTimeError::InvalidInput(_))
));
}
#[test]
fn test_from_day_tod_zero() {
let t = Time::<Glonass>::from_day_tod(
0,
DurationParts {
seconds: 0,
nanos: 0,
},
)
.unwrap();
assert_eq!(t, Time::<Glonass>::EPOCH);
}
#[test]
fn test_from_day_tod_roundtrip() {
let t = Time::<Glonass>::from_day_tod(
10_512,
DurationParts {
seconds: 43_200,
nanos: 0,
},
)
.unwrap();
assert_eq!(t.day(), 10_512);
assert_eq!(t.tod_seconds(), 43_200);
}
#[test]
fn test_from_day_tod_invalid() {
assert!(matches!(
Time::<Glonass>::from_day_tod(
0,
DurationParts {
seconds: 86_400,
nanos: 0
}
),
Err(GnssTimeError::InvalidInput(_))
));
}
#[test]
fn test_add_positive_duration() {
let t = Time::<Gps>::from_seconds(100);
assert_eq!((t + Duration::from_seconds(50)).as_seconds(), 150);
}
#[test]
fn test_add_negative_duration_moves_back() {
let t = Time::<Gps>::from_seconds(100);
assert_eq!((t + Duration::from_nanos(-50_000_000_000)).as_seconds(), 50);
}
#[test]
fn test_roundtrip_add_sub() {
let t = Time::<Galileo>::from_seconds(1_000_000);
let d = Duration::from_seconds(12_345);
assert_eq!(t + d - d, t);
}
#[test]
fn test_sub_times_positive() {
let a = Time::<Gps>::from_seconds(200);
let b = Time::<Gps>::from_seconds(100);
assert_eq!((a - b).as_seconds(), 100);
}
#[test]
fn test_sub_times_negative() {
let a = Time::<Gps>::from_seconds(100);
let b = Time::<Gps>::from_seconds(200);
assert_eq!((a - b).as_seconds(), -100);
}
#[test]
fn test_sub_same_is_zero() {
let t = Time::<Gps>::from_seconds(42);
assert!((t - t).is_zero());
}
#[test]
#[should_panic]
fn test_add_overflow_panics() {
let _ = Time::<Gps>::MAX + Duration::ONE_NANOSECOND;
}
#[test]
fn test_checked_add_overflow() {
assert!(Time::<Gps>::MAX
.checked_add(Duration::ONE_NANOSECOND)
.is_none());
}
#[test]
fn test_checked_sub_underflow() {
assert!(Time::<Gps>::EPOCH
.checked_sub_duration(Duration::ONE_NANOSECOND)
.is_none());
}
#[test]
fn test_saturating_add_clamps() {
assert_eq!(
Time::<Gps>::MAX.saturating_add(Duration::from_seconds(1)),
Time::<Gps>::MAX
);
}
#[test]
fn test_gps_to_tai_adds_19s() {
let gps = Time::<Gps>::from_seconds(100);
let tai = gps.to_tai().unwrap();
assert_eq!(tai.as_seconds(), 119);
}
#[test]
fn test_tai_to_gps_subtracts_19s() {
let tai = Time::<Tai>::from_seconds(119);
let gps = Time::<Gps>::from_tai(tai).unwrap();
assert_eq!(gps.as_seconds(), 100);
}
#[test]
fn test_roundtrip_via_tai() {
let original = Time::<Gps>::from_seconds(5_000_000);
let back = Time::<Gps>::from_tai(original.to_tai().unwrap()).unwrap();
assert_eq!(original, back);
}
#[test]
fn test_gps_galileo_identity_via_tai() {
let gps = Time::<Gps>::from_seconds(12_345);
let gal = gps.try_convert::<Galileo>().unwrap();
assert_eq!(gps.as_nanos(), gal.as_nanos());
}
#[test]
fn test_gps_to_beidou_via_tai() {
let gps = Time::<Gps>::from_seconds(100);
let bdt = gps.try_convert::<Beidou>().unwrap();
assert_eq!(bdt.as_seconds(), 86);
}
#[test]
fn test_contextual_scale_to_tai_fails() {
let glo = Time::<Glonass>::from_seconds(100);
assert!(matches!(
glo.to_tai(),
Err(GnssTimeError::LeapSecondsRequired)
));
}
#[test]
fn test_tai_to_contextual_fails() {
let tai = Time::<Tai>::from_seconds(100);
assert!(matches!(
Time::<Utc>::from_tai(tai),
Err(GnssTimeError::LeapSecondsRequired)
));
}
#[test]
fn test_to_tai_overflow() {
let t = Time::<Gps>::from_nanos(u64::MAX);
assert!(matches!(t.to_tai(), Err(GnssTimeError::Overflow)));
}
#[test]
fn test_from_tai_underflow() {
let tai = Time::<Tai>::from_nanos(0);
assert!(matches!(
Time::<Gps>::from_tai(tai),
Err(GnssTimeError::Overflow)
));
}
#[test]
fn test_gps_display_week_tow_format() {
let t = Time::<Gps>::from_week_tow(
2345,
DurationParts {
seconds: 432_000,
nanos: 0,
},
)
.unwrap();
assert_eq!(t.to_string(), "GPS 2345:432000.000");
}
#[test]
fn test_gps_display_epoch_is_week_0() {
let s = Time::<Gps>::EPOCH.to_string();
assert_eq!(s, "GPS 0:000000.000");
}
#[test]
fn test_gps_display_tow_zero_padded() {
let t = Time::<Gps>::from_week_tow(
1,
DurationParts {
seconds: 1,
nanos: 0,
},
)
.unwrap();
assert_eq!(t.to_string(), "GPS 1:000001.000");
}
#[test]
fn test_gps_display_with_millis() {
let t = Time::<Gps>::from_week_tow(
100,
DurationParts {
seconds: 0,
nanos: 500_000_000,
},
)
.unwrap();
assert_eq!(t.to_string(), "GPS 100:000000.500");
}
#[test]
fn test_glonass_display_day_tod_format() {
let t = Time::<Glonass>::from_day_tod(
10_512,
DurationParts {
seconds: 43_200,
nanos: 0,
},
)
.unwrap();
assert_eq!(t.to_string(), "GLO 10512:43200.000");
}
#[test]
fn test_glonass_display_epoch() {
let s = Time::<Glonass>::EPOCH.to_string();
assert_eq!(s, "GLO 0:00000.000");
}
#[test]
fn test_galileo_display_week_format() {
let s = Time::<Galileo>::EPOCH.to_string();
assert!(s.starts_with("GAL "));
assert!(s.contains(':'));
}
#[test]
fn test_tai_display_simple_format() {
let t = Time::<Tai>::from_seconds(1_000_000_000);
let s = t.to_string();
assert!(s.starts_with("TAI +"));
assert!(s.contains("1000000000s"));
}
#[test]
fn test_utc_display_simple_format() {
let s = Time::<Utc>::EPOCH.to_string();
assert!(s.starts_with("UTC +"));
}
#[test]
fn test_debug_shows_scale_and_nanos() {
let t = Time::<Glonass>::from_nanos(777);
let s = format!("{t:?}");
assert!(s.contains("GLO") && s.contains("777"));
}
#[test]
fn test_ordering() {
let t0 = Time::<Gps>::from_seconds(0);
let t1 = Time::<Gps>::from_seconds(1);
let t2 = Time::<Gps>::from_seconds(2);
let mut v = vec![t2, t0, t1];
v.sort();
assert_eq!(v, vec![t0, t1, t2]);
}
#[test]
fn test_glonass_day_accessor() {
let t = Time::<Glonass>::from_day_tod(
42,
DurationParts {
seconds: 3600,
nanos: 0,
},
)
.unwrap();
assert_eq!(t.day(), 42);
assert_eq!(t.tod_seconds(), 3600);
}
#[test]
fn test_time_max_behavior() {
let max = Time::<Gps>::MAX;
let one_ns = Duration::ONE_NANOSECOND;
assert!(max.checked_add(one_ns).is_none());
assert_eq!(max.saturating_add(one_ns), max);
assert!(max.try_add(one_ns).is_err());
}
#[test]
fn test_max_is_u64_max() {
assert_eq!(Time::<Gps>::MAX.as_nanos(), u64::MAX);
assert_eq!(Time::<Glonass>::MAX.as_nanos(), u64::MAX);
assert_eq!(Time::<Galileo>::MAX.as_nanos(), u64::MAX);
assert_eq!(Time::<Beidou>::MAX.as_nanos(), u64::MAX);
assert_eq!(Time::<Tai>::MAX.as_nanos(), u64::MAX);
assert_eq!(Time::<Utc>::MAX.as_nanos(), u64::MAX);
}
#[test]
fn test_nanos_per_year_is_correct() {
let expected: u64 = 365 * 24 * 3_600 * 1_000_000_000;
assert_eq!(Time::<Gps>::NANOS_PER_YEAR, expected);
}
#[test]
fn test_max_covers_at_least_500_years() {
let years = Time::<Gps>::MAX.as_nanos() / Time::<Gps>::NANOS_PER_YEAR;
assert!(
years >= 500,
"MAX should cover at least 500 years, got {years}"
);
}
#[test]
fn test_checked_add_one_ns_before_max_succeeds() {
let t = Time::<Gps>::from_nanos(u64::MAX - 1);
let result = t.checked_add(Duration::from_nanos(1));
assert_eq!(result, Some(Time::<Gps>::MAX));
}
#[test]
fn test_checked_add_at_max_overflows() {
assert!(Time::<Gps>::MAX
.checked_add(Duration::from_nanos(1))
.is_none());
}
#[test]
fn test_checked_add_large_positive_overflows() {
let t = Time::<Gps>::from_nanos(u64::MAX - 100);
assert!(t.checked_add(Duration::from_seconds(1)).is_none());
}
#[test]
fn test_checked_sub_one_ns_after_epoch_succeeds() {
let t = Time::<Gps>::from_nanos(1);
let result = t.checked_sub_duration(Duration::from_nanos(1));
assert_eq!(result, Some(Time::<Gps>::EPOCH));
}
#[test]
fn test_checked_sub_at_epoch_underflows() {
assert!(Time::<Gps>::EPOCH
.checked_sub_duration(Duration::from_nanos(1))
.is_none());
}
#[test]
fn test_checked_sub_large_amount_underflows() {
let t = Time::<Gps>::from_nanos(50);
assert!(t.checked_sub_duration(Duration::from_seconds(1)).is_none());
}
#[test]
fn test_saturating_add_clamps_at_max() {
assert_eq!(
Time::<Gps>::MAX.saturating_add(Duration::from_nanos(1)),
Time::<Gps>::MAX
);
assert_eq!(
Time::<Gps>::MAX.saturating_add(Duration::from_seconds(9999)),
Time::<Gps>::MAX
);
}
#[test]
fn test_saturating_add_negative_clamps_at_epoch() {
assert_eq!(
Time::<Gps>::EPOCH.saturating_add(Duration::from_nanos(-1)),
Time::<Gps>::EPOCH
);
}
#[test]
fn test_saturating_add_normal_value_works() {
let t = Time::<Gps>::from_seconds(100);
assert_eq!(
t.saturating_add(Duration::from_seconds(50)),
Time::<Gps>::from_seconds(150)
);
}
#[test]
fn test_saturating_sub_clamps_at_epoch() {
assert_eq!(
Time::<Gps>::EPOCH.saturating_sub_duration(Duration::from_nanos(1)),
Time::<Gps>::EPOCH
);
}
#[test]
fn test_saturating_sub_normal_value_works() {
let t = Time::<Gps>::from_seconds(100);
assert_eq!(
t.saturating_sub_duration(Duration::from_seconds(30)),
Time::<Gps>::from_seconds(70)
);
}
#[test]
fn test_try_add_overflow_returns_err() {
let result = Time::<Gps>::MAX.try_add(Duration::from_nanos(1));
assert!(matches!(result, Err(GnssTimeError::Overflow)));
}
#[test]
fn test_try_sub_duration_underflow_returns_err() {
let result = Time::<Gps>::EPOCH.try_sub_duration(Duration::from_nanos(1));
assert!(matches!(result, Err(GnssTimeError::Overflow)));
}
#[test]
fn test_try_add_valid_value_works() {
let t = Time::<Gps>::from_seconds(1_000);
let result = t.try_add(Duration::from_seconds(500)).unwrap();
assert_eq!(result.as_seconds(), 1_500);
}
#[test]
#[should_panic]
fn test_add_operator_panics_at_max() {
let _ = Time::<Gps>::MAX + Duration::from_nanos(1);
}
#[test]
#[should_panic]
fn test_sub_operator_panics_at_epoch() {
let _ = Time::<Gps>::EPOCH - Duration::from_nanos(1);
}
#[test]
fn test_checked_elapsed_zero_gives_zero_duration() {
let t = Time::<Gps>::from_seconds(1_000);
assert_eq!(t.checked_elapsed(t), Some(Duration::ZERO));
}
#[test]
fn test_checked_elapsed_overflows_when_gap_exceeds_i64() {
let result = Time::<Gps>::MAX.checked_elapsed(Time::<Gps>::EPOCH);
assert!(result.is_none(), "gap exceeds i64::MAX so must return None");
}
#[test]
fn test_checked_elapsed_within_i64_range_works() {
let a = Time::<Gps>::from_seconds(1_000_000);
let b = Time::<Gps>::from_seconds(500_000);
let elapsed = a.checked_elapsed(b).unwrap();
assert_eq!(elapsed.as_seconds(), 500_000);
}
}