Skip to main content

lox_io/spice/
lsk.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::{fs::read_to_string, num::ParseIntError, path::Path};
7
8use lox_core::i64::consts::{SECONDS_PER_DAY, SECONDS_PER_HALF_DAY};
9use lox_time::{
10    Time,
11    calendar_dates::{Date, DateError},
12    deltas::TimeDelta,
13    time_scales::Tai,
14    utc::{
15        Utc,
16        leap_seconds::{
17            LeapSecondsProvider, find_leap_seconds_tai, find_leap_seconds_utc, is_leap_second,
18            is_leap_second_date,
19        },
20    },
21};
22use thiserror::Error;
23
24use crate::spice::{Kernel, KernelError};
25
26const LEAP_SECONDS_KERNEL_KEY: &str = "DELTET/DELTA_AT";
27
28/// Error type related to parsing leap seconds data from a NAIF Leap Seconds Kernel.
29#[derive(Debug, Error)]
30pub enum LeapSecondsKernelError {
31    #[error(transparent)]
32    Io(#[from] std::io::Error),
33    #[error(transparent)]
34    Kernel(#[from] KernelError),
35    #[error(
36        "no leap seconds found in kernel under key `{}`",
37        LEAP_SECONDS_KERNEL_KEY
38    )]
39    NoLeapSeconds,
40    #[error(transparent)]
41    ParseInt(#[from] ParseIntError),
42    #[error(transparent)]
43    DateError(#[from] DateError),
44}
45
46/// In-memory representation of a NAIF Leap Seconds Kernel.
47///
48/// Most users should prefer [BuiltinLeapSeconds] to implementing their own [LeapSecondsProvider]
49/// using the kernel.
50#[derive(Debug, Clone)]
51pub struct LeapSecondsKernel {
52    epochs_utc: Vec<i64>,
53    epochs_tai: Vec<i64>,
54    leap_seconds: Vec<i64>,
55}
56
57impl LeapSecondsKernel {
58    /// Parse a NAIF Leap Seconds Kernel from a string.
59    ///
60    /// # Errors
61    ///
62    /// - [LeapSecondsKernelError::Kernel] if the kernel format is unparseable.
63    /// - [LeapSecondsKernelError::NoLeapSeconds] if the kernel contains no leap second data.
64    /// - [LeapSecondsKernelError::ParseInt] if a leap second entry in the kernel can't be
65    ///   represented as an i64.
66    /// - [LeapSecondsKernelError::DateError] if a date contained within the kernel is not
67    ///   represented as a valid ISO 8601 string.
68    pub fn from_string(kernel: impl AsRef<str>) -> Result<Self, LeapSecondsKernelError> {
69        let kernel = Kernel::from_string(kernel.as_ref())?;
70        let data = kernel
71            .get_timestamp_array(LEAP_SECONDS_KERNEL_KEY)
72            .ok_or(LeapSecondsKernelError::NoLeapSeconds)?;
73        let mut epochs_utc: Vec<i64> = Vec::with_capacity(data.len() / 2);
74        let mut epochs_tai: Vec<i64> = Vec::with_capacity(data.len() / 2);
75        let mut leap_seconds: Vec<i64> = Vec::with_capacity(data.len() / 2);
76        for chunk in data.chunks(2) {
77            let ls = chunk[0].parse::<i64>()?;
78            let date = Date::from_iso(
79                &chunk[1]
80                    .replace("JAN", "01")
81                    .replace("JUL", "07")
82                    .replace("-1", "-01"),
83            )?;
84            let epoch = date.j2000_day_number() * SECONDS_PER_DAY - SECONDS_PER_HALF_DAY;
85            epochs_utc.push(epoch);
86            epochs_tai.push(epoch + ls - 1);
87            leap_seconds.push(ls);
88        }
89        Ok(Self {
90            epochs_utc,
91            epochs_tai,
92            leap_seconds,
93        })
94    }
95
96    /// Parse a NAIF Leap Seconds Kernel located at `path`.
97    ///
98    /// # Errors
99    ///
100    /// - [LeapSecondsKernelError::Io] if the file at `path` can't be read.
101    /// - [LeapSecondsKernelError::Kernel] if the kernel format is unparseable.
102    /// - [LeapSecondsKernelError::NoLeapSeconds] if the kernel contains no leap second data.
103    /// - [LeapSecondsKernelError::ParseInt] if a leap second entry in the kernel can't be
104    ///   represented as an i64.
105    /// - [LeapSecondsKernelError::DateError] if a date contained within the kernel is not
106    ///   represented as a valid ISO 8601 string.
107    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, LeapSecondsKernelError> {
108        let path = path.as_ref();
109        let kernel = read_to_string(path)?;
110        Self::from_string(kernel)
111    }
112}
113
114impl LeapSecondsProvider for LeapSecondsKernel {
115    fn delta_tai_utc(&self, tai: Time<Tai>) -> TimeDelta {
116        find_leap_seconds_tai(&self.epochs_tai, &self.leap_seconds, tai)
117    }
118
119    fn delta_utc_tai(&self, utc: Utc) -> TimeDelta {
120        find_leap_seconds_utc(&self.epochs_utc, &self.leap_seconds, utc)
121    }
122
123    fn is_leap_second_date(&self, date: Date) -> bool {
124        is_leap_second_date(&self.epochs_tai, date)
125    }
126
127    fn is_leap_second(&self, tai: Time<Tai>) -> bool {
128        is_leap_second(&self.epochs_tai, tai)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use lox_time::utc;
136    use lox_time::{
137        time,
138        utc::leap_seconds::{LEAP_SECOND_EPOCHS_TAI, LEAP_SECOND_EPOCHS_UTC, LEAP_SECONDS},
139    };
140    use rstest::rstest;
141    use std::sync::OnceLock;
142
143    #[test]
144    fn test_leap_seconds_kernel() {
145        let lsk = kernel();
146        assert_eq!(lsk.leap_seconds.len(), 28);
147        assert_eq!(lsk.epochs_utc.len(), 28);
148        assert_eq!(lsk.epochs_tai.len(), 28);
149        assert_eq!(lsk.leap_seconds, &LEAP_SECONDS);
150        assert_eq!(lsk.epochs_utc, &LEAP_SECOND_EPOCHS_UTC);
151        assert_eq!(lsk.epochs_tai, &LEAP_SECOND_EPOCHS_TAI);
152    }
153
154    const KERNEL: &str = "KPL/LSK
155
156\\begindata
157
158DELTET/DELTA_AT        = ( 10,   @1972-JAN-1
159                           11,   @1972-JUL-1
160                           12,   @1973-JAN-1
161                           13,   @1974-JAN-1
162                           14,   @1975-JAN-1
163                           15,   @1976-JAN-1
164                           16,   @1977-JAN-1
165                           17,   @1978-JAN-1
166                           18,   @1979-JAN-1
167                           19,   @1980-JAN-1
168                           20,   @1981-JUL-1
169                           21,   @1982-JUL-1
170                           22,   @1983-JUL-1
171                           23,   @1985-JUL-1
172                           24,   @1988-JAN-1
173                           25,   @1990-JAN-1
174                           26,   @1991-JAN-1
175                           27,   @1992-JUL-1
176                           28,   @1993-JUL-1
177                           29,   @1994-JUL-1
178                           30,   @1996-JAN-1
179                           31,   @1997-JUL-1
180                           32,   @1999-JAN-1
181                           33,   @2006-JAN-1
182                           34,   @2009-JAN-1
183                           35,   @2012-JUL-1
184                           36,   @2015-JUL-1
185                           37,   @2017-JAN-1 )
186
187\\begintext";
188
189    #[rstest]
190    #[case::before_utc(
191        time!(Tai, 1971, 12, 31, 23, 59, 59.0).unwrap(),
192        utc!(1971, 12, 31, 23, 59, 59.0).unwrap(),
193        TimeDelta::ZERO,
194    )]
195    #[case::j2000(Time::default(), Utc::default(), TimeDelta::from_seconds(32))]
196    #[case::new_year_1972(
197        time!(Tai, 1972, 1, 1, 0, 0, 10.0).unwrap(),
198        utc!(1972, 1, 1).unwrap(),
199        TimeDelta::from_seconds(10),
200    )]
201    #[case::new_year_2017(
202        time!(Tai, 2017, 1, 1, 0, 0, 37.0).unwrap(),
203        utc!(2017, 1, 1, 0, 0, 0.0).unwrap(),
204        TimeDelta::from_seconds(37),
205    )]
206    #[case::new_year_2024(
207        time!(Tai, 2024, 1, 1).unwrap(),
208        utc!(2024, 1, 1).unwrap(),
209        TimeDelta::from_seconds(37),
210    )]
211    fn test_leap_seconds_kernel_leap_seconds(
212        #[case] tai: Time<Tai>,
213        #[case] utc: Utc,
214        #[case] expected: TimeDelta,
215    ) {
216        let lsk = kernel();
217        let ls_tai = lsk.delta_tai_utc(tai);
218        let ls_utc = lsk.delta_utc_tai(utc);
219        assert_eq!(ls_tai, expected);
220        assert_eq!(ls_utc, -expected);
221    }
222
223    #[rstest]
224    #[case(Date::new(2000, 12, 31).unwrap(), false)]
225    #[case(Date::new(2016, 12, 31).unwrap(), true)]
226    fn test_is_leap_second_date(#[case] date: Date, #[case] expected: bool) {
227        let actual = kernel().is_leap_second_date(date);
228        assert_eq!(actual, expected);
229    }
230
231    #[rstest]
232    #[case(time!(Tai, 2017, 1, 1, 0, 0, 35.0).unwrap(), false)]
233    #[case(time!(Tai, 2017, 1, 1, 0, 0, 36.0).unwrap(), true)]
234    fn test_is_leap_second(#[case] tai: Time<Tai>, #[case] expected: bool) {
235        let actual = kernel().is_leap_second(tai);
236        assert_eq!(actual, expected);
237    }
238
239    fn kernel() -> &'static LeapSecondsKernel {
240        static LSK: OnceLock<LeapSecondsKernel> = OnceLock::new();
241        LSK.get_or_init(|| LeapSecondsKernel::from_string(KERNEL).expect("file should be parsable"))
242    }
243}