diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index a3d7003..73c4930 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@master @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@master with: @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@master with: @@ -78,7 +78,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@master with: diff --git a/.github/workflows/formal_verification.yml b/.github/workflows/formal_verification.yml index d486c79..0f4aabf 100644 --- a/.github/workflows/formal_verification.yml +++ b/.github/workflows/formal_verification.yml @@ -19,7 +19,7 @@ jobs: continue-on-error: true steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Clean Cargo.toml for Kani run: | diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index da65579..8220fa7 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -25,14 +25,12 @@ jobs: matrix: target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' check-latest: false allow-prereleases: false - - name: Remove bad python - run: ls -l `which python`; ls -l /usr/bin/python* - name: Build wheels uses: PyO3/maturin-action@v1 with: @@ -63,7 +61,8 @@ jobs: shell: bash run: | set -e - pip install hifitime --find-links dist --force-reinstall + ls dist + pip install hifitime --find-links dist --force-reinstall --no-index -vv pip install pytest pytest - name: pytest @@ -79,7 +78,8 @@ jobs: pip3 install -U pip pytest run: | set -e - pip3 install hifitime --find-links dist --force-reinstall + ls dist + pip3 install hifitime --find-links dist --force-reinstall --no-index -vv pytest windows: @@ -88,7 +88,7 @@ jobs: matrix: target: [x64, x86] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' @@ -109,7 +109,7 @@ jobs: shell: bash run: | set -e - pip install hifitime --find-links dist --force-reinstall + pip install hifitime --find-links dist --force-reinstall --no-index -vv pip install pytest pytest @@ -119,7 +119,7 @@ jobs: matrix: target: [x86_64, aarch64] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' @@ -139,14 +139,14 @@ jobs: shell: bash run: | set -e - pip install hifitime --find-links dist --force-reinstall + pip install hifitime --find-links dist --force-reinstall --no-index -vv pip install pytest pytest sdist: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build sdist uses: PyO3/maturin-action@v1 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 276a69b..cd53182 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@master @@ -33,14 +33,14 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] rust: - - { version: "1.74", name: MSRV } + - { version: "1.77", name: MSRV } - { version: stable, name: stable } runs-on: ${{ matrix.os }} name: Test Suite (${{ matrix.os }}, ${{ matrix.rust.name }}) steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install ${{ matrix.rust.name }} toolchain uses: dtolnay/rust-toolchain@master @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install nightly toolchain uses: dtolnay/rust-toolchain@master @@ -86,12 +86,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: 1.64 # Run lints using the MSRV not latest + toolchain: 1.77 components: rustfmt, clippy - name: Run cargo fmt @@ -106,7 +106,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@master @@ -127,7 +127,7 @@ jobs: RUSTFLAGS: --cfg __ui_tests - name: Upload coverage report - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: ./lcov.txt @@ -138,7 +138,7 @@ jobs: if: github.ref_type == 'tag' steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@master diff --git a/.gitignore b/.gitignore index c0506f0..08a2ebf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ Cargo.lock .env .venv -dist/ \ No newline at end of file +dist/ +.venv +src/epoch.rs +.vscode/settings.json diff --git a/Cargo.toml b/Cargo.toml index be22e28..352b238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hifitime" -version = "4.0.0-dev" +version = "4.0.0-alpha" authors = ["Christopher Rabotin "] description = "Ultra-precise date and time handling in Rust for scientific applications with leap second support" homepage = "https://nyxspace.com/" @@ -21,7 +21,10 @@ name = "hifitime" serde = { version = "1.0.155", optional = true } serde_derive = { version = "1.0.155", optional = true } der = { version = "0.7.8", features = ["derive", "real"], optional = true } -pyo3 = { version = "0.21.1", features = ["extension-module"], optional = true } +pyo3 = { version = "0.21.1", features = [ + "extension-module", + "inventory", +], optional = true } num-traits = { version = "0.2.15", default-features = false, features = [ "libm", ] } diff --git a/README.md b/README.md index fbb4f73..7879da9 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ let example_now = Epoch::from_gregorian_utc_hms(2015, 8, 17, 22, 55, 01); // We'll round to the nearest fifteen days let this_much_ago = example_now - previous_post; -assert_eq!(format!("{this_much_ago}"), "191 days 11 h 32 min 29 s"); +assert_eq!(format!("{this_much_ago}"), "191 days 11 h 32 min 28 s"); let about_this_much_ago_floor = this_much_ago.floor(15.days()); assert_eq!(format!("{about_this_much_ago_floor}"), "180 days"); let about_this_much_ago_ceil = this_much_ago.ceil(15.days()); @@ -311,7 +311,9 @@ In order to provide full interoperability with NAIF, hifitime uses the NAIF algo ## 4.0.0 (WIP) -+ Minimum Support Rust Version bumped to 1.74.0 ++ Minimum Support Rust Version (MSRV) bumped to 1.77.0 ++ Major refactoring of the code for ease of maintenance and removal of deprecrated functions from 3.x ++ Centralization of all time scale conversions into the `to_time_scale` function -- huge effort by [@gwbres](https://github.com/gwbres) ## 3.9.0 @@ -414,3 +416,21 @@ Huge thanks to [@gwbres](https://github.com/gwbres) who put in all of the work f # Important Update on Versioning Strategy We want to inform our users of an important change in Hifitime's versioning approach. Starting with version 3.9.0, minor version updates may include changes that could potentially break backward compatibility. While we strive to maintain stability and minimize disruptions, this change allows us to incorporate significant improvements and adapt more swiftly to evolving user needs. We recommend users to carefully review the release notes for each update, even minor ones, to understand any potential impacts on their existing implementations. Our commitment to providing a robust and dynamic time management library remains steadfast, and we believe this change in versioning will better serve the evolving demands of our community. + +# Development + +Thanks for considering to help out on Hifitime! + +For Rust development, `cargo` is all you need, along with build tools for the minimum supported Rust version. + +## Python development + +First, please install [maturin](https://www.maturin.rs/) and set up a Python virtual environment from which to develop. Also make sure that the package version in Cargo.toml is _greater_ than any published version. For example, if the latest version published on [PyPi](https://pypi.org/project/hifitime/) is 4.0.0-a.0 (for alpha-0), make sure that you change the Cargo.toml file such that you're at least in version `alpha-1`, or the `pip install` will download from PyPi instead of installing from the local folder. To run the Python tests, you must install `pytest` in your virtual environment. + +The exact steps should be: + +1. Jump into the virtual environment: `source .venv/bin/activate` (e.g.) +1. Make sure pytest is installed: `pip install pytest` +1. Build hifitime specifying the output folder of the Python egg: `maturin build -F python --out dist` +1. Install the egg: `pip install dist/hifitime-4.0.0.dev1-cp311-cp311-linux_x86_64.whl` +1. Run the tests using the environmental pytest: `.venv/bin/pytest` \ No newline at end of file diff --git a/examples/python/basic.py b/examples/python/basic.py index a152384..60e202e 100644 --- a/examples/python/basic.py +++ b/examples/python/basic.py @@ -1,6 +1,6 @@ """ * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 a301117..fce1051 100644 --- a/examples/python/timescales.py +++ b/examples/python/timescales.py @@ -1,6 +1,6 @@ """ * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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/asn1der.rs b/src/asn1der.rs index e21fe0c..462ef6d 100644 --- a/src/asn1der.rs +++ b/src/asn1der.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 impl 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 40da931..0000000 --- a/src/deprecated.rs +++ /dev/null @@ -1,613 +0,0 @@ -/* - * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 d95f12a..0000000 --- a/src/duration.rs +++ /dev/null @@ -1,1626 +0,0 @@ -/* - * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 greater 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 = "python", pyo3(module = "hifitime"))] -#[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 __getnewargs__(&self) -> Result<(String,), PyErr> { - Ok((format!("{self}"),)) - } - - #[cfg(feature = "python")] - fn __str__(&self) -> String { - format!("{self}") - } - - #[cfg(feature = "python")] - fn __repr__(&self) -> String { - format!("{self} @ {self:p}") - } - - #[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 __le__(&self, other: Self) -> bool { - *self <= other - } - - #[cfg(feature = "python")] - fn __lt__(&self, other: Self) -> bool { - *self < other - } - - #[cfg(feature = "python")] - fn __ge__(&self, other: Self) -> bool { - *self >= other - } - - #[cfg(feature = "python")] - fn __gt__(&self, other: Self) -> bool { - *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" - #[allow(clippy::absurd_extreme_comparisons)] - 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 0000000..76a6b9b --- /dev/null +++ b/src/duration/kani.rs @@ -0,0 +1,122 @@ +/* +* 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, Errors}; +use crate::NANOSECONDS_PER_CENTURY; + +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 + let _ = 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 0000000..ef8fb3f --- /dev/null +++ b/src/duration/mod.rs @@ -0,0 +1,777 @@ +/* +* 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"))] +#[allow(unused_imports)] // Import is indeed used. +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 greater 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 = "python", pyo3(module = "hifitime"))] +#[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, + ) + } + + /// Returns the subdivision of duration in this unit, if such is available. Does not work with Week or Century. + /// + /// # Example + /// ``` + /// use hifitime::{Duration, TimeUnits, Unit}; + /// + /// let two_hours_three_min = 2.hours() + 3.minutes(); + /// assert_eq!(two_hours_three_min.subdivision(Unit::Hour), Some(2.hours())); + /// assert_eq!(two_hours_three_min.subdivision(Unit::Minute), Some(3.minutes())); + /// assert_eq!(two_hours_three_min.subdivision(Unit::Second), Some(Duration::ZERO)); + /// assert_eq!(two_hours_three_min.subdivision(Unit::Week), None); + /// ``` + #[must_use] + pub fn subdivision(&self, unit: Unit) -> Option { + let (_, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) = + self.decompose(); + + match unit { + Unit::Nanosecond => Some((nanoseconds as i64) * unit), + Unit::Microsecond => Some((microseconds as i64) * unit), + Unit::Millisecond => Some((milliseconds as i64) * unit), + Unit::Second => Some((seconds as i64) * unit), + Unit::Minute => Some((minutes as i64) * unit), + Unit::Hour => Some((hours as i64) * unit), + Unit::Day => Some((days as i64) * unit), + Unit::Week | Unit::Century => None, + } + } + + /// 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 0000000..7c1d1c3 --- /dev/null +++ b/src/duration/ops.rs @@ -0,0 +1,386 @@ +/* +* 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}; + +#[cfg(not(feature = "std"))] +#[allow(unused_imports)] // Import is indeed used. +use num_traits::Float; + +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" + #[allow(clippy::absurd_extreme_comparisons)] + fn add(mut self, mut rhs: Self) -> Duration { + // Ensure that the durations are normalized to avoid extra logic to handle under/overflows + self.normalize(); + rhs.normalize(); + + // Check that the addition fits in an i16 + match self.centuries.checked_add(rhs.centuries) { + None => { + // Overflowed, so we've hit the bound. + if self.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) => { + self.centuries = centuries; + } + } + + if self.centuries == Self::MIN.centuries && self.nanoseconds < Self::MIN.nanoseconds { + // Then we do the operation backward + match self + .nanoseconds + .checked_sub(NANOSECONDS_PER_CENTURY - rhs.nanoseconds) + { + Some(nanos) => self.nanoseconds = nanos, + None => { + self.centuries += 1; // Safe because we're at the MIN + self.nanoseconds = rhs.nanoseconds + } + } + } else { + match self.nanoseconds.checked_add(rhs.nanoseconds) { + Some(nanoseconds) => self.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 self.centuries.checked_add(rhs.centuries) { + None => return Self::MAX, + Some(centuries) => self.centuries = centuries, + }; + // Now it will fit! + self.nanoseconds += rhs.nanoseconds; + } + } + } + + self.normalize(); + self + } +} + +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(mut self, mut rhs: Self) -> Self { + // Ensure that the durations are normalized to avoid extra logic to handle under/overflows + self.normalize(); + rhs.normalize(); + match self.centuries.checked_sub(rhs.centuries) { + None => { + // Underflowed, so we've hit the min + return Self::MIN; + } + Some(centuries) => { + self.centuries = centuries; + } + } + + match self.nanoseconds.checked_sub(rhs.nanoseconds) { + None => { + // Decrease the number of centuries, and realign + match self.centuries.checked_sub(1) { + Some(centuries) => { + self.centuries = centuries; + self.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; + } + }; + } + Some(nanos) => self.nanoseconds = nanos, + }; + + self.normalize(); + self + } +} + +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 0000000..8112115 --- /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 0000000..48de16f --- /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} @ {self:p}") + } + + 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 __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, + } + } + + fn __getnewargs__(&self) -> Result<(String,), PyErr> { + Ok((format!("{self}"),)) + } + + // Python constructors + + #[classmethod] + #[pyo3(name = "ZERO")] + fn zero(_cls: &Bound<'_, PyType>) -> Duration { + Duration::ZERO + } + + #[classmethod] + #[pyo3(name = "EPSILON")] + fn epsilon(_cls: &Bound<'_, PyType>) -> Duration { + Duration::EPSILON + } + + #[classmethod] + #[pyo3(name = "MAX")] + fn py_from_max(_cls: &Bound<'_, PyType>) -> Duration { + Duration::MAX + } + + #[classmethod] + #[pyo3(name = "MIN")] + fn py_from_min(_cls: &Bound<'_, PyType>) -> Duration { + Duration::MIN + } + + #[classmethod] + #[pyo3(name = "MIN_POSITIVE")] + fn min_positive(_cls: &Bound<'_, PyType>) -> Duration { + Duration::MIN_POSITIVE + } + + #[classmethod] + #[pyo3(name = "MIN_NEGATIVE")] + fn min_negative(_cls: &Bound<'_, PyType>) -> Duration { + Duration::MIN_NEGATIVE + } + + #[classmethod] + #[pyo3(name = "from_parts")] + /// Create a normalized duration from its parts + fn py_from_parts(_cls: &Bound<'_, 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: &Bound<'_, 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: &Bound<'_, 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 0000000..9339514 --- /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 657fad6..7fa2b91 100644 --- a/src/efmt/consts.rs +++ b/src/efmt/consts.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 76f73f5..a741e28 100644 --- a/src/efmt/format.rs +++ b/src/efmt/format.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 5eec8d9..26df41a 100644 --- a/src/efmt/formatter.rs +++ b/src/efmt/formatter.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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,6 +15,7 @@ use crate::{parser::Token, Duration, Epoch, TimeScale}; use super::format::Format; #[cfg(not(feature = "std"))] +#[allow(unused_imports)] // Import is indeed used. use num_traits::Float; #[derive(Copy, Clone, Default, Debug, PartialEq)] @@ -99,8 +100,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 +128,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 cc0e9f8..83eff8d 100644 --- a/src/efmt/mod.rs +++ b/src/efmt/mod.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 3bed59f..327331d 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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. @@ -9,25 +9,26 @@ */ use crate::duration::{Duration, Unit}; +use crate::efmt::format::Format; use crate::leap_seconds::{LatestLeapSeconds, LeapSecondProvider}; -use crate::parser::Token; +use crate::Weekday; 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, HIFITIME_REF_YEAR, 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; +#[cfg(not(kani))] +use crate::parser::Token; +#[cfg(not(kani))] +use crate::ParsingErrors; use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; use core::fmt; use core::hash::{Hash, Hasher}; use core::ops::{Add, AddAssign, Sub, SubAssign}; -use crate::ParsingErrors; -use crate::Weekday; - #[cfg(feature = "python")] use pyo3::prelude::*; @@ -40,17 +41,19 @@ use pyo3::types::PyType; #[cfg(feature = "python")] use crate::leap_seconds_file::LeapSecondsFile; +#[cfg(not(kani))] #[cfg(feature = "serde")] -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use core::str::FromStr; -#[cfg(not(feature = "std"))] -use num_traits::{Euclid, Float}; - #[cfg(feature = "ut1")] use crate::ut1::Ut1Provider; +#[cfg(not(feature = "std"))] +#[allow(unused_imports)] // Import is indeed used. +use num_traits::Float; + const TT_OFFSET_MS: i64 = 32_184; const ET_OFFSET_US: i64 = 32_184_935; @@ -121,23 +124,53 @@ 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 = "python", pyo3(module = "hifitime"))] -#[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); + } +} + +#[cfg(not(kani))] +#[cfg(feature = "serde")] +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) + } +} + +#[cfg(not(kani))] +#[cfg(feature = "serde")] +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 } } @@ -151,7 +184,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, + } } } @@ -161,7 +197,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, + } } } @@ -169,7 +208,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, + } } } @@ -192,7 +234,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, + } } } @@ -201,7 +246,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, + } } } @@ -214,29 +262,39 @@ 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); + if self.time_scale == other.time_scale { + self.duration == other.duration + } else { + // If one of the two time scales does not include leap seconds, + // we always convert the time scale with leap seconds into the + // time scale that does NOT have leap seconds. + if self.time_scale.uses_leap_seconds() != other.time_scale.uses_leap_seconds() { + if self.time_scale.uses_leap_seconds() { + self.to_time_scale(other.time_scale).duration == other.duration + } else { + self.duration == other.to_time_scale(self.time_scale).duration + } + } else { + // Otherwise it does not matter + self.duration == other.to_time_scale(self.time_scale).duration + } + } } } 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) } } @@ -253,7 +311,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); @@ -262,19 +320,143 @@ 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 + *self + } else { + // Now we need to convert from the current time scale into the desired time scale. + // Let's first compute this epoch from its current time scale into TAI. + let prime_epoch_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 - delta_et_tai.seconds() + self.time_scale.prime_epoch_offset() + } + TimeScale::TDB => { + let gamma = Self::inner_g(self.duration.to_seconds()); + + let delta_tdb_tai = gamma * Unit::Second + TT_OFFSET_MS * Unit::Millisecond; + + // Offset back to J1900. + self.duration - delta_tdb_tai + self.time_scale.prime_epoch_offset() + } + TimeScale::UTC => { + // Assume this is TAI + let mut tai_assumption = *self; + tai_assumption.time_scale = TimeScale::TAI; + self.duration + tai_assumption.leap_seconds(true).unwrap_or(0.0).seconds() + } + TimeScale::GPST => self.duration + GPST_REF_EPOCH.to_tai_duration(), + TimeScale::GST => self.duration + GST_REF_EPOCH.to_tai_duration(), + TimeScale::BDT => self.duration + BDT_REF_EPOCH.to_tai_duration(), + TimeScale::QZSST => self.duration + QZSST_REF_EPOCH.to_tai_duration(), + }; + + // Convert to the desired time scale from the TAI duration + let ts_ref_offset = match ts { + TimeScale::TAI => prime_epoch_offset, + TimeScale::TT => prime_epoch_offset + TT_OFFSET_MS.milliseconds(), + TimeScale::ET => { + // Run a Newton Raphston to convert find the correct value of the ... ?! + + let mut seconds = (prime_epoch_offset - ts.prime_epoch_offset()).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. + prime_epoch_offset + delta_et_tai.seconds() - ts.prime_epoch_offset() + } + TimeScale::TDB => { + // Iterate to convert find the correct value of the + let mut seconds = (prime_epoch_offset - ts.prime_epoch_offset()).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(); + + prime_epoch_offset + delta_tdb_tai - ts.prime_epoch_offset() + } + TimeScale::UTC => { + // Assume it's TAI + let epoch = Self { + duration: prime_epoch_offset, + time_scale: TimeScale::TAI, + }; + // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds + prime_epoch_offset - epoch.leap_seconds(true).unwrap_or(0.0).seconds() + } + TimeScale::GPST => prime_epoch_offset - GPST_REF_EPOCH.to_tai_duration(), + TimeScale::GST => prime_epoch_offset - GST_REF_EPOCH.to_tai_duration(), + TimeScale::BDT => prime_epoch_offset - BDT_REF_EPOCH.to_tai_duration(), + TimeScale::QZSST => prime_epoch_offset - QZSST_REF_EPOCH.to_tai_duration(), + }; + + Self { + duration: ts_ref_offset, + time_scale: ts, + } } } @@ -282,7 +464,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, } } @@ -316,14 +498,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] @@ -341,34 +516,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] @@ -384,7 +550,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 @@ -424,7 +590,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 @@ -464,10 +630,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] @@ -490,28 +653,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] @@ -529,16 +671,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] @@ -565,14 +698,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] @@ -580,27 +713,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] @@ -732,72 +859,58 @@ impl Epoch { return Err(Errors::Carry); } - let (years_since_1900, mut duration_wrt_1900) = match year.checked_sub(1900) { + let mut duration_wrt_ref = match year.checked_sub(HIFITIME_REF_YEAR) { None => return Err(Errors::Overflow), - Some(years_since_1900) => match years_since_1900.checked_mul(365) { + Some(years_since_ref) => match years_since_ref.checked_mul(365) { None => return Err(Errors::Overflow), - Some(days) => (years_since_1900, Unit::Day * i64::from(days)), + Some(days) => Unit::Day * i64::from(days), }, - }; + } - time_scale.gregorian_epoch_offset(); - // 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); + // Now add the leap days for all the years prior to the current year + if year >= HIFITIME_REF_YEAR { + // Add days + for year in HIFITIME_REF_YEAR..year { + if is_leap_year(year) { + duration_wrt_ref += Unit::Day; + } + } + // Remove ref hours from duration to correct for the time scale not starting at midnight + // duration_wrt_ref -= Unit::Hour * time_scale.ref_hour() as i64; } else { - // we don't need to fix the offset, since jan 1896 has had the leap, when counting back from 1900 - duration_wrt_1900 += Unit::Day * i64::from(years_since_1900 / 4); - duration_wrt_1900 -= Unit::Day * i64::from(years_since_1900 / 100); - // every 400 years we correct our correction. The first one before 1900 is 1600 (years_since_1900 = -300) - // so we subtract 100 to correct the offset - duration_wrt_1900 += Unit::Day * i64::from((years_since_1900 - 100) / 400); - }; + // Remove days + for year in year..HIFITIME_REF_YEAR { + if is_leap_year(year) { + duration_wrt_ref -= Unit::Day; + } + } + // Add ref hours + // duration_wrt_ref += Unit::Hour * time_scale.ref_hour() as i64; + } // 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) + Unit::Nanosecond * i64::from(nanos); + if second == 60 { // Herein lies the whole ambiguity of leap seconds. Two different UTC dates exist at the - // same number of second afters J1900.0. - duration_wrt_1900 -= Unit::Second; + // same number of second after J1900.0. + 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, }) } @@ -855,15 +968,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] @@ -1099,9 +1213,9 @@ impl Epoch { decomposed[5].try_into().unwrap(), decomposed[6].try_into().unwrap(), ts, - ); + )?; - Ok(epoch? + tz) + Ok(epoch + tz) } /// Initializes an Epoch from the provided Format. @@ -1128,7 +1242,7 @@ impl Epoch { // Compute the TAI to UT1 offset at this time. // We have the time in TAI. But we were given UT1. // The offset is provided as offset = TAI - UT1 <=> TAI = UT1 + offset - e.duration_since_j1900_tai += e.ut1_offset(provider).unwrap_or(Duration::ZERO); + e.duration += e.ut1_offset(provider).unwrap_or(Duration::ZERO); e.time_scale = TimeScale::TAI; e } @@ -1149,9 +1263,12 @@ 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, days, mut hours, minutes, seconds, milliseconds, microseconds, nanos) = + (duration + ts.gregorian_epoch_offset()).decompose(); let days_f64 = if sign < 0 { -(days as f64) @@ -1160,19 +1277,18 @@ impl Epoch { }; let (mut year, mut days_in_year) = div_rem_f64(days_f64, DAYS_PER_YEAR_NLD); - // TAI is defined at 1900, so a negative time is before 1900 and positive is after 1900. - year += 1900; + year += HIFITIME_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 >= HIFITIME_REF_YEAR { + for year in HIFITIME_REF_YEAR..year { if is_leap_year(year) { days_in_year -= 1.0; } } } else { - for year in year..1900 { + for year in year..HIFITIME_REF_YEAR { if is_leap_year(year) { days_in_year += 1.0; } @@ -1205,6 +1321,15 @@ impl Epoch { month += 1; } + if hours >= 24 { + hours -= 24; + if year >= HIFITIME_REF_YEAR { + day += 1.0; + } else { + day -= 1.0; + } + } + if day <= 0.0 || days_in_year < 0.0 { // We've overflowed backward month = 12; @@ -1350,98 +1475,98 @@ impl Epoch { #[cfg(feature = "python")] #[classmethod] /// Creates a new Epoch from a Duration as the time difference between this epoch and TAI reference epoch. - const fn init_from_tai_duration(_cls: &PyType, duration: Duration) -> Self { + const fn init_from_tai_duration(_cls: &Bound<'_, PyType>, duration: Duration) -> Self { Self::from_tai_duration(duration) } #[cfg(feature = "python")] #[classmethod] /// Creates a new Epoch from its centuries and nanosecond since the TAI reference epoch. - fn init_from_tai_parts(_cls: &PyType, centuries: i16, nanoseconds: u64) -> Self { + fn init_from_tai_parts(_cls: &Bound<'_, PyType>, centuries: i16, nanoseconds: u64) -> Self { Self::from_tai_parts(centuries, nanoseconds) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from the provided TAI seconds since 1900 January 01 at midnight - fn init_from_tai_seconds(_cls: &PyType, seconds: f64) -> Self { + fn init_from_tai_seconds(_cls: &Bound<'_, PyType>, seconds: f64) -> Self { Self::from_tai_seconds(seconds) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from the provided TAI days since 1900 January 01 at midnight - fn init_from_tai_days(_cls: &PyType, days: f64) -> Self { + fn init_from_tai_days(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_tai_days(days) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from the provided UTC seconds since 1900 January 01 at midnight - fn init_from_utc_seconds(_cls: &PyType, seconds: f64) -> Self { + fn init_from_utc_seconds(_cls: &Bound<'_, PyType>, seconds: f64) -> Self { Self::from_utc_seconds(seconds) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from the provided UTC days since 1900 January 01 at midnight - fn init_from_utc_days(_cls: &PyType, days: f64) -> Self { + fn init_from_utc_days(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_utc_days(days) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from given MJD in TAI time scale - fn init_from_mjd_tai(_cls: &PyType, days: f64) -> Self { + fn init_from_mjd_tai(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_mjd_tai(days) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from given MJD in UTC time scale - fn init_from_mjd_utc(_cls: &PyType, days: f64) -> Self { + fn init_from_mjd_utc(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_mjd_utc(days) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from given JDE in TAI time scale - fn init_from_jde_tai(_cls: &PyType, days: f64) -> Self { + fn init_from_jde_tai(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_jde_tai(days) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from given JDE in UTC time scale - fn init_from_jde_utc(_cls: &PyType, days: f64) -> Self { + fn init_from_jde_utc(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_jde_utc(days) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from the provided TT seconds (approximated to 32.184s delta from TAI) - fn init_from_tt_seconds(_cls: &PyType, seconds: f64) -> Self { + fn init_from_tt_seconds(_cls: &Bound<'_, PyType>, seconds: f64) -> Self { Self::from_tt_seconds(seconds) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from the provided TT seconds (approximated to 32.184s delta from TAI) - fn init_from_tt_duration(_cls: &PyType, duration: Duration) -> Self { + fn init_from_tt_duration(_cls: &Bound<'_, PyType>, duration: Duration) -> Self { Self::from_tt_duration(duration) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from the Ephemeris Time seconds past 2000 JAN 01 (J2000 reference) - fn init_from_et_seconds(_cls: &PyType, seconds_since_j2000: f64) -> Epoch { + fn init_from_et_seconds(_cls: &Bound<'_, PyType>, seconds_since_j2000: f64) -> Epoch { Self::from_et_seconds(seconds_since_j2000) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from the Ephemeris Time duration past 2000 JAN 01 (J2000 reference) - fn init_from_et_duration(_cls: &PyType, duration_since_j2000: Duration) -> Self { + fn init_from_et_duration(_cls: &Bound<'_, PyType>, duration_since_j2000: Duration) -> Self { Self::from_et_duration(duration_since_j2000) } @@ -1450,28 +1575,28 @@ impl Epoch { /// Initialize an Epoch from Dynamic Barycentric Time (TDB) seconds past 2000 JAN 01 midnight (difference than SPICE) /// NOTE: This uses the ESA algorithm, which is a notch more complicated than the SPICE algorithm, but more precise. /// In fact, SPICE algorithm is precise +/- 30 microseconds for a century whereas ESA algorithm should be exactly correct. - fn init_from_tdb_seconds(_cls: &PyType, seconds_j2000: f64) -> Epoch { + fn init_from_tdb_seconds(_cls: &Bound<'_, PyType>, seconds_j2000: f64) -> Epoch { Self::from_tdb_seconds(seconds_j2000) } #[cfg(feature = "python")] #[classmethod] /// Initialize from Dynamic Barycentric Time (TDB) (same as SPICE ephemeris time) whose epoch is 2000 JAN 01 noon TAI. - fn init_from_tdb_duration(_cls: &PyType, duration_since_j2000: Duration) -> Epoch { + fn init_from_tdb_duration(_cls: &Bound<'_, PyType>, duration_since_j2000: Duration) -> Epoch { Self::from_tdb_duration(duration_since_j2000) } #[cfg(feature = "python")] #[classmethod] /// Initialize from the JDE days - fn init_from_jde_et(_cls: &PyType, days: f64) -> Self { + fn init_from_jde_et(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_jde_et(days) } #[cfg(feature = "python")] #[classmethod] /// Initialize from Dynamic Barycentric Time (TDB) (same as SPICE ephemeris time) in JD days - fn init_from_jde_tdb(_cls: &PyType, days: f64) -> Self { + fn init_from_jde_tdb(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_jde_tdb(days) } @@ -1479,7 +1604,7 @@ impl Epoch { #[classmethod] /// Initialize an Epoch from the number of seconds since the GPS Time Epoch, /// defined as UTC midnight of January 5th to 6th 1980 (cf. ). - fn init_from_gpst_seconds(_cls: &PyType, seconds: f64) -> Self { + fn init_from_gpst_seconds(_cls: &Bound<'_, PyType>, seconds: f64) -> Self { Self::from_gpst_seconds(seconds) } @@ -1487,7 +1612,7 @@ impl Epoch { #[classmethod] /// Initialize an Epoch from the number of days since the GPS Time Epoch, /// defined as UTC midnight of January 5th to 6th 1980 (cf. ). - fn init_from_gpst_days(_cls: &PyType, days: f64) -> Self { + fn init_from_gpst_days(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_gpst_days(days) } @@ -1496,7 +1621,7 @@ impl Epoch { /// Initialize an Epoch from the number of nanoseconds since the GPS Time 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. - fn init_from_gpst_nanoseconds(_cls: &PyType, nanoseconds: u64) -> Self { + fn init_from_gpst_nanoseconds(_cls: &Bound<'_, PyType>, nanoseconds: u64) -> Self { Self::from_gpst_nanoseconds(nanoseconds) } @@ -1504,7 +1629,7 @@ impl Epoch { #[classmethod] /// Initialize an Epoch from the number of seconds since the QZSS Time Epoch, /// defined as UTC midnight of January 5th to 6th 1980 (cf. ). - fn init_from_qzsst_seconds(_cls: &PyType, seconds: f64) -> Self { + fn init_from_qzsst_seconds(_cls: &Bound<'_, PyType>, seconds: f64) -> Self { Self::from_qzsst_seconds(seconds) } @@ -1512,7 +1637,7 @@ impl Epoch { #[classmethod] /// Initialize an Epoch from the number of days since the QZSS Time Epoch, /// defined as UTC midnight of January 5th to 6th 1980 (cf. ). - fn init_from_qzsst_days(_cls: &PyType, days: f64) -> Self { + fn init_from_qzsst_days(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_qzsst_days(days) } @@ -1521,7 +1646,7 @@ impl Epoch { /// Initialize an Epoch from the number of nanoseconds since the QZSS Time Epoch, /// defined as UTC midnight of January 5th to 6th 1980 (cf. ). /// This may be useful for time keeping devices that use QZSS as a time source. - fn init_from_qzsst_nanoseconds(_cls: &PyType, nanoseconds: u64) -> Self { + fn init_from_qzsst_nanoseconds(_cls: &Bound<'_, PyType>, nanoseconds: u64) -> Self { Self::from_qzsst_nanoseconds(nanoseconds) } @@ -1530,7 +1655,7 @@ impl Epoch { /// Initialize an Epoch from the number of seconds since the Galileo Time Epoch, /// starting on August 21st 1999 Midnight UT, /// (cf. ). - fn init_from_gst_seconds(_cls: &PyType, seconds: f64) -> Self { + fn init_from_gst_seconds(_cls: &Bound<'_, PyType>, seconds: f64) -> Self { Self::from_gst_seconds(seconds) } @@ -1539,7 +1664,7 @@ impl Epoch { /// Initialize an Epoch from the number of days since the Galileo Time Epoch, /// starting on August 21st 1999 Midnight UT, /// (cf. ). - fn init_from_gst_days(_cls: &PyType, days: f64) -> Self { + fn init_from_gst_days(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_gst_days(days) } @@ -1549,7 +1674,7 @@ impl Epoch { /// starting on August 21st 1999 Midnight UT, /// (cf. ). /// This may be useful for time keeping devices that use GST as a time source. - fn init_from_gst_nanoseconds(_cls: &PyType, nanoseconds: u64) -> Self { + fn init_from_gst_nanoseconds(_cls: &Bound<'_, PyType>, nanoseconds: u64) -> Self { Self::from_gst_nanoseconds(nanoseconds) } @@ -1557,7 +1682,7 @@ impl Epoch { #[classmethod] /// Initialize an Epoch from the number of seconds since the BeiDou Time Epoch, /// defined as January 1st 2006 (cf. ). - fn init_from_bdt_seconds(_cls: &PyType, seconds: f64) -> Self { + fn init_from_bdt_seconds(_cls: &Bound<'_, PyType>, seconds: f64) -> Self { Self::from_bdt_seconds(seconds) } @@ -1565,7 +1690,7 @@ impl Epoch { #[classmethod] /// Initialize an Epoch from the number of days since the BeiDou Time Epoch, /// defined as January 1st 2006 (cf. ). - fn init_from_bdt_days(_cls: &PyType, days: f64) -> Self { + fn init_from_bdt_days(_cls: &Bound<'_, PyType>, days: f64) -> Self { Self::from_bdt_days(days) } @@ -1574,28 +1699,28 @@ impl Epoch { /// Initialize an Epoch from the number of days since the BeiDou Time Epoch, /// defined as January 1st 2006 (cf. ). /// This may be useful for time keeping devices that use BDT as a time source. - fn init_from_bdt_nanoseconds(_cls: &PyType, nanoseconds: u64) -> Self { + fn init_from_bdt_nanoseconds(_cls: &Bound<'_, PyType>, nanoseconds: u64) -> Self { Self::from_bdt_nanoseconds(nanoseconds) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from the provided UNIX second timestamp since UTC midnight 1970 January 01. - fn init_from_unix_seconds(_cls: &PyType, seconds: f64) -> Self { + fn init_from_unix_seconds(_cls: &Bound<'_, PyType>, seconds: f64) -> Self { Self::from_unix_seconds(seconds) } #[cfg(feature = "python")] #[classmethod] /// Initialize an Epoch from the provided UNIX millisecond timestamp since UTC midnight 1970 January 01. - fn init_from_unix_milliseconds(_cls: &PyType, milliseconds: f64) -> Self { + fn init_from_unix_milliseconds(_cls: &Bound<'_, PyType>, milliseconds: f64) -> Self { Self::from_unix_milliseconds(milliseconds) } #[cfg(feature = "python")] #[classmethod] fn init_from_gregorian( - _cls: &PyType, + _cls: &Bound<'_, PyType>, year: i32, month: u8, day: u8, @@ -1611,7 +1736,7 @@ impl Epoch { #[cfg(feature = "python")] #[classmethod] fn init_from_gregorian_at_noon( - _cls: &PyType, + _cls: &Bound<'_, PyType>, year: i32, month: u8, day: u8, @@ -1623,7 +1748,7 @@ impl Epoch { #[cfg(feature = "python")] #[classmethod] fn init_from_gregorian_at_midnight( - _cls: &PyType, + _cls: &Bound<'_, PyType>, year: i32, month: u8, day: u8, @@ -1636,7 +1761,7 @@ impl Epoch { #[classmethod] /// Attempts to build an Epoch from the provided Gregorian date and time in TAI. fn maybe_init_from_gregorian_tai( - _cls: &PyType, + _cls: &Bound<'_, PyType>, year: i32, month: u8, day: u8, @@ -1654,7 +1779,7 @@ impl Epoch { /// NOTE: If the time scale is TDB, this function assumes that the SPICE format is used #[allow(clippy::too_many_arguments)] fn maybe_init_from_gregorian( - _cls: &PyType, + _cls: &Bound<'_, PyType>, year: i32, month: u8, day: u8, @@ -1672,7 +1797,7 @@ impl Epoch { /// Builds an Epoch from the provided Gregorian date and time in TAI. If invalid date is provided, this function will panic. /// Use maybe_from_gregorian_tai if unsure. fn init_from_gregorian_tai( - _cls: &PyType, + _cls: &Bound<'_, PyType>, year: i32, month: u8, day: u8, @@ -1687,14 +1812,24 @@ impl Epoch { #[cfg(feature = "python")] #[classmethod] /// Initialize from the Gregorian date at midnight in TAI. - fn init_from_gregorian_tai_at_midnight(_cls: &PyType, year: i32, month: u8, day: u8) -> Self { + fn init_from_gregorian_tai_at_midnight( + _cls: &Bound<'_, PyType>, + year: i32, + month: u8, + day: u8, + ) -> Self { Self::from_gregorian_tai_at_midnight(year, month, day) } #[cfg(feature = "python")] #[classmethod] /// Initialize from the Gregorian date at noon in TAI - fn init_from_gregorian_tai_at_noon(_cls: &PyType, year: i32, month: u8, day: u8) -> Self { + fn init_from_gregorian_tai_at_noon( + _cls: &Bound<'_, PyType>, + year: i32, + month: u8, + day: u8, + ) -> Self { Self::from_gregorian_tai_at_noon(year, month, day) } @@ -1702,7 +1837,7 @@ impl Epoch { #[classmethod] /// Initialize from the Gregorian date and time (without the nanoseconds) in TAI fn init_from_gregorian_tai_hms( - _cls: &PyType, + _cls: &Bound<'_, PyType>, year: i32, month: u8, day: u8, @@ -1717,7 +1852,7 @@ impl Epoch { #[classmethod] /// Attempts to build an Epoch from the provided Gregorian date and time in UTC. fn maybe_init_from_gregorian_utc( - _cls: &PyType, + _cls: &Bound<'_, PyType>, year: i32, month: u8, day: u8, @@ -1734,7 +1869,7 @@ impl Epoch { /// Builds an Epoch from the provided Gregorian date and time in TAI. If invalid date is provided, this function will panic. /// Use maybe_from_gregorian_tai if unsure. fn init_from_gregorian_utc( - _cls: &PyType, + _cls: &Bound<'_, PyType>, year: i32, month: u8, day: u8, @@ -1749,14 +1884,24 @@ impl Epoch { #[cfg(feature = "python")] #[classmethod] /// Initialize from Gregorian date in UTC at midnight - fn init_from_gregorian_utc_at_midnight(_cls: &PyType, year: i32, month: u8, day: u8) -> Self { + fn init_from_gregorian_utc_at_midnight( + _cls: &Bound<'_, PyType>, + year: i32, + month: u8, + day: u8, + ) -> Self { Self::from_gregorian_utc_at_midnight(year, month, day) } #[cfg(feature = "python")] #[classmethod] /// Initialize from Gregorian date in UTC at noon - fn init_from_gregorian_utc_at_noon(_cls: &PyType, year: i32, month: u8, day: u8) -> Self { + fn init_from_gregorian_utc_at_noon( + _cls: &Bound<'_, PyType>, + year: i32, + month: u8, + day: u8, + ) -> Self { Self::from_gregorian_utc_at_noon(year, month, day) } @@ -1764,7 +1909,7 @@ impl Epoch { #[classmethod] /// Initialize from the Gregorian date and time (without the nanoseconds) in UTC fn init_from_gregorian_utc_hms( - _cls: &PyType, + _cls: &Bound<'_, PyType>, year: i32, month: u8, day: u8, @@ -1778,7 +1923,7 @@ impl Epoch { #[cfg(feature = "python")] #[classmethod] /// Equivalent to `datetime.strptime`, refer to for format options - fn strptime(_cls: &PyType, epoch_str: String, format_str: String) -> PyResult { + fn strptime(_cls: &Bound<'_, PyType>, epoch_str: String, format_str: String) -> PyResult { Self::from_format_str(&epoch_str, &format_str).map_err(|e| PyErr::from(e)) } @@ -1807,32 +1952,11 @@ impl Epoch { format!("{}", Formatter::new(*self, ISO8601_STD))[..26].to_string() } - /// 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. @@ -1849,68 +1973,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.to_time_scale(TimeScale::TAI).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] @@ -1928,8 +2029,7 @@ impl Epoch { #[must_use] /// Returns this time in a Duration past J1900 counted in UTC pub fn to_utc_duration(&self) -> Duration { - // TAI = UTC + leap_seconds <=> UTC = TAI - leap_seconds - self.duration_since_j1900_tai - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second + self.to_time_scale(TimeScale::UTC).duration } #[must_use] @@ -1960,7 +2060,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] @@ -1998,7 +2098,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] @@ -2034,7 +2134,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] @@ -2086,7 +2186,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. ). @@ -2110,8 +2210,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. ). @@ -2135,7 +2234,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 @@ -2233,21 +2332,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] @@ -2266,25 +2351,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] @@ -2308,7 +2375,9 @@ impl Epoch { #[must_use] pub fn to_jde_et_duration(&self) -> Duration { - self.to_et_duration() + Unit::Day * (J1900_OFFSET + MJD_OFFSET) + J2000_TO_J1900_DURATION + self.to_et_duration() + + Unit::Day * (J1900_OFFSET + MJD_OFFSET) + + TimeScale::ET.prime_epoch_offset() } #[must_use] @@ -2318,7 +2387,9 @@ impl Epoch { #[must_use] pub fn to_jde_tdb_duration(&self) -> Duration { - self.to_tdb_duration() + Unit::Day * (J1900_OFFSET + MJD_OFFSET) + J2000_TO_J1900_DURATION + self.to_tdb_duration() + + Unit::Day * (J1900_OFFSET + MJD_OFFSET) + + TimeScale::TDB.prime_epoch_offset() } #[must_use] @@ -2365,7 +2436,7 @@ impl Epoch { /// // let dt_str = "2017-01-14T00:31:55 UTC"; /// // let dt = Epoch::from_gregorian_str(dt_str).unwrap() /// - /// let (y, m, d, h, min, s, _) = dt.as_gregorian_utc(); + /// let (y, m, d, h, min, s, _) = dt.to_gregorian_utc(); /// assert_eq!(y, 2017); /// assert_eq!(m, 1); /// assert_eq!(d, 14); @@ -2373,10 +2444,11 @@ impl Epoch { /// assert_eq!(min, 31); /// assert_eq!(s, 55); /// #[cfg(feature = "std")] - /// assert_eq!("2017-01-14T00:31:55 UTC", dt.as_gregorian_utc_str().to_owned()); + /// assert_eq!("2017-01-14T00:31:55 UTC", format!("{dt}")); /// ``` 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] @@ -2396,7 +2468,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")] @@ -2404,18 +2477,14 @@ 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")] #[must_use] /// Returns this time in a Duration past J1900 counted in UT1 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.time_scale = TimeScale::TAI; - me + Self::from_tai_duration(self.to_ut1_duration(provider)) } #[must_use] @@ -2438,7 +2507,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] @@ -2462,7 +2531,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] @@ -2479,15 +2548,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] @@ -2495,7 +2556,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 @@ -2606,7 +2667,7 @@ impl Epoch { /// Returns the duration since the start of the year pub fn duration_in_year(&self) -> Duration { let start_of_year = Self::from_gregorian(self.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] @@ -2618,7 +2679,7 @@ impl Epoch { #[must_use] /// Returns the number of Gregorian years of this epoch in the current time scale. pub fn year(&self) -> i32 { - Self::compute_gregorian(self.duration_since_j1900_tai).0 + Self::compute_gregorian(self.duration, self.time_scale).0 } #[must_use] @@ -2629,32 +2690,32 @@ impl Epoch { /// Returns the hours of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn hours(&self) -> u64 { - self.to_duration().decompose().2 + self.duration.decompose().2 } /// Returns the minutes of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn minutes(&self) -> u64 { - self.to_duration().decompose().3 + self.duration.decompose().3 } /// Returns the seconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn seconds(&self) -> u64 { - self.to_duration().decompose().4 + self.duration.decompose().4 } /// Returns the milliseconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn milliseconds(&self) -> u64 { - self.to_duration().decompose().5 + self.duration.decompose().5 } /// Returns the microseconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn microseconds(&self) -> u64 { - self.to_duration().decompose().6 + self.duration.decompose().6 } /// Returns the nanoseconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn nanoseconds(&self) -> u64 { - self.to_duration().decompose().7 + self.duration.decompose().7 } /// Returns a copy of self where the time is set to the provided hours, minutes, seconds @@ -2662,7 +2723,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, @@ -2686,7 +2747,7 @@ impl Epoch { /// /// 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), @@ -2695,9 +2756,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, @@ -2721,7 +2782,7 @@ impl Epoch { /// 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); /// // If the other Epoch is in another time scale, it does not matter, it will be converted to the correct time scale. - /// let other = other_utc.in_time_scale(TimeScale::TDB); + /// let other = other_utc.to_time_scale(TimeScale::TDB); /// /// assert_eq!( /// epoch.with_time_from(other), @@ -2730,7 +2791,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) = @@ -2755,7 +2816,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, @@ -2769,7 +2830,7 @@ impl Epoch { /// /// 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_strict_from(other), @@ -2777,8 +2838,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, @@ -2795,7 +2856,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() } @@ -2817,7 +2878,7 @@ impl Epoch { #[cfg(feature = "python")] #[classmethod] - fn system_now(_cls: &PyType) -> Result { + fn system_now(_cls: &Bound<'_, PyType>) -> Result { Self::now() } @@ -2858,43 +2919,12 @@ impl Epoch { } } - #[deprecated( - since = "3.8.0", - note = "Prefer using `format!(\"{}\", epoch)` directly" - )] - #[cfg(feature = "std")] - #[must_use] - /// Converts the Epoch to UTC Gregorian in the ISO8601 format. - pub fn to_gregorian_utc_str(&self) -> String { - format!("{}", self) - } - - #[deprecated( - since = "3.8.0", - note = "Prefer using `format!(\"{:x}\", epoch)` directly" - )] - #[cfg(feature = "std")] - #[must_use] - /// Converts the Epoch to TAI Gregorian in the ISO8601 format with " TAI" appended to the string - pub fn to_gregorian_tai_str(&self) -> String { - format!("{:x}", self) - } - #[cfg(feature = "std")] #[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!( @@ -2912,7 +2942,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", @@ -3054,7 +3086,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)) } }, @@ -3068,7 +3100,7 @@ impl fmt::Debug for Epoch { /// Print this epoch in Gregorian in the time scale used at initialization fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let (y, mm, dd, hh, min, s, nanos) = - Self::compute_gregorian(self.to_duration_since_j1900()); + Self::compute_gregorian(self.duration, self.time_scale); if nanos == 0 { write!( f, @@ -3089,7 +3121,8 @@ impl fmt::Display for Epoch { /// The default format of an epoch is in UTC fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let ts = TimeScale::UTC; - let (y, mm, dd, hh, min, s, nanos) = Self::compute_gregorian(self.to_utc_duration()); + let (y, mm, dd, hh, min, s, nanos) = + Self::compute_gregorian(self.to_duration_in_time_scale(ts), ts); if nanos == 0 { write!( f, @@ -3110,7 +3143,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, @@ -3131,7 +3165,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, @@ -3153,7 +3188,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, @@ -3175,7 +3210,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, @@ -3287,12 +3322,26 @@ fn div_rem_f64_test() { } #[test] -fn test_days_tdb_j2000() { +fn test_days_et_j2000() { + /* + WARNING: THIS ASSUMES THE UTC EPOCH in SPICE! + Verification via SPICE: load naif0012.txt (contains leap seconds until 2017-JAN-1) + In [6]: sp.str2et("2022-11-30 12:00:00") + Out[6]: 723081669.183061 + In [7]: from hifitime import * + In [8]: Unit.Second*723081669.183061 + Out[8]: 8369 days 1 min 9 s 183 ms 60 μs 992 ns @ 0x7fcd1559ef80 + In [9]: (Unit.Second*723081669.183061).to_unit(Unit.Day) + Out[9]: 8369.000800729873 + In [10]: (Unit.Second*723081669.183061).to_unit(Unit.Century) + Out[10]: 0.2291307542978747 + + */ let e = Epoch::from_tai_duration(Duration::from_parts(1, 723038437000000000)); - let days_d = e.to_tdb_days_since_j2000(); - let centuries_t = e.to_tdb_centuries_since_j2000(); - assert!((days_d - 8369.000800729798).abs() < f64::EPSILON); - assert!((centuries_t - 0.22913075429787266).abs() < f64::EPSILON); + let days_d = e.to_et_days_since_j2000(); + let centuries_t = e.to_et_centuries_since_j2000(); + assert!((days_d - 8369.000800729873).abs() < f64::EPSILON); + assert!((centuries_t - 0.2291307542978747).abs() < f64::EPSILON); } #[test] @@ -3330,7 +3379,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); @@ -3349,12 +3398,12 @@ fn formal_epoch_reciprocity_tai() { // Check that no error occurs on initialization let seconds: f64 = kani::any(); if seconds.is_finite() { - Epoch::from_tai_seconds(seconds); + let _ = Epoch::from_tai_seconds(seconds); } let days: f64 = kani::any(); if days.is_finite() { - Epoch::from_tai_days(days); + let _ = Epoch::from_tai_days(days); } } @@ -3375,7 +3424,7 @@ fn formal_epoch_reciprocity_tt() { // Check that no error occurs on initialization let seconds: f64 = kani::any(); if seconds.is_finite() { - Epoch::from_tt_seconds(seconds); + let _ = Epoch::from_tt_seconds(seconds); } // No TT Days initializer } @@ -3391,7 +3440,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.reference_epoch() - TimeScale::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. @@ -3400,13 +3449,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); } } @@ -3414,12 +3461,13 @@ fn formal_epoch_reciprocity_tdb() { #[cfg(kani)] #[kani::proof] +#[test] fn formal_epoch_reciprocity_gpst() { let duration: Duration = kani::any(); // GPST let time_scale: TimeScale = TimeScale::GPST; - let ts_offset = TimeScale::GPST.ref_epoch() - TimeScale::TAI.ref_epoch(); + let ts_offset = TimeScale::GPST.reference_epoch() - TimeScale::TAI.reference_epoch(); if duration > Duration::MIN + ts_offset && duration < Duration::MAX - ts_offset { let epoch: Epoch = Epoch::from_duration(duration, time_scale); assert_eq!(epoch.to_duration_in_time_scale(time_scale), duration); @@ -3428,10 +3476,10 @@ fn formal_epoch_reciprocity_gpst() { // Check that no error occurs on initialization let seconds: f64 = kani::any(); if seconds.is_finite() { - Epoch::from_gpst_seconds(seconds); + let _ = Epoch::from_gpst_seconds(seconds); } - Epoch::from_gpst_nanoseconds(kani::any()); + let _ = Epoch::from_gpst_nanoseconds(kani::any()); } #[cfg(kani)] @@ -3441,7 +3489,7 @@ fn formal_epoch_reciprocity_gst() { // GST let time_scale: TimeScale = TimeScale::GST; - let ts_offset = TimeScale::GST.ref_epoch() - TimeScale::TAI.ref_epoch(); + let ts_offset = TimeScale::GST.reference_epoch() - TimeScale::TAI.reference_epoch(); if duration > Duration::MIN + ts_offset && duration < Duration::MAX - ts_offset { let epoch: Epoch = Epoch::from_duration(duration, time_scale); assert_eq!(epoch.to_duration_in_time_scale(time_scale), duration); @@ -3450,15 +3498,15 @@ fn formal_epoch_reciprocity_gst() { // Check that no error occurs on initialization let seconds: f64 = kani::any(); if seconds.is_finite() { - Epoch::from_gst_seconds(seconds); + let _ = Epoch::from_gst_seconds(seconds); } let days: f64 = kani::any(); if days.is_finite() { - Epoch::from_gst_days(days); + let _ = Epoch::from_gst_days(days); } - Epoch::from_gst_nanoseconds(kani::any()); + let _ = Epoch::from_gst_nanoseconds(kani::any()); } #[cfg(kani)] @@ -3468,7 +3516,7 @@ fn formal_epoch_reciprocity_bdt() { // BDT let time_scale: TimeScale = TimeScale::BDT; - let ts_offset = TimeScale::BDT.ref_epoch() - TimeScale::TAI.ref_epoch(); + let ts_offset = TimeScale::BDT.reference_epoch() - TimeScale::TAI.reference_epoch(); if duration > Duration::MIN + ts_offset && duration < Duration::MAX - ts_offset { let epoch: Epoch = Epoch::from_duration(duration, time_scale); assert_eq!(epoch.to_duration_in_time_scale(time_scale), duration); @@ -3477,15 +3525,15 @@ fn formal_epoch_reciprocity_bdt() { // Check that no error occurs on initialization let seconds: f64 = kani::any(); if seconds.is_finite() { - Epoch::from_bdt_seconds(seconds); + let _ = Epoch::from_bdt_seconds(seconds); } let days: f64 = kani::any(); if days.is_finite() { - Epoch::from_bdt_days(days); + let _ = Epoch::from_bdt_days(days); } - Epoch::from_bdt_nanoseconds(kani::any()); + let _ = Epoch::from_bdt_nanoseconds(kani::any()); } #[cfg(kani)] @@ -3495,15 +3543,15 @@ fn formal_epoch_julian() { if days.is_finite() { // The initializers will fail on subnormal days. - Epoch::from_mjd_bdt(days); - Epoch::from_mjd_gpst(days); - Epoch::from_mjd_gst(days); - Epoch::from_mjd_tai(days); - Epoch::from_jde_bdt(days); - Epoch::from_jde_gpst(days); - Epoch::from_jde_gst(days); - Epoch::from_jde_tai(days); - Epoch::from_jde_et(days); - Epoch::from_jde_tai(days); + let _ = Epoch::from_mjd_bdt(days); + let _ = Epoch::from_mjd_gpst(days); + let _ = Epoch::from_mjd_gst(days); + let _ = Epoch::from_mjd_tai(days); + let _ = Epoch::from_jde_bdt(days); + let _ = Epoch::from_jde_gpst(days); + let _ = Epoch::from_jde_gst(days); + let _ = Epoch::from_jde_tai(days); + let _ = Epoch::from_jde_et(days); + let _ = Epoch::from_jde_tai(days); } } diff --git a/src/errors.rs b/src/errors.rs index b8d9e03..c725329 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 195f498..94ba9bf 100644 --- a/src/leap_seconds.rs +++ b/src/leap_seconds.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 0c613ca..dd04034 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) 2023 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 7402011..8ba346e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,8 @@ #![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] - /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 +36,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,37 +52,13 @@ 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; -/// The duration between J2000 and J1900: one century **minus** twelve hours. J1900 starts at _noon_ but J2000 is at midnight. +/// The duration between J2000 and J1900 is exactly one century, both references start at noon. pub const J2000_TO_J1900_DURATION: Duration = Duration { - centuries: 0, - nanoseconds: 3_155_716_800_000_000_000, -}; - -/// The Ephemeris Time reference epoch J2000. -pub const J2000_REF_EPOCH_ET: Epoch = Epoch { - duration_since_j1900_tai: Duration { - centuries: 0, - nanoseconds: 3_155_716_767_816_072_748, - }, - time_scale: TimeScale::ET, -}; - -/// The Dynamic Barycentric Time reference epoch J2000. -pub const J2000_REF_EPOCH_TDB: Epoch = Epoch { - duration_since_j1900_tai: Duration { - centuries: 0, - nanoseconds: 3_155_716_767_816_072_704, - }, - time_scale: TimeScale::ET, + centuries: 1, + nanoseconds: 0, }; // Epoch formatting module is called `efmt` to avoid collision with `std::fmt` and `core::fmt`. @@ -121,15 +98,10 @@ 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 8837dae..c2f2b2f 100644 --- a/src/month.rs +++ b/src/month.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 9ebbde2..f28350b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 abbee37..f8161e9 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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. @@ -23,7 +23,7 @@ impl std::convert::From for PyErr { } #[pymodule] -fn hifitime(_py: Python, m: &PyModule) -> PyResult<()> { +fn hifitime(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/system_time.rs b/src/system_time.rs index 1b39fdb..9b0646c 100644 --- a/src/system_time.rs +++ b/src/system_time.rs @@ -1,5 +1,9 @@ use crate::{Duration, Errors}; +/// Converts the webtime Duration into a hifitime Duration. +/// +/// Clippy thinks these are the same type, but they aren't. +#[allow(clippy::unnecessary_fallible_conversions)] pub(crate) fn duration_since_unix_epoch() -> Result { web_time::SystemTime::now() .duration_since(web_time::SystemTime::UNIX_EPOCH) diff --git a/src/timescale/fmt.rs b/src/timescale/fmt.rs new file mode 100644 index 0000000..28d83f1 --- /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 0000000..5d53ab9 --- /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 52% rename from src/timescale.rs rename to src/timescale/mod.rs index 39bbe52..c8ea7c8 100644 --- a/src/timescale.rs +++ b/src/timescale/mod.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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,19 @@ 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, -}; +use crate::{Duration, Epoch, Unit, 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. . pub const GPST_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { centuries: 0, nanoseconds: 2_524_953_619_000_000_000, // XXX @@ -40,19 +36,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_819_000_000_000, // 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_433_000_000_000, //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; @@ -63,9 +64,12 @@ pub const UNIX_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { nanoseconds: 2_208_988_800_000_000_000, }); +/// Reference year of the Hifitime prime epoch. +pub(crate) const HIFITIME_REF_YEAR: i32 = 1900; + /// 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 { @@ -79,34 +83,19 @@ pub enum TimeScale { TDB, /// Universal Coordinated Time UTC, - /// GPS Time scale + /// GPS Time scale whose reference epoch is UTC midnight between 05 January and 06 January 1980; cf. . |UTC - TAI| = 19 Leap Seconds on that day. GPST, /// Galileo Time scale 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,50 +115,47 @@ 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 { - 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, - // 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, + /// Returns this time scale's reference epoch: Time Scale initialization date, + /// expressed as an Epoch in TAI + pub const fn reference_epoch(self) -> Epoch { + Epoch { + duration: Duration::ZERO, + time_scale: self, } } -} -impl fmt::Display for TimeScale { - /// Prints given TimeScale - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + /// Returns the duration between this time scale's reference epoch and the hifitime "prime epoch" of 1900-01-01 00:00:00 TAI (the NTP prime epoch). + /// This is used to compute the Gregorian date representations in any time scale. + pub(crate) const fn prime_epoch_offset(self) -> Duration { 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::ET | TimeScale::TDB => { + // Both ET and TDB are defined at J2000, which is 2000-01-01 12:00:00 and there were only 36524 days in the 20th century. + // Hence, this math is the output of (Unit.Century*1 + Unit.Hour*12 - Unit.Day*1).to_parts() via Hifitime in Python. + Duration { + centuries: 0, + nanoseconds: 3155716800000000000, + } + } + TimeScale::GPST | TimeScale::QZSST => Duration { + centuries: 0, + nanoseconds: 2_524_953_619_000_000_000, + }, + TimeScale::GST => Duration { + centuries: 0, + nanoseconds: 3_144_268_819_000_000_000, + }, + TimeScale::BDT => Duration { + centuries: 1, + nanoseconds: 189_302_433_000_000_000, + }, + _ => Duration::ZERO, } } -} -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}"), - } + pub(crate) fn gregorian_epoch_offset(self) -> Duration { + let prime_offset = self.prime_epoch_offset(); + + prime_offset - prime_offset.subdivision(Unit::Second).unwrap() } } @@ -217,61 +203,50 @@ 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(); + #[test] + #[cfg(feature = "std")] + fn test_ref_epoch() { + use crate::{Duration, Epoch, Unit}; + let prime_e = Epoch::from_duration(Duration::ZERO, TimeScale::TAI); + assert_eq!(prime_e.duration, Duration::ZERO); + assert_eq!(format!("{prime_e:?}"), "1900-01-01T00:00:00 TAI"); + // NOTE: There are only 36524 days in the 20th century, but one century is 36425, so we "overflow" the next century by one day! + assert_eq!( + format!("{:?}", prime_e + Unit::Century * 1), + "2000-01-02T00:00:00 TAI" + ); + + assert_eq!( + format!("{:?}", TimeScale::ET.reference_epoch()), + "2000-01-01T12:00:00 ET" + ); + } } diff --git a/src/timeseries.rs b/src/timeseries.rs index c31dc61..f9cea4a 100644 --- a/src/timeseries.rs +++ b/src/timeseries.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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. @@ -13,17 +13,12 @@ use super::{Duration, Epoch}; use core::fmt; #[cfg(not(feature = "std"))] +#[allow(unused_imports)] // Import is indeed used. use num_traits::Float; #[cfg(feature = "python")] use pyo3::prelude::*; -#[cfg(feature = "python")] -use pyo3::pyclass::CompareOp; - -#[cfg(feature = "python")] -use pyo3::exceptions::PyTypeError; - /* NOTE: This is taken from itertools: https://docs.rs/itertools-num/0.1.3/src/itertools_num/linspace.rs.html#78-93 . @@ -319,7 +314,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; @@ -336,13 +331,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 { @@ -354,7 +356,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 ac6ef85..290d3b4 100644 --- a/src/timeunits.rs +++ b/src/timeunits.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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. @@ -11,15 +11,17 @@ use core::ops::{Add, Mul, Sub}; #[cfg(not(feature = "std"))] +#[allow(unused_imports)] // Import is indeed used. use num_traits::Float; #[cfg(feature = "python")] 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 +35,7 @@ pub enum Unit { Minute, Hour, Day, + Week, /// 36525 days, is the number of days per century in the Julian calendar Century, } @@ -67,6 +70,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 +164,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 +207,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 +230,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 +245,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 +286,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 +317,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 9c491b2..af4aba6 100644 --- a/src/ut1.rs +++ b/src/ut1.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 484f3ac..a2b2f18 100644 --- a/src/weekday.rs +++ b/src/weekday.rs @@ -1,6 +1,6 @@ /* * Hifitime, part of the Nyx Space tools - * Copyright (C) 2023 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 dbbb1b9..1f391dd 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -4,14 +4,22 @@ extern crate core; use hifitime::{ is_gregorian_valid, Duration, Epoch, Errors, ParsingErrors, TimeScale, TimeUnits, Unit, Weekday, BDT_REF_EPOCH, DAYS_GPS_TAI_OFFSET, DAYS_PER_YEAR, 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_OFFSET, 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}; use core::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 @@ -127,6 +135,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); @@ -162,10 +171,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. @@ -329,10 +340,22 @@ fn datetime_invalid_dates() { #[test] fn gpst() { + use core::str::FromStr; let ref_gps = Epoch::from_gregorian_utc_at_midnight(1980, 01, 06); + let gpst_from_str = Epoch::from_str("1980-01-06T00:00:00 GPST").unwrap(); + assert_eq!( + gpst_from_str.duration, + Duration::ZERO, + "Initialization at ref epoch should be zero" + ); + + assert_eq!(gpst_from_str, Epoch::from_gpst_seconds(0.0),); + // Test 1sec into GPS timescale let gps_1sec = Epoch::from_gpst_seconds(1.0); + assert_eq!(format!("{gps_1sec:?}"), "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 @@ -358,42 +381,62 @@ 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 + format!("{}", TimeScale::GPST.reference_epoch()), + "1980-01-06T00:00:00 UTC" + ); + assert_eq!( + format!("{:x}", TimeScale::GPST.reference_epoch()), + "1980-01-06T00:00:19 TAI" + ); + assert_eq!(format!("{:o}", TimeScale::GPST.reference_epoch()), "0"); + assert_eq!( + format!("{:?}", TimeScale::GPST.reference_epoch()), + "1980-01-06T00:00:00 GPST" ); assert_eq!( - gps_epoch.to_tai_seconds(), - Epoch::from_gregorian_utc_at_midnight(1980, 1, 6).to_tai_seconds() + Epoch::from_gpst_days(0.0).to_duration_since_j1900(), + GPST_REF_EPOCH.duration ); + + // assert_eq!( + // 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); @@ -457,7 +500,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!( @@ -502,7 +545,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!( @@ -561,11 +604,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 @@ -748,17 +791,17 @@ 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 - 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); - assert!(dbg!(sp_ex.to_tai_seconds() - from_et_s.to_tai_seconds()).abs() < 3e-6); + // Validate UTC to ET when initializing from UTC + assert!((sp_ex.to_et_seconds() - expected_et_s).abs() < EPSILON); + assert!((sp_ex.to_tdb_seconds() - expected_et_s).abs() < max_tdb_et_err.to_seconds()); + assert!((sp_ex.to_jde_utc_days() - 2455964.9739931).abs() < 1e-7); + assert!((sp_ex.to_tai_seconds() - from_et_s.to_tai_seconds()).abs() < 3e-6); // Second example let sp_ex = Epoch::from_gregorian_utc_at_midnight(2002, 2, 7); let expected_et_s = 66_312_064.184_938_76; - 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!((sp_ex.to_et_seconds() - expected_et_s).abs() < max_prec.to_seconds()); + assert!((sp_ex.to_tdb_seconds() - expected_et_s).abs() < max_tdb_et_err.to_seconds()); assert!( (sp_ex.to_tai_seconds() - Epoch::from_tdb_seconds(expected_et_s).to_tai_seconds()).abs() < 1e-5 @@ -767,11 +810,10 @@ fn spice_et_tdb() { // Third example let sp_ex = Epoch::from_gregorian_utc_hms(1996, 2, 7, 11, 22, 33); let expected_et_s = -123_035_784.815_060_48; - 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!((sp_ex.to_et_seconds() - expected_et_s).abs() < max_prec.to_seconds()); + assert!((sp_ex.to_tdb_seconds() - expected_et_s).abs() < max_tdb_et_err.to_seconds()); assert!( - dbg!(sp_ex.to_tai_seconds() - Epoch::from_tdb_seconds(expected_et_s).to_tai_seconds()) - .abs() + (sp_ex.to_tai_seconds() - Epoch::from_tdb_seconds(expected_et_s).to_tai_seconds()).abs() < 1e-5 ); // Fourth example @@ -786,8 +828,8 @@ fn spice_et_tdb() { */ let sp_ex = Epoch::from_gregorian_utc_hms(2015, 2, 7, 11, 22, 33); let expected_et_s = 476580220.1849411; - 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!((sp_ex.to_et_seconds() - expected_et_s).abs() < max_prec.to_seconds()); + assert!((sp_ex.to_tdb_seconds() - expected_et_s).abs() < max_tdb_et_err.to_seconds()); assert!((sp_ex.to_jde_utc_days() - 2457060.9739931).abs() < 1e-7); // JDE TDB tests @@ -861,10 +903,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()) ); @@ -888,13 +929,37 @@ 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())); -} +// #[test] +// 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::from_seconds(12349.433_f64); +// let epoch = Epoch::from_duration(duration, ts); +// let utc_epoch = epoch.to_time_scale(TimeScale::UTC); +// // let utc_epoch = Epoch::from_duration(duration, TimeScale::UTC).to_time_scale(ts); +// assert_eq!( +// (epoch - utc_epoch).abs().to_seconds(), +// leap_t0 as f64, +// "|{:?} - {:?}| should be {} secs for {ts}", +// epoch, +// utc_epoch, +// leap_t0 +// ); +// } +// } #[test] fn test_rfc3339() { @@ -971,11 +1036,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"), @@ -983,21 +1048,18 @@ fn test_format() { } } - assert_eq!( - format!("{epoch:?}"), - match ts { - TimeScale::TAI => format!("{epoch:x}"), - TimeScale::ET => format!("{epoch:E}"), - TimeScale::TDB => format!("{epoch:e}"), - TimeScale::TT => format!("{epoch:X}"), - TimeScale::UTC => format!("{epoch}"), - TimeScale::GPST => format!("{epoch:x}").replace("TAI", "GPST"), - TimeScale::GST => format!("{epoch:x}").replace("TAI", "GST"), - TimeScale::BDT => format!("{epoch:x}").replace("TAI", "BDT"), - TimeScale::QZSST => format!("{epoch:x}").replace("TAI", "QZSST"), - _ => format!("{epoch:x}").replace("TAI", "GNSS"), // non exhaustive GNSS time scales - } - ); + let with_direct_fmt = match ts { + TimeScale::TAI => Some(format!("{epoch:x}")), + TimeScale::ET => Some(format!("{epoch:E}")), + TimeScale::TDB => Some(format!("{epoch:e}")), + TimeScale::TT => Some(format!("{epoch:X}")), + TimeScale::UTC => Some(format!("{epoch}")), + _ => None, + }; + + if let Some(fmt) = with_direct_fmt { + assert_eq!(format!("{epoch:?}"), fmt, "issue with {ts}"); + } // Check that we can correctly parse the date we print. match Epoch::from_str(&format!("{epoch:?}")) { @@ -1008,8 +1070,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!( @@ -1017,15 +1079,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 ) } }; @@ -1040,8 +1102,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] @@ -1049,9 +1111,9 @@ fn ops() { // Test adding a second let sp_ex: Epoch = Epoch::from_gregorian_utc_hms(2012, 2, 7, 11, 22, 33) + Unit::Second * 1.0; let expected_et_s = 381_885_819.184_935_87; - assert!(dbg!(sp_ex.to_tdb_seconds() - expected_et_s - 1.0).abs() < 2.6e-6); + assert!((sp_ex.to_et_seconds() - expected_et_s - 1.0).abs() < 2.6e-6); let sp_ex: Epoch = sp_ex - Unit::Second * 1.0; - assert!((sp_ex.to_tdb_seconds() - expected_et_s).abs() < 2.6e-6); + assert!((sp_ex.to_et_seconds() - expected_et_s).abs() < 2.6e-6); } #[test] @@ -1238,10 +1300,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, @@ -1324,7 +1385,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); @@ -1425,7 +1486,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}"); } }; @@ -1505,7 +1566,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), @@ -1563,7 +1624,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), @@ -1583,7 +1644,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), @@ -1591,10 +1652,10 @@ 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.reference_epoch() - TimeScale::QZSST.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) @@ -1602,7 +1663,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), @@ -1623,7 +1684,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), @@ -1635,7 +1696,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), @@ -1647,7 +1708,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), @@ -1659,7 +1720,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), @@ -1668,12 +1729,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.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), @@ -1683,12 +1744,12 @@ 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); + TimeScale::GST.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), @@ -1701,7 +1762,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.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(), @@ -1709,7 +1770,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), @@ -1723,7 +1784,7 @@ fn test_time_of_week() { TimeScale::BDT, ); let expected_tai = - TimeScale::BDT.ref_epoch() + Duration::from_days(70.0) + Duration::from_hours(36.25); + TimeScale::BDT.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(), @@ -1731,7 +1792,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), @@ -1752,7 +1813,7 @@ fn test_day_of_year() { TimeScale::ET, TimeScale::TDB, ] { - let epoch = utc_epoch.in_time_scale(*ts); + let epoch = utc_epoch.to_time_scale(*ts); let (year, days) = epoch.year_days_of_year(); let rebuilt = Epoch::from_day_of_year(year, days, *ts); if *ts == TimeScale::ET || *ts == TimeScale::TDB { @@ -1879,6 +1940,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() { @@ -1926,7 +1997,7 @@ fn regression_test_gh_272() { let (years, day_of_year) = epoch.year_days_of_year(); - assert!(dbg!(day_of_year) < DAYS_PER_YEAR); + assert!((day_of_year) < DAYS_PER_YEAR); assert!(day_of_year > 0.0); assert_eq!(day_of_year, 355.0); diff --git a/tests/python/test_duration.py b/tests/python/test_duration.py new file mode 100644 index 0000000..7733883 --- /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