Skip to main content

tempoch_core/
context.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Conversion context.
5
6use crate::data::active::{active_time_data, time_data_eop_at};
7use crate::eop::EopValues;
8use qtty::{Day, Second};
9use std::sync::Arc;
10use tempoch_time_data::TimeDataBundle;
11
12/// Explicit, immutable context for conversions that need one.
13///
14/// A `TimeContext` snapshots the active time-data bundle at construction time
15/// and selects which parts of that snapshot back context-required conversions.
16/// The default constructor [`TimeContext::new`] uses the monthly ΔT series from
17/// the captured bundle, matching the behaviour of previous versions;
18/// [`TimeContext::with_builtin_eop`] selects the daily IERS `finals2000A.all`
19/// series from that same snapshot for the highest-fidelity bundled UT1 path
20/// inside its coverage window.
21///
22/// # ΔT / UT1 accuracy
23///
24/// | Epoch range | Default context (monthly ΔT) | `with_builtin_eop()` |
25/// |---|---|---|
26/// | Pre-948 CE | ±hundreds of s (Stephenson & Houlden quadratic) | same (outside EOP range) |
27/// | 948–1619 | ±15 s (Stephenson & Houlden) | same |
28/// | 1620–1973 | ±0.1–1 s (Meeus biennial table) | same |
29/// | 1973 – EOP start | ~0.01 s (USNO monthly) | same |
30/// | EOP observed range | < 15 ms from the bundled daily IERS-derived path over the compiled observed overlap | preferred highest-fidelity bundled UT1 path |
31/// | EOP prediction range | < 0.2 s from the bundled short-range daily prediction over the compiled prediction overlap | preferred highest-fidelity bundled UT1 path |
32/// | Beyond EOP | monthly ΔT only; prediction uncertainty grows | falls back to monthly ΔT |
33///
34/// The builtin EOP is only consulted inside the captured bundle's coverage;
35/// outside of that range the monthly ΔT path applies unchanged. Construct a
36/// fresh context after refreshing the active bundle if you want to use the
37/// updated runtime data.
38#[derive(Debug, Clone)]
39pub struct TimeContext {
40    data: Arc<TimeDataBundle>,
41    eop: EopSource,
42    utc_pre_definition: bool,
43}
44
45#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
46enum EopSource {
47    /// Use the monthly ΔT series only (default, bit-compatible with
48    /// pre-EOP tempoch releases).
49    #[default]
50    None,
51    /// Consult the compiled daily IERS `finals2000A.all` series when the
52    /// requested epoch is within coverage, and fall back to the monthly ΔT
53    /// series otherwise.
54    Builtin,
55}
56
57impl Default for TimeContext {
58    #[inline]
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl TimeContext {
65    #[inline]
66    fn snapshot(eop: EopSource) -> Self {
67        Self {
68            data: active_time_data(),
69            eop,
70            utc_pre_definition: false,
71        }
72    }
73
74    /// Construct a default context backed by the monthly ΔT table.
75    ///
76    /// This is the lightweight, always-available choice. It does not consult
77    /// the daily EOP series even when the bundled data contains one.
78    #[inline]
79    pub fn new() -> Self {
80        Self::snapshot(EopSource::None)
81    }
82
83    /// Construct a context that prefers the compiled daily IERS
84    /// `finals2000A.all` series for UT1 conversions when the epoch is
85    /// within its coverage window.
86    ///
87    /// Outside the bundled EOP coverage, this falls back to the same monthly
88    /// ΔT path used by [`TimeContext::new`].
89    #[inline]
90    pub fn with_builtin_eop() -> Self {
91        Self::snapshot(EopSource::Builtin)
92    }
93
94    #[inline]
95    pub(crate) fn time_data(&self) -> &TimeDataBundle {
96        self.data.as_ref()
97    }
98
99    /// Allow UTC conversions for dates before 1961-01-01.
100    ///
101    /// By default, [`Time::<UTC>::try_from_chrono_with`](crate::Time::try_from_chrono_with) and related conversions
102    /// return [`crate::ConversionError::UtcBeforeDefinition`] for any date
103    /// before MJD 37 300 (1961-01-01), because UTC was not an international
104    /// standard before that date and the back-extrapolated offset is
105    /// historically fabricated.
106    ///
107    /// Calling this method on a context opts into the approximate
108    /// continuation: the first official UTC-TAI segment is extrapolated
109    /// backwards. Round-trips close, but the values are not
110    /// standards-defined UTC.
111    ///
112    /// # Example
113    /// ```
114    /// use tempoch_core::{TimeContext, Time, UTC};
115    /// use chrono::DateTime;
116    ///
117    /// let dt = DateTime::from_timestamp(-631_152_000, 0).unwrap();
118    /// let ctx = TimeContext::new().allow_pre_definition_utc();
119    /// let utc = Time::<UTC>::try_from_chrono_with(dt, &ctx).unwrap();
120    /// ```
121    #[inline]
122    pub fn allow_pre_definition_utc(mut self) -> Self {
123        self.utc_pre_definition = true;
124        self
125    }
126
127    #[inline]
128    pub(crate) fn allows_pre_definition_utc(&self) -> bool {
129        self.utc_pre_definition
130    }
131    /// Interpolated EOP at `mjd_utc`, if this context has an EOP source and
132    /// the MJD is in range.
133    ///
134    /// This exposes the same interpolated values that context-backed scale
135    /// conversions consult internally, so callers can inspect or reuse them
136    /// without reimplementing the lookup path.
137    #[inline]
138    pub fn eop_at(&self, mjd_utc: Day) -> Option<EopValues> {
139        match self.eop {
140            EopSource::None => None,
141            EopSource::Builtin => time_data_eop_at(self.time_data(), mjd_utc),
142        }
143    }
144
145    /// Interpolated `UT1 - UTC` from the context's EOP source, if available.
146    ///
147    /// Returns `None` when this context is monthly-ΔT-only or when the epoch is
148    /// outside the captured EOP coverage window.
149    #[inline]
150    pub fn ut1_minus_utc(&self, mjd_utc: Day) -> Option<Second> {
151        self.eop_at(mjd_utc).map(|v| v.ut1_minus_utc)
152    }
153}