Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 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 @@ -116,7 +116,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"
Expand Down
40 changes: 40 additions & 0 deletions core/engine/src/builtins/temporal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
135 changes: 123 additions & 12 deletions core/engine/src/builtins/temporal/plain_date/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

// TODO (nekevss): DOCS DOCS AND MORE DOCS

use std::str::FromStr;

use crate::{
builtins::{
options::{get_option, get_options_object},
Expand All @@ -14,24 +16,27 @@ 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;
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.
Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -807,12 +841,29 @@ 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, «»).
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 Err(JsNativeError::error()
.with_message("CalendarDateFields not yet implemented.")
.into());
return calendar
.date_from_fields(&mut fields, overflow)
.map_err(Into::into);
}

// 5. If item is not a String, throw a TypeError exception.
Expand All @@ -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 changes: 78 additions & 8 deletions core/engine/src/builtins/temporal/plain_date_time/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -975,16 +1006,55 @@ 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" », «»).
// "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).
// TODO: Implement d-g.
return Err(JsNativeError::range()
.with_message("Not yet implemented.")
.into());
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.
Expand Down
Loading