From 7d025bc6ee9c5f24967df5c634654eb25c1a9d17 Mon Sep 17 00:00:00 2001 From: Kevin Ness <46825870+nekevss@users.noreply.github.com> Date: Thu, 18 Jul 2024 18:42:26 -0400 Subject: [PATCH] Implement more Temporal functionality (#3924) * Implement more Temporal functionality * Correct equals method length * patch withPlainTime docs * Correct time method binding * Add ParseTemporalTimeString handling * cargo fmt * Update temporal_rs and add compare methods --- Cargo.lock | 3 +- Cargo.toml | 2 +- .../src/builtins/temporal/duration/mod.rs | 20 ++- .../src/builtins/temporal/plain_date/mod.rs | 35 +++- .../builtins/temporal/plain_date_time/mod.rs | 170 +++++++++++++++++- .../src/builtins/temporal/plain_time/mod.rs | 73 +++++++- 6 files changed, 272 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b367fdf462..71c8c2a5a1 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=2e6750a16a46c321a1c7092def880c62f3d1ac91#2e6750a16a46c321a1c7092def880c62f3d1ac91" dependencies = [ "bitflags 2.6.0", "icu_calendar", diff --git a/Cargo.toml b/Cargo.toml index a460e1de5b..3e304837eb 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 = "2e6750a16a46c321a1c7092def880c62f3d1ac91" } 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 c5e07e116b..15632ecea9 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 c05f33b1db..f314785c4d 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. @@ -213,6 +214,7 @@ impl IntrinsicObject for PlainDate { Attribute::CONFIGURABLE, ) .static_method(Self::from, js_string!("from"), 2) + .static_method(Self::compare, js_string!("compare"), 2) .method(Self::to_plain_year_month, js_string!("toPlainYearMonth"), 0) .method(Self::to_plain_month_day, js_string!("toPlainMonthDay"), 0) .method(Self::get_iso_fields, js_string!("getISOFields"), 0) @@ -252,7 +254,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, @@ -488,6 +490,7 @@ impl PlainDate { // ==== `PlainDate` method implementations ==== impl PlainDate { + /// 3.2.2 Temporal.PlainDate.from ( item [ , options ] ) fn from(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { let item = args.get_or_undefined(0); let options = args.get(1); @@ -505,6 +508,14 @@ impl PlainDate { ) .map(Into::into) } + + /// 3.2.3 Temporal.PlainDate.compare ( one, two ) + fn compare(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let one = to_temporal_date(args.get_or_undefined(0), None, context)?; + let two = to_temporal_date(args.get_or_undefined(1), None, context)?; + + Ok((one.cmp(&two) as i8).into()) + } } // ==== `PlainDate.prototype` method implementation ==== @@ -608,10 +619,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 +800,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 9539a540d3..57698f641b 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,15 @@ impl IntrinsicObject for PlainDateTime { Attribute::CONFIGURABLE, ) .static_method(Self::from, js_string!("from"), 2) + .static_method(Self::compare, js_string!("compare"), 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"), 1) .build(); } @@ -332,7 +344,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, @@ -625,6 +637,7 @@ impl PlainDateTime { // ==== PlainDateTime method implemenations ==== impl PlainDateTime { + /// 5.2.2 Temporal.PlainDateTime.from ( item [ , options ] ) fn from(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { let item = args.get_or_undefined(0); // 1. Set options to ? GetOptionsObject(options). @@ -646,11 +659,59 @@ impl PlainDateTime { // 3. Return ? ToTemporalDateTime(item, options). create_temporal_datetime(dt, None, context).map(Into::into) } + + /// 5.2.3 Temporal.PlainDateTime.compare ( one, two ) + fn compare(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Set one to ? ToTemporalDateTime(one). + let one = to_temporal_datetime(args.get_or_undefined(0), None, context)?; + // 2. Set two to ? ToTemporalDateTime(two). + let two = to_temporal_datetime(args.get_or_undefined(1), None, context)?; + + // 3. Return 𝔽(CompareISODateTime(one.[[ISOYear]], one.[[ISOMonth]], one.[[ISODay]], + // one.[[ISOHour]], one.[[ISOMinute]], one.[[ISOSecond]], one.[[ISOMillisecond]], + // one.[[ISOMicrosecond]], one.[[ISONanosecond]], two.[[ISOYear]], two.[[ISOMonth]], + // two.[[ISODay]], two.[[ISOHour]], two.[[ISOMinute]], two.[[ISOSecond]], + // two.[[ISOMillisecond]], two.[[ISOMicrosecond]], two.[[ISONanosecond]])). + Ok((one.cmp(&two) as i8).into()) + } } // ==== 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 +734,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 +759,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 8f184c0d1e..4c5b9236e4 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, }; @@ -108,8 +109,11 @@ impl IntrinsicObject for PlainTime { Attribute::CONFIGURABLE, ) .static_method(Self::from, js_string!("from"), 2) + .static_method(Self::compare, js_string!("compare"), 2) .method(Self::add, js_string!("add"), 1) .method(Self::subtract, js_string!("subtract"), 1) + .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) @@ -290,6 +294,7 @@ impl PlainTime { // ==== PlainTime method implementations ==== impl PlainTime { + /// 4.2.2 Temporal.PlainTime.from ( item [ , options ] ) fn from(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { let item = args.get_or_undefined(0); // 1. Set options to ? GetOptionsObject(options). @@ -315,6 +320,19 @@ impl PlainTime { // 4. Return ? ToTemporalTime(item, overflow). create_temporal_time(time, None, context).map(Into::into) } + + /// 4.2.3 Temporal.PlainTime.compare ( one, two ) + fn compare(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Set one to ? ToTemporalTime(one). + let one = to_temporal_time(args.get_or_undefined(0), None, context)?; + // 2. Set two to ? ToTemporalTime(two). + let two = to_temporal_time(args.get_or_undefined(1), None, context)?; + // 3. Return 𝔽(CompareTemporalTime(one.[[ISOHour]], one.[[ISOMinute]], one.[[ISOSecond]], + // one.[[ISOMillisecond]], one.[[ISOMicrosecond]], one.[[ISONanosecond]], two.[[ISOHour]], + // two.[[ISOMinute]], two.[[ISOSecond]], two.[[ISOMillisecond]], two.[[ISOMicrosecond]], + // two.[[ISONanosecond]])). + Ok((one.cmp(&two) as i8).into()) + } } // ==== PlainTime.prototype method implementations ==== @@ -326,7 +344,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 +362,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 +374,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.") })?; @@ -588,13 +644,12 @@ 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::