From ced2904d226819c8a7f4dabf358785a2f48d8a24 Mon Sep 17 00:00:00 2001 From: Kevin <46825870+nekevss@users.noreply.github.com> Date: Tue, 23 Jan 2024 17:43:02 -0500 Subject: [PATCH] Temporal `Instant` migration cont. and related changes (#3601) * Migrate more of Instant * Migrate round and some other adjustments * Complete initial work on Instant * Add some docs * nanos -> quotient in roundNumberToIncrementAsIfPositive * Update comments on todos --- core/engine/src/bigint.rs | 11 - core/engine/src/builtins/options.rs | 19 + .../src/builtins/temporal/instant/mod.rs | 400 ++++-------------- core/engine/src/builtins/temporal/mod.rs | 323 +------------- core/engine/src/builtins/temporal/options.rs | 5 +- core/temporal/src/components/duration.rs | 14 + core/temporal/src/components/duration/time.rs | 14 + core/temporal/src/components/instant.rs | 218 +++++++++- core/temporal/src/lib.rs | 6 +- core/temporal/src/options.rs | 11 - core/temporal/src/utils.rs | 65 ++- 11 files changed, 404 insertions(+), 682 deletions(-) diff --git a/core/engine/src/bigint.rs b/core/engine/src/bigint.rs index 783ab10efe..367ddeda75 100644 --- a/core/engine/src/bigint.rs +++ b/core/engine/src/bigint.rs @@ -228,17 +228,6 @@ impl JsBigInt { Self::new(x.inner.as_ref().clone().add(y.inner.as_ref())) } - /// Utility function for performing `+` operation on more than two values. - #[inline] - #[cfg(feature = "temporal")] - pub(crate) fn add_n(values: &[Self]) -> Self { - let mut result = Self::zero(); - for big_int in values { - result = Self::add(&result, big_int); - } - result - } - /// Performs the `-` operation. #[inline] #[must_use] diff --git a/core/engine/src/builtins/options.rs b/core/engine/src/builtins/options.rs index 4628f0eee4..f25899a240 100644 --- a/core/engine/src/builtins/options.rs +++ b/core/engine/src/builtins/options.rs @@ -110,6 +110,20 @@ impl OptionType for JsString { } } +impl OptionType for f64 { + fn from_value(value: JsValue, context: &mut Context) -> JsResult { + let value = value.to_number(context)?; + + if !value.is_finite() { + return Err(JsNativeError::range() + .with_message("roundingIncrement must be finite.") + .into()); + } + + Ok(value) + } +} + #[derive(Debug, Copy, Clone, Default)] pub(crate) enum RoundingMode { Ceil, @@ -171,6 +185,7 @@ impl fmt::Display for RoundingMode { } } +// TODO: remove once confirmed. #[cfg(feature = "temporal")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum UnsignedRoundingMode { @@ -182,7 +197,9 @@ pub(crate) enum UnsignedRoundingMode { } impl RoundingMode { + // TODO: remove once confirmed. #[cfg(feature = "temporal")] + #[allow(dead_code)] pub(crate) const fn negate(self) -> Self { use RoundingMode::{ Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc, @@ -201,7 +218,9 @@ impl RoundingMode { } } + // TODO: remove once confirmed. #[cfg(feature = "temporal")] + #[allow(dead_code)] pub(crate) const fn get_unsigned_round_mode(self, is_negative: bool) -> UnsignedRoundingMode { use RoundingMode::{ Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc, diff --git a/core/engine/src/builtins/temporal/instant/mod.rs b/core/engine/src/builtins/temporal/instant/mod.rs index 1717547917..d1375f8031 100644 --- a/core/engine/src/builtins/temporal/instant/mod.rs +++ b/core/engine/src/builtins/temporal/instant/mod.rs @@ -1,11 +1,11 @@ //! Boa's implementation of ECMAScript's `Temporal.Instant` builtin object. -#![allow(dead_code)] use crate::{ builtins::{ - options::{get_option, get_options_object, RoundingMode}, - temporal::options::{ - get_temporal_rounding_increment, get_temporal_unit, TemporalUnitGroup, + options::{get_option, get_options_object}, + temporal::{ + duration::{create_temporal_duration, to_temporal_duration_record}, + options::{get_temporal_unit, TemporalUnitGroup}, }, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, }, @@ -20,18 +20,17 @@ use crate::{ }; use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; -use boa_temporal::{components::Duration, options::TemporalUnit}; - -use super::{ns_max_instant, ns_min_instant, MIS_PER_DAY, MS_PER_DAY, NS_PER_DAY}; - -const NANOSECONDS_PER_SECOND: i64 = 10_000_000_000; -const NANOSECONDS_PER_MINUTE: i64 = 600_000_000_000; -const NANOSECONDS_PER_HOUR: i64 = 36_000_000_000_000; +use boa_temporal::{ + components::Instant as InnerInstant, + options::{TemporalRoundingMode, TemporalUnit}, +}; /// The `Temporal.Instant` object. #[derive(Debug, Clone, Trace, Finalize, JsData)] +// SAFETY: Instant does not contain any traceable values. +#[boa_gc(unsafe_empty_trace)] pub struct Instant { - pub(crate) nanoseconds: JsBigInt, + pub(crate) inner: InnerInstant, } impl BuiltInObject for Instant { @@ -131,13 +130,10 @@ impl BuiltInConstructor for Instant { let epoch_nanos = args.get_or_undefined(0).to_bigint(context)?; // 3. If ! IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. - if !is_valid_epoch_nanos(&epoch_nanos) { - return Err(JsNativeError::range() - .with_message("Temporal.Instant must have a valid epochNanoseconds.") - .into()); - }; + // NOTE: boa_temporal::Instant asserts that the epochNanoseconds are valid. + let instant = InnerInstant::new(epoch_nanos.as_inner().clone())?; // 4. Return ? CreateTemporalInstant(epochNanoseconds, NewTarget). - create_temporal_instant(epoch_nanos, Some(new_target.clone()), context) + create_temporal_instant(instant, Some(new_target.clone()), context) } } @@ -159,11 +155,7 @@ impl Instant { JsNativeError::typ().with_message("the this object must be an instant object.") })?; // 3. Let ns be instant.[[Nanoseconds]]. - let ns = &instant.nanoseconds; - // 4. Let s be floor(ℝ(ns) / 10e9). - let s = (ns.to_f64() / 10e9).floor(); - // 5. Return 𝔽(s). - Ok(s.into()) + Ok(instant.inner.epoch_seconds().into()) } /// 8.3.4 get Temporal.Instant.prototype.epochMilliseconds @@ -181,11 +173,9 @@ impl Instant { JsNativeError::typ().with_message("the this object must be an instant object.") })?; // 3. Let ns be instant.[[Nanoseconds]]. - let ns = &instant.nanoseconds; // 4. Let ms be floor(ℝ(ns) / 106). - let ms = (ns.to_f64() / 10e6).floor(); // 5. Return 𝔽(ms). - Ok(ms.into()) + Ok(instant.inner.epoch_milliseconds().into()) } /// 8.3.5 get Temporal.Instant.prototype.epochMicroseconds @@ -203,13 +193,10 @@ impl Instant { JsNativeError::typ().with_message("the this object must be an instant object.") })?; // 3. Let ns be instant.[[Nanoseconds]]. - let ns = &instant.nanoseconds; // 4. Let µs be floor(ℝ(ns) / 103). - let micro_s = (ns.to_f64() / 10e3).floor(); // 5. Return ℤ(µs). - let big_int = JsBigInt::try_from(micro_s).map_err(|_| { - JsNativeError::typ().with_message("Could not convert microseconds to JsBigInt value") - })?; + let big_int = JsBigInt::try_from(instant.inner.epoch_microseconds()) + .expect("valid microseconds is in range of BigInt"); Ok(big_int.into()) } @@ -228,9 +215,10 @@ impl Instant { JsNativeError::typ().with_message("the this object must be an instant object.") })?; // 3. Let ns be instant.[[Nanoseconds]]. - let ns = &instant.nanoseconds; // 4. Return ns. - Ok(ns.clone().into()) + let big_int = JsBigInt::try_from(instant.inner.epoch_nanoseconds()) + .expect("valid nanoseconds is in range of BigInt"); + Ok(big_int.into()) } /// 8.3.7 `Temporal.Instant.prototype.add ( temporalDurationLike )` @@ -249,8 +237,9 @@ impl Instant { })?; // 3. Return ? AddDurationToOrSubtractDurationFromInstant(add, instant, temporalDurationLike). - let temporal_duration_like = args.get_or_undefined(0); - add_or_subtract_duration_from_instant(true, &instant, temporal_duration_like, context) + let temporal_duration_like = to_temporal_duration_record(args.get_or_undefined(0))?; + let result = instant.inner.add(temporal_duration_like)?; + create_temporal_instant(result, None, context) } /// 8.3.8 `Temporal.Instant.prototype.subtract ( temporalDurationLike )` @@ -269,8 +258,9 @@ impl Instant { })?; // 3. Return ? AddDurationToOrSubtractDurationFromInstant(subtract, instant, temporalDurationLike). - let temporal_duration_like = args.get_or_undefined(0); - add_or_subtract_duration_from_instant(false, &instant, temporal_duration_like, context) + let temporal_duration_like = to_temporal_duration_record(args.get_or_undefined(0))?; + let result = instant.inner.subtract(temporal_duration_like)?; + create_temporal_instant(result, None, context) } /// 8.3.9 `Temporal.Instant.prototype.until ( other [ , options ] )` @@ -289,9 +279,18 @@ impl Instant { })?; // 3. Return ? DifferenceTemporalInstant(until, instant, other, options). - let other = args.get_or_undefined(0); - let option = args.get_or_undefined(1); - diff_temporal_instant(true, &instant, other, option, context) + let other = to_temporal_instant(args.get_or_undefined(0))?; + + // Fetch the necessary options. + let options = get_options_object(args.get_or_undefined(1))?; + let mode = get_option::(&options, utf16!("roundingMode"), context)?; + let increment = get_option::(&options, utf16!("roundingIncrement"), context)?; + let smallest_unit = get_option::(&options, utf16!("smallestUnit"), context)?; + let largest_unit = get_option::(&options, utf16!("largestUnit"), context)?; + let result = instant + .inner + .until(&other, mode, increment, smallest_unit, largest_unit)?; + create_temporal_duration(result.into(), None, context).map(Into::into) } /// 8.3.10 `Temporal.Instant.prototype.since ( other [ , options ] )` @@ -310,9 +309,16 @@ impl Instant { })?; // 3. Return ? DifferenceTemporalInstant(since, instant, other, options). - let other = args.get_or_undefined(0); - let option = args.get_or_undefined(1); - diff_temporal_instant(false, &instant, other, option, context) + let other = to_temporal_instant(args.get_or_undefined(0))?; + let options = get_options_object(args.get_or_undefined(1))?; + let mode = get_option::(&options, utf16!("roundingMode"), context)?; + let increment = get_option::(&options, utf16!("roundingIncrement"), context)?; + let smallest_unit = get_option::(&options, utf16!("smallestUnit"), context)?; + let largest_unit = get_option::(&options, utf16!("largestUnit"), context)?; + let result = instant + .inner + .since(&other, mode, increment, smallest_unit, largest_unit)?; + create_temporal_duration(result.into(), None, context).map(Into::into) } /// 8.3.11 `Temporal.Instant.prototype.round ( roundTo )` @@ -362,11 +368,12 @@ impl Instant { // 6. NOTE: The following steps read options and perform independent validation in // alphabetical order (ToTemporalRoundingIncrement reads "roundingIncrement" and ToTemporalRoundingMode reads "roundingMode"). // 7. Let roundingIncrement be ? ToTemporalRoundingIncrement(roundTo). - let rounding_increment = get_temporal_rounding_increment(&round_to, context)?; + let rounding_increment = + get_option::(&round_to, utf16!("roundingIncrement"), context)?; // 8. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand"). let rounding_mode = - get_option(&round_to, utf16!("roundingMode"), context)?.unwrap_or_default(); + get_option::(&round_to, utf16!("roundingMode"), context)?; // 9. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit"), time, required). let smallest_unit = get_temporal_unit( @@ -378,43 +385,28 @@ impl Instant { )? .ok_or_else(|| JsNativeError::range().with_message("smallestUnit cannot be undefined."))?; - let maximum = match smallest_unit { - // 10. If smallestUnit is "hour"), then - // a. Let maximum be HoursPerDay. - TemporalUnit::Hour => 24u64, - // 11. Else if smallestUnit is "minute"), then - // a. Let maximum be MinutesPerHour × HoursPerDay. - TemporalUnit::Minute => 14400u64, - // 12. Else if smallestUnit is "second"), then - // a. Let maximum be SecondsPerMinute × MinutesPerHour × HoursPerDay. - TemporalUnit::Second => 86400u64, - // 13. Else if smallestUnit is "millisecond"), then - // a. Let maximum be ℝ(msPerDay). - TemporalUnit::Millisecond => MS_PER_DAY as u64, - // 14. Else if smallestUnit is "microsecond"), then - // a. Let maximum be 10^3 × ℝ(msPerDay). - TemporalUnit::Microsecond => MIS_PER_DAY as u64, - // 15. Else, - // a. Assert: smallestUnit is "nanosecond". - // b. Let maximum be nsPerDay. - TemporalUnit::Nanosecond => NS_PER_DAY as u64, - // unreachable here functions as 15.a. - _ => unreachable!(), - }; - + // 10. If smallestUnit is "hour"), then + // a. Let maximum be HoursPerDay. + // 11. Else if smallestUnit is "minute"), then + // a. Let maximum be MinutesPerHour × HoursPerDay. + // 12. Else if smallestUnit is "second"), then + // a. Let maximum be SecondsPerMinute × MinutesPerHour × HoursPerDay. + // 13. Else if smallestUnit is "millisecond"), then + // a. Let maximum be ℝ(msPerDay). + // 14. Else if smallestUnit is "microsecond"), then + // a. Let maximum be 10^3 × ℝ(msPerDay). + // 15. Else, + // a. Assert: smallestUnit is "nanosecond". + // b. Let maximum be nsPerDay. + // unreachable here functions as 15.a. // 16. Perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, true). - super::validate_temporal_rounding_increment(rounding_increment.into(), maximum, true)?; - // 17. Let roundedNs be RoundTemporalInstant(instant.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode). - let rounded_ns = round_temporal_instant( - &instant.nanoseconds, - rounding_increment.into(), - smallest_unit, - rounding_mode, - )?; + let result = instant + .inner + .round(rounding_increment, smallest_unit, rounding_mode)?; // 18. Return ! CreateTemporalInstant(roundedNs). - create_temporal_instant(rounded_ns, None, context) + create_temporal_instant(result, None, context) } /// 8.3.12 `Temporal.Instant.prototype.equals ( other )` @@ -434,7 +426,7 @@ impl Instant { let other = args.get_or_undefined(0); let other_instant = to_temporal_instant(other)?; - if instant.nanoseconds != other_instant.nanoseconds { + if instant.inner != other_instant { return Ok(false.into()); } Ok(true.into()) @@ -467,30 +459,17 @@ impl Instant { // -- Instant Abstract Operations -- -/// 8.5.1 `IsValidEpochNanoseconds ( epochNanoseconds )` -#[inline] -fn is_valid_epoch_nanos(epoch_nanos: &JsBigInt) -> bool { - // 1. Assert: Type(epochNanoseconds) is BigInt. - // 2. If ℝ(epochNanoseconds) < nsMinInstant or ℝ(epochNanoseconds) > nsMaxInstant, then - if epoch_nanos.to_f64() < ns_min_instant().to_f64() - || epoch_nanos.to_f64() > ns_max_instant().to_f64() - { - // a. Return false. - return false; - } - // 3. Return true. - true -} +// 8.5.1 `IsValidEpochNanoseconds ( epochNanoseconds )` +// Implemented in `boa_temporal` /// 8.5.2 `CreateTemporalInstant ( epochNanoseconds [ , newTarget ] )` #[inline] fn create_temporal_instant( - epoch_nanos: JsBigInt, + instant: InnerInstant, new_target: Option, context: &mut Context, ) -> JsResult { // 1. Assert: ! IsValidEpochNanoseconds(epochNanoseconds) is true. - assert!(is_valid_epoch_nanos(&epoch_nanos)); // 2. If newTarget is not present, set newTarget to %Temporal.Instant%. let new_target = new_target.unwrap_or_else(|| { context @@ -506,12 +485,7 @@ fn create_temporal_instant( get_prototype_from_constructor(&new_target, StandardConstructors::instant, context)?; // 4. Set object.[[Nanoseconds]] to epochNanoseconds. - let obj = JsObject::from_proto_and_data( - proto, - Instant { - nanoseconds: epoch_nanos, - }, - ); + let obj = JsObject::from_proto_and_data(proto, Instant { inner: instant }); // 5. Return object. Ok(obj.into()) @@ -519,231 +493,9 @@ fn create_temporal_instant( /// 8.5.3 `ToTemporalInstant ( item )` #[inline] -fn to_temporal_instant(_: &JsValue) -> JsResult { +fn to_temporal_instant(_: &JsValue) -> JsResult { // TODO: Need to implement parsing. Err(JsNativeError::error() .with_message("Instant parsing is not yet implemented.") .into()) } - -/// 8.5.6 `AddInstant ( epochNanoseconds, hours, minutes, seconds, milliseconds, microseconds, nanoseconds )` -#[inline] -fn add_instant( - epoch_nanos: &JsBigInt, - hours: i32, - minutes: i32, - seconds: i32, - millis: i32, - micros: i32, - nanos: i32, -) -> JsResult { - let result = JsBigInt::add_n(&[ - JsBigInt::mul( - &JsBigInt::from(hours), - &JsBigInt::from(NANOSECONDS_PER_HOUR), - ), - JsBigInt::mul( - &JsBigInt::from(minutes), - &JsBigInt::from(NANOSECONDS_PER_MINUTE), - ), - JsBigInt::mul( - &JsBigInt::from(seconds), - &JsBigInt::from(NANOSECONDS_PER_SECOND), - ), - JsBigInt::mul(&JsBigInt::from(millis), &JsBigInt::from(10_000_000_i32)), - JsBigInt::mul(&JsBigInt::from(micros), &JsBigInt::from(1000_i32)), - JsBigInt::add(&JsBigInt::from(nanos), epoch_nanos), - ]); - if !is_valid_epoch_nanos(&result) { - return Err(JsNativeError::range() - .with_message("result is not a valid epoch nanosecond value.") - .into()); - } - Ok(result) -} - -/// 8.5.7 `DifferenceInstant ( ns1, ns2, roundingIncrement, smallestUnit, largestUnit, roundingMode )` -#[inline] -fn diff_instant( - ns1: &JsBigInt, - ns2: &JsBigInt, - _rounding_increment: f64, - _smallest_unit: TemporalUnit, - _largest_unit: TemporalUnit, - _rounding_mode: RoundingMode, - _context: &mut Context, -) -> JsResult { - // 1. Let difference be ℝ(ns2) - ℝ(ns1). - let difference = JsBigInt::sub(ns1, ns2); - // 2. Let nanoseconds be remainder(difference, 1000). - let _nanoseconds = JsBigInt::rem(&difference, &JsBigInt::from(1000)); - // 3. Let microseconds be remainder(truncate(difference / 1000), 1000). - let truncated_micro = JsBigInt::try_from((&difference.to_f64() / 1000_f64).trunc()) - .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; - let _microseconds = JsBigInt::rem(&truncated_micro, &JsBigInt::from(1000)); - - // 4. Let milliseconds be remainder(truncate(difference / 106), 1000). - let truncated_milli = JsBigInt::try_from((&difference.to_f64() / 1_000_000_f64).trunc()) - .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; - let _milliseconds = JsBigInt::rem(&truncated_milli, &JsBigInt::from(1000)); - - // 5. Let seconds be truncate(difference / 10^9). - let _seconds = (&difference.to_f64() / 1_000_000_000_f64).trunc(); - - // TODO: Update to new Temporal library - // 6. If smallestUnit is "nanosecond" and roundingIncrement is 1, then - // a. Return ! BalanceTimeDuration(0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, largestUnit). - // 7. Let roundResult be ! RoundDuration(0, 0, 0, 0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, roundingIncrement, smallestUnit, largestUnit, roundingMode). - // 8. Assert: roundResult.[[Days]] is 0. - // 9. Return ! BalanceTimeDuration(0, roundResult.[[Hours]], roundResult.[[Minutes]], - // roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], - // roundResult.[[Nanoseconds]], largestUnit). - - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) -} - -/// 8.5.8 `RoundTemporalInstant ( ns, increment, unit, roundingMode )` -#[inline] -fn round_temporal_instant( - ns: &JsBigInt, - increment: f64, - unit: TemporalUnit, - rounding_mode: RoundingMode, -) -> JsResult { - let increment_ns = match unit { - // 1. If unit is "hour"), then - TemporalUnit::Hour => { - // a. Let incrementNs be increment × 3.6 × 10^12. - increment as i64 * NANOSECONDS_PER_HOUR - } - // 2. Else if unit is "minute"), then - TemporalUnit::Minute => { - // a. Let incrementNs be increment × 6 × 10^10. - increment as i64 * NANOSECONDS_PER_MINUTE - } - // 3. Else if unit is "second"), then - TemporalUnit::Second => { - // a. Let incrementNs be increment × 10^9. - increment as i64 * NANOSECONDS_PER_SECOND - } - // 4. Else if unit is "millisecond"), then - TemporalUnit::Millisecond => { - // a. Let incrementNs be increment × 10^6. - increment as i64 * 1_000_000 - } - // 5. Else if unit is "microsecond"), then - TemporalUnit::Microsecond => { - // a. Let incrementNs be increment × 10^3. - increment as i64 * 1000 - } - // 6. Else, - TemporalUnit::Nanosecond => { - // NOTE: We shouldn't have to assert here as `unreachable` asserts instead. - // a. Assert: unit is "nanosecond". - // b. Let incrementNs be increment. - increment as i64 - } - _ => unreachable!(), - }; - - // 7. Return ℤ(RoundNumberToIncrementAsIfPositive(ℝ(ns), incrementNs, roundingMode)). - super::round_to_increment_as_if_positive(ns, increment_ns, rounding_mode) -} - -/// 8.5.10 `DifferenceTemporalInstant ( operation, instant, other, options )` -#[inline] -fn diff_temporal_instant( - op: bool, - instant: &Instant, - other: &JsValue, - options: &JsValue, - context: &mut Context, -) -> JsResult { - // 1. If operation is since, let sign be -1. Otherwise, let sign be 1. - let _sign = if op { 1_f64 } else { -1_f64 }; - // 2. Set other to ? ToTemporalInstant(other). - let other = to_temporal_instant(other)?; - // 3. Let resolvedOptions be ? CopyOptions(options). - let resolved_options = - super::snapshot_own_properties(&get_options_object(options)?, None, None, context)?; - - // 4. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, time, « », "nanosecond"), "second"). - let settings = super::get_diff_settings( - op, - &resolved_options, - TemporalUnitGroup::Time, - &[], - TemporalUnit::Nanosecond, - TemporalUnit::Second, - context, - )?; - - // 5. Let result be DifferenceInstant(instant.[[Nanoseconds]], other.[[Nanoseconds]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[LargestUnit]], settings.[[RoundingMode]]). - let _result = diff_instant( - &instant.nanoseconds, - &other.nanoseconds, - settings.3, - settings.0, - settings.1, - settings.2, - context, - )?; - - // TODO: diff_instant will error so this shouldn't run. - unimplemented!(); - // 6. Return ! CreateTemporalDuration(0, 0, 0, 0, sign × result.[[Hours]], sign × result.[[Minutes]], sign × result.[[Seconds]], sign × result.[[Milliseconds]], sign × result.[[Microseconds]], sign × result.[[Nanoseconds]]). -} - -/// 8.5.11 `AddDurationToOrSubtractDurationFromInstant ( operation, instant, temporalDurationLike )` -#[inline] -fn add_or_subtract_duration_from_instant( - op: bool, - instant: &Instant, - temporal_duration_like: &JsValue, - context: &mut Context, -) -> JsResult { - // 1. If operation is subtract, let sign be -1. Otherwise, let sign be 1. - let sign = if op { 1 } else { -1 }; - // 2. Let duration be ? ToTemporalDurationRecord(temporalDurationLike). - let duration = super::to_temporal_duration_record(temporal_duration_like)?; - // 3. If duration.[[Days]] is not 0, throw a RangeError exception. - if duration.date().days() != 0_f64 { - return Err(JsNativeError::range() - .with_message("DurationDays cannot be 0") - .into()); - } - // 4. If duration.[[Months]] is not 0, throw a RangeError exception. - if duration.date().months() != 0_f64 { - return Err(JsNativeError::range() - .with_message("DurationMonths cannot be 0") - .into()); - } - // 5. If duration.[[Weeks]] is not 0, throw a RangeError exception. - if duration.date().weeks() != 0_f64 { - return Err(JsNativeError::range() - .with_message("DurationWeeks cannot be 0") - .into()); - } - // 6. If duration.[[Years]] is not 0, throw a RangeError exception. - if duration.date().years() != 0_f64 { - return Err(JsNativeError::range() - .with_message("DurationYears cannot be 0") - .into()); - } - // 7. Let ns be ? AddInstant(instant.[[Nanoseconds]], sign × duration.[[Hours]], - // sign × duration.[[Minutes]], sign × duration.[[Seconds]], sign × duration.[[Milliseconds]], - // sign × duration.[[Microseconds]], sign × duration.[[Nanoseconds]]). - let new = add_instant( - &instant.nanoseconds, - sign * duration.time().hours() as i32, - sign * duration.time().minutes() as i32, - sign * duration.time().seconds() as i32, - sign * duration.time().milliseconds() as i32, - sign * duration.time().microseconds() as i32, - sign * duration.time().nanoseconds() as i32, - )?; - // 8. Return ! CreateTemporalInstant(ns). - create_temporal_instant(new, None, context) -} diff --git a/core/engine/src/builtins/temporal/mod.rs b/core/engine/src/builtins/temporal/mod.rs index d3dcc64882..4147370764 100644 --- a/core/engine/src/builtins/temporal/mod.rs +++ b/core/engine/src/builtins/temporal/mod.rs @@ -22,41 +22,30 @@ mod zoned_date_time; #[cfg(test)] mod tests; -use self::options::{get_temporal_rounding_increment, get_temporal_unit, TemporalUnitGroup}; pub use self::{ calendar::*, duration::*, instant::*, now::*, plain_date::*, plain_date_time::*, plain_month_day::*, plain_time::*, plain_year_month::*, time_zone::*, zoned_date_time::*, }; use crate::{ - builtins::{ - iterable::IteratorRecord, - options::{get_option, RoundingMode, UnsignedRoundingMode}, - BuiltInBuilder, BuiltInObject, IntrinsicObject, - }, + builtins::{iterable::IteratorRecord, BuiltInBuilder, BuiltInObject, IntrinsicObject}, context::intrinsics::Intrinsics, js_string, property::Attribute, realm::Realm, - string::{common::StaticJsStrings, utf16}, + string::common::StaticJsStrings, value::Type, Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, }; use boa_profiler::Profiler; -use boa_temporal::options::TemporalUnit; - -// Relavant numeric constants -/// Nanoseconds per day constant: 8.64e+13 -pub(crate) const NS_PER_DAY: i64 = 86_400_000_000_000; -/// Microseconds per day constant: 8.64e+10 -pub(crate) const MIS_PER_DAY: i64 = 8_640_000_000; -/// Milliseconds per day constant: 8.64e+7 -pub(crate) const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000; +use boa_temporal::NS_PER_DAY; +// TODO: Remove in favor of `boa_temporal` pub(crate) fn ns_max_instant() -> JsBigInt { JsBigInt::from(i128::from(NS_PER_DAY) * 100_000_000_i128) } +// TODO: Remove in favor of `boa_temporal` pub(crate) fn ns_min_instant() -> JsBigInt { JsBigInt::from(i128::from(NS_PER_DAY) * -100_000_000_i128) } @@ -226,9 +215,7 @@ pub(crate) fn _iterator_to_list_of_types( // Note: implemented on IsoDateRecord. // Abstract Operation 13.3 `EpochDaysToEpochMs` -pub(crate) fn _epoch_days_to_epoch_ms(day: i32, time: i32) -> f64 { - f64::from(day).mul_add(f64::from(MS_PER_DAY), f64::from(time)) -} +// Migrated to `boa_temporal` // 13.4 Date Equations // implemented in temporal/date_equations.rs @@ -306,127 +293,14 @@ pub(crate) fn to_relative_temporal_object( // 13.26 `GetUnsignedRoundingMode ( roundingMode, isNegative )` // Implemented on RoundingMode in builtins/options.rs -/// 13.27 `ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode )` -#[inline] -fn apply_unsigned_rounding_mode( - x: f64, - r1: f64, - r2: f64, - unsigned_rounding_mode: UnsignedRoundingMode, -) -> f64 { - // 1. If x is equal to r1, return r1. - if (x - r1).abs() == 0.0 { - return r1; - }; - // 2. Assert: r1 < x < r2. - assert!(r1 < x && x < r2); - // 3. Assert: unsignedRoundingMode is not undefined. - - // 4. If unsignedRoundingMode is zero, return r1. - if unsigned_rounding_mode == UnsignedRoundingMode::Zero { - return r1; - }; - // 5. If unsignedRoundingMode is infinity, return r2. - if unsigned_rounding_mode == UnsignedRoundingMode::Infinity { - return r2; - }; - - // 6. Let d1 be x – r1. - let d1 = x - r1; - // 7. Let d2 be r2 – x. - let d2 = r2 - x; - // 8. If d1 < d2, return r1. - if d1 < d2 { - return r1; - } - // 9. If d2 < d1, return r2. - if d2 < d1 { - return r2; - } - // 10. Assert: d1 is equal to d2. - assert!((d1 - d2).abs() == 0.0); +// 13.27 `ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode )` +// Migrated to `boa_temporal` - // 11. If unsignedRoundingMode is half-zero, return r1. - if unsigned_rounding_mode == UnsignedRoundingMode::HalfZero { - return r1; - }; - // 12. If unsignedRoundingMode is half-infinity, return r2. - if unsigned_rounding_mode == UnsignedRoundingMode::HalfInfinity { - return r2; - }; - // 13. Assert: unsignedRoundingMode is half-even. - assert!(unsigned_rounding_mode == UnsignedRoundingMode::HalfEven); - // 14. Let cardinality be (r1 / (r2 – r1)) modulo 2. - let cardinality = (r1 / (r2 - r1)) % 2.0; - // 15. If cardinality is 0, return r1. - if cardinality == 0.0 { - return r1; - } - // 16. Return r2. - r2 -} +// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )` +// Migrated to `boa_temporal` -/// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )` -pub(crate) fn _round_number_to_increment( - x: f64, - increment: f64, - rounding_mode: RoundingMode, -) -> f64 { - // 1. Let quotient be x / increment. - let mut quotient = x / increment; - - // 2. If quotient < 0, then - let is_negative = if quotient < 0_f64 { - // a. Let isNegative be true. - // b. Set quotient to -quotient. - quotient = -quotient; - true - // 3. Else, - } else { - // a. Let isNegative be false. - false - }; - - // 4. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative). - let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(is_negative); - // 5. Let r1 be the largest integer such that r1 ≤ quotient. - let r1 = quotient.ceil(); - // 6. Let r2 be the smallest integer such that r2 > quotient. - let r2 = quotient.floor(); - // 7. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode). - let mut rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode); - // 8. If isNegative is true, set rounded to -rounded. - if is_negative { - rounded = -rounded; - }; - // 9. Return rounded × increment. - rounded * increment -} - -/// 13.29 `RoundNumberToIncrementAsIfPositive ( x, increment, roundingMode )` -#[inline] -pub(crate) fn round_to_increment_as_if_positive( - ns: &JsBigInt, - increment: i64, - rounding_mode: RoundingMode, -) -> JsResult { - // 1. Let quotient be x / increment. - let q = ns.to_f64() / increment as f64; - // 2. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, false). - let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(false); - // 3. Let r1 be the largest integer such that r1 ≤ quotient. - let r1 = q.trunc(); - // 4. Let r2 be the smallest integer such that r2 > quotient. - let r2 = q.trunc() + 1.0; - // 5. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode). - let rounded = apply_unsigned_rounding_mode(q, r1, r2, unsigned_rounding_mode); - - // 6. Return rounded × increment. - let rounded = JsBigInt::try_from(rounded) - .map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; - - Ok(JsBigInt::mul(&rounded, &JsBigInt::from(increment))) -} +// 13.29 `RoundNumberToIncrementAsIfPositive ( x, increment, roundingMode )` +// Migrated to `boa_temporal` /// 13.43 `ToPositiveIntegerWithTruncation ( argument )` #[inline] @@ -480,174 +354,13 @@ pub(crate) fn to_integer_if_integral(arg: &JsValue, context: &mut Context) -> Js // See fields.rs // NOTE: op -> true == until | false == since -/// 13.47 `GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, fallbackSmallestUnit, smallestLargestDefaultUnit )` -#[inline] -pub(crate) fn get_diff_settings( - op: bool, - options: &JsObject, - unit_group: TemporalUnitGroup, - disallowed_units: &[TemporalUnit], - fallback_smallest_unit: TemporalUnit, - smallest_largest_default_unit: TemporalUnit, - context: &mut Context, -) -> JsResult<(TemporalUnit, TemporalUnit, RoundingMode, f64)> { - // 1. NOTE: The following steps read options and perform independent validation in alphabetical order (ToTemporalRoundingIncrement reads "roundingIncrement" and ToTemporalRoundingMode reads "roundingMode"). - // 2. Let largestUnit be ? GetTemporalUnit(options, "largestUnit", unitGroup, "auto"). - let mut largest_unit = - get_temporal_unit(options, utf16!("largestUnit"), unit_group, None, context)? - .unwrap_or(TemporalUnit::Auto); - - // 3. If disallowedUnits contains largestUnit, throw a RangeError exception. - if disallowed_units.contains(&largest_unit) { - return Err(JsNativeError::range() - .with_message("largestUnit is not an allowed unit.") - .into()); - } - - // 4. Let roundingIncrement be ? ToTemporalRoundingIncrement(options). - let rounding_increment = get_temporal_rounding_increment(options, context)?; - - // 5. Let roundingMode be ? ToTemporalRoundingMode(options, "trunc"). - let mut rounding_mode = - get_option(options, utf16!("roundingMode"), context)?.unwrap_or(RoundingMode::Trunc); - - // 6. If operation is since, then - if !op { - // a. Set roundingMode to ! NegateTemporalRoundingMode(roundingMode). - rounding_mode = rounding_mode.negate(); - } - - // 7. Let smallestUnit be ? GetTemporalUnit(options, "smallestUnit", unitGroup, fallbackSmallestUnit). - let smallest_unit = - get_temporal_unit(options, utf16!("smallestUnit"), unit_group, None, context)? - .unwrap_or(fallback_smallest_unit); - - // 8. If disallowedUnits contains smallestUnit, throw a RangeError exception. - if disallowed_units.contains(&smallest_unit) { - return Err(JsNativeError::range() - .with_message("smallestUnit is not an allowed unit.") - .into()); - } - - // 9. Let defaultLargestUnit be ! LargerOfTwoTemporalUnits(smallestLargestDefaultUnit, smallestUnit). - let default_largest_unit = core::cmp::max(smallest_largest_default_unit, smallest_unit); - - // 10. If largestUnit is "auto", set largestUnit to defaultLargestUnit. - if largest_unit == TemporalUnit::Auto { - largest_unit = default_largest_unit; - } - - // 11. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception. - if largest_unit != core::cmp::max(largest_unit, smallest_unit) { - return Err(JsNativeError::range() - .with_message("largestUnit must be larger than smallestUnit") - .into()); - } - - // 12. Let maximum be ! MaximumTemporalDurationRoundingIncrement(smallestUnit). - let maximum = smallest_unit.to_maximum_rounding_increment(); - - // 13. If maximum is not undefined, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false). - if let Some(max) = maximum { - validate_temporal_rounding_increment(rounding_increment.into(), max.into(), false)?; - } - - // 14. Return the Record { [[SmallestUnit]]: smallestUnit, [[LargestUnit]]: largestUnit, [[RoundingMode]]: roundingMode, [[RoundingIncrement]]: roundingIncrement, }. - Ok(( - smallest_unit, - largest_unit, - rounding_mode, - rounding_increment.into(), - )) -} +// 13.47 `GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, fallbackSmallestUnit, smallestLargestDefaultUnit )` +// Migrated to `boa_temporal` // NOTE: used for MergeFields methods. Potentially can be omitted in favor of `TemporalFields`. -/// 14.6 `CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] )` -pub(crate) fn copy_data_properties( - target: &JsObject, - source: &JsValue, - excluded_keys: &Vec, - excluded_values: Option<&Vec>, - context: &mut Context, -) -> JsResult<()> { - // 1. If source is undefined or null, return unused. - if source.is_null_or_undefined() { - return Ok(()); - } - - // 2. Let from be ! ToObject(source). - let from = source.to_object(context)?; - - // 3. Let keys be ? from.[[OwnPropertyKeys]](). - let keys = from.__own_property_keys__(context)?; - - // 4. For each element nextKey of keys, do - for next_key in keys { - // a. Let excluded be false. - let mut excluded = false; - // b. For each element e of excludedItemsexcludedKeys, do - for e in excluded_keys { - // i. If SameValue(e, nextKey) is true, then - if next_key.to_string() == e.to_std_string_escaped() { - // 1. Set excluded to true. - excluded = true; - } - } - - // c. If excluded is false, then - if !excluded { - // i. Let desc be ? from.[[GetOwnProperty]](nextKey). - let desc = from.__get_own_property__(&next_key, &mut context.into())?; - // ii. If desc is not undefined and desc.[[Enumerable]] is true, then - match desc { - Some(d) - if d.enumerable() - .expect("enumerable field must be set per spec.") => - { - // 1. Let propValue be ? Get(from, nextKey). - let prop_value = from.get(next_key.clone(), context)?; - // 2. If excludedValues is present, then - if let Some(values) = excluded_values { - // a. For each element e of excludedValues, do - for e in values { - // i. If SameValue(e, propValue) is true, then - if JsValue::same_value(e, &prop_value) { - // i. Set excluded to true. - excluded = true; - } - } - } - - // 3. PerformIf excluded is false, perform ! CreateDataPropertyOrThrow(target, nextKey, propValue). - if !excluded { - target.create_data_property_or_throw(next_key, prop_value, context)?; - } - } - _ => {} - } - } - } - - // 5. Return unused. - Ok(()) -} +// 14.6 `CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] )` +// Migrated or repurposed to `boa_temporal`/`fields.rs` // Note: Deviates from Proposal spec -> proto appears to be always null across the specification. -/// 14.7 `SnapshotOwnProperties ( source, proto [ , excludedKeys [ , excludedValues ] ] )` -fn snapshot_own_properties( - source: &JsObject, - excluded_keys: Option>, - excluded_values: Option>, - context: &mut Context, -) -> JsResult { - // 1. Let copy be OrdinaryObjectCreate(proto). - let copy = JsObject::with_null_proto(); - // 2. If excludedKeys is not present, set excludedKeys to « ». - let keys = excluded_keys.unwrap_or_default(); - // 3. If excludedValues is not present, set excludedValues to « ». - let values = excluded_values.unwrap_or_default(); - // 4. Perform ? CopyDataProperties(copy, source, excludedKeys, excludedValues). - copy_data_properties(©, &source.clone().into(), &keys, Some(&values), context)?; - // 5. Return copy. - Ok(copy) -} +// 14.7 `SnapshotOwnProperties ( source, proto [ , excludedKeys [ , excludedValues ] ] )` +// Migrated or repurposed to `boa_temporal`/`fields.rs` diff --git a/core/engine/src/builtins/temporal/options.rs b/core/engine/src/builtins/temporal/options.rs index b881cdbbf7..5c9a04a822 100644 --- a/core/engine/src/builtins/temporal/options.rs +++ b/core/engine/src/builtins/temporal/options.rs @@ -13,11 +13,13 @@ use crate::{ js_string, Context, JsNativeError, JsObject, JsResult, }; use boa_temporal::options::{ - ArithmeticOverflow, DurationOverflow, InstantDisambiguation, OffsetDisambiguation, TemporalUnit, + ArithmeticOverflow, DurationOverflow, InstantDisambiguation, OffsetDisambiguation, + TemporalRoundingMode, TemporalUnit, }; // TODO: Expand docs on the below options. +// TODO: Remove and refactor: migrate to `boa_temporal` #[inline] pub(crate) fn get_temporal_rounding_increment( options: &JsObject, @@ -131,3 +133,4 @@ impl ParsableOptionType for ArithmeticOverflow {} impl ParsableOptionType for DurationOverflow {} impl ParsableOptionType for InstantDisambiguation {} impl ParsableOptionType for OffsetDisambiguation {} +impl ParsableOptionType for TemporalRoundingMode {} diff --git a/core/temporal/src/components/duration.rs b/core/temporal/src/components/duration.rs index 0f0255a1b7..c6aff0fbc8 100644 --- a/core/temporal/src/components/duration.rs +++ b/core/temporal/src/components/duration.rs @@ -61,6 +61,11 @@ impl Duration { pub(crate) fn one_week(week_value: f64) -> Self { Self::from_date_duration(DateDuration::new_unchecked(0f64, 0f64, week_value, 0f64)) } + + /// Utility that checks whether `Duration`'s `DateDuration` is empty. + pub(crate) fn is_time_duration(&self) -> bool { + self.date().iter().any(|x| x != 0f64) + } } // ==== Public Duration API ==== @@ -934,6 +939,15 @@ fn duration_sign(set: &Vec) -> i32 { 0 } +impl From for Duration { + fn from(value: TimeDuration) -> Self { + Self { + time: value, + date: DateDuration::default(), + } + } +} + // ==== FromStr trait impl ==== impl FromStr for Duration { diff --git a/core/temporal/src/components/duration/time.rs b/core/temporal/src/components/duration/time.rs index 242a29ce01..eb5ee90b19 100644 --- a/core/temporal/src/components/duration/time.rs +++ b/core/temporal/src/components/duration/time.rs @@ -292,6 +292,20 @@ impl TimeDuration { } } + /// Returns a negated `TimeDuration`. + #[inline] + #[must_use] + pub fn neg(&self) -> Self { + Self { + hours: self.hours * -1f64, + minutes: self.minutes * -1f64, + seconds: self.seconds * -1f64, + milliseconds: self.milliseconds * -1f64, + microseconds: self.microseconds * -1f64, + nanoseconds: self.nanoseconds * -1f64, + } + } + /// Balances a `TimeDuration` given a day value and the largest unit. `balance` will return /// the balanced `day` and `TimeDuration`. /// diff --git a/core/temporal/src/components/instant.rs b/core/temporal/src/components/instant.rs index d00a70cd0c..41c2a1da95 100644 --- a/core/temporal/src/components/instant.rs +++ b/core/temporal/src/components/instant.rs @@ -1,13 +1,17 @@ //! An implementation of the Temporal Instant. use crate::{ - components::duration::TimeDuration, - options::{DifferenceSettings, TemporalUnit}, - TemporalError, TemporalResult, + components::{duration::TimeDuration, Duration}, + options::{TemporalRoundingMode, TemporalUnit}, + utils, TemporalError, TemporalResult, MS_PER_DAY, NS_PER_DAY, }; use num_bigint::BigInt; -use num_traits::ToPrimitive; +use num_traits::{FromPrimitive, ToPrimitive}; + +const NANOSECONDS_PER_SECOND: f64 = 1e9; +const NANOSECONDS_PER_MINUTE: f64 = 60f64 * NANOSECONDS_PER_SECOND; +const NANOSECONDS_PER_HOUR: f64 = 60f64 * NANOSECONDS_PER_MINUTE; /// The native Rust implementation of `Temporal.Instant` #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -18,6 +22,23 @@ pub struct Instant { // ==== Private API ==== impl Instant { + /// Adds a `TimeDuration` to the current `Instant`. + /// + /// Temporal-Proposal equivalent: `AddDurationToOrSubtractDurationFrom`. + pub(crate) fn add_to_instant(&self, duration: &TimeDuration) -> TemporalResult { + let result = self.epoch_nanoseconds() + + duration.nanoseconds + + (duration.microseconds * 1000f64) + + (duration.milliseconds * 1_000_000f64) + + (duration.seconds * NANOSECONDS_PER_SECOND) + + (duration.minutes * NANOSECONDS_PER_MINUTE) + + (duration.hours * NANOSECONDS_PER_HOUR); + let nanos = BigInt::from_f64(result).ok_or_else(|| { + TemporalError::range().with_message("Duration added to instant exceeded valid range.") + })?; + Self::new(nanos) + } + // TODO: Add test for `diff_instant`. // NOTE(nekevss): As the below is internal, op will be left as a boolean // with a `since` op being true and `until` being false. @@ -27,35 +48,89 @@ impl Instant { &self, op: bool, other: &Self, - settings: DifferenceSettings, // TODO: Determine DifferenceSettings fate -> is there a better way to approach this. + rounding_mode: Option, + rounding_increment: Option, + largest_unit: Option, + smallest_unit: Option, ) -> TemporalResult { - let diff = self - .nanos - .to_f64() - .expect("valid instant is representable by f64.") - - other - .nanos - .to_f64() - .expect("Valid instant nanos is representable by f64."); + // diff the instant and determine its component values. + let diff = self.to_f64() - other.to_f64(); let nanos = diff.rem_euclid(1000f64); let micros = (diff / 1000f64).trunc().rem_euclid(1000f64); let millis = (diff / 1_000_000f64).trunc().rem_euclid(1000f64); - let secs = (diff / 1_000_000_000f64).trunc(); + let secs = (diff / NANOSECONDS_PER_SECOND).trunc(); - if settings.smallest_unit == TemporalUnit::Nanosecond { + // Handle the settings provided to `diff_instant` + let rounding_increment = rounding_increment.unwrap_or(1.0); + let rounding_mode = if op { + rounding_mode + .unwrap_or(TemporalRoundingMode::Trunc) + .negate() + } else { + rounding_mode.unwrap_or(TemporalRoundingMode::Trunc) + }; + let smallest_unit = smallest_unit.unwrap_or(TemporalUnit::Nanosecond); + // Use the defaultlargestunit which is max smallestlargestdefault and smallestunit + let largest_unit = largest_unit.unwrap_or(smallest_unit.max(TemporalUnit::Second)); + + // TODO: validate roundingincrement + // Steps 11-13 of 13.47 GetDifferenceSettings + + if smallest_unit == TemporalUnit::Nanosecond { let (_, result) = TimeDuration::new_unchecked(0f64, 0f64, secs, millis, micros, nanos) - .balance(0f64, settings.largest_unit)?; + .balance(0f64, largest_unit)?; return Ok(result); } let (round_result, _) = TimeDuration::new(0f64, 0f64, secs, millis, micros, nanos)?.round( - settings.rounding_increment, - settings.smallest_unit, - settings.rounding_mode, + rounding_increment, + smallest_unit, + rounding_mode, )?; - let (_, result) = round_result.balance(0f64, settings.largest_unit)?; + let (_, result) = round_result.balance(0f64, largest_unit)?; Ok(result) } + + /// Rounds a current `Instant` given the resolved options, returning a `BigInt` result. + pub(crate) fn round_instant( + &self, + increment: f64, + unit: TemporalUnit, + rounding_mode: TemporalRoundingMode, + ) -> TemporalResult { + let increment_nanos = match unit { + TemporalUnit::Hour => increment * NANOSECONDS_PER_HOUR, + TemporalUnit::Minute => increment * NANOSECONDS_PER_MINUTE, + TemporalUnit::Second => increment * NANOSECONDS_PER_SECOND, + TemporalUnit::Millisecond => increment * 1_000_000f64, + TemporalUnit::Microsecond => increment * 1_000f64, + TemporalUnit::Nanosecond => increment, + _ => { + return Err(TemporalError::range() + .with_message("Invalid unit provided for Instant::round.")) + } + }; + + let rounded = utils::round_number_to_increment_as_if_positive( + self.to_f64(), + increment_nanos, + rounding_mode, + ); + + BigInt::from_f64(rounded) + .ok_or_else(|| TemporalError::range().with_message("Invalid rounded Instant value.")) + } + + /// 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.nanos + .to_f64() + .expect("A valid instant is representable by f64.") + } } // ==== Public API ==== @@ -71,6 +146,105 @@ impl Instant { Ok(Self { nanos }) } + /// Adds a `Duration` to the current `Instant`, returning an error if the `Duration` + /// contains a `DateDuration`. + #[inline] + pub fn add(&self, duration: Duration) -> TemporalResult { + if !duration.is_time_duration() { + return Err(TemporalError::range() + .with_message("DateDuration values cannot be added to instant.")); + } + self.add_time_duration(duration.time()) + } + + /// Adds a `TimeDuration` to `Instant`. + #[inline] + pub fn add_time_duration(&self, duration: &TimeDuration) -> TemporalResult { + self.add_to_instant(duration) + } + + /// Subtract a `Duration` to the current `Instant`, returning an error if the `Duration` + /// contains a `DateDuration`. + #[inline] + pub fn subtract(&self, duration: Duration) -> TemporalResult { + if !duration.is_time_duration() { + return Err(TemporalError::range() + .with_message("DateDuration values cannot be added to instant.")); + } + self.subtract_time_duration(duration.time()) + } + + /// Subtracts a `TimeDuration` to `Instant`. + #[inline] + pub fn subtract_time_duration(&self, duration: &TimeDuration) -> TemporalResult { + self.add_to_instant(&duration.neg()) + } + + /// Returns a `TimeDuration` representing the duration since provided `Instant` + #[inline] + pub fn since( + &self, + other: &Self, + rounding_mode: Option, + rounding_increment: Option, + largest_unit: Option, + smallest_unit: Option, + ) -> TemporalResult { + self.diff_instant( + true, + other, + rounding_mode, + rounding_increment, + largest_unit, + smallest_unit, + ) + } + + /// Returns a `TimeDuration` representing the duration until provided `Instant` + #[inline] + pub fn until( + &self, + other: &Self, + rounding_mode: Option, + rounding_increment: Option, + largest_unit: Option, + smallest_unit: Option, + ) -> TemporalResult { + self.diff_instant( + false, + other, + rounding_mode, + rounding_increment, + largest_unit, + smallest_unit, + ) + } + + /// Returns an `Instant` by rounding the current `Instant` according to the provided settings. + pub fn round( + &self, + increment: Option, + unit: TemporalUnit, // smallestUnit is required on Instant::round + rounding_mode: Option, + ) -> TemporalResult { + let increment = utils::to_rounding_increment(increment)?; + let mode = rounding_mode.unwrap_or(TemporalRoundingMode::HalfExpand); + let maximum = match unit { + TemporalUnit::Hour => 24u64, + TemporalUnit::Minute => 24 * 60, + TemporalUnit::Second => 24 * 3600, + TemporalUnit::Millisecond => MS_PER_DAY as u64, + TemporalUnit::Microsecond => MS_PER_DAY as u64 * 1000, + TemporalUnit::Nanosecond => NS_PER_DAY as u64, + _ => return Err(TemporalError::range().with_message("Invalid roundTo unit provided.")), + }; + // NOTE: to_rounding_increment returns an f64 within a u32 range. + utils::validate_temporal_rounding_increment(increment as u32, maximum, true)?; + + let round_result = self.round_instant(increment, unit, mode)?; + Self::new(round_result) + } + /// Returns the `epochSeconds` value for this `Instant`. #[must_use] pub fn epoch_seconds(&self) -> f64 { @@ -101,9 +275,7 @@ impl Instant { /// Returns the `epochNanoseconds` value for this `Instant`. #[must_use] pub fn epoch_nanoseconds(&self) -> f64 { - self.nanos - .to_f64() - .expect("A validated Instant should be within a valid f64") + self.to_f64() } } diff --git a/core/temporal/src/lib.rs b/core/temporal/src/lib.rs index 3674c244b2..c8ca605fad 100644 --- a/core/temporal/src/lib.rs +++ b/core/temporal/src/lib.rs @@ -61,11 +61,9 @@ pub type TemporalResult = Result; // Relevant numeric constants /// Nanoseconds per day constant: 8.64e+13 -#[doc(hidden)] -pub(crate) const NS_PER_DAY: i64 = 86_400_000_000_000; +pub const NS_PER_DAY: i64 = 86_400_000_000_000; /// Milliseconds per day constant: 8.64e+7 -#[doc(hidden)] -pub(crate) const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000; +pub const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000; /// Max Instant nanosecond constant #[doc(hidden)] pub(crate) const NS_MAX_INSTANT: i128 = NS_PER_DAY as i128 * 100_000_000i128; diff --git a/core/temporal/src/options.rs b/core/temporal/src/options.rs index 0f80123743..93fe94978d 100644 --- a/core/temporal/src/options.rs +++ b/core/temporal/src/options.rs @@ -7,17 +7,6 @@ use core::{fmt, str::FromStr}; use crate::TemporalError; -// NOTE: Currently the `DifferenceSetting` is the record returned from 13.47 `GetDifferenceSetting`. -// This should be reassessed once Instant is added to the builtin `Temporal.Instant`. -/// The settings for a difference Op -#[derive(Debug, Clone, Copy)] -pub struct DifferenceSettings { - pub(crate) rounding_mode: TemporalRoundingMode, - pub(crate) rounding_increment: f64, - pub(crate) largest_unit: TemporalUnit, - pub(crate) smallest_unit: TemporalUnit, -} - // ==== Options enums and methods ==== /// The relevant unit that should be used for the operation that diff --git a/core/temporal/src/utils.rs b/core/temporal/src/utils.rs index a4e303c694..7e858e97c3 100644 --- a/core/temporal/src/utils.rs +++ b/core/temporal/src/utils.rs @@ -2,13 +2,32 @@ use crate::{ options::{TemporalRoundingMode, TemporalUnsignedRoundingMode}, - MS_PER_DAY, + TemporalError, TemporalResult, MS_PER_DAY, }; use std::ops::Mul; // NOTE: Review the below for optimizations and add ALOT of tests. +/// Converts and validates an `Option` rounding increment value into a valid increment result. +pub(crate) fn to_rounding_increment(increment: Option) -> TemporalResult { + let inc = increment.unwrap_or(1.0); + + if !inc.is_finite() { + return Err(TemporalError::range().with_message("roundingIncrement must be finite.")); + } + + let integer = inc.trunc(); + + if !(1.0..=1_000_000_000f64).contains(&integer) { + return Err( + TemporalError::range().with_message("roundingIncrement is not within a valid range.") + ); + } + + Ok(integer) +} + fn apply_unsigned_rounding_mode( x: f64, r1: f64, @@ -91,9 +110,9 @@ pub(crate) fn round_number_to_increment( // 4. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative). let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(is_negative); // 5. Let r1 be the largest integer such that r1 ≤ quotient. - let r1 = quotient.ceil(); + let r1 = quotient.floor(); // 6. Let r2 be the smallest integer such that r2 > quotient. - let r2 = quotient.floor(); + let r2 = quotient.ceil(); // 7. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode). let mut rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode); // 8. If isNegative is true, set rounded to -rounded. @@ -104,6 +123,46 @@ pub(crate) fn round_number_to_increment( rounded * increment } +/// Rounds provided number assuming that the increment is greater than 0. +pub(crate) fn round_number_to_increment_as_if_positive( + nanos: f64, + increment_nanos: f64, + rounding_mode: TemporalRoundingMode, +) -> f64 { + // 1. Let quotient be x / increment. + let quotient = nanos / increment_nanos; + // 2. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, false). + let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(false); + // 3. Let r1 be the largest integer such that r1 ≤ quotient. + let r1 = quotient.floor(); + // 4. Let r2 be the smallest integer such that r2 > quotient. + let r2 = quotient.ceil(); + // 5. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode). + let rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode); + // 6. Return rounded × increment. + rounded * increment_nanos +} + +pub(crate) fn validate_temporal_rounding_increment( + increment: u32, + dividend: u64, + inclusive: bool, +) -> TemporalResult<()> { + let max = if inclusive { dividend } else { dividend - 1 }; + + if u64::from(increment) > max { + return Err(TemporalError::range().with_message("roundingIncrement exceeds maximum.")); + } + + if dividend.rem_euclid(u64::from(increment)) != 0 { + return Err( + TemporalError::range().with_message("dividend is not divisble by roundingIncrement.") + ); + } + + Ok(()) +} + // ==== Begin Date Equations ==== pub(crate) const MS_PER_HOUR: f64 = 3_600_000f64;