use crate::foundation::error::ConversionError;
use crate::model::scale::{CoordinateScale, BDT, GPST, GST, QZSST};
use crate::model::time::Time;
const SECONDS_PER_WEEK: qtty::i128::Second = qtty::i128::Second::new(7 * 86_400);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GnssWeek {
pub week: qtty::u32::Week,
pub seconds_of_week: qtty::u32::Second,
pub subsecond_nanos: qtty::u32::Nanosecond,
}
impl GnssWeek {
pub fn new(
week: qtty::u32::Week,
seconds_of_week: qtty::u32::Second,
subsecond_nanos: qtty::u32::Nanosecond,
) -> Result<Self, ConversionError> {
if seconds_of_week.value() as i128 >= SECONDS_PER_WEEK.value()
|| subsecond_nanos.value() >= 1_000_000_000
{
return Err(ConversionError::OutOfRange);
}
Ok(Self {
week,
seconds_of_week,
subsecond_nanos,
})
}
pub fn subsecond_nanoseconds_u(&self) -> qtty::u32::Nanosecond {
self.subsecond_nanos
}
pub fn seconds_of_week_u(&self) -> qtty::u32::Second {
self.seconds_of_week
}
pub fn new_with_nanoseconds_u(
week: qtty::u32::Week,
seconds_of_week: qtty::u32::Second,
subsecond: qtty::u32::Nanosecond,
) -> Result<Self, ConversionError> {
Self::new(week, seconds_of_week, subsecond)
}
pub fn to_duration_since_epoch(&self) -> crate::ExactDuration {
let week_count = self.week.value() as i128;
let sow = self.seconds_of_week.value() as i128;
let seconds = week_count * SECONDS_PER_WEEK.value() + sow;
let nanos = seconds * 1_000_000_000 + self.subsecond_nanos.value() as i128;
crate::ExactDuration::from_nanos(nanos)
}
}
pub trait GnssWeekScale: CoordinateScale {
fn epoch_j2000_seconds() -> f64;
fn rollover_period_weeks() -> u32;
}
const GPST_EPOCH_J2000_SECONDS: f64 = -630_763_200.0;
const GST_EPOCH_J2000_SECONDS: f64 = -11_447_987.0;
const BDT_EPOCH_J2000_SECONDS: f64 = 189_345_600.0;
const QZSST_EPOCH_J2000_SECONDS: f64 = GPST_EPOCH_J2000_SECONDS;
impl GnssWeekScale for GPST {
fn epoch_j2000_seconds() -> f64 {
GPST_EPOCH_J2000_SECONDS
}
fn rollover_period_weeks() -> u32 {
1024
}
}
impl GnssWeekScale for GST {
fn epoch_j2000_seconds() -> f64 {
GST_EPOCH_J2000_SECONDS
}
fn rollover_period_weeks() -> u32 {
4096
}
}
impl GnssWeekScale for BDT {
fn epoch_j2000_seconds() -> f64 {
BDT_EPOCH_J2000_SECONDS
}
fn rollover_period_weeks() -> u32 {
8192
}
}
impl GnssWeekScale for QZSST {
fn epoch_j2000_seconds() -> f64 {
QZSST_EPOCH_J2000_SECONDS
}
fn rollover_period_weeks() -> u32 {
1024
}
}
impl<S: GnssWeekScale> Time<S> {
pub fn to_gnss_week(&self) -> Result<GnssWeek, ConversionError> {
let (hi, lo) = self.to_j2000s().raw_seconds_pair();
let hi_val = hi.value();
let lo_val = lo.value();
let hi_int = hi_val.round();
let sub_sec = (hi_val - hi_int) + lo_val;
let epoch_i128 = S::epoch_j2000_seconds() as i128;
let hi_i128 = hi_int as i64 as i128;
let mut secs_since_epoch = hi_i128 - epoch_i128;
let raw_nanos = (sub_sec * 1.0e9).round() as i64;
let sub_nanos = if raw_nanos < 0 {
secs_since_epoch -= 1;
(raw_nanos + 1_000_000_000) as u32
} else if raw_nanos >= 1_000_000_000 {
secs_since_epoch += 1;
(raw_nanos - 1_000_000_000) as u32
} else {
raw_nanos as u32
};
if secs_since_epoch < 0 {
return Err(ConversionError::OutOfRange);
}
let total_secs = secs_since_epoch as u64;
let week_u64 = total_secs / SECONDS_PER_WEEK.value() as u64;
if week_u64 > u32::MAX as u64 {
return Err(ConversionError::OutOfRange);
}
let week = week_u64 as u32;
let seconds_of_week = (total_secs % SECONDS_PER_WEEK.value() as u64) as u32;
Ok(GnssWeek {
week: qtty::u32::Week::new(week),
seconds_of_week: qtty::u32::Second::new(seconds_of_week),
subsecond_nanos: qtty::u32::Nanosecond::new(sub_nanos),
})
}
pub fn from_gnss_week(gw: GnssWeek) -> Result<Self, ConversionError> {
let epoch = Time::<S>::from_raw_j2000_seconds(qtty::Second::new(S::epoch_j2000_seconds()))?;
Ok(epoch.add_exact(gw.to_duration_since_epoch()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::iso::parse_rfc3339_utc;
#[test]
fn gps_epoch_is_week_zero_second_zero() {
let utc = parse_rfc3339_utc("1980-01-06T00:00:00Z").unwrap();
let gpst: Time<GPST> = utc.to::<GPST>();
let gw = gpst.to_gnss_week().unwrap();
assert_eq!(gw.week.value(), 0, "expected week 0, got {gw:?}");
assert_eq!(gw.seconds_of_week.value(), 0, "expected sow=0, got {gw:?}");
assert_eq!(gw.subsecond_nanos.value(), 0, "expected ns=0, got {gw:?}");
}
#[test]
fn galileo_epoch_is_week_zero_second_zero() {
let utc = parse_rfc3339_utc("1999-08-22T00:00:00Z").unwrap();
let gst: Time<GST> = utc.to::<GST>();
let gw = gst.to_gnss_week().unwrap();
assert_eq!(gw.week.value(), 0, "expected GST week 0, got {gw:?}");
assert_eq!(gw.seconds_of_week.value(), 0, "expected sow=0, got {gw:?}");
assert_eq!(gw.subsecond_nanos.value(), 0, "expected ns=0, got {gw:?}");
}
#[test]
fn beidou_epoch_is_week_zero_second_zero() {
let utc = parse_rfc3339_utc("2006-01-01T00:00:00Z").unwrap();
let bdt: Time<BDT> = utc.to::<BDT>();
let gw = bdt.to_gnss_week().unwrap();
assert_eq!(gw.week.value(), 0, "expected BDT week 0, got {gw:?}");
assert_eq!(gw.seconds_of_week.value(), 0, "expected sow=0, got {gw:?}");
assert_eq!(gw.subsecond_nanos.value(), 0, "expected ns=0, got {gw:?}");
}
#[test]
fn qzsst_aligned_with_gpst() {
let utc = parse_rfc3339_utc("1980-01-06T00:00:00Z").unwrap();
let q: Time<QZSST> = utc.to::<QZSST>();
let gp: Time<GPST> = utc.to::<GPST>();
let qw = q.to_gnss_week().unwrap();
let gw = gp.to_gnss_week().unwrap();
assert_eq!(qw.week, gw.week);
assert_eq!(qw.seconds_of_week, gw.seconds_of_week);
assert_eq!(qw.subsecond_nanos, gw.subsecond_nanos);
}
#[test]
fn gps_week_round_trip_nanosecond_accurate() {
let gw = GnssWeek::new(
qtty::u32::Week::new(2200),
qtty::u32::Second::new(345_600),
qtty::u32::Nanosecond::new(123_456_789),
)
.unwrap();
let t = Time::<GPST>::from_gnss_week(gw).unwrap();
let back = t.to_gnss_week().unwrap();
assert_eq!(back.week, gw.week, "week mismatch: {back:?} vs {gw:?}");
assert_eq!(
back.seconds_of_week, gw.seconds_of_week,
"sow mismatch: {back:?} vs {gw:?}"
);
let ns_delta =
(back.subsecond_nanos.value() as i64 - gw.subsecond_nanos.value() as i64).abs();
assert!(
ns_delta <= 200,
"subsecond_nanos drift {ns_delta} ns: {back:?} vs {gw:?}"
);
}
#[test]
fn gps_week_boundary() {
let gw = GnssWeek::new(
qtty::u32::Week::new(2200),
qtty::u32::Second::new(604_799),
qtty::u32::Nanosecond::new(999_999_999),
)
.unwrap();
let t = Time::<GPST>::from_gnss_week(gw).unwrap();
let back = t.to_gnss_week().unwrap();
assert_eq!(back.week, gw.week, "week mismatch at boundary: {back:?}");
assert_eq!(
back.seconds_of_week, gw.seconds_of_week,
"sow mismatch at boundary: {back:?}"
);
let ns_delta =
(back.subsecond_nanos.value() as i64 - gw.subsecond_nanos.value() as i64).abs();
assert!(
ns_delta <= 200,
"subsecond_nanos drift {ns_delta} ns at boundary: {back:?}"
);
}
#[test]
fn gps_week_1024_no_rollover() {
let gw = GnssWeek::new(
qtty::u32::Week::new(1024),
qtty::u32::Second::new(0),
qtty::u32::Nanosecond::new(0),
)
.unwrap();
let t = Time::<GPST>::from_gnss_week(gw).unwrap();
let back = t.to_gnss_week().unwrap();
assert_eq!(back.week.value(), 1024);
assert_eq!(back.seconds_of_week.value(), 0);
assert_eq!(back.subsecond_nanos.value(), 0);
}
#[test]
fn gps_week_2048_no_rollover() {
let gw = GnssWeek::new(
qtty::u32::Week::new(2048),
qtty::u32::Second::new(0),
qtty::u32::Nanosecond::new(0),
)
.unwrap();
let t = Time::<GPST>::from_gnss_week(gw).unwrap();
let back = t.to_gnss_week().unwrap();
assert_eq!(back.week.value(), 2048);
assert_eq!(back.seconds_of_week.value(), 0);
assert_eq!(back.subsecond_nanos.value(), 0);
}
#[test]
fn rollover_periods_are_documented() {
assert_eq!(<GPST as GnssWeekScale>::rollover_period_weeks(), 1024);
assert_eq!(<GST as GnssWeekScale>::rollover_period_weeks(), 4096);
assert_eq!(<BDT as GnssWeekScale>::rollover_period_weeks(), 8192);
assert_eq!(<QZSST as GnssWeekScale>::rollover_period_weeks(), 1024);
}
#[test]
fn out_of_range_inputs_rejected() {
assert!(GnssWeek::new(
qtty::u32::Week::new(0),
qtty::u32::Second::new(604_800),
qtty::u32::Nanosecond::new(0),
)
.is_err());
assert!(GnssWeek::new(
qtty::u32::Week::new(0),
qtty::u32::Second::new(0),
qtty::u32::Nanosecond::new(1_000_000_000),
)
.is_err());
}
#[test]
fn subsecond_nanoseconds_u_matches_field() {
let gw = GnssWeek::new(
qtty::u32::Week::new(100),
qtty::u32::Second::new(12_345),
qtty::u32::Nanosecond::new(987_654_321),
)
.unwrap();
assert_eq!(gw.subsecond_nanoseconds_u().value(), 987_654_321_u32);
}
#[test]
fn new_with_nanoseconds_u_accepts_valid() {
let ns = qtty::u32::Nanosecond::new(123_456_789);
let gw = GnssWeek::new_with_nanoseconds_u(
qtty::u32::Week::new(500),
qtty::u32::Second::new(100_000),
ns,
)
.unwrap();
assert_eq!(gw.subsecond_nanos.value(), 123_456_789);
}
#[test]
fn new_with_nanoseconds_u_rejects_invalid() {
let big = qtty::u32::Nanosecond::new(1_000_000_000);
assert!(GnssWeek::new_with_nanoseconds_u(
qtty::u32::Week::new(0),
qtty::u32::Second::new(0),
big,
)
.is_err());
}
#[test]
fn to_gnss_week_overflow_returns_out_of_range() {
let gw_max = GnssWeek {
week: qtty::u32::Week::new(u32::MAX),
seconds_of_week: qtty::u32::Second::new(0),
subsecond_nanos: qtty::u32::Nanosecond::new(0),
};
let dur = gw_max.to_duration_since_epoch();
let (_s, _n) = dur
.as_seconds_i64_nanos_checked()
.expect("should fit in i64");
let epoch =
Time::<GPST>::from_raw_j2000_seconds(qtty::Second::new(GPST_EPOCH_J2000_SECONDS))
.unwrap();
let t = epoch.add_exact(dur);
let back = t.to_gnss_week().unwrap();
assert_eq!(back.week.value(), u32::MAX);
let overflow_secs = (u32::MAX as i128 + 1) * SECONDS_PER_WEEK.value();
let epoch_j2000 = GPST_EPOCH_J2000_SECONDS as i128;
let j2000_secs = epoch_j2000 + overflow_secs;
let t2 =
Time::<GPST>::from_raw_j2000_seconds(qtty::Second::new(j2000_secs as f64)).unwrap();
let result = t2.to_gnss_week();
assert!(
result.is_err(),
"expected OutOfRange for week > u32::MAX, got {result:?}"
);
}
}