use std::{
ops::{Add, AddAssign, Sub, SubAssign},
time::Duration,
};
use crate::time::{MJD, UTC_LEAPS, UtcParams, UtcTime, WEEK, consts};
#[derive(Debug, Copy, Clone)]
pub struct GpsTime {
tow: f64,
wn: i16,
}
pub const GAL_TIME_START: GpsTime = GpsTime {
wn: consts::GAL_WEEK_TO_GPS_WEEK,
tow: consts::GAL_SECOND_TO_GPS_SECOND,
};
pub const BDS_TIME_START: GpsTime = GpsTime {
wn: consts::BDS_WEEK_TO_GPS_WEEK,
tow: consts::BDS_SECOND_TO_GPS_SECOND,
};
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, thiserror::Error)]
pub enum InvalidGpsTime {
#[error("Invalid Week Number: {0}")]
InvalidWN(i16),
#[error("Invalid Time of Week: {0}")]
InvalidTOW(f64),
}
impl GpsTime {
pub fn new(wn: i16, tow: f64) -> Result<GpsTime, InvalidGpsTime> {
if wn < 0 {
Err(InvalidGpsTime::InvalidWN(wn))
} else if !tow.is_finite() || tow < 0.0 || tow >= WEEK.as_secs_f64() {
Err(InvalidGpsTime::InvalidTOW(tow))
} else {
Ok(GpsTime { tow, wn })
}
}
pub(crate) const fn new_unchecked(wn: i16, tow: f64) -> GpsTime {
GpsTime { tow, wn }
}
#[must_use]
pub fn from_parts(
year: u16,
month: u8,
day: u8,
hour: u8,
minute: u8,
seconds: f64,
utc_params: &UtcParams,
) -> GpsTime {
MJD::from_parts(year, month, day, hour, minute, seconds).to_gps(utc_params)
}
#[must_use]
pub fn from_parts_hardcoded(
year: u16,
month: u8,
day: u8,
hour: u8,
minute: u8,
seconds: f64,
) -> GpsTime {
MJD::from_parts(year, month, day, hour, minute, seconds).to_gps_hardcoded()
}
#[must_use]
pub fn wn(&self) -> i16 {
self.wn
}
#[must_use]
pub fn tow(&self) -> f64 {
self.tow
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.tow.is_finite()
&& self.tow >= 0.0
&& self.tow < f64::from(consts::WEEK_SECS)
&& self.wn >= 0
}
fn normalize(&mut self) {
while self.tow < 0.0 {
self.tow += f64::from(consts::WEEK_SECS);
self.wn -= 1;
}
while self.tow >= f64::from(consts::WEEK_SECS) {
self.tow -= f64::from(consts::WEEK_SECS);
self.wn += 1;
}
}
pub fn add_duration(&mut self, duration: &Duration) {
self.tow += duration.as_secs_f64();
self.normalize();
}
pub fn subtract_duration(&mut self, duration: &Duration) {
self.tow -= duration.as_secs_f64();
self.normalize();
}
#[must_use]
pub fn diff(&self, other: &Self) -> f64 {
let dt = self.tow - other.tow;
dt + f64::from(self.wn - other.wn) * f64::from(consts::WEEK_SECS)
}
fn internal_to_utc(self, params: Option<&UtcParams>) -> UtcTime {
let (is_lse, dt_utc) = params.map_or_else(
|| {
(
self.is_leap_second_event_hardcoded(),
self.gps_utc_offset_hardcoded(),
)
},
|p| (self.is_leap_second_event(p), self.gps_utc_offset(p)),
);
let mut tow_utc = self.tow - dt_utc;
if is_lse {
tow_utc -= 1.0;
}
let mut utc_time = GpsTime {
wn: self.wn,
tow: tow_utc,
};
utc_time.normalize();
let mut utc_time: UtcTime = UtcTime::from_gps_no_leap(utc_time);
if is_lse {
assert_eq!(utc_time.hour(), 23);
assert_eq!(utc_time.minute(), 59);
assert_eq!(utc_time.seconds_int(), 59);
utc_time.add_second();
}
utc_time
}
#[must_use]
pub fn to_utc(self, utc_params: &UtcParams) -> UtcTime {
self.internal_to_utc(Some(utc_params))
}
#[must_use]
pub fn to_utc_hardcoded(self) -> UtcTime {
self.internal_to_utc(None)
}
pub(crate) fn gps_utc_offset(&self, utc_params: &UtcParams) -> f64 {
let dt = self.diff(&utc_params.tot());
let mut dt_utc: f64 =
utc_params.a0() + (utc_params.a1() * dt) + (utc_params.a2() * dt * dt);
if self.diff(&utc_params.t_lse()) >= 1.0 {
dt_utc += f64::from(utc_params.dt_lsf());
} else {
dt_utc += f64::from(utc_params.dt_ls());
}
dt_utc
}
pub(crate) fn gps_utc_offset_hardcoded(&self) -> f64 {
for (t_leap, offset) in UTC_LEAPS.iter().rev() {
if self.diff(t_leap) >= 1.0 {
return *offset;
}
}
0.0
}
pub(crate) fn utc_gps_offset(&self, utc_params: &UtcParams) -> f64 {
let dt = self.diff(&utc_params.tot()) + f64::from(utc_params.dt_ls());
let mut dt_utc = utc_params.a0() + utc_params.a1() * dt + utc_params.a2() * dt * dt;
if self.diff(&utc_params.t_lse()) >= (f64::from(-utc_params.dt_ls()) - dt_utc) {
dt_utc += f64::from(utc_params.dt_lsf());
} else {
dt_utc += f64::from(utc_params.dt_ls());
}
-dt_utc
}
pub(crate) fn utc_gps_offset_hardcoded(&self) -> f64 {
for (t_leap, offset) in UTC_LEAPS.iter().rev() {
if self.diff(t_leap) >= (-offset + 1.0) {
return -offset;
}
}
0.0
}
#[must_use]
pub fn is_leap_second_event(&self, params: &UtcParams) -> bool {
let dt = self.diff(¶ms.t_lse());
(0.0..1.0).contains(&dt)
}
#[must_use]
pub fn is_leap_second_event_hardcoded(&self) -> bool {
for (t_leap, _offset) in UTC_LEAPS.iter().rev() {
let dt = self.diff(t_leap);
if dt > 1.0 {
return false;
}
if (0.0..1.0).contains(&dt) {
return true;
}
}
false
}
#[must_use]
pub fn to_mjd(self, utc_params: &UtcParams) -> MJD {
self.to_utc(utc_params).to_mjd()
}
#[must_use]
pub fn to_mjd_hardcoded(self) -> MJD {
self.to_utc_hardcoded().to_mjd()
}
#[must_use]
pub fn round_to_epoch(&self, soln_freq: f64) -> GpsTime {
let rounded_tow = (self.tow * soln_freq).round() / soln_freq;
let mut rounded_time = Self::new_unchecked(self.wn, rounded_tow);
rounded_time.normalize();
rounded_time
}
#[must_use]
pub fn floor_to_epoch(&self, soln_freq: f64) -> GpsTime {
let rounded_tow = (self.tow * soln_freq).floor() / soln_freq;
let mut rounded_time = GpsTime::new_unchecked(self.wn, rounded_tow);
rounded_time.normalize();
rounded_time
}
#[must_use]
pub fn to_gal(self) -> GalTime {
assert!(self.is_valid());
assert!(self >= GAL_TIME_START);
GalTime {
wn: self.wn() - consts::GAL_WEEK_TO_GPS_WEEK,
tow: self.tow(),
}
}
#[must_use]
pub fn to_bds(self) -> BdsTime {
assert!(self.is_valid());
assert!(self >= BDS_TIME_START);
let bds = GpsTime {
wn: self.wn() - consts::BDS_WEEK_TO_GPS_WEEK,
tow: self.tow(),
};
let bds = bds - Duration::from_secs_f64(consts::BDS_SECOND_TO_GPS_SECOND);
BdsTime {
wn: bds.wn(),
tow: bds.tow(),
}
}
#[rustversion::since(1.62)]
#[must_use]
pub fn total_cmp(&self, other: &GpsTime) -> std::cmp::Ordering {
if self.wn() == other.wn() {
let other = other.tow();
self.tow().total_cmp(&other)
} else {
self.wn().cmp(&other.wn())
}
}
#[must_use]
pub fn to_fractional_year(&self, utc_params: &UtcParams) -> f64 {
let utc = self.to_utc(utc_params);
utc.to_fractional_year()
}
#[must_use]
pub fn to_fractional_year_hardcoded(&self) -> f64 {
let utc = self.to_utc_hardcoded();
utc.to_fractional_year()
}
#[must_use]
pub fn to_date(self, utc_params: &UtcParams) -> (u16, u8, u8, u8, u8, f64) {
self.to_utc(utc_params).to_date()
}
#[must_use]
pub fn to_date_hardcoded(self) -> (u16, u8, u8, u8, u8, f64) {
self.to_utc_hardcoded().to_date()
}
}
impl Default for GpsTime {
fn default() -> Self {
GpsTime::new_unchecked(0, 0.0)
}
}
impl PartialEq for GpsTime {
fn eq(&self, other: &Self) -> bool {
let diff_seconds = self.diff(other).abs();
diff_seconds < consts::JIFFY
}
}
impl PartialOrd for GpsTime {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let diff_seconds = self.diff(other);
if diff_seconds.abs() < consts::JIFFY {
Some(std::cmp::Ordering::Equal)
} else if diff_seconds > 0.0 {
Some(std::cmp::Ordering::Greater)
} else {
Some(std::cmp::Ordering::Less)
}
}
}
impl Add<Duration> for GpsTime {
type Output = Self;
fn add(mut self, rhs: Duration) -> Self {
self.add_duration(&rhs);
self
}
}
impl AddAssign<Duration> for GpsTime {
fn add_assign(&mut self, rhs: Duration) {
self.add_duration(&rhs);
}
}
impl Sub<Duration> for GpsTime {
type Output = Self;
fn sub(mut self, rhs: Duration) -> Self::Output {
self.subtract_duration(&rhs);
self
}
}
impl SubAssign<Duration> for GpsTime {
fn sub_assign(&mut self, rhs: Duration) {
self.subtract_duration(&rhs);
}
}
impl From<GalTime> for GpsTime {
fn from(gal: GalTime) -> Self {
gal.to_gps()
}
}
impl From<BdsTime> for GpsTime {
fn from(bds: BdsTime) -> Self {
bds.to_gps()
}
}
#[derive(Debug, Copy, Clone)]
pub struct GalTime {
wn: i16,
tow: f64,
}
impl GalTime {
pub fn new(wn: i16, tow: f64) -> Result<GalTime, InvalidGpsTime> {
if wn < 0 {
Err(InvalidGpsTime::InvalidWN(wn))
} else if !tow.is_finite() || tow < 0.0 || tow >= WEEK.as_secs_f64() {
Err(InvalidGpsTime::InvalidTOW(tow))
} else {
Ok(GalTime { wn, tow })
}
}
#[must_use]
pub fn wn(&self) -> i16 {
self.wn
}
#[must_use]
pub fn tow(&self) -> f64 {
self.tow
}
#[must_use]
pub fn to_gps(self) -> GpsTime {
GpsTime {
wn: self.wn + consts::GAL_WEEK_TO_GPS_WEEK,
tow: self.tow,
}
}
#[must_use]
pub fn to_bds(self) -> BdsTime {
self.to_gps().to_bds()
}
}
impl From<GpsTime> for GalTime {
fn from(gps: GpsTime) -> Self {
gps.to_gal()
}
}
impl From<BdsTime> for GalTime {
fn from(bds: BdsTime) -> Self {
bds.to_gal()
}
}
#[derive(Debug, Copy, Clone)]
pub struct BdsTime {
wn: i16,
tow: f64,
}
impl BdsTime {
pub fn new(wn: i16, tow: f64) -> Result<BdsTime, InvalidGpsTime> {
if wn < 0 {
Err(InvalidGpsTime::InvalidWN(wn))
} else if !tow.is_finite() || tow < 0.0 || tow >= WEEK.as_secs_f64() {
Err(InvalidGpsTime::InvalidTOW(tow))
} else {
Ok(BdsTime { wn, tow })
}
}
#[must_use]
pub fn wn(&self) -> i16 {
self.wn
}
#[must_use]
pub fn tow(&self) -> f64 {
self.tow
}
#[must_use]
pub fn to_gps(self) -> GpsTime {
let gps = GpsTime {
wn: self.wn() + consts::BDS_WEEK_TO_GPS_WEEK,
tow: self.tow(),
};
gps + Duration::from_secs_f64(consts::BDS_SECOND_TO_GPS_SECOND)
}
#[must_use]
pub fn to_gal(self) -> GalTime {
self.to_gps().to_gal()
}
}
impl From<GpsTime> for BdsTime {
fn from(gps: GpsTime) -> Self {
gps.to_bds()
}
}
impl From<GalTime> for BdsTime {
fn from(gal: GalTime) -> Self {
gal.to_bds()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validity() {
assert!(GpsTime::new(0, 0.0).is_ok());
assert!(GpsTime::new(-1, -1.0).is_err());
assert!(GpsTime::new(-1, -1.0).is_err());
assert!(GpsTime::new(12, WEEK.as_secs_f64()).is_err());
assert!(GpsTime::new(12, f64::NAN).is_err());
assert!(GpsTime::new(12, f64::INFINITY).is_err());
}
#[test]
fn equality() {
let t1 = GpsTime::new(10, 234.567).unwrap();
assert!(t1 == t1);
let t2 = GpsTime::new(10, 234.5678).unwrap();
assert!(t1 != t2);
assert!(t2 != t1);
}
#[test]
fn ordering() {
let t1 = GpsTime::new(10, 234.566).unwrap();
let t2 = GpsTime::new(10, 234.567).unwrap();
let t3 = GpsTime::new(10, 234.568).unwrap();
assert!(t1 < t2);
assert!(t1 < t3);
assert!(t2 > t1);
assert!(t2 < t3);
assert!(t3 > t1);
assert!(t3 > t2);
assert!(t1 <= t1);
assert!(t1 >= t1);
assert!(t1 <= t2);
assert!(t1 <= t3);
assert!(t2 >= t1);
assert!(t2 <= t2);
assert!(t2 >= t2);
assert!(t2 <= t3);
assert!(t3 >= t1);
assert!(t3 >= t2);
assert!(t3 <= t3);
assert!(t3 >= t3);
}
#[rustversion::since(1.62)]
#[test]
fn total_order() {
use std::cmp::Ordering;
let t1 = GpsTime::new(10, 234.566).unwrap();
let t2 = GpsTime::new(10, 234.567).unwrap();
let t3 = GpsTime::new(10, 234.568).unwrap();
assert!(t1.total_cmp(&t2) == Ordering::Less);
assert!(t2.total_cmp(&t3) == Ordering::Less);
assert!(t1.total_cmp(&t3) == Ordering::Less);
assert!(t2.total_cmp(&t1) == Ordering::Greater);
assert!(t3.total_cmp(&t2) == Ordering::Greater);
assert!(t3.total_cmp(&t1) == Ordering::Greater);
assert!(t1.total_cmp(&t1) == Ordering::Equal);
}
#[test]
fn add_duration() {
let mut t = GpsTime::new(0, 0.0).unwrap();
let t_expected = GpsTime::new(0, 1.001).unwrap();
let d = Duration::new(1, 1_000_000);
t.add_duration(&d);
assert_eq!(t, t_expected);
let t = GpsTime::new(0, 0.0).unwrap();
let t = t + d;
assert_eq!(t, t_expected);
let mut t = GpsTime::new(0, 0.0).unwrap();
t += d;
assert_eq!(t, t_expected);
}
#[test]
fn subtract_duration() {
let mut t = GpsTime::new(0, 1.001).unwrap();
let t_expected = GpsTime::new(0, 0.0).unwrap();
let d = Duration::new(1, 1_000_000);
t.subtract_duration(&d);
assert_eq!(t, t_expected);
t.subtract_duration(&d);
assert!(!t.is_valid());
let t = GpsTime::new(0, 1.001).unwrap();
let t = t - d;
assert_eq!(t, t_expected);
let mut t = GpsTime::new(0, 1.001).unwrap();
t -= d;
assert_eq!(t, t_expected);
}
#[test]
fn round_to_epoch() {
let soln_freq = 10.0;
let epsilon = 1e-5;
let test_cases = [
GpsTime::new_unchecked(1234, 567_890.01),
GpsTime::new_unchecked(1234, 567_890.050_1),
GpsTime::new_unchecked(1234, 604_800.06),
];
let expectations = [
GpsTime::new_unchecked(1234, 567_890.00),
GpsTime::new_unchecked(1234, 567_890.10),
GpsTime::new_unchecked(1235, 0.1),
];
for (test_case, expectation) in test_cases.iter().zip(expectations.iter()) {
let rounded = test_case.round_to_epoch(soln_freq);
let diff = if &rounded >= expectation {
rounded.diff(expectation)
} else {
expectation.diff(&rounded)
};
assert!(diff < epsilon);
}
}
#[test]
fn floor_to_epoch() {
let soln_freq = 10.0;
let epsilon = 1e-6;
let test_cases = [
GpsTime::new_unchecked(1234, 567_890.01),
GpsTime::new_unchecked(1234, 567_890.050_1),
GpsTime::new_unchecked(1234, 604_800.06),
];
let expectations = [
GpsTime::new_unchecked(1234, 567_890.00),
GpsTime::new_unchecked(1234, 567_890.00),
GpsTime::new_unchecked(1235, 0.0),
];
for (test_case, expectation) in test_cases.iter().zip(expectations.iter()) {
let rounded = test_case.floor_to_epoch(soln_freq);
assert!(rounded.diff(expectation) < epsilon);
}
}
#[test]
fn gps_to_gal() {
let gal = GAL_TIME_START.to_gal();
assert_eq!(gal.wn(), 0);
assert!(gal.tow().abs() < 1e-9);
let gps = gal.to_gps();
assert_eq!(gps.wn(), consts::GAL_WEEK_TO_GPS_WEEK);
assert!(gps.tow().abs() < 1e-9);
assert!(GalTime::new(-1, 0.0).is_err());
assert!(GalTime::new(0, -1.0).is_err());
assert!(GalTime::new(0, f64::from(consts::WEEK_SECS) + 1.0).is_err());
}
#[test]
fn gps_to_bds() {
let bds = BDS_TIME_START.to_bds();
assert_eq!(bds.wn(), 0);
assert!(bds.tow().abs() < 1e-9);
let gps = bds.to_gps();
assert_eq!(gps.wn(), consts::BDS_WEEK_TO_GPS_WEEK);
assert!((gps.tow() - consts::BDS_SECOND_TO_GPS_SECOND).abs() < 1e-9);
assert!(BdsTime::new(-1, 0.0).is_err());
assert!(BdsTime::new(0, -1.0).is_err());
assert!(BdsTime::new(0, f64::from(consts::WEEK_SECS) + 1.0).is_err());
}
}