diff --git a/.gitignore b/.gitignore index 6eb156e..c0506f0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ **/*.rs.bk Cargo.lock .env +.venv dist/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d7ff25e..1b2e412 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hifitime" -version = "3.8.6" +version = "3.8.7" authors = ["Christopher Rabotin "] description = "Ultra-precise date and time handling in Rust for scientific applications with leap second support" homepage = "https://nyxspace.com/" @@ -20,7 +20,7 @@ name = "hifitime" [dependencies] 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 } +der = { version = "0.7.8", features = ["derive", "real"], optional = true } pyo3 = { version = "0.20.0", features = ["extension-module"], optional = true } num-traits = { version = "0.2.15", default-features = false, features = [ "libm", @@ -34,19 +34,31 @@ tabled = { version = "0.15.0", optional = true } openssl = { version = "0.10", features = ["vendored"], optional = true } [target.wasm32-unknown-unknown.dependencies] -js-sys = { version = "0.3"} -wasm-bindgen_rs = { package = "wasm-bindgen", version = "0.2"} -web-sys = { version = "0.3", features = ['Window', 'Performance', 'PerformanceTiming'] } +js-sys = { version = "0.3" } +wasm-bindgen_rs = { package = "wasm-bindgen", version = "0.2" } +web-sys = { version = "0.3", features = [ + 'Window', + 'Performance', + 'PerformanceTiming', +] } [target.wasm32-unknown-emscripten.dependencies] -js-sys = { version = "0.3"} -wasm-bindgen_rs = { package = "wasm-bindgen", version = "0.2"} -web-sys = { version = "0.3", features = ['Window', 'Performance', 'PerformanceTiming'] } +js-sys = { version = "0.3" } +wasm-bindgen_rs = { package = "wasm-bindgen", version = "0.2" } +web-sys = { version = "0.3", features = [ + 'Window', + 'Performance', + 'PerformanceTiming', +] } [target.asmjs-unknown-emscripten.dependencies] -js-sys = { version = "0.3"} -wasm-bindgen_rs = { package = "wasm-bindgen", version = "0.2"} -web-sys = { version = "0.3", features = ['Window', 'Performance', 'PerformanceTiming'] } +js-sys = { version = "0.3" } +wasm-bindgen_rs = { package = "wasm-bindgen", version = "0.2" } +web-sys = { version = "0.3", features = [ + 'Window', + 'Performance', + 'PerformanceTiming', +] } [dev-dependencies] serde_json = "1.0.91" diff --git a/README.md b/README.md index 3f57bbe..2bd4337 100644 --- a/README.md +++ b/README.md @@ -309,6 +309,10 @@ In order to provide full interoperability with NAIF, hifitime uses the NAIF algo # Changelog +## 3.8.7 + +Update to der version 0.7.x. + ## 3.8.5 Changes from 3.8.2 are only dependency upgrades until this release. diff --git a/src/asn1der.rs b/src/asn1der.rs index 0dbc2ba..45a7f1a 100644 --- a/src/asn1der.rs +++ b/src/asn1der.rs @@ -21,7 +21,7 @@ impl Encode for Duration { centuries.encoded_len()? + nanoseconds.encoded_len()? } - fn encode(&self, encoder: &mut dyn Writer) -> der::Result<()> { + fn encode(&self, encoder: &mut impl Writer) -> der::Result<()> { let (centuries, nanoseconds) = self.to_parts(); centuries.encode(encoder)?; nanoseconds.encode(encoder) @@ -43,7 +43,7 @@ impl Encode for Epoch { ts.encoded_len()? + self.to_duration().encoded_len()? } - fn encode(&self, encoder: &mut dyn Writer) -> der::Result<()> { + fn encode(&self, encoder: &mut impl Writer) -> der::Result<()> { let ts: u8 = self.time_scale.into(); ts.encode(encoder)?; @@ -76,7 +76,7 @@ impl Encode for Unit { converted.encoded_len() } - fn encode(&self, encoder: &mut dyn Writer) -> der::Result<()> { + fn encode(&self, encoder: &mut impl Writer) -> der::Result<()> { let converted: u8 = self.into(); converted.encode(encoder) } diff --git a/src/duration.rs b/src/duration.rs index 43260d8..d8403c5 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -62,6 +62,7 @@ pub const NANOSECONDS_PER_CENTURY: u64 = DAYS_PER_CENTURY_U64 * NANOSECONDS_PER_ #[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, @@ -694,6 +695,11 @@ impl Duration { } } + #[cfg(feature = "python")] + fn __getnewargs__(&self) -> Result<(String,), PyErr> { + Ok((format!("{self}"),)) + } + #[cfg(feature = "python")] fn __str__(&self) -> String { format!("{self}") @@ -701,7 +707,7 @@ impl Duration { #[cfg(feature = "python")] fn __repr__(&self) -> String { - format!("{self}") + format!("{self} @ {self:p}") } #[cfg(feature = "python")] diff --git a/src/efmt/consts.rs b/src/efmt/consts.rs index 3c2f21e..657fad6 100644 --- a/src/efmt/consts.rs +++ b/src/efmt/consts.rs @@ -453,3 +453,61 @@ pub const RFC2822_LONG: Format = Format { ], num_items: 7, }; + +/// The ISO8601 format without the time scale +pub const ISO8601_STD: Format = Format { + items: [ + Some(Item { + token: Token::Year, + sep_char: Some('-'), + second_sep_char: None, + optional: false, + }), + Some(Item { + token: Token::Month, + sep_char: Some('-'), + second_sep_char: None, + optional: false, + }), + Some(Item { + token: Token::Day, + sep_char: Some('T'), + second_sep_char: None, + optional: false, + }), + Some(Item { + token: Token::Hour, + sep_char: Some(':'), + second_sep_char: None, + optional: false, + }), + Some(Item { + token: Token::Minute, + sep_char: Some(':'), + second_sep_char: None, + optional: false, + }), + Some(Item { + token: Token::Second, + sep_char: Some('.'), + second_sep_char: None, + optional: false, + }), + Some(Item { + token: Token::Subsecond, + sep_char: Some(' '), + optional: false, + second_sep_char: None, + }), + None, + None, + None, + None, + None, + None, + None, + None, + None, + ], + num_items: 7, +}; diff --git a/src/epoch.rs b/src/epoch.rs index 24c625f..e859258 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -124,6 +124,7 @@ const CUMULATIVE_DAYS_FOR_MONTH: [u16; 12] = { #[derive(Copy, Clone, Eq, Default)] #[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. @@ -1687,13 +1688,13 @@ impl Epoch { #[cfg(feature = "python")] #[classmethod] - /// Equivalent to `datetime.strptime, refer to for format options + /// 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 + /// Equivalent to `datetime.strftime`, refer to for format options fn strftime(&self, format_str: String) -> PyResult { use crate::efmt::Formatter; let fmt = Format::from_str(&format_str) @@ -1702,6 +1703,21 @@ impl Epoch { Ok(format!("{}", Formatter::new(*self, fmt))) } + #[cfg(feature = "python")] + /// Equivalent to `datetime.isoformat`, and truncated to 23 chars, refer to for format options + fn isoformat(&self) -> PyResult { + Ok(self.to_isoformat()) + } + + #[cfg(feature = "std")] + #[must_use] + /// The standard ISO format of this epoch (six digits of subseconds), refer to for format options + pub fn to_isoformat(&self) -> String { + use crate::efmt::consts::ISO8601_STD; + use crate::efmt::Formatter; + 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). /// @@ -2673,6 +2689,11 @@ impl Epoch { } } + #[cfg(feature = "python")] + fn __getnewargs__(&self) -> Result<(String,), PyErr> { + Ok((format!("{self:?}"),)) + } + #[cfg(feature = "python")] #[classmethod] fn system_now(_cls: &PyType) -> Result { @@ -2686,7 +2707,7 @@ impl Epoch { #[cfg(feature = "python")] fn __repr__(&self) -> String { - format!("{self:?}") + format!("{self:?} @ {self:p}") } #[cfg(feature = "python")] diff --git a/src/leap_seconds.rs b/src/leap_seconds.rs index a02f166..195f498 100644 --- a/src/leap_seconds.rs +++ b/src/leap_seconds.rs @@ -105,7 +105,7 @@ impl LatestLeapSeconds { } fn __repr__(&self) -> String { - format!("{self:?}") + format!("{self:?} @ {self:p}") } } diff --git a/src/leap_seconds_file.rs b/src/leap_seconds_file.rs index efbad59..0c613ca 100644 --- a/src/leap_seconds_file.rs +++ b/src/leap_seconds_file.rs @@ -87,7 +87,7 @@ impl LeapSecondsFile { } fn __repr__(&self) -> String { - format!("{self:?}") + format!("{self:?} @ {self:p}") } } diff --git a/src/timeseries.rs b/src/timeseries.rs index d04fded..c31dc61 100644 --- a/src/timeseries.rs +++ b/src/timeseries.rs @@ -17,6 +17,13 @@ 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 . @@ -26,6 +33,7 @@ NOTE: This is taken from itertools: https://docs.rs/itertools-num/0.1.3/src/iter /// An iterator of a sequence of evenly spaced Epochs. #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "python", pyclass)] +#[cfg_attr(feature = "python", pyo3(module = "hifitime"))] pub struct TimeSeries { start: Epoch, duration: Duration, @@ -222,6 +230,10 @@ impl TimeSeries { } } + fn __getnewargs__(&self) -> Result<(Epoch, Epoch, Duration, bool), PyErr> { + Ok((self.start, self.start + self.duration, self.step, self.incl)) + } + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } @@ -235,7 +247,12 @@ impl TimeSeries { } fn __repr__(&self) -> String { - format!("{self}") + format!("{self:?} @ {self:p}") + } + + #[cfg(feature = "python")] + fn __eq__(&self, other: Self) -> bool { + *self == other } } diff --git a/src/ut1.rs b/src/ut1.rs index bf3a360..9c491b2 100644 --- a/src/ut1.rs +++ b/src/ut1.rs @@ -129,7 +129,7 @@ impl Ut1Provider { } fn __repr__(&self) -> String { - format!("{self}") + format!("{self:?} @ {self:p}") } } diff --git a/tests/efmt.rs b/tests/efmt.rs index 8612f42..2089ac5 100644 --- a/tests/efmt.rs +++ b/tests/efmt.rs @@ -15,6 +15,19 @@ fn epoch_parse_with_format() { assert_eq!(ISO8601_FLEX.parse("2015-02-07T11:22:33.0 UTC").unwrap(), e); assert_eq!(ISO8601_FLEX.parse("2015-02-07T11:22:33").unwrap(), e); + assert_eq!(ISO8601_STD.parse("2015-02-07T11:22:33.0").unwrap(), e); + + #[cfg(feature = "std")] + { + // Test an epoch that's much more precise than usual time keepers + let e_prec = Epoch::from_gregorian_utc(2015, 2, 7, 11, 22, 33, 123456789); + assert_eq!(e_prec.to_isoformat(), "2015-02-07T11:22:33.123456"); + assert_ne!( + e_prec.to_isoformat(), + Formatter::new(e_prec, ISO8601).to_string() + ); + } + assert_eq!(RFC3339.parse("2015-02-07T11:22:33.0 UTC").unwrap(), e); assert!(RFC3339.parse("2018-02-13T23:08:32Z").is_ok()); diff --git a/tests/python/test_epoch.py b/tests/python/test_epoch.py index 1e39c8f..2b32ba6 100644 --- a/tests/python/test_epoch.py +++ b/tests/python/test_epoch.py @@ -1,5 +1,6 @@ from hifitime import Epoch, TimeSeries, Unit, Duration from datetime import datetime +import pickle def test_strtime(): @@ -16,6 +17,8 @@ def test_strtime(): assert Epoch.strptime(dt_fmt, "%A, %d %B %Y %H:%M:%S") == epoch + assert pickle.loads(pickle.dumps(epoch)) == epoch + def test_utcnow(): epoch = Epoch.system_now() @@ -40,10 +43,16 @@ def test_time_series(): ) print(time_series) + assert pickle.loads(pickle.dumps(time_series)) == time_series + for num, epoch in enumerate(time_series): print(f"#{num}:\t{epoch}") assert num == 10 + # Once consummed, the iterator in the time series will be different, + # so the pickling will return something different + assert pickle.loads(pickle.dumps(time_series)) != time_series + def test_duration_eq(): """ @@ -54,4 +63,7 @@ def test_duration_eq(): assert Unit.Second * 1.0 >= Duration("0 ns") assert Unit.Second * 1.0 > Duration("0 ns") assert Duration("0 ns") <= Unit.Second * 1.0 - assert Duration("0 ns") < Unit.Second * 1.0 \ No newline at end of file + assert Duration("0 ns") < Unit.Second * 1.0 + + dur = Duration("37 min 26 s") + assert pickle.loads(pickle.dumps(dur)) == dur