diff --git a/Cargo.lock b/Cargo.lock index 51967dfe..4e80fc16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,7 @@ name = "lox-io" version = "0.0.0" dependencies = [ "nom", + "thiserror", ] [[package]] @@ -464,6 +465,7 @@ version = "0.1.0" dependencies = [ "float_eq", "lox-eop", + "lox-io", "lox-utils", "mockall", "num", diff --git a/crates/lox-io/Cargo.toml b/crates/lox-io/Cargo.toml index feea8cef..bc6e3a64 100644 --- a/crates/lox-io/Cargo.toml +++ b/crates/lox-io/Cargo.toml @@ -9,3 +9,4 @@ rust-version.workspace = true [dependencies] nom.workspace = true +thiserror.workspace = true diff --git a/crates/lox-io/src/spice.rs b/crates/lox-io/src/spice.rs index b5e8b70f..01306670 100644 --- a/crates/lox-io/src/spice.rs +++ b/crates/lox-io/src/spice.rs @@ -10,19 +10,27 @@ use std::collections::HashMap; use nom::branch::alt; use nom::bytes::complete::{tag, take_until, take_while, take_while1}; -use nom::character::complete::{alpha1, line_ending, multispace0, one_of}; +use nom::character::complete::{alpha1, digit1, line_ending, multispace0, one_of}; use nom::combinator::{map, map_res, recognize, rest}; +use nom::error::Error; use nom::multi::{fold_many1, many0, many1}; use nom::number::complete::{double, float}; use nom::sequence::{delimited, preceded, separated_pair, terminated, tuple}; -use nom::IResult; +use nom::{Finish, IResult}; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq)] +#[error(transparent)] +pub struct KernelError(#[from] Error); #[derive(Clone, Debug, PartialEq)] enum Value { Double(f64), String(String), + Timestamp(String), DoubleArray(Vec), StringArray(Vec), + TimestampArray(Vec), } #[derive(Clone, Debug, PartialEq)] @@ -34,14 +42,14 @@ pub struct Kernel { type Entries = Vec<(String, Value)>; impl Kernel { - pub fn from_string(input: &str) -> Result { - if let Ok(("", (type_id, entries, _))) = kernel(input) { - Ok(Self { + pub fn from_string(input: &str) -> Result { + let result = kernel(input).map_err(|e| e.to_owned()).finish(); + match result { + Ok((_, (type_id, entries, _))) => Ok(Self { type_id: type_id.to_string(), items: entries.into_iter().collect(), - }) - } else { - Err("kernel parsing failed") + }), + Err(err) => Err(KernelError(err)), } } @@ -67,6 +75,15 @@ impl Kernel { } } + pub fn get_timestamp_array(&self, key: &str) -> Option<&Vec> { + let value = self.items.get(key)?; + if let Value::TimestampArray(v) = value { + Some(v) + } else { + None + } + } + pub fn keys(&self) -> Vec<&String> { self.items.keys().collect() } @@ -120,8 +137,26 @@ fn spice_string(s: &str) -> IResult<&str, String> { parser(s) } +fn timestamp(s: &str) -> IResult<&str, String> { + let mut parser = map( + // NASA NAIF's LSK kernels break their own rules and mix timestamps with integers within a + // single array. + // See: https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/kernel.html#Variable%20Value%20Rules + alt(( + preceded(tag("@"), take_while1(|c| !is_separator(c))), + digit1, + )), + String::from, + ); + parser(s) +} + +fn is_separator(c: char) -> bool { + c.is_whitespace() || c == ',' +} + fn separator(s: &str) -> IResult<&str, &str> { - take_while1(|x: char| x.is_whitespace() || x == ',')(s) + take_while1(is_separator)(s) } fn double_array(s: &str) -> IResult<&str, Value> { @@ -148,6 +183,18 @@ fn string_array(s: &str) -> IResult<&str, Value> { parser(s) } +fn timestamp_array(s: &str) -> IResult<&str, Value> { + let mut parser = map( + delimited( + terminated(tag("("), separator), + many1(terminated(timestamp, separator)), + tag(")"), + ), + Value::TimestampArray, + ); + parser(s) +} + fn double_value(s: &str) -> IResult<&str, Value> { let mut parser = map(spice_double, Value::Double); parser(s) @@ -158,8 +205,13 @@ fn string_value(s: &str) -> IResult<&str, Value> { parser(s) } +fn timestamp_value(s: &str) -> IResult<&str, Value> { + let mut parser = map(timestamp, Value::Timestamp); + parser(s) +} + fn array_value(s: &str) -> IResult<&str, Value> { - let mut parser = alt((double_array, string_array)); + let mut parser = alt((double_array, string_array, timestamp_array)); parser(s) } @@ -171,7 +223,7 @@ fn key_value(s: &str) -> IResult<&str, (String, Value)> { take_while(char::is_whitespace), ), terminated(tag("="), take_while1(char::is_whitespace)), - alt((double_value, string_value, array_value)), + alt((double_value, string_value, timestamp_value, array_value)), ), |kv: (&str, Value)| (kv.0.to_string(), kv.1), ); @@ -243,6 +295,11 @@ mod tests { ); } + #[test] + fn test_timestamp() { + assert_eq!(timestamp("@1972-JAN-1"), Ok(("", "1972-JAN-1".to_string()))); + } + #[test] fn test_separator() { assert_eq!(separator(" "), Ok(("", " "))); @@ -289,6 +346,23 @@ mod tests { ); } + #[test] + fn test_timestamp_array() { + let input = "( @1972-JAN-1,@1972-JAN-1 \ + @1972-JAN-1 )"; + assert_eq!( + timestamp_array(input), + Ok(( + "", + Value::TimestampArray(vec!( + "1972-JAN-1".to_string(), + "1972-JAN-1".to_string(), + "1972-JAN-1".to_string() + )) + )) + ); + } + #[test] fn test_array() { let exp_float = Value::DoubleArray(vec![6378.1366, 6378.1366, 6356.7519]); diff --git a/crates/lox-io/tests/lsk.rs b/crates/lox-io/tests/lsk.rs new file mode 100644 index 00000000..f96f5bb0 --- /dev/null +++ b/crates/lox-io/tests/lsk.rs @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use lox_io::spice::Kernel; + +#[test] +fn test_lsk() { + let lsk = include_str!("../../../data/naif0012.tls"); + let kernel = Kernel::from_string(lsk).expect("file should be parsable"); + assert_eq!(kernel.type_id(), "LSK"); + + assert!(!kernel.keys().is_empty()); + + let exp = vec![ + "10", + "1972-JAN-1", + "11", + "1972-JUL-1", + "12", + "1973-JAN-1", + "13", + "1974-JAN-1", + "14", + "1975-JAN-1", + "15", + "1976-JAN-1", + "16", + "1977-JAN-1", + "17", + "1978-JAN-1", + "18", + "1979-JAN-1", + "19", + "1980-JAN-1", + "20", + "1981-JUL-1", + "21", + "1982-JUL-1", + "22", + "1983-JUL-1", + "23", + "1985-JUL-1", + "24", + "1988-JAN-1", + "25", + "1990-JAN-1", + "26", + "1991-JAN-1", + "27", + "1992-JUL-1", + "28", + "1993-JUL-1", + "29", + "1994-JUL-1", + "30", + "1996-JAN-1", + "31", + "1997-JUL-1", + "32", + "1999-JAN-1", + "33", + "2006-JAN-1", + "34", + "2009-JAN-1", + "35", + "2012-JUL-1", + "36", + "2015-JUL-1", + "37", + "2017-JAN-1", + ]; + let act = kernel + .get_timestamp_array("DELTET/DELTA_AT") + .expect("array should be present"); + assert_eq!(act, &exp); + + assert!(kernel.get_timestamp_array("DELTET/DELTA_T_A").is_none()); + assert!(kernel.get_double("DELTET/DELTA_AT").is_none()); + assert!(kernel.get_double_array("DELTET/DELTA_AT").is_none()); +} diff --git a/crates/lox-time/Cargo.toml b/crates/lox-time/Cargo.toml index effcbdbe..9d20eea2 100644 --- a/crates/lox-time/Cargo.toml +++ b/crates/lox-time/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true authors.workspace = true [dependencies] +lox-io.workspace = true lox-utils.workspace = true lox-eop.workspace = true diff --git a/crates/lox-time/src/calendar_dates.rs b/crates/lox-time/src/calendar_dates.rs index 5600a4bd..e848a9f4 100644 --- a/crates/lox-time/src/calendar_dates.rs +++ b/crates/lox-time/src/calendar_dates.rs @@ -120,7 +120,7 @@ impl Date { Err(DateError::InvalidDate(year, month, day)) } else { let calendar = calendar(year, month, day); - let check = Date::from_days_since_j2000(days_since_j2000(calendar, year, month, day)); + let check = Date::from_days_since_j2000(j2000_day_number(calendar, year, month, day)); if check.year() != year || check.month() != month || check.day() != day { Err(DateError::InvalidDate(year, month, day)) @@ -201,12 +201,17 @@ impl Date { day, }) } + + pub fn j2000_day_number(&self) -> i64 { + j2000_day_number(self.calendar, self.year, self.month, self.day) + } } impl JulianDate for Date { fn julian_date(&self, epoch: Epoch, unit: Unit) -> f64 { - let mut seconds = - days_since_j2000(self.calendar, self.year, self.month, self.day) * SECONDS_PER_DAY; + let mut seconds = j2000_day_number(self.calendar, self.year, self.month, self.day) + * SECONDS_PER_DAY + - SECONDS_PER_HALF_DAY; seconds = match epoch { Epoch::JulianDate => seconds + SECONDS_BETWEEN_JD_AND_J2000, Epoch::ModifiedJulianDate => seconds + SECONDS_BETWEEN_MJD_AND_J2000, @@ -309,7 +314,7 @@ fn calendar(year: i64, month: u8, day: u8) -> Calendar { } } -fn days_since_j2000(calendar: Calendar, year: i64, month: u8, day: u8) -> i64 { +fn j2000_day_number(calendar: Calendar, year: i64, month: u8, day: u8) -> i64 { let d1 = last_day_of_year_j2k(calendar, year - 1); let d2 = find_day_in_year(month, day, is_leap_year(calendar, year)); d1 + d2 as i64 @@ -377,17 +382,35 @@ mod tests { assert_eq!(actual, expected); } + #[test] + fn test_date_jd_epoch() { + let date = Date::default(); + assert_eq!(date.days_since_julian_epoch(), 2451544.5); + } + #[test] fn test_date_julian_date() { let date = Date::new(2100, 1, 1).unwrap(); - assert_eq!(date.seconds_since_j2000(), SECONDS_PER_JULIAN_CENTURY); - assert_eq!(date.days_since_j2000(), DAYS_PER_JULIAN_CENTURY); - assert_eq!(date.centuries_since_j2000(), 1.0); - assert_eq!(date.centuries_since_j1950(), 1.5); + assert_eq!( + date.seconds_since_j2000(), + SECONDS_PER_JULIAN_CENTURY - SECONDS_PER_HALF_DAY as f64 + ); + assert_eq!(date.days_since_j2000(), DAYS_PER_JULIAN_CENTURY - 0.5); + assert_eq!( + date.centuries_since_j2000(), + 1.0 - 0.5 / DAYS_PER_JULIAN_CENTURY + ); + assert_eq!( + date.centuries_since_j1950(), + 1.5 - 0.5 / DAYS_PER_JULIAN_CENTURY + ); assert_eq!( date.centuries_since_modified_julian_epoch(), - 2.411211498973306 + 2.411211498973306 - 0.5 / DAYS_PER_JULIAN_CENTURY + ); + assert_eq!( + date.centuries_since_julian_epoch(), + 68.11964407939767 - 0.5 / DAYS_PER_JULIAN_CENTURY ); - assert_eq!(date.centuries_since_julian_epoch(), 68.11964407939767); } } diff --git a/crates/lox-time/src/lib.rs b/crates/lox-time/src/lib.rs index 728c70a0..36a94490 100644 --- a/crates/lox-time/src/lib.rs +++ b/crates/lox-time/src/lib.rs @@ -114,7 +114,7 @@ impl Time { /// Instantiates a [Time] in the given `scale` from a [Date] and a [TimeOfDay]. pub fn from_date_and_time(scale: T, date: Date, time: TimeOfDay) -> Result { - let mut seconds = ((date.days_since_j2000() - 0.5) * time::SECONDS_PER_DAY) + let mut seconds = (date.days_since_j2000() * time::SECONDS_PER_DAY) .to_i64() .unwrap_or_else(|| { unreachable!( diff --git a/crates/lox-time/src/transformations.rs b/crates/lox-time/src/transformations.rs index 5ec445e4..3557bd6c 100644 --- a/crates/lox-time/src/transformations.rs +++ b/crates/lox-time/src/transformations.rs @@ -15,6 +15,7 @@ use crate::constants::julian_dates::J77; use crate::deltas::TimeDelta; use crate::subsecond::Subsecond; use crate::time_scales::{Tai, Tcb, Tcg, Tdb, TimeScale, Tt}; +use crate::utc::Utc; use crate::Time; /// TransformTimeScale transforms a [Time] in [TimeScale] `T` to the corresponding [Time] in @@ -196,6 +197,12 @@ fn delta_tdb_tt(time: Time) -> TimeDelta { }) } +pub trait LeapSecondsProvider { + fn delta_tai_utc(&self, tai: Time) -> Option; + + fn delta_utc_tai(&self, utc: Utc) -> Option; +} + #[cfg(test)] mod tests { use float_eq::assert_float_eq; diff --git a/crates/lox-time/src/utc.rs b/crates/lox-time/src/utc.rs index 3aae4a0c..81a7f48e 100644 --- a/crates/lox-time/src/utc.rs +++ b/crates/lox-time/src/utc.rs @@ -4,11 +4,11 @@ use num::ToPrimitive; use thiserror::Error; use crate::calendar_dates::{CalendarDate, Date, DateError}; -use crate::constants::i64::SECONDS_PER_HALF_DAY; use crate::deltas::TimeDelta; use crate::julian_dates::JulianDate; use crate::time_of_day::{CivilTime, TimeOfDay, TimeOfDayError}; +pub mod leap_seconds; pub mod transformations; #[derive(Debug, Clone, Error, PartialEq, Eq, PartialOrd, Ord)] @@ -58,8 +58,7 @@ impl Utc { self, self.date.seconds_since_j2000() ) - }) + self.time.second_of_day() - - SECONDS_PER_HALF_DAY; + }) + self.time.second_of_day(); TimeDelta { seconds, subsecond: self.time.subsecond(), diff --git a/crates/lox-time/src/utc/leap_seconds.rs b/crates/lox-time/src/utc/leap_seconds.rs new file mode 100644 index 00000000..7ef9da58 --- /dev/null +++ b/crates/lox-time/src/utc/leap_seconds.rs @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2024. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::calendar_dates::{Date, DateError}; +use crate::constants::i64::{SECONDS_PER_DAY, SECONDS_PER_HALF_DAY}; +use crate::deltas::TimeDelta; +use crate::prelude::{CivilTime, Tai}; +use crate::Time; +use lox_io::spice::{Kernel, KernelError}; +use std::fs::read_to_string; +use std::num::ParseIntError; +use std::path::Path; +use thiserror::Error; + +use crate::transformations::LeapSecondsProvider; +use crate::utc::Utc; + +const LEAP_SECONDS_KERNEL_KEY: &str = "DELTET/DELTA_AT"; + +const LEAP_SECOND_EPOCHS_UTC: [i64; 28] = [ + -883656000, -867931200, -852033600, -820497600, -788961600, -757425600, -725803200, -694267200, + -662731200, -631195200, -583934400, -552398400, -520862400, -457704000, -378734400, -315576000, + -284040000, -236779200, -205243200, -173707200, -126273600, -79012800, -31579200, 189345600, + 284040000, 394372800, 488980800, 536500800, +]; + +const LEAP_SECOND_EPOCHS_TAI: [i64; 28] = [ + -883655991, -867931190, -852033589, -820497588, -788961587, -757425586, -725803185, -694267184, + -662731183, -631195182, -583934381, -552398380, -520862379, -457703978, -378734377, -315575976, + -284039975, -236779174, -205243173, -173707172, -126273571, -79012770, -31579169, 189345632, + 284040033, 394372834, 488980835, 536500836, +]; + +const LEAP_SECONDS: [i64; 28] = [ + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, + 34, 35, 36, 37, +]; + +pub struct BuiltinLeapSeconds; + +impl LeapSecondsProvider for BuiltinLeapSeconds { + fn delta_tai_utc(&self, tai: Time) -> Option { + find_leap_seconds_tai(&LEAP_SECOND_EPOCHS_TAI, &LEAP_SECONDS, tai) + } + + fn delta_utc_tai(&self, utc: Utc) -> Option { + find_leap_seconds_utc(&LEAP_SECOND_EPOCHS_UTC, &LEAP_SECONDS, utc) + } +} + +#[derive(Debug, Error)] +pub enum LeapSecondsKernelError { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Kernel(#[from] KernelError), + #[error( + "no leap seconds found in kernel under key `{}`", + LEAP_SECONDS_KERNEL_KEY + )] + NoLeapSeconds, + #[error(transparent)] + ParseInt(#[from] ParseIntError), + #[error(transparent)] + DateError(#[from] DateError), +} + +pub struct LeapSecondsKernel { + epochs_utc: Vec, + epochs_tai: Vec, + leap_seconds: Vec, +} + +impl LeapSecondsKernel { + pub fn from_string(kernel: impl AsRef) -> Result { + let kernel = Kernel::from_string(kernel.as_ref())?; + let data = kernel + .get_timestamp_array(LEAP_SECONDS_KERNEL_KEY) + .ok_or(LeapSecondsKernelError::NoLeapSeconds)?; + let mut epochs_utc: Vec = Vec::with_capacity(data.len() / 2); + let mut epochs_tai: Vec = Vec::with_capacity(data.len() / 2); + let mut leap_seconds: Vec = Vec::with_capacity(data.len() / 2); + for chunk in data.chunks(2) { + let ls = chunk[0].parse::()?; + let date = Date::from_iso( + &chunk[1] + .replace("JAN", "01") + .replace("JUL", "07") + .replace("-1", "-01"), + )?; + let epoch = date.j2000_day_number() * SECONDS_PER_DAY - SECONDS_PER_HALF_DAY; + epochs_utc.push(epoch); + epochs_tai.push(epoch + ls - 1); + leap_seconds.push(ls); + } + Ok(Self { + epochs_utc, + epochs_tai, + leap_seconds, + }) + } + + pub fn from_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + let kernel = read_to_string(path)?; + Self::from_string(kernel) + } +} + +impl LeapSecondsProvider for LeapSecondsKernel { + fn delta_tai_utc(&self, tai: Time) -> Option { + find_leap_seconds_tai(&self.epochs_tai, &self.leap_seconds, tai) + } + + fn delta_utc_tai(&self, utc: Utc) -> Option { + find_leap_seconds_utc(&self.epochs_utc, &self.leap_seconds, utc) + } +} + +fn find_leap_seconds(epochs: &[i64], leap_seconds: &[i64], seconds: i64) -> Option { + if seconds < epochs[0] { + return None; + } + let idx = epochs.partition_point(|&epoch| epoch <= seconds) - 1; + let seconds = leap_seconds[idx]; + Some(TimeDelta::from_seconds(seconds)) +} + +fn find_leap_seconds_tai( + epochs: &[i64], + leap_seconds: &[i64], + tai: Time, +) -> Option { + find_leap_seconds(epochs, leap_seconds, tai.seconds()) +} + +fn find_leap_seconds_utc(epochs: &[i64], leap_seconds: &[i64], utc: Utc) -> Option { + find_leap_seconds(epochs, leap_seconds, utc.to_delta().seconds).map(|mut ls| { + if utc.second() == 60 { + ls.seconds -= 1; + } + -ls + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + use std::sync::OnceLock; + + use crate::deltas::TimeDelta; + use crate::time; + use crate::time_scales::Tai; + use crate::transformations::LeapSecondsProvider; + use crate::utc; + use crate::utc::Utc; + use crate::Time; + + #[rstest] + #[case::j2000(Time::default(), Utc::default(), 32)] + #[case::new_year_1972(time!(Tai, 1972, 1, 1, 0, 0, 10.0).unwrap(), utc!(1972, 1, 1).unwrap(), 10)] + #[case::new_year_2017(time!(Tai, 2017, 1, 1, 0, 0, 37.0).unwrap(), utc!(2017, 1, 1, 0, 0, 0.0).unwrap(), 37)] + #[case::new_year_2024(time!(Tai, 2024, 1, 1).unwrap(), utc!(2024, 1, 1).unwrap(), 37)] + fn test_builtin_leap_seconds(#[case] tai: Time, #[case] utc: Utc, #[case] expected: i64) { + let ls_tai = BuiltinLeapSeconds.delta_tai_utc(tai).unwrap(); + let ls_utc = BuiltinLeapSeconds.delta_utc_tai(utc).unwrap(); + assert_eq!(ls_tai, TimeDelta::from_seconds(expected)); + assert_eq!(ls_utc, TimeDelta::from_seconds(-expected)); + } + + #[rstest] + #[case::before_utc( + time!(Tai, 1971, 12, 31, 23, 59, 59.0).unwrap(), + utc!(1971, 12, 31, 23, 59, 59.0).unwrap(), + None, + )] + #[case::j2000(Time::default(), Utc::default(), Some(TimeDelta::from_seconds(32)))] + #[case::new_year_1972( + time!(Tai, 1972, 1, 1, 0, 0, 10.0).unwrap(), + utc!(1972, 1, 1).unwrap(), + Some(TimeDelta::from_seconds(10)), + )] + #[case::new_year_2017( + time!(Tai, 2017, 1, 1, 0, 0, 37.0).unwrap(), + utc!(2017, 1, 1, 0, 0, 0.0).unwrap(), + Some(TimeDelta::from_seconds(37)), + )] + #[case::new_year_2024( + time!(Tai, 2024, 1, 1).unwrap(), + utc!(2024, 1, 1).unwrap(), + Some(TimeDelta::from_seconds(37)), + )] + fn test_leap_seconds_kernel_leap_seconds( + #[case] tai: Time, + #[case] utc: Utc, + #[case] expected: Option, + ) { + let lsk = kernel(); + let ls_tai = lsk.delta_tai_utc(tai); + let ls_utc = lsk.delta_utc_tai(utc); + assert_eq!(ls_tai, expected); + assert_eq!(ls_utc, expected.map(|ls| -ls)); + } + + #[test] + fn test_leap_seconds_kernel() { + let lsk = kernel(); + assert_eq!(lsk.leap_seconds.len(), 28); + assert_eq!(lsk.epochs_utc.len(), 28); + assert_eq!(lsk.epochs_tai.len(), 28); + assert_eq!(lsk.leap_seconds, &LEAP_SECONDS); + assert_eq!(lsk.epochs_utc, &LEAP_SECOND_EPOCHS_UTC); + assert_eq!(lsk.epochs_tai, &LEAP_SECOND_EPOCHS_TAI); + } + + const KERNEL: &str = "KPL/LSK + +\\begindata + +DELTET/DELTA_AT = ( 10, @1972-JAN-1 + 11, @1972-JUL-1 + 12, @1973-JAN-1 + 13, @1974-JAN-1 + 14, @1975-JAN-1 + 15, @1976-JAN-1 + 16, @1977-JAN-1 + 17, @1978-JAN-1 + 18, @1979-JAN-1 + 19, @1980-JAN-1 + 20, @1981-JUL-1 + 21, @1982-JUL-1 + 22, @1983-JUL-1 + 23, @1985-JUL-1 + 24, @1988-JAN-1 + 25, @1990-JAN-1 + 26, @1991-JAN-1 + 27, @1992-JUL-1 + 28, @1993-JUL-1 + 29, @1994-JUL-1 + 30, @1996-JAN-1 + 31, @1997-JUL-1 + 32, @1999-JAN-1 + 33, @2006-JAN-1 + 34, @2009-JAN-1 + 35, @2012-JUL-1 + 36, @2015-JUL-1 + 37, @2017-JAN-1 ) + +\\begintext"; + + fn kernel() -> &'static LeapSecondsKernel { + static LSK: OnceLock = OnceLock::new(); + LSK.get_or_init(|| LeapSecondsKernel::from_string(KERNEL).expect("file should be parsable")) + } +} diff --git a/crates/lox-time/src/utc/transformations/from1972.rs b/crates/lox-time/src/utc/transformations/from1972.rs index c742f07d..6f28a012 100644 --- a/crates/lox-time/src/utc/transformations/from1972.rs +++ b/crates/lox-time/src/utc/transformations/from1972.rs @@ -19,8 +19,9 @@ use lox_utils::slices::is_sorted_asc; use crate::calendar_dates::Date; use crate::deltas::TimeDelta; use crate::julian_dates::JulianDate; -use crate::time_of_day::CivilTime; use crate::time_scales::Tai; +use crate::transformations::LeapSecondsProvider; +use crate::utc::leap_seconds::BuiltinLeapSeconds; use crate::utc::Utc; use crate::Time; @@ -33,7 +34,7 @@ const MJD_LEAP_SECOND_EPOCHS: [u64; 28] = [ impl Date { pub fn is_leap_second_date(&self) -> bool { - let mjd = (self.days_since_modified_julian_epoch().ceil()).to_u64(); + let mjd = (self.days_since_modified_julian_epoch() + 1.0).to_u64(); if let Some(mjd) = mjd { return MJD_LEAP_SECOND_EPOCHS.contains(&mjd); } @@ -82,39 +83,12 @@ pub const LEAP_SECONDS: [i64; 28] = [ /// For dates from 1972-01-01, returns a [TimeDelta] representing the count of leap seconds between /// TAI and UTC. Returns `None` for dates before 1972. pub fn delta_tai_utc(tai: Time) -> Option { - j2000_tai_leap_second_epochs() - .iter() - .rev() - .zip(LEAP_SECONDS.iter().rev()) - .find_map(|(&epoch, &leap_seconds)| { - if epoch <= tai.seconds() { - Some(TimeDelta::from_seconds(leap_seconds)) - } else { - None - } - }) + BuiltinLeapSeconds.delta_tai_utc(tai) } /// UTC minus TAI. Calculates the correct leap second count for dates after 1972 by simple lookup. pub fn delta_utc_tai(utc: Utc) -> Option { - let base_time = utc.to_delta(); - j2000_utc_leap_second_epochs() - .iter() - .rev() - .zip(LEAP_SECONDS.iter().rev()) - .find_map(|(&epoch, &leap_seconds)| { - if epoch <= base_time.seconds { - Some(TimeDelta::from_seconds(leap_seconds)) - } else { - None - } - }) - .map(|mut delta| { - if utc.second() == 60 { - delta.seconds -= 1; - } - -delta - }) + BuiltinLeapSeconds.delta_utc_tai(utc) } impl Time { diff --git a/crates/lox-time/tests/dates.rs b/crates/lox-time/tests/dates.rs index 83636fb5..e4199c2c 100644 --- a/crates/lox-time/tests/dates.rs +++ b/crates/lox-time/tests/dates.rs @@ -6,7 +6,6 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ -use num::ToPrimitive; use rstest::rstest; use lox_time::calendar_dates::Date; @@ -36,10 +35,8 @@ use lox_time::calendar_dates::Date; #[case(2000, 2, 29, 59)] #[case(2000, 3, 1, 60)] fn test_dates(#[case] year: i64, #[case] month: u8, #[case] day: u8, #[case] exp: i64) { - use lox_time::julian_dates::JulianDate; - let date = Date::new(year, month, day).expect("date should be valid"); - assert_eq!(exp, date.days_since_j2000().to_i64().unwrap()); + assert_eq!(exp, date.j2000_day_number()); } #[test] diff --git a/crates/lox-time/tests/lsk.rs b/crates/lox-time/tests/lsk.rs new file mode 100644 index 00000000..e0976e82 --- /dev/null +++ b/crates/lox-time/tests/lsk.rs @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use lox_time::utc::leap_seconds::LeapSecondsKernel; + +#[test] +fn test_lsk_from_file() { + assert!(LeapSecondsKernel::from_file("../../data/naif0012.tls").is_ok()); +} diff --git a/data/naif0012.tls b/data/naif0012.tls new file mode 100644 index 00000000..e1afdee1 --- /dev/null +++ b/data/naif0012.tls @@ -0,0 +1,152 @@ +KPL/LSK + + +LEAPSECONDS KERNEL FILE +=========================================================================== + +Modifications: +-------------- + +2016, Jul. 14 NJB Modified file to account for the leapsecond that + will occur on December 31, 2016. + +2015, Jan. 5 NJB Modified file to account for the leapsecond that + will occur on June 30, 2015. + +2012, Jan. 5 NJB Modified file to account for the leapsecond that + will occur on June 30, 2012. + +2008, Jul. 7 NJB Modified file to account for the leapsecond that + will occur on December 31, 2008. + +2005, Aug. 3 NJB Modified file to account for the leapsecond that + will occur on December 31, 2005. + +1998, Jul 17 WLT Modified file to account for the leapsecond that + will occur on December 31, 1998. + +1997, Feb 22 WLT Modified file to account for the leapsecond that + will occur on June 30, 1997. + +1995, Dec 14 KSZ Corrected date of last leapsecond from 1-1-95 + to 1-1-96. + +1995, Oct 25 WLT Modified file to account for the leapsecond that + will occur on Dec 31, 1995. + +1994, Jun 16 WLT Modified file to account for the leapsecond on + June 30, 1994. + +1993, Feb. 22 CHA Modified file to account for the leapsecond on + June 30, 1993. + +1992, Mar. 6 HAN Modified file to account for the leapsecond on + June 30, 1992. + +1990, Oct. 8 HAN Modified file to account for the leapsecond on + Dec. 31, 1990. + + +Explanation: +------------ + +The contents of this file are used by the routine DELTET to compute the +time difference + +[1] DELTA_ET = ET - UTC + +the increment to be applied to UTC to give ET. + +The difference between UTC and TAI, + +[2] DELTA_AT = TAI - UTC + +is always an integral number of seconds. The value of DELTA_AT was 10 +seconds in January 1972, and increases by one each time a leap second +is declared. Combining [1] and [2] gives + +[3] DELTA_ET = ET - (TAI - DELTA_AT) + + = (ET - TAI) + DELTA_AT + +The difference (ET - TAI) is periodic, and is given by + +[4] ET - TAI = DELTA_T_A + K sin E + +where DELTA_T_A and K are constant, and E is the eccentric anomaly of the +heliocentric orbit of the Earth-Moon barycenter. Equation [4], which ignores +small-period fluctuations, is accurate to about 0.000030 seconds. + +The eccentric anomaly E is given by + +[5] E = M + EB sin M + +where M is the mean anomaly, which in turn is given by + +[6] M = M + M t + 0 1 + +where t is the number of ephemeris seconds past J2000. + +Thus, in order to compute DELTA_ET, the following items are necessary. + + DELTA_TA + K + EB + M0 + M1 + DELTA_AT after each leap second. + +The numbers, and the formulation, are taken from the following sources. + + 1) Moyer, T.D., Transformation from Proper Time on Earth to + Coordinate Time in Solar System Barycentric Space-Time Frame + of Reference, Parts 1 and 2, Celestial Mechanics 23 (1981), + 33-56 and 57-68. + + 2) Moyer, T.D., Effects of Conversion to the J2000 Astronomical + Reference System on Algorithms for Computing Time Differences + and Clock Rates, JPL IOM 314.5--942, 1 October 1985. + +The variable names used above are consistent with those used in the +Astronomical Almanac. + +\begindata + +DELTET/DELTA_T_A = 32.184 +DELTET/K = 1.657D-3 +DELTET/EB = 1.671D-2 +DELTET/M = ( 6.239996D0 1.99096871D-7 ) + +DELTET/DELTA_AT = ( 10, @1972-JAN-1 + 11, @1972-JUL-1 + 12, @1973-JAN-1 + 13, @1974-JAN-1 + 14, @1975-JAN-1 + 15, @1976-JAN-1 + 16, @1977-JAN-1 + 17, @1978-JAN-1 + 18, @1979-JAN-1 + 19, @1980-JAN-1 + 20, @1981-JUL-1 + 21, @1982-JUL-1 + 22, @1983-JUL-1 + 23, @1985-JUL-1 + 24, @1988-JAN-1 + 25, @1990-JAN-1 + 26, @1991-JAN-1 + 27, @1992-JUL-1 + 28, @1993-JUL-1 + 29, @1994-JUL-1 + 30, @1996-JAN-1 + 31, @1997-JUL-1 + 32, @1999-JAN-1 + 33, @2006-JAN-1 + 34, @2009-JAN-1 + 35, @2012-JUL-1 + 36, @2015-JUL-1 + 37, @2017-JAN-1 ) + +\begintext + + diff --git a/tools/lox-gen/Cargo.lock b/tools/lox-gen/Cargo.lock index 4759be5a..6dc1d2e8 100644 --- a/tools/lox-gen/Cargo.lock +++ b/tools/lox-gen/Cargo.lock @@ -24,6 +24,7 @@ name = "lox-io" version = "0.0.0" dependencies = [ "nom", + "thiserror", ] [[package]]