use crate::{
Beidou, Glonass, GnssTimeError, Gps, IntoScale, IntoScaleWith, LeapSecondsProvider, Tai, Time,
Utc,
};
pub const TAI_OFFSET_GPS_NS: i64 = 19 * 1_000_000_000;
pub const TAI_OFFSET_GALILEO_NS: i64 = 19 * 1_000_000_000;
pub const TAI_OFFSET_BEIDOU_NS: i64 = 33 * 1_000_000_000;
pub const TAI_OFFSET_TAI_NS: i64 = 0;
pub const GLONASS_UTC_EPOCH_SHIFT_NS: i64 = 757_371_600 * 1_000_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConversionKind {
Fixed,
Identity,
EpochShift,
Contextual,
SameScale,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ScaleId {
Glonass,
Gps,
Galileo,
Beidou,
Tai,
Utc,
}
pub struct ConversionMatrix;
#[derive(Debug)]
pub struct ConversionChain {
pub glonass: Time<Glonass>,
pub gps: Time<Gps>,
pub utc: Time<Utc>,
pub tai: Time<Tai>,
}
impl ScaleId {
pub const ALL: [ScaleId; 6] = [
ScaleId::Glonass,
ScaleId::Gps,
ScaleId::Galileo,
ScaleId::Beidou,
ScaleId::Tai,
ScaleId::Utc,
];
#[inline]
#[must_use]
pub const fn name(self) -> &'static str {
match self {
ScaleId::Glonass => "GLO",
ScaleId::Gps => "GPS",
ScaleId::Galileo => "GAL",
ScaleId::Beidou => "BDT",
ScaleId::Tai => "TAI",
ScaleId::Utc => "UTC",
}
}
#[inline]
#[must_use]
pub const fn conversion_kind(
self,
target: ScaleId,
) -> ConversionKind {
use ConversionKind::*;
use ScaleId::*;
match (self, target) {
(a, b) if a as u8 == b as u8 => SameScale,
(Gps, Tai) | (Tai, Gps) => Fixed,
(Gps, Galileo) | (Galileo, Gps) => Identity,
(Gps, Beidou) | (Beidou, Gps) => Fixed,
(Galileo, Beidou) | (Beidou, Galileo) => Fixed,
(Galileo, Tai) | (Tai, Galileo) => Fixed,
(Beidou, Tai) | (Tai, Beidou) => Fixed,
(Glonass, Utc) | (Utc, Glonass) => EpochShift,
(Gps, Utc) | (Utc, Gps) => Contextual,
(Gps, Glonass) | (Glonass, Gps) => Contextual,
(Galileo, Utc) | (Utc, Galileo) => Contextual,
(Galileo, Glonass) | (Glonass, Galileo) => Contextual,
(Beidou, Utc) | (Utc, Beidou) => Contextual,
(Beidou, Glonass) | (Glonass, Beidou) => Contextual,
(Tai, Utc) | (Utc, Tai) => Contextual,
(Tai, Glonass) | (Glonass, Tai) => Contextual,
_ => Contextual,
}
}
#[inline]
#[must_use]
pub const fn is_fixed(
self,
target: ScaleId,
) -> bool {
matches!(
self.conversion_kind(target),
ConversionKind::Fixed | ConversionKind::Identity | ConversionKind::EpochShift
)
}
#[inline]
#[must_use]
pub const fn needs_leap_seconds(
self,
target: ScaleId,
) -> bool {
matches!(self.conversion_kind(target), ConversionKind::Contextual)
}
}
impl ConversionMatrix {
#[inline]
#[must_use]
pub fn new() -> Self {
ConversionMatrix
}
#[must_use]
pub fn path_count(
&self,
contextual: bool,
) -> usize {
let mut count = 0;
for &from in &ScaleId::ALL {
for &to in &ScaleId::ALL {
if from != to {
let kind = from.conversion_kind(to);
let is_ctx = matches!(kind, ConversionKind::Contextual);
if contextual == is_ctx {
count += 1;
}
}
}
}
count
}
#[inline]
#[must_use]
pub fn kind(
&self,
from: ScaleId,
to: ScaleId,
) -> ConversionKind {
from.conversion_kind(to)
}
}
impl Default for ConversionMatrix {
fn default() -> Self {
ConversionMatrix::new()
}
}
pub fn beidou_via_gps_to_glonass_via_utc<P: LeapSecondsProvider>(
bdt: Time<Beidou>,
ls: &P,
) -> Result<ConversionChain, GnssTimeError> {
let gps: Time<Gps> = bdt.into_scale()?;
let glo: Time<Glonass> = gps.into_scale_with(ls)?;
let utc: Time<Utc> = glo.into_scale()?;
let tai: Time<Tai> = gps.into_scale()?;
Ok(ConversionChain {
gps,
glonass: glo,
utc,
tai,
})
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use std::vec;
use super::*;
#[test]
fn test_scale_id_names_are_correct() {
assert_eq!(ScaleId::Glonass.name(), "GLO");
assert_eq!(ScaleId::Gps.name(), "GPS");
assert_eq!(ScaleId::Galileo.name(), "GAL");
assert_eq!(ScaleId::Beidou.name(), "BDT");
assert_eq!(ScaleId::Tai.name(), "TAI");
assert_eq!(ScaleId::Utc.name(), "UTC");
}
#[test]
fn test_same_scale_is_same_scale() {
for &s in &ScaleId::ALL {
assert_eq!(s.conversion_kind(s), ConversionKind::SameScale);
}
}
#[test]
fn test_gps_galileo_is_identity() {
assert_eq!(
ScaleId::Gps.conversion_kind(ScaleId::Galileo),
ConversionKind::Identity
);
assert_eq!(
ScaleId::Galileo.conversion_kind(ScaleId::Gps),
ConversionKind::Identity
);
}
#[test]
fn test_gps_tai_is_fixed() {
assert_eq!(
ScaleId::Gps.conversion_kind(ScaleId::Tai),
ConversionKind::Fixed
);
assert_eq!(
ScaleId::Tai.conversion_kind(ScaleId::Gps),
ConversionKind::Fixed
);
}
#[test]
fn test_gps_beidou_is_fixed() {
assert_eq!(
ScaleId::Gps.conversion_kind(ScaleId::Beidou),
ConversionKind::Fixed
);
assert_eq!(
ScaleId::Beidou.conversion_kind(ScaleId::Gps),
ConversionKind::Fixed
);
}
#[test]
fn test_glonass_utc_is_epoch_shift() {
assert_eq!(
ScaleId::Glonass.conversion_kind(ScaleId::Utc),
ConversionKind::EpochShift
);
assert_eq!(
ScaleId::Utc.conversion_kind(ScaleId::Glonass),
ConversionKind::EpochShift
);
}
#[test]
fn test_contextual_conversions_require_leap_seconds() {
let contextual_pairs = [
(ScaleId::Gps, ScaleId::Utc),
(ScaleId::Gps, ScaleId::Glonass),
(ScaleId::Galileo, ScaleId::Utc),
(ScaleId::Galileo, ScaleId::Glonass),
(ScaleId::Beidou, ScaleId::Utc),
(ScaleId::Beidou, ScaleId::Glonass),
];
for (from, to) in contextual_pairs {
assert!(
from.needs_leap_seconds(to),
"{:?} → {:?} should be contextual",
from,
to
);
assert!(
to.needs_leap_seconds(from),
"{:?} → {:?} should be contextual",
to,
from
);
}
}
#[test]
fn test_fixed_conversions_dont_need_leap_seconds() {
let fixed_pairs = [
(ScaleId::Gps, ScaleId::Tai),
(ScaleId::Gps, ScaleId::Galileo),
(ScaleId::Gps, ScaleId::Beidou),
(ScaleId::Galileo, ScaleId::Beidou),
(ScaleId::Glonass, ScaleId::Utc),
];
for (from, to) in fixed_pairs {
assert!(from.is_fixed(to), "{:?} → {:?} should be fixed", from, to);
assert!(to.is_fixed(from), "{:?} → {:?} should be fixed", to, from);
}
}
#[test]
fn test_tai_offset_constants_are_correct() {
assert_eq!(TAI_OFFSET_GPS_NS, 19_000_000_000);
assert_eq!(TAI_OFFSET_GALILEO_NS, 19_000_000_000);
assert_eq!(TAI_OFFSET_BEIDOU_NS, 33_000_000_000);
assert_eq!(TAI_OFFSET_TAI_NS, 0);
assert_eq!(GLONASS_UTC_EPOCH_SHIFT_NS, 757_371_600_000_000_000);
}
#[test]
fn test_matrix_counts_are_correct() {
let m = ConversionMatrix::new();
assert_eq!(m.path_count(false), 14, "14 fixed paths");
assert_eq!(m.path_count(true), 16, "16 contextual paths");
}
#[test]
fn test_all_off_diagonal_cells_are_classified() {
for &from in &ScaleId::ALL {
for &to in &ScaleId::ALL {
if from != to {
let kind = from.conversion_kind(to);
assert_ne!(
kind,
ConversionKind::SameScale,
"{:?}→{:?} should not be SameScale",
from,
to
);
}
}
}
}
#[test]
fn test_matrix_is_symmetric_in_kind_category() {
for &from in &ScaleId::ALL {
for &to in &ScaleId::ALL {
if from != to {
let fwd_fixed = from.is_fixed(to);
let rev_fixed = to.is_fixed(from);
assert_eq!(
fwd_fixed, rev_fixed,
"{:?}↔{:?}: fixed classification must be symmetric",
from, to
);
}
}
}
}
}