|
| 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 | +} |
0 commit comments