Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement more toString and toJSON methods on Temporal builtins #4126

Merged
merged 12 commits into from
Jan 15, 2025
26 changes: 12 additions & 14 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 @@ -112,7 +112,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
Loading
Loading