Skip to main content

tempoch_core/
scalar.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Scalar-value adapter for time scale dispatch.
5//!
6//! This module is the single authoritative conversion matrix between `f64`
7//! scalar values in various scales and [`Time<TT>`]. FFI and other
8//! scalar-oriented layers should delegate here instead of reimplementing the
9//! dispatch logic themselves.
10//!
11//! # Usage
12//!
13//! An FFI crate maps its own integer discriminants to [`ScaleKind`] and then
14//! calls [`time_tt_from_scalar`] / [`time_tt_to_scalar`] for all roundtrips
15//! through the canonical TT axis. Arithmetic helpers
16//! ([`scalar_difference_in_days`], [`scalar_add_days`]) handle the
17//! seconds-vs-days distinction for the [`ScaleKind::Unix`] encoding.
18
19use crate::constats::GPS_EPOCH_JD_TAI;
20use crate::context::TimeContext;
21use crate::error::ConversionError;
22use crate::representation::{JulianDate, ModifiedJulianDate, UnixTime};
23use crate::scale::{TAI, TCB, TCG, TDB, TT, UT1, UTC};
24use crate::time::Time;
25use qtty::{Day, Second};
26
27/// Identifies a time scale or scalar encoding for dispatch.
28///
29/// `ScaleKind` is the Rust-native counterpart to C ABI scale identifiers.
30/// FFI adapters map their own integer discriminants to `ScaleKind` and then
31/// delegate all conversion logic to [`time_tt_from_scalar`] and
32/// [`time_tt_to_scalar`] rather than reimplementing the dispatch matrix.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ScaleKind {
35    /// Julian Day (TT) — equivalently Julian Ephemeris Date (JDE). Value in days.
36    JdTt,
37    /// Modified Julian Day on the TT axis. Value in days.
38    MjdTt,
39    /// Barycentric Dynamical Time, Julian days on the TDB axis.
40    Tdb,
41    /// International Atomic Time, Julian days on the TAI axis.
42    Tai,
43    /// Geocentric Coordinate Time, Julian days on the TCG axis.
44    Tcg,
45    /// Barycentric Coordinate Time, Julian days on the TCB axis.
46    Tcb,
47    /// GPS days since [`GPS_EPOCH_JD_TAI`] on the TAI axis.
48    ///
49    /// The unit is **Julian days** (not GPS seconds). A value of `1.0`
50    /// represents one Julian day (86 400 s) elapsed since the GPS epoch.
51    /// This is distinct from conventional GPS time which is expressed in
52    /// integer seconds or (week, seconds-of-week). Divide by 86 400 to
53    /// convert from GPS seconds to this representation.
54    GpsDays,
55    /// Universal Time UT1, Julian days on the UT1 axis.
56    Ut1,
57    /// Unix / POSIX time in seconds since 1970-01-01T00:00:00 UTC.
58    Unix,
59}
60
61/// Convert a scalar in the given scale to [`Time<TT>`].
62///
63/// This is the single authoritative entry point for scalar → `Time<TT>`.
64/// For context-free scales the `ctx` argument is unused; for
65/// [`ScaleKind::Ut1`] `ctx` supplies the ΔT table used by the UT1→TT
66/// conversion.
67#[inline]
68pub fn time_tt_from_scalar(
69    value: f64,
70    kind: ScaleKind,
71    ctx: &TimeContext,
72) -> Result<Time<TT>, ConversionError> {
73    match kind {
74        ScaleKind::JdTt => JulianDate::<TT>::try_new(Day::new(value)).map(|e| e.to_time()),
75        ScaleKind::MjdTt => ModifiedJulianDate::<TT>::try_new(Day::new(value)).map(|e| e.to_time()),
76        ScaleKind::Tdb => {
77            JulianDate::<TDB>::try_new(Day::new(value)).map(|e| e.to_time().to_scale::<TT>())
78        }
79        ScaleKind::Tai => {
80            JulianDate::<TAI>::try_new(Day::new(value)).map(|e| e.to_time().to_scale::<TT>())
81        }
82        ScaleKind::Tcg => {
83            JulianDate::<TCG>::try_new(Day::new(value)).map(|e| e.to_time().to_scale::<TT>())
84        }
85        ScaleKind::Tcb => {
86            JulianDate::<TCB>::try_new(Day::new(value)).map(|e| e.to_time().to_scale::<TT>())
87        }
88        ScaleKind::GpsDays => JulianDate::<TAI>::try_new(GPS_EPOCH_JD_TAI + Day::new(value))
89            .map(|e| e.to_time().to_scale::<TT>()),
90        ScaleKind::Ut1 => JulianDate::<UT1>::try_new(Day::new(value))
91            .and_then(|e| e.to_time().to_scale_with::<TT>(ctx)),
92        ScaleKind::Unix => UnixTime::try_new(Second::new(value))
93            .and_then(|e| e.to_time_with(ctx))
94            .map(|t| t.to_scale::<TT>()),
95    }
96}
97
98/// Convert a [`Time<TT>`] value to a scalar in the given scale.
99///
100/// This is the single authoritative entry point for `Time<TT>` → scalar.
101/// For context-free scales the `ctx` argument is unused; for
102/// [`ScaleKind::Ut1`] and [`ScaleKind::Unix`] `ctx` supplies the ΔT /
103/// UTC-TAI table.
104#[inline]
105pub fn time_tt_to_scalar(
106    tt: Time<TT>,
107    kind: ScaleKind,
108    ctx: &TimeContext,
109) -> Result<f64, ConversionError> {
110    use crate::representation::{JD, MJD};
111    match kind {
112        ScaleKind::JdTt => Ok(tt.to::<JD>().raw() / Day::new(1.0)),
113        ScaleKind::MjdTt => Ok(tt.to::<MJD>().raw() / Day::new(1.0)),
114        ScaleKind::Tdb => Ok(tt.to_scale::<TDB>().to::<JD>().raw() / Day::new(1.0)),
115        ScaleKind::Tai => Ok(tt.to_scale::<TAI>().to::<JD>().raw() / Day::new(1.0)),
116        ScaleKind::Tcg => Ok(tt.to_scale::<TCG>().to::<JD>().raw() / Day::new(1.0)),
117        ScaleKind::Tcb => Ok(tt.to_scale::<TCB>().to::<JD>().raw() / Day::new(1.0)),
118        ScaleKind::GpsDays => {
119            Ok((tt.to_scale::<TAI>().to::<JD>().raw() - GPS_EPOCH_JD_TAI) / Day::new(1.0))
120        }
121        ScaleKind::Ut1 => Ok(tt.to_scale_with::<UT1>(ctx)?.to::<JD>().raw() / Day::new(1.0)),
122        ScaleKind::Unix => tt
123            .to_scale::<UTC>()
124            .raw_unix_seconds_with(ctx)
125            .map(|s| s / Second::new(1.0)),
126    }
127}
128
129/// Compute the difference between two scalar values in the same scale, in days.
130///
131/// For [`ScaleKind::Unix`] (seconds), the raw difference is converted to days.
132/// For all other scales the raw difference already represents days.
133#[inline]
134pub fn scalar_difference_in_days(lhs: f64, rhs: f64, kind: ScaleKind) -> f64 {
135    match kind {
136        ScaleKind::Unix => Second::new(lhs - rhs).to::<qtty::unit::Day>() / Day::new(1.0),
137        _ => lhs - rhs,
138    }
139}
140
141/// Add a day-valued duration to a scalar in the given scale.
142///
143/// For [`ScaleKind::Unix`] (seconds) the duration is converted to seconds
144/// before adding. For all other scales the duration is added directly as days.
145#[inline]
146pub fn scalar_add_days(value: f64, days: Day, kind: ScaleKind) -> f64 {
147    match kind {
148        ScaleKind::Unix => value + days.to::<qtty::unit::Second>() / Second::new(1.0),
149        _ => value + days / Day::new(1.0),
150    }
151}