diff --git a/Cargo.lock b/Cargo.lock index 5ac3aeddfd5..8e454872306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3201,7 +3201,7 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "temporal_rs" version = "0.0.3" -source = "git+https://github.com/boa-dev/temporal.git?rev=8ef18fedc401761875b815a692443bf54ce9bd96#8ef18fedc401761875b815a692443bf54ce9bd96" +source = "git+https://github.com/boa-dev/temporal.git?rev=af94bbc31d409a2bfdce473e667e08e16c677149#af94bbc31d409a2bfdce473e667e08e16c677149" dependencies = [ "bitflags 2.6.0", "icu_calendar", diff --git a/Cargo.toml b/Cargo.toml index 139d147a628..2813e381718 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = "8ef18fedc401761875b815a692443bf54ce9bd96" } +temporal_rs = { git = "https://github.com/boa-dev/temporal.git", rev = "af94bbc31d409a2bfdce473e667e08e16c677149" } web-time = "1.1.0" criterion = "0.5.1" float-cmp = "0.9.0" diff --git a/core/engine/src/builtins/temporal/duration/mod.rs b/core/engine/src/builtins/temporal/duration/mod.rs index aa05f17d4c2..822e2c6c521 100644 --- a/core/engine/src/builtins/temporal/duration/mod.rs +++ b/core/engine/src/builtins/temporal/duration/mod.rs @@ -17,8 +17,9 @@ use boa_gc::{Finalize, Trace}; use boa_macros::js_str; use boa_profiler::Profiler; use temporal_rs::{ - components::Duration as InnerDuration, + components::{duration::PartialDuration, Duration as InnerDuration}, options::{RelativeTo, RoundingIncrement, RoundingOptions, TemporalRoundingMode, TemporalUnit}, + primitive::FiniteF64, }; use super::{ @@ -218,61 +219,61 @@ impl BuiltInConstructor for Duration { } // 2. If years is undefined, let y be 0; else let y be ? ToIntegerIfIntegral(years). - let years = f64::from( + let years = FiniteF64::from( args.first() .map_or(Ok(0), |y| to_integer_if_integral(y, context))?, ); // 3. If months is undefined, let mo be 0; else let mo be ? ToIntegerIfIntegral(months). - let months = f64::from( + let months = FiniteF64::from( args.get(1) .map_or(Ok(0), |mo| to_integer_if_integral(mo, context))?, ); // 4. If weeks is undefined, let w be 0; else let w be ? ToIntegerIfIntegral(weeks). - let weeks = f64::from( + let weeks = FiniteF64::from( args.get(2) .map_or(Ok(0), |wk| to_integer_if_integral(wk, context))?, ); // 5. If days is undefined, let d be 0; else let d be ? ToIntegerIfIntegral(days). - let days = f64::from( + let days = FiniteF64::from( args.get(3) .map_or(Ok(0), |d| to_integer_if_integral(d, context))?, ); // 6. If hours is undefined, let h be 0; else let h be ? ToIntegerIfIntegral(hours). - let hours = f64::from( + let hours = FiniteF64::from( args.get(4) .map_or(Ok(0), |h| to_integer_if_integral(h, context))?, ); // 7. If minutes is undefined, let m be 0; else let m be ? ToIntegerIfIntegral(minutes). - let minutes = f64::from( + let minutes = FiniteF64::from( args.get(5) .map_or(Ok(0), |m| to_integer_if_integral(m, context))?, ); // 8. If seconds is undefined, let s be 0; else let s be ? ToIntegerIfIntegral(seconds). - let seconds = f64::from( + let seconds = FiniteF64::from( args.get(6) .map_or(Ok(0), |s| to_integer_if_integral(s, context))?, ); // 9. If milliseconds is undefined, let ms be 0; else let ms be ? ToIntegerIfIntegral(milliseconds). - let milliseconds = f64::from( + let milliseconds = FiniteF64::from( args.get(7) .map_or(Ok(0), |ms| to_integer_if_integral(ms, context))?, ); // 10. If microseconds is undefined, let mis be 0; else let mis be ? ToIntegerIfIntegral(microseconds). - let microseconds = f64::from( + let microseconds = FiniteF64::from( args.get(8) .map_or(Ok(0), |mis| to_integer_if_integral(mis, context))?, ); // 11. If nanoseconds is undefined, let ns be 0; else let ns be ? ToIntegerIfIntegral(nanoseconds). - let nanoseconds = f64::from( + let nanoseconds = FiniteF64::from( args.get(9) .map_or(Ok(0), |ns| to_integer_if_integral(ns, context))?, ); @@ -310,16 +311,16 @@ impl Duration { let inner = &duration.inner; match field { - DateTimeValues::Year => Ok(JsValue::Rational(inner.years())), - DateTimeValues::Month => Ok(JsValue::Rational(inner.months())), - DateTimeValues::Week => Ok(JsValue::Rational(inner.weeks())), - DateTimeValues::Day => Ok(JsValue::Rational(inner.days())), - DateTimeValues::Hour => Ok(JsValue::Rational(inner.hours())), - DateTimeValues::Minute => Ok(JsValue::Rational(inner.minutes())), - DateTimeValues::Second => Ok(JsValue::Rational(inner.seconds())), - DateTimeValues::Millisecond => Ok(JsValue::Rational(inner.milliseconds())), - DateTimeValues::Microsecond => Ok(JsValue::Rational(inner.microseconds())), - DateTimeValues::Nanosecond => Ok(JsValue::Rational(inner.nanoseconds())), + DateTimeValues::Year => Ok(JsValue::Rational(inner.years().as_inner())), + DateTimeValues::Month => Ok(JsValue::Rational(inner.months().as_inner())), + DateTimeValues::Week => Ok(JsValue::Rational(inner.weeks().as_inner())), + DateTimeValues::Day => Ok(JsValue::Rational(inner.days().as_inner())), + DateTimeValues::Hour => Ok(JsValue::Rational(inner.hours().as_inner())), + DateTimeValues::Minute => Ok(JsValue::Rational(inner.minutes().as_inner())), + DateTimeValues::Second => Ok(JsValue::Rational(inner.seconds().as_inner())), + DateTimeValues::Millisecond => Ok(JsValue::Rational(inner.milliseconds().as_inner())), + DateTimeValues::Microsecond => Ok(JsValue::Rational(inner.microseconds().as_inner())), + DateTimeValues::Nanosecond => Ok(JsValue::Rational(inner.nanoseconds().as_inner())), DateTimeValues::MonthCode => unreachable!( "Any other DateTimeValue fields on Duration would be an implementation error." ), @@ -458,101 +459,79 @@ impl Duration { // a. Let years be temporalDurationLike.[[Years]]. // 5. Else, // a. Let years be duration.[[Years]]. - let years = if temporal_duration_like.years().is_nan() { - duration.inner.years() - } else { - temporal_duration_like.years() - }; + let years = temporal_duration_like + .years + .unwrap_or(duration.inner.years()); // 6. If temporalDurationLike.[[Months]] is not undefined, then // a. Let months be temporalDurationLike.[[Months]]. // 7. Else, // a. Let months be duration.[[Months]]. - let months = if temporal_duration_like.months().is_nan() { - duration.inner.months() - } else { - temporal_duration_like.months() - }; + let months = temporal_duration_like + .months + .unwrap_or(duration.inner.months()); // 8. If temporalDurationLike.[[Weeks]] is not undefined, then // a. Let weeks be temporalDurationLike.[[Weeks]]. // 9. Else, // a. Let weeks be duration.[[Weeks]]. - let weeks = if temporal_duration_like.weeks().is_nan() { - duration.inner.weeks() - } else { - temporal_duration_like.weeks() - }; + let weeks = temporal_duration_like + .weeks + .unwrap_or(duration.inner.weeks()); // 10. If temporalDurationLike.[[Days]] is not undefined, then // a. Let days be temporalDurationLike.[[Days]]. // 11. Else, // a. Let days be duration.[[Days]]. - let days = if temporal_duration_like.days().is_nan() { - duration.inner.days() - } else { - temporal_duration_like.days() - }; + let days = temporal_duration_like.days.unwrap_or(duration.inner.days()); // 12. If temporalDurationLike.[[Hours]] is not undefined, then // a. Let hours be temporalDurationLike.[[Hours]]. // 13. Else, // a. Let hours be duration.[[Hours]]. - let hours = if temporal_duration_like.hours().is_nan() { - duration.inner.hours() - } else { - temporal_duration_like.hours() - }; + let hours = temporal_duration_like + .hours + .unwrap_or(duration.inner.hours()); // 14. If temporalDurationLike.[[Minutes]] is not undefined, then // a. Let minutes be temporalDurationLike.[[Minutes]]. // 15. Else, // a. Let minutes be duration.[[Minutes]]. - let minutes = if temporal_duration_like.minutes().is_nan() { - duration.inner.minutes() - } else { - temporal_duration_like.minutes() - }; + let minutes = temporal_duration_like + .minutes + .unwrap_or(duration.inner.minutes()); // 16. If temporalDurationLike.[[Seconds]] is not undefined, then // a. Let seconds be temporalDurationLike.[[Seconds]]. // 17. Else, // a. Let seconds be duration.[[Seconds]]. - let seconds = if temporal_duration_like.seconds().is_nan() { - duration.inner.seconds() - } else { - temporal_duration_like.seconds() - }; + let seconds = temporal_duration_like + .seconds + .unwrap_or(duration.inner.seconds()); // 18. If temporalDurationLike.[[Milliseconds]] is not undefined, then // a. Let milliseconds be temporalDurationLike.[[Milliseconds]]. // 19. Else, // a. Let milliseconds be duration.[[Milliseconds]]. - let milliseconds = if temporal_duration_like.milliseconds().is_nan() { - duration.inner.milliseconds() - } else { - temporal_duration_like.milliseconds() - }; + let milliseconds = temporal_duration_like + .milliseconds + .unwrap_or(duration.inner.milliseconds()); // 20. If temporalDurationLike.[[Microseconds]] is not undefined, then // a. Let microseconds be temporalDurationLike.[[Microseconds]]. // 21. Else, // a. Let microseconds be duration.[[Microseconds]]. - let microseconds = if temporal_duration_like.microseconds().is_nan() { - duration.inner.microseconds() - } else { - temporal_duration_like.microseconds() - }; + let microseconds = temporal_duration_like + .microseconds + .unwrap_or(duration.inner.microseconds()); // 22. If temporalDurationLike.[[Nanoseconds]] is not undefined, then // a. Let nanoseconds be temporalDurationLike.[[Nanoseconds]]. // 23. Else, // a. Let nanoseconds be duration.[[Nanoseconds]]. - let nanoseconds = if temporal_duration_like.nanoseconds().is_nan() { - duration.inner.nanoseconds() - } else { - temporal_duration_like.nanoseconds() - }; + let nanoseconds = temporal_duration_like + .nanoseconds + .unwrap_or(duration.inner.nanoseconds()); // 24. Return ? CreateTemporalDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). let new_duration = InnerDuration::new( @@ -827,7 +806,27 @@ impl Duration { // -- Duration Abstract Operations -- -/// 7.5.9 `ToTemporalDurationRecord ( temporalDurationLike )` +/// 7.5.12 `ToTemporalDuration ( item )` +pub(crate) fn to_temporal_duration( + item: &JsValue, + context: &mut Context, +) -> JsResult { + // 1a. If Type(item) is Object + // 1b. and item has an [[InitializedTemporalDuration]] internal slot, then + if let Some(duration) = item + .as_object() + .and_then(JsObject::downcast_ref::) + { + return Ok(duration.inner); + } + + // 2. Let result be ? ToTemporalDurationRecord(item). + let result = to_temporal_duration_record(item, context)?; + // 3. Return ! CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], result.[[Hours]], result.[[Minutes]], result.[[Seconds]], result.[[Milliseconds]], result.[[Microseconds]], result.[[Nanoseconds]]). + Ok(result) +} + +/// 7.5.13 `ToTemporalDurationRecord ( temporalDurationLike )` pub(crate) fn to_temporal_duration_record( temporal_duration_like: &JsValue, context: &mut Context, @@ -859,6 +858,7 @@ pub(crate) fn to_temporal_duration_record( let partial = to_temporal_partial_duration(temporal_duration_like, context)?; // 5. If partial.[[Years]] is not undefined, set result.[[Years]] to partial.[[Years]]. + // 6. If partial.[[Months]] is not undefined, set result.[[Months]] to partial.[[Months]]. // 7. If partial.[[Weeks]] is not undefined, set result.[[Weeks]] to partial.[[Weeks]]. // 8. If partial.[[Days]] is not undefined, set result.[[Days]] to partial.[[Days]]. @@ -871,7 +871,7 @@ pub(crate) fn to_temporal_duration_record( // 15. If ! IsValidDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], result.[[Hours]], result.[[Minutes]], result.[[Seconds]], result.[[Milliseconds]], result.[[Microseconds]], result.[[Nanoseconds]]) is false, then // a. Throw a RangeError exception. // 16. Return result. - InnerDuration::from_partial(&partial).map_err(Into::into) + InnerDuration::from_partial_duration(partial).map_err(Into::into) } /// 7.5.14 `CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , newTarget ] )` @@ -919,7 +919,7 @@ pub(crate) fn create_temporal_duration( pub(crate) fn to_temporal_partial_duration( duration_like: &JsValue, context: &mut Context, -) -> JsResult { +) -> JsResult { // 1. If Type(temporalDurationLike) is not Object, then let JsValue::Object(unknown_object) = duration_like else { // a. Throw a TypeError exception. @@ -929,77 +929,106 @@ pub(crate) fn to_temporal_partial_duration( }; // 2. Let result be a new partial Duration Record with each field set to undefined. - let mut result = InnerDuration::partial(); + let mut result = PartialDuration::default(); // 3. NOTE: The following steps read properties and perform independent validation in alphabetical order. // 4. Let days be ? Get(temporalDurationLike, "days"). let days = unknown_object.get(js_str!("days"), context)?; if !days.is_undefined() { // 5. If days is not undefined, set result.[[Days]] to ? ToIntegerIfIntegral(days). - result.set_days(f64::from(to_integer_if_integral(&days, context)?)); + let _ = result + .days + .insert(FiniteF64::from(to_integer_if_integral(&days, context)?)); } // 6. Let hours be ? Get(temporalDurationLike, "hours"). let hours = unknown_object.get(js_str!("hours"), context)?; // 7. If hours is not undefined, set result.[[Hours]] to ? ToIntegerIfIntegral(hours). if !hours.is_undefined() { - result.set_hours(f64::from(to_integer_if_integral(&hours, context)?)); + let _ = result + .hours + .insert(FiniteF64::from(to_integer_if_integral(&hours, context)?)); } // 8. Let microseconds be ? Get(temporalDurationLike, "microseconds"). let microseconds = unknown_object.get(js_str!("microseconds"), context)?; // 9. If microseconds is not undefined, set result.[[Microseconds]] to ? ToIntegerIfIntegral(microseconds). if !microseconds.is_undefined() { - result.set_microseconds(f64::from(to_integer_if_integral(µseconds, context)?)); + let _ = result + .microseconds + .insert(FiniteF64::from(to_integer_if_integral( + µseconds, + context, + )?)); } // 10. Let milliseconds be ? Get(temporalDurationLike, "milliseconds"). let milliseconds = unknown_object.get(js_str!("milliseconds"), context)?; // 11. If milliseconds is not undefined, set result.[[Milliseconds]] to ? ToIntegerIfIntegral(milliseconds). if !milliseconds.is_undefined() { - result.set_milliseconds(f64::from(to_integer_if_integral(&milliseconds, context)?)); + let _ = result + .milliseconds + .insert(FiniteF64::from(to_integer_if_integral( + &milliseconds, + context, + )?)); } // 12. Let minutes be ? Get(temporalDurationLike, "minutes"). let minutes = unknown_object.get(js_str!("minutes"), context)?; // 13. If minutes is not undefined, set result.[[Minutes]] to ? ToIntegerIfIntegral(minutes). if !minutes.is_undefined() { - result.set_minutes(f64::from(to_integer_if_integral(&minutes, context)?)); + let _ = result + .minutes + .insert(FiniteF64::from(to_integer_if_integral(&minutes, context)?)); } // 14. Let months be ? Get(temporalDurationLike, "months"). let months = unknown_object.get(js_str!("months"), context)?; // 15. If months is not undefined, set result.[[Months]] to ? ToIntegerIfIntegral(months). if !months.is_undefined() { - result.set_months(f64::from(to_integer_if_integral(&months, context)?)); + let _ = result + .months + .insert(FiniteF64::from(to_integer_if_integral(&months, context)?)); } // 16. Let nanoseconds be ? Get(temporalDurationLike, "nanoseconds"). let nanoseconds = unknown_object.get(js_str!("nanoseconds"), context)?; // 17. If nanoseconds is not undefined, set result.[[Nanoseconds]] to ? ToIntegerIfIntegral(nanoseconds). if !nanoseconds.is_undefined() { - result.set_nanoseconds(f64::from(to_integer_if_integral(&nanoseconds, context)?)); + let _ = result + .nanoseconds + .insert(FiniteF64::from(to_integer_if_integral( + &nanoseconds, + context, + )?)); } // 18. Let seconds be ? Get(temporalDurationLike, "seconds"). let seconds = unknown_object.get(js_str!("seconds"), context)?; // 19. If seconds is not undefined, set result.[[Seconds]] to ? ToIntegerIfIntegral(seconds). if !seconds.is_undefined() { - result.set_seconds(f64::from(to_integer_if_integral(&seconds, context)?)); + let _ = result + .seconds + .insert(FiniteF64::from(to_integer_if_integral(&seconds, context)?)); } // 20. Let weeks be ? Get(temporalDurationLike, "weeks"). let weeks = unknown_object.get(js_str!("weeks"), context)?; // 21. If weeks is not undefined, set result.[[Weeks]] to ? ToIntegerIfIntegral(weeks). if !weeks.is_undefined() { - result.set_weeks(f64::from(to_integer_if_integral(&weeks, context)?)); + let _ = result + .weeks + .insert(FiniteF64::from(to_integer_if_integral(&weeks, context)?)); } // 22. Let years be ? Get(temporalDurationLike, "years"). let years = unknown_object.get(js_str!("years"), context)?; // 23. If years is not undefined, set result.[[Years]] to ? ToIntegerIfIntegral(years). if !years.is_undefined() { - result.set_years(f64::from(to_integer_if_integral(&years, context)?)); + let _ = result + .years + .insert(FiniteF64::from(to_integer_if_integral(&years, context)?)); } // TODO: Implement this functionality better in `temporal_rs`. @@ -1007,16 +1036,7 @@ pub(crate) fn to_temporal_partial_duration( // is undefined, and hours is undefined, and minutes is undefined, and seconds is // undefined, and milliseconds is undefined, and microseconds is undefined, and // nanoseconds is undefined, throw a TypeError exception. - if result.years().is_nan() - && result.months().is_nan() - && result.weeks().is_nan() - && result.days().is_nan() - && result.minutes().is_nan() - && result.seconds().is_nan() - && result.milliseconds().is_nan() - && result.microseconds().is_nan() - && result.nanoseconds().is_nan() - { + if result.is_empty() { return Err(JsNativeError::typ() .with_message("PartialDurationRecord must have a defined field.") .into()); diff --git a/core/engine/src/builtins/temporal/options.rs b/core/engine/src/builtins/temporal/options.rs index e5c5fefcb89..06b83e2087c 100644 --- a/core/engine/src/builtins/temporal/options.rs +++ b/core/engine/src/builtins/temporal/options.rs @@ -15,7 +15,7 @@ use crate::{ }; use boa_macros::js_str; use temporal_rs::options::{ - ArithmeticOverflow, DifferenceSettings, DurationOverflow, InstantDisambiguation, + ArithmeticOverflow, CalendarName, DifferenceSettings, DurationOverflow, InstantDisambiguation, OffsetDisambiguation, RoundingIncrement, TemporalRoundingMode, TemporalUnit, }; @@ -116,6 +116,7 @@ impl ParsableOptionType for DurationOverflow {} impl ParsableOptionType for InstantDisambiguation {} impl ParsableOptionType for OffsetDisambiguation {} impl ParsableOptionType for TemporalRoundingMode {} +impl ParsableOptionType for CalendarName {} impl OptionType for RoundingIncrement { fn from_value(value: JsValue, context: &mut Context) -> JsResult { diff --git a/core/engine/src/builtins/temporal/plain_month_day/mod.rs b/core/engine/src/builtins/temporal/plain_month_day/mod.rs index 6966e276487..37ad9f8ae61 100644 --- a/core/engine/src/builtins/temporal/plain_month_day/mod.rs +++ b/core/engine/src/builtins/temporal/plain_month_day/mod.rs @@ -1,15 +1,22 @@ //! Boa's implementation of the ECMAScript `Temporal.PlainMonthDay` builtin object. #![allow(dead_code, unused_variables)] +use std::str::FromStr; + use crate::{ - builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + builtins::{ + options::{get_option, get_options_object}, + BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + js_string, object::internal_methods::get_prototype_from_constructor, property::Attribute, realm::Realm, string::StaticJsStrings, - Context, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, + Context, JsArgs, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, }; use boa_gc::{Finalize, Trace}; +use boa_macros::js_str; use boa_profiler::Profiler; use temporal_rs::{ @@ -18,8 +25,11 @@ use temporal_rs::{ DateTime, MonthDay as InnerMonthDay, }, iso::IsoDateSlots, + options::{ArithmeticOverflow, CalendarName}, }; +use super::{calendar::to_temporal_calendar_slot_value, DateTimeValues}; + /// The `Temporal.PlainMonthDay` object. #[derive(Debug, Clone, Trace, Finalize, JsData)] #[boa_gc(unsafe_empty_trace)] // TODO: Remove this!!! `InnerMonthDay` could contain `Trace` types. @@ -33,6 +43,81 @@ 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 { + 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 { + let month_day = this + .as_object() + .and_then(JsObject::downcast_ref::) + .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 { + Self::get_internal_field(this, &DateTimeValues::Day) + } + + fn get_year(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Year) + } + + fn get_month_code(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::MonthCode) + } + + fn get_calendar_id(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let month_day = this + .as_object() + .and_then(JsObject::downcast_ref::) + .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 { + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + let month_day = this + .as_object() + .and_then(JsObject::downcast_ref::) + .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::(&options, js_str!("calendarName"), context)? + .unwrap_or(CalendarName::Auto); + + Ok(month_day_to_string(inner, show_calendar)) + } +} impl IsoDateSlots for JsObject { fn iso_date(&self) -> temporal_rs::iso::IsoDate { self.borrow().data().inner.iso_date() @@ -52,6 +137,17 @@ impl BuiltInObject for PlainMonthDay { impl IntrinsicObject for PlainMonthDay { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + let get_day = BuiltInBuilder::callable(realm, Self::get_day) + .name(js_string!("get day")) + .build(); + + let get_month_code = BuiltInBuilder::callable(realm, Self::get_month_code) + .name(js_string!("get monthCode")) + .build(); + + let get_calendar_id = BuiltInBuilder::callable(realm, Self::get_calendar_id) + .name(js_string!("get calendarId")) + .build(); BuiltInBuilder::from_standard_constructor::(realm) .property( @@ -59,6 +155,26 @@ impl IntrinsicObject for PlainMonthDay { StaticJsStrings::PLAIN_MD_TAG, Attribute::CONFIGURABLE, ) + .accessor( + js_string!("day"), + Some(get_day), + None, + Attribute::CONFIGURABLE, + ) + .accessor( + js_string!("monthCode"), + Some(get_month_code), + None, + Attribute::CONFIGURABLE, + ) + .accessor( + js_string!("calendarId"), + Some(get_calendar_id), + None, + Attribute::CONFIGURABLE, + ) + .method(Self::to_string, js_string!("toString"), 1) + .static_method(Self::from, js_string!("from"), 2) .build(); } @@ -86,6 +202,40 @@ impl BuiltInConstructor for PlainMonthDay { // ==== `PlainMonthDay` Abstract Operations ==== +fn month_day_to_string(inner: &InnerMonthDay, show_calendar: CalendarName) -> 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}"); + + 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, + CalendarName::Critical | CalendarName::Always | CalendarName::Auto + )) && !(matches!(show_calendar, CalendarName::Auto) && calendar_id == "iso8601") + { + let flag = if matches!(show_calendar, CalendarName::Critical) { + "!" + } else { + "" + }; + result.push_str(&format!("[{flag}c={calendar_id}]",)); + } + // 8. Return result. + js_string!(result).into() +} + pub(crate) fn create_temporal_month_day( inner: InnerMonthDay, new_target: Option<&JsValue>, @@ -93,9 +243,9 @@ pub(crate) fn create_temporal_month_day( ) -> JsResult { // 1. If IsValidISODate(referenceISOYear, isoMonth, isoDay) is false, throw a RangeError exception. // 2. If ISODateTimeWithinLimits(referenceISOYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0) is false, throw a RangeError exception. - if DateTime::validate(&inner) { + if !DateTime::validate(&inner) { return Err(JsNativeError::range() - .with_message("PlainMonthDay is not a valid ISO date time.") + .with_message("PlainMonthDay does not hold a valid ISO date time.") .into()); } @@ -128,3 +278,44 @@ pub(crate) fn create_temporal_month_day( // 9. Return object. Ok(obj.into()) } + +fn to_temporal_month_day( + item: &JsValue, + options: &JsObject, + context: &mut Context, +) -> JsResult { + let overflow = get_option::(options, js_str!("overflow"), context)? + .unwrap_or(ArithmeticOverflow::Constrain); + + // get the calendar property (string) from the item object + let calender_id = item.get_v(js_str!("calendar"), context)?; + let calendar = to_temporal_calendar_slot_value(&calender_id)?; + + let inner = if let Some(item_obj) = item + .as_object() + .and_then(JsObject::downcast_ref::) + { + item_obj.inner.clone() + } else if let Some(item_string) = item.as_string() { + InnerMonthDay::from_str(item_string.to_std_string_escaped().as_str())? + } else if item.is_object() { + InnerMonthDay::new( + item.get_v(js_str!("month"), context) + .expect("Month not found") + .to_i32(context) + .expect("Cannot convert month to i32"), + item.get_v(js_str!("day"), context) + .expect("Day not found") + .to_i32(context) + .expect("Cannot convert day to i32"), + calendar, + overflow, + )? + } else { + return Err(JsNativeError::typ() + .with_message("item must be an object or a string") + .into()); + }; + + create_temporal_month_day(inner, None, context) +} diff --git a/core/engine/src/builtins/temporal/plain_year_month/mod.rs b/core/engine/src/builtins/temporal/plain_year_month/mod.rs index 492549133d2..74c90bd9c1a 100644 --- a/core/engine/src/builtins/temporal/plain_year_month/mod.rs +++ b/core/engine/src/builtins/temporal/plain_year_month/mod.rs @@ -1,7 +1,12 @@ //! Boa's implementation of the `Temporal.PlainYearMonth` builtin object. +use std::str::FromStr; + use crate::{ - builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + builtins::{ + options::{get_option, get_options_object}, + BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, js_string, object::internal_methods::get_prototype_from_constructor, @@ -11,20 +16,19 @@ use crate::{ Context, JsArgs, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, }; use boa_gc::{Finalize, Trace}; +use boa_macros::js_str; use boa_profiler::Profiler; use temporal_rs::{ - iso::IsoDateSlots, - { - components::{ - calendar::{Calendar as InnerCalendar, GetTemporalCalendar}, - YearMonth as InnerYearMonth, - }, - options::ArithmeticOverflow, + components::{ + calendar::{Calendar as InnerCalendar, GetTemporalCalendar}, + Duration, YearMonth as InnerYearMonth, }, + iso::IsoDateSlots, + options::{ArithmeticOverflow, CalendarName}, }; -use super::calendar; +use super::{calendar::to_temporal_calendar_slot_value, to_temporal_duration, DateTimeValues}; /// The `Temporal.PlainYearMonth` object. #[derive(Debug, Clone, Trace, Finalize, JsData)] @@ -145,12 +149,14 @@ impl IntrinsicObject for PlainYearMonth { None, Attribute::CONFIGURABLE, ) + .static_method(Self::from, js_string!("from"), 2) .method(Self::with, js_string!("with"), 2) .method(Self::add, js_string!("add"), 2) .method(Self::subtract, js_string!("subtract"), 2) .method(Self::until, js_string!("until"), 2) .method(Self::since, js_string!("since"), 2) .method(Self::equals, js_string!("equals"), 1) + .method(Self::to_string, js_string!("toString"), 1) .build(); } @@ -193,7 +199,7 @@ impl BuiltInConstructor for PlainYearMonth { // 4. Let m be ? ToIntegerWithTruncation(isoMonth). let m = super::to_integer_with_truncation(args.get_or_undefined(1), context)?; // 5. Let calendar be ? ToTemporalCalendarSlotValue(calendarLike, "iso8601"). - let calendar = calendar::to_temporal_calendar_slot_value(args.get_or_undefined(2))?; + let calendar = to_temporal_calendar_slot_value(args.get_or_undefined(2))?; // 7. Return ? CreateTemporalYearMonth(y, m, calendar, ref, NewTarget). let inner = InnerYearMonth::new(y, m, ref_day, calendar, ArithmeticOverflow::Reject)?; @@ -202,55 +208,151 @@ impl BuiltInConstructor for PlainYearMonth { } } -// ==== `PlainYearMonth` Accessor Implementations ==== +// ==== `Temporal.PlainYearMonth` static Methods ==== impl PlainYearMonth { - fn get_calendar_id(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + // 9.2.2 `Temporal.PlainYearMonth.from ( item [ , options ] )` + fn from(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let item = args.get_or_undefined(0); + // 1. If Type(item) is Object or Type(item) is String and item is not null, then + + let inner = if item.is_object() { + // 9.2.2.2 + if let Some(data) = item + .as_object() + .and_then(JsObject::downcast_ref::) + { + // Perform ? [GetTemporalOverflowOption](https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloverflowoption)(options). + let options = get_options_object(args.get_or_undefined(1))?; + let _ = get_option::(&options, js_str!("overflow"), context)?; + data.inner.clone() + } else { + let options = get_options_object(args.get_or_undefined(1))?; + let overflow = get_option(&options, js_str!("overflow"), context)? + .unwrap_or(ArithmeticOverflow::Constrain); + + // a. Let calendar be ? ToTemporalCalendar(item). + let calendar = to_temporal_calendar_slot_value(args.get_or_undefined(1))?; + InnerYearMonth::new( + super::to_integer_with_truncation( + &item.get_v(js_str!("year"), context)?, + context, + )?, + super::to_integer_with_truncation( + &item.get_v(js_str!("month"), context)?, + context, + )?, + super::to_integer_with_truncation( + &item.get_v(js_str!("day"), context)?, + context, + ) + .ok(), + calendar, + overflow, + )? + } + } else if let Some(item_as_string) = item.as_string() { + InnerYearMonth::from_str(item_as_string.to_std_string_escaped().as_str())? + } else { + return Err(JsNativeError::typ() + .with_message("item must be an object, string, or null.") + .into()); + }; + + // b. Return ? ToTemporalYearMonth(item, calendar). + create_temporal_year_month(inner, None, context) } +} - fn get_year(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) +// ==== `PlainYearMonth` Accessor Implementations ====/ + +impl PlainYearMonth { + fn get_internal_field(this: &JsValue, field: &DateTimeValues) -> JsResult { + let year_month = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainYearMonth object.") + })?; + let inner = &year_month.inner; + match field { + DateTimeValues::Year => Ok(inner.iso_year().into()), + DateTimeValues::Month => Ok(inner.iso_month().into()), + DateTimeValues::MonthCode => { + Ok(JsString::from(InnerYearMonth::month_code(inner)?.as_str()).into()) + } + _ => unreachable!(), + } } - fn get_month(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + fn get_calendar_id(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + let obj = this + .as_object() + .ok_or_else(|| JsNativeError::typ().with_message("this must be an object."))?; + + let Ok(year_month) = obj.clone().downcast::() else { + return Err(JsNativeError::typ() + .with_message("the this object must be a PlainYearMonth object.") + .into()); + }; + + Ok(js_string!(year_month.get_calendar().identifier()).into()) } - fn get_month_code(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + fn get_year(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Year) } - fn get_days_in_year(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + fn get_month(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Month) } - fn get_days_in_month(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + fn get_month_code(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::MonthCode) } - fn get_months_in_year(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + fn get_days_in_year(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + let year_month = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainYearMonth object.") + })?; + let inner = &year_month.inner; + Ok(inner.get_days_in_year()?.into()) } - fn get_in_leap_year(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + fn get_days_in_month(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + let year_month = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainYearMonth object.") + })?; + let inner = &year_month.inner; + Ok(inner.get_days_in_month()?.into()) + } + + fn get_months_in_year(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + let year_month = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainYearMonth object.") + })?; + let inner = &year_month.inner; + Ok(inner.get_months_in_year()?.into()) + } + + fn get_in_leap_year(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + let year_month = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainYearMonth object.") + })?; + + Ok(year_month.inner.in_leap_year().into()) } } @@ -263,16 +365,18 @@ impl PlainYearMonth { .into()) } - fn add(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::typ() - .with_message("not yet implemented.") - .into()) + fn add(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let duration_like = args.get_or_undefined(0); + let options = get_options_object(args.get_or_undefined(1))?; + + add_or_subtract_duration(true, this, duration_like, &options, context) } - fn subtract(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::typ() - .with_message("not yet implemented.") - .into()) + fn subtract(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let duration_like = args.get_or_undefined(0); + let options = get_options_object(args.get_or_undefined(1))?; + + add_or_subtract_duration(false, this, duration_like, &options, context) } fn until(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { @@ -292,6 +396,27 @@ impl PlainYearMonth { .with_message("not yet implemented.") .into()) } + + fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let YearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + let year_month = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainYearMonth object.") + })?; + + let inner = &year_month.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::(&options, js_str!("calendarName"), context)? + .unwrap_or(CalendarName::Auto); + + Ok(year_month_to_string(inner, show_calendar)) + } } // ==== Abstract Operations ==== @@ -299,7 +424,7 @@ impl PlainYearMonth { // 9.5.2 `RegulateISOYearMonth ( year, month, overflow )` // Implemented on `TemporalFields`. -// 9.5.5 `CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , newTarget ] )` +// 9.5.6 `CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , newTarget ] )` pub(crate) fn create_temporal_year_month( ym: InnerYearMonth, new_target: Option<&JsValue>, @@ -338,3 +463,73 @@ pub(crate) fn create_temporal_year_month( // 9. Return object. Ok(obj.into()) } + +// 9.5.9 AddDurationToOrSubtractDurationFromPlainYearMonth ( operation, yearMonth, temporalDurationLike, options ) +fn add_or_subtract_duration( + is_addition: bool, + this: &JsValue, + duration_like: &JsValue, + options: &JsObject, + context: &mut Context, +) -> JsResult { + let duration: Duration = if duration_like.is_object() { + to_temporal_duration(duration_like, context)? + } else if let Some(duration_string) = duration_like.as_string() { + Duration::from_str(duration_string.to_std_string_escaped().as_str())? + } else { + return Err(JsNativeError::typ() + .with_message("cannot handler string durations yet.") + .into()); + }; + + let overflow = + get_option(options, js_str!("overflow"), context)?.unwrap_or(ArithmeticOverflow::Constrain); + + let year_month = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainYearMonth object.") + })?; + + let inner = &year_month.inner; + let year_month_result = if is_addition { + inner.add_duration(&duration, overflow)? + } else { + inner.subtract_duration(&duration, overflow)? + }; + + create_temporal_year_month(year_month_result, None, context) +} + +fn year_month_to_string(inner: &InnerYearMonth, show_calendar: CalendarName) -> JsValue { + // Let year be PadISOYear(yearMonth.[[ISOYear]]). + let year = inner.padded_iso_year_string(); + // Let month be ToZeroPaddedDecimalString(yearMonth.[[ISOMonth]], 2). + let month = inner.iso_month().to_string(); + + // Let result be the string-concatenation of year, the code unit 0x002D (HYPHEN-MINUS), and month. + let mut result = format!("{year}-{month:0>2}"); + + // 5. If showCalendar is one of "always" or "critical", or if calendarIdentifier is not "iso8601", then + // a. Let day be ToZeroPaddedDecimalString(yearMonth.[[ISODay]], 2). + // b. Set result to the string-concatenation of result, the code unit 0x002D (HYPHEN-MINUS), and day. + // 6. Let calendarString be FormatCalendarAnnotation(calendarIdentifier, showCalendar). + // 7. Set result to the string-concatenation of result and calendarString. + if matches!( + show_calendar, + CalendarName::Critical | CalendarName::Always | CalendarName::Auto + ) && !(matches!(show_calendar, CalendarName::Auto) && inner.calendar_id() == "iso8601") + { + let calendar = inner.calendar_id(); + let calendar_string = calendar.to_string(); + let flag = if matches!(show_calendar, CalendarName::Critical) { + "!" + } else { + "" + }; + result.push_str(&format!("[{flag}c={calendar_string}]",)); + } + // 8. Return result. + js_string!(result).into() +}