Browse Source

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
pull/3609/head
Kevin 10 months ago committed by GitHub
parent
commit
ced2904d22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 11
      core/engine/src/bigint.rs
  2. 19
      core/engine/src/builtins/options.rs
  3. 372
      core/engine/src/builtins/temporal/instant/mod.rs
  4. 323
      core/engine/src/builtins/temporal/mod.rs
  5. 5
      core/engine/src/builtins/temporal/options.rs
  6. 14
      core/temporal/src/components/duration.rs
  7. 14
      core/temporal/src/components/duration/time.rs
  8. 218
      core/temporal/src/components/instant.rs
  9. 6
      core/temporal/src/lib.rs
  10. 11
      core/temporal/src/options.rs
  11. 65
      core/temporal/src/utils.rs

11
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]

19
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<Self> {
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,

372
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::<TemporalRoundingMode>(&options, utf16!("roundingMode"), context)?;
let increment = get_option::<f64>(&options, utf16!("roundingIncrement"), context)?;
let smallest_unit = get_option::<TemporalUnit>(&options, utf16!("smallestUnit"), context)?;
let largest_unit = get_option::<TemporalUnit>(&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::<TemporalRoundingMode>(&options, utf16!("roundingMode"), context)?;
let increment = get_option::<f64>(&options, utf16!("roundingIncrement"), context)?;
let smallest_unit = get_option::<TemporalUnit>(&options, utf16!("smallestUnit"), context)?;
let largest_unit = get_option::<TemporalUnit>(&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::<f64>(&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::<TemporalRoundingMode>(&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!(),
};
// 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<JsValue>,
context: &mut Context,
) -> JsResult<JsValue> {
// 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<Instant> {
fn to_temporal_instant(_: &JsValue) -> JsResult<InnerInstant> {
// 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<JsBigInt> {
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<Duration> {
// 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<JsBigInt> {
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<JsValue> {
// 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<JsValue> {
// 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)
}

323
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<JsBigInt> {
// 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<JsString>,
excluded_values: Option<&Vec<JsValue>>,
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<Vec<JsString>>,
excluded_values: Option<Vec<JsValue>>,
context: &mut Context,
) -> JsResult<JsObject> {
// 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(&copy, &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`

5
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 {}

14
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<f64>) -> i32 {
0
}
impl From<TimeDuration> for Duration {
fn from(value: TimeDuration) -> Self {
Self {
time: value,
date: DateDuration::default(),
}
}
}
// ==== FromStr trait impl ====
impl FromStr for Duration {

14
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`.
///

218
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<Self> {
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<TemporalRoundingMode>,
rounding_increment: Option<f64>,
largest_unit: Option<TemporalUnit>,
smallest_unit: Option<TemporalUnit>,
) -> TemporalResult<TimeDuration> {
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();
// 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));
if settings.smallest_unit == TemporalUnit::Nanosecond {
// 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<BigInt> {
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<Self> {
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> {
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<Self> {
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> {
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<TemporalRoundingMode>,
rounding_increment: Option<f64>,
largest_unit: Option<TemporalUnit>,
smallest_unit: Option<TemporalUnit>,
) -> TemporalResult<TimeDuration> {
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<TemporalRoundingMode>,
rounding_increment: Option<f64>,
largest_unit: Option<TemporalUnit>,
smallest_unit: Option<TemporalUnit>,
) -> TemporalResult<TimeDuration> {
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<f64>,
unit: TemporalUnit, // smallestUnit is required on Instant::round
rounding_mode: Option<TemporalRoundingMode>,
) -> TemporalResult<Self> {
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()
}
}

6
core/temporal/src/lib.rs

@ -61,11 +61,9 @@ pub type TemporalResult<T> = Result<T, TemporalError>;
// 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;

11
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

65
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<f64>` rounding increment value into a valid increment result.
pub(crate) fn to_rounding_increment(increment: Option<f64>) -> TemporalResult<f64> {
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;

Loading…
Cancel
Save