Skip to content

Commit 07a0b9e

Browse files
committed
Implement LeapSecondsProvider
1 parent 3c29dff commit 07a0b9e

File tree

4 files changed

+250
-31
lines changed

4 files changed

+250
-31
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/lox-time/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ license.workspace = true
77
authors.workspace = true
88

99
[dependencies]
10+
lox-io.workspace = true
1011
lox-utils.workspace = true
1112
lox-eop.workspace = true
1213

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*
2+
* Copyright (c) 2024. Helge Eichhorn and the LOX contributors
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
use crate::calendar_dates::Date;
10+
use crate::constants::i64::{SECONDS_PER_DAY, SECONDS_PER_HALF_DAY};
11+
use lox_io::spice::{Kernel, KernelError};
12+
use std::fs::read_to_string;
13+
use std::num::ParseIntError;
14+
use std::path::Path;
15+
use thiserror::Error;
16+
17+
use crate::deltas::TimeDelta;
18+
use crate::time_of_day::CivilTime;
19+
use crate::time_scales::Tai;
20+
use crate::utc::Utc;
21+
use crate::Time;
22+
23+
const LEAP_SECONDS_KEY: &str = "DELTET/DELTA_AT";
24+
25+
const LEAP_SECOND_EPOCHS_UTC: [i64; 28] = [
26+
-883656000, -867931200, -852033600, -820497600, -788961600, -757425600, -725803200, -694267200,
27+
-662731200, -631195200, -583934400, -552398400, -520862400, -457704000, -378734400, -315576000,
28+
-284040000, -236779200, -205243200, -173707200, -126273600, -79012800, -31579200, 189345600,
29+
284040000, 394372800, 488980800, 536500800,
30+
];
31+
32+
const LEAP_SECOND_EPOCHS_TAI: [i64; 28] = [
33+
-883655991, -867931190, -852033589, -820497588, -788961587, -757425586, -725803185, -694267184,
34+
-662731183, -631195182, -583934381, -552398380, -520862379, -457703978, -378734377, -315575976,
35+
-284039975, -236779174, -205243173, -173707172, -126273571, -79012770, -31579169, 189345632,
36+
284040033, 394372834, 488980835, 536500836,
37+
];
38+
39+
const LEAP_SECONDS: [i64; 28] = [
40+
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
41+
34, 35, 36, 37,
42+
];
43+
44+
pub trait LeapSecondsProvider {
45+
fn epochs_utc(&self) -> &[i64];
46+
fn epochs_tai(&self) -> &[i64];
47+
fn leap_seconds(&self) -> &[i64];
48+
49+
fn find_leap_seconds(&self, epochs: &[i64], seconds: i64) -> Option<TimeDelta> {
50+
if seconds < epochs[0] {
51+
return None;
52+
}
53+
let idx = epochs.partition_point(|&epoch| epoch <= seconds) - 1;
54+
let seconds = self.leap_seconds()[idx];
55+
Some(TimeDelta::from_seconds(seconds))
56+
}
57+
58+
fn delta_tai_utc(&self, tai: Time<Tai>) -> Option<TimeDelta> {
59+
self.find_leap_seconds(self.epochs_tai(), tai.seconds())
60+
}
61+
62+
fn delta_utc_tai(&self, utc: Utc) -> Option<TimeDelta> {
63+
self.find_leap_seconds(self.epochs_utc(), utc.to_delta().seconds)
64+
.map(|mut ls| {
65+
if utc.second() == 60 {
66+
ls.seconds -= 1;
67+
}
68+
-ls
69+
})
70+
}
71+
}
72+
73+
pub struct BuiltinLeapSeconds;
74+
75+
impl LeapSecondsProvider for BuiltinLeapSeconds {
76+
fn epochs_utc(&self) -> &[i64] {
77+
&LEAP_SECOND_EPOCHS_UTC
78+
}
79+
80+
fn epochs_tai(&self) -> &[i64] {
81+
&LEAP_SECOND_EPOCHS_TAI
82+
}
83+
84+
fn leap_seconds(&self) -> &[i64] {
85+
&LEAP_SECONDS
86+
}
87+
}
88+
89+
#[derive(Debug, Error)]
90+
pub enum LskError {
91+
#[error(transparent)]
92+
Io(#[from] std::io::Error),
93+
#[error(transparent)]
94+
Kernel(#[from] KernelError),
95+
#[error("no leap seconds found in kernel under key `{}`", LEAP_SECONDS_KEY)]
96+
NoLeapSeconds,
97+
#[error(transparent)]
98+
ParseInt(#[from] ParseIntError),
99+
}
100+
101+
pub struct Lsk {
102+
epochs_utc: Vec<i64>,
103+
epochs_tai: Vec<i64>,
104+
leap_seconds: Vec<i64>,
105+
}
106+
107+
impl Lsk {
108+
pub fn from_string(kernel: impl AsRef<str>) -> Result<Self, LskError> {
109+
let kernel = Kernel::from_string(kernel.as_ref())?;
110+
let data = kernel
111+
.get_timestamp_array(LEAP_SECONDS_KEY)
112+
.ok_or(LskError::NoLeapSeconds)?;
113+
let mut epochs_utc: Vec<i64> = vec![];
114+
let mut epochs_tai: Vec<i64> = vec![];
115+
let mut leap_seconds: Vec<i64> = vec![];
116+
data.chunks(2).for_each(|chunk| {
117+
if chunk.len() != 2 {
118+
return;
119+
}
120+
let ls = chunk[0].parse::<i64>();
121+
let date = Date::from_iso(
122+
&chunk[1]
123+
.replace("JAN", "01")
124+
.replace("JUL", "07")
125+
.replace("-1", "-01"),
126+
);
127+
if let (Ok(ls), Ok(date)) = (ls, date) {
128+
let epoch = date.j2000_day_number() * SECONDS_PER_DAY - SECONDS_PER_HALF_DAY;
129+
epochs_utc.push(epoch);
130+
epochs_tai.push(epoch + ls - 1);
131+
leap_seconds.push(ls);
132+
}
133+
});
134+
Ok(Self {
135+
epochs_utc,
136+
epochs_tai,
137+
leap_seconds,
138+
})
139+
}
140+
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, LskError> {
141+
let path = path.as_ref();
142+
let kernel = read_to_string(path)?;
143+
Self::from_string(kernel)
144+
}
145+
}
146+
147+
impl LeapSecondsProvider for Lsk {
148+
fn epochs_utc(&self) -> &[i64] {
149+
&self.epochs_utc
150+
}
151+
152+
fn epochs_tai(&self) -> &[i64] {
153+
&self.epochs_tai
154+
}
155+
156+
fn leap_seconds(&self) -> &[i64] {
157+
&self.leap_seconds
158+
}
159+
}
160+
161+
#[cfg(test)]
162+
mod tests {
163+
use super::*;
164+
use rstest::rstest;
165+
use std::sync::OnceLock;
166+
167+
use crate::time;
168+
use crate::utc;
169+
170+
#[rstest]
171+
#[case::j2000(Time::default(), Utc::default(), 32)]
172+
#[case::new_year_1972(time!(Tai, 1972, 1, 1, 0, 0, 10.0).unwrap(), utc!(1972, 1, 1).unwrap(), 10)]
173+
#[case::new_year_2017(time!(Tai, 2017, 1, 1, 0, 0, 37.0).unwrap(), utc!(2017, 1, 1, 0, 0, 0.0).unwrap(), 37)]
174+
#[case::new_year_2024(time!(Tai, 2024, 1, 1).unwrap(), utc!(2024, 1, 1).unwrap(), 37)]
175+
fn test_builtin_leap_seconds(#[case] tai: Time<Tai>, #[case] utc: Utc, #[case] expected: i64) {
176+
let ls_tai = BuiltinLeapSeconds.delta_tai_utc(tai).unwrap();
177+
let ls_utc = BuiltinLeapSeconds.delta_utc_tai(utc).unwrap();
178+
assert_eq!(ls_tai, TimeDelta::from_seconds(expected));
179+
assert_eq!(ls_utc, TimeDelta::from_seconds(-expected));
180+
}
181+
182+
#[rstest]
183+
#[case::j2000(Time::default(), Utc::default(), 32)]
184+
#[case::new_year_1972(time!(Tai, 1972, 1, 1, 0, 0, 10.0).unwrap(), utc!(1972, 1, 1).unwrap(), 10)]
185+
#[case::new_year_2017(time!(Tai, 2017, 1, 1, 0, 0, 37.0).unwrap(), utc!(2017, 1, 1, 0, 0, 0.0).unwrap(), 37)]
186+
#[case::new_year_2024(time!(Tai, 2024, 1, 1).unwrap(), utc!(2024, 1, 1).unwrap(), 37)]
187+
fn test_lsk_leap_seconds(#[case] tai: Time<Tai>, #[case] utc: Utc, #[case] expected: i64) {
188+
let lsk = kernel();
189+
let ls_tai = lsk.delta_tai_utc(tai).unwrap();
190+
let ls_utc = lsk.delta_utc_tai(utc).unwrap();
191+
assert_eq!(ls_tai, TimeDelta::from_seconds(expected));
192+
assert_eq!(ls_utc, TimeDelta::from_seconds(-expected));
193+
}
194+
195+
#[test]
196+
fn test_lsk() {
197+
let lsk = kernel();
198+
assert_eq!(lsk.epochs_utc().len(), 28);
199+
assert_eq!(lsk.epochs_tai().len(), 28);
200+
assert_eq!(lsk.leap_seconds().len(), 28);
201+
assert_eq!(lsk.epochs_utc(), &LEAP_SECOND_EPOCHS_UTC);
202+
assert_eq!(lsk.epochs_tai(), &LEAP_SECOND_EPOCHS_TAI);
203+
}
204+
205+
const KERNEL: &str = "KPL/LSK
206+
207+
\\begindata
208+
209+
DELTET/DELTA_AT = ( 10, @1972-JAN-1
210+
11, @1972-JUL-1
211+
12, @1973-JAN-1
212+
13, @1974-JAN-1
213+
14, @1975-JAN-1
214+
15, @1976-JAN-1
215+
16, @1977-JAN-1
216+
17, @1978-JAN-1
217+
18, @1979-JAN-1
218+
19, @1980-JAN-1
219+
20, @1981-JUL-1
220+
21, @1982-JUL-1
221+
22, @1983-JUL-1
222+
23, @1985-JUL-1
223+
24, @1988-JAN-1
224+
25, @1990-JAN-1
225+
26, @1991-JAN-1
226+
27, @1992-JUL-1
227+
28, @1993-JUL-1
228+
29, @1994-JUL-1
229+
30, @1996-JAN-1
230+
31, @1997-JUL-1
231+
32, @1999-JAN-1
232+
33, @2006-JAN-1
233+
34, @2009-JAN-1
234+
35, @2012-JUL-1
235+
36, @2015-JUL-1
236+
37, @2017-JAN-1 )
237+
238+
\\begintext";
239+
240+
fn kernel() -> &'static Lsk {
241+
static LSK: OnceLock<Lsk> = OnceLock::new();
242+
LSK.get_or_init(|| Lsk::from_string(KERNEL).expect("file should be parsable"))
243+
}
244+
}

crates/lox-time/src/utc/transformations/from1972.rs

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use lox_utils::slices::is_sorted_asc;
1919
use crate::calendar_dates::Date;
2020
use crate::deltas::TimeDelta;
2121
use crate::julian_dates::JulianDate;
22-
use crate::time_of_day::CivilTime;
2322
use crate::time_scales::Tai;
23+
use crate::utc::leap_seconds::{BuiltinLeapSeconds, LeapSecondsProvider};
2424
use crate::utc::Utc;
2525
use crate::Time;
2626

@@ -33,7 +33,7 @@ const MJD_LEAP_SECOND_EPOCHS: [u64; 28] = [
3333

3434
impl Date {
3535
pub fn is_leap_second_date(&self) -> bool {
36-
let mjd = (self.days_since_modified_julian_epoch().ceil()).to_u64();
36+
let mjd = (self.days_since_modified_julian_epoch() + 1.0).to_u64();
3737
if let Some(mjd) = mjd {
3838
return MJD_LEAP_SECOND_EPOCHS.contains(&mjd);
3939
}
@@ -82,39 +82,12 @@ pub const LEAP_SECONDS: [i64; 28] = [
8282
/// For dates from 1972-01-01, returns a [TimeDelta] representing the count of leap seconds between
8383
/// TAI and UTC. Returns `None` for dates before 1972.
8484
pub fn delta_tai_utc(tai: Time<Tai>) -> Option<TimeDelta> {
85-
j2000_tai_leap_second_epochs()
86-
.iter()
87-
.rev()
88-
.zip(LEAP_SECONDS.iter().rev())
89-
.find_map(|(&epoch, &leap_seconds)| {
90-
if epoch <= tai.seconds() {
91-
Some(TimeDelta::from_seconds(leap_seconds))
92-
} else {
93-
None
94-
}
95-
})
85+
BuiltinLeapSeconds.delta_tai_utc(tai)
9686
}
9787

9888
/// UTC minus TAI. Calculates the correct leap second count for dates after 1972 by simple lookup.
9989
pub fn delta_utc_tai(utc: Utc) -> Option<TimeDelta> {
100-
let base_time = utc.to_delta();
101-
j2000_utc_leap_second_epochs()
102-
.iter()
103-
.rev()
104-
.zip(LEAP_SECONDS.iter().rev())
105-
.find_map(|(&epoch, &leap_seconds)| {
106-
if epoch <= base_time.seconds {
107-
Some(TimeDelta::from_seconds(leap_seconds))
108-
} else {
109-
None
110-
}
111-
})
112-
.map(|mut delta| {
113-
if utc.second() == 60 {
114-
delta.seconds -= 1;
115-
}
116-
-delta
117-
})
90+
BuiltinLeapSeconds.delta_utc_tai(utc)
11891
}
11992

12093
impl Time<Tai> {

0 commit comments

Comments
 (0)