From ea52c74de0fd23c06d5c232c5d3ab313caf43da3 Mon Sep 17 00:00:00 2001 From: Kevin Ness Date: Wed, 17 Jul 2024 20:00:30 -0400 Subject: [PATCH 1/7] Implement more Temporal functionality --- Cargo.lock | 3 +- Cargo.toml | 2 +- .../src/builtins/temporal/duration/mod.rs | 20 ++- .../src/builtins/temporal/plain_date/mod.rs | 25 ++- .../builtins/temporal/plain_date_time/mod.rs | 153 +++++++++++++++++- .../src/builtins/temporal/plain_time/mod.rs | 49 +++++- 6 files changed, 226 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b367fdf4625..ecd5f410a8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3200,8 +3200,7 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "temporal_rs" version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6f351ef929946476b4107c09348c9e25ba1155ff8e3867b1914878a0fb553f" +source = "git+https://github.com/boa-dev/temporal.git?rev=44ed454dbcf751ee43c8b9a9dacf2923e6e3f071#44ed454dbcf751ee43c8b9a9dacf2923e6e3f071" dependencies = [ "bitflags 2.6.0", "icu_calendar", diff --git a/Cargo.toml b/Cargo.toml index a460e1de5b5..f48b1b89c49 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 = "0.0.3" +temporal_rs = { git = "https://github.com/boa-dev/temporal.git", rev = "44ed454dbcf751ee43c8b9a9dacf2923e6e3f071" } web-time = "1.1.0" criterion = "0.5.1" float-cmp = "0.9.0" diff --git a/core/engine/src/builtins/temporal/duration/mod.rs b/core/engine/src/builtins/temporal/duration/mod.rs index c5e07e116b5..15632ecea9d 100644 --- a/core/engine/src/builtins/temporal/duration/mod.rs +++ b/core/engine/src/builtins/temporal/duration/mod.rs @@ -571,14 +571,22 @@ impl Duration { } /// 7.3.16 `Temporal.Duration.prototype.negated ( )` - pub(crate) fn negated(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + pub(crate) fn negated( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { // 1. Let duration be the this value. // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). // 3. Return ! CreateNegatedTemporalDuration(duration). + let duration = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Duration object.") + })?; - Err(JsNativeError::range() - .with_message("not yet implemented.") - .into()) + create_temporal_duration(duration.inner.negated(), None, context).map(Into::into) } /// 7.3.17 `Temporal.Duration.prototype.abs ( )` @@ -595,9 +603,7 @@ impl Duration { JsNativeError::typ().with_message("this value must be a Duration object.") })?; - let abs = duration.inner.abs(); - - create_temporal_duration(abs, None, context).map(Into::into) + create_temporal_duration(duration.inner.abs(), None, context).map(Into::into) } /// 7.3.18 `Temporal.Duration.prototype.add ( other [ , options ] )` diff --git a/core/engine/src/builtins/temporal/plain_date/mod.rs b/core/engine/src/builtins/temporal/plain_date/mod.rs index c05f33b1db8..6daccec5d03 100644 --- a/core/engine/src/builtins/temporal/plain_date/mod.rs +++ b/core/engine/src/builtins/temporal/plain_date/mod.rs @@ -29,8 +29,9 @@ use temporal_rs::{ }; use super::{ - calendar, create_temporal_datetime, create_temporal_duration, options::get_difference_settings, - to_temporal_duration_record, to_temporal_time, PlainDateTime, ZonedDateTime, + calendar::to_temporal_calendar_slot_value, create_temporal_datetime, create_temporal_duration, + options::get_difference_settings, to_temporal_duration_record, to_temporal_time, PlainDateTime, + ZonedDateTime, }; /// The `Temporal.PlainDate` object. @@ -252,7 +253,7 @@ impl BuiltInConstructor for PlainDate { let iso_year = super::to_integer_with_truncation(args.get_or_undefined(0), context)?; let iso_month = super::to_integer_with_truncation(args.get_or_undefined(1), context)?; let iso_day = super::to_integer_with_truncation(args.get_or_undefined(2), context)?; - let calendar_slot = calendar::to_temporal_calendar_slot_value(args.get_or_undefined(3))?; + let calendar_slot = to_temporal_calendar_slot_value(args.get_or_undefined(3))?; let date = InnerDate::new( iso_year, @@ -608,10 +609,18 @@ impl PlainDate { .into()) } - fn with_calendar(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + /// 3.3.26 Temporal.PlainDate.prototype.withCalendar ( calendarLike ) + fn with_calendar(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let date = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainDate object.") + })?; + + let calendar = to_temporal_calendar_slot_value(args.get_or_undefined(0))?; + + create_temporal_date(date.inner.with_calendar(calendar)?, None, context).map(Into::into) } fn until(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { @@ -781,7 +790,7 @@ pub(crate) fn to_temporal_date( let _o = get_option(&options_obj, js_str!("overflow"), context)? .unwrap_or(ArithmeticOverflow::Constrain); - let date = InnerDate::from_datetime(date_time.inner()); + let date = InnerDate::from(date_time.inner().clone()); // ii. Return ! CreateTemporalDate(item.[[ISOYear]], item.[[ISOMonth]], item.[[ISODay]], item.[[Calendar]]). return Ok(date); diff --git a/core/engine/src/builtins/temporal/plain_date_time/mod.rs b/core/engine/src/builtins/temporal/plain_date_time/mod.rs index 9539a540d39..cfd3cccdcf1 100644 --- a/core/engine/src/builtins/temporal/plain_date_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_date_time/mod.rs @@ -4,7 +4,7 @@ use crate::{ builtins::{ options::{get_option, get_options_object}, - temporal::{calendar, to_integer_with_truncation}, + temporal::to_integer_with_truncation, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, @@ -28,10 +28,15 @@ use temporal_rs::{ DateTime as InnerDateTime, }, iso::{IsoDate, IsoDateSlots}, - options::ArithmeticOverflow, + options::{ArithmeticOverflow, RoundingIncrement, RoundingOptions, TemporalRoundingMode}, }; -use super::{to_temporal_duration_record, PlainDate, ZonedDateTime}; +use super::{ + calendar::to_temporal_calendar_slot_value, + create_temporal_duration, + options::{get_difference_settings, get_temporal_unit, TemporalUnitGroup}, + to_temporal_duration_record, to_temporal_time, PlainDate, ZonedDateTime, +}; /// The `Temporal.PlainDateTime` object. #[derive(Debug, Clone, Trace, Finalize, JsData)] @@ -272,8 +277,14 @@ impl IntrinsicObject for PlainDateTime { Attribute::CONFIGURABLE, ) .static_method(Self::from, js_string!("from"), 2) + .method(Self::with_plain_time, js_string!("withPlainTime"), 1) + .method(Self::with_calendar, js_string!("withCalendar"), 1) .method(Self::add, js_string!("add"), 2) .method(Self::subtract, js_string!("subtract"), 2) + .method(Self::until, js_string!("until"), 2) + .method(Self::since, js_string!("since"), 2) + .method(Self::round, js_string!("round"), 1) + .method(Self::equals, js_string!("equals"), 2) .build(); } @@ -332,7 +343,7 @@ impl BuiltInConstructor for PlainDateTime { .get(8) .map_or(Ok(0), |v| to_integer_with_truncation(v, context))?; // 11. Let calendar be ? ToTemporalCalendarSlotValue(calendarLike, "iso8601"). - let calendar_slot = calendar::to_temporal_calendar_slot_value(args.get_or_undefined(9))?; + let calendar_slot = to_temporal_calendar_slot_value(args.get_or_undefined(9))?; let dt = InnerDateTime::new( iso_year, @@ -651,6 +662,39 @@ impl PlainDateTime { // ==== PlainDateTime.prototype method implementations ==== impl PlainDateTime { + /// 5.3.26 Temporal.PlainDateTime.prototype.withPlainTime ( [ plainTimeLike ] ) + fn with_plain_time( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let dt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainDateTime object.") + })?; + + let time = to_temporal_time(args.get_or_undefined(0), None, context)?; + + create_temporal_datetime(dt.inner.with_time(time)?, None, context).map(Into::into) + } + + /// 5.3.27 Temporal.PlainDateTime.prototype.withCalendar ( calendarLike ) + fn with_calendar(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let dt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainDateTime object.") + })?; + + let calendar = to_temporal_calendar_slot_value(args.get_or_undefined(0))?; + + create_temporal_datetime(dt.inner.with_calendar(calendar)?, None, context).map(Into::into) + } + + /// 5.3.28 Temporal.PlainDateTime.prototype.add ( temporalDurationLike [ , options ] ) fn add(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { // 1. Let temporalDate be the this value. // 2. Perform ? RequireInternalSlot(temporalDate, [[InitializedTemporalDate]]). @@ -673,6 +717,7 @@ impl PlainDateTime { create_temporal_datetime(dt.inner.add(&duration, overflow)?, None, context).map(Into::into) } + /// 5.3.29 Temporal.PlainDateTime.prototype.subtract ( temporalDurationLike [ , options ] ) fn subtract(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { // 1. Let temporalDate be the this value. // 2. Perform ? RequireInternalSlot(temporalDate, [[InitializedTemporalDate]]). @@ -697,6 +742,106 @@ impl PlainDateTime { .map(Into::into) } + /// 5.3.30 Temporal.PlainDateTime.prototype.until ( other [ , options ] ) + fn until(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let dt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainDateTime object.") + })?; + + let other = to_temporal_datetime(args.get_or_undefined(0), None, context)?; + + let options = get_options_object(args.get_or_undefined(1))?; + let settings = get_difference_settings(&options, context)?; + + create_temporal_duration(dt.inner.until(&other, settings)?, None, context).map(Into::into) + } + + /// 5.3.31 Temporal.PlainDateTime.prototype.since ( other [ , options ] ) + fn since(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let dt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainDateTime object.") + })?; + + let other = to_temporal_datetime(args.get_or_undefined(0), None, context)?; + + let options = get_options_object(args.get_or_undefined(1))?; + let settings = get_difference_settings(&options, context)?; + + create_temporal_duration(dt.inner.until(&other, settings)?, None, context).map(Into::into) + } + + // TODO(nekevss): finish after temporal_rs impl + /// 5.3.32 Temporal.PlainDateTime.prototype.round ( roundTo ) + fn round(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let dt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainTime object.") + })?; + + let round_to = match args.first() { + // 3. If roundTo is undefined, then + None | Some(JsValue::Undefined) => { + return Err(JsNativeError::typ() + .with_message("roundTo cannot be undefined.") + .into()) + } + // 4. If Type(roundTo) is String, then + Some(JsValue::String(rt)) => { + // a. Let paramString be roundTo. + let param_string = rt.clone(); + // b. Set roundTo to OrdinaryObjectCreate(null). + let new_round_to = JsObject::with_null_proto(); + // c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit", paramString). + new_round_to.create_data_property_or_throw( + js_str!("smallestUnit"), + param_string, + context, + )?; + new_round_to + } + // 5. Else, + Some(round_to) => { + // a. Set roundTo to ? GetOptionsObject(roundTo). + get_options_object(round_to)? + } + }; + + let (plain_relative_to, zoned_relative_to) = + super::to_relative_temporal_object(&round_to, context)?; + + let mut options = RoundingOptions::default(); + + options.increment = + get_option::(&round_to, js_str!("roundingIncrement"), context)?; + + // 8. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand"). + options.rounding_mode = + get_option::(&round_to, js_str!("roundingMode"), context)?; + + // 9. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit", TIME, REQUIRED, undefined). + options.smallest_unit = get_temporal_unit( + &round_to, + js_str!("smallestUnit"), + TemporalUnitGroup::Time, + None, + context, + )?; + + // TODO: implement in temporal_rs + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } + + /// 5.3.33 Temporal.PlainDateTime.prototype.equals ( other ) fn equals(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { // 1. Let dateTime be the this value. // 2. Perform ? RequireInternalSlot(dateTime, [[InitializedTemporalDateTime]]). diff --git a/core/engine/src/builtins/temporal/plain_time/mod.rs b/core/engine/src/builtins/temporal/plain_time/mod.rs index 8f184c0d1e1..8f70d93ddbd 100644 --- a/core/engine/src/builtins/temporal/plain_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_time/mod.rs @@ -22,7 +22,8 @@ use temporal_rs::{ }; use super::{ - options::{get_temporal_unit, TemporalUnitGroup}, + create_temporal_duration, + options::{get_difference_settings, get_temporal_unit, TemporalUnitGroup}, to_integer_with_truncation, to_temporal_duration_record, PlainDateTime, ZonedDateTime, }; @@ -110,6 +111,8 @@ impl IntrinsicObject for PlainTime { .static_method(Self::from, js_string!("from"), 2) .method(Self::add, js_string!("add"), 1) .method(Self::subtract, js_string!("subtract"), 1) + .method(Self::until, js_string!("subtract"), 2) + .method(Self::since, js_string!("subtract"), 2) .method(Self::round, js_string!("round"), 1) .method(Self::equals, js_string!("equals"), 1) .method(Self::get_iso_fields, js_string!("getISOFields"), 0) @@ -326,7 +329,7 @@ impl PlainTime { // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]). let time = this .as_object() - .and_then(JsObject::downcast_ref::) + .and_then(JsObject::downcast_ref::) .ok_or_else(|| { JsNativeError::typ().with_message("the this object must be a PlainTime object.") })?; @@ -344,7 +347,7 @@ impl PlainTime { // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]). let time = this .as_object() - .and_then(JsObject::downcast_ref::) + .and_then(JsObject::downcast_ref::) .ok_or_else(|| { JsNativeError::typ().with_message("the this object must be a PlainTime object.") })?; @@ -356,13 +359,51 @@ impl PlainTime { create_temporal_time(time.inner.subtract(&duration)?, None, context).map(Into::into) } + /// 4.3.12 Temporal.PlainTime.prototype.until ( other [ , options ] ) + fn until(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let time = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainTime object.") + })?; + + let other = to_temporal_time(args.get_or_undefined(0), None, context)?; + + let settings = + get_difference_settings(&get_options_object(args.get_or_undefined(1))?, context)?; + + let result = time.inner.until(&other, settings)?; + + create_temporal_duration(result, None, context).map(Into::into) + } + + /// 4.3.13 Temporal.PlainTime.prototype.since ( other [ , options ] ) + fn since(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let time = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainTime object.") + })?; + + let other = to_temporal_time(args.get_or_undefined(0), None, context)?; + + let settings = + get_difference_settings(&get_options_object(args.get_or_undefined(1))?, context)?; + + let result = time.inner.since(&other, settings)?; + + create_temporal_duration(result, None, context).map(Into::into) + } + /// 4.3.14 Temporal.PlainTime.prototype.round ( roundTo ) fn round(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { // 1. Let temporalTime be the this value. // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]). let time = this .as_object() - .and_then(JsObject::downcast_ref::) + .and_then(JsObject::downcast_ref::) .ok_or_else(|| { JsNativeError::typ().with_message("the this object must be a PlainTime object.") })?; From 877d0ac0598a95df5f9ebb604387193094296286 Mon Sep 17 00:00:00 2001 From: Kevin Ness Date: Wed, 17 Jul 2024 20:07:55 -0400 Subject: [PATCH 2/7] Correct equals method length --- core/engine/src/builtins/temporal/plain_date_time/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/engine/src/builtins/temporal/plain_date_time/mod.rs b/core/engine/src/builtins/temporal/plain_date_time/mod.rs index cfd3cccdcf1..0087306b4b5 100644 --- a/core/engine/src/builtins/temporal/plain_date_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_date_time/mod.rs @@ -284,7 +284,7 @@ impl IntrinsicObject for PlainDateTime { .method(Self::until, js_string!("until"), 2) .method(Self::since, js_string!("since"), 2) .method(Self::round, js_string!("round"), 1) - .method(Self::equals, js_string!("equals"), 2) + .method(Self::equals, js_string!("equals"), 1) .build(); } From df5d6cc392951b37aef3f0bc017e535cfdf6e432 Mon Sep 17 00:00:00 2001 From: Kevin Ness Date: Wed, 17 Jul 2024 20:14:31 -0400 Subject: [PATCH 3/7] patch withPlainTime docs --- core/engine/src/builtins/temporal/plain_date_time/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/engine/src/builtins/temporal/plain_date_time/mod.rs b/core/engine/src/builtins/temporal/plain_date_time/mod.rs index 0087306b4b5..a136dd42913 100644 --- a/core/engine/src/builtins/temporal/plain_date_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_date_time/mod.rs @@ -662,7 +662,7 @@ impl PlainDateTime { // ==== PlainDateTime.prototype method implementations ==== impl PlainDateTime { - /// 5.3.26 Temporal.PlainDateTime.prototype.withPlainTime ( [ plainTimeLike ] ) + /// 5.3.26 Temporal.PlainDateTime.prototype.withPlainTime ( `[ plainTimeLike ]` ) fn with_plain_time( this: &JsValue, args: &[JsValue], From 615845f2d3ee24a629f00a57e9598d37e6694af3 Mon Sep 17 00:00:00 2001 From: Kevin Ness Date: Wed, 17 Jul 2024 20:27:21 -0400 Subject: [PATCH 4/7] Correct time method binding --- core/engine/src/builtins/temporal/plain_time/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/engine/src/builtins/temporal/plain_time/mod.rs b/core/engine/src/builtins/temporal/plain_time/mod.rs index 8f70d93ddbd..7ecff1b9c93 100644 --- a/core/engine/src/builtins/temporal/plain_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_time/mod.rs @@ -111,8 +111,8 @@ impl IntrinsicObject for PlainTime { .static_method(Self::from, js_string!("from"), 2) .method(Self::add, js_string!("add"), 1) .method(Self::subtract, js_string!("subtract"), 1) - .method(Self::until, js_string!("subtract"), 2) - .method(Self::since, js_string!("subtract"), 2) + .method(Self::until, js_string!("until"), 2) + .method(Self::since, js_string!("since"), 2) .method(Self::round, js_string!("round"), 1) .method(Self::equals, js_string!("equals"), 1) .method(Self::get_iso_fields, js_string!("getISOFields"), 0) From 82ba0f87c95634680e1b6ec7aeeaf76218546924 Mon Sep 17 00:00:00 2001 From: Kevin Ness Date: Wed, 17 Jul 2024 21:21:50 -0400 Subject: [PATCH 5/7] Add ParseTemporalTimeString handling --- core/engine/src/builtins/temporal/plain_time/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/engine/src/builtins/temporal/plain_time/mod.rs b/core/engine/src/builtins/temporal/plain_time/mod.rs index 7ecff1b9c93..4a334281b8a 100644 --- a/core/engine/src/builtins/temporal/plain_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_time/mod.rs @@ -629,13 +629,13 @@ pub(crate) fn to_temporal_time( .into()) } // 3. Else, - JsValue::String(_str) => { + JsValue::String(str) => { // b. Let result be ? ParseTemporalTimeString(item). // c. Assert: IsValidTime(result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]]) is true. - // TODO: Add time parsing to `temporal_rs` - Err(JsNativeError::typ() - .with_message("Invalid value for converting to PlainTime.") - .into()) + str + .to_std_string_escaped() + .parse::