diff --git a/Cargo.lock b/Cargo.lock index 938758e5ab5..7a689d161c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3549,16 +3549,14 @@ checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" [[package]] name = "temporal_rs" version = "0.0.4" -source = "git+https://github.com/boa-dev/temporal.git?rev=436b07d9b27e3e2274905c9a4eabf8bbff9ad9ec#436b07d9b27e3e2274905c9a4eabf8bbff9ad9ec" +source = "git+https://github.com/boa-dev/temporal.git?rev=53fc1fc11f039574000d3d22a5d06d75836a4494#53fc1fc11f039574000d3d22a5d06d75836a4494" dependencies = [ - "bitflags 2.7.0", "combine", "iana-time-zone", "icu_calendar 2.0.0-beta1", "ixdtf", "jiff-tzdb", "num-traits", - "rustc-hash 2.1.0", "tinystr 0.8.0", "tzif", "web-time", diff --git a/Cargo.toml b/Cargo.toml index 5736505c6b4..8ceb2ee1742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,7 +113,7 @@ intrusive-collections = "0.9.7" cfg-if = "1.0.0" either = "1.13.0" sys-locale = "0.3.2" -temporal_rs = { git = "https://github.com/boa-dev/temporal.git", rev = "436b07d9b27e3e2274905c9a4eabf8bbff9ad9ec", features = ["tzdb"] } +temporal_rs = { git = "https://github.com/boa-dev/temporal.git", rev = "53fc1fc11f039574000d3d22a5d06d75836a4494", features = ["tzdb"] } web-time = "1.1.0" criterion = "0.5.1" float-cmp = "0.10.0" diff --git a/core/engine/src/builtins/temporal/instant/mod.rs b/core/engine/src/builtins/temporal/instant/mod.rs index 5177673d17a..7c6a8a026a9 100644 --- a/core/engine/src/builtins/temporal/instant/mod.rs +++ b/core/engine/src/builtins/temporal/instant/mod.rs @@ -1,6 +1,7 @@ //! Boa's implementation of ECMAScript's `Temporal.Instant` builtin object. -use super::options::get_difference_settings; +use super::options::{get_difference_settings, get_digits_option}; +use super::to_temporal_timezone_identifier; use crate::value::JsVariant; use crate::{ builtins::{ @@ -25,6 +26,7 @@ use crate::{ use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; use num_traits::ToPrimitive; +use temporal_rs::options::{TemporalUnit, ToStringRoundingOptions}; use temporal_rs::{ options::{RoundingIncrement, RoundingOptions, TemporalRoundingMode}, Instant as InnerInstant, @@ -91,6 +93,8 @@ impl IntrinsicObject for Instant { .method(Self::round, js_string!("round"), 1) .method(Self::equals, js_string!("equals"), 1) .method(Self::to_zoned_date_time, js_string!("toZonedDateTime"), 1) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) .method( Self::to_zoned_date_time_iso, @@ -477,6 +481,60 @@ impl Instant { .into()) } + fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let instant = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ() + .with_message("the this object must be a Temporal.Instant object.") + })?; + + let options = get_options_object(args.get_or_undefined(0))?; + + let precision = get_digits_option(&options, context)?; + let rounding_mode = + get_option::(&options, js_string!("roundingMode"), context)?; + let smallest_unit = + get_option::(&options, js_string!("smallestUnit"), context)?; + // NOTE: There may be an order-of-operations here due to a check on Unit groups and smallest_unit value. + let timezone = options + .get(js_string!("timeZone"), context)? + .map(|v| to_temporal_timezone_identifier(v, context)) + .transpose()?; + + let options = ToStringRoundingOptions { + precision, + smallest_unit, + rounding_mode, + }; + + let ixdtf = instant.inner.to_ixdtf_string_with_provider( + timezone.as_ref(), + options, + context.tz_provider(), + )?; + + Ok(JsString::from(ixdtf).into()) + } + + fn to_json(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let instant = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ() + .with_message("the this object must be a Temporal.Instant object.") + })?; + + let ixdtf = instant.inner.to_ixdtf_string_with_provider( + None, + ToStringRoundingOptions::default(), + context.tz_provider(), + )?; + Ok(JsString::from(ixdtf).into()) + } + pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { Err(JsNativeError::typ() .with_message("`valueOf` not supported by Temporal built-ins. See 'compare', 'equals', or `toString`") diff --git a/core/engine/src/builtins/temporal/options.rs b/core/engine/src/builtins/temporal/options.rs index 3213435bbab..58d4110976a 100644 --- a/core/engine/src/builtins/temporal/options.rs +++ b/core/engine/src/builtins/temporal/options.rs @@ -12,9 +12,13 @@ use crate::{ builtins::options::{get_option, OptionType, ParsableOptionType}, js_string, Context, JsNativeError, JsObject, JsResult, JsString, JsValue, }; -use temporal_rs::options::{ - ArithmeticOverflow, DifferenceSettings, Disambiguation, DisplayCalendar, DurationOverflow, - OffsetDisambiguation, RoundingIncrement, TemporalRoundingMode, TemporalUnit, +use temporal_rs::{ + options::{ + ArithmeticOverflow, DifferenceSettings, Disambiguation, DisplayCalendar, DisplayOffset, + DisplayTimeZone, DurationOverflow, OffsetDisambiguation, RoundingIncrement, + TemporalRoundingMode, TemporalUnit, + }, + parsers::Precision, }; // TODO: Expand docs on the below options. @@ -62,6 +66,43 @@ pub(crate) fn get_difference_settings( Ok(settings) } +pub(crate) fn get_digits_option(options: &JsObject, context: &mut Context) -> JsResult { + // 1. Let digitsValue be ? Get(options, "fractionalSecondDigits"). + let digits_value = options.get(js_string!("fractionalSecondDigits"), context)?; + // 2. If digitsValue is undefined, return auto. + if digits_value.is_undefined() { + return Ok(Precision::Auto); + } + // 3. If digitsValue is not a Number, then + let Some(digits_number) = digits_value.as_number() else { + // a. If ? ToString(digitsValue) is not "auto", throw a RangeError exception. + if digits_value.to_string(context)? != js_string!("auto") { + return Err(JsNativeError::range() + .with_message("fractionalSecondDigits must be a digit or 'auto'") + .into()); + } + // b. Return auto. + return Ok(Precision::Auto); + }; + + // 4. If digitsValue is NaN, +∞𝔽, or -∞𝔽, throw a RangeError exception. + if !digits_number.is_finite() { + return Err(JsNativeError::range() + .with_message("fractionalSecondDigits must be a finite number") + .into()); + } + // 5. Let digitCount be floor(ℝ(digitsValue)). + let digits = digits_number.floor() as i32; + // 6. If digitCount < 0 or digitCount > 9, throw a RangeError exception. + if !(0..=9).contains(&digits) { + return Err(JsNativeError::range() + .with_message("fractionalSecondDigits must be in an inclusive range of 0-9") + .into()); + } + // 7. Return digitCount. + Ok(Precision::Digit(digits as u8)) +} + #[derive(Debug, Clone, Copy)] #[allow(unused)] pub(crate) enum TemporalUnitGroup { @@ -117,6 +158,8 @@ impl ParsableOptionType for Disambiguation {} impl ParsableOptionType for OffsetDisambiguation {} impl ParsableOptionType for TemporalRoundingMode {} impl ParsableOptionType for DisplayCalendar {} +impl ParsableOptionType for DisplayOffset {} +impl ParsableOptionType for DisplayTimeZone {} impl OptionType for RoundingIncrement { fn from_value(value: JsValue, context: &mut Context) -> JsResult { 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 7bd400448e2..462e3d3d038 100644 --- a/core/engine/src/builtins/temporal/plain_date_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_date_time/mod.rs @@ -24,7 +24,10 @@ use boa_profiler::Profiler; mod tests; use temporal_rs::{ - options::{ArithmeticOverflow, RoundingIncrement, RoundingOptions, TemporalRoundingMode}, + options::{ + ArithmeticOverflow, DisplayCalendar, RoundingIncrement, RoundingOptions, + TemporalRoundingMode, TemporalUnit, ToStringRoundingOptions, + }, partial::PartialDateTime, PlainDateTime as InnerDateTime, PlainTime, }; @@ -32,7 +35,7 @@ use temporal_rs::{ use super::{ calendar::{get_temporal_calendar_slot_value_with_default, to_temporal_calendar_slot_value}, create_temporal_duration, - options::{get_difference_settings, get_temporal_unit, TemporalUnitGroup}, + options::{get_difference_settings, get_digits_option, get_temporal_unit, TemporalUnitGroup}, to_temporal_duration_record, to_temporal_time, PlainDate, ZonedDateTime, }; use crate::value::JsVariant; @@ -279,6 +282,8 @@ impl IntrinsicObject for PlainDateTime { .method(Self::since, js_string!("since"), 1) .method(Self::round, js_string!("round"), 1) .method(Self::equals, js_string!("equals"), 1) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) .build(); } @@ -933,6 +938,50 @@ impl PlainDateTime { Ok((dt.inner == other).into()) } + fn to_string(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 options = get_options_object(args.get_or_undefined(0))?; + + let show_calendar = + get_option::(&options, js_string!("calendarName"), context)? + .unwrap_or(DisplayCalendar::Auto); + let precision = get_digits_option(&options, context)?; + let rounding_mode = + get_option::(&options, js_string!("roundingMode"), context)?; + let smallest_unit = + get_option::(&options, js_string!("smallestUnit"), context)?; + + let ixdtf = dt.inner.to_ixdtf_string( + ToStringRoundingOptions { + precision, + smallest_unit, + rounding_mode, + }, + show_calendar, + )?; + Ok(JsString::from(ixdtf).into()) + } + + fn to_json(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 ixdtf = dt + .inner + .to_ixdtf_string(ToStringRoundingOptions::default(), DisplayCalendar::Auto)?; + Ok(JsString::from(ixdtf).into()) + } + pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { Err(JsNativeError::typ() .with_message("`valueOf` not supported by Temporal built-ins. See 'compare', 'equals', or `toString`") diff --git a/core/engine/src/builtins/temporal/plain_time/mod.rs b/core/engine/src/builtins/temporal/plain_time/mod.rs index 35cbe4a6742..84cbd121b2a 100644 --- a/core/engine/src/builtins/temporal/plain_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_time/mod.rs @@ -5,7 +5,7 @@ use super::{ options::{get_difference_settings, get_temporal_unit, TemporalUnitGroup}, to_temporal_duration_record, PlainDateTime, ZonedDateTime, }; -use crate::value::JsVariant; +use crate::{builtins::temporal::options::get_digits_option, value::JsVariant}; use crate::{ builtins::{ options::{get_option, get_options_object}, @@ -23,7 +23,7 @@ use crate::{ use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; use temporal_rs::{ - options::{ArithmeticOverflow, TemporalRoundingMode}, + options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit, ToStringRoundingOptions}, partial::PartialTime, PlainTime as PlainTimeInner, }; @@ -118,7 +118,8 @@ impl IntrinsicObject for PlainTime { .method(Self::since, js_string!("since"), 1) .method(Self::round, js_string!("round"), 1) .method(Self::equals, js_string!("equals"), 1) - .method(Self::get_iso_fields, js_string!("getISOFields"), 0) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) .build(); } @@ -530,58 +531,50 @@ impl PlainTime { Ok((time.inner == other).into()) } - /// 4.3.18 Temporal.PlainTime.prototype.getISOFields ( ) - fn get_iso_fields(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { - // 1. Let temporalTime be the this value. - // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]). + /// 4.3.16 `Temporal.PlainTime.prototype.toString ( [ options ] )` + fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { 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.") })?; - // 3. Let fields be OrdinaryObjectCreate(%Object.prototype%). - let fields = JsObject::with_object_proto(context.intrinsics()); + let options = get_options_object(args.get_or_undefined(0))?; - // 4. Perform ! CreateDataPropertyOrThrow(fields, "isoHour", 𝔽(temporalTime.[[ISOHour]])). - fields.create_data_property_or_throw(js_string!("isoHour"), time.inner.hour(), context)?; - // 5. Perform ! CreateDataPropertyOrThrow(fields, "isoMicrosecond", 𝔽(temporalTime.[[ISOMicrosecond]])). - fields.create_data_property_or_throw( - js_string!("isoMicrosecond"), - time.inner.microsecond(), - context, - )?; - // 6. Perform ! CreateDataPropertyOrThrow(fields, "isoMillisecond", 𝔽(temporalTime.[[ISOMillisecond]])). - fields.create_data_property_or_throw( - js_string!("isoMillisecond"), - time.inner.millisecond(), - context, - )?; - // 7. Perform ! CreateDataPropertyOrThrow(fields, "isoMinute", 𝔽(temporalTime.[[ISOMinute]])). - fields.create_data_property_or_throw( - js_string!("isoMinute"), - time.inner.minute(), - context, - )?; - // 8. Perform ! CreateDataPropertyOrThrow(fields, "isoNanosecond", 𝔽(temporalTime.[[ISONanosecond]])). - fields.create_data_property_or_throw( - js_string!("isoNanosecond"), - time.inner.nanosecond(), - context, - )?; - // 9. Perform ! CreateDataPropertyOrThrow(fields, "isoSecond", 𝔽(temporalTime.[[ISOSecond]])). - fields.create_data_property_or_throw( - js_string!("isoSecond"), - time.inner.second(), - context, - )?; + let precision = get_digits_option(&options, context)?; + let rounding_mode = + get_option::(&options, js_string!("roundingMode"), context)?; + let smallest_unit = + get_option::(&options, js_string!("smallestUnit"), context)?; + + let options = ToStringRoundingOptions { + precision, + rounding_mode, + smallest_unit, + }; - // 10. Return fields. - Ok(fields.into()) + let ixdtf = time.inner.to_ixdtf_string(options)?; + + Ok(JsString::from(ixdtf).into()) + } + + /// 4.3.18 `Temporal.PlainTime.prototype.toJSON ( )` + fn to_json(this: &JsValue, _: &[JsValue], _: &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 ixdtf = time + .inner + .to_ixdtf_string(ToStringRoundingOptions::default())?; + Ok(JsString::from(ixdtf).into()) } - /// 4.3.22 Temporal.PlainTime.prototype.valueOf ( ) + /// 4.3.19 Temporal.PlainTime.prototype.valueOf ( ) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { // 1. Throw a TypeError exception. Err(JsNativeError::typ() diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index 5d989d086f6..15b3dbec9af 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use crate::{ builtins::{ options::{get_option, get_options_object}, + temporal::options::get_digits_option, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, @@ -19,7 +20,10 @@ use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; use num_traits::ToPrimitive; use temporal_rs::{ - options::{ArithmeticOverflow, Disambiguation, OffsetDisambiguation}, + options::{ + ArithmeticOverflow, Disambiguation, DisplayCalendar, DisplayOffset, DisplayTimeZone, + OffsetDisambiguation, TemporalRoundingMode, TemporalUnit, ToStringRoundingOptions, + }, partial::PartialZonedDateTime, Calendar, TimeZone, ZonedDateTime as ZonedDateTimeInner, }; @@ -325,6 +329,8 @@ impl IntrinsicObject for ZonedDateTime { .method(Self::add, js_string!("add"), 1) .method(Self::subtract, js_string!("subtract"), 1) .method(Self::equals, js_string!("equals"), 1) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) .method(Self::start_of_day, js_string!("startOfDay"), 0) .method(Self::to_instant, js_string!("toInstant"), 0) @@ -430,16 +436,15 @@ impl ZonedDateTime { /// 6.3.4 get `Temporal.ZonedDateTime.prototype.timeZoneId` fn get_timezone_id(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - let _zdt = this + let zdt = this .as_object() .and_then(JsObject::downcast_ref::) .ok_or_else(|| { JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") })?; - Err(JsNativeError::error() - .with_message("Not yet implemented.") - .into()) + let tz_id = zdt.inner.timezone().id()?; + Ok(JsString::from(tz_id).into()) } /// 6.3.5 get `Temporal.ZonedDateTime.prototype.era` @@ -932,6 +937,66 @@ impl ZonedDateTime { Ok((zdt.inner == other).into()) } + fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let zdt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") + })?; + + let options = get_options_object(args.get_or_undefined(0))?; + + let show_calendar = + get_option::(&options, js_string!("calendarName"), context)? + .unwrap_or(DisplayCalendar::Auto); + let precision = get_digits_option(&options, context)?; + let show_offset = get_option::(&options, js_string!("offset"), context)? + .unwrap_or(DisplayOffset::Auto); + let rounding_mode = + get_option::(&options, js_string!("roundingMode"), context)?; + let smallest_unit = + get_option::(&options, js_string!("smallestUnit"), context)?; + // NOTE: There may be an order-of-operations here due to a check on Unit groups and smallest_unit value. + let display_timezone = + get_option::(&options, js_string!("offset"), context)? + .unwrap_or(DisplayTimeZone::Auto); + + let options = ToStringRoundingOptions { + precision, + smallest_unit, + rounding_mode, + }; + let ixdtf = zdt.inner.to_ixdtf_string_with_provider( + show_offset, + display_timezone, + show_calendar, + options, + context.tz_provider(), + )?; + + Ok(JsString::from(ixdtf).into()) + } + + fn to_json(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let zdt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") + })?; + + let ixdtf = zdt.inner.to_ixdtf_string_with_provider( + DisplayOffset::Auto, + DisplayTimeZone::Auto, + DisplayCalendar::Auto, + ToStringRoundingOptions::default(), + context.tz_provider(), + )?; + + Ok(JsString::from(ixdtf).into()) + } + /// 6.3.44 `Temporal.ZonedDateTime.prototype.valueOf ( )` pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { Err(JsNativeError::typ()