Skip to main content

sidereon_core/reduced_orbit/
time.rs

1//! Time-scale bridge for reduced-orbit fitting/evaluation.
2
3use crate::astro::time::civil;
4use crate::astro::time::model::TimeScale;
5use crate::astro::time::scales::TimeScales;
6
7/// A UTC calendar instant `(year, month, day, hour, minute, second)`, the form
8/// the core [`TimeScales::from_utc`] consumes. The Elixir layer produces these
9/// from each sample/query epoch; no `Instant`->`TimeScales` bridge exists in the
10/// core crate, so the calendar tuple is carried explicitly to the boundary.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct CalendarEpoch {
13    /// Calendar year.
14    pub year: i32,
15    /// Calendar month, 1-12.
16    pub month: i32,
17    /// Calendar day of month, 1-31.
18    pub day: i32,
19    /// Hour of day, 0-23.
20    pub hour: i32,
21    /// Minute of hour, 0-59.
22    pub minute: i32,
23    /// Second of minute, fractional.
24    pub second: f64,
25}
26
27impl CalendarEpoch {
28    /// Construct a calendar epoch from its components.
29    pub const fn new(year: i32, month: i32, day: i32, hour: i32, minute: i32, second: f64) -> Self {
30        Self {
31            year,
32            month,
33            day,
34            hour,
35            minute,
36            second,
37        }
38    }
39
40    /// Build the core [`TimeScales`] for this instant, interpreted in `scale`.
41    ///
42    /// Delegates to the canonical [`TimeScales::from_scale`]: non-UTC scales are
43    /// converted to the UTC calendar label before the Skyfield split is built,
44    /// so the Earth orientation used by the frame transforms is correct rather
45    /// than offset by the scale's leap-second gap.
46    pub(crate) fn time_scales(self, scale: TimeScale) -> TimeScales {
47        TimeScales::from_scale(
48            scale,
49            self.year,
50            self.month,
51            self.day,
52            self.hour,
53            self.minute,
54            self.second,
55        )
56        .expect("calendar epoch has a finite second")
57    }
58}
59
60/// Seconds between two calendar epochs via their J2000-TT split day numbers.
61pub(crate) fn dt_seconds(t0: &TimeScales, t: &TimeScales) -> f64 {
62    civil::seconds_between_splits(t.jd_whole, t.tt_fraction, t0.jd_whole, t0.tt_fraction)
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    /// GLONASST = UTC(SU) + 3 h: a GLONASST calendar instant resolves to the
70    /// same TT scales as the UTC instant three hours earlier (no leap term in
71    /// the 3 h shift).
72    #[test]
73    fn glonasst_resolves_as_utc_plus_three_hours() {
74        // 2020-06-15 03:00:00 GLONASST == 2020-06-15 00:00:00 UTC.
75        let glo = CalendarEpoch::new(2020, 6, 15, 3, 0, 0.0).time_scales(TimeScale::Glonasst);
76        let utc = CalendarEpoch::new(2020, 6, 15, 0, 0, 0.0).time_scales(TimeScale::Utc);
77        assert_eq!(glo, utc);
78    }
79
80    /// QZSST is synchronous with GPST, so a QZSST calendar instant resolves to
81    /// the same scales as the identically-labelled GPST instant.
82    #[test]
83    fn qzsst_resolves_identically_to_gpst() {
84        let qzs = CalendarEpoch::new(2020, 6, 15, 12, 0, 0.0).time_scales(TimeScale::Qzsst);
85        let gps = CalendarEpoch::new(2020, 6, 15, 12, 0, 0.0).time_scales(TimeScale::Gpst);
86        assert_eq!(qzs, gps);
87    }
88
89    /// The 3 h GLONASST->UTC shift correctly crosses the day/year boundary
90    /// around the 2017 leap. (Inputs are regular seconds; positive-leap `:60`
91    /// labels in GLONASST are not a bridge input - the leap-aware reasoning lives
92    /// in the offset helpers, which key off the UTC leap table.)
93    #[test]
94    fn glonasst_three_hour_shift_crosses_2017_boundary() {
95        // 2017-01-01 03:00:00 GLONASST == 2017-01-01 00:00:00 UTC (post-leap).
96        let post = CalendarEpoch::new(2017, 1, 1, 3, 0, 0.0).time_scales(TimeScale::Glonasst);
97        let post_utc = CalendarEpoch::new(2017, 1, 1, 0, 0, 0.0).time_scales(TimeScale::Utc);
98        assert_eq!(post, post_utc);
99
100        // 2017-01-01 02:59:59 GLONASST == 2016-12-31 23:59:59 UTC (pre-leap).
101        let pre = CalendarEpoch::new(2017, 1, 1, 2, 59, 59.0).time_scales(TimeScale::Glonasst);
102        let pre_utc = CalendarEpoch::new(2016, 12, 31, 23, 59, 59.0).time_scales(TimeScale::Utc);
103        assert_eq!(pre, pre_utc);
104    }
105}