Skip to content

Commit

Permalink
Implement toString and toJSON methods for the remaining builtins …
Browse files Browse the repository at this point in the history
…`Duration`, `PlainMonthDay`, and `PlainYearMonth` (#4135)

* Implement methods to for temporal_rs testing

* Bump temporal_rs for new methods and related fixes

* cargo fmt

* Fix docs

* Fix ZonedDateTime property name bug
  • Loading branch information
nekevss authored Jan 18, 2025
1 parent 718dfd9 commit d55c691
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 178 deletions.
5 changes: 2 additions & 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 = "53fc1fc11f039574000d3d22a5d06d75836a4494", features = ["tzdb"] }
temporal_rs = { git = "https://github.com/boa-dev/temporal.git", rev = "c61468264e27bed14bd7717f2153a7178e2dfe5f", features = ["tzdb"] }
web-time = "1.1.0"
criterion = "0.5.1"
float-cmp = "0.10.0"
Expand Down
56 changes: 45 additions & 11 deletions core/engine/src/builtins/temporal/duration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use super::{
get_relative_to_option,
options::{get_temporal_unit, TemporalUnitGroup},
options::{get_digits_option, get_temporal_unit, TemporalUnitGroup},
DateTimeValues,
};
use crate::value::JsVariant;
Expand All @@ -23,7 +23,10 @@ use crate::{
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
use temporal_rs::{
options::{RoundingIncrement, RoundingOptions, TemporalRoundingMode, TemporalUnit},
options::{
RoundingIncrement, RoundingOptions, TemporalRoundingMode, TemporalUnit,
ToStringRoundingOptions,
},
partial::PartialDuration,
Duration as InnerDuration,
};
Expand Down Expand Up @@ -190,7 +193,7 @@ impl IntrinsicObject for Duration {
.method(Self::subtract, js_string!("subtract"), 1)
.method(Self::round, js_string!("round"), 1)
.method(Self::total, js_string!("total"), 1)
.method(Self::to_string, js_string!("toString"), 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 @@ -811,17 +814,48 @@ impl Duration {
}

/// 7.3.22 `Temporal.Duration.prototype.toString ( [ options ] )`
pub(crate) fn to_string(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
pub(crate) fn to_string(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let duration = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a Duration 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)?;

let result = duration.inner.to_temporal_string(ToStringRoundingOptions {
precision,
smallest_unit,
rounding_mode,
})?;

Ok(JsString::from(result).into())
}

/// 7.3.23 `Temporal.Duration.prototype.toJSON ( )`
pub(crate) fn to_json(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
pub(crate) fn to_json(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
let duration = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a Duration object.")
})?;

let result = duration
.inner
.to_temporal_string(ToStringRoundingOptions::default())?;

Ok(JsString::from(result).into())
}

pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Expand Down
2 changes: 1 addition & 1 deletion core/engine/src/builtins/temporal/now.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ impl Now {
fn time_zone_id(_: &JsValue, _args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Return ! SystemTimeZone().
system_time_zone(context)?
.id()
.identifier()
.map(|s| JsValue::from(js_string!(s.as_str())))
.map_err(Into::into)
}
Expand Down
215 changes: 95 additions & 120 deletions core/engine/src/builtins/temporal/plain_month_day/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//! Boa's implementation of the ECMAScript `Temporal.PlainMonthDay` builtin object.
#![allow(dead_code, unused_variables)]
use std::str::FromStr;

use crate::{
Expand Down Expand Up @@ -40,89 +39,6 @@ impl PlainMonthDay {
}
}

// ==== `Temporal.PlainMonthDay` static Methods ====
impl PlainMonthDay {
// 10.2.2 Temporal.PlainMonthDay.from ( item [ , options ] )
fn from(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let options = get_options_object(args.get_or_undefined(1))?;
let item = args.get_or_undefined(0);
to_temporal_month_day(item, &options, context)
}
}

// === `PlainMonthDay` Accessor Implementations ===== /

impl PlainMonthDay {
fn get_internal_field(this: &JsValue, field: &DateTimeValues) -> JsResult<JsValue> {
let month_day = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a PlainMonthDay object.")
})?;
let inner = &month_day.inner;
match field {
DateTimeValues::Day => Ok(inner.iso_day().into()),
DateTimeValues::MonthCode => Ok(js_string!(inner.month_code()?.to_string()).into()),
_ => unreachable!(),
}
}

fn get_day(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Self::get_internal_field(this, &DateTimeValues::Day)
}

fn get_year(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Self::get_internal_field(this, &DateTimeValues::Year)
}

fn get_month_code(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Self::get_internal_field(this, &DateTimeValues::MonthCode)
}

fn get_calendar_id(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let month_day = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a PlainMonthDay object.")
})?;
let inner = &month_day.inner;
Ok(js_string!(inner.calendar().identifier()).into())
}
}

// ==== `Temporal.PlainMonthDay` Methods ====
impl PlainMonthDay {
// 10.3.7 Temporal.PlainMonthDay.prototype.toString ( [ options ] )
fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let monthDay be the this value.
// 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]).
let month_day = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a PlainMonthDay object.")
})?;
let inner = &month_day.inner;
// 3. Set options to ? NormalizeOptionsObject(options).
let options = get_options_object(args.get_or_undefined(0))?;
// 4. Let showCalendar be ? ToShowCalendarOption(options).
// Get calendarName from the options object
let show_calendar =
get_option::<DisplayCalendar>(&options, js_string!("calendarName"), context)?
.unwrap_or(DisplayCalendar::Auto);

Ok(month_day_to_string(inner, show_calendar))
}

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`")
.into())
}
}

impl BuiltInObject for PlainMonthDay {
const NAME: JsString = StaticJsStrings::PLAIN_MD_NAME;
}
Expand Down Expand Up @@ -166,9 +82,10 @@ impl IntrinsicObject for PlainMonthDay {
None,
Attribute::CONFIGURABLE,
)
.method(Self::to_string, js_string!("toString"), 1)
.static_method(Self::from, js_string!("from"), 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)
.static_method(Self::from, js_string!("from"), 2)
.build();
}

Expand Down Expand Up @@ -229,44 +146,102 @@ impl BuiltInConstructor for PlainMonthDay {
}
}

// ==== `PlainMonthDay` Abstract Operations ====
// ==== `Temporal.PlainMonthDay` static Methods ====

fn month_day_to_string(inner: &InnerMonthDay, show_calendar: DisplayCalendar) -> JsValue {
// Let month be monthDay.[[ISOMonth]] formatted as a two-digit decimal number, padded to the left with a zero if necessary
let month = inner.iso_month().to_string();

// 2. Let day be ! FormatDayOfMonth(monthDay.[[ISODay]]).
let day = inner.iso_day().to_string();

// 3. Let result be the string-concatenation of month and the code unit 0x002D (HYPHEN-MINUS).
let mut result = format!("{month:0>2}-{day:0>2}");

// 4. Let calendarId be monthDay.[[Calendar]].[[id]].
let calendar_id = inner.calendar().identifier();

// 5. Let calendar be monthDay.[[Calendar]].
// 6. If showCalendar is "auto", then
// a. Set showCalendar to "always".
// 7. If showCalendar is "always", then
// a. Let calendarString be ! FormatCalendarAnnotation(calendar).
// b. Set result to the string-concatenation of result, the code unit 0x0040 (COMMERCIAL AT), and calendarString.
if (matches!(
show_calendar,
DisplayCalendar::Critical | DisplayCalendar::Always | DisplayCalendar::Auto
)) && !(matches!(show_calendar, DisplayCalendar::Auto) && calendar_id == "iso8601")
{
let year = inner.iso_year().to_string();
let flag = if matches!(show_calendar, DisplayCalendar::Critical) {
"!"
} else {
""
};
result = format!("{year}-{result}[{flag}u-ca={calendar_id}]");
impl PlainMonthDay {
// 10.2.2 Temporal.PlainMonthDay.from ( item [ , options ] )
fn from(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let options = get_options_object(args.get_or_undefined(1))?;
let item = args.get_or_undefined(0);
to_temporal_month_day(item, &options, context)
}
}

// ==== `PlainMonthDay` Accessor Implementations ====

impl PlainMonthDay {
fn get_internal_field(this: &JsValue, field: &DateTimeValues) -> JsResult<JsValue> {
let month_day = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a PlainMonthDay object.")
})?;
let inner = &month_day.inner;
match field {
DateTimeValues::Day => Ok(inner.iso_day().into()),
DateTimeValues::MonthCode => Ok(js_string!(inner.month_code()?.to_string()).into()),
_ => unreachable!(),
}
}

fn get_calendar_id(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
let month_day = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a PlainMonthDay object.")
})?;
Ok(js_string!(month_day.inner.calendar().identifier()).into())
}

fn get_day(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Self::get_internal_field(this, &DateTimeValues::Day)
}

fn get_month_code(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Self::get_internal_field(this, &DateTimeValues::MonthCode)
}
}

// ==== `Temporal.PlainMonthDay` Methods ====

impl PlainMonthDay {
/// 10.3.8 `Temporal.PlainMonthDay.prototype.toString ( [ options ] )`
fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let monthDay be the this value.
// 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]).
let month_day = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a PlainMonthDay object.")
})?;

// 3. Set options to ? NormalizeOptionsObject(options).
let options = get_options_object(args.get_or_undefined(0))?;
// 4. Let showCalendar be ? ToShowCalendarOption(options).
// Get calendarName from the options object
let show_calendar =
get_option::<DisplayCalendar>(&options, js_string!("calendarName"), context)?
.unwrap_or(DisplayCalendar::Auto);

let ixdtf = month_day.inner.to_ixdtf_string(show_calendar);
Ok(JsString::from(ixdtf).into())
}

/// 10.3.10 Temporal.PlainMonthDay.prototype.toJSON ( )
pub(crate) fn to_json(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
let month_day = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a PlainMonthDay object.")
})?;

Ok(JsString::from(month_day.inner.to_string()).into())
}

/// 9.3.11 Temporal.PlainMonthDay.prototype.valueOf ( )
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`")
.into())
}
// 8. Return result.
js_string!(result).into()
}

// ==== `PlainMonthDay` Abstract Operations ====

pub(crate) fn create_temporal_month_day(
inner: InnerMonthDay,
new_target: Option<&JsValue>,
Expand Down
Loading

0 comments on commit d55c691

Please sign in to comment.