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}