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::format::{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///
34/// Variant names follow the `<Format><Scale>` convention: the prefix names
35/// the encoding format (e.g. `Jd` = Julian Day, `Mjd` = Modified Julian Day)
36/// and the suffix names the time scale (e.g. `Tt`, `Tai`, `Tdb`).
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ScaleKind {
39    /// Julian Day on the TT axis (equivalently: Julian Ephemeris Date). Value in days.
40    JdTt,
41    /// Modified Julian Day on the TT axis. Value in days.
42    MjdTt,
43    /// Julian Day on the TDB axis. Value in days.
44    JdTdb,
45    /// Julian Day on the TAI axis. Value in days.
46    JdTai,
47    /// Julian Day on the TCG axis. Value in days.
48    JdTcg,
49    /// Julian Day on the TCB axis. Value in days.
50    JdTcb,
51    /// Julian Day offset from the GPS epoch, on the TAI axis.
52    ///
53    /// The unit is **Julian days** (not GPS seconds). A value of `1.0`
54    /// represents one Julian day (86 400 s) elapsed since the GPS epoch.
55    /// This is distinct from conventional GPS time which is expressed in
56    /// integer seconds or (week, seconds-of-week). Divide by 86 400 to
57    /// convert from GPS seconds to this encoding.
58    JdGps,
59    /// Julian Day on the UT1 axis. Value in days.
60    JdUt1,
61    /// Unix / POSIX time in seconds since 1970-01-01T00:00:00 UTC.
62    Unix,
63}
64
65/// Convert a scalar in the given scale to [`Time<TT>`].
66///
67/// This is the single authoritative entry point for scalar → `Time<TT>`.
68/// For context-free scales the `ctx` argument is unused; for
69/// [`ScaleKind::Ut1`] `ctx` supplies the ΔT table used by the UT1→TT
70/// conversion.
71#[inline]
72pub fn time_tt_from_scalar(
73    value: f64,
74    kind: ScaleKind,
75    ctx: &TimeContext,
76) -> Result<Time<TT>, ConversionError> {
77    match kind {
78        ScaleKind::JdTt => JulianDate::<TT>::try_new(Day::new(value)).map(|e| e.to_time()),
79        ScaleKind::MjdTt => ModifiedJulianDate::<TT>::try_new(Day::new(value)).map(|e| e.to_time()),
80        ScaleKind::JdTdb => {
81            JulianDate::<TDB>::try_new(Day::new(value)).map(|e| e.to_time().to_scale::<TT>())
82        }
83        ScaleKind::JdTai => {
84            JulianDate::<TAI>::try_new(Day::new(value)).map(|e| e.to_time().to_scale::<TT>())
85        }
86        ScaleKind::JdTcg => {
87            JulianDate::<TCG>::try_new(Day::new(value)).map(|e| e.to_time().to_scale::<TT>())
88        }
89        ScaleKind::JdTcb => {
90            JulianDate::<TCB>::try_new(Day::new(value)).map(|e| e.to_time().to_scale::<TT>())
91        }
92        ScaleKind::JdGps => JulianDate::<TAI>::try_new(GPS_EPOCH_JD_TAI.raw() + Day::new(value))
93            .map(|e| e.to_time().to_scale::<TT>()),
94        ScaleKind::JdUt1 => JulianDate::<UT1>::try_new(Day::new(value))
95            .and_then(|e| e.to_time().to_scale_with::<TT>(ctx)),
96        ScaleKind::Unix => UnixTime::try_new(Second::new(value))
97            .and_then(|e| e.to_time_with(ctx))
98            .map(|t| t.to_scale::<TT>()),
99    }
100}
101
102/// Convert a [`Time<TT>`] value to a scalar in the given scale.
103///
104/// This is the single authoritative entry point for `Time<TT>` → scalar.
105/// For context-free scales the `ctx` argument is unused; for
106/// [`ScaleKind::Ut1`] and [`ScaleKind::Unix`] `ctx` supplies the ΔT /
107/// UTC-TAI table.
108#[inline]
109pub fn time_tt_to_scalar(
110    tt: Time<TT>,
111    kind: ScaleKind,
112    ctx: &TimeContext,
113) -> Result<f64, ConversionError> {
114    use crate::format::{JD, MJD};
115    match kind {
116        ScaleKind::JdTt => Ok(tt.to::<JD>().raw() / Day::new(1.0)),
117        ScaleKind::MjdTt => Ok(tt.to::<MJD>().raw() / Day::new(1.0)),
118        ScaleKind::JdTdb => Ok(tt.to_scale::<TDB>().to::<JD>().raw() / Day::new(1.0)),
119        ScaleKind::JdTai => Ok(tt.to_scale::<TAI>().to::<JD>().raw() / Day::new(1.0)),
120        ScaleKind::JdTcg => Ok(tt.to_scale::<TCG>().to::<JD>().raw() / Day::new(1.0)),
121        ScaleKind::JdTcb => Ok(tt.to_scale::<TCB>().to::<JD>().raw() / Day::new(1.0)),
122        ScaleKind::JdGps => {
123            Ok((tt.to_scale::<TAI>().to::<JD>().raw() - GPS_EPOCH_JD_TAI.raw()) / Day::new(1.0))
124        }
125        ScaleKind::JdUt1 => Ok(tt.to_scale_with::<UT1>(ctx)?.to::<JD>().raw() / Day::new(1.0)),
126        ScaleKind::Unix => tt
127            .to_scale::<UTC>()
128            .raw_unix_seconds_with(ctx)
129            .map(|s| s / Second::new(1.0)),
130    }
131}
132
133/// Compute the difference between two scalar values in the same scale, in days.
134///
135/// For [`ScaleKind::Unix`] (seconds), the raw difference is converted to days.
136/// For all other scales the raw difference already represents days.
137#[inline]
138pub fn scalar_difference_in_days(lhs: f64, rhs: f64, kind: ScaleKind) -> f64 {
139    match kind {
140        ScaleKind::Unix => Second::new(lhs - rhs).to::<qtty::unit::Day>() / Day::new(1.0),
141        _ => lhs - rhs,
142    }
143}
144
145/// Add a day-valued duration to a scalar in the given scale.
146///
147/// For [`ScaleKind::Unix`] (seconds) the duration is converted to seconds
148/// before adding. For all other scales the duration is added directly as days.
149#[inline]
150pub fn scalar_add_days(value: f64, days: Day, kind: ScaleKind) -> f64 {
151    match kind {
152        ScaleKind::Unix => value + days.to::<qtty::unit::Second>() / Second::new(1.0),
153        _ => value + days / Day::new(1.0),
154    }
155}