From 3925c8782fcc896bd025d66a32c51a6e6ff053ac Mon Sep 17 00:00:00 2001 From: Kevin Ness <46825870+nekevss@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:48:21 -0600 Subject: [PATCH] Add an `EpochNanosecond` new type (#116) This PR adds an `EpochNanosecond` new type for the internal value of `Instant`. The primary goal of this PR is to clean up the external API around dealing with `Instant` nanoseconds while guaranteeing that the `EpochNanoseconds` value is valid. --- src/components/datetime.rs | 2 +- src/components/instant.rs | 122 +++++++++++++++++--------------- src/components/mod.rs | 2 +- src/components/now.rs | 11 +-- src/components/tz.rs | 21 +++--- src/components/zoneddatetime.rs | 23 +++--- src/lib.rs | 5 ++ 7 files changed, 99 insertions(+), 87 deletions(-) diff --git a/src/components/datetime.rs b/src/components/datetime.rs index 9429d1cf..74388af3 100644 --- a/src/components/datetime.rs +++ b/src/components/datetime.rs @@ -77,7 +77,7 @@ impl PlainDateTime { offset: f64, calendar: Calendar, ) -> TemporalResult { - let iso = IsoDateTime::from_epoch_nanos(&instant.epoch_nanos, offset)?; + let iso = IsoDateTime::from_epoch_nanos(&instant.as_i128(), offset)?; Ok(Self { iso, calendar }) } diff --git a/src/components/instant.rs b/src/components/instant.rs index f9a10d3b..78bbce07 100644 --- a/src/components/instant.rs +++ b/src/components/instant.rs @@ -12,10 +12,10 @@ use crate::{ parsers::parse_instant, primitive::FiniteF64, rounding::{IncrementRounder, Round}, - Sign, TemporalError, TemporalResult, TemporalUnwrap, + Sign, TemporalError, TemporalResult, TemporalUnwrap, NS_MAX_INSTANT, }; -use num_traits::{Euclid, FromPrimitive, ToPrimitive}; +use num_traits::{Euclid, FromPrimitive}; use super::duration::normalized::NormalizedTimeDuration; @@ -23,11 +23,51 @@ const NANOSECONDS_PER_SECOND: f64 = 1e9; const NANOSECONDS_PER_MINUTE: f64 = 60f64 * NANOSECONDS_PER_SECOND; const NANOSECONDS_PER_HOUR: f64 = 60f64 * NANOSECONDS_PER_MINUTE; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct EpochNanoseconds(i128); + +impl TryFrom for EpochNanoseconds { + type Error = TemporalError; + fn try_from(value: i128) -> Result { + if !is_valid_epoch_nanos(&value) { + return Err(TemporalError::range() + .with_message("Instant nanoseconds are not within a valid epoch range.")); + } + Ok(Self(value)) + } +} + +impl TryFrom for EpochNanoseconds { + type Error = TemporalError; + fn try_from(value: u128) -> Result { + if (NS_MAX_INSTANT as u128) < value { + return Err(TemporalError::range() + .with_message("Instant nanoseconds are not within a valid epoch range.")); + } + Ok(Self(value as i128)) + } +} + +impl TryFrom for EpochNanoseconds { + type Error = TemporalError; + fn try_from(value: f64) -> Result { + let Some(value) = i128::from_f64(value) else { + return Err(TemporalError::range() + .with_message("Instant nanoseconds are not within a valid epoch range.")); + }; + Self::try_from(value) + } +} + /// The native Rust implementation of `Temporal.Instant` #[non_exhaustive] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Instant { - pub(crate) epoch_nanos: i128, +pub struct Instant(EpochNanoseconds); + +impl From for Instant { + fn from(value: EpochNanoseconds) -> Self { + Self(value) + } } // ==== Private API ==== @@ -38,17 +78,15 @@ impl Instant { /// /// Temporal-Proposal equivalent: `AddDurationToOrSubtractDurationFrom`. pub(crate) fn add_to_instant(&self, duration: &TimeDuration) -> TemporalResult { - let result = self.epoch_nanoseconds() + let current_nanos = self.epoch_nanoseconds() as f64; + let result = current_nanos + duration.nanoseconds.0 + (duration.microseconds.0 * 1000f64) + (duration.milliseconds.0 * 1_000_000f64) + (duration.seconds.0 * NANOSECONDS_PER_SECOND) + (duration.minutes.0 * NANOSECONDS_PER_MINUTE) + (duration.hours.0 * NANOSECONDS_PER_HOUR); - let nanos = i128::from_f64(result).ok_or_else(|| { - TemporalError::range().with_message("Duration added to instant exceeded valid range.") - })?; - Self::try_new(nanos) + Ok(Self::from(EpochNanoseconds::try_from(result)?)) } // TODO: Add test for `diff_instant`. @@ -76,10 +114,8 @@ impl Instant { // Below are the steps from Difference Instant. // 5. Let diffRecord be DifferenceInstant(instant.[[Nanoseconds]], other.[[Nanoseconds]], // settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[RoundingMode]]). - let diff = NormalizedTimeDuration::from_nanosecond_difference( - other.epoch_nanos, - self.epoch_nanos, - )?; + let diff = + NormalizedTimeDuration::from_nanosecond_difference(other.as_i128(), self.as_i128())?; let (round_record, _) = diff.round(FiniteF64::default(), resolved_options)?; // 6. Let norm be diffRecord.[[NormalizedTimeDuration]]. @@ -127,21 +163,15 @@ impl Instant { return Err(TemporalError::range().with_message("Increment exceeded a valid range.")); }; - let rounded = IncrementRounder::::from_positive_parts(self.epoch_nanos, increment)? + let rounded = IncrementRounder::::from_positive_parts(self.as_i128(), increment)? .round_as_positive(resolved_options.rounding_mode); Ok(rounded.into()) } - /// Utility for converting `Instant` to f64. - /// - /// # Panics - /// - /// This function will panic if called on an invalid `Instant`. - pub(crate) fn to_f64(&self) -> f64 { - self.epoch_nanos - .to_f64() - .expect("A valid instant is representable by f64.") + // Utility for converting `Instant` to `i128`. + pub fn as_i128(&self) -> i128 { + self.0 .0 } } @@ -150,25 +180,15 @@ impl Instant { impl Instant { /// Create a new validated `Instant`. #[inline] - pub fn try_new(epoch_nanoseconds: i128) -> TemporalResult { - if !is_valid_epoch_nanos(&epoch_nanoseconds) { - return Err(TemporalError::range() - .with_message("Instant nanoseconds are not within a valid epoch range.")); - } - Ok(Self { - epoch_nanos: epoch_nanoseconds, - }) + pub fn try_new(nanoseconds: i128) -> TemporalResult { + Ok(Self::from(EpochNanoseconds::try_from(nanoseconds)?)) } pub fn from_epoch_milliseconds(epoch_milliseconds: i128) -> TemporalResult { let epoch_nanos = epoch_milliseconds .checked_mul(1_000_000) .unwrap_or(i128::MAX); - if !is_valid_epoch_nanos(&epoch_nanos) { - return Err(TemporalError::range() - .with_message("Instant nanoseconds are not within a valid epoch range.")); - } - Ok(Self { epoch_nanos }) + Self::try_new(epoch_nanos) } /// Adds a `Duration` to the current `Instant`, returning an error if the `Duration` @@ -235,35 +255,26 @@ impl Instant { /// Returns the `epochSeconds` value for this `Instant`. #[must_use] - pub fn epoch_seconds(&self) -> f64 { - (&self.epoch_nanos / 1_000_000_000) - .to_f64() - .expect("A validated Instant should be within a valid f64") - .floor() + pub fn epoch_seconds(&self) -> i128 { + self.as_i128() / 1_000_000_000 } /// Returns the `epochMilliseconds` value for this `Instant`. #[must_use] - pub fn epoch_milliseconds(&self) -> f64 { - (&self.epoch_nanos / 1_000_000) - .to_f64() - .expect("A validated Instant should be within a valid f64") - .floor() + pub fn epoch_milliseconds(&self) -> i128 { + self.as_i128() / 1_000_000 } /// Returns the `epochMicroseconds` value for this `Instant`. #[must_use] - pub fn epoch_microseconds(&self) -> f64 { - (&self.epoch_nanos / 1_000) - .to_f64() - .expect("A validated Instant should be within a valid f64") - .floor() + pub fn epoch_microseconds(&self) -> i128 { + self.as_i128() / 1_000 } /// Returns the `epochNanoseconds` value for this `Instant`. #[must_use] - pub fn epoch_nanoseconds(&self) -> f64 { - self.to_f64() + pub fn epoch_nanoseconds(&self) -> i128 { + self.as_i128() } } @@ -326,7 +337,6 @@ mod tests { primitive::FiniteF64, NS_MAX_INSTANT, NS_MIN_INSTANT, }; - use num_traits::ToPrimitive; #[test] #[allow(clippy::float_cmp)] @@ -338,8 +348,8 @@ mod tests { let max_instant = Instant::try_new(max).unwrap(); let min_instant = Instant::try_new(min).unwrap(); - assert_eq!(max_instant.epoch_nanoseconds(), max.to_f64().unwrap()); - assert_eq!(min_instant.epoch_nanoseconds(), min.to_f64().unwrap()); + assert_eq!(max_instant.epoch_nanoseconds(), max); + assert_eq!(min_instant.epoch_nanoseconds(), min); let max_plus_one = NS_MAX_INSTANT + 1; let min_minus_one = NS_MIN_INSTANT - 1; diff --git a/src/components/mod.rs b/src/components/mod.rs index 2e569f0c..c7664c2e 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -32,7 +32,7 @@ pub use datetime::{PartialDateTime, PlainDateTime}; #[doc(inline)] pub use duration::Duration; #[doc(inline)] -pub use instant::Instant; +pub use instant::{EpochNanoseconds, Instant}; #[doc(inline)] pub use month_day::PlainMonthDay; #[doc(inline)] diff --git a/src/components/now.rs b/src/components/now.rs index 9d513faf..b6d955c0 100644 --- a/src/components/now.rs +++ b/src/components/now.rs @@ -13,7 +13,7 @@ use crate::{iso::IsoDateTime, TemporalUnwrap}; use super::{ calendar::Calendar, tz::{TimeZone, TzProvider}, - Instant, PlainDateTime, + EpochNanoseconds, Instant, PlainDateTime, }; /// The Temporal Now object. @@ -54,14 +54,9 @@ fn system_date_time( let tz = tz.unwrap_or(sys::get_system_tz_identifier()?.into()); // 3. Let epochNs be SystemUTCEpochNanoseconds(). // TODO: Handle u128 -> i128 better for system nanoseconds - let epoch_ns = sys::get_system_nanoseconds()?; + let epoch_ns = EpochNanoseconds::try_from(sys::get_system_nanoseconds()?)?; // 4. Return GetISODateTimeFor(timeZone, epochNs). - tz.get_iso_datetime_for( - &Instant { - epoch_nanos: epoch_ns as i128, - }, - provider, - ) + tz.get_iso_datetime_for(&Instant::from(epoch_ns), provider) } #[cfg(feature = "std")] diff --git a/src/components/tz.rs b/src/components/tz.rs index 374d8b57..228abec4 100644 --- a/src/components/tz.rs +++ b/src/components/tz.rs @@ -7,6 +7,7 @@ use core::{iter::Peekable, str::Chars}; use num_traits::ToPrimitive; +use crate::components::instant::EpochNanoseconds; use crate::{ components::{duration::normalized::NormalizedTimeDuration, Instant}, iso::{IsoDate, IsoDateTime}, @@ -96,8 +97,8 @@ impl TimeZone { instant: &Instant, provider: &impl TzProvider, ) -> TemporalResult { - let nanos = self.get_offset_nanos_for(instant.epoch_nanos, provider)?; - IsoDateTime::from_epoch_nanos(&instant.epoch_nanos, nanos.to_f64().unwrap_or(0.0)) + let nanos = self.get_offset_nanos_for(instant.as_i128(), provider)?; + IsoDateTime::from_epoch_nanos(&instant.as_i128(), nanos.to_f64().unwrap_or(0.0)) } } @@ -125,7 +126,7 @@ impl TimeZone { iso: IsoDateTime, disambiguation: Disambiguation, provider: &impl TzProvider, - ) -> TemporalResult { + ) -> TemporalResult { // 1. Let possibleEpochNs be ? GetPossibleEpochNanoseconds(timeZone, isoDateTime). let possible_nanos = self.get_possible_epoch_ns_for(iso, provider)?; // 2. Return ? DisambiguatePossibleEpochNanoseconds(possibleEpochNs, timeZone, isoDateTime, disambiguation). @@ -209,22 +210,24 @@ impl TimeZone { iso: IsoDateTime, disambiguation: Disambiguation, provider: &impl TzProvider, - ) -> TemporalResult { + ) -> TemporalResult { // 1. Let n be possibleEpochNs's length. let n = nanos.len(); // 2. If n = 1, then if n == 1 { // a. Return possibleEpochNs[0]. - return Ok(nanos[0]); + return EpochNanoseconds::try_from(nanos[0]); // 3. If n ≠ 0, then } else if n != 0 { match disambiguation { // a. If disambiguation is earlier or compatible, then // i. Return possibleEpochNs[0]. - Disambiguation::Compatible | Disambiguation::Earlier => return Ok(nanos[0]), + Disambiguation::Compatible | Disambiguation::Earlier => { + return EpochNanoseconds::try_from(nanos[0]) + } // b. If disambiguation is later, then // i. Return possibleEpochNs[n - 1]. - Disambiguation::Later => return Ok(nanos[n - 1]), + Disambiguation::Later => return EpochNanoseconds::try_from(nanos[n - 1]), // c. Assert: disambiguation is reject. // d. Throw a RangeError exception. Disambiguation::Reject => { @@ -300,7 +303,7 @@ impl TimeZone { let possible = self.get_possible_epoch_ns_for(earlier, provider)?; // f. Assert: possibleEpochNs is not empty. // g. Return possibleEpochNs[0]. - return Ok(possible[0]); + return EpochNanoseconds::try_from(possible[0]); } // 17. Assert: disambiguation is compatible or later. // 18. Let timeDuration be TimeDurationFromComponents(0, 0, 0, 0, 0, nanoseconds). @@ -322,7 +325,7 @@ impl TimeZone { let n = possible.len(); // 24. Assert: n ≠ 0. // 25. Return possibleEpochNs[n - 1]. - Ok(possible[n - 1]) + EpochNanoseconds::try_from(possible[n - 1]) } } diff --git a/src/components/zoneddatetime.rs b/src/components/zoneddatetime.rs index f6feed4d..ec18516e 100644 --- a/src/components/zoneddatetime.rs +++ b/src/components/zoneddatetime.rs @@ -85,10 +85,7 @@ impl ZonedDateTime { )?; // 7. Return ? AddInstant(intermediateNs, duration.[[Time]]). - Instant { - epoch_nanos: intermediate_ns, - } - .add_to_instant(duration.time()) + Instant::from(intermediate_ns).add_to_instant(duration.time()) } #[inline] @@ -109,11 +106,13 @@ impl ZonedDateTime { // 6. Let timeZone be zonedDateTime.[[TimeZone]]. // 7. Let internalDuration be ToInternalDurationRecord(duration). // 8. Let epochNanoseconds be ? AddZonedDateTime(zonedDateTime.[[EpochNanoseconds]], timeZone, calendar, internalDuration, overflow). - let epoch_ns = self - .add_as_instant(duration, overflow, provider)? - .epoch_nanos; + let epoch_ns = self.add_as_instant(duration, overflow, provider)?; // 9. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar). - Self::try_new(epoch_ns, self.calendar().clone(), self.tz().clone()) + Ok(Self::new_unchecked( + epoch_ns, + self.calendar().clone(), + self.tz().clone(), + )) } } @@ -143,25 +142,25 @@ impl ZonedDateTime { /// Returns the `epochSeconds` value of this `ZonedDateTime`. #[must_use] - pub fn epoch_seconds(&self) -> f64 { + pub fn epoch_seconds(&self) -> i128 { self.instant.epoch_seconds() } /// Returns the `epochMilliseconds` value of this `ZonedDateTime`. #[must_use] - pub fn epoch_milliseconds(&self) -> f64 { + pub fn epoch_milliseconds(&self) -> i128 { self.instant.epoch_milliseconds() } /// Returns the `epochMicroseconds` value of this `ZonedDateTime`. #[must_use] - pub fn epoch_microseconds(&self) -> f64 { + pub fn epoch_microseconds(&self) -> i128 { self.instant.epoch_microseconds() } /// Returns the `epochNanoseconds` value of this `ZonedDateTime`. #[must_use] - pub fn epoch_nanoseconds(&self) -> f64 { + pub fn epoch_nanoseconds(&self) -> i128 { self.instant.epoch_nanoseconds() } } diff --git a/src/lib.rs b/src/lib.rs index 35af6674..4039a1ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,6 +88,11 @@ pub mod partial { }; } +// TODO: Potentially bikeshed how `EpochNanoseconds` should be exported. +pub mod time { + pub use crate::components::EpochNanoseconds; +} + pub use crate::components::{ calendar::Calendar, tz::TimeZone, Duration, Instant, PlainDate, PlainDateTime, PlainMonthDay, PlainTime, PlainYearMonth, ZonedDateTime,