Skip to main content

lox_time/utc/
leap_seconds.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
6/*!
7    Module `leap_seconds` exposes the [LeapSecondsProvider] trait for defining sources of leap
8    second data. Lox's standard implementation, [BuiltinLeapSeconds], is suitable for most
9    applications.
10
11    `leap_seconds` additionally exposes the lower-level [LeapSecondsKernel] for working directly
12    with [NAIF Leap Seconds Kernel][LSK] data.
13
14    [LSK]: https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/time.html#The%20Leapseconds%20Kernel%20LSK
15*/
16
17use crate::calendar_dates::Date;
18use crate::deltas::{TimeDelta, ToDelta};
19use crate::time::Time;
20use crate::time_of_day::CivilTime;
21use crate::time_scales::Tai;
22use crate::utc::Utc;
23use lox_core::i64::consts::SECONDS_PER_DAY;
24
25/// UTC epochs (seconds since J2000) at which each leap second takes effect.
26pub const LEAP_SECOND_EPOCHS_UTC: [i64; 28] = [
27    -883656000, -867931200, -852033600, -820497600, -788961600, -757425600, -725803200, -694267200,
28    -662731200, -631195200, -583934400, -552398400, -520862400, -457704000, -378734400, -315576000,
29    -284040000, -236779200, -205243200, -173707200, -126273600, -79012800, -31579200, 189345600,
30    284040000, 394372800, 488980800, 536500800,
31];
32
33/// TAI epochs (seconds since J2000) corresponding to each leap second.
34pub const LEAP_SECOND_EPOCHS_TAI: [i64; 28] = [
35    -883655991, -867931190, -852033589, -820497588, -788961587, -757425586, -725803185, -694267184,
36    -662731183, -631195182, -583934381, -552398380, -520862379, -457703978, -378734377, -315575976,
37    -284039975, -236779174, -205243173, -173707172, -126273571, -79012770, -31579169, 189345632,
38    284040033, 394372834, 488980835, 536500836,
39];
40
41/// Cumulative TAI−UTC offset in seconds at each leap second epoch.
42pub const LEAP_SECONDS: [i64; 28] = [
43    10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
44    34, 35, 36, 37,
45];
46
47/// Implementers of `LeapSecondsProvider` provide the offset between TAI and UTC in leap seconds at
48/// an instant in either time scale.
49pub trait LeapSecondsProvider {
50    /// The difference in leap seconds between TAI and UTC at the given TAI instant.
51    fn delta_tai_utc(&self, tai: Time<Tai>) -> TimeDelta {
52        find_leap_seconds_tai(&LEAP_SECOND_EPOCHS_TAI, &LEAP_SECONDS, tai)
53    }
54
55    /// The difference in leap seconds between UTC and TAI at the given UTC instant.
56    fn delta_utc_tai(&self, utc: Utc) -> TimeDelta {
57        find_leap_seconds_utc(&LEAP_SECOND_EPOCHS_UTC, &LEAP_SECONDS, utc)
58    }
59
60    /// Returns `true` if a leap second occurs on `date`.
61    fn is_leap_second_date(&self, date: Date) -> bool {
62        is_leap_second_date(&LEAP_SECOND_EPOCHS_UTC, date)
63    }
64
65    /// Returns `true` if a leap second occurs at `tai`.
66    fn is_leap_second(&self, tai: Time<Tai>) -> bool {
67        is_leap_second(&LEAP_SECOND_EPOCHS_TAI, tai)
68    }
69}
70
71/// `lox-time`'s default [LeapSecondsProvider], suitable for most applications.
72///
73/// `BuiltinLeapSeconds` relies on a hard-coded table of leap second data. As new leap seconds are
74/// announced, `lox-time` will be updated to include the new data, reflected by a minor version
75/// change. If this is unsuitable for your use case, we recommend implementing [LeapSecondsProvider]
76/// manually.
77#[derive(Debug)]
78pub struct DefaultLeapSecondsProvider;
79
80impl LeapSecondsProvider for DefaultLeapSecondsProvider {}
81
82fn find_leap_seconds(epochs: &[i64], leap_seconds: &[i64], seconds: i64) -> TimeDelta {
83    if seconds < epochs[0] {
84        return TimeDelta::ZERO;
85    }
86    let idx = epochs.partition_point(|&epoch| epoch <= seconds) - 1;
87    let seconds = leap_seconds[idx];
88    TimeDelta::from_seconds(seconds)
89}
90
91/// Returns the TAI−UTC leap second offset at the given TAI instant.
92pub fn find_leap_seconds_tai(epochs: &[i64], leap_seconds: &[i64], tai: Time<Tai>) -> TimeDelta {
93    let seconds = tai.seconds().expect("TAI time should have finite seconds");
94    find_leap_seconds(epochs, leap_seconds, seconds)
95}
96
97/// Returns the UTC−TAI leap second offset at the given UTC instant.
98pub fn find_leap_seconds_utc(epochs: &[i64], leap_seconds: &[i64], utc: Utc) -> TimeDelta {
99    let seconds = utc
100        .to_delta()
101        .seconds()
102        .expect("UTC time should have finite seconds");
103    let mut ls = find_leap_seconds(epochs, leap_seconds, seconds);
104    if utc.second() == 60 {
105        ls -= TimeDelta::from_seconds(1);
106    }
107    -ls
108}
109
110/// Returns `true` if a leap second occurs on `date`.
111pub fn is_leap_second_date(epochs: &[i64], date: Date) -> bool {
112    let epochs: Vec<i64> = epochs
113        .iter()
114        .map(|&epoch| epoch / SECONDS_PER_DAY)
115        .collect();
116    let day_number = date.j2000_day_number();
117    epochs.binary_search(&day_number).is_ok()
118}
119
120/// Returns `true` if a leap second occurs at the given TAI instant.
121pub fn is_leap_second(epochs: &[i64], tai: Time<Tai>) -> bool {
122    match tai.seconds() {
123        Some(seconds) => epochs.binary_search(&seconds).is_ok(),
124        None => false,
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use rstest::rstest;
132
133    use crate::deltas::TimeDelta;
134    use crate::time;
135    use crate::time::Time;
136    use crate::time_scales::Tai;
137    use crate::utc;
138    use crate::utc::LeapSecondsProvider;
139    use crate::utc::Utc;
140
141    #[rstest]
142    #[case::j2000(Time::default(), Utc::default(), 32)]
143    #[case::new_year_1972(time!(Tai, 1972, 1, 1, 0, 0, 10.0).unwrap(), utc!(1972, 1, 1).unwrap(), 10)]
144    #[case::new_year_2017(time!(Tai, 2017, 1, 1, 0, 0, 37.0).unwrap(), utc!(2017, 1, 1, 0, 0, 0.0).unwrap(), 37)]
145    #[case::new_year_2024(time!(Tai, 2024, 1, 1).unwrap(), utc!(2024, 1, 1).unwrap(), 37)]
146    fn test_builtin_leap_seconds(#[case] tai: Time<Tai>, #[case] utc: Utc, #[case] expected: i64) {
147        let ls_tai = DefaultLeapSecondsProvider.delta_tai_utc(tai);
148        let ls_utc = DefaultLeapSecondsProvider.delta_utc_tai(utc);
149        assert_eq!(ls_tai, TimeDelta::from_seconds(expected));
150        assert_eq!(ls_utc, TimeDelta::from_seconds(-expected));
151    }
152
153    #[rstest]
154    #[case(Date::new(2000, 12, 31).unwrap(), false)]
155    #[case(Date::new(2016, 12, 31).unwrap(), true)]
156    fn test_is_leap_second_date(#[case] date: Date, #[case] expected: bool) {
157        let actual = DefaultLeapSecondsProvider.is_leap_second_date(date);
158        assert_eq!(actual, expected);
159    }
160
161    #[rstest]
162    #[case(time!(Tai, 2017, 1, 1, 0, 0, 35.0).unwrap(), false)]
163    #[case(time!(Tai, 2017, 1, 1, 0, 0, 36.0).unwrap(), true)]
164    fn test_is_leap_second(#[case] tai: Time<Tai>, #[case] expected: bool) {
165        let actual = DefaultLeapSecondsProvider.is_leap_second(tai);
166        assert_eq!(actual, expected);
167    }
168}