Browse Source

Build out partial record functionality, property bag construction, and `with` methods (#3955)

* Build out partial functionality and with methods

* Use temporal_rs's TinyAsciiStr

* Apply review feedback for specification comments

* Bump temporal-rs and add DateTime::with
pull/3969/head
Kevin Ness 3 months ago committed by GitHub
parent
commit
31e4990d6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      Cargo.lock
  2. 2
      Cargo.toml
  3. 40
      core/engine/src/builtins/temporal/mod.rs
  4. 135
      core/engine/src/builtins/temporal/plain_date/mod.rs
  5. 86
      core/engine/src/builtins/temporal/plain_date_time/mod.rs
  6. 103
      core/engine/src/builtins/temporal/plain_time/mod.rs
  7. 34
      core/engine/src/value/mod.rs

2
Cargo.lock generated

@ -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",

2
Cargo.toml

@ -111,7 +111,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"

40
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<Option<&'value JsObject>> {
// 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::<PlainDate>()
|| obj.is::<PlainDateTime>()
|| obj.is::<PlainMonthDay>()
|| obj.is::<PlainYearMonth>()
|| obj.is::<PlainTime>()
|| obj.is::<ZonedDateTime>()
{
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`

135
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<JsValue> {
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<JsValue> {
// 1. Let temporalDate be the this value.
// 2. Perform ? RequireInternalSlot(temporalDate, [[InitializedTemporalDate]]).
let date = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.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::<ArithmeticOverflow>(&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,13 +841,30 @@ 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::<ArithmeticOverflow>(&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, «»).
// g. Return ? CalendarDateFromFields(calendar, fields, options).
return Err(JsNativeError::error()
.with_message("CalendarDateFields not yet implemented.")
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 calendar
.date_from_fields(&mut fields, overflow)
.map_err(Into::into);
}
// 5. If item is not a String, throw a TypeError exception.
let JsValue::String(date_like_string) = item else {
@ -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<PartialDate> {
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::<JsError>::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,
})
}

86
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<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 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::<ArithmeticOverflow>(&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,17 +1006,56 @@ 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" », «»).
// g. Let result be ? InterpretTemporalDateTimeFields(calendarRec, fields, resolvedOptions).
// TODO: Implement d-g.
return Err(JsNativeError::range()
.with_message("Not yet implemented.")
// "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).
let overflow = get_option::<ArithmeticOverflow>(&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.
let Some(string) = value.as_string() else {

103
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<JsValue> {
// 1.Let temporalTime be the this value.
// 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]).
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.")
})?;
// 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::<ArithmeticOverflow>(&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<JsValue> {
let time = this
@ -598,8 +626,8 @@ pub(crate) fn create_temporal_time(
pub(crate) fn to_temporal_time(
value: &JsValue,
_overflow: Option<ArithmeticOverflow>,
_context: &mut Context,
overflow: Option<ArithmeticOverflow>,
context: &mut Context,
) -> JsResult<Time> {
// 1.If overflow is not present, set overflow to "constrain".
// 2. If item is an Object, then
@ -625,23 +653,24 @@ pub(crate) fn to_temporal_time(
// i. Return ! CreateTemporalTime(item.[[ISOHour]], item.[[ISOMinute]],
// item.[[ISOSecond]], item.[[ISOMillisecond]], item.[[ISOMicrosecond]],
// item.[[ISONanosecond]]).
return Ok(Time::new(
dt.inner.hour().into(),
dt.inner.minute().into(),
dt.inner.second().into(),
dt.inner.millisecond().into(),
dt.inner.microsecond().into(),
dt.inner.nanosecond().into(),
ArithmeticOverflow::Reject,
)?);
return Ok(Time::from(dt.inner.clone()));
}
// d. Let result be ? ToTemporalTimeRecord(item).
// e. Set result to ? RegulateTime(result.[[Hour]], result.[[Minute]],
// result.[[Second]], result.[[Millisecond]], result.[[Microsecond]],
// result.[[Nanosecond]], overflow).
Err(JsNativeError::range()
.with_message("Not yet implemented.")
.into())
let partial = to_partial_time_record(object, context)?;
Time::new(
partial.hour.unwrap_or(0),
partial.minute.unwrap_or(0),
partial.second.unwrap_or(0),
partial.millisecond.unwrap_or(0),
partial.microsecond.unwrap_or(0),
partial.nanosecond.unwrap_or(0),
overflow.unwrap_or(ArithmeticOverflow::Constrain),
)
.map_err(Into::into)
}
// 3. Else,
JsValue::String(str) => {
@ -659,3 +688,47 @@ pub(crate) fn to_temporal_time(
// 4. Return ! CreateTemporalTime(result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]]).
}
pub(crate) fn to_partial_time_record(
partial_object: &JsObject,
context: &mut Context,
) -> JsResult<PartialTime> {
let hour = partial_object
.get(js_str!("hour"), context)?
.map(|v| super::to_integer_if_integral(v, context))
.transpose()?;
let minute = partial_object
.get(js_str!("minute"), context)?
.map(|v| super::to_integer_if_integral(v, context))
.transpose()?;
let second = partial_object
.get(js_str!("second"), context)?
.map(|v| super::to_integer_if_integral(v, context))
.transpose()?;
let millisecond = partial_object
.get(js_str!("millisecond"), context)?
.map(|v| super::to_integer_if_integral(v, context))
.transpose()?;
let microsecond = partial_object
.get(js_str!("microsecond"), context)?
.map(|v| super::to_integer_if_integral(v, context))
.transpose()?;
let nanosecond = partial_object
.get(js_str!("nanosecond"), context)?
.map(|v| super::to_integer_if_integral(v, context))
.transpose()?;
Ok(PartialTime {
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
})
}

34
core/engine/src/value/mod.rs

@ -1029,6 +1029,40 @@ impl JsValue {
.into()
}
/// Maps a `JsValue` into a `Option<T>` where T is the result of an
/// operation on a defined value. If the value is `JsValue::undefined`,
/// then `JsValue::map` will return None.
///
/// # Example
///
/// ```
/// use boa_engine::{JsValue, Context};
///
/// let mut context = Context::default();
///
/// let defined_value = JsValue::from(5);
/// let undefined = JsValue::undefined();
///
/// let defined_result = defined_value.map(|v| v.add(&JsValue::from(5), &mut context)).transpose().unwrap();
/// let undefined_result = undefined.map(|v| v.add(&JsValue::from(5), &mut context)).transpose().unwrap();
///
/// assert_eq!(defined_result, Some(JsValue::Integer(10)));
/// assert_eq!(undefined_result, None);
///
/// ```
///
#[inline]
#[must_use]
pub fn map<T, F>(&self, f: F) -> Option<T>
where
F: FnOnce(&JsValue) -> T,
{
if self.is_undefined() {
return None;
}
Some(f(self))
}
/// Abstract operation `IsArray ( argument )`
///
/// Check if a value is an array.

Loading…
Cancel
Save