use crate::{
beidou_to_glonass, beidou_to_utc, galileo_to_glonass, galileo_to_utc, glonass_to_beidou,
glonass_to_galileo, glonass_to_gps, glonass_to_utc, gps_to_glonass, gps_to_utc, utc_to_beidou,
utc_to_galileo, utc_to_glonass, utc_to_gps, Beidou, Galileo, Glonass, GnssTimeError, Gps,
LeapSecondsProvider, Tai, Time, TimeScale, Utc,
};
#[must_use = "conversion result must be used; ignoring it discards the converted time"]
pub trait IntoScale<Target: TimeScale>: Sized {
fn into_scale(self) -> Result<Time<Target>, GnssTimeError>;
}
#[must_use = "conversion result must be used; ignoring it discards the converted time"]
pub trait IntoScaleWith<Target: TimeScale>: Sized {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Target>, GnssTimeError>;
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Target>>, GnssTimeError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[must_use = "ConvertResult contains ambiguity information; call .into_inner() or match explicitly"]
pub enum ConvertResult<T> {
Exact(T),
AmbiguousLeapSecond(T),
}
impl<T> ConvertResult<T> {
#[inline]
#[must_use]
pub fn into_inner(self) -> T {
match self {
Self::Exact(t) | Self::AmbiguousLeapSecond(t) => t,
}
}
#[inline]
#[must_use]
pub fn is_exact(&self) -> bool {
matches!(self, Self::Exact(_))
}
#[inline]
#[must_use]
pub fn is_ambiguous(&self) -> bool {
matches!(self, Self::AmbiguousLeapSecond(_))
}
}
impl IntoScale<Glonass> for Time<Utc> {
#[inline]
fn into_scale(self) -> Result<Time<Glonass>, GnssTimeError> {
utc_to_glonass(self)
}
}
impl IntoScaleWith<Glonass> for Time<Gps> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Glonass>, GnssTimeError> {
gps_to_glonass(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Glonass>>, GnssTimeError> {
Ok(ConvertResult::Exact(gps_to_glonass(self, &ls)?))
}
}
impl IntoScaleWith<Glonass> for Time<Galileo> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Glonass>, GnssTimeError> {
galileo_to_glonass(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Glonass>>, GnssTimeError> {
Ok(ConvertResult::Exact(galileo_to_glonass(self, &ls)?))
}
}
impl IntoScaleWith<Glonass> for Time<Beidou> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Glonass>, GnssTimeError> {
beidou_to_glonass(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Glonass>>, GnssTimeError> {
Ok(ConvertResult::Exact(beidou_to_glonass(self, &ls)?))
}
}
impl IntoScale<Gps> for Time<Galileo> {
#[inline]
fn into_scale(self) -> Result<Time<Gps>, GnssTimeError> {
self.try_convert::<Gps>()
}
}
impl IntoScale<Gps> for Time<Beidou> {
#[inline]
fn into_scale(self) -> Result<Time<Gps>, GnssTimeError> {
self.try_convert::<Gps>()
}
}
impl IntoScale<Gps> for Time<Tai> {
#[inline]
fn into_scale(self) -> Result<Time<Gps>, GnssTimeError> {
Time::<Gps>::from_tai(self)
}
}
impl IntoScaleWith<Gps> for Time<Glonass> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Gps>, GnssTimeError> {
glonass_to_gps(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Gps>>, GnssTimeError> {
Ok(ConvertResult::Exact(glonass_to_gps(self, &ls)?))
}
}
impl IntoScaleWith<Gps> for Time<Utc> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Gps>, GnssTimeError> {
utc_to_gps(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Gps>>, GnssTimeError> {
Ok(ConvertResult::Exact(utc_to_gps(self, &ls)?))
}
}
impl IntoScale<Galileo> for Time<Gps> {
#[inline]
fn into_scale(self) -> Result<Time<Galileo>, GnssTimeError> {
self.try_convert::<Galileo>()
}
}
impl IntoScale<Galileo> for Time<Beidou> {
#[inline]
fn into_scale(self) -> Result<Time<Galileo>, GnssTimeError> {
self.try_convert::<Galileo>()
}
}
impl IntoScaleWith<Galileo> for Time<Glonass> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Galileo>, GnssTimeError> {
glonass_to_galileo(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Galileo>>, GnssTimeError> {
Ok(ConvertResult::Exact(glonass_to_galileo(self, &ls)?))
}
}
impl IntoScaleWith<Galileo> for Time<Utc> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Galileo>, GnssTimeError> {
utc_to_galileo(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Galileo>>, GnssTimeError> {
Ok(ConvertResult::Exact(utc_to_galileo(self, &ls)?))
}
}
impl IntoScale<Beidou> for Time<Gps> {
#[inline]
fn into_scale(self) -> Result<Time<Beidou>, GnssTimeError> {
self.try_convert::<Beidou>()
}
}
impl IntoScale<Beidou> for Time<Galileo> {
#[inline]
fn into_scale(self) -> Result<Time<Beidou>, GnssTimeError> {
self.try_convert::<Beidou>()
}
}
impl IntoScaleWith<Beidou> for Time<Utc> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Beidou>, GnssTimeError> {
utc_to_beidou(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Beidou>>, GnssTimeError> {
Ok(ConvertResult::Exact(utc_to_beidou(self, &ls)?))
}
}
impl IntoScaleWith<Beidou> for Time<Glonass> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Beidou>, GnssTimeError> {
glonass_to_beidou(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Beidou>>, GnssTimeError> {
Ok(ConvertResult::Exact(glonass_to_beidou(self, &ls)?))
}
}
impl IntoScale<Utc> for Time<Glonass> {
#[inline]
fn into_scale(self) -> Result<Time<Utc>, GnssTimeError> {
glonass_to_utc(self)
}
}
impl IntoScaleWith<Utc> for Time<Gps> {
#[inline]
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Utc>, GnssTimeError> {
gps_to_utc(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Utc>>, GnssTimeError> {
let utc = gps_to_utc(self, &ls)?;
let tai = self.to_tai()?;
let n_at = ls.tai_minus_utc_at(tai);
let tai_prev = if tai.as_nanos() >= 1_000_000_000 {
Time::<Tai>::from_nanos(tai.as_nanos() - 1_000_000_000)
} else {
tai
};
let n_before = ls.tai_minus_utc_at(tai_prev);
if n_at != n_before {
Ok(ConvertResult::AmbiguousLeapSecond(utc))
} else {
Ok(ConvertResult::Exact(utc))
}
}
}
impl IntoScaleWith<Utc> for Time<Galileo> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Utc>, GnssTimeError> {
galileo_to_utc(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Utc>>, GnssTimeError> {
Ok(ConvertResult::Exact(galileo_to_utc(self, &ls)?))
}
}
impl IntoScaleWith<Utc> for Time<Beidou> {
fn into_scale_with<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<Time<Utc>, GnssTimeError> {
beidou_to_utc(self, &ls)
}
fn into_scale_with_checked<P: LeapSecondsProvider>(
self,
ls: P,
) -> Result<ConvertResult<Time<Utc>>, GnssTimeError> {
Ok(ConvertResult::Exact(beidou_to_utc(self, &ls)?))
}
}
impl IntoScale<Tai> for Time<Gps> {
#[inline]
fn into_scale(self) -> Result<Time<Tai>, GnssTimeError> {
self.to_tai()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DurationParts, LeapSeconds};
#[test]
fn test_gps_to_tai_adds_19_seconds() {
let gps = Time::<Gps>::from_seconds(100);
let tai: Time<Tai> = gps.into_scale().unwrap();
assert_eq!(tai.as_seconds(), 119);
}
#[test]
fn test_tai_to_gps_subtracts_19_seconds() {
let tai = Time::<Tai>::from_seconds(119);
let gps: Time<Gps> = tai.into_scale().unwrap();
assert_eq!(gps.as_seconds(), 100);
}
#[test]
fn test_gps_tai_gps_roundtrip() {
let gps = Time::<Gps>::from_week_tow(
2345,
DurationParts {
seconds: 432_000,
nanos: 0,
},
)
.unwrap();
let tai: Time<Tai> = gps.into_scale().unwrap();
let back: Time<Gps> = tai.into_scale().unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_tai_to_gps_underflow_at_tai_zero() {
let tai = Time::<Tai>::EPOCH;
let result: Result<Time<Gps>, _> = tai.into_scale();
assert!(matches!(result, Err(GnssTimeError::Overflow)));
}
#[test]
fn test_gps_to_galileo_preserves_nanos() {
let gps = Time::<Gps>::from_seconds(12_345_678);
let gal: Time<Galileo> = gps.into_scale().unwrap();
assert_eq!(gps.as_nanos(), gal.as_nanos());
}
#[test]
fn test_galileo_to_gps_preserves_nanos() {
let gal = Time::<Galileo>::from_seconds(99_999_999);
let gps: Time<Gps> = gal.into_scale().unwrap();
assert_eq!(gal.as_nanos(), gps.as_nanos());
}
#[test]
fn test_gps_galileo_gps_roundtrip() {
let gps = Time::<Gps>::from_week_tow(
2000,
DurationParts {
seconds: 123_456,
nanos: 789_000_000,
},
)
.unwrap();
let gal: Time<Galileo> = gps.into_scale().unwrap();
let back: Time<Gps> = gal.into_scale().unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_gps_to_beidou_subtracts_14_seconds() {
let gps = Time::<Gps>::from_seconds(100);
let bdt: Time<Beidou> = gps.into_scale().unwrap();
assert_eq!(bdt.as_seconds(), 86); }
#[test]
fn test_beidou_to_gps_adds_14_seconds() {
let bdt = Time::<Beidou>::from_seconds(86);
let gps: Time<Gps> = bdt.into_scale().unwrap();
assert_eq!(gps.as_seconds(), 100);
}
#[test]
fn test_gps_beidou_gps_roundtrip() {
let gps = Time::<Gps>::from_week_tow(
2100,
DurationParts {
seconds: 86_400,
nanos: 0,
},
)
.unwrap();
let bdt: Time<Beidou> = gps.into_scale().unwrap();
let back: Time<Gps> = bdt.into_scale().unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_galileo_beidou_roundtrip() {
let gal = Time::<Galileo>::from_seconds(1_000_000_000);
let bdt: Time<Beidou> = gal.into_scale().unwrap();
let back: Time<Galileo> = bdt.into_scale().unwrap();
assert_eq!(gal, back);
}
#[test]
fn test_glonass_epoch_to_utc_nanos() {
let glo = Time::<Glonass>::EPOCH;
let utc: Time<Utc> = glo.into_scale().unwrap();
assert_eq!(utc.as_nanos(), 757_371_600_000_000_000);
}
#[test]
fn test_utc_at_glonass_epoch_gives_zero() {
let utc = Time::<Utc>::from_nanos(757_371_600_000_000_000);
let glo: Time<Glonass> = utc.into_scale().unwrap();
assert_eq!(glo, Time::<Glonass>::EPOCH);
}
#[test]
fn test_glonass_utc_glonass_roundtrip() {
let glo = Time::<Glonass>::from_day_tod(
10_000,
DurationParts {
seconds: 36_000,
nanos: 0,
},
)
.unwrap();
let utc: Time<Utc> = glo.into_scale().unwrap();
let back: Time<Glonass> = utc.into_scale().unwrap();
assert_eq!(glo, back);
}
#[test]
fn test_utc_before_glonass_epoch_is_error() {
let utc = Time::<Utc>::EPOCH;
let result: Result<Time<Glonass>, _> = utc.into_scale();
assert!(matches!(result, Err(GnssTimeError::Overflow)));
}
#[test]
fn test_gps_utc_gps_roundtrip_at_gps_epoch() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::EPOCH;
let utc: Time<Utc> = gps.into_scale_with(ls).unwrap();
let back: Time<Gps> = utc.into_scale_with(ls).unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_gps_utc_gps_roundtrip_at_2020() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::from_week_tow(
2086,
DurationParts {
seconds: 0,
nanos: 0,
},
)
.unwrap();
let utc: Time<Utc> = gps.into_scale_with(ls).unwrap();
let back: Time<Gps> = utc.into_scale_with(ls).unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_gps_utc_roundtrip_exact_at_nanosecond_level() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::from_nanos(1_167_264_100_123_456_789);
let utc: Time<Utc> = gps.into_scale_with(ls).unwrap();
let back: Time<Gps> = utc.into_scale_with(ls).unwrap();
assert_eq!(gps, back); }
#[test]
fn test_gps_leads_utc_by_18s_at_2017_01_01() {
let ls = LeapSeconds::builtin();
let expected_utc_s: u64 = 16_437 * 86_400;
let gps_s: u64 = 1_167_264_000 + 18; let gps = Time::<Gps>::from_seconds(gps_s);
let utc: Time<Utc> = gps.into_scale_with(ls).unwrap();
assert_eq!(utc.as_seconds(), expected_utc_s);
}
#[test]
fn test_gps_leads_utc_by_13s_at_1999_01_01() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::from_seconds(599_184_013);
let utc: Time<Utc> = gps.into_scale_with(ls).unwrap();
let expected_utc_s: u64 = 9_862 * 86_400;
assert_eq!(utc.as_seconds(), expected_utc_s);
}
#[test]
fn test_gps_glonass_gps_roundtrip() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::from_week_tow(
2100,
DurationParts {
seconds: 86_400,
nanos: 0,
},
)
.unwrap();
let glo: Time<Glonass> = gps.into_scale_with(ls).unwrap();
let back: Time<Gps> = glo.into_scale_with(ls).unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_normal_gps_gives_exact_convert_result() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::from_week_tow(
2086,
DurationParts {
seconds: 0,
nanos: 0,
},
)
.unwrap();
let result: ConvertResult<Time<Utc>> = gps.into_scale_with_checked(ls).unwrap();
assert!(result.is_exact());
}
#[test]
fn test_utc_to_gps_always_exact() {
let ls = LeapSeconds::builtin();
let utc = Time::<Utc>::from_nanos(757_371_600_000_000_000 + 1_000_000_000);
let result: ConvertResult<Time<Gps>> = utc.into_scale_with_checked(ls).unwrap();
assert!(result.is_exact());
}
#[test]
fn test_into_inner_returns_value() {
let t = Time::<Gps>::from_seconds(100);
let r = ConvertResult::Exact(t);
assert_eq!(r.into_inner(), t);
let t2 = Time::<Gps>::from_seconds(200);
let r2 = ConvertResult::AmbiguousLeapSecond(t2);
assert_eq!(r2.into_inner(), t2);
}
#[test]
fn test_gps_to_tai_overflow_at_max() {
let gps = Time::<Gps>::MAX;
let result: Result<Time<Tai>, _> = gps.into_scale();
assert!(matches!(result, Err(GnssTimeError::Overflow)));
}
#[test]
fn test_into_scale_gps_tai_matches_to_tai() {
let gps = Time::<Gps>::from_seconds(999_999);
let via_trait: Time<Tai> = gps.into_scale().unwrap();
let via_method = gps.to_tai().unwrap();
assert_eq!(via_trait, via_method);
}
#[test]
fn test_into_scale_with_gps_utc_matches_gps_to_utc() {
use crate::leap::{gps_to_utc, LeapSeconds};
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::from_seconds(599_184_013);
let via_trait: Time<Utc> = gps.into_scale_with(ls).unwrap();
let via_fn = gps_to_utc(gps, ls).unwrap();
assert_eq!(via_trait, via_fn);
}
#[test]
fn test_gps_to_utc_detects_leap_second_ambiguity() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::from_seconds(1_167_264_018);
let result: ConvertResult<Time<Utc>> = gps.into_scale_with_checked(ls).unwrap();
assert!(matches!(result, ConvertResult::AmbiguousLeapSecond(_)));
}
#[test]
fn test_all_roundtrip_invariants() {
let ls = LeapSeconds::builtin();
let gps_values = [
Time::<Gps>::from_week_tow(
2086,
DurationParts {
seconds: 0,
nanos: 0,
},
)
.unwrap(),
Time::<Gps>::from_week_tow(
2100,
DurationParts {
seconds: 86_400,
nanos: 0,
},
)
.unwrap(),
Time::<Gps>::from_nanos(1_167_264_100_123_456_789),
];
for gps in gps_values {
let utc: Time<Utc> = gps.into_scale_with(ls).unwrap();
let back: Time<Gps> = utc.into_scale_with(ls).unwrap();
assert_eq!(gps, back);
let gal: Time<Galileo> = gps.into_scale().unwrap();
let back: Time<Gps> = gal.into_scale().unwrap();
assert_eq!(gps, back);
let bdt: Time<Beidou> = gps.into_scale().unwrap();
let back: Time<Gps> = bdt.into_scale().unwrap();
assert_eq!(gps, back);
}
}
#[test]
fn test_gps_epoch_to_utc_is_exact() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::EPOCH;
let utc: Time<Utc> = gps.into_scale_with(ls).unwrap();
assert_eq!(utc.as_seconds(), 252_892_800);
}
#[test]
fn test_gps_epoch_utc_roundtrip() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::EPOCH;
let utc: Time<Utc> = gps.into_scale_with(ls).unwrap();
let back: Time<Gps> = utc.into_scale_with(ls).unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_glonass_roundtrip_invariants_supported_range() {
let ls = LeapSeconds::builtin();
let gps_values = [
Time::<Gps>::from_week_tow(
2086,
DurationParts {
seconds: 0,
nanos: 0,
},
)
.unwrap(),
Time::<Gps>::from_week_tow(
2100,
DurationParts {
seconds: 86_400,
nanos: 0,
},
)
.unwrap(),
Time::<Gps>::from_nanos(1_167_264_100_123_456_789),
];
for gps in gps_values {
let glo: Time<Glonass> = gps.into_scale_with(ls).unwrap();
let back: Time<Gps> = glo.into_scale_with(ls).unwrap();
assert_eq!(gps, back);
}
}
#[test]
fn test_checked_variants_contract() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::from_week_tow(
2000,
DurationParts {
seconds: 0,
nanos: 0,
},
)
.unwrap();
let res: ConvertResult<Time<Utc>> = gps.into_scale_with_checked(ls).unwrap();
match res {
ConvertResult::Exact(_) => {}
ConvertResult::AmbiguousLeapSecond(_) => panic!("unexpected ambiguity"),
}
}
#[test]
fn test_convert_result_consistency() {
let t = Time::<Gps>::from_seconds(42);
let exact = ConvertResult::Exact(t);
assert!(exact.is_exact());
assert!(!exact.is_ambiguous());
let amb = ConvertResult::AmbiguousLeapSecond(t);
assert!(!amb.is_exact());
assert!(amb.is_ambiguous());
}
#[test]
fn test_gps_to_tai_overflow_near_max() {
let gps = Time::<Gps>::from_nanos(Time::<Gps>::MAX.as_nanos() - 1);
let result: Result<Time<Tai>, _> = gps.into_scale();
assert!(matches!(result, Err(GnssTimeError::Overflow)));
}
#[test]
fn test_gps_to_tai_near_overflow_succeeds() {
let gps = Time::<Gps>::from_nanos(Time::<Gps>::MAX.as_nanos() - 20_000_000_000);
let tai: Time<Tai> = gps.into_scale().unwrap();
assert!(tai.as_nanos() > gps.as_nanos());
}
#[test]
fn test_glonass_utc_symmetry_random() {
let utc = Time::<Utc>::from_nanos(800_000_000_000_000_000);
let glo: Time<Glonass> = utc.into_scale().unwrap();
let back: Time<Utc> = glo.into_scale().unwrap();
assert_eq!(utc, back);
}
}