diff --git a/.gitignore b/.gitignore index 6eb156e4..bfa72501 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ **/*.rs.bk Cargo.lock .env -dist/ \ No newline at end of file +dist/ +.venv +src/epoch.rs +.vscode/settings.json diff --git a/Cargo.toml b/Cargo.toml index 382c5f05..8eb9fc9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hifitime" -version = "3.8.2" +version = "4.0.0-dev" authors = ["Christopher Rabotin "] description = "Ultra-precise date and time handling in Rust for scientific applications with leap second support" homepage = "https://nyxspace.com/" 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 0e3e7731..30f0e5f0 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. @@ -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:?}"); @@ -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/deprecated.rs b/src/deprecated.rs deleted file mode 100644 index 4b6e3685..00000000 --- a/src/deprecated.rs +++ /dev/null @@ -1,613 +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/duration.rs b/src/duration.rs deleted file mode 100644 index d733ac5f..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) * 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) * 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..ccacc830 --- /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) * 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..d659aabf --- /dev/null +++ b/src/duration/mod.rs @@ -0,0 +1,746 @@ +/* +* 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::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 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 + /// + /// # 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) + } + } +} + +#[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); +} + +#[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/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 3922910a..e57f239d 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. @@ -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) { @@ -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.duration, self.epoch.time_scale); // And format. for (i, maybe_item) in self .format 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 b86020f4..b3cf3639 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. @@ -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_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_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; @@ -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")] @@ -123,22 +123,48 @@ 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)] #[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 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, } +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 + 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; fn sub(self, other: Self) -> Duration { - self.duration_since_j1900_tai - other.duration_since_j1900_tai + self.duration - other.to_time_scale(self.time_scale).duration } } @@ -152,7 +178,10 @@ impl Sub for Epoch { type Output = Self; fn sub(self, duration: Duration) -> Self { - self.set(self.to_duration() - duration) + Self { + duration: self.duration - duration, + time_scale: self.time_scale, + } } } @@ -162,7 +191,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.duration + seconds * Unit::Second, + time_scale: self.time_scale, + } } } @@ -170,7 +202,10 @@ impl Add for Epoch { type Output = Self; fn add(self, duration: Duration) -> Self { - self.set(self.to_duration() + duration) + Self { + duration: self.duration + duration, + time_scale: self.time_scale, + } } } @@ -193,7 +228,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.duration - unit * 1, + time_scale: self.time_scale, + } } } @@ -202,7 +240,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.duration + unit * 1, + time_scale: self.time_scale, + } } } @@ -215,29 +256,23 @@ 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 - } -} - -impl Hash for Epoch { - fn hash(&self, hasher: &mut H) { - self.duration_since_j1900_tai.hash(hasher); + self.time_scale == other.time_scale && self.duration == other.duration } } 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.to_time_scale(self.time_scale).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.to_time_scale(self.time_scale).duration) } } @@ -254,7 +289,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.to_tai_duration().to_seconds() >= leap_second.timestamp_tai_s && (!iers_only || leap_second.announced_by_iers) { return Some(leap_second.delta_at); @@ -263,19 +298,131 @@ impl Epoch { None } - /// Makes a copy of self and sets the duration and time scale appropriately given the new duration + /// 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(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), + pub const 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 + /// + /// 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, just return a copy + return *self; + } + // 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 => 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, + } + } + 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, + }, } } @@ -283,7 +430,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, time_scale: TimeScale::TAI, } } @@ -317,14 +464,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 +482,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] @@ -385,7 +516,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 @@ -425,7 +556,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 @@ -465,10 +596,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 +619,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 +637,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] @@ -566,14 +664,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] @@ -581,27 +679,21 @@ 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] /// 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] @@ -667,13 +759,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] @@ -727,37 +813,35 @@ 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); - } 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) @@ -765,29 +849,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()) - } - // 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()) - } + Ok(Self { + duration: duration_wrt_ref, + time_scale, }) } @@ -845,15 +912,16 @@ 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_since_j1900_tai += if_tai.leap_seconds(true).unwrap_or(0.0) * Unit::Second; - if_tai.time_scale = TimeScale::UTC; - Ok(if_tai) + Self::maybe_from_gregorian( + year, + month, + day, + hour, + minute, + second, + nanos, + TimeScale::UTC, + ) } #[must_use] @@ -1108,13 +1176,13 @@ 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. // 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 } @@ -1135,9 +1203,32 @@ 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) { - let (sign, days, hours, minutes, seconds, milliseconds, microseconds, nanos) = - duration_j1900.decompose(); + pub(crate) fn compute_gregorian( + duration: Duration, + ts: TimeScale, + ) -> (i32, u8, u8, u8, u8, u8, u32) { + 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(); + + // // 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) @@ -1145,20 +1236,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); - // TAI is defined at 1900, so a negative time is before 1900 and positive is after 1900. - year += 1900; + 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 >= 1900 { - for year in 1900..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..1900 { + for year in year..ref_year { if is_leap_year(year) { days_in_year += 1.0; } @@ -1773,32 +1864,11 @@ 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.to_duration_in_time_scale(self.time_scale) - } - #[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_since_j1900_tai, - 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. @@ -1815,68 +1885,45 @@ 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_since_j1900_tai, - 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 { - self.duration_since_j1900_tai.to_seconds() + self.to_tai_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 + pub fn to_tai_duration(&self) -> Duration { + (self.time_scale.tai_reference_epoch() + self.duration).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.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_since_j1900_tai.to_parts() + pub fn to_tai_parts(&self) -> (i16, u64) { + self.to_tai_duration().to_parts() } #[must_use] @@ -1894,14 +1941,13 @@ 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_since_j1900_tai - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second + self.to_time_scale(TimeScale::UTC).duration } #[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] @@ -1926,7 +1972,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.to_tai_duration() + Unit::Day * J1900_OFFSET).to_unit(unit) } #[must_use] @@ -1964,7 +2010,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.to_tai_duration() + Unit::Day * J1900_OFFSET + Unit::Day * MJD_OFFSET } #[must_use] @@ -2000,7 +2046,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.to_time_scale(TimeScale::TT).duration } #[must_use] @@ -2052,7 +2098,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.to_time_scale(TimeScale::GPST).duration } /// Returns nanoseconds past GPS Time Epoch, defined as UTC midnight of January 5th to 6th 1980 (cf. ). @@ -2076,8 +2122,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_since_j1900_tai - 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. ). @@ -2101,7 +2146,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.to_time_scale(TimeScale::GST).duration } /// Returns nanoseconds past GST (Galileo) Time Epoch, starting on August 21st 1999 Midnight UT @@ -2128,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] @@ -2199,21 +2244,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(); - 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_since_j1900_tai + delta_et_tai * Unit::Second - J2000_TO_J1900_DURATION + self.to_time_scale(TimeScale::ET).duration } #[must_use] @@ -2232,25 +2263,7 @@ 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_since_j1900_tai - 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_since_j1900_tai + delta_tdb_tai - J2000_TO_J1900_DURATION + self.to_time_scale(TimeScale::TDB).duration } #[must_use] @@ -2342,7 +2355,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] @@ -2362,7 +2376,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")] @@ -2370,7 +2385,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.to_tai_duration() - self.ut1_offset(provider).unwrap_or(Duration::ZERO) } #[cfg(feature = "ut1")] @@ -2379,7 +2394,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 } @@ -2404,7 +2419,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] @@ -2428,7 +2443,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] @@ -2445,15 +2460,7 @@ impl Epoch { /// ); /// ``` pub fn round(&self, duration: Duration) -> Self { - 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 in_time_scale(&self, new_time_scale: TimeScale) -> Self { - let mut me = *self; - me.time_scale = new_time_scale; - me + Self::from_duration(self.duration.round(duration), self.time_scale) } #[must_use] @@ -2461,7 +2468,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 @@ -2571,9 +2578,9 @@ 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() + self.duration - start_of_year.duration } #[must_use] @@ -2586,39 +2593,39 @@ 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(), ) } /// 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 @@ -2626,7 +2633,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, @@ -2659,9 +2666,9 @@ 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.in_time_scale(self.time_scale); + let other = other.to_time_scale(self.time_scale); Self::from_duration( Duration::compose( sign, @@ -2694,7 +2701,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) = @@ -2719,7 +2726,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, @@ -2741,8 +2748,8 @@ 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 (sign, days, _, _, _, _, _, _) = self.duration.decompose(); + let other = other.to_time_scale(self.time_scale); Self::from_duration( Duration::compose( sign, @@ -2759,7 +2766,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() } @@ -2843,17 +2850,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!( @@ -2871,7 +2869,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", @@ -3014,7 +3014,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)) } }, @@ -3025,42 +3025,27 @@ 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.to_duration_since_j1900()); - 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,) } } 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.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 ) } } @@ -3070,7 +3055,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, @@ -3091,7 +3077,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, @@ -3113,7 +3100,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, @@ -3135,7 +3122,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, @@ -3290,7 +3277,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#""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); @@ -3351,7 +3338,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. @@ -3360,13 +3347,11 @@ 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); - 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); } } @@ -3374,6 +3359,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/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 46e80fe8..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. @@ -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. @@ -51,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; @@ -68,7 +64,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 +73,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, }, @@ -121,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, }; } diff --git a/src/month.rs b/src/month.rs index b0f7d984..c2f2b2fd 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/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 59% rename from src/timescale.rs rename to src/timescale/mod.rs index 22ab3a82..17058941 100644 --- a/src/timescale.rs +++ b/src/timescale/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. @@ -15,23 +15,24 @@ 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, Unit, 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. 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 @@ -40,19 +41,24 @@ 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. +/// |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; /// 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, + 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; @@ -65,7 +71,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 { @@ -85,28 +91,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 } } @@ -126,49 +117,39 @@ 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, } } -} -impl fmt::Display for TimeScale { - /// Prints given TimeScale - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + 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) -> i32 { 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"), + TimeScale::TT | TimeScale::ET | TimeScale::TDB => 2000, + TimeScale::UTC | TimeScale::TAI => 1900, + TimeScale::GPST | TimeScale::QZSST => 1980, + TimeScale::BDT => 2006, + TimeScale::GST => 1997, } } -} -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 { + pub(crate) const fn ref_hour(&self) -> i64 { 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}"), + TimeScale::ET | TimeScale::TDB => 0, + _ => 12, } } } @@ -217,61 +198,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/src/timeseries.rs b/src/timeseries.rs index bc53f8ae..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. @@ -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/src/timeunits.rs b/src/timeunits.rs index fc535b60..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. @@ -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); 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/epoch.rs b/tests/epoch.rs index efc4fc58..aa0cdf17 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}; @@ -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 @@ -130,6 +137,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); @@ -165,10 +173,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. @@ -336,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 @@ -361,49 +373,55 @@ 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_since_j1900_tai + 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); 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); @@ -460,7 +478,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 +523,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!( @@ -563,11 +581,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 @@ -750,7 +768,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); @@ -863,10 +881,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()) ); @@ -891,11 +908,35 @@ 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_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, 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::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] @@ -973,11 +1014,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 +1051,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 +1060,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 +1083,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] @@ -1240,10 +1281,9 @@ 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, + TimeScale::TAI, TimeScale::ET, TimeScale::TDB, TimeScale::TT, @@ -1326,7 +1366,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 +1467,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 +1547,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 +1605,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 +1625,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,10 +1633,11 @@ 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 = 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) @@ -1604,7 +1645,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), @@ -1625,7 +1666,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), @@ -1637,7 +1678,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), @@ -1649,7 +1690,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), @@ -1661,7 +1702,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), @@ -1670,12 +1711,12 @@ 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)); // <=>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), @@ -1684,13 +1725,14 @@ 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)); // <=>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), @@ -1703,7 +1745,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(), @@ -1711,7 +1753,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), @@ -1724,8 +1766,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(), @@ -1733,7 +1776,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), @@ -1754,7 +1797,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 { @@ -1876,6 +1919,16 @@ 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); +} + #[cfg(feature = "std")] #[test] fn test_leap_seconds_file() { 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