diff --git a/Cargo.lock b/Cargo.lock index 71c8c2a5a1b..5ec50218054 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3200,7 +3200,7 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "temporal_rs" version = "0.0.3" -source = "git+https://github.com/boa-dev/temporal.git?rev=2e6750a16a46c321a1c7092def880c62f3d1ac91#2e6750a16a46c321a1c7092def880c62f3d1ac91" +source = "git+https://github.com/boa-dev/temporal.git?rev=8ef18fedc401761875b815a692443bf54ce9bd96#8ef18fedc401761875b815a692443bf54ce9bd96" dependencies = [ "bitflags 2.6.0", "icu_calendar", diff --git a/Cargo.toml b/Cargo.toml index 3e304837eba..f7fcbd05065 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,7 +116,7 @@ intrusive-collections = "0.9.6" cfg-if = "1.0.0" either = "1.13.0" sys-locale = "0.3.1" -temporal_rs = { git = "https://github.com/boa-dev/temporal.git", rev = "2e6750a16a46c321a1c7092def880c62f3d1ac91" } +temporal_rs = { git = "https://github.com/boa-dev/temporal.git", rev = "8ef18fedc401761875b815a692443bf54ce9bd96" } web-time = "1.1.0" criterion = "0.5.1" float-cmp = "0.9.0" diff --git a/core/engine/src/builtins/temporal/instant/mod.rs b/core/engine/src/builtins/temporal/instant/mod.rs index 3dbe6654d69..28c95e2c633 100644 --- a/core/engine/src/builtins/temporal/instant/mod.rs +++ b/core/engine/src/builtins/temporal/instant/mod.rs @@ -6,6 +6,7 @@ use crate::{ temporal::{ duration::{create_temporal_duration, to_temporal_duration_record}, options::{get_temporal_unit, TemporalUnitGroup}, + ZonedDateTime, }, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, }, @@ -15,13 +16,18 @@ use crate::{ property::Attribute, realm::Realm, string::StaticJsStrings, + value::PreferredType, Context, JsArgs, JsBigInt, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, }; use boa_gc::{Finalize, Trace}; use boa_macros::js_str; use boa_profiler::Profiler; -use temporal_rs::{components::Instant as InnerInstant, options::TemporalRoundingMode}; +use num_traits::ToPrimitive; +use temporal_rs::{ + components::Instant as InnerInstant, + options::{RoundingIncrement, RoundingOptions, TemporalRoundingMode}, +}; use super::options::get_difference_settings; @@ -87,6 +93,18 @@ impl IntrinsicObject for Instant { None, Attribute::CONFIGURABLE, ) + .static_method(Self::from, js_string!("from"), 1) + .static_method( + Self::from_epoch_milliseconds, + js_string!("fromEpochMilliseconds"), + 1, + ) + .static_method( + Self::from_epoch_nanoseconds, + js_string!("fromEpochNanoseconds"), + 1, + ) + .static_method(Self::compare, js_string!("compare"), 1) .method(Self::add, js_string!("add"), 1) .method(Self::subtract, js_string!("subtract"), 1) .method(Self::until, js_string!("until"), 2) @@ -130,14 +148,80 @@ 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. - // NOTE: boa_temporal::Instant asserts that the epochNanoseconds are valid. - let instant = InnerInstant::new(epoch_nanos.as_inner().clone())?; + // NOTE: temporal_rs::Instant asserts that the epochNanoseconds are valid. + let instant = InnerInstant::new(epoch_nanos.as_inner().to_i128().unwrap_or(i128::MAX))?; // 4. Return ? CreateTemporalInstant(epochNanoseconds, NewTarget). create_temporal_instant(instant, Some(new_target.clone()), context) } } -// -- Instant method implementations -- +// ==== Instant Static method implementations ==== + +impl Instant { + pub(crate) fn from(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. If item is an Object and item has an [[InitializedTemporalInstant]] internal slot, then + // a. Return ! CreateTemporalInstant(item.[[Nanoseconds]]). + // 2. Return ? ToTemporalInstant(item). + create_temporal_instant( + to_temporal_instant(args.get_or_undefined(0), context)?, + None, + context, + ) + .map(Into::into) + } + + pub(crate) fn from_epoch_milliseconds( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Set epochMilliseconds to ? ToNumber(epochMilliseconds). + let epoch_millis = args.get_or_undefined(0).to_number(context)?; + // 2. Set epochMilliseconds to ? NumberToBigInt(epochMilliseconds). + // 3. Let epochNanoseconds be epochMilliseconds × ℤ(10**6). + // 4. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. + // 5. Return ! CreateTemporalInstant(epochNanoseconds). + create_temporal_instant( + InnerInstant::from_epoch_milliseconds(epoch_millis.to_i128().unwrap_or(i128::MAX))?, + None, + context, + ) + .map(Into::into) + } + + pub(crate) fn from_epoch_nanoseconds( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Set epochNanoseconds to ? ToBigInt(epochNanoseconds). + let epoch_nanos = args.get_or_undefined(0).to_bigint(context)?; + // 2. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. + // 3. Return ! CreateTemporalInstant(epochNanoseconds). + let nanos = epoch_nanos.as_inner().to_i128(); + create_temporal_instant( + InnerInstant::new(nanos.unwrap_or(i128::MAX))?, + None, + context, + ) + .map(Into::into) + } + + pub(crate) fn compare( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Set one to ? ToTemporalInstant(one). + let one = to_temporal_instant(args.get_or_undefined(0), context)?; + // 2. Set two to ? ToTemporalInstant(two). + let two = to_temporal_instant(args.get_or_undefined(1), context)?; + // 3. Return 𝔽(CompareEpochNanoseconds(one.[[Nanoseconds]], two.[[Nanoseconds]])). + Ok((one.cmp(&two) as i8).into()) + } +} + +// ==== Instant method implementations ==== impl Instant { /// 8.3.3 get Temporal.Instant.prototype.epochSeconds @@ -281,7 +365,7 @@ impl Instant { })?; // 3. Return ? DifferenceTemporalInstant(until, instant, other, options). - let other = to_temporal_instant(args.get_or_undefined(0))?; + let other = to_temporal_instant(args.get_or_undefined(0), context)?; // Fetch the necessary options. let settings = @@ -306,7 +390,7 @@ impl Instant { })?; // 3. Return ? DifferenceTemporalInstant(since, instant, other, options). - let other = to_temporal_instant(args.get_or_undefined(0))?; + let other = to_temporal_instant(args.get_or_undefined(0), context)?; let settings = get_difference_settings(&get_options_object(args.get_or_undefined(1))?, context)?; let result = instant.inner.since(&other, settings)?; @@ -358,12 +442,13 @@ impl Instant { // 6. NOTE: The following steps read options and perform independent validation in // alphabetical order (ToTemporalRoundingIncrement reads "roundingIncrement" and ToTemporalRoundingMode reads "roundingMode"). + let mut options = RoundingOptions::default(); // 7. Let roundingIncrement be ? ToTemporalRoundingIncrement(roundTo). - let rounding_increment = - get_option::(&round_to, js_str!("roundingIncrement"), context)?; + options.increment = + get_option::(&round_to, js_str!("roundingIncrement"), context)?; // 8. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand"). - let rounding_mode = + options.rounding_mode = get_option::(&round_to, js_str!("roundingMode"), context)?; // 9. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit"), time, required). @@ -376,6 +461,8 @@ impl Instant { )? .ok_or_else(|| JsNativeError::range().with_message("smallestUnit cannot be undefined."))?; + options.smallest_unit = Some(smallest_unit); + // 10. If smallestUnit is "hour"), then // a. Let maximum be HoursPerDay. // 11. Else if smallestUnit is "minute"), then @@ -392,16 +479,18 @@ impl Instant { // unreachable here functions as 15.a. // 16. Perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, true). // 17. Let roundedNs be RoundTemporalInstant(instant.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode). - let result = instant - .inner - .round(rounding_increment, smallest_unit, rounding_mode)?; + let result = instant.inner.round(options)?; // 18. Return ! CreateTemporalInstant(roundedNs). create_temporal_instant(result, None, context) } /// 8.3.12 `Temporal.Instant.prototype.equals ( other )` - pub(crate) fn equals(this: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult { + pub(crate) fn equals( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { // 1. Let instant be the this value. // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). // 4. If instant.[[Nanoseconds]] ≠ other.[[Nanoseconds]], return false. @@ -415,7 +504,7 @@ impl Instant { // 3. Set other to ? ToTemporalInstant(other). let other = args.get_or_undefined(0); - let other_instant = to_temporal_instant(other)?; + let other_instant = to_temporal_instant(other, context)?; if instant.inner != other_instant { return Ok(false.into()); @@ -484,9 +573,42 @@ fn create_temporal_instant( /// 8.5.3 `ToTemporalInstant ( item )` #[inline] -fn to_temporal_instant(_: &JsValue) -> JsResult { - // TODO: Need to implement parsing. - Err(JsNativeError::error() - .with_message("Instant parsing is not yet implemented.") - .into()) +fn to_temporal_instant(item: &JsValue, context: &mut Context) -> JsResult { + // 1.If item is an Object, then + let item = if let Some(obj) = item.as_object() { + // a. If item has an [[InitializedTemporalInstant]] internal slot, then + // i. Return item. + // b. If item has an [[InitializedTemporalZonedDateTime]] internal slot, then + // i. Return ! CreateTemporalInstant(item.[[Nanoseconds]]). + // c. NOTE: This use of ToPrimitive allows Instant-like objects to be converted. + // d. Set item to ? ToPrimitive(item, string). + if let Some(instant) = obj.downcast_ref::() { + return Ok(instant.inner.clone()); + } else if let Some(_zdt) = obj.downcast_ref::() { + return Err(JsNativeError::error() + .with_message("Not yet implemented.") + .into()); + } + item.to_primitive(context, PreferredType::String)? + } else { + item.clone() + }; + + let Some(string_to_parse) = item.as_string() else { + return Err(JsNativeError::typ() + .with_message("Invalid type to convert to a Temporal.Instant.") + .into()); + }; + + // 3. Let parsed be ? ParseTemporalInstantString(item). + // 4. If parsed.[[TimeZone]].[[Z]] is true, let offsetNanoseconds be 0; otherwise, let offsetNanoseconds be ! ParseDateTimeUTCOffset(parsed.[[TimeZone]].[[OffsetString]]). + // 5. If abs(ISODateToEpochDays(parsed.[[Year]], parsed.[[Month]] - 1, parsed.[[Day]])) > 10**8, throw a RangeError exception. + // 6. Let epochNanoseconds be GetUTCEpochNanoseconds(parsed.[[Year]], parsed.[[Month]], parsed.[[Day]], parsed.[[Hour]], parsed.[[Minute]], parsed.[[Second]], parsed.[[Millisecond]], parsed.[[Microsecond]], parsed.[[Nanosecond]], offsetNanoseconds). + // 7. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. + // 8. Return ! CreateTemporalInstant(epochNanoseconds). + // 2. If item is not a String, throw a TypeError exception. + string_to_parse + .to_std_string_escaped() + .parse::() + .map_err(Into::into) }