diff --git a/Cargo.lock b/Cargo.lock index a1ebf92d051..985f42d2b0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3220,7 +3220,7 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "temporal_rs" version = "0.0.3" -source = "git+https://github.com/boa-dev/temporal.git?rev=af94bbc31d409a2bfdce473e667e08e16c677149#af94bbc31d409a2bfdce473e667e08e16c677149" +source = "git+https://github.com/boa-dev/temporal.git?rev=1e7901d07a83211e62373ab94284a7d1ada4c913#1e7901d07a83211e62373ab94284a7d1ada4c913" dependencies = [ "bitflags 2.6.0", "icu_calendar", diff --git a/Cargo.toml b/Cargo.toml index 95af34c2968..8ba416c7331 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 = "af94bbc31d409a2bfdce473e667e08e16c677149" } +temporal_rs = { git = "https://github.com/boa-dev/temporal.git", rev = "1e7901d07a83211e62373ab94284a7d1ada4c913" } web-time = "1.1.0" criterion = "0.5.1" float-cmp = "0.9.0" diff --git a/core/engine/src/builtins/temporal/mod.rs b/core/engine/src/builtins/temporal/mod.rs index 3eaa7aae7c9..65bb87cf235 100644 --- a/core/engine/src/builtins/temporal/mod.rs +++ b/core/engine/src/builtins/temporal/mod.rs @@ -274,6 +274,46 @@ pub(crate) fn to_relative_temporal_object( // 13.26 `GetUnsignedRoundingMode ( roundingMode, isNegative )` // Implemented on RoundingMode in builtins/options.rs +// 13.26 IsPartialTemporalObject ( object ) +pub(crate) fn is_partial_temporal_object<'value>( + value: &'value JsValue, + context: &mut Context, +) -> JsResult> { + // 1. If value is not an Object, return false. + let Some(obj) = value.as_object() else { + return Ok(None); + }; + + // 2. If value has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], + // [[InitializedTemporalMonthDay]], [[InitializedTemporalTime]], + // [[InitializedTemporalYearMonth]], or + // [[InitializedTemporalZonedDateTime]] internal slot, return false. + if obj.is::() + || obj.is::() + || obj.is::() + || obj.is::() + || obj.is::() + || obj.is::() + { + return Ok(None); + } + + // 3. Let calendarProperty be ? Get(value, "calendar"). + let calendar_property = obj.get(js_str!("calendar"), context)?; + // 4. If calendarProperty is not undefined, return false. + if !calendar_property.is_undefined() { + return Ok(None); + } + // 5. Let timeZoneProperty be ? Get(value, "timeZone"). + let time_zone_property = obj.get(js_str!("timeZone"), context)?; + // 6. If timeZoneProperty is not undefined, return false. + if !time_zone_property.is_undefined() { + return Ok(None); + } + // 7. Return true. + Ok(Some(obj)) +} + // 13.27 `ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode )` // Migrated to `temporal_rs` diff --git a/core/engine/src/builtins/temporal/plain_date/mod.rs b/core/engine/src/builtins/temporal/plain_date/mod.rs index 28123ceddd8..77f05f5b0d6 100644 --- a/core/engine/src/builtins/temporal/plain_date/mod.rs +++ b/core/engine/src/builtins/temporal/plain_date/mod.rs @@ -3,6 +3,8 @@ // TODO (nekevss): DOCS DOCS AND MORE DOCS +use std::str::FromStr; + use crate::{ builtins::{ options::{get_option, get_options_object}, @@ -14,7 +16,8 @@ use crate::{ property::Attribute, realm::Realm, string::StaticJsStrings, - Context, JsArgs, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, + Context, JsArgs, JsData, JsError, JsNativeError, JsObject, JsResult, JsString, JsSymbol, + JsValue, }; use boa_gc::{Finalize, Trace}; use boa_macros::js_str; @@ -22,16 +25,18 @@ use boa_profiler::Profiler; use temporal_rs::{ components::{ calendar::{Calendar, GetTemporalCalendar}, - Date as InnerDate, DateTime, + Date as InnerDate, DateTime, MonthCode, PartialDate, }, iso::IsoDateSlots, options::ArithmeticOverflow, + TemporalFields, TinyAsciiStr, }; use super::{ - 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, + calendar::{get_temporal_calendar_slot_value_with_default, 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. @@ -613,10 +618,39 @@ impl PlainDate { .map(Into::into) } - fn with(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + // 3.3.24 Temporal.PlainDate.prototype.with ( temporalDateLike [ , options ] ) + fn with(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let temporalDate be the this value. + // 2. Perform ? RequireInternalSlot(temporalDate, [[InitializedTemporalDate]]). + 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.") + })?; + + // 3. If ? IsPartialTemporalObject(temporalDateLike) is false, throw a TypeError exception. + let Some(partial_object) = + super::is_partial_temporal_object(args.get_or_undefined(0), context)? + else { + return Err(JsNativeError::typ() + .with_message("with object was not a PartialTemporalObject.") + .into()); + }; + let options = get_options_object(args.get_or_undefined(1))?; + + // SKIP: Steps 4-9 are handled by the with method of temporal_rs's Date + // 4. Let resolvedOptions be ? SnapshotOwnProperties(? GetOptionsObject(options), null). + // 5. Let calendarRec be ? CreateCalendarMethodsRecord(temporalDate.[[Calendar]], « date-from-fields, fields, merge-fields »). + // 6. Let fieldsResult be ? PrepareCalendarFieldsAndFieldNames(calendarRec, temporalDate, « "day", "month", "monthCode", "year" »). + // 7. Let partialDate be ? PrepareTemporalFields(temporalDateLike, fieldsResult.[[FieldNames]], partial). + // 8. Let fields be ? CalendarMergeFields(calendarRec, fieldsResult.[[Fields]], partialDate). + // 9. Set fields to ? PrepareTemporalFields(fields, fieldsResult.[[FieldNames]], «»). + let overflow = get_option::(&options, js_str!("overflow"), context)?; + let partial = to_partial_date_record(partial_object, context)?; + + // 10. Return ? CalendarDateFromFields(calendarRec, fields, resolvedOptions). + create_temporal_date(date.inner.with(partial, overflow)?, None, context).map(Into::into) } /// 3.3.26 Temporal.PlainDate.prototype.withCalendar ( calendarLike ) @@ -807,12 +841,29 @@ pub(crate) fn to_temporal_date( } // d. Let calendar be ? GetTemporalCalendarSlotValueWithISODefault(item). + let calendar = get_temporal_calendar_slot_value_with_default(object, context)?; + let overflow = + get_option::(&options_obj, js_str!("overflow"), context)? + .unwrap_or(ArithmeticOverflow::Constrain); + // e. Let fieldNames be ? CalendarFields(calendar, « "day", "month", "monthCode", "year" »). // f. Let fields be ? PrepareTemporalFields(item, fieldNames, «»). + let partial = to_partial_date_record(object, context)?; + // TODO: Move validation to `temporal_rs`. + if !(partial.day.is_some() + && (partial.month.is_some() || partial.month_code.is_some()) + && (partial.year.is_some() || (partial.era.is_some() && partial.era_year.is_some()))) + { + return Err(JsNativeError::typ() + .with_message("A partial date must have at least one defined field.") + .into()); + } + let mut fields = TemporalFields::from(partial); + // g. Return ? CalendarDateFromFields(calendar, fields, options). - return Err(JsNativeError::error() - .with_message("CalendarDateFields not yet implemented.") - .into()); + return calendar + .date_from_fields(&mut fields, overflow) + .map_err(Into::into); } // 5. If item is not a String, throw a TypeError exception. @@ -837,3 +888,63 @@ pub(crate) fn to_temporal_date( Ok(result) } + +pub(crate) fn to_partial_date_record( + partial_object: &JsObject, + context: &mut Context, +) -> JsResult { + let day = partial_object + .get(js_str!("day"), context)? + .map(|v| super::to_integer_if_integral(v, context)) + .transpose()?; + let month = partial_object + .get(js_str!("month"), context)? + .map(|v| super::to_integer_if_integral(v, context)) + .transpose()?; + let month_code = partial_object + .get(js_str!("monthCode"), context)? + .map(|v| { + let JsValue::String(month_code) = + v.to_primitive(context, crate::value::PreferredType::String)? + else { + return Err(JsNativeError::typ() + .with_message("The monthCode field value must be a string.") + .into()); + }; + MonthCode::from_str(&month_code.to_std_string_escaped()).map_err(Into::::into) + }) + .transpose()?; + let year = partial_object + .get(js_str!("year"), context)? + .map(|v| super::to_integer_if_integral(v, context)) + .transpose()?; + let era_year = partial_object + .get(js_str!("eraYear"), context)? + .map(|v| super::to_integer_if_integral(v, context)) + .transpose()?; + let era = partial_object + .get(js_str!("era"), context)? + .map(|v| { + let JsValue::String(era) = + v.to_primitive(context, crate::value::PreferredType::String)? + else { + return Err(JsError::from( + JsNativeError::typ() + .with_message("The monthCode field value must be a string."), + )); + }; + // TODO: double check if an invalid monthCode is a range or type error. + TinyAsciiStr::<16>::from_str(&era.to_std_string_escaped()) + .map_err(|e| JsError::from(JsNativeError::range().with_message(e.to_string()))) + }) + .transpose()?; + + Ok(PartialDate { + year, + month, + month_code, + day, + era, + era_year, + }) +} 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 d22a966dee4..a2a582754d2 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::to_integer_with_truncation, + temporal::{to_integer_with_truncation, to_partial_date_record, to_partial_time_record}, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, @@ -25,14 +25,15 @@ mod tests; use temporal_rs::{ components::{ calendar::{Calendar, GetTemporalCalendar}, - DateTime as InnerDateTime, + DateTime as InnerDateTime, PartialDateTime, Time, }, iso::{IsoDate, IsoDateSlots}, options::{ArithmeticOverflow, RoundingIncrement, RoundingOptions, TemporalRoundingMode}, + TemporalFields, }; use super::{ - calendar::to_temporal_calendar_slot_value, + calendar::{get_temporal_calendar_slot_value_with_default, 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, @@ -278,6 +279,7 @@ impl IntrinsicObject for PlainDateTime { ) .static_method(Self::from, js_string!("from"), 1) .static_method(Self::compare, js_string!("compare"), 2) + .method(Self::with, js_string!("with"), 1) .method(Self::with_plain_time, js_string!("withPlainTime"), 1) .method(Self::with_calendar, js_string!("withCalendar"), 1) .method(Self::add, js_string!("add"), 1) @@ -679,6 +681,35 @@ impl PlainDateTime { // ==== PlainDateTime.prototype method implementations ==== impl PlainDateTime { + /// 5.3.25 Temporal.PlainDateTime.prototype.with ( temporalDateTimeLike [ , options ] ) + fn with(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 Some(partial_object) = + super::is_partial_temporal_object(args.get_or_undefined(0), context)? + else { + return Err(JsNativeError::typ() + .with_message("with object was not a PartialTemporalObject.") + .into()); + }; + let options = get_options_object(args.get_or_undefined(1))?; + + let date = to_partial_date_record(partial_object, context)?; + let time = to_partial_time_record(partial_object, context)?; + + let partial_dt = PartialDateTime { date, time }; + + let overflow = get_option::(&options, js_str!("overflow"), context)?; + + create_temporal_datetime(dt.inner.with(partial_dt, overflow)?, None, context) + .map(Into::into) + } + /// 5.3.26 Temporal.PlainDateTime.prototype.withPlainTime ( `[ plainTimeLike ]` ) fn with_plain_time( this: &JsValue, @@ -975,16 +1006,55 @@ pub(crate) fn to_temporal_datetime( date.inner.calendar().clone(), )?); } + // d. Let calendar be ? GetTemporalCalendarSlotValueWithISODefault(item). + let calendar = get_temporal_calendar_slot_value_with_default(object, context)?; + // e. Let calendarRec be ? CreateCalendarMethodsRecord(calendar, « date-from-fields, fields »). // f. Let fields be ? PrepareCalendarFields(calendarRec, item, « "day", "month", // "monthCode", "year" », « "hour", "microsecond", "millisecond", "minute", - // "nanosecond", "second" », «»). + // "nanosecond", "second" », «») + let partial_date = to_partial_date_record(object, context)?; + let partial_time = to_partial_time_record(object, context)?; + // TODO: Move validation to `temporal_rs`. + if !(partial_date.day.is_some() + && (partial_date.month.is_some() || partial_date.month_code.is_some()) + && (partial_date.year.is_some() + || (partial_date.era.is_some() && partial_date.era_year.is_some()))) + { + return Err(JsNativeError::typ() + .with_message("A partial date must have at least one defined field.") + .into()); + } // g. Let result be ? InterpretTemporalDateTimeFields(calendarRec, fields, resolvedOptions). - // TODO: Implement d-g. - return Err(JsNativeError::range() - .with_message("Not yet implemented.") - .into()); + let overflow = get_option::(&options, js_str!("overflow"), context)?; + let date = calendar.date_from_fields( + &mut TemporalFields::from(partial_date), + overflow.unwrap_or(ArithmeticOverflow::Constrain), + )?; + let time = Time::new( + partial_time.hour.unwrap_or(0), + partial_time.minute.unwrap_or(0), + partial_time.second.unwrap_or(0), + partial_time.millisecond.unwrap_or(0), + partial_time.microsecond.unwrap_or(0), + partial_time.nanosecond.unwrap_or(0), + ArithmeticOverflow::Constrain, + )?; + + return InnerDateTime::new( + date.iso_year(), + date.iso_month().into(), + date.iso_day().into(), + time.hour().into(), + time.minute().into(), + time.second().into(), + time.millisecond().into(), + time.microsecond().into(), + time.nanosecond().into(), + calendar, + ) + .map_err(Into::into); } // 4. Else, // a. If item is not a String, throw a TypeError exception. diff --git a/core/engine/src/builtins/temporal/plain_time/mod.rs b/core/engine/src/builtins/temporal/plain_time/mod.rs index ca3840debea..18b13725390 100644 --- a/core/engine/src/builtins/temporal/plain_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_time/mod.rs @@ -17,7 +17,7 @@ use boa_gc::{Finalize, Trace}; use boa_macros::js_str; use boa_profiler::Profiler; use temporal_rs::{ - components::Time, + components::{PartialTime, Time}, options::{ArithmeticOverflow, TemporalRoundingMode}, }; @@ -112,6 +112,7 @@ impl IntrinsicObject for PlainTime { .static_method(Self::compare, js_string!("compare"), 2) .method(Self::add, js_string!("add"), 1) .method(Self::subtract, js_string!("subtract"), 1) + .method(Self::with, js_string!("with"), 1) .method(Self::until, js_string!("until"), 1) .method(Self::since, js_string!("since"), 1) .method(Self::round, js_string!("round"), 1) @@ -374,6 +375,33 @@ impl PlainTime { create_temporal_time(time.inner.subtract(&duration)?, None, context).map(Into::into) } + fn with(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::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainTime object.") + })?; + + // 3. If ? IsPartialTemporalObject(temporalTimeLike) is false, throw a TypeError exception. + // 4. Set options to ? GetOptionsObject(options). + let Some(partial_object) = + super::is_partial_temporal_object(args.get_or_undefined(0), context)? + else { + return Err(JsNativeError::typ() + .with_message("with object was not a PartialTemporalObject.") + .into()); + }; + + let options = get_options_object(args.get_or_undefined(1))?; + let overflow = get_option::(&options, js_str!("overflow"), context)?; + let partial = to_partial_time_record(partial_object, context)?; + + create_temporal_time(time.inner.with(partial, overflow)?, 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 @@ -598,8 +626,8 @@ pub(crate) fn create_temporal_time( pub(crate) fn to_temporal_time( value: &JsValue, - _overflow: Option, - _context: &mut Context, + overflow: Option, + context: &mut Context, ) -> JsResult