Skip to content

Commit

Permalink
Implement more toString and toJSON methods on Temporal builtins (#…
Browse files Browse the repository at this point in the history
…4126)

* Bump temporal_rs and add Date string methods

* Add zdt handling in to_instant op

* Cargo fmt

* Fix failing test output

* Implement more supported to_string methods

* Implement more to_string and to_json methods

* Bump msrv to 1.84 to support Option::<T>::expect const fn

* Revert MSRV bump

* Bump temporal_rs to msrv version

* Bump temporal_rs for bug fix to cross-epoch broken test
  • Loading branch information
nekevss authored Jan 15, 2025
1 parent ca86fdf commit b2ba5f6
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 60 deletions.
4 changes: 1 addition & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
60 changes: 59 additions & 1 deletion core/engine/src/builtins/temporal/instant/mod.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -477,6 +481,60 @@ impl Instant {
.into())
}

fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let instant = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.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::<TemporalRoundingMode>(&options, js_string!("roundingMode"), context)?;
let smallest_unit =
get_option::<TemporalUnit>(&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<JsValue> {
let instant = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.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<JsValue> {
Err(JsNativeError::typ()
.with_message("`valueOf` not supported by Temporal built-ins. See 'compare', 'equals', or `toString`")
Expand Down
49 changes: 46 additions & 3 deletions core/engine/src/builtins/temporal/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -62,6 +66,43 @@ pub(crate) fn get_difference_settings(
Ok(settings)
}

pub(crate) fn get_digits_option(options: &JsObject, context: &mut Context) -> JsResult<Precision> {
// 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 {
Expand Down Expand Up @@ -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<Self> {
Expand Down
53 changes: 51 additions & 2 deletions core/engine/src/builtins/temporal/plain_date_time/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ 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,
};

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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -933,6 +938,50 @@ impl PlainDateTime {
Ok((dt.inner == other).into())
}

fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let dt = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.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::<DisplayCalendar>(&options, js_string!("calendarName"), context)?
.unwrap_or(DisplayCalendar::Auto);
let precision = get_digits_option(&options, context)?;
let rounding_mode =
get_option::<TemporalRoundingMode>(&options, js_string!("roundingMode"), context)?;
let smallest_unit =
get_option::<TemporalUnit>(&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<JsValue> {
let dt = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.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<JsValue> {
Err(JsNativeError::typ()
.with_message("`valueOf` not supported by Temporal built-ins. See 'compare', 'equals', or `toString`")
Expand Down
83 changes: 38 additions & 45 deletions core/engine/src/builtins/temporal/plain_time/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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,
};
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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<JsValue> {
// 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<JsValue> {
let time = this
.as_object()
.and_then(JsObject::downcast_ref::<PlainTime>)
.and_then(JsObject::downcast_ref::<Self>)
.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::<TemporalRoundingMode>(&options, js_string!("roundingMode"), context)?;
let smallest_unit =
get_option::<TemporalUnit>(&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<JsValue> {
let time = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.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<JsValue> {
// 1. Throw a TypeError exception.
Err(JsNativeError::typ()
Expand Down
Loading

0 comments on commit b2ba5f6

Please sign in to comment.