From c8648ef25f2cdb108bee0663c9a08c8f5f738c37 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 27 May 2023 10:18:16 +0200 Subject: [PATCH 01/37] epoch::from_duration slight improvements Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 185 +++++++++++++++++++++-------------------------- src/timescale.rs | 11 ++- tests/epoch.rs | 19 +++-- 3 files changed, 102 insertions(+), 113 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 20aa3fee..45279ab2 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -263,22 +263,77 @@ impl Epoch { None } - /// Makes a copy of self and sets the duration and time scale appropriately given the new duration - #[must_use] - pub fn from_duration(new_duration: Duration, time_scale: TimeScale) -> Self { - match time_scale { - TimeScale::TAI => Self::from_tai_duration(new_duration), - TimeScale::TT => Self::from_tt_duration(new_duration), - TimeScale::ET => Self::from_et_duration(new_duration), - TimeScale::TDB => Self::from_tdb_duration(new_duration), - TimeScale::UTC => Self::from_utc_duration(new_duration), - TimeScale::GPST => Self::from_gpst_duration(new_duration), - TimeScale::QZSST => Self::from_qzsst_duration(new_duration), - TimeScale::GST => Self::from_gst_duration(new_duration), - TimeScale::BDT => Self::from_bdt_duration(new_duration), + /// Creates an epoch from given duration expressed in given timescale. + /// In case of ET, TDB Timescales, a duration since J2000 is expected. + #[must_use] + pub fn from_duration(new_duration: Duration, ts: TimeScale) -> Self { + match ts { + TimeScale::TAI => Self { + duration_since_j1900_tai: new_duration, + time_scale: TimeScale::TAI, + }, + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds_j2000 = new_duration.to_seconds(); + for _ in 0..5 { + seconds_j2000 += -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds_j2000 + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) + .sin(); + } + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); + // Match SPICE by changing the UTC definition. + Self { + duration_since_j1900_tai: (new_duration.to_seconds() - delta_et_tai) + * Unit::Second + + J2000_TO_J1900_DURATION, + time_scale: TimeScale::ET, + } + } + TimeScale::TDB => { + let gamma = Self::inner_g(new_duration.to_seconds()); + let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + // Offset back to J1900. + Self { + duration_since_j1900_tai: new_duration - delta_tdb_tai + + J2000_TO_J1900_DURATION, + time_scale: TimeScale::TDB, + } + } + ts => { + let mut tai_epoch = ts.tai_reference_epoch(); + tai_epoch += new_duration; + match ts { + TimeScale::TT => { + tai_epoch -= TT_OFFSET_MS * Unit::Millisecond; + } + _ => {} + } + // leap second management + if ts.uses_leap_seconds() { + tai_epoch += tai_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + } + tai_epoch.with_timescale(ts) + } } } + /// Assigns given TimeScale to self. + /// This is not a conversion operation, but + /// simply a definition. + /// To convert into a timescale, + /// you should use .into(TimeScale) + pub fn with_timescale(&self, ts: TimeScale) -> Self { + let mut s = self.clone(); + s.time_scale = ts; + s + } + #[must_use] /// Creates a new Epoch from a Duration as the time difference between this epoch and TAI reference epoch. pub const fn from_tai_duration(duration: Duration) -> Self { @@ -317,14 +372,7 @@ impl Epoch { #[must_use] /// Initialize an Epoch from the provided UTC seconds since 1900 January 01 at midnight pub fn from_utc_duration(duration: Duration) -> Self { - let mut e = Self::from_tai_duration(duration); - // Compute the TAI to UTC offset at this time. - // We have the time in TAI. But we were given UTC. - // Hence, we need to _add_ the leap seconds to get the actual TAI time. - // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - e.duration_since_j1900_tai += e.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - e.time_scale = TimeScale::UTC; - e + Self::from_duration(duration, TimeScale::UTC) } #[must_use] @@ -342,34 +390,25 @@ impl Epoch { #[must_use] /// Initialize an Epoch from the provided duration since 1980 January 6 at midnight pub fn from_gpst_duration(duration: Duration) -> Self { - let mut me = Self::from_tai_duration(GPST_REF_EPOCH.to_tai_duration() + duration); - me.time_scale = TimeScale::GPST; - me + Self::from_duration(duration, TimeScale::GPST) } #[must_use] /// Initialize an Epoch from the provided duration since 1980 January 6 at midnight pub fn from_qzsst_duration(duration: Duration) -> Self { - // QZSST and GPST share the same reference epoch - let mut me = Self::from_tai_duration(GPST_REF_EPOCH.to_tai_duration() + duration); - me.time_scale = TimeScale::QZSST; - me + Self::from_duration(duration, TimeScale::QZSST) } #[must_use] /// Initialize an Epoch from the provided duration since August 21st 1999 midnight pub fn from_gst_duration(duration: Duration) -> Self { - let mut me = Self::from_tai_duration(GST_REF_EPOCH.to_tai_duration() + duration); - me.time_scale = TimeScale::GST; - me + Self::from_duration(duration, TimeScale::GST) } #[must_use] /// Initialize an Epoch from the provided duration since January 1st midnight pub fn from_bdt_duration(duration: Duration) -> Self { - let mut me = Self::from_tai_duration(BDT_REF_EPOCH.to_tai_duration() + duration); - me.time_scale = TimeScale::BDT; - me + Self::from_duration(duration, TimeScale::BDT) } #[must_use] @@ -465,10 +504,7 @@ impl Epoch { #[must_use] /// Initialize an Epoch from the provided TT seconds (approximated to 32.184s delta from TAI) pub fn from_tt_duration(duration: Duration) -> Self { - Self { - duration_since_j1900_tai: duration - Unit::Millisecond * TT_OFFSET_MS, - time_scale: TimeScale::TT, - } + Self::from_duration(duration, TimeScale::TT) } #[must_use] @@ -491,28 +527,7 @@ impl Epoch { /// In order to match SPICE, the as_et_duration() function will manually get rid of that difference. #[must_use] pub fn from_et_duration(duration_since_j2000: Duration) -> Self { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds_j2000 = duration_since_j2000.to_seconds(); - for _ in 0..5 { - seconds_j2000 += -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds_j2000 - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) - .sin(); - } - - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = - Self::delta_et_tai(seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - - // Match SPICE by changing the UTC definition. - Self { - duration_since_j1900_tai: (duration_since_j2000.to_seconds() - delta_et_tai) - * Unit::Second - + J2000_TO_J1900_DURATION, - time_scale: TimeScale::ET, - } + Self::from_duration(duration_since_j2000, TimeScale::ET) } #[must_use] @@ -530,16 +545,7 @@ impl Epoch { #[must_use] /// Initialize from Dynamic Barycentric Time (TDB) (same as SPICE ephemeris time) whose epoch is 2000 JAN 01 noon TAI. pub fn from_tdb_duration(duration_since_j2000: Duration) -> Epoch { - let gamma = Self::inner_g(duration_since_j2000.to_seconds()); - - let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - - // Offset back to J1900. - Self { - duration_since_j1900_tai: duration_since_j2000 - delta_tdb_tai - + J2000_TO_J1900_DURATION, - time_scale: TimeScale::TDB, - } + Self::from_duration(duration_since_j2000, TimeScale::TDB) } #[must_use] @@ -581,13 +587,7 @@ impl Epoch { /// defined as UTC midnight of January 5th to 6th 1980 (cf. ). /// This may be useful for time keeping devices that use GPS as a time source. pub fn from_gpst_nanoseconds(nanoseconds: u64) -> Self { - Self::from_duration( - Duration { - centuries: 0, - nanoseconds, - }, - TimeScale::GPST, - ) + Self::from_duration(nanoseconds as f64 * Unit::Nanosecond, TimeScale::GPST) } #[must_use] @@ -667,13 +667,7 @@ impl Epoch { /// starting on January 1st 2006 (cf. ). /// This may be useful for time keeping devices that use BDT as a time source. pub fn from_bdt_nanoseconds(nanoseconds: u64) -> Self { - Self::from_duration( - Duration { - centuries: 0, - nanoseconds, - }, - TimeScale::BDT, - ) + Self::from_duration(nanoseconds as f64 * Unit::Nanosecond, TimeScale::BDT) } #[must_use] @@ -769,24 +763,12 @@ impl Epoch { // NOTE: For ET and TDB, we make sure to offset the duration back to J2000 since those functions expect a J2000 input. Ok(match time_scale { - TimeScale::TAI => Self::from_tai_duration(duration_wrt_1900), - TimeScale::TT => Self::from_tt_duration(duration_wrt_1900), TimeScale::ET => Self::from_et_duration(duration_wrt_1900 - J2000_TO_J1900_DURATION), TimeScale::TDB => Self::from_tdb_duration(duration_wrt_1900 - J2000_TO_J1900_DURATION), - TimeScale::UTC => Self::from_utc_duration(duration_wrt_1900), - TimeScale::GPST => { - Self::from_gpst_duration(duration_wrt_1900 - GPST_REF_EPOCH.to_tai_duration()) - } - // QZSS and GPST share the same reference epoch - TimeScale::QZSST => { - Self::from_qzsst_duration(duration_wrt_1900 - GPST_REF_EPOCH.to_tai_duration()) - } - TimeScale::GST => { - Self::from_gst_duration(duration_wrt_1900 - GST_REF_EPOCH.to_tai_duration()) - } - TimeScale::BDT => { - Self::from_bdt_duration(duration_wrt_1900 - BDT_REF_EPOCH.to_tai_duration()) - } + ts => Self::from_duration( + duration_wrt_1900 - ts.tai_reference_epoch().to_tai_duration(), + ts, + ), }) } @@ -3344,7 +3326,7 @@ fn formal_epoch_reciprocity_tdb() { let duration = Duration::from_parts(19510, 3155759999999997938); // TDB - let ts_offset = TimeScale::TDB.ref_epoch() - TimeScale::TAI.ref_epoch(); + let ts_offset = TimeScale::TDB.tai_reference_epoch() - TimeScale::TAI.tai_reference_epoch(); if duration > Duration::MIN + ts_offset && duration < Duration::MAX - ts_offset { // We guard TDB from durations that are would hit the MIN or the MAX. // TDB is centered on J2000 but the Epoch is on J1900. So on initialization, we offset by one century and twelve hours. @@ -3367,6 +3349,7 @@ fn formal_epoch_reciprocity_tdb() { #[cfg(kani)] #[kani::proof] +#[test] fn formal_epoch_reciprocity_gpst() { let duration: Duration = kani::any(); diff --git a/src/timescale.rs b/src/timescale.rs index 22ab3a82..56d77a00 100644 --- a/src/timescale.rs +++ b/src/timescale.rs @@ -40,6 +40,9 @@ pub const SECONDS_GPS_TAI_OFFSET: f64 = 2_524_953_619.0; pub const SECONDS_GPS_TAI_OFFSET_I64: i64 = 2_524_953_619; pub const DAYS_GPS_TAI_OFFSET: f64 = SECONDS_GPS_TAI_OFFSET / SECONDS_PER_DAY; +/// QZSS and GPS share the same reference epoch +pub const QZSST_REF_EPOCH: Epoch = GPST_REF_EPOCH; + /// GST (Galileo) reference epoch is 13 seconds before 1999 August 21 UTC at midnight. pub const GST_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { centuries: 0, @@ -126,18 +129,18 @@ impl TimeScale { matches!(self, Self::GPST | Self::GST | Self::BDT | Self::QZSST) } - /// Returns Reference Epoch (t(0)) for given timescale - pub const fn ref_epoch(&self) -> Epoch { + /// Returns Self's Reference Epoch: Time Scale initialization date, + /// expressed as an Epoch in TAI + pub const fn tai_reference_epoch(&self) -> Epoch { match self { Self::GPST => GPST_REF_EPOCH, Self::GST => GST_REF_EPOCH, Self::BDT => BDT_REF_EPOCH, Self::ET => J2000_REF_EPOCH_ET, Self::TDB => J2000_REF_EPOCH_TDB, + Self::QZSST => QZSST_REF_EPOCH, // Explicit on purpose in case more time scales end up being supported. Self::TT | Self::TAI | Self::UTC => J1900_REF_EPOCH, - // QZSS time shares the same starting point as GPST - Self::QZSST => GPST_REF_EPOCH, } } } diff --git a/tests/epoch.rs b/tests/epoch.rs index b6ce55ae..163cadd7 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1243,7 +1243,7 @@ fn test_timescale_recip() { assert_eq!(utc_epoch, utc_epoch.set(utc_epoch.to_duration())); // Test that we can convert this epoch into another time scale and re-initialize it correctly from that value. for ts in &[ - // TimeScale::TAI, + //TimeScale::TAI, TimeScale::ET, TimeScale::TDB, TimeScale::TT, @@ -1596,7 +1596,8 @@ fn test_time_of_week() { let epoch_qzsst = epoch.in_time_scale(TimeScale::QZSST); assert_eq!(epoch.to_gregorian_utc(), epoch_qzsst.to_gregorian_utc()); - let gps_qzss_offset = TimeScale::GPST.ref_epoch() - TimeScale::QZSST.ref_epoch(); + let gps_qzss_offset = + TimeScale::GPST.tai_reference_epoch() - TimeScale::QZSST.tai_reference_epoch(); assert_eq!(gps_qzss_offset.total_nanoseconds(), 0); // no offset // 06/01/1980 01:00:00 = 1H into GPST <=> (0, 3_618_000_000_000) @@ -1670,7 +1671,7 @@ fn test_time_of_week() { // 1H into Galileo timescale let epoch = Epoch::from_time_of_week(0, 3_600_000_000_000, TimeScale::GST); - let expected_tai = TimeScale::GST.ref_epoch() + Duration::from_hours(1.0); + let expected_tai = TimeScale::GST.tai_reference_epoch() + Duration::from_hours(1.0); assert_eq!(epoch.to_gregorian_utc(), expected_tai.to_gregorian_utc()); assert_eq!(epoch.to_time_of_week(), (0, 3_600_000_000_000)); @@ -1684,8 +1685,9 @@ fn test_time_of_week() { // 1W + 128H into Galileo timescale let epoch = Epoch::from_time_of_week(1, 128 * 3600 * 1_000_000_000, TimeScale::GST); - let expected_tai = - TimeScale::GST.ref_epoch() + Duration::from_days(7.0) + Duration::from_hours(128.0); + let expected_tai = TimeScale::GST.tai_reference_epoch() + + Duration::from_days(7.0) + + Duration::from_hours(128.0); assert_eq!(epoch.to_gregorian_utc(), expected_tai.to_gregorian_utc()); assert_eq!(epoch.to_time_of_week(), (1, 128 * 3600 * 1_000_000_000)); @@ -1703,7 +1705,7 @@ fn test_time_of_week() { 13 * 3600 * 1_000_000_000 + 1800 * 1_000_000_000, TimeScale::BDT, ); - let expected_tai = TimeScale::BDT.ref_epoch() + Duration::from_hours(13.5); + let expected_tai = TimeScale::BDT.tai_reference_epoch() + Duration::from_hours(13.5); assert_eq!(epoch.to_gregorian_utc(), expected_tai.to_gregorian_utc()); assert_eq!( epoch.to_time_of_week(), @@ -1724,8 +1726,9 @@ fn test_time_of_week() { 36 * 3600 * 1_000_000_000 + 900 * 1_000_000_000, TimeScale::BDT, ); - let expected_tai = - TimeScale::BDT.ref_epoch() + Duration::from_days(70.0) + Duration::from_hours(36.25); + let expected_tai = TimeScale::BDT.tai_reference_epoch() + + Duration::from_days(70.0) + + Duration::from_hours(36.25); assert_eq!(epoch.to_gregorian_utc(), expected_tai.to_gregorian_utc()); assert_eq!( epoch.to_time_of_week(), From 0062d93f5b80eec9f5f38a158c3ccc8169d2a7d2 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 28 May 2023 11:30:15 +0200 Subject: [PATCH 02/37] rename epoch inner duration Signed-off-by: Guillaume W. Bres --- src/asn1der.rs | 2 +- src/epoch.rs | 76 +++++++++++++++++++++++++------------------------- src/lib.rs | 4 +-- tests/epoch.rs | 24 ++++++++-------- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/asn1der.rs b/src/asn1der.rs index 0e3e7731..43fe14a6 100644 --- a/src/asn1der.rs +++ b/src/asn1der.rs @@ -126,7 +126,7 @@ fn test_encdec() { let encdec_epoch = Epoch::from_der(&buf).unwrap(); // Check that the duration in J1900 TAI is the same assert_eq!( - encdec_epoch.duration_since_j1900_tai, epoch.duration_since_j1900_tai, + encdec_epoch.duration, epoch.duration, "Decoded epoch incorrect ({ts:?}):\ngot: {encdec_epoch}\nexp: {epoch}", ); // Check that the time scale used is preserved diff --git a/src/epoch.rs b/src/epoch.rs index 45279ab2..8e5b00dd 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -128,8 +128,8 @@ const CUMULATIVE_DAYS_FOR_MONTH: [u16; 12] = { #[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Epoch { - /// An Epoch is always stored as the duration of since J1900 in the TAI time scale. - pub duration_since_j1900_tai: Duration, + /// An Epoch is always stored as the duration since the beginning of its time scale + pub duration: Duration, /// Time scale used during the initialization of this Epoch. pub time_scale: TimeScale, } @@ -138,7 +138,7 @@ impl Sub for Epoch { type Output = Duration; fn sub(self, other: Self) -> Duration { - self.duration_since_j1900_tai - other.duration_since_j1900_tai + self.duration - other.duration } } @@ -215,29 +215,29 @@ impl AddAssign for Epoch { /// Equality only checks the duration since J1900 match in TAI, because this is how all of the epochs are referenced. impl PartialEq for Epoch { fn eq(&self, other: &Self) -> bool { - self.duration_since_j1900_tai == other.duration_since_j1900_tai + self.duration == other.duration } } impl Hash for Epoch { fn hash(&self, hasher: &mut H) { - self.duration_since_j1900_tai.hash(hasher); + self.duration.hash(hasher); } } impl PartialOrd for Epoch { fn partial_cmp(&self, other: &Self) -> Option { Some( - self.duration_since_j1900_tai - .cmp(&other.duration_since_j1900_tai), + self.duration + .cmp(&other.duration), ) } } impl Ord for Epoch { fn cmp(&self, other: &Self) -> Ordering { - self.duration_since_j1900_tai - .cmp(&other.duration_since_j1900_tai) + self.duration + .cmp(&other.duration) } } @@ -254,7 +254,7 @@ impl Epoch { provider: L, ) -> Option { for leap_second in provider.rev() { - if self.duration_since_j1900_tai.to_seconds() >= leap_second.timestamp_tai_s + if self.duration.to_seconds() >= leap_second.timestamp_tai_s && (!iers_only || leap_second.announced_by_iers) { return Some(leap_second.delta_at); @@ -269,7 +269,7 @@ impl Epoch { pub fn from_duration(new_duration: Duration, ts: TimeScale) -> Self { match ts { TimeScale::TAI => Self { - duration_since_j1900_tai: new_duration, + duration: new_duration, time_scale: TimeScale::TAI, }, TimeScale::ET => { @@ -289,7 +289,7 @@ impl Epoch { ); // Match SPICE by changing the UTC definition. Self { - duration_since_j1900_tai: (new_duration.to_seconds() - delta_et_tai) + duration: (new_duration.to_seconds() - delta_et_tai) * Unit::Second + J2000_TO_J1900_DURATION, time_scale: TimeScale::ET, @@ -300,7 +300,7 @@ impl Epoch { let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; // Offset back to J1900. Self { - duration_since_j1900_tai: new_duration - delta_tdb_tai + duration: new_duration - delta_tdb_tai + J2000_TO_J1900_DURATION, time_scale: TimeScale::TDB, } @@ -338,7 +338,7 @@ impl Epoch { /// Creates a new Epoch from a Duration as the time difference between this epoch and TAI reference epoch. pub const fn from_tai_duration(duration: Duration) -> Self { Self { - duration_since_j1900_tai: duration, + duration: duration, time_scale: TimeScale::TAI, } } @@ -424,7 +424,7 @@ impl Epoch { // always refer to TAI/mjd let mut e = Self::from_mjd_tai(days); if time_scale.uses_leap_seconds() { - e.duration_since_j1900_tai += e.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + e.duration += e.leap_seconds(true).unwrap_or(0.0) * Unit::Second; } e.time_scale = time_scale; e @@ -464,7 +464,7 @@ impl Epoch { // always refer to TAI/jde let mut e = Self::from_jde_tai(days); if time_scale.uses_leap_seconds() { - e.duration_since_j1900_tai += e.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + e.duration += e.leap_seconds(true).unwrap_or(0.0) * Unit::Second; } e.time_scale = time_scale; e @@ -832,7 +832,7 @@ impl Epoch { // We have the time in TAI. But we were given UTC. // Hence, we need to _add_ the leap seconds to get the actual TAI time. // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - if_tai.duration_since_j1900_tai += if_tai.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + if_tai.duration += if_tai.leap_seconds(true).unwrap_or(0.0) * Unit::Second; if_tai.time_scale = TimeScale::UTC; Ok(if_tai) } @@ -1095,7 +1095,7 @@ impl Epoch { // Compute the TAI to UT1 offset at this time. // We have the time in TAI. But we were given UT1. // The offset is provided as offset = TAI - UT1 <=> TAI = UT1 + offset - e.duration_since_j1900_tai += e.ut1_offset(provider).unwrap_or(Duration::ZERO); + e.duration += e.ut1_offset(provider).unwrap_or(Duration::ZERO); e.time_scale = TimeScale::TAI; e } @@ -1764,7 +1764,7 @@ impl Epoch { /// This is needed to correctly perform duration conversions in dynamical time scales (e.g. TDB). pub fn to_duration_in_time_scale(&self, time_scale: TimeScale) -> Duration { match time_scale { - TimeScale::TAI => self.duration_since_j1900_tai, + TimeScale::TAI => self.duration, TimeScale::TT => self.to_tt_duration(), TimeScale::ET => self.to_et_duration(), TimeScale::TDB => self.to_tdb_duration(), @@ -1801,7 +1801,7 @@ impl Epoch { pub fn to_duration_since_j1900_in_time_scale(&self, time_scale: TimeScale) -> Duration { match time_scale { TimeScale::ET => self.to_et_duration_since_j1900(), - TimeScale::TAI => self.duration_since_j1900_tai, + TimeScale::TAI => self.duration, TimeScale::TT => self.to_tt_duration(), TimeScale::TDB => self.to_tdb_duration_since_j1900(), TimeScale::UTC => self.to_utc_duration(), @@ -1833,25 +1833,25 @@ impl Epoch { #[must_use] /// Returns the number of TAI seconds since J1900 pub fn to_tai_seconds(&self) -> f64 { - self.duration_since_j1900_tai.to_seconds() + self.duration.to_seconds() } #[must_use] /// Returns this time in a Duration past J1900 counted in TAI pub const fn to_tai_duration(&self) -> Duration { - self.duration_since_j1900_tai + self.duration } #[must_use] /// Returns the epoch as a floating point value in the provided unit pub fn to_tai(&self, unit: Unit) -> f64 { - self.duration_since_j1900_tai.to_unit(unit) + self.duration.to_unit(unit) } #[must_use] /// Returns the TAI parts of this duration pub const fn to_tai_parts(&self) -> (i16, u64) { - self.duration_since_j1900_tai.to_parts() + self.duration.to_parts() } #[must_use] @@ -1870,7 +1870,7 @@ impl Epoch { /// Returns this time in a Duration past J1900 counted in UTC pub fn to_utc_duration(&self) -> Duration { // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - self.duration_since_j1900_tai - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second + self.duration - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second } #[must_use] @@ -1901,7 +1901,7 @@ impl Epoch { #[must_use] /// Returns this epoch as a duration in the requested units in MJD TAI pub fn to_mjd_tai(&self, unit: Unit) -> f64 { - (self.duration_since_j1900_tai + Unit::Day * J1900_OFFSET).to_unit(unit) + (self.duration + Unit::Day * J1900_OFFSET).to_unit(unit) } #[must_use] @@ -1939,7 +1939,7 @@ impl Epoch { #[must_use] /// Returns the Julian Days from epoch 01 Jan -4713 12:00 (noon) as a Duration pub fn to_jde_tai_duration(&self) -> Duration { - self.duration_since_j1900_tai + Unit::Day * J1900_OFFSET + Unit::Day * MJD_OFFSET + self.duration + Unit::Day * J1900_OFFSET + Unit::Day * MJD_OFFSET } #[must_use] @@ -1975,7 +1975,7 @@ impl Epoch { #[must_use] /// Returns `Duration` past TAI epoch in Terrestrial Time (TT). pub fn to_tt_duration(&self) -> Duration { - self.duration_since_j1900_tai + Unit::Millisecond * TT_OFFSET_MS + self.duration + Unit::Millisecond * TT_OFFSET_MS } #[must_use] @@ -2027,7 +2027,7 @@ impl Epoch { #[must_use] /// Returns `Duration` past GPS time Epoch. pub fn to_gpst_duration(&self) -> Duration { - self.duration_since_j1900_tai - GPST_REF_EPOCH.to_tai_duration() + self.duration - GPST_REF_EPOCH.to_tai_duration() } /// Returns nanoseconds past GPS Time Epoch, defined as UTC midnight of January 5th to 6th 1980 (cf. ). @@ -2052,7 +2052,7 @@ impl Epoch { /// Returns `Duration` past QZSS time Epoch. pub fn to_qzsst_duration(&self) -> Duration { // GPST and QZSST share the same reference epoch - self.duration_since_j1900_tai - GPST_REF_EPOCH.to_tai_duration() + self.duration - GPST_REF_EPOCH.to_tai_duration() } /// Returns nanoseconds past QZSS Time Epoch, defined as UTC midnight of January 5th to 6th 1980 (cf. ). @@ -2076,7 +2076,7 @@ impl Epoch { #[must_use] /// Returns `Duration` past GST (Galileo) time Epoch. pub fn to_gst_duration(&self) -> Duration { - self.duration_since_j1900_tai - GST_REF_EPOCH.to_tai_duration() + self.duration - GST_REF_EPOCH.to_tai_duration() } /// Returns nanoseconds past GST (Galileo) Time Epoch, starting on August 21st 1999 Midnight UT @@ -2175,7 +2175,7 @@ impl Epoch { /// In order to match SPICE, the as_et_duration() function will manually get rid of that difference. pub fn to_et_duration(&self) -> Duration { // Run a Newton Raphston to convert find the correct value of the - let mut seconds = (self.duration_since_j1900_tai - J2000_TO_J1900_DURATION).to_seconds(); + let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); for _ in 0..5 { seconds -= -NAIF_K * (NAIF_M0 + NAIF_M1 * seconds + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) @@ -2188,7 +2188,7 @@ impl Epoch { Self::delta_et_tai(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); // Match SPICE by changing the UTC definition. - self.duration_since_j1900_tai + delta_et_tai * Unit::Second - J2000_TO_J1900_DURATION + self.duration + delta_et_tai * Unit::Second - J2000_TO_J1900_DURATION } #[must_use] @@ -2208,7 +2208,7 @@ impl Epoch { /// 8. Reverse the algorithm given that approximation: compute the `g` offset, compute the difference between TDB and TAI, add the TT offset (32.184 s), and offset by the difference between J1900 and J2000. pub fn to_tdb_duration(&self) -> Duration { // Iterate to convert find the correct value of the - let mut seconds = (self.duration_since_j1900_tai - J2000_TO_J1900_DURATION).to_seconds(); + let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. for _ in 0..5 { let next = seconds - Self::inner_g(seconds); @@ -2225,7 +2225,7 @@ impl Epoch { let gamma = Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - self.duration_since_j1900_tai + delta_tdb_tai - J2000_TO_J1900_DURATION + self.duration + delta_tdb_tai - J2000_TO_J1900_DURATION } #[must_use] @@ -2345,7 +2345,7 @@ impl Epoch { /// Returns this time in a Duration past J1900 counted in UT1 pub fn to_ut1_duration(&self, provider: Ut1Provider) -> Duration { // TAI = UT1 + offset <=> UTC = TAI - offset - self.duration_since_j1900_tai - self.ut1_offset(provider).unwrap_or(Duration::ZERO) + self.duration - self.ut1_offset(provider).unwrap_or(Duration::ZERO) } #[cfg(feature = "ut1")] @@ -2354,7 +2354,7 @@ impl Epoch { pub fn to_ut1(&self, provider: Ut1Provider) -> Self { let mut me = *self; // TAI = UT1 + offset <=> UTC = TAI - offset - me.duration_since_j1900_tai -= self.ut1_offset(provider).unwrap_or(Duration::ZERO); + me.duration -= self.ut1_offset(provider).unwrap_or(Duration::ZERO); me.time_scale = TimeScale::TAI; me } @@ -3265,7 +3265,7 @@ fn cumulative_days_for_month() { #[cfg(feature = "serde")] fn test_serdes() { let e = Epoch::from_gregorian_utc(2020, 01, 01, 0, 0, 0, 0); - let content = r#"{"duration_since_j1900_tai":{"centuries":1,"nanoseconds":631065637000000000},"time_scale":"UTC"}"#; + let content = r#"{"duration":{"centuries":1,"nanoseconds":631065637000000000},"time_scale":"UTC"}"#; assert_eq!(content, serde_json::to_string(&e).unwrap()); let parsed: Epoch = serde_json::from_str(content).unwrap(); assert_eq!(e, parsed); diff --git a/src/lib.rs b/src/lib.rs index 46e80fe8..4f326037 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,7 +68,7 @@ pub const J2000_TO_J1900_DURATION: Duration = Duration { /// The Ephemeris Time reference epoch J2000. pub const J2000_REF_EPOCH_ET: Epoch = Epoch { - duration_since_j1900_tai: Duration { + duration: Duration { centuries: 0, nanoseconds: 3_155_716_767_816_072_748, }, @@ -77,7 +77,7 @@ pub const J2000_REF_EPOCH_ET: Epoch = Epoch { /// The Dynamic Barycentric Time reference epoch J2000. pub const J2000_REF_EPOCH_TDB: Epoch = Epoch { - duration_since_j1900_tai: Duration { + duration: Duration { centuries: 0, nanoseconds: 3_155_716_767_816_072_704, }, diff --git a/tests/epoch.rs b/tests/epoch.rs index 163cadd7..63adeb50 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -381,7 +381,7 @@ fn gpst() { assert_eq!(format!("{:o}", gps_epoch), "0"); assert_eq!( Epoch::from_gpst_days(0.0).to_duration_since_j1900(), - gps_epoch.duration_since_j1900_tai + gps_epoch.duration ); assert_eq!( @@ -460,7 +460,7 @@ fn galileo_time_scale() { assert_eq!(format!("{:x}", GST_REF_EPOCH), "1999-08-22T00:00:19 TAI"); assert_eq!( Epoch::from_gst_days(0.0).to_duration_since_j1900(), - gst_epoch.duration_since_j1900_tai + gst_epoch.duration ); assert_eq!( @@ -505,7 +505,7 @@ fn beidou_time_scale() { assert_eq!( Epoch::from_bdt_days(0.0).to_duration_since_j1900(), - bdt_epoch.duration_since_j1900_tai + bdt_epoch.duration ); assert_eq!( @@ -973,11 +973,11 @@ fn test_format() { match i { 0 => assert_eq!(format!("{epoch:x}"), "2020-09-06T23:24:29.000000002 TAI"), 1 => { - assert_eq!(epoch.duration_since_j1900_tai, 1 * Unit::Second); + assert_eq!(epoch.duration, 1 * Unit::Second); assert_eq!(format!("{epoch:x}"), "1900-01-01T00:00:01 TAI") } 2 => { - assert_eq!(epoch.duration_since_j1900_tai, -1 * Unit::Second); + assert_eq!(epoch.duration, -1 * Unit::Second); assert_eq!(format!("{epoch:x}"), "1899-12-31T23:59:59 TAI") } 3 => assert_eq!(format!("{epoch:x}"), "1820-09-06T23:24:29.000000002 TAI"), @@ -1010,8 +1010,8 @@ fn test_format() { (rebuilt - *epoch) < 30.0 * Unit::Microsecond, "#{i} error = {}\ngot = {}\nwant: {}", rebuilt - *epoch, - rebuilt.duration_since_j1900_tai, - epoch.duration_since_j1900_tai + rebuilt.duration, + epoch.duration ) } else { assert_eq!( @@ -1019,15 +1019,15 @@ fn test_format() { epoch, "#{i} error = {}\ngot = {}\nwant: {}", rebuilt - *epoch, - rebuilt.duration_since_j1900_tai, - epoch.duration_since_j1900_tai + rebuilt.duration, + epoch.duration ) } } Err(e) => { panic!( "#{i} {e:?} with {epoch:?} (duration since j1900 = {})", - epoch.duration_since_j1900_tai + epoch.duration ) } }; @@ -1042,8 +1042,8 @@ fn test_format() { let epoch_post = Epoch::from_gregorian_tai_hms(1900, 1, 1, 0, 0, 1); let epoch_pre = Epoch::from_gregorian_tai_hms(1899, 12, 31, 23, 59, 59); - assert_eq!(epoch_post.duration_since_j1900_tai.decompose().0, 0); - assert_eq!(epoch_pre.duration_since_j1900_tai.decompose().0, -1); + assert_eq!(epoch_post.duration.decompose().0, 0); + assert_eq!(epoch_pre.duration.decompose().0, -1); } #[test] From b6580ea895213fa6b69834263230859c4937560f Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 28 May 2023 11:57:47 +0200 Subject: [PATCH 03/37] introducing to_time_scale Signed-off-by: Guillaume W. Bres --- src/deprecated.rs | 36 ++++--- src/efmt/formatter.rs | 4 +- src/epoch.rs | 222 +++++++++++++++++++----------------------- tests/epoch.rs | 66 +++++++++---- 4 files changed, 165 insertions(+), 163 deletions(-) diff --git a/src/deprecated.rs b/src/deprecated.rs index 4b6e3685..f40b6eb8 100644 --- a/src/deprecated.rs +++ b/src/deprecated.rs @@ -78,14 +78,16 @@ impl Epoch { self.to_duration_since_j1900() } - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_duration_since_j1900_in_time_scale(&self, time_scale: TimeScale) -> Duration { - self.to_duration_since_j1900_in_time_scale(time_scale) - } + /* + #[must_use] + #[deprecated( + note = "Prefix for this function is now `to_` instead of `as_`.", + since = "3.5.0" + )] + pub fn as_duration_since_j1900_in_time_scale(&self, time_scale: TimeScale) -> Duration { + self.to_duration_since_j1900_in_time_scale(time_scale) + } + */ #[must_use] #[deprecated( @@ -96,14 +98,16 @@ impl Epoch { self.to_tai_seconds() } - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub const fn as_tai_duration(&self) -> Duration { - self.to_tai_duration() - } + /* + #[must_use] + #[deprecated( + note = "Prefix for this function is now `to_` instead of `as_`.", + since = "3.5.0" + )] + pub const fn as_tai_duration(&self) -> Duration { + self.to_tai_duration() + } + */ #[must_use] #[deprecated( diff --git a/src/efmt/formatter.rs b/src/efmt/formatter.rs index 3922910a..974b7dd8 100644 --- a/src/efmt/formatter.rs +++ b/src/efmt/formatter.rs @@ -99,8 +99,8 @@ impl Formatter { } } - pub fn in_time_scale(epoch: Epoch, format: Format, time_scale: TimeScale) -> Self { - Self::new(epoch.in_time_scale(time_scale), format) + pub fn to_time_scale(epoch: Epoch, format: Format, time_scale: TimeScale) -> Self { + Self::new(epoch.to_time_scale(time_scale), format) } pub fn set_timezone(&mut self, offset: Duration) { diff --git a/src/epoch.rs b/src/epoch.rs index 8e5b00dd..4c9fc3a2 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -13,9 +13,9 @@ use crate::leap_seconds::{LatestLeapSeconds, LeapSecondProvider}; use crate::parser::Token; use crate::{ Errors, MonthName, TimeScale, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, GPST_REF_EPOCH, - GST_REF_EPOCH, J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET, NANOSECONDS_PER_DAY, - NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, - UNIX_REF_EPOCH, + GST_REF_EPOCH, J1900_OFFSET, J2000_REF_EPOCH, J2000_TO_J1900_DURATION, MJD_OFFSET, + NANOSECONDS_PER_DAY, NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, + NANOSECONDS_PER_SECOND_U32, UNIX_REF_EPOCH, }; use crate::efmt::format::Format; @@ -138,7 +138,7 @@ impl Sub for Epoch { type Output = Duration; fn sub(self, other: Self) -> Duration { - self.duration - other.duration + self.duration - other.to_time_scale(self.time_scale).duration } } @@ -152,7 +152,10 @@ impl Sub for Epoch { type Output = Self; fn sub(self, duration: Duration) -> Self { - self.set(self.to_duration() - duration) + Self { + duration: self.to_duration() - duration, + time_scale: self.time_scale, + } } } @@ -162,7 +165,10 @@ impl Add for Epoch { type Output = Self; fn add(self, seconds: f64) -> Self { - self.set(self.to_duration() + seconds * Unit::Second) + Self { + duration: self.to_duration() + seconds * Unit::Second, + time_scale: self.time_scale, + } } } @@ -170,7 +176,10 @@ impl Add for Epoch { type Output = Self; fn add(self, duration: Duration) -> Self { - self.set(self.to_duration() + duration) + Self { + duration: self.to_duration() + duration, + time_scale: self.time_scale, + } } } @@ -193,7 +202,10 @@ impl Sub for Epoch { #[allow(clippy::identity_op)] fn sub(self, unit: Unit) -> Self { - self.set(self.to_duration() - unit * 1) + Self { + duration: self.to_duration() - unit * 1, + time_scale: self.time_scale, + } } } @@ -202,7 +214,10 @@ impl Add for Epoch { #[allow(clippy::identity_op)] fn add(self, unit: Unit) -> Self { - self.set(self.to_duration() + unit * 1) + Self { + duration: self.to_duration() + unit * 1, + time_scale: self.time_scale, + } } } @@ -215,7 +230,7 @@ impl AddAssign for Epoch { /// Equality only checks the duration since J1900 match in TAI, because this is how all of the epochs are referenced. impl PartialEq for Epoch { fn eq(&self, other: &Self) -> bool { - self.duration == other.duration + self.duration == other.to_time_scale(self.time_scale).duration } } @@ -229,7 +244,7 @@ impl PartialOrd for Epoch { fn partial_cmp(&self, other: &Self) -> Option { Some( self.duration - .cmp(&other.duration), + .cmp(&other.to_time_scale(self.time_scale).duration), ) } } @@ -237,7 +252,7 @@ impl PartialOrd for Epoch { impl Ord for Epoch { fn cmp(&self, other: &Self) -> Ordering { self.duration - .cmp(&other.duration) + .cmp(&other.to_time_scale(self.time_scale).duration) } } @@ -266,59 +281,56 @@ impl Epoch { /// Creates an epoch from given duration expressed in given timescale. /// In case of ET, TDB Timescales, a duration since J2000 is expected. #[must_use] - pub fn from_duration(new_duration: Duration, ts: TimeScale) -> Self { + pub fn from_duration(duration: Duration, ts: TimeScale) -> Self { + Self { + duration, + time_scale: ts, + } + } + + pub fn to_duration_since_j1900(&self) -> Duration { + self.to_time_scale(TimeScale::TAI).duration + } + + #[must_use] + /// Converts self to another time scale + pub fn to_time_scale(&self, ts: TimeScale) -> Self { match ts { - TimeScale::TAI => Self { - duration: new_duration, - time_scale: TimeScale::TAI, - }, - TimeScale::ET => { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds_j2000 = new_duration.to_seconds(); - for _ in 0..5 { - seconds_j2000 += -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds_j2000 - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) - .sin(); - } - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); - // Match SPICE by changing the UTC definition. - Self { - duration: (new_duration.to_seconds() - delta_et_tai) - * Unit::Second - + J2000_TO_J1900_DURATION, - time_scale: TimeScale::ET, - } + TimeScale::TAI => { + // conversion to TAI: remove time scale reference point + let mut new_epoch = self.clone(); + new_epoch.duration -= ts.tai_reference_epoch().duration; + new_epoch.with_time_scale(TimeScale::TAI) } TimeScale::TDB => { - let gamma = Self::inner_g(new_duration.to_seconds()); + // first convert back to TAI + let mut tai_epoch = self.to_time_scale(TimeScale::TAI); + // seconds past J2000 + //TODO: this operation is not feasible is Self is not PAST J2000 + let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; + let seconds_since_j2000 = duration_since_j2000.to_seconds(); + let gamma = Self::inner_g(seconds_since_j2000); let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - // Offset back to J1900. - Self { - duration: new_duration - delta_tdb_tai - + J2000_TO_J1900_DURATION, - time_scale: TimeScale::TDB, - } + tai_epoch += delta_tdb_tai; + tai_epoch -= J2000_TO_J1900_DURATION; // TDB time scale is expressed past J2000 + tai_epoch.with_time_scale(TimeScale::TDB) } ts => { - let mut tai_epoch = ts.tai_reference_epoch(); - tai_epoch += new_duration; + // first convert back to TAI + let mut tai_epoch = self.to_time_scale(TimeScale::TAI); + // leap second management + if ts.uses_leap_seconds() { + // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds + tai_epoch += tai_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + } + // time scale specificities match ts { TimeScale::TT => { tai_epoch -= TT_OFFSET_MS * Unit::Millisecond; } _ => {} } - // leap second management - if ts.uses_leap_seconds() { - tai_epoch += tai_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - } - tai_epoch.with_timescale(ts) + tai_epoch.with_time_scale(ts) } } } @@ -761,15 +773,11 @@ impl Epoch { duration_wrt_1900 -= Unit::Second; } - // NOTE: For ET and TDB, we make sure to offset the duration back to J2000 since those functions expect a J2000 input. - Ok(match time_scale { - TimeScale::ET => Self::from_et_duration(duration_wrt_1900 - J2000_TO_J1900_DURATION), - TimeScale::TDB => Self::from_tdb_duration(duration_wrt_1900 - J2000_TO_J1900_DURATION), - ts => Self::from_duration( - duration_wrt_1900 - ts.tai_reference_epoch().to_tai_duration(), - ts, - ), - }) + Ok(Self { + duration: duration_wrt_1900, + time_scale: TimeScale::TAI, + } + .to_time_scale(time_scale)) } #[must_use] @@ -1756,24 +1764,14 @@ impl Epoch { /// 2. If an epoch was initialized as Epoch::from_..._tdb(...) then the duration will be the UTC duration from J2000 because the TDB reference epoch is J2000. #[must_use] pub fn to_duration(&self) -> Duration { - self.to_duration_in_time_scale(self.time_scale) + self.duration } #[must_use] /// Returns this epoch with respect to the provided time scale. /// This is needed to correctly perform duration conversions in dynamical time scales (e.g. TDB). - pub fn to_duration_in_time_scale(&self, time_scale: TimeScale) -> Duration { - match time_scale { - TimeScale::TAI => self.duration, - TimeScale::TT => self.to_tt_duration(), - TimeScale::ET => self.to_et_duration(), - TimeScale::TDB => self.to_tdb_duration(), - TimeScale::UTC => self.to_utc_duration(), - TimeScale::BDT => self.to_bdt_duration(), - TimeScale::GST => self.to_gst_duration(), - // GPST and QZSST share the same properties - TimeScale::GPST | TimeScale::QZSST => self.to_gpst_duration(), - } + pub fn to_duration_in_time_scale(&self, ts: TimeScale) -> Duration { + self.to_time_scale(ts).duration } /// Attempts to return the number of nanoseconds since the reference epoch of the provided time scale. @@ -1790,46 +1788,23 @@ impl Epoch { } } - /// Returns this epoch in duration since J1900 in the time scale this epoch was created in. - #[must_use] - pub fn to_duration_since_j1900(&self) -> Duration { - self.to_duration_since_j1900_in_time_scale(self.time_scale) - } - - /// Returns this epoch in duration since J1900 with respect to the provided time scale. - #[must_use] - pub fn to_duration_since_j1900_in_time_scale(&self, time_scale: TimeScale) -> Duration { - match time_scale { - TimeScale::ET => self.to_et_duration_since_j1900(), - TimeScale::TAI => self.duration, - TimeScale::TT => self.to_tt_duration(), - TimeScale::TDB => self.to_tdb_duration_since_j1900(), - TimeScale::UTC => self.to_utc_duration(), - // GPST and QZSST share the same properties - TimeScale::GPST | TimeScale::QZSST => { - self.to_gpst_duration() + GPST_REF_EPOCH.to_tai_duration() + /* + /// Makes a copy of self and sets the duration and time scale appropriately given the new duration + #[must_use] + pub fn set(&self, new_duration: Duration) -> Self { + match self.time_scale { + TimeScale::TAI => Self::from_tai_duration(new_duration), + TimeScale::TT => Self::from_tt_duration(new_duration), + TimeScale::ET => Self::from_et_duration(new_duration), + TimeScale::TDB => Self::from_tdb_duration(new_duration), + TimeScale::UTC => Self::from_utc_duration(new_duration), + // GPST and QZSST share the same properties + TimeScale::GPST | TimeScale::QZSST => Self::from_gpst_duration(new_duration), + TimeScale::GST => Self::from_gst_duration(new_duration), + TimeScale::BDT => Self::from_bdt_duration(new_duration), } - TimeScale::GST => self.to_gst_duration() + GST_REF_EPOCH.to_tai_duration(), - TimeScale::BDT => self.to_bdt_duration() + BDT_REF_EPOCH.to_tai_duration(), - } - } - - /// Makes a copy of self and sets the duration and time scale appropriately given the new duration - #[must_use] - pub fn set(&self, new_duration: Duration) -> Self { - match self.time_scale { - TimeScale::TAI => Self::from_tai_duration(new_duration), - TimeScale::TT => Self::from_tt_duration(new_duration), - TimeScale::ET => Self::from_et_duration(new_duration), - TimeScale::TDB => Self::from_tdb_duration(new_duration), - TimeScale::UTC => Self::from_utc_duration(new_duration), - // GPST and QZSST share the same properties - TimeScale::GPST | TimeScale::QZSST => Self::from_gpst_duration(new_duration), - TimeScale::GST => Self::from_gst_duration(new_duration), - TimeScale::BDT => Self::from_bdt_duration(new_duration), } - } - + */ #[must_use] /// Returns the number of TAI seconds since J1900 pub fn to_tai_seconds(&self) -> f64 { @@ -1838,8 +1813,8 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in TAI - pub const fn to_tai_duration(&self) -> Duration { - self.duration + pub fn to_tai_duration(&self) -> Duration { + self.to_time_scale(TimeScale::TAI).duration } #[must_use] @@ -1869,8 +1844,7 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in UTC pub fn to_utc_duration(&self) -> Duration { - // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - self.duration - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second + self.to_time_scale(TimeScale::UTC).duration } #[must_use] @@ -2051,8 +2025,7 @@ impl Epoch { #[must_use] /// Returns `Duration` past QZSS time Epoch. pub fn to_qzsst_duration(&self) -> Duration { - // GPST and QZSST share the same reference epoch - self.duration - GPST_REF_EPOCH.to_tai_duration() + self.to_time_scale(TimeScale::QZSST).duration } /// Returns nanoseconds past QZSS Time Epoch, defined as UTC midnight of January 5th to 6th 1980 (cf. ). @@ -2425,7 +2398,7 @@ impl Epoch { #[must_use] /// Copies this epoch and sets it to the new time scale provided. - pub fn in_time_scale(&self, new_time_scale: TimeScale) -> Self { + pub fn with_time_scale(&self, new_time_scale: TimeScale) -> Self { let mut me = *self; me.time_scale = new_time_scale; me @@ -2636,7 +2609,7 @@ impl Epoch { let (sign, days, _, _, _, milliseconds, microseconds, nanoseconds) = self.to_duration().decompose(); // Shadow other with the provided other epoch but in the correct time scale. - let other = other.in_time_scale(self.time_scale); + let other = other.to_time_scale(self.time_scale); Self::from_duration( Duration::compose( sign, @@ -2717,7 +2690,7 @@ impl Epoch { /// ``` pub fn with_hms_strict_from(&self, other: Self) -> Self { let (sign, days, _, _, _, _, _, _) = self.to_duration().decompose(); - let other = other.in_time_scale(self.time_scale); + let other = other.to_time_scale(self.time_scale); Self::from_duration( Duration::compose( sign, @@ -3003,7 +2976,7 @@ impl fmt::Debug for Epoch { /// Print this epoch in Gregorian in the time scale used at initialization fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let (y, mm, dd, hh, min, s, nanos) = - Self::compute_gregorian(self.to_duration_since_j1900()); + Self::compute_gregorian(self.to_time_scale(TimeScale::TAI).duration); if nanos == 0 { write!( f, @@ -3265,7 +3238,8 @@ fn cumulative_days_for_month() { #[cfg(feature = "serde")] fn test_serdes() { let e = Epoch::from_gregorian_utc(2020, 01, 01, 0, 0, 0, 0); - let content = r#"{"duration":{"centuries":1,"nanoseconds":631065637000000000},"time_scale":"UTC"}"#; + let content = + r#"{"duration":{"centuries":1,"nanoseconds":631065637000000000},"time_scale":"UTC"}"#; assert_eq!(content, serde_json::to_string(&e).unwrap()); let parsed: Epoch = serde_json::from_str(content).unwrap(); assert_eq!(e, parsed); diff --git a/tests/epoch.rs b/tests/epoch.rs index 63adeb50..0488d46f 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -4,8 +4,8 @@ extern crate core; use hifitime::{ is_gregorian_valid, Duration, Epoch, Errors, ParsingErrors, TimeScale, TimeUnits, Unit, Weekday, BDT_REF_EPOCH, DAYS_GPS_TAI_OFFSET, GPST_REF_EPOCH, GST_REF_EPOCH, J1900_OFFSET, - J1900_REF_EPOCH, J2000_OFFSET, MJD_OFFSET, SECONDS_BDT_TAI_OFFSET, SECONDS_GPS_TAI_OFFSET, - SECONDS_GST_TAI_OFFSET, SECONDS_PER_DAY, + J1900_REF_EPOCH, J2000_OFFSET, J2000_REF_EPOCH, J2000_TO_J1900_DURATION, MJD_OFFSET, + SECONDS_BDT_TAI_OFFSET, SECONDS_GPS_TAI_OFFSET, SECONDS_GST_TAI_OFFSET, SECONDS_PER_DAY, }; use hifitime::efmt::{Format, Formatter}; @@ -563,11 +563,11 @@ fn unix() { let unix_epoch = Epoch::from_gregorian_utc_at_midnight(1970, 1, 1); assert_eq!( - format!("{}", unix_epoch.in_time_scale(TimeScale::UTC)), + format!("{}", unix_epoch.to_time_scale(TimeScale::UTC)), "1970-01-01T00:00:00 UTC" ); assert_eq!( - format!("{:x}", unix_epoch.in_time_scale(TimeScale::TAI)), + format!("{:x}", unix_epoch.to_time_scale(TimeScale::TAI)), "1970-01-01T00:00:00 TAI" ); // Print as UNIX seconds @@ -1240,7 +1240,6 @@ fn regression_test_gh_145() { fn test_timescale_recip() { // The general test function used throughout this verification. let recip_func = |utc_epoch: Epoch| { - assert_eq!(utc_epoch, utc_epoch.set(utc_epoch.to_duration())); // Test that we can convert this epoch into another time scale and re-initialize it correctly from that value. for ts in &[ //TimeScale::TAI, @@ -1326,7 +1325,7 @@ fn test_add_durations_over_leap_seconds() { // Noon UTC after the first leap second is in fact ten seconds _after_ noon TAI. // Hence, there are as many TAI seconds since Epoch between UTC Noon and TAI Noon + 10s. let pre_ls_utc = Epoch::from_gregorian_utc_at_noon(1971, 12, 31); - let pre_ls_tai = pre_ls_utc.in_time_scale(TimeScale::TAI); + let pre_ls_tai = pre_ls_utc.to_time_scale(TimeScale::TAI); // Before the first leap second, there is no time difference between both epochs (because only IERS announced leap seconds are accounted for by default). assert_eq!(pre_ls_utc - pre_ls_tai, Duration::ZERO); @@ -1427,7 +1426,7 @@ fn test_weekday() { TimeScale::TT, TimeScale::UTC, ] { - let e_ts = e.in_time_scale(new_time_scale); + let e_ts = e.to_time_scale(new_time_scale); assert_eq!(e_ts.weekday(), expect, "error with {new_time_scale}"); } }; @@ -1507,7 +1506,7 @@ fn test_get_time() { let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 10, 11, 12, 13); let other_utc = Epoch::from_gregorian_utc(2024, 12, 01, 20, 21, 22, 23); - let other = other_utc.in_time_scale(TimeScale::TDB); + let other = other_utc.to_time_scale(TimeScale::TDB); assert_eq!( epoch.with_hms_from(other), @@ -1565,7 +1564,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (0, 10 * 1_000_000_000 + 10)); // TAI<=>UTC - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1585,7 +1584,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 00)); assert_eq!(epoch.to_time_of_week(), (2238, 345_618_000_000_000)); - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(utc_wk, utc_tow, TimeScale::UTC), @@ -1593,7 +1592,7 @@ fn test_time_of_week() { ); // GPST and QZSST share the same properties at all times - let epoch_qzsst = epoch.in_time_scale(TimeScale::QZSST); + let epoch_qzsst = epoch.to_time_scale(TimeScale::QZSST); assert_eq!(epoch.to_gregorian_utc(), epoch_qzsst.to_gregorian_utc()); let gps_qzss_offset = @@ -1605,7 +1604,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (1980, 01, 06, 01, 00, 0 + 18, 00)); assert_eq!(epoch.to_time_of_week(), (0, 3_618_000_000_000)); - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(utc_wk, utc_tow, TimeScale::UTC), @@ -1626,7 +1625,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (24, 306_457_000_000_000)); // <=>UTC - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1638,7 +1637,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 01)); // <=>UTC - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1650,7 +1649,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 12, 00, 00, 00)); // <=>UTC - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1662,7 +1661,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 15, 27, 19, 10)); // <=>UTC - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1676,7 +1675,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (0, 3_600_000_000_000)); // <=>UTC - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1692,7 +1691,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (1, 128 * 3600 * 1_000_000_000)); // <=>UTC - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1713,7 +1712,7 @@ fn test_time_of_week() { ); // <=>UTC - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1736,7 +1735,7 @@ fn test_time_of_week() { ); // <=>UTC - let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1757,7 +1756,7 @@ fn test_day_of_year() { TimeScale::ET, TimeScale::TDB, ] { - let epoch = utc_epoch.in_time_scale(*ts); + let epoch = utc_epoch.to_time_scale(*ts); let (year, days) = epoch.year_days_of_year(); let rebuilt = Epoch::from_day_of_year(year, days, *ts); if *ts == TimeScale::ET || *ts == TimeScale::TDB { @@ -1879,6 +1878,31 @@ fn test_epoch_formatter() { ); } +#[test] +fn test_to_tai_time_scale() { + let j1900_ref = J1900_REF_EPOCH; + assert_eq!(j1900_ref, j1900_ref.to_time_scale(TimeScale::TAI)); + let j2000_ref = J2000_REF_EPOCH; + assert_eq!(j2000_ref, j2000_ref.to_time_scale(TimeScale::TAI)); + let j2000_to_j1900 = j2000_ref - j1900_ref; + assert_eq!(j2000_to_j1900, J2000_TO_J1900_DURATION); +} + +#[test] +fn test_to_utc_time_scale() { + let gpst_tai_ref = TimeScale::GPST.tai_reference_epoch(); + // there were 19 leap seconds on the day GPST was "started" + let gpst_utc_delta = 19.0; // leaps + let gpst_utc_ref = gpst_tai_ref.to_time_scale(TimeScale::UTC); + assert_eq!((gpst_utc_ref - gpst_tai_ref).to_seconds(), gpst_utc_delta); + + let bdt_tai_ref = TimeScale::BDT.tai_reference_epoch(); + // there were 33 leap seconds on the day BDT was "started" + let bdt_utc_delta = 33.0; // leaps + let bdt_utc_ref = bdt_tai_ref.to_time_scale(TimeScale::UTC); + assert_eq!((bdt_utc_ref - bdt_tai_ref).to_seconds(), bdt_utc_delta); +} + #[cfg(feature = "std")] #[test] fn test_leap_seconds_file() { From 5de034bd090b0a81d66b8d01312ae3b712aa821a Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 28 May 2023 13:10:50 +0200 Subject: [PATCH 04/37] introducing week unit Signed-off-by: Guillaume W. Bres --- src/lib.rs | 2 ++ src/timeunits.rs | 24 +++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4f326037..51831319 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,8 @@ pub const DAYS_PER_YEAR_NLD: f64 = 365.0; /// `DAYS_PER_CENTURY` corresponds to the number of days per century in the Julian calendar. pub const DAYS_PER_CENTURY: f64 = 36525.0; pub const DAYS_PER_CENTURY_I64: i64 = 36525; +pub const DAYS_PER_WEEK: f64 = 7.0; +pub const DAYS_PER_WEEK_I64: i64 = 7; /// `SECONDS_PER_MINUTE` defines the number of seconds per minute. pub const SECONDS_PER_MINUTE: f64 = 60.0; /// `SECONDS_PER_HOUR` defines the number of seconds per hour. diff --git a/src/timeunits.rs b/src/timeunits.rs index fc535b60..37befe51 100644 --- a/src/timeunits.rs +++ b/src/timeunits.rs @@ -17,9 +17,10 @@ use num_traits::Float; use pyo3::prelude::*; use crate::{ - Duration, DAYS_PER_CENTURY, NANOSECONDS_PER_CENTURY, NANOSECONDS_PER_DAY, NANOSECONDS_PER_HOUR, - NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_MINUTE, - NANOSECONDS_PER_SECOND, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE, + Duration, DAYS_PER_CENTURY, DAYS_PER_WEEK, DAYS_PER_WEEK_I64, NANOSECONDS_PER_CENTURY, + NANOSECONDS_PER_DAY, NANOSECONDS_PER_HOUR, NANOSECONDS_PER_MICROSECOND, + NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_MINUTE, NANOSECONDS_PER_SECOND, SECONDS_PER_DAY, + SECONDS_PER_HOUR, SECONDS_PER_MINUTE, }; /// An Enum to perform time unit conversions. @@ -33,6 +34,7 @@ pub enum Unit { Minute, Hour, Day, + Week, /// 36525 days, is the number of days per century in the Julian calendar Century, } @@ -67,6 +69,9 @@ pub trait TimeUnits: Copy + Mul { fn centuries(self) -> Duration { self * Unit::Century } + fn weeks(self) -> Duration { + self * Unit::Week + } fn days(self) -> Duration { self * Unit::Day } @@ -158,6 +163,7 @@ impl Unit { pub fn in_seconds(&self) -> f64 { match self { Unit::Century => DAYS_PER_CENTURY * SECONDS_PER_DAY, + Unit::Week => DAYS_PER_WEEK * SECONDS_PER_DAY, Unit::Day => SECONDS_PER_DAY, Unit::Hour => SECONDS_PER_HOUR, Unit::Minute => SECONDS_PER_MINUTE, @@ -200,7 +206,8 @@ impl From for u8 { Unit::Minute => 4, Unit::Hour => 5, Unit::Day => 6, - Unit::Century => 7, + Unit::Week => 7, + Unit::Century => 8, Unit::Second => 0, } } @@ -222,7 +229,8 @@ impl From for Unit { 4 => Unit::Minute, 5 => Unit::Hour, 6 => Unit::Day, - 7 => Unit::Century, + 7 => Unit::Week, + 8 => Unit::Century, _ => Unit::Second, } } @@ -236,6 +244,7 @@ impl Mul for Unit { fn mul(self, q: i64) -> Duration { let factor = match self { Unit::Century => NANOSECONDS_PER_CENTURY as i64, + Unit::Week => NANOSECONDS_PER_DAY as i64 * DAYS_PER_WEEK_I64, Unit::Day => NANOSECONDS_PER_DAY as i64, Unit::Hour => NANOSECONDS_PER_HOUR as i64, Unit::Minute => NANOSECONDS_PER_MINUTE as i64, @@ -276,6 +285,7 @@ impl Mul for Unit { fn mul(self, q: f64) -> Duration { let factor = match self { Unit::Century => NANOSECONDS_PER_CENTURY as f64, + Unit::Week => NANOSECONDS_PER_DAY as f64 * DAYS_PER_WEEK, Unit::Day => NANOSECONDS_PER_DAY as f64, Unit::Hour => NANOSECONDS_PER_HOUR as f64, Unit::Minute => NANOSECONDS_PER_MINUTE as f64, @@ -306,8 +316,8 @@ fn test_unit_conversion() { for unit_u8 in 0..u8::MAX { let unit = Unit::from(unit_u8); let unit_u8_back: u8 = unit.into(); - // If the u8 is greater than 8, it isn't valid and necessarily encoded as Second. - if unit_u8 < 8 { + // If the u8 is greater than 9, it isn't valid and necessarily encoded as Second. + if unit_u8 < 9 { assert_eq!(unit_u8_back, unit_u8, "got {unit_u8_back} want {unit_u8}"); } else { assert_eq!(unit, Unit::Second); From 9d0afbd62c5395900276f85d2492e28004ec9c7c Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 28 May 2023 16:11:12 +0200 Subject: [PATCH 05/37] fix eq and comparison issue: compare in same time scale Signed-off-by: Guillaume W. Bres --- src/efmt/formatter.rs | 3 +- src/epoch.rs | 68 ++++++++++++++++++++++++++----------------- src/timescale.rs | 2 +- src/timeseries.rs | 15 +++++++--- tests/epoch.rs | 19 ++++++++++-- 5 files changed, 71 insertions(+), 36 deletions(-) diff --git a/src/efmt/formatter.rs b/src/efmt/formatter.rs index 974b7dd8..94f45cc7 100644 --- a/src/efmt/formatter.rs +++ b/src/efmt/formatter.rs @@ -127,7 +127,8 @@ impl fmt::Display for Formatter { if self.format.need_gregorian() { // This is a specific branch so we don't recompute the gregorian information for each token. - let (y, mm, dd, hh, min, s, nanos) = Epoch::compute_gregorian(self.epoch.to_duration()); + let (y, mm, dd, hh, min, s, nanos) = + Epoch::compute_gregorian(self.epoch.to_duration_in_time_scale(TimeScale::TAI)); // And format. for (i, maybe_item) in self .format diff --git a/src/epoch.rs b/src/epoch.rs index 4c9fc3a2..f165e390 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -22,7 +22,7 @@ use crate::efmt::format::Format; use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; use core::fmt; -use core::hash::{Hash, Hasher}; +use core::hash::Hash; use core::ops::{Add, AddAssign, Sub, SubAssign}; use crate::ParsingErrors; @@ -123,7 +123,7 @@ const CUMULATIVE_DAYS_FOR_MONTH: [u16; 12] = { /// Defines a nanosecond-precision Epoch. /// /// Refer to the appropriate functions for initializing this Epoch from different time scales or representations. -#[derive(Copy, Clone, Eq, Default)] +#[derive(Copy, Clone, Default, Eq, Hash)] #[repr(C)] #[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -234,12 +234,6 @@ impl PartialEq for Epoch { } } -impl Hash for Epoch { - fn hash(&self, hasher: &mut H) { - self.duration.hash(hasher); - } -} - impl PartialOrd for Epoch { fn partial_cmp(&self, other: &Self) -> Option { Some( @@ -297,11 +291,41 @@ impl Epoch { pub fn to_time_scale(&self, ts: TimeScale) -> Self { match ts { TimeScale::TAI => { - // conversion to TAI: remove time scale reference point let mut new_epoch = self.clone(); + // if previous time scale supported leap seconds: remove them + if self.time_scale.uses_leap_seconds() { + new_epoch -= new_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + } + // conversion to TAI: remove time scale reference point new_epoch.duration -= ts.tai_reference_epoch().duration; new_epoch.with_time_scale(TimeScale::TAI) } + TimeScale::ET => { + // first convert back to TAI + let mut tai_epoch = self.to_time_scale(TimeScale::TAI); + // seconds past J2000 + //TODO: this operation is not feasible is Self is not PAST J2000 + let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; + let mut seconds_since_j2000 = duration_since_j2000.to_seconds(); + for _ in 0..5 { + seconds_since_j2000 += -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds_since_j2000 + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_since_j2000).sin()) + .sin(); + } + // At this point, we have a good estimate of the number of seconds + // of this epoch. Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds_since_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); + // Match the SPICE by changing the UTC definition + Self { + duration: (tai_epoch.duration.to_seconds() - delta_et_tai) * Unit::Second + + J2000_TO_J1900_DURATION, + time_scale: TimeScale::ET, + } + } TimeScale::TDB => { // first convert back to TAI let mut tai_epoch = self.to_time_scale(TimeScale::TAI); @@ -312,7 +336,7 @@ impl Epoch { let gamma = Self::inner_g(seconds_since_j2000); let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; tai_epoch += delta_tdb_tai; - tai_epoch -= J2000_TO_J1900_DURATION; // TDB time scale is expressed past J2000 + tai_epoch += J2000_TO_J1900_DURATION; // TDB time scale is expressed past J2000 tai_epoch.with_time_scale(TimeScale::TDB) } ts => { @@ -320,7 +344,7 @@ impl Epoch { let mut tai_epoch = self.to_time_scale(TimeScale::TAI); // leap second management if ts.uses_leap_seconds() { - // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds + // new time scale supports leap seconds: add them tai_epoch += tai_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; } // time scale specificities @@ -775,7 +799,7 @@ impl Epoch { Ok(Self { duration: duration_wrt_1900, - time_scale: TimeScale::TAI, + time_scale: TimeScale::UTC, } .to_time_scale(time_scale)) } @@ -834,15 +858,8 @@ impl Epoch { second: u8, nanos: u32, ) -> Result { - let mut if_tai = - Self::maybe_from_gregorian_tai(year, month, day, hour, minute, second, nanos)?; - // Compute the TAI to UTC offset at this time. - // We have the time in TAI. But we were given UTC. - // Hence, we need to _add_ the leap seconds to get the actual TAI time. - // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - if_tai.duration += if_tai.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - if_tai.time_scale = TimeScale::UTC; - Ok(if_tai) + let if_tai = Self::maybe_from_gregorian_tai(year, month, day, hour, minute, second, nanos)?; + Ok(if_tai.to_time_scale(TimeScale::UTC)) } #[must_use] @@ -3310,12 +3327,9 @@ fn formal_epoch_reciprocity_tdb() { let epoch: Epoch = Epoch::from_duration(duration, time_scale); let out_duration = epoch.to_duration_in_time_scale(time_scale); assert_eq!(out_duration.centuries, duration.centuries); - if out_duration.nanoseconds > duration.nanoseconds { - assert!(out_duration.nanoseconds - duration.nanoseconds < 500_000); - } else if out_duration.nanoseconds < duration.nanoseconds { - assert!(duration.nanoseconds - out_duration.nanoseconds < 500_000); - } - // Else: they match and we're happy. + assert_eq!(out_duration.nanoseconds, duration.nanoseconds); + let error = (out_duration.nanoseconds - duration.nanoseconds) as f64; + assert!(error.abs() < 500_000.0, "error: {}", error); } } diff --git a/src/timescale.rs b/src/timescale.rs index 56d77a00..c7a033b6 100644 --- a/src/timescale.rs +++ b/src/timescale.rs @@ -68,7 +68,7 @@ pub const UNIX_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { /// Enum of the different time systems available #[non_exhaustive] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TimeScale { diff --git a/src/timeseries.rs b/src/timeseries.rs index bc53f8ae..cda7958e 100644 --- a/src/timeseries.rs +++ b/src/timeseries.rs @@ -302,7 +302,7 @@ mod tests { use crate::{Epoch, TimeSeries, Unit}; #[test] - fn test_timeseries() { + fn test_exclusive_timeseries() { let start = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14); let end = Epoch::from_gregorian_utc_at_noon(2017, 1, 14); let step = Unit::Hour * 2; @@ -319,13 +319,20 @@ mod tests { assert_ne!(epoch, end, "Ending epoch of exclusive time series is wrong"); } #[cfg(feature = "std")] - println!("{}", epoch); + println!("tests::exclusive_timeseries::{}", epoch); count += 1; } assert_eq!(count, 6, "Should have five items in this iterator"); + } - count = 0; + #[test] + fn test_inclusive_timeseries() { + let start = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14); + let end = Epoch::from_gregorian_utc_at_noon(2017, 1, 14); + let step = Unit::Hour * 2; + + let mut count = 0; let time_series = TimeSeries::inclusive(start, end, step); for epoch in time_series { if count == 0 { @@ -337,7 +344,7 @@ mod tests { assert_eq!(epoch, end, "Ending epoch of inclusive time series is wrong"); } #[cfg(feature = "std")] - println!("{}", epoch); + println!("tests::inclusive_timeseries::{}", epoch); count += 1; } diff --git a/tests/epoch.rs b/tests/epoch.rs index 0488d46f..e2834072 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -24,6 +24,19 @@ fn test_const_ops() { assert!((j2000_offset.to_unit(Unit::Day) - J2000_OFFSET).abs() < f64::EPSILON); } +#[test] +fn test_from_gregorian() { + let utc_epoch = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14); + let tai_epoch = Epoch::from_gregorian_at_midnight(2017, 1, 14, TimeScale::TAI); + assert_eq!(utc_epoch.to_time_scale(TimeScale::TAI), tai_epoch); + assert_eq!(utc_epoch, tai_epoch.to_time_scale(TimeScale::UTC)); + + let utc_epoch = Epoch::from_gregorian_utc(2016, 12, 31, 23, 59, 59, 0); + let tai_epoch = Epoch::from_gregorian(2016, 12, 31, 23, 59, 59, 0, TimeScale::TAI); + assert_eq!(utc_epoch.to_time_scale(TimeScale::TAI), tai_epoch); + assert_eq!(utc_epoch, tai_epoch.to_time_scale(TimeScale::UTC)); +} + #[allow(clippy::float_equality_without_abs)] #[test] fn utc_epochs() { @@ -1242,9 +1255,9 @@ fn test_timescale_recip() { let recip_func = |utc_epoch: Epoch| { // Test that we can convert this epoch into another time scale and re-initialize it correctly from that value. for ts in &[ - //TimeScale::TAI, - TimeScale::ET, - TimeScale::TDB, + TimeScale::TAI, + //TimeScale::ET, + //TimeScale::TDB, TimeScale::TT, TimeScale::UTC, ] { From 7ffad087cb38b5f035e9aa60efe8004863ac0d88 Mon Sep 17 00:00:00 2001 From: gwbres Date: Tue, 30 May 2023 20:01:00 +0200 Subject: [PATCH 06/37] Update src/epoch.rs Co-authored-by: Chris --- src/epoch.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/epoch.rs b/src/epoch.rs index f165e390..0e36f25d 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -272,7 +272,9 @@ impl Epoch { None } - /// Creates an epoch from given duration expressed in given timescale. + /// Creates an epoch from given duration expressed in given timescale, i.e. since the given time scale's reference epoch. + /// + /// For example, if the duration is 1 day and the time scale is Ephemeris Time, then this will create an epoch of 2000-01-02 at midnight ET. If the duration is 1 day and the time scale is TAI, this will create an epoch of 1900-01-02 at noon, because the TAI reference epoch in Hifitime is chosen to be the J1900 epoch. /// In case of ET, TDB Timescales, a duration since J2000 is expected. #[must_use] pub fn from_duration(duration: Duration, ts: TimeScale) -> Self { From e47727278e8535f351fd9ae998731d6644ffbd60 Mon Sep 17 00:00:00 2001 From: gwbres Date: Tue, 30 May 2023 20:02:13 +0200 Subject: [PATCH 07/37] Update src/epoch.rs Co-authored-by: Chris --- src/epoch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/epoch.rs b/src/epoch.rs index 0e36f25d..aa760734 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -290,7 +290,7 @@ impl Epoch { #[must_use] /// Converts self to another time scale - pub fn to_time_scale(&self, ts: TimeScale) -> Self { + pub fn into_time_scale(&self, ts: TimeScale) -> Self { match ts { TimeScale::TAI => { let mut new_epoch = self.clone(); From 17f05684ed46c5d3b5663a77df0b4f90ae29cfb8 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Tue, 30 May 2023 21:22:40 +0200 Subject: [PATCH 08/37] working on compute_gregorian() Signed-off-by: Guillaume W. Bres --- src/efmt/formatter.rs | 4 +- src/epoch.rs | 140 ++++++++++++++++++++---------------------- tests/epoch.rs | 59 ++++++++++-------- 3 files changed, 103 insertions(+), 100 deletions(-) diff --git a/src/efmt/formatter.rs b/src/efmt/formatter.rs index 94f45cc7..f0f85642 100644 --- a/src/efmt/formatter.rs +++ b/src/efmt/formatter.rs @@ -100,7 +100,7 @@ impl Formatter { } pub fn to_time_scale(epoch: Epoch, format: Format, time_scale: TimeScale) -> Self { - Self::new(epoch.to_time_scale(time_scale), format) + Self::new(epoch.into_time_scale(time_scale), format) } pub fn set_timezone(&mut self, offset: Duration) { @@ -128,7 +128,7 @@ impl fmt::Display for Formatter { if self.format.need_gregorian() { // This is a specific branch so we don't recompute the gregorian information for each token. let (y, mm, dd, hh, min, s, nanos) = - Epoch::compute_gregorian(self.epoch.to_duration_in_time_scale(TimeScale::TAI)); + Epoch::compute_gregorian(self.epoch.duration, self.epoch.time_scale); // And format. for (i, maybe_item) in self .format diff --git a/src/epoch.rs b/src/epoch.rs index aa760734..1520b587 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -138,7 +138,7 @@ impl Sub for Epoch { type Output = Duration; fn sub(self, other: Self) -> Duration { - self.duration - other.to_time_scale(self.time_scale).duration + self.duration - other.into_time_scale(self.time_scale).duration } } @@ -230,7 +230,7 @@ impl AddAssign for Epoch { /// Equality only checks the duration since J1900 match in TAI, because this is how all of the epochs are referenced. impl PartialEq for Epoch { fn eq(&self, other: &Self) -> bool { - self.duration == other.to_time_scale(self.time_scale).duration + self.duration == other.into_time_scale(self.time_scale).duration } } @@ -238,7 +238,7 @@ impl PartialOrd for Epoch { fn partial_cmp(&self, other: &Self) -> Option { Some( self.duration - .cmp(&other.to_time_scale(self.time_scale).duration), + .cmp(&other.into_time_scale(self.time_scale).duration), ) } } @@ -246,7 +246,7 @@ impl PartialOrd for Epoch { impl Ord for Epoch { fn cmp(&self, other: &Self) -> Ordering { self.duration - .cmp(&other.to_time_scale(self.time_scale).duration) + .cmp(&other.into_time_scale(self.time_scale).duration) } } @@ -273,7 +273,7 @@ impl Epoch { } /// Creates an epoch from given duration expressed in given timescale, i.e. since the given time scale's reference epoch. - /// + /// /// For example, if the duration is 1 day and the time scale is Ephemeris Time, then this will create an epoch of 2000-01-02 at midnight ET. If the duration is 1 day and the time scale is TAI, this will create an epoch of 1900-01-02 at noon, because the TAI reference epoch in Hifitime is chosen to be the J1900 epoch. /// In case of ET, TDB Timescales, a duration since J2000 is expected. #[must_use] @@ -285,7 +285,7 @@ impl Epoch { } pub fn to_duration_since_j1900(&self) -> Duration { - self.to_time_scale(TimeScale::TAI).duration + self.into_time_scale(TimeScale::TAI).duration } #[must_use] @@ -300,13 +300,14 @@ impl Epoch { } // conversion to TAI: remove time scale reference point new_epoch.duration -= ts.tai_reference_epoch().duration; - new_epoch.with_time_scale(TimeScale::TAI) + new_epoch.time_scale = TimeScale::TAI; + new_epoch } TimeScale::ET => { // first convert back to TAI - let mut tai_epoch = self.to_time_scale(TimeScale::TAI); + let mut tai_epoch = self.into_time_scale(TimeScale::TAI); // seconds past J2000 - //TODO: this operation is not feasible is Self is not PAST J2000 + //NB: this operation is not feasible is Self is not PAST J2000 let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; let mut seconds_since_j2000 = duration_since_j2000.to_seconds(); for _ in 0..5 { @@ -330,20 +331,21 @@ impl Epoch { } TimeScale::TDB => { // first convert back to TAI - let mut tai_epoch = self.to_time_scale(TimeScale::TAI); + let mut tai_epoch = self.into_time_scale(TimeScale::TAI); // seconds past J2000 - //TODO: this operation is not feasible is Self is not PAST J2000 + //NB: this operation is not feasible is Self is not PAST J2000 + //let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; let seconds_since_j2000 = duration_since_j2000.to_seconds(); let gamma = Self::inner_g(seconds_since_j2000); let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - tai_epoch += delta_tdb_tai; - tai_epoch += J2000_TO_J1900_DURATION; // TDB time scale is expressed past J2000 - tai_epoch.with_time_scale(TimeScale::TDB) + tai_epoch -= delta_tdb_tai; + tai_epoch.time_scale = TimeScale::TDB; + tai_epoch } ts => { // first convert back to TAI - let mut tai_epoch = self.to_time_scale(TimeScale::TAI); + let mut tai_epoch = self.into_time_scale(TimeScale::TAI); // leap second management if ts.uses_leap_seconds() { // new time scale supports leap seconds: add them @@ -356,22 +358,12 @@ impl Epoch { } _ => {} } - tai_epoch.with_time_scale(ts) + tai_epoch.time_scale = ts; + tai_epoch } } } - /// Assigns given TimeScale to self. - /// This is not a conversion operation, but - /// simply a definition. - /// To convert into a timescale, - /// you should use .into(TimeScale) - pub fn with_timescale(&self, ts: TimeScale) -> Self { - let mut s = self.clone(); - s.time_scale = ts; - s - } - #[must_use] /// Creates a new Epoch from a Duration as the time difference between this epoch and TAI reference epoch. pub const fn from_tai_duration(duration: Duration) -> Self { @@ -801,9 +793,9 @@ impl Epoch { Ok(Self { duration: duration_wrt_1900, - time_scale: TimeScale::UTC, + time_scale: TimeScale::TAI, } - .to_time_scale(time_scale)) + .into_time_scale(time_scale)) } #[must_use] @@ -860,8 +852,16 @@ impl Epoch { second: u8, nanos: u32, ) -> Result { - let if_tai = Self::maybe_from_gregorian_tai(year, month, day, hour, minute, second, nanos)?; - Ok(if_tai.to_time_scale(TimeScale::UTC)) + Self::maybe_from_gregorian( + year, + month, + day, + hour, + minute, + second, + nanos, + TimeScale::UTC, + ) } #[must_use] @@ -1143,9 +1143,15 @@ impl Epoch { 1.658e-3 * (g + 1.67e-2 * g.sin()).sin() } - pub(crate) fn compute_gregorian(duration_j1900: Duration) -> (i32, u8, u8, u8, u8, u8, u32) { + pub(crate) fn compute_gregorian( + duration: Duration, + ts: TimeScale, + ) -> (i32, u8, u8, u8, u8, u8, u32) { + let ts_offset = ts.tai_reference_epoch(); + let offset_duration = duration + ts_offset.duration; + let (sign, days, hours, minutes, seconds, milliseconds, microseconds, nanos) = - duration_j1900.decompose(); + offset_duration.decompose(); let days_f64 = if sign < 0 { -(days as f64) @@ -1154,8 +1160,7 @@ impl Epoch { }; let (mut year, mut days_in_year) = div_rem_f64(days_f64, DAYS_PER_YEAR_NLD); - // TAI is defined at 1900, so a negative time is before 1900 and positive is after 1900. - year += 1900; + year += 1900; // NB: this assumes all known time scales started after J1900 // Base calculation was on 365 days, so we need to remove one day in seconds per leap year // between 1900 and `year` @@ -1790,7 +1795,7 @@ impl Epoch { /// Returns this epoch with respect to the provided time scale. /// This is needed to correctly perform duration conversions in dynamical time scales (e.g. TDB). pub fn to_duration_in_time_scale(&self, ts: TimeScale) -> Duration { - self.to_time_scale(ts).duration + self.into_time_scale(ts).duration } /// Attempts to return the number of nanoseconds since the reference epoch of the provided time scale. @@ -1833,7 +1838,7 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in TAI pub fn to_tai_duration(&self) -> Duration { - self.to_time_scale(TimeScale::TAI).duration + self.into_time_scale(TimeScale::TAI).duration } #[must_use] @@ -1863,7 +1868,7 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in UTC pub fn to_utc_duration(&self) -> Duration { - self.to_time_scale(TimeScale::UTC).duration + self.into_time_scale(TimeScale::UTC).duration } #[must_use] @@ -2044,7 +2049,7 @@ impl Epoch { #[must_use] /// Returns `Duration` past QZSS time Epoch. pub fn to_qzsst_duration(&self) -> Duration { - self.to_time_scale(TimeScale::QZSST).duration + self.into_time_scale(TimeScale::QZSST).duration } /// Returns nanoseconds past QZSS Time Epoch, defined as UTC midnight of January 5th to 6th 1980 (cf. ). @@ -2309,7 +2314,8 @@ impl Epoch { /// assert_eq!("2017-01-14T00:31:55 UTC", dt.as_gregorian_utc_str().to_owned()); /// ``` pub fn to_gregorian_utc(&self) -> (i32, u8, u8, u8, u8, u8, u32) { - Self::compute_gregorian(self.to_utc_duration()) + let ts = TimeScale::UTC; + Self::compute_gregorian(self.to_duration_in_time_scale(ts), ts) } #[must_use] @@ -2329,7 +2335,8 @@ impl Epoch { /// assert_eq!(s, 0); /// ``` pub fn to_gregorian_tai(&self) -> (i32, u8, u8, u8, u8, u8, u32) { - Self::compute_gregorian(self.to_tai_duration()) + let ts = TimeScale::TAI; + Self::compute_gregorian(self.to_duration_in_time_scale(ts), ts) } #[cfg(feature = "ut1")] @@ -2415,14 +2422,6 @@ impl Epoch { Self::from_duration(self.to_duration().round(duration), self.time_scale) } - #[must_use] - /// Copies this epoch and sets it to the new time scale provided. - pub fn with_time_scale(&self, new_time_scale: TimeScale) -> Self { - let mut me = *self; - me.time_scale = new_time_scale; - me - } - #[must_use] /// Converts this epoch into the time of week, represented as a rolling week counter into that time scale /// and the number of nanoseconds elapsed in current week (since closest Sunday midnight). @@ -2538,7 +2537,7 @@ impl Epoch { #[must_use] /// Returns the duration since the start of the year pub fn duration_in_year(&self) -> Duration { - let year = Self::compute_gregorian(self.to_duration()).0; + let year = Self::compute_gregorian(self.duration, self.time_scale).0; let start_of_year = Self::from_gregorian(year, 1, 1, 0, 0, 0, 0, self.time_scale); self.to_duration() - start_of_year.to_duration() } @@ -2553,7 +2552,7 @@ impl Epoch { /// Returns the year and the days in the year so far (days of year). pub fn year_days_of_year(&self) -> (i32, f64) { ( - Self::compute_gregorian(self.to_duration()).0, + Self::compute_gregorian(self.duration, self.time_scale).0, self.day_of_year(), ) } @@ -2628,7 +2627,7 @@ impl Epoch { let (sign, days, _, _, _, milliseconds, microseconds, nanoseconds) = self.to_duration().decompose(); // Shadow other with the provided other epoch but in the correct time scale. - let other = other.to_time_scale(self.time_scale); + let other = other.into_time_scale(self.time_scale); Self::from_duration( Duration::compose( sign, @@ -2709,7 +2708,7 @@ impl Epoch { /// ``` pub fn with_hms_strict_from(&self, other: Self) -> Self { let (sign, days, _, _, _, _, _, _) = self.to_duration().decompose(); - let other = other.to_time_scale(self.time_scale); + let other = other.into_time_scale(self.time_scale); Self::from_duration( Duration::compose( sign, @@ -2726,7 +2725,7 @@ impl Epoch { } pub fn month_name(&self) -> MonthName { - let month = Self::compute_gregorian(self.to_duration()).1; + let month = Self::compute_gregorian(self.duration, self.time_scale).1; month.into() } @@ -2810,17 +2809,8 @@ impl Epoch { #[must_use] /// Converts the Epoch to Gregorian in the provided time scale and in the ISO8601 format with the time scale appended to the string pub fn to_gregorian_str(&self, time_scale: TimeScale) -> String { - let (y, mm, dd, hh, min, s, nanos) = Self::compute_gregorian(match time_scale { - TimeScale::TT => self.to_tt_duration(), - TimeScale::TAI => self.to_tai_duration(), - TimeScale::ET => self.to_et_duration_since_j1900(), - TimeScale::TDB => self.to_tdb_duration_since_j1900(), - TimeScale::UTC => self.to_utc_duration(), - // GPST and QZSST share the same properties - TimeScale::GPST | TimeScale::QZSST => self.to_utc_duration(), - TimeScale::GST => self.to_utc_duration(), - TimeScale::BDT => self.to_utc_duration(), - }); + let (y, mm, dd, hh, min, s, nanos) = + Self::compute_gregorian(self.duration, self.time_scale); if nanos == 0 { format!( @@ -2838,7 +2828,9 @@ impl Epoch { #[cfg(feature = "std")] /// Returns this epoch in UTC in the RFC3339 format pub fn to_rfc3339(&self) -> String { - let (y, mm, dd, hh, min, s, nanos) = Self::compute_gregorian(self.to_utc_duration()); + let ts = TimeScale::UTC; + let (y, mm, dd, hh, min, s, nanos) = + Self::compute_gregorian(self.to_duration_in_time_scale(ts), ts); if nanos == 0 { format!( "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}+00:00", @@ -2995,7 +2987,7 @@ impl fmt::Debug for Epoch { /// Print this epoch in Gregorian in the time scale used at initialization fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let (y, mm, dd, hh, min, s, nanos) = - Self::compute_gregorian(self.to_time_scale(TimeScale::TAI).duration); + Self::compute_gregorian(self.duration, self.time_scale); if nanos == 0 { write!( f, @@ -3016,7 +3008,8 @@ impl fmt::Display for Epoch { /// The default format of an epoch is in UTC fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let ts = TimeScale::UTC; - let (y, mm, dd, hh, min, s, nanos) = Self::compute_gregorian(self.to_utc_duration()); + let (y, mm, dd, hh, min, s, nanos) = + Self::compute_gregorian(self.to_duration_in_time_scale(ts), ts); if nanos == 0 { write!( f, @@ -3037,7 +3030,8 @@ impl fmt::LowerHex for Epoch { /// Prints the Epoch in TAI fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let ts = TimeScale::TAI; - let (y, mm, dd, hh, min, s, nanos) = Self::compute_gregorian(self.to_tai_duration()); + let (y, mm, dd, hh, min, s, nanos) = + Self::compute_gregorian(self.to_duration_in_time_scale(ts), ts); if nanos == 0 { write!( f, @@ -3058,7 +3052,8 @@ impl fmt::UpperHex for Epoch { /// Prints the Epoch in TT fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let ts = TimeScale::TT; - let (y, mm, dd, hh, min, s, nanos) = Self::compute_gregorian(self.to_tt_duration()); + let (y, mm, dd, hh, min, s, nanos) = + Self::compute_gregorian(self.to_duration_in_time_scale(ts), ts); if nanos == 0 { write!( f, @@ -3080,7 +3075,7 @@ impl fmt::LowerExp for Epoch { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let ts = TimeScale::TDB; let (y, mm, dd, hh, min, s, nanos) = - Self::compute_gregorian(self.to_tdb_duration_since_j1900()); + Self::compute_gregorian(self.to_duration_in_time_scale(ts), ts); if nanos == 0 { write!( f, @@ -3102,7 +3097,7 @@ impl fmt::UpperExp for Epoch { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let ts = TimeScale::ET; let (y, mm, dd, hh, min, s, nanos) = - Self::compute_gregorian(self.to_et_duration_since_j1900()); + Self::compute_gregorian(self.to_duration_in_time_scale(ts), ts); if nanos == 0 { write!( f, @@ -3328,6 +3323,7 @@ fn formal_epoch_reciprocity_tdb() { let time_scale: TimeScale = TimeScale::TDB; let epoch: Epoch = Epoch::from_duration(duration, time_scale); let out_duration = epoch.to_duration_in_time_scale(time_scale); + assert_eq!((out_duration - duration).to_seconds(), 0.0); assert_eq!(out_duration.centuries, duration.centuries); assert_eq!(out_duration.nanoseconds, duration.nanoseconds); let error = (out_duration.nanoseconds - duration.nanoseconds) as f64; diff --git a/tests/epoch.rs b/tests/epoch.rs index e2834072..48821995 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -15,6 +15,13 @@ use core::f64::EPSILON; #[cfg(not(feature = "std"))] use std::f64::EPSILON; +#[test] +fn test_basic_ops() { + let utc_epoch = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14) + 1 * Unit::Second; + let epoch_bis = Epoch::from_gregorian_utc_hms(2017, 1, 14, 00, 00, 1); + assert_eq!(utc_epoch, epoch_bis); +} + #[test] fn test_const_ops() { // Tests that multiplying a constant with a unit returns the correct number in that same unit @@ -28,13 +35,13 @@ fn test_const_ops() { fn test_from_gregorian() { let utc_epoch = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14); let tai_epoch = Epoch::from_gregorian_at_midnight(2017, 1, 14, TimeScale::TAI); - assert_eq!(utc_epoch.to_time_scale(TimeScale::TAI), tai_epoch); - assert_eq!(utc_epoch, tai_epoch.to_time_scale(TimeScale::UTC)); + assert_eq!(utc_epoch.into_time_scale(TimeScale::TAI), tai_epoch); + assert_eq!(utc_epoch, tai_epoch.into_time_scale(TimeScale::UTC)); let utc_epoch = Epoch::from_gregorian_utc(2016, 12, 31, 23, 59, 59, 0); let tai_epoch = Epoch::from_gregorian(2016, 12, 31, 23, 59, 59, 0, TimeScale::TAI); - assert_eq!(utc_epoch.to_time_scale(TimeScale::TAI), tai_epoch); - assert_eq!(utc_epoch, tai_epoch.to_time_scale(TimeScale::UTC)); + assert_eq!(utc_epoch.into_time_scale(TimeScale::TAI), tai_epoch); + assert_eq!(utc_epoch, tai_epoch.into_time_scale(TimeScale::UTC)); } #[allow(clippy::float_equality_without_abs)] @@ -576,11 +583,11 @@ fn unix() { let unix_epoch = Epoch::from_gregorian_utc_at_midnight(1970, 1, 1); assert_eq!( - format!("{}", unix_epoch.to_time_scale(TimeScale::UTC)), + format!("{}", unix_epoch.into_time_scale(TimeScale::UTC)), "1970-01-01T00:00:00 UTC" ); assert_eq!( - format!("{:x}", unix_epoch.to_time_scale(TimeScale::TAI)), + format!("{:x}", unix_epoch.into_time_scale(TimeScale::TAI)), "1970-01-01T00:00:00 TAI" ); // Print as UNIX seconds @@ -1338,7 +1345,7 @@ fn test_add_durations_over_leap_seconds() { // Noon UTC after the first leap second is in fact ten seconds _after_ noon TAI. // Hence, there are as many TAI seconds since Epoch between UTC Noon and TAI Noon + 10s. let pre_ls_utc = Epoch::from_gregorian_utc_at_noon(1971, 12, 31); - let pre_ls_tai = pre_ls_utc.to_time_scale(TimeScale::TAI); + let pre_ls_tai = pre_ls_utc.into_time_scale(TimeScale::TAI); // Before the first leap second, there is no time difference between both epochs (because only IERS announced leap seconds are accounted for by default). assert_eq!(pre_ls_utc - pre_ls_tai, Duration::ZERO); @@ -1439,7 +1446,7 @@ fn test_weekday() { TimeScale::TT, TimeScale::UTC, ] { - let e_ts = e.to_time_scale(new_time_scale); + let e_ts = e.into_time_scale(new_time_scale); assert_eq!(e_ts.weekday(), expect, "error with {new_time_scale}"); } }; @@ -1519,7 +1526,7 @@ fn test_get_time() { let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 10, 11, 12, 13); let other_utc = Epoch::from_gregorian_utc(2024, 12, 01, 20, 21, 22, 23); - let other = other_utc.to_time_scale(TimeScale::TDB); + let other = other_utc.into_time_scale(TimeScale::TDB); assert_eq!( epoch.with_hms_from(other), @@ -1577,7 +1584,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (0, 10 * 1_000_000_000 + 10)); // TAI<=>UTC - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1597,7 +1604,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 00)); assert_eq!(epoch.to_time_of_week(), (2238, 345_618_000_000_000)); - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(utc_wk, utc_tow, TimeScale::UTC), @@ -1605,7 +1612,7 @@ fn test_time_of_week() { ); // GPST and QZSST share the same properties at all times - let epoch_qzsst = epoch.to_time_scale(TimeScale::QZSST); + let epoch_qzsst = epoch.into_time_scale(TimeScale::QZSST); assert_eq!(epoch.to_gregorian_utc(), epoch_qzsst.to_gregorian_utc()); let gps_qzss_offset = @@ -1617,7 +1624,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (1980, 01, 06, 01, 00, 0 + 18, 00)); assert_eq!(epoch.to_time_of_week(), (0, 3_618_000_000_000)); - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(utc_wk, utc_tow, TimeScale::UTC), @@ -1638,7 +1645,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (24, 306_457_000_000_000)); // <=>UTC - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1650,7 +1657,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 01)); // <=>UTC - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1662,7 +1669,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 12, 00, 00, 00)); // <=>UTC - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1674,7 +1681,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 15, 27, 19, 10)); // <=>UTC - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1688,7 +1695,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (0, 3_600_000_000_000)); // <=>UTC - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1704,7 +1711,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (1, 128 * 3600 * 1_000_000_000)); // <=>UTC - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1725,7 +1732,7 @@ fn test_time_of_week() { ); // <=>UTC - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1748,7 +1755,7 @@ fn test_time_of_week() { ); // <=>UTC - let epoch_utc = epoch.to_time_scale(TimeScale::UTC); + let epoch_utc = epoch.into_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1769,7 +1776,7 @@ fn test_day_of_year() { TimeScale::ET, TimeScale::TDB, ] { - let epoch = utc_epoch.to_time_scale(*ts); + let epoch = utc_epoch.into_time_scale(*ts); let (year, days) = epoch.year_days_of_year(); let rebuilt = Epoch::from_day_of_year(year, days, *ts); if *ts == TimeScale::ET || *ts == TimeScale::TDB { @@ -1894,9 +1901,9 @@ fn test_epoch_formatter() { #[test] fn test_to_tai_time_scale() { let j1900_ref = J1900_REF_EPOCH; - assert_eq!(j1900_ref, j1900_ref.to_time_scale(TimeScale::TAI)); + assert_eq!(j1900_ref, j1900_ref.into_time_scale(TimeScale::TAI)); let j2000_ref = J2000_REF_EPOCH; - assert_eq!(j2000_ref, j2000_ref.to_time_scale(TimeScale::TAI)); + assert_eq!(j2000_ref, j2000_ref.into_time_scale(TimeScale::TAI)); let j2000_to_j1900 = j2000_ref - j1900_ref; assert_eq!(j2000_to_j1900, J2000_TO_J1900_DURATION); } @@ -1906,13 +1913,13 @@ fn test_to_utc_time_scale() { let gpst_tai_ref = TimeScale::GPST.tai_reference_epoch(); // there were 19 leap seconds on the day GPST was "started" let gpst_utc_delta = 19.0; // leaps - let gpst_utc_ref = gpst_tai_ref.to_time_scale(TimeScale::UTC); + let gpst_utc_ref = gpst_tai_ref.into_time_scale(TimeScale::UTC); assert_eq!((gpst_utc_ref - gpst_tai_ref).to_seconds(), gpst_utc_delta); let bdt_tai_ref = TimeScale::BDT.tai_reference_epoch(); // there were 33 leap seconds on the day BDT was "started" let bdt_utc_delta = 33.0; // leaps - let bdt_utc_ref = bdt_tai_ref.to_time_scale(TimeScale::UTC); + let bdt_utc_ref = bdt_tai_ref.into_time_scale(TimeScale::UTC); assert_eq!((bdt_utc_ref - bdt_tai_ref).to_seconds(), bdt_utc_delta); } From 6df531a4c785a630a8aa4e9ab34210cbfe4b75b6 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sun, 11 Jun 2023 22:08:31 -0600 Subject: [PATCH 09/37] Fixed unit tests Removed deprecated module --- src/deprecated.rs | 617 ---------------------------------------------- src/epoch.rs | 164 ++++++------ src/lib.rs | 12 +- 3 files changed, 94 insertions(+), 699 deletions(-) delete mode 100644 src/deprecated.rs diff --git a/src/deprecated.rs b/src/deprecated.rs deleted file mode 100644 index f40b6eb8..00000000 --- a/src/deprecated.rs +++ /dev/null @@ -1,617 +0,0 @@ -/* - * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) - * This Source Code Form is subject to the terms of the Apache - * v. 2.0. If a copy of the Apache License was not distributed with this - * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. - * - * Documentation: https://nyxspace.com/ - */ - -use crate::prelude::*; - -#[deprecated(since = "3.5.0", note = "TimeSystem has been renamed to TimeScale")] -pub type TimeSystem = TimeScale; - -impl Duration { - #[must_use] - #[deprecated(note = "Prefer to_seconds()", since = "3.5.0")] - pub fn in_seconds(&self) -> f64 { - self.to_seconds() - } - - /// Returns the value of this duration in the requested unit. - #[must_use] - #[deprecated(note = "Prefer to_unit()", since = "3.5.0")] - pub fn in_unit(&self, unit: Unit) -> f64 { - self.to_unit(unit) - } -} - -impl Epoch { - #[must_use] - /// Get the accumulated number of leap seconds up to this Epoch accounting only for the IERS leap seconds. - /// For the leap seconds _and_ the scaling in "prehistoric" times from 1960 to 1972, use `leap_seconds()`. - #[deprecated(note = "Prefer leap_seconds_iers or leap_seconds", since = "3.4.0")] - pub fn get_num_leap_seconds(&self) -> i32 { - self.leap_seconds_iers() - } - - #[must_use] - #[deprecated(note = "Prefer as_tdb_duration", since = "3.4.0")] - /// Returns the duration since Dynamic Barycentric Time (TDB) J2000 (used for Archinal et al. rotations) - pub fn as_tdb_duration_since_j2000(&self) -> Duration { - self.to_tdb_duration() - } - - #[must_use] - #[deprecated(note = "Prefer as_et_duration", since = "3.4.0")] - /// Returns the duration since Ephemeris Time (ET) J2000 (used for Archinal et al. rotations) - pub fn as_et_duration_since_j2000(&self) -> Duration { - self.to_et_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_duration(&self) -> Duration { - self.to_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_duration_in_time_scale(&self, time_scale: TimeScale) -> Duration { - self.to_duration_in_time_scale(time_scale) - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_duration_since_j1900(&self) -> Duration { - self.to_duration_since_j1900() - } - - /* - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_duration_since_j1900_in_time_scale(&self, time_scale: TimeScale) -> Duration { - self.to_duration_since_j1900_in_time_scale(time_scale) - } - */ - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tai_seconds(&self) -> f64 { - self.to_tai_seconds() - } - - /* - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub const fn as_tai_duration(&self) -> Duration { - self.to_tai_duration() - } - */ - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tai(&self, unit: Unit) -> f64 { - self.to_tai(unit) - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tai_days(&self) -> f64 { - self.to_tai_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_utc_seconds(&self) -> f64 { - self.to_utc_seconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_utc_duration(&self) -> Duration { - self.to_utc_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_utc(&self, unit: Unit) -> f64 { - self.to_utc(unit) - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_utc_days(&self) -> f64 { - self.to_utc_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_mjd_tai_days(&self) -> f64 { - self.to_mjd_tai_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_mjd_tai_seconds(&self) -> f64 { - self.to_mjd_tai_seconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_mjd_tai(&self, unit: Unit) -> f64 { - self.to_mjd_tai(unit) - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_mjd_utc_days(&self) -> f64 { - self.to_mjd_utc_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_mjd_utc(&self, unit: Unit) -> f64 { - self.to_mjd_utc(unit) - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_mjd_utc_seconds(&self) -> f64 { - self.to_mjd_utc_seconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_tai_days(&self) -> f64 { - self.to_jde_tai_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_tai(&self, unit: Unit) -> f64 { - self.to_jde_tai(unit) - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_tai_duration(&self) -> Duration { - self.to_jde_tai_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_tai_seconds(&self) -> f64 { - self.to_jde_tai_seconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_utc_days(&self) -> f64 { - self.to_jde_utc_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_utc_duration(&self) -> Duration { - self.to_jde_utc_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_utc_seconds(&self) -> f64 { - self.to_jde_utc_seconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tt_seconds(&self) -> f64 { - self.to_tt_seconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tt_duration(&self) -> Duration { - self.to_tt_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tt_days(&self) -> f64 { - self.to_tt_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tt_centuries_j2k(&self) -> f64 { - self.to_tt_centuries_j2k() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tt_since_j2k(&self) -> Duration { - self.to_tt_since_j2k() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_tt_days(&self) -> f64 { - self.to_jde_tt_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_tt_duration(&self) -> Duration { - self.to_jde_tt_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_mjd_tt_days(&self) -> f64 { - self.to_mjd_tt_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_mjd_tt_duration(&self) -> Duration { - self.to_mjd_tt_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_gpst_seconds(&self) -> f64 { - self.to_gpst_seconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_gpst_duration(&self) -> Duration { - self.to_gpst_duration() - } - - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_gpst_nanoseconds(&self) -> Result { - self.to_gpst_nanoseconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_gpst_days(&self) -> f64 { - self.to_gpst_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_unix(&self, unit: Unit) -> f64 { - self.to_unix(unit) - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_unix_seconds(&self) -> f64 { - self.to_unix_seconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_unix_milliseconds(&self) -> f64 { - self.to_unix_milliseconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_unix_days(&self) -> f64 { - self.to_unix_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_et_seconds(&self) -> f64 { - self.to_et_seconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_et_duration_since_j1900(&self) -> Duration { - self.to_et_duration_since_j1900() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_et_duration(&self) -> Duration { - self.to_et_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tdb_duration(&self) -> Duration { - self.to_tdb_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tdb_seconds(&self) -> f64 { - self.to_tdb_seconds() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tdb_duration_since_j1900(&self) -> Duration { - self.to_tdb_duration_since_j1900() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_et_days(&self) -> f64 { - self.to_jde_et_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_et_duration(&self) -> Duration { - self.to_jde_et_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_et(&self, unit: Unit) -> f64 { - self.to_jde_et(unit) - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_tdb_duration(&self) -> Duration { - self.to_jde_tdb_duration() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_jde_tdb_days(&self) -> f64 { - self.to_jde_tdb_days() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tdb_days_since_j2000(&self) -> f64 { - self.to_tdb_days_since_j2000() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_tdb_centuries_since_j2000(&self) -> f64 { - self.to_tdb_centuries_since_j2000() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_et_days_since_j2000(&self) -> f64 { - self.to_et_days_since_j2000() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_et_centuries_since_j2000(&self) -> f64 { - self.to_et_centuries_since_j2000() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_gregorian_utc(&self) -> (i32, u8, u8, u8, u8, u8, u32) { - self.to_gregorian_utc() - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_gregorian_tai(&self) -> (i32, u8, u8, u8, u8, u8, u32) { - self.to_gregorian_tai() - } -} - -#[cfg(feature = "std")] -impl Epoch { - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_gregorian_utc_str(&self) -> String { - format!("{}", self) - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_gregorian_tai_str(&self) -> String { - format!("{:x}", self) - } - - #[must_use] - #[deprecated( - note = "Prefix for this function is now `to_` instead of `as_`.", - since = "3.5.0" - )] - pub fn as_gregorian_str(&self, ts: TimeScale) -> String { - self.to_gregorian_str(ts) - } -} diff --git a/src/epoch.rs b/src/epoch.rs index 1520b587..2dcd060d 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -41,7 +41,7 @@ use pyo3::types::PyType; use crate::leap_seconds_file::LeapSecondsFile; #[cfg(feature = "serde")] -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use core::str::FromStr; #[cfg(feature = "std")] @@ -126,7 +126,6 @@ const CUMULATIVE_DAYS_FOR_MONTH: [u16; 12] = { #[derive(Copy, Clone, Default, Eq, Hash)] #[repr(C)] #[cfg_attr(feature = "python", pyclass)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Epoch { /// An Epoch is always stored as the duration since the beginning of its time scale pub duration: Duration, @@ -134,6 +133,26 @@ pub struct Epoch { pub time_scale: TimeScale, } +impl Serialize for Epoch { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = self.to_string(); // Assuming `Display` is implemented for `Epoch` + serializer.serialize_str(&s) + } +} + +impl<'de> Deserialize<'de> for Epoch { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Epoch::from_str(&s).map_err(serde::de::Error::custom) + } +} + impl Sub for Epoch { type Output = Duration; @@ -291,75 +310,80 @@ impl Epoch { #[must_use] /// Converts self to another time scale pub fn into_time_scale(&self, ts: TimeScale) -> Self { - match ts { - TimeScale::TAI => { - let mut new_epoch = self.clone(); - // if previous time scale supported leap seconds: remove them - if self.time_scale.uses_leap_seconds() { - new_epoch -= new_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - } - // conversion to TAI: remove time scale reference point - new_epoch.duration -= ts.tai_reference_epoch().duration; - new_epoch.time_scale = TimeScale::TAI; - new_epoch - } - TimeScale::ET => { - // first convert back to TAI - let mut tai_epoch = self.into_time_scale(TimeScale::TAI); - // seconds past J2000 - //NB: this operation is not feasible is Self is not PAST J2000 - let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; - let mut seconds_since_j2000 = duration_since_j2000.to_seconds(); - for _ in 0..5 { - seconds_since_j2000 += -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds_since_j2000 - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_since_j2000).sin()) - .sin(); + if ts == self.time_scale { + // Do nothing + *self + } else { + match ts { + TimeScale::TAI => { + let mut new_epoch = *self; + // if previous time scale supported leap seconds: remove them + if self.time_scale.uses_leap_seconds() { + new_epoch -= new_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + } + // conversion to TAI: remove time scale reference point + new_epoch.duration -= ts.tai_reference_epoch().duration; + new_epoch.time_scale = TimeScale::TAI; + new_epoch } - // At this point, we have a good estimate of the number of seconds - // of this epoch. Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds_since_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); - // Match the SPICE by changing the UTC definition - Self { - duration: (tai_epoch.duration.to_seconds() - delta_et_tai) * Unit::Second - + J2000_TO_J1900_DURATION, - time_scale: TimeScale::ET, + TimeScale::ET => { + // first convert back to TAI + let tai_epoch = self.into_time_scale(TimeScale::TAI); + // seconds past J2000 + //NB: this operation is not feasible is Self is not PAST J2000 + let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; + let mut seconds_since_j2000 = duration_since_j2000.to_seconds(); + for _ in 0..5 { + seconds_since_j2000 += -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds_since_j2000 + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_since_j2000).sin()) + .sin(); + } + // At this point, we have a good estimate of the number of seconds + // of this epoch. Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds_since_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); + // Match the SPICE by changing the UTC definition + Self { + duration: (tai_epoch.duration.to_seconds() - delta_et_tai) * Unit::Second + + J2000_TO_J1900_DURATION, + time_scale: TimeScale::ET, + } } - } - TimeScale::TDB => { - // first convert back to TAI - let mut tai_epoch = self.into_time_scale(TimeScale::TAI); - // seconds past J2000 - //NB: this operation is not feasible is Self is not PAST J2000 - //let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; - let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; - let seconds_since_j2000 = duration_since_j2000.to_seconds(); - let gamma = Self::inner_g(seconds_since_j2000); - let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - tai_epoch -= delta_tdb_tai; - tai_epoch.time_scale = TimeScale::TDB; - tai_epoch - } - ts => { - // first convert back to TAI - let mut tai_epoch = self.into_time_scale(TimeScale::TAI); - // leap second management - if ts.uses_leap_seconds() { - // new time scale supports leap seconds: add them - tai_epoch += tai_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + TimeScale::TDB => { + // first convert back to TAI + let mut tai_epoch = self.into_time_scale(TimeScale::TAI); + // seconds past J2000 + //NB: this operation is not feasible is Self is not PAST J2000 + //let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; + let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; + let seconds_since_j2000 = duration_since_j2000.to_seconds(); + let gamma = Self::inner_g(seconds_since_j2000); + let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + tai_epoch -= delta_tdb_tai; + tai_epoch.time_scale = TimeScale::TDB; + tai_epoch } - // time scale specificities - match ts { - TimeScale::TT => { - tai_epoch -= TT_OFFSET_MS * Unit::Millisecond; + ts => { + // first convert back to TAI + let mut tai_epoch = self.into_time_scale(TimeScale::TAI); + // leap second management + if ts.uses_leap_seconds() { + // new time scale supports leap seconds: add them + tai_epoch += tai_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + } + // time scale specificities + match ts { + TimeScale::TT => { + tai_epoch -= TT_OFFSET_MS * Unit::Millisecond; + } + _ => {} } - _ => {} + tai_epoch.time_scale = ts; + tai_epoch } - tai_epoch.time_scale = ts; - tai_epoch } } } @@ -793,9 +817,8 @@ impl Epoch { Ok(Self { duration: duration_wrt_1900, - time_scale: TimeScale::TAI, - } - .into_time_scale(time_scale)) + time_scale, + }) } #[must_use] @@ -3252,8 +3275,7 @@ fn cumulative_days_for_month() { #[cfg(feature = "serde")] fn test_serdes() { let e = Epoch::from_gregorian_utc(2020, 01, 01, 0, 0, 0, 0); - let content = - r#"{"duration":{"centuries":1,"nanoseconds":631065637000000000},"time_scale":"UTC"}"#; + let content = r#""2020-01-01T00:00:00 UTC""#; assert_eq!(content, serde_json::to_string(&e).unwrap()); let parsed: Epoch = serde_json::from_str(content).unwrap(); assert_eq!(e, parsed); diff --git a/src/lib.rs b/src/lib.rs index 51831319..3d0ab405 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,12 +53,6 @@ pub const SECONDS_PER_YEAR: f64 = 31_557_600.0; pub const SECONDS_PER_YEAR_I64: i64 = 31_557_600; /// `SECONDS_PER_TROPICAL_YEAR` corresponds to the number of seconds per tropical year from [NAIF SPICE](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/tyear_c.html). pub const SECONDS_PER_TROPICAL_YEAR: f64 = 31_556_925.974_7; -/// `SECONDS_PER_SIDERAL_YEAR` corresponds to the number of seconds per sidereal year from [NIST](https://www.nist.gov/pml/special-publication-811/nist-guide-si-appendix-b-conversion-factors/nist-guide-si-appendix-b9#TIME). -#[deprecated( - since = "3.8.0", - note = "Use SECONDS_PER_SIDEREAL_YEAR instead (does not have the typo)" -)] -pub const SECONDS_PER_SIDERAL_YEAR: f64 = 31_558_150.0; /// `SECONDS_PER_SIDEREAL_YEAR` corresponds to the number of seconds per sidereal year from [NIST](https://www.nist.gov/pml/special-publication-811/nist-guide-si-appendix-b-conversion-factors/nist-guide-si-appendix-b9#TIME). pub const SECONDS_PER_SIDEREAL_YEAR: f64 = 31_558_150.0; @@ -123,15 +117,11 @@ mod leap_seconds_file; #[cfg(feature = "ut1")] pub mod ut1; -/// This module defines all of the deprecated methods. -mod deprecated; - #[allow(deprecated)] pub mod prelude { pub use crate::efmt::{Format, Formatter}; pub use crate::{ - deprecated::TimeSystem, Duration, Epoch, Errors, Freq, Frequencies, TimeScale, TimeSeries, - TimeUnits, Unit, Weekday, + Duration, Epoch, Errors, Freq, Frequencies, TimeScale, TimeSeries, TimeUnits, Unit, Weekday, }; } From c6218efbb4c2cb6185f8ecdd32aea281656236cd Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sun, 11 Jun 2023 23:10:46 -0600 Subject: [PATCH 10/37] Spread out duration.rs into a module Update copyright --- .gitignore | 3 +- examples/python/basic.py | 2 +- examples/python/timescales.py | 2 +- pyproject.toml | 2 +- src/asn1der.rs | 2 +- src/duration.rs | 1611 --------------------------------- src/duration/kani.rs | 121 +++ src/duration/mod.rs | 764 ++++++++++++++++ src/duration/ops.rs | 377 ++++++++ src/duration/parse.rs | 184 ++++ src/duration/python.rs | 314 +++++++ src/duration/std.rs | 45 + src/efmt/consts.rs | 2 +- src/efmt/format.rs | 2 +- src/efmt/formatter.rs | 2 +- src/efmt/mod.rs | 2 +- src/epoch.rs | 12 +- src/errors.rs | 2 +- src/leap_seconds.rs | 2 +- src/leap_seconds_file.rs | 2 +- src/lib.rs | 2 +- src/month.rs | 2 +- src/parser.rs | 2 +- src/python.rs | 2 +- src/timescale.rs | 2 +- src/timeseries.rs | 2 +- src/timeunits.rs | 2 +- src/ut1.rs | 2 +- src/weekday.rs | 2 +- tests/python/test_duration.py | 8 + 30 files changed, 1841 insertions(+), 1638 deletions(-) delete mode 100644 src/duration.rs create mode 100644 src/duration/kani.rs create mode 100644 src/duration/mod.rs create mode 100644 src/duration/ops.rs create mode 100644 src/duration/parse.rs create mode 100644 src/duration/python.rs create mode 100644 src/duration/std.rs create mode 100644 tests/python/test_duration.py diff --git a/.gitignore b/.gitignore index 6eb156e4..8d58288b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ **/*.rs.bk Cargo.lock .env -dist/ \ No newline at end of file +dist/ +.venv \ No newline at end of file diff --git a/examples/python/basic.py b/examples/python/basic.py index c2e48209..60e202ef 100644 --- a/examples/python/basic.py +++ b/examples/python/basic.py @@ -1,6 +1,6 @@ """ * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/examples/python/timescales.py b/examples/python/timescales.py index 162e40a9..fce10512 100644 --- a/examples/python/timescales.py +++ b/examples/python/timescales.py @@ -1,6 +1,6 @@ """ * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/pyproject.toml b/pyproject.toml index fd60678d..3be79017 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=0.13,<0.14"] +requires = ["maturin>=1.1,<1.2"] build-backend = "maturin" [project] diff --git a/src/asn1der.rs b/src/asn1der.rs index 43fe14a6..b35ed9cd 100644 --- a/src/asn1der.rs +++ b/src/asn1der.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/duration.rs b/src/duration.rs deleted file mode 100644 index 684d7c36..00000000 --- a/src/duration.rs +++ /dev/null @@ -1,1611 +0,0 @@ -/* - * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) - * This Source Code Form is subject to the terms of the Apache - * v. 2.0. If a copy of the Apache License was not distributed with this - * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. - * - * Documentation: https://nyxspace.com/ - */ - -use crate::ParsingErrors; -use crate::{Errors, SECONDS_PER_CENTURY, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE}; - -pub use crate::{Freq, Frequencies, TimeUnits, Unit}; - -#[cfg(feature = "std")] -extern crate core; -use core::cmp::Ordering; -use core::convert::TryInto; -use core::fmt; -use core::hash::{Hash, Hasher}; -use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; - -#[cfg(feature = "serde")] -use serde_derive::{Deserialize, Serialize}; - -use core::str::FromStr; - -#[cfg(feature = "python")] -use pyo3::prelude::*; -#[cfg(feature = "python")] -use pyo3::pyclass::CompareOp; -#[cfg(feature = "python")] -use pyo3::types::PyType; - -#[cfg(not(feature = "std"))] -use num_traits::Float; - -#[cfg(kani)] -use kani::Arbitrary; - -pub const DAYS_PER_CENTURY_U64: u64 = 36_525; -pub const NANOSECONDS_PER_MICROSECOND: u64 = 1_000; -pub const NANOSECONDS_PER_MILLISECOND: u64 = 1_000 * NANOSECONDS_PER_MICROSECOND; -pub const NANOSECONDS_PER_SECOND: u64 = 1_000 * NANOSECONDS_PER_MILLISECOND; -pub(crate) const NANOSECONDS_PER_SECOND_U32: u32 = 1_000_000_000; -pub const NANOSECONDS_PER_MINUTE: u64 = 60 * NANOSECONDS_PER_SECOND; -pub const NANOSECONDS_PER_HOUR: u64 = 60 * NANOSECONDS_PER_MINUTE; -pub const NANOSECONDS_PER_DAY: u64 = 24 * NANOSECONDS_PER_HOUR; -pub const NANOSECONDS_PER_CENTURY: u64 = DAYS_PER_CENTURY_U64 * NANOSECONDS_PER_DAY; - -/// Defines generally usable durations for nanosecond precision valid for 32,768 centuries in either direction, and only on 80 bits / 10 octets. -/// -/// **Important conventions:** -/// 1. The negative durations can be mentally modeled "BC" years. One hours before 01 Jan 0000, it was "-1" years but 365 days and 23h into the current day. -/// It was decided that the nanoseconds corresponds to the nanoseconds _into_ the current century. In other words, -/// a duration with centuries = -1 and nanoseconds = 0 is _a smaller duration_ (further from zero) than centuries = -1 and nanoseconds = 1. -/// Duration zero minus one nanosecond returns a century of -1 and a nanosecond set to the number of nanoseconds in one century minus one. -/// That difference is exactly 1 nanoseconds, where the former duration is "closer to zero" than the latter. -/// As such, the largest negative duration that can be represented sets the centuries to i16::MAX and its nanoseconds to NANOSECONDS_PER_CENTURY. -/// 2. It was also decided that opposite durations are equal, e.g. -15 minutes == 15 minutes. If the direction of time matters, use the signum function. -#[derive(Clone, Copy, Debug, PartialOrd, Eq, Ord)] -#[repr(C)] -#[cfg_attr(feature = "python", pyclass)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Duration { - pub(crate) centuries: i16, - pub(crate) nanoseconds: u64, -} - -#[cfg(kani)] -impl Arbitrary for Duration { - #[inline(always)] - fn any() -> Self { - let centuries: i16 = kani::any(); - let nanoseconds: u64 = kani::any(); - - Duration::from_parts(centuries, nanoseconds) - } -} - -impl PartialEq for Duration { - fn eq(&self, other: &Self) -> bool { - if self.centuries == other.centuries { - self.nanoseconds == other.nanoseconds - } else if (self.centuries.saturating_sub(other.centuries)).saturating_abs() == 1 - && (self.centuries == 0 || other.centuries == 0) - { - // Special case where we're at the zero crossing - if self.centuries < 0 { - // Self is negative, - (NANOSECONDS_PER_CENTURY - self.nanoseconds) == other.nanoseconds - } else { - // Other is negative - (NANOSECONDS_PER_CENTURY - other.nanoseconds) == self.nanoseconds - } - } else { - false - } - } -} - -impl Hash for Duration { - fn hash(&self, hasher: &mut H) { - self.centuries.hash(hasher); - self.nanoseconds.hash(hasher); - } -} - -impl Default for Duration { - fn default() -> Self { - Duration::ZERO - } -} - -// Defines the methods that should be classmethods in Python, but must be redefined as per https://github.com/PyO3/pyo3/issues/1003#issuecomment-844433346 -impl Duration { - /// Builds a new duration from the number of centuries and the number of nanoseconds - #[must_use] - #[deprecated(note = "Prefer from_parts()", since = "3.6.0")] - pub fn new(centuries: i16, nanoseconds: u64) -> Self { - let mut out = Self { - centuries, - nanoseconds, - }; - out.normalize(); - out - } - - #[must_use] - /// Create a normalized duration from its parts - pub fn from_parts(centuries: i16, nanoseconds: u64) -> Self { - let mut me = Self { - centuries, - nanoseconds, - }; - me.normalize(); - me - } - - #[must_use] - /// Converts the total nanoseconds as i128 into this Duration (saving 48 bits) - pub fn from_total_nanoseconds(nanos: i128) -> Self { - // In this function, we simply check that the input data can be casted. The `normalize` function will check whether more work needs to be done. - if nanos == 0 { - Self::ZERO - } else { - let centuries_i128 = nanos.div_euclid(NANOSECONDS_PER_CENTURY.into()); - let remaining_nanos_i128 = nanos.rem_euclid(NANOSECONDS_PER_CENTURY.into()); - if centuries_i128 > i16::MAX.into() { - Self::MAX - } else if centuries_i128 < i16::MIN.into() { - Self::MIN - } else { - // We know that the centuries fit, and we know that the nanos are less than the number - // of nanos per centuries, and rem_euclid guarantees that it's positive, so the - // casting will work fine every time. - Self::from_parts(centuries_i128 as i16, remaining_nanos_i128 as u64) - } - } - } - - #[must_use] - /// Create a new duration from the truncated nanoseconds (+/- 2927.1 years of duration) - pub fn from_truncated_nanoseconds(nanos: i64) -> Self { - if nanos < 0 { - let ns = nanos.unsigned_abs(); - // Note: i64::MIN corresponds to a duration just past -3 centuries, so we can't hit the Duration::MIN here. - let extra_centuries = ns.div_euclid(NANOSECONDS_PER_CENTURY); - let rem_nanos = ns.rem_euclid(NANOSECONDS_PER_CENTURY); - Self::from_parts( - -1 - (extra_centuries as i16), - NANOSECONDS_PER_CENTURY - rem_nanos, - ) - } else { - Self::from_parts(0, nanos.unsigned_abs()) - } - } - - /// Creates a new duration from the provided unit - #[must_use] - pub fn from_f64(value: f64, unit: Unit) -> Self { - unit * value - } - - /// Creates a new duration from the provided number of days - #[must_use] - pub fn from_days(value: f64) -> Self { - value * Unit::Day - } - - /// Creates a new duration from the provided number of hours - #[must_use] - pub fn from_hours(value: f64) -> Self { - value * Unit::Hour - } - - /// Creates a new duration from the provided number of seconds - #[must_use] - pub fn from_seconds(value: f64) -> Self { - value * Unit::Second - } - - /// Creates a new duration from the provided number of milliseconds - #[must_use] - pub fn from_milliseconds(value: f64) -> Self { - value * Unit::Millisecond - } - - /// Creates a new duration from the provided number of microsecond - #[must_use] - pub fn from_microseconds(value: f64) -> Self { - value * Unit::Microsecond - } - - /// Creates a new duration from the provided number of nanoseconds - #[must_use] - pub fn from_nanoseconds(value: f64) -> Self { - value * Unit::Nanosecond - } - - /// Creates a new duration from its parts. Set the sign to a negative number for the duration to be negative. - #[allow(clippy::too_many_arguments)] - #[must_use] - pub fn compose( - sign: i8, - days: u64, - hours: u64, - minutes: u64, - seconds: u64, - milliseconds: u64, - microseconds: u64, - nanoseconds: u64, - ) -> Self { - Self::compose_f64( - sign, - days as f64, - hours as f64, - minutes as f64, - seconds as f64, - milliseconds as f64, - microseconds as f64, - nanoseconds as f64, - ) - } - - /// Creates a new duration from its parts. Set the sign to a negative number for the duration to be negative. - #[allow(clippy::too_many_arguments)] - #[must_use] - pub fn compose_f64( - sign: i8, - days: f64, - hours: f64, - minutes: f64, - seconds: f64, - milliseconds: f64, - microseconds: f64, - nanoseconds: f64, - ) -> Self { - let me: Self = days.days() - + hours.hours() - + minutes.minutes() - + seconds.seconds() - + milliseconds.milliseconds() - + microseconds.microseconds() - + nanoseconds.nanoseconds(); - if sign < 0 { - -me - } else { - me - } - } - - /// Initializes a Duration from a timezone offset - #[must_use] - pub fn from_tz_offset(sign: i8, hours: i64, minutes: i64) -> Self { - let dur = hours * Unit::Hour + minutes * Unit::Minute; - if sign < 0 { - -dur - } else { - dur - } - } -} - -#[cfg_attr(feature = "python", pymethods)] -impl Duration { - fn normalize(&mut self) { - let extra_centuries = self.nanoseconds.div_euclid(NANOSECONDS_PER_CENTURY); - // We can skip this whole step if the div_euclid shows that we didn't overflow the number of nanoseconds per century - if extra_centuries > 0 { - let rem_nanos = self.nanoseconds.rem_euclid(NANOSECONDS_PER_CENTURY); - - if self.centuries == i16::MAX { - if self.nanoseconds.saturating_add(rem_nanos) > Self::MAX.nanoseconds { - // Saturated max - *self = Self::MAX; - } - // Else, we're near the MAX but we're within the MAX in nanoseconds, so let's not do anything here. - } else if *self != Self::MAX && *self != Self::MIN { - // The bounds are valid as is, no wrapping needed when rem_nanos is not zero. - match self.centuries.checked_add(extra_centuries as i16) { - Some(centuries) => { - self.centuries = centuries; - self.nanoseconds = rem_nanos; - } - None => { - if self.centuries >= 0 { - // Saturated max again - *self = Self::MAX; - } else { - // Saturated min - *self = Self::MIN; - } - } - } - } - } - } - - #[must_use] - /// Returns the centuries and nanoseconds of this duration - /// NOTE: These items are not public to prevent incorrect durations from being created by modifying the values of the structure directly. - pub const fn to_parts(&self) -> (i16, u64) { - (self.centuries, self.nanoseconds) - } - - /// Returns the total nanoseconds in a signed 128 bit integer - #[must_use] - pub fn total_nanoseconds(&self) -> i128 { - if self.centuries == -1 { - -i128::from(NANOSECONDS_PER_CENTURY - self.nanoseconds) - } else if self.centuries >= 0 { - i128::from(self.centuries) * i128::from(NANOSECONDS_PER_CENTURY) - + i128::from(self.nanoseconds) - } else { - // Centuries negative by a decent amount - i128::from(self.centuries) * i128::from(NANOSECONDS_PER_CENTURY) - - i128::from(self.nanoseconds) - } - } - - /// Returns the truncated nanoseconds in a signed 64 bit integer, if the duration fits. - pub fn try_truncated_nanoseconds(&self) -> Result { - // If it fits, we know that the nanoseconds also fit. abs() will fail if the centuries are min'ed out. - if self.centuries == i16::MIN || self.centuries.abs() >= 3 { - Err(Errors::Overflow) - } else if self.centuries == -1 { - Ok(-((NANOSECONDS_PER_CENTURY - self.nanoseconds) as i64)) - } else if self.centuries >= 0 { - match i64::from(self.centuries).checked_mul(NANOSECONDS_PER_CENTURY as i64) { - Some(centuries_as_ns) => { - match centuries_as_ns.checked_add(self.nanoseconds as i64) { - Some(truncated_ns) => Ok(truncated_ns), - None => Err(Errors::Overflow), - } - } - None => Err(Errors::Overflow), - } - } else { - // Centuries negative by a decent amount - Ok( - i64::from(self.centuries + 1) * NANOSECONDS_PER_CENTURY as i64 - + self.nanoseconds as i64, - ) - } - } - - /// Returns the truncated nanoseconds in a signed 64 bit integer, if the duration fits. - /// WARNING: This function will NOT fail and will return the i64::MIN or i64::MAX depending on - /// the sign of the centuries if the Duration does not fit on aa i64 - #[must_use] - pub fn truncated_nanoseconds(&self) -> i64 { - match self.try_truncated_nanoseconds() { - Ok(val) => val, - Err(_) => { - if self.centuries < 0 { - i64::MIN - } else { - i64::MAX - } - } - } - } - - /// Returns this duration in seconds f64. - /// For high fidelity comparisons, it is recommended to keep using the Duration structure. - #[must_use] - pub fn to_seconds(&self) -> f64 { - // Compute the seconds and nanoseconds that we know this fits on a 64bit float - let seconds = self.nanoseconds.div_euclid(NANOSECONDS_PER_SECOND); - let subseconds = self.nanoseconds.rem_euclid(NANOSECONDS_PER_SECOND); - if self.centuries == 0 { - (seconds as f64) + (subseconds as f64) * 1e-9 - } else { - f64::from(self.centuries) * SECONDS_PER_CENTURY - + (seconds as f64) - + (subseconds as f64) * 1e-9 - } - } - - #[must_use] - pub fn to_unit(&self, unit: Unit) -> f64 { - self.to_seconds() * unit.from_seconds() - } - - /// Returns the absolute value of this duration - #[must_use] - pub fn abs(&self) -> Self { - if self.centuries.is_negative() { - -*self - } else { - *self - } - } - - /// Returns the sign of this duration - /// + 0 if the number is zero - /// + 1 if the number is positive - /// + -1 if the number is negative - #[must_use] - pub const fn signum(&self) -> i8 { - self.centuries.signum() as i8 - } - - /// Decomposes a Duration in its sign, days, hours, minutes, seconds, ms, us, ns - #[must_use] - pub fn decompose(&self) -> (i8, u64, u64, u64, u64, u64, u64, u64) { - let sign = self.signum(); - - match self.try_truncated_nanoseconds() { - Ok(total_ns) => { - let ns_left = total_ns.abs(); - - let (days, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_DAY as i64); - let (hours, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_HOUR as i64); - let (minutes, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_MINUTE as i64); - let (seconds, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_SECOND as i64); - let (milliseconds, ns_left) = - div_rem_i64(ns_left, NANOSECONDS_PER_MILLISECOND as i64); - let (microseconds, ns_left) = - div_rem_i64(ns_left, NANOSECONDS_PER_MICROSECOND as i64); - - // Everything should fit in the expected types now - ( - sign, - days.try_into().unwrap(), - hours.try_into().unwrap(), - minutes.try_into().unwrap(), - seconds.try_into().unwrap(), - milliseconds.try_into().unwrap(), - microseconds.try_into().unwrap(), - ns_left.try_into().unwrap(), - ) - } - Err(_) => { - // Doesn't fit on a i64, so let's use the slower i128 - let total_ns = self.total_nanoseconds(); - let ns_left = total_ns.abs(); - - let (days, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_DAY)); - let (hours, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_HOUR)); - let (minutes, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_MINUTE)); - let (seconds, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_SECOND)); - let (milliseconds, ns_left) = - div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_MILLISECOND)); - let (microseconds, ns_left) = - div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_MICROSECOND)); - - // Everything should fit in the expected types now - ( - sign, - days.try_into().unwrap(), - hours.try_into().unwrap(), - minutes.try_into().unwrap(), - seconds.try_into().unwrap(), - milliseconds.try_into().unwrap(), - microseconds.try_into().unwrap(), - ns_left.try_into().unwrap(), - ) - } - } - } - - /// Floors this duration to the closest duration from the bottom - /// - /// # Example - /// ``` - /// use hifitime::{Duration, TimeUnits}; - /// - /// let two_hours_three_min = 2.hours() + 3.minutes(); - /// assert_eq!(two_hours_three_min.floor(1.hours()), 2.hours()); - /// assert_eq!(two_hours_three_min.floor(30.minutes()), 2.hours()); - /// // This is zero because we floor by a duration longer than the current duration, rounding it down - /// assert_eq!(two_hours_three_min.floor(4.hours()), 0.hours()); - /// assert_eq!(two_hours_three_min.floor(1.seconds()), two_hours_three_min); - /// assert_eq!(two_hours_three_min.floor(1.hours() + 1.minutes()), 2.hours() + 2.minutes()); - /// assert_eq!(two_hours_three_min.floor(1.hours() + 5.minutes()), 1.hours() + 5.minutes()); - /// ``` - pub fn floor(&self, duration: Self) -> Self { - // Note that we don't use checked_sub because, at most, this will be zero. - // match self - // .total_nanoseconds() - // .checked_sub(self.total_nanoseconds() % duration.abs().total_nanoseconds()) - // { - // Some(total_ns) => Self::from_total_nanoseconds(total_ns), - // None => Self::MIN, - // } - - Self::from_total_nanoseconds( - self.total_nanoseconds() - self.total_nanoseconds() % duration.total_nanoseconds(), - ) - } - - /// Ceils this duration to the closest provided duration - /// - /// This simply floors then adds the requested duration - /// - /// # Example - /// ``` - /// use hifitime::{Duration, TimeUnits}; - /// - /// let two_hours_three_min = 2.hours() + 3.minutes(); - /// assert_eq!(two_hours_three_min.ceil(1.hours()), 3.hours()); - /// assert_eq!(two_hours_three_min.ceil(30.minutes()), 2.hours() + 30.minutes()); - /// assert_eq!(two_hours_three_min.ceil(4.hours()), 4.hours()); - /// assert_eq!(two_hours_three_min.ceil(1.seconds()), two_hours_three_min + 1.seconds()); - /// assert_eq!(two_hours_three_min.ceil(1.hours() + 5.minutes()), 2.hours() + 10.minutes()); - /// ``` - pub fn ceil(&self, duration: Self) -> Self { - let floored = self.floor(duration); - match floored - .total_nanoseconds() - .checked_add(duration.abs().total_nanoseconds()) - { - Some(total_ns) => Self::from_total_nanoseconds(total_ns), - None => Self::MAX, - } - } - - /// Rounds this duration to the closest provided duration - /// - /// This performs both a `ceil` and `floor` and returns the value which is the closest to current one. - /// # Example - /// ``` - /// use hifitime::{Duration, TimeUnits}; - /// - /// let two_hours_three_min = 2.hours() + 3.minutes(); - /// assert_eq!(two_hours_three_min.round(1.hours()), 2.hours()); - /// assert_eq!(two_hours_three_min.round(30.minutes()), 2.hours()); - /// assert_eq!(two_hours_three_min.round(4.hours()), 4.hours()); - /// assert_eq!(two_hours_three_min.round(1.seconds()), two_hours_three_min); - /// assert_eq!(two_hours_three_min.round(1.hours() + 5.minutes()), 2.hours() + 10.minutes()); - /// ``` - pub fn round(&self, duration: Self) -> Self { - let floored = self.floor(duration); - let ceiled = self.ceil(duration); - if *self - floored < (ceiled - *self).abs() { - floored - } else { - ceiled - } - } - - /// Rounds this duration to the largest units represented in this duration. - /// - /// This is useful to provide an approximate human duration. Under the hood, this function uses `round`, - /// so the "tipping point" of the rounding is half way to the next increment of the greatest unit. - /// As shown below, one example is that 35 hours and 59 minutes rounds to 1 day, but 36 hours and 1 minute rounds - /// to 2 days because 2 days is closer to 36h 1 min than 36h 1 min is to 1 day. - /// - /// # Example - /// - /// ``` - /// use hifitime::{Duration, TimeUnits}; - /// - /// assert_eq!((2.hours() + 3.minutes()).approx(), 2.hours()); - /// assert_eq!((24.hours() + 3.minutes()).approx(), 1.days()); - /// assert_eq!((35.hours() + 59.minutes()).approx(), 1.days()); - /// assert_eq!((36.hours() + 1.minutes()).approx(), 2.days()); - /// assert_eq!((47.hours() + 3.minutes()).approx(), 2.days()); - /// assert_eq!((49.hours() + 3.minutes()).approx(), 2.days()); - /// ``` - pub fn approx(&self) -> Self { - let (_, days, hours, minutes, seconds, milli, us, _) = self.decompose(); - - let round_to = if days > 0 { - 1 * Unit::Day - } else if hours > 0 { - 1 * Unit::Hour - } else if minutes > 0 { - 1 * Unit::Minute - } else if seconds > 0 { - 1 * Unit::Second - } else if milli > 0 { - 1 * Unit::Millisecond - } else if us > 0 { - 1 * Unit::Microsecond - } else { - 1 * Unit::Nanosecond - }; - - self.round(round_to) - } - - /// Returns the minimum of the two durations. - /// - /// ``` - /// use hifitime::TimeUnits; - /// - /// let d0 = 20.seconds(); - /// let d1 = 21.seconds(); - /// - /// assert_eq!(d0, d1.min(d0)); - /// assert_eq!(d0, d0.min(d1)); - /// ``` - /// - /// _Note:_ this uses a pointer to `self` which will be copied immediately because Python requires a pointer. - pub fn min(&self, other: Self) -> Self { - if *self < other { - *self - } else { - other - } - } - - /// Returns the maximum of the two durations. - /// - /// ``` - /// use hifitime::TimeUnits; - /// - /// let d0 = 20.seconds(); - /// let d1 = 21.seconds(); - /// - /// assert_eq!(d1, d1.max(d0)); - /// assert_eq!(d1, d0.max(d1)); - /// ``` - /// - /// _Note:_ this uses a pointer to `self` which will be copied immediately because Python requires a pointer. - pub fn max(&self, other: Self) -> Self { - if *self > other { - *self - } else { - other - } - } - - /// Returns whether this is a negative or positive duration. - pub const fn is_negative(&self) -> bool { - self.centuries.is_negative() - } - - /// A duration of exactly zero nanoseconds - pub const ZERO: Self = Self { - centuries: 0, - nanoseconds: 0, - }; - - /// Maximum duration that can be represented - pub const MAX: Self = Self { - centuries: i16::MAX, - nanoseconds: NANOSECONDS_PER_CENTURY, - }; - - /// Minimum duration that can be represented - pub const MIN: Self = Self { - centuries: i16::MIN, - nanoseconds: 0, - }; - - /// Smallest duration that can be represented - pub const EPSILON: Self = Self { - centuries: 0, - nanoseconds: 1, - }; - - /// Minimum positive duration is one nanoseconds - pub const MIN_POSITIVE: Self = Self::EPSILON; - - /// Minimum negative duration is minus one nanosecond - pub const MIN_NEGATIVE: Self = Self { - centuries: -1, - nanoseconds: NANOSECONDS_PER_CENTURY - 1, - }; - - // Python helpers - - #[cfg(feature = "python")] - #[new] - fn new_py(string_repr: String) -> PyResult { - match Self::from_str(&string_repr) { - Ok(d) => Ok(d), - Err(e) => Err(PyErr::from(e)), - } - } - - #[cfg(feature = "python")] - fn __str__(&self) -> String { - format!("{self}") - } - - #[cfg(feature = "python")] - fn __repr__(&self) -> String { - format!("{self}") - } - - #[cfg(feature = "python")] - fn __add__(&self, other: Self) -> Duration { - *self + other - } - - #[cfg(feature = "python")] - fn __sub__(&self, other: Self) -> Duration { - *self - other - } - - #[cfg(feature = "python")] - fn __mul__(&self, other: f64) -> Duration { - *self * other - } - - #[cfg(feature = "python")] - fn __div__(&self, other: f64) -> Duration { - *self / other - } - - #[cfg(feature = "python")] - fn __eq__(&self, other: Self) -> bool { - *self == other - } - - #[cfg(feature = "python")] - fn __richcmp__(&self, other: Self, op: CompareOp) -> bool { - match op { - CompareOp::Lt => *self < other, - CompareOp::Le => *self <= other, - CompareOp::Eq => *self == other, - CompareOp::Ne => *self != other, - CompareOp::Gt => *self > other, - CompareOp::Ge => *self >= other, - } - } - - // Python constructors - - #[cfg(feature = "python")] - #[classmethod] - fn zero(_cls: &PyType) -> Duration { - Duration::ZERO - } - - #[cfg(feature = "python")] - #[classmethod] - fn epsilon(_cls: &PyType) -> Duration { - Duration::EPSILON - } - - #[cfg(feature = "python")] - #[classmethod] - fn init_from_max(_cls: &PyType) -> Duration { - Duration::MAX - } - - #[cfg(feature = "python")] - #[classmethod] - fn init_from_min(_cls: &PyType) -> Duration { - Duration::MIN - } - - #[cfg(feature = "python")] - #[classmethod] - fn min_positive(_cls: &PyType) -> Duration { - Duration::MIN_POSITIVE - } - - #[cfg(feature = "python")] - #[classmethod] - fn min_negative(_cls: &PyType) -> Duration { - Duration::MIN_NEGATIVE - } - - #[cfg(feature = "python")] - #[classmethod] - /// Create a normalized duration from its parts - fn init_from_parts(_cls: &PyType, centuries: i16, nanoseconds: u64) -> Self { - Self::from_parts(centuries, nanoseconds) - } - - /// Creates a new duration from its parts - #[allow(clippy::too_many_arguments)] - #[cfg(feature = "python")] - #[classmethod] - #[must_use] - fn init_from_all_parts( - _cls: &PyType, - sign: i8, - days: u64, - hours: u64, - minutes: u64, - seconds: u64, - milliseconds: u64, - microseconds: u64, - nanoseconds: u64, - ) -> Self { - Self::compose( - sign, - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - ) - } - - #[cfg(feature = "python")] - #[classmethod] - fn init_from_total_nanoseconds(_cls: &PyType, nanos: i128) -> Self { - Self::from_total_nanoseconds(nanos) - } - - #[cfg(feature = "python")] - #[classmethod] - /// Create a new duration from the truncated nanoseconds (+/- 2927.1 years of duration) - fn init_from_truncated_nanoseconds(_cls: &PyType, nanos: i64) -> Self { - Self::from_truncated_nanoseconds(nanos) - } -} - -impl Mul for Duration { - type Output = Duration; - fn mul(self, q: i64) -> Self::Output { - Duration::from_total_nanoseconds( - self.total_nanoseconds() - .saturating_mul((q * Unit::Nanosecond).total_nanoseconds()), - ) - } -} - -impl Mul for Duration { - type Output = Duration; - fn mul(self, q: f64) -> Self::Output { - // Make sure that we don't trim the number by finding its precision - let mut p: i32 = 0; - let mut new_val = q; - let ten: f64 = 10.0; - - loop { - if (new_val.floor() - new_val).abs() < f64::EPSILON { - // Yay, we've found the precision of this number - break; - } - // Multiply by the precision - // https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b760579f103b7192c20413ebbe167b90 - p += 1; - new_val = q * ten.powi(p); - } - - Duration::from_total_nanoseconds( - self.total_nanoseconds() - .saturating_mul(new_val as i128) - .saturating_div(10_i128.pow(p.try_into().unwrap())), - ) - } -} - -macro_rules! impl_ops_for_type { - ($type:ident) => { - impl Mul for $type { - type Output = Duration; - fn mul(self, q: Unit) -> Duration { - // Apply the reflexive property - q * self - } - } - - impl Mul<$type> for Freq { - type Output = Duration; - - /// Converts the input values to i128 and creates a duration from that - /// This method will necessarily ignore durations below nanoseconds - fn mul(self, q: $type) -> Duration { - let total_ns = match self { - Freq::GigaHertz => 1.0 / (q as f64), - Freq::MegaHertz => (NANOSECONDS_PER_MICROSECOND as f64) / (q as f64), - Freq::KiloHertz => NANOSECONDS_PER_MILLISECOND as f64 / (q as f64), - Freq::Hertz => (NANOSECONDS_PER_SECOND as f64) / (q as f64), - }; - if total_ns.abs() < (i64::MAX as f64) { - Duration::from_truncated_nanoseconds(total_ns as i64) - } else { - Duration::from_total_nanoseconds(total_ns as i128) - } - } - } - - impl Mul for $type { - type Output = Duration; - fn mul(self, q: Freq) -> Duration { - // Apply the reflexive property - q * self - } - } - - #[allow(clippy::suspicious_arithmetic_impl)] - impl Div<$type> for Duration { - type Output = Duration; - fn div(self, q: $type) -> Self::Output { - Duration::from_total_nanoseconds( - self.total_nanoseconds() - .saturating_div((q * Unit::Nanosecond).total_nanoseconds()), - ) - } - } - - impl Mul for $type { - type Output = Duration; - fn mul(self, q: Self::Output) -> Self::Output { - // Apply the reflexive property - q * self - } - } - - impl TimeUnits for $type {} - - impl Frequencies for $type {} - }; -} - -impl fmt::Display for Duration { - // Prints this duration with automatic selection of the units, i.e. everything that isn't zero is ignored - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.total_nanoseconds() == 0 { - write!(f, "0 ns") - } else { - let (sign, days, hours, minutes, seconds, milli, us, nano) = self.decompose(); - if sign == -1 { - write!(f, "-")?; - } - - let values = [days, hours, minutes, seconds, milli, us, nano]; - let units = ["days", "h", "min", "s", "ms", "μs", "ns"]; - - let mut insert_space = false; - for (val, unit) in values.iter().zip(units.iter()) { - if *val > 0 { - if insert_space { - write!(f, " ")?; - } - write!(f, "{} {}", val, unit)?; - insert_space = true; - } - } - Ok(()) - } - } -} - -impl fmt::LowerExp for Duration { - // Prints the duration with appropriate units - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let seconds_f64 = self.to_seconds(); - let seconds_f64_abs = seconds_f64.abs(); - if seconds_f64_abs < 1e-5 { - fmt::Display::fmt(&(seconds_f64 * 1e9), f)?; - write!(f, " ns") - } else if seconds_f64_abs < 1e-2 { - fmt::Display::fmt(&(seconds_f64 * 1e3), f)?; - write!(f, " ms") - } else if seconds_f64_abs < 3.0 * SECONDS_PER_MINUTE { - fmt::Display::fmt(&(seconds_f64), f)?; - write!(f, " s") - } else if seconds_f64_abs < SECONDS_PER_HOUR { - fmt::Display::fmt(&(seconds_f64 / SECONDS_PER_MINUTE), f)?; - write!(f, " min") - } else if seconds_f64_abs < SECONDS_PER_DAY { - fmt::Display::fmt(&(seconds_f64 / SECONDS_PER_HOUR), f)?; - write!(f, " h") - } else { - fmt::Display::fmt(&(seconds_f64 / SECONDS_PER_DAY), f)?; - write!(f, " days") - } - } -} - -impl Add for Duration { - type Output = Duration; - - /// # Addition of Durations - /// Durations are centered on zero duration. Of the tuple, only the centuries may be negative, the nanoseconds are always positive - /// and represent the nanoseconds _into_ the current centuries. - /// - /// ## Examples - /// + `Duration { centuries: 0, nanoseconds: 1 }` is a positive duration of zero centuries and one nanosecond. - /// + `Duration { centuries: -1, nanoseconds: 1 }` is a negative duration representing "one century before zero minus one nanosecond" - fn add(self, rhs: Self) -> Duration { - // Check that the addition fits in an i16 - let mut me = self; - match me.centuries.checked_add(rhs.centuries) { - None => { - // Overflowed, so we've hit the bound. - if me.centuries < 0 { - // We've hit the negative bound, so return MIN. - return Self::MIN; - } else { - // We've hit the positive bound, so return MAX. - return Self::MAX; - } - } - Some(centuries) => { - me.centuries = centuries; - } - } - - if me.centuries == Self::MIN.centuries && self.nanoseconds < Self::MIN.nanoseconds { - // Then we do the operation backward - match me - .nanoseconds - .checked_sub(NANOSECONDS_PER_CENTURY - rhs.nanoseconds) - { - Some(nanos) => me.nanoseconds = nanos, - None => { - me.centuries += 1; // Safe because we're at the MIN - me.nanoseconds = rhs.nanoseconds - } - } - } else { - match me.nanoseconds.checked_add(rhs.nanoseconds) { - Some(nanoseconds) => me.nanoseconds = nanoseconds, - None => { - // Rare case where somehow the input data was not normalized. So let's normalize it and call add again. - let mut rhs = rhs; - rhs.normalize(); - - match me.centuries.checked_add(rhs.centuries) { - None => return Self::MAX, - Some(centuries) => me.centuries = centuries, - }; - // Now it will fit! - me.nanoseconds += rhs.nanoseconds; - } - } - } - - me.normalize(); - me - } -} - -impl AddAssign for Duration { - fn add_assign(&mut self, rhs: Duration) { - *self = *self + rhs; - } -} - -impl Sub for Duration { - type Output = Self; - - /// # Subtraction - /// This operation is a notch confusing with negative durations. - /// As described in the `Duration` structure, a Duration of (-1, NANOSECONDS_PER_CENTURY-1) is closer to zero - /// than (-1, 0). - /// - /// ## Algorithm - /// - /// ### A > B, and both are positive - /// - /// If A > B, then A.centuries is subtracted by B.centuries, and A.nanoseconds is subtracted by B.nanoseconds. - /// If an overflow occurs, e.g. A.nanoseconds < B.nanoseconds, the number of nanoseconds is increased by the number of nanoseconds per century, - /// and the number of centuries is decreased by one. - /// - /// ``` - /// use hifitime::{Duration, NANOSECONDS_PER_CENTURY}; - /// - /// let a = Duration::from_parts(1, 1); - /// let b = Duration::from_parts(0, 10); - /// let c = Duration::from_parts(0, NANOSECONDS_PER_CENTURY - 9); - /// assert_eq!(a - b, c); - /// ``` - /// - /// ### A < B, and both are positive - /// - /// In this case, the resulting duration will be negative. The number of centuries is a signed integer, so it is set to the difference of A.centuries - B.centuries. - /// The number of nanoseconds however must be wrapped by the number of nanoseconds per century. - /// For example:, let A = (0, 1) and B = (1, 10), then the resulting duration will be (-2, NANOSECONDS_PER_CENTURY - (10 - 1)). In this case, the centuries are set - /// to -2 because B is _two_ centuries into the future (the number of centuries into the future is zero-indexed). - /// ``` - /// use hifitime::{Duration, NANOSECONDS_PER_CENTURY}; - /// - /// let a = Duration::from_parts(0, 1); - /// let b = Duration::from_parts(1, 10); - /// let c = Duration::from_parts(-2, NANOSECONDS_PER_CENTURY - 9); - /// assert_eq!(a - b, c); - /// ``` - /// - /// ### A > B, both are negative - /// - /// In this case, we try to stick to normal arithmatics: (-9 - -10) = (-9 + 10) = +1. - /// In this case, we can simply add the components of the duration together. - /// For example, let A = (-1, NANOSECONDS_PER_CENTURY - 2), and B = (-1, NANOSECONDS_PER_CENTURY - 1). Respectively, A is _two_ nanoseconds _before_ Duration::ZERO - /// and B is _one_ nanosecond before Duration::ZERO. Then, A-B should be one nanoseconds before zero, i.e. (-1, NANOSECONDS_PER_CENTURY - 1). - /// This is because we _subtract_ "negative one nanosecond" from a "negative minus two nanoseconds", which corresponds to _adding_ the opposite, and the - /// opposite of "negative one nanosecond" is "positive one nanosecond". - /// - /// ``` - /// use hifitime::{Duration, NANOSECONDS_PER_CENTURY}; - /// - /// let a = Duration::from_parts(-1, NANOSECONDS_PER_CENTURY - 9); - /// let b = Duration::from_parts(-1, NANOSECONDS_PER_CENTURY - 10); - /// let c = Duration::from_parts(0, 1); - /// assert_eq!(a - b, c); - /// ``` - /// - /// ### A < B, both are negative - /// - /// Just like in the prior case, we try to stick to normal arithmatics: (-10 - -9) = (-10 + 9) = -1. - /// - /// ``` - /// use hifitime::{Duration, NANOSECONDS_PER_CENTURY}; - /// - /// let a = Duration::from_parts(-1, NANOSECONDS_PER_CENTURY - 10); - /// let b = Duration::from_parts(-1, NANOSECONDS_PER_CENTURY - 9); - /// let c = Duration::from_parts(-1, NANOSECONDS_PER_CENTURY - 1); - /// assert_eq!(a - b, c); - /// ``` - /// - /// ### MIN is the minimum - /// - /// One cannot subtract anything from the MIN. - /// - /// ``` - /// use hifitime::Duration; - /// - /// let one_ns = Duration::from_parts(0, 1); - /// assert_eq!(Duration::MIN - one_ns, Duration::MIN); - /// ``` - fn sub(self, rhs: Self) -> Self { - let mut me = self; - match me.centuries.checked_sub(rhs.centuries) { - None => { - // Underflowed, so we've hit the min - return Self::MIN; - } - Some(centuries) => { - me.centuries = centuries; - } - } - - match me.nanoseconds.checked_sub(rhs.nanoseconds) { - None => { - // Decrease the number of centuries, and realign - match me.centuries.checked_sub(1) { - Some(centuries) => { - me.centuries = centuries; - me.nanoseconds = me.nanoseconds + NANOSECONDS_PER_CENTURY - rhs.nanoseconds; - } - None => { - // We're at the min number of centuries already, and we have extra nanos, so we're saturated the duration limit - return Self::MIN; - } - }; - // me.nanoseconds = me.nanoseconds + NANOSECONDS_PER_CENTURY - rhs.nanoseconds; - } - Some(nanos) => me.nanoseconds = nanos, - }; - - me.normalize(); - me - } -} - -impl SubAssign for Duration { - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -// Allow adding with a Unit directly -impl Add for Duration { - type Output = Self; - - #[allow(clippy::identity_op)] - fn add(self, rhs: Unit) -> Self { - self + rhs * 1 - } -} - -impl AddAssign for Duration { - #[allow(clippy::identity_op)] - fn add_assign(&mut self, rhs: Unit) { - *self = *self + rhs * 1; - } -} - -impl Sub for Duration { - type Output = Duration; - - #[allow(clippy::identity_op)] - fn sub(self, rhs: Unit) -> Duration { - self - rhs * 1 - } -} - -impl SubAssign for Duration { - #[allow(clippy::identity_op)] - fn sub_assign(&mut self, rhs: Unit) { - *self = *self - rhs * 1; - } -} - -impl PartialEq for Duration { - #[allow(clippy::identity_op)] - fn eq(&self, unit: &Unit) -> bool { - *self == *unit * 1 - } -} - -impl PartialOrd for Duration { - #[allow(clippy::identity_op, clippy::comparison_chain)] - fn partial_cmp(&self, unit: &Unit) -> Option { - let unit_deref = *unit; - let unit_as_duration: Duration = unit_deref * 1; - if self < &unit_as_duration { - Some(Ordering::Less) - } else if self > &unit_as_duration { - Some(Ordering::Greater) - } else { - Some(Ordering::Equal) - } - } -} - -impl Neg for Duration { - type Output = Self; - - #[must_use] - fn neg(self) -> Self::Output { - if self == Self::MIN { - Self::MAX - } else if self == Self::MAX { - Self::MIN - } else { - match NANOSECONDS_PER_CENTURY.checked_sub(self.nanoseconds) { - Some(nanoseconds) => { - // yay - Self::from_parts(-self.centuries - 1, nanoseconds) - } - None => { - if self > Duration::ZERO { - let dur_to_max = Self::MAX - self; - Self::MIN + dur_to_max - } else { - let dur_to_min = Self::MIN + self; - Self::MAX - dur_to_min - } - } - } - } - } -} - -#[cfg(not(kani))] -impl FromStr for Duration { - type Err = Errors; - - /// Attempts to convert a simple string to a Duration. Does not yet support complicated durations. - /// - /// Identifiers: - /// + d, days, day - /// + h, hours, hour - /// + min, mins, minute - /// + s, second, seconds - /// + ms, millisecond, milliseconds - /// + us, microsecond, microseconds - /// + ns, nanosecond, nanoseconds - /// + `+` or `-` indicates a timezone offset - /// - /// # Example - /// ``` - /// use hifitime::{Duration, Unit}; - /// use std::str::FromStr; - /// - /// assert_eq!(Duration::from_str("1 d").unwrap(), Unit::Day * 1); - /// assert_eq!(Duration::from_str("10.598 days").unwrap(), Unit::Day * 10.598); - /// assert_eq!(Duration::from_str("10.598 min").unwrap(), Unit::Minute * 10.598); - /// assert_eq!(Duration::from_str("10.598 us").unwrap(), Unit::Microsecond * 10.598); - /// assert_eq!(Duration::from_str("10.598 seconds").unwrap(), Unit::Second * 10.598); - /// assert_eq!(Duration::from_str("10.598 nanosecond").unwrap(), Unit::Nanosecond * 10.598); - /// assert_eq!(Duration::from_str("5 h 256 ms 1 ns").unwrap(), 5 * Unit::Hour + 256 * Unit::Millisecond + Unit::Nanosecond); - /// assert_eq!(Duration::from_str("-01:15:30").unwrap(), -(1 * Unit::Hour + 15 * Unit::Minute + 30 * Unit::Second)); - /// assert_eq!(Duration::from_str("+3615").unwrap(), 36 * Unit::Hour + 15 * Unit::Minute); - /// ``` - fn from_str(s_in: &str) -> Result { - // Each part of a duration as days, hours, minutes, seconds, millisecond, microseconds, and nanoseconds - let mut decomposed = [0.0_f64; 7]; - - let mut prev_idx = 0; - let mut seeking_number = true; - let mut latest_value = 0.0; - - let s = s_in.trim(); - - if s.is_empty() { - return Err(Errors::ParseError(ParsingErrors::ValueError)); - } - - // There is at least one character, so we can unwrap this. - if let Some(char) = s.chars().next() { - if char == '+' || char == '-' { - // This is a timezone offset. - let offset_sign = if char == '-' { -1 } else { 1 }; - - let indexes: (usize, usize, usize) = (1, 3, 5); - let colon = if s.len() == 3 || s.len() == 5 || s.len() == 7 { - // There is a zero or even number of separators between the hours, minutes, and seconds. - // Only zero (or one) characters separator is supported. This will return a ValueError later if there is - // an even but greater than one character separator. - 0 - } else if s.len() == 4 || s.len() == 6 || s.len() == 9 { - // There is an odd number of characters as a separator between the hours, minutes, and seconds. - // Only one character separator is supported. This will return a ValueError later if there is - // an odd but greater than one character separator. - 1 - } else { - // This invalid - return Err(Errors::ParseError(ParsingErrors::ValueError)); - }; - - // Fetch the hours - let hours: i64 = match lexical_core::parse(s[indexes.0..indexes.1].as_bytes()) { - Ok(val) => val, - Err(_) => return Err(Errors::ParseError(ParsingErrors::ValueError)), - }; - - let mut minutes: i64 = 0; - let mut seconds: i64 = 0; - - match s.get(indexes.1 + colon..indexes.2 + colon) { - None => { - //Do nothing, we've reached the end of the useful data. - } - Some(subs) => { - // Fetch the minutes - match lexical_core::parse(subs.as_bytes()) { - Ok(val) => minutes = val, - Err(_) => return Err(Errors::ParseError(ParsingErrors::ValueError)), - } - - match s.get(indexes.2 + 2 * colon..) { - None => { - // Do nothing, there are no seconds inthis offset - } - Some(subs) => { - if !subs.is_empty() { - // Fetch the seconds - match lexical_core::parse(subs.as_bytes()) { - Ok(val) => seconds = val, - Err(_) => { - return Err(Errors::ParseError( - ParsingErrors::ValueError, - )) - } - } - } - } - } - } - } - - // Return the constructed offset - if offset_sign == -1 { - return Ok(-(hours * Unit::Hour - + minutes * Unit::Minute - + seconds * Unit::Second)); - } else { - return Ok(hours * Unit::Hour - + minutes * Unit::Minute - + seconds * Unit::Second); - } - } - }; - - for (idx, char) in s.chars().enumerate() { - if char == ' ' || idx == s.len() - 1 { - if seeking_number { - if prev_idx == idx { - // We've reached the end of the string and it didn't end with a unit - return Err(Errors::ParseError(ParsingErrors::UnknownOrMissingUnit)); - } - // We've found a new space so let's parse whatever precedes it - match lexical_core::parse(s[prev_idx..idx].as_bytes()) { - Ok(val) => latest_value = val, - Err(_) => return Err(Errors::ParseError(ParsingErrors::ValueError)), - } - // We'll now seek a unit - seeking_number = false; - } else { - // We're seeking a unit not a number, so let's parse the unit we just found and remember the position. - let end_idx = if idx == s.len() - 1 { idx + 1 } else { idx }; - let pos = match &s[prev_idx..end_idx] { - "d" | "days" | "day" => 0, - "h" | "hours" | "hour" => 1, - "min" | "mins" | "minute" | "minutes" => 2, - "s" | "second" | "seconds" => 3, - "ms" | "millisecond" | "milliseconds" => 4, - "us" | "microsecond" | "microseconds" => 5, - "ns" | "nanosecond" | "nanoseconds" => 6, - _ => { - return Err(Errors::ParseError(ParsingErrors::UnknownOrMissingUnit)); - } - }; - // Store the value - decomposed[pos] = latest_value; - // Now we switch to seeking a value - seeking_number = true; - } - prev_idx = idx + 1; - } - } - - Ok(Duration::compose_f64( - 1, - decomposed[0], - decomposed[1], - decomposed[2], - decomposed[3], - decomposed[4], - decomposed[5], - decomposed[6], - )) - } -} - -impl_ops_for_type!(f64); -impl_ops_for_type!(i64); - -const fn div_rem_i128(me: i128, rhs: i128) -> (i128, i128) { - (me.div_euclid(rhs), me.rem_euclid(rhs)) -} - -const fn div_rem_i64(me: i64, rhs: i64) -> (i64, i64) { - (me.div_euclid(rhs), me.rem_euclid(rhs)) -} - -#[cfg(feature = "std")] -impl From for std::time::Duration { - /// Converts a duration into an std::time::Duration - /// - /// # Limitations - /// 1. If the duration is negative, this will return a std::time::Duration::ZERO. - /// 2. If the duration larger than the MAX duration, this will return std::time::Duration::MAX - fn from(hf_duration: Duration) -> Self { - let (sign, days, hours, minutes, seconds, milli, us, nano) = hf_duration.decompose(); - if sign < 0 { - std::time::Duration::ZERO - } else { - // Build the seconds separately from the nanos. - let above_ns_f64: f64 = - Duration::compose(sign, days, hours, minutes, seconds, milli, us, 0).to_seconds(); - std::time::Duration::new(above_ns_f64 as u64, nano as u32) - } - } -} - -#[cfg(feature = "std")] -impl From for Duration { - /// Converts a duration into an std::time::Duration - /// - /// # Limitations - /// 1. If the duration is negative, this will return a std::time::Duration::ZERO. - /// 2. If the duration larger than the MAX duration, this will return std::time::Duration::MAX - fn from(std_duration: std::time::Duration) -> Self { - std_duration.as_secs_f64() * Unit::Second - } -} - -#[test] -#[cfg(feature = "serde")] -fn test_serdes() { - let dt = Duration::from_seconds(10.1); - let content = r#"{"centuries":0,"nanoseconds":10100000000}"#; - assert_eq!(content, serde_json::to_string(&dt).unwrap()); - let parsed: Duration = serde_json::from_str(content).unwrap(); - assert_eq!(dt, parsed); -} - -#[test] -fn test_bounds() { - let min = Duration::MIN; - assert_eq!(min.centuries, i16::MIN); - assert_eq!(min.nanoseconds, 0); - - let max = Duration::MAX; - assert_eq!(max.centuries, i16::MAX); - assert_eq!(max.nanoseconds, NANOSECONDS_PER_CENTURY); - - let min_p = Duration::MIN_POSITIVE; - assert_eq!(min_p.centuries, 0); - assert_eq!(min_p.nanoseconds, 1); - - let min_n = Duration::MIN_NEGATIVE; - assert_eq!(min_n.centuries, -1); - assert_eq!(min_n.nanoseconds, NANOSECONDS_PER_CENTURY - 1); - - let min_n1 = Duration::MIN - 1 * Unit::Nanosecond; - assert_eq!(min_n1, Duration::MIN); - - let max_n1 = Duration::MAX - 1 * Unit::Nanosecond; - assert_eq!(max_n1.centuries, i16::MAX); - assert_eq!(max_n1.nanoseconds, NANOSECONDS_PER_CENTURY - 1); -} - -#[cfg(kani)] -#[kani::proof] -fn formal_duration_normalize_any() { - let dur: Duration = kani::any(); - // Check that decompose never fails - dur.decompose(); -} - -#[cfg(kani)] -#[kani::proof] -fn formal_duration_truncated_ns_reciprocity() { - let nanoseconds: i64 = kani::any(); - let dur_from_part = Duration::from_truncated_nanoseconds(nanoseconds); - - let u_ns = dur_from_part.nanoseconds; - let centuries = dur_from_part.centuries; - if centuries <= -3 || centuries >= 3 { - // Then it does not fit on a i64, so this function should return an error - assert_eq!( - dur_from_part.try_truncated_nanoseconds(), - Err(Errors::Overflow) - ); - } else if centuries == -1 { - // If we are negative by just enough that the centuries is negative, then the truncated seconds - // should be the unsigned nanoseconds wrapped by the number of nanoseconds per century. - - let expect_rslt = -((NANOSECONDS_PER_CENTURY - u_ns) as i64); - - let recip_ns = dur_from_part.try_truncated_nanoseconds().unwrap(); - assert_eq!(recip_ns, expect_rslt); - } else if centuries < 0 { - // We fit on a i64 but we need to account for the number of nanoseconds wrapped to the negative centuries. - - let nanos = u_ns.rem_euclid(NANOSECONDS_PER_CENTURY); - let expect_rslt = i64::from(centuries + 1) * NANOSECONDS_PER_CENTURY as i64 + nanos as i64; - - let recip_ns = dur_from_part.try_truncated_nanoseconds().unwrap(); - assert_eq!(recip_ns, expect_rslt); - } else { - // Positive duration but enough to fit on an i64. - let recip_ns = dur_from_part.try_truncated_nanoseconds().unwrap(); - - assert_eq!(recip_ns, nanoseconds); - } -} - -#[cfg(kani)] -mod tests { - use super::*; - - macro_rules! repeat_test { - ($test_name:ident, $bounds:expr) => { - #[kani::proof] - fn $test_name() { - for pair in $bounds.windows(2) { - let seconds: f64 = kani::any(); - - kani::assume(seconds > pair[0]); - kani::assume(seconds < pair[1]); - - if seconds.is_finite() { - let big_seconds = seconds * 1e9; - let floored = big_seconds.floor(); - // Remove the sub nanoseconds -- but this can lead to rounding errors! - let truncated_ns = floored * 1e-9; - - let duration: Duration = Duration::from_seconds(truncated_ns); - let truncated_out = duration.to_seconds(); - let floored_out = truncated_out * 1e9; - - // So we check that the data times 1e9 matches the rounded data - if floored != floored_out { - let floored_out_bits = floored_out.to_bits(); - let floored_bits = floored.to_bits(); - - // Allow for ONE bit error on the LSB - if floored_out_bits > floored_bits { - assert_eq!(floored_out_bits - floored_bits, 1); - } else { - assert_eq!(floored_bits - floored_out_bits, 1); - } - } else { - assert_eq!(floored_out, floored); - } - } - } - } - }; - } - - repeat_test!(test_dur_f64_recip_0, [1e-9, 1e-8, 1e-7, 1e-6, 1e-5]); - repeat_test!(test_dur_f64_recip_1, [1e-5, 1e-4, 1e-3]); - // repeat_test!(test_dur_f64_recip_2, [1e-2, 1e-1, 1e0]); - // repeat_test!(test_dur_f64_recip_3, [1e0, 1e1, 1e2]); - // repeat_test!(test_dur_f64_recip_4, [1e2, 1e3, 1e4]); - // repeat_test!(test_dur_f64_recip_5, [1e4, 1e5]); - // repeat_test!(test_dur_f64_recip_6, [1e5, 1e6]); -} diff --git a/src/duration/kani.rs b/src/duration/kani.rs new file mode 100644 index 00000000..ad17dab1 --- /dev/null +++ b/src/duration/kani.rs @@ -0,0 +1,121 @@ +/* +* Hifitime, part of the Nyx Space tools +* Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) +* This Source Code Form is subject to the terms of the Apache +* v. 2.0. If a copy of the Apache License was not distributed with this +* file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. +* +* Documentation: https://nyxspace.com/ +*/ + +// Here lives all of the formal verification for Duration. + +use super::Duration; + +use kani::Arbitrary; + +impl Arbitrary for Duration { + #[inline(always)] + fn any() -> Self { + let centuries: i16 = kani::any(); + let nanoseconds: u64 = kani::any(); + + Duration::from_parts(centuries, nanoseconds) + } +} + +#[kani::proof] +fn formal_duration_normalize_any() { + let dur: Duration = kani::any(); + // Check that decompose never fails + dur.decompose(); +} + +#[kani::proof] +fn formal_duration_truncated_ns_reciprocity() { + let nanoseconds: i64 = kani::any(); + let dur_from_part = Duration::from_truncated_nanoseconds(nanoseconds); + + let u_ns = dur_from_part.nanoseconds; + let centuries = dur_from_part.centuries; + if centuries <= -3 || centuries >= 3 { + // Then it does not fit on a i64, so this function should return an error + assert_eq!( + dur_from_part.try_truncated_nanoseconds(), + Err(Errors::Overflow) + ); + } else if centuries == -1 { + // If we are negative by just enough that the centuries is negative, then the truncated seconds + // should be the unsigned nanoseconds wrapped by the number of nanoseconds per century. + + let expect_rslt = -((NANOSECONDS_PER_CENTURY - u_ns) as i64); + + let recip_ns = dur_from_part.try_truncated_nanoseconds().unwrap(); + assert_eq!(recip_ns, expect_rslt); + } else if centuries < 0 { + // We fit on a i64 but we need to account for the number of nanoseconds wrapped to the negative centuries. + + let nanos = u_ns.rem_euclid(NANOSECONDS_PER_CENTURY); + let expect_rslt = i64::from(centuries + 1) * NANOSECONDS_PER_CENTURY as i64 + nanos as i64; + + let recip_ns = dur_from_part.try_truncated_nanoseconds().unwrap(); + assert_eq!(recip_ns, expect_rslt); + } else { + // Positive duration but enough to fit on an i64. + let recip_ns = dur_from_part.try_truncated_nanoseconds().unwrap(); + + assert_eq!(recip_ns, nanoseconds); + } +} + +mod tests { + use super::*; + + macro_rules! repeat_test { + ($test_name:ident, $bounds:expr) => { + #[kani::proof] + fn $test_name() { + for pair in $bounds.windows(2) { + let seconds: f64 = kani::any(); + + kani::assume(seconds > pair[0]); + kani::assume(seconds < pair[1]); + + if seconds.is_finite() { + let big_seconds = seconds * 1e9; + let floored = big_seconds.floor(); + // Remove the sub nanoseconds -- but this can lead to rounding errors! + let truncated_ns = floored * 1e-9; + + let duration: Duration = Duration::from_seconds(truncated_ns); + let truncated_out = duration.to_seconds(); + let floored_out = truncated_out * 1e9; + + // So we check that the data times 1e9 matches the rounded data + if floored != floored_out { + let floored_out_bits = floored_out.to_bits(); + let floored_bits = floored.to_bits(); + + // Allow for ONE bit error on the LSB + if floored_out_bits > floored_bits { + assert_eq!(floored_out_bits - floored_bits, 1); + } else { + assert_eq!(floored_bits - floored_out_bits, 1); + } + } else { + assert_eq!(floored_out, floored); + } + } + } + } + }; + } + + repeat_test!(test_dur_f64_recip_0, [1e-9, 1e-8, 1e-7, 1e-6, 1e-5]); + repeat_test!(test_dur_f64_recip_1, [1e-5, 1e-4, 1e-3]); + // repeat_test!(test_dur_f64_recip_2, [1e-2, 1e-1, 1e0]); + // repeat_test!(test_dur_f64_recip_3, [1e0, 1e1, 1e2]); + // repeat_test!(test_dur_f64_recip_4, [1e2, 1e3, 1e4]); + // repeat_test!(test_dur_f64_recip_5, [1e4, 1e5]); + // repeat_test!(test_dur_f64_recip_6, [1e5, 1e6]); +} diff --git a/src/duration/mod.rs b/src/duration/mod.rs new file mode 100644 index 00000000..b67c7a10 --- /dev/null +++ b/src/duration/mod.rs @@ -0,0 +1,764 @@ +/* +* Hifitime, part of the Nyx Space tools +* Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) +* This Source Code Form is subject to the terms of the Apache +* v. 2.0. If a copy of the Apache License was not distributed with this +* file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. +* +* Documentation: https://nyxspace.com/ +*/ + +use crate::{Errors, SECONDS_PER_CENTURY, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE}; + +pub use crate::{Freq, Frequencies, TimeUnits, Unit}; + +#[cfg(feature = "std")] +mod std; +use core::cmp::Ordering; +use core::convert::TryInto; +use core::fmt; +use core::hash::{Hash, Hasher}; + +#[cfg(feature = "serde")] +use serde_derive::{Deserialize, Serialize}; + +#[cfg(not(kani))] +pub mod parse; + +#[cfg(feature = "python")] +mod python; + +#[cfg(feature = "python")] +use pyo3::prelude::pyclass; + +#[cfg(not(feature = "std"))] +use num_traits::Float; + +#[cfg(kani)] +mod kani; + +pub const DAYS_PER_CENTURY_U64: u64 = 36_525; +pub const NANOSECONDS_PER_MICROSECOND: u64 = 1_000; +pub const NANOSECONDS_PER_MILLISECOND: u64 = 1_000 * NANOSECONDS_PER_MICROSECOND; +pub const NANOSECONDS_PER_SECOND: u64 = 1_000 * NANOSECONDS_PER_MILLISECOND; +pub(crate) const NANOSECONDS_PER_SECOND_U32: u32 = 1_000_000_000; +pub const NANOSECONDS_PER_MINUTE: u64 = 60 * NANOSECONDS_PER_SECOND; +pub const NANOSECONDS_PER_HOUR: u64 = 60 * NANOSECONDS_PER_MINUTE; +pub const NANOSECONDS_PER_DAY: u64 = 24 * NANOSECONDS_PER_HOUR; +pub const NANOSECONDS_PER_CENTURY: u64 = DAYS_PER_CENTURY_U64 * NANOSECONDS_PER_DAY; + +pub mod ops; + +/// Defines generally usable durations for nanosecond precision valid for 32,768 centuries in either direction, and only on 80 bits / 10 octets. +/// +/// **Important conventions:** +/// 1. The negative durations can be mentally modeled "BC" years. One hours before 01 Jan 0000, it was "-1" years but 365 days and 23h into the current day. +/// It was decided that the nanoseconds corresponds to the nanoseconds _into_ the current century. In other words, +/// a duration with centuries = -1 and nanoseconds = 0 is _a smaller duration_ (further from zero) than centuries = -1 and nanoseconds = 1. +/// Duration zero minus one nanosecond returns a century of -1 and a nanosecond set to the number of nanoseconds in one century minus one. +/// That difference is exactly 1 nanoseconds, where the former duration is "closer to zero" than the latter. +/// As such, the largest negative duration that can be represented sets the centuries to i16::MAX and its nanoseconds to NANOSECONDS_PER_CENTURY. +/// 2. It was also decided that opposite durations are equal, e.g. -15 minutes == 15 minutes. If the direction of time matters, use the signum function. +#[derive(Clone, Copy, Debug, PartialOrd, Eq, Ord)] +#[repr(C)] +#[cfg_attr(feature = "python", pyclass)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Duration { + pub(crate) centuries: i16, + pub(crate) nanoseconds: u64, +} + +impl PartialEq for Duration { + fn eq(&self, other: &Self) -> bool { + if self.centuries == other.centuries { + self.nanoseconds == other.nanoseconds + } else if (self.centuries.saturating_sub(other.centuries)).saturating_abs() == 1 + && (self.centuries == 0 || other.centuries == 0) + { + // Special case where we're at the zero crossing + if self.centuries < 0 { + // Self is negative, + (NANOSECONDS_PER_CENTURY - self.nanoseconds) == other.nanoseconds + } else { + // Other is negative + (NANOSECONDS_PER_CENTURY - other.nanoseconds) == self.nanoseconds + } + } else { + false + } + } +} + +impl Hash for Duration { + fn hash(&self, hasher: &mut H) { + self.centuries.hash(hasher); + self.nanoseconds.hash(hasher); + } +} + +impl Default for Duration { + fn default() -> Self { + Duration::ZERO + } +} + +// Defines the methods that should be classmethods in Python, but must be redefined as per https://github.com/PyO3/pyo3/issues/1003#issuecomment-844433346 +impl Duration { + /// A duration of exactly zero nanoseconds + pub const ZERO: Self = Self { + centuries: 0, + nanoseconds: 0, + }; + + /// Maximum duration that can be represented + pub const MAX: Self = Self { + centuries: i16::MAX, + nanoseconds: NANOSECONDS_PER_CENTURY, + }; + + /// Minimum duration that can be represented + pub const MIN: Self = Self { + centuries: i16::MIN, + nanoseconds: 0, + }; + + /// Smallest duration that can be represented + pub const EPSILON: Self = Self { + centuries: 0, + nanoseconds: 1, + }; + + /// Minimum positive duration is one nanoseconds + pub const MIN_POSITIVE: Self = Self::EPSILON; + + /// Minimum negative duration is minus one nanosecond + pub const MIN_NEGATIVE: Self = Self { + centuries: -1, + nanoseconds: NANOSECONDS_PER_CENTURY - 1, + }; + + #[must_use] + /// Create a normalized duration from its parts + pub fn from_parts(centuries: i16, nanoseconds: u64) -> Self { + let mut me = Self { + centuries, + nanoseconds, + }; + me.normalize(); + me + } + + #[must_use] + /// Converts the total nanoseconds as i128 into this Duration (saving 48 bits) + pub fn from_total_nanoseconds(nanos: i128) -> Self { + // In this function, we simply check that the input data can be casted. The `normalize` function will check whether more work needs to be done. + if nanos == 0 { + Self::ZERO + } else { + let centuries_i128 = nanos.div_euclid(NANOSECONDS_PER_CENTURY.into()); + let remaining_nanos_i128 = nanos.rem_euclid(NANOSECONDS_PER_CENTURY.into()); + if centuries_i128 > i16::MAX.into() { + Self::MAX + } else if centuries_i128 < i16::MIN.into() { + Self::MIN + } else { + // We know that the centuries fit, and we know that the nanos are less than the number + // of nanos per centuries, and rem_euclid guarantees that it's positive, so the + // casting will work fine every time. + Self::from_parts(centuries_i128 as i16, remaining_nanos_i128 as u64) + } + } + } + + #[must_use] + /// Create a new duration from the truncated nanoseconds (+/- 2927.1 years of duration) + pub fn from_truncated_nanoseconds(nanos: i64) -> Self { + if nanos < 0 { + let ns = nanos.unsigned_abs(); + // Note: i64::MIN corresponds to a duration just past -3 centuries, so we can't hit the Duration::MIN here. + let extra_centuries = ns.div_euclid(NANOSECONDS_PER_CENTURY); + let rem_nanos = ns.rem_euclid(NANOSECONDS_PER_CENTURY); + Self::from_parts( + -1 - (extra_centuries as i16), + NANOSECONDS_PER_CENTURY - rem_nanos, + ) + } else { + Self::from_parts(0, nanos.unsigned_abs()) + } + } + + /// Creates a new duration from the provided number of days + #[must_use] + pub fn from_days(value: f64) -> Self { + value * Unit::Day + } + + /// Creates a new duration from the provided number of hours + #[must_use] + pub fn from_hours(value: f64) -> Self { + value * Unit::Hour + } + + /// Creates a new duration from the provided number of seconds + #[must_use] + pub fn from_seconds(value: f64) -> Self { + value * Unit::Second + } + + /// Creates a new duration from the provided number of milliseconds + #[must_use] + pub fn from_milliseconds(value: f64) -> Self { + value * Unit::Millisecond + } + + /// Creates a new duration from the provided number of microsecond + #[must_use] + pub fn from_microseconds(value: f64) -> Self { + value * Unit::Microsecond + } + + /// Creates a new duration from the provided number of nanoseconds + #[must_use] + pub fn from_nanoseconds(value: f64) -> Self { + value * Unit::Nanosecond + } + + /// Creates a new duration from its parts. Set the sign to a negative number for the duration to be negative. + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn compose( + sign: i8, + days: u64, + hours: u64, + minutes: u64, + seconds: u64, + milliseconds: u64, + microseconds: u64, + nanoseconds: u64, + ) -> Self { + Self::compose_f64( + sign, + days as f64, + hours as f64, + minutes as f64, + seconds as f64, + milliseconds as f64, + microseconds as f64, + nanoseconds as f64, + ) + } + + /// Creates a new duration from its parts. Set the sign to a negative number for the duration to be negative. + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn compose_f64( + sign: i8, + days: f64, + hours: f64, + minutes: f64, + seconds: f64, + milliseconds: f64, + microseconds: f64, + nanoseconds: f64, + ) -> Self { + let me: Self = days.days() + + hours.hours() + + minutes.minutes() + + seconds.seconds() + + milliseconds.milliseconds() + + microseconds.microseconds() + + nanoseconds.nanoseconds(); + if sign < 0 { + -me + } else { + me + } + } + + /// Initializes a Duration from a timezone offset + #[must_use] + pub fn from_tz_offset(sign: i8, hours: i64, minutes: i64) -> Self { + let dur = hours * Unit::Hour + minutes * Unit::Minute; + if sign < 0 { + -dur + } else { + dur + } + } +} + +impl Duration { + fn normalize(&mut self) { + let extra_centuries = self.nanoseconds.div_euclid(NANOSECONDS_PER_CENTURY); + // We can skip this whole step if the div_euclid shows that we didn't overflow the number of nanoseconds per century + if extra_centuries > 0 { + let rem_nanos = self.nanoseconds.rem_euclid(NANOSECONDS_PER_CENTURY); + + if self.centuries == i16::MAX { + if self.nanoseconds.saturating_add(rem_nanos) > Self::MAX.nanoseconds { + // Saturated max + *self = Self::MAX; + } + // Else, we're near the MAX but we're within the MAX in nanoseconds, so let's not do anything here. + } else if *self != Self::MAX && *self != Self::MIN { + // The bounds are valid as is, no wrapping needed when rem_nanos is not zero. + match self.centuries.checked_add(extra_centuries as i16) { + Some(centuries) => { + self.centuries = centuries; + self.nanoseconds = rem_nanos; + } + None => { + if self.centuries >= 0 { + // Saturated max again + *self = Self::MAX; + } else { + // Saturated min + *self = Self::MIN; + } + } + } + } + } + } + + #[must_use] + /// Returns the centuries and nanoseconds of this duration + /// NOTE: These items are not public to prevent incorrect durations from being created by modifying the values of the structure directly. + pub const fn to_parts(&self) -> (i16, u64) { + (self.centuries, self.nanoseconds) + } + + /// Returns the total nanoseconds in a signed 128 bit integer + #[must_use] + pub fn total_nanoseconds(&self) -> i128 { + if self.centuries == -1 { + -i128::from(NANOSECONDS_PER_CENTURY - self.nanoseconds) + } else if self.centuries >= 0 { + i128::from(self.centuries) * i128::from(NANOSECONDS_PER_CENTURY) + + i128::from(self.nanoseconds) + } else { + // Centuries negative by a decent amount + i128::from(self.centuries) * i128::from(NANOSECONDS_PER_CENTURY) + - i128::from(self.nanoseconds) + } + } + + /// Returns the truncated nanoseconds in a signed 64 bit integer, if the duration fits. + pub fn try_truncated_nanoseconds(&self) -> Result { + // If it fits, we know that the nanoseconds also fit. abs() will fail if the centuries are min'ed out. + if self.centuries == i16::MIN || self.centuries.abs() >= 3 { + Err(Errors::Overflow) + } else if self.centuries == -1 { + Ok(-((NANOSECONDS_PER_CENTURY - self.nanoseconds) as i64)) + } else if self.centuries >= 0 { + match i64::from(self.centuries).checked_mul(NANOSECONDS_PER_CENTURY as i64) { + Some(centuries_as_ns) => { + match centuries_as_ns.checked_add(self.nanoseconds as i64) { + Some(truncated_ns) => Ok(truncated_ns), + None => Err(Errors::Overflow), + } + } + None => Err(Errors::Overflow), + } + } else { + // Centuries negative by a decent amount + Ok( + i64::from(self.centuries + 1) * NANOSECONDS_PER_CENTURY as i64 + + self.nanoseconds as i64, + ) + } + } + + /// Returns the truncated nanoseconds in a signed 64 bit integer, if the duration fits. + /// WARNING: This function will NOT fail and will return the i64::MIN or i64::MAX depending on + /// the sign of the centuries if the Duration does not fit on aa i64 + #[must_use] + pub fn truncated_nanoseconds(&self) -> i64 { + match self.try_truncated_nanoseconds() { + Ok(val) => val, + Err(_) => { + if self.centuries < 0 { + i64::MIN + } else { + i64::MAX + } + } + } + } + + /// Returns this duration in seconds f64. + /// For high fidelity comparisons, it is recommended to keep using the Duration structure. + #[must_use] + pub fn to_seconds(&self) -> f64 { + // Compute the seconds and nanoseconds that we know this fits on a 64bit float + let seconds = self.nanoseconds.div_euclid(NANOSECONDS_PER_SECOND); + let subseconds = self.nanoseconds.rem_euclid(NANOSECONDS_PER_SECOND); + if self.centuries == 0 { + (seconds as f64) + (subseconds as f64) * 1e-9 + } else { + f64::from(self.centuries) * SECONDS_PER_CENTURY + + (seconds as f64) + + (subseconds as f64) * 1e-9 + } + } + + #[must_use] + pub fn to_unit(&self, unit: Unit) -> f64 { + self.to_seconds() * unit.from_seconds() + } + + /// Returns the absolute value of this duration + #[must_use] + pub fn abs(&self) -> Self { + if self.centuries.is_negative() { + -*self + } else { + *self + } + } + + /// Returns the sign of this duration + /// + 0 if the number is zero + /// + 1 if the number is positive + /// + -1 if the number is negative + #[must_use] + pub const fn signum(&self) -> i8 { + self.centuries.signum() as i8 + } + + /// Decomposes a Duration in its sign, days, hours, minutes, seconds, ms, us, ns + #[must_use] + pub fn decompose(&self) -> (i8, u64, u64, u64, u64, u64, u64, u64) { + let sign = self.signum(); + + match self.try_truncated_nanoseconds() { + Ok(total_ns) => { + let ns_left = total_ns.abs(); + + let (days, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_DAY as i64); + let (hours, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_HOUR as i64); + let (minutes, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_MINUTE as i64); + let (seconds, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_SECOND as i64); + let (milliseconds, ns_left) = + div_rem_i64(ns_left, NANOSECONDS_PER_MILLISECOND as i64); + let (microseconds, ns_left) = + div_rem_i64(ns_left, NANOSECONDS_PER_MICROSECOND as i64); + + // Everything should fit in the expected types now + ( + sign, + days.try_into().unwrap(), + hours.try_into().unwrap(), + minutes.try_into().unwrap(), + seconds.try_into().unwrap(), + milliseconds.try_into().unwrap(), + microseconds.try_into().unwrap(), + ns_left.try_into().unwrap(), + ) + } + Err(_) => { + // Doesn't fit on a i64, so let's use the slower i128 + let total_ns = self.total_nanoseconds(); + let ns_left = total_ns.abs(); + + let (days, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_DAY)); + let (hours, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_HOUR)); + let (minutes, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_MINUTE)); + let (seconds, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_SECOND)); + let (milliseconds, ns_left) = + div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_MILLISECOND)); + let (microseconds, ns_left) = + div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_MICROSECOND)); + + // Everything should fit in the expected types now + ( + sign, + days.try_into().unwrap(), + hours.try_into().unwrap(), + minutes.try_into().unwrap(), + seconds.try_into().unwrap(), + milliseconds.try_into().unwrap(), + microseconds.try_into().unwrap(), + ns_left.try_into().unwrap(), + ) + } + } + } + + /// Floors this duration to the closest duration from the bottom + /// + /// # Example + /// ``` + /// use hifitime::{Duration, TimeUnits}; + /// + /// let two_hours_three_min = 2.hours() + 3.minutes(); + /// assert_eq!(two_hours_three_min.floor(1.hours()), 2.hours()); + /// assert_eq!(two_hours_three_min.floor(30.minutes()), 2.hours()); + /// // This is zero because we floor by a duration longer than the current duration, rounding it down + /// assert_eq!(two_hours_three_min.floor(4.hours()), 0.hours()); + /// assert_eq!(two_hours_three_min.floor(1.seconds()), two_hours_three_min); + /// assert_eq!(two_hours_three_min.floor(1.hours() + 1.minutes()), 2.hours() + 2.minutes()); + /// assert_eq!(two_hours_three_min.floor(1.hours() + 5.minutes()), 1.hours() + 5.minutes()); + /// ``` + pub fn floor(&self, duration: Self) -> Self { + Self::from_total_nanoseconds( + self.total_nanoseconds() - self.total_nanoseconds() % duration.total_nanoseconds(), + ) + } + + /// Ceils this duration to the closest provided duration + /// + /// This simply floors then adds the requested duration + /// + /// # Example + /// ``` + /// use hifitime::{Duration, TimeUnits}; + /// + /// let two_hours_three_min = 2.hours() + 3.minutes(); + /// assert_eq!(two_hours_three_min.ceil(1.hours()), 3.hours()); + /// assert_eq!(two_hours_three_min.ceil(30.minutes()), 2.hours() + 30.minutes()); + /// assert_eq!(two_hours_three_min.ceil(4.hours()), 4.hours()); + /// assert_eq!(two_hours_three_min.ceil(1.seconds()), two_hours_three_min + 1.seconds()); + /// assert_eq!(two_hours_three_min.ceil(1.hours() + 5.minutes()), 2.hours() + 10.minutes()); + /// ``` + pub fn ceil(&self, duration: Self) -> Self { + let floored = self.floor(duration); + match floored + .total_nanoseconds() + .checked_add(duration.abs().total_nanoseconds()) + { + Some(total_ns) => Self::from_total_nanoseconds(total_ns), + None => Self::MAX, + } + } + + /// Rounds this duration to the closest provided duration + /// + /// This performs both a `ceil` and `floor` and returns the value which is the closest to current one. + /// # Example + /// ``` + /// use hifitime::{Duration, TimeUnits}; + /// + /// let two_hours_three_min = 2.hours() + 3.minutes(); + /// assert_eq!(two_hours_three_min.round(1.hours()), 2.hours()); + /// assert_eq!(two_hours_three_min.round(30.minutes()), 2.hours()); + /// assert_eq!(two_hours_three_min.round(4.hours()), 4.hours()); + /// assert_eq!(two_hours_three_min.round(1.seconds()), two_hours_three_min); + /// assert_eq!(two_hours_three_min.round(1.hours() + 5.minutes()), 2.hours() + 10.minutes()); + /// ``` + pub fn round(&self, duration: Self) -> Self { + let floored = self.floor(duration); + let ceiled = self.ceil(duration); + if *self - floored < (ceiled - *self).abs() { + floored + } else { + ceiled + } + } + + /// Rounds this duration to the largest units represented in this duration. + /// + /// This is useful to provide an approximate human duration. Under the hood, this function uses `round`, + /// so the "tipping point" of the rounding is half way to the next increment of the greatest unit. + /// As shown below, one example is that 35 hours and 59 minutes rounds to 1 day, but 36 hours and 1 minute rounds + /// to 2 days because 2 days is closer to 36h 1 min than 36h 1 min is to 1 day. + /// + /// # Example + /// + /// ``` + /// use hifitime::{Duration, TimeUnits}; + /// + /// assert_eq!((2.hours() + 3.minutes()).approx(), 2.hours()); + /// assert_eq!((24.hours() + 3.minutes()).approx(), 1.days()); + /// assert_eq!((35.hours() + 59.minutes()).approx(), 1.days()); + /// assert_eq!((36.hours() + 1.minutes()).approx(), 2.days()); + /// assert_eq!((47.hours() + 3.minutes()).approx(), 2.days()); + /// assert_eq!((49.hours() + 3.minutes()).approx(), 2.days()); + /// ``` + pub fn approx(&self) -> Self { + let (_, days, hours, minutes, seconds, milli, us, _) = self.decompose(); + + let round_to = if days > 0 { + 1 * Unit::Day + } else if hours > 0 { + 1 * Unit::Hour + } else if minutes > 0 { + 1 * Unit::Minute + } else if seconds > 0 { + 1 * Unit::Second + } else if milli > 0 { + 1 * Unit::Millisecond + } else if us > 0 { + 1 * Unit::Microsecond + } else { + 1 * Unit::Nanosecond + }; + + self.round(round_to) + } + + // Returns the minimum of the two durations. + /// + /// ``` + /// use hifitime::TimeUnits; + /// + /// let d0 = 20.seconds(); + /// let d1 = 21.seconds(); + /// + /// assert_eq!(d0, d1.min(d0)); + /// assert_eq!(d0, d0.min(d1)); + /// ``` + pub fn min(self, other: Self) -> Self { + if self < other { + self + } else { + other + } + } + + /// Returns the maximum of the two durations. + /// + /// ``` + /// use hifitime::TimeUnits; + /// + /// let d0 = 20.seconds(); + /// let d1 = 21.seconds(); + /// + /// assert_eq!(d1, d1.max(d0)); + /// assert_eq!(d1, d0.max(d1)); + /// ``` + pub fn max(self, other: Self) -> Self { + if self > other { + self + } else { + other + } + } + + /// Returns whether this is a negative or positive duration. + pub const fn is_negative(&self) -> bool { + self.centuries.is_negative() + } +} + +impl fmt::Display for Duration { + // Prints this duration with automatic selection of the units, i.e. everything that isn't zero is ignored + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.total_nanoseconds() == 0 { + write!(f, "0 ns") + } else { + let (sign, days, hours, minutes, seconds, milli, us, nano) = self.decompose(); + if sign == -1 { + write!(f, "-")?; + } + + let values = [days, hours, minutes, seconds, milli, us, nano]; + let units = ["days", "h", "min", "s", "ms", "μs", "ns"]; + + let mut insert_space = false; + for (val, unit) in values.iter().zip(units.iter()) { + if *val > 0 { + if insert_space { + write!(f, " ")?; + } + write!(f, "{} {}", val, unit)?; + insert_space = true; + } + } + Ok(()) + } + } +} + +impl fmt::LowerExp for Duration { + // Prints the duration with appropriate units + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let seconds_f64 = self.to_seconds(); + let seconds_f64_abs = seconds_f64.abs(); + if seconds_f64_abs < 1e-5 { + fmt::Display::fmt(&(seconds_f64 * 1e9), f)?; + write!(f, " ns") + } else if seconds_f64_abs < 1e-2 { + fmt::Display::fmt(&(seconds_f64 * 1e3), f)?; + write!(f, " ms") + } else if seconds_f64_abs < 3.0 * SECONDS_PER_MINUTE { + fmt::Display::fmt(&(seconds_f64), f)?; + write!(f, " s") + } else if seconds_f64_abs < SECONDS_PER_HOUR { + fmt::Display::fmt(&(seconds_f64 / SECONDS_PER_MINUTE), f)?; + write!(f, " min") + } else if seconds_f64_abs < SECONDS_PER_DAY { + fmt::Display::fmt(&(seconds_f64 / SECONDS_PER_HOUR), f)?; + write!(f, " h") + } else { + fmt::Display::fmt(&(seconds_f64 / SECONDS_PER_DAY), f)?; + write!(f, " days") + } + } +} + +impl PartialEq for Duration { + #[allow(clippy::identity_op)] + fn eq(&self, unit: &Unit) -> bool { + *self == *unit * 1 + } +} + +impl PartialOrd for Duration { + #[allow(clippy::identity_op, clippy::comparison_chain)] + fn partial_cmp(&self, unit: &Unit) -> Option { + let unit_deref = *unit; + let unit_as_duration: Duration = unit_deref * 1; + if self < &unit_as_duration { + Some(Ordering::Less) + } else if self > &unit_as_duration { + Some(Ordering::Greater) + } else { + Some(Ordering::Equal) + } + } +} + +const fn div_rem_i128(me: i128, rhs: i128) -> (i128, i128) { + (me.div_euclid(rhs), me.rem_euclid(rhs)) +} + +const fn div_rem_i64(me: i64, rhs: i64) -> (i64, i64) { + (me.div_euclid(rhs), me.rem_euclid(rhs)) +} + +#[test] +#[cfg(feature = "serde")] +fn test_serdes() { + let dt = Duration::from_seconds(10.1); + let content = r#"{"centuries":0,"nanoseconds":10100000000}"#; + assert_eq!(content, serde_json::to_string(&dt).unwrap()); + let parsed: Duration = serde_json::from_str(content).unwrap(); + assert_eq!(dt, parsed); +} + +#[test] +fn test_bounds() { + let min = Duration::MIN; + assert_eq!(min.centuries, i16::MIN); + assert_eq!(min.nanoseconds, 0); + + let max = Duration::MAX; + assert_eq!(max.centuries, i16::MAX); + assert_eq!(max.nanoseconds, NANOSECONDS_PER_CENTURY); + + let min_p = Duration::MIN_POSITIVE; + assert_eq!(min_p.centuries, 0); + assert_eq!(min_p.nanoseconds, 1); + + let min_n = Duration::MIN_NEGATIVE; + assert_eq!(min_n.centuries, -1); + assert_eq!(min_n.nanoseconds, NANOSECONDS_PER_CENTURY - 1); + + let min_n1 = Duration::MIN - 1 * Unit::Nanosecond; + assert_eq!(min_n1, Duration::MIN); + + let max_n1 = Duration::MAX - 1 * Unit::Nanosecond; + assert_eq!(max_n1.centuries, i16::MAX); + assert_eq!(max_n1.nanoseconds, NANOSECONDS_PER_CENTURY - 1); +} diff --git a/src/duration/ops.rs b/src/duration/ops.rs new file mode 100644 index 00000000..69c0605f --- /dev/null +++ b/src/duration/ops.rs @@ -0,0 +1,377 @@ +/* +* Hifitime, part of the Nyx Space tools +* Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) +* This Source Code Form is subject to the terms of the Apache +* v. 2.0. If a copy of the Apache License was not distributed with this +* file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. +* +* Documentation: https://nyxspace.com/ +*/ + +// Here lives all of the operations on Duration. + +use crate::{ + NANOSECONDS_PER_CENTURY, NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, + NANOSECONDS_PER_SECOND, +}; + +use super::{Duration, Freq, Frequencies, TimeUnits, Unit}; + +use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; + +macro_rules! impl_ops_for_type { + ($type:ident) => { + impl Mul for $type { + type Output = Duration; + fn mul(self, q: Unit) -> Duration { + // Apply the reflexive property + q * self + } + } + + impl Mul<$type> for Freq { + type Output = Duration; + + /// Converts the input values to i128 and creates a duration from that + /// This method will necessarily ignore durations below nanoseconds + fn mul(self, q: $type) -> Duration { + let total_ns = match self { + Freq::GigaHertz => 1.0 / (q as f64), + Freq::MegaHertz => (NANOSECONDS_PER_MICROSECOND as f64) / (q as f64), + Freq::KiloHertz => NANOSECONDS_PER_MILLISECOND as f64 / (q as f64), + Freq::Hertz => (NANOSECONDS_PER_SECOND as f64) / (q as f64), + }; + if total_ns.abs() < (i64::MAX as f64) { + Duration::from_truncated_nanoseconds(total_ns as i64) + } else { + Duration::from_total_nanoseconds(total_ns as i128) + } + } + } + + impl Mul for $type { + type Output = Duration; + fn mul(self, q: Freq) -> Duration { + // Apply the reflexive property + q * self + } + } + + #[allow(clippy::suspicious_arithmetic_impl)] + impl Div<$type> for Duration { + type Output = Duration; + fn div(self, q: $type) -> Self::Output { + Duration::from_total_nanoseconds( + self.total_nanoseconds() + .saturating_div((q * Unit::Nanosecond).total_nanoseconds()), + ) + } + } + + impl Mul for $type { + type Output = Duration; + fn mul(self, q: Self::Output) -> Self::Output { + // Apply the reflexive property + q * self + } + } + + impl TimeUnits for $type {} + + impl Frequencies for $type {} + }; +} + +impl_ops_for_type!(f64); +impl_ops_for_type!(i64); + +impl Mul for Duration { + type Output = Duration; + fn mul(self, q: i64) -> Self::Output { + Duration::from_total_nanoseconds( + self.total_nanoseconds() + .saturating_mul((q * Unit::Nanosecond).total_nanoseconds()), + ) + } +} + +impl Mul for Duration { + type Output = Duration; + fn mul(self, q: f64) -> Self::Output { + // Make sure that we don't trim the number by finding its precision + let mut p: i32 = 0; + let mut new_val = q; + let ten: f64 = 10.0; + + loop { + if (new_val.floor() - new_val).abs() < f64::EPSILON { + // Yay, we've found the precision of this number + break; + } + // Multiply by the precision + // https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b760579f103b7192c20413ebbe167b90 + p += 1; + new_val = q * ten.powi(p); + } + + Duration::from_total_nanoseconds( + self.total_nanoseconds() + .saturating_mul(new_val as i128) + .saturating_div(10_i128.pow(p.try_into().unwrap())), + ) + } +} + +impl Add for Duration { + type Output = Duration; + + /// # Addition of Durations + /// Durations are centered on zero duration. Of the tuple, only the centuries may be negative, the nanoseconds are always positive + /// and represent the nanoseconds _into_ the current centuries. + /// + /// ## Examples + /// + `Duration { centuries: 0, nanoseconds: 1 }` is a positive duration of zero centuries and one nanosecond. + /// + `Duration { centuries: -1, nanoseconds: 1 }` is a negative duration representing "one century before zero minus one nanosecond" + fn add(self, rhs: Self) -> Duration { + // Check that the addition fits in an i16 + let mut me = self; + match me.centuries.checked_add(rhs.centuries) { + None => { + // Overflowed, so we've hit the bound. + if me.centuries < 0 { + // We've hit the negative bound, so return MIN. + return Self::MIN; + } else { + // We've hit the positive bound, so return MAX. + return Self::MAX; + } + } + Some(centuries) => { + me.centuries = centuries; + } + } + + if me.centuries == Self::MIN.centuries && self.nanoseconds < Self::MIN.nanoseconds { + // Then we do the operation backward + match me + .nanoseconds + .checked_sub(NANOSECONDS_PER_CENTURY - rhs.nanoseconds) + { + Some(nanos) => me.nanoseconds = nanos, + None => { + me.centuries += 1; // Safe because we're at the MIN + me.nanoseconds = rhs.nanoseconds + } + } + } else { + match me.nanoseconds.checked_add(rhs.nanoseconds) { + Some(nanoseconds) => me.nanoseconds = nanoseconds, + None => { + // Rare case where somehow the input data was not normalized. So let's normalize it and call add again. + let mut rhs = rhs; + rhs.normalize(); + + match me.centuries.checked_add(rhs.centuries) { + None => return Self::MAX, + Some(centuries) => me.centuries = centuries, + }; + // Now it will fit! + me.nanoseconds += rhs.nanoseconds; + } + } + } + + me.normalize(); + me + } +} + +impl AddAssign for Duration { + fn add_assign(&mut self, rhs: Duration) { + *self = *self + rhs; + } +} + +impl Sub for Duration { + type Output = Self; + + /// # Subtraction + /// This operation is a notch confusing with negative durations. + /// As described in the `Duration` structure, a Duration of (-1, NANOSECONDS_PER_CENTURY-1) is closer to zero + /// than (-1, 0). + /// + /// ## Algorithm + /// + /// ### A > B, and both are positive + /// + /// If A > B, then A.centuries is subtracted by B.centuries, and A.nanoseconds is subtracted by B.nanoseconds. + /// If an overflow occurs, e.g. A.nanoseconds < B.nanoseconds, the number of nanoseconds is increased by the number of nanoseconds per century, + /// and the number of centuries is decreased by one. + /// + /// ``` + /// use hifitime::{Duration, NANOSECONDS_PER_CENTURY}; + /// + /// let a = Duration::from_parts(1, 1); + /// let b = Duration::from_parts(0, 10); + /// let c = Duration::from_parts(0, NANOSECONDS_PER_CENTURY - 9); + /// assert_eq!(a - b, c); + /// ``` + /// + /// ### A < B, and both are positive + /// + /// In this case, the resulting duration will be negative. The number of centuries is a signed integer, so it is set to the difference of A.centuries - B.centuries. + /// The number of nanoseconds however must be wrapped by the number of nanoseconds per century. + /// For example:, let A = (0, 1) and B = (1, 10), then the resulting duration will be (-2, NANOSECONDS_PER_CENTURY - (10 - 1)). In this case, the centuries are set + /// to -2 because B is _two_ centuries into the future (the number of centuries into the future is zero-indexed). + /// ``` + /// use hifitime::{Duration, NANOSECONDS_PER_CENTURY}; + /// + /// let a = Duration::from_parts(0, 1); + /// let b = Duration::from_parts(1, 10); + /// let c = Duration::from_parts(-2, NANOSECONDS_PER_CENTURY - 9); + /// assert_eq!(a - b, c); + /// ``` + /// + /// ### A > B, both are negative + /// + /// In this case, we try to stick to normal arithmatics: (-9 - -10) = (-9 + 10) = +1. + /// In this case, we can simply add the components of the duration together. + /// For example, let A = (-1, NANOSECONDS_PER_CENTURY - 2), and B = (-1, NANOSECONDS_PER_CENTURY - 1). Respectively, A is _two_ nanoseconds _before_ Duration::ZERO + /// and B is _one_ nanosecond before Duration::ZERO. Then, A-B should be one nanoseconds before zero, i.e. (-1, NANOSECONDS_PER_CENTURY - 1). + /// This is because we _subtract_ "negative one nanosecond" from a "negative minus two nanoseconds", which corresponds to _adding_ the opposite, and the + /// opposite of "negative one nanosecond" is "positive one nanosecond". + /// + /// ``` + /// use hifitime::{Duration, NANOSECONDS_PER_CENTURY}; + /// + /// let a = Duration::from_parts(-1, NANOSECONDS_PER_CENTURY - 9); + /// let b = Duration::from_parts(-1, NANOSECONDS_PER_CENTURY - 10); + /// let c = Duration::from_parts(0, 1); + /// assert_eq!(a - b, c); + /// ``` + /// + /// ### A < B, both are negative + /// + /// Just like in the prior case, we try to stick to normal arithmatics: (-10 - -9) = (-10 + 9) = -1. + /// + /// ``` + /// use hifitime::{Duration, NANOSECONDS_PER_CENTURY}; + /// + /// let a = Duration::from_parts(-1, NANOSECONDS_PER_CENTURY - 10); + /// let b = Duration::from_parts(-1, NANOSECONDS_PER_CENTURY - 9); + /// let c = Duration::from_parts(-1, NANOSECONDS_PER_CENTURY - 1); + /// assert_eq!(a - b, c); + /// ``` + /// + /// ### MIN is the minimum + /// + /// One cannot subtract anything from the MIN. + /// + /// ``` + /// use hifitime::Duration; + /// + /// let one_ns = Duration::from_parts(0, 1); + /// assert_eq!(Duration::MIN - one_ns, Duration::MIN); + /// ``` + fn sub(self, rhs: Self) -> Self { + let mut me = self; + match me.centuries.checked_sub(rhs.centuries) { + None => { + // Underflowed, so we've hit the min + return Self::MIN; + } + Some(centuries) => { + me.centuries = centuries; + } + } + + match me.nanoseconds.checked_sub(rhs.nanoseconds) { + None => { + // Decrease the number of centuries, and realign + match me.centuries.checked_sub(1) { + Some(centuries) => { + me.centuries = centuries; + me.nanoseconds = me.nanoseconds + NANOSECONDS_PER_CENTURY - rhs.nanoseconds; + } + None => { + // We're at the min number of centuries already, and we have extra nanos, so we're saturated the duration limit + return Self::MIN; + } + }; + // me.nanoseconds = me.nanoseconds + NANOSECONDS_PER_CENTURY - rhs.nanoseconds; + } + Some(nanos) => me.nanoseconds = nanos, + }; + + me.normalize(); + me + } +} + +impl SubAssign for Duration { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +// Allow adding with a Unit directly +impl Add for Duration { + type Output = Self; + + #[allow(clippy::identity_op)] + fn add(self, rhs: Unit) -> Self { + self + rhs * 1 + } +} + +impl AddAssign for Duration { + #[allow(clippy::identity_op)] + fn add_assign(&mut self, rhs: Unit) { + *self = *self + rhs * 1; + } +} + +impl Sub for Duration { + type Output = Duration; + + #[allow(clippy::identity_op)] + fn sub(self, rhs: Unit) -> Duration { + self - rhs * 1 + } +} + +impl SubAssign for Duration { + #[allow(clippy::identity_op)] + fn sub_assign(&mut self, rhs: Unit) { + *self = *self - rhs * 1; + } +} + +impl Neg for Duration { + type Output = Self; + + #[must_use] + fn neg(self) -> Self::Output { + if self == Self::MIN { + Self::MAX + } else if self == Self::MAX { + Self::MIN + } else { + match NANOSECONDS_PER_CENTURY.checked_sub(self.nanoseconds) { + Some(nanoseconds) => { + // yay + Self::from_parts(-self.centuries - 1, nanoseconds) + } + None => { + if self > Duration::ZERO { + let dur_to_max = Self::MAX - self; + Self::MIN + dur_to_max + } else { + let dur_to_min = Self::MIN + self; + Self::MAX - dur_to_min + } + } + } + } + } +} diff --git a/src/duration/parse.rs b/src/duration/parse.rs new file mode 100644 index 00000000..81121150 --- /dev/null +++ b/src/duration/parse.rs @@ -0,0 +1,184 @@ +/* +* Hifitime, part of the Nyx Space tools +* Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) +* This Source Code Form is subject to the terms of the Apache +* v. 2.0. If a copy of the Apache License was not distributed with this +* file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. +* +* Documentation: https://nyxspace.com/ +*/ + +use super::{Duration, Unit}; +use crate::{Errors, ParsingErrors}; +use core::str::FromStr; + +impl FromStr for Duration { + type Err = Errors; + + /// Attempts to convert a simple string to a Duration. Does not yet support complicated durations. + /// + /// Identifiers: + /// + d, days, day + /// + h, hours, hour + /// + min, mins, minute + /// + s, second, seconds + /// + ms, millisecond, milliseconds + /// + us, microsecond, microseconds + /// + ns, nanosecond, nanoseconds + /// + `+` or `-` indicates a timezone offset + /// + /// # Example + /// ``` + /// use hifitime::{Duration, Unit}; + /// use std::str::FromStr; + /// + /// assert_eq!(Duration::from_str("1 d").unwrap(), Unit::Day * 1); + /// assert_eq!(Duration::from_str("10.598 days").unwrap(), Unit::Day * 10.598); + /// assert_eq!(Duration::from_str("10.598 min").unwrap(), Unit::Minute * 10.598); + /// assert_eq!(Duration::from_str("10.598 us").unwrap(), Unit::Microsecond * 10.598); + /// assert_eq!(Duration::from_str("10.598 seconds").unwrap(), Unit::Second * 10.598); + /// assert_eq!(Duration::from_str("10.598 nanosecond").unwrap(), Unit::Nanosecond * 10.598); + /// assert_eq!(Duration::from_str("5 h 256 ms 1 ns").unwrap(), 5 * Unit::Hour + 256 * Unit::Millisecond + Unit::Nanosecond); + /// assert_eq!(Duration::from_str("-01:15:30").unwrap(), -(1 * Unit::Hour + 15 * Unit::Minute + 30 * Unit::Second)); + /// assert_eq!(Duration::from_str("+3615").unwrap(), 36 * Unit::Hour + 15 * Unit::Minute); + /// ``` + fn from_str(s_in: &str) -> Result { + // Each part of a duration as days, hours, minutes, seconds, millisecond, microseconds, and nanoseconds + let mut decomposed = [0.0_f64; 7]; + + let mut prev_idx = 0; + let mut seeking_number = true; + let mut latest_value = 0.0; + + let s = s_in.trim(); + + if s.is_empty() { + return Err(Errors::ParseError(ParsingErrors::ValueError)); + } + + // There is at least one character, so we can unwrap this. + if let Some(char) = s.chars().next() { + if char == '+' || char == '-' { + // This is a timezone offset. + let offset_sign = if char == '-' { -1 } else { 1 }; + + let indexes: (usize, usize, usize) = (1, 3, 5); + let colon = if s.len() == 3 || s.len() == 5 || s.len() == 7 { + // There is a zero or even number of separators between the hours, minutes, and seconds. + // Only zero (or one) characters separator is supported. This will return a ValueError later if there is + // an even but greater than one character separator. + 0 + } else if s.len() == 4 || s.len() == 6 || s.len() == 9 { + // There is an odd number of characters as a separator between the hours, minutes, and seconds. + // Only one character separator is supported. This will return a ValueError later if there is + // an odd but greater than one character separator. + 1 + } else { + // This invalid + return Err(Errors::ParseError(ParsingErrors::ValueError)); + }; + + // Fetch the hours + let hours: i64 = match lexical_core::parse(s[indexes.0..indexes.1].as_bytes()) { + Ok(val) => val, + Err(_) => return Err(Errors::ParseError(ParsingErrors::ValueError)), + }; + + let mut minutes: i64 = 0; + let mut seconds: i64 = 0; + + match s.get(indexes.1 + colon..indexes.2 + colon) { + None => { + //Do nothing, we've reached the end of the useful data. + } + Some(subs) => { + // Fetch the minutes + match lexical_core::parse(subs.as_bytes()) { + Ok(val) => minutes = val, + Err(_) => return Err(Errors::ParseError(ParsingErrors::ValueError)), + } + + match s.get(indexes.2 + 2 * colon..) { + None => { + // Do nothing, there are no seconds inthis offset + } + Some(subs) => { + if !subs.is_empty() { + // Fetch the seconds + match lexical_core::parse(subs.as_bytes()) { + Ok(val) => seconds = val, + Err(_) => { + return Err(Errors::ParseError( + ParsingErrors::ValueError, + )) + } + } + } + } + } + } + } + + // Return the constructed offset + if offset_sign == -1 { + return Ok(-(hours * Unit::Hour + + minutes * Unit::Minute + + seconds * Unit::Second)); + } else { + return Ok(hours * Unit::Hour + + minutes * Unit::Minute + + seconds * Unit::Second); + } + } + }; + + for (idx, char) in s.chars().enumerate() { + if char == ' ' || idx == s.len() - 1 { + if seeking_number { + if prev_idx == idx { + // We've reached the end of the string and it didn't end with a unit + return Err(Errors::ParseError(ParsingErrors::UnknownOrMissingUnit)); + } + // We've found a new space so let's parse whatever precedes it + match lexical_core::parse(s[prev_idx..idx].as_bytes()) { + Ok(val) => latest_value = val, + Err(_) => return Err(Errors::ParseError(ParsingErrors::ValueError)), + } + // We'll now seek a unit + seeking_number = false; + } else { + // We're seeking a unit not a number, so let's parse the unit we just found and remember the position. + let end_idx = if idx == s.len() - 1 { idx + 1 } else { idx }; + let pos = match &s[prev_idx..end_idx] { + "d" | "days" | "day" => 0, + "h" | "hours" | "hour" => 1, + "min" | "mins" | "minute" | "minutes" => 2, + "s" | "second" | "seconds" => 3, + "ms" | "millisecond" | "milliseconds" => 4, + "us" | "microsecond" | "microseconds" => 5, + "ns" | "nanosecond" | "nanoseconds" => 6, + _ => { + return Err(Errors::ParseError(ParsingErrors::UnknownOrMissingUnit)); + } + }; + // Store the value + decomposed[pos] = latest_value; + // Now we switch to seeking a value + seeking_number = true; + } + prev_idx = idx + 1; + } + } + + Ok(Duration::compose_f64( + 1, + decomposed[0], + decomposed[1], + decomposed[2], + decomposed[3], + decomposed[4], + decomposed[5], + decomposed[6], + )) + } +} diff --git a/src/duration/python.rs b/src/duration/python.rs new file mode 100644 index 00000000..d6d29dee --- /dev/null +++ b/src/duration/python.rs @@ -0,0 +1,314 @@ +/* +* Hifitime, part of the Nyx Space tools +* Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) +* This Source Code Form is subject to the terms of the Apache +* v. 2.0. If a copy of the Apache License was not distributed with this +* file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. +* +* Documentation: https://nyxspace.com/ +*/ + +// Here lives all of the implementations that are only built with the std flag + +use super::{Duration, Unit}; + +use pyo3::prelude::*; +use pyo3::pyclass::CompareOp; +use pyo3::types::PyType; +use std::str::FromStr; + +#[pymethods] +impl Duration { + #[must_use] + /// Returns the centuries and nanoseconds of this duration + /// NOTE: These items are not public to prevent incorrect durations from being created by modifying the values of the structure directly. + #[pyo3(name = "to_parts")] + pub const fn py_to_parts(&self) -> (i16, u64) { + (self.centuries, self.nanoseconds) + } + + /// Returns the total nanoseconds in a signed 128 bit integer + #[pyo3(name = "total_nanoseconds")] + pub fn py_total_nanoseconds(&self) -> i128 { + self.total_nanoseconds() + } + + /// Returns this duration in seconds f64. + /// For high fidelity comparisons, it is recommended to keep using the Duration structure. + #[pyo3(name = "to_seconds")] + pub fn py_to_seconds(&self) -> f64 { + self.to_seconds() + } + + #[pyo3(name = "to_unit")] + pub fn py_to_unit(&self, unit: Unit) -> f64 { + self.to_unit(unit) + } + + /// Returns the absolute value of this duration + #[pyo3(name = "abs")] + pub fn py_abs(&self) -> Self { + self.abs() + } + + /// Returns the sign of this duration + /// + 0 if the number is zero + /// + 1 if the number is positive + /// + -1 if the number is negative + #[pyo3(name = "signum")] + pub const fn py_signum(&self) -> i8 { + self.signum() + } + + /// Decomposes a Duration in its sign, days, hours, minutes, seconds, ms, us, ns + #[pyo3(name = "decompose")] + pub fn py_decompose(&self) -> (i8, u64, u64, u64, u64, u64, u64, u64) { + self.decompose() + } + + /// Floors this duration to the closest duration from the bottom + /// + /// # Example + /// ``` + /// use hifitime::{Duration, TimeUnits}; + /// + /// let two_hours_three_min = 2.hours() + 3.minutes(); + /// assert_eq!(two_hours_three_min.floor(1.hours()), 2.hours()); + /// assert_eq!(two_hours_three_min.floor(30.minutes()), 2.hours()); + /// // This is zero because we floor by a duration longer than the current duration, rounding it down + /// assert_eq!(two_hours_three_min.floor(4.hours()), 0.hours()); + /// assert_eq!(two_hours_three_min.floor(1.seconds()), two_hours_three_min); + /// assert_eq!(two_hours_three_min.floor(1.hours() + 1.minutes()), 2.hours() + 2.minutes()); + /// assert_eq!(two_hours_three_min.floor(1.hours() + 5.minutes()), 1.hours() + 5.minutes()); + /// ``` + #[pyo3(name = "floor")] + pub fn py_floor(&self, duration: Self) -> Self { + self.floor(duration) + } + + /// Ceils this duration to the closest provided duration + /// + /// This simply floors then adds the requested duration + /// + /// # Example + /// ``` + /// use hifitime::{Duration, TimeUnits}; + /// + /// let two_hours_three_min = 2.hours() + 3.minutes(); + /// assert_eq!(two_hours_three_min.ceil(1.hours()), 3.hours()); + /// assert_eq!(two_hours_three_min.ceil(30.minutes()), 2.hours() + 30.minutes()); + /// assert_eq!(two_hours_three_min.ceil(4.hours()), 4.hours()); + /// assert_eq!(two_hours_three_min.ceil(1.seconds()), two_hours_three_min + 1.seconds()); + /// assert_eq!(two_hours_three_min.ceil(1.hours() + 5.minutes()), 2.hours() + 10.minutes()); + /// ``` + #[pyo3(name = "ceil")] + pub fn py_ceil(&self, duration: Self) -> Self { + self.ceil(duration) + } + + /// Rounds this duration to the closest provided duration + /// + /// This performs both a `ceil` and `floor` and returns the value which is the closest to current one. + /// # Example + /// ``` + /// use hifitime::{Duration, TimeUnits}; + /// + /// let two_hours_three_min = 2.hours() + 3.minutes(); + /// assert_eq!(two_hours_three_min.round(1.hours()), 2.hours()); + /// assert_eq!(two_hours_three_min.round(30.minutes()), 2.hours()); + /// assert_eq!(two_hours_three_min.round(4.hours()), 4.hours()); + /// assert_eq!(two_hours_three_min.round(1.seconds()), two_hours_three_min); + /// assert_eq!(two_hours_three_min.round(1.hours() + 5.minutes()), 2.hours() + 10.minutes()); + /// ``` + #[pyo3(name = "round")] + pub fn py_round(&self, duration: Self) -> Self { + self.round(duration) + } + + /// Rounds this duration to the largest units represented in this duration. + /// + /// This is useful to provide an approximate human duration. Under the hood, this function uses `round`, + /// so the "tipping point" of the rounding is half way to the next increment of the greatest unit. + /// As shown below, one example is that 35 hours and 59 minutes rounds to 1 day, but 36 hours and 1 minute rounds + /// to 2 days because 2 days is closer to 36h 1 min than 36h 1 min is to 1 day. + /// + /// # Example + /// + /// ``` + /// use hifitime::{Duration, TimeUnits}; + /// + /// assert_eq!((2.hours() + 3.minutes()).approx(), 2.hours()); + /// assert_eq!((24.hours() + 3.minutes()).approx(), 1.days()); + /// assert_eq!((35.hours() + 59.minutes()).approx(), 1.days()); + /// assert_eq!((36.hours() + 1.minutes()).approx(), 2.days()); + /// assert_eq!((47.hours() + 3.minutes()).approx(), 2.days()); + /// assert_eq!((49.hours() + 3.minutes()).approx(), 2.days()); + /// ``` + #[pyo3(name = "approx")] + pub fn py_approx(&self) -> Self { + self.approx() + } + + /// Returns the minimum of the two durations. + /// + /// ``` + /// use hifitime::TimeUnits; + /// + /// let d0 = 20.seconds(); + /// let d1 = 21.seconds(); + /// + /// assert_eq!(d0, d1.min(d0)); + /// assert_eq!(d0, d0.min(d1)); + /// ``` + #[pyo3(name = "min")] + pub fn py_min(&self, other: Self) -> Self { + *(self.min(&other)) + } + + /// Returns the maximum of the two durations. + /// + /// ``` + /// use hifitime::TimeUnits; + /// + /// let d0 = 20.seconds(); + /// let d1 = 21.seconds(); + /// + /// assert_eq!(d1, d1.max(d0)); + /// assert_eq!(d1, d0.max(d1)); + /// ``` + #[pyo3(name = "max")] + pub fn py_max(&self, other: Self) -> Self { + *(self.max(&other)) + } + + /// Returns whether this is a negative or positive duration. + #[pyo3(name = "is_negative")] + pub fn py_is_negative(&self) -> bool { + self.is_negative() + } + + #[new] + fn new_py(string_repr: String) -> PyResult { + match Self::from_str(&string_repr) { + Ok(d) => Ok(d), + Err(e) => Err(PyErr::from(e)), + } + } + + fn __str__(&self) -> String { + format!("{self}") + } + + fn __repr__(&self) -> String { + format!("{self}") + } + + fn __add__(&self, other: Self) -> Duration { + *self + other + } + + fn __sub__(&self, other: Self) -> Duration { + *self - other + } + + fn __mul__(&self, other: f64) -> Duration { + *self * other + } + + fn __div__(&self, other: f64) -> Duration { + *self / other + } + + fn __eq__(&self, other: Self) -> bool { + *self == other + } + + fn __richcmp__(&self, other: Self, op: CompareOp) -> bool { + match op { + CompareOp::Lt => *self < other, + CompareOp::Le => *self <= other, + CompareOp::Eq => *self == other, + CompareOp::Ne => *self != other, + CompareOp::Gt => *self > other, + CompareOp::Ge => *self >= other, + } + } + + // Python constructors + + #[classmethod] + #[pyo3(name = "ZERO")] + fn zero(_cls: &PyType) -> Duration { + Duration::ZERO + } + + #[classmethod] + #[pyo3(name = "EPSILON")] + fn epsilon(_cls: &PyType) -> Duration { + Duration::EPSILON + } + + #[classmethod] + #[pyo3(name = "MAX")] + fn py_from_max(_cls: &PyType) -> Duration { + Duration::MAX + } + + #[classmethod] + #[pyo3(name = "MIN")] + fn py_from_min(_cls: &PyType) -> Duration { + Duration::MIN + } + + #[classmethod] + #[pyo3(name = "MIN_POSITIVE")] + fn min_positive(_cls: &PyType) -> Duration { + Duration::MIN_POSITIVE + } + + #[classmethod] + #[pyo3(name = "MIN_NEGATIVE")] + fn min_negative(_cls: &PyType) -> Duration { + Duration::MIN_NEGATIVE + } + + #[classmethod] + #[pyo3(name = "from_parts")] + /// Create a normalized duration from its parts + fn py_from_parts(_cls: &PyType, centuries: i16, nanoseconds: u64) -> Self { + Self::from_parts(centuries, nanoseconds) + } + + /// Creates a new duration from its parts + #[allow(clippy::too_many_arguments)] + #[classmethod] + #[pyo3(name = "from_all_parts")] + fn py_from_all_parts( + _cls: &PyType, + sign: i8, + days: u64, + hours: u64, + minutes: u64, + seconds: u64, + milliseconds: u64, + microseconds: u64, + nanoseconds: u64, + ) -> Self { + Self::compose( + sign, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + ) + } + + #[classmethod] + #[pyo3(name = "from_total_nanoseconds")] + fn py_from_total_nanoseconds(_cls: &PyType, nanos: i128) -> Self { + Self::from_total_nanoseconds(nanos) + } +} diff --git a/src/duration/std.rs b/src/duration/std.rs new file mode 100644 index 00000000..93395146 --- /dev/null +++ b/src/duration/std.rs @@ -0,0 +1,45 @@ +/* +* Hifitime, part of the Nyx Space tools +* Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) +* This Source Code Form is subject to the terms of the Apache +* v. 2.0. If a copy of the Apache License was not distributed with this +* file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. +* +* Documentation: https://nyxspace.com/ +*/ + +// Here lives all of the implementations that are only built with the std flag + +extern crate core; + +use super::{Duration, Unit}; + +impl From for std::time::Duration { + /// Converts a duration into an std::time::Duration + /// + /// # Limitations + /// 1. If the duration is negative, this will return a std::time::Duration::ZERO. + /// 2. If the duration larger than the MAX duration, this will return std::time::Duration::MAX + fn from(hf_duration: Duration) -> Self { + let (sign, days, hours, minutes, seconds, milli, us, nano) = hf_duration.decompose(); + if sign < 0 { + std::time::Duration::ZERO + } else { + // Build the seconds separately from the nanos. + let above_ns_f64: f64 = + Duration::compose(sign, days, hours, minutes, seconds, milli, us, 0).to_seconds(); + std::time::Duration::new(above_ns_f64 as u64, nano as u32) + } + } +} + +impl From for Duration { + /// Converts a duration into an std::time::Duration + /// + /// # Limitations + /// 1. If the duration is negative, this will return a std::time::Duration::ZERO. + /// 2. If the duration larger than the MAX duration, this will return std::time::Duration::MAX + fn from(std_duration: std::time::Duration) -> Self { + std_duration.as_secs_f64() * Unit::Second + } +} diff --git a/src/efmt/consts.rs b/src/efmt/consts.rs index 020795f2..778e38fc 100644 --- a/src/efmt/consts.rs +++ b/src/efmt/consts.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/efmt/format.rs b/src/efmt/format.rs index 4333eb44..1e05143c 100644 --- a/src/efmt/format.rs +++ b/src/efmt/format.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/efmt/formatter.rs b/src/efmt/formatter.rs index f0f85642..1c3fd2b2 100644 --- a/src/efmt/formatter.rs +++ b/src/efmt/formatter.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/efmt/mod.rs b/src/efmt/mod.rs index 8f193283..83eff8d5 100644 --- a/src/efmt/mod.rs +++ b/src/efmt/mod.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/epoch.rs b/src/epoch.rs index 2dcd060d..2f5e38e4 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. @@ -626,14 +626,14 @@ impl Epoch { /// Initialize an Epoch from the number of seconds since the GPS Time Epoch, /// defined as UTC midnight of January 5th to 6th 1980 (cf. ). pub fn from_gpst_seconds(seconds: f64) -> Self { - Self::from_duration(Duration::from_f64(seconds, Unit::Second), TimeScale::GPST) + Self::from_duration(seconds * Unit::Second, TimeScale::GPST) } #[must_use] /// Initialize an Epoch from the number of days since the GPS Time Epoch, /// defined as UTC midnight of January 5th to 6th 1980 (cf. ). pub fn from_gpst_days(days: f64) -> Self { - Self::from_duration(Duration::from_f64(days, Unit::Day), TimeScale::GPST) + Self::from_duration(days * Unit::Day, TimeScale::GPST) } #[must_use] @@ -648,14 +648,14 @@ impl Epoch { /// Initialize an Epoch from the number of seconds since the QZSS Time Epoch, /// defined as UTC midnight of January 5th to 6th 1980 (cf. ). pub fn from_qzsst_seconds(seconds: f64) -> Self { - Self::from_duration(Duration::from_f64(seconds, Unit::Second), TimeScale::QZSST) + Self::from_duration(seconds * Unit::Second, TimeScale::QZSST) } #[must_use] /// Initialize an Epoch from the number of days since the QZSS Time Epoch, /// defined as UTC midnight of January 5th to 6th 1980 (cf. ). pub fn from_qzsst_days(days: f64) -> Self { - Self::from_duration(Duration::from_f64(days, Unit::Day), TimeScale::QZSST) + Self::from_duration(days * Unit::Day, TimeScale::QZSST) } #[must_use] @@ -2996,7 +2996,7 @@ impl FromStr for Epoch { TimeScale::TDB => Ok(Self::from_tdb_seconds(value)), TimeScale::TT => Ok(Self::from_tt_seconds(value)), ts => { - let secs = Duration::from_f64(value, Unit::Second); + let secs = value * Unit::Second; Ok(Self::from_duration(secs, ts)) } }, diff --git a/src/errors.rs b/src/errors.rs index a7eb91c2..a03f7192 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/leap_seconds.rs b/src/leap_seconds.rs index 54687c6b..c0f0ac43 100644 --- a/src/leap_seconds.rs +++ b/src/leap_seconds.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/leap_seconds_file.rs b/src/leap_seconds_file.rs index a0a1dd46..0e9b0ae8 100644 --- a/src/leap_seconds_file.rs +++ b/src/leap_seconds_file.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/lib.rs b/src/lib.rs index 3d0ab405..e5d1271d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/month.rs b/src/month.rs index 5fe29ea4..fcde44d8 100644 --- a/src/month.rs +++ b/src/month.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/parser.rs b/src/parser.rs index 78e498d9..02d5c3ae 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/python.rs b/src/python.rs index ca95994d..de9c0df1 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/timescale.rs b/src/timescale.rs index c7a033b6..64008ef1 100644 --- a/src/timescale.rs +++ b/src/timescale.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/timeseries.rs b/src/timeseries.rs index cda7958e..20996f57 100644 --- a/src/timeseries.rs +++ b/src/timeseries.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/timeunits.rs b/src/timeunits.rs index 37befe51..fac413cc 100644 --- a/src/timeunits.rs +++ b/src/timeunits.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/ut1.rs b/src/ut1.rs index e88afb46..c4a827fa 100644 --- a/src/ut1.rs +++ b/src/ut1.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/src/weekday.rs b/src/weekday.rs index fe3be0fd..a2b2f18f 100644 --- a/src/weekday.rs +++ b/src/weekday.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) * This Source Code Form is subject to the terms of the Apache * v. 2.0. If a copy of the Apache License was not distributed with this * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/tests/python/test_duration.py b/tests/python/test_duration.py new file mode 100644 index 00000000..77338834 --- /dev/null +++ b/tests/python/test_duration.py @@ -0,0 +1,8 @@ +from hifitime import Duration + +def test_consts_init(): + print(Duration.MIN()) + print(Duration.MIN_POSITIVE()) + print(Duration.MIN_NEGATIVE()) + print(Duration.MAX()) + print(Duration.EPSILON()) \ No newline at end of file From 0885a427e248c34c90e8ccc545e3b3ee7de6b877 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Fri, 16 Jun 2023 00:15:15 -0600 Subject: [PATCH 11/37] Modicum of progress but TDB/ET conversions are broken --- src/duration/mod.rs | 2 +- src/efmt/formatter.rs | 2 +- src/epoch.rs | 323 ++++++++++++++++++++++++++++-------------- tests/epoch.rs | 60 ++++---- 4 files changed, 247 insertions(+), 140 deletions(-) diff --git a/src/duration/mod.rs b/src/duration/mod.rs index 12e674b9..b67c7a10 100644 --- a/src/duration/mod.rs +++ b/src/duration/mod.rs @@ -338,7 +338,7 @@ impl Duration { + i128::from(self.nanoseconds) } else { // Centuries negative by a decent amount - i128::from(self.centuries + 1) * i128::from(NANOSECONDS_PER_CENTURY) + i128::from(self.centuries) * i128::from(NANOSECONDS_PER_CENTURY) - i128::from(self.nanoseconds) } } diff --git a/src/efmt/formatter.rs b/src/efmt/formatter.rs index 1c3fd2b2..e57f239d 100644 --- a/src/efmt/formatter.rs +++ b/src/efmt/formatter.rs @@ -100,7 +100,7 @@ impl Formatter { } pub fn to_time_scale(epoch: Epoch, format: Format, time_scale: TimeScale) -> Self { - Self::new(epoch.into_time_scale(time_scale), format) + Self::new(epoch.to_time_scale(time_scale), format) } pub fn set_timezone(&mut self, offset: Duration) { diff --git a/src/epoch.rs b/src/epoch.rs index 7f2db486..d78e7a34 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -157,7 +157,7 @@ impl Sub for Epoch { type Output = Duration; fn sub(self, other: Self) -> Duration { - self.duration - other.into_time_scale(self.time_scale).duration + self.duration - other.to_time_scale(self.time_scale).duration } } @@ -249,7 +249,11 @@ impl AddAssign for Epoch { /// Equality only checks the duration since J1900 match in TAI, because this is how all of the epochs are referenced. impl PartialEq for Epoch { fn eq(&self, other: &Self) -> bool { - self.duration == other.into_time_scale(self.time_scale).duration + if self.time_scale == other.time_scale { + self.duration == other.duration + } else { + self.duration == other.to_time_scale(self.time_scale).duration + } } } @@ -257,7 +261,7 @@ impl PartialOrd for Epoch { fn partial_cmp(&self, other: &Self) -> Option { Some( self.duration - .cmp(&other.into_time_scale(self.time_scale).duration), + .cmp(&other.to_time_scale(self.time_scale).duration), ) } } @@ -265,7 +269,7 @@ impl PartialOrd for Epoch { impl Ord for Epoch { fn cmp(&self, other: &Self) -> Ordering { self.duration - .cmp(&other.into_time_scale(self.time_scale).duration) + .cmp(&other.to_time_scale(self.time_scale).duration) } } @@ -296,7 +300,7 @@ impl Epoch { /// For example, if the duration is 1 day and the time scale is Ephemeris Time, then this will create an epoch of 2000-01-02 at midnight ET. If the duration is 1 day and the time scale is TAI, this will create an epoch of 1900-01-02 at noon, because the TAI reference epoch in Hifitime is chosen to be the J1900 epoch. /// In case of ET, TDB Timescales, a duration since J2000 is expected. #[must_use] - pub fn from_duration(duration: Duration, ts: TimeScale) -> Self { + pub const fn from_duration(duration: Duration, ts: TimeScale) -> Self { Self { duration, time_scale: ts, @@ -304,87 +308,186 @@ impl Epoch { } pub fn to_duration_since_j1900(&self) -> Duration { - self.into_time_scale(TimeScale::TAI).duration + self.to_time_scale(TimeScale::TAI).duration } #[must_use] /// Converts self to another time scale - pub fn into_time_scale(&self, ts: TimeScale) -> Self { + /// + /// As per the [Rust naming convention](https://rust-lang.github.io/api-guidelines/naming.html#ad-hoc-conversions-follow-as_-to_-into_-conventions-c-conv), + /// this borrows an Epoch and returns an owned Epoch. + pub fn to_time_scale(&self, ts: TimeScale) -> Self { if ts == self.time_scale { // Do nothing *self } else { - match ts { - TimeScale::TAI => { - let mut new_epoch = *self; - // if previous time scale supported leap seconds: remove them - if self.time_scale.uses_leap_seconds() { - new_epoch -= new_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - } - // conversion to TAI: remove time scale reference point - new_epoch.duration -= ts.tai_reference_epoch().duration; - new_epoch.time_scale = TimeScale::TAI; - new_epoch - } - TimeScale::ET => { - // first convert back to TAI - let tai_epoch = self.into_time_scale(TimeScale::TAI); - // seconds past J2000 - //NB: this operation is not feasible is Self is not PAST J2000 - let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; - let mut seconds_since_j2000 = duration_since_j2000.to_seconds(); - for _ in 0..5 { - seconds_since_j2000 += -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds_since_j2000 - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_since_j2000).sin()) - .sin(); - } - // At this point, we have a good estimate of the number of seconds - // of this epoch. Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds_since_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); - // Match the SPICE by changing the UTC definition - Self { - duration: (tai_epoch.duration.to_seconds() - delta_et_tai) * Unit::Second - + J2000_TO_J1900_DURATION, - time_scale: TimeScale::ET, - } + let mut new_epoch = *self; + // If time dilation matters, we'll convert to TAI first because the math is known from there. + if self.time_scale == TimeScale::TDB { + let gamma = Self::inner_g(self.duration.to_seconds()); + + let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + + // Offset back to J1900. + new_epoch.duration = self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION; + new_epoch.time_scale = TimeScale::TAI; + } else if self.time_scale == TimeScale::ET { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds_j2000 = self.duration.to_seconds(); + for _ in 0..5 { + seconds_j2000 += -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds_j2000 + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) + .sin(); } - TimeScale::TDB => { - // first convert back to TAI - let mut tai_epoch = self.into_time_scale(TimeScale::TAI); - // seconds past J2000 - //NB: this operation is not feasible is Self is not PAST J2000 - //let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; - let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; - let seconds_since_j2000 = duration_since_j2000.to_seconds(); - let gamma = Self::inner_g(seconds_since_j2000); - let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - tai_epoch -= delta_tdb_tai; - tai_epoch.time_scale = TimeScale::TDB; - tai_epoch + + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); + + // Match SPICE by changing the UTC definition. + new_epoch.duration = (self.duration.to_seconds() - delta_et_tai) * Unit::Second + + J2000_TO_J1900_DURATION; + new_epoch.time_scale = TimeScale::TAI; + } + // If the destination time scale requires time dilation, apply it here. + if ts == TimeScale::ET { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); + for _ in 0..5 { + seconds -= -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) + .sin(); } - ts => { - // first convert back to TAI - let mut tai_epoch = self.into_time_scale(TimeScale::TAI); - // leap second management - if ts.uses_leap_seconds() { - // new time scale supports leap seconds: add them - tai_epoch += tai_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - } - // time scale specificities - match ts { - TimeScale::TT => { - tai_epoch -= TT_OFFSET_MS * Unit::Millisecond; - } - _ => {} + + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let delta_et_tai = + Self::delta_et_tai(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); + + // Match SPICE by changing the UTC definition. + new_epoch.duration += delta_et_tai * Unit::Second - J2000_TO_J1900_DURATION; + new_epoch.time_scale = TimeScale::TAI; + } else if ts == TimeScale::TDB { + // Iterate to convert find the correct value of the + let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); + let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. + for _ in 0..5 { + let next = seconds - Self::inner_g(seconds); + let new_delta = (next - seconds).abs(); + if (new_delta - delta).abs() < 1e-9 { + break; } - tai_epoch.time_scale = ts; - tai_epoch + seconds = next; // Loop + delta = new_delta; + } + + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let gamma = + Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); + let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + + new_epoch.duration += delta_tdb_tai - J2000_TO_J1900_DURATION; + new_epoch.time_scale = TimeScale::TAI; + } + // Note that we check the time scale of `new_epoch` because we might have changed it above! + if new_epoch.time_scale.uses_leap_seconds() != ts.uses_leap_seconds() { + // Now we need to add or subtract the leap seconds + if new_epoch.time_scale.uses_leap_seconds() { + // Compute the TAI to UTC offset at this time. + // We have the time in TAI. But we were given UTC. + // Hence, we need to _add_ the leap seconds to get the actual TAI time. + // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds + new_epoch.duration += + new_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + } else { + new_epoch.duration -= + new_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; } } + + // And now add the difference between the reference epochs. + // XXX: This is a problem because the reference epochs are not in TAI... so we end up overflowing the stack here. + // Maybe they should all be in TAI? + // new_epoch.duration += self.time_scale.tai_reference_epoch() - ts.tai_reference_epoch(); + new_epoch + // match ts { + // TimeScale::TAI => { + // // if previous time scale supported leap seconds: remove them + // if self.time_scale.uses_leap_seconds() { + // new_epoch.duration -= + // new_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + // } + // // conversion to TAI: remove time scale reference point + // new_epoch.duration -= ts.tai_reference_epoch().duration; + // new_epoch.time_scale = TimeScale::TAI; + // new_epoch + // } + // TimeScale::ET => { + // // first convert back to TAI + // let tai_epoch = self.to_time_scale(TimeScale::TAI); + // // seconds past J2000 + // // NB: this operation is not feasible is Self is not PAST J2000 + // let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; + // let mut seconds_since_j2000 = duration_since_j2000.to_seconds(); + // for _ in 0..5 { + // seconds_since_j2000 += -NAIF_K + // * (NAIF_M0 + // + NAIF_M1 * seconds_since_j2000 + // + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_since_j2000).sin()) + // .sin(); + // } + // // At this point, we have a good estimate of the number of seconds + // // of this epoch. Reverse the algorithm: + // let delta_et_tai = Self::delta_et_tai( + // seconds_since_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + // ); + // // Match the SPICE by changing the UTC definition + // Self { + // duration: (tai_epoch.duration.to_seconds() - delta_et_tai) * Unit::Second + // + J2000_TO_J1900_DURATION, + // time_scale: TimeScale::ET, + // } + // } + // TimeScale::TDB => { + // // first convert back to TAI + // let mut tai_epoch = self.to_time_scale(TimeScale::TAI); + // // seconds past J2000 + // //NB: this operation is not feasible is Self is not PAST J2000 + // //let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; + // let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; + // let seconds_since_j2000 = duration_since_j2000.to_seconds(); + // let gamma = Self::inner_g(seconds_since_j2000); + // let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + // tai_epoch -= delta_tdb_tai; + // tai_epoch.time_scale = TimeScale::TDB; + // tai_epoch + // } + // ts => { + // // first convert back to TAI + // let mut tai_epoch = self.to_time_scale(TimeScale::TAI); + // // leap second management + // if ts.uses_leap_seconds() { + // // new time scale supports leap seconds: add them + // tai_epoch += tai_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + // } + // // time scale specificities + // match ts { + // TimeScale::TT => { + // tai_epoch -= TT_OFFSET_MS * Unit::Millisecond; + // } + // _ => {} + // } + // tai_epoch.time_scale = ts; + // tai_epoch + // } + // } } } @@ -1825,7 +1928,7 @@ impl Epoch { /// Returns this epoch with respect to the provided time scale. /// This is needed to correctly perform duration conversions in dynamical time scales (e.g. TDB). pub fn to_duration_in_time_scale(&self, ts: TimeScale) -> Duration { - self.into_time_scale(ts).duration + self.to_time_scale(ts).duration } /// Attempts to return the number of nanoseconds since the reference epoch of the provided time scale. @@ -1868,7 +1971,7 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in TAI pub fn to_tai_duration(&self) -> Duration { - self.into_time_scale(TimeScale::TAI).duration + self.to_time_scale(TimeScale::TAI).duration } #[must_use] @@ -1898,7 +2001,7 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in UTC pub fn to_utc_duration(&self) -> Duration { - self.into_time_scale(TimeScale::UTC).duration + self.to_time_scale(TimeScale::UTC).duration } #[must_use] @@ -2079,7 +2182,7 @@ impl Epoch { #[must_use] /// Returns `Duration` past QZSS time Epoch. pub fn to_qzsst_duration(&self) -> Duration { - self.into_time_scale(TimeScale::QZSST).duration + self.to_time_scale(TimeScale::QZSST).duration } /// Returns nanoseconds past QZSS Time Epoch, defined as UTC midnight of January 5th to 6th 1980 (cf. ). @@ -2201,21 +2304,22 @@ impl Epoch { /// /// In order to match SPICE, the as_et_duration() function will manually get rid of that difference. pub fn to_et_duration(&self) -> Duration { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); - for _ in 0..5 { - seconds -= -NAIF_K - * (NAIF_M0 + NAIF_M1 * seconds + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) - .sin(); - } + self.to_time_scale(TimeScale::ET).duration + // // Run a Newton Raphston to convert find the correct value of the + // let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); + // for _ in 0..5 { + // seconds -= -NAIF_K + // * (NAIF_M0 + NAIF_M1 * seconds + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) + // .sin(); + // } - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = - Self::delta_et_tai(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); + // // At this point, we have a good estimate of the number of seconds of this epoch. + // // Reverse the algorithm: + // let delta_et_tai = + // Self::delta_et_tai(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - // Match SPICE by changing the UTC definition. - self.duration + delta_et_tai * Unit::Second - J2000_TO_J1900_DURATION + // // Match SPICE by changing the UTC definition. + // self.duration + delta_et_tai * Unit::Second - J2000_TO_J1900_DURATION } #[must_use] @@ -2234,25 +2338,26 @@ impl Epoch { /// 7. At this stage, we have a good approximation of the TDB seconds since J2000. /// 8. Reverse the algorithm given that approximation: compute the `g` offset, compute the difference between TDB and TAI, add the TT offset (32.184 s), and offset by the difference between J1900 and J2000. pub fn to_tdb_duration(&self) -> Duration { - // Iterate to convert find the correct value of the - let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); - let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. - for _ in 0..5 { - let next = seconds - Self::inner_g(seconds); - let new_delta = (next - seconds).abs(); - if (new_delta - delta).abs() < 1e-9 { - break; - } - seconds = next; // Loop - delta = new_delta; - } + self.to_time_scale(TimeScale::TDB).duration + // // Iterate to convert find the correct value of the + // let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); + // let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. + // for _ in 0..5 { + // let next = seconds - Self::inner_g(seconds); + // let new_delta = (next - seconds).abs(); + // if (new_delta - delta).abs() < 1e-9 { + // break; + // } + // seconds = next; // Loop + // delta = new_delta; + // } - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let gamma = Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + // // At this point, we have a good estimate of the number of seconds of this epoch. + // // Reverse the algorithm: + // let gamma = Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); + // let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - self.duration + delta_tdb_tai - J2000_TO_J1900_DURATION + // self.duration + delta_tdb_tai - J2000_TO_J1900_DURATION } #[must_use] @@ -2657,7 +2762,7 @@ impl Epoch { let (sign, days, _, _, _, milliseconds, microseconds, nanoseconds) = self.to_duration().decompose(); // Shadow other with the provided other epoch but in the correct time scale. - let other = other.into_time_scale(self.time_scale); + let other = other.to_time_scale(self.time_scale); Self::from_duration( Duration::compose( sign, @@ -2738,7 +2843,7 @@ impl Epoch { /// ``` pub fn with_hms_strict_from(&self, other: Self) -> Self { let (sign, days, _, _, _, _, _, _) = self.to_duration().decompose(); - let other = other.into_time_scale(self.time_scale); + let other = other.to_time_scale(self.time_scale); Self::from_duration( Duration::compose( sign, diff --git a/tests/epoch.rs b/tests/epoch.rs index 0bcf0085..afb68ba4 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -35,13 +35,13 @@ fn test_const_ops() { fn test_from_gregorian() { let utc_epoch = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14); let tai_epoch = Epoch::from_gregorian_at_midnight(2017, 1, 14, TimeScale::TAI); - assert_eq!(utc_epoch.into_time_scale(TimeScale::TAI), tai_epoch); - assert_eq!(utc_epoch, tai_epoch.into_time_scale(TimeScale::UTC)); + assert_eq!(utc_epoch.to_time_scale(TimeScale::TAI), tai_epoch); + assert_eq!(utc_epoch, tai_epoch.to_time_scale(TimeScale::UTC)); let utc_epoch = Epoch::from_gregorian_utc(2016, 12, 31, 23, 59, 59, 0); let tai_epoch = Epoch::from_gregorian(2016, 12, 31, 23, 59, 59, 0, TimeScale::TAI); - assert_eq!(utc_epoch.into_time_scale(TimeScale::TAI), tai_epoch); - assert_eq!(utc_epoch, tai_epoch.into_time_scale(TimeScale::UTC)); + assert_eq!(utc_epoch.to_time_scale(TimeScale::TAI), tai_epoch); + assert_eq!(utc_epoch, tai_epoch.to_time_scale(TimeScale::UTC)); } #[allow(clippy::float_equality_without_abs)] @@ -185,10 +185,12 @@ fn utc_tai() { assert_eq!(flp_from_secs_tai, flp_from_greg_tai); // Right after the discontinuity, UTC time should be ten seconds behind TAI, i.e. TAI is ten seconds ahead of UTC // In other words, the following date times are equal: + let in_tai = Epoch::from_gregorian_tai_hms(1972, 1, 1, 0, 0, 10); + let in_utc = Epoch::from_gregorian_utc_at_midnight(1972, 1, 1); assert_eq!( - Epoch::from_gregorian_tai_hms(1972, 1, 1, 0, 0, 10), - Epoch::from_gregorian_utc_at_midnight(1972, 1, 1), - "UTC discontinuity failed" + in_tai, in_utc, + "UTC discontinuity failed:\n{:?}\t{:?}", + in_tai.duration, in_utc.duration ); // Noon UTC after the first leap second is in fact ten seconds _after_ noon TAI. // Hence, there are as many TAI seconds since Epoch between UTC Noon and TAI Noon + 10s. @@ -583,11 +585,11 @@ fn unix() { let unix_epoch = Epoch::from_gregorian_utc_at_midnight(1970, 1, 1); assert_eq!( - format!("{}", unix_epoch.into_time_scale(TimeScale::UTC)), + format!("{}", unix_epoch.to_time_scale(TimeScale::UTC)), "1970-01-01T00:00:00 UTC" ); assert_eq!( - format!("{:x}", unix_epoch.into_time_scale(TimeScale::TAI)), + format!("{:x}", unix_epoch.to_time_scale(TimeScale::TAI)), "1970-01-01T00:00:00 TAI" ); // Print as UNIX seconds @@ -1345,7 +1347,7 @@ fn test_add_durations_over_leap_seconds() { // Noon UTC after the first leap second is in fact ten seconds _after_ noon TAI. // Hence, there are as many TAI seconds since Epoch between UTC Noon and TAI Noon + 10s. let pre_ls_utc = Epoch::from_gregorian_utc_at_noon(1971, 12, 31); - let pre_ls_tai = pre_ls_utc.into_time_scale(TimeScale::TAI); + let pre_ls_tai = pre_ls_utc.to_time_scale(TimeScale::TAI); // Before the first leap second, there is no time difference between both epochs (because only IERS announced leap seconds are accounted for by default). assert_eq!(pre_ls_utc - pre_ls_tai, Duration::ZERO); @@ -1446,7 +1448,7 @@ fn test_weekday() { TimeScale::TT, TimeScale::UTC, ] { - let e_ts = e.into_time_scale(new_time_scale); + let e_ts = e.to_time_scale(new_time_scale); assert_eq!(e_ts.weekday(), expect, "error with {new_time_scale}"); } }; @@ -1526,7 +1528,7 @@ fn test_get_time() { let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 10, 11, 12, 13); let other_utc = Epoch::from_gregorian_utc(2024, 12, 01, 20, 21, 22, 23); - let other = other_utc.into_time_scale(TimeScale::TDB); + let other = other_utc.to_time_scale(TimeScale::TDB); assert_eq!( epoch.with_hms_from(other), @@ -1584,7 +1586,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (0, 10 * 1_000_000_000 + 10)); // TAI<=>UTC - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1604,7 +1606,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 00)); assert_eq!(epoch.to_time_of_week(), (2238, 345_618_000_000_000)); - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(utc_wk, utc_tow, TimeScale::UTC), @@ -1612,7 +1614,7 @@ fn test_time_of_week() { ); // GPST and QZSST share the same properties at all times - let epoch_qzsst = epoch.into_time_scale(TimeScale::QZSST); + let epoch_qzsst = epoch.to_time_scale(TimeScale::QZSST); assert_eq!(epoch.to_gregorian_utc(), epoch_qzsst.to_gregorian_utc()); let gps_qzss_offset = @@ -1624,7 +1626,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (1980, 01, 06, 01, 00, 0 + 18, 00)); assert_eq!(epoch.to_time_of_week(), (0, 3_618_000_000_000)); - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(utc_wk, utc_tow, TimeScale::UTC), @@ -1645,7 +1647,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (24, 306_457_000_000_000)); // <=>UTC - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1657,7 +1659,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 01)); // <=>UTC - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1669,7 +1671,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 12, 00, 00, 00)); // <=>UTC - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1681,7 +1683,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 15, 27, 19, 10)); // <=>UTC - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1695,7 +1697,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (0, 3_600_000_000_000)); // <=>UTC - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1711,7 +1713,7 @@ fn test_time_of_week() { assert_eq!(epoch.to_time_of_week(), (1, 128 * 3600 * 1_000_000_000)); // <=>UTC - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1732,7 +1734,7 @@ fn test_time_of_week() { ); // <=>UTC - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1755,7 +1757,7 @@ fn test_time_of_week() { ); // <=>UTC - let epoch_utc = epoch.into_time_scale(TimeScale::UTC); + let epoch_utc = epoch.to_time_scale(TimeScale::UTC); let (week, tow) = epoch_utc.to_time_of_week(); assert_eq!( Epoch::from_time_of_week(week, tow, TimeScale::UTC), @@ -1776,7 +1778,7 @@ fn test_day_of_year() { TimeScale::ET, TimeScale::TDB, ] { - let epoch = utc_epoch.into_time_scale(*ts); + let epoch = utc_epoch.to_time_scale(*ts); let (year, days) = epoch.year_days_of_year(); let rebuilt = Epoch::from_day_of_year(year, days, *ts); if *ts == TimeScale::ET || *ts == TimeScale::TDB { @@ -1901,9 +1903,9 @@ fn test_epoch_formatter() { #[test] fn test_to_tai_time_scale() { let j1900_ref = J1900_REF_EPOCH; - assert_eq!(j1900_ref, j1900_ref.into_time_scale(TimeScale::TAI)); + assert_eq!(j1900_ref, j1900_ref.to_time_scale(TimeScale::TAI)); let j2000_ref = J2000_REF_EPOCH; - assert_eq!(j2000_ref, j2000_ref.into_time_scale(TimeScale::TAI)); + assert_eq!(j2000_ref, j2000_ref.to_time_scale(TimeScale::TAI)); let j2000_to_j1900 = j2000_ref - j1900_ref; assert_eq!(j2000_to_j1900, J2000_TO_J1900_DURATION); } @@ -1913,13 +1915,13 @@ fn test_to_utc_time_scale() { let gpst_tai_ref = TimeScale::GPST.tai_reference_epoch(); // there were 19 leap seconds on the day GPST was "started" let gpst_utc_delta = 19.0; // leaps - let gpst_utc_ref = gpst_tai_ref.into_time_scale(TimeScale::UTC); + let gpst_utc_ref = gpst_tai_ref.to_time_scale(TimeScale::UTC); assert_eq!((gpst_utc_ref - gpst_tai_ref).to_seconds(), gpst_utc_delta); let bdt_tai_ref = TimeScale::BDT.tai_reference_epoch(); // there were 33 leap seconds on the day BDT was "started" let bdt_utc_delta = 33.0; // leaps - let bdt_utc_ref = bdt_tai_ref.into_time_scale(TimeScale::UTC); + let bdt_utc_ref = bdt_tai_ref.to_time_scale(TimeScale::UTC); assert_eq!((bdt_utc_ref - bdt_tai_ref).to_seconds(), bdt_utc_delta); } From ac709dffa31a018ef6dc78e87c8ba6458e993896 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 17 Jun 2023 13:42:32 -0600 Subject: [PATCH 12/37] Progress in time scale conversion with new arch --- src/epoch.rs | 319 ++++++++++--------------- src/timescale/fmt.rs | 76 ++++++ src/timescale/kani.rs | 25 ++ src/{timescale.rs => timescale/mod.rs} | 133 +++-------- tests/epoch.rs | 5 +- 5 files changed, 253 insertions(+), 305 deletions(-) create mode 100644 src/timescale/fmt.rs create mode 100644 src/timescale/kani.rs rename src/{timescale.rs => timescale/mod.rs} (63%) diff --git a/src/epoch.rs b/src/epoch.rs index d78e7a34..270d3678 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -12,10 +12,10 @@ use crate::duration::{Duration, Unit}; use crate::leap_seconds::{LatestLeapSeconds, LeapSecondProvider}; use crate::parser::Token; use crate::{ - Errors, MonthName, TimeScale, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, GPST_REF_EPOCH, - GST_REF_EPOCH, J1900_OFFSET, J2000_REF_EPOCH, J2000_TO_J1900_DURATION, MJD_OFFSET, - NANOSECONDS_PER_DAY, NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, - NANOSECONDS_PER_SECOND_U32, UNIX_REF_EPOCH, + Errors, MonthName, TimeScale, TimeUnits, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, + GPST_REF_EPOCH, GST_REF_EPOCH, J1900_OFFSET, J2000_REF_EPOCH, J2000_TO_J1900_DURATION, + MJD_OFFSET, NANOSECONDS_PER_DAY, NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, + NANOSECONDS_PER_SECOND_U32, QZSST_REF_EPOCH, UNIX_REF_EPOCH, }; use crate::efmt::format::Format; @@ -318,176 +318,133 @@ impl Epoch { /// this borrows an Epoch and returns an owned Epoch. pub fn to_time_scale(&self, ts: TimeScale) -> Self { if ts == self.time_scale { - // Do nothing + // Do nothing, just return a copy *self } else { - let mut new_epoch = *self; - // If time dilation matters, we'll convert to TAI first because the math is known from there. - if self.time_scale == TimeScale::TDB { - let gamma = Self::inner_g(self.duration.to_seconds()); - - let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - - // Offset back to J1900. - new_epoch.duration = self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION; - new_epoch.time_scale = TimeScale::TAI; - } else if self.time_scale == TimeScale::ET { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds_j2000 = self.duration.to_seconds(); - for _ in 0..5 { - seconds_j2000 += -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds_j2000 - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) - .sin(); + // Now we need to convert from the current time scale into the desired time scale. + let j1900_tai_offset = match self.time_scale { + TimeScale::TAI => self.duration, + TimeScale::TT => self.duration - TT_OFFSET_MS.milliseconds(), + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds_j2000 = self.duration.to_seconds(); + for _ in 0..5 { + seconds_j2000 += -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds_j2000 + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) + .sin(); + } + + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); + + // Match SPICE by changing the UTC definition. + (self.duration.to_seconds() - delta_et_tai).seconds() + J2000_TO_J1900_DURATION } + TimeScale::TDB => { + let gamma = Self::inner_g(self.duration.to_seconds()); - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); + let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - // Match SPICE by changing the UTC definition. - new_epoch.duration = (self.duration.to_seconds() - delta_et_tai) * Unit::Second - + J2000_TO_J1900_DURATION; - new_epoch.time_scale = TimeScale::TAI; - } - // If the destination time scale requires time dilation, apply it here. - if ts == TimeScale::ET { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); - for _ in 0..5 { - seconds -= -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) - .sin(); + // Offset back to J1900. + self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION } + TimeScale::UTC => self.duration + self.leap_seconds(true).unwrap_or(0.0).seconds(), + TimeScale::GPST => self.duration + GPST_REF_EPOCH.to_tai_duration(), + TimeScale::GST => self.duration + GST_REF_EPOCH.to_tai_duration(), + TimeScale::BDT => self.duration + BDT_REF_EPOCH.to_tai_duration(), + TimeScale::QZSST => self.duration + QZSST_REF_EPOCH.to_tai_duration(), + }; + // Convert to the desired time scale from the TAI duration + match ts { + TimeScale::TAI => Self { + duration: j1900_tai_offset, + time_scale: TimeScale::TAI, + }, + TimeScale::TT => Self { + duration: j1900_tai_offset + TT_OFFSET_MS.milliseconds(), + time_scale: TimeScale::TT, + }, + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); + for _ in 0..5 { + seconds -= -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) + .sin(); + } - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = - Self::delta_et_tai(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - - // Match SPICE by changing the UTC definition. - new_epoch.duration += delta_et_tai * Unit::Second - J2000_TO_J1900_DURATION; - new_epoch.time_scale = TimeScale::TAI; - } else if ts == TimeScale::TDB { - // Iterate to convert find the correct value of the - let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); - let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. - for _ in 0..5 { - let next = seconds - Self::inner_g(seconds); - let new_delta = (next - seconds).abs(); - if (new_delta - delta).abs() < 1e-9 { - break; + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); + + // Match SPICE by changing the UTC definition. + Self { + duration: j1900_tai_offset + delta_et_tai.seconds() + - J2000_TO_J1900_DURATION, + time_scale: TimeScale::ET, } - seconds = next; // Loop - delta = new_delta; } + TimeScale::TDB => { + // Iterate to convert find the correct value of the + let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); + let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. + for _ in 0..5 { + let next = seconds - Self::inner_g(seconds); + let new_delta = (next - seconds).abs(); + if (new_delta - delta).abs() < 1e-9 { + break; + } + seconds = next; // Loop + delta = new_delta; + } - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let gamma = - Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let gamma = + Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); + let delta_tdb_tai = gamma.seconds() + TT_OFFSET_MS.milliseconds(); - new_epoch.duration += delta_tdb_tai - J2000_TO_J1900_DURATION; - new_epoch.time_scale = TimeScale::TAI; - } - // Note that we check the time scale of `new_epoch` because we might have changed it above! - if new_epoch.time_scale.uses_leap_seconds() != ts.uses_leap_seconds() { - // Now we need to add or subtract the leap seconds - if new_epoch.time_scale.uses_leap_seconds() { - // Compute the TAI to UTC offset at this time. - // We have the time in TAI. But we were given UTC. - // Hence, we need to _add_ the leap seconds to get the actual TAI time. + Self { + duration: j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION, + time_scale: TimeScale::TDB, + } + } + TimeScale::UTC => { + // Assume it's TAI + let mut epoch = Self { + duration: j1900_tai_offset, + time_scale: TimeScale::UTC, + }; // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - new_epoch.duration += - new_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - } else { - new_epoch.duration -= - new_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + epoch.duration -= epoch.leap_seconds(true).unwrap_or(0.0).seconds(); + epoch } + TimeScale::GPST => Self { + duration: j1900_tai_offset - GPST_REF_EPOCH.to_tai_duration(), + time_scale: TimeScale::GPST, + }, + TimeScale::GST => Self { + duration: j1900_tai_offset - GST_REF_EPOCH.to_tai_duration(), + time_scale: TimeScale::GST, + }, + TimeScale::BDT => Self { + duration: j1900_tai_offset - BDT_REF_EPOCH.to_tai_duration(), + time_scale: TimeScale::BDT, + }, + TimeScale::QZSST => Self { + duration: j1900_tai_offset - QZSST_REF_EPOCH.to_tai_duration(), + time_scale: TimeScale::QZSST, + }, } - - // And now add the difference between the reference epochs. - // XXX: This is a problem because the reference epochs are not in TAI... so we end up overflowing the stack here. - // Maybe they should all be in TAI? - // new_epoch.duration += self.time_scale.tai_reference_epoch() - ts.tai_reference_epoch(); - new_epoch - // match ts { - // TimeScale::TAI => { - // // if previous time scale supported leap seconds: remove them - // if self.time_scale.uses_leap_seconds() { - // new_epoch.duration -= - // new_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - // } - // // conversion to TAI: remove time scale reference point - // new_epoch.duration -= ts.tai_reference_epoch().duration; - // new_epoch.time_scale = TimeScale::TAI; - // new_epoch - // } - // TimeScale::ET => { - // // first convert back to TAI - // let tai_epoch = self.to_time_scale(TimeScale::TAI); - // // seconds past J2000 - // // NB: this operation is not feasible is Self is not PAST J2000 - // let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; - // let mut seconds_since_j2000 = duration_since_j2000.to_seconds(); - // for _ in 0..5 { - // seconds_since_j2000 += -NAIF_K - // * (NAIF_M0 - // + NAIF_M1 * seconds_since_j2000 - // + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_since_j2000).sin()) - // .sin(); - // } - // // At this point, we have a good estimate of the number of seconds - // // of this epoch. Reverse the algorithm: - // let delta_et_tai = Self::delta_et_tai( - // seconds_since_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - // ); - // // Match the SPICE by changing the UTC definition - // Self { - // duration: (tai_epoch.duration.to_seconds() - delta_et_tai) * Unit::Second - // + J2000_TO_J1900_DURATION, - // time_scale: TimeScale::ET, - // } - // } - // TimeScale::TDB => { - // // first convert back to TAI - // let mut tai_epoch = self.to_time_scale(TimeScale::TAI); - // // seconds past J2000 - // //NB: this operation is not feasible is Self is not PAST J2000 - // //let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; - // let duration_since_j2000 = tai_epoch - J2000_REF_EPOCH; - // let seconds_since_j2000 = duration_since_j2000.to_seconds(); - // let gamma = Self::inner_g(seconds_since_j2000); - // let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - // tai_epoch -= delta_tdb_tai; - // tai_epoch.time_scale = TimeScale::TDB; - // tai_epoch - // } - // ts => { - // // first convert back to TAI - // let mut tai_epoch = self.to_time_scale(TimeScale::TAI); - // // leap second management - // if ts.uses_leap_seconds() { - // // new time scale supports leap seconds: add them - // tai_epoch += tai_epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - // } - // // time scale specificities - // match ts { - // TimeScale::TT => { - // tai_epoch -= TT_OFFSET_MS * Unit::Millisecond; - // } - // _ => {} - // } - // tai_epoch.time_scale = ts; - // tai_epoch - // } - // } } } @@ -915,7 +872,7 @@ impl Epoch { + Unit::Nanosecond * i64::from(nanos); if second == 60 { // Herein lies the whole ambiguity of leap seconds. Two different UTC dates exist at the - // same number of second afters J1900.0. + // same number of second after J1900.0. duration_wrt_1900 -= Unit::Second; } @@ -1965,7 +1922,7 @@ impl Epoch { #[must_use] /// Returns the number of TAI seconds since J1900 pub fn to_tai_seconds(&self) -> f64 { - self.duration.to_seconds() + self.to_tai_duration().to_seconds() } #[must_use] @@ -2158,7 +2115,7 @@ impl Epoch { #[must_use] /// Returns `Duration` past GPS time Epoch. pub fn to_gpst_duration(&self) -> Duration { - self.duration - GPST_REF_EPOCH.to_tai_duration() + self.to_time_scale(TimeScale::GPST).duration } /// Returns nanoseconds past GPS Time Epoch, defined as UTC midnight of January 5th to 6th 1980 (cf. ). @@ -2206,7 +2163,7 @@ impl Epoch { #[must_use] /// Returns `Duration` past GST (Galileo) time Epoch. pub fn to_gst_duration(&self) -> Duration { - self.duration - GST_REF_EPOCH.to_tai_duration() + self.to_time_scale(TimeScale::GST).duration } /// Returns nanoseconds past GST (Galileo) Time Epoch, starting on August 21st 1999 Midnight UT @@ -2305,21 +2262,6 @@ impl Epoch { /// In order to match SPICE, the as_et_duration() function will manually get rid of that difference. pub fn to_et_duration(&self) -> Duration { self.to_time_scale(TimeScale::ET).duration - // // Run a Newton Raphston to convert find the correct value of the - // let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); - // for _ in 0..5 { - // seconds -= -NAIF_K - // * (NAIF_M0 + NAIF_M1 * seconds + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) - // .sin(); - // } - - // // At this point, we have a good estimate of the number of seconds of this epoch. - // // Reverse the algorithm: - // let delta_et_tai = - // Self::delta_et_tai(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - - // // Match SPICE by changing the UTC definition. - // self.duration + delta_et_tai * Unit::Second - J2000_TO_J1900_DURATION } #[must_use] @@ -2339,25 +2281,6 @@ impl Epoch { /// 8. Reverse the algorithm given that approximation: compute the `g` offset, compute the difference between TDB and TAI, add the TT offset (32.184 s), and offset by the difference between J1900 and J2000. pub fn to_tdb_duration(&self) -> Duration { self.to_time_scale(TimeScale::TDB).duration - // // Iterate to convert find the correct value of the - // let mut seconds = (self.duration - J2000_TO_J1900_DURATION).to_seconds(); - // let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. - // for _ in 0..5 { - // let next = seconds - Self::inner_g(seconds); - // let new_delta = (next - seconds).abs(); - // if (new_delta - delta).abs() < 1e-9 { - // break; - // } - // seconds = next; // Loop - // delta = new_delta; - // } - - // // At this point, we have a good estimate of the number of seconds of this epoch. - // // Reverse the algorithm: - // let gamma = Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - // let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - - // self.duration + delta_tdb_tai - J2000_TO_J1900_DURATION } #[must_use] diff --git a/src/timescale/fmt.rs b/src/timescale/fmt.rs new file mode 100644 index 00000000..28d83f15 --- /dev/null +++ b/src/timescale/fmt.rs @@ -0,0 +1,76 @@ +/* + * Hifitime, part of the Nyx Space tools + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) + * This Source Code Form is subject to the terms of the Apache + * v. 2.0. If a copy of the Apache License was not distributed with this + * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. + * + * Documentation: https://nyxspace.com/ + */ + +use core::fmt; +use core::str::FromStr; + +use crate::{Errors, ParsingErrors}; + +use super::TimeScale; + +impl fmt::Display for TimeScale { + /// Prints given TimeScale + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::TAI => write!(f, "TAI"), + Self::TT => write!(f, "TT"), + Self::ET => write!(f, "ET"), + Self::TDB => write!(f, "TDB"), + Self::UTC => write!(f, "UTC"), + Self::GPST => write!(f, "GPST"), + Self::GST => write!(f, "GST"), + Self::BDT => write!(f, "BDT"), + Self::QZSST => write!(f, "QZSST"), + } + } +} + +impl fmt::LowerHex for TimeScale { + /// Prints given TimeScale in RINEX format + /// ie., standard GNSS constellation name is preferred when possible + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::GPST => write!(f, "GPS"), + Self::GST => write!(f, "GAL"), + Self::BDT => write!(f, "BDS"), + Self::QZSST => write!(f, "QZSS"), + _ => write!(f, "{self}"), + } + } +} + +impl FromStr for TimeScale { + type Err = Errors; + + fn from_str(s: &str) -> Result { + let val = s.trim(); + if val == "UTC" { + Ok(Self::UTC) + } else if val == "TT" { + Ok(Self::TT) + } else if val == "TAI" { + Ok(Self::TAI) + } else if val == "TDB" { + Ok(Self::TDB) + } else if val == "ET" { + Ok(Self::ET) + } else if val == "GPST" || val == "GPS" { + Ok(Self::GPST) + } else if val == "GST" || val == "GAL" { + Ok(Self::GST) + } else if val == "BDT" || val == "BDS" { + Ok(Self::BDT) + } else if val == "QZSST" || val == "QZSS" { + Ok(Self::QZSST) + } else { + Err(Errors::ParseError(ParsingErrors::TimeSystem)) + } + } +} diff --git a/src/timescale/kani.rs b/src/timescale/kani.rs new file mode 100644 index 00000000..c189ee56 --- /dev/null +++ b/src/timescale/kani.rs @@ -0,0 +1,25 @@ +/* + * Hifitime, part of the Nyx Space tools + * Copyright (C) 2023 Christopher Rabotin et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors) + * This Source Code Form is subject to the terms of the Apache + * v. 2.0. If a copy of the Apache License was not distributed with this + * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. + * + * Documentation: https://nyxspace.com/ + */ + +use super::Timescale; +use kani::Arbitrary; + +impl Arbitrary for TimeScale { + #[inline(always)] + fn any() -> Self { + let ts_u8: u8 = kani::any(); + Self::from(ts_u8) + } +} + +#[kani::proof] +fn formal_time_scale() { + let _time_scale: TimeScale = kani::any(); +} diff --git a/src/timescale.rs b/src/timescale/mod.rs similarity index 63% rename from src/timescale.rs rename to src/timescale/mod.rs index 64008ef1..92dcd409 100644 --- a/src/timescale.rs +++ b/src/timescale/mod.rs @@ -15,14 +15,13 @@ use pyo3::prelude::*; use serde_derive::{Deserialize, Serialize}; #[cfg(kani)] -use kani::Arbitrary; +mod kani; -use core::fmt; -use core::str::FromStr; +mod fmt; use crate::{ - Duration, Epoch, Errors, ParsingErrors, J2000_REF_EPOCH_ET, J2000_REF_EPOCH_TDB, - J2000_TO_J1900_DURATION, SECONDS_PER_DAY, + Duration, Epoch, J2000_REF_EPOCH_ET, J2000_REF_EPOCH_TDB, J2000_TO_J1900_DURATION, + SECONDS_PER_DAY, }; /// The J1900 reference epoch (1900-01-01 at noon) TAI. @@ -88,28 +87,13 @@ pub enum TimeScale { GST, /// BeiDou Time scale BDT, - /// QZSS Time scale has the same properties as GPST, - /// but with dedicated clocks + /// QZSS Time scale has the same properties as GPST but with dedicated clocks QZSST, } -#[cfg(kani)] -impl Arbitrary for TimeScale { - #[inline(always)] - fn any() -> Self { - let ts_u8: u8 = kani::any(); - Self::from(ts_u8) - } -} - impl Default for TimeScale { /// Builds default TAI time scale fn default() -> Self { - /* - * We use TAI as default Time scale, - * because `Epoch` is always defined with respect to TAI. - * Also, a default `Epoch` is then a null duration into TAI. - */ Self::TAI } } @@ -145,37 +129,6 @@ impl TimeScale { } } -impl fmt::Display for TimeScale { - /// Prints given TimeScale - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::TAI => write!(f, "TAI"), - Self::TT => write!(f, "TT"), - Self::ET => write!(f, "ET"), - Self::TDB => write!(f, "TDB"), - Self::UTC => write!(f, "UTC"), - Self::GPST => write!(f, "GPST"), - Self::GST => write!(f, "GST"), - Self::BDT => write!(f, "BDT"), - Self::QZSST => write!(f, "QZSST"), - } - } -} - -impl fmt::LowerHex for TimeScale { - /// Prints given TimeScale in RINEX format - /// ie., standard GNSS constellation name is preferred when possible - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::GPST => write!(f, "GPS"), - Self::GST => write!(f, "GAL"), - Self::BDT => write!(f, "BDS"), - Self::QZSST => write!(f, "QZSS"), - _ => write!(f, "{self}"), - } - } -} - #[cfg_attr(feature = "python", pymethods)] impl TimeScale { /// Returns true if self takes leap seconds into account @@ -220,61 +173,31 @@ impl From for TimeScale { } } -impl FromStr for TimeScale { - type Err = Errors; - - fn from_str(s: &str) -> Result { - let val = s.trim(); - if val == "UTC" { - Ok(Self::UTC) - } else if val == "TT" { - Ok(Self::TT) - } else if val == "TAI" { - Ok(Self::TAI) - } else if val == "TDB" { - Ok(Self::TDB) - } else if val == "ET" { - Ok(Self::ET) - } else if val == "GPST" || val == "GPS" { - Ok(Self::GPST) - } else if val == "GST" || val == "GAL" { - Ok(Self::GST) - } else if val == "BDT" || val == "BDS" { - Ok(Self::BDT) - } else if val == "QZSST" || val == "QZSS" { - Ok(Self::QZSST) - } else { - Err(Errors::ParseError(ParsingErrors::TimeSystem)) - } +#[cfg(test)] +mod unit_test_timescale { + use super::TimeScale; + + #[test] + #[cfg(feature = "serde")] + fn test_serdes() { + let ts = TimeScale::UTC; + let content = "\"UTC\""; + assert_eq!(content, serde_json::to_string(&ts).unwrap()); + let parsed: TimeScale = serde_json::from_str(content).unwrap(); + assert_eq!(ts, parsed); } -} -#[test] -#[cfg(feature = "serde")] -fn test_serdes() { - let ts = TimeScale::UTC; - let content = "\"UTC\""; - assert_eq!(content, serde_json::to_string(&ts).unwrap()); - let parsed: TimeScale = serde_json::from_str(content).unwrap(); - assert_eq!(ts, parsed); -} - -#[test] -fn test_ts() { - for ts_u8 in 0..u8::MAX { - let ts = TimeScale::from(ts_u8); - let ts_u8_back: u8 = ts.into(); - // If the u8 is greater than 5, it isn't valid and necessarily encoded as TAI. - if ts_u8 < 9 { - assert_eq!(ts_u8_back, ts_u8, "got {ts_u8_back} want {ts_u8}"); - } else { - assert_eq!(ts, TimeScale::TAI); + #[test] + fn test_ts() { + for ts_u8 in 0..u8::MAX { + let ts = TimeScale::from(ts_u8); + let ts_u8_back: u8 = ts.into(); + // If the u8 is greater than 5, it isn't valid and necessarily encoded as TAI. + if ts_u8 < 9 { + assert_eq!(ts_u8_back, ts_u8, "got {ts_u8_back} want {ts_u8}"); + } else { + assert_eq!(ts, TimeScale::TAI); + } } } } - -#[cfg(kani)] -#[kani::proof] -fn formal_time_scale() { - let _time_scale: TimeScale = kani::any(); -} diff --git a/tests/epoch.rs b/tests/epoch.rs index afb68ba4..a8de59df 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -150,6 +150,7 @@ fn utc_epochs() { // Just after to the 2017 leap second, there should be an offset of 37 seconds between UTC and TAI let this_epoch = Epoch::from_tai_seconds(3_692_217_600.0); let epoch_utc = Epoch::from_gregorian_utc_hms(2016, 12, 31, 23, 59, 24); + assert_eq!(epoch_utc, this_epoch, "Incorrect epoch"); assert!(this_epoch.to_tai_seconds() - epoch_utc.to_utc_seconds() - 37.0 < EPSILON); @@ -425,7 +426,7 @@ fn gpst() { assert!( (epoch.to_tai_seconds() - SECONDS_GPS_TAI_OFFSET - epoch.to_gpst_seconds()).abs() < EPSILON ); - assert!((epoch.to_tai_days() - DAYS_GPS_TAI_OFFSET - epoch.to_gpst_days()).abs() < 1e-11); + assert!(dbg!(epoch.to_tai_days() - DAYS_GPS_TAI_OFFSET - epoch.to_gpst_days()).abs() < 1e-11); // 1 Jan 1980 is 5 days before the GPS epoch. let epoch = Epoch::from_gregorian_utc_at_midnight(1980, 1, 1); @@ -772,7 +773,7 @@ fn spice_et_tdb() { // Check reciprocity let from_et_s = Epoch::from_tdb_seconds(expected_et_s); assert!((from_et_s.to_tdb_seconds() - expected_et_s).abs() < EPSILON); - // Validate UTC to ET when initialization from UTC + // Validate UTC to ET when initializing from UTC assert!(dbg!(sp_ex.to_et_seconds() - expected_et_s).abs() < max_prec.to_seconds()); assert!(dbg!(sp_ex.to_tdb_seconds() - expected_et_s).abs() < max_tdb_et_err.to_seconds()); assert!(dbg!(sp_ex.to_jde_utc_days() - 2455964.9739931).abs() < 1e-7); From e70be45f61aececee69df31c7b7e7dcdc17721fd Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 17 Jun 2023 14:58:22 -0600 Subject: [PATCH 13/37] Also convert the time scale with leap seconds into the one without leap seconds --- src/epoch.rs | 97 ++++++++++++++++++++++++++------------------------ tests/epoch.rs | 13 ------- 2 files changed, 51 insertions(+), 59 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 270d3678..5c911cf1 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -13,8 +13,8 @@ use crate::leap_seconds::{LatestLeapSeconds, LeapSecondProvider}; use crate::parser::Token; use crate::{ Errors, MonthName, TimeScale, TimeUnits, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, - GPST_REF_EPOCH, GST_REF_EPOCH, J1900_OFFSET, J2000_REF_EPOCH, J2000_TO_J1900_DURATION, - MJD_OFFSET, NANOSECONDS_PER_DAY, NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, + GPST_REF_EPOCH, GST_REF_EPOCH, J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET, + NANOSECONDS_PER_DAY, NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, QZSST_REF_EPOCH, UNIX_REF_EPOCH, }; @@ -22,7 +22,7 @@ use crate::efmt::format::Format; use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; use core::fmt; -use core::hash::Hash; +use core::hash::{Hash, Hasher}; use core::ops::{Add, AddAssign, Sub, SubAssign}; use crate::ParsingErrors; @@ -123,7 +123,7 @@ const CUMULATIVE_DAYS_FOR_MONTH: [u16; 12] = { /// Defines a nanosecond-precision Epoch. /// /// Refer to the appropriate functions for initializing this Epoch from different time scales or representations. -#[derive(Copy, Clone, Default, Eq, Hash)] +#[derive(Copy, Clone, Default, Eq)] #[repr(C)] #[cfg_attr(feature = "python", pyclass)] pub struct Epoch { @@ -133,6 +133,13 @@ pub struct Epoch { pub time_scale: TimeScale, } +impl Hash for Epoch { + fn hash(&self, state: &mut H) { + self.duration.hash(state); + self.time_scale.hash(state); + } +} + impl Serialize for Epoch { fn serialize(&self, serializer: S) -> Result where @@ -252,7 +259,19 @@ impl PartialEq for Epoch { if self.time_scale == other.time_scale { self.duration == other.duration } else { - self.duration == other.to_time_scale(self.time_scale).duration + // If one of the two time scales does not include leap seconds, + // we always convert the time scale with leap seconds into the + // time scale that does NOT have leap seconds. + if self.time_scale.uses_leap_seconds() != other.time_scale.uses_leap_seconds() { + if self.time_scale.uses_leap_seconds() { + self.to_time_scale(other.time_scale).duration == other.duration + } else { + self.duration == other.to_time_scale(self.time_scale).duration + } + } else { + // Otherwise it does not matter + self.duration == other.to_time_scale(self.time_scale).duration + } } } } @@ -286,7 +305,7 @@ impl Epoch { provider: L, ) -> Option { for leap_second in provider.rev() { - if self.duration.to_seconds() >= leap_second.timestamp_tai_s + if self.to_tai_duration().to_seconds() >= leap_second.timestamp_tai_s && (!iers_only || leap_second.announced_by_iers) { return Some(leap_second.delta_at); @@ -322,6 +341,7 @@ impl Epoch { *self } else { // Now we need to convert from the current time scale into the desired time scale. + // Let's first compute this epoch from its current time scale into TAI. let j1900_tai_offset = match self.time_scale { TimeScale::TAI => self.duration, TimeScale::TT => self.duration - TT_OFFSET_MS.milliseconds(), @@ -353,22 +373,22 @@ impl Epoch { // Offset back to J1900. self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION } - TimeScale::UTC => self.duration + self.leap_seconds(true).unwrap_or(0.0).seconds(), + TimeScale::UTC => { + // Assume this is TAI + let mut tai_assumption = *self; + tai_assumption.time_scale = TimeScale::TAI; + self.duration + tai_assumption.leap_seconds(true).unwrap_or(0.0).seconds() + } TimeScale::GPST => self.duration + GPST_REF_EPOCH.to_tai_duration(), TimeScale::GST => self.duration + GST_REF_EPOCH.to_tai_duration(), TimeScale::BDT => self.duration + BDT_REF_EPOCH.to_tai_duration(), TimeScale::QZSST => self.duration + QZSST_REF_EPOCH.to_tai_duration(), }; + // Convert to the desired time scale from the TAI duration - match ts { - TimeScale::TAI => Self { - duration: j1900_tai_offset, - time_scale: TimeScale::TAI, - }, - TimeScale::TT => Self { - duration: j1900_tai_offset + TT_OFFSET_MS.milliseconds(), - time_scale: TimeScale::TT, - }, + let ts_ref_offset = match ts { + TimeScale::TAI => j1900_tai_offset, + TimeScale::TT => j1900_tai_offset + TT_OFFSET_MS.milliseconds(), TimeScale::ET => { // Run a Newton Raphston to convert find the correct value of the let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); @@ -387,11 +407,7 @@ impl Epoch { ); // Match SPICE by changing the UTC definition. - Self { - duration: j1900_tai_offset + delta_et_tai.seconds() - - J2000_TO_J1900_DURATION, - time_scale: TimeScale::ET, - } + j1900_tai_offset + delta_et_tai.seconds() - J2000_TO_J1900_DURATION } TimeScale::TDB => { // Iterate to convert find the correct value of the @@ -413,37 +429,26 @@ impl Epoch { Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); let delta_tdb_tai = gamma.seconds() + TT_OFFSET_MS.milliseconds(); - Self { - duration: j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION, - time_scale: TimeScale::TDB, - } + j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION } TimeScale::UTC => { // Assume it's TAI - let mut epoch = Self { + let epoch = Self { duration: j1900_tai_offset, - time_scale: TimeScale::UTC, + time_scale: TimeScale::TAI, }; // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - epoch.duration -= epoch.leap_seconds(true).unwrap_or(0.0).seconds(); - epoch + j1900_tai_offset - epoch.leap_seconds(true).unwrap_or(0.0).seconds() } - TimeScale::GPST => Self { - duration: j1900_tai_offset - GPST_REF_EPOCH.to_tai_duration(), - time_scale: TimeScale::GPST, - }, - TimeScale::GST => Self { - duration: j1900_tai_offset - GST_REF_EPOCH.to_tai_duration(), - time_scale: TimeScale::GST, - }, - TimeScale::BDT => Self { - duration: j1900_tai_offset - BDT_REF_EPOCH.to_tai_duration(), - time_scale: TimeScale::BDT, - }, - TimeScale::QZSST => Self { - duration: j1900_tai_offset - QZSST_REF_EPOCH.to_tai_duration(), - time_scale: TimeScale::QZSST, - }, + TimeScale::GPST => j1900_tai_offset - GPST_REF_EPOCH.to_tai_duration(), + TimeScale::GST => j1900_tai_offset - GST_REF_EPOCH.to_tai_duration(), + TimeScale::BDT => j1900_tai_offset - BDT_REF_EPOCH.to_tai_duration(), + TimeScale::QZSST => j1900_tai_offset - QZSST_REF_EPOCH.to_tai_duration(), + }; + + Self { + duration: ts_ref_offset, + time_scale: ts, } } } @@ -452,7 +457,7 @@ impl Epoch { /// Creates a new Epoch from a Duration as the time difference between this epoch and TAI reference epoch. pub const fn from_tai_duration(duration: Duration) -> Self { Self { - duration: duration, + duration, time_scale: TimeScale::TAI, } } diff --git a/tests/epoch.rs b/tests/epoch.rs index a8de59df..def79d51 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -31,19 +31,6 @@ fn test_const_ops() { assert!((j2000_offset.to_unit(Unit::Day) - J2000_OFFSET).abs() < f64::EPSILON); } -#[test] -fn test_from_gregorian() { - let utc_epoch = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14); - let tai_epoch = Epoch::from_gregorian_at_midnight(2017, 1, 14, TimeScale::TAI); - assert_eq!(utc_epoch.to_time_scale(TimeScale::TAI), tai_epoch); - assert_eq!(utc_epoch, tai_epoch.to_time_scale(TimeScale::UTC)); - - let utc_epoch = Epoch::from_gregorian_utc(2016, 12, 31, 23, 59, 59, 0); - let tai_epoch = Epoch::from_gregorian(2016, 12, 31, 23, 59, 59, 0, TimeScale::TAI); - assert_eq!(utc_epoch.to_time_scale(TimeScale::TAI), tai_epoch); - assert_eq!(utc_epoch, tai_epoch.to_time_scale(TimeScale::UTC)); -} - #[allow(clippy::float_equality_without_abs)] #[test] fn utc_epochs() { From 5c4a2eb86e0c7fc631d711c46b4b217b738f9c2c Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 17 Jun 2023 15:31:59 -0600 Subject: [PATCH 14/37] Fix gregorian computation with new arch --- .gitignore | 3 +- src/epoch.rs | 67 ++++++++++++++++++++++---------------------- src/timescale/mod.rs | 8 ++++++ tests/epoch.rs | 15 ---------- 4 files changed, 44 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index 8d58288b..ebe757a1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ Cargo.lock .env dist/ -.venv \ No newline at end of file +.venv +src/epoch.rs diff --git a/src/epoch.rs b/src/epoch.rs index 5c911cf1..5424ae10 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -840,37 +840,34 @@ impl Epoch { return Err(Errors::Carry); } - let years_since_1900 = year - 1900; - let mut duration_wrt_1900 = Unit::Day * i64::from(365 * years_since_1900); - - // count leap years - if years_since_1900 > 0 { - // we don't count the leap year in 1904, since jan 1904 hasn't had the leap yet, - // so we push it back to 1905, same for all other leap years - let years_after_1900 = years_since_1900 - 1; - duration_wrt_1900 += Unit::Day * i64::from(years_after_1900 / 4); - duration_wrt_1900 -= Unit::Day * i64::from(years_after_1900 / 100); - // every 400 years we correct our correction. The first one after 1900 is 2000 (years_since_1900 = 100) - // so we add 300 to correct the offset - duration_wrt_1900 += Unit::Day * i64::from((years_after_1900 + 300) / 400); + let years_since_ref = year - time_scale.ref_year(); + let mut duration_wrt_ref = Unit::Day * i64::from(365 * years_since_ref); + + // Now add the leap days for all the years prior to the current year + if year >= time_scale.ref_year() { + for year in time_scale.ref_year()..year { + if is_leap_year(year) { + duration_wrt_ref += Unit::Day; + } + } } else { - // we don't need to fix the offset, since jan 1896 has had the leap, when counting back from 1900 - duration_wrt_1900 += Unit::Day * i64::from(years_since_1900 / 4); - duration_wrt_1900 -= Unit::Day * i64::from(years_since_1900 / 100); - // every 400 years we correct our correction. The first one before 1900 is 1600 (years_since_1900 = -300) - // so we subtract 100 to correct the offset - duration_wrt_1900 += Unit::Day * i64::from((years_since_1900 - 100) / 400); - }; + // Remove days + for year in year..time_scale.ref_year() { + if is_leap_year(year) { + duration_wrt_ref -= Unit::Day; + } + } + } // Add the seconds for the months prior to the current month - duration_wrt_1900 += Unit::Day * i64::from(CUMULATIVE_DAYS_FOR_MONTH[(month - 1) as usize]); + duration_wrt_ref += Unit::Day * i64::from(CUMULATIVE_DAYS_FOR_MONTH[(month - 1) as usize]); if is_leap_year(year) && month > 2 { // NOTE: If on 29th of February, then the day is not finished yet, and therefore // the extra seconds are added below as per a normal day. - duration_wrt_1900 += Unit::Day; + duration_wrt_ref += Unit::Day; } - duration_wrt_1900 += Unit::Day * i64::from(day - 1) + duration_wrt_ref += Unit::Day * i64::from(day - 1) + Unit::Hour * i64::from(hour) + Unit::Minute * i64::from(minute) + Unit::Second * i64::from(second) @@ -878,11 +875,11 @@ impl Epoch { if second == 60 { // Herein lies the whole ambiguity of leap seconds. Two different UTC dates exist at the // same number of second after J1900.0. - duration_wrt_1900 -= Unit::Second; + duration_wrt_ref -= Unit::Second; } Ok(Self { - duration: duration_wrt_1900, + duration: duration_wrt_ref, time_scale, }) } @@ -1236,11 +1233,8 @@ impl Epoch { duration: Duration, ts: TimeScale, ) -> (i32, u8, u8, u8, u8, u8, u32) { - let ts_offset = ts.tai_reference_epoch(); - let offset_duration = duration + ts_offset.duration; - let (sign, days, hours, minutes, seconds, milliseconds, microseconds, nanos) = - offset_duration.decompose(); + duration.decompose(); let days_f64 = if sign < 0 { -(days as f64) @@ -1249,18 +1243,18 @@ impl Epoch { }; let (mut year, mut days_in_year) = div_rem_f64(days_f64, DAYS_PER_YEAR_NLD); - year += 1900; // NB: this assumes all known time scales started after J1900 + year += ts.ref_year(); // NB: this assumes all known time scales started after J1900 // Base calculation was on 365 days, so we need to remove one day in seconds per leap year // between 1900 and `year` - if year >= 1900 { - for year in 1900..year { + if year >= ts.ref_year() { + for year in ts.ref_year()..year { if is_leap_year(year) { days_in_year -= 1.0; } } } else { - for year in year..1900 { + for year in year..ts.ref_year() { if is_leap_year(year) { days_in_year += 1.0; } @@ -3491,3 +3485,10 @@ fn formal_epoch_julian() { Epoch::from_jde_tai(days); } } + +#[test] +fn test_from_str_tdb2() { + use core::str::FromStr; + let greg = "2020-01-31T00:00:00 TDB"; + assert_eq!(greg, format!("{:e}", Epoch::from_str(greg).unwrap())); +} diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index 92dcd409..df7a6f26 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -127,6 +127,14 @@ impl TimeScale { Self::TT | Self::TAI | Self::UTC => J1900_REF_EPOCH, } } + + /// Returns the reference year of this time scale, used to convert the Gregorian format + pub(crate) const fn ref_year(&self) -> i32 { + match self { + TimeScale::ET | TimeScale::TDB => 2000, + _ => 1900, + } + } } #[cfg_attr(feature = "python", pymethods)] diff --git a/tests/epoch.rs b/tests/epoch.rs index def79d51..9d64c8fa 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1898,21 +1898,6 @@ fn test_to_tai_time_scale() { assert_eq!(j2000_to_j1900, J2000_TO_J1900_DURATION); } -#[test] -fn test_to_utc_time_scale() { - let gpst_tai_ref = TimeScale::GPST.tai_reference_epoch(); - // there were 19 leap seconds on the day GPST was "started" - let gpst_utc_delta = 19.0; // leaps - let gpst_utc_ref = gpst_tai_ref.to_time_scale(TimeScale::UTC); - assert_eq!((gpst_utc_ref - gpst_tai_ref).to_seconds(), gpst_utc_delta); - - let bdt_tai_ref = TimeScale::BDT.tai_reference_epoch(); - // there were 33 leap seconds on the day BDT was "started" - let bdt_utc_delta = 33.0; // leaps - let bdt_utc_ref = bdt_tai_ref.to_time_scale(TimeScale::UTC); - assert_eq!((bdt_utc_ref - bdt_tai_ref).to_seconds(), bdt_utc_delta); -} - #[cfg(feature = "std")] #[test] fn test_leap_seconds_file() { From fb0e8096741ccf3452f78f24ef25ff9829c6a562 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 17 Jun 2023 16:04:44 -0600 Subject: [PATCH 15/37] Might have found a bug in duration decompose --- .gitignore | 1 + src/duration/mod.rs | 17 +++++++++++++++++ src/epoch.rs | 16 +++++++++++----- tests/epoch.rs | 3 +-- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index ebe757a1..bfa72501 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Cargo.lock dist/ .venv src/epoch.rs +.vscode/settings.json diff --git a/src/duration/mod.rs b/src/duration/mod.rs index b67c7a10..45d6bc3b 100644 --- a/src/duration/mod.rs +++ b/src/duration/mod.rs @@ -762,3 +762,20 @@ fn test_bounds() { assert_eq!(max_n1.centuries, i16::MAX); assert_eq!(max_n1.nanoseconds, NANOSECONDS_PER_CENTURY - 1); } + +#[test] +fn test_decompose() { + let d = -73000.days(); + let out_days = d.to_unit(Unit::Day); + assert_eq!(out_days, -73000.0); + let (sign, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) = + d.decompose(); + assert_eq!(sign, -1); + assert_eq!(days, 73000); + assert_eq!(hours, 0); + assert_eq!(minutes, 0); + assert_eq!(seconds, 0); + assert_eq!(milliseconds, 0); + assert_eq!(microseconds, 0); + assert_eq!(nanoseconds, 0); +} diff --git a/src/epoch.rs b/src/epoch.rs index 5424ae10..be94412b 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -840,8 +840,14 @@ impl Epoch { return Err(Errors::Carry); } - let years_since_ref = year - time_scale.ref_year(); - let mut duration_wrt_ref = Unit::Day * i64::from(365 * years_since_ref); + let years_since_ref = dbg!(year - time_scale.ref_year()); + let mut duration_wrt_ref = Unit::Day * i64::from(dbg!(365 * years_since_ref)); + + println!( + "{}\t{}", + Epoch::from_tai_duration(duration_wrt_ref), + duration_wrt_ref + ); // Now add the leap days for all the years prior to the current year if year >= time_scale.ref_year() { @@ -3487,8 +3493,8 @@ fn formal_epoch_julian() { } #[test] -fn test_from_str_tdb2() { +fn delete_me_test_from_str_ut() { use core::str::FromStr; - let greg = "2020-01-31T00:00:00 TDB"; - assert_eq!(greg, format!("{:e}", Epoch::from_str(greg).unwrap())); + let e1700 = Epoch::from_str("1700-01-01T00:00:00 TAI").unwrap(); + assert_eq!(format!("{e1700:x}"), "1700-01-01T00:00:00 TAI"); } diff --git a/tests/epoch.rs b/tests/epoch.rs index 9d64c8fa..eb457002 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -873,10 +873,9 @@ fn test_from_str() { let greg = "2020-01-31T00:00:00 TDB"; assert_eq!(greg, format!("{:e}", Epoch::from_str(greg).unwrap())); - // Newton Raphson of ET leads to an 11 nanosecond error in this case. let greg = "2020-01-31T00:00:00 ET"; assert_eq!( - "2020-01-31T00:00:00.000000011 ET", + "2020-01-31T00:00:00 ET", format!("{:E}", Epoch::from_str(greg).unwrap()) ); From 3606ef6f8661d4bf163e991c32e81d35347d1e2c Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 17 Jun 2023 16:24:06 -0600 Subject: [PATCH 16/37] Fix decompose in duration --- src/duration/mod.rs | 91 ++++++++++++++------------------------------- src/epoch.rs | 17 +-------- 2 files changed, 30 insertions(+), 78 deletions(-) diff --git a/src/duration/mod.rs b/src/duration/mod.rs index 45d6bc3b..d659aabf 100644 --- a/src/duration/mod.rs +++ b/src/duration/mod.rs @@ -15,7 +15,6 @@ pub use crate::{Freq, Frequencies, TimeUnits, Unit}; #[cfg(feature = "std")] mod std; use core::cmp::Ordering; -use core::convert::TryInto; use core::fmt; use core::hash::{Hash, Hasher}; @@ -429,60 +428,34 @@ impl Duration { /// Decomposes a Duration in its sign, days, hours, minutes, seconds, ms, us, ns #[must_use] pub fn decompose(&self) -> (i8, u64, u64, u64, u64, u64, u64, u64) { - let sign = self.signum(); - - match self.try_truncated_nanoseconds() { - Ok(total_ns) => { - let ns_left = total_ns.abs(); - - let (days, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_DAY as i64); - let (hours, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_HOUR as i64); - let (minutes, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_MINUTE as i64); - let (seconds, ns_left) = div_rem_i64(ns_left, NANOSECONDS_PER_SECOND as i64); - let (milliseconds, ns_left) = - div_rem_i64(ns_left, NANOSECONDS_PER_MILLISECOND as i64); - let (microseconds, ns_left) = - div_rem_i64(ns_left, NANOSECONDS_PER_MICROSECOND as i64); - - // Everything should fit in the expected types now - ( - sign, - days.try_into().unwrap(), - hours.try_into().unwrap(), - minutes.try_into().unwrap(), - seconds.try_into().unwrap(), - milliseconds.try_into().unwrap(), - microseconds.try_into().unwrap(), - ns_left.try_into().unwrap(), - ) - } - Err(_) => { - // Doesn't fit on a i64, so let's use the slower i128 - let total_ns = self.total_nanoseconds(); - let ns_left = total_ns.abs(); - - let (days, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_DAY)); - let (hours, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_HOUR)); - let (minutes, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_MINUTE)); - let (seconds, ns_left) = div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_SECOND)); - let (milliseconds, ns_left) = - div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_MILLISECOND)); - let (microseconds, ns_left) = - div_rem_i128(ns_left, i128::from(NANOSECONDS_PER_MICROSECOND)); - - // Everything should fit in the expected types now - ( - sign, - days.try_into().unwrap(), - hours.try_into().unwrap(), - minutes.try_into().unwrap(), - seconds.try_into().unwrap(), - milliseconds.try_into().unwrap(), - microseconds.try_into().unwrap(), - ns_left.try_into().unwrap(), - ) - } - } + let mut me = *self; + let sign = me.signum(); + me = me.abs(); + let days = me.to_unit(Unit::Day).floor(); + me -= days.days(); + let hours = me.to_unit(Unit::Hour).floor(); + me -= hours.hours(); + let minutes = me.to_unit(Unit::Minute).floor(); + me -= minutes.minutes(); + let seconds = me.to_unit(Unit::Second).floor(); + me -= seconds.seconds(); + let milliseconds = me.to_unit(Unit::Millisecond).floor(); + me -= milliseconds.milliseconds(); + let microseconds = me.to_unit(Unit::Microsecond).floor(); + me -= microseconds.microseconds(); + let nanoseconds = me.to_unit(Unit::Nanosecond).round(); + + // Everything should fit in the expected types now + ( + sign, + days as u64, + hours as u64, + minutes as u64, + seconds as u64, + milliseconds as u64, + microseconds as u64, + nanoseconds as u64, + ) } /// Floors this duration to the closest duration from the bottom @@ -719,14 +692,6 @@ impl PartialOrd for Duration { } } -const fn div_rem_i128(me: i128, rhs: i128) -> (i128, i128) { - (me.div_euclid(rhs), me.rem_euclid(rhs)) -} - -const fn div_rem_i64(me: i64, rhs: i64) -> (i64, i64) { - (me.div_euclid(rhs), me.rem_euclid(rhs)) -} - #[test] #[cfg(feature = "serde")] fn test_serdes() { diff --git a/src/epoch.rs b/src/epoch.rs index be94412b..52931c6a 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -840,14 +840,8 @@ impl Epoch { return Err(Errors::Carry); } - let years_since_ref = dbg!(year - time_scale.ref_year()); - let mut duration_wrt_ref = Unit::Day * i64::from(dbg!(365 * years_since_ref)); - - println!( - "{}\t{}", - Epoch::from_tai_duration(duration_wrt_ref), - duration_wrt_ref - ); + let years_since_ref = year - time_scale.ref_year(); + let mut duration_wrt_ref = Unit::Day * i64::from(365 * years_since_ref); // Now add the leap days for all the years prior to the current year if year >= time_scale.ref_year() { @@ -3491,10 +3485,3 @@ fn formal_epoch_julian() { Epoch::from_jde_tai(days); } } - -#[test] -fn delete_me_test_from_str_ut() { - use core::str::FromStr; - let e1700 = Epoch::from_str("1700-01-01T00:00:00 TAI").unwrap(); - assert_eq!(format!("{e1700:x}"), "1700-01-01T00:00:00 TAI"); -} From 53d6ca5f198f5a31858ad1af8f7bdfb77256d807 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 17 Jun 2023 16:30:58 -0600 Subject: [PATCH 17/37] Replace references to `self.duration` to ensure TAI conversion --- src/epoch.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 52931c6a..51756fb8 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -1933,13 +1933,13 @@ impl Epoch { #[must_use] /// Returns the epoch as a floating point value in the provided unit pub fn to_tai(&self, unit: Unit) -> f64 { - self.duration.to_unit(unit) + self.to_tai_duration().to_unit(unit) } #[must_use] /// Returns the TAI parts of this duration - pub const fn to_tai_parts(&self) -> (i16, u64) { - self.duration.to_parts() + pub fn to_tai_parts(&self) -> (i16, u64) { + self.to_tai_duration().to_parts() } #[must_use] @@ -1988,7 +1988,7 @@ impl Epoch { #[must_use] /// Returns this epoch as a duration in the requested units in MJD TAI pub fn to_mjd_tai(&self, unit: Unit) -> f64 { - (self.duration + Unit::Day * J1900_OFFSET).to_unit(unit) + (self.to_tai_duration() + Unit::Day * J1900_OFFSET).to_unit(unit) } #[must_use] @@ -2026,7 +2026,7 @@ impl Epoch { #[must_use] /// Returns the Julian Days from epoch 01 Jan -4713 12:00 (noon) as a Duration pub fn to_jde_tai_duration(&self) -> Duration { - self.duration + Unit::Day * J1900_OFFSET + Unit::Day * MJD_OFFSET + self.to_tai_duration() + Unit::Day * J1900_OFFSET + Unit::Day * MJD_OFFSET } #[must_use] @@ -2062,7 +2062,7 @@ impl Epoch { #[must_use] /// Returns `Duration` past TAI epoch in Terrestrial Time (TT). pub fn to_tt_duration(&self) -> Duration { - self.duration + Unit::Millisecond * TT_OFFSET_MS + self.to_time_scale(TimeScale::TT).duration } #[must_use] @@ -2401,7 +2401,7 @@ impl Epoch { /// Returns this time in a Duration past J1900 counted in UT1 pub fn to_ut1_duration(&self, provider: Ut1Provider) -> Duration { // TAI = UT1 + offset <=> UTC = TAI - offset - self.duration - self.ut1_offset(provider).unwrap_or(Duration::ZERO) + self.to_tai_duration() - self.ut1_offset(provider).unwrap_or(Duration::ZERO) } #[cfg(feature = "ut1")] From 5899c9383bcbe9ed41cb9ad6bdcc18f03641c0d2 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 17 Jun 2023 17:20:27 -0600 Subject: [PATCH 18/37] Likely bug to be fixed I think there is a bug in the Gregorian initialization because it does account for the hour in the reference epoch. The gregorian initialization and formatter should account for the reference epoch of the time scale. --- src/timescale/mod.rs | 7 +++++++ tests/epoch.rs | 12 +++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index df7a6f26..de012848 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -135,6 +135,13 @@ impl TimeScale { _ => 1900, } } + + pub(crate) const fn ref_hour(&self) -> i64 { + match self { + TimeScale::ET | TimeScale::TDB => 0, + _ => 12, + } + } } #[cfg_attr(feature = "python", pymethods)] diff --git a/tests/epoch.rs b/tests/epoch.rs index eb457002..8aef2c99 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -900,11 +900,13 @@ fn test_from_str() { } #[test] -fn test_from_str_tdb() { - use core::str::FromStr; - - let greg = "2020-01-31T00:00:00 TDB"; - assert_eq!(greg, format!("{:e}", Epoch::from_str(greg).unwrap())); +fn test_ref_epoch_delta() { + let e_tai = Epoch::from_gregorian_at_noon(2022, 9, 6, TimeScale::TAI); + let e_tdb = Epoch::from_gregorian_at_noon(2022, 9, 6, TimeScale::TDB); + // Since we store the durations with respect to their time scale reference epochs, + // the difference between the same epoch but in different time scales should be + // exactly the difference between the reference epochs. + assert_eq!(e_tai.duration - e_tdb.duration, J2000_TO_J1900_DURATION); } #[test] From faa31cc5372afbb114f2eefbfbd686732fbba220 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 2 Sep 2023 09:29:00 +0200 Subject: [PATCH 19/37] add more tests Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 201 +++++++++++++++++++++---------------------- src/timescale/mod.rs | 6 +- tests/epoch.rs | 32 +++++-- 3 files changed, 130 insertions(+), 109 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 51756fb8..d1de50dd 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -338,118 +338,117 @@ impl Epoch { pub fn to_time_scale(&self, ts: TimeScale) -> Self { if ts == self.time_scale { // Do nothing, just return a copy - *self - } else { - // Now we need to convert from the current time scale into the desired time scale. - // Let's first compute this epoch from its current time scale into TAI. - let j1900_tai_offset = match self.time_scale { - TimeScale::TAI => self.duration, - TimeScale::TT => self.duration - TT_OFFSET_MS.milliseconds(), - TimeScale::ET => { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds_j2000 = self.duration.to_seconds(); - for _ in 0..5 { - seconds_j2000 += -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds_j2000 - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) - .sin(); - } - - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); - - // Match SPICE by changing the UTC definition. - (self.duration.to_seconds() - delta_et_tai).seconds() + J2000_TO_J1900_DURATION + return *self; + } + // Now we need to convert from the current time scale into the desired time scale. + // Let's first compute this epoch from its current time scale into TAI. + let j1900_tai_offset = match self.time_scale { + TimeScale::TAI => self.duration, + TimeScale::TT => self.duration - TT_OFFSET_MS.milliseconds(), + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds_j2000 = self.duration.to_seconds(); + for _ in 0..5 { + seconds_j2000 += -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds_j2000 + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) + .sin(); } - TimeScale::TDB => { - let gamma = Self::inner_g(self.duration.to_seconds()); - let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); - // Offset back to J1900. - self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION - } - TimeScale::UTC => { - // Assume this is TAI - let mut tai_assumption = *self; - tai_assumption.time_scale = TimeScale::TAI; - self.duration + tai_assumption.leap_seconds(true).unwrap_or(0.0).seconds() - } - TimeScale::GPST => self.duration + GPST_REF_EPOCH.to_tai_duration(), - TimeScale::GST => self.duration + GST_REF_EPOCH.to_tai_duration(), - TimeScale::BDT => self.duration + BDT_REF_EPOCH.to_tai_duration(), - TimeScale::QZSST => self.duration + QZSST_REF_EPOCH.to_tai_duration(), - }; + // Match SPICE by changing the UTC definition. + (self.duration.to_seconds() - delta_et_tai).seconds() + J2000_TO_J1900_DURATION + } + TimeScale::TDB => { + let gamma = Self::inner_g(self.duration.to_seconds()); - // Convert to the desired time scale from the TAI duration - let ts_ref_offset = match ts { - TimeScale::TAI => j1900_tai_offset, - TimeScale::TT => j1900_tai_offset + TT_OFFSET_MS.milliseconds(), - TimeScale::ET => { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); - for _ in 0..5 { - seconds -= -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) - .sin(); - } + let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); + // Offset back to J1900. + self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION + } + TimeScale::UTC => { + // Assume this is TAI + let mut tai_assumption = *self; + tai_assumption.time_scale = TimeScale::TAI; + self.duration + tai_assumption.leap_seconds(true).unwrap_or(0.0).seconds() + } + TimeScale::GPST => self.duration + GPST_REF_EPOCH.to_tai_duration(), + TimeScale::GST => self.duration + GST_REF_EPOCH.to_tai_duration(), + TimeScale::BDT => self.duration + BDT_REF_EPOCH.to_tai_duration(), + TimeScale::QZSST => self.duration + QZSST_REF_EPOCH.to_tai_duration(), + }; - // Match SPICE by changing the UTC definition. - j1900_tai_offset + delta_et_tai.seconds() - J2000_TO_J1900_DURATION + // Convert to the desired time scale from the TAI duration + let ts_ref_offset = match ts { + TimeScale::TAI => j1900_tai_offset, + TimeScale::TT => j1900_tai_offset + TT_OFFSET_MS.milliseconds(), + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); + for _ in 0..5 { + seconds -= -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) + .sin(); } - TimeScale::TDB => { - // Iterate to convert find the correct value of the - let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); - let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. - for _ in 0..5 { - let next = seconds - Self::inner_g(seconds); - let new_delta = (next - seconds).abs(); - if (new_delta - delta).abs() < 1e-9 { - break; - } - seconds = next; // Loop - delta = new_delta; - } - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let gamma = - Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - let delta_tdb_tai = gamma.seconds() + TT_OFFSET_MS.milliseconds(); + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); - j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION - } - TimeScale::UTC => { - // Assume it's TAI - let epoch = Self { - duration: j1900_tai_offset, - time_scale: TimeScale::TAI, - }; - // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - j1900_tai_offset - epoch.leap_seconds(true).unwrap_or(0.0).seconds() + // Match SPICE by changing the UTC definition. + j1900_tai_offset + delta_et_tai.seconds() - J2000_TO_J1900_DURATION + } + TimeScale::TDB => { + // Iterate to convert find the correct value of the + let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); + let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. + for _ in 0..5 { + let next = seconds - Self::inner_g(seconds); + let new_delta = (next - seconds).abs(); + if (new_delta - delta).abs() < 1e-9 { + break; + } + seconds = next; // Loop + delta = new_delta; } - TimeScale::GPST => j1900_tai_offset - GPST_REF_EPOCH.to_tai_duration(), - TimeScale::GST => j1900_tai_offset - GST_REF_EPOCH.to_tai_duration(), - TimeScale::BDT => j1900_tai_offset - BDT_REF_EPOCH.to_tai_duration(), - TimeScale::QZSST => j1900_tai_offset - QZSST_REF_EPOCH.to_tai_duration(), - }; - Self { - duration: ts_ref_offset, - time_scale: ts, + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let gamma = + Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); + let delta_tdb_tai = gamma.seconds() + TT_OFFSET_MS.milliseconds(); + + j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION } + TimeScale::UTC => { + // Assume it's TAI + let epoch = Self { + duration: j1900_tai_offset, + time_scale: TimeScale::TAI, + }; + // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds + j1900_tai_offset - epoch.leap_seconds(true).unwrap_or(0.0).seconds() + } + TimeScale::GPST => j1900_tai_offset - GPST_REF_EPOCH.to_tai_duration(), + TimeScale::GST => j1900_tai_offset - GST_REF_EPOCH.to_tai_duration(), + TimeScale::BDT => j1900_tai_offset - BDT_REF_EPOCH.to_tai_duration(), + TimeScale::QZSST => j1900_tai_offset - QZSST_REF_EPOCH.to_tai_duration(), + }; + + Self { + duration: ts_ref_offset, + time_scale: ts, } } @@ -1243,7 +1242,7 @@ impl Epoch { }; let (mut year, mut days_in_year) = div_rem_f64(days_f64, DAYS_PER_YEAR_NLD); - year += ts.ref_year(); // NB: this assumes all known time scales started after J1900 + year += ts.ref_year(); // NB: this assumes time scales all started after J1900 // Base calculation was on 365 days, so we need to remove one day in seconds per leap year // between 1900 and `year` diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index de012848..cfa633af 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -28,9 +28,11 @@ use crate::{ pub const J1900_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration::ZERO); /// The J2000 reference epoch (2000-01-01 at midnight) TAI. +/// |UTC - TAI| = XX Leap Seconds on that day. pub const J2000_REF_EPOCH: Epoch = Epoch::from_tai_duration(J2000_TO_J1900_DURATION); /// GPS reference epoch is UTC midnight between 05 January and 06 January 1980; cf. . +/// |UTC - TAI| = 19 Leap Seconds on that day. pub const GPST_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { centuries: 0, nanoseconds: 2_524_953_619_000_000_000, // XXX @@ -39,10 +41,11 @@ pub const SECONDS_GPS_TAI_OFFSET: f64 = 2_524_953_619.0; pub const SECONDS_GPS_TAI_OFFSET_I64: i64 = 2_524_953_619; pub const DAYS_GPS_TAI_OFFSET: f64 = SECONDS_GPS_TAI_OFFSET / SECONDS_PER_DAY; -/// QZSS and GPS share the same reference epoch +/// QZSS and GPS share the same reference epoch. pub const QZSST_REF_EPOCH: Epoch = GPST_REF_EPOCH; /// GST (Galileo) reference epoch is 13 seconds before 1999 August 21 UTC at midnight. +/// |UTC - TAI| = XX Leap Seconds on that day. pub const GST_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { centuries: 0, nanoseconds: 3_144_268_819_000_000_000, @@ -52,6 +55,7 @@ pub const SECONDS_GST_TAI_OFFSET_I64: i64 = 3_144_268_819; /// BDT(BeiDou): 2005 Dec 31st Midnight /// BDT (BeiDou) reference epoch is 2005 December 31st UTC at midnight. **This time scale is synchronized with UTC.** +/// |UTC - TAI| = XX Leap Seconds on that day. pub const BDT_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { centuries: 1, nanoseconds: 189_302_433_000_000_000, diff --git a/tests/epoch.rs b/tests/epoch.rs index 8aef2c99..07caa94d 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -900,13 +900,31 @@ fn test_from_str() { } #[test] -fn test_ref_epoch_delta() { - let e_tai = Epoch::from_gregorian_at_noon(2022, 9, 6, TimeScale::TAI); - let e_tdb = Epoch::from_gregorian_at_noon(2022, 9, 6, TimeScale::TDB); - // Since we store the durations with respect to their time scale reference epochs, - // the difference between the same epoch but in different time scales should be - // exactly the difference between the reference epochs. - assert_eq!(e_tai.duration - e_tdb.duration, J2000_TO_J1900_DURATION); +fn test_timescale_leapsec() { + /* + * Time difference between Time Scales that do not support leap sec. + * and UTC, is always the amount of UTC leap seconds on the day + * said time scale was "initiated" + */ + for (ts, leap_t0) in vec![ + (TimeScale::GPST, 19), + (TimeScale::QZSST, 19), + (TimeScale::GST, 0), + (TimeScale::BDT, 0), + (TimeScale::TDB, 0), + (TimeScale::ET, 0), + (TimeScale::TT, 0), + ] { + assert!(!ts.uses_leap_seconds()); + //let duration: Duration = kani::any(); + let duration = Duration::from_seconds(12349.433_f64); + let epoch = Epoch::from_duration(duration, ts); + let utc_epoch = epoch.to_time_scale(TimeScale::UTC); + assert_eq!( + (epoch - utc_epoch).abs().to_seconds(), + leap_t0 as f64, + "|{} - {}| should be {} secs", epoch, utc_epoch, leap_t0); + } } #[test] From 8e19bfa793ea559fd40f171665be614728ebf934 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 2 Sep 2023 10:18:09 +0200 Subject: [PATCH 20/37] improve to_time_scale, initial_year.. Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 247 ++++++++++++++++++++++++------------------- src/timescale/mod.rs | 30 ++++-- tests/epoch.rs | 2 +- 3 files changed, 160 insertions(+), 119 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index d1de50dd..6b68f58e 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -340,116 +340,143 @@ impl Epoch { // Do nothing, just return a copy return *self; } - // Now we need to convert from the current time scale into the desired time scale. - // Let's first compute this epoch from its current time scale into TAI. - let j1900_tai_offset = match self.time_scale { - TimeScale::TAI => self.duration, - TimeScale::TT => self.duration - TT_OFFSET_MS.milliseconds(), - TimeScale::ET => { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds_j2000 = self.duration.to_seconds(); - for _ in 0..5 { - seconds_j2000 += -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds_j2000 - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) - .sin(); - } - - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); - - // Match SPICE by changing the UTC definition. - (self.duration.to_seconds() - delta_et_tai).seconds() + J2000_TO_J1900_DURATION - } - TimeScale::TDB => { - let gamma = Self::inner_g(self.duration.to_seconds()); + // TAI is our reference case in other conversions + if ts == TimeScale::TAI { + let j1900_tai_offset = match self.time_scale { + TimeScale::TT => self.duration - TT_OFFSET_MS.milliseconds(), + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds_j2000 = self.duration.to_seconds(); + for _ in 0..5 { + seconds_j2000 += -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds_j2000 + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) + .sin(); + } - let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); - // Offset back to J1900. - self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION - } - TimeScale::UTC => { - // Assume this is TAI - let mut tai_assumption = *self; - tai_assumption.time_scale = TimeScale::TAI; - self.duration + tai_assumption.leap_seconds(true).unwrap_or(0.0).seconds() - } - TimeScale::GPST => self.duration + GPST_REF_EPOCH.to_tai_duration(), - TimeScale::GST => self.duration + GST_REF_EPOCH.to_tai_duration(), - TimeScale::BDT => self.duration + BDT_REF_EPOCH.to_tai_duration(), - TimeScale::QZSST => self.duration + QZSST_REF_EPOCH.to_tai_duration(), - }; - - // Convert to the desired time scale from the TAI duration - let ts_ref_offset = match ts { - TimeScale::TAI => j1900_tai_offset, - TimeScale::TT => j1900_tai_offset + TT_OFFSET_MS.milliseconds(), - TimeScale::ET => { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); - for _ in 0..5 { - seconds -= -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) - .sin(); + // Match SPICE by changing the UTC definition. + (self.duration.to_seconds() - delta_et_tai).seconds() + J2000_TO_J1900_DURATION } + TimeScale::TDB => { + let gamma = Self::inner_g(self.duration.to_seconds()); - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); + let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - // Match SPICE by changing the UTC definition. - j1900_tai_offset + delta_et_tai.seconds() - J2000_TO_J1900_DURATION - } - TimeScale::TDB => { - // Iterate to convert find the correct value of the - let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); - let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. - for _ in 0..5 { - let next = seconds - Self::inner_g(seconds); - let new_delta = (next - seconds).abs(); - if (new_delta - delta).abs() < 1e-9 { - break; - } - seconds = next; // Loop - delta = new_delta; + // Offset back to J1900. + self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION } - - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let gamma = - Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - let delta_tdb_tai = gamma.seconds() + TT_OFFSET_MS.milliseconds(); - - j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION - } - TimeScale::UTC => { - // Assume it's TAI - let epoch = Self { - duration: j1900_tai_offset, - time_scale: TimeScale::TAI, - }; - // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - j1900_tai_offset - epoch.leap_seconds(true).unwrap_or(0.0).seconds() - } - TimeScale::GPST => j1900_tai_offset - GPST_REF_EPOCH.to_tai_duration(), - TimeScale::GST => j1900_tai_offset - GST_REF_EPOCH.to_tai_duration(), - TimeScale::BDT => j1900_tai_offset - BDT_REF_EPOCH.to_tai_duration(), - TimeScale::QZSST => j1900_tai_offset - QZSST_REF_EPOCH.to_tai_duration(), - }; - + TimeScale::UTC => { + // Assume this is TAI + let mut tai_assumption = *self; + tai_assumption.time_scale = TimeScale::TAI; + self.duration + tai_assumption.leap_seconds(true).unwrap_or(0.0).seconds() + } + TimeScale::GPST => self.duration + GPST_REF_EPOCH.to_tai_duration(), + TimeScale::GST => self.duration + GST_REF_EPOCH.to_tai_duration(), + TimeScale::BDT => self.duration + BDT_REF_EPOCH.to_tai_duration(), + TimeScale::QZSST => self.duration + QZSST_REF_EPOCH.to_tai_duration(), + _ => unreachable!(), + }; + return Self::from_tai_duration(j1900_tai_offset); + } + /* + * Other cases : convert back to TAI + * and simply add the difference between TimeScale initial points + * + * positive: + * TAI(J1900) self.time_scale(t0) + * |--------------------x--------------------->self + * |--------------------------x--------------------->target + * target.time_scale(t0) + * + * negative: + * TAI(J1900) self.time_scale(t0) + * |-------------------x--------------------->self + * |----------------x--------------------->target + * target.time_scale(t0) + */ + let mut tai_epoch = self.to_time_scale(TimeScale::TAI); + // shift in time + let tai_delta = self.time_scale.tai_reference_epoch() - ts.tai_reference_epoch(); + tai_epoch -= tai_delta; + // remove time scale origin (in TAI) + tai_epoch -= ts.tai_reference_epoch().to_tai_duration(); Self { - duration: ts_ref_offset, + duration: tai_epoch.duration, time_scale: ts, } + //// Convert to the desired time scale from the TAI duration + //let ts_ref_offset = match ts { + // TimeScale::TAI => j1900_tai_offset, + // TimeScale::TT => j1900_tai_offset + TT_OFFSET_MS.milliseconds(), + // TimeScale::ET => { + // // Run a Newton Raphston to convert find the correct value of the + // let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); + // for _ in 0..5 { + // seconds -= -NAIF_K + // * (NAIF_M0 + // + NAIF_M1 * seconds + // + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) + // .sin(); + // } + + // // At this point, we have a good estimate of the number of seconds of this epoch. + // // Reverse the algorithm: + // let delta_et_tai = Self::delta_et_tai( + // seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + // ); + + // // Match SPICE by changing the UTC definition. + // j1900_tai_offset + delta_et_tai.seconds() - J2000_TO_J1900_DURATION + // } + // TimeScale::TDB => { + // // Iterate to convert find the correct value of the + // let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); + // let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. + // for _ in 0..5 { + // let next = seconds - Self::inner_g(seconds); + // let new_delta = (next - seconds).abs(); + // if (new_delta - delta).abs() < 1e-9 { + // break; + // } + // seconds = next; // Loop + // delta = new_delta; + // } + + // // At this point, we have a good estimate of the number of seconds of this epoch. + // // Reverse the algorithm: + // let gamma = + // Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); + // let delta_tdb_tai = gamma.seconds() + TT_OFFSET_MS.milliseconds(); + + // j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION + // } + // TimeScale::UTC => { + // // Assume it's TAI + // let epoch = Self { + // duration: j1900_tai_offset, + // time_scale: TimeScale::TAI, + // }; + // // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds + // j1900_tai_offset - epoch.leap_seconds(true).unwrap_or(0.0).seconds() + // } + // TimeScale::GPST => j1900_tai_offset - GPST_REF_EPOCH.to_tai_duration(), + // TimeScale::GST => j1900_tai_offset - GST_REF_EPOCH.to_tai_duration(), + // TimeScale::BDT => j1900_tai_offset - BDT_REF_EPOCH.to_tai_duration(), + // TimeScale::QZSST => j1900_tai_offset - QZSST_REF_EPOCH.to_tai_duration(), + //}; + + //Self { + // duration: ts_ref_offset, + // time_scale: ts, + //} } #[must_use] @@ -839,19 +866,19 @@ impl Epoch { return Err(Errors::Carry); } - let years_since_ref = year - time_scale.ref_year(); + let years_since_ref = year - time_scale.initial_year(); let mut duration_wrt_ref = Unit::Day * i64::from(365 * years_since_ref); // Now add the leap days for all the years prior to the current year - if year >= time_scale.ref_year() { - for year in time_scale.ref_year()..year { + if year >= time_scale.initial_year() { + for year in time_scale.initial_year()..year { if is_leap_year(year) { duration_wrt_ref += Unit::Day; } } } else { // Remove days - for year in year..time_scale.ref_year() { + for year in year..time_scale.initial_year() { if is_leap_year(year) { duration_wrt_ref -= Unit::Day; } @@ -1230,7 +1257,7 @@ impl Epoch { pub(crate) fn compute_gregorian( duration: Duration, - ts: TimeScale, + ts: TimeScale ) -> (i32, u8, u8, u8, u8, u8, u32) { let (sign, days, hours, minutes, seconds, milliseconds, microseconds, nanos) = duration.decompose(); @@ -1242,18 +1269,18 @@ impl Epoch { }; let (mut year, mut days_in_year) = div_rem_f64(days_f64, DAYS_PER_YEAR_NLD); - year += ts.ref_year(); // NB: this assumes time scales all started after J1900 + year += ts.initial_year(); // NB: this assumes time scales all started after J1900 // Base calculation was on 365 days, so we need to remove one day in seconds per leap year // between 1900 and `year` - if year >= ts.ref_year() { - for year in ts.ref_year()..year { + if year >= ts.initial_year() { + for year in ts.initial_year()..year { if is_leap_year(year) { days_in_year -= 1.0; } } } else { - for year in year..ts.ref_year() { + for year in year..ts.initial_year() { if is_leap_year(year) { days_in_year += 1.0; } diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index cfa633af..d3ea4359 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -20,7 +20,7 @@ mod kani; mod fmt; use crate::{ - Duration, Epoch, J2000_REF_EPOCH_ET, J2000_REF_EPOCH_TDB, J2000_TO_J1900_DURATION, + Duration, Epoch, Unit, J2000_REF_EPOCH_ET, J2000_REF_EPOCH_TDB, J2000_TO_J1900_DURATION, SECONDS_PER_DAY, }; @@ -131,13 +131,13 @@ impl TimeScale { Self::TT | Self::TAI | Self::UTC => J1900_REF_EPOCH, } } - - /// Returns the reference year of this time scale, used to convert the Gregorian format - pub(crate) const fn ref_year(&self) -> i32 { - match self { - TimeScale::ET | TimeScale::TDB => 2000, - _ => 1900, - } + + /// Returns the year offset compared to J1900 TAI. + /// This assues all supported time scales were "started" past J1900. + pub(crate) fn initial_year(&self) -> i32 { + self.tai_reference_epoch() + .to_tai_duration() + .to_unit(Unit::Day).floor() as i32 / 365 + 1900 } pub(crate) const fn ref_hour(&self) -> i64 { @@ -219,4 +219,18 @@ mod unit_test_timescale { } } } + + #[test] + fn test_initial_year() { + for (ts, y0) in vec![ + (TimeScale::TAI, 1900), + (TimeScale::UTC, 1900), + (TimeScale::GPST, 1980), + (TimeScale::BDT, 2006), + (TimeScale::GST, 1999), + ] { + let y = ts.initial_year(); + assert_eq!(y, y0, "wrong initial year of {} for {}", y, ts); + } + } } diff --git a/tests/epoch.rs b/tests/epoch.rs index 07caa94d..ea86d55f 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -921,7 +921,7 @@ fn test_timescale_leapsec() { let epoch = Epoch::from_duration(duration, ts); let utc_epoch = epoch.to_time_scale(TimeScale::UTC); assert_eq!( - (epoch - utc_epoch).abs().to_seconds(), + (epoch - utc_epoch).to_seconds(), leap_t0 as f64, "|{} - {}| should be {} secs", epoch, utc_epoch, leap_t0); } From d1e6236db581b9dc59c1e0137b5fc8737a41cade Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 2 Sep 2023 11:20:42 +0200 Subject: [PATCH 21/37] on the right track.. Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 174 ++++++++++--------------------------------- src/timescale/mod.rs | 7 +- tests/epoch.rs | 22 +++--- 3 files changed, 58 insertions(+), 145 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 6b68f58e..91e26a27 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -339,144 +339,47 @@ impl Epoch { if ts == self.time_scale { // Do nothing, just return a copy return *self; - } + } // TAI is our reference case in other conversions if ts == TimeScale::TAI { - let j1900_tai_offset = match self.time_scale { - TimeScale::TT => self.duration - TT_OFFSET_MS.milliseconds(), - TimeScale::ET => { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds_j2000 = self.duration.to_seconds(); - for _ in 0..5 { - seconds_j2000 += -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds_j2000 - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) - .sin(); - } - - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); - - // Match SPICE by changing the UTC definition. - (self.duration.to_seconds() - delta_et_tai).seconds() + J2000_TO_J1900_DURATION - } - TimeScale::TDB => { - let gamma = Self::inner_g(self.duration.to_seconds()); - - let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - - // Offset back to J1900. - self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION - } + match self.time_scale { + TimeScale::TT => unreachable!("TT"), + TimeScale::ET => unreachable!("ET"), + TimeScale::TDB => unreachable!("TDB"), TimeScale::UTC => { - // Assume this is TAI - let mut tai_assumption = *self; - tai_assumption.time_scale = TimeScale::TAI; - self.duration + tai_assumption.leap_seconds(true).unwrap_or(0.0).seconds() + return *self - Duration::from_seconds(self.leap_seconds(true).unwrap_or(0.0)); } - TimeScale::GPST => self.duration + GPST_REF_EPOCH.to_tai_duration(), - TimeScale::GST => self.duration + GST_REF_EPOCH.to_tai_duration(), - TimeScale::BDT => self.duration + BDT_REF_EPOCH.to_tai_duration(), - TimeScale::QZSST => self.duration + QZSST_REF_EPOCH.to_tai_duration(), - _ => unreachable!(), - }; - return Self::from_tai_duration(j1900_tai_offset); + _ => { + /* + * simple case + * self.time_scale(t0) + * |--------------------->self + * TAI(J1900) + * |------------------------------------------>target + */ + return Self { + duration: { + self.time_scale.tai_reference_epoch().to_tai_duration() + self.duration + }, + time_scale: TimeScale::TAI, + }; + } + } } - /* - * Other cases : convert back to TAI - * and simply add the difference between TimeScale initial points - * - * positive: - * TAI(J1900) self.time_scale(t0) - * |--------------------x--------------------->self - * |--------------------------x--------------------->target - * target.time_scale(t0) - * - * negative: - * TAI(J1900) self.time_scale(t0) - * |-------------------x--------------------->self - * |----------------x--------------------->target - * target.time_scale(t0) - */ - let mut tai_epoch = self.to_time_scale(TimeScale::TAI); - // shift in time - let tai_delta = self.time_scale.tai_reference_epoch() - ts.tai_reference_epoch(); - tai_epoch -= tai_delta; - // remove time scale origin (in TAI) - tai_epoch -= ts.tai_reference_epoch().to_tai_duration(); - Self { - duration: tai_epoch.duration, - time_scale: ts, + match ts { + TimeScale::UTC => { + self.to_time_scale(TimeScale::TAI) + + Duration::from_seconds(self.leap_seconds(true).unwrap_or(0.0)) + } + TimeScale::GPST | TimeScale::BDT | TimeScale::GST | TimeScale::QZSST => Self { + duration: { + self.to_duration_in_time_scale(TimeScale::TAI) + - ts.tai_reference_epoch().duration + }, + time_scale: ts, + }, + _ => unreachable!(), } - //// Convert to the desired time scale from the TAI duration - //let ts_ref_offset = match ts { - // TimeScale::TAI => j1900_tai_offset, - // TimeScale::TT => j1900_tai_offset + TT_OFFSET_MS.milliseconds(), - // TimeScale::ET => { - // // Run a Newton Raphston to convert find the correct value of the - // let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); - // for _ in 0..5 { - // seconds -= -NAIF_K - // * (NAIF_M0 - // + NAIF_M1 * seconds - // + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) - // .sin(); - // } - - // // At this point, we have a good estimate of the number of seconds of this epoch. - // // Reverse the algorithm: - // let delta_et_tai = Self::delta_et_tai( - // seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - // ); - - // // Match SPICE by changing the UTC definition. - // j1900_tai_offset + delta_et_tai.seconds() - J2000_TO_J1900_DURATION - // } - // TimeScale::TDB => { - // // Iterate to convert find the correct value of the - // let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); - // let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson. - // for _ in 0..5 { - // let next = seconds - Self::inner_g(seconds); - // let new_delta = (next - seconds).abs(); - // if (new_delta - delta).abs() < 1e-9 { - // break; - // } - // seconds = next; // Loop - // delta = new_delta; - // } - - // // At this point, we have a good estimate of the number of seconds of this epoch. - // // Reverse the algorithm: - // let gamma = - // Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - // let delta_tdb_tai = gamma.seconds() + TT_OFFSET_MS.milliseconds(); - - // j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION - // } - // TimeScale::UTC => { - // // Assume it's TAI - // let epoch = Self { - // duration: j1900_tai_offset, - // time_scale: TimeScale::TAI, - // }; - // // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - // j1900_tai_offset - epoch.leap_seconds(true).unwrap_or(0.0).seconds() - // } - // TimeScale::GPST => j1900_tai_offset - GPST_REF_EPOCH.to_tai_duration(), - // TimeScale::GST => j1900_tai_offset - GST_REF_EPOCH.to_tai_duration(), - // TimeScale::BDT => j1900_tai_offset - BDT_REF_EPOCH.to_tai_duration(), - // TimeScale::QZSST => j1900_tai_offset - QZSST_REF_EPOCH.to_tai_duration(), - //}; - - //Self { - // duration: ts_ref_offset, - // time_scale: ts, - //} } #[must_use] @@ -1257,7 +1160,7 @@ impl Epoch { pub(crate) fn compute_gregorian( duration: Duration, - ts: TimeScale + ts: TimeScale, ) -> (i32, u8, u8, u8, u8, u8, u32) { let (sign, days, hours, minutes, seconds, milliseconds, microseconds, nanos) = duration.decompose(); @@ -1953,7 +1856,10 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in TAI pub fn to_tai_duration(&self) -> Duration { - self.to_time_scale(TimeScale::TAI).duration + Duration::from_seconds( + self.duration.to_seconds() + + self.time_scale.tai_reference_epoch().duration.to_seconds(), + ) } #[must_use] diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index d3ea4359..760a32a5 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -131,13 +131,16 @@ impl TimeScale { Self::TT | Self::TAI | Self::UTC => J1900_REF_EPOCH, } } - + /// Returns the year offset compared to J1900 TAI. /// This assues all supported time scales were "started" past J1900. pub(crate) fn initial_year(&self) -> i32 { self.tai_reference_epoch() .to_tai_duration() - .to_unit(Unit::Day).floor() as i32 / 365 + 1900 + .to_unit(Unit::Day) + .floor() as i32 + / 365 + + 1900 } pub(crate) const fn ref_hour(&self) -> i64 { diff --git a/tests/epoch.rs b/tests/epoch.rs index ea86d55f..5a61e221 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -909,21 +909,25 @@ fn test_timescale_leapsec() { for (ts, leap_t0) in vec![ (TimeScale::GPST, 19), (TimeScale::QZSST, 19), - (TimeScale::GST, 0), - (TimeScale::BDT, 0), - (TimeScale::TDB, 0), - (TimeScale::ET, 0), - (TimeScale::TT, 0), + (TimeScale::GST, 32), + (TimeScale::BDT, 33), + //(TimeScale::TDB, 0), + //(TimeScale::ET, 0), + //(TimeScale::TT, 0), ] { assert!(!ts.uses_leap_seconds()); - //let duration: Duration = kani::any(); + //let duration: Duration = kani::any(); let duration = Duration::from_seconds(12349.433_f64); let epoch = Epoch::from_duration(duration, ts); let utc_epoch = epoch.to_time_scale(TimeScale::UTC); assert_eq!( - (epoch - utc_epoch).to_seconds(), + (epoch - utc_epoch).abs().to_seconds(), leap_t0 as f64, - "|{} - {}| should be {} secs", epoch, utc_epoch, leap_t0); + "|{} - {}| should be {} secs", + epoch, + utc_epoch, + leap_t0 + ); } } @@ -1274,7 +1278,7 @@ fn test_timescale_recip() { TimeScale::TAI, //TimeScale::ET, //TimeScale::TDB, - TimeScale::TT, + //TimeScale::TT, TimeScale::UTC, ] { let converted = utc_epoch.to_duration_in_time_scale(*ts); From ce02a2862355530bdccb11cf48dc0d71c7b041f5 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 2 Sep 2023 11:46:45 +0200 Subject: [PATCH 22/37] on the right track.. Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 91e26a27..86ab6328 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -259,19 +259,20 @@ impl PartialEq for Epoch { if self.time_scale == other.time_scale { self.duration == other.duration } else { - // If one of the two time scales does not include leap seconds, - // we always convert the time scale with leap seconds into the - // time scale that does NOT have leap seconds. - if self.time_scale.uses_leap_seconds() != other.time_scale.uses_leap_seconds() { - if self.time_scale.uses_leap_seconds() { - self.to_time_scale(other.time_scale).duration == other.duration - } else { - self.duration == other.to_time_scale(self.time_scale).duration - } - } else { - // Otherwise it does not matter - self.duration == other.to_time_scale(self.time_scale).duration - } + self.duration == other.to_time_scale(self.time_scale).duration + //// If one of the two time scales does not include leap seconds, + //// we always convert the time scale with leap seconds into the + //// time scale that does NOT have leap seconds. + //if self.time_scale.uses_leap_seconds() != other.time_scale.uses_leap_seconds() { + // if self.time_scale.uses_leap_seconds() { + // self.to_time_scale(other.time_scale).duration == other.duration + // } else { + // self.duration == other.to_time_scale(self.time_scale).duration + // } + //} else { + // // Otherwise it does not matter + // self.duration == other.to_time_scale(self.time_scale).duration + //} } } } @@ -347,7 +348,11 @@ impl Epoch { TimeScale::ET => unreachable!("ET"), TimeScale::TDB => unreachable!("TDB"), TimeScale::UTC => { - return *self - Duration::from_seconds(self.leap_seconds(true).unwrap_or(0.0)); + return Self { + duration: self.duration + - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second, + time_scale: ts, + }; } _ => { /* @@ -368,8 +373,12 @@ impl Epoch { } match ts { TimeScale::UTC => { - self.to_time_scale(TimeScale::TAI) - + Duration::from_seconds(self.leap_seconds(true).unwrap_or(0.0)) + let tai = self.to_time_scale(TimeScale::TAI); + Self { + duration: tai.duration + - Duration::from_seconds(tai.leap_seconds(true).unwrap_or(0.0)), + time_scale: ts, + } } TimeScale::GPST | TimeScale::BDT | TimeScale::GST | TimeScale::QZSST => Self { duration: { @@ -769,7 +778,10 @@ impl Epoch { return Err(Errors::Carry); } - let years_since_ref = year - time_scale.initial_year(); + let years_since_ref = match year > time_scale.initial_year() { + true => year - time_scale.initial_year(), + false => time_scale.initial_year() - year, + }; let mut duration_wrt_ref = Unit::Day * i64::from(365 * years_since_ref); // Now add the leap days for all the years prior to the current year From a225d8d969308f5d02117621f78d0f3b92dddd57 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 2 Sep 2023 11:58:42 +0200 Subject: [PATCH 23/37] working on tai_utc Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 86ab6328..88ef62a6 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -373,12 +373,10 @@ impl Epoch { } match ts { TimeScale::UTC => { - let tai = self.to_time_scale(TimeScale::TAI); - Self { - duration: tai.duration - - Duration::from_seconds(tai.leap_seconds(true).unwrap_or(0.0)), - time_scale: ts, - } + let mut utc = self.to_time_scale(TimeScale::TAI); + utc.time_scale = TimeScale::UTC; + utc.duration += utc.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + utc } TimeScale::GPST | TimeScale::BDT | TimeScale::GST | TimeScale::QZSST => Self { duration: { From 7e10d823adad5278909747900f6718b931253f4d Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 2 Sep 2023 13:04:54 +0200 Subject: [PATCH 24/37] working on tai_utc Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 50 ++++++++++++++++++++++++-------------------- src/timescale/mod.rs | 39 ++++++++++++++++++---------------- tests/epoch.rs | 38 +++++++++++++++++++-------------- 3 files changed, 70 insertions(+), 57 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 88ef62a6..cb8a4af0 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -375,16 +375,16 @@ impl Epoch { TimeScale::UTC => { let mut utc = self.to_time_scale(TimeScale::TAI); utc.time_scale = TimeScale::UTC; - utc.duration += utc.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + utc.duration -= utc.leap_seconds(true).unwrap_or(0.0) * Unit::Second; utc } - TimeScale::GPST | TimeScale::BDT | TimeScale::GST | TimeScale::QZSST => Self { - duration: { - self.to_duration_in_time_scale(TimeScale::TAI) - - ts.tai_reference_epoch().duration - }, - time_scale: ts, - }, + TimeScale::GPST | TimeScale::BDT | TimeScale::GST | TimeScale::QZSST => { + let tai = self.to_time_scale(TimeScale::TAI); + Self { + duration: tai.duration + ts.tai_reference_epoch().duration, + time_scale: ts, + } + } _ => unreachable!(), } } @@ -776,22 +776,26 @@ impl Epoch { return Err(Errors::Carry); } - let years_since_ref = match year > time_scale.initial_year() { - true => year - time_scale.initial_year(), - false => time_scale.initial_year() - year, + let (ts_year, ts_day, ts_month, ts_hh, ts_mm_, ts_ss, ts_ns) = time_scale.decompose(); + let mut ts_year = ts_year as i32; + + let years_since_ref = match year > ts_year { + true => year - ts_year, + false => ts_year - year, }; + let mut duration_wrt_ref = Unit::Day * i64::from(365 * years_since_ref); // Now add the leap days for all the years prior to the current year - if year >= time_scale.initial_year() { - for year in time_scale.initial_year()..year { + if year >= ts_year { + for year in ts_year..year { if is_leap_year(year) { duration_wrt_ref += Unit::Day; } } } else { // Remove days - for year in year..time_scale.initial_year() { + for year in year..ts_year { if is_leap_year(year) { duration_wrt_ref -= Unit::Day; } @@ -1182,18 +1186,21 @@ impl Epoch { }; let (mut year, mut days_in_year) = div_rem_f64(days_f64, DAYS_PER_YEAR_NLD); - year += ts.initial_year(); // NB: this assumes time scales all started after J1900 + + let (ts_year, ts_month, ts_day, ts_hh, ts_mm, ts_ss, ts_ns) = ts.decompose(); + let ts_year = ts_year as i32; + year += ts_year; // NB: this assumes time scales all started after J1900 // Base calculation was on 365 days, so we need to remove one day in seconds per leap year // between 1900 and `year` - if year >= ts.initial_year() { - for year in ts.initial_year()..year { + if year >= ts_year { + for year in ts_year..year { if is_leap_year(year) { days_in_year -= 1.0; } } } else { - for year in year..ts.initial_year() { + for year in year..ts_year { if is_leap_year(year) { days_in_year += 1.0; } @@ -1866,10 +1873,7 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in TAI pub fn to_tai_duration(&self) -> Duration { - Duration::from_seconds( - self.duration.to_seconds() - + self.time_scale.tai_reference_epoch().duration.to_seconds(), - ) + self.duration + self.time_scale.tai_reference_epoch().duration } #[must_use] @@ -1905,7 +1909,7 @@ impl Epoch { #[must_use] /// Returns the number of UTC seconds since the TAI epoch pub fn to_utc(&self, unit: Unit) -> f64 { - self.to_utc_duration().to_unit(unit) + self.to_time_scale(TimeScale::UTC).duration.to_unit(unit) } #[must_use] diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index 760a32a5..ccd70e03 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -132,15 +132,13 @@ impl TimeScale { } } - /// Returns the year offset compared to J1900 TAI. - /// This assues all supported time scales were "started" past J1900. - pub(crate) fn initial_year(&self) -> i32 { - self.tai_reference_epoch() - .to_tai_duration() - .to_unit(Unit::Day) - .floor() as i32 - / 365 - + 1900 + pub(crate) fn decompose(&self) -> (i32, u8, u8, u8, u8, u8, u32) { + match self { + Self::GPST => (1980, 01, 06, 00, 00, 00, 00), + Self::BDT => (2005, 01, 01, 00, 00, 00, 00), + Self::GST => (1999, 21, 07, 00, 00, 00, 00), + _ => (1900, 01, 01, 00, 00, 00, 00), + } } pub(crate) const fn ref_hour(&self) -> i64 { @@ -224,16 +222,21 @@ mod unit_test_timescale { } #[test] - fn test_initial_year() { - for (ts, y0) in vec![ - (TimeScale::TAI, 1900), - (TimeScale::UTC, 1900), - (TimeScale::GPST, 1980), - (TimeScale::BDT, 2006), - (TimeScale::GST, 1999), + fn ts_decompose() { + for (ts, decomposed) in vec![ + (TimeScale::TAI, (1900, 01, 01, 00, 00, 00, 00)), + (TimeScale::UTC, (1900, 01, 01, 00, 00, 00, 00)), + (TimeScale::GPST, (1980, 01, 06, 00, 00, 00, 00)), + (TimeScale::BDT, (2005, 01, 01, 00, 00, 00, 00)), + (TimeScale::GST, (1999, 21, 07, 00, 00, 00, 00)), ] { - let y = ts.initial_year(); - assert_eq!(y, y0, "wrong initial year of {} for {}", y, ts); + assert_eq!( + ts.decompose(), + decomposed, + "wrong {} t(=0) decomposition {:?}", + ts, + decomposed + ); } } } diff --git a/tests/epoch.rs b/tests/epoch.rs index 5a61e221..206aa268 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -371,42 +371,48 @@ fn gpst() { assert_eq!(gps.to_gpst_seconds(), qzss.to_qzsst_seconds()); assert_eq!(gps.to_gpst_nanoseconds(), qzss.to_qzsst_nanoseconds()); - let now = Epoch::from_gregorian_tai_hms(2019, 8, 24, 3, 49, 9); - assert_eq!( - Epoch::from_gpst_nanoseconds(now.to_gpst_nanoseconds().unwrap()), - now, - "To/from (recip.) GPST nanoseconds failed" - ); + let gpst = Epoch::from_gregorian(2019, 8, 24, 3, 49, 9, 0, TimeScale::GPST); + + let nanos = gpst.to_gpst_nanoseconds(); + assert!(nanos.is_ok(), "to_gpst_nanos should have been feasible"); + let nanos = nanos.unwrap(); + + let recip = Epoch::from_gpst_nanoseconds(nanos); + assert_eq!(gpst, recip, "GPST reciprocal failure"); + assert!( - (now.to_tai_seconds() - SECONDS_GPS_TAI_OFFSET - now.to_gpst_seconds()).abs() < EPSILON + (gpst.to_tai_seconds() - SECONDS_GPS_TAI_OFFSET - gpst.to_gpst_seconds()).abs() < EPSILON ); + assert!( - now.to_gpst_seconds() + SECONDS_GPS_TAI_OFFSET > now.to_utc_seconds(), + gpst.to_gpst_seconds() < gpst.to_utc_seconds(), "GPS Time is not ahead of UTC" ); - let gps_epoch = Epoch::from_tai_seconds(SECONDS_GPS_TAI_OFFSET); assert_eq!(format!("{}", GPST_REF_EPOCH), "1980-01-06T00:00:00 UTC"); assert_eq!(format!("{:x}", GPST_REF_EPOCH), "1980-01-06T00:00:19 TAI"); - assert_eq!(format!("{:o}", gps_epoch), "0"); + assert_eq!(format!("{:o}", GPST_REF_EPOCH), "0"); + assert_eq!( Epoch::from_gpst_days(0.0).to_duration_since_j1900(), - gps_epoch.duration + GPST_REF_EPOCH.duration ); assert_eq!( - gps_epoch.to_tai_seconds(), + GPST_REF_EPOCH.to_utc_seconds(), Epoch::from_gregorian_utc_at_midnight(1980, 1, 6).to_tai_seconds() ); + assert!( - gps_epoch.to_gpst_seconds().abs() < EPSILON, + GPST_REF_EPOCH.to_gpst_seconds().abs() < EPSILON, "The number of seconds from the GPS epoch was not 0: {}", - gps_epoch.to_gpst_seconds() + GPST_REF_EPOCH.to_gpst_seconds() ); + assert!( - gps_epoch.to_gpst_days().abs() < EPSILON, + GPST_REF_EPOCH.to_gpst_days().abs() < EPSILON, "The number of days from the GPS epoch was not 0: {}", - gps_epoch.to_gpst_days() + GPST_REF_EPOCH.to_gpst_days() ); let epoch = Epoch::from_gregorian_utc_at_midnight(1972, 1, 1); From e95b5196cfd4fe310362fa84c0dd1581662d6839 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 2 Sep 2023 13:31:01 +0200 Subject: [PATCH 25/37] working on tai_utc Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index cb8a4af0..77d6f8fc 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -777,7 +777,7 @@ impl Epoch { } let (ts_year, ts_day, ts_month, ts_hh, ts_mm_, ts_ss, ts_ns) = time_scale.decompose(); - let mut ts_year = ts_year as i32; + let ts_year = ts_year as i32; let years_since_ref = match year > ts_year { true => year - ts_year, @@ -1873,7 +1873,11 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in TAI pub fn to_tai_duration(&self) -> Duration { - self.duration + self.time_scale.tai_reference_epoch().duration + let mut dur = self.duration + self.time_scale.tai_reference_epoch().duration; + if self.time_scale.uses_leap_seconds() { + dur += self.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + } + dur } #[must_use] From 324c67697dd6b160108a384593dad70b4e134c70 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 2 Sep 2023 15:10:18 +0200 Subject: [PATCH 26/37] working on tai_utc Signed-off-by: Guillaume W. Bres --- src/asn1der.rs | 6 ++--- src/epoch.rs | 64 +++++++++++++++++--------------------------- src/timescale/mod.rs | 6 ++--- 3 files changed, 31 insertions(+), 45 deletions(-) diff --git a/src/asn1der.rs b/src/asn1der.rs index b35ed9cd..30f0e5f0 100644 --- a/src/asn1der.rs +++ b/src/asn1der.rs @@ -40,14 +40,14 @@ impl<'a> Decode<'a> for Duration { impl Encode for Epoch { fn encoded_len(&self) -> der::Result { let ts: u8 = self.time_scale.into(); - ts.encoded_len()? + self.to_duration().encoded_len()? + ts.encoded_len()? + self.duration.encoded_len()? } fn encode(&self, encoder: &mut dyn Writer) -> der::Result<()> { let ts: u8 = self.time_scale.into(); ts.encode(encoder)?; - self.to_duration().encode(encoder) + self.duration.encode(encoder) } } @@ -114,7 +114,7 @@ fn test_encdec() { TimeScale::QZSST => epoch.to_qzsst_duration(), }; - let e_dur = epoch.to_duration(); + let e_dur = epoch.duration; assert_eq!(e_dur, duration, "{ts:?}"); diff --git a/src/epoch.rs b/src/epoch.rs index 77d6f8fc..78c9152d 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -179,7 +179,7 @@ impl Sub for Epoch { fn sub(self, duration: Duration) -> Self { Self { - duration: self.to_duration() - duration, + duration: self.duration - duration, time_scale: self.time_scale, } } @@ -192,7 +192,7 @@ impl Add for Epoch { fn add(self, seconds: f64) -> Self { Self { - duration: self.to_duration() + seconds * Unit::Second, + duration: self.duration + seconds * Unit::Second, time_scale: self.time_scale, } } @@ -203,7 +203,7 @@ impl Add for Epoch { fn add(self, duration: Duration) -> Self { Self { - duration: self.to_duration() + duration, + duration: self.duration + duration, time_scale: self.time_scale, } } @@ -229,7 +229,7 @@ impl Sub for Epoch { #[allow(clippy::identity_op)] fn sub(self, unit: Unit) -> Self { Self { - duration: self.to_duration() - unit * 1, + duration: self.duration - unit * 1, time_scale: self.time_scale, } } @@ -241,7 +241,7 @@ impl Add for Epoch { #[allow(clippy::identity_op)] fn add(self, unit: Unit) -> Self { Self { - duration: self.to_duration() + unit * 1, + duration: self.duration + unit * 1, time_scale: self.time_scale, } } @@ -364,7 +364,8 @@ impl Epoch { */ return Self { duration: { - self.time_scale.tai_reference_epoch().to_tai_duration() + self.duration + let mut dur = self.time_scale.tai_reference_epoch() + self.duration; + dur.duration }, time_scale: TimeScale::TAI, }; @@ -1145,7 +1146,7 @@ impl Epoch { /// /// # Warning /// The time scale of this Epoch will be set to TAI! This is to ensure that no additional computations will change the duration since it's stored in TAI. - /// However, this also means that calling `to_duration()` on this Epoch will return the TAI duration and not the UT1 duration! + /// However, this also means that calling `duration` on this Epoch will return the TAI duration and not the UT1 duration! pub fn from_ut1_duration(duration: Duration, provider: Ut1Provider) -> Self { let mut e = Self::from_tai_duration(duration); // Compute the TAI to UT1 offset at this time. @@ -1815,17 +1816,6 @@ impl Epoch { Ok(format!("{}", Formatter::new(*self, fmt))) } - /// Returns this epoch with respect to the time scale this epoch was created in. - /// This is needed to correctly perform duration conversions in dynamical time scales (e.g. TDB). - /// - /// # Examples - /// 1. If an epoch was initialized as Epoch::from_..._utc(...) then the duration will be the UTC duration from J1900. - /// 2. If an epoch was initialized as Epoch::from_..._tdb(...) then the duration will be the UTC duration from J2000 because the TDB reference epoch is J2000. - #[must_use] - pub fn to_duration(&self) -> Duration { - self.duration - } - #[must_use] /// Returns this epoch with respect to the provided time scale. /// This is needed to correctly perform duration conversions in dynamical time scales (e.g. TDB). @@ -1873,11 +1863,7 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in TAI pub fn to_tai_duration(&self) -> Duration { - let mut dur = self.duration + self.time_scale.tai_reference_epoch().duration; - if self.time_scale.uses_leap_seconds() { - dur += self.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - } - dur + (self.time_scale.tai_reference_epoch() + self.duration).duration } #[must_use] @@ -2385,7 +2371,7 @@ impl Epoch { /// ); /// ``` pub fn floor(&self, duration: Duration) -> Self { - Self::from_duration(self.to_duration().floor(duration), self.time_scale) + Self::from_duration(self.duration.floor(duration), self.time_scale) } #[must_use] @@ -2409,7 +2395,7 @@ impl Epoch { /// ); /// ``` pub fn ceil(&self, duration: Duration) -> Self { - Self::from_duration(self.to_duration().ceil(duration), self.time_scale) + Self::from_duration(self.duration.ceil(duration), self.time_scale) } #[must_use] @@ -2426,7 +2412,7 @@ impl Epoch { /// ); /// ``` pub fn round(&self, duration: Duration) -> Self { - Self::from_duration(self.to_duration().round(duration), self.time_scale) + Self::from_duration(self.duration.round(duration), self.time_scale) } #[must_use] @@ -2434,7 +2420,7 @@ impl Epoch { /// and the number of nanoseconds elapsed in current week (since closest Sunday midnight). /// This is usually how GNSS receivers describe a timestamp. pub fn to_time_of_week(&self) -> (u32, u64) { - let total_nanoseconds = self.to_duration().total_nanoseconds(); + let total_nanoseconds = self.duration.total_nanoseconds(); let weeks = total_nanoseconds / NANOSECONDS_PER_DAY as i128 / Weekday::DAYS_PER_WEEK_I128; // elapsed nanoseconds in current week: // remove previously determined nb of weeks @@ -2546,7 +2532,7 @@ impl Epoch { pub fn duration_in_year(&self) -> Duration { let year = Self::compute_gregorian(self.duration, self.time_scale).0; let start_of_year = Self::from_gregorian(year, 1, 1, 0, 0, 0, 0, self.time_scale); - self.to_duration() - start_of_year.to_duration() + self.duration - start_of_year.duration } #[must_use] @@ -2566,32 +2552,32 @@ impl Epoch { /// Returns the hours of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn hours(&self) -> u64 { - self.to_duration().decompose().2 + self.duration.decompose().2 } /// Returns the minutes of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn minutes(&self) -> u64 { - self.to_duration().decompose().3 + self.duration.decompose().3 } /// Returns the seconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn seconds(&self) -> u64 { - self.to_duration().decompose().4 + self.duration.decompose().4 } /// Returns the milliseconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn milliseconds(&self) -> u64 { - self.to_duration().decompose().5 + self.duration.decompose().5 } /// Returns the microseconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn microseconds(&self) -> u64 { - self.to_duration().decompose().6 + self.duration.decompose().6 } /// Returns the nanoseconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn nanoseconds(&self) -> u64 { - self.to_duration().decompose().7 + self.duration.decompose().7 } /// Returns a copy of self where the time is set to the provided hours, minutes, seconds @@ -2599,7 +2585,7 @@ impl Epoch { /// Warning: this does _not_ set the subdivisions of second to zero. pub fn with_hms(&self, hours: u64, minutes: u64, seconds: u64) -> Self { let (sign, days, _, _, _, milliseconds, microseconds, nanoseconds) = - self.to_duration().decompose(); + self.duration.decompose(); Self::from_duration( Duration::compose( sign, @@ -2632,7 +2618,7 @@ impl Epoch { /// ``` pub fn with_hms_from(&self, other: Self) -> Self { let (sign, days, _, _, _, milliseconds, microseconds, nanoseconds) = - self.to_duration().decompose(); + self.duration.decompose(); // Shadow other with the provided other epoch but in the correct time scale. let other = other.to_time_scale(self.time_scale); Self::from_duration( @@ -2667,7 +2653,7 @@ impl Epoch { /// ``` pub fn with_time_from(&self, other: Self) -> Self { // Grab days from self - let (sign, days, _, _, _, _, _, _) = self.to_duration().decompose(); + let (sign, days, _, _, _, _, _, _) = self.duration.decompose(); // Grab everything else from other let (_, _, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) = @@ -2692,7 +2678,7 @@ impl Epoch { /// Invalid number of hours, minutes, and seconds will overflow into their higher unit. /// Warning: this will set the subdivisions of seconds to zero. pub fn with_hms_strict(&self, hours: u64, minutes: u64, seconds: u64) -> Self { - let (sign, days, _, _, _, _, _, _) = self.to_duration().decompose(); + let (sign, days, _, _, _, _, _, _) = self.duration.decompose(); Self::from_duration( Duration::compose(sign, days, hours, minutes, seconds, 0, 0, 0), self.time_scale, @@ -2714,7 +2700,7 @@ impl Epoch { /// ); /// ``` pub fn with_hms_strict_from(&self, other: Self) -> Self { - let (sign, days, _, _, _, _, _, _) = self.to_duration().decompose(); + let (sign, days, _, _, _, _, _, _) = self.duration.decompose(); let other = other.to_time_scale(self.time_scale); Self::from_duration( Duration::compose( diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index ccd70e03..bd0c263b 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -35,7 +35,7 @@ pub const J2000_REF_EPOCH: Epoch = Epoch::from_tai_duration(J2000_TO_J1900_DURAT /// |UTC - TAI| = 19 Leap Seconds on that day. pub const GPST_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { centuries: 0, - nanoseconds: 2_524_953_619_000_000_000, // XXX + nanoseconds: 2_524_953_600_000_000_000, // XXX }); pub const SECONDS_GPS_TAI_OFFSET: f64 = 2_524_953_619.0; pub const SECONDS_GPS_TAI_OFFSET_I64: i64 = 2_524_953_619; @@ -48,7 +48,7 @@ pub const QZSST_REF_EPOCH: Epoch = GPST_REF_EPOCH; /// |UTC - TAI| = XX Leap Seconds on that day. pub const GST_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { centuries: 0, - nanoseconds: 3_144_268_819_000_000_000, + nanoseconds: 3_144_268_800_000_000_000, }); pub const SECONDS_GST_TAI_OFFSET: f64 = 3_144_268_819.0; pub const SECONDS_GST_TAI_OFFSET_I64: i64 = 3_144_268_819; @@ -58,7 +58,7 @@ pub const SECONDS_GST_TAI_OFFSET_I64: i64 = 3_144_268_819; /// |UTC - TAI| = XX Leap Seconds on that day. pub const BDT_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { centuries: 1, - nanoseconds: 189_302_433_000_000_000, + nanoseconds: 189_302_400_000_000_000, }); pub const SECONDS_BDT_TAI_OFFSET: f64 = 3_345_062_433.0; pub const SECONDS_BDT_TAI_OFFSET_I64: i64 = 3_345_062_433; From 295f46f65e165245b0b2e7fb711c84419cf5b542 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 2 Sep 2023 15:43:51 +0200 Subject: [PATCH 27/37] change print behavior Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 18 ++++++++++-------- src/timescale/mod.rs | 2 +- tests/epoch.rs | 2 ++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 78c9152d..3947b761 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -777,8 +777,8 @@ impl Epoch { return Err(Errors::Carry); } - let (ts_year, ts_day, ts_month, ts_hh, ts_mm_, ts_ss, ts_ns) = time_scale.decompose(); - let ts_year = ts_year as i32; + let (ts_year, ts_month, ts_day, ts_hh, ts_mm_, ts_ss, ts_ns) = time_scale.decompose(); + println!("{:?}", time_scale.decompose()); let years_since_ref = match year > ts_year { true => year - ts_year, @@ -787,6 +787,10 @@ impl Epoch { let mut duration_wrt_ref = Unit::Day * i64::from(365 * years_since_ref); + if ts_day > 1 { + duration_wrt_ref -= i64::from(ts_day) * Unit::Day; + } + // Now add the leap days for all the years prior to the current year if year >= ts_year { for year in ts_year..year { @@ -1177,7 +1181,7 @@ impl Epoch { duration: Duration, ts: TimeScale, ) -> (i32, u8, u8, u8, u8, u8, u32) { - let (sign, days, hours, minutes, seconds, milliseconds, microseconds, nanos) = + let (sign, mut days, hours, minutes, seconds, milliseconds, microseconds, nanos) = duration.decompose(); let days_f64 = if sign < 0 { @@ -2998,22 +3002,20 @@ impl fmt::Debug for Epoch { } impl fmt::Display for Epoch { - /// The default format of an epoch is in UTC fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let ts = TimeScale::UTC; let (y, mm, dd, hh, min, s, nanos) = - Self::compute_gregorian(self.to_duration_in_time_scale(ts), ts); + Self::compute_gregorian(self.duration, self.time_scale); if nanos == 0 { write!( f, "{:04}-{:02}-{:02}T{:02}:{:02}:{:02} {}", - y, mm, dd, hh, min, s, ts + y, mm, dd, hh, min, s, self.time_scale ) } else { write!( f, "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09} {}", - y, mm, dd, hh, min, s, nanos, ts + y, mm, dd, hh, min, s, nanos, self.time_scale ) } } diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index bd0c263b..4c719d76 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -35,7 +35,7 @@ pub const J2000_REF_EPOCH: Epoch = Epoch::from_tai_duration(J2000_TO_J1900_DURAT /// |UTC - TAI| = 19 Leap Seconds on that day. pub const GPST_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { centuries: 0, - nanoseconds: 2_524_953_600_000_000_000, // XXX + nanoseconds: 2_524_953_619_000_000_000, // XXX }); pub const SECONDS_GPS_TAI_OFFSET: f64 = 2_524_953_619.0; pub const SECONDS_GPS_TAI_OFFSET_I64: i64 = 2_524_953_619; diff --git a/tests/epoch.rs b/tests/epoch.rs index 206aa268..fa10e1ed 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -346,6 +346,8 @@ fn gpst() { // Test 1sec into GPS timescale let gps_1sec = Epoch::from_gpst_seconds(1.0); + assert_eq!(gps_1sec.to_string(), "1980-01-06T00:00:01 GPST"); + assert_eq!(gps_1sec, ref_gps + 1.0 * Unit::Second); // 1sec into QZSS time scale returns the same date From 52fec8f5ef309f490dad8289c1c0530f1565f070 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 3 Sep 2023 10:17:24 +0200 Subject: [PATCH 28/37] revert to_time_scale() Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 140 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 99 insertions(+), 41 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 3947b761..16351a19 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -340,53 +340,111 @@ impl Epoch { if ts == self.time_scale { // Do nothing, just return a copy return *self; - } - // TAI is our reference case in other conversions - if ts == TimeScale::TAI { - match self.time_scale { - TimeScale::TT => unreachable!("TT"), - TimeScale::ET => unreachable!("ET"), - TimeScale::TDB => unreachable!("TDB"), + } else { + // Now we need to convert from the current time scale + // into the desired time scale. + let j1900_tai_offset = match self.time_scale { + TimeScale::TAI => self.duration, + TimeScale::TT => self.duration - TT_OFFSET_MS.milliseconds(), + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds_j2000 = self.duration.to_seconds(); + for _ in 0..5 { + seconds_j2000 += -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds_j2000 + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) + .sin(); + } + // At this point, we have a good estimate of the number of seconds + // of this epoch. Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); + // Match SPICE by changing the UTC definition. + (self.duration.to_seconds() - delta_et_tai).seconds() + J2000_TO_J1900_DURATION + } + TimeScale::TDB => { + let gamma = Self::inner_g(self.duration.to_seconds()); + let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION + } TimeScale::UTC => { - return Self { - duration: self.duration - - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second, - time_scale: ts, - }; + self.duration + self.leap_seconds(true).unwrap_or(0.0) * Unit::Second + } + TimeScale::GPST | TimeScale::GST | TimeScale::BDT | TimeScale::QZSST => { + self.duration + ts.tai_reference_epoch().to_tai_duration() + } + }; + // Convert to the desired time scale from the TAI duration + match ts { + TimeScale::TAI => Self { + duration: j1900_tai_offset, + time_scale: TimeScale::TAI, + }, + TimeScale::TT => Self { + duration: j1900_tai_offset + TT_OFFSET_MS.milliseconds(), + time_scale: TimeScale::TT, + }, + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); + for _ in 0..5 { + seconds -= -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) + .sin(); + } + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); + // Match SPICE by changing the UTC definition. + Self { + duration: j1900_tai_offset + delta_et_tai.seconds() + - J2000_TO_J1900_DURATION, + time_scale: TimeScale::ET, + } + } + TimeScale::TDB => { + // Iterate to convert find the corret value of the + let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); + let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson + for _ in 0..5 { + let next = seconds - Self::inner_g(seconds); + let new_delta = (next - seconds).abs(); + if (new_delta - delta).abs() < 1e-9 { + break; + } + seconds = next; // Loop + delta = new_delta; + } + // At this point, we have a good estimate of the number of seconds + // of this Epoch. Reverse the algorithm: + let gamma = + Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); + let delta_tdb_tai = gamma.seconds() + TT_OFFSET_MS.milliseconds(); + Self { + duration: j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION, + time_scale: TimeScale::TDB, + } } - _ => { - /* - * simple case - * self.time_scale(t0) - * |--------------------->self - * TAI(J1900) - * |------------------------------------------>target - */ - return Self { - duration: { - let mut dur = self.time_scale.tai_reference_epoch() + self.duration; - dur.duration - }, - time_scale: TimeScale::TAI, + TimeScale::UTC => { + // Assume it's TAI + let mut epoch = Self { + duration: j1900_tai_offset, + time_scale: TimeScale::UTC, }; + epoch.duration += epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + epoch } - } - } - match ts { - TimeScale::UTC => { - let mut utc = self.to_time_scale(TimeScale::TAI); - utc.time_scale = TimeScale::UTC; - utc.duration -= utc.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - utc - } - TimeScale::GPST | TimeScale::BDT | TimeScale::GST | TimeScale::QZSST => { - let tai = self.to_time_scale(TimeScale::TAI); - Self { - duration: tai.duration + ts.tai_reference_epoch().duration, + TimeScale::GPST | TimeScale::GST | TimeScale::BDT | TimeScale::QZSST => Self { + duration: j1900_tai_offset - ts.tai_reference_epoch().to_tai_duration(), time_scale: ts, - } + }, } - _ => unreachable!(), } } From 233d4496c1248d8213e49ebcd56334a851917bb4 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 3 Sep 2023 11:03:43 +0200 Subject: [PATCH 29/37] time_scale.decompose() Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 60 +++++++++++++++++++++++++------------------- src/timescale/mod.rs | 28 ++------------------- 2 files changed, 36 insertions(+), 52 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 16351a19..b292b9d2 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -259,20 +259,20 @@ impl PartialEq for Epoch { if self.time_scale == other.time_scale { self.duration == other.duration } else { - self.duration == other.to_time_scale(self.time_scale).duration - //// If one of the two time scales does not include leap seconds, - //// we always convert the time scale with leap seconds into the - //// time scale that does NOT have leap seconds. - //if self.time_scale.uses_leap_seconds() != other.time_scale.uses_leap_seconds() { - // if self.time_scale.uses_leap_seconds() { - // self.to_time_scale(other.time_scale).duration == other.duration - // } else { - // self.duration == other.to_time_scale(self.time_scale).duration - // } - //} else { - // // Otherwise it does not matter - // self.duration == other.to_time_scale(self.time_scale).duration - //} + // self.duration == other.to_time_scale(self.time_scale).duration + // If one of the two time scales does not include leap seconds, + // we always convert the time scale with leap seconds into the + // time scale that does NOT have leap seconds. + if self.time_scale.uses_leap_seconds() != other.time_scale.uses_leap_seconds() { + if self.time_scale.uses_leap_seconds() { + self.to_time_scale(other.time_scale).duration == other.duration + } else { + self.duration == other.to_time_scale(self.time_scale).duration + } + } else { + // Otherwise it does not matter + self.duration == other.to_time_scale(self.time_scale).duration + } } } } @@ -835,8 +835,8 @@ impl Epoch { return Err(Errors::Carry); } - let (ts_year, ts_month, ts_day, ts_hh, ts_mm_, ts_ss, ts_ns) = time_scale.decompose(); - println!("{:?}", time_scale.decompose()); + let (ts_sign, ts_days, ts_hours, ts_minutes, ts_seconds, ts_ms, ts_us, ts_nanos) = + time_scale.decompose(); let years_since_ref = match year > ts_year { true => year - ts_year, @@ -845,10 +845,6 @@ impl Epoch { let mut duration_wrt_ref = Unit::Day * i64::from(365 * years_since_ref); - if ts_day > 1 { - duration_wrt_ref -= i64::from(ts_day) * Unit::Day; - } - // Now add the leap days for all the years prior to the current year if year >= ts_year { for year in ts_year..year { @@ -1239,8 +1235,24 @@ impl Epoch { duration: Duration, ts: TimeScale, ) -> (i32, u8, u8, u8, u8, u8, u32) { - let (sign, mut days, hours, minutes, seconds, milliseconds, microseconds, nanos) = - duration.decompose(); + let ( + sign, + mut days, + mut hours, + mut minutes, + mut seconds, + mut milliseconds, + mut microseconds, + mut nanos, + ) = duration.decompose(); + + let (ts_sign, ts_days, ts_hours, ts_minutes, ts_seconds, ts_ms, ts_us, ts_nanos) = + ts.decompose(); + + // days += ts_days as u64; + // hours += ts_hours; + // seconds += ts_seconds; + // nanos += ts_nanos; let days_f64 = if sign < 0 { -(days as f64) @@ -1250,10 +1262,6 @@ impl Epoch { let (mut year, mut days_in_year) = div_rem_f64(days_f64, DAYS_PER_YEAR_NLD); - let (ts_year, ts_month, ts_day, ts_hh, ts_mm, ts_ss, ts_ns) = ts.decompose(); - let ts_year = ts_year as i32; - year += ts_year; // NB: this assumes time scales all started after J1900 - // Base calculation was on 365 days, so we need to remove one day in seconds per leap year // between 1900 and `year` if year >= ts_year { diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index 4c719d76..97e45df8 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -132,13 +132,8 @@ impl TimeScale { } } - pub(crate) fn decompose(&self) -> (i32, u8, u8, u8, u8, u8, u32) { - match self { - Self::GPST => (1980, 01, 06, 00, 00, 00, 00), - Self::BDT => (2005, 01, 01, 00, 00, 00, 00), - Self::GST => (1999, 21, 07, 00, 00, 00, 00), - _ => (1900, 01, 01, 00, 00, 00, 00), - } + pub(crate) const fn decompose(&self) -> (i8, u64, u64, u64, u64, u64, u64, u64) { + self.tai_reference_epoch().duration.decompose() } pub(crate) const fn ref_hour(&self) -> i64 { @@ -220,23 +215,4 @@ mod unit_test_timescale { } } } - - #[test] - fn ts_decompose() { - for (ts, decomposed) in vec![ - (TimeScale::TAI, (1900, 01, 01, 00, 00, 00, 00)), - (TimeScale::UTC, (1900, 01, 01, 00, 00, 00, 00)), - (TimeScale::GPST, (1980, 01, 06, 00, 00, 00, 00)), - (TimeScale::BDT, (2005, 01, 01, 00, 00, 00, 00)), - (TimeScale::GST, (1999, 21, 07, 00, 00, 00, 00)), - ] { - assert_eq!( - ts.decompose(), - decomposed, - "wrong {} t(=0) decomposition {:?}", - ts, - decomposed - ); - } - } } From 75350b117a964fbbadefacc9313c28d9355f25fa Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 3 Sep 2023 11:16:26 +0200 Subject: [PATCH 30/37] revert from_gregorian and compute_gregorian Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 90 ++++++++++++++++++++++++++------------------ src/timescale/mod.rs | 12 +++++- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index b292b9d2..fc33978c 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -835,54 +835,66 @@ impl Epoch { return Err(Errors::Carry); } - let (ts_sign, ts_days, ts_hours, ts_minutes, ts_seconds, ts_ms, ts_us, ts_nanos) = - time_scale.decompose(); - - let years_since_ref = match year > ts_year { - true => year - ts_year, - false => ts_year - year, - }; - - let mut duration_wrt_ref = Unit::Day * i64::from(365 * years_since_ref); - - // Now add the leap days for all the years prior to the current year - if year >= ts_year { - for year in ts_year..year { - if is_leap_year(year) { - duration_wrt_ref += Unit::Day; - } - } + let years_since_1900 = year + time_scale.ref_year() as i32; + let mut duration_wrt_1900 = Unit::Day * i64::from(365 * years_since_1900); + + // count leap years + if years_since_1900 > 0 { + // we don't count the leap year in 1904, since jan 1904 hasn't had the leap yet, + // so we push it back to 1905, same for all other leap years + let years_after_1900 = years_since_1900 - 1; + duration_wrt_1900 += Unit::Day * i64::from(years_after_1900 / 4); + duration_wrt_1900 -= Unit::Day * i64::from(years_after_1900 / 100); + // every 400 years we correct our correction. The first one after 1900 is 2000 (years_since_1900 = 100) + // so we add 300 to correct the offset + duration_wrt_1900 += Unit::Day * i64::from((years_after_1900 + 300) / 400); } else { - // Remove days - for year in year..ts_year { - if is_leap_year(year) { - duration_wrt_ref -= Unit::Day; - } - } - } + // we don't need to fix the offset, since jan 1896 has had the leap, when counting back from 1900 + duration_wrt_1900 += Unit::Day * i64::from(years_since_1900 / 4); + duration_wrt_1900 -= Unit::Day * i64::from(years_since_1900 / 100); + // every 400 years we correct our correction. The first one before 1900 is 1600 (years_since_1900 = -300) + // so we subtract 100 to correct the offset + duration_wrt_1900 += Unit::Day * i64::from((years_since_1900 - 100) / 400); + }; // Add the seconds for the months prior to the current month - duration_wrt_ref += Unit::Day * i64::from(CUMULATIVE_DAYS_FOR_MONTH[(month - 1) as usize]); + duration_wrt_1900 += Unit::Day * i64::from(CUMULATIVE_DAYS_FOR_MONTH[(month - 1) as usize]); if is_leap_year(year) && month > 2 { // NOTE: If on 29th of February, then the day is not finished yet, and therefore // the extra seconds are added below as per a normal day. - duration_wrt_ref += Unit::Day; + duration_wrt_1900 += Unit::Day; } - duration_wrt_ref += Unit::Day * i64::from(day - 1) + duration_wrt_1900 += Unit::Day * i64::from(day - 1) + Unit::Hour * i64::from(hour) + Unit::Minute * i64::from(minute) + Unit::Second * i64::from(second) + Unit::Nanosecond * i64::from(nanos); if second == 60 { // Herein lies the whole ambiguity of leap seconds. Two different UTC dates exist at the - // same number of second after J1900.0. - duration_wrt_ref -= Unit::Second; + // same number of second afters J1900.0. + duration_wrt_1900 -= Unit::Second; } - Ok(Self { - duration: duration_wrt_ref, - time_scale, + // NOTE: For ET and TDB, we make sure to offset the duration back to J2000 since those functions expect a J2000 input. + Ok(match time_scale { + TimeScale::TAI => Self::from_tai_duration(duration_wrt_1900), + TimeScale::TT => Self::from_tt_duration(duration_wrt_1900), + TimeScale::ET => Self::from_et_duration(duration_wrt_1900 - J2000_TO_J1900_DURATION), + TimeScale::TDB => Self::from_tdb_duration(duration_wrt_1900 - J2000_TO_J1900_DURATION), + TimeScale::UTC => Self::from_utc_duration(duration_wrt_1900), + TimeScale::GPST => { + Self::from_gpst_duration(duration_wrt_1900 - GPST_REF_EPOCH.to_tai_duration()) + } + TimeScale::QZSST => { + Self::from_qzsst_duration(duration_wrt_1900 - GPST_REF_EPOCH.to_tai_duration()) + } + TimeScale::GST => { + Self::from_gst_duration(duration_wrt_1900 - GST_REF_EPOCH.to_tai_duration()) + } + TimeScale::BDT => { + Self::from_bdt_duration(duration_wrt_1900 - BDT_REF_EPOCH.to_tai_duration()) + } }) } @@ -1249,10 +1261,16 @@ impl Epoch { let (ts_sign, ts_days, ts_hours, ts_minutes, ts_seconds, ts_ms, ts_us, ts_nanos) = ts.decompose(); - // days += ts_days as u64; - // hours += ts_hours; - // seconds += ts_seconds; - // nanos += ts_nanos; + // apply the time scale reference offset + days += ts_days; + hours += ts_hours; + minutes += ts_minutes; + seconds += ts_seconds; + milliseconds += ts_ms; + microseconds += ts_us; + nanos += ts_nanos; + + let ts_year = ts.ref_year() as i32; let days_f64 = if sign < 0 { -(days as f64) diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index 97e45df8..52df1c83 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -132,10 +132,20 @@ impl TimeScale { } } - pub(crate) const fn decompose(&self) -> (i8, u64, u64, u64, u64, u64, u64, u64) { + pub(crate) fn decompose(&self) -> (i8, u64, u64, u64, u64, u64, u64, u64) { self.tai_reference_epoch().duration.decompose() } + pub(crate) const fn ref_year(&self) -> u32 { + match self { + TimeScale::TT | TimeScale::ET | TimeScale::TDB => 2000, + TimeScale::UTC | TimeScale::TAI => 1900, + TimeScale::GPST | TimeScale::QZSST => 1980, + TimeScale::BDT => 2006, + TimeScale::GST => 1997, + } + } + pub(crate) const fn ref_hour(&self) -> i64 { match self { TimeScale::ET | TimeScale::TDB => 0, From 88e544dfd44cd330d11b06c1db86bd7ab8258ba3 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 3 Sep 2023 11:31:37 +0200 Subject: [PATCH 31/37] revert to previously almost correct gregorian Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 100 ++++++++++++++++++------------------------- src/timescale/mod.rs | 2 +- 2 files changed, 42 insertions(+), 60 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index fc33978c..0f200aa1 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -835,37 +835,35 @@ impl Epoch { return Err(Errors::Carry); } - let years_since_1900 = year + time_scale.ref_year() as i32; - let mut duration_wrt_1900 = Unit::Day * i64::from(365 * years_since_1900); - - // count leap years - if years_since_1900 > 0 { - // we don't count the leap year in 1904, since jan 1904 hasn't had the leap yet, - // so we push it back to 1905, same for all other leap years - let years_after_1900 = years_since_1900 - 1; - duration_wrt_1900 += Unit::Day * i64::from(years_after_1900 / 4); - duration_wrt_1900 -= Unit::Day * i64::from(years_after_1900 / 100); - // every 400 years we correct our correction. The first one after 1900 is 2000 (years_since_1900 = 100) - // so we add 300 to correct the offset - duration_wrt_1900 += Unit::Day * i64::from((years_after_1900 + 300) / 400); - } else { - // we don't need to fix the offset, since jan 1896 has had the leap, when counting back from 1900 - duration_wrt_1900 += Unit::Day * i64::from(years_since_1900 / 4); - duration_wrt_1900 -= Unit::Day * i64::from(years_since_1900 / 100); - // every 400 years we correct our correction. The first one before 1900 is 1600 (years_since_1900 = -300) - // so we subtract 100 to correct the offset - duration_wrt_1900 += Unit::Day * i64::from((years_since_1900 - 100) / 400); - }; + let years_since_ref = year - time_scale.ref_year(); + let mut duration_wrt_ref = Unit::Day * i64::from(365 * years_since_ref); + let ref_year = time_scale.ref_year(); + + // Now add the leap days for all years prior the current year + if year >= ref_year { + for year in ref_year..year { + if is_leap_year(year) { + duration_wrt_ref += Unit::Day; + } + } + } + + // Remove days + for year in year..ref_year { + if is_leap_year(year) { + duration_wrt_ref -= Unit::Day; + } + } // Add the seconds for the months prior to the current month - duration_wrt_1900 += Unit::Day * i64::from(CUMULATIVE_DAYS_FOR_MONTH[(month - 1) as usize]); + duration_wrt_ref += Unit::Day * i64::from(CUMULATIVE_DAYS_FOR_MONTH[(month - 1) as usize]); if is_leap_year(year) && month > 2 { // NOTE: If on 29th of February, then the day is not finished yet, and therefore // the extra seconds are added below as per a normal day. - duration_wrt_1900 += Unit::Day; + duration_wrt_ref += Unit::Day; } - duration_wrt_1900 += Unit::Day * i64::from(day - 1) + duration_wrt_ref += Unit::Day * i64::from(day - 1) + Unit::Hour * i64::from(hour) + Unit::Minute * i64::from(minute) + Unit::Second * i64::from(second) @@ -873,28 +871,12 @@ impl Epoch { if second == 60 { // Herein lies the whole ambiguity of leap seconds. Two different UTC dates exist at the // same number of second afters J1900.0. - duration_wrt_1900 -= Unit::Second; + duration_wrt_ref -= Unit::Second; } - // NOTE: For ET and TDB, we make sure to offset the duration back to J2000 since those functions expect a J2000 input. - Ok(match time_scale { - TimeScale::TAI => Self::from_tai_duration(duration_wrt_1900), - TimeScale::TT => Self::from_tt_duration(duration_wrt_1900), - TimeScale::ET => Self::from_et_duration(duration_wrt_1900 - J2000_TO_J1900_DURATION), - TimeScale::TDB => Self::from_tdb_duration(duration_wrt_1900 - J2000_TO_J1900_DURATION), - TimeScale::UTC => Self::from_utc_duration(duration_wrt_1900), - TimeScale::GPST => { - Self::from_gpst_duration(duration_wrt_1900 - GPST_REF_EPOCH.to_tai_duration()) - } - TimeScale::QZSST => { - Self::from_qzsst_duration(duration_wrt_1900 - GPST_REF_EPOCH.to_tai_duration()) - } - TimeScale::GST => { - Self::from_gst_duration(duration_wrt_1900 - GST_REF_EPOCH.to_tai_duration()) - } - TimeScale::BDT => { - Self::from_bdt_duration(duration_wrt_1900 - BDT_REF_EPOCH.to_tai_duration()) - } + Ok(Self { + duration: duration_wrt_ref, + time_scale, }) } @@ -1258,19 +1240,17 @@ impl Epoch { mut nanos, ) = duration.decompose(); - let (ts_sign, ts_days, ts_hours, ts_minutes, ts_seconds, ts_ms, ts_us, ts_nanos) = - ts.decompose(); - - // apply the time scale reference offset - days += ts_days; - hours += ts_hours; - minutes += ts_minutes; - seconds += ts_seconds; - milliseconds += ts_ms; - microseconds += ts_us; - nanos += ts_nanos; + // let (ts_sign, ts_days, ts_hours, ts_minutes, ts_seconds, ts_ms, ts_us, ts_nanos) = + // ts.decompose(); - let ts_year = ts.ref_year() as i32; + // // apply the time scale reference offset + // days += ts_days; + // hours += ts_hours; + // minutes += ts_minutes; + // seconds += ts_seconds; + // milliseconds += ts_ms; + // microseconds += ts_us; + // nanos += ts_nanos; let days_f64 = if sign < 0 { -(days as f64) @@ -1278,18 +1258,20 @@ impl Epoch { days as f64 }; + let ref_year = ts.ref_year(); let (mut year, mut days_in_year) = div_rem_f64(days_f64, DAYS_PER_YEAR_NLD); + year += ref_year; // Base calculation was on 365 days, so we need to remove one day in seconds per leap year // between 1900 and `year` - if year >= ts_year { - for year in ts_year..year { + if year >= ref_year { + for year in ref_year..year { if is_leap_year(year) { days_in_year -= 1.0; } } } else { - for year in year..ts_year { + for year in year..ref_year { if is_leap_year(year) { days_in_year += 1.0; } diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index 52df1c83..17058941 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -136,7 +136,7 @@ impl TimeScale { self.tai_reference_epoch().duration.decompose() } - pub(crate) const fn ref_year(&self) -> u32 { + pub(crate) const fn ref_year(&self) -> i32 { match self { TimeScale::TT | TimeScale::ET | TimeScale::TDB => 2000, TimeScale::UTC | TimeScale::TAI => 1900, From 49c2a2389bc6c444b4416a4f523a5337d0ac2606 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Mon, 18 Sep 2023 09:37:02 +0200 Subject: [PATCH 32/37] Modify Epoch.debug() behavior {:?} for an Epoch now shows the duration in the associated timescale. Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 0f200aa1..ac752f6a 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -3047,23 +3047,9 @@ impl FromStr for Epoch { } impl fmt::Debug for Epoch { - /// Print this epoch in Gregorian in the time scale used at initialization + /// Prints Self as a [Duration] within the associated [TimeScale] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let (y, mm, dd, hh, min, s, nanos) = - Self::compute_gregorian(self.duration, self.time_scale); - if nanos == 0 { - write!( - f, - "{:04}-{:02}-{:02}T{:02}:{:02}:{:02} {}", - y, mm, dd, hh, min, s, self.time_scale - ) - } else { - write!( - f, - "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09} {}", - y, mm, dd, hh, min, s, nanos, self.time_scale - ) - } + write!(f, "{}({})", self.duration, self.time_scale,) } } From f47d416a59a28b739776a771c8aa0371224233b7 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 29 Sep 2023 17:49:05 +0200 Subject: [PATCH 33/37] modify PartialEq behavior Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index ac752f6a..5870c391 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -256,24 +256,8 @@ impl AddAssign for Epoch { /// Equality only checks the duration since J1900 match in TAI, because this is how all of the epochs are referenced. impl PartialEq for Epoch { fn eq(&self, other: &Self) -> bool { - if self.time_scale == other.time_scale { - self.duration == other.duration - } else { - // self.duration == other.to_time_scale(self.time_scale).duration - // If one of the two time scales does not include leap seconds, - // we always convert the time scale with leap seconds into the - // time scale that does NOT have leap seconds. - if self.time_scale.uses_leap_seconds() != other.time_scale.uses_leap_seconds() { - if self.time_scale.uses_leap_seconds() { - self.to_time_scale(other.time_scale).duration == other.duration - } else { - self.duration == other.to_time_scale(self.time_scale).duration - } - } else { - // Otherwise it does not matter - self.duration == other.to_time_scale(self.time_scale).duration - } - } + self.time_scale == other.time_scale + && self.duration == other.duration } } From 97ad926d2eea68d280be31e34d553df57b49d953 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 29 Sep 2023 19:23:20 +0200 Subject: [PATCH 34/37] reduce indentation Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 194 +++++++++++++++++++++++++-------------------------- 1 file changed, 94 insertions(+), 100 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 5870c391..092d3e96 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -256,8 +256,7 @@ impl AddAssign for Epoch { /// Equality only checks the duration since J1900 match in TAI, because this is how all of the epochs are referenced. impl PartialEq for Epoch { fn eq(&self, other: &Self) -> bool { - self.time_scale == other.time_scale - && self.duration == other.duration + self.time_scale == other.time_scale && self.duration == other.duration } } @@ -324,111 +323,106 @@ impl Epoch { if ts == self.time_scale { // Do nothing, just return a copy return *self; - } else { - // Now we need to convert from the current time scale - // into the desired time scale. - let j1900_tai_offset = match self.time_scale { - TimeScale::TAI => self.duration, - TimeScale::TT => self.duration - TT_OFFSET_MS.milliseconds(), - TimeScale::ET => { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds_j2000 = self.duration.to_seconds(); - for _ in 0..5 { - seconds_j2000 += -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds_j2000 - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) - .sin(); - } - // At this point, we have a good estimate of the number of seconds - // of this epoch. Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); - // Match SPICE by changing the UTC definition. - (self.duration.to_seconds() - delta_et_tai).seconds() + J2000_TO_J1900_DURATION - } - TimeScale::TDB => { - let gamma = Self::inner_g(self.duration.to_seconds()); - let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; - self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION + } + // Now we need to convert from the current time scale + // into the desired time scale. + let j1900_tai_offset = match self.time_scale { + TimeScale::TAI => self.duration, + TimeScale::TT => self.duration - TT_OFFSET_MS.milliseconds(), + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds_j2000 = self.duration.to_seconds(); + for _ in 0..5 { + seconds_j2000 += -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds_j2000 + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds_j2000).sin()) + .sin(); } - TimeScale::UTC => { - self.duration + self.leap_seconds(true).unwrap_or(0.0) * Unit::Second + // At this point, we have a good estimate of the number of seconds + // of this epoch. Reverse the algorithm: + let delta_et_tai = Self::delta_et_tai( + seconds_j2000 - (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), + ); + // Match SPICE by changing the UTC definition. + (self.duration.to_seconds() - delta_et_tai).seconds() + J2000_TO_J1900_DURATION + } + TimeScale::TDB => { + let gamma = Self::inner_g(self.duration.to_seconds()); + let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + self.duration - delta_tdb_tai + J2000_TO_J1900_DURATION + } + TimeScale::UTC => self.duration + self.leap_seconds(true).unwrap_or(0.0) * Unit::Second, + TimeScale::GPST | TimeScale::GST | TimeScale::BDT | TimeScale::QZSST => { + self.duration + ts.tai_reference_epoch().to_tai_duration() + } + }; + // Convert to the desired time scale from the TAI duration + match ts { + TimeScale::TAI => Self { + duration: j1900_tai_offset, + time_scale: TimeScale::TAI, + }, + TimeScale::TT => Self { + duration: j1900_tai_offset + TT_OFFSET_MS.milliseconds(), + time_scale: TimeScale::TT, + }, + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the + let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); + for _ in 0..5 { + seconds -= -NAIF_K + * (NAIF_M0 + + NAIF_M1 * seconds + + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) + .sin(); } - TimeScale::GPST | TimeScale::GST | TimeScale::BDT | TimeScale::QZSST => { - self.duration + ts.tai_reference_epoch().to_tai_duration() + // At this point, we have a good estimate of the number of seconds of this epoch. + // Reverse the algorithm: + let delta_et_tai = + Self::delta_et_tai(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); + // Match SPICE by changing the UTC definition. + Self { + duration: j1900_tai_offset + delta_et_tai.seconds() - J2000_TO_J1900_DURATION, + time_scale: TimeScale::ET, } - }; - // Convert to the desired time scale from the TAI duration - match ts { - TimeScale::TAI => Self { - duration: j1900_tai_offset, - time_scale: TimeScale::TAI, - }, - TimeScale::TT => Self { - duration: j1900_tai_offset + TT_OFFSET_MS.milliseconds(), - time_scale: TimeScale::TT, - }, - TimeScale::ET => { - // Run a Newton Raphston to convert find the correct value of the - let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); - for _ in 0..5 { - seconds -= -NAIF_K - * (NAIF_M0 - + NAIF_M1 * seconds - + NAIF_EB * (NAIF_M0 + NAIF_M1 * seconds).sin()) - .sin(); - } - // At this point, we have a good estimate of the number of seconds of this epoch. - // Reverse the algorithm: - let delta_et_tai = Self::delta_et_tai( - seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds(), - ); - // Match SPICE by changing the UTC definition. - Self { - duration: j1900_tai_offset + delta_et_tai.seconds() - - J2000_TO_J1900_DURATION, - time_scale: TimeScale::ET, - } - } - TimeScale::TDB => { - // Iterate to convert find the corret value of the - let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); - let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson - for _ in 0..5 { - let next = seconds - Self::inner_g(seconds); - let new_delta = (next - seconds).abs(); - if (new_delta - delta).abs() < 1e-9 { - break; - } - seconds = next; // Loop - delta = new_delta; - } - // At this point, we have a good estimate of the number of seconds - // of this Epoch. Reverse the algorithm: - let gamma = - Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); - let delta_tdb_tai = gamma.seconds() + TT_OFFSET_MS.milliseconds(); - Self { - duration: j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION, - time_scale: TimeScale::TDB, + } + TimeScale::TDB => { + // Iterate to convert find the corret value of the + let mut seconds = (j1900_tai_offset - J2000_TO_J1900_DURATION).to_seconds(); + let mut delta = 1e8; // Arbitrary large number, greater than first step of Newton Raphson + for _ in 0..5 { + let next = seconds - Self::inner_g(seconds); + let new_delta = (next - seconds).abs(); + if (new_delta - delta).abs() < 1e-9 { + break; } + seconds = next; // Loop + delta = new_delta; } - TimeScale::UTC => { - // Assume it's TAI - let mut epoch = Self { - duration: j1900_tai_offset, - time_scale: TimeScale::UTC, - }; - epoch.duration += epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - epoch + // At this point, we have a good estimate of the number of seconds + // of this Epoch. Reverse the algorithm: + let gamma = + Self::inner_g(seconds + (TT_OFFSET_MS * Unit::Millisecond).to_seconds()); + let delta_tdb_tai = gamma.seconds() + TT_OFFSET_MS.milliseconds(); + Self { + duration: j1900_tai_offset + delta_tdb_tai - J2000_TO_J1900_DURATION, + time_scale: TimeScale::TDB, } - TimeScale::GPST | TimeScale::GST | TimeScale::BDT | TimeScale::QZSST => Self { - duration: j1900_tai_offset - ts.tai_reference_epoch().to_tai_duration(), - time_scale: ts, - }, } + TimeScale::UTC => { + // Assume it's TAI + let mut epoch = Self { + duration: j1900_tai_offset, + time_scale: TimeScale::UTC, + }; + epoch.duration += epoch.leap_seconds(true).unwrap_or(0.0) * Unit::Second; + epoch + } + TimeScale::GPST | TimeScale::GST | TimeScale::BDT | TimeScale::QZSST => Self { + duration: j1900_tai_offset - ts.tai_reference_epoch().to_tai_duration(), + time_scale: ts, + }, } } From 451c29a3451806c2891089284079e575f5ad632a Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 29 Sep 2023 19:29:35 +0200 Subject: [PATCH 35/37] ... Signed-off-by: Guillaume W. Bres --- tests/epoch.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/epoch.rs b/tests/epoch.rs index fa10e1ed..864955f5 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1305,20 +1305,20 @@ fn test_timescale_recip() { assert_eq!(utc_epoch, from_dur); } - // RFC3339 test - #[cfg(feature = "std")] - { - use core::str::FromStr; - let to_rfc = utc_epoch.to_rfc3339(); - let from_rfc = Epoch::from_str(&to_rfc).unwrap(); - - println!( - "{} for {utc_epoch}\tto = {to_rfc}\tfrom={from_rfc}", - from_rfc - utc_epoch - ); - - assert_eq!(from_rfc, utc_epoch); - } + //// RFC3339 test + //#[cfg(feature = "std")] + //{ + // use core::str::FromStr; + // let to_rfc = utc_epoch.to_rfc3339(); + // let from_rfc = Epoch::from_str(&to_rfc).unwrap(); + + // println!( + // "{} for {utc_epoch}\tto = {to_rfc}\tfrom={from_rfc}", + // from_rfc - utc_epoch + // ); + + // assert_eq!(from_rfc, utc_epoch); + //} } }; From 7f6441e6734b4d76b9b3de0d6a4a35fee1232135 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 29 Sep 2023 19:30:44 +0200 Subject: [PATCH 36/37] ... Signed-off-by: Guillaume W. Bres --- tests/epoch.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/epoch.rs b/tests/epoch.rs index 864955f5..aa0cdf17 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1284,9 +1284,9 @@ fn test_timescale_recip() { // Test that we can convert this epoch into another time scale and re-initialize it correctly from that value. for ts in &[ TimeScale::TAI, - //TimeScale::ET, - //TimeScale::TDB, - //TimeScale::TT, + TimeScale::ET, + TimeScale::TDB, + TimeScale::TT, TimeScale::UTC, ] { let converted = utc_epoch.to_duration_in_time_scale(*ts); @@ -1305,20 +1305,20 @@ fn test_timescale_recip() { assert_eq!(utc_epoch, from_dur); } - //// RFC3339 test - //#[cfg(feature = "std")] - //{ - // use core::str::FromStr; - // let to_rfc = utc_epoch.to_rfc3339(); - // let from_rfc = Epoch::from_str(&to_rfc).unwrap(); + // RFC3339 test + #[cfg(feature = "std")] + { + use core::str::FromStr; + let to_rfc = utc_epoch.to_rfc3339(); + let from_rfc = Epoch::from_str(&to_rfc).unwrap(); - // println!( - // "{} for {utc_epoch}\tto = {to_rfc}\tfrom={from_rfc}", - // from_rfc - utc_epoch - // ); + println!( + "{} for {utc_epoch}\tto = {to_rfc}\tfrom={from_rfc}", + from_rfc - utc_epoch + ); - // assert_eq!(from_rfc, utc_epoch); - //} + assert_eq!(from_rfc, utc_epoch); + } } }; From 729160d74eaeae88d9b36ab9f7e0f7b85b998e94 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 30 Sep 2023 11:25:42 +0200 Subject: [PATCH 37/37] fix to_bdt_duration Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/epoch.rs b/src/epoch.rs index 092d3e96..b3cf3639 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -2173,7 +2173,7 @@ impl Epoch { #[must_use] /// Returns `Duration` past BDT (BeiDou) time Epoch. pub fn to_bdt_duration(&self) -> Duration { - self.to_tai_duration() - BDT_REF_EPOCH.to_tai_duration() + self.to_time_scale(TimeScale::BDT).duration } #[must_use]