Skip to content

Commit

Permalink
Add an EpochNanosecond new type (#116)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nekevss authored Dec 4, 2024
1 parent 1fc7c16 commit 3925c87
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 87 deletions.
2 changes: 1 addition & 1 deletion src/components/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl PlainDateTime {
offset: f64,
calendar: Calendar,
) -> TemporalResult<Self> {
let iso = IsoDateTime::from_epoch_nanos(&instant.epoch_nanos, offset)?;
let iso = IsoDateTime::from_epoch_nanos(&instant.as_i128(), offset)?;
Ok(Self { iso, calendar })
}

Expand Down
122 changes: 66 additions & 56 deletions src/components/instant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,62 @@ 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;

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<i128> for EpochNanoseconds {
type Error = TemporalError;
fn try_from(value: i128) -> Result<Self, Self::Error> {
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<u128> for EpochNanoseconds {
type Error = TemporalError;
fn try_from(value: u128) -> Result<Self, Self::Error> {
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<f64> for EpochNanoseconds {
type Error = TemporalError;
fn try_from(value: f64) -> Result<Self, Self::Error> {
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<EpochNanoseconds> for Instant {
fn from(value: EpochNanoseconds) -> Self {
Self(value)
}
}

// ==== Private API ====
Expand All @@ -38,17 +78,15 @@ impl Instant {
///
/// Temporal-Proposal equivalent: `AddDurationToOrSubtractDurationFrom`.
pub(crate) fn add_to_instant(&self, duration: &TimeDuration) -> TemporalResult<Self> {
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`.
Expand Down Expand Up @@ -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]].
Expand Down Expand Up @@ -127,21 +163,15 @@ impl Instant {
return Err(TemporalError::range().with_message("Increment exceeded a valid range."));
};

let rounded = IncrementRounder::<i128>::from_positive_parts(self.epoch_nanos, increment)?
let rounded = IncrementRounder::<i128>::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
}
}

Expand All @@ -150,25 +180,15 @@ impl Instant {
impl Instant {
/// Create a new validated `Instant`.
#[inline]
pub fn try_new(epoch_nanoseconds: i128) -> TemporalResult<Self> {
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<Self> {
Ok(Self::from(EpochNanoseconds::try_from(nanoseconds)?))
}

pub fn from_epoch_milliseconds(epoch_milliseconds: i128) -> TemporalResult<Self> {
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`
Expand Down Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -326,7 +337,6 @@ mod tests {
primitive::FiniteF64,
NS_MAX_INSTANT, NS_MIN_INSTANT,
};
use num_traits::ToPrimitive;

#[test]
#[allow(clippy::float_cmp)]
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
11 changes: 3 additions & 8 deletions src/components/now.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")]
Expand Down
21 changes: 12 additions & 9 deletions src/components/tz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -96,8 +97,8 @@ impl TimeZone {
instant: &Instant,
provider: &impl TzProvider,
) -> TemporalResult<IsoDateTime> {
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))
}
}

Expand Down Expand Up @@ -125,7 +126,7 @@ impl TimeZone {
iso: IsoDateTime,
disambiguation: Disambiguation,
provider: &impl TzProvider,
) -> TemporalResult<i128> {
) -> TemporalResult<EpochNanoseconds> {
// 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).
Expand Down Expand Up @@ -209,22 +210,24 @@ impl TimeZone {
iso: IsoDateTime,
disambiguation: Disambiguation,
provider: &impl TzProvider,
) -> TemporalResult<i128> {
) -> TemporalResult<EpochNanoseconds> {
// 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 => {
Expand Down Expand Up @@ -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).
Expand All @@ -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])
}
}

Expand Down
23 changes: 11 additions & 12 deletions src/components/zoneddatetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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(),
))
}
}

Expand Down Expand Up @@ -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()
}
}
Expand Down
Loading

0 comments on commit 3925c87

Please sign in to comment.