use crate::{
tables::BUILTIN_TABLE, Beidou, CivilDate, Galileo, Glonass, GnssTimeError, Gps, Tai, Time, Utc,
};
pub const RUNTIME_CAPACITY: usize = 64;
static BUILTIN_LEAP_SECONDS: LeapSeconds = LeapSeconds {
entries: &BUILTIN_TABLE,
};
const GLONASS_FROM_UTC_EPOCH_NS: i64 = {
let to_1996 = CivilDate::new(1972, 1, 1).nanos_until(CivilDate::new(1996, 1, 1));
to_1996 - 3 * 3_600 * 1_000_000_000_i64
};
const _VERIFY_GLONASS_OFFSET: () = {
let s = GLONASS_FROM_UTC_EPOCH_NS / 1_000_000_000;
assert!(
s == 757_371_600,
"GLONASS -> UTC epoch offset must be 757371600 s"
);
};
const UTC_TO_GPS_EPOCH_NS: i64 = CivilDate::new(1972, 1, 1).nanos_until(CivilDate::new(1980, 1, 6));
const _VERIFY_UTC_GPS_OFFSET: () = {
let s = UTC_TO_GPS_EPOCH_NS / 1_000_000_000;
assert!(
s == 252_892_800,
"UTC -> GPS epoch offset must be 252892800 s (2927 days)"
);
};
pub trait LeapSecondsProvider {
fn tai_minus_utc_at(
&self,
tai: Time<Tai>,
) -> i32;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[must_use = "handle the extension error; ignoring it means the table was not updated"]
#[non_exhaustive]
pub enum LeapExtendError {
NotStrictlyAscending,
NonUnitIncrement,
BufferFull,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LeapEntry {
pub tai_nanos: u64,
pub tai_minus_utc: i32,
}
pub struct LeapSeconds {
entries: &'static [LeapEntry], }
#[derive(Debug)]
pub struct RuntimeLeapSeconds {
buf: [LeapEntry; RUNTIME_CAPACITY],
len: usize,
}
impl LeapEntry {
#[inline]
#[must_use]
pub const fn new(
tai_nanos: u64,
tai_minus_utc: i32,
) -> Self {
LeapEntry {
tai_nanos,
tai_minus_utc,
}
}
}
impl LeapSeconds {
#[inline]
#[must_use]
pub fn builtin() -> &'static LeapSeconds {
&BUILTIN_LEAP_SECONDS
}
#[inline]
#[must_use]
pub const fn from_slice(entries: &'static [LeapEntry]) -> Self {
Self { entries }
}
#[inline]
#[must_use]
pub const fn from_table(entries: &'static [LeapEntry]) -> Self {
Self { entries }
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[inline]
#[must_use]
pub fn entries(&self) -> &[LeapEntry] {
self.entries
}
#[inline]
#[must_use]
pub const fn last_update(&self) -> Option<Time<Tai>> {
if self.entries.len() <= 1 {
return None;
}
let last = &self.entries[self.entries.len() - 1];
Some(Time::<Tai>::from_nanos(last.tai_nanos))
}
#[inline]
#[must_use]
pub const fn current_tai_minus_utc(&self) -> i32 {
if self.entries.is_empty() {
return 19;
}
self.entries[self.entries.len() - 1].tai_minus_utc
}
}
impl RuntimeLeapSeconds {
#[inline]
#[must_use]
pub fn new() -> Self {
Self {
buf: [LeapEntry::new(0, 0); RUNTIME_CAPACITY],
len: 0,
}
}
#[must_use]
pub fn from_builtin() -> Self {
assert!(
BUILTIN_TABLE.len() <= RUNTIME_CAPACITY,
"BUILTIN_TABLE exceeds RUNTIME_CAPACITY"
);
let mut rt = Self::new();
for &entry in BUILTIN_TABLE.iter() {
rt.buf[rt.len] = entry;
rt.len += 1;
}
rt
}
#[inline]
pub fn from_slice(entries: &[LeapEntry]) -> Result<Self, LeapExtendError> {
if entries.len() > RUNTIME_CAPACITY {
return Err(LeapExtendError::BufferFull);
}
let mut rt = Self::new();
for &entry in entries {
rt.buf[rt.len] = entry;
rt.len += 1;
}
Ok(rt)
}
pub fn try_extend(
&mut self,
entry: LeapEntry,
) -> Result<(), LeapExtendError> {
if self.len >= RUNTIME_CAPACITY {
return Err(LeapExtendError::BufferFull);
}
if self.len > 0 {
let last = &self.buf[self.len - 1];
if entry.tai_nanos <= last.tai_nanos {
return Err(LeapExtendError::NotStrictlyAscending);
}
if entry.tai_minus_utc != last.tai_minus_utc + 1 {
return Err(LeapExtendError::NonUnitIncrement);
}
}
self.buf[self.len] = entry;
self.len += 1;
Ok(())
}
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
#[must_use]
pub fn entries(&self) -> &[LeapEntry] {
&self.buf[..self.len]
}
#[inline]
#[must_use]
pub const fn last_update(&self) -> Option<Time<Tai>> {
if self.len <= 1 {
return None;
}
Some(Time::<Tai>::from_nanos(self.buf[self.len - 1].tai_nanos))
}
#[inline]
#[must_use]
pub const fn current_tai_minus_utc(&self) -> i32 {
if self.len == 0 {
return 19;
}
self.buf[self.len - 1].tai_minus_utc
}
}
impl LeapSecondsProvider for LeapSeconds {
fn tai_minus_utc_at(
&self,
tai: Time<Tai>,
) -> i32 {
let nanos = tai.as_nanos();
let entries = self.entries;
if entries.is_empty() {
return 19; }
match entries.binary_search_by_key(&nanos, |e| e.tai_nanos) {
Ok(i) => entries[i].tai_minus_utc,
Err(0) => entries[0].tai_minus_utc,
Err(i) => entries[i - 1].tai_minus_utc,
}
}
}
impl LeapSecondsProvider for RuntimeLeapSeconds {
fn tai_minus_utc_at(
&self,
tai: Time<Tai>,
) -> i32 {
let entries = self.entries();
let nanos = tai.as_nanos();
if entries.is_empty() {
return 19;
}
match entries.binary_search_by_key(&nanos, |e| e.tai_nanos) {
Ok(i) => entries[i].tai_minus_utc,
Err(0) => entries[0].tai_minus_utc,
Err(i) => entries[i - 1].tai_minus_utc,
}
}
}
impl<P: LeapSecondsProvider> LeapSecondsProvider for &P {
fn tai_minus_utc_at(
&self,
tai: Time<Tai>,
) -> i32 {
(*self).tai_minus_utc_at(tai)
}
}
pub fn glonass_to_utc(glo: Time<Glonass>) -> Result<Time<Utc>, GnssTimeError> {
let utc_ns = (glo.as_nanos() as i128) + (GLONASS_FROM_UTC_EPOCH_NS as i128);
if utc_ns < 0 || utc_ns > u64::MAX as i128 {
return Err(GnssTimeError::Overflow);
}
Ok(Time::<Utc>::from_nanos(utc_ns as u64))
}
pub fn glonass_to_gps<P: LeapSecondsProvider>(
glo: Time<Glonass>,
ls: &P,
) -> Result<Time<Gps>, GnssTimeError> {
let utc = glonass_to_utc(glo)?;
utc_to_gps(utc, ls)
}
pub fn glonass_to_galileo<P: LeapSecondsProvider>(
glo: Time<Glonass>,
ls: &P,
) -> Result<Time<Galileo>, GnssTimeError> {
let utc = glonass_to_utc(glo)?;
utc_to_galileo(utc, ls)
}
pub fn glonass_to_beidou<P: LeapSecondsProvider>(
glo: Time<Glonass>,
ls: &P,
) -> Result<Time<Beidou>, GnssTimeError> {
let utc = glonass_to_utc(glo)?;
utc_to_beidou(utc, ls)
}
pub fn gps_to_utc<P: LeapSecondsProvider>(
gps: Time<Gps>,
ls: &P,
) -> Result<Time<Utc>, GnssTimeError> {
let tai = gps.to_tai()?;
let n = ls.tai_minus_utc_at(tai);
let utc_ns = (gps.as_nanos() as i128) - ((n - 19) as i128 * 1_000_000_000_i128)
+ (UTC_TO_GPS_EPOCH_NS as i128);
if utc_ns < 0 || utc_ns > u64::MAX as i128 {
return Err(GnssTimeError::Overflow);
}
Ok(Time::<Utc>::from_nanos(utc_ns as u64))
}
pub fn gps_to_glonass<P: LeapSecondsProvider>(
gps: Time<Gps>,
ls: &P,
) -> Result<Time<Glonass>, GnssTimeError> {
let utc = gps_to_utc(gps, ls)?;
utc_to_glonass(utc)
}
pub fn galileo_to_utc<P: LeapSecondsProvider>(
gal: Time<Galileo>,
ls: &P,
) -> Result<Time<Utc>, GnssTimeError> {
let gps = gal.try_convert::<Gps>()?;
gps_to_utc(gps, ls)
}
pub fn galileo_to_glonass<P: LeapSecondsProvider>(
gal: Time<Galileo>,
ls: &P,
) -> Result<Time<Glonass>, GnssTimeError> {
let utc = galileo_to_utc(gal, ls)?;
utc_to_glonass(utc)
}
pub fn beidou_to_utc<P: LeapSecondsProvider>(
bdt: Time<Beidou>,
ls: &P,
) -> Result<Time<Utc>, GnssTimeError> {
let gps = bdt.try_convert::<Gps>()?;
gps_to_utc(gps, ls)
}
pub fn beidou_to_glonass<P: LeapSecondsProvider>(
bdt: Time<Beidou>,
ls: &P,
) -> Result<Time<Glonass>, GnssTimeError> {
let utc = beidou_to_utc(bdt, ls)?;
utc_to_glonass(utc)
}
pub fn utc_to_glonass(utc: Time<Utc>) -> Result<Time<Glonass>, GnssTimeError> {
let glo_ns = (utc.as_nanos() as i128) - (GLONASS_FROM_UTC_EPOCH_NS as i128);
if glo_ns < 0 || glo_ns > u64::MAX as i128 {
return Err(GnssTimeError::Overflow);
}
Ok(Time::<Glonass>::from_nanos(glo_ns as u64))
}
pub fn utc_to_gps<P: LeapSecondsProvider>(
utc: Time<Utc>,
ls: &P,
) -> Result<Time<Gps>, GnssTimeError> {
let approx_tai_ns =
(utc.as_nanos() as i128) - (UTC_TO_GPS_EPOCH_NS as i128) + 19_000_000_000_i128;
let tai1 = if approx_tai_ns >= 0 && approx_tai_ns <= u64::MAX as i128 {
Time::<Tai>::from_nanos(approx_tai_ns as u64)
} else {
Time::<Tai>::EPOCH
};
let n1 = ls.tai_minus_utc_at(tai1);
let refined_tai_ns = (utc.as_nanos() as i128) - (UTC_TO_GPS_EPOCH_NS as i128)
+ (n1 as i128 * 1_000_000_000_i128);
let tai2 = if refined_tai_ns >= 0 && refined_tai_ns <= u64::MAX as i128 {
Time::<Tai>::from_nanos(refined_tai_ns as u64)
} else {
tai1
};
let n = ls.tai_minus_utc_at(tai2);
let gps_ns = (utc.as_nanos() as i128) + ((n - 19) as i128 * 1_000_000_000_i128)
- (UTC_TO_GPS_EPOCH_NS as i128);
if gps_ns < 0 || gps_ns > u64::MAX as i128 {
return Err(GnssTimeError::Overflow);
}
Ok(Time::<Gps>::from_nanos(gps_ns as u64))
}
pub fn utc_to_galileo<P: LeapSecondsProvider>(
utc: Time<Utc>,
ls: &P,
) -> Result<Time<Galileo>, GnssTimeError> {
let gps = utc_to_gps(utc, ls)?;
gps.try_convert::<Galileo>()
}
pub fn utc_to_beidou<P: LeapSecondsProvider>(
utc: Time<Utc>,
ls: &P,
) -> Result<Time<Beidou>, GnssTimeError> {
let gps = utc_to_gps(utc, ls)?;
gps.try_convert::<Beidou>()
}
impl core::fmt::Display for LeapExtendError {
fn fmt(
&self,
f: &mut core::fmt::Formatter<'_>,
) -> core::fmt::Result {
match self {
LeapExtendError::NotStrictlyAscending => {
f.write_str("new entry tai_nanos is not strictly greater than the last entry")
}
LeapExtendError::NonUnitIncrement => {
f.write_str("new entry tai_minus_utc be exactly one more tham the last entry")
}
LeapExtendError::BufferFull => {
f.write_str("runtime leap-second buffer is full; cannot add more entries")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for LeapExtendError {}
impl Default for RuntimeLeapSeconds {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use std::string::ToString;
use super::*;
use crate::{scale::Gps, DurationParts};
#[test]
fn test_utc_to_gps_epoch_offset_is_252892800_seconds() {
assert_eq!(UTC_TO_GPS_EPOCH_NS / 1_000_000_000, 252_892_800);
}
#[test]
fn test_glonass_epoch_offset_is_757371600_seconds() {
assert_eq!(GLONASS_FROM_UTC_EPOCH_NS / 1_000_000_000, 757_371_600);
}
#[test]
fn test_builtin_table_length() {
assert_eq!(LeapSeconds::builtin().len(), 19);
}
#[test]
fn test_utc_to_gps_epoch_offset_is_2927_days() {
assert_eq!(UTC_TO_GPS_EPOCH_NS / 1_000_000_000 / 86_400, 2927);
}
#[test]
fn test_glonass_epoch_offset_from_utc_epoch_is_correct() {
assert_eq!(GLONASS_FROM_UTC_EPOCH_NS / 1_000_000_000, 757_371_600);
}
#[test]
fn test_builtin_table_is_sorted() {
let entries = LeapSeconds::builtin().entries();
for w in entries.windows(2) {
assert!(
w[0].tai_nanos < w[1].tai_nanos,
"table not sorted at {:?}",
w
);
}
}
#[test]
fn test_builtin_table_starts_with_tai_minus_utc_19() {
assert_eq!(LeapSeconds::builtin().entries()[0].tai_minus_utc, 19);
}
#[test]
fn test_builtin_table_ends_with_tai_minus_utc_37() {
let last = *LeapSeconds::builtin().entries().last().unwrap();
assert_eq!(last.tai_minus_utc, 37);
}
#[test]
fn test_builtin_table_has_monotone_increasing_tai_minus_utc() {
let entries = LeapSeconds::builtin().entries();
for w in entries.windows(2) {
assert_eq!(
w[1].tai_minus_utc,
w[0].tai_minus_utc + 1,
"expected each entry to increment by 1"
);
}
}
#[test]
fn test_builtin_table_matches_iers_bulletin_c() {
const GPS_EPOCH_UNIX: u64 = 315_964_800;
let iers_events: &[(u64, i32)] = &[
(362_793_600, 20), (394_329_600, 21), (425_865_600, 22), (489_024_000, 23), (567_993_600, 24), (631_152_000, 25), (662_688_000, 26), (709_948_800, 27), (741_484_800, 28), (773_020_800, 29), (820_454_400, 30), (867_715_200, 31), (915_148_800, 32), (1_136_073_600, 33), (1_230_768_000, 34), (1_341_100_800, 35), (1_435_708_800, 36), (1_483_228_800, 37), ];
let entries = LeapSeconds::builtin().entries();
assert_eq!(entries[0].tai_nanos, 0);
assert_eq!(entries[0].tai_minus_utc, 19);
for (idx, &(unix, expected_n)) in iers_events.iter().enumerate() {
let gps_s = unix - GPS_EPOCH_UNIX;
let expected_threshold = (gps_s + expected_n as u64) * 1_000_000_000;
let entry = &entries[idx + 1];
assert_eq!(
entry.tai_nanos,
expected_threshold,
"threshold mismatch at IERS event {} (unix={})",
idx + 1,
unix
);
assert_eq!(
entry.tai_minus_utc,
expected_n,
"tai_minus_utc mismatch at IERS event {} (unix={})",
idx + 1,
unix
);
}
}
#[test]
fn test_last_update_builtin_is_2017_threshold() {
let last = LeapSeconds::builtin()
.last_update()
.expect("builtin must have last_update");
assert_eq!(last.as_nanos(), 1_167_264_037_000_000_000);
}
#[test]
fn test_last_update_single_entry_is_none() {
static SINGLE: [LeapEntry; 1] = [LeapEntry::new(0, 37)];
let ls = LeapSeconds::from_slice(&SINGLE);
assert!(ls.last_update().is_none());
}
#[test]
fn test_last_update_empty_is_none() {
static EMPTY: [LeapEntry; 0] = [];
let ls = LeapSeconds::from_slice(&EMPTY);
assert!(ls.last_update().is_none());
}
#[test]
fn test_current_tai_minus_utc_builtin_is_37() {
assert_eq!(LeapSeconds::builtin().current_tai_minus_utc(), 37);
}
#[test]
fn test_current_tai_minus_utc_empty_is_fallback_19() {
static EMPTY: [LeapEntry; 0] = [];
let ls = LeapSeconds::from_slice(&EMPTY);
assert_eq!(ls.current_tai_minus_utc(), 19);
}
#[test]
fn test_from_slice_and_from_table_are_equivalent() {
static TABLE: [LeapEntry; 2] = [LeapEntry::new(0, 19), LeapEntry::new(1_000_000, 20)];
let ls_slice = LeapSeconds::from_slice(&TABLE);
let ls_table = LeapSeconds::from_table(&TABLE);
assert_eq!(ls_slice.len(), ls_table.len());
assert_eq!(
ls_slice.entries()[0].tai_nanos,
ls_table.entries()[0].tai_nanos
);
}
#[test]
fn test_lookup_at_tai_zero_returns_19() {
let ls = LeapSeconds::builtin();
assert_eq!(ls.tai_minus_utc_at(Time::<Tai>::EPOCH), 19);
}
#[test]
fn test_lookup_at_max_tai_returns_37() {
let ls = LeapSeconds::builtin();
assert_eq!(ls.tai_minus_utc_at(Time::<Tai>::MAX), 37);
}
#[test]
fn test_lookup_at_max_tai_returns_last_value() {
let ls = LeapSeconds::builtin();
assert_eq!(ls.tai_minus_utc_at(Time::<Tai>::MAX), 37);
}
#[test]
fn test_lookup_at_exact_2017_threshold_returns_37() {
let ls = LeapSeconds::builtin();
let tai = Time::<Tai>::from_nanos(1_167_264_037_000_000_000);
assert_eq!(ls.tai_minus_utc_at(tai), 37);
}
#[test]
fn test_lookup_one_ns_before_2017_threshold_returns_36() {
let ls = LeapSeconds::builtin();
let tai = Time::<Tai>::from_nanos(1_167_264_037_000_000_000 - 1);
assert_eq!(ls.tai_minus_utc_at(tai), 36);
}
#[test]
fn test_lookup_at_1999_threshold_returns_32() {
let ls = LeapSeconds::builtin();
let tai = Time::<Tai>::from_nanos(599_184_032_000_000_000);
assert_eq!(ls.tai_minus_utc_at(tai), 32);
}
#[test]
fn test_lookup_one_ns_before_1999_threshold_returns_31() {
let ls = LeapSeconds::builtin();
let tai = Time::<Tai>::from_nanos(599_184_032_000_000_000 - 1);
assert_eq!(ls.tai_minus_utc_at(tai), 31);
}
#[test]
fn test_gps_utc_gps_roundtrip_at_gps_epoch() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::EPOCH;
let utc = gps_to_utc(gps, &ls).unwrap();
let back = utc_to_gps(utc, &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 = gps_to_utc(gps, &ls).unwrap();
let back = utc_to_gps(utc, &ls).unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_gps_epoch_utc_is_correct_offset_from_utc_epoch() {
let ls = LeapSeconds::builtin();
let utc = gps_to_utc(Time::<Gps>::EPOCH, &ls).unwrap();
assert_eq!(utc.as_nanos(), 252_892_800_000_000_000);
}
#[test]
fn test_gps_minus_utc_is_18s_at_2017_01_01() {
let ls = LeapSeconds::builtin();
let gps_s: u64 = 1_167_264_000 + 18;
let gps = Time::<Gps>::from_seconds(gps_s);
let utc = gps_to_utc(gps, &ls).unwrap();
let expected_utc_ns: u64 = 16_437 * 86_400 * 1_000_000_000;
assert_eq!(utc.as_nanos(), expected_utc_ns);
}
#[test]
fn test_gps_minus_utc_is_13s_at_1999_01_01() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::from_seconds(599_184_013);
let utc = gps_to_utc(gps, &ls).unwrap();
let expected_utc_s: u64 = 9_862 * 86_400;
assert_eq!(utc.as_seconds(), expected_utc_s);
}
#[test]
fn test_leap_second_transition_1999_gps_jumps_by_2s() {
let ls = LeapSeconds::builtin();
let gps_before = Time::<Gps>::from_seconds(599_184_011);
let gps_after = Time::<Gps>::from_seconds(599_184_013);
let utc_before = gps_to_utc(gps_before, &ls).unwrap();
let utc_after = gps_to_utc(gps_after, &ls).unwrap();
let diff = (utc_after - utc_before).as_seconds();
assert_eq!(diff, 1, "GPS jumped 2s but UTC advanced 1s (leap second)");
}
#[test]
fn test_leap_second_transition_2017_gps_jumps_by_2s() {
let ls = LeapSeconds::builtin();
let gps_before = Time::<Gps>::from_seconds(1_167_263_999 + 17);
let gps_after = Time::<Gps>::from_seconds(1_167_264_000 + 18);
let utc_before = gps_to_utc(gps_before, &ls).unwrap();
let utc_after = gps_to_utc(gps_after, &ls).unwrap();
let diff = (utc_after - utc_before).as_seconds();
assert_eq!(diff, 1, "GPS jumped 2s but UTC advanced 1s");
}
#[test]
fn test_glonass_epoch_to_utc_gives_correct_nanos() {
let utc = glonass_to_utc(Time::<Glonass>::EPOCH).unwrap();
assert_eq!(utc.as_nanos(), GLONASS_FROM_UTC_EPOCH_NS as u64);
}
#[test]
fn test_utc_to_glonass_epoch_gives_zero() {
let utc = Time::<Utc>::from_nanos(GLONASS_FROM_UTC_EPOCH_NS as u64);
let glo = utc_to_glonass(utc).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: 43_200,
nanos: 0,
},
)
.unwrap();
let utc = glonass_to_utc(glo).unwrap();
let back = utc_to_glonass(utc).unwrap();
assert_eq!(glo, back);
}
#[test]
fn test_utc_before_glonass_epoch_returns_error() {
let utc = Time::<Utc>::EPOCH;
assert!(matches!(utc_to_glonass(utc), Err(GnssTimeError::Overflow)));
}
#[test]
fn test_glonass_offset_is_exactly_3_hours_less_than_day_boundary() {
let three_hours_ns: i64 = 3 * 3_600 * 1_000_000_000;
let days_ns: i64 = 8766 * 86_400 * 1_000_000_000;
assert_eq!(GLONASS_FROM_UTC_EPOCH_NS, days_ns - three_hours_ns);
}
#[test]
fn test_gps_to_glonass_to_gps_roundtrip() {
let ls = LeapSeconds::builtin();
let gps = Time::<Gps>::from_week_tow(
2100,
DurationParts {
seconds: 86400,
nanos: 0,
},
)
.unwrap();
let glo = gps_to_glonass(gps, &ls).unwrap();
let back = glonass_to_gps(glo, &ls).unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_custom_provider_works() {
struct Always37;
impl LeapSecondsProvider for Always37 {
fn tai_minus_utc_at(
&self,
_: Time<Tai>,
) -> i32 {
37
}
}
let gps = Time::<Gps>::from_seconds(1_000_000_000);
let utc = gps_to_utc(gps, &Always37).unwrap();
let back = utc_to_gps(utc, &Always37).unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_empty_table_returns_fallback_19() {
static EMPTY: [LeapEntry; 0] = [];
let ls = LeapSeconds::from_table(&EMPTY);
assert_eq!(
ls.tai_minus_utc_at(Time::<Tai>::from_seconds(1_000_000)),
19
);
}
#[test]
fn test_runtime_from_builtin_has_19_entries() {
assert_eq!(RuntimeLeapSeconds::from_builtin().len(), 19);
}
#[test]
fn test_runtime_from_builtin_current_is_37() {
assert_eq!(
RuntimeLeapSeconds::from_builtin().current_tai_minus_utc(),
37
);
}
#[test]
fn test_runtime_try_extend_valid() {
let mut rt = RuntimeLeapSeconds::from_builtin();
rt.try_extend(LeapEntry::new(9_999_999_999_000_000_000, 38))
.unwrap();
assert_eq!(rt.len(), 20);
assert_eq!(rt.current_tai_minus_utc(), 38);
}
#[test]
fn test_runtime_try_extend_last_update_updated() {
let mut rt = RuntimeLeapSeconds::from_builtin();
rt.try_extend(LeapEntry::new(9_999_999_999_000_000_000, 38))
.unwrap();
let last = rt.last_update().unwrap();
assert_eq!(last.as_nanos(), 9_999_999_999_000_000_000);
}
#[test]
fn test_runtime_try_extend_not_ascending_error() {
let mut rt = RuntimeLeapSeconds::from_builtin();
let err = rt
.try_extend(LeapEntry::new(1_167_264_037_000_000_000, 38))
.unwrap_err();
assert_eq!(err, LeapExtendError::NotStrictlyAscending);
}
#[test]
fn test_runtime_try_extend_non_unit_increment_error() {
let mut rt = RuntimeLeapSeconds::from_builtin();
let err = rt
.try_extend(LeapEntry::new(9_999_999_999_000_000_000, 39))
.unwrap_err();
assert_eq!(err, LeapExtendError::NonUnitIncrement);
}
#[test]
fn test_runtime_from_slice_too_large_returns_buffer_full() {
let big: std::vec::Vec<LeapEntry> = (0..RUNTIME_CAPACITY + 1)
.map(|i| LeapEntry::new(i as u64 * 1_000_000_000, 19 + i as i32))
.collect();
let err = RuntimeLeapSeconds::from_slice(&big).unwrap_err();
assert_eq!(err, LeapExtendError::BufferFull);
}
#[test]
fn test_runtime_provider_matches_static_at_all_thresholds() {
let rt = RuntimeLeapSeconds::from_builtin();
let ls = LeapSeconds::builtin();
let test_nanos: &[u64] = &[
0,
46_828_820_000_000_000,
599_184_032_000_000_000,
1_167_264_037_000_000_000,
u64::MAX,
];
for &nanos in test_nanos {
let tai = Time::<Tai>::from_nanos(nanos);
assert_eq!(
rt.tai_minus_utc_at(tai),
ls.tai_minus_utc_at(tai),
"mismatch at tai_nanos={}",
nanos
);
}
}
#[test]
fn test_runtime_empty_last_update_is_none() {
assert!(RuntimeLeapSeconds::new().last_update().is_none());
}
#[test]
fn test_runtime_single_entry_last_update_is_none() {
let mut rt = RuntimeLeapSeconds::new();
rt.try_extend(LeapEntry::new(0, 19)).unwrap();
assert!(rt.last_update().is_none());
}
#[test]
fn test_gps_utc_gps_roundtrip_with_runtime_table() {
let rt = RuntimeLeapSeconds::from_builtin();
let gps = Time::<Gps>::from_week_tow(
2086,
DurationParts {
seconds: 0,
nanos: 0,
},
)
.unwrap();
let utc = gps_to_utc(gps, &rt).unwrap();
let back = utc_to_gps(utc, &rt).unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_gps_utc_roundtrip_extended_table() {
let mut rt = RuntimeLeapSeconds::from_builtin();
rt.try_extend(LeapEntry::new(9_999_999_999_000_000_000, 38))
.unwrap();
let gps = Time::<Gps>::from_week_tow(
2086,
DurationParts {
seconds: 0,
nanos: 0,
},
)
.unwrap();
let utc = gps_to_utc(gps, &rt).unwrap();
let back = utc_to_gps(utc, &rt).unwrap();
assert_eq!(gps, back);
}
#[test]
fn test_gps_epoch_utc_is_correct() {
let ls = LeapSeconds::builtin();
let utc = gps_to_utc(Time::<Gps>::EPOCH, &ls).unwrap();
assert_eq!(utc.as_nanos(), 252_892_800_000_000_000);
}
#[test]
fn test_custom_provider_roundtrip() {
struct Always37;
impl LeapSecondsProvider for Always37 {
fn tai_minus_utc_at(
&self,
_: Time<Tai>,
) -> i32 {
37
}
}
let gps = Time::<Gps>::from_seconds(1_000_000_000);
let utc = gps_to_utc(gps, &Always37).unwrap();
let back = utc_to_gps(utc, &Always37).unwrap();
assert_eq!(gps, back);
}
}