1use 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#[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#[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 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 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}