From 968782e24a5f4979857700d3504fe86816969cbe Mon Sep 17 00:00:00 2001 From: Helge Eichhorn Date: Wed, 10 Jul 2024 10:48:19 +0200 Subject: [PATCH] WIP --- crates/lox-time/src/lib.rs | 102 ++- crates/lox-time/src/python/time.rs | 143 +--- crates/lox-time/src/python/ut1.rs | 27 +- crates/lox-time/src/python/utc.rs | 1 - .../src/time_scales/transformations.rs | 6 +- crates/lox-time/src/transformations.rs | 741 ------------------ crates/lox-time/src/ut1.rs | 74 +- crates/lox-time/src/utc.rs | 23 +- crates/lox-time/src/utc/leap_seconds.rs | 7 +- crates/lox-time/src/utc/transformations.rs | 116 +-- .../src/utc/transformations/before1972.rs | 2 +- 11 files changed, 176 insertions(+), 1066 deletions(-) delete mode 100644 crates/lox-time/src/transformations.rs diff --git a/crates/lox-time/src/lib.rs b/crates/lox-time/src/lib.rs index 52dd6b55..22e9c58f 100644 --- a/crates/lox-time/src/lib.rs +++ b/crates/lox-time/src/lib.rs @@ -59,6 +59,9 @@ use crate::prelude::transformations::ToScale; use crate::subsecond::Subsecond; use crate::time_scales::transformations::{OffsetProvider, ToTai, TryToScale}; use crate::time_scales::{DynTimeScale, TimeScale}; +use crate::utc::leap_seconds::BuiltinLeapSeconds; +use crate::utc::transformations::to_utc_with_provider; +use crate::utc::{LeapSecondsProvider, Utc, UtcError}; pub mod calendar_dates; pub mod constants; @@ -73,7 +76,6 @@ pub mod subsecond; pub(crate) mod test_helpers; pub mod time_of_day; pub mod time_scales; -pub mod transformations; pub mod ut1; pub mod utc; @@ -316,31 +318,79 @@ impl Time { self.subsecond.into() } - // pub fn try_to_scale(&self, scale: U, provider: &P) -> Result, P::Error> - // where - // T: TryToScale, - // U: TimeScale, - // P: OffsetProvider, - // { - // let delta = self.scale.try_to_scale(&scale, self.to_delta(), provider)?; - // Ok(Time::from_delta(scale, delta)) - // } - // - // pub fn to_scale(&self, scale: U) -> Time - // where - // T: ToScale, - // { - // let delta = self.scale.to_scale(&scale, self.to_delta()); - // Time::from_delta(scale, delta) - // } - // - // pub fn to_tai(&self) -> Time - // where - // T: ToScale, - // { - // let delta = self.scale.to_tai(self.to_delta()); - // Time::from_delta(Tai, delta) - // } + pub fn try_to_scale(&self, scale: U, provider: &P) -> Result, P::Error> + where + T: TryToScale, + U: TimeScale, + P: OffsetProvider, + { + let delta = self.scale.try_to_scale(&scale, self.to_delta(), provider)?; + Ok(Time::from_delta(scale, delta)) + } + + pub fn to_scale(&self, scale: U) -> Time + where + T: ToScale, + { + let delta = self.scale.to_scale(&scale, self.to_delta()); + Time::from_delta(scale, delta) + } + + pub fn to_tai(&self) -> Time + where + T: ToScale, + { + self.to_scale(Tai) + } + + pub fn to_tcb(&self) -> Time + where + T: ToScale, + { + self.to_scale(Tcb) + } + + pub fn to_tcg(&self) -> Time + where + T: ToScale, + { + self.to_scale(Tcg) + } + + pub fn to_tdb(&self) -> Time + where + T: ToScale, + { + self.to_scale(Tdb) + } + + pub fn to_tt(&self) -> Time + where + T: ToScale, + { + self.to_scale(Tt) + } + + pub fn try_to_ut1(&self, provider: &P) -> Result, P::Error> + where + T: TryToScale, + { + self.try_to_scale(Ut1, provider) + } + + pub fn to_utc_with_provider(&self, provider: &impl LeapSecondsProvider) -> Result + where + T: ToScale, + { + to_utc_with_provider(self.to_tai(), provider) + } + + pub fn to_utc(&self) -> Result + where + T: ToScale, + { + self.to_utc_with_provider(&BuiltinLeapSeconds) + } } impl IsClose for Time { diff --git a/crates/lox-time/src/python/time.rs b/crates/lox-time/src/python/time.rs index 8dbbad36..d3bfebe7 100644 --- a/crates/lox-time/src/python/time.rs +++ b/crates/lox-time/src/python/time.rs @@ -16,22 +16,20 @@ use pyo3::{pyclass, pymethods, Bound, PyAny, PyErr, PyObject, PyResult, Python}; use lox_utils::is_close::IsClose; -use super::ut1::PyNoOpOffsetProvider; use crate::calendar_dates::{CalendarDate, Date}; use crate::deltas::{TimeDelta, ToDelta}; use crate::julian_dates::{Epoch, JulianDate, Unit}; -use crate::prelude::{CivilTime, Tai, Tcb, Tcg, Tdb, TimeScale, Tt, Ut1}; +use crate::prelude::{CivilTime, TimeScale}; use crate::python::deltas::PyTimeDelta; -use crate::python::time_scales::PyTimeScale; -use crate::python::ut1::{PyDeltaUt1Provider, PyUt1Provider}; +use crate::python::ut1::PyUt1Provider; use crate::python::utc::PyUtc; use crate::subsecond::{InvalidSubsecond, Subsecond}; use crate::time_of_day::TimeOfDay; use crate::time_scales::{DynTimeScale, InvalidTimeScale}; -use crate::transformations::{ToTai, ToTcb, ToTcg, ToTdb, ToTt, TryToScale}; -use crate::utc::transformations::ToUtc; use crate::{DynTime, Time, TimeError, TimeLike}; +use super::ut1::PyNoOpOffsetProvider; + impl From for PyErr { fn from(value: InvalidTimeScale) -> Self { PyValueError::new_err(value.to_string()) @@ -267,60 +265,45 @@ impl PyTime { self.0.decimal_seconds() } - pub fn to_tai(&self, provider: Option<&Bound<'_, PyUt1Provider>>) -> PyResult { + pub fn to_scale( + &self, + scale: &str, + provider: Option<&Bound<'_, PyUt1Provider>>, + ) -> PyResult { + let scale: DynTimeScale = scale.parse()?; let time = match provider { - Some(provider) => self.try_to_scale(Tai, provider.get())?, - None => self.try_to_scale(Tai, &PyNoOpOffsetProvider)?, + Some(provider) => self.0.try_to_scale(scale, provider.get())?, + None => self.0.try_to_scale(scale, &PyNoOpOffsetProvider)?, }; - Ok(PyTime(time.with_scale(PyTimeScale::Tai))) + Ok(PyTime(time)) + } + + pub fn to_tai(&self, provider: Option<&Bound<'_, PyUt1Provider>>) -> PyResult { + self.to_scale("TAI", provider) } pub fn to_tcb(&self, provider: Option<&Bound<'_, PyUt1Provider>>) -> PyResult { - let time = match provider { - Some(provider) => self.try_to_scale(Tcb, provider.get())?, - None => self.try_to_scale(Tcb, &PyNoOpOffsetProvider)?, - }; - Ok(PyTime(time.with_scale(PyTimeScale::Tcb))) + self.to_scale("TCB", provider) } pub fn to_tcg(&self, provider: Option<&Bound<'_, PyUt1Provider>>) -> PyResult { - let time = match provider { - Some(provider) => self.try_to_scale(Tcg, provider.get())?, - None => self.try_to_scale(Tcg, &PyNoOpOffsetProvider)?, - }; - Ok(PyTime(time.with_scale(PyTimeScale::Tcg))) + self.to_scale("TCG", provider) } pub fn to_tdb(&self, provider: Option<&Bound<'_, PyUt1Provider>>) -> PyResult { - let time = match provider { - Some(provider) => self.try_to_scale(Tdb, provider.get())?, - None => self.try_to_scale(Tdb, &PyNoOpOffsetProvider)?, - }; - Ok(PyTime(time.with_scale(PyTimeScale::Tdb))) + self.to_scale("TDB", provider) } pub fn to_tt(&self, provider: Option<&Bound<'_, PyUt1Provider>>) -> PyResult { - let time = match provider { - Some(provider) => self.try_to_scale(Tt, provider.get())?, - None => self.try_to_scale(Tt, &PyNoOpOffsetProvider)?, - }; - Ok(PyTime(time.with_scale(PyTimeScale::Tt))) + self.to_scale("TT", provider) } pub fn to_ut1(&self, provider: Option<&Bound<'_, PyUt1Provider>>) -> PyResult { - let time = match provider { - Some(provider) => self.try_to_scale(Ut1, provider.get())?, - None => self.try_to_scale(Ut1, &PyNoOpOffsetProvider)?, - }; - Ok(PyTime(time.with_scale(PyTimeScale::Ut1))) + self.to_scale("TAI", provider) } - pub fn to_utc(&self, provider: Option<&Bound<'_, PyUt1Provider>>) -> PyResult { - let tai = match provider { - Some(provider) => self.try_to_scale(Tai, provider.get())?, - None => self.try_to_scale(Tai, &PyNoOpOffsetProvider)?, - }; - Ok(PyUtc(tai.to_utc()?)) + pub fn to_utc(&self, _provider: Option<&Bound<'_, PyUt1Provider>>) -> PyResult { + todo!() } } @@ -374,84 +357,6 @@ impl CivilTime for PyTime { impl TimeLike for PyTime {} -impl TryToScale for PyTime { - fn try_to_scale(&self, _scale: Tai, provider: &T) -> PyResult> { - match self.0.scale() { - PyTimeScale::Tai => Ok(self.0.with_scale(Tai)), - PyTimeScale::Tcb => Ok(self.0.with_scale(Tcb).to_tai()), - PyTimeScale::Tcg => Ok(self.0.with_scale(Tcg).to_tai()), - PyTimeScale::Tdb => Ok(self.0.with_scale(Tdb).to_tai()), - PyTimeScale::Tt => Ok(self.0.with_scale(Tt).to_tai()), - PyTimeScale::Ut1 => self.0.with_scale(Ut1).try_to_scale(Tai, provider), - } - } -} - -impl TryToScale for PyTime { - fn try_to_scale(&self, _scale: Tcg, provider: &T) -> PyResult> { - match self.0.scale() { - PyTimeScale::Tai => Ok(self.0.with_scale(Tai).to_tcg()), - PyTimeScale::Tcb => Ok(self.0.with_scale(Tcb).to_tcg()), - PyTimeScale::Tcg => Ok(self.0.with_scale(Tcg)), - PyTimeScale::Tdb => Ok(self.0.with_scale(Tdb).to_tcg()), - PyTimeScale::Tt => Ok(self.0.with_scale(Tt).to_tcg()), - PyTimeScale::Ut1 => self.0.with_scale(Ut1).try_to_scale(Tcg, provider), - } - } -} - -impl TryToScale for PyTime { - fn try_to_scale(&self, _scale: Tcb, provider: &T) -> PyResult> { - match self.0.scale() { - PyTimeScale::Tai => Ok(self.0.with_scale(Tai).to_tcb()), - PyTimeScale::Tcb => Ok(self.0.with_scale(Tcb)), - PyTimeScale::Tcg => Ok(self.0.with_scale(Tcg).to_tcb()), - PyTimeScale::Tdb => Ok(self.0.with_scale(Tdb).to_tcb()), - PyTimeScale::Tt => Ok(self.0.with_scale(Tt).to_tcb()), - PyTimeScale::Ut1 => self.0.with_scale(Ut1).try_to_scale(Tcb, provider), - } - } -} - -impl TryToScale for PyTime { - fn try_to_scale(&self, _scale: Tdb, provider: &T) -> Result, T::Error> { - match self.0.scale() { - PyTimeScale::Tai => Ok(self.0.with_scale(Tai).to_tdb()), - PyTimeScale::Tcb => Ok(self.0.with_scale(Tcb).to_tdb()), - PyTimeScale::Tcg => Ok(self.0.with_scale(Tcg).to_tdb()), - PyTimeScale::Tdb => Ok(self.0.with_scale(Tdb)), - PyTimeScale::Tt => Ok(self.0.with_scale(Tt).to_tdb()), - PyTimeScale::Ut1 => self.0.with_scale(Ut1).try_to_scale(Tdb, provider), - } - } -} - -impl TryToScale for PyTime { - fn try_to_scale(&self, _scale: Tt, provider: &T) -> PyResult> { - match self.0.scale() { - PyTimeScale::Tai => Ok(self.0.with_scale(Tai).to_tt()), - PyTimeScale::Tcb => Ok(self.0.with_scale(Tcb).to_tt()), - PyTimeScale::Tcg => Ok(self.0.with_scale(Tcg).to_tt()), - PyTimeScale::Tdb => Ok(self.0.with_scale(Tdb).to_tt()), - PyTimeScale::Tt => Ok(self.0.with_scale(Tt)), - PyTimeScale::Ut1 => self.0.with_scale(Ut1).try_to_scale(Tt, provider), - } - } -} - -impl TryToScale for PyTime { - fn try_to_scale(&self, _scale: Ut1, provider: &T) -> PyResult> { - match self.0.scale() { - PyTimeScale::Tai => self.0.with_scale(Tai).try_to_scale(Ut1, provider), - PyTimeScale::Tcb => self.0.with_scale(Tcb).try_to_scale(Ut1, provider), - PyTimeScale::Tcg => self.0.with_scale(Tcg).try_to_scale(Ut1, provider), - PyTimeScale::Tdb => self.0.with_scale(Tdb).try_to_scale(Ut1, provider), - PyTimeScale::Tt => self.0.with_scale(Tt).try_to_scale(Ut1, provider), - PyTimeScale::Ut1 => Ok(self.0.with_scale(Ut1)), - } - } -} - impl IsClose for PyTime { const DEFAULT_RELATIVE: f64 = 1e-10; diff --git a/crates/lox-time/src/python/ut1.rs b/crates/lox-time/src/python/ut1.rs index cbc948f4..b3fa54bd 100644 --- a/crates/lox-time/src/python/ut1.rs +++ b/crates/lox-time/src/python/ut1.rs @@ -6,14 +6,13 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +use pyo3::exceptions::PyValueError; +use pyo3::{pyclass, pymethods, PyErr, PyResult}; + use crate::deltas::TimeDelta; -use crate::time_scales::{Tai, Ut1}; -use crate::transformations::OffsetProvider; +use crate::time_scales::transformations::OffsetProvider; use crate::ut1::{DeltaUt1Tai, DeltaUt1TaiError, DeltaUt1TaiProvider, ExtrapolatedDeltaUt1Tai}; use crate::utc::leap_seconds::BuiltinLeapSeconds; -use crate::Time; -use pyo3::exceptions::PyValueError; -use pyo3::{pyclass, pymethods, PyErr, PyResult}; impl From for PyErr { fn from(value: ExtrapolatedDeltaUt1Tai) -> Self { @@ -33,24 +32,20 @@ impl OffsetProvider for PyNoOpOffsetProvider { type Error = PyErr; } -pub trait PyDeltaUt1Provider: DeltaUt1TaiProvider + OffsetProvider {} - impl DeltaUt1TaiProvider for PyNoOpOffsetProvider { - fn delta_ut1_tai(&self, _tai: &Time) -> PyResult { + fn delta_ut1_tai(&self, _delta: TimeDelta) -> PyResult { Err(PyValueError::new_err( "`provider` argument needs to be present for UT1 transformations", )) } - fn delta_tai_ut1(&self, _ut1: &Time) -> PyResult { + fn delta_tai_ut1(&self, _delta: TimeDelta) -> PyResult { Err(PyValueError::new_err( "`provider` argument needs to be present for UT1 transformations", )) } } -impl PyDeltaUt1Provider for PyNoOpOffsetProvider {} - #[pyclass(name = "UT1Provider", module = "lox_space", frozen)] #[derive(Clone, Debug, PartialEq)] pub struct PyUt1Provider(pub DeltaUt1Tai); @@ -69,17 +64,15 @@ impl OffsetProvider for PyUt1Provider { } impl DeltaUt1TaiProvider for PyUt1Provider { - fn delta_ut1_tai(&self, tai: &Time) -> PyResult { - self.0.delta_ut1_tai(tai).map_err(|err| err.into()) + fn delta_ut1_tai(&self, delta: TimeDelta) -> PyResult { + self.0.delta_ut1_tai(delta).map_err(|err| err.into()) } - fn delta_tai_ut1(&self, ut1: &Time) -> PyResult { - self.0.delta_tai_ut1(ut1).map_err(|err| err.into()) + fn delta_tai_ut1(&self, delta: TimeDelta) -> PyResult { + self.0.delta_tai_ut1(delta).map_err(|err| err.into()) } } -impl PyDeltaUt1Provider for PyUt1Provider {} - #[cfg(test)] mod tests { use pyo3::{Bound, Python}; diff --git a/crates/lox-time/src/python/utc.rs b/crates/lox-time/src/python/utc.rs index 2a59615c..6530ec16 100644 --- a/crates/lox-time/src/python/utc.rs +++ b/crates/lox-time/src/python/utc.rs @@ -11,7 +11,6 @@ use crate::prelude::CivilTime; use crate::python::time::PyTime; use crate::python::time_scales::PyTimeScale; use crate::python::ut1::PyUt1Provider; -use crate::transformations::{ToTai, ToTcb, ToTcg, ToTdb, ToTt, ToUt1}; use crate::utc::{Utc, UtcError}; use pyo3::exceptions::PyValueError; use pyo3::types::PyType; diff --git a/crates/lox-time/src/time_scales/transformations.rs b/crates/lox-time/src/time_scales/transformations.rs index f337047f..52cb0cb3 100644 --- a/crates/lox-time/src/time_scales/transformations.rs +++ b/crates/lox-time/src/time_scales/transformations.rs @@ -10,6 +10,7 @@ use crate::deltas::TimeDelta; use crate::prelude::{Tai, Tt}; use crate::subsecond::Subsecond; use crate::time_scales::{DynTimeScale, Tcb, Tcg, Tdb, TimeScale, Ut1}; +use crate::ut1::DeltaUt1TaiProvider; pub trait OffsetProvider { type Error: std::error::Error; @@ -350,11 +351,6 @@ impl_fallible!(Tdb, Tt); // TAI <-> UT1 // ///////////////// -pub trait DeltaUt1TaiProvider: OffsetProvider { - fn delta_ut1_tai(&self, delta: TimeDelta) -> Result; - fn delta_tai_ut1(&self, delta: TimeDelta) -> Result; -} - impl TryToScale for Tai { fn try_offset( &self, diff --git a/crates/lox-time/src/transformations.rs b/crates/lox-time/src/transformations.rs deleted file mode 100644 index 1ffd1adc..00000000 --- a/crates/lox-time/src/transformations.rs +++ /dev/null @@ -1,741 +0,0 @@ -/* - * Copyright (c) 2024. Helge Eichhorn and the LOX contributors - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - */ - -/*! - Module `transformations` provides traits for transforming between pairs of [TimeScale]s, together - with default implementations for the most commonly used time scale pairs. -*/ - -use std::convert::Infallible; - -use crate::calendar_dates::Date; -use crate::constants::julian_dates::J77; -use crate::deltas::{TimeDelta, ToDelta}; -use crate::subsecond::Subsecond; -use crate::time_scales::{Tai, Tcb, Tcg, Tdb, TimeScale, Tt, Ut1}; -use crate::ut1::DeltaUt1TaiProvider; -use crate::utc::Utc; -use crate::Time; - -/// Marker trait denoting a type that returns an offset between a pair of [TimeScale]s. -pub trait OffsetProvider { - type Error: std::error::Error; -} - -/// A no-op [OffsetProvider] equivalent to `()`, used to guide the type system when implementing -/// transformations with constant offsets. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] -pub struct NoOpOffsetProvider; - -impl OffsetProvider for NoOpOffsetProvider { - type Error = Infallible; -} - -/// The base trait underlying all time scale transformations. -/// -/// By default, `TryToScale` assumes that no [OffsetProvider] is required and that the -/// transformation is infallible. -pub trait TryToScale: ToDelta { - fn try_to_scale(&self, scale: T, provider: &U) -> Result, U::Error>; -} - -/// `ToScale` narrows [TryToScale] for the case where no [OffsetProvider] is required and the -/// transformation is infallible. -pub trait ToScale: TryToScale { - fn to_scale(&self, scale: T) -> Time { - self.try_to_scale(scale, &NoOpOffsetProvider).unwrap() - } -} - -/// Blanket implementation of [ToScale] for all types that implement [TryToScale] infallibly with -/// no [OffsetProvider]. -impl> ToScale for U {} - -/// Convenience trait and default implementation for infallible conversions to [Tai] in terms of -/// [ToScale]. -pub trait ToTai: ToScale { - fn to_tai(&self) -> Time { - self.to_scale(Tai) - } -} - -/// Convenience trait and default implementation for infallible conversions to [Tt] in terms of -/// [ToScale]. -pub trait ToTt: ToScale { - fn to_tt(&self) -> Time { - self.to_scale(Tt) - } -} - -/// Convenience trait and default implementation for infallible conversions to [Tcg] in terms of -/// [ToScale]. -pub trait ToTcg: ToScale { - fn to_tcg(&self) -> Time { - self.to_scale(Tcg) - } -} - -/// Convenience trait and default implementation for infallible conversions to [Tcb] in terms of -/// [ToScale]. -pub trait ToTcb: ToScale { - fn to_tcb(&self) -> Time { - self.to_scale(Tcb) - } -} - -/// Convenience trait and default implementation for infallible conversions to [Tdb] in terms of -/// [ToScale]. -pub trait ToTdb: ToScale { - fn to_tdb(&self) -> Time { - self.to_scale(Tdb) - } -} - -/// Convenience trait and default implementation for conversions to [Ut1] in terms of [TryToScale]. -pub trait ToUt1: TryToScale { - fn try_to_ut1(&self, provider: &T) -> Result, T::Error> { - self.try_to_scale(Ut1, provider) - } -} - -// No-ops - -impl TryToScale for Time { - fn try_to_scale(&self, _scale: Tai, _provider: &T) -> Result, T::Error> { - Ok(*self) - } -} - -impl ToTai for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, _scale: Tcb, _provider: &T) -> Result, T::Error> { - Ok(*self) - } -} - -impl ToTcb for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, _scale: Tcg, _provider: &T) -> Result, T::Error> { - Ok(*self) - } -} - -impl ToTcg for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, _scale: Tdb, _provider: &T) -> Result, T::Error> { - Ok(*self) - } -} - -impl ToTdb for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, _scale: Tt, _provider: &T) -> Result, T::Error> { - Ok(*self) - } -} - -impl ToTt for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, _scale: Ut1, _provider: &T) -> Result, T::Error> { - Ok(*self) - } -} - -impl ToUt1 for Time {} - -// TAI <-> TT - -/// The constant offset between TAI and TT. -pub const D_TAI_TT: TimeDelta = TimeDelta { - seconds: 32, - subsecond: Subsecond(0.184), -}; - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tt, _provider: &T) -> Result, T::Error> { - Ok(self.with_scale_and_delta(scale, D_TAI_TT)) - } -} - -impl ToTt for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tai, _provider: &T) -> Result, T::Error> { - Ok(self.with_scale_and_delta(scale, -D_TAI_TT)) - } -} - -impl ToTai for Time {} - -// TT <-> TCG - -/// The difference between J2000 TT and 1977 January 1.0 TAI as TT. -const J77_TT: f64 = -7.25803167816e8; - -/// The rate of change of TCG with respect to TT. -const LG: f64 = 6.969290134e-10; - -/// The rate of change of TT with respect to TCG. -const INV_LG: f64 = LG / (1.0 - LG); - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tcg, _provider: &T) -> Result, T::Error> { - let time = self.to_delta().to_decimal_seconds(); - let raw_delta = INV_LG * (time - J77_TT); - let delta = TimeDelta::from_decimal_seconds(raw_delta).unwrap_or_else(|err| { - panic!( - "Calculated TT to TCG offset `{}` could not be converted to `TimeDelta`: {}", - raw_delta, err - ); - }); - Ok(self.with_scale_and_delta(scale, delta)) - } -} - -impl ToTcg for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tt, _provider: &T) -> Result, T::Error> { - let time = self.to_delta().to_decimal_seconds(); - let raw_delta = -LG * (time - J77_TT); - let delta = TimeDelta::from_decimal_seconds(raw_delta).unwrap_or_else(|err| { - panic!( - "Calculated TCG to TT offset `{}` could not be converted to `TimeDelta`: {}", - raw_delta, err - ); - }); - Ok(self.with_scale_and_delta(scale, delta)) - } -} - -impl ToTt for Time {} - -// TDB <-> TCB - -/// 1977 January 1.0 TAI -const TT_0: f64 = J77.seconds as f64 + D_TAI_TT.seconds as f64 + D_TAI_TT.subsecond.0; - -/// The rate of change of TDB with respect to TCB. -const LB: f64 = 1.550519768e-8; - -/// The rate of change of TCB with respect to TDB. -const INV_LB: f64 = LB / (1.0 - LB); - -/// Constant term of TDB − TT formula of Fairhead & Bretagnon (1990). -const TDB_0: f64 = -6.55e-5; - -const TCB_77: f64 = TDB_0 + LB * TT_0; - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tcb, _provider: &T) -> Result, T::Error> { - let dt = self.to_delta().to_decimal_seconds(); - let raw_delta = -TCB_77 / (1.0 - LB) + INV_LB * dt; - let delta = TimeDelta::from_decimal_seconds(raw_delta).unwrap_or_else(|err| { - panic!( - "Calculated TDB to TCB offset `{}` could not be converted to `TimeDelta`: {}", - raw_delta, err - ); - }); - Ok(self.with_scale_and_delta(scale, delta)) - } -} - -impl ToTcb for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tdb, _provider: &T) -> Result, T::Error> { - let dt = self.to_delta().to_decimal_seconds(); - let raw_delta = TCB_77 - LB * dt; - let delta = TimeDelta::from_decimal_seconds(raw_delta).unwrap_or_else(|err| { - panic!( - "Calculated TCB to TDB offset `{}` could not be converted to `TimeDelta`: {}", - raw_delta, err - ); - }); - Ok(self.with_scale_and_delta(scale, delta)) - } -} - -impl ToTdb for Time {} - -// TT <-> TDB - -const K: f64 = 1.657e-3; -const EB: f64 = 1.671e-2; -const M_0: f64 = 6.239996; -const M_1: f64 = 1.99096871e-7; - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tdb, _provider: &T) -> Result, T::Error> { - let tt = self.to_delta().to_decimal_seconds(); - let g = M_0 + M_1 * tt; - let raw_delta = K * (g + EB * g.sin()).sin(); - let delta = TimeDelta::from_decimal_seconds(raw_delta).unwrap_or_else(|err| { - panic!( - "Calculated TT to TDB offset `{}` could not be converted to `TimeDelta`: {}", - raw_delta, err, - ) - }); - Ok(self.with_scale_and_delta(scale, delta)) - } -} - -impl ToTdb for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tt, _provider: &T) -> Result, T::Error> { - let tdb = self.to_delta().to_decimal_seconds(); - let mut tt = tdb; - let mut raw_delta = 0.0; - for _ in 1..3 { - let g = M_0 + M_1 * tt; - raw_delta = -K * (g + EB * g.sin()).sin(); - tt = tdb + raw_delta; - } - - let delta = TimeDelta::from_decimal_seconds(raw_delta).unwrap_or_else(|err| { - panic!( - "Calculated TDB to TT offset `{}` could not be converted to `TimeDelta`: {}", - raw_delta, err, - ) - }); - Ok(self.with_scale_and_delta(scale, delta)) - } -} - -impl ToTt for Time {} - -// TAI <-> UT1 - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Ut1, provider: &T) -> Result, T::Error> { - let delta_ut1_tai = provider.delta_ut1_tai(self)?; - Ok(self.with_scale_and_delta(scale, delta_ut1_tai)) - } -} - -impl ToUt1 for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tai, provider: &T) -> Result, T::Error> { - let delta_tai_ut1 = provider.delta_tai_ut1(self)?; - Ok(self.with_scale_and_delta(scale, delta_tai_ut1)) - } -} - -// Multi-step transformations - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tai, provider: &T) -> Result, T::Error> { - self.to_tt().try_to_scale(scale, provider) - } -} - -impl ToTai for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tdb, provider: &T) -> Result, T::Error> { - self.to_tt().try_to_scale(scale, provider) - } -} - -impl ToTdb for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tcg, provider: &T) -> Result, T::Error> { - self.to_tt().try_to_scale(scale, provider) - } -} - -impl ToTcg for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tcg, provider: &T) -> Result, T::Error> { - self.to_tt().try_to_scale(scale, provider) - } -} - -impl ToTcg for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tai, provider: &T) -> Result, T::Error> { - self.to_tt().try_to_scale(scale, provider) - } -} - -impl ToTai for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tdb, provider: &T) -> Result, T::Error> { - self.to_tt().try_to_scale(scale, provider) - } -} - -impl ToTdb for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tcb, provider: &T) -> Result, T::Error> { - self.to_tdb().try_to_scale(scale, provider) - } -} - -impl ToTcb for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tcb, provider: &T) -> Result, T::Error> { - self.to_tdb().try_to_scale(scale, provider) - } -} - -impl ToTcb for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tcb, provider: &T) -> Result, T::Error> { - self.to_tdb().try_to_scale(scale, provider) - } -} - -impl ToTcb for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tt, provider: &T) -> Result, T::Error> { - self.to_tdb().try_to_scale(scale, provider) - } -} - -impl ToTt for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tcg, provider: &T) -> Result, T::Error> { - self.to_tdb().try_to_scale(scale, provider) - } -} - -impl ToTcg for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Tai, provider: &T) -> Result, T::Error> { - self.to_tdb().to_tt().try_to_scale(scale, provider) - } -} - -impl ToTai for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Ut1, provider: &T) -> Result, T::Error> { - let tai = self.to_tai(); - let delta_ut1_tai = provider.delta_ut1_tai(&tai)?; - Ok(tai.with_scale_and_delta(scale, delta_ut1_tai)) - } -} - -impl ToUt1 for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Ut1, provider: &T) -> Result, T::Error> { - let tai = self.to_tai(); - let delta_ut1_tai = provider.delta_ut1_tai(&tai)?; - Ok(tai.with_scale_and_delta(scale, delta_ut1_tai)) - } -} - -impl ToUt1 for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Ut1, provider: &T) -> Result, T::Error> { - let tai = self.to_tai(); - let delta_ut1_tai = provider.delta_ut1_tai(&tai)?; - Ok(tai.with_scale_and_delta(scale, delta_ut1_tai)) - } -} - -impl ToUt1 for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, scale: Ut1, provider: &T) -> Result, T::Error> { - let tai = self.to_tai(); - let delta_ut1_tai = provider.delta_ut1_tai(&tai)?; - Ok(tai.with_scale_and_delta(scale, delta_ut1_tai)) - } -} - -impl ToUt1 for Time {} - -impl TryToScale for Time { - fn try_to_scale(&self, _scale: Tt, provider: &T) -> Result, T::Error> { - let tai = self.try_to_scale(Tai, provider)?; - Ok(tai.to_tt()) - } -} - -impl TryToScale for Time { - fn try_to_scale(&self, _scale: Tcg, provider: &T) -> Result, T::Error> { - let tai = self.try_to_scale(Tai, provider)?; - Ok(tai.to_tcg()) - } -} - -impl TryToScale for Time { - fn try_to_scale(&self, _scale: Tcb, provider: &T) -> Result, T::Error> { - let tai = self.try_to_scale(Tai, provider)?; - Ok(tai.to_tcb()) - } -} - -impl TryToScale for Time { - fn try_to_scale(&self, _scale: Tdb, provider: &T) -> Result, T::Error> { - let tai = self.try_to_scale(Tai, provider)?; - Ok(tai.to_tdb()) - } -} - -/// Implementers of `LeapSecondsProvider` provide the offset between TAI and UTC in leap seconds at -/// an instant in either time scale. -pub trait LeapSecondsProvider: OffsetProvider { - /// The difference in leap seconds between TAI and UTC at the given TAI instant. - fn delta_tai_utc(&self, tai: Time) -> Option; - - /// The difference in leap seconds between UTC and TAI at the given UTC instant. - fn delta_utc_tai(&self, utc: Utc) -> Option; - - /// Returns `true` if a leap second occurs on `date`. - fn is_leap_second_date(&self, date: Date) -> bool; - - /// Returns `true` if a leap second occurs at `tai`. - fn is_leap_second(&self, tai: Time) -> bool; -} - -#[cfg(test)] -mod tests { - - use float_eq::assert_float_eq; - use rstest::rstest; - - use crate::constants::julian_dates::{J0, SECONDS_BETWEEN_JD_AND_J2000}; - use crate::subsecond::Subsecond; - use crate::test_helpers::delta_ut1_tai; - use crate::time; - - use super::*; - - // Transformations are tested for agreement with both ERFA and AstroTime.jl. - - const PANIC_INDUCING_DELTA: TimeDelta = TimeDelta { - seconds: 0, - subsecond: Subsecond(f64::NAN), - }; - - #[test] - fn test_transform_all() { - let tai_exp: Time = Time::default(); - let tt_exp = tai_exp.to_tt(); - let tcg_exp = tt_exp.to_tcg(); - let tdb_exp = tt_exp.to_tdb(); - let tcb_exp = tdb_exp.to_tcb(); - - let tt_act = tai_exp.to_tt(); - let tcg_act = tai_exp.to_tcg(); - let tdb_act = tai_exp.to_tdb(); - let tcb_act = tai_exp.to_tcb(); - - assert_eq!(tt_exp, tt_act); - assert_eq!(tcg_exp, tcg_act); - assert_eq!(tdb_exp, tdb_act); - assert_eq!(tcb_exp, tcb_act); - - let tai_act = tt_exp.to_tai(); - let tcg_act = tt_exp.to_tcg(); - let tdb_act = tt_exp.to_tdb(); - let tcb_act = tt_exp.to_tcb(); - - assert_eq!(tai_exp, tai_act); - assert_eq!(tcg_exp, tcg_act); - assert_eq!(tdb_exp, tdb_act); - assert_eq!(tcb_exp, tcb_act); - - let tai_act = tcg_exp.to_tai(); - let tt_act = tcg_exp.to_tt(); - let tdb_act = tcg_exp.to_tdb(); - let tcb_act = tcg_exp.to_tcb(); - - assert_eq!(tai_exp, tai_act); - assert_eq!(tt_exp, tt_act); - assert_eq!(tdb_exp, tdb_act); - assert_eq!(tcb_exp, tcb_act); - - let tai_act = tdb_exp.to_tai(); - let tt_act = tdb_exp.to_tt(); - let tcg_act = tdb_exp.to_tcg(); - let tcb_act = tdb_exp.to_tcb(); - - assert_eq!(tai_exp, tai_act); - assert_eq!(tt_exp, tt_act); - assert_eq!(tcg_exp, tcg_act); - assert_eq!(tcb_exp, tcb_act); - - let tai_act = tcb_exp.to_tai(); - let tt_act = tcb_exp.to_tt(); - let tcg_act = tcb_exp.to_tcg(); - let tdb_act = tcb_exp.to_tdb(); - - assert_eq!(tai_exp, tai_act); - assert_eq!(tt_exp, tt_act); - assert_eq!(tcg_exp, tcg_act); - assert_eq!(tdb_exp, tdb_act); - } - - #[test] - fn test_time_no_ops() { - let tai = time!(Tai, 2000, 1, 1).unwrap(); - assert_eq!(tai, tai.to_tai()); - let tcb = time!(Tcb, 2000, 1, 1).unwrap(); - assert_eq!(tcb, tcb.to_tcb()); - let tcg = time!(Tcg, 2000, 1, 1).unwrap(); - assert_eq!(tcg, tcg.to_tcg()); - let tdb = time!(Tdb, 2000, 1, 1).unwrap(); - assert_eq!(tdb, tdb.to_tdb()); - let tt = time!(Tt, 2000, 1, 1).unwrap(); - assert_eq!(tt, tt.to_tt()); - let ut1 = time!(Ut1, 2000, 1, 1).unwrap(); - assert_eq!(ut1, ut1.try_to_ut1(delta_ut1_tai()).unwrap()); - } - - #[test] - fn test_all_scales_to_ut1() { - let provider = delta_ut1_tai(); - - let tai = time!(Tai, 2024, 5, 17, 12, 13, 14.0).unwrap(); - let exp = tai.try_to_ut1(provider).unwrap(); - - let tt = tai.to_tt(); - let act = tt.try_to_ut1(provider).unwrap(); - assert_eq!(act, exp); - let tcg = tai.to_tcg(); - let act = tcg.try_to_ut1(provider).unwrap(); - assert_eq!(act, exp); - let tcb = tai.to_tcb(); - let act = tcb.try_to_ut1(provider).unwrap(); - assert_eq!(act, exp); - let tdb = tai.to_tdb(); - let act = tdb.try_to_ut1(provider).unwrap(); - assert_eq!(act, exp); - } - - #[test] - fn test_ut1_to_tai() { - let provider = delta_ut1_tai(); - let expected = time!(Tai, 2024, 5, 17, 12, 13, 14.0).unwrap(); - let actual = expected - .try_to_ut1(provider) - .unwrap() - .try_to_scale(Tai, provider) - .unwrap(); - assert_eq!(expected, actual) - } - - #[test] - fn test_transform_tai_tt() { - let tai = Time::new(Tai, 0, Subsecond::default()); - let tt = tai.to_tt(); - let expected = Time::new(Tt, 32, Subsecond(0.184)); - assert_eq!(expected, tt); - } - - #[test] - fn test_transform_tt_tai() { - let tt = Time::new(Tt, 32, Subsecond(0.184)); - let tai = tt.to_tai(); - let expected = Time::new(Tai, 0, Subsecond::default()); - assert_eq!(expected, tai); - } - - #[rstest] - #[case::j0( - Time::from_delta(Tt, J0), - Time::from_delta(Tcg, TimeDelta::new(-211813488148, Subsecond(0.886_867_966_488_467))) - )] - #[case::j2000( - Time::new(Tt, 0, Subsecond::default()), - Time::new(Tcg, 0, Subsecond(0.505_833_286_021_129)) - )] - #[should_panic] - #[case::unrepresentable(Time::from_delta(Tt, PANIC_INDUCING_DELTA), Time::default())] - fn test_transform_tt_tcg(#[case] tt: Time, #[case] expected: Time) { - let tcg = tt.to_tcg(); - assert_eq!(expected, tcg); - } - - #[rstest] - #[case::j0( - Time::from_delta(Tcg, J0), - Time::from_delta(Tt, TimeDelta::new(-211813487853, Subsecond(0.113_131_930_984_139))) - )] - #[case::j2000(Time::new(Tcg, 0, Subsecond::default()), Time::new(Tt, -1, Subsecond(0.494_166_714_331_400)))] - #[should_panic] - #[case::unrepresentable(Time::from_delta(Tcg, PANIC_INDUCING_DELTA), Time::default())] - fn test_transform_tcg_tt(#[case] tcg: Time, #[case] expected: Time) { - let tt = tcg.to_tt(); - assert_eq!(expected.seconds(), tt.seconds()); - assert_float_eq!(expected.subsecond(), tt.subsecond(), abs <= 1e-12); - } - - #[rstest] - #[case::j0( - Time::from_delta(Tcb, J0), - Time::from_delta(Tdb, TimeDelta::new(-SECONDS_BETWEEN_JD_AND_J2000 + 3272, Subsecond(0.956_215_636_550_950))) - )] - #[case::j2000(Time::j2000(Tcb), Time::new(Tdb, -12, Subsecond(0.746_212_906_242_706)))] - fn test_transform_tcb_tdb(#[case] tcb: Time, #[case] expected: Time) { - let tdb = tcb.to_tdb(); - assert_eq!(expected.seconds(), tdb.seconds()); - // Lox and ERFA agree to the picosecond. However, the paper from which these formulae derive - // (Fairhead & Bretagnon, 1990) provide coefficients for transformations with only - // nanosecond accuracy. Chasing greater accuracy may not be practical or useful. - assert_float_eq!(expected.subsecond(), tdb.subsecond(), abs <= 1e-15); - } - - #[rstest] - #[case::j0( - Time::from_delta(Tdb, J0), - Time::from_delta(Tcb, TimeDelta::new(-SECONDS_BETWEEN_JD_AND_J2000 - 3273, Subsecond(0.043_733_615_615_110))) - )] - #[case::j2000(Time::j2000(Tdb), Time::new(Tcb, 11, Subsecond(0.253_787_268_249_489)))] - fn test_transform_tdb_tcb(#[case] tdb: Time, #[case] expected: Time) { - let tcb = tdb.to_tcb(); - assert_eq!(expected.seconds(), tcb.seconds()); - assert_float_eq!(expected.subsecond(), tcb.subsecond(), abs <= 1e-12); - } - - #[rstest] - #[case::j0(Time::from_delta(Tt, J0), Time::from_delta(Tdb, TimeDelta::new(-SECONDS_BETWEEN_JD_AND_J2000, Subsecond(0.001_600_955_458_249))))] - #[case::j2000(Time::j2000(Tt), Time::from_delta(Tdb, TimeDelta::new(-1, Subsecond(0.999_927_263_223_809))))] - #[should_panic] - #[case::unrepresentable(Time::from_delta(Tt, PANIC_INDUCING_DELTA), Time::default())] - fn test_transform_tt_tdb(#[case] tt: Time, #[case] expected: Time) { - let tdb = tt.to_tdb(); - assert_eq!(expected, tdb); - } - - #[rstest] - #[case::j0(Time::from_delta(Tdb, J0), Time::from_delta(Tt, TimeDelta::new(-SECONDS_BETWEEN_JD_AND_J2000 - 1, Subsecond(0.998_399_044_541_884))))] - #[case::j2000( - Time::j2000(Tdb), - Time::from_delta(Tt, TimeDelta::new(0, Subsecond(0.000_072_736_776_166))) - )] - #[should_panic] - #[case::unrepresentable(Time::from_delta(Tdb, PANIC_INDUCING_DELTA), Time::default())] - fn test_transform_tdb_tt(#[case] tdb: Time, #[case] expected: Time) { - let tt = tdb.to_tt(); - assert_eq!(expected, tt); - } -} diff --git a/crates/lox-time/src/ut1.rs b/crates/lox-time/src/ut1.rs index 04e33ec8..c9f2ca93 100644 --- a/crates/lox-time/src/ut1.rs +++ b/crates/lox-time/src/ut1.rs @@ -7,10 +7,10 @@ */ /*! - Module `ut1` exposes [DeltaUt1TaiProvider], which describes an API for providing the delta + Module `ut1` exposes [DeltaUt1TaiProviderOld], which describes an API for providing the delta between UT1 and TAI at a time of interest. - [DeltaUt1Tai] is `lox-time`'s default implementation of [DeltaUt1TaiProvider], which parses + [DeltaUt1Tai] is `lox-time`'s default implementation of [DeltaUt1TaiProviderOld], which parses Earth Orientation Parameters from an IERS CSV file. */ @@ -27,11 +27,10 @@ use crate::calendar_dates::{CalendarDate, Date}; use crate::constants::i64::SECONDS_PER_DAY; use crate::constants::julian_dates::SECONDS_BETWEEN_MJD_AND_J2000; use crate::deltas::TimeDelta; -use crate::julian_dates::JulianDate; use crate::subsecond::Subsecond; -use crate::time_scales::{Tai, Ut1}; -use crate::transformations::{LeapSecondsProvider, OffsetProvider}; -use crate::utc::Utc; +use crate::time_scales::transformations::OffsetProvider; +use crate::time_scales::Tai; +use crate::utc::{LeapSecondsProvider, Utc}; use crate::Time; /// Implementers of `DeltaUt1TaiProvider` provide the difference between UT1 and TAI at an instant @@ -39,12 +38,12 @@ use crate::Time; /// /// This crate provides a standard implementation over IERS Earth Orientation Parameters in /// [DeltaUt1Tai]. + pub trait DeltaUt1TaiProvider: OffsetProvider { /// Returns the difference between UT1 and TAI at the given TAI instant. - fn delta_ut1_tai(&self, tai: &Time) -> Result; - + fn delta_ut1_tai(&self, delta: TimeDelta) -> Result; /// Returns the difference between TAI and UT1 at the given UT1 instant. - fn delta_tai_ut1(&self, ut1: &Time) -> Result; + fn delta_tai_ut1(&self, delta: TimeDelta) -> Result; } /// Error type returned when [DeltaUt1Tai] instantiation fails. @@ -84,7 +83,7 @@ impl ExtrapolatedDeltaUt1Tai { } } -/// Provides a standard implementation of [DeltaUt1TaiProvider] based on cubic spline interpolation +/// Provides a standard implementation of [DeltaUt1TaiProviderOld] based on cubic spline interpolation /// of the target time over IERS Earth Orientation Parameters. #[derive(Clone, Debug, PartialEq)] pub struct DeltaUt1Tai(Series, Vec>); @@ -133,38 +132,6 @@ impl OffsetProvider for DeltaUt1Tai { } impl DeltaUt1TaiProvider for DeltaUt1Tai { - fn delta_ut1_tai(&self, tai: &Time) -> Result { - let seconds = tai.seconds_since_j2000(); - let (t0, _) = self.0.first(); - let (tn, _) = self.0.last(); - let val = self.0.interpolate(seconds); - if seconds < t0 || seconds > tn { - return Err(ExtrapolatedDeltaUt1Tai::new(t0, tn, seconds, val)); - } - Ok(TimeDelta::from_decimal_seconds(val).unwrap()) - } - - fn delta_tai_ut1(&self, ut1: &Time) -> Result { - let seconds = ut1.seconds_since_j2000(); - let (t0, _) = self.0.first(); - let (tn, _) = self.0.last(); - // Use the UT1 offset as an initial guess even though the table is based on TAI - let mut val = self.0.interpolate(seconds); - // Interpolate again with the adjusted offsets - for _ in 0..2 { - val = self.0.interpolate(seconds - val); - } - if seconds < t0 || seconds > tn { - return Err(ExtrapolatedDeltaUt1Tai::new(t0, tn, seconds, -val)); - } - Ok(-TimeDelta::from_decimal_seconds(val).unwrap()) - } -} -impl crate::time_scales::transformations::OffsetProvider for DeltaUt1Tai { - type Error = ExtrapolatedDeltaUt1Tai; -} - -impl crate::time_scales::transformations::DeltaUt1TaiProvider for DeltaUt1Tai { fn delta_ut1_tai(&self, delta: TimeDelta) -> Result { let seconds = delta.to_decimal_seconds(); let (t0, _) = self.0.first(); @@ -198,11 +165,12 @@ mod tests { use float_eq::assert_float_eq; use rstest::rstest; + use super::*; use crate::subsecond::Subsecond; use crate::test_helpers::delta_ut1_tai; use crate::time; - - use super::*; + use crate::time_scales::DynTimeScale::Ut1; + use crate::ToDelta; #[rstest] #[case(536414400, -36.40775963091942)] @@ -293,30 +261,30 @@ mod tests { #[case(536499400, -36.40868580909562)] #[case(536500400, -36.40869742010849)] fn test_delta_ut1_tai_orekit(#[case] seconds: i64, #[case] expected: f64) { - let tai = Time::new(Tai, seconds, Subsecond::default()); - let ut1 = Time::new(Ut1, seconds, Subsecond::default()); + let tai = TimeDelta::new(seconds, Subsecond::default()); + let ut1 = TimeDelta::new(seconds, Subsecond::default()); let provider = delta_ut1_tai(); - let actual = provider.delta_ut1_tai(&tai).unwrap().to_decimal_seconds(); + let actual = provider.delta_ut1_tai(tai).unwrap().to_decimal_seconds(); assert_float_eq!(actual, expected, rel <= 1e-6); - let actual = provider.delta_tai_ut1(&ut1).unwrap().to_decimal_seconds(); + let actual = provider.delta_tai_ut1(ut1).unwrap().to_decimal_seconds(); assert_float_eq!(actual, -expected, rel <= 1e-6); } #[rstest] - #[case(time!(Tai, 1973, 1, 1).unwrap(), Err(ExtrapolatedDeltaUt1Tai { + #[case(time!(Tai, 1973, 1, 1).unwrap().to_delta(), Err(ExtrapolatedDeltaUt1Tai { req_date: Date::new(1973, 1, 1).unwrap(), min_date: Date::new(1973, 1, 2).unwrap(), max_date: Date::new(2025, 3, 15).unwrap(), extrapolated_value: TimeDelta::from_decimal_seconds(-11.188739245677642).unwrap(), }))] - #[case(time!(Tai, 2025, 3, 16).unwrap(), Err(ExtrapolatedDeltaUt1Tai { + #[case(time!(Tai, 2025, 3, 16).unwrap().to_delta(), Err(ExtrapolatedDeltaUt1Tai { req_date: Date::new(2025, 3, 16).unwrap(), min_date: Date::new(1973, 1, 2).unwrap(), max_date: Date::new(2025, 3, 15).unwrap(), extrapolated_value: TimeDelta::from_decimal_seconds(-36.98893121380733).unwrap(), }))] fn test_delta_ut1_tai_extrapolation( - #[case] time: Time, + #[case] time: TimeDelta, #[case] expected: Result, ) { let provider = delta_ut1_tai(); @@ -325,14 +293,14 @@ mod tests { .extrapolated_value .to_decimal_seconds(); let actual = provider - .delta_ut1_tai(&time) + .delta_ut1_tai(time) .unwrap_err() .extrapolated_value .to_decimal_seconds(); assert_float_eq!(actual, expected, rel <= 1e-8); let ut1 = time.with_scale_and_delta(Ut1, TimeDelta::from_decimal_seconds(actual).unwrap()); let actual = provider - .delta_tai_ut1(&ut1) + .delta_tai_ut1(ut1) .unwrap_err() .extrapolated_value .to_decimal_seconds(); diff --git a/crates/lox-time/src/utc.rs b/crates/lox-time/src/utc.rs index d4f28cd6..7d92b98f 100644 --- a/crates/lox-time/src/utc.rs +++ b/crates/lox-time/src/utc.rs @@ -20,17 +20,34 @@ use itertools::Itertools; use num::ToPrimitive; use thiserror::Error; +use self::leap_seconds::BuiltinLeapSeconds; use crate::calendar_dates::{CalendarDate, Date, DateError}; use crate::deltas::{TimeDelta, ToDelta}; use crate::julian_dates::JulianDate; +use crate::prelude::Tai; use crate::time_of_day::{CivilTime, TimeOfDay, TimeOfDayError}; -use crate::transformations::LeapSecondsProvider; - -use self::leap_seconds::BuiltinLeapSeconds; +use crate::time_scales::transformations::OffsetProvider; +use crate::Time; pub mod leap_seconds; pub mod transformations; +/// Implementers of `LeapSecondsProvider` provide the offset between TAI and UTC in leap seconds at +/// an instant in either time scale. +pub trait LeapSecondsProvider: OffsetProvider { + /// The difference in leap seconds between TAI and UTC at the given TAI instant. + fn delta_tai_utc(&self, tai: Time) -> Option; + + /// The difference in leap seconds between UTC and TAI at the given UTC instant. + fn delta_utc_tai(&self, utc: Utc) -> Option; + + /// Returns `true` if a leap second occurs on `date`. + fn is_leap_second_date(&self, date: Date) -> bool; + + /// Returns `true` if a leap second occurs at `tai`. + fn is_leap_second(&self, tai: Time) -> bool; +} + /// Error type returned when attempting to construct a [Utc] instance from invalid inputs. #[derive(Debug, Clone, Error, PartialEq, Eq, PartialOrd, Ord)] pub enum UtcError { diff --git a/crates/lox-time/src/utc/leap_seconds.rs b/crates/lox-time/src/utc/leap_seconds.rs index 4dd2f967..627f8b02 100644 --- a/crates/lox-time/src/utc/leap_seconds.rs +++ b/crates/lox-time/src/utc/leap_seconds.rs @@ -21,6 +21,8 @@ use crate::calendar_dates::{Date, DateError}; use crate::constants::i64::{SECONDS_PER_DAY, SECONDS_PER_HALF_DAY}; use crate::deltas::{TimeDelta, ToDelta}; use crate::prelude::{CivilTime, Tai}; +use crate::time_scales::transformations::OffsetProvider; +use crate::utc::{LeapSecondsProvider, Utc}; use crate::Time; use lox_io::spice::{Kernel, KernelError}; use std::convert::Infallible; @@ -29,9 +31,6 @@ use std::num::ParseIntError; use std::path::Path; use thiserror::Error; -use crate::transformations::{LeapSecondsProvider, OffsetProvider}; -use crate::utc::Utc; - const LEAP_SECONDS_KERNEL_KEY: &str = "DELTET/DELTA_AT"; const LEAP_SECOND_EPOCHS_UTC: [i64; 28] = [ @@ -240,8 +239,8 @@ mod tests { use crate::deltas::TimeDelta; use crate::time; use crate::time_scales::Tai; - use crate::transformations::LeapSecondsProvider; use crate::utc; + use crate::utc::LeapSecondsProvider; use crate::utc::Utc; use crate::Time; diff --git a/crates/lox-time/src/utc/transformations.rs b/crates/lox-time/src/utc/transformations.rs index b7fe076b..fe762584 100644 --- a/crates/lox-time/src/utc/transformations.rs +++ b/crates/lox-time/src/utc/transformations.rs @@ -18,17 +18,7 @@ use crate::time_scales::Tcb; use crate::time_scales::Tcg; use crate::time_scales::Tdb; use crate::time_scales::Tt; -use crate::time_scales::Ut1; -use crate::transformations::LeapSecondsProvider; -use crate::transformations::NoOpOffsetProvider; -use crate::transformations::ToTai; -use crate::transformations::ToTcb; -use crate::transformations::ToTcg; -use crate::transformations::ToTdb; -use crate::transformations::ToTt; -use crate::transformations::ToUt1; -use crate::transformations::TryToScale; -use crate::ut1::DeltaUt1TaiProvider; +use crate::utc::LeapSecondsProvider; use crate::{utc, Time}; use super::leap_seconds::BuiltinLeapSeconds; @@ -36,6 +26,25 @@ use super::{Utc, UtcError}; mod before1972; +pub fn to_utc_with_provider( + time: Time, + provider: &impl LeapSecondsProvider, +) -> Result { + let delta = if time < *tai_at_utc_1972_01_01() { + before1972::delta_tai_utc(time) + } else { + provider.delta_tai_utc(time) + } + .ok_or(UtcError::UtcUndefined)?; + let mut utc = Utc::from_delta(time.to_delta() - delta); + if provider.is_leap_second(time) { + utc.time = TimeOfDay::new(utc.hour(), utc.minute(), 60) + .unwrap() + .with_subsecond(utc.time.subsecond()); + } + Ok(utc) +} + pub trait ToUtc { fn to_utc_with_provider(&self, provider: &impl LeapSecondsProvider) -> Result; @@ -92,90 +101,6 @@ impl ToUtc for Time { } } -impl TryToScale for Utc { - fn try_to_scale(&self, _scale: Tai, provider: &T) -> Result, T::Error> { - let delta = if self < utc_1972_01_01() { - before1972::delta_utc_tai(self) - } else { - provider.delta_utc_tai(*self) - } - .unwrap_or_else(|| { - // Utc objects are always in range. - unreachable!("failed to calculate UTC-TAI delta for Utc `{:?}`", self); - }); - - Ok(Time::from_delta(Tai, self.to_delta() - delta)) - } -} - -impl TryToScale for Utc { - fn try_to_scale( - &self, - scale: Tai, - _provider: &NoOpOffsetProvider, - ) -> Result, Infallible> { - self.try_to_scale(scale, &BuiltinLeapSeconds) - } -} - -impl ToTai for Utc {} - -impl TryToScale for Utc { - fn try_to_scale( - &self, - scale: Tt, - provider: &NoOpOffsetProvider, - ) -> Result, Infallible> { - self.to_tai().try_to_scale(scale, provider) - } -} - -impl ToTt for Utc {} - -impl TryToScale for Utc { - fn try_to_scale( - &self, - scale: Tdb, - provider: &NoOpOffsetProvider, - ) -> Result, Infallible> { - self.to_tt().try_to_scale(scale, provider) - } -} - -impl ToTdb for Utc {} - -impl TryToScale for Utc { - fn try_to_scale( - &self, - scale: Tcb, - provider: &NoOpOffsetProvider, - ) -> Result, Infallible> { - self.to_tdb().try_to_scale(scale, provider) - } -} - -impl ToTcb for Utc {} - -impl TryToScale for Utc { - fn try_to_scale( - &self, - scale: Tcg, - provider: &NoOpOffsetProvider, - ) -> Result, Infallible> { - self.to_tt().try_to_scale(scale, provider) - } -} - -impl ToTcg for Utc {} - -impl TryToScale for Utc { - fn try_to_scale(&self, scale: Ut1, provider: &T) -> Result, T::Error> { - self.to_tai().try_to_scale(scale, provider) - } -} - -impl ToUt1 for Utc {} - fn utc_1972_01_01() -> &'static Utc { static UTC_1972: OnceLock = OnceLock::new(); UTC_1972.get_or_init(|| utc!(1972, 1, 1).unwrap()) @@ -196,7 +121,6 @@ fn tai_at_utc_1972_01_01() -> &'static Time { mod test { use crate::test_helpers::delta_ut1_tai; use crate::time; - use crate::transformations::{ToTcb, ToTcg, ToTdb, ToTt}; use rstest::rstest; use crate::subsecond::Subsecond; diff --git a/crates/lox-time/src/utc/transformations/before1972.rs b/crates/lox-time/src/utc/transformations/before1972.rs index 376b4f16..94ebe02f 100644 --- a/crates/lox-time/src/utc/transformations/before1972.rs +++ b/crates/lox-time/src/utc/transformations/before1972.rs @@ -65,7 +65,7 @@ pub fn delta_utc_tai(utc: &Utc) -> Option { } /// TAI minus UTC. -pub fn delta_tai_utc(tai: &Time) -> Option { +pub fn delta_tai_utc(tai: Time) -> Option { // Invariant: EPOCHS must be sorted for the search below to work debug_assert!(is_sorted_asc(&EPOCHS));