From 621de458b64eb6b4f44d172ab77b324c855f06fa Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Thu, 13 Apr 2023 23:14:32 -0600 Subject: [PATCH 1/9] Add strptime and strftime in Python and from_format_str in Rust --- Cargo.toml | 2 +- src/epoch.rs | 24 ++++++++++++++++++++++++ src/ut1.rs | 7 +++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fc331a37..69408b20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ name = "hifitime" serde = {version = "1.0.155", optional = true} serde_derive = {version = "1.0.155", optional = true} der = {version = "0.6.1", features = ["derive", "real"], optional = true} -pyo3 = { version = "0.18.1", features = ["extension-module"], optional = true} +pyo3 = { version = "0.18.1", features = ["extension-module"], optional = true } num-traits = {version = "0.2.15", default-features = false, features = ["libm"]} lexical-core = {version = "0.8.5", default-features = false, features = ["parse-integers", "parse-floats"]} reqwest = { version = "0.11", features = ["blocking", "json"], optional = true} diff --git a/src/epoch.rs b/src/epoch.rs index ee22dbbb..12e53c70 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -34,6 +34,9 @@ use pyo3::prelude::*; #[cfg(feature = "python")] use pyo3::pyclass::CompareOp; +#[cfg(feature = "python")] +use pyo3::types::PyType; + #[cfg(feature = "python")] use crate::leap_seconds_file::LeapSecondsFile; @@ -1041,6 +1044,13 @@ impl Epoch { format.parse(s_in) } + /// Initializes an Epoch from the Format as a string. + pub fn from_format_str(s_in: &str, format_str: &str) -> Result { + Format::from_str(format_str) + .map_err(Errors::ParseError)? + .parse(s_in) + } + #[cfg(feature = "ut1")] #[must_use] /// Initialize an Epoch from the provided UT1 duration since 1900 January 01 at midnight @@ -1649,6 +1659,20 @@ impl Epoch { Self::from_gregorian_utc_hms(year, month, day, hour, minute, second) } + #[cfg(feature = "python")] + #[classmethod] + /// Equivalent to `datetime.strptime, refer to for format options + fn strptime(_cls: &PyType, epoch_str: String, format_str: String) -> PyResult { + Self::from_format_str(epoch_str, format_str).map_err(|e| PyErr::from(e)) + } + + #[cfg(feature = "python")] + /// Equivalent to `datetime.strftime, refer to for format options + fn strftime(&self, format_str: String) -> PyResult { + let fmt = Format::from_str(format_str).map_err(|e| PyErr::from(e))?; + Ok(format!("{}", Formatter::new(self, fmt))) + } + /// Returns this epoch with respect to the time scale this epoch was created in. /// This is needed to correctly perform duration conversions in dynamical time scales (e.g. TDB). /// diff --git a/src/ut1.rs b/src/ut1.rs index a4484f48..e88afb46 100644 --- a/src/ut1.rs +++ b/src/ut1.rs @@ -99,11 +99,10 @@ impl Ut1Provider { return Err(Errors::ParseError(ParsingErrors::UnknownFormat)); } - let mjd_tai_days: f64; - match lexical_core::parse(data[0].trim().as_bytes()) { - Ok(val) => mjd_tai_days = val, + let mjd_tai_days: f64 = match lexical_core::parse(data[0].trim().as_bytes()) { + Ok(val) => val, Err(_) => return Err(Errors::ParseError(ParsingErrors::ValueError)), - } + }; let delta_ut1_ms: f64; match lexical_core::parse(data[3].trim().as_bytes()) { From b3ffe6d96937c47a9d2a861eb42984c43dab6151 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Thu, 13 Apr 2023 23:53:43 -0600 Subject: [PATCH 2/9] Add python test --- .github/workflows/python.yml | 117 +++++++++++++++++++++++++++++----- examples/python/basic.py | 25 +++++--- examples/python/timescales.py | 79 ++++++++++++++--------- src/epoch.rs | 9 ++- tests/.gitignore | 1 + tests/python/test_epoch.py | 46 +++++++++++++ 6 files changed, 219 insertions(+), 58 deletions(-) create mode 100644 tests/.gitignore create mode 100644 tests/python/test_epoch.py diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 2bde600c..581e2efb 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -1,70 +1,157 @@ -name: Python lib +# This file is autogenerated by maturin v0.14.17 +# To update, run +# +# maturin generate-ci --pytest -o .github/workflows/python.yml github +# +name: CI on: push: branches: + - main - master tags: - - "*" + - '*' pull_request: workflow_dispatch: +permissions: + contents: read + jobs: linux: runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] steps: - uses: actions/checkout@v3 - - uses: messense/maturin-action@v1 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter -F python + sccache: 'true' manylinux: auto - command: build - args: --release --sdist -o dist --find-interpreter -F python - - name: Upload wheels uses: actions/upload-artifact@v3 with: name: wheels path: dist + - name: pytest + if: ${{ startsWith(matrix.target, 'x86_64') }} + shell: bash + run: | + set -e + pip install hifitime --find-links dist --force-reinstall + pip install pytest + pytest + - name: pytest + if: ${{ !startsWith(matrix.target, 'x86') && matrix.target != 'ppc64' }} + uses: uraimo/run-on-arch-action@v2.5.0 + with: + arch: ${{ matrix.target }} + distro: ubuntu22.04 + githubToken: ${{ github.token }} + install: | + apt-get update + apt-get install -y --no-install-recommends python3 python3-pip + pip3 install -U pip pytest + run: | + set -e + pip3 install hifitime --find-links dist --force-reinstall + pytest windows: runs-on: windows-latest + strategy: + matrix: + target: [x64, x86] steps: - uses: actions/checkout@v3 - - uses: messense/maturin-action@v1 + - uses: actions/setup-python@v4 with: - command: build - args: --release -o dist --find-interpreter -F python + python-version: '3.10' + architecture: ${{ matrix.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter -F python + sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v3 with: name: wheels path: dist + - name: pytest + if: ${{ !startsWith(matrix.target, 'aarch64') }} + shell: bash + run: | + set -e + pip install hifitime --find-links dist --force-reinstall + pip install pytest + pytest macos: runs-on: macos-latest + strategy: + matrix: + target: [x86_64, aarch64] steps: - uses: actions/checkout@v3 - - uses: messense/maturin-action@v1 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 with: - command: build - args: --release -o dist --universal2 --find-interpreter -F python + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v3 with: name: wheels path: dist + - name: pytest + if: ${{ !startsWith(matrix.target, 'aarch64') }} + shell: bash + run: | + set -e + pip install hifitime --find-links dist --force-reinstall + pip install pytest + pytest + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist release: name: Release runs-on: ubuntu-latest - if: github.ref_type == 'tag' - needs: [macos, windows, linux] + if: "startsWith(github.ref, 'refs/tags/')" + needs: [linux, windows, macos, sdist] steps: - uses: actions/download-artifact@v3 with: name: wheels - name: Publish to PyPI - uses: messense/maturin-action@v1 + uses: PyO3/maturin-action@v1 env: MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} with: diff --git a/examples/python/basic.py b/examples/python/basic.py index da9b1ad7..c2e48209 100644 --- a/examples/python/basic.py +++ b/examples/python/basic.py @@ -1,4 +1,4 @@ -''' +""" * Hifitime, part of the Nyx Space tools * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) * This Source Code Form is subject to the terms of the Apache @@ -6,7 +6,7 @@ * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. * * Documentation: https://nyxspace.com/ -''' +""" from hifitime import Duration, Epoch, TimeSeries, TimeScale, Unit @@ -30,7 +30,7 @@ print(f"min positive = {Duration.min_positive()}") # And more importantly, it does not suffer from rounding issues, even when the duration are very large. - print(f"Max duration: {Duration.max()}") # 1196851200 days + print(f"Max duration: {Duration.max()}") # 1196851200 days print(f"Nanosecond precision: {Duration.max() - Unit.Nanosecond * 1.0}") assert f"{Unit.Day * 1.2}" == "1 days 4 h 48 min" assert f"{Unit.Day * 1.200001598974}" == "1 days 4 h 48 min 138 ms 151 μs 353 ns" @@ -40,16 +40,21 @@ # You can also get all of the epochs between two different epochs at a specific step size. # This is like numpy's `linspace` with high fidelity durations - time_series = TimeSeries(Epoch.system_now(), - Epoch.system_now() + Unit.Day * 0.3, - Unit.Hour * 0.5, - inclusive=True) + time_series = TimeSeries( + Epoch.system_now(), + Epoch.system_now() + Unit.Day * 0.3, + Unit.Hour * 0.5, + inclusive=True, + ) print(time_series) - for (num, epoch) in enumerate(time_series): + for num, epoch in enumerate(time_series): print(f"#{num}:\t{epoch}") e1 = Epoch.system_now() e3 = e1 + Unit.Day * 1.5998 epoch_delta = e3.timedelta(e1) - assert epoch_delta == Unit.Day * 1 + Unit.Hour * 14 + Unit.Minute * 23 + Unit.Second * 42.720 - print(epoch_delta) \ No newline at end of file + assert ( + epoch_delta + == Unit.Day * 1 + Unit.Hour * 14 + Unit.Minute * 23 + Unit.Second * 42.720 + ) + print(epoch_delta) diff --git a/examples/python/timescales.py b/examples/python/timescales.py index b915e0f1..162e40a9 100644 --- a/examples/python/timescales.py +++ b/examples/python/timescales.py @@ -1,4 +1,4 @@ -''' +""" * Hifitime, part of the Nyx Space tools * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) * This Source Code Form is subject to the terms of the Apache @@ -6,35 +6,46 @@ * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. * * Documentation: https://nyxspace.com/ -''' +""" try: import plotly.express as px except ImportError: - print('\nThis script requires `plotly` (pip install plotly)\n') + print("\nThis script requires `plotly` (pip install plotly)\n") try: import pandas as pd except ImportError: - print('\nThis script requires `pandas` (pip install pandas)\n') + print("\nThis script requires `pandas` (pip install pandas)\n") from hifitime import Epoch, TimeSeries, Unit if __name__ == "__main__": - ''' + """ The purpose of this script is to plot the differences between time systems. It will plot the difference between TAI, UTC, and all of the other timescales supported by hifitime. Then, as a separate plot, it will remove the UTC line to make the difference between other timescales more evident. - ''' + """ # Start by building a time series from 1970 until 2023 with a step of 30 days. - ts = TimeSeries(Epoch('1970-01-01 00:00:00 UTC'), Epoch('2023-01-01 00:00:00 UTC'), - Unit.Day * 30.0, True) + ts = TimeSeries( + Epoch("1970-01-01 00:00:00 UTC"), + Epoch("2023-01-01 00:00:00 UTC"), + Unit.Day * 30.0, + True, + ) # Define the storage array data = [] # Define the columns - columns = ["UTC Epoch", "Δ TT (s)", "Δ ET (s)", "Δ TDB (s)", "Δ UTC (s)", "ET-TDB (s)"] + columns = [ + "UTC Epoch", + "Δ TT (s)", + "Δ ET (s)", + "Δ TDB (s)", + "Δ UTC (s)", + "ET-TDB (s)", + ] for epoch in ts: delta_utc = epoch.to_utc_duration() - epoch.to_tai_duration() @@ -45,34 +56,42 @@ # Convert the epoch into a pandas datetime pd_epoch = pd.to_datetime(str(epoch)) # Build the pandas series - data.append([ - pd_epoch, - delta_tt.to_seconds(), - delta_et.to_seconds(), - delta_tdb.to_seconds(), - delta_utc.to_seconds(), - delta_et_tdb.to_seconds(), - ]) + data.append( + [ + pd_epoch, + delta_tt.to_seconds(), + delta_et.to_seconds(), + delta_tdb.to_seconds(), + delta_utc.to_seconds(), + delta_et_tdb.to_seconds(), + ] + ) df = pd.DataFrame(data, columns=columns) - fig = px.line(df, - x='UTC Epoch', - y=columns[1:-1], - title="Time scale deviation with respect to TAI") + fig = px.line( + df, + x="UTC Epoch", + y=columns[1:-1], + title="Time scale deviation with respect to TAI", + ) fig.write_html("./target/time-scale-deviation.html") fig.show() - fig = px.line(df, - x='UTC Epoch', - y=columns[1:-2], - title="Time scale deviation with respect to TAI (excl. UTC)") + fig = px.line( + df, + x="UTC Epoch", + y=columns[1:-2], + title="Time scale deviation with respect to TAI (excl. UTC)", + ) fig.write_html("./target/time-scale-deviation-no-utc.html") fig.show() - fig = px.line(df, - x='UTC Epoch', - y=columns[-1], - title="Time scale deviation of TDB and ET with respect to TAI") + fig = px.line( + df, + x="UTC Epoch", + y=columns[-1], + title="Time scale deviation of TDB and ET with respect to TAI", + ) fig.write_html("./target/time-scale-deviation-tdb-et.html") - fig.show() \ No newline at end of file + fig.show() diff --git a/src/epoch.rs b/src/epoch.rs index 12e53c70..25f9308e 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -1663,14 +1663,17 @@ impl Epoch { #[classmethod] /// Equivalent to `datetime.strptime, refer to for format options fn strptime(_cls: &PyType, epoch_str: String, format_str: String) -> PyResult { - Self::from_format_str(epoch_str, format_str).map_err(|e| PyErr::from(e)) + Self::from_format_str(&epoch_str, &format_str).map_err(|e| PyErr::from(e)) } #[cfg(feature = "python")] /// Equivalent to `datetime.strftime, refer to for format options fn strftime(&self, format_str: String) -> PyResult { - let fmt = Format::from_str(format_str).map_err(|e| PyErr::from(e))?; - Ok(format!("{}", Formatter::new(self, fmt))) + use crate::efmt::Formatter; + let fmt = Format::from_str(&format_str) + .map_err(Errors::ParseError) + .map_err(|e| PyErr::from(e))?; + Ok(format!("{}", Formatter::new(*self, fmt))) } /// Returns this epoch with respect to the time scale this epoch was created in. diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..ed8ebf58 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/tests/python/test_epoch.py b/tests/python/test_epoch.py new file mode 100644 index 00000000..0c57509d --- /dev/null +++ b/tests/python/test_epoch.py @@ -0,0 +1,46 @@ +from hifitime import Epoch, TimeSeries, Unit +from datetime import datetime + + +def test_strtime(): + """ + Tests both strp and strftime + """ + epoch = Epoch("2023-04-13 23:31:17 UTC") + dt = datetime(2023, 4, 13, 23, 31, 17) + + epoch_fmt = epoch.strftime("%A, %d %B %Y %H:%M:%S") + dt_fmt = dt.strftime("%A, %d %B %Y %H:%M:%S") + + assert epoch_fmt == dt_fmt + + assert Epoch.strptime(dt_fmt, "%A, %d %B %Y %H:%M:%S") == epoch + + +def test_utcnow(): + epoch = Epoch.system_now() + dt = datetime.utcnow() + + # Hifitime uses a different clock to Python and print down to the nanosecond + assert dt.isoformat()[:24] == f"{epoch}"[:24] + + +def test_time_series(): + """ + Time series are really cool way to iterate through a time without skipping a beat. + """ + + nye = Epoch("2022-12-31 23:59:00 UTC") + + time_series = TimeSeries( + nye, + nye + Unit.Second * 10, + Unit.Second * 1, + inclusive=True, + ) + print(time_series) + + for num, epoch in enumerate(time_series): + print(f"#{num}:\t{epoch}") + + assert num == 10 From 3a9a5a9a618d73f2b6236a3670fac03ab6b9cff5 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Fri, 14 Apr 2023 00:18:28 -0600 Subject: [PATCH 3/9] Uncovered bug in time series over a leap second --- src/timeseries.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/timeseries.rs b/src/timeseries.rs index c1f82722..d2084e5a 100644 --- a/src/timeseries.rs +++ b/src/timeseries.rs @@ -353,4 +353,21 @@ mod tests { assert_eq!(times.len(), steps as usize); assert_eq!(times.len(), times.size_hint().0); } + + #[test] + fn ts_over_leap_second() { + panic!("This is a bug!"); + let start = Epoch::from_gregorian_utc(2016, 12, 31, 23, 59, 59, 0); + let times = TimeSeries::exclusive(start, start + Unit::Second * 5, Unit::Second * 1); + let mut cnt = 0; + + for epoch in times { + #[cfg(feature = "std")] + println!("{:?}", epoch); + assert!(start < epoch); + cnt += 1; + } + + assert_eq!(cnt, 10); + } } From 33ba4a842d48d4574f1c63068024fd095161e75a Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 15 Apr 2023 17:12:39 -0600 Subject: [PATCH 4/9] Rewriting the timeseries --- src/timeseries.rs | 89 +++++++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/src/timeseries.rs b/src/timeseries.rs index d2084e5a..6d0707c4 100644 --- a/src/timeseries.rs +++ b/src/timeseries.rs @@ -28,9 +28,9 @@ NOTE: This is taken from itertools: https://docs.rs/itertools-num/0.1.3/src/iter #[cfg_attr(feature = "python", pyclass)] pub struct TimeSeries { start: Epoch, - end: Epoch, + duration: Duration, step: Duration, - cur: Epoch, + cur_offset: Duration, incl: bool, } @@ -54,9 +54,9 @@ impl TimeSeries { // Start one step prior to start because next() just moves forward Self { start, - end, + duration: end - start, step, - cur: start - step, + cur_offset: -step, incl: false, } } @@ -80,9 +80,9 @@ impl TimeSeries { // Start one step prior to start because next() just moves forward Self { start, - end, + duration: end - start, step, - cur: start - step, + cur_offset: -step, incl: true, } } @@ -96,9 +96,9 @@ impl fmt::Display for TimeSeries { "TimeSeries [{} : {} : {}]", self.start, if self.incl { - self.end + self.start + self.duration } else { - self.end - self.step + self.start + self.duration - self.step }, self.step ) @@ -113,9 +113,9 @@ impl fmt::LowerHex for TimeSeries { "TimeSeries [{:x} : {:x} : {}]", self.start, if self.incl { - self.end + self.start + self.duration } else { - self.end - self.step + self.start + self.duration - self.step }, self.step ) @@ -130,9 +130,9 @@ impl fmt::UpperHex for TimeSeries { "TimeSeries [{:X} : {:X} : {}]", self.start, if self.incl { - self.end + self.start + self.duration } else { - self.end - self.step + self.start + self.duration - self.step }, self.step ) @@ -147,9 +147,9 @@ impl fmt::LowerExp for TimeSeries { "TimeSeries [{:e} : {:e} : {}]", self.start, if self.incl { - self.end + self.start + self.duration } else { - self.end - self.step + self.start + self.duration - self.step }, self.step ) @@ -164,9 +164,9 @@ impl fmt::UpperExp for TimeSeries { "TimeSeries [{:E} : {:E} : {}]", self.start, if self.incl { - self.end + self.start + self.duration } else { - self.end - self.step + self.start + self.duration - self.step }, self.step ) @@ -181,9 +181,9 @@ impl fmt::Pointer for TimeSeries { "TimeSeries [{:p} : {:p} : {}]", self.start, if self.incl { - self.end + self.start + self.duration } else { - self.end - self.step + self.start + self.duration - self.step }, self.step ) @@ -198,9 +198,9 @@ impl fmt::Octal for TimeSeries { "TimeSeries [{:o} : {:o} : {}]", self.start, if self.incl { - self.end + self.start + self.duration } else { - self.end - self.step + self.start + self.duration - self.step }, self.step ) @@ -244,12 +244,14 @@ impl Iterator for TimeSeries { #[inline] fn next(&mut self) -> Option { - let next_item = self.cur + self.step; - if (!self.incl && next_item >= self.end) || (self.incl && next_item > self.end) { + let next_offset = self.cur_offset + self.step; + if (!self.incl && next_offset >= self.duration) + || (self.incl && next_offset > self.duration) + { None } else { - self.cur = next_item; - Some(next_item) + self.cur_offset = next_offset; + Some(self.start + next_offset) } } @@ -261,11 +263,13 @@ impl Iterator for TimeSeries { impl DoubleEndedIterator for TimeSeries { #[inline] fn next_back(&mut self) -> Option { - let next_item = self.cur - self.step; - if next_item < self.start { + // Offset from the end of the iterator + let offset = self.cur_offset - self.step; + if offset < -self.duration - self.step { None } else { - Some(next_item) + self.cur_offset = offset; + Some(self.start - offset) } } } @@ -275,7 +279,7 @@ where TimeSeries: Iterator, { fn len(&self) -> usize { - let approx = ((self.end - self.start).to_seconds() / self.step.to_seconds()).abs(); + let approx = (self.duration.to_seconds() / self.step.to_seconds()).abs(); if self.incl { if approx.ceil() >= usize::MAX as f64 { usize::MAX @@ -356,18 +360,37 @@ mod tests { #[test] fn ts_over_leap_second() { - panic!("This is a bug!"); let start = Epoch::from_gregorian_utc(2016, 12, 31, 23, 59, 59, 0); let times = TimeSeries::exclusive(start, start + Unit::Second * 5, Unit::Second * 1); + let expect_end = start + Unit::Second * 4; let mut cnt = 0; + let mut cur_epoch = start; for epoch in times { - #[cfg(feature = "std")] - println!("{:?}", epoch); - assert!(start < epoch); cnt += 1; + cur_epoch = epoch; + } + + assert_eq!(cnt, 5); // Five because the first item is always inclusive + assert_eq!(cur_epoch, expect_end, "incorrect last item in iterator"); + } + + #[test] + fn ts_backward() { + let start = Epoch::from_gregorian_utc(2015, 1, 1, 12, 0, 0, 0); + let times = TimeSeries::exclusive(start, start + Unit::Second * 5, Unit::Second * 1); + let expect_end = start + Unit::Second * 4; + let mut cnt = 0; + let mut cur_epoch = start; + + for epoch in times.rev() { + cnt += 1; + cur_epoch = epoch; + let expect = start + Unit::Second * (5 - cnt); + println!("{epoch}\twant: {expect}"); } - assert_eq!(cnt, 10); + assert_eq!(cnt, 5); // Five because the first item is always inclusive + assert_eq!(cur_epoch, expect_end, "incorrect last item in iterator"); } } From c75a198df183efcb3826af2238e1a47a395b9cb2 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 15 Apr 2023 17:22:44 -0600 Subject: [PATCH 5/9] Simpler iteration in TimeSeries --- src/timeseries.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/timeseries.rs b/src/timeseries.rs index 6d0707c4..bc53f8ae 100644 --- a/src/timeseries.rs +++ b/src/timeseries.rs @@ -30,7 +30,7 @@ pub struct TimeSeries { start: Epoch, duration: Duration, step: Duration, - cur_offset: Duration, + cur: i64, incl: bool, } @@ -56,7 +56,7 @@ impl TimeSeries { start, duration: end - start, step, - cur_offset: -step, + cur: 0, incl: false, } } @@ -82,7 +82,7 @@ impl TimeSeries { start, duration: end - start, step, - cur_offset: -step, + cur: 0, incl: true, } } @@ -244,13 +244,13 @@ impl Iterator for TimeSeries { #[inline] fn next(&mut self) -> Option { - let next_offset = self.cur_offset + self.step; + let next_offset = self.cur * self.step; if (!self.incl && next_offset >= self.duration) || (self.incl && next_offset > self.duration) { None } else { - self.cur_offset = next_offset; + self.cur += 1; Some(self.start + next_offset) } } @@ -264,12 +264,15 @@ impl DoubleEndedIterator for TimeSeries { #[inline] fn next_back(&mut self) -> Option { // Offset from the end of the iterator - let offset = self.cur_offset - self.step; - if offset < -self.duration - self.step { + self.cur += 1; + let offset = self.cur * self.step; + // if offset < -self.duration - self.step { + if (!self.incl && offset > self.duration) + || (self.incl && offset > self.duration + self.step) + { None } else { - self.cur_offset = offset; - Some(self.start - offset) + Some(self.start + self.duration - offset) } } } @@ -379,7 +382,6 @@ mod tests { fn ts_backward() { let start = Epoch::from_gregorian_utc(2015, 1, 1, 12, 0, 0, 0); let times = TimeSeries::exclusive(start, start + Unit::Second * 5, Unit::Second * 1); - let expect_end = start + Unit::Second * 4; let mut cnt = 0; let mut cur_epoch = start; @@ -387,10 +389,10 @@ mod tests { cnt += 1; cur_epoch = epoch; let expect = start + Unit::Second * (5 - cnt); - println!("{epoch}\twant: {expect}"); + assert_eq!(expect, epoch, "incorrect item in iterator"); } assert_eq!(cnt, 5); // Five because the first item is always inclusive - assert_eq!(cur_epoch, expect_end, "incorrect last item in iterator"); + assert_eq!(cur_epoch, start, "incorrect last item in iterator"); } } From dfe6a26b16d8c0cc84ce4831f29ac48d4f80cf02 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 15 Apr 2023 17:26:53 -0600 Subject: [PATCH 6/9] Fix macos build --- .github/workflows/python.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 581e2efb..2e8c97fe 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -110,7 +110,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist --find-interpreter + args: --release --out dist --find-interpreter -F python sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v3 @@ -134,7 +134,7 @@ jobs: uses: PyO3/maturin-action@v1 with: command: sdist - args: --out dist + args: --out dist -F python - name: Upload sdist uses: actions/upload-artifact@v3 with: From 52baaeadd588aef592669af27f4295b17a1f43a5 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 15 Apr 2023 17:31:24 -0600 Subject: [PATCH 7/9] Maybe fix sdist --- .github/workflows/python.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 2e8c97fe..d71241b0 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -3,7 +3,7 @@ # # maturin generate-ci --pytest -o .github/workflows/python.yml github # -name: CI +name: Python CI on: push: @@ -134,7 +134,7 @@ jobs: uses: PyO3/maturin-action@v1 with: command: sdist - args: --out dist -F python + args: --out dist -- -F python - name: Upload sdist uses: actions/upload-artifact@v3 with: From 223e07f4a173cd5383d2689a5167aa9ab2936837 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 15 Apr 2023 18:49:56 -0600 Subject: [PATCH 8/9] Ease python datetime.now test --- .github/workflows/python.yml | 2 +- tests/python/test_epoch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index d71241b0..36570d11 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -134,7 +134,7 @@ jobs: uses: PyO3/maturin-action@v1 with: command: sdist - args: --out dist -- -F python + args: --out dist - name: Upload sdist uses: actions/upload-artifact@v3 with: diff --git a/tests/python/test_epoch.py b/tests/python/test_epoch.py index 0c57509d..0e2c7265 100644 --- a/tests/python/test_epoch.py +++ b/tests/python/test_epoch.py @@ -22,7 +22,7 @@ def test_utcnow(): dt = datetime.utcnow() # Hifitime uses a different clock to Python and print down to the nanosecond - assert dt.isoformat()[:24] == f"{epoch}"[:24] + assert dt.isoformat()[:21] == f"{epoch}"[:21] def test_time_series(): From f574232b5a1ed915c43500e93ac4a96af3939347 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 15 Apr 2023 19:07:27 -0600 Subject: [PATCH 9/9] Add leap second indexing test --- src/leap_seconds.rs | 14 ++++++++++++++ tests/efmt.rs | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/leap_seconds.rs b/src/leap_seconds.rs index e44b5cc1..54687c6b 100644 --- a/src/leap_seconds.rs +++ b/src/leap_seconds.rs @@ -147,3 +147,17 @@ impl Index for LatestLeapSeconds { } impl LeapSecondProvider for LatestLeapSeconds {} + +#[test] +fn leap_second_fetch() { + let leap_seconds = LatestLeapSeconds::default(); + + assert_eq!( + leap_seconds[0], + LeapSecond::new(1_893_369_600.0, 1.417818, false), + ); + assert_eq!( + leap_seconds[41], + LeapSecond::new(3_692_217_600.0, 37.0, true) + ); +} diff --git a/tests/efmt.rs b/tests/efmt.rs index eab4fe6e..7086b76c 100644 --- a/tests/efmt.rs +++ b/tests/efmt.rs @@ -90,4 +90,14 @@ fn epoch_format_rfc2822() { format!("{}", Formatter::new(epoch - 2 * Unit::Microsecond, RFC2822)), "Sat, 07 Feb 2015 11:22:32" ); + + assert_eq!( + Epoch::from_format_str("Sat, 07 Feb 2015 11:22:33", "%a, %d %b %Y %H:%M:%S").unwrap(), + epoch + ); + + assert_eq!( + Epoch::from_str_with_format("Sat, 07 Feb 2015 11:22:33", RFC2822).unwrap(), + epoch + ); }