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}