Skip to main content

lox_time/utc/
transformations.rs

1// SPDX-FileCopyrightText: 2024 Angus Morrison <github@angus-morrison.com>
2// SPDX-FileCopyrightText: 2024 Helge Eichhorn <git@helgeeichhorn.de>
3//
4// SPDX-License-Identifier: MPL-2.0
5
6use std::sync::OnceLock;
7
8use crate::deltas::TimeDelta;
9use crate::deltas::ToDelta;
10use crate::offsets::DefaultOffsetProvider;
11use crate::offsets::Offset;
12use crate::time::DynTime;
13use crate::time_of_day::CivilTime;
14use crate::time_of_day::TimeOfDay;
15use crate::time_scales::TimeScale;
16use crate::time_scales::{DynTimeScale, Tai};
17use crate::{time::Time, utc};
18
19use super::LeapSecondsProvider;
20use super::Utc;
21use super::leap_seconds::DefaultLeapSecondsProvider;
22
23mod before1972;
24
25impl Utc {
26    /// Returns the TAI−UTC offset at this UTC instant.
27    pub fn offset_tai(&self, provider: &impl LeapSecondsProvider) -> TimeDelta {
28        if self < utc_1972_01_01() {
29            before1972::delta_utc_tai(self)
30        } else {
31            provider.delta_utc_tai(*self)
32        }
33    }
34
35    /// Converts this UTC instant to TAI using the given leap-seconds provider.
36    pub fn to_time_with_provider(&self, provider: &impl LeapSecondsProvider) -> Time<Tai> {
37        let offset = self.offset_tai(provider);
38        Time::from_delta(Tai, self.to_delta() - offset)
39    }
40
41    /// Converts this UTC instant to TAI using the built-in leap-seconds table.
42    pub fn to_time(&self) -> Time<Tai> {
43        self.to_time_with_provider(&DefaultLeapSecondsProvider)
44    }
45
46    /// Converts this UTC instant to a [`DynTime`] in TAI using the given provider.
47    pub fn to_dyn_time_with_provider(&self, provider: &impl LeapSecondsProvider) -> DynTime {
48        let offset = self.offset_tai(provider);
49        Time::from_delta(DynTimeScale::Tai, self.to_delta() - offset)
50    }
51
52    /// Converts this UTC instant to a [`DynTime`] in TAI using the built-in table.
53    pub fn to_dyn_time(&self) -> DynTime {
54        self.to_dyn_time_with_provider(&DefaultLeapSecondsProvider)
55    }
56}
57
58/// Trait for types that can be converted to [`Utc`].
59pub trait ToUtc {
60    /// Converts to UTC using the given leap-seconds provider.
61    fn to_utc_with_provider(&self, provider: &impl LeapSecondsProvider) -> Utc;
62
63    /// Converts to UTC using the built-in leap-seconds table.
64    fn to_utc(&self) -> Utc {
65        self.to_utc_with_provider(&DefaultLeapSecondsProvider)
66    }
67}
68
69impl ToUtc for Utc {
70    fn to_utc_with_provider(&self, _provider: &impl LeapSecondsProvider) -> Utc {
71        *self
72    }
73}
74
75impl<T> ToUtc for Time<T>
76where
77    T: TimeScale + Copy,
78    DefaultOffsetProvider: Offset<T, Tai>,
79{
80    fn to_utc_with_provider(&self, provider: &impl LeapSecondsProvider) -> Utc {
81        let tai = self.to_scale(Tai);
82        assert!(
83            tai.seconds().is_some(),
84            "NaN TimeDelta cannot be converted to UTC"
85        );
86        let delta = if &tai < tai_at_utc_1972_01_01() {
87            before1972::delta_tai_utc(&tai)
88        } else {
89            provider.delta_tai_utc(tai)
90        };
91        let mut utc = Utc::from_delta(tai.to_delta() - delta)
92            .expect("finite TAI time should produce valid UTC");
93        if provider.is_leap_second(tai) {
94            utc.time = TimeOfDay::new(utc.hour(), utc.minute(), 60)
95                .unwrap()
96                .with_subsecond(utc.time.subsecond());
97        }
98        utc
99    }
100}
101
102fn utc_1972_01_01() -> &'static Utc {
103    static UTC_1972: OnceLock<Utc> = OnceLock::new();
104    UTC_1972.get_or_init(|| utc!(1972, 1, 1).unwrap())
105}
106
107fn tai_at_utc_1972_01_01() -> &'static Time<Tai> {
108    const LEAP_SECONDS_1972: i64 = 10;
109    static TAI_AT_UTC_1972_01_01: OnceLock<Time<Tai>> = OnceLock::new();
110    TAI_AT_UTC_1972_01_01.get_or_init(|| {
111        let utc = utc_1972_01_01();
112        let base_time = utc.to_delta();
113        let leap_seconds = TimeDelta::from_seconds(LEAP_SECONDS_1972);
114        Time::from_delta(Tai, base_time + leap_seconds)
115    })
116}
117
118#[cfg(test)]
119mod test {
120    use crate::subsecond::Subsecond;
121    use crate::time;
122    use crate::time_scales::{Tcb, Tcg, Tdb, Tt};
123    use rstest::rstest;
124
125    use super::*;
126
127    #[test]
128    fn test_utc_to_utc() {
129        let utc0 = utc!(2000, 1, 1).unwrap();
130        let utc1 = utc0.to_utc();
131        assert_eq!(utc0, utc1);
132    }
133
134    #[rstest]
135    #[case::before_1972(utc_1971_01_01(), tai_at_utc_1971_01_01())]
136    #[case::before_leap_second(utc_1s_before_2016_leap_second(), tai_1s_before_2016_leap_second())]
137    #[case::during_leap_second(utc_during_2016_leap_second(), tai_during_2016_leap_second())]
138    #[case::after_leap_second(utc_1s_after_2016_leap_second(), tai_1s_after_2016_leap_second())]
139    fn test_utc_to_tai(#[case] utc: &Utc, #[case] expected: &Time<Tai>) {
140        let actual = utc.to_time();
141        assert_eq!(*expected, actual);
142    }
143
144    #[rstest]
145    #[case::before_utc_1972(tai_at_utc_1971_01_01(), *utc_1971_01_01())]
146    #[case::utc_1972(tai_at_utc_1972_01_01(), *utc_1972_01_01())]
147    #[case::before_leap_second(tai_1s_before_2016_leap_second(), *utc_1s_before_2016_leap_second())]
148    #[case::during_leap_second(tai_during_2016_leap_second(), *utc_during_2016_leap_second())]
149    #[case::after_leap_second(tai_1s_after_2016_leap_second(), *utc_1s_after_2016_leap_second())]
150    #[case::before_1960(tai_before_1960(), *utc_before_1960())]
151    fn test_tai_to_utc(#[case] tai: &Time<Tai>, #[case] expected: Utc) {
152        let actual = tai.to_utc();
153        assert_eq!(expected, actual);
154    }
155
156    #[test]
157    fn test_all_scales_to_utc() {
158        use lox_test_utils::assert_approx_eq;
159
160        let tai = time!(Tai, 2024, 5, 17, 12, 13, 14.0).unwrap();
161        let exp = tai.to_utc();
162        let tt = tai.to_scale(Tt);
163        let act = tt.to_utc();
164        assert_eq!(act, exp);
165        let tcg = tai.to_scale(Tcg);
166        let act = tcg.to_utc();
167        assert_eq!(act, exp);
168        // TCB conversions have lower precision due to the multi-step transformation
169        let tcb = tai.to_scale(Tcb);
170        let act = tcb.to_utc();
171        assert_approx_eq!(act, exp);
172        let tdb = tai.to_scale(Tdb);
173        let act = tdb.to_utc();
174        assert_eq!(act, exp);
175    }
176
177    /*
178        The following fixtures are derived from a mixture of direct calculation and, in the case
179        where inherent rounding errors prevent exact calculation, by cross-referencing with the
180        observed outputs. The latter case is marked with a comment.
181    */
182
183    fn utc_1971_01_01() -> &'static Utc {
184        static UTC_1971: OnceLock<Utc> = OnceLock::new();
185        UTC_1971.get_or_init(|| utc!(1971, 1, 1).unwrap())
186    }
187
188    fn tai_at_utc_1971_01_01() -> &'static Time<Tai> {
189        // const DELTA: TimeDelta = TimeDelta::builder()
190        //     .seconds(8)
191        //     .milliseconds(946)
192        //     .microseconds(162)
193        //     .build();
194        const DELTA: TimeDelta = TimeDelta::from_seconds_and_subsecond_f64(8.0, 0.9461620000000011);
195
196        static TAI_AT_UTC_1971_01_01: OnceLock<Time<Tai>> = OnceLock::new();
197        TAI_AT_UTC_1971_01_01.get_or_init(|| {
198            let utc = utc_1971_01_01();
199            let base = utc.to_delta();
200            Time::from_delta(Tai, base + DELTA)
201        })
202    }
203
204    // 2016-12-31T23:59:59.000 UTC
205    fn utc_1s_before_2016_leap_second() -> &'static Utc {
206        static BEFORE_LEAP_SECOND: OnceLock<Utc> = OnceLock::new();
207        BEFORE_LEAP_SECOND.get_or_init(|| utc!(2016, 12, 31, 23, 59, 59.0).unwrap())
208    }
209
210    // 2017-01-01T00:00:35.000 TAI
211    fn tai_1s_before_2016_leap_second() -> &'static Time<Tai> {
212        static BEFORE_LEAP_SECOND: OnceLock<Time<Tai>> = OnceLock::new();
213        BEFORE_LEAP_SECOND.get_or_init(|| Time::new(Tai, 536500835, Subsecond::default()))
214    }
215
216    // 2016-12-31T23:59:60.000 UTC
217    fn utc_during_2016_leap_second() -> &'static Utc {
218        static DURING_LEAP_SECOND: OnceLock<Utc> = OnceLock::new();
219        DURING_LEAP_SECOND.get_or_init(|| utc!(2016, 12, 31, 23, 59, 60.0).unwrap())
220    }
221
222    // 2017-01-01T00:00:36.000 TAI
223    fn tai_during_2016_leap_second() -> &'static Time<Tai> {
224        static DURING_LEAP_SECOND: OnceLock<Time<Tai>> = OnceLock::new();
225        DURING_LEAP_SECOND.get_or_init(|| Time::new(Tai, 536500836, Subsecond::default()))
226    }
227
228    // 2017-01-01T00:00:00.000 UTC
229    fn utc_1s_after_2016_leap_second() -> &'static Utc {
230        static AFTER_LEAP_SECOND: OnceLock<Utc> = OnceLock::new();
231        AFTER_LEAP_SECOND.get_or_init(|| utc!(2017, 1, 1).unwrap())
232    }
233
234    // 2017-01-01T00:00:37.000 TAI
235    fn tai_1s_after_2016_leap_second() -> &'static Time<Tai> {
236        static AFTER_LEAP_SECOND: OnceLock<Time<Tai>> = OnceLock::new();
237        AFTER_LEAP_SECOND.get_or_init(|| Time::new(Tai, 536500837, Subsecond::default()))
238    }
239
240    fn utc_before_1960() -> &'static Utc {
241        static UTC_BEFORE_1960: OnceLock<Utc> = OnceLock::new();
242        UTC_BEFORE_1960.get_or_init(|| utc!(1959, 12, 31).unwrap())
243    }
244
245    // 1959-12-31T00:00:00.000 TAI (same as UTC since offset is zero pre-1960)
246    fn tai_before_1960() -> &'static Time<Tai> {
247        static TAI_BEFORE_1960: OnceLock<Time<Tai>> = OnceLock::new();
248        TAI_BEFORE_1960.get_or_init(|| {
249            let utc = utc_before_1960();
250            utc.to_time()
251        })
252    }
253}