From ae31e501bf0cdc3a83cedb905e0f2fb00cf1ac3f Mon Sep 17 00:00:00 2001 From: Kevin <46825870+nekevss@users.noreply.github.com> Date: Wed, 11 Oct 2023 21:41:04 -0400 Subject: [PATCH] First portion of the Temporal implementation (#3277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Started with the Temporal implementation * Implemented some useful functions * Updaating some spec references * Initial work on TimeZone and Instant * More work completed on Temporal.Duration and Temporal.Instant * General scaffolding and heavy work on Instant and Duration complete * ZonedDateTime and Calendar started with further work on duration abstract ops * Further work on temporal work and clippy fixes * Post rebase fixes/reverts * Add BuiltinCalendar and begin IsoCalendar impl * More work completed on calendar/date/yearmonth/monthday * Calendar and iso impl close to completion - no datelike parsing * Initial work on temporal ISO8601 parsing and grammar * Post rebase fixes and updates * More on parser/Duration and work through clippy lints * Fix bug on peek_n and add temporal cfg * Fix clippy lints on parser tests * Build out calendar with icu_calendar, add some tests, and misc. * Fix spec hyperlinks * Parser clean up and invalid annotations * Add Duration and Temporal Parsing * Remove IsoYearMonthRecord * Post rebase update * Fix and add to ISO Parser docs * Parser/ast cleanup and duration refactor/additions * Review feedback, options update, and duration changes * Review changes, general cleanup, and post rebase fixes * Fix time zone parsing issue/test logic * Clean up parse output nodes * Apply review feedback and various fixes * Review feedback and get_option changes * Review feedback --------- Co-authored-by: Iban Eguia Moraza Co-authored-by: José Julián Espina --- boa_ast/Cargo.toml | 1 + boa_ast/src/lib.rs | 2 + boa_ast/src/temporal/mod.rs | 113 + boa_engine/Cargo.toml | 2 +- boa_engine/src/bigint.rs | 11 + boa_engine/src/builtins/date/mod.rs | 2 +- boa_engine/src/builtins/date/utils.rs | 6 +- boa_engine/src/builtins/mod.rs | 24 + boa_engine/src/builtins/options.rs | 47 + .../src/builtins/temporal/calendar/iso.rs | 367 +++ .../src/builtins/temporal/calendar/mod.rs | 2303 +++++++++++++++++ .../src/builtins/temporal/calendar/tests.rs | 22 + .../src/builtins/temporal/calendar/utils.rs | 107 + .../src/builtins/temporal/date_equations.rs | 121 + .../src/builtins/temporal/duration/mod.rs | 1039 ++++++++ .../src/builtins/temporal/duration/record.rs | 1820 +++++++++++++ .../src/builtins/temporal/duration/tests.rs | 27 + boa_engine/src/builtins/temporal/fields.rs | 587 +++++ .../src/builtins/temporal/instant/mod.rs | 784 ++++++ boa_engine/src/builtins/temporal/mod.rs | 660 +++++ boa_engine/src/builtins/temporal/now.rs | 188 ++ boa_engine/src/builtins/temporal/options.rs | 384 +++ .../src/builtins/temporal/plain_date/iso.rs | 236 ++ .../src/builtins/temporal/plain_date/mod.rs | 567 ++++ .../builtins/temporal/plain_date_time/iso.rs | 100 + .../builtins/temporal/plain_date_time/mod.rs | 148 ++ .../builtins/temporal/plain_month_day/mod.rs | 122 + .../src/builtins/temporal/plain_time/mod.rs | 62 + .../builtins/temporal/plain_year_month/mod.rs | 327 +++ boa_engine/src/builtins/temporal/tests.rs | 52 + .../src/builtins/temporal/time_zone/mod.rs | 491 ++++ .../builtins/temporal/zoned_date_time/mod.rs | 133 + boa_engine/src/context/intrinsics.rs | 214 ++ boa_engine/src/object/jsobject.rs | 105 + boa_engine/src/object/mod.rs | 394 ++- boa_engine/src/string/common.rs | 24 + boa_parser/Cargo.toml | 1 + boa_parser/src/error/mod.rs | 1 + boa_parser/src/lib.rs | 2 + .../src/parser/expression/assignment/yield.rs | 2 +- boa_parser/src/parser/mod.rs | 4 + boa_parser/src/temporal/annotations.rs | 205 ++ boa_parser/src/temporal/date_time.rs | 373 +++ boa_parser/src/temporal/duration.rs | 275 ++ boa_parser/src/temporal/grammar.rs | 136 + boa_parser/src/temporal/mod.rs | 348 +++ boa_parser/src/temporal/tests.rs | 190 ++ boa_parser/src/temporal/time.rs | 146 ++ boa_parser/src/temporal/time_zone.rs | 263 ++ 49 files changed, 13531 insertions(+), 7 deletions(-) create mode 100644 boa_ast/src/temporal/mod.rs create mode 100644 boa_engine/src/builtins/temporal/calendar/iso.rs create mode 100644 boa_engine/src/builtins/temporal/calendar/mod.rs create mode 100644 boa_engine/src/builtins/temporal/calendar/tests.rs create mode 100644 boa_engine/src/builtins/temporal/calendar/utils.rs create mode 100644 boa_engine/src/builtins/temporal/date_equations.rs create mode 100644 boa_engine/src/builtins/temporal/duration/mod.rs create mode 100644 boa_engine/src/builtins/temporal/duration/record.rs create mode 100644 boa_engine/src/builtins/temporal/duration/tests.rs create mode 100644 boa_engine/src/builtins/temporal/fields.rs create mode 100644 boa_engine/src/builtins/temporal/instant/mod.rs create mode 100644 boa_engine/src/builtins/temporal/mod.rs create mode 100644 boa_engine/src/builtins/temporal/now.rs create mode 100644 boa_engine/src/builtins/temporal/options.rs create mode 100644 boa_engine/src/builtins/temporal/plain_date/iso.rs create mode 100644 boa_engine/src/builtins/temporal/plain_date/mod.rs create mode 100644 boa_engine/src/builtins/temporal/plain_date_time/iso.rs create mode 100644 boa_engine/src/builtins/temporal/plain_date_time/mod.rs create mode 100644 boa_engine/src/builtins/temporal/plain_month_day/mod.rs create mode 100644 boa_engine/src/builtins/temporal/plain_time/mod.rs create mode 100644 boa_engine/src/builtins/temporal/plain_year_month/mod.rs create mode 100644 boa_engine/src/builtins/temporal/tests.rs create mode 100644 boa_engine/src/builtins/temporal/time_zone/mod.rs create mode 100644 boa_engine/src/builtins/temporal/zoned_date_time/mod.rs create mode 100644 boa_parser/src/temporal/annotations.rs create mode 100644 boa_parser/src/temporal/date_time.rs create mode 100644 boa_parser/src/temporal/duration.rs create mode 100644 boa_parser/src/temporal/grammar.rs create mode 100644 boa_parser/src/temporal/mod.rs create mode 100644 boa_parser/src/temporal/tests.rs create mode 100644 boa_parser/src/temporal/time.rs create mode 100644 boa_parser/src/temporal/time_zone.rs diff --git a/boa_ast/Cargo.toml b/boa_ast/Cargo.toml index 9f9b3d1752c..867c159b9cb 100644 --- a/boa_ast/Cargo.toml +++ b/boa_ast/Cargo.toml @@ -13,6 +13,7 @@ rust-version.workspace = true [features] serde = ["dep:serde", "boa_interner/serde", "bitflags/serde", "num-bigint/serde"] arbitrary = ["dep:arbitrary", "boa_interner/arbitrary", "num-bigint/arbitrary"] +experimental = [] [dependencies] boa_interner.workspace = true diff --git a/boa_ast/src/lib.rs b/boa_ast/src/lib.rs index 921eea03922..ad6d3770e05 100644 --- a/boa_ast/src/lib.rs +++ b/boa_ast/src/lib.rs @@ -91,6 +91,8 @@ pub mod operations; pub mod pattern; pub mod property; pub mod statement; +#[cfg(feature = "experimental")] +pub mod temporal; pub mod visitor; use boa_interner::{Interner, ToIndentedString, ToInternedString}; diff --git a/boa_ast/src/temporal/mod.rs b/boa_ast/src/temporal/mod.rs new file mode 100644 index 00000000000..a5bc0172de3 --- /dev/null +++ b/boa_ast/src/temporal/mod.rs @@ -0,0 +1,113 @@ +//! AST nodes for Temporal's implementation of ISO8601 grammar. + +/// An ISO Date Node consisting of non-validated date fields and calendar value. +#[derive(Default, Debug)] +pub struct IsoDate { + /// Date Year + pub year: i32, + /// Date Month + pub month: i32, + /// Date Day + pub day: i32, + /// The calendar value. + pub calendar: Option, +} + +/// The `IsoTime` node consists of non-validated time fields. +#[derive(Default, Debug, Clone, Copy)] +pub struct IsoTime { + /// An hour value between 0-23 + pub hour: u8, + /// A minute value between 0-59 + pub minute: u8, + /// A second value between 0-60 + pub second: u8, + /// A millisecond value between 0-999 + pub millisecond: u16, + /// A microsecond value between 0-999 + pub microsecond: u16, + /// A nanosecond value between 0-999 + pub nanosecond: u16, +} + +impl IsoTime { + #[must_use] + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + /// A utility initialization function to create `ISOTime` from the `TimeSpec` components. + pub fn from_components(hour: u8, minute: u8, second: u8, fraction: f64) -> Self { + // Note: Precision on nanoseconds drifts, so opting for round over floor or ceil for now. + // e.g. 0.329402834 becomes 329.402833.999 + let millisecond = fraction * 1000.0; + let micros = millisecond.rem_euclid(1.0) * 1000.0; + let nanos = micros.rem_euclid(1.0) * 1000.0; + + Self { + hour, + minute, + second, + millisecond: millisecond.floor() as u16, + microsecond: micros.floor() as u16, + nanosecond: nanos.round() as u16, + } + } +} + +/// The `IsoDateTime` node output by the ISO parser +#[derive(Default, Debug)] +pub struct IsoDateTime { + /// The `ISODate` record + pub date: IsoDate, + /// The `ISOTime` record + pub time: IsoTime, + /// The `TimeZone` value for this `ISODateTime` + pub tz: Option, +} + +/// `TimeZone` data +#[derive(Default, Debug, Clone)] +pub struct TimeZone { + /// TimeZoneIANAName + pub name: Option, + /// TimeZoneOffset + pub offset: Option, +} + +/// A full precision `UtcOffset` +#[derive(Debug, Clone, Copy)] +pub struct UTCOffset { + /// The `+`/`-` sign of this `UtcOffset` + pub sign: i8, + /// The hour value of the `UtcOffset` + pub hour: u8, + /// The minute value of the `UtcOffset`. + pub minute: u8, + /// The second value of the `UtcOffset`. + pub second: u8, + /// Any sub second components of the `UTCOffset` + pub fraction: f64, +} + +/// An `IsoDuration` Node output by the ISO parser. +#[derive(Debug, Default, Clone, Copy)] +pub struct IsoDuration { + /// Years value. + pub years: i32, + /// Months value. + pub months: i32, + /// Weeks value. + pub weeks: i32, + /// Days value. + pub days: i32, + /// Hours value. + pub hours: i32, + /// Minutes value. + pub minutes: f64, + /// Seconds value. + pub seconds: f64, + /// Milliseconds value. + pub milliseconds: f64, + /// Microseconds value. + pub microseconds: f64, + /// Nanoseconds value. + pub nanoseconds: f64, +} diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 6b58a20ef05..d05198fd55b 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -47,7 +47,7 @@ trace = [] annex-b = ["boa_parser/annex-b"] # Enable experimental features, like Stage 3 proposals. -experimental = [] +experimental = ["boa_parser/experimental", "dep:icu_calendar"] [dependencies] boa_interner.workspace = true diff --git a/boa_engine/src/bigint.rs b/boa_engine/src/bigint.rs index fa087f7c925..f5e29728320 100644 --- a/boa_engine/src/bigint.rs +++ b/boa_engine/src/bigint.rs @@ -225,6 +225,17 @@ impl JsBigInt { Self::new(x.inner.as_ref().clone().add(y.inner.as_ref())) } + /// Utility function for performing `+` operation on more than two values. + #[inline] + #[cfg(feature = "experimental")] + pub(crate) fn add_n(values: &[Self]) -> Self { + let mut result = Self::zero(); + for big_int in values { + result = Self::add(&result, big_int); + } + result + } + /// Performs the `-` operation. #[inline] #[must_use] diff --git a/boa_engine/src/builtins/date/mod.rs b/boa_engine/src/builtins/date/mod.rs index 7c481443e6e..b25ddcc2960 100644 --- a/boa_engine/src/builtins/date/mod.rs +++ b/boa_engine/src/builtins/date/mod.rs @@ -7,7 +7,7 @@ //! [spec]: https://tc39.es/ecma262/#sec-date-objects //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date -mod utils; +pub(crate) mod utils; use utils::{make_date, make_day, make_time, replace_params, time_clip, DateParameters}; #[cfg(test)] diff --git a/boa_engine/src/builtins/date/utils.rs b/boa_engine/src/builtins/date/utils.rs index c0f39cc9c75..0ccd071ecbb 100644 --- a/boa_engine/src/builtins/date/utils.rs +++ b/boa_engine/src/builtins/date/utils.rs @@ -39,7 +39,7 @@ pub(super) const fn day_from_year(year: i64) -> i64 { /// Abstract operation [`MakeTime`][spec]. /// /// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-maketime -pub(super) fn make_time(hour: i64, min: i64, sec: i64, ms: i64) -> Option { +pub(crate) fn make_time(hour: i64, min: i64, sec: i64, ms: i64) -> Option { // 1. If hour is not finite or min is not finite or sec is not finite or ms is not finite, return NaN. // 2. Let h be 𝔽(! ToIntegerOrInfinity(hour)). // 3. Let m be 𝔽(! ToIntegerOrInfinity(min)). @@ -59,7 +59,7 @@ pub(super) fn make_time(hour: i64, min: i64, sec: i64, ms: i64) -> Option { /// Abstract operation [`MakeDay`][spec]. /// /// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-makeday -pub(super) fn make_day(mut year: i64, mut month: i64, date: i64) -> Option { +pub(crate) fn make_day(mut year: i64, mut month: i64, date: i64) -> Option { // 1. If year is not finite or month is not finite or date is not finite, return NaN. // 2. Let y be 𝔽(! ToIntegerOrInfinity(year)). // 3. Let m be 𝔽(! ToIntegerOrInfinity(month)). @@ -101,7 +101,7 @@ pub(super) fn make_day(mut year: i64, mut month: i64, date: i64) -> Option /// Abstract operation [`MakeDate`][spec]. /// /// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-makedate -pub(super) fn make_date(day: i64, time: i64) -> Option { +pub(crate) fn make_date(day: i64, time: i64) -> Option { // 1. If day is not finite or time is not finite, return NaN. // 2. Let tv be day × msPerDay + time. // 3. If tv is not finite, return NaN. diff --git a/boa_engine/src/builtins/mod.rs b/boa_engine/src/builtins/mod.rs index 5e0d9df2ec5..c05907e7a35 100644 --- a/boa_engine/src/builtins/mod.rs +++ b/boa_engine/src/builtins/mod.rs @@ -43,6 +43,9 @@ pub mod intl; #[cfg(any(feature = "intl", feature = "experimental"))] pub(crate) mod options; +#[cfg(feature = "experimental")] +pub mod temporal; + pub(crate) use self::{ array::Array, async_function::AsyncFunction, @@ -275,6 +278,22 @@ impl Realm { intl::segmenter::SegmentIterator::init(self); intl::PluralRules::init(self); } + + #[cfg(feature = "experimental")] + { + temporal::TimeZone::init(self); + temporal::Temporal::init(self); + temporal::Now::init(self); + temporal::Instant::init(self); + temporal::Duration::init(self); + temporal::PlainDate::init(self); + temporal::PlainTime::init(self); + temporal::PlainDateTime::init(self); + temporal::PlainMonthDay::init(self); + temporal::PlainYearMonth::init(self); + temporal::ZonedDateTime::init(self); + temporal::Calendar::init(self); + } } } @@ -374,6 +393,11 @@ pub(crate) fn set_default_global_bindings(context: &mut Context<'_>) -> JsResult #[cfg(feature = "intl")] global_binding::(context)?; + #[cfg(feature = "experimental")] + { + global_binding::(context)?; + } + Ok(()) } diff --git a/boa_engine/src/builtins/options.rs b/boa_engine/src/builtins/options.rs index 5d2bf962c1f..004682246ec 100644 --- a/boa_engine/src/builtins/options.rs +++ b/boa_engine/src/builtins/options.rs @@ -124,6 +124,53 @@ pub(crate) enum RoundingMode { HalfEven, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum UnsignedRoundingMode { + Infinity, + Zero, + HalfInfinity, + HalfZero, + HalfEven, +} + +impl RoundingMode { + pub(crate) const fn negate(self) -> Self { + use RoundingMode::{ + Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc, + }; + + match self { + Ceil => Self::Floor, + Floor => Self::Ceil, + HalfCeil => Self::HalfFloor, + HalfFloor => Self::HalfCeil, + Trunc => Self::Trunc, + Expand => Self::Expand, + HalfTrunc => Self::HalfTrunc, + HalfExpand => Self::HalfExpand, + HalfEven => Self::HalfEven, + } + } + + pub(crate) const fn get_unsigned_round_mode(self, is_negative: bool) -> UnsignedRoundingMode { + use RoundingMode::{ + Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc, + }; + + match self { + Ceil if !is_negative => UnsignedRoundingMode::Infinity, + Ceil => UnsignedRoundingMode::Zero, + Floor if !is_negative => UnsignedRoundingMode::Zero, + Floor | Trunc | Expand => UnsignedRoundingMode::Infinity, + HalfCeil if !is_negative => UnsignedRoundingMode::HalfInfinity, + HalfCeil | HalfTrunc => UnsignedRoundingMode::HalfZero, + HalfFloor if !is_negative => UnsignedRoundingMode::HalfZero, + HalfFloor | HalfExpand => UnsignedRoundingMode::HalfInfinity, + HalfEven => UnsignedRoundingMode::HalfEven, + } + } +} + #[derive(Debug)] pub(crate) struct ParseRoundingModeError; diff --git a/boa_engine/src/builtins/temporal/calendar/iso.rs b/boa_engine/src/builtins/temporal/calendar/iso.rs new file mode 100644 index 00000000000..4074aa3fa97 --- /dev/null +++ b/boa_engine/src/builtins/temporal/calendar/iso.rs @@ -0,0 +1,367 @@ +//! Implementation of the "iso8601" `BuiltinCalendar`. + +use crate::{ + builtins::temporal::{ + self, create_temporal_date, + date_equations::mathematical_days_in_year, + options::{ArithmeticOverflow, TemporalUnit}, + plain_date::iso::IsoDateRecord, + }, + js_string, + property::PropertyKey, + string::utf16, + Context, JsNativeError, JsResult, JsString, JsValue, +}; + +use super::BuiltinCalendar; + +use icu_calendar::{ + iso::Iso, + week::{RelativeUnit, WeekCalculator}, + Calendar, Date, +}; + +pub(crate) struct IsoCalendar; + +impl BuiltinCalendar for IsoCalendar { + /// Temporal 15.8.2.1 `Temporal.prototype.dateFromFields( fields [, options])` - Supercedes 12.5.4 + /// + /// This is a basic implementation for an iso8601 calendar's `dateFromFields` method. + fn date_from_fields( + &self, + fields: &mut temporal::TemporalFields, + overflow: ArithmeticOverflow, + context: &mut Context<'_>, + ) -> JsResult { + // NOTE: we are in ISO by default here. + // a. Perform ? ISOResolveMonth(fields). + // b. Let result be ? ISODateFromFields(fields, overflow). + fields.iso_resolve_month()?; + + // Extra: handle reulating/overflow until implemented on `icu_calendar` + fields.regulate(overflow)?; + + let date = Date::try_new_iso_date( + fields.year().unwrap_or(0), + fields.month().unwrap_or(250) as u8, + fields.day().unwrap_or(250) as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + // 9. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601"). + Ok(create_temporal_date( + IsoDateRecord::from_date_iso(date), + js_string!("iso8601").into(), + None, + context, + )? + .into()) + } + + /// 12.5.5 `Temporal.Calendar.prototype.yearMonthFromFields ( fields [ , options ] )` + /// + /// This is a basic implementation for an iso8601 calendar's `yearMonthFromFields` method. + fn year_month_from_fields( + &self, + fields: &mut temporal::TemporalFields, + overflow: ArithmeticOverflow, + context: &mut Context<'_>, + ) -> JsResult { + // 9. If calendar.[[Identifier]] is "iso8601", then + // a. Perform ? ISOResolveMonth(fields). + fields.iso_resolve_month()?; + + // b. Let result be ? ISOYearMonthFromFields(fields, overflow). + fields.regulate_year_month(overflow); + + let result = Date::try_new_iso_date( + fields.year().unwrap_or(0), + fields.month().unwrap_or(250) as u8, + fields.day().unwrap_or(20) as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + // 10. Return ? CreateTemporalYearMonth(result.[[Year]], result.[[Month]], "iso8601", result.[[ReferenceISODay]]). + temporal::create_temporal_year_month( + IsoDateRecord::from_date_iso(result), + js_string!("iso8601").into(), + None, + context, + ) + } + + /// 12.5.6 `Temporal.Calendar.prototype.monthDayFromFields ( fields [ , options ] )` + /// + /// This is a basic implementation for an iso8601 calendar's `monthDayFromFields` method. + fn month_day_from_fields( + &self, + fields: &mut temporal::TemporalFields, + overflow: ArithmeticOverflow, + context: &mut Context<'_>, + ) -> JsResult { + // 8. Perform ? ISOResolveMonth(fields). + fields.iso_resolve_month()?; + + fields.regulate(overflow)?; + + // TODO: double check error mapping is correct for specifcation/test262. + // 9. Let result be ? ISOMonthDayFromFields(fields, overflow). + let result = Date::try_new_iso_date( + fields.year().unwrap_or(1972), + fields.month().unwrap_or(250) as u8, + fields.day().unwrap_or(250) as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + // 10. Return ? CreateTemporalMonthDay(result.[[Month]], result.[[Day]], "iso8601", result.[[ReferenceISOYear]]). + temporal::create_temporal_month_day( + IsoDateRecord::from_date_iso(result), + js_string!("iso8601").into(), + None, + context, + ) + } + + /// 12.5.7 `Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] )` + /// + /// Below implements the basic implementation for an iso8601 calendar's `dateAdd` method. + fn date_add( + &self, + _date: &temporal::PlainDate, + _duration: &temporal::duration::DurationRecord, + _overflow: ArithmeticOverflow, + _context: &mut Context<'_>, + ) -> JsResult { + // TODO: Not stable on `ICU4X`. Implement once completed. + Err(JsNativeError::range() + .with_message("feature not implemented.") + .into()) + + // 9. Let result be ? AddISODate(date.[[ISOYear]], date.[[ISOMonth]], date.[[ISODay]], duration.[[Years]], duration.[[Months]], duration.[[Weeks]], balanceResult.[[Days]], overflow). + // 10. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601"). + } + + /// 12.5.8 `Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] )` + /// + /// Below implements the basic implementation for an iso8601 calendar's `dateUntil` method. + fn date_until( + &self, + _one: &temporal::PlainDate, + _two: &temporal::PlainDate, + _largest_unit: TemporalUnit, + _: &mut Context<'_>, + ) -> JsResult { + // TODO: Not stable on `ICU4X`. Implement once completed. + Err(JsNativeError::range() + .with_message("Feature not yet implemented.") + .into()) + + // 9. Let result be DifferenceISODate(one.[[ISOYear]], one.[[ISOMonth]], one.[[ISODay]], two.[[ISOYear]], two.[[ISOMonth]], two.[[ISODay]], largestUnit). + // 10. Return ! CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], 0, 0, 0, 0, 0, 0). + } + + /// `Temporal.Calendar.prototype.era( dateLike )` for iso8601 calendar. + fn era(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + // Returns undefined on iso8601. + Ok(JsValue::undefined()) + } + + /// `Temporal.Calendar.prototype.eraYear( dateLike )` for iso8601 calendar. + fn era_year(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + // Returns undefined on iso8601. + Ok(JsValue::undefined()) + } + + /// Returns the `year` for the `Iso` calendar. + fn year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + let date = Date::try_new_iso_date( + date_like.year(), + date_like.month() as u8, + date_like.day() as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + Ok(date.year().number.into()) + } + + /// Returns the `month` for the `Iso` calendar. + fn month(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + let date = Date::try_new_iso_date( + date_like.year(), + date_like.month() as u8, + date_like.day() as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + Ok(date.month().ordinal.into()) + } + + /// Returns the `monthCode` for the `Iso` calendar. + fn month_code(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + let date = Date::try_new_iso_date( + date_like.year(), + date_like.month() as u8, + date_like.day() as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + Ok(JsString::from(date.month().code.to_string()).into()) + } + + /// Returns the `day` for the `Iso` calendar. + fn day(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + let date = Date::try_new_iso_date( + date_like.year(), + date_like.month() as u8, + date_like.day() as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + Ok(date.day_of_month().0.into()) + } + + /// Returns the `dayOfWeek` for the `Iso` calendar. + fn day_of_week(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + let date = Date::try_new_iso_date( + date_like.year(), + date_like.month() as u8, + date_like.day() as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + Ok((date.day_of_week() as u8).into()) + } + + /// Returns the `dayOfYear` for the `Iso` calendar. + fn day_of_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + let date = Date::try_new_iso_date( + date_like.year(), + date_like.month() as u8, + date_like.day() as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + Ok(i32::from(date.day_of_year_info().day_of_year).into()) + } + + /// Returns the `weekOfYear` for the `Iso` calendar. + fn week_of_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + // TODO: Determine `ICU4X` equivalent. + let date = Date::try_new_iso_date( + date_like.year(), + date_like.month() as u8, + date_like.day() as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + let week_calculator = WeekCalculator::default(); + + let week_of = date + .week_of_year(&week_calculator) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + Ok(week_of.week.into()) + } + + /// Returns the `yearOfWeek` for the `Iso` calendar. + fn year_of_week(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + // TODO: Determine `ICU4X` equivalent. + let date = Date::try_new_iso_date( + date_like.year(), + date_like.month() as u8, + date_like.day() as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + let week_calculator = WeekCalculator::default(); + + let week_of = date + .week_of_year(&week_calculator) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + match week_of.unit { + RelativeUnit::Previous => Ok((date.year().number - 1).into()), + RelativeUnit::Current => Ok(date.year().number.into()), + RelativeUnit::Next => Ok((date.year().number + 1).into()), + } + } + + /// Returns the `daysInWeek` value for the `Iso` calendar. + fn days_in_week(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + Ok(7.into()) + } + + /// Returns the `daysInMonth` value for the `Iso` calendar. + fn days_in_month(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + let date = Date::try_new_iso_date( + date_like.year(), + date_like.month() as u8, + date_like.day() as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + Ok(date.days_in_month().into()) + } + + /// Returns the `daysInYear` value for the `Iso` calendar. + fn days_in_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + let date = Date::try_new_iso_date( + date_like.year(), + date_like.month() as u8, + date_like.day() as u8, + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + Ok(date.days_in_year().into()) + } + + /// Return the amount of months in an ISO Calendar. + fn months_in_year(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + Ok(12.into()) + } + + /// Returns whether provided date is in a leap year according to this calendar. + fn in_leap_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult { + // `ICU4X`'s `CalendarArithmetic` is currently private. + if mathematical_days_in_year(date_like.year()) == 366 { + return Ok(true.into()); + } + Ok(false.into()) + } + + // Resolve the fields for the iso calendar. + fn resolve_fields(&self, fields: &mut temporal::TemporalFields, _: &str) -> JsResult<()> { + fields.iso_resolve_month()?; + Ok(()) + } + + /// Returns the ISO field descriptors, which is not called for the iso8601 calendar. + fn field_descriptors(&self, _: &[String]) -> Vec<(String, bool)> { + // NOTE(potential improvement): look into implementing field descriptors and call + // ISO like any other calendar? + // Field descriptors is unused on ISO8601. + unreachable!() + } + + /// Returns the `CalendarFieldKeysToIgnore` implementation for ISO. + fn field_keys_to_ignore(&self, additional_keys: Vec) -> Vec { + let mut result = Vec::new(); + for key in additional_keys { + let key_string = key.to_string(); + result.push(key); + if key_string.as_str() == "month" { + result.push(utf16!("monthCode").into()); + } else if key_string.as_str() == "monthCode" { + result.push(utf16!("month").into()); + } + } + result + } + + // NOTE: This is currently not a name that is compliant with + // the Temporal proposal. For debugging purposes only. + /// Returns the debug name. + fn debug_name(&self) -> &str { + Iso.debug_name() + } +} diff --git a/boa_engine/src/builtins/temporal/calendar/mod.rs b/boa_engine/src/builtins/temporal/calendar/mod.rs new file mode 100644 index 00000000000..d58f35f6807 --- /dev/null +++ b/boa_engine/src/builtins/temporal/calendar/mod.rs @@ -0,0 +1,2303 @@ +//! An implementation of the `Temporal` proposal's Calendar builtin. + +use self::iso::IsoCalendar; + +use super::{ + options::{ArithmeticOverflow, TemporalUnit, TemporalUnitGroup}, + plain_date::iso::IsoDateRecord, + PlainDate, TemporalFields, +}; +use crate::{ + builtins::{ + iterable::IteratorHint, + options::{get_option, get_options_object}, + temporal, Array, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + }, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + js_string, + object::{internal_methods::get_prototype_from_constructor, ObjectData}, + property::{Attribute, PropertyKey}, + realm::Realm, + string::{common::StaticJsStrings, utf16}, + Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; +use rustc_hash::FxHashMap; + +mod iso; +pub(crate) mod utils; + +#[cfg(test)] +mod tests; + +// TODO: Determine how many methods actually need the context on them while using +// `icu_calendar`. +// +// NOTE (re above's TODO): Most likely context is only going to be needed for `dateFromFields`, +// `yearMonthFromFields`, `monthDayFromFields`, `dateAdd`, and `dateUntil`. +/// A trait for implementing a Builtin Calendar's Calendar Protocol in Rust. +pub(crate) trait BuiltinCalendar { + /// Creates a `Temporal.PlainDate` object from provided fields. + fn date_from_fields( + &self, + fields: &mut TemporalFields, + overflow: ArithmeticOverflow, + context: &mut Context<'_>, + ) -> JsResult; + /// Creates a `Temporal.PlainYearMonth` object from the provided fields. + fn year_month_from_fields( + &self, + fields: &mut TemporalFields, + overflow: ArithmeticOverflow, + context: &mut Context<'_>, + ) -> JsResult; + /// Creates a `Temporal.PlainMonthDay` object from the provided fields. + fn month_day_from_fields( + &self, + fields: &mut TemporalFields, + overflow: ArithmeticOverflow, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns a `Temporal.PlainDate` based off an added date. + fn date_add( + &self, + date: &PlainDate, + duration: &temporal::DurationRecord, + overflow: ArithmeticOverflow, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns a `Temporal.Duration` representing the duration between two dates. + fn date_until( + &self, + one: &PlainDate, + two: &PlainDate, + largest_unit: TemporalUnit, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns the era for a given `temporaldatelike`. + fn era(&self, date_like: &IsoDateRecord, context: &mut Context<'_>) -> JsResult; + /// Returns the era year for a given `temporaldatelike` + fn era_year(&self, date_like: &IsoDateRecord, context: &mut Context<'_>) -> JsResult; + /// Returns the `year` for a given `temporaldatelike` + fn year(&self, date_like: &IsoDateRecord, context: &mut Context<'_>) -> JsResult; + /// Returns the `month` for a given `temporaldatelike` + fn month(&self, date_like: &IsoDateRecord, context: &mut Context<'_>) -> JsResult; + /// Returns the `monthCode` for a given `temporaldatelike` + fn month_code(&self, date_like: &IsoDateRecord, context: &mut Context<'_>) + -> JsResult; + /// Returns the `day` for a given `temporaldatelike` + fn day(&self, date_like: &IsoDateRecord, context: &mut Context<'_>) -> JsResult; + /// Returns a value representing the day of the week for a date. + fn day_of_week( + &self, + date_like: &IsoDateRecord, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns a value representing the day of the year for a given calendar. + fn day_of_year( + &self, + date_like: &IsoDateRecord, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns a value representing the week of the year for a given calendar. + fn week_of_year( + &self, + date_like: &IsoDateRecord, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns the year of a given week. + fn year_of_week( + &self, + date_like: &IsoDateRecord, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns the days in a week for a given calendar. + fn days_in_week( + &self, + date_like: &IsoDateRecord, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns the days in a month for a given calendar. + fn days_in_month( + &self, + date_like: &IsoDateRecord, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns the days in a year for a given calendar. + fn days_in_year( + &self, + date_like: &IsoDateRecord, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns the months in a year for a given calendar. + fn months_in_year( + &self, + date_like: &IsoDateRecord, + context: &mut Context<'_>, + ) -> JsResult; + /// Returns whether a value is within a leap year according to the designated calendar. + fn in_leap_year( + &self, + date_like: &IsoDateRecord, + context: &mut Context<'_>, + ) -> JsResult; + /// Resolve the `TemporalFields` for the implemented Calendar + fn resolve_fields(&self, fields: &mut TemporalFields, r#type: &str) -> JsResult<()>; + /// Return this calendar's a fieldName and whether it is required depending on type (date, day-month). + fn field_descriptors(&self, r#type: &[String]) -> Vec<(String, bool)>; + /// Return the fields to ignore for this Calendar based on provided keys. + fn field_keys_to_ignore(&self, additional_keys: Vec) -> Vec; + /// Debug name + fn debug_name(&self) -> &str; +} + +impl core::fmt::Debug for dyn BuiltinCalendar { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.debug_name()) + } +} + +// ==== Calendar Abstractions ==== + +const ISO: &[u16] = utf16!("iso8601"); + +// NOTE: potentially move these to `Realm`, so that there can be +// host defined calendars. +// Returns a map of all available calendars. +fn available_calendars() -> FxHashMap<&'static [u16], Box> { + let mut map = FxHashMap::default(); + let iso: Box = Box::new(IsoCalendar); + map.insert(ISO, iso); + + map +} + +// Returns if an identifier is a builtin calendar. +pub(crate) fn is_builtin_calendar(identifier: &JsString) -> bool { + let calendars = available_calendars(); + // TODO: Potentially implement `to_ascii_lowercase`. + calendars.contains_key(identifier.as_slice()) +} + +/// The `Temporal.Calendar` object. +#[derive(Debug)] +pub struct Calendar { + identifier: JsString, +} + +impl BuiltInObject for Calendar { + const NAME: JsString = StaticJsStrings::CALENDAR; +} + +impl IntrinsicObject for Calendar { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + let get_id = BuiltInBuilder::callable(realm, Self::get_id) + .name(js_string!("get Id")) + .build(); + + BuiltInBuilder::from_standard_constructor::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::CONFIGURABLE, + ) + .accessor(utf16!("id"), Some(get_id), None, Attribute::default()) + .method(Self::date_from_fields, js_string!("dateFromFields"), 2) + .method( + Self::year_month_from_fields, + js_string!("yearMonthFromFields"), + 2, + ) + .method( + Self::month_day_from_fields, + js_string!("monthDayFromFields"), + 2, + ) + .method(Self::date_add, js_string!("dateAdd"), 3) + .method(Self::date_until, js_string!("dateUntil"), 3) + .method(Self::era, js_string!("era"), 1) + .method(Self::era_year, js_string!("eraYear"), 1) + .method(Self::year, js_string!("year"), 1) + .method(Self::month, js_string!("month"), 1) + .method(Self::month_code, js_string!("monthCode"), 1) + .method(Self::day, js_string!("day"), 1) + .method(Self::day_of_week, js_string!("dayOfWeek"), 1) + .method(Self::day_of_year, js_string!("dayOfYear"), 1) + .method(Self::week_of_year, js_string!("weekOfYear"), 1) + .method(Self::year_of_week, js_string!("yearOfWeek"), 1) + .method(Self::days_in_week, js_string!("daysInWeek"), 1) + .method(Self::days_in_month, js_string!("daysInMonth"), 1) + .method(Self::days_in_year, js_string!("daysInYear"), 1) + .method(Self::months_in_year, js_string!("monthsInYear"), 1) + .method(Self::in_leap_year, js_string!("inLeapYear"), 1) + .method(Self::fields, js_string!("fields"), 1) + .method(Self::merge_fields, js_string!("mergeFields"), 2) + .method(Self::get_id, js_string!("toString"), 0) + .method(Self::get_id, js_string!("toJSON"), 0) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInConstructor for Calendar { + const LENGTH: usize = 1; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::calendar; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. If NewTarget is undefined, then + if new_target.is_undefined() { + // a. Throw a TypeError exception. + return Err(JsNativeError::typ() + .with_message( + "newTarget cannot be undefined when constructing a Temporal.Calendar object.", + ) + .into()); + } + + let identifier = args.get_or_undefined(0); + + // 2. If id is not a String, throw a TypeError exception. + let JsValue::String(id) = identifier else { + return Err(JsNativeError::typ() + .with_message("Calendar id must be a string.") + .into()); + }; + + // 3. If IsBuiltinCalendar(id) is false, then + if !is_builtin_calendar(id) { + // a. Throw a RangeError exception. + return Err(JsNativeError::range() + .with_message("Calendar ID must be a valid builtin calendar.") + .into()); + } + + // 4. Return ? CreateTemporalCalendar(id, NewTarget). + create_temporal_calendar(id, Some(new_target.clone()), context) + } +} + +impl Calendar { + fn get_id(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + let o = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let o = o.borrow(); + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + Ok(calendar.identifier.clone().into()) + } + + /// 15.8.2.1 `Temporal.Calendar.prototype.dateFromFields ( fields [ , options ] )` - Supercedes 12.5.4 + fn date_from_fields( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let calendar be the this value. + // 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + // Retrieve the current CalendarProtocol. + let available_calendars = available_calendars(); + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + // 3. If Type(fields) is not Object, throw a TypeError exception. + let fields = args.get_or_undefined(0); + let fields_obj = fields.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("fields parameter must be an object.") + })?; + + // 4. Set options to ? GetOptionsObject(options). + let options = get_options_object(args.get_or_undefined(1))?; + + // 5. Let relevantFieldNames be « "day", "month", "monthCode", "year" ». + let mut relevant_field_names = Vec::from([ + "day".to_owned(), + "month".to_owned(), + "monthCode".to_owned(), + "year".to_owned(), + ]); + + // 6. If calendar.[[Identifier]] is "iso8601", then + let mut fields = if calendar.identifier.as_slice() == ISO { + // a. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « "year", "day" »). + let mut required_fields = Vec::from(["year".to_owned(), "day".to_owned()]); + temporal::TemporalFields::from_js_object( + fields_obj, + &mut relevant_field_names, + &mut required_fields, + None, + false, + None, + context, + )? + // 7. Else, + } else { + // a. Let calendarRelevantFieldDescriptors be CalendarFieldDescriptors(calendar.[[Identifier]], date). + let calendar_relevant_fields = this_calendar.field_descriptors(&["date".to_owned()]); + // b. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « », calendarRelevantFieldDescriptors). + temporal::TemporalFields::from_js_object( + fields_obj, + &mut relevant_field_names, + &mut Vec::new(), + Some(calendar_relevant_fields), + false, + None, + context, + )? + }; + + // 8. Let overflow be ? ToTemporalOverflow(options). + let overflow = get_option(&options, utf16!("overflow"), context)? + .unwrap_or(ArithmeticOverflow::Constrain); + + // NOTE: implement the below on the calenar itself + // 9. If calendar.[[Identifier]] is "iso8601", then + // a. Perform ? ISOResolveMonth(fields). + // b. Let result be ? ISODateFromFields(fields, overflow). + // 10. Else, + // a. Perform ? CalendarResolveFields(calendar.[[Identifier]], fields, date). + // b. Let result be ? CalendarDateToISO(calendar.[[Identifier]], fields, overflow). + + this_calendar.date_from_fields(&mut fields, overflow, context) + } + + /// 15.8.2.2 `Temporal.Calendar.prototype.yearMonthFromFields ( fields [ , options ] )` - Supercedes 12.5.5 + fn year_month_from_fields( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + let fields = args.get_or_undefined(0); + let fields_obj = fields.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("fields parameter must be an object.") + })?; + + // 5. Set options to ? GetOptionsObject(options). + let options = get_options_object(args.get_or_undefined(1))?; + + let mut relevant_field_names = Vec::from([ + "year".to_owned(), + "month".to_owned(), + "monthCode".to_owned(), + ]); + + // 6. Set fields to ? PrepareTemporalFields(fields, « "month", "monthCode", "year" », « "year" »). + let mut fields = if calendar.identifier.as_slice() == ISO { + // a. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « "year" »). + let mut required_fields = Vec::from(["year".to_owned()]); + temporal::TemporalFields::from_js_object( + fields_obj, + &mut relevant_field_names, + &mut required_fields, + None, + false, + None, + context, + )? + } else { + // a. Let calendarRelevantFieldDescriptors be CalendarFieldDescriptors(calendar.[[Identifier]], year-month). + // b. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « », calendarRelevantFieldDescriptors). + + let calendar_relevant_fields = + this_calendar.field_descriptors(&["year-month".to_owned()]); + temporal::TemporalFields::from_js_object( + fields_obj, + &mut relevant_field_names, + &mut Vec::new(), + Some(calendar_relevant_fields), + false, + None, + context, + )? + // TODO: figure out the below. Maybe a method on fields? + // c. Let firstDayIndex be the 1-based index of the first day of the month described by fields (i.e., 1 unless the month's first day is skipped by this calendar.) + // d. Perform ! CreateDataPropertyOrThrow(fields, "day", 𝔽(firstDayIndex)). + }; + + // 7. Let overflow be ? ToTemporalOverflow(options). + let overflow = get_option::(&options, utf16!("overflow"), context)? + .unwrap_or(ArithmeticOverflow::Constrain); + + this_calendar.year_month_from_fields(&mut fields, overflow, context) + } + + /// 15.8.2.3 `Temporal.Calendar.prototype.monthDayFromFields ( fields [ , options ] )` - Supercedes 12.5.6 + fn month_day_from_fields( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let calendar be the this value. + // 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + // 3. If Type(fields) is not Object, throw a TypeError exception. + let fields = args.get_or_undefined(0); + let fields_obj = fields.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("fields parameter must be an object.") + })?; + + // 4. Set options to ? GetOptionsObject(options). + let options = get_options_object(args.get_or_undefined(1))?; + + // 5. Let relevantFieldNames be « "day", "month", "monthCode", "year" ». + let mut relevant_field_names = Vec::from([ + "day".to_owned(), + "month".to_owned(), + "monthCode".to_owned(), + "year".to_owned(), + ]); + + // 6. If calendar.[[Identifier]] is "iso8601", then + let mut fields = if calendar.identifier.as_slice() == ISO { + // a. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « "day" »). + let mut required_fields = Vec::from(["day".to_owned()]); + temporal::TemporalFields::from_js_object( + fields_obj, + &mut relevant_field_names, + &mut required_fields, + None, + false, + None, + context, + )? + // 7. Else, + } else { + // a. Let calendarRelevantFieldDescriptors be CalendarFieldDescriptors(calendar.[[Identifier]], month-day). + let calendar_relevant_fields = + this_calendar.field_descriptors(&["month-day".to_owned()]); + // b. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « », calendarRelevantFieldDescriptors). + temporal::TemporalFields::from_js_object( + fields_obj, + &mut relevant_field_names, + &mut Vec::new(), + Some(calendar_relevant_fields), + false, + None, + context, + )? + }; + + // 8. Let overflow be ? ToTemporalOverflow(options). + let overflow = get_option(&options, utf16!("overflow"), context)? + .unwrap_or(ArithmeticOverflow::Constrain); + + this_calendar.month_day_from_fields(&mut fields, overflow, context) + } + + /// 15.8.2.4 `Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] )` - supercedes 12.5.7 + fn date_add(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + // 1. Let calendar be the this value. + // 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]). + // 3. Assert: calendar.[[Identifier]] is "iso8601". + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + // 4. Set date to ? ToTemporalDate(date). + let date_like = args.get_or_undefined(0); + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + + // 5. Set duration to ? ToTemporalDuration(duration). + let duration_like = args.get_or_undefined(1); + let mut duration = temporal::duration::to_temporal_duration(duration_like)?; + + // 6. Set options to ? GetOptionsObject(options). + let options = args.get_or_undefined(2); + let options_obj = get_options_object(options)?; + + // 7. Let overflow be ? ToTemporalOverflow(options). + let overflow = get_option(&options_obj, utf16!("overflow"), context)? + .unwrap_or(ArithmeticOverflow::Constrain); + + // 8. Let balanceResult be ? BalanceTimeDuration(duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], "day"). + duration.balance_time_duration(TemporalUnit::Day, None)?; + + this_calendar.date_add(&date, &duration, overflow, context) + } + + ///15.8.2.5 `Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] )` - Supercedes 12.5.8 + fn date_until( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let calendar be the this value. + // 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]). + // 3. Assert: calendar.[[Identifier]] is "iso8601". + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + // 4. Set one to ? ToTemporalDate(one). + let one = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; + // 5. Set two to ? ToTemporalDate(two). + let two = temporal::plain_date::to_temporal_date(args.get_or_undefined(1), None, context)?; + + // 6. Set options to ? GetOptionsObject(options). + let options = get_options_object(args.get_or_undefined(2))?; + + // 7. Let largestUnit be ? GetTemporalUnit(options, "largestUnit", date, "auto"). + // 8. If largestUnit is "auto", set largestUnit to "day". + let largest_unit = super::options::get_temporal_unit( + &options, + utf16!("largestUnit"), + TemporalUnitGroup::Date, + None, + context, + )? + .unwrap_or(TemporalUnit::Day); + + this_calendar.date_until(&one, &two, largest_unit, context) + } + + /// 15.8.2.6 `Temporal.Calendar.prototype.era ( temporalDateLike )` + fn era(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let date_like = args.get_or_undefined(0); + + let date_info = match date_like { + JsValue::Object(o) if o.is_plain_date_time() => { + let obj = o.borrow(); + let date_time = obj.as_plain_date_time().expect("obj must be a DateTime."); + + date_time.inner.iso_date() + } + JsValue::Object(o) if o.is_plain_date() => { + let obj = o.borrow(); + let date = obj.as_plain_date().expect("Must be a Date"); + + date.inner + } + JsValue::Object(o) if o.is_plain_year_month() => { + let obj = o.borrow(); + let ym = obj.as_plain_year_month().expect("must be a YearMonth."); + + ym.inner + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + date.inner + } + }; + + this_calendar.era(&date_info, context) + } + + /// 15.8.2.7 `Temporal.Calendar.prototype.eraYear ( temporalDateLike )` + fn era_year(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let date_like = args.get_or_undefined(0); + + let date_info = match date_like { + JsValue::Object(o) if o.is_plain_date_time() => { + let obj = o.borrow(); + let date_time = obj.as_plain_date_time().expect("obj must be a DateTime."); + + date_time.inner.iso_date() + } + JsValue::Object(o) if o.is_plain_date() => { + let obj = o.borrow(); + let date = obj.as_plain_date().expect("Must be a Date"); + + date.inner + } + JsValue::Object(o) if o.is_plain_year_month() => { + let obj = o.borrow(); + let ym = obj.as_plain_year_month().expect("must be a YearMonth."); + + ym.inner + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + date.inner + } + }; + + this_calendar.era_year(&date_info, context) + } + + /// 15.8.2.8 `Temporal.Calendar.prototype.year ( temporalDateLike )` + fn year(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let date_like = args.get_or_undefined(0); + + let date_record = match date_like { + JsValue::Object(o) if o.is_plain_date_time() => { + let obj = o.borrow(); + let date_time = obj.as_plain_date_time().expect("obj must be a DateTime."); + + date_time.inner.iso_date() + } + JsValue::Object(o) if o.is_plain_date() => { + let obj = o.borrow(); + let date = obj.as_plain_date().expect("Must be a Date"); + + date.inner + } + JsValue::Object(o) if o.is_plain_year_month() => { + let obj = o.borrow(); + let ym = obj.as_plain_year_month().expect("must be a YearMonth."); + + ym.inner + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + date.inner + } + }; + + this_calendar.year(&date_record, context) + } + + /// 15.8.2.9 `Temporal.Calendar.prototype.month ( temporalDateLike )` + fn month(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let date_like = args.get_or_undefined(0); + + // 3. If Type(temporalDateLike) is Object and temporalDateLike has an [[InitializedTemporalMonthDay]] internal slot, then + // 3.a. Throw a TypeError exception. + // 4. If Type(temporalDateLike) is not Object or temporalDateLike does not have an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], or [[InitializedTemporalYearMonth]] internal slot, then + // 4.a. Set temporalDateLike to ? ToTemporalDate(temporalDateLike). + let date_record = match date_like { + JsValue::Object(o) if o.is_plain_date_time() => { + let obj = o.borrow(); + let date_time = obj.as_plain_date_time().expect("obj must be a DateTime."); + + date_time.inner.iso_date() + } + JsValue::Object(o) if o.is_plain_date() => { + let obj = o.borrow(); + let date = obj.as_plain_date().expect("Must be a Date"); + + date.inner + } + JsValue::Object(o) if o.is_plain_year_month() => { + let obj = o.borrow(); + let ym = obj.as_plain_year_month().expect("must be a YearMonth."); + + ym.inner + } + JsValue::Object(o) if o.is_plain_month_day() => { + return Err(JsNativeError::typ() + .with_message("month cannot be called with PlainMonthDay object.") + .into()) + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + date.inner + } + }; + + this_calendar.month(&date_record, context) + } + + /// 15.8.2.10 `Temporal.Calendar.prototype.monthCode ( temporalDateLike )` + fn month_code( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let date_like = args.get_or_undefined(0); + + let date_record = match date_like { + JsValue::Object(o) if o.is_plain_date_time() => { + let obj = o.borrow(); + let date_time = obj.as_plain_date_time().expect("obj must be a DateTime."); + + date_time.inner.iso_date() + } + JsValue::Object(o) if o.is_plain_date() => { + let obj = o.borrow(); + let date = obj.as_plain_date().expect("Must be a Date"); + + date.inner + } + JsValue::Object(o) if o.is_plain_year_month() => { + let obj = o.borrow(); + let ym = obj.as_plain_year_month().expect("must be a YearMonth."); + + ym.inner + } + JsValue::Object(o) if o.is_plain_month_day() => { + let obj = o.borrow(); + let md = obj.as_plain_month_day().expect("must be a MonthDay."); + + md.inner + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + date.inner + } + }; + + this_calendar.month_code(&date_record, context) + } + + /// 15.8.2.11 `Temporal.Calendar.prototype.day ( temporalDateLike )` + fn day(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let date_like = args.get_or_undefined(0); + + let date_record = match date_like { + JsValue::Object(o) if o.is_plain_date_time() => { + let obj = o.borrow(); + let date_time = obj.as_plain_date_time().expect("obj must be a DateTime."); + + date_time.inner.iso_date() + } + JsValue::Object(o) if o.is_plain_date() => { + let obj = o.borrow(); + let date = obj.as_plain_date().expect("Must be a Date"); + + date.inner + } + JsValue::Object(o) if o.is_plain_month_day() => { + let obj = o.borrow(); + let md = obj.as_plain_month_day().expect("must be a MonthDay."); + + md.inner + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + date.inner + } + }; + + this_calendar.day(&date_record, context) + } + + /// 15.8.2.12 `Temporal.Calendar.prototype.dayOfWeek ( dateOrDateTime )` + fn day_of_week( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let calendar be the this value. + // 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + // 3. Let temporalDate be ? ToTemporalDate(temporalDateLike). + let date = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; + + this_calendar.day_of_week(&date.inner, context) + } + + /// 15.8.2.13 `Temporal.Calendar.prototype.dayOfYear ( temporalDateLike )` + fn day_of_year( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let o = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let o = o.borrow(); + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + // 3. Let temporalDate be ? ToTemporalDate(temporalDateLike). + let date = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; + + this_calendar.day_of_year(&date.inner, context) + } + + /// 15.8.2.14 `Temporal.Calendar.prototype.weekOfYear ( temporalDateLike )` + fn week_of_year( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + // 3. Let temporalDate be ? ToTemporalDate(temporalDateLike). + let date = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; + + this_calendar.week_of_year(&date.inner, context) + } + + /// 15.8.2.15 `Temporal.Calendar.prototype.yearOfWeek ( temporalDateLike )` + fn year_of_week( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + // 3. Let temporalDate be ? ToTemporalDate(temporalDateLike). + let date = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; + + this_calendar.year_of_week(&date.inner, context) + } + + /// 15.8.2.16 `Temporal.Calendar.prototype.daysInWeek ( temporalDateLike )` + fn days_in_week( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + // 3. Let temporalDate be ? ToTemporalDate(temporalDateLike). + let date = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; + + this_calendar.days_in_week(&date.inner, context) + } + + /// 15.8.2.17 `Temporal.Calendar.prototype.daysInMonth ( temporalDateLike )` + fn days_in_month( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let date_like = args.get_or_undefined(0); + + let date_record = match date_like { + JsValue::Object(o) if o.is_plain_date_time() => { + let obj = o.borrow(); + let date_time = obj.as_plain_date_time().expect("obj must be a DateTime."); + + date_time.inner.iso_date() + } + JsValue::Object(o) if o.is_plain_date() => { + let obj = o.borrow(); + let date = obj.as_plain_date().expect("Must be a Date"); + + date.inner + } + JsValue::Object(o) if o.is_plain_year_month() => { + let obj = o.borrow(); + let ym = obj.as_plain_year_month().expect("must be a YearMonth."); + + ym.inner + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + date.inner + } + }; + + this_calendar.days_in_month(&date_record, context) + } + + /// 15.8.2.18 `Temporal.Calendar.prototype.daysInYear ( temporalDateLike )` + fn days_in_year( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let date_like = args.get_or_undefined(0); + + let date_record = match date_like { + JsValue::Object(o) if o.is_plain_date_time() => { + let obj = o.borrow(); + let date_time = obj.as_plain_date_time().expect("obj must be a DateTime."); + + date_time.inner.iso_date() + } + JsValue::Object(o) if o.is_plain_date() => { + let obj = o.borrow(); + let date = obj.as_plain_date().expect("Must be a Date"); + + date.inner + } + JsValue::Object(o) if o.is_plain_year_month() => { + let obj = o.borrow(); + let ym = obj.as_plain_year_month().expect("must be a YearMonth."); + + ym.inner + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + date.inner + } + }; + + this_calendar.days_in_year(&date_record, context) + } + + /// 15.8.2.19 `Temporal.Calendar.prototype.monthsInYear ( temporalDateLike )` + fn months_in_year( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let date_like = args.get_or_undefined(0); + + let date_record = match date_like { + JsValue::Object(o) if o.is_plain_date_time() => { + let obj = o.borrow(); + let date_time = obj.as_plain_date_time().expect("obj must be a DateTime."); + + date_time.inner.iso_date() + } + JsValue::Object(o) if o.is_plain_date() => { + let obj = o.borrow(); + let date = obj.as_plain_date().expect("Must be a Date"); + + date.inner + } + JsValue::Object(o) if o.is_plain_year_month() => { + let obj = o.borrow(); + let ym = obj.as_plain_year_month().expect("must be a YearMonth."); + + ym.inner + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + date.inner + } + }; + + this_calendar.months_in_year(&date_record, context) + } + + /// 15.8.2.20 `Temporal.Calendar.prototype.inLeapYear ( temporalDateLike )` + fn in_leap_year( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let date_like = args.get_or_undefined(0); + + let date_record = match date_like { + JsValue::Object(o) if o.is_plain_date_time() => { + let obj = o.borrow(); + let date_time = obj.as_plain_date_time().expect("obj must be a DateTime."); + + date_time.inner.iso_date() + } + JsValue::Object(o) if o.is_plain_date() => { + let obj = o.borrow(); + let date = obj.as_plain_date().expect("Must be a Date"); + + date.inner + } + JsValue::Object(o) if o.is_plain_year_month() => { + let obj = o.borrow(); + let ym = obj.as_plain_year_month().expect("must be a YearMonth."); + + ym.inner + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + date.inner + } + }; + + this_calendar.in_leap_year(&date_record, context) + } + + /// 15.8.2.21 `Temporal.Calendar.prototype.fields ( fields )` + fn fields(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + // 1. Let calendar be the this value. + // 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + // 3. Let iteratorRecord be ? GetIterator(fields, sync). + let mut iterator_record = + args.get_or_undefined(0) + .get_iterator(context, Some(IteratorHint::Sync), None)?; + + // 4. Let fieldNames be a new empty List. + let mut fields_names = Vec::new(); + + // 5. Let next be true. + // 6. Repeat, while next is not false, + while iterator_record.step(context)? { + // a. Set next to ? IteratorStep(iteratorRecord). + // b. If next is not false, then + // i. Let nextValue be ? IteratorValue(next). + let next_value = iterator_record.value(context)?; + + // ii. If Type(nextValue) is not String, then + if let JsValue::String(value) = next_value { + // iii. If fieldNames contains nextValue, then + // 1. Let completion be ThrowCompletion(a newly created RangeError object). + // 2. Return ? IteratorClose(iteratorRecord, completion). + // iv. If nextValue is not one of "year", "month", "monthCode", or "day", then + // 1. Let completion be ThrowCompletion(a newly created RangeError object). + // 2. Return ? IteratorClose(iteratorRecord, completion). + // v. Append nextValue to the end of the List fieldNames. + let this_name = value.to_std_string_escaped(); + match this_name.as_str() { + "year" | "month" | "monthCode" | "day" + if !fields_names.contains(&this_name) => + { + fields_names.push(this_name); + } + _ => { + let completion = Err(JsNativeError::range() + .with_message("Invalid field name string.") + .into()); + return iterator_record.close(completion, context); + } + } + } else { + // 1. Let completion be ThrowCompletion(a newly created TypeError object). + let completion = Err(JsNativeError::typ() + .with_message("field must be of type string") + .into()); + // 2. Return ? IteratorClose(iteratorRecord, completion). + return iterator_record.close(completion, context); + } + } + + // 7. Let result be fieldNames. + // 8. If calendar.[[Identifier]] is not "iso8601", then + if calendar.identifier.as_slice() != ISO { + // a. NOTE: Every built-in calendar preserves all input field names in output. + // b. Let extraFieldDescriptors be CalendarFieldDescriptors(calendar.[[Identifier]], fieldNames). + let extended_fields = this_calendar.field_descriptors(&fields_names); + // c. For each Calendar Field Descriptor Record desc of extraFieldDescriptors, do + for descriptor in extended_fields { + // i. Append desc.[[Property]] to result. + fields_names.push(descriptor.0); + } + } + + // 9. Return CreateArrayFromList(result). + Ok(Array::create_array_from_list( + fields_names + .iter() + .map(|s| JsString::from(s.clone()).into()), + context, + ) + .into()) + } + + /// 15.8.2.22 `Temporal.Calendar.prototype.mergeFields ( fields, additionalFields )` + fn merge_fields( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let calendar be the this value. + // 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Calendar must be an object.") + })?; + let calendar = o.as_calendar().ok_or_else(|| { + JsNativeError::typ() + .with_message("the this value of Calendar must be a Calendar object.") + })?; + + let available_calendars = available_calendars(); + + let this_calendar = available_calendars + .get(calendar.identifier.as_slice()) + .expect("builtin must exist"); + + let fields = args.get_or_undefined(0).to_object(context)?; + let additional_fields = args.get_or_undefined(1).to_object(context)?; + + // 3. Let fieldsCopy be ? SnapshotOwnProperties(? ToObject(fields), null, « », « undefined »). + let fields_copy = temporal::snapshot_own_properties( + &fields, + Some(Vec::new()), + Some(Vec::from([JsValue::undefined()])), + context, + )?; + + // 4. Let additionalFieldsCopy be ? SnapshotOwnProperties(? ToObject(additionalFields), null, « », « undefined »). + let additional_fields_copy = temporal::snapshot_own_properties( + &additional_fields, + Some(Vec::new()), + Some(Vec::from([JsValue::undefined()])), + context, + )?; + + // 5. NOTE: Every property of fieldsCopy and additionalFieldsCopy is an enumerable data property with non-undefined value, but some property keys may be Symbols. + // 6. Let additionalKeys be ! additionalFieldsCopy.[[OwnPropertyKeys]](). + let add_keys = additional_fields_copy.__own_property_keys__(context)?; + + // 7. If calendar.[[Identifier]] is "iso8601", then + // a. Let overriddenKeys be ISOFieldKeysToIgnore(additionalKeys). + // 8. Else, + // a. Let overriddenKeys be CalendarFieldKeysToIgnore(calendar, additionalKeys). + let overridden_keys = this_calendar.field_keys_to_ignore(add_keys); + + // 9. Let merged be OrdinaryObjectCreate(null). + let merged = JsObject::with_null_proto(); + + // 10. NOTE: The following steps ensure that property iteration order of merged + // matches that of fields as modified by omitting overridden properties and + // appending non-overlapping properties from additionalFields in iteration order. + // 11. Let fieldsKeys be ! fieldsCopy.[[OwnPropertyKeys]](). + let field_keys = fields_copy.__own_property_keys__(context)?; + // 12. For each element key of fieldsKeys, do + for key in field_keys { + // a. Let propValue be undefined. + // b. If overriddenKeys contains key, then + let prop_value = if overridden_keys.contains(&key) { + // i. Set propValue to ! Get(additionalFieldsCopy, key). + additional_fields_copy.get(key.clone(), context)? + // c. Else, + } else { + // i. Set propValue to ! Get(fieldsCopy, key). + fields_copy.get(key.clone(), context)? + }; + + // d. If propValue is not undefined, perform ! CreateDataPropertyOrThrow(merged, key, propValue). + if !prop_value.is_undefined() { + merged.create_data_property_or_throw(key, prop_value, context)?; + } + } + + // 13. Perform ! CopyDataProperties(merged, additionalFieldsCopy, « »). + temporal::copy_data_properties( + &merged, + &additional_fields_copy.into(), + &Vec::new(), + None, + context, + )?; + + // 14. Return merged. + Ok(merged.into()) + } +} + +// -- `Calendar` Abstract Operations -- + +/// 12.2.1 `CreateTemporalCalendar ( identifier [ , newTarget ] )` +pub(crate) fn create_temporal_calendar( + identifier: &JsString, + new_target: Option, + context: &mut Context<'_>, +) -> JsResult { + // 1. Assert: IsBuiltinCalendar(identifier) is true. + assert!(is_builtin_calendar(identifier)); + + let calendar = Calendar { + identifier: identifier.clone(), + }; + // 2. If newTarget is not provided, set newTarget to %Temporal.Calendar%. + let new_target = new_target.unwrap_or_else(|| { + context + .realm() + .intrinsics() + .constructors() + .calendar() + .constructor() + .into() + }); + + // 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.Calendar.prototype%", « [[InitializedTemporalCalendar]], [[Identifier]] »). + let proto = + get_prototype_from_constructor(&new_target, StandardConstructors::calendar, context)?; + + let obj = JsObject::from_proto_and_data(proto, ObjectData::calendar(calendar)); + + // 4. Set object.[[Identifier]] to the ASCII-lowercase of identifier. + // 5. Return object. + Ok(obj.into()) +} + +/// 12.2.21 `GetTemporalCalendarSlotValueWithISODefault ( item )` +#[allow(unused)] +pub(crate) fn get_temporal_calendar_slot_value_with_default( + item: &JsObject, + context: &mut Context<'_>, +) -> JsResult { + // 1. If item has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal slot, then + // a. Return item.[[Calendar]]. + if item.is_plain_date() { + let obj = item.borrow(); + let date = obj.as_plain_date(); + if let Some(date) = date { + let calendar = date.calendar.clone(); + drop(obj); + return Ok(calendar); + } + } else if item.is_plain_date_time() { + let obj = item.borrow(); + let date_time = obj.as_plain_date_time(); + if let Some(dt) = date_time { + let calendar = dt.calendar.clone(); + drop(obj); + return Ok(calendar); + } + } else if item.is_plain_year_month() { + let obj = item.borrow(); + let year_month = obj.as_plain_year_month(); + if let Some(ym) = year_month { + let calendar = ym.calendar.clone(); + drop(obj); + return Ok(calendar); + } + } else if item.is_plain_month_day() { + let obj = item.borrow(); + let month_day = obj.as_plain_month_day(); + if let Some(md) = month_day { + let calendar = md.calendar.clone(); + drop(obj); + return Ok(calendar); + } + } else if item.is_zoned_date_time() { + return Err(JsNativeError::range() + .with_message("Not yet implemented.") + .into()); + } + + // 2. Let calendarLike be ? Get(item, "calendar"). + let calendar_like = item.get(utf16!("calendar"), context)?; + + // 3. Return ? ToTemporalCalendarSlotValue(calendarLike, "iso8601"). + to_temporal_calendar_slot_value(&calendar_like, Some(ISO.into())) +} + +#[allow(unused)] +fn to_temporal_calendar_slot_value( + calendar_like: &JsValue, + default: Option, +) -> JsResult { + // 1. If temporalCalendarLike is undefined and default is present, then + if calendar_like.is_undefined() { + if let Some(default) = default { + // a. Assert: IsBuiltinCalendar(default) is true. + if is_builtin_calendar(&default) { + // b. Return default. + return Ok(default.into()); + } + } + // 2. If Type(temporalCalendarLike) is Object, then + } else if let Some(calendar_like) = calendar_like.as_object() { + // a. If temporalCalendarLike has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal slot, then + // i. Return temporalCalendarLike.[[Calendar]]. + if calendar_like.is_plain_date() { + let obj = calendar_like.borrow(); + let date = obj.as_plain_date(); + if let Some(date) = date { + let calendar = date.calendar.clone(); + return Ok(calendar); + } + } else if calendar_like.is_plain_date_time() { + let obj = calendar_like.borrow(); + let date_time = obj.as_plain_date_time(); + if let Some(dt) = date_time { + let calendar = dt.calendar.clone(); + return Ok(calendar); + } + } else if calendar_like.is_plain_year_month() { + let obj = calendar_like.borrow(); + let year_month = obj.as_plain_year_month(); + if let Some(ym) = year_month { + let calendar = ym.calendar.clone(); + return Ok(calendar); + } + } else if calendar_like.is_plain_month_day() { + let obj = calendar_like.borrow(); + let month_day = obj.as_plain_month_day(); + if let Some(md) = month_day { + let calendar = md.calendar.clone(); + return Ok(calendar); + } + } else if calendar_like.is_zoned_date_time() { + return Err(JsNativeError::range() + .with_message("Not yet implemented.") + .into()); + } + + // TODO: implement ObjectImplementsTemporalCalendarProtocol + // b. If ? ObjectImplementsTemporalCalendarProtocol(temporalCalendarLike) is false, throw a TypeError exception. + // c. Return temporalCalendarLike. + return Ok(calendar_like.clone().into()); + } + + // 3. If temporalCalendarLike is not a String, throw a TypeError exception. + if !calendar_like.is_string() { + return Err(JsNativeError::typ() + .with_message("temporalCalendarLike is not a string.") + .into()); + } + + // TODO: 4-6 + // 4. Let identifier be ? ParseTemporalCalendarString(temporalCalendarLike). + // 5. If IsBuiltinCalendar(identifier) is false, throw a RangeError exception. + // 6. Return the ASCII-lowercase of identifier. + Ok(js_string!(ISO).into()) +} + +// ---------------------------- AbstractCalendar Methods ---------------------------- +// +// The above refers to the functions in the Abstract Operations section of the Calendar +// spec takes either a calendar identifier or `Temporal.Calendar` and calls the a +// function that aligns with a method on `Temporal.Calendar`. These functions appear +// to be a second completely abstract builtin calendar implementation itself, so +// separating them from the other Abstract Operations seems both natural and will +// hopefully make any changes more maintainable. +// +// NOTE: Instead of creating temporal calendar it may be more efficient to retrieve +// the protocol and call the value directly in rust, something to consider. + +/// A helper method to assess a identifier vs Calendar and calling a designated method. +fn call_method_on_abstract_calendar( + calendar: &JsValue, + method: &JsString, + args: &[JsValue], + context: &mut Context<'_>, +) -> JsResult { + // If Calendar is a string + let this_calendar = match calendar { + JsValue::String(id) => create_temporal_calendar(id, None, context)? + .as_object() + .expect("CreateTemporalCalendar must return JsObject.") + .clone(), + JsValue::Object(calendar) => calendar.clone(), + _ => unreachable!(), + }; + + let method = this_calendar.get(method.as_ref(), context)?; + method.call(&this_calendar.into(), args, context) +} + +/// 12.2.2 `CalendarFields ( calendar, fieldNames )` +/// +/// Returns either a normal completion containing a List of Strings, or a throw completion. +#[allow(unused)] +pub(crate) fn calendar_fields( + calendar: &JsValue, + field_names: Vec, + context: &mut Context<'_>, +) -> JsResult> { + let field_names = Array::create_array_from_list(field_names, context); + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Let fieldsArray be ? Call(%Temporal.Calendar.prototype.fields%, calendar, « CreateArrayFromList(fieldNames) »). + // c. Return ! CreateListFromArrayLike(fieldsArray, « String »). + // 2. Let fieldsArray be ? Invoke(calendar, "fields", « CreateArrayFromList(fieldNames) »). + let fields_array = call_method_on_abstract_calendar( + calendar, + &JsString::from("fields"), + &[field_names.into()], + context, + )?; + + // 3. Let iteratorRecord be ? GetIterator(fieldsArray, sync). + let mut iterator_record = fields_array.get_iterator(context, Some(IteratorHint::Sync), None)?; + // 4. Return ? IteratorToListOfType(iteratorRecord, « String »). + super::iterator_to_list_of_types(&mut iterator_record, &[crate::value::Type::String], context) +} + +/// 12.2.3 `CalendarMergeFields ( calendar, fields, additionalFields )` +/// +/// Returns either a normal completion containing an Object, or a throw completion. +#[allow(unused)] +pub(crate) fn calendar_merge_fields( + calendar: &JsValue, + fields: &TemporalFields, + additional_fields: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.mergeFields%, calendar, « fields, additionalFields »). + // 2. Let result be ? Invoke(calendar, "mergeFields", « fields, additionalFields »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("mergeFields"), + &[fields.as_object(context)?.into(), additional_fields.clone()], + context, + )?; + + // 3. If Type(result) is not Object, throw a TypeError exception. + // 4. Return result. + match result { + JsValue::Object(o) => Ok(o), + _ => Err(JsNativeError::typ() + .with_message("mergeFields must return an object") + .into()), + } +} + +/// 12.2.4 `CalendarDateAdd ( calendar, date, duration [ , options [ , dateAdd ] ] )` +/// +/// Returns either a normal completion containing a `Temporal.PlainDate`, or an abrupt completion. +#[allow(unused)] +pub(crate) fn calendar_date_add( + calendar: &JsValue, + date: &JsObject, + duration: &JsObject, + options: Option, + context: &mut Context<'_>, +) -> JsResult { + // 1. If options is not present, set options to undefined. + let options = options.unwrap_or(JsValue::undefined()); + + // 2. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.dateAdd%, calendar, « date, duration, options »). + // 3. If dateAdd is not present, set dateAdd to ? GetMethod(calendar, "dateAdd"). + // 4. Let addedDate be ? Call(dateAdd, calendar, « date, duration, options »). + let added_date = call_method_on_abstract_calendar( + calendar, + &JsString::from("dateAdd"), + &[date.clone().into(), duration.clone().into(), options], + context, + )?; + + // 5. Perform ? RequireInternalSlot(addedDate, [[InitializedTemporalDate]]). + // 6. Return addedDate. + match added_date { + JsValue::Object(o) if o.is_plain_date() => Ok(o), + _ => Err(JsNativeError::typ() + .with_message("dateAdd returned a value other than a Temoporal.PlainDate") + .into()), + } +} + +/// 12.2.5 `CalendarDateUntil ( calendar, one, two, options [ , dateUntil ] )` +/// +/// Returns either a normal completion containing a `Temporal.Duration`, or an abrupt completion. +#[allow(unused)] +pub(crate) fn calendar_date_until( + calendar: &JsValue, + one: &JsObject, + two: &JsObject, + options: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.dateUntil%, calendar, « one, two, options »). + // 2. If dateUntil is not present, set dateUntil to ? GetMethod(calendar, "dateUntil"). + // 3. Let duration be ? Call(dateUntil, calendar, « one, two, options »). + let duration = call_method_on_abstract_calendar( + calendar, + &JsString::from("dateUntil"), + &[one.clone().into(), two.clone().into(), options.clone()], + context, + )?; + + // 4. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + // 5. Return duration. + match duration { + JsValue::Object(o) if o.is_duration() => { + let obj = o.borrow(); + let dur = obj + .as_duration() + .expect("Value is confirmed to be a duration."); + let record = dur.inner; + drop(obj); + Ok(record) + } + _ => Err(JsNativeError::typ() + .with_message("Calendar dateUntil must return a Duration") + .into()), + } +} + +/// 12.2.6 `CalendarYear ( calendar, dateLike )` +/// +/// Returns either a normal completion containing an integer, or an abrupt completion. +#[allow(unused)] +pub(crate) fn calendar_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.year%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "year", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("year"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarYear result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarYear was not integral.") + .into()); + } + + // 5. Return ℝ(result). + Ok(number) +} + +/// 12.2.7 `CalendarMonth ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_month( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.month%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "month", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("month"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarYear result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarMonth was not integral.") + .into()); + } + + // 5. If result < 1𝔽, throw a RangeError exception. + if number < 1.0 { + return Err(JsNativeError::range() + .with_message("month must be 1 or greater.") + .into()); + } + + // 6. Return ℝ(result). + Ok(number) +} + +/// 12.2.8 `CalendarMonthCode ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_month_code( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.monthCode%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "monthCode", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("monthCode"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not String, throw a TypeError exception. + // 4. Return result. + match result { + JsValue::String(s) => Ok(s), + _ => Err(JsNativeError::typ() + .with_message("monthCode must be a String.") + .into()), + } +} + +/// 12.2.9 `CalendarDay ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_day( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.day%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "day", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("day"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarYear result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarDay was not integral.") + .into()); + } + + // 5. If result < 1𝔽, throw a RangeError exception. + if number < 1.0 { + return Err(JsNativeError::range() + .with_message("day must be 1 or greater.") + .into()); + } + + // 6. Return ℝ(result). + Ok(number) +} + +/// 12.2.10 `CalendarDayOfWeek ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_day_of_week( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.dayOfWeek%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "dayOfWeek", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("dayOfWeek"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarDayOfWeek result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarDayOfWeek was not integral.") + .into()); + } + + // 5. If result < 1𝔽, throw a RangeError exception. + if number < 1.0 { + return Err(JsNativeError::range() + .with_message("dayOfWeek must be 1 or greater.") + .into()); + } + + // 6. Return ℝ(result). + Ok(number) +} + +/// 12.2.11 `CalendarDayOfYear ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_day_of_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.dayOfYear%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "dayOfYear", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("dayOfYear"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarDayOfYear result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarDayOfYear was not integral.") + .into()); + } + + // 5. If result < 1𝔽, throw a RangeError exception. + if number < 1.0 { + return Err(JsNativeError::range() + .with_message("dayOfYear must be 1 or greater.") + .into()); + } + + // 6. Return ℝ(result). + Ok(number) +} + +/// 12.2.12 `CalendarWeekOfYear ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_week_of_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.weekOfYear%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "weekOfYear", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("weekOfYear"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarWeekOfYear result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarWeekOfYear was not integral.") + .into()); + } + + // 5. If result < 1𝔽, throw a RangeError exception. + if number < 1.0 { + return Err(JsNativeError::range() + .with_message("weekOfYear must be 1 or greater.") + .into()); + } + + // 6. Return ℝ(result). + Ok(number) +} + +/// 12.2.13 `CalendarYearOfWeek ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_year_of_week( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.yearOfWeek%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "yearOfWeek", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("yearOfWeek"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarYearOfWeek result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarYearOfWeek was not integral.") + .into()); + } + + // 5. Return ℝ(result). + Ok(number) +} + +/// 12.2.14 `CalendarDaysInWeek ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_days_in_week( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.daysInWeek%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "daysInWeek", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("daysInWeek"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarDaysInWeek result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarDaysInWeek was not integral.") + .into()); + } + + // 5. If result < 1𝔽, throw a RangeError exception. + if number < 1.0 { + return Err(JsNativeError::range() + .with_message("daysInWeek must be 1 or greater.") + .into()); + } + + // 6. Return ℝ(result). + Ok(number) +} + +/// 12.2.15 `CalendarDaysInMonth ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_days_in_month( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.daysInMonth%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "daysInMonth", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("daysInMonth"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarDaysInMonth result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarDaysInMonth was not integral.") + .into()); + } + + // 5. If result < 1𝔽, throw a RangeError exception. + if number < 1.0 { + return Err(JsNativeError::range() + .with_message("daysInMonth must be 1 or greater.") + .into()); + } + + // 6. Return ℝ(result). + Ok(number) +} + +/// 12.2.16 `CalendarDaysInYear ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_days_in_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.daysInYear%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "daysInYear", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("daysInYear"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarDaysInYear result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarDaysInYear was not integral.") + .into()); + } + + // 5. If result < 1𝔽, throw a RangeError exception. + if number < 1.0 { + return Err(JsNativeError::range() + .with_message("daysInYear must be 1 or greater.") + .into()); + } + + // 6. Return ℝ(result). + Ok(number) +} + +/// 12.2.17 `CalendarMonthsInYear ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_months_in_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.monthsInYear%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "monthsInYear", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("monthsInYear"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Number, throw a TypeError exception. + let Some(number) = result.as_number() else { + return Err(JsNativeError::typ() + .with_message("CalendarMonthsInYear result must be a number.") + .into()); + }; + + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return Err(JsNativeError::range() + .with_message("CalendarMonthsInYear was not integral.") + .into()); + } + + // 5. If result < 1𝔽, throw a RangeError exception. + if number < 1.0 { + return Err(JsNativeError::range() + .with_message("monthsInYear must be 1 or greater.") + .into()); + } + + // 6. Return ℝ(result). + Ok(number) +} + +/// 12.2.18 `CalendarInLeapYear ( calendar, dateLike )` +#[allow(unused)] +pub(crate) fn calendar_in_lear_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.inLeapYear%, calendar, « dateLike »). + // 2. Let result be ? Invoke(calendar, "inLeapYear", « dateLike »). + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("inLeapYear"), + &[datelike.clone()], + context, + )?; + + // 3. If Type(result) is not Boolean, throw a TypeError exception. + // 4. Return result. + match result { + JsValue::Boolean(b) => Ok(b), + _ => Err(JsNativeError::typ() + .with_message("inLeapYear result must be a boolean.") + .into()), + } +} + +/// 12.2.24 `CalendarDateFromFields ( calendar, fields [ , options [ , dateFromFields ] ] )` +#[allow(unused)] +pub(crate) fn calendar_date_from_fields( + _calendar: &JsValue, + _fields: &JsObject, + options: Option<&JsValue>, + _date_from_fields: Option<&JsObject>, +) -> JsResult { + let _options = match options { + Some(o) => o.clone(), + _ => JsValue::undefined(), + }; + // 1. If options is not present, set options to undefined. + // 2. If calendar is a String, then + // a. Set calendar to ! CreateTemporalCalendar(calendar). + // b. Return ? Call(%Temporal.Calendar.prototype.dateFromFields%, calendar, « fields, options »). + // 3. If dateFromFields is not present, set dateFromFields to ? GetMethod(calendar, "dateFromFields"). + // 4. Let date be ? Call(calendar, dateFromFields, « fields, options »). + // 5. Perform ? RequireInternalSlot(date, [[InitializedTemporalDate]]). + // 6. Return date. + + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) +} diff --git a/boa_engine/src/builtins/temporal/calendar/tests.rs b/boa_engine/src/builtins/temporal/calendar/tests.rs new file mode 100644 index 00000000000..de24f03efc2 --- /dev/null +++ b/boa_engine/src/builtins/temporal/calendar/tests.rs @@ -0,0 +1,22 @@ +use crate::{js_string, run_test_actions, TestAction}; + +#[test] +fn calendar_constructor() { + // TODO: Add other BuiltinCalendars + run_test_actions([TestAction::assert_eq( + "new Temporal.Calendar('iso8601').id", + js_string!("iso8601"), + )]); +} + +#[test] +fn calendar_methods() { + run_test_actions([ + TestAction::run("let iso = new Temporal.Calendar('iso8601');"), + TestAction::assert_eq("iso.inLeapYear('2020-11-20')", true), + TestAction::assert_eq("iso.daysInYear('2020-11-20')", 366), + TestAction::assert_eq("iso.daysInYear('2021-11-20')", 365), + TestAction::assert_eq("iso.monthsInYear('2021-11-20')", 12), + TestAction::assert_eq("iso.daysInWeek('2021-11-20')", 7), + ]); +} diff --git a/boa_engine/src/builtins/temporal/calendar/utils.rs b/boa_engine/src/builtins/temporal/calendar/utils.rs new file mode 100644 index 00000000000..2e7b6f0eba0 --- /dev/null +++ b/boa_engine/src/builtins/temporal/calendar/utils.rs @@ -0,0 +1,107 @@ +//! Calendar utility calculations + +// TODO: determine if any of the below are needed. + +use crate::builtins::temporal::{self, date_equations, plain_date::iso::IsoDateRecord}; +use crate::JsString; + +/// 12.2.31 `ISODaysInMonth ( year, month )` +pub(crate) fn iso_days_in_month(year: i32, month: i32) -> i32 { + match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 => { + 28 + temporal::date_equations::mathematical_in_leap_year( + temporal::date_equations::epoch_time_for_year(year), + ) + } + _ => unreachable!("an invalid month value is an implementation error."), + } +} + +/// 12.2.32 `ToISOWeekOfYear ( year, month, day )` +/// +/// Takes an `[[IsoYear]]`, `[[IsoMonth]]`, and `[[IsoDay]]` and returns a (week, year) record. +#[allow(unused)] +pub(crate) fn to_iso_week_of_year(year: i32, month: i32, day: i32) -> (i32, i32) { + // Function constants + // 2. Let wednesday be 3. + // 3. Let thursday be 4. + // 4. Let friday be 5. + // 5. Let saturday be 6. + // 6. Let daysInWeek be 7. + // 7. Let maxWeekNumber be 53. + let day_of_year = to_iso_day_of_year(year, month, day); + let day_of_week = to_iso_day_of_week(year, month, day); + let week = (day_of_week + 7 - day_of_week + 3) / 7; + + if week < 1 { + let first_day_of_year = to_iso_day_of_week(year, 1, 1); + if first_day_of_year == 5 { + return (53, year - 1); + } else if first_day_of_year == 6 + && date_equations::mathematical_in_leap_year(date_equations::epoch_time_for_year( + year - 1, + )) == 1 + { + return (52, year - 1); + } + return (52, year - 1); + } else if week == 53 { + let days_in_year = date_equations::mathematical_days_in_year(year); + let days_later_in_year = days_in_year - day_of_year; + let days_after_thursday = 4 - day_of_week; + if days_later_in_year < days_after_thursday { + return (1, year - 1); + } + } + (week, year) +} + +/// 12.2.33 `ISOMonthCode ( month )` +#[allow(unused)] +fn iso_month_code(month: i32) -> JsString { + // TODO: optimize + if month < 10 { + JsString::from(format!("M0{month}")) + } else { + JsString::from(format!("M{month}")) + } +} + +// 12.2.34 `ISOResolveMonth ( fields )` +// Note: currently implemented on TemporalFields -> implement in this mod? + +// 12.2.35 ISODateFromFields ( fields, overflow ) +// Note: implemented on IsoDateRecord. + +// 12.2.36 ISOYearMonthFromFields ( fields, overflow ) +// TODO: implement on a IsoYearMonthRecord + +// 12.2.37 ISOMonthDayFromFields ( fields, overflow ) +// TODO: implement as method on IsoDateRecord. + +// 12.2.38 IsoFieldKeysToIgnore +// TODO: determine usefulness. + +/// 12.2.39 `ToISODayOfYear ( year, month, day )` +#[allow(unused)] +fn to_iso_day_of_year(year: i32, month: i32, day: i32) -> i32 { + // TODO: update fn parameter to take IsoDateRecord. + let iso = IsoDateRecord::new(year, month - 1, day); + let epoch_days = iso.as_epoch_days(); + date_equations::epoch_time_to_day_in_year(temporal::epoch_days_to_epoch_ms(epoch_days, 0)) + 1 +} + +/// 12.2.40 `ToISODayOfWeek ( year, month, day )` +#[allow(unused)] +pub(crate) fn to_iso_day_of_week(year: i32, month: i32, day: i32) -> i32 { + let iso = IsoDateRecord::new(year, month - 1, day); + let epoch_days = iso.as_epoch_days(); + let day_of_week = + date_equations::epoch_time_to_week_day(temporal::epoch_days_to_epoch_ms(epoch_days, 0)); + if day_of_week == 0 { + return 7; + } + day_of_week +} diff --git a/boa_engine/src/builtins/temporal/date_equations.rs b/boa_engine/src/builtins/temporal/date_equations.rs new file mode 100644 index 00000000000..201a90386dc --- /dev/null +++ b/boa_engine/src/builtins/temporal/date_equations.rs @@ -0,0 +1,121 @@ +//! This file represents all equations listed under section 13.4 of the [Temporal Specification][spec] +//! +//! [spec]: https://tc39.es/proposal-temporal/#sec-date-equations + +use std::ops::Mul; + +pub(crate) fn epoch_time_to_day_number(t: f64) -> i32 { + (t / f64::from(super::MS_PER_DAY)).floor() as i32 +} + +pub(crate) fn mathematical_days_in_year(y: i32) -> i32 { + if y % 4 != 0 { + 365 + } else if y % 4 == 0 && y % 100 != 0 { + 366 + } else if y % 100 == 0 && y % 400 != 0 { + 365 + } else { + // Assert that y is divisble by 400 to ensure we are returning the correct result. + assert_eq!(y % 400, 0); + 366 + } +} + +pub(crate) fn epoch_day_number_for_year(y: f64) -> f64 { + 365.0f64.mul_add(y - 1970.0, ((y - 1969.0) / 4.0).floor()) - ((y - 1901.0) / 100.0).floor() + + ((y - 1601.0) / 400.0).floor() +} + +pub(crate) fn epoch_time_for_year(y: i32) -> f64 { + f64::from(super::MS_PER_DAY) * epoch_day_number_for_year(f64::from(y)) +} + +// NOTE: The below returns the epoch years (years since 1970). The spec +// appears to assume the below returns with the epoch applied. +pub(crate) fn epoch_time_to_epoch_year(t: f64) -> i32 { + // roughly calculate the largest possible year given the time t, + // then check and refine the year. + let day_count = epoch_time_to_day_number(t); + let mut year = day_count / 365; + loop { + if epoch_time_for_year(year) <= t { + break; + } + year -= 1; + } + + year + 1970 +} + +/// Returns either 1 (true) or 0 (false) +pub(crate) fn mathematical_in_leap_year(t: f64) -> i32 { + mathematical_days_in_year(epoch_time_to_epoch_year(t)) - 365 +} + +pub(crate) fn epoch_time_to_month_in_year(t: f64) -> i32 { + const DAYS: [i32; 11] = [30, 58, 89, 120, 150, 181, 212, 242, 272, 303, 333]; + const LEAP_DAYS: [i32; 11] = [30, 59, 90, 121, 151, 182, 213, 242, 272, 303, 334]; + + let in_leap_year = mathematical_in_leap_year(t) == 1; + let day = epoch_time_to_day_in_year(t); + + let result = if in_leap_year { + LEAP_DAYS.binary_search(&day) + } else { + DAYS.binary_search(&day) + }; + + match result { + Ok(i) | Err(i) => i as i32, + } +} + +pub(crate) fn epoch_time_for_month_given_year(m: i32, y: i32) -> f64 { + let leap_day = mathematical_days_in_year(y) - 365; + + let days = match m { + 0 => 1, + 1 => 31, + 2 => 59 + leap_day, + 3 => 90 + leap_day, + 4 => 121 + leap_day, + 5 => 151 + leap_day, + 6 => 182 + leap_day, + 7 => 213 + leap_day, + 8 => 243 + leap_day, + 9 => 273 + leap_day, + 10 => 304 + leap_day, + 11 => 334 + leap_day, + _ => unreachable!(), + }; + + (super::NS_PER_DAY as f64).mul(f64::from(days)) +} + +pub(crate) fn epoch_time_to_date(t: f64) -> i32 { + const OFFSETS: [i16; 12] = [ + 1, -30, -58, -89, -119, -150, -180, -211, -242, -272, -303, -333, + ]; + let day_in_year = epoch_time_to_day_in_year(t); + let in_leap_year = mathematical_in_leap_year(t); + let month = epoch_time_to_month_in_year(t); + + // Cast from i32 to usize should be safe as the return must be 0-11 + let mut date = day_in_year + i32::from(OFFSETS[month as usize]); + + if month >= 2 { + date -= in_leap_year; + } + + date +} + +pub(crate) fn epoch_time_to_day_in_year(t: f64) -> i32 { + epoch_time_to_day_number(t) + - (epoch_day_number_for_year(f64::from(epoch_time_to_epoch_year(t))) as i32) +} + +pub(crate) fn epoch_time_to_week_day(t: f64) -> i32 { + (epoch_time_to_day_number(t) + 4) % 7 +} diff --git a/boa_engine/src/builtins/temporal/duration/mod.rs b/boa_engine/src/builtins/temporal/duration/mod.rs new file mode 100644 index 00000000000..5319b96c078 --- /dev/null +++ b/boa_engine/src/builtins/temporal/duration/mod.rs @@ -0,0 +1,1039 @@ +// Boa's implementation of the `Temporal.Duration` Builtin Object. + +use crate::{ + builtins::{ + options::{get_option, get_options_object, RoundingMode}, + temporal::validate_temporal_rounding_increment, + BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + }, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + js_string, + object::{internal_methods::get_prototype_from_constructor, ObjectData}, + property::Attribute, + realm::Realm, + string::{common::StaticJsStrings, utf16}, + Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; + +use super::{ + calendar, + options::{ + get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup, + }, + to_integer_if_integral, DateTimeValues, +}; + +mod record; + +#[cfg(test)] +mod tests; + +pub(crate) use record::{DateDuration, DurationRecord, TimeDuration}; + +/// The `Temporal.Duration` object. +/// +/// Per [spec], `Duration` records are float64-representable integers +/// +/// [spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances +#[derive(Debug, Clone, Copy)] +pub struct Duration { + pub(crate) inner: DurationRecord, +} + +impl BuiltInObject for Duration { + const NAME: JsString = StaticJsStrings::DURATION; +} + +impl IntrinsicObject for Duration { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + let get_years = BuiltInBuilder::callable(realm, Self::get_years) + .name(js_string!("get Years")) + .build(); + + let get_months = BuiltInBuilder::callable(realm, Self::get_months) + .name(js_string!("get Months")) + .build(); + + let get_weeks = BuiltInBuilder::callable(realm, Self::get_weeks) + .name(js_string!("get Weeks")) + .build(); + + let get_days = BuiltInBuilder::callable(realm, Self::get_days) + .name(js_string!("get Days")) + .build(); + + let get_hours = BuiltInBuilder::callable(realm, Self::get_hours) + .name(js_string!("get Hours")) + .build(); + + let get_minutes = BuiltInBuilder::callable(realm, Self::get_minutes) + .name(js_string!("get Minutes")) + .build(); + + let get_seconds = BuiltInBuilder::callable(realm, Self::get_seconds) + .name(js_string!("get Seconds")) + .build(); + + let get_milliseconds = BuiltInBuilder::callable(realm, Self::get_milliseconds) + .name(js_string!("get Milliseconds")) + .build(); + + let get_microseconds = BuiltInBuilder::callable(realm, Self::get_microseconds) + .name(js_string!("get Microseconds")) + .build(); + + let get_nanoseconds = BuiltInBuilder::callable(realm, Self::get_nanoseconds) + .name(js_string!("get Nanoseconds")) + .build(); + + let get_sign = BuiltInBuilder::callable(realm, Self::get_sign) + .name(js_string!("get Sign")) + .build(); + + let is_blank = BuiltInBuilder::callable(realm, Self::get_blank) + .name(js_string!("get blank")) + .build(); + + BuiltInBuilder::from_standard_constructor::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .accessor(utf16!("years"), Some(get_years), None, Attribute::default()) + .accessor( + utf16!("months"), + Some(get_months), + None, + Attribute::default(), + ) + .accessor(utf16!("weeks"), Some(get_weeks), None, Attribute::default()) + .accessor(utf16!("days"), Some(get_days), None, Attribute::default()) + .accessor(utf16!("hours"), Some(get_hours), None, Attribute::default()) + .accessor( + utf16!("minutes"), + Some(get_minutes), + None, + Attribute::default(), + ) + .accessor( + utf16!("seconds"), + Some(get_seconds), + None, + Attribute::default(), + ) + .accessor( + utf16!("milliseconds"), + Some(get_milliseconds), + None, + Attribute::default(), + ) + .accessor( + utf16!("microseconds"), + Some(get_microseconds), + None, + Attribute::default(), + ) + .accessor( + utf16!("nanoseconds"), + Some(get_nanoseconds), + None, + Attribute::default(), + ) + .accessor(utf16!("sign"), Some(get_sign), None, Attribute::default()) + .accessor(utf16!("blank"), Some(is_blank), None, Attribute::default()) + .method(Self::with, js_string!("with"), 1) + .method(Self::negated, js_string!("negated"), 0) + .method(Self::abs, js_string!("abs"), 0) + .method(Self::add, js_string!("add"), 2) + .method(Self::subtract, js_string!("subtract"), 2) + .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_json, js_string!("toJSON"), 0) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInConstructor for Duration { + const LENGTH: usize = 10; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::duration; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. If NewTarget is undefined, then + if new_target.is_undefined() { + // a. Throw a TypeError exception. + return Err(JsNativeError::typ() + .with_message("NewTarget cannot be undefined for Temporal.Duration constructor.") + .into()); + } + + // 2. If years is undefined, let y be 0; else let y be ? ToIntegerIfIntegral(years). + let years = f64::from( + args.get(0) + .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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + args.get(8) + .map_or(Ok(0), |ns| to_integer_if_integral(ns, context))?, + ); + + let record = DurationRecord::new( + DateDuration::new(years, months, weeks, days), + TimeDuration::new( + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + ), + ); + + // 12. Return ? CreateTemporalDuration(y, mo, w, d, h, m, s, ms, mis, ns, NewTarget). + create_temporal_duration(record, Some(new_target), context).map(Into::into) + } +} + +// -- Duration accessor property implementations -- + +impl Duration { + // Internal utility function for getting `Duration` field values. + fn get_internal_field(this: &JsValue, field: &DateTimeValues) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Duration must be an object.") + })?; + let duration = o.as_duration().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a Duration object.") + })?; + + match field { + DateTimeValues::Year => Ok(JsValue::Rational(duration.inner.years())), + DateTimeValues::Month => Ok(JsValue::Rational(duration.inner.months())), + DateTimeValues::Week => Ok(JsValue::Rational(duration.inner.weeks())), + DateTimeValues::Day => Ok(JsValue::Rational(duration.inner.days())), + DateTimeValues::Hour => Ok(JsValue::Rational(duration.inner.hours())), + DateTimeValues::Minute => Ok(JsValue::Rational(duration.inner.minutes())), + DateTimeValues::Second => Ok(JsValue::Rational(duration.inner.seconds())), + DateTimeValues::Millisecond => Ok(JsValue::Rational(duration.inner.milliseconds())), + DateTimeValues::Microsecond => Ok(JsValue::Rational(duration.inner.microseconds())), + DateTimeValues::Nanosecond => Ok(JsValue::Rational(duration.inner.nanoseconds())), + DateTimeValues::MonthCode => unreachable!( + "Any other DateTimeValue fields on Duration would be an implementation error." + ), + } + } + + /// 7.3.3 get Temporal.Duration.prototype.years + fn get_years(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Year) + } + + // 7.3.4 get Temporal.Duration.prototype.months + fn get_months(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Month) + } + + /// 7.3.5 get Temporal.Duration.prototype.weeks + fn get_weeks(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Week) + } + + /// 7.3.6 get Temporal.Duration.prototype.days + fn get_days(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Day) + } + + /// 7.3.7 get Temporal.Duration.prototype.hours + fn get_hours(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Hour) + } + + /// 7.3.8 get Temporal.Duration.prototype.minutes + fn get_minutes(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Minute) + } + + /// 7.3.9 get Temporal.Duration.prototype.seconds + fn get_seconds(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Second) + } + + /// 7.3.10 get Temporal.Duration.prototype.milliseconds + fn get_milliseconds(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Millisecond) + } + + /// 7.3.11 get Temporal.Duration.prototype.microseconds + fn get_microseconds(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Microsecond) + } + + /// 7.3.12 get Temporal.Duration.prototype.nanoseconds + fn get_nanoseconds(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Nanosecond) + } + + /// 7.3.13 get Temporal.Duration.prototype.sign + fn get_sign(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Duration must be an object.") + })?; + let duration = o.as_duration().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a Duration object.") + })?; + + // 3. Return 𝔽(! DurationSign(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], + // duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], + // duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]])). + Ok(duration.inner.duration_sign().into()) + } + + /// 7.3.14 get Temporal.Duration.prototype.blank + fn get_blank(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Duration must be an object.") + })?; + let duration = o.as_duration().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a Duration object.") + })?; + + // 3. Let sign be ! DurationSign(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], + // duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], + // duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]). + let sign = duration.inner.duration_sign(); + + // 4. If sign = 0, return true. + // 5. Return false. + match sign { + 0 => Ok(true.into()), + _ => Ok(false.into()), + } + } +} + +// -- Duration Method implementations -- + +impl Duration { + /// 7.3.15 `Temporal.Duration.prototype.with ( temporalDurationLike )` + pub(crate) fn with( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Duration must be an object.") + })?; + let duration = o.as_duration().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a Duration object.") + })?; + + // 3. Let temporalDurationLike be ? ToTemporalPartialDurationRecord(temporalDurationLike). + let temporal_duration_like = + DurationRecord::from_partial_js_object(args.get_or_undefined(0), context)?; + + // 4. If temporalDurationLike.[[Years]] is not undefined, then + // 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() + }; + + // 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() + }; + + // 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() + }; + + // 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() + }; + + // 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() + }; + + // 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() + }; + + // 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() + }; + + // 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() + }; + + // 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() + }; + + // 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() + }; + + // 24. Return ? CreateTemporalDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + let new_duration = DurationRecord::new( + DateDuration::new(years, months, weeks, days), + TimeDuration::new( + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + ), + ); + + new_duration.as_object(context).map(Into::into) + } + + /// 7.3.16 `Temporal.Duration.prototype.negated ( )` + pub(crate) fn negated(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + // 3. Return ! CreateNegatedTemporalDuration(duration). + + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } + + /// 7.3.17 `Temporal.Duration.prototype.abs ( )` + pub(crate) fn abs( + this: &JsValue, + _: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + // 3. Return ! CreateTemporalDuration(abs(duration.[[Years]]), abs(duration.[[Months]]), + // abs(duration.[[Weeks]]), abs(duration.[[Days]]), abs(duration.[[Hours]]), abs(duration.[[Minutes]]), + // abs(duration.[[Seconds]]), abs(duration.[[Milliseconds]]), abs(duration.[[Microseconds]]), abs(duration.[[Nanoseconds]])). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Duration must be an object.") + })?; + let duration = o.as_duration().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a Duration object.") + })?; + + let abs = duration.inner.abs(); + + abs.as_object(context).map(Into::into) + } + + /// 7.3.18 `Temporal.Duration.prototype.add ( other [ , options ] )` + pub(crate) fn add(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } + + /// 7.3.19 `Temporal.Duration.prototype.subtract ( other [ , options ] )` + pub(crate) fn subtract(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } + + /// 7.3.20 `Temporal.Duration.prototype.round ( roundTo )` + pub(crate) fn round( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Duration must be an object.") + })?; + let duration = o.as_duration().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a Duration object.") + })?; + + let round_to = args.get_or_undefined(0); + let round_to = match round_to { + // 3. If roundTo is undefined, then + JsValue::Undefined => { + return Err(JsNativeError::typ() + .with_message("roundTo cannot be undefined.") + .into()) + } + // 4. If Type(roundTo) is String, then + JsValue::String(rt) => { + // a. Let paramString be roundTo. + let param_string = rt.clone(); + // b. Set roundTo to OrdinaryObjectCreate(null). + let new_round_to = JsObject::with_null_proto(); + // c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit", paramString). + new_round_to.create_data_property_or_throw( + utf16!("smallestUnit"), + param_string, + context, + )?; + new_round_to + } + // 5. Else, + _ => { + // a. Set roundTo to ? GetOptionsObject(roundTo). + get_options_object(round_to)? + } + }; + + // NOTE: 6 & 7 unused in favor of `is_none()`. + // 6. Let smallestUnitPresent be true. + // 7. Let largestUnitPresent be true. + + // 8. NOTE: The following steps read options and perform independent validation in alphabetical order + // (ToRelativeTemporalObject reads "relativeTo", ToTemporalRoundingIncrement reads "roundingIncrement" and ToTemporalRoundingMode reads "roundingMode"). + + // 9. Let largestUnit be ? GetTemporalUnit(roundTo, "largestUnit", datetime, undefined, « "auto" »). + let largest_unit = get_temporal_unit( + &round_to, + utf16!("largestUnit"), + TemporalUnitGroup::DateTime, + Some([TemporalUnit::Auto].into()), + context, + )?; + + // 10. Let relativeTo be ? ToRelativeTemporalObject(roundTo). + let relative_to = super::to_relative_temporal_object(&round_to, context)?; + + // 11. Let roundingIncrement be ? ToTemporalRoundingIncrement(roundTo). + let rounding_increment = get_temporal_rounding_increment(&round_to, context)?; + + // 12. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand"). + let rounding_mode = get_option(&round_to, utf16!("roundingMode"), context)? + .unwrap_or(RoundingMode::HalfExpand); + + // 13. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit", datetime, undefined). + let smallest_unit = get_temporal_unit( + &round_to, + utf16!("smallestUnit"), + TemporalUnitGroup::DateTime, + None, + context, + )?; + + // NOTE: execute step 19 earlier before initial values are shadowed. + // 19. If smallestUnitPresent is false and largestUnitPresent is false, then + if smallest_unit.is_none() && largest_unit.is_none() { + // a. Throw a RangeError exception. + return Err(JsNativeError::range() + .with_message("smallestUnit or largestUnit must be present.") + .into()); + } + + // 14. If smallestUnit is undefined, then + let smallest_unit = if let Some(unit) = smallest_unit { + unit + } else { + // a. Set smallestUnitPresent to false. + // b. Set smallestUnit to "nanosecond". + TemporalUnit::Nanosecond + }; + + // 15. Let defaultLargestUnit be ! DefaultTemporalLargestUnit(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]]). + let mut default_largest_unit = duration.inner.default_temporal_largest_unit(); + + // 16. Set defaultLargestUnit to ! LargerOfTwoTemporalUnits(defaultLargestUnit, smallestUnit). + default_largest_unit = core::cmp::max(default_largest_unit, smallest_unit); + + // 17. If largestUnit is undefined, then + let largest_unit = match largest_unit { + Some(TemporalUnit::Auto) => default_largest_unit, + Some(u) => u, + None => { + // a. Set largestUnitPresent to false. + // b. Set largestUnit to defaultLargestUnit. + default_largest_unit + } + }; + + // 20. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception. + if core::cmp::max(largest_unit, smallest_unit) != largest_unit { + return Err(JsNativeError::range() + .with_message("largestUnit must be larger than smallestUnit") + .into()); + } + + // 21. Let maximum be ! MaximumTemporalDurationRoundingIncrement(smallestUnit). + let maximum = smallest_unit.to_maximum_rounding_increment(); + + // 22. If maximum is not undefined, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false). + if let Some(max) = maximum { + validate_temporal_rounding_increment(rounding_increment, f64::from(max), false)?; + } + + let mut unbalance_duration = DurationRecord::from_date_duration(duration.inner.date()); + + // 23. Let unbalanceResult be ? UnbalanceDateDurationRelative(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], largestUnit, relativeTo). + unbalance_duration.unbalance_duration_relative(largest_unit, &relative_to, context)?; + + let mut roundable_duration = + DurationRecord::new(unbalance_duration.date(), duration.inner.time()); + + // 24. Let roundResult be (? RoundDuration(unbalanceResult.[[Years]], unbalanceResult.[[Months]], unbalanceResult.[[Weeks]], + // unbalanceResult.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], + // duration.[[Microseconds]], duration.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode, relativeTo)).[[DurationRecord]]. + let _rem = roundable_duration.round_duration( + rounding_increment, + smallest_unit, + rounding_mode, + Some(&relative_to), + context, + )?; + + // 25. Let roundResult be roundRecord.[[DurationRecord]]. + // 26. If relativeTo is not undefined and relativeTo has an [[InitializedTemporalZonedDateTime]] internal slot, then + match relative_to { + JsValue::Object(o) if o.is_zoned_date_time() => { + // TODO: AdjustRoundedDurationDays requires 6.5.5 AddZonedDateTime. + // a. Set roundResult to ? AdjustRoundedDurationDays(roundResult.[[Years]], roundResult.[[Months]], roundResult.[[Weeks]], roundResult.[[Days]], roundResult.[[Hours]], roundResult.[[Minutes]], roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], roundResult.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode, relativeTo). + // b. Let balanceResult be ? BalanceTimeDurationRelative(roundResult.[[Days]], roundResult.[[Hours]], roundResult.[[Minutes]], roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], roundResult.[[Nanoseconds]], largestUnit, relativeTo). + return Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()); + } + // 27. Else, + _ => { + // a. Let balanceResult be ? BalanceTimeDuration(roundResult.[[Days]], roundResult.[[Hours]], roundResult.[[Minutes]], roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], roundResult.[[Nanoseconds]], largestUnit). + roundable_duration.balance_time_duration(largest_unit, None)?; + } + } + // 28. Let result be ? BalanceDateDurationRelative(roundResult.[[Years]], roundResult.[[Months]], roundResult.[[Weeks]], balanceResult.[[Days]], largestUnit, relativeTo). + // 29. Return ! CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], balanceResult.[[Hours]], balanceResult.[[Minutes]], balanceResult.[[Seconds]], balanceResult.[[Milliseconds]], balanceResult.[[Microseconds]], balanceResult.[[Nanoseconds]]). + + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } + + /// 7.3.21 `Temporal.Duration.prototype.total ( totalOf )` + pub(crate) fn total( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Duration must be an object.") + })?; + let duration = o.as_duration().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a Duration object.") + })?; + + let total_of = args.get_or_undefined(0); + + let total_of = match total_of { + // 3. If totalOf is undefined, throw a TypeError exception. + JsValue::Undefined => { + return Err(JsNativeError::typ() + .with_message("totalOf cannot be undefined.") + .into()); + } + // 4. If Type(totalOf) is String, then + JsValue::String(param_string) => { + // a. Let paramString be totalOf. + // b. Set totalOf to OrdinaryObjectCreate(null). + let total_of = JsObject::with_null_proto(); + // c. Perform ! CreateDataPropertyOrThrow(totalOf, "unit", paramString). + total_of.create_data_property_or_throw( + utf16!("unit"), + param_string.clone(), + context, + )?; + total_of + } + // 5. Else, + _ => { + // a. Set totalOf to ? GetOptionsObject(totalOf). + get_options_object(total_of)? + } + }; + + // 6. NOTE: The following steps read options and perform independent validation in alphabetical order (ToRelativeTemporalObject reads "relativeTo"). + // 7. Let relativeTo be ? ToRelativeTemporalObject(totalOf). + // NOTE TO SELF: Should relative_to_temporal_object just return a JsValue and we live with the expect? + let relative_to = super::to_relative_temporal_object(&total_of, context)?; + + // 8. Let unit be ? GetTemporalUnit(totalOf, "unit", datetime, required). + let unit = get_temporal_unit( + &total_of, + utf16!("unit"), + TemporalUnitGroup::DateTime, + None, + context, + )? + .ok_or_else(|| JsNativeError::range().with_message("unit cannot be undefined."))?; + + let mut unbalance_duration = DurationRecord::from_date_duration(duration.inner.date()); + + // 9. Let unbalanceResult be ? UnbalanceDurationRelative(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], unit, relativeTo). + unbalance_duration.unbalance_duration_relative(unit, &relative_to, context)?; + + // 10. Let intermediate be undefined. + let mut _intermediate = JsValue::undefined(); + + // 11. If Type(relativeTo) is Object and relativeTo has an [[InitializedTemporalZonedDateTime]] internal slot, then + if relative_to.is_object() + && relative_to + .as_object() + .expect("relative_to must be an object") + .is_zoned_date_time() + { + // a. Set intermediate to ? MoveRelativeZonedDateTime(relativeTo, unbalanceResult.[[Years]], unbalanceResult.[[Months]], unbalanceResult.[[Weeks]], 0). + return Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()); + } + + let mut balance_duration = DurationRecord::new( + DateDuration::new(0.0, 0.0, 0.0, unbalance_duration.days()), + duration.inner.time(), + ); + // 12. Let balanceResult be ? BalancePossiblyInfiniteDuration(unbalanceResult.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], unit, intermediate). + balance_duration.balance_possibly_infinite_duration(unit, Some(&relative_to))?; + + // 13. If balanceResult is positive overflow, return +∞𝔽. + if balance_duration.is_positive_overflow() { + return Ok(f64::INFINITY.into()); + }; + + // 14. If balanceResult is negative overflow, return -∞𝔽. + if balance_duration.is_negative_overflow() { + return Ok(f64::NEG_INFINITY.into()); + } + + // TODO: determine whether and how to assert 15. + // 15. Assert: balanceResult is a Time Duration Record. + + // 16. Let roundRecord be ? RoundDuration(unbalanceResult.[[Years]], unbalanceResult.[[Months]], unbalanceResult.[[Weeks]], balanceResult.[[Days]], + // balanceResult.[[Hours]], balanceResult.[[Minutes]], balanceResult.[[Seconds]], balanceResult.[[Milliseconds]], balanceResult.[[Microseconds]], + // balanceResult.[[Nanoseconds]], 1, unit, "trunc", relativeTo). + // 17. Let roundResult be roundRecord.[[DurationRecord]]. + let mut round_record = DurationRecord::new( + DateDuration::new( + unbalance_duration.years(), + unbalance_duration.months(), + unbalance_duration.weeks(), + balance_duration.days(), + ), + balance_duration.time(), + ); + + let remainder = round_record.round_duration( + 1_f64, + unit, + RoundingMode::Trunc, + Some(&relative_to), + context, + )?; + + let whole = match unit { + // 18. If unit is "year", then + // a. Let whole be roundResult.[[Years]]. + TemporalUnit::Year => round_record.years(), + // 19. Else if unit is "month", then + // a. Let whole be roundResult.[[Months]]. + TemporalUnit::Month => round_record.months(), + // 20. Else if unit is "week", then + // a. Let whole be roundResult.[[Weeks]]. + TemporalUnit::Week => round_record.weeks(), + // 21. Else if unit is "day", then + // a. Let whole be roundResult.[[Days]]. + TemporalUnit::Day => round_record.days(), + // 22. Else if unit is "hour", then + // a. Let whole be roundResult.[[Hours]]. + TemporalUnit::Hour => round_record.hours(), + // 23. Else if unit is "minute", then + // a. Let whole be roundResult.[[Minutes]]. + TemporalUnit::Minute => round_record.minutes(), + // 24. Else if unit is "second", then + // a. Let whole be roundResult.[[Seconds]]. + TemporalUnit::Second => round_record.seconds(), + // 25. Else if unit is "millisecond", then + // a. Let whole be roundResult.[[Milliseconds]]. + TemporalUnit::Millisecond => round_record.milliseconds(), + // 26. Else if unit is "microsecond", then + // a. Let whole be roundResult.[[Microseconds]]. + TemporalUnit::Microsecond => round_record.microseconds(), + // 27. Else, + // b. Let whole be roundResult.[[Nanoseconds]]. + TemporalUnit::Nanosecond => round_record.nanoseconds(), + // a. Assert: unit is "nanosecond". + TemporalUnit::Auto=> unreachable!("Unit must be a valid temporal unit. Any other value would be an implementation error."), + }; + + // 28. Return 𝔽(whole + roundRecord.[[Remainder]]). + Ok((whole + remainder).into()) + } + + /// 7.3.22 `Temporal.Duration.prototype.toString ( [ options ] )` + pub(crate) fn to_string( + _this: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } + + /// 7.3.23 `Temporal.Duration.prototype.toJSON ( )` + pub(crate) fn to_json( + _this: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } +} + +// -- Duration Abstract Operations -- + +/// 7.5.8 `ToTemporalDuration ( item )` +pub(crate) fn to_temporal_duration(item: &JsValue) -> JsResult { + // 1a. If Type(item) is Object + if item.is_object() { + // 1b. and item has an [[InitializedTemporalDuration]] internal slot, then + let o = item + .as_object() + .expect("Value must be an object in this instance."); + if o.is_duration() { + // a. Return item. + let obj = o.borrow(); + let duration = obj.as_duration().expect("must be a duration."); + return Ok(duration.inner); + } + } + + // 2. Let result be ? ToTemporalDurationRecord(item). + let result = to_temporal_duration_record(item)?; + // 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.9 `ToTemporalDurationRecord ( temporalDurationLike )` +pub(crate) fn to_temporal_duration_record( + _temporal_duration_like: &JsValue, +) -> JsResult { + Err(JsNativeError::range() + .with_message("Duration Parsing is not yet complete.") + .into()) +} + +/// 7.5.14 `CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , newTarget ] )` +pub(crate) fn create_temporal_duration( + record: DurationRecord, + new_target: Option<&JsValue>, + context: &mut Context<'_>, +) -> JsResult { + // 1. If ! IsValidDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) is false, throw a RangeError exception. + if !record.is_valid_duration() { + return Err(JsNativeError::range() + .with_message("Duration values are not valid.") + .into()); + } + + // 2. If newTarget is not present, set newTarget to %Temporal.Duration%. + let new_target = if let Some(target) = new_target { + target.clone() + } else { + context + .realm() + .intrinsics() + .constructors() + .duration() + .constructor() + .into() + }; + + // 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.Duration.prototype%", « [[InitializedTemporalDuration]], [[Years]], [[Months]], [[Weeks]], [[Days]], [[Hours]], [[Minutes]], [[Seconds]], [[Milliseconds]], [[Microseconds]], [[Nanoseconds]] »). + let prototype = + get_prototype_from_constructor(&new_target, StandardConstructors::duration, context)?; + + // 4. Set object.[[Years]] to ℝ(𝔽(years)). + // 5. Set object.[[Months]] to ℝ(𝔽(months)). + // 6. Set object.[[Weeks]] to ℝ(𝔽(weeks)). + // 7. Set object.[[Days]] to ℝ(𝔽(days)). + // 8. Set object.[[Hours]] to ℝ(𝔽(hours)). + // 9. Set object.[[Minutes]] to ℝ(𝔽(minutes)). + // 10. Set object.[[Seconds]] to ℝ(𝔽(seconds)). + // 11. Set object.[[Milliseconds]] to ℝ(𝔽(milliseconds)). + // 12. Set object.[[Microseconds]] to ℝ(𝔽(microseconds)). + // 13. Set object.[[Nanoseconds]] to ℝ(𝔽(nanoseconds)). + + let obj = + JsObject::from_proto_and_data(prototype, ObjectData::duration(Duration { inner: record })); + // 14. Return object. + Ok(obj) +} + +/// 7.5.23 `DaysUntil ( earlier, later )` +fn days_until(earlier: &JsObject, later: &JsObject) -> i32 { + // 1. Let epochDays1 be ISODateToEpochDays(earlier.[[ISOYear]], earlier.[[ISOMonth]] - 1, earlier.[[ISODay]]). + let obj = earlier.borrow(); + let date_one = obj + .as_plain_date() + .expect("earlier must be a PlainDate obj."); + + let epoch_days_one = date_one.inner.as_epoch_days(); + + drop(obj); + + // 2. Let epochDays2 be ISODateToEpochDays(later.[[ISOYear]], later.[[ISOMonth]] - 1, later.[[ISODay]]). + let obj = later.borrow(); + let date_two = obj + .as_plain_date() + .expect("earlier must be a PlainDate obj."); + + let epoch_days_two = date_two.inner.as_epoch_days(); + + // 3. Return epochDays2 - epochDays1. + epoch_days_two - epoch_days_one +} + +/// Abstract Operation 7.5.24 `MoveRelativeDate ( calendar, relativeTo, duration, dateAdd )` +fn move_relative_date( + calendar: &JsValue, + relative_to: &JsObject, + duration: &JsObject, + context: &mut Context<'_>, +) -> JsResult<(JsObject, f64)> { + let new_date = calendar::calendar_date_add(calendar, relative_to, duration, None, context)?; + let days = f64::from(days_until(relative_to, &new_date)); + Ok((new_date, days)) +} diff --git a/boa_engine/src/builtins/temporal/duration/record.rs b/boa_engine/src/builtins/temporal/duration/record.rs new file mode 100644 index 00000000000..d05f07aecb1 --- /dev/null +++ b/boa_engine/src/builtins/temporal/duration/record.rs @@ -0,0 +1,1820 @@ +use crate::{ + builtins::{ + options::RoundingMode, + temporal::{self, create_temporal_date, options::TemporalUnit, to_temporal_date}, + }, + js_string, + string::utf16, + Context, JsNativeError, JsObject, JsResult, JsValue, +}; + +use super::super::{calendar, to_integer_if_integral, zoned_date_time}; + +// ==== `DateDuration` ==== + +/// `DateDuration` represents the [date duration record][spec] of the `DurationRecord.` +/// +/// These fields are laid out in the [Temporal Proposal][field spec] as 64-bit floating point numbers. +/// +/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-date-duration-records +/// [field spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances +#[derive(Debug, Default, Clone, Copy)] +pub(crate) struct DateDuration { + years: f64, + months: f64, + weeks: f64, + days: f64, +} + +impl DateDuration { + pub(crate) const fn new(years: f64, months: f64, weeks: f64, days: f64) -> Self { + Self { + years, + months, + weeks, + days, + } + } + + pub(crate) const fn partial() -> Self { + Self { + years: f64::NAN, + months: f64::NAN, + weeks: f64::NAN, + days: f64::NAN, + } + } +} + +impl<'a> IntoIterator for &'a DateDuration { + type Item = f64; + type IntoIter = DateIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + DateIter { + date: self, + index: 0, + } + } +} + +pub(crate) struct DateIter<'a> { + date: &'a DateDuration, + index: usize, +} + +impl Iterator for DateIter<'_> { + type Item = f64; + + fn next(&mut self) -> Option { + let result = match self.index { + 0 => Some(self.date.years), + 1 => Some(self.date.months), + 2 => Some(self.date.weeks), + 3 => Some(self.date.days), + _ => None, + }; + self.index += 1; + result + } +} + +// ==== `TimeDuration` ==== + +/// `TimeDuration` represents the [Time Duration record][spec] of the `DurationRecord.` +/// +/// These fields are laid out in the [Temporal Proposal][field spec] as 64-bit floating point numbers. +/// +/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-time-duration-records +/// [field spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances +#[derive(Debug, Default, Clone, Copy)] +pub(crate) struct TimeDuration { + hours: f64, + minutes: f64, + seconds: f64, + milliseconds: f64, + microseconds: f64, + nanoseconds: f64, +} + +impl TimeDuration { + pub(crate) const fn new( + hours: f64, + minutes: f64, + seconds: f64, + milliseconds: f64, + microseconds: f64, + nanoseconds: f64, + ) -> Self { + Self { + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + } + } + + pub(crate) const fn partial() -> Self { + Self { + hours: f64::NAN, + minutes: f64::NAN, + seconds: f64::NAN, + milliseconds: f64::NAN, + microseconds: f64::NAN, + nanoseconds: f64::NAN, + } + } +} + +impl<'a> IntoIterator for &'a TimeDuration { + type Item = f64; + type IntoIter = TimeIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + TimeIter { + time: self, + index: 0, + } + } +} + +pub(crate) struct TimeIter<'a> { + time: &'a TimeDuration, + index: usize, +} + +impl Iterator for TimeIter<'_> { + type Item = f64; + + fn next(&mut self) -> Option { + let result = match self.index { + 0 => Some(self.time.hours), + 1 => Some(self.time.minutes), + 2 => Some(self.time.seconds), + 3 => Some(self.time.milliseconds), + 4 => Some(self.time.microseconds), + 5 => Some(self.time.nanoseconds), + _ => None, + }; + self.index += 1; + result + } +} + +// ==== `DurationRecord` ==== + +/// The `DurationRecord` is a native Rust implementation of the `Duration` builtin +/// object internal fields and is primarily defined by Abtract Operation 7.5.1-5. +#[derive(Debug, Clone, Copy, Default)] +pub(crate) struct DurationRecord { + date: DateDuration, + time: TimeDuration, +} + +impl DurationRecord { + pub(crate) const fn new(date: DateDuration, time: TimeDuration) -> Self { + Self { date, time } + } + + pub(crate) const fn partial() -> Self { + Self { + date: DateDuration::partial(), + time: TimeDuration::partial(), + } + } + + pub(crate) fn from_date_duration(date: DateDuration) -> Self { + Self { + date, + time: TimeDuration::default(), + } + } + + pub(crate) const fn from_day_and_time(day: f64, time: TimeDuration) -> Self { + Self { + date: DateDuration::new(0.0, 0.0, 0.0, day), + time, + } + } + + /// Equivalent to 7.5.13 `ToTemporalPartialDurationRecord ( temporalDurationLike )` + /// + /// Takes an unknown `JsObject` and attempts to create a partial duration + pub(crate) fn from_partial_js_object( + duration_like: &JsValue, + context: &mut Context<'_>, + ) -> JsResult { + // 1. If Type(temporalDurationLike) is not Object, then + let JsValue::Object(unknown_object) = duration_like else { + // a. Throw a TypeError exception. + return Err(JsNativeError::typ() + .with_message("temporalDurationLike must be an object.") + .into()); + }; + + // 2. Let result be a new partial Duration Record with each field set to undefined. + let mut result = Self::partial(); + + // 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(utf16!("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)?)); + } + + // 6. Let hours be ? Get(temporalDurationLike, "hours"). + let hours = unknown_object.get(utf16!("hours"), context)?; + // 7. If hours is not undefined, set result.[[Hours]] to ? ToIntegerIfIntegral(hours). + if !hours.is_undefined() { + result.set_days(f64::from(to_integer_if_integral(&hours, context)?)); + } + + // 8. Let microseconds be ? Get(temporalDurationLike, "microseconds"). + let microseconds = unknown_object.get(utf16!("microseconds"), context)?; + // 9. If microseconds is not undefined, set result.[[Microseconds]] to ? ToIntegerIfIntegral(microseconds). + if !microseconds.is_undefined() { + result.set_days(f64::from(to_integer_if_integral(µseconds, context)?)); + } + + // 10. Let milliseconds be ? Get(temporalDurationLike, "milliseconds"). + let milliseconds = unknown_object.get(utf16!("milliseconds"), context)?; + // 11. If milliseconds is not undefined, set result.[[Milliseconds]] to ? ToIntegerIfIntegral(milliseconds). + if !milliseconds.is_undefined() { + result.set_days(f64::from(to_integer_if_integral(&milliseconds, context)?)); + } + + // 12. Let minutes be ? Get(temporalDurationLike, "minutes"). + let minutes = unknown_object.get(utf16!("minutes"), context)?; + // 13. If minutes is not undefined, set result.[[Minutes]] to ? ToIntegerIfIntegral(minutes). + if !minutes.is_undefined() { + result.set_days(f64::from(to_integer_if_integral(&minutes, context)?)); + } + + // 14. Let months be ? Get(temporalDurationLike, "months"). + let months = unknown_object.get(utf16!("months"), context)?; + // 15. If months is not undefined, set result.[[Months]] to ? ToIntegerIfIntegral(months). + if !months.is_undefined() { + result.set_days(f64::from(to_integer_if_integral(&months, context)?)); + } + + // 16. Let nanoseconds be ? Get(temporalDurationLike, "nanoseconds"). + let nanoseconds = unknown_object.get(utf16!("nanoseconds"), context)?; + // 17. If nanoseconds is not undefined, set result.[[Nanoseconds]] to ? ToIntegerIfIntegral(nanoseconds). + if !nanoseconds.is_undefined() { + result.set_days(f64::from(to_integer_if_integral(&nanoseconds, context)?)); + } + + // 18. Let seconds be ? Get(temporalDurationLike, "seconds"). + let seconds = unknown_object.get(utf16!("seconds"), context)?; + // 19. If seconds is not undefined, set result.[[Seconds]] to ? ToIntegerIfIntegral(seconds). + if !seconds.is_undefined() { + result.set_days(f64::from(to_integer_if_integral(&seconds, context)?)); + } + + // 20. Let weeks be ? Get(temporalDurationLike, "weeks"). + let weeks = unknown_object.get(utf16!("weeks"), context)?; + // 21. If weeks is not undefined, set result.[[Weeks]] to ? ToIntegerIfIntegral(weeks). + if !weeks.is_undefined() { + result.set_days(f64::from(to_integer_if_integral(&weeks, context)?)); + } + + // 22. Let years be ? Get(temporalDurationLike, "years"). + let years = unknown_object.get(utf16!("years"), context)?; + // 23. If years is not undefined, set result.[[Years]] to ? ToIntegerIfIntegral(years). + if !years.is_undefined() { + result.set_days(f64::from(to_integer_if_integral(&years, context)?)); + } + + // 24. If years is undefined, and months is undefined, and weeks is undefined, and days 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.into_iter().all(f64::is_nan) { + return Err(JsNativeError::typ() + .with_message("no valid Duration fields on temporalDurationLike.") + .into()); + } + + // 25. Return result. + Ok(result) + } +} + +// -- `DurationRecord` bubble/balance methods -- + +impl DurationRecord { + /// Balance/bubble the current unit from one step down. + fn balance_hours(&mut self) { + // 1. Set hours to floor(minutes / 60). + self.set_hours((self.minutes() / 60_f64).floor()); + // 2. Set minutes to minutes modulo 60. + self.set_minutes(self.minutes() % 60_f64); + } + + /// Balance/bubble the current unit from one step down. + fn balance_minutes(&mut self) { + // 1. Set minutes to floor(seconds / 60). + self.set_minutes((self.seconds() / 60_f64).floor()); + // 2. Set seconds to seconds modulo 60. + self.set_seconds(self.seconds() % 60_f64); + } + + /// Balance/bubble the current unit from one step down. + fn balance_seconds(&mut self) { + // 1. Set seconds to floor(milliseconds / 1000). + self.set_seconds((self.milliseconds() / 1_000_f64).floor()); + // 2. Set milliseconds to milliseconds modulo 1000. + self.set_milliseconds(self.milliseconds() % 1_000_f64); + } + + /// Balance/bubble the current unit from one step down. + fn balance_milliseconds(&mut self) { + // c. Set milliseconds to floor(microseconds / 1000). + self.set_milliseconds((self.microseconds() / 1_000_f64).floor()); + // d. Set microseconds to microseconds modulo 1000. + self.set_microseconds(self.microseconds() % 1_000_f64); + } + + /// Balance/bubble the current unit from one step down. + fn balance_microseconds(&mut self) { + // a. Set microseconds to floor(nanoseconds / 1000). + self.set_microseconds((self.nanoseconds() / 1_000_f64).floor()); + // b. Set nanoseconds to nanoseconds modulo 1000. + self.set_nanoseconds(self.nanoseconds() % 1_000_f64); + } +} + +// ==== `DurationRecord` getter/setter methods ==== + +impl DurationRecord { + /// Return this `DurationRecord`'s `DateDuration` + pub(crate) const fn date(&self) -> DateDuration { + self.date + } + + /// Return this `DurationRecord`'s `TimeDuration` + pub(crate) const fn time(&self) -> TimeDuration { + self.time + } + + /// Set this `DurationRecord`'s `TimeDuration`. + pub(crate) fn set_time_duration(&mut self, time: TimeDuration) { + self.time = time; + } + + /// Set the value for `years`. + pub(crate) fn set_years(&mut self, y: f64) { + self.date.years = y; + } + + /// Return the value for `years`. + pub(crate) const fn years(&self) -> f64 { + self.date.years + } + + /// Set the value for `months`. + pub(crate) fn set_months(&mut self, mo: f64) { + self.date.months = mo; + } + + /// Return the value for `months`. + pub(crate) const fn months(&self) -> f64 { + self.date.months + } + + /// Set the value for `weeks`. + pub(crate) fn set_weeks(&mut self, w: f64) { + self.date.weeks = w; + } + + /// Return the value for `weeks`. + pub(crate) const fn weeks(&self) -> f64 { + self.date.weeks + } + + /// Set the value for `days`. + pub(crate) fn set_days(&mut self, d: f64) { + self.date.days = d; + } + + /// Return the value for `days`. + pub(crate) const fn days(&self) -> f64 { + self.date.days + } + + /// Set the value for `hours`. + pub(crate) fn set_hours(&mut self, h: f64) { + self.time.hours = h; + } + + /// Return the value for `hours`. + pub(crate) const fn hours(&self) -> f64 { + self.time.hours + } + + /// Set the value for `minutes`. + pub(crate) fn set_minutes(&mut self, m: f64) { + self.time.minutes = m; + } + + /// Return the value for `minutes`. + pub(crate) const fn minutes(&self) -> f64 { + self.time.minutes + } + + /// Set the value for `seconds`. + pub(crate) fn set_seconds(&mut self, s: f64) { + self.time.seconds = s; + } + + /// Return the value for `seconds`. + pub(crate) const fn seconds(&self) -> f64 { + self.time.seconds + } + + /// Set the value for `milliseconds`. + pub(crate) fn set_milliseconds(&mut self, ms: f64) { + self.time.milliseconds = ms; + } + + /// Return the value for `milliseconds`. + pub(crate) const fn milliseconds(&self) -> f64 { + self.time.milliseconds + } + + /// Set the value for `microseconds`. + pub(crate) fn set_microseconds(&mut self, mis: f64) { + self.time.microseconds = mis; + } + + /// Return the value for `microseconds`. + pub(crate) const fn microseconds(&self) -> f64 { + self.time.microseconds + } + + /// Set the value for `nanoseconds`. + pub(crate) fn set_nanoseconds(&mut self, ns: f64) { + self.time.nanoseconds = ns; + } + + /// Return the value for `nanoseconds`. + pub(crate) const fn nanoseconds(&self) -> f64 { + self.time.nanoseconds + } +} + +impl<'a> IntoIterator for &'a DurationRecord { + type Item = f64; + type IntoIter = DurationIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + DurationIter { + duration: self, + index: 0, + } + } +} + +pub(crate) struct DurationIter<'a> { + duration: &'a DurationRecord, + index: usize, +} + +impl Iterator for DurationIter<'_> { + type Item = f64; + + fn next(&mut self) -> Option { + let result = match self.index { + 0 => Some(self.duration.years()), + 1 => Some(self.duration.months()), + 2 => Some(self.duration.weeks()), + 3 => Some(self.duration.days()), + 4 => Some(self.duration.hours()), + 5 => Some(self.duration.minutes()), + 6 => Some(self.duration.seconds()), + 7 => Some(self.duration.milliseconds()), + 8 => Some(self.duration.microseconds()), + 9 => Some(self.duration.nanoseconds()), + _ => None, + }; + self.index += 1; + result + } +} + +// ==== DurationRecord method ==== + +impl DurationRecord { + pub(crate) fn abs(&self) -> Self { + Self { + date: DateDuration::new( + self.years().abs(), + self.months().abs(), + self.weeks().abs(), + self.days().abs(), + ), + time: TimeDuration::new( + self.hours().abs(), + self.minutes().abs(), + self.seconds().abs(), + self.milliseconds().abs(), + self.microseconds().abs(), + self.nanoseconds().abs(), + ), + } + } +} + +// ==== Abstract Operations implemented on `DurationRecord` ==== + +impl DurationRecord { + // TODO: look into making this destructive / Into. + // Trace current callers and check whether the value + // can be fed a native `DurationRecord` instead. + /// Creates a `Duration` object from the current `DurationRecord`. + pub(crate) fn as_object(&self, context: &mut Context<'_>) -> JsResult { + super::create_temporal_duration(*self, None, context) + } + + /// Returns the duration time values as a vec + fn time_values(&self) -> Vec { + self.time.into_iter().collect() + } + + // Note(nekevss): This currently assumes that an overflow has been stored into the years + // column as the duration is nonviable and storing it in years allows for invalidating + // the duration the fastest. + /// Determines if the `DurationRecord` has overflowed. + #[inline] + fn is_overfowed(&self) -> bool { + self.years().is_infinite() + } + + #[inline] + pub(crate) fn is_positive_overflow(&self) -> bool { + self.years().is_infinite() && self.years().is_sign_positive() + } + + #[inline] + pub(crate) fn is_negative_overflow(&self) -> bool { + self.years().is_infinite() && self.years().is_sign_negative() + } + + /// 7.5.10 `DurationSign ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds )` + /// + /// Determines the sign for the current self. + pub(crate) fn duration_sign(&self) -> i32 { + // 1. For each value v of « years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds », do + for v in self { + // a. If v < 0, return -1. + if v < 0_f64 { + return -1; + // b. If v > 0, return 1. + } else if v > 0_f64 { + return 1; + } + } + // 2. Return 0. + 0 + } + + /// 7.5.11 `IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds )` + /// + /// Checks if the current `DurationRecord` is a valid self. + pub(crate) fn is_valid_duration(&self) -> bool { + // 1. Let sign be ! DurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + let sign = self.duration_sign(); + // 2. For each value v of « years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds », do + for v in self { + // a. If 𝔽(v) is not finite, return false. + if !v.is_finite() { + return false; + } + // b. If v < 0 and sign > 0, return false. + if v < 0_f64 && sign > 0 { + return false; + } + // c. If v > 0 and sign < 0, return false. + if v > 0_f64 && sign < 0 { + return false; + } + } + // 3. Return true. + true + } + + /// 7.5.12 `DefaultTemporalLargestUnit ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds )` + pub(crate) fn default_temporal_largest_unit(&self) -> TemporalUnit { + for (index, value) in self.into_iter().enumerate() { + if value != 0.0 { + match index { + 0 => return TemporalUnit::Year, + 1 => return TemporalUnit::Month, + 2 => return TemporalUnit::Week, + 3 => return TemporalUnit::Day, + 4 => return TemporalUnit::Hour, + 5 => return TemporalUnit::Minute, + 6 => return TemporalUnit::Second, + 7 => return TemporalUnit::Millisecond, + 8 => return TemporalUnit::Microsecond, + _ => {} + } + } + } + + TemporalUnit::Nanosecond + } + + // TODO: implement on `DurationRecord` + /// 7.5.17 `TotalDurationNanoseconds ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, offsetShift )` + fn total_duration_nanoseconds(&self, offset_shift: f64) -> f64 { + let nanoseconds = if self.days() == 0_f64 { + self.nanoseconds() + } else { + self.nanoseconds() - offset_shift + }; + + self.days() + .mul_add(24_f64, self.hours()) + .mul_add(60_f64, self.minutes()) + .mul_add(60_f64, self.seconds()) + .mul_add(1_000_f64, self.milliseconds()) + .mul_add(1_000_f64, self.microseconds()) + .mul_add(1_000_f64, nanoseconds) + } + + /// Abstract Operation 7.5.18 `BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit [ , relativeTo ] )` + pub(crate) fn balance_time_duration( + &mut self, + largest_unit: TemporalUnit, + relative_to: Option<&JsValue>, + ) -> JsResult<()> { + // 1. Let balanceResult be ? BalancePossiblyInfiniteDuration(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit, relativeTo). + self.balance_possibly_infinite_duration(largest_unit, relative_to)?; + // 2. If balanceResult is positive overflow or negative overflow, then + if self.is_overfowed() { + // a. Throw a RangeError exception. + return Err(JsNativeError::range() + .with_message("duration overflowed viable range.") + .into()); + } + // 3. Else, + // a. Return balanceResult. + Ok(()) + } + + /// Abstract Operation 7.5.19 `BalancePossiblyInfiniteDuration ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit [ , relativeTo ] )` + pub(crate) fn balance_possibly_infinite_duration( + &mut self, + largest_unit: TemporalUnit, + relative_to: Option<&JsValue>, + ) -> JsResult<()> { + // 1. If relativeTo is not present, set relativeTo to undefined. + let relative_to = if let Some(value) = relative_to { + value.clone() + } else { + JsValue::undefined() + }; + + // 2. If Type(relativeTo) is Object and relativeTo has an [[InitializedTemporalZonedDateTime]] internal slot, then + if relative_to.is_object() + && relative_to + .as_object() + .expect("relative_to must be an object here.") + .is_zoned_date_time() + { + // TODO + // a. Let endNs be ? AddZonedDateTime(relativeTo.[[Nanoseconds]], relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + // b. Set nanoseconds to ℝ(endNs - relativeTo.[[Nanoseconds]]). + self.set_nanoseconds(0_f64); + // 3. Else, + } else { + // a. Set nanoseconds to ! TotalDurationNanoseconds(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 0). + self.set_nanoseconds(self.total_duration_nanoseconds(0.0)); + } + + match largest_unit { + // 4. If largestUnit is one of "year", "month", "week", or "day", then + TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week | TemporalUnit::Day => { + // a. Let result be ? NanosecondsToDays(nanoseconds, relativeTo). + let _result = temporal::zoned_date_time::nanoseconds_to_days( + self.nanoseconds(), + &relative_to, + ); + // b. Set days to result.[[Days]]. + // c. Set nanoseconds to result.[[Nanoseconds]]. + return Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()); + } + // 5. Else, + // a. Set days to 0. + _ => self.set_days(0_f64), + } + + // 6. Set hours, minutes, seconds, milliseconds, and microseconds to 0. + let new_time = TimeDuration::new(0_f64, 0_f64, 0_f64, 0_f64, 0_f64, self.nanoseconds()); + self.time = new_time; + + // 7. If nanoseconds < 0, let sign be -1; else, let sign be 1. + let sign = if self.nanoseconds() < 0_f64 { + -1_f64 + } else { + 1_f64 + }; + // 8. Set nanoseconds to abs(nanoseconds). + self.set_nanoseconds(self.nanoseconds().abs()); + + match largest_unit { + // 9. If largestUnit is "year", "month", "week", "day", or "hour", then + TemporalUnit::Year + | TemporalUnit::Month + | TemporalUnit::Week + | TemporalUnit::Day + | TemporalUnit::Hour => { + // a. Set microseconds to floor(nanoseconds / 1000). + // b. Set nanoseconds to nanoseconds modulo 1000. + self.balance_microseconds(); + + // c. Set milliseconds to floor(microseconds / 1000). + // d. Set microseconds to microseconds modulo 1000. + self.balance_milliseconds(); + + // e. Set seconds to floor(milliseconds / 1000). + // f. Set milliseconds to milliseconds modulo 1000. + self.balance_minutes(); + + // g. Set minutes to floor(seconds / 60). + // h. Set seconds to seconds modulo 60. + self.balance_minutes(); + + // i. Set hours to floor(minutes / 60). + // j. Set minutes to minutes modulo 60. + self.balance_hours(); + } + // 10. Else if largestUnit is "minute", then + TemporalUnit::Minute => { + // a. Set microseconds to floor(nanoseconds / 1000). + // b. Set nanoseconds to nanoseconds modulo 1000. + self.balance_microseconds(); + + // c. Set milliseconds to floor(microseconds / 1000). + // d. Set microseconds to microseconds modulo 1000. + self.balance_milliseconds(); + + // e. Set seconds to floor(milliseconds / 1000). + // f. Set milliseconds to milliseconds modulo 1000. + self.balance_seconds(); + + // g. Set minutes to floor(seconds / 60). + // h. Set seconds to seconds modulo 60. + self.balance_minutes(); + } + // 11. Else if largestUnit is "second", then + TemporalUnit::Second => { + // a. Set microseconds to floor(nanoseconds / 1000). + // b. Set nanoseconds to nanoseconds modulo 1000. + self.balance_microseconds(); + + // c. Set milliseconds to floor(microseconds / 1000). + // d. Set microseconds to microseconds modulo 1000. + self.balance_milliseconds(); + + // e. Set seconds to floor(milliseconds / 1000). + // f. Set milliseconds to milliseconds modulo 1000. + self.balance_seconds(); + } + // 12. Else if largestUnit is "millisecond", then + TemporalUnit::Millisecond => { + // a. Set microseconds to floor(nanoseconds / 1000). + // b. Set nanoseconds to nanoseconds modulo 1000. + self.balance_microseconds(); + + // c. Set milliseconds to floor(microseconds / 1000). + // d. Set microseconds to microseconds modulo 1000. + self.balance_milliseconds(); + } + // 13. Else if largestUnit is "microsecond", then + TemporalUnit::Microsecond => { + // a. Set microseconds to floor(nanoseconds / 1000). + // b. Set nanoseconds to nanoseconds modulo 1000. + self.balance_microseconds(); + } + // 14. Else, + // a. Assert: largestUnit is "nanosecond". + _ => assert!(largest_unit == TemporalUnit::Nanosecond), + } + + // 15. For each value v of « days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds », do + for value in self.time_values() { + // a. If 𝔽(v) is not finite, then + if !value.is_finite() { + // i. If sign = 1, then + if sign as i32 == 1 { + // 1. Return positive overflow. + self.set_years(f64::INFINITY); + return Ok(()); + } + // ii. Else if sign = -1, then + // 1. Return negative overflow. + self.set_years(f64::NEG_INFINITY); + return Ok(()); + } + } + + // NOTE (nekevss): diviate from spec here as the current implementation with `DurationRecord` means that we create the record and than mutate values. + // 16. Return ? CreateTimeDurationRecord(days, hours × sign, minutes × sign, seconds × sign, milliseconds × sign, microseconds × sign, nanoseconds × sign). + self.set_hours(self.hours() * sign); + self.set_minutes(self.minutes() * sign); + self.set_seconds(self.seconds() * sign); + self.set_milliseconds(self.milliseconds() * sign); + self.set_microseconds(self.microseconds() * sign); + self.set_nanoseconds(self.nanoseconds() * sign); + + // `CreateTimeDurationRecord` validates that the record that would be created is a valid duration, so validate here + if !self.is_valid_duration() { + return Err(JsNativeError::range() + .with_message("TimeDurationRecord was not a valid duration.") + .into()); + } + + Ok(()) + } + + /// 7.5.20 `UnbalanceDurationRelative ( years, months, weeks, days, largestUnit, relativeTo )` + pub(crate) fn unbalance_duration_relative( + &mut self, + largest_unit: TemporalUnit, + relative_to: &JsValue, + context: &mut Context<'_>, + ) -> JsResult<()> { + // 1. Let allZero be false. + // 2. If years = 0, and months = 0, and weeks = 0, and days = 0, set allZero to true. + let all_zero = self.years() == 0_f64 + && self.months() == 0_f64 + && self.weeks() == 0_f64 + && self.days() == 0_f64; + + // 3. If largestUnit is "year" or allZero is true, then + if largest_unit == TemporalUnit::Year || all_zero { + // a. Return ! CreateDateDurationRecord(years, months, weeks, days). + return Ok(()); + }; + + // 4. Let sign be ! DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0). + let sign = self.duration_sign(); + // 5. Assert: sign ≠ 0. + assert!(sign != 0); + + // 6. Let oneYear be ! CreateTemporalDuration(sign, 0, 0, 0, 0, 0, 0, 0, 0, 0). + let _one_year = Self::new( + DateDuration::new(f64::from(sign), 0.0, 0.0, 0.0), + TimeDuration::default(), + ); + // 7. Let oneMonth be ! CreateTemporalDuration(0, sign, 0, 0, 0, 0, 0, 0, 0, 0). + let _one_month = Self::new( + DateDuration::new(0.0, f64::from(sign), 0.0, 0.0), + TimeDuration::default(), + ); + // 8. Let oneWeek be ! CreateTemporalDuration(0, 0, sign, 0, 0, 0, 0, 0, 0, 0). + let _one_week = Self::new( + DateDuration::new(0.0, 0.0, f64::from(sign), 0.0), + TimeDuration::default(), + ); + + // 9. If relativeTo is not undefined, then + let _calendar = if relative_to.is_undefined() { + // 10. Else + // a. Let calendar be undefined. + None + } else { + // a. Set relativeTo to ? ToTemporalDate(relativeTo). + let relative_to = to_temporal_date( + &relative_to + .as_object() + .expect("relative_to must be an object") + .clone() + .into(), + None, + context, + )?; + + // b. Let calendar be relativeTo.[[Calendar]]. + let calendar = relative_to.calendar; + + Some(calendar) + }; + + // 11. If largestUnit is "month", then + // a. If calendar is undefined, then + // i. Throw a RangeError exception. + // b. If calendar is an Object, then + // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). + // ii. Let dateUntil be ? GetMethod(calendar, "dateUntil"). + // c. Else, + // i. Let dateAdd be unused. + // ii. Let dateUntil be unused. + // d. Repeat, while years ≠ 0, + // i. Let newRelativeTo be ? CalendarDateAdd(calendar, relativeTo, oneYear, undefined, dateAdd). + // ii. Let untilOptions be OrdinaryObjectCreate(null). + // iii. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). + // iv. Let untilResult be ? CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil). + // v. Let oneYearMonths be untilResult.[[Months]]. + // vi. Set relativeTo to newRelativeTo. + // vii. Set years to years - sign. + // viii. Set months to months + oneYearMonths. + // 12. Else if largestUnit is "week", then + // a. If calendar is undefined, then + // i. Throw a RangeError exception. + // b. If calendar is an Object, then + // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). + // c. Else, + // i. Let dateAdd be unused. + // d. Repeat, while years ≠ 0, + // i. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd). + // ii. Set relativeTo to moveResult.[[RelativeTo]]. + // iii. Set days to days + moveResult.[[Days]]. + // iv. Set years to years - sign. + // e. Repeat, while months ≠ 0, + // i. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). + // ii. Set relativeTo to moveResult.[[RelativeTo]]. + // iii. Set days to days + moveResult.[[Days]]. + // iv. Set months to months - sign. + // 13. Else, + // a. If years ≠ 0, or months ≠ 0, or weeks ≠ zero, then + // i. If calendar is undefined, then + // 1. Throw a RangeError exception. + // ii. If calendar is an Object, then + // 1. Let dateAdd be ? GetMethod(calendar, "dateAdd"). + // iii. Else, + // 1. Let dateAdd be unused. + // iv. Repeat, while years ≠ 0, + // 1. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd). + // 2. Set relativeTo to moveResult.[[RelativeTo]]. + // 3. Set days to days + moveResult.[[Days]]. + // 4. Set years to years - sign. + // v. Repeat, while months ≠ 0, + // 1. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). + // 2. Set relativeTo to moveResult.[[RelativeTo]]. + // 3. Set days to days +moveResult.[[Days]]. + // 4. Set months to months - sign. + // vi. Repeat, while weeks ≠ 0, + // 1. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd). + // 2. Set relativeTo to moveResult.[[RelativeTo]]. + // 3. Set days to days + moveResult.[[Days]]. + // 4. Set weeks to weeks - sign. + // 14. Return ? CreateDateDurationRecord(years, months, weeks, days). + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } + + /// `BalanceDateDurationRelative` + #[allow(unused)] + pub(crate) fn balance_date_duration_relative( + &mut self, + largest_unit: TemporalUnit, + relative_to: &JsValue, + context: &mut Context<'_>, + ) -> JsResult<()> { + // 1. Let allZero be false. + // 2. If years = 0, and months = 0, and weeks = 0, and days = 0, set allZero to true. + let all_zero = self.years() == 0.0 + && self.months() == 0.0 + && self.weeks() == 0.0 + && self.days() == 0.0; + + // 3. If largestUnit is not one of "year", "month", or "week", or allZero is true, then + match largest_unit { + TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week if !all_zero => {} + _ => { + // a. Return ! CreateDateDurationRecord(years, months, weeks, days). + return Ok(()); + } + } + + // 4. If relativeTo is undefined, then + if relative_to.is_undefined() { + // a. Throw a RangeError exception. + return Err(JsNativeError::range() + .with_message("relativeTo cannot be undefined.") + .into()); + } + + // 5. Let sign be ! DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0). + // 6. Assert: sign ≠ 0. + let sign = self.duration_sign(); + + // 7. Let oneYear be ! CreateTemporalDuration(sign, 0, 0, 0, 0, 0, 0, 0, 0, 0). + let one_year = Self::new( + DateDuration::new(f64::from(sign), 0.0, 0.0, 0.0), + TimeDuration::default(), + ); + // 8. Let oneMonth be ! CreateTemporalDuration(0, sign, 0, 0, 0, 0, 0, 0, 0, 0). + let one_month = Self::new( + DateDuration::new(0.0, f64::from(sign), 0.0, 0.0), + TimeDuration::default(), + ); + // 9. Let oneWeek be ! CreateTemporalDuration(0, 0, sign, 0, 0, 0, 0, 0, 0, 0). + let _one_week = Self::new( + DateDuration::new(0.0, 0.0, f64::from(sign), 0.0), + TimeDuration::default(), + ); + + // 10. Set relativeTo to ? ToTemporalDate(relativeTo). + let date = to_temporal_date(relative_to, None, context)?; + + // 11. Let calendar be relativeTo.[[Calendar]]. + let calendar = &date.calendar.clone(); + + let relative_to = create_temporal_date(date.inner, date.calendar, None, context)?; + + match largest_unit { + // 12. If largestUnit is "year", then + TemporalUnit::Year => { + // a. If calendar is an Object, then + // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). + // b. Else, + // i. Let dateAdd be unused. + // c. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd). + let move_result = super::move_relative_date( + calendar, + &relative_to, + &one_year.as_object(context)?, + context, + )?; + + // d. Let newRelativeTo be moveResult.[[RelativeTo]]. + let mut new_relative = move_result.0; + // e. Let oneYearDays be moveResult.[[Days]]. + let mut one_year_days = move_result.1; + + // f. Repeat, while abs(days) ≥ abs(oneYearDays), + while self.days().abs() >= one_year_days.abs() { + // i. Set days to days - oneYearDays. + self.set_days(self.days() - one_year_days); + + // ii. Set years to years + sign. + self.set_years(self.years() + f64::from(sign)); + + // iii. Set relativeTo to newRelativeTo. + let relative_to = new_relative; + // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd). + let move_result = super::move_relative_date( + calendar, + &relative_to, + &one_year.as_object(context)?, + context, + )?; + + // v. Set newRelativeTo to moveResult.[[RelativeTo]]. + new_relative = move_result.0; + // vi. Set oneYearDays to moveResult.[[Days]]. + one_year_days = move_result.1; + } + + // g. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). + let move_result = super::move_relative_date( + calendar, + &relative_to, + &one_month.as_object(context)?, + context, + )?; + + // h. Set newRelativeTo to moveResult.[[RelativeTo]]. + let mut new_relative = move_result.0; + // i. Let oneMonthDays be moveResult.[[Days]]. + let mut one_month_days = move_result.1; + + // j. Repeat, while abs(days) ≥ abs(oneMonthDays), + while self.days().abs() >= one_month_days.abs() { + // i. Set days to days - oneMonthDays. + self.set_days(self.days() - one_month_days); + // ii. Set months to months + sign. + self.set_months(self.months() + f64::from(sign)); + // iii. Set relativeTo to newRelativeTo. + + let relative_to = new_relative; + // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). + let move_result = super::move_relative_date( + calendar, + &relative_to, + &one_month.as_object(context)?, + context, + )?; + + // v. Set newRelativeTo to moveResult.[[RelativeTo]]. + new_relative = move_result.0; + // vi. Set oneMonthDays to moveResult.[[Days]]. + one_month_days = move_result.1; + } + + // k. Set newRelativeTo to ? CalendarDateAdd(calendar, relativeTo, oneYear, undefined, dateAdd). + // l. If calendar is an Object, then + // i. Let dateUntil be ? GetMethod(calendar, "dateUntil"). + // m. Else, + // i. Let dateUntil be unused. + // n. Let untilOptions be OrdinaryObjectCreate(null). + // o. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). + // p. Let untilResult be ? CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil). + // q. Let oneYearMonths be untilResult.[[Months]]. + // r. Repeat, while abs(months) ≥ abs(oneYearMonths), + // i. Set months to months - oneYearMonths. + // ii. Set years to years + sign. + // iii. Set relativeTo to newRelativeTo. + // iv. Set newRelativeTo to ? CalendarDateAdd(calendar, relativeTo, oneYear, undefined, dateAdd). + // v. Set untilOptions to OrdinaryObjectCreate(null). + // vi. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). + // vii. Set untilResult to ? CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil). + // viii. Set oneYearMonths to untilResult.[[Months]]. + } + // 13. Else if largestUnit is "month", then + TemporalUnit::Month => { + // a. If calendar is an Object, then + // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). + // b. Else, + // i. Let dateAdd be unused. + // c. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). + // d. Let newRelativeTo be moveResult.[[RelativeTo]]. + // e. Let oneMonthDays be moveResult.[[Days]]. + // f. Repeat, while abs(days) ≥ abs(oneMonthDays), + // i. Set days to days - oneMonthDays. + // ii. Set months to months + sign. + // iii. Set relativeTo to newRelativeTo. + // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). + // v. Set newRelativeTo to moveResult.[[RelativeTo]]. + // vi. Set oneMonthDays to moveResult.[[Days]]. + } + // 14. Else, + TemporalUnit::Week => { + // a. Assert: largestUnit is "week". + // b. If calendar is an Object, then + // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). + // c. Else, + // i. Let dateAdd be unused. + // d. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd). + // e. Let newRelativeTo be moveResult.[[RelativeTo]]. + // f. Let oneWeekDays be moveResult.[[Days]]. + // g. Repeat, while abs(days) ≥ abs(oneWeekDays), + // i. Set days to days - oneWeekDays. + // ii. Set weeks to weeks + sign. + // iii. Set relativeTo to newRelativeTo. + // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd). + // v. Set newRelativeTo to moveResult.[[RelativeTo]]. + // vi. Set oneWeekDays to moveResult.[[Days]]. + todo!("week not implemented yet.") + } + _ => unreachable!(), + } + + // 15. Return ! CreateDateDurationRecord(years, months, weeks, days). + + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } + + /// Abstract Operation 7.5.26 `RoundDuration ( years, months, weeks, days, hours, minutes, + /// seconds, milliseconds, microseconds, nanoseconds, increment, unit, + /// roundingMode [ , relativeTo ] )` + pub(crate) fn round_duration( + &mut self, + increment: f64, + unit: TemporalUnit, + rounding_mode: RoundingMode, + relative_to: Option<&JsValue>, + context: &mut Context<'_>, + ) -> JsResult { + // 1. If relativeTo is not present, set relativeTo to undefined. + let relative_to = if let Some(val) = relative_to { + val.clone() + } else { + JsValue::undefined() + }; + + // 2. If unit is "year", "month", or "week", and relativeTo is undefined, then + if relative_to.is_undefined() + && (unit == TemporalUnit::Year + || unit == TemporalUnit::Month + || unit == TemporalUnit::Week) + { + // a. Throw a RangeError exception. + return Err(JsNativeError::range() + .with_message("relativeTo was out of range while rounding self.") + .into()); + } + + // TODO: Handle `ZonedDateTime` + // 3. Let zonedRelativeTo be undefined. + let zoned_relative_to = JsValue::undefined(); + + // 4. If relativeTo is not undefined, then + let (calendar, relative_to) = if relative_to.is_object() { + let relative_to_obj = relative_to.as_object().expect( + "relativeTo must be a Temporal.ZonedDateTime or Temporal.PlainDate object if defined.", + ); + // a. If relativeTo has an [[InitializedTemporalZonedDateTime]] internal slot, then + if relative_to_obj.is_zoned_date_time() { + // i. Set zonedRelativeTo to relativeTo. + // TODO: ii. Set relativeTo to ? ToTemporalDate(relativeTo). + return Err(JsNativeError::range() + .with_message("ZonedDateTime is not yet implemented.") + .into()); + // b. Else, + }; + + let obj = relative_to_obj.borrow(); + let plain_date = obj.as_plain_date().expect("object must be a PlainDate"); + + // c. Let calendar be relativeTo.[[Calendar]]. + let calendar = plain_date.calendar.clone(); + + drop(obj); + + (Some(calendar), Some(relative_to_obj)) + // 5. Else, + } else { + // a. NOTE: calendar will not be used below. + (None, None) + }; + + // 6. If unit is one of "year", "month", "week", or "day", then + let fractional_secs = match unit { + TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week | TemporalUnit::Day => { + // a. Let nanoseconds be ! TotalDurationNanoseconds(0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 0). + let nanoseconds = + Self::from_day_and_time(0.0, self.time()).total_duration_nanoseconds(0.0); + + // b. Let intermediate be undefined. + let intermediate = JsValue::undefined(); + // c. If zonedRelativeTo is not undefined, then + if !zoned_relative_to.is_undefined() { + // i. Let intermediate be ? MoveRelativeZonedDateTime(zonedRelativeTo, years, months, weeks, days). + return Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()); + } + // d. Let result be ? NanosecondsToDays(nanoseconds, intermediate). + let result = zoned_date_time::nanoseconds_to_days(nanoseconds, &intermediate)?; + + // e. Set days to days + result.[[Days]] + result.[[Nanoseconds]] / result.[[DayLength]]. + let days = self.days() as i32; + self.set_days(f64::from(days + result.0 + result.1 / result.2)); + + // f. Set hours, minutes, seconds, milliseconds, microseconds, and nanoseconds to 0. + self.set_time_duration(TimeDuration::default()); + + 0_f64 + } + // 7. Else, + _ => { + // a. Let fractionalSeconds be nanoseconds × 10-9 + microseconds × 10-6 + milliseconds × 10-3 + seconds. + self.seconds().mul_add( + 1000_f64, + self.nanoseconds() + .mul_add(1_000_000_000_f64, self.microseconds() * 1_000_000_f64), + ) + } + }; + + // 8. Let remainder be undefined. + // We begin matching against unit and return the remainder value. + let remainder = match unit { + // 9. If unit is "year", then + TemporalUnit::Year => { + // This should be safe as we throw a range error if relative_to does not exist. + assert!(calendar.is_some() && relative_to.is_some()); + let calendar_obj = calendar.expect("calendar must exist at this point."); + let relative_to = relative_to.expect("relative_to must exist at this point."); + + // a. Let yearsDuration be ! CreateTemporalDuration(years, 0, 0, 0, 0, 0, 0, 0, 0, 0). + let years_duration = super::create_temporal_duration( + Self::new( + DateDuration::new(self.years(), 0.0, 0.0, 0.0), + TimeDuration::default(), + ), + None, + context, + )?; + + // b. If calendar is an Object, then + // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). + // c. Else, + // i. Let dateAdd be unused. + // d. Let yearsLater be ? CalendarDateAdd(calendar, relativeTo, yearsDuration, undefined, dateAdd). + + let years_later = calendar::calendar_date_add( + &calendar_obj, + relative_to, + &years_duration, + None, + context, + )?; + + // e. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0). + let years_months_weeks = super::create_temporal_duration( + Self::new( + DateDuration::new(self.years(), self.months(), self.weeks(), 0.0), + TimeDuration::default(), + ), + None, + context, + )?; + + // f. Let yearsMonthsWeeksLater be ? CalendarDateAdd(calendar, relativeTo, yearsMonthsWeeks, undefined, dateAdd). + let years_months_weeks_later = calendar::calendar_date_add( + &calendar_obj, + relative_to, + &years_months_weeks, + None, + context, + )?; + + // g. Let monthsWeeksInDays be DaysUntil(yearsLater, yearsMonthsWeeksLater). + let months_weeks_in_days = + super::days_until(&years_later, &years_months_weeks_later); + + // h. Set relativeTo to yearsLater. + let relative_to = years_later; + + // i. Let days be days + monthsWeeksInDays. + self.set_days(self.days() + f64::from(months_weeks_in_days)); + + // j. Let wholeDaysDuration be ? CreateTemporalDuration(0, 0, 0, truncate(days), 0, 0, 0, 0, 0, 0). + let whole_days_duration = super::create_temporal_duration( + Self::new( + DateDuration::new(0.0, 0.0, 0.0, self.days().trunc()), + TimeDuration::default(), + ), + None, + context, + )?; + + // k. Let wholeDaysLater be ? CalendarDateAdd(calendar, relativeTo, wholeDaysDuration, undefined, dateAdd). + let whole_days_later = calendar::calendar_date_add( + &calendar_obj, + &relative_to, + &whole_days_duration, + None, + context, + )?; + + // l. Let untilOptions be OrdinaryObjectCreate(null). + let until_options = JsObject::with_null_proto(); + // m. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "year"). + until_options.create_data_property_or_throw( + utf16!("largestUnit"), + js_string!("year"), + context, + )?; + + // n. Let timePassed be ? CalendarDateUntil(calendar, relativeTo, wholeDaysLater, untilOptions). + let time_passed = calendar::calendar_date_until( + &calendar_obj, + &relative_to, + &whole_days_later, + &until_options.into(), + context, + )?; + + // o. Let yearsPassed be timePassed.[[Years]]. + let years_passed = time_passed.years(); + // p. Set years to years + yearsPassed. + self.set_years(self.years() + years_passed); + + // q. Let oldRelativeTo be relativeTo. + let old_relative_to = relative_to.clone(); + + // r. Let yearsDuration be ! CreateTemporalDuration(yearsPassed, 0, 0, 0, 0, 0, 0, 0, 0, 0). + let years_duration = super::create_temporal_duration( + Self::new( + DateDuration::new(years_passed, 0.0, 0.0, 0.0), + TimeDuration::default(), + ), + None, + context, + )?; + + // s. Set relativeTo to ? CalendarDateAdd(calendar, relativeTo, yearsDuration, undefined, dateAdd). + let relative_to = calendar::calendar_date_add( + &calendar_obj, + &relative_to, + &years_duration, + None, + context, + )?; + + // t. Let daysPassed be DaysUntil(oldRelativeTo, relativeTo). + let days_passed = super::days_until(&old_relative_to, &relative_to); + + // u. Set days to days - daysPassed. + self.set_days(self.days() - f64::from(days_passed)); + + // v. If days < 0, let sign be -1; else, let sign be 1. + let sign = if self.days() < 0_f64 { -1 } else { 1 }; + + // w. Let oneYear be ! CreateTemporalDuration(sign, 0, 0, 0, 0, 0, 0, 0, 0, 0). + let one_year = super::create_temporal_duration( + Self::new( + DateDuration::new(f64::from(sign), 0.0, 0.0, 0.0), + TimeDuration::default(), + ), + None, + context, + )?; + + // x. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd). + let move_result = + super::move_relative_date(&calendar_obj, &relative_to, &one_year, context)?; + + // y. Let oneYearDays be moveResult.[[Days]]. + let one_year_days = move_result.1; + // z. Let fractionalYears be years + days / abs(oneYearDays). + let fractional_years = self.years() + self.days() / one_year_days.abs(); + + // ?. Set years to RoundNumberToIncrement(fractionalYears, increment, roundingMode). + self.set_years(temporal::round_number_to_increment( + fractional_years, + increment, + rounding_mode, + )); + + // ?. Set months, weeks, and days to 0. + self.set_months(0_f64); + self.set_weeks(0_f64); + self.set_days(0_f64); + + fractional_years - self.years() + } + // 10. Else if unit is "month", then + TemporalUnit::Month => { + let mut relative_to = relative_to + .expect("relative_to must exist if unit is a month") + .clone(); + let calendar_obj = calendar.expect("calendar must exist at this point."); + + // a. Let yearsMonths be ! CreateTemporalDuration(years, months, 0, 0, 0, 0, 0, 0, 0, 0). + let years_month = super::create_temporal_duration( + Self::new( + DateDuration::new(self.years(), self.months(), 0.0, 0.0), + TimeDuration::default(), + ), + None, + context, + )?; + + // b. If calendar is an Object, then + // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). + // c. Else, + // i. Let dateAdd be unused. + + // d. Let yearsMonthsLater be ? CalendarDateAdd(calendar, relativeTo, yearsMonths, undefined, dateAdd). + let years_months_later = calendar::calendar_date_add( + &calendar_obj, + &relative_to, + &years_month, + None, + context, + )?; + + // e. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0). + let years_months_weeks = super::create_temporal_duration( + Self::new( + DateDuration::new(self.years(), self.months(), self.weeks(), 0.0), + TimeDuration::default(), + ), + None, + context, + )?; + + // f. Let yearsMonthsWeeksLater be ? CalendarDateAdd(calendar, relativeTo, yearsMonthsWeeks, undefined, dateAdd). + let years_months_weeks_later = calendar::calendar_date_add( + &calendar_obj, + &relative_to, + &years_months_weeks, + None, + context, + )?; + // g. Let weeksInDays be DaysUntil(yearsMonthsLater, yearsMonthsWeeksLater). + let weeks_in_days = + super::days_until(&years_months_later, &years_months_weeks_later); + + // h. Set relativeTo to yearsMonthsLater. + relative_to = years_months_later; + + // i. Let days be days + weeksInDays. + self.set_days(self.days() + f64::from(weeks_in_days)); + + // j. If days < 0, let sign be -1; else, let sign be 1. + let sign = if self.days() < 0_f64 { -1_f64 } else { 1_f64 }; + + // k. Let oneMonth be ! CreateTemporalDuration(0, sign, 0, 0, 0, 0, 0, 0, 0, 0). + let one_month = super::create_temporal_duration( + Self::new( + DateDuration::new(0.0, sign, 0.0, 0.0), + TimeDuration::default(), + ), + None, + context, + )?; + + // l. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). + let move_result = + super::move_relative_date(&calendar_obj, &relative_to, &one_month, context)?; + + // m. Set relativeTo to moveResult.[[RelativeTo]]. + relative_to = move_result.0; + // n. Let oneMonthDays be moveResult.[[Days]]. + let mut one_month_days = move_result.1; + + // o. Repeat, while abs(days) ≥ abs(oneMonthDays), + while self.days().abs() >= one_month_days.abs() { + // i. Set months to months + sign. + self.set_months(self.months() + sign); + // ii. Set days to days - oneMonthDays. + self.set_days(self.days() - one_month_days); + // iii. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). + let move_result = super::move_relative_date( + &calendar_obj, + &relative_to, + &one_month.clone(), + context, + )?; + + // iv. Set relativeTo to moveResult.[[RelativeTo]]. + relative_to = move_result.0; + // v. Set oneMonthDays to moveResult.[[Days]]. + one_month_days = move_result.1; + } + + // p. Let fractionalMonths be months + days / abs(oneMonthDays). + let fractional_months = self.months() + (self.days() / one_month_days.abs()); + // q. Set months to RoundNumberToIncrement(fractionalMonths, increment, roundingMode). + self.set_months(temporal::round_number_to_increment( + fractional_months, + increment, + rounding_mode, + )); + // r. Set remainder to fractionalMonths - months. + // s. Set weeks and days to 0. + self.set_weeks(0_f64); + self.set_days(0_f64); + fractional_months - self.months() + } + // 11. Else if unit is "week", then + TemporalUnit::Week => { + let mut relative_to = relative_to + .expect("relative_to must exist if unit is a month") + .clone(); + let calendar_obj = calendar.expect("calendar must exist at this point."); + // a. If days < 0, let sign be -1; else, let sign be 1. + let sign = if self.days() < 0_f64 { -1_f64 } else { 1_f64 }; + // b. Let oneWeek be ! CreateTemporalDuration(0, 0, sign, 0, 0, 0, 0, 0, 0, 0). + let one_week = super::create_temporal_duration( + Self::new( + DateDuration::new(0.0, 0.0, sign, 0.0), + TimeDuration::default(), + ), + None, + context, + )?; + + // c. If calendar is an Object, then + // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). + // d. Else, + // i. Let dateAdd be unused. + + // e. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd). + let move_result = + super::move_relative_date(&calendar_obj, &relative_to, &one_week, context)?; + + // f. Set relativeTo to moveResult.[[RelativeTo]]. + relative_to = move_result.0; + // g. Let oneWeekDays be moveResult.[[Days]]. + let mut one_week_days = move_result.1; + + // h. Repeat, while abs(days) ≥ abs(oneWeekDays), + while one_week_days.abs() <= self.days().abs() { + // i. Set weeks to weeks + sign. + self.set_weeks(self.weeks() + sign); + // ii. Set days to days - oneWeekDays. + self.set_days(self.days() - one_week_days); + // iii. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd). + let move_result = super::move_relative_date( + &calendar_obj, + &relative_to, + &one_week.clone(), + context, + )?; + // iv. Set relativeTo to moveResult.[[RelativeTo]]. + relative_to = move_result.0; + // v. Set oneWeekDays to moveResult.[[Days]]. + one_week_days = move_result.1; + } + + // i. Let fractionalWeeks be weeks + days / abs(oneWeekDays). + let fractional_weeks = self.weeks() + (self.days() / one_week_days.abs()); + + // j. Set weeks to RoundNumberToIncrement(fractionalWeeks, increment, roundingMode). + self.set_weeks(temporal::round_number_to_increment( + fractional_weeks, + increment, + rounding_mode, + )); + // k. Set remainder to fractionalWeeks - weeks. + // l. Set days to 0. + self.set_days(0_f64); + fractional_weeks - self.weeks() + } + // 12. Else if unit is "day", then + TemporalUnit::Day => { + // a. Let fractionalDays be days. + let fractional_days = self.days(); + // b. Set days to RoundNumberToIncrement(days, increment, roundingMode). + self.set_days(temporal::round_number_to_increment( + self.days(), + increment, + rounding_mode, + )); + // c. Set remainder to fractionalDays - days. + fractional_days - self.days() + } + // 13. Else if unit is "hour", then + TemporalUnit::Hour => { + // a. Let fractionalHours be (fractionalSeconds / 60 + minutes) / 60 + hours. + let fractional_hours = + (fractional_secs / (60_f64 + self.minutes())) / 60_f64 + self.hours(); + // b. Set hours to RoundNumberToIncrement(fractionalHours, increment, roundingMode). + self.set_hours(temporal::round_number_to_increment( + fractional_hours, + increment, + rounding_mode, + )); + // d. Set minutes, seconds, milliseconds, microseconds, and nanoseconds to 0. + self.set_time_duration(TimeDuration::new(self.hours(), 0.0, 0.0, 0.0, 0.0, 0.0)); + + // c. Set remainder to fractionalHours - hours. + fractional_hours - self.hours() + } + // 14. Else if unit is "minute", then + TemporalUnit::Minute => { + // a. Let fractionalMinutes be fractionalSeconds / 60 + minutes. + let fraction_minutes = fractional_secs / 60_f64 + self.minutes(); + // b. Set minutes to RoundNumberToIncrement(fractionalMinutes, increment, roundingMode). + self.set_minutes(temporal::round_number_to_increment( + fraction_minutes, + increment, + rounding_mode, + )); + // d. Set seconds, milliseconds, microseconds, and nanoseconds to 0. + self.set_seconds(0_f64); + self.set_milliseconds(0_f64); + self.set_microseconds(0_f64); + self.set_nanoseconds(0_f64); + // c. Set remainder to fractionalMinutes - minutes. + fraction_minutes - self.minutes() + } + // 15. Else if unit is "second", then + TemporalUnit::Second => { + // a. Set seconds to RoundNumberToIncrement(fractionalSeconds, increment, roundingMode). + self.set_seconds(temporal::round_number_to_increment( + fractional_secs, + increment, + rounding_mode, + )); + // c. Set milliseconds, microseconds, and nanoseconds to 0. + self.set_milliseconds(0_f64); + self.set_microseconds(0_f64); + self.set_nanoseconds(0_f64); + // b. Set remainder to fractionalSeconds - seconds. + fractional_secs - self.seconds() + } + // 16. Else if unit is "millisecond", then + TemporalUnit::Millisecond => { + // a. Let fractionalMilliseconds be nanoseconds × 10-6 + microseconds × 10-3 + milliseconds. + let fractional_millis = self + .nanoseconds() + .mul_add(1_000_000_f64, self.microseconds() * 1_000_f64) + + self.milliseconds(); + // b. Set milliseconds to RoundNumberToIncrement(fractionalMilliseconds, increment, roundingMode). + self.set_milliseconds(temporal::round_number_to_increment( + fractional_millis, + increment, + rounding_mode, + )); + // d. Set microseconds and nanoseconds to 0. + self.set_microseconds(0_f64); + self.set_nanoseconds(0_f64); + // c. Set remainder to fractionalMilliseconds - milliseconds. + fractional_millis - self.milliseconds() + } + // 17. Else if unit is "microsecond", then + TemporalUnit::Microsecond => { + // a. Let fractionalMicroseconds be nanoseconds × 10-3 + microseconds. + let fractional_micros = self.nanoseconds().mul_add(1_000_f64, self.microseconds()); + // b. Set microseconds to RoundNumberToIncrement(fractionalMicroseconds, increment, roundingMode). + self.set_microseconds(temporal::round_number_to_increment( + fractional_micros, + increment, + rounding_mode, + )); + // d. Set nanoseconds to 0. + self.set_nanoseconds(0_f64); + // c. Set remainder to fractionalMicroseconds - microseconds. + fractional_micros - self.microseconds() + } + // 18. Else, + TemporalUnit::Nanosecond => { + // a. Assert: unit is "nanosecond". + // b. Set remainder to nanoseconds. + let remainder = self.nanoseconds(); + // c. Set nanoseconds to RoundNumberToIncrement(nanoseconds, increment, roundingMode). + self.set_nanoseconds(temporal::round_number_to_increment( + self.nanoseconds(), + increment, + rounding_mode, + )); + // d. Set remainder to remainder - nanoseconds. + remainder - self.nanoseconds() + } + TemporalUnit::Auto => unreachable!(), + }; + + // 19. Assert: days is an integer. + assert!(self.days().fract() == 0.0); + + // 20. Let duration be ? CreateDurationRecord(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + // 21. Return the Record { [[DurationRecord]]: duration, [[Remainder]]: remainder }. + Ok(remainder) + } + + /// 7.5.27 `AdjustRoundedDurationDays ( years, months, weeks, days, hours, minutes, seconds, milliseconds, + /// microseconds, nanoseconds, increment, unit, roundingMode, relativeTo )` + #[allow(unused)] + pub(crate) fn adjust_rounded_duration_days( + &mut self, + increment: f64, + unit: TemporalUnit, + rounding_mode: RoundingMode, + relative_to: Option<&JsValue>, + context: &mut Context<'_>, + ) -> JsResult<()> { + // 1. If Type(relativeTo) is not Object; or relativeTo does not have an [[InitializedTemporalZonedDateTime]] + // internal slot; or unit is one of "year", "month", "week", or "day"; or unit is "nanosecond" and increment is 1, then + let relative_to = match relative_to { + Some(rt) + if rt.is_object() + && rt.as_object().expect("must be object").is_zoned_date_time() => + { + let obj = rt.as_object().expect("This must be an object."); + let obj = obj.borrow(); + // a. Return ! CreateDurationRecord(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + obj.as_zoned_date_time() + .expect("Object must be a ZonedDateTime.") + .clone() + } + _ => return Ok(()), + }; + + match unit { + // a. Return ! CreateDurationRecord(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week | TemporalUnit::Day => { + return Ok(()) + } + // a. Return ! CreateDurationRecord(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + TemporalUnit::Nanosecond if (increment - 1_f64).abs() < f64::EPSILON => return Ok(()), + _ => {} + } + + // 2. Let timeRemainderNs be ! TotalDurationNanoseconds(0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 0). + let time_remainder_ns = + Self::from_day_and_time(0.0, self.time()).total_duration_nanoseconds(0.0); + + // 3. If timeRemainderNs = 0, let direction be 0. + let _direction = if time_remainder_ns == 0_f64 { + 0 + // 4. Else if timeRemainderNs < 0, let direction be -1. + } else if time_remainder_ns < 0_f64 { + -1 + // 5. Else, let direction be 1. + } else { + 1 + }; + + // TODO: 6.5.5 AddZonedDateTime + // 6. Let dayStart be ? AddZonedDateTime(relativeTo.[[Nanoseconds]], relativeTo.[[TimeZone]], relativeTo.[[Calendar]], years, months, weeks, days, 0, 0, 0, 0, 0, 0). + // 7. Let dayEnd be ? AddZonedDateTime(dayStart, relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, direction, 0, 0, 0, 0, 0, 0). + // 8. Let dayLengthNs be ℝ(dayEnd - dayStart). + // 9. If (timeRemainderNs - dayLengthNs) × direction < 0, then + // a. Return ! CreateDurationRecord(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + // 10. Set timeRemainderNs to ℝ(RoundTemporalInstant(ℤ(timeRemainderNs - dayLengthNs), increment, unit, roundingMode)). + // 11. Let adjustedDateDuration be ? AddDuration(years, months, weeks, days, 0, 0, 0, 0, 0, 0, 0, 0, 0, direction, 0, 0, 0, 0, 0, 0, relativeTo). + // 12. Let adjustedTimeDuration be ? BalanceDuration(0, 0, 0, 0, 0, 0, timeRemainderNs, "hour"). + // 13. Return ! CreateDurationRecord(adjustedDateDuration.[[Years]], adjustedDateDuration.[[Months]], adjustedDateDuration.[[Weeks]], + // adjustedDateDuration.[[Days]], adjustedTimeDuration.[[Hours]], adjustedTimeDuration.[[Minutes]], adjustedTimeDuration.[[Seconds]], + // adjustedTimeDuration.[[Milliseconds]], adjustedTimeDuration.[[Microseconds]], adjustedTimeDuration.[[Nanoseconds]]). + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } +} diff --git a/boa_engine/src/builtins/temporal/duration/tests.rs b/boa_engine/src/builtins/temporal/duration/tests.rs new file mode 100644 index 00000000000..4deadf08195 --- /dev/null +++ b/boa_engine/src/builtins/temporal/duration/tests.rs @@ -0,0 +1,27 @@ +use crate::{run_test_actions, TestAction}; + +#[test] +fn duration_constructor() { + run_test_actions([ + TestAction::run("let dur = new Temporal.Duration(1, 1, 0, 1)"), + TestAction::assert_eq("dur.years", 1), + TestAction::assert_eq("dur.months", 1), + TestAction::assert_eq("dur.weeks", 0), + TestAction::assert_eq("dur.days", 1), + TestAction::assert_eq("dur.milliseconds", 0), + ]); +} + +#[test] +fn duration_abs() { + run_test_actions([ + TestAction::run("let dur = new Temporal.Duration(-1, -1, 0, -1)"), + TestAction::assert_eq("dur.sign", -1), + TestAction::run("let abs = dur.abs()"), + TestAction::assert_eq("abs.years", 1), + TestAction::assert_eq("abs.months", 1), + TestAction::assert_eq("abs.weeks", 0), + TestAction::assert_eq("abs.days", 1), + TestAction::assert_eq("abs.milliseconds", 0), + ]); +} diff --git a/boa_engine/src/builtins/temporal/fields.rs b/boa_engine/src/builtins/temporal/fields.rs new file mode 100644 index 00000000000..d0a860d5eb7 --- /dev/null +++ b/boa_engine/src/builtins/temporal/fields.rs @@ -0,0 +1,587 @@ +//! A Rust native implementation of the `fields` object used in `Temporal`. + +use crate::{ + js_string, property::PropertyKey, value::PreferredType, Context, JsNativeError, JsObject, + JsResult, JsString, JsValue, +}; + +use super::options::ArithmeticOverflow; + +use bitflags::bitflags; +use rustc_hash::FxHashSet; + +bitflags! { + #[derive(Debug, PartialEq, Eq)] + pub struct FieldMap: u16 { + const YEAR = 0b0000_0000_0000_0001; + const MONTH = 0b0000_0000_0000_0010; + const MONTH_CODE = 0b0000_0000_0000_0100; + const DAY = 0b0000_0000_0000_1000; + const HOUR = 0b0000_0000_0001_0000; + const MINUTE = 0b0000_0000_0010_0000; + const SECOND = 0b0000_0000_0100_0000; + const MILLISECOND = 0b0000_0000_1000_0000; + const MICROSECOND = 0b0000_0001_0000_0000; + const NANOSECOND = 0b0000_0010_0000_0000; + const OFFSET = 0b0000_0100_0000_0000; + const ERA = 0b0000_1000_0000_0000; + const ERA_YEAR = 0b0001_0000_0000_0000; + const TIME_ZONE = 0b0010_0000_0000_0000; + } +} + +/// The temporal fields are laid out in the Temporal proposal under section 13.46 `PrepareTemporalFields` +/// with conversion and defaults laid out by Table 17 (displayed below). +/// +/// `TemporalFields` is meant to act as a native Rust implementation +/// of the fields. +/// +/// +/// ## Table 17: Temporal field requirements +/// +/// | Property | Conversion | Default | +/// | -------------|---------------------------------|------------| +/// | "year" | `ToIntegerWithTruncation` | undefined | +/// | "month" | `ToPositiveIntegerWithTruncation` | undefined | +/// | "monthCode" | `ToPrimitiveAndRequireString` | undefined | +/// | "day" | `ToPositiveIntegerWithTruncation` | undefined | +/// | "hour" | `ToIntegerWithTruncation` | +0𝔽 | +/// | "minute" | `ToIntegerWithTruncation` | +0𝔽 | +/// | "second" | `ToIntegerWithTruncation` | +0𝔽 | +/// | "millisecond"| `ToIntegerWithTruncation` | +0𝔽 | +/// | "microsecond"| `ToIntegerWithTruncation` | +0𝔽 | +/// | "nanosecond" | `ToIntegerWithTruncation` | +0𝔽 | +/// | "offset" | `ToPrimitiveAndRequireString` | undefined | +/// | "era" | `ToPrimitiveAndRequireString` | undefined | +/// | "eraYear" | `ToIntegerWithTruncation` | undefined | +/// | "timeZone" | | undefined | +/// +#[derive(Debug)] +pub(crate) struct TemporalFields { + bit_map: FieldMap, + year: Option, + month: Option, + month_code: Option, // TODO: Switch to icu compatible value. + day: Option, + hour: i32, + minute: i32, + second: i32, + millisecond: i32, + microsecond: i32, + nanosecond: i32, + offset: Option, + era: Option, // TODO: switch to icu compatible value. + era_year: Option, // TODO: switch to icu compatible value. + time_zone: Option, // TODO: figure out the identifier for TimeZone. +} + +impl Default for TemporalFields { + fn default() -> Self { + Self { + bit_map: FieldMap::empty(), + year: None, + month: None, + month_code: None, + day: None, + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + microsecond: 0, + nanosecond: 0, + offset: None, + era: None, + era_year: None, + time_zone: None, + } + } +} + +impl TemporalFields { + pub(crate) const fn year(&self) -> Option { + self.year + } + + pub(crate) const fn month(&self) -> Option { + self.month + } + + pub(crate) const fn day(&self) -> Option { + self.day + } +} + +impl TemporalFields { + #[inline] + fn set_field_value( + &mut self, + field: &str, + value: &JsValue, + context: &mut Context<'_>, + ) -> JsResult<()> { + match field { + "year" => self.set_year(value, context)?, + "month" => self.set_month(value, context)?, + "monthCode" => self.set_month_code(value, context)?, + "day" => self.set_day(value, context)?, + "hour" => self.set_hour(value, context)?, + "minute" => self.set_minute(value, context)?, + "second" => self.set_second(value, context)?, + "millisecond" => self.set_milli(value, context)?, + "microsecond" => self.set_micro(value, context)?, + "nanosecond" => self.set_nano(value, context)?, + "offset" => self.set_offset(value, context)?, + "era" => self.set_era(value, context)?, + "eraYear" => self.set_era_year(value, context)?, + "timeZone" => self.set_time_zone(value), + _ => unreachable!(), + } + + Ok(()) + } + + #[inline] + fn set_year(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let y = super::to_integer_with_truncation(value, context)?; + self.year = Some(y); + self.bit_map.set(FieldMap::YEAR, true); + Ok(()) + } + + #[inline] + fn set_month(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let mo = super::to_positive_integer_with_trunc(value, context)?; + self.year = Some(mo); + self.bit_map.set(FieldMap::MONTH, true); + Ok(()) + } + + #[inline] + fn set_month_code(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let mc = value.to_primitive(context, PreferredType::String)?; + if let Some(string) = mc.as_string() { + self.month_code = Some(string.clone()); + } else { + return Err(JsNativeError::typ() + .with_message("ToPrimativeAndRequireString must be of type String.") + .into()); + } + + self.bit_map.set(FieldMap::MONTH_CODE, true); + + Ok(()) + } + + #[inline] + fn set_day(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let d = super::to_positive_integer_with_trunc(value, context)?; + self.day = Some(d); + self.bit_map.set(FieldMap::DAY, true); + Ok(()) + } + + #[inline] + fn set_hour(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let h = super::to_integer_with_truncation(value, context)?; + self.hour = h; + self.bit_map.set(FieldMap::HOUR, true); + Ok(()) + } + + #[inline] + fn set_minute(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let m = super::to_integer_with_truncation(value, context)?; + self.minute = m; + self.bit_map.set(FieldMap::MINUTE, true); + Ok(()) + } + + #[inline] + fn set_second(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let sec = super::to_integer_with_truncation(value, context)?; + self.second = sec; + self.bit_map.set(FieldMap::SECOND, true); + Ok(()) + } + + #[inline] + fn set_milli(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let milli = super::to_integer_with_truncation(value, context)?; + self.millisecond = milli; + self.bit_map.set(FieldMap::MILLISECOND, true); + Ok(()) + } + + #[inline] + fn set_micro(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let micro = super::to_integer_with_truncation(value, context)?; + self.microsecond = micro; + self.bit_map.set(FieldMap::MICROSECOND, true); + Ok(()) + } + + #[inline] + fn set_nano(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let nano = super::to_integer_with_truncation(value, context)?; + self.nanosecond = nano; + self.bit_map.set(FieldMap::NANOSECOND, true); + Ok(()) + } + + #[inline] + fn set_offset(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let mc = value.to_primitive(context, PreferredType::String)?; + if let Some(string) = mc.as_string() { + self.offset = Some(string.clone()); + } else { + return Err(JsNativeError::typ() + .with_message("ToPrimativeAndRequireString must be of type String.") + .into()); + } + self.bit_map.set(FieldMap::OFFSET, true); + + Ok(()) + } + + #[inline] + fn set_era(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let mc = value.to_primitive(context, PreferredType::String)?; + if let Some(string) = mc.as_string() { + self.era = Some(string.clone()); + } else { + return Err(JsNativeError::typ() + .with_message("ToPrimativeAndRequireString must be of type String.") + .into()); + } + self.bit_map.set(FieldMap::ERA, true); + + Ok(()) + } + + #[inline] + fn set_era_year(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { + let ey = super::to_integer_with_truncation(value, context)?; + self.era_year = Some(ey); + self.bit_map.set(FieldMap::ERA_YEAR, true); + Ok(()) + } + + #[inline] + fn set_time_zone(&mut self, value: &JsValue) { + let tz = value.as_string().cloned(); + self.time_zone = tz; + self.bit_map.set(FieldMap::TIME_ZONE, true); + } +} + +impl TemporalFields { + // TODO: Shift to JsString or utf16 over String. + /// A method for creating a Native representation for `TemporalFields` from + /// a `JsObject`. + /// + /// This is the equivalant to Abstract Operation 13.46 `PrepareTemporalFields` + pub(crate) fn from_js_object( + fields: &JsObject, + field_names: &mut Vec, + required_fields: &mut Vec, // None when Partial + extended_fields: Option>, + partial: bool, + dup_behaviour: Option, + context: &mut Context<'_>, + ) -> JsResult { + // 1. If duplicateBehaviour is not present, set duplicateBehaviour to throw. + let dup_option = dup_behaviour.unwrap_or_else(|| js_string!("throw")); + + // 2. Let result be OrdinaryObjectCreate(null). + let mut result = Self::default(); + + // 3. Let any be false. + let mut any = false; + // 4. If extraFieldDescriptors is present, then + if let Some(extra_fields) = extended_fields { + for (field_name, required) in extra_fields { + // a. For each Calendar Field Descriptor Record desc of extraFieldDescriptors, do + // i. Assert: fieldNames does not contain desc.[[Property]]. + // ii. Append desc.[[Property]] to fieldNames. + field_names.push(field_name.clone()); + + // iii. If desc.[[Required]] is true and requiredFields is a List, then + if required && !partial { + // 1. Append desc.[[Property]] to requiredFields. + required_fields.push(field_name); + } + } + } + + // 5. Let sortedFieldNames be SortStringListByCodeUnit(fieldNames). + // 6. Let previousProperty be undefined. + let mut dups_map = FxHashSet::default(); + + // 7. For each property name property of sortedFieldNames, do + for field in &*field_names { + // a. If property is one of "constructor" or "__proto__", then + if field.as_str() == "constructor" || field.as_str() == "__proto__" { + // i. Throw a RangeError exception. + return Err(JsNativeError::range() + .with_message("constructor or proto is out of field range.") + .into()); + } + + let new_value = dups_map.insert(field); + + // b. If property is not equal to previousProperty, then + if new_value { + // i. Let value be ? Get(fields, property). + let value = + fields.get(PropertyKey::from(JsString::from(field.clone())), context)?; + // ii. If value is not undefined, then + if !value.is_undefined() { + // 1. Set any to true. + any = true; + + // 2. If property is in the Property column of Table 17 and there is a Conversion value in the same row, then + // a. Let Conversion be the Conversion value of the same row. + // b. If Conversion is ToIntegerWithTruncation, then + // i. Set value to ? ToIntegerWithTruncation(value). + // ii. Set value to 𝔽(value). + // c. Else if Conversion is ToPositiveIntegerWithTruncation, then + // i. Set value to ? ToPositiveIntegerWithTruncation(value). + // ii. Set value to 𝔽(value). + // d. Else, + // i. Assert: Conversion is ToPrimitiveAndRequireString. + // ii. NOTE: Non-primitive values are supported here for consistency with other fields, but such values must coerce to Strings. + // iii. Set value to ? ToPrimitive(value, string). + // iv. If value is not a String, throw a TypeError exception. + // 3. Perform ! CreateDataPropertyOrThrow(result, property, value). + result.set_field_value(field, &value, context)?; + // iii. Else if requiredFields is a List, then + } else if !partial { + // 1. If requiredFields contains property, then + if required_fields.contains(field) { + // a. Throw a TypeError exception. + return Err(JsNativeError::typ() + .with_message("A required TemporalField was not provided.") + .into()); + } + + // NOTE: Values set to a default on init. + // 2. If property is in the Property column of Table 17, then + // a. Set value to the corresponding Default value of the same row. + // 3. Perform ! CreateDataPropertyOrThrow(result, property, value). + } + // c. Else if duplicateBehaviour is throw, then + } else if dup_option.to_std_string_escaped() == "throw" { + // i. Throw a RangeError exception. + return Err(JsNativeError::range() + .with_message("Cannot have a duplicate field") + .into()); + } + // d. Set previousProperty to property. + } + + // 8. If requiredFields is partial and any is false, then + if partial && !any { + // a. Throw a TypeError exception. + return Err(JsNativeError::range() + .with_message("requiredFields cannot be partial when any is false") + .into()); + } + + // 9. Return result. + Ok(result) + } + + /// Convert a `TemporalFields` struct into a `JsObject`. + pub(crate) fn as_object(&self, context: &mut Context<'_>) -> JsResult { + let obj = JsObject::with_null_proto(); + + for bit in self.bit_map.iter() { + match bit { + FieldMap::YEAR => { + obj.create_data_property_or_throw( + js_string!("year"), + self.year.map_or(JsValue::undefined(), JsValue::from), + context, + )?; + } + FieldMap::MONTH => { + obj.create_data_property_or_throw( + js_string!("month"), + self.month.map_or(JsValue::undefined(), JsValue::from), + context, + )?; + } + FieldMap::MONTH_CODE => { + obj.create_data_property_or_throw( + js_string!("monthCode"), + self.month_code + .as_ref() + .map_or(JsValue::undefined(), |f| f.clone().into()), + context, + )?; + } + FieldMap::DAY => { + obj.create_data_property( + js_string!("day"), + self.day().map_or(JsValue::undefined(), JsValue::from), + context, + )?; + } + FieldMap::HOUR => { + obj.create_data_property(js_string!("hour"), self.hour, context)?; + } + FieldMap::MINUTE => { + obj.create_data_property(js_string!("minute"), self.minute, context)?; + } + FieldMap::SECOND => { + obj.create_data_property_or_throw(js_string!("second"), self.second, context)?; + } + FieldMap::MILLISECOND => { + obj.create_data_property_or_throw( + js_string!("millisecond"), + self.millisecond, + context, + )?; + } + FieldMap::MICROSECOND => { + obj.create_data_property_or_throw( + js_string!("microsecond"), + self.microsecond, + context, + )?; + } + FieldMap::NANOSECOND => { + obj.create_data_property_or_throw( + js_string!("nanosecond"), + self.nanosecond, + context, + )?; + } + FieldMap::OFFSET => { + obj.create_data_property_or_throw( + js_string!("offset"), + self.offset + .as_ref() + .map_or(JsValue::undefined(), |s| s.clone().into()), + context, + )?; + } + FieldMap::ERA => { + obj.create_data_property_or_throw( + js_string!("era"), + self.era + .as_ref() + .map_or(JsValue::undefined(), |s| s.clone().into()), + context, + )?; + } + FieldMap::ERA_YEAR => { + obj.create_data_property_or_throw( + js_string!("eraYear"), + self.era_year.map_or(JsValue::undefined(), JsValue::from), + context, + )?; + } + FieldMap::TIME_ZONE => { + obj.create_data_property_or_throw( + js_string!("timeZone"), + self.time_zone + .as_ref() + .map_or(JsValue::undefined(), |s| s.clone().into()), + context, + )?; + } + _ => unreachable!(), + } + } + + Ok(obj) + } + + // Note placeholder until overflow is implemented on `ICU4x`'s Date. + /// A function to regulate the current `TemporalFields` according to the overflow value + pub(crate) fn regulate(&mut self, overflow: ArithmeticOverflow) -> JsResult<()> { + if let (Some(year), Some(month), Some(day)) = (self.year(), self.month(), self.day()) { + match overflow { + ArithmeticOverflow::Constrain => { + let m = month.clamp(1, 12); + let days_in_month = super::calendar::utils::iso_days_in_month(year, month); + let d = day.clamp(1, days_in_month); + + self.month = Some(m); + self.day = Some(d); + } + ArithmeticOverflow::Reject => { + return Err(JsNativeError::range() + .with_message("TemporalFields is out of a valid range.") + .into()) + } + } + } + Ok(()) + } + + pub(crate) fn regulate_year_month(&mut self, overflow: ArithmeticOverflow) { + match self.month { + Some(month) if overflow == ArithmeticOverflow::Constrain => { + let m = month.clamp(1, 12); + self.month = Some(m); + } + _ => {} + } + } + + /// Resolve the month and monthCode on this `TemporalFields`. + pub(crate) fn iso_resolve_month(&mut self) -> JsResult<()> { + if self.month_code.is_none() { + if self.month.is_some() { + return Ok(()); + } + + return Err(JsNativeError::range() + .with_message("month and MonthCode values cannot both be undefined.") + .into()); + } + + let unresolved_month_code = self + .month_code + .as_ref() + .expect("monthCode must exist at this point."); + + let month_code_integer = month_code_to_integer(unresolved_month_code)?; + + let new_month = match self.month { + Some(month) if month != month_code_integer => { + return Err(JsNativeError::range() + .with_message("month and monthCode cannot be resolved.") + .into()) + } + _ => month_code_integer, + }; + + self.month = Some(new_month); + + Ok(()) + } +} + +fn month_code_to_integer(mc: &JsString) -> JsResult { + match mc.to_std_string_escaped().as_str() { + "M01" => Ok(1), + "M02" => Ok(2), + "M03" => Ok(3), + "M04" => Ok(4), + "M05" => Ok(5), + "M06" => Ok(6), + "M07" => Ok(7), + "M08" => Ok(8), + "M09" => Ok(9), + "M10" => Ok(10), + "M11" => Ok(11), + "M12" => Ok(12), + "M13" => Ok(13), + _ => Err(JsNativeError::range() + .with_message("monthCode is not within the valid values.") + .into()), + } +} diff --git a/boa_engine/src/builtins/temporal/instant/mod.rs b/boa_engine/src/builtins/temporal/instant/mod.rs new file mode 100644 index 00000000000..00a1b01a376 --- /dev/null +++ b/boa_engine/src/builtins/temporal/instant/mod.rs @@ -0,0 +1,784 @@ +//! Boa's implementation of ECMAScript's `Temporal.Instant` builtin object. +#![allow(dead_code)] + +use crate::{ + builtins::{ + options::{get_option, get_options_object, RoundingMode}, + temporal::{ + duration::{DateDuration, TimeDuration}, + options::{ + get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup, + }, + }, + BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + }, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + js_string, + object::{internal_methods::get_prototype_from_constructor, ObjectData}, + property::Attribute, + realm::Realm, + string::{common::StaticJsStrings, utf16}, + Context, JsArgs, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; + +use super::{duration, ns_max_instant, ns_min_instant, MIS_PER_DAY, MS_PER_DAY, NS_PER_DAY}; + +const NANOSECONDS_PER_SECOND: i64 = 10_000_000_000; +const NANOSECONDS_PER_MINUTE: i64 = 600_000_000_000; +const NANOSECONDS_PER_HOUR: i64 = 36_000_000_000_000; + +/// The `Temporal.Instant` object. +#[derive(Debug, Clone)] +pub struct Instant { + pub(crate) nanoseconds: JsBigInt, +} + +impl BuiltInObject for Instant { + const NAME: JsString = StaticJsStrings::INSTANT; +} + +impl IntrinsicObject for Instant { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + let get_seconds = BuiltInBuilder::callable(realm, Self::get_epoc_seconds) + .name(js_string!("get epochSeconds")) + .build(); + + let get_millis = BuiltInBuilder::callable(realm, Self::get_epoc_milliseconds) + .name(js_string!("get epochMilliseconds")) + .build(); + + let get_micros = BuiltInBuilder::callable(realm, Self::get_epoc_microseconds) + .name(js_string!("get epochMicroseconds")) + .build(); + + let get_nanos = BuiltInBuilder::callable(realm, Self::get_epoc_nanoseconds) + .name(js_string!("get epochNanoseconds")) + .build(); + + BuiltInBuilder::from_standard_constructor::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::CONFIGURABLE, + ) + .accessor( + utf16!("epochSeconds"), + Some(get_seconds), + None, + Attribute::default(), + ) + .accessor( + utf16!("epochMilliseconds"), + Some(get_millis), + None, + Attribute::default(), + ) + .accessor( + utf16!("epochMicroseconds"), + Some(get_micros), + None, + Attribute::default(), + ) + .accessor( + utf16!("epochNanoseconds"), + Some(get_nanos), + None, + Attribute::default(), + ) + .method(Self::add, js_string!("add"), 1) + .method(Self::subtract, js_string!("subtract"), 1) + .method(Self::until, js_string!("until"), 2) + .method(Self::since, js_string!("since"), 2) + .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_zoned_date_time_iso, + js_string!("toZonedDateTimeISO"), + 1, + ) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInConstructor for Instant { + const LENGTH: usize = 1; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::instant; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. If NewTarget is undefined, then + if new_target.is_undefined() { + // a. Throw a TypeError exception. + return Err(JsNativeError::typ() + .with_message("Temporal.Instant new target cannot be undefined.") + .into()); + }; + + // 2. Let epochNanoseconds be ? ToBigInt(epochNanoseconds). + let epoch_nanos = args.get_or_undefined(0).to_bigint(context)?; + + // 3. If ! IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. + if !is_valid_epoch_nanos(&epoch_nanos) { + return Err(JsNativeError::range() + .with_message("Temporal.Instant must have a valid epochNanoseconds.") + .into()); + }; + // 4. Return ? CreateTemporalInstant(epochNanoseconds, NewTarget). + create_temporal_instant(epoch_nanos, Some(new_target.clone()), context) + } +} + +// -- Instant method implementations -- + +impl Instant { + /// 8.3.3 get Temporal.Instant.prototype.epochSeconds + pub(crate) fn get_epoc_seconds( + this: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Instant must be an object.") + })?; + let instant = o.as_instant().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be an instant object.") + })?; + // 3. Let ns be instant.[[Nanoseconds]]. + let ns = &instant.nanoseconds; + // 4. Let s be floor(ℝ(ns) / 10e9). + let s = (ns.to_f64() / 10e9).floor(); + // 5. Return 𝔽(s). + Ok(s.into()) + } + + /// 8.3.4 get Temporal.Instant.prototype.epochMilliseconds + pub(crate) fn get_epoc_milliseconds( + this: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Instant must be an object.") + })?; + let instant = o.as_instant().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be an instant object.") + })?; + // 3. Let ns be instant.[[Nanoseconds]]. + let ns = &instant.nanoseconds; + // 4. Let ms be floor(ℝ(ns) / 106). + let ms = (ns.to_f64() / 10e6).floor(); + // 5. Return 𝔽(ms). + Ok(ms.into()) + } + + /// 8.3.5 get Temporal.Instant.prototype.epochMicroseconds + pub(crate) fn get_epoc_microseconds( + this: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Instant must be an object.") + })?; + let instant = o.as_instant().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be an instant object.") + })?; + // 3. Let ns be instant.[[Nanoseconds]]. + let ns = &instant.nanoseconds; + // 4. Let µs be floor(ℝ(ns) / 103). + let micro_s = (ns.to_f64() / 10e3).floor(); + // 5. Return ℤ(µs). + let big_int = JsBigInt::try_from(micro_s).map_err(|_| { + JsNativeError::typ().with_message("Could not convert microseconds to JsBigInt value") + })?; + Ok(big_int.into()) + } + + /// 8.3.6 get Temporal.Instant.prototype.epochNanoseconds + pub(crate) fn get_epoc_nanoseconds( + this: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Instant must be an object.") + })?; + let instant = o.as_instant().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be an instant object.") + })?; + // 3. Let ns be instant.[[Nanoseconds]]. + let ns = &instant.nanoseconds; + // 4. Return ns. + Ok(ns.clone().into()) + } + + /// 8.3.7 `Temporal.Instant.prototype.add ( temporalDurationLike )` + pub(crate) fn add( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Instant must be an object.") + })?; + let instant = o.as_instant().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be an instant object.") + })?; + + // 3. Return ? AddDurationToOrSubtractDurationFromInstant(add, instant, temporalDurationLike). + let temporal_duration_like = args.get_or_undefined(0); + add_or_subtract_duration_from_instant(true, instant, temporal_duration_like, context) + } + + /// 8.3.8 `Temporal.Instant.prototype.subtract ( temporalDurationLike )` + pub(crate) fn subtract( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Instant must be an object.") + })?; + let instant = o.as_instant().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be an instant object.") + })?; + + // 3. Return ? AddDurationToOrSubtractDurationFromInstant(subtract, instant, temporalDurationLike). + let temporal_duration_like = args.get_or_undefined(0); + add_or_subtract_duration_from_instant(false, instant, temporal_duration_like, context) + } + + /// 8.3.9 `Temporal.Instant.prototype.until ( other [ , options ] )` + pub(crate) fn until( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Instant must be an object.") + })?; + let instant = o.as_instant().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be an instant object.") + })?; + + // 3. Return ? DifferenceTemporalInstant(until, instant, other, options). + let other = args.get_or_undefined(0); + let option = args.get_or_undefined(1); + diff_temporal_instant(true, instant, other, option, context) + } + + /// 8.3.10 `Temporal.Instant.prototype.since ( other [ , options ] )` + pub(crate) fn since( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Instant must be an object.") + })?; + let instant = o.as_instant().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be an instant object.") + })?; + + // 3. Return ? DifferenceTemporalInstant(since, instant, other, options). + let other = args.get_or_undefined(0); + let option = args.get_or_undefined(1); + diff_temporal_instant(false, instant, other, option, context) + } + + /// 8.3.11 `Temporal.Instant.prototype.round ( roundTo )` + pub(crate) fn round( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Instant must be an object.") + })?; + let instant = o.as_instant().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be an instant object.") + })?; + + let round_to = args.get_or_undefined(0); + // 3. If roundTo is undefined, then + if round_to.is_undefined() { + // a. Throw a TypeError exception. + return Err(JsNativeError::typ() + .with_message("roundTo cannot be undefined.") + .into()); + }; + // 4. If Type(roundTo) is String, then + let round_to = if round_to.is_string() { + // a. Let paramString be roundTo. + let param_string = round_to + .as_string() + .expect("roundTo is confirmed to be a string here."); + // b. Set roundTo to OrdinaryObjectCreate(null). + let new_round_to = JsObject::with_null_proto(); + // c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit"), paramString). + new_round_to.create_data_property_or_throw( + utf16!("smallestUnit"), + param_string.clone(), + context, + )?; + new_round_to + // 5. Else, + } else { + // a. Set roundTo to ? GetOptionsObject(roundTo). + get_options_object(round_to)? + }; + + // 6. NOTE: The following steps read options and perform independent validation in + // alphabetical order (ToTemporalRoundingIncrement reads "roundingIncrement" and ToTemporalRoundingMode reads "roundingMode"). + // 7. Let roundingIncrement be ? ToTemporalRoundingIncrement(roundTo). + let rounding_increment = get_temporal_rounding_increment(&round_to, context)?; + + // 8. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand"). + let rounding_mode = + get_option(&round_to, utf16!("roundingMode"), context)?.unwrap_or_default(); + + // 9. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit"), time, required). + let smallest_unit = get_temporal_unit( + &round_to, + utf16!("smallestUnit"), + TemporalUnitGroup::Time, + None, + context, + )? + .ok_or_else(|| JsNativeError::range().with_message("smallestUnit cannot be undefined."))?; + + let maximum = match smallest_unit { + // 10. If smallestUnit is "hour"), then + // a. Let maximum be HoursPerDay. + TemporalUnit::Hour => 24, + // 11. Else if smallestUnit is "minute"), then + // a. Let maximum be MinutesPerHour × HoursPerDay. + TemporalUnit::Minute => 14400, + // 12. Else if smallestUnit is "second"), then + // a. Let maximum be SecondsPerMinute × MinutesPerHour × HoursPerDay. + TemporalUnit::Second => 86400, + // 13. Else if smallestUnit is "millisecond"), then + // a. Let maximum be ℝ(msPerDay). + TemporalUnit::Millisecond => i64::from(MS_PER_DAY), + // 14. Else if smallestUnit is "microsecond"), then + // a. Let maximum be 10^3 × ℝ(msPerDay). + TemporalUnit::Microsecond => MIS_PER_DAY, + // 15. Else, + // a. Assert: smallestUnit is "nanosecond". + // b. Let maximum be nsPerDay. + TemporalUnit::Nanosecond => NS_PER_DAY, + // unreachable here functions as 15.a. + _ => unreachable!(), + }; + + // 16. Perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, true). + super::validate_temporal_rounding_increment(rounding_increment, maximum as f64, true)?; + + // 17. Let roundedNs be RoundTemporalInstant(instant.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode). + let rounded_ns = round_temporal_instant( + &instant.nanoseconds, + rounding_increment, + smallest_unit, + rounding_mode, + )?; + + // 18. Return ! CreateTemporalInstant(roundedNs). + create_temporal_instant(rounded_ns, None, context) + } + + /// 8.3.12 `Temporal.Instant.prototype.equals ( other )` + pub(crate) fn equals( + this: &JsValue, + args: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + // 4. If instant.[[Nanoseconds]] ≠ other.[[Nanoseconds]], return false. + // 5. Return true. + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value of Instant must be an object.") + })?; + let instant = o.as_instant().ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be an instant object.") + })?; + + // 3. Set other to ? ToTemporalInstant(other). + let other = args.get_or_undefined(0); + let other_instant = to_temporal_instant(other)?; + + if instant.nanoseconds != other_instant.nanoseconds { + return Ok(false.into()); + } + Ok(true.into()) + } + + /// 8.3.17 `Temporal.Instant.prototype.toZonedDateTime ( item )` + pub(crate) fn to_zoned_date_time( + _: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + // TODO: Complete + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + /// 8.3.18 `Temporal.Instant.prototype.toZonedDateTimeISO ( timeZone )` + pub(crate) fn to_zoned_date_time_iso( + _: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + // TODO Complete + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } +} + +// -- Instant Abstract Operations -- + +/// 8.5.1 `IsValidEpochNanoseconds ( epochNanoseconds )` +#[inline] +fn is_valid_epoch_nanos(epoch_nanos: &JsBigInt) -> bool { + // 1. Assert: Type(epochNanoseconds) is BigInt. + // 2. If ℝ(epochNanoseconds) < nsMinInstant or ℝ(epochNanoseconds) > nsMaxInstant, then + if epoch_nanos.to_f64() < ns_min_instant().to_f64() + || epoch_nanos.to_f64() > ns_max_instant().to_f64() + { + // a. Return false. + return false; + } + // 3. Return true. + true +} + +/// 8.5.2 `CreateTemporalInstant ( epochNanoseconds [ , newTarget ] )` +#[inline] +fn create_temporal_instant( + epoch_nanos: JsBigInt, + new_target: Option, + context: &mut Context<'_>, +) -> JsResult { + // 1. Assert: ! IsValidEpochNanoseconds(epochNanoseconds) is true. + assert!(is_valid_epoch_nanos(&epoch_nanos)); + // 2. If newTarget is not present, set newTarget to %Temporal.Instant%. + let new_target = new_target.unwrap_or_else(|| { + context + .realm() + .intrinsics() + .constructors() + .instant() + .constructor() + .into() + }); + // 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.Instant.prototype%"), « [[InitializedTemporalInstant]], [[Nanoseconds]] »). + let proto = + get_prototype_from_constructor(&new_target, StandardConstructors::instant, context)?; + + // 4. Set object.[[Nanoseconds]] to epochNanoseconds. + let obj = JsObject::from_proto_and_data( + proto, + ObjectData::instant(Instant { + nanoseconds: epoch_nanos, + }), + ); + + // 5. Return object. + Ok(obj.into()) +} + +/// 8.5.3 `ToTemporalInstant ( item )` +#[inline] +fn to_temporal_instant(_: &JsValue) -> JsResult { + // TODO: Need to implement parsing. + Err(JsNativeError::error() + .with_message("Instant parsing is not yet implemented.") + .into()) +} + +/// 8.5.6 `AddInstant ( epochNanoseconds, hours, minutes, seconds, milliseconds, microseconds, nanoseconds )` +#[inline] +fn add_instant( + epoch_nanos: &JsBigInt, + hours: i32, + minutes: i32, + seconds: i32, + millis: i32, + micros: i32, + nanos: i32, +) -> JsResult { + let result = JsBigInt::add_n(&[ + JsBigInt::mul( + &JsBigInt::from(hours), + &JsBigInt::from(NANOSECONDS_PER_HOUR), + ), + JsBigInt::mul( + &JsBigInt::from(minutes), + &JsBigInt::from(NANOSECONDS_PER_MINUTE), + ), + JsBigInt::mul( + &JsBigInt::from(seconds), + &JsBigInt::from(NANOSECONDS_PER_SECOND), + ), + JsBigInt::mul(&JsBigInt::from(millis), &JsBigInt::from(10_000_000_i32)), + JsBigInt::mul(&JsBigInt::from(micros), &JsBigInt::from(1000_i32)), + JsBigInt::add(&JsBigInt::from(nanos), epoch_nanos), + ]); + if !is_valid_epoch_nanos(&result) { + return Err(JsNativeError::range() + .with_message("result is not a valid epoch nanosecond value.") + .into()); + } + Ok(result) +} + +/// 8.5.7 `DifferenceInstant ( ns1, ns2, roundingIncrement, smallestUnit, largestUnit, roundingMode )` +#[inline] +fn diff_instant( + ns1: &JsBigInt, + ns2: &JsBigInt, + rounding_increment: f64, + smallest_unit: TemporalUnit, + largest_unit: TemporalUnit, + rounding_mode: RoundingMode, + context: &mut Context<'_>, +) -> JsResult { + // 1. Let difference be ℝ(ns2) - ℝ(ns1). + let difference = JsBigInt::sub(ns1, ns2); + // 2. Let nanoseconds be remainder(difference, 1000). + let nanoseconds = JsBigInt::rem(&difference, &JsBigInt::from(1000)); + // 3. Let microseconds be remainder(truncate(difference / 1000), 1000). + let truncated_micro = JsBigInt::try_from((&difference.to_f64() / 1000_f64).trunc()) + .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; + let microseconds = JsBigInt::rem(&truncated_micro, &JsBigInt::from(1000)); + + // 4. Let milliseconds be remainder(truncate(difference / 106), 1000). + let truncated_milli = JsBigInt::try_from((&difference.to_f64() / 1_000_000_f64).trunc()) + .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; + let milliseconds = JsBigInt::rem(&truncated_milli, &JsBigInt::from(1000)); + + // 5. Let seconds be truncate(difference / 109). + let seconds = (&difference.to_f64() / 1_000_000_000_f64).trunc(); + + // 6. Let roundResult be ! RoundDuration(0, 0, 0, 0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, roundingIncrement, smallestUnit, largestUnit, roundingMode). + let mut roundable_duration = duration::DurationRecord::new( + DateDuration::default(), + TimeDuration::new( + 0.0, + 0.0, + seconds, + milliseconds.to_f64(), + microseconds.to_f64(), + nanoseconds.to_f64(), + ), + ); + let _rem = roundable_duration.round_duration( + rounding_increment, + smallest_unit, + rounding_mode, + None, + context, + )?; + + // 7. Assert: roundResult.[[Days]] is 0. + assert_eq!(roundable_duration.days() as i32, 0); + + // 8. Return ! BalanceTimeDuration(0, roundResult.[[Hours]], roundResult.[[Minutes]], + // roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], + // roundResult.[[Nanoseconds]], largestUnit). + roundable_duration.balance_time_duration(largest_unit, None)?; + + Ok(roundable_duration) +} + +/// 8.5.8 `RoundTemporalInstant ( ns, increment, unit, roundingMode )` +#[inline] +fn round_temporal_instant( + ns: &JsBigInt, + increment: f64, + unit: TemporalUnit, + rounding_mode: RoundingMode, +) -> JsResult { + let increment_ns = match unit { + // 1. If unit is "hour"), then + TemporalUnit::Hour => { + // a. Let incrementNs be increment × 3.6 × 10^12. + increment as i64 * NANOSECONDS_PER_HOUR + } + // 2. Else if unit is "minute"), then + TemporalUnit::Minute => { + // a. Let incrementNs be increment × 6 × 10^10. + increment as i64 * NANOSECONDS_PER_MINUTE + } + // 3. Else if unit is "second"), then + TemporalUnit::Second => { + // a. Let incrementNs be increment × 10^9. + increment as i64 * NANOSECONDS_PER_SECOND + } + // 4. Else if unit is "millisecond"), then + TemporalUnit::Millisecond => { + // a. Let incrementNs be increment × 10^6. + increment as i64 * 1_000_000 + } + // 5. Else if unit is "microsecond"), then + TemporalUnit::Microsecond => { + // a. Let incrementNs be increment × 10^3. + increment as i64 * 1000 + } + // 6. Else, + TemporalUnit::Nanosecond => { + // NOTE: We shouldn't have to assert here as `unreachable` asserts instead. + // a. Assert: unit is "nanosecond". + // b. Let incrementNs be increment. + increment as i64 + } + _ => unreachable!(), + }; + + // 7. Return ℤ(RoundNumberToIncrementAsIfPositive(ℝ(ns), incrementNs, roundingMode)). + super::round_to_increment_as_if_positive(ns, increment_ns, rounding_mode) +} + +/// 8.5.10 `DifferenceTemporalInstant ( operation, instant, other, options )` +#[inline] +fn diff_temporal_instant( + op: bool, + instant: &Instant, + other: &JsValue, + options: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If operation is since, let sign be -1. Otherwise, let sign be 1. + let sign = if op { 1_f64 } else { -1_f64 }; + // 2. Set other to ? ToTemporalInstant(other). + let other = to_temporal_instant(other)?; + // 3. Let resolvedOptions be ? CopyOptions(options). + let resolved_options = + super::snapshot_own_properties(&get_options_object(options)?, None, None, context)?; + + // 4. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, time, « », "nanosecond"), "second"). + let settings = super::get_diff_settings( + op, + &resolved_options, + TemporalUnitGroup::Time, + &[], + TemporalUnit::Nanosecond, + TemporalUnit::Second, + context, + )?; + + // 5. Let result be DifferenceInstant(instant.[[Nanoseconds]], other.[[Nanoseconds]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[LargestUnit]], settings.[[RoundingMode]]). + let result = diff_instant( + &instant.nanoseconds, + &other.nanoseconds, + settings.3, + settings.0, + settings.1, + settings.2, + context, + )?; + + // 6. Return ! CreateTemporalDuration(0, 0, 0, 0, sign × result.[[Hours]], sign × result.[[Minutes]], sign × result.[[Seconds]], sign × result.[[Milliseconds]], sign × result.[[Microseconds]], sign × result.[[Nanoseconds]]). + Ok(duration::create_temporal_duration( + duration::DurationRecord::new( + DateDuration::default(), + TimeDuration::new( + sign * result.hours(), + sign * result.minutes(), + sign * result.seconds(), + sign * result.milliseconds(), + sign * result.microseconds(), + sign * result.nanoseconds(), + ), + ), + None, + context, + )? + .into()) +} + +/// 8.5.11 `AddDurationToOrSubtractDurationFromInstant ( operation, instant, temporalDurationLike )` +#[inline] +fn add_or_subtract_duration_from_instant( + op: bool, + instant: &Instant, + temporal_duration_like: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If operation is subtract, let sign be -1. Otherwise, let sign be 1. + let sign = if op { 1 } else { -1 }; + // 2. Let duration be ? ToTemporalDurationRecord(temporalDurationLike). + let duration = super::to_temporal_duration_record(temporal_duration_like)?; + // 3. If duration.[[Days]] is not 0, throw a RangeError exception. + if duration.days() != 0_f64 { + return Err(JsNativeError::range() + .with_message("DurationDays cannot be 0") + .into()); + } + // 4. If duration.[[Months]] is not 0, throw a RangeError exception. + if duration.months() != 0_f64 { + return Err(JsNativeError::range() + .with_message("DurationMonths cannot be 0") + .into()); + } + // 5. If duration.[[Weeks]] is not 0, throw a RangeError exception. + if duration.weeks() != 0_f64 { + return Err(JsNativeError::range() + .with_message("DurationWeeks cannot be 0") + .into()); + } + // 6. If duration.[[Years]] is not 0, throw a RangeError exception. + if duration.years() != 0_f64 { + return Err(JsNativeError::range() + .with_message("DurationYears cannot be 0") + .into()); + } + // 7. Let ns be ? AddInstant(instant.[[Nanoseconds]], sign × duration.[[Hours]], + // sign × duration.[[Minutes]], sign × duration.[[Seconds]], sign × duration.[[Milliseconds]], + // sign × duration.[[Microseconds]], sign × duration.[[Nanoseconds]]). + let new = add_instant( + &instant.nanoseconds, + sign * duration.hours() as i32, + sign * duration.minutes() as i32, + sign * duration.seconds() as i32, + sign * duration.milliseconds() as i32, + sign * duration.microseconds() as i32, + sign * duration.nanoseconds() as i32, + )?; + // 8. Return ! CreateTemporalInstant(ns). + create_temporal_instant(new, None, context) +} diff --git a/boa_engine/src/builtins/temporal/mod.rs b/boa_engine/src/builtins/temporal/mod.rs new file mode 100644 index 00000000000..7cfdf891eb2 --- /dev/null +++ b/boa_engine/src/builtins/temporal/mod.rs @@ -0,0 +1,660 @@ +//! The ECMAScript `Temporal` stage 3 built-in implementation. +//! +//! More information: +//! +//! [spec]: https://tc39.es/proposal-temporal/ + +mod calendar; +mod date_equations; +mod duration; +mod fields; +mod instant; +mod now; +mod options; +mod plain_date; +mod plain_date_time; +mod plain_month_day; +mod plain_time; +mod plain_year_month; +mod time_zone; +mod zoned_date_time; + +#[cfg(feature = "experimental")] +#[cfg(test)] +mod tests; + +pub(crate) use fields::TemporalFields; + +use self::options::{ + get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup, +}; +pub use self::{ + calendar::*, duration::*, instant::*, now::*, plain_date::*, plain_date_time::*, + plain_month_day::*, plain_time::*, plain_year_month::*, time_zone::*, zoned_date_time::*, +}; + +use crate::{ + builtins::{ + iterable::IteratorRecord, + options::{get_option, RoundingMode, UnsignedRoundingMode}, + BuiltInBuilder, BuiltInObject, IntrinsicObject, + }, + context::intrinsics::Intrinsics, + js_string, + property::Attribute, + realm::Realm, + string::{common::StaticJsStrings, utf16}, + value::Type, + Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; + +// Relavant numeric constants +/// Nanoseconds per day constant: 8.64e+13 +pub(crate) const NS_PER_DAY: i64 = 86_400_000_000_000; +/// Microseconds per day constant: 8.64e+10 +pub(crate) const MIS_PER_DAY: i64 = 8_640_000_000; +/// Milliseconds per day constant: 8.64e+7 +pub(crate) const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000; + +pub(crate) fn ns_max_instant() -> JsBigInt { + JsBigInt::from(i128::from(NS_PER_DAY) * 100_000_000_i128) +} + +pub(crate) fn ns_min_instant() -> JsBigInt { + JsBigInt::from(i128::from(NS_PER_DAY) * -100_000_000_i128) +} + +// An enum representing common fields across `Temporal` objects. +#[allow(unused)] +pub(crate) enum DateTimeValues { + Year, + Month, + MonthCode, + Week, + Day, + Hour, + Minute, + Second, + Millisecond, + Microsecond, + Nanosecond, +} + +/// The [`Temporal`][spec] builtin object. +/// +/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-objects +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Temporal; + +impl BuiltInObject for Temporal { + const NAME: JsString = StaticJsStrings::TEMPORAL; +} + +impl IntrinsicObject for Temporal { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + BuiltInBuilder::with_intrinsic::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("Now"), + realm.intrinsics().objects().now(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("Calendar"), + realm.intrinsics().constructors().calendar().constructor(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("Duration"), + realm.intrinsics().constructors().duration().constructor(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("Instant"), + realm.intrinsics().constructors().instant().constructor(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("PlainDate"), + realm.intrinsics().constructors().plain_date().constructor(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("PlainDateTime"), + realm + .intrinsics() + .constructors() + .plain_date_time() + .constructor(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("PlainMonthDay"), + realm + .intrinsics() + .constructors() + .plain_month_day() + .constructor(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("PlainTime"), + realm.intrinsics().constructors().plain_time().constructor(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("PlainYearMonth"), + realm + .intrinsics() + .constructors() + .plain_year_month() + .constructor(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("TimeZone"), + realm.intrinsics().constructors().time_zone().constructor(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + js_string!("ZonedDateTime"), + realm + .intrinsics() + .constructors() + .zoned_date_time() + .constructor(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + intrinsics.objects().temporal() + } +} + +// -- Temporal Abstract Operations -- + +/// Abstract operation `ToZeroPaddedDecimalString ( n, minLength )` +/// +/// The abstract operation `ToZeroPaddedDecimalString` takes arguments `n` (a non-negative integer) +/// and `minLength` (a non-negative integer) and returns a String. +fn to_zero_padded_decimal_string(n: u64, min_length: usize) -> String { + format!("{n:0min_length$}") +} + +/// Abstract Operation 13.1 [`IteratorToListOfType`][proposal] +/// +/// [proposal]: https://tc39.es/proposal-temporal/#sec-iteratortolistoftype +pub(crate) fn iterator_to_list_of_types( + iterator: &mut IteratorRecord, + element_types: &[Type], + context: &mut Context<'_>, +) -> JsResult> { + // 1. Let values be a new empty List. + let mut values = Vec::new(); + + // 2. Let next be true. + // 3. Repeat, while next is not false, + // a. Set next to ? IteratorStep(iteratorRecord). + // b. If next is not false, then + while iterator.step(context)? { + // i. Let nextValue be ? IteratorValue(next). + let next_value = iterator.value(context)?; + // ii. If Type(nextValue) is not an element of elementTypes, then + if element_types.contains(&next_value.get_type()) { + // 1. Let completion be ThrowCompletion(a newly created TypeError object). + let completion = JsNativeError::typ() + .with_message("IteratorNext is not within allowed type values."); + + // NOTE: The below should return as we are forcing a ThrowCompletion. + // 2. Return ? IteratorClose(iteratorRecord, completion). + let _never = iterator.close(Err(completion.into()), context)?; + } + // iii. Append nextValue to the end of the List values. + values.push(next_value); + } + + // 4. Return values. + Ok(values) +} + +/// 13.2 `ISODateToEpochDays ( year, month, date )` +// Note: implemented on IsoDateRecord. + +// Abstract Operation 13.3 `EpochDaysToEpochMs` +pub(crate) fn epoch_days_to_epoch_ms(day: i32, time: i32) -> f64 { + f64::from(day).mul_add(f64::from(MS_PER_DAY), f64::from(time)) +} + +// 13.4 Date Equations +// implemented in temporal/date_equations.rs + +// Abstract Operation 13.5 `GetOptionsObject ( options )` +// Implemented in builtin/options.rs + +// 13.6 `GetOption ( options, property, type, values, default )` +// Implemented in builtin/options.rs + +/// 13.7 `ToTemporalOverflow (options)` +// Now implemented in temporal/options.rs + +/// 13.10 `ToTemporalRoundingMode ( normalizedOptions, fallback )` +// Now implemented in builtin/options.rs + +// 13.11 `NegateTemporalRoundingMode ( roundingMode )` +// Now implemented in builtin/options.rs + +// 13.16 `ToTemporalRoundingIncrement ( normalizedOptions )` +// Now implemented in temporal/options.rs + +/// 13.17 `ValidateTemporalRoundingIncrement ( increment, dividend, inclusive )` +#[inline] +pub(crate) fn validate_temporal_rounding_increment( + increment: f64, + dividend: f64, + inclusive: bool, +) -> JsResult<()> { + // 1. If inclusive is true, then + let maximum = if inclusive { + // a. Let maximum be dividend. + dividend + // 2. Else, + } else { + // a. Assert: dividend > 1. + assert!(dividend > 1.0); + // b. Let maximum be dividend - 1. + dividend - 1.0 + }; + + // 3. If increment > maximum, throw a RangeError exception. + if increment > maximum { + return Err(JsNativeError::range() + .with_message("increment is exceeds the range of the allowed maximum.") + .into()); + } + // 4. If dividend modulo increment ≠ 0, then + if dividend % increment != 0.0 { + // a. Throw a RangeError exception. + return Err(JsNativeError::range() + .with_message("Temporal rounding increment is not valid.") + .into()); + } + // 5. Return unused. + Ok(()) +} + +/// 13.21 `ToRelativeTemporalObject ( options )` +pub(crate) fn to_relative_temporal_object( + _options: &JsObject, + _context: &mut Context<'_>, +) -> JsResult { + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) +} + +// 13.22 `LargerOfTwoTemporalUnits ( u1, u2 )` +// use core::cmp::max + +// 13.23 `MaximumTemporalDurationRoundingIncrement ( unit )` +// Implemented on TemporalUnit in temporal/options.rs + +// 13.26 `GetUnsignedRoundingMode ( roundingMode, isNegative )` +// Implemented on RoundingMode in builtins/options.rs + +/// 13.27 `ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode )` +#[inline] +fn apply_unsigned_rounding_mode( + x: f64, + r1: f64, + r2: f64, + unsigned_rounding_mode: UnsignedRoundingMode, +) -> f64 { + // 1. If x is equal to r1, return r1. + if (x - r1).abs() == 0.0 { + return r1; + }; + // 2. Assert: r1 < x < r2. + assert!(r1 < x && x < r2); + // 3. Assert: unsignedRoundingMode is not undefined. + + // 4. If unsignedRoundingMode is zero, return r1. + if unsigned_rounding_mode == UnsignedRoundingMode::Zero { + return r1; + }; + // 5. If unsignedRoundingMode is infinity, return r2. + if unsigned_rounding_mode == UnsignedRoundingMode::Infinity { + return r2; + }; + + // 6. Let d1 be x – r1. + let d1 = x - r1; + // 7. Let d2 be r2 – x. + let d2 = r2 - x; + // 8. If d1 < d2, return r1. + if d1 < d2 { + return r1; + } + // 9. If d2 < d1, return r2. + if d2 < d1 { + return r2; + } + // 10. Assert: d1 is equal to d2. + assert!((d1 - d2).abs() == 0.0); + + // 11. If unsignedRoundingMode is half-zero, return r1. + if unsigned_rounding_mode == UnsignedRoundingMode::HalfZero { + return r1; + }; + // 12. If unsignedRoundingMode is half-infinity, return r2. + if unsigned_rounding_mode == UnsignedRoundingMode::HalfInfinity { + return r2; + }; + // 13. Assert: unsignedRoundingMode is half-even. + assert!(unsigned_rounding_mode == UnsignedRoundingMode::HalfEven); + // 14. Let cardinality be (r1 / (r2 – r1)) modulo 2. + let cardinality = (r1 / (r2 - r1)) % 2.0; + // 15. If cardinality is 0, return r1. + if cardinality == 0.0 { + return r1; + } + // 16. Return r2. + r2 +} + +/// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )` +pub(crate) fn round_number_to_increment( + x: f64, + increment: f64, + rounding_mode: RoundingMode, +) -> f64 { + // 1. Let quotient be x / increment. + let mut quotient = x / increment; + + // 2. If quotient < 0, then + let is_negative = if quotient < 0_f64 { + // a. Let isNegative be true. + // b. Set quotient to -quotient. + quotient = -quotient; + true + // 3. Else, + } else { + // a. Let isNegative be false. + false + }; + + // 4. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative). + let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(is_negative); + // 5. Let r1 be the largest integer such that r1 ≤ quotient. + let r1 = quotient.ceil(); + // 6. Let r2 be the smallest integer such that r2 > quotient. + let r2 = quotient.floor(); + // 7. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode). + let mut rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode); + // 8. If isNegative is true, set rounded to -rounded. + if is_negative { + rounded = -rounded; + }; + // 9. Return rounded × increment. + rounded * increment +} + +/// 13.29 `RoundNumberToIncrementAsIfPositive ( x, increment, roundingMode )` +#[inline] +pub(crate) fn round_to_increment_as_if_positive( + ns: &JsBigInt, + increment: i64, + rounding_mode: RoundingMode, +) -> JsResult { + // 1. Let quotient be x / increment. + let q = ns.to_f64() / increment as f64; + // 2. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, false). + let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(false); + // 3. Let r1 be the largest integer such that r1 ≤ quotient. + let r1 = q.trunc(); + // 4. Let r2 be the smallest integer such that r2 > quotient. + let r2 = q.trunc() + 1.0; + // 5. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode). + let rounded = apply_unsigned_rounding_mode(q, r1, r2, unsigned_rounding_mode); + + // 6. Return rounded × increment. + let rounded = JsBigInt::try_from(rounded) + .map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; + + Ok(JsBigInt::mul(&rounded, &JsBigInt::from(increment))) +} + +/// 13.43 `ToPositiveIntegerWithTruncation ( argument )` +#[inline] +pub(crate) fn to_positive_integer_with_trunc( + value: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. Let integer be ? ToIntegerWithTruncation(argument). + let int = to_integer_with_truncation(value, context)?; + // 2. If integer ≤ 0, throw a RangeError exception. + if int <= 0 { + return Err(JsNativeError::range() + .with_message("value is not a positive integer") + .into()); + } + // 3. Return integer. + Ok(int) +} + +/// 13.44 `ToIntegerWithTruncation ( argument )` +#[inline] +pub(crate) fn to_integer_with_truncation( + value: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. Let number be ? ToNumber(argument). + let number = value.to_number(context)?; + // 2. If number is NaN, +∞𝔽 or -∞𝔽, throw a RangeError exception. + if number.is_nan() || number.is_infinite() { + return Err(JsNativeError::range() + .with_message("truncation target must be an integer.") + .into()); + } + // 3. Return truncate(ℝ(number)). + Ok(number.trunc() as i32) +} + +/// Abstract operation 13.45 `ToIntegerIfIntegral( argument )` +#[inline] +pub(crate) fn to_integer_if_integral(arg: &JsValue, context: &mut Context<'_>) -> JsResult { + // 1. Let number be ? ToNumber(argument). + // 2. If IsIntegralNumber(number) is false, throw a RangeError exception. + // 3. Return ℝ(number). + if !arg.is_integer() { + return Err(JsNativeError::range() + .with_message("value to convert is not an integral number.") + .into()); + } + + arg.to_i32(context) +} + +// 13.46 `PrepareTemporalFields ( fields, fieldNames, requiredFields [ , duplicateBehaviour ] )` +// See fields.rs + +// NOTE: op -> true == until | false == since +/// 13.47 `GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, fallbackSmallestUnit, smallestLargestDefaultUnit )` +#[inline] +pub(crate) fn get_diff_settings( + op: bool, + options: &JsObject, + unit_group: TemporalUnitGroup, + disallowed_units: &[TemporalUnit], + fallback_smallest_unit: TemporalUnit, + smallest_largest_default_unit: TemporalUnit, + context: &mut Context<'_>, +) -> JsResult<(TemporalUnit, TemporalUnit, RoundingMode, f64)> { + // 1. NOTE: The following steps read options and perform independent validation in alphabetical order (ToTemporalRoundingIncrement reads "roundingIncrement" and ToTemporalRoundingMode reads "roundingMode"). + // 2. Let largestUnit be ? GetTemporalUnit(options, "largestUnit", unitGroup, "auto"). + let mut largest_unit = + get_temporal_unit(options, utf16!("largestUnit"), unit_group, None, context)? + .unwrap_or(TemporalUnit::Auto); + + // 3. If disallowedUnits contains largestUnit, throw a RangeError exception. + if disallowed_units.contains(&largest_unit) { + return Err(JsNativeError::range() + .with_message("largestUnit is not an allowed unit.") + .into()); + } + + // 4. Let roundingIncrement be ? ToTemporalRoundingIncrement(options). + let rounding_increment = get_temporal_rounding_increment(options, context)?; + + // 5. Let roundingMode be ? ToTemporalRoundingMode(options, "trunc"). + let mut rounding_mode = + get_option(options, utf16!("roundingMode"), context)?.unwrap_or(RoundingMode::Trunc); + + // 6. If operation is since, then + if !op { + // a. Set roundingMode to ! NegateTemporalRoundingMode(roundingMode). + rounding_mode = rounding_mode.negate(); + } + + // 7. Let smallestUnit be ? GetTemporalUnit(options, "smallestUnit", unitGroup, fallbackSmallestUnit). + let smallest_unit = + get_temporal_unit(options, utf16!("smallestUnit"), unit_group, None, context)? + .unwrap_or(fallback_smallest_unit); + + // 8. If disallowedUnits contains smallestUnit, throw a RangeError exception. + if disallowed_units.contains(&smallest_unit) { + return Err(JsNativeError::range() + .with_message("smallestUnit is not an allowed unit.") + .into()); + } + + // 9. Let defaultLargestUnit be ! LargerOfTwoTemporalUnits(smallestLargestDefaultUnit, smallestUnit). + let default_largest_unit = core::cmp::max(smallest_largest_default_unit, smallest_unit); + + // 10. If largestUnit is "auto", set largestUnit to defaultLargestUnit. + if largest_unit == TemporalUnit::Auto { + largest_unit = default_largest_unit; + } + + // 11. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception. + if largest_unit != core::cmp::max(largest_unit, smallest_unit) { + return Err(JsNativeError::range() + .with_message("largestUnit must be larger than smallestUnit") + .into()); + } + + // 12. Let maximum be ! MaximumTemporalDurationRoundingIncrement(smallestUnit). + let maximum = smallest_unit.to_maximum_rounding_increment(); + + // 13. If maximum is not undefined, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false). + if let Some(max) = maximum { + validate_temporal_rounding_increment(rounding_increment, f64::from(max), false)?; + } + + // 14. Return the Record { [[SmallestUnit]]: smallestUnit, [[LargestUnit]]: largestUnit, [[RoundingMode]]: roundingMode, [[RoundingIncrement]]: roundingIncrement, }. + Ok(( + smallest_unit, + largest_unit, + rounding_mode, + rounding_increment, + )) +} + +// NOTE: used for MergeFields methods. Potentially can be omitted in favor of `TemporalFields`. +/// 14.6 `CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] )` +pub(crate) fn copy_data_properties( + target: &JsObject, + source: &JsValue, + excluded_keys: &Vec, + excluded_values: Option<&Vec>, + context: &mut Context<'_>, +) -> JsResult<()> { + // 1. If source is undefined or null, return unused. + if source.is_null_or_undefined() { + return Ok(()); + } + + // 2. Let from be ! ToObject(source). + let from = source.to_object(context)?; + + // 3. Let keys be ? from.[[OwnPropertyKeys]](). + let keys = from.__own_property_keys__(context)?; + + // 4. For each element nextKey of keys, do + for next_key in keys { + // a. Let excluded be false. + let mut excluded = false; + // b. For each element e of excludedItemsexcludedKeys, do + for e in excluded_keys { + // i. If SameValue(e, nextKey) is true, then + if next_key.to_string() == e.to_std_string_escaped() { + // 1. Set excluded to true. + excluded = true; + } + } + + // c. If excluded is false, then + if !excluded { + // i. Let desc be ? from.[[GetOwnProperty]](nextKey). + let desc = from.__get_own_property__(&next_key, context)?; + // ii. If desc is not undefined and desc.[[Enumerable]] is true, then + match desc { + Some(d) + if d.enumerable() + .expect("enumerable field must be set per spec.") => + { + // 1. Let propValue be ? Get(from, nextKey). + let prop_value = from.get(next_key.clone(), context)?; + // 2. If excludedValues is present, then + if let Some(values) = excluded_values { + // a. For each element e of excludedValues, do + for e in values { + // i. If SameValue(e, propValue) is true, then + if JsValue::same_value(e, &prop_value) { + // i. Set excluded to true. + excluded = true; + } + } + } + + // 3. PerformIf excluded is false, perform ! CreateDataPropertyOrThrow(target, nextKey, propValue). + if !excluded { + target.create_data_property_or_throw(next_key, prop_value, context)?; + } + } + _ => {} + } + } + } + + // 5. Return unused. + Ok(()) +} + +// Note: Deviates from Proposal spec -> proto appears to be always null across the specification. +/// 14.7 `SnapshotOwnProperties ( source, proto [ , excludedKeys [ , excludedValues ] ] )` +fn snapshot_own_properties( + source: &JsObject, + excluded_keys: Option>, + excluded_values: Option>, + context: &mut Context<'_>, +) -> JsResult { + // 1. Let copy be OrdinaryObjectCreate(proto). + let copy = JsObject::with_null_proto(); + // 2. If excludedKeys is not present, set excludedKeys to « ». + let keys = excluded_keys.unwrap_or_default(); + // 3. If excludedValues is not present, set excludedValues to « ». + let values = excluded_values.unwrap_or_default(); + // 4. Perform ? CopyDataProperties(copy, source, excludedKeys, excludedValues). + copy_data_properties(©, &source.clone().into(), &keys, Some(&values), context)?; + // 5. Return copy. + Ok(copy) +} diff --git a/boa_engine/src/builtins/temporal/now.rs b/boa_engine/src/builtins/temporal/now.rs new file mode 100644 index 00000000000..a67023a8d5f --- /dev/null +++ b/boa_engine/src/builtins/temporal/now.rs @@ -0,0 +1,188 @@ +//! Boa's implementation of `Temporal.Now` ECMAScript Builtin object. + +use crate::{ + builtins::{ + temporal::{create_temporal_time_zone, default_time_zone}, + BuiltInBuilder, BuiltInObject, IntrinsicObject, + }, + context::intrinsics::Intrinsics, + js_string, + property::Attribute, + realm::Realm, + string::common::StaticJsStrings, + Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; + +use super::{ns_max_instant, ns_min_instant}; +use std::time::SystemTime; + +/// JavaScript `Temporal.Now` object. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Now; + +impl IntrinsicObject for Now { + /// Initializes the `Temporal.Now` object. + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + // is an ordinary object. + // has a [[Prototype]] internal slot whose value is %Object.prototype%. + // is not a function object. + // does not have a [[Construct]] internal method; it cannot be used as a constructor with the new operator. + // does not have a [[Call]] internal method; it cannot be invoked as a function. + BuiltInBuilder::with_intrinsic::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_method(Self::time_zone_id, js_string!("timeZoneId"), 0) + .static_method(Self::instant, js_string!("instant"), 0) + .static_method(Self::plain_date_time, js_string!("plainDateTime"), 2) + .static_method(Self::plain_date_time_iso, js_string!("plainDateTimeISO"), 1) + .static_method(Self::zoned_date_time, js_string!("zonedDateTime"), 2) + .static_method(Self::zoned_date_time_iso, js_string!("zonedDateTimeISO"), 1) + .static_method(Self::plain_date, js_string!("plainDate"), 2) + .static_method(Self::plain_date_iso, js_string!("plainDateISO"), 1) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + intrinsics.objects().now() + } +} + +impl BuiltInObject for Now { + const NAME: JsString = StaticJsStrings::NOW; +} + +impl Now { + /// `Temporal.Now.timeZoneId ( )` + /// + /// More information: + /// - [ECMAScript specififcation][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal.now.timezone + #[allow(clippy::unnecessary_wraps)] + fn time_zone_id( + _: &JsValue, + _args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Return ! SystemTimeZone(). + Ok(system_time_zone(context).expect("retrieving the system timezone must not fail")) + } + + /// `Temporal.Now.instant()` + fn instant(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + /// `Temporal.Now.plainDateTime()` + fn plain_date_time(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + /// `Temporal.Now.plainDateTimeISO` + fn plain_date_time_iso(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + /// `Temporal.Now.zonedDateTime` + fn zoned_date_time(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + /// `Temporal.Now.zonedDateTimeISO` + fn zoned_date_time_iso(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + /// `Temporal.Now.plainDate()` + fn plain_date(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + /// `Temporal.Now.plainDateISO` + fn plain_date_iso(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } +} + +// -- Temporal.Now abstract operations -- + +/// 2.3.1 `HostSystemUTCEpochNanoseconds ( global )` +fn host_system_utc_epoch_nanoseconds() -> JsResult { + // TODO: Implement `SystemTime::now()` calls for `no_std` + let epoch_nanos = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|e| JsNativeError::range().with_message(e.to_string()))? + .as_nanos(); + Ok(clamp_epoc_nanos(JsBigInt::from(epoch_nanos))) +} + +fn clamp_epoc_nanos(ns: JsBigInt) -> JsBigInt { + let max = ns_max_instant(); + let min = ns_min_instant(); + ns.clamp(min, max) +} + +/// 2.3.2 `SystemUTCEpochMilliseconds` +#[allow(unused)] +fn system_utc_epoch_millis() -> JsResult { + let now = host_system_utc_epoch_nanoseconds()?; + Ok(now.to_f64().div_euclid(1_000_000_f64).floor()) +} + +/// 2.3.3 `SystemUTCEpochNanoseconds` +#[allow(unused)] +fn system_utc_epoch_nanos() -> JsResult { + host_system_utc_epoch_nanoseconds() +} + +/// `SystemInstant` +#[allow(unused)] +fn system_instant() { + todo!() +} + +/// `SystemDateTime` +#[allow(unused)] +fn system_date_time() { + todo!() +} + +/// `SystemZonedDateTime` +#[allow(unused)] +fn system_zoned_date_time() { + todo!() +} + +/// Abstract operation `SystemTimeZone ( )` +/// +/// More information: +/// - [ECMAScript specififcation][spec] +/// +/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-systemtimezone +#[allow(unused)] +fn system_time_zone(context: &mut Context<'_>) -> JsResult { + // 1. Let identifier be ! DefaultTimeZone(). + let identifier = default_time_zone(context); + // 2. Return ! CreateTemporalTimeZone(identifier). + create_temporal_time_zone(identifier, None, context) +} diff --git a/boa_engine/src/builtins/temporal/options.rs b/boa_engine/src/builtins/temporal/options.rs new file mode 100644 index 00000000000..17b88ce94e4 --- /dev/null +++ b/boa_engine/src/builtins/temporal/options.rs @@ -0,0 +1,384 @@ +//! Temporal Option types. + +// Implementation Note: +// +// The below Option types are adapted from the types laid out by +// the Temporal proposal's polyfill types that can be found at the +// below link. +// +// https://github.com/tc39/proposal-temporal/blob/main/polyfill/index.d.ts + +use crate::{ + builtins::options::{get_option, ParsableOptionType}, + js_string, Context, JsNativeError, JsObject, JsResult, +}; +use std::{fmt, str::FromStr}; + +// TODO: Expand docs on the below options. + +#[inline] +pub(crate) fn get_temporal_rounding_increment( + options: &JsObject, + context: &mut Context<'_>, +) -> JsResult { + // 1. Let increment be ? GetOption(normalizedOptions, "roundingIncrement", "number", undefined, 1𝔽). + let value = options.get(js_string!("roundingIncrement"), context)?; + + let increment = if value.is_undefined() { + 1.0 + } else { + value.to_number(context)? + }; + + // 2. If increment is not finite, throw a RangeError exception. + if !increment.is_finite() { + return Err(JsNativeError::range() + .with_message("rounding increment was out of range.") + .into()); + } + + // 3. Let integerIncrement be truncate(ℝ(increment)). + let integer_increment = increment.trunc(); + + // 4. If integerIncrement < 1 or integerIncrement > 10^9, throw a RangeError exception. + if (1.0..=1_000_000_000.0).contains(&integer_increment) { + return Err(JsNativeError::range() + .with_message("rounding increment was out of range.") + .into()); + } + + // 5. Return integerIncrement. + Ok(integer_increment) +} + +/// Gets the `TemporalUnit` from an options object. +#[inline] +pub(crate) fn get_temporal_unit( + options: &JsObject, + key: &[u16], + unit_group: TemporalUnitGroup, + extra_values: Option>, + context: &mut Context<'_>, +) -> JsResult> { + let extra = extra_values.unwrap_or_default(); + let mut unit_values = unit_group.group(); + unit_values.extend(extra); + + let unit = get_option(options, key, context)?; + + if let Some(u) = &unit { + if !unit_values.contains(u) { + return Err(JsNativeError::range() + .with_message("TemporalUnit was not part of the valid UnitGroup.") + .into()); + } + } + + Ok(unit) +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum TemporalUnitGroup { + Date, + Time, + DateTime, +} + +impl TemporalUnitGroup { + fn group(self) -> Vec { + use TemporalUnitGroup::{Date, DateTime, Time}; + + match self { + Date => date_units().collect(), + Time => time_units().collect(), + DateTime => datetime_units().collect(), + } + } +} + +fn time_units() -> impl Iterator { + [ + TemporalUnit::Hour, + TemporalUnit::Minute, + TemporalUnit::Second, + TemporalUnit::Millisecond, + TemporalUnit::Microsecond, + TemporalUnit::Nanosecond, + ] + .iter() + .copied() +} + +fn date_units() -> impl Iterator { + [ + TemporalUnit::Year, + TemporalUnit::Month, + TemporalUnit::Week, + TemporalUnit::Day, + ] + .iter() + .copied() +} + +fn datetime_units() -> impl Iterator { + date_units().chain(time_units()) +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum TemporalUnit { + Auto = 0, + Nanosecond, + Microsecond, + Millisecond, + Second, + Minute, + Hour, + Day, + Week, + Month, + Year, +} + +impl TemporalUnit { + pub(crate) fn to_maximum_rounding_increment(self) -> Option { + use TemporalUnit::{ + Auto, Day, Hour, Microsecond, Millisecond, Minute, Month, Nanosecond, Second, Week, + Year, + }; + // 1. If unit is "year", "month", "week", or "day", then + // a. Return undefined. + // 2. If unit is "hour", then + // a. Return 24. + // 3. If unit is "minute" or "second", then + // a. Return 60. + // 4. Assert: unit is one of "millisecond", "microsecond", or "nanosecond". + // 5. Return 1000. + match self { + Year | Month | Week | Day => None, + Hour => Some(24), + Minute | Second => Some(60), + Millisecond | Microsecond | Nanosecond => Some(1000), + Auto => unreachable!(), + } + } +} + +#[derive(Debug)] +pub(crate) struct ParseTemporalUnitError; + +impl fmt::Display for ParseTemporalUnitError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid TemporalUnit") + } +} + +impl FromStr for TemporalUnit { + type Err = ParseTemporalUnitError; + + fn from_str(s: &str) -> Result { + match s { + "auto" => Ok(Self::Auto), + "year" | "years" => Ok(Self::Year), + "month" | "months" => Ok(Self::Month), + "week" | "weeks" => Ok(Self::Week), + "day" | "days" => Ok(Self::Day), + "hour" | "hours" => Ok(Self::Hour), + "minute" | "minutes" => Ok(Self::Minute), + "second" | "seconds" => Ok(Self::Second), + "millisecond" | "milliseconds" => Ok(Self::Millisecond), + "microsecond" | "microseconds" => Ok(Self::Microsecond), + "nanosecond" | "nanoseconds" => Ok(Self::Nanosecond), + _ => Err(ParseTemporalUnitError), + } + } +} + +impl ParsableOptionType for TemporalUnit {} + +impl fmt::Display for TemporalUnit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Auto => "auto", + Self::Year => "constrain", + Self::Month => "month", + Self::Week => "week", + Self::Day => "day", + Self::Hour => "hour", + Self::Minute => "minute", + Self::Second => "second", + Self::Millisecond => "millsecond", + Self::Microsecond => "microsecond", + Self::Nanosecond => "nanosecond", + } + .fmt(f) + } +} + +/// `ArithmeticOverflow` can also be used as an +/// assignment overflow and consists of the "constrain" +/// and "reject" options. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ArithmeticOverflow { + Constrain, + Reject, +} + +#[derive(Debug)] +pub(crate) struct ParseArithmeticOverflowError; + +impl fmt::Display for ParseArithmeticOverflowError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid overflow value") + } +} + +impl FromStr for ArithmeticOverflow { + type Err = ParseArithmeticOverflowError; + + fn from_str(s: &str) -> Result { + match s { + "constrain" => Ok(Self::Constrain), + "reject" => Ok(Self::Reject), + _ => Err(ParseArithmeticOverflowError), + } + } +} + +impl ParsableOptionType for ArithmeticOverflow {} + +impl fmt::Display for ArithmeticOverflow { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Constrain => "constrain", + Self::Reject => "reject", + } + .fmt(f) + } +} + +/// `Duration` overflow options. +pub(crate) enum DurationOverflow { + Constrain, + Balance, +} + +#[derive(Debug)] +pub(crate) struct ParseDurationOverflowError; + +impl fmt::Display for ParseDurationOverflowError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid duration overflow value") + } +} + +impl FromStr for DurationOverflow { + type Err = ParseDurationOverflowError; + + fn from_str(s: &str) -> Result { + match s { + "constrain" => Ok(Self::Constrain), + "balance" => Ok(Self::Balance), + _ => Err(ParseDurationOverflowError), + } + } +} + +impl ParsableOptionType for DurationOverflow {} + +impl fmt::Display for DurationOverflow { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Constrain => "constrain", + Self::Balance => "balance", + } + .fmt(f) + } +} + +/// The disambiguation options for an instant. +pub(crate) enum InstantDisambiguation { + Compatible, + Earlier, + Later, + Reject, +} + +#[derive(Debug)] +pub(crate) struct ParseInstantDisambiguationError; + +impl fmt::Display for ParseInstantDisambiguationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid instant disambiguation value") + } +} +impl FromStr for InstantDisambiguation { + type Err = ParseInstantDisambiguationError; + + fn from_str(s: &str) -> Result { + match s { + "compatible" => Ok(Self::Compatible), + "earlier" => Ok(Self::Earlier), + "later" => Ok(Self::Later), + "reject" => Ok(Self::Reject), + _ => Err(ParseInstantDisambiguationError), + } + } +} + +impl ParsableOptionType for InstantDisambiguation {} + +impl fmt::Display for InstantDisambiguation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Compatible => "compatible", + Self::Earlier => "earlier", + Self::Later => "later", + Self::Reject => "reject", + } + .fmt(f) + } +} + +/// Offset disambiguation options. +pub(crate) enum OffsetDisambiguation { + Use, + Prefer, + Ignore, + Reject, +} + +#[derive(Debug)] +pub(crate) struct ParseOffsetDisambiguationError; + +impl fmt::Display for ParseOffsetDisambiguationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid offset disambiguation value") + } +} + +impl FromStr for OffsetDisambiguation { + type Err = ParseOffsetDisambiguationError; + + fn from_str(s: &str) -> Result { + match s { + "use" => Ok(Self::Use), + "prefer" => Ok(Self::Prefer), + "ignore" => Ok(Self::Ignore), + "reject" => Ok(Self::Reject), + _ => Err(ParseOffsetDisambiguationError), + } + } +} + +impl ParsableOptionType for OffsetDisambiguation {} + +impl fmt::Display for OffsetDisambiguation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Use => "use", + Self::Prefer => "prefer", + Self::Ignore => "ignore", + Self::Reject => "reject", + } + .fmt(f) + } +} diff --git a/boa_engine/src/builtins/temporal/plain_date/iso.rs b/boa_engine/src/builtins/temporal/plain_date/iso.rs new file mode 100644 index 00000000000..c85dd1f7cd8 --- /dev/null +++ b/boa_engine/src/builtins/temporal/plain_date/iso.rs @@ -0,0 +1,236 @@ +//! An `IsoDateRecord` that represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots. + +use crate::{ + builtins::temporal::{self, TemporalFields}, + JsNativeError, JsResult, JsString, +}; + +use icu_calendar::{Date, Iso}; + +// TODO: Move ISODateRecord to a more generalized location. + +// TODO: Determine whether month/day should be u8 or i32. + +/// `IsoDateRecord` serves as an record for the `[[ISOYear]]`, `[[ISOMonth]]`, +/// and `[[ISODay]]` internal fields. +/// +/// These fields are used for the `Temporal.PlainDate` object, the +/// `Temporal.YearMonth` object, and the `Temporal.MonthDay` object. +#[derive(Debug, Clone, Copy, Default)] +pub(crate) struct IsoDateRecord { + year: i32, + month: i32, + day: i32, +} + +// TODO: determine whether the below is neccessary. +impl IsoDateRecord { + pub(crate) const fn year(&self) -> i32 { + self.year + } + pub(crate) const fn month(&self) -> i32 { + self.month + } + pub(crate) const fn day(&self) -> i32 { + self.day + } +} + +impl IsoDateRecord { + // TODO: look into using Date across the board...TBD. + /// Creates `[[ISOYear]]`, `[[isoMonth]]`, `[[isoDay]]` fields from `ICU4X`'s `Date` struct. + pub(crate) fn from_date_iso(date: Date) -> Self { + Self { + year: date.year().number, + month: date.month().ordinal as i32, + day: i32::from(date.days_in_month()), + } + } +} + +impl IsoDateRecord { + /// 3.5.2 `CreateISODateRecord` + pub(crate) const fn new(year: i32, month: i32, day: i32) -> Self { + Self { year, month, day } + } + + /// 3.5.6 `RegulateISODate` + pub(crate) fn from_unregulated( + year: i32, + month: i32, + day: i32, + overflow: &JsString, + ) -> JsResult { + match overflow.to_std_string_escaped().as_str() { + "constrain" => { + let m = month.clamp(1, 12); + let days_in_month = temporal::calendar::utils::iso_days_in_month(year, month); + let d = day.clamp(1, days_in_month); + Ok(Self::new(year, m, d)) + } + "reject" => { + let date = Self::new(year, month, day); + if !date.is_valid() { + return Err(JsNativeError::range() + .with_message("not a valid ISO date.") + .into()); + } + Ok(date) + } + _ => unreachable!(), + } + } + + /// 12.2.35 `ISODateFromFields ( fields, overflow )` + /// + /// Note: fields.month must be resolved prior to using `from_temporal_fields` + pub(crate) fn from_temporal_fields( + fields: &TemporalFields, + overflow: &JsString, + ) -> JsResult { + Self::from_unregulated( + fields.year().expect("Cannot fail per spec"), + fields.month().expect("cannot fail after resolution"), + fields.day().expect("cannot fail per spec"), + overflow, + ) + } + + /// Create a Month-Day record from a `TemporalFields` object. + pub(crate) fn month_day_from_temporal_fields( + fields: &TemporalFields, + overflow: &JsString, + ) -> JsResult { + match fields.year() { + Some(year) => Self::from_unregulated( + year, + fields.month().expect("month must exist."), + fields.day().expect("cannot fail per spec"), + overflow, + ), + None => Self::from_unregulated( + 1972, + fields.month().expect("cannot fail per spec"), + fields.day().expect("cannot fail per spec."), + overflow, + ), + } + } + + /// Within `YearMonth` valid limits + pub(crate) const fn within_year_month_limits(&self) -> bool { + if self.year < -271_821 || self.year > 275_760 { + return false; + } + + if self.year == -271_821 && self.month < 4 { + return false; + } + + if self.year == 275_760 && self.month > 9 { + return true; + } + + true + } + + /// 3.5.5 `DifferenceISODate` + pub(crate) fn diff_iso_date( + &self, + o: &Self, + largest_unit: &JsString, + ) -> JsResult { + debug_assert!(self.is_valid()); + // TODO: Implement on `ICU4X`. + + Err(JsNativeError::range() + .with_message("not yet implemented.") + .into()) + } + + /// 3.5.7 `IsValidISODate` + pub(crate) fn is_valid(&self) -> bool { + if self.month < 1 || self.month > 12 { + return false; + } + + let days_in_month = temporal::calendar::utils::iso_days_in_month(self.year, self.month); + + if self.day < 1 || self.day > days_in_month { + return false; + } + true + } + + /// 13.2 `IsoDateToEpochDays` + pub(crate) fn as_epoch_days(&self) -> i32 { + // 1. Let resolvedYear be year + floor(month / 12). + let resolved_year = self.year + (f64::from(self.month) / 12_f64).floor() as i32; + // 2. Let resolvedMonth be month modulo 12. + let resolved_month = self.month % 12; + + // 3. Find a time t such that EpochTimeToEpochYear(t) is resolvedYear, EpochTimeToMonthInYear(t) is resolvedMonth, and EpochTimeToDate(t) is 1. + let year_t = temporal::date_equations::epoch_time_for_year(resolved_year); + let month_t = temporal::date_equations::epoch_time_for_month_given_year( + resolved_month, + resolved_year, + ); + + // 4. Return EpochTimeToDayNumber(t) + date - 1. + temporal::date_equations::epoch_time_to_day_number(year_t + month_t) + self.day - 1 + } + + // NOTE: Implementing as mut self so balance is applied to self, but TBD. + /// 3.5.8 `BalanceIsoDate` + pub(crate) fn balance(&mut self) { + let epoch_days = self.as_epoch_days(); + let ms = temporal::epoch_days_to_epoch_ms(epoch_days, 0); + + // Balance current values + self.year = temporal::date_equations::epoch_time_to_epoch_year(ms); + self.month = temporal::date_equations::epoch_time_to_month_in_year(ms); + self.day = temporal::date_equations::epoch_time_to_date(ms); + } + + // NOTE: Used in AddISODate only, so could possibly be deleted in the future. + /// 9.5.4 `BalanceISOYearMonth ( year, month )` + pub(crate) fn balance_year_month(&mut self) { + self.year += (self.month - 1) / 12; + self.month = ((self.month - 1) % 12) + 1; + } + + /// 3.5.11 `AddISODate ( year, month, day, years, months, weeks, days, overflow )` + pub(crate) fn add_iso_date( + &self, + years: i32, + months: i32, + weeks: i32, + days: i32, + overflow: &JsString, + ) -> JsResult { + // 1. Assert: year, month, day, years, months, weeks, and days are integers. + // 2. Assert: overflow is either "constrain" or "reject". + let mut intermediate = Self::new(self.year + years, self.month + months, 0); + + // 3. Let intermediate be ! BalanceISOYearMonth(year + years, month + months). + intermediate.balance_year_month(); + + // 4. Let intermediate be ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], day, overflow). + let mut new_date = Self::from_unregulated( + intermediate.year(), + intermediate.month(), + self.day, + overflow, + )?; + + // 5. Set days to days + 7 × weeks. + // 6. Let d be intermediate.[[Day]] + days. + let additional_days = days + (weeks * 7); + new_date.day += additional_days; + + // 7. Return BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d). + new_date.balance(); + + Ok(new_date) + } +} diff --git a/boa_engine/src/builtins/temporal/plain_date/mod.rs b/boa_engine/src/builtins/temporal/plain_date/mod.rs new file mode 100644 index 00000000000..5fe1de68473 --- /dev/null +++ b/boa_engine/src/builtins/temporal/plain_date/mod.rs @@ -0,0 +1,567 @@ +//! Boa's implementation of the ECMAScript `Temporal.PlainDate` builtin object. +#![allow(dead_code, unused_variables)] + +use crate::{ + 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, ObjectData}, + property::Attribute, + realm::Realm, + string::{common::StaticJsStrings, utf16}, + Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_parser::temporal::{IsoCursor, TemporalDateTimeString}; +use boa_profiler::Profiler; + +use super::{options::ArithmeticOverflow, plain_date::iso::IsoDateRecord, plain_date_time}; + +pub(crate) mod iso; + +/// The `Temporal.PlainDate` object. +#[derive(Debug, Clone)] +pub struct PlainDate { + pub(crate) inner: IsoDateRecord, + pub(crate) calendar: JsValue, // Calendar can probably be stored as a JsObject. +} + +impl BuiltInObject for PlainDate { + const NAME: JsString = StaticJsStrings::PLAIN_DATE; +} + +impl IntrinsicObject for PlainDate { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + let get_calendar_id = BuiltInBuilder::callable(realm, Self::get_calendar_id) + .name(js_string!("get calendarId")) + .build(); + + let get_year = BuiltInBuilder::callable(realm, Self::get_year) + .name(js_string!("get year")) + .build(); + + let get_month = BuiltInBuilder::callable(realm, Self::get_month) + .name(js_string!("get month")) + .build(); + + let get_month_code = BuiltInBuilder::callable(realm, Self::get_month_code) + .name(js_string!("get monthCode")) + .build(); + + let get_day = BuiltInBuilder::callable(realm, Self::get_day) + .name(js_string!("get day")) + .build(); + + let get_day_of_week = BuiltInBuilder::callable(realm, Self::get_day_of_week) + .name(js_string!("get dayOfWeek")) + .build(); + + let get_day_of_year = BuiltInBuilder::callable(realm, Self::get_day_of_year) + .name(js_string!("get dayOfYear")) + .build(); + + let get_week_of_year = BuiltInBuilder::callable(realm, Self::get_week_of_year) + .name(js_string!("get weekOfYear")) + .build(); + + let get_year_of_week = BuiltInBuilder::callable(realm, Self::get_year_of_week) + .name(js_string!("get yearOfWeek")) + .build(); + + let get_days_in_week = BuiltInBuilder::callable(realm, Self::get_days_in_week) + .name(js_string!("get daysInWeek")) + .build(); + + let get_days_in_month = BuiltInBuilder::callable(realm, Self::get_days_in_month) + .name(js_string!("get daysInMonth")) + .build(); + + let get_days_in_year = BuiltInBuilder::callable(realm, Self::get_days_in_year) + .name(js_string!("get daysInYear")) + .build(); + + let get_months_in_year = BuiltInBuilder::callable(realm, Self::get_months_in_year) + .name(js_string!("get monthsInYear")) + .build(); + + let get_in_leap_year = BuiltInBuilder::callable(realm, Self::get_in_leap_year) + .name(js_string!("get inLeapYear")) + .build(); + + BuiltInBuilder::from_standard_constructor::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::CONFIGURABLE, + ) + .accessor( + utf16!("calendarId"), + Some(get_calendar_id), + None, + Attribute::default(), + ) + .accessor(utf16!("year"), Some(get_year), None, Attribute::default()) + .accessor(utf16!("month"), Some(get_month), None, Attribute::default()) + .accessor( + utf16!("monthCode"), + Some(get_month_code), + None, + Attribute::default(), + ) + .accessor(utf16!("day"), Some(get_day), None, Attribute::default()) + .accessor( + utf16!("dayOfWeek"), + Some(get_day_of_week), + None, + Attribute::default(), + ) + .accessor( + utf16!("dayOfYear"), + Some(get_day_of_year), + None, + Attribute::default(), + ) + .accessor( + utf16!("weekOfYear"), + Some(get_week_of_year), + None, + Attribute::default(), + ) + .accessor( + utf16!("yearOfWeek"), + Some(get_year_of_week), + None, + Attribute::default(), + ) + .accessor( + utf16!("daysInWeek"), + Some(get_days_in_week), + None, + Attribute::default(), + ) + .accessor( + utf16!("daysInMonth"), + Some(get_days_in_month), + None, + Attribute::default(), + ) + .accessor( + utf16!("daysInYear"), + Some(get_days_in_year), + None, + Attribute::default(), + ) + .accessor( + utf16!("monthsInYear"), + Some(get_months_in_year), + None, + Attribute::default(), + ) + .accessor( + utf16!("inLeapYear"), + Some(get_in_leap_year), + None, + Attribute::default(), + ) + .method(Self::to_plain_year_month, js_string!("toPlainYearMonth"), 0) + .method(Self::to_plain_month_day, js_string!("toPlainMonthDay"), 0) + .method(Self::get_iso_fields, js_string!("getISOFields"), 0) + .method(Self::get_calendar, js_string!("getCalendar"), 0) + .method(Self::add, js_string!("add"), 2) + .method(Self::subtract, js_string!("subtract"), 2) + .method(Self::with, js_string!("with"), 2) + .method(Self::with_calendar, js_string!("withCalendar"), 1) + .method(Self::until, js_string!("until"), 2) + .method(Self::since, js_string!("since"), 2) + .method(Self::equals, js_string!("equals"), 1) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInConstructor for PlainDate { + const LENGTH: usize = 0; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::plain_date; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + if new_target.is_undefined() { + return Err(JsNativeError::typ() + .with_message("NewTarget cannot be undefined.") + .into()); + }; + + let iso_year = super::to_integer_with_truncation(args.get_or_undefined(0), context)?; + let iso_month = super::to_integer_with_truncation(args.get_or_undefined(1), context)?; + let iso_day = super::to_integer_with_truncation(args.get_or_undefined(2), context)?; + let default_calendar = JsValue::from(js_string!("iso8601")); + let calendar_like = args.get(3).unwrap_or(&default_calendar); + + let iso = IsoDateRecord::new(iso_year, iso_month, iso_day); + + Ok(create_temporal_date(iso, calendar_like.clone(), Some(new_target), context)?.into()) + } +} + +// -- `PlainDate` getter methods -- +impl PlainDate { + fn get_calendar_id(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn get_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 { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_month_code(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_day(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_day_of_week(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_day_of_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_week_of_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_year_of_week(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_days_in_week(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 { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_days_in_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_months_in_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_in_leap_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } +} + +// ==== `PlainDate.prototype` method implementation ==== + +impl PlainDate { + fn to_plain_year_month( + this: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn to_plain_month_day(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_iso_fields(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_calendar(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn add(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn subtract(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn with(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn with_calendar(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn until(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn since(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn equals(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } +} + +// -- `PlainDate` Abstract Operations -- + +// 3.5.2 `CreateIsoDateRecord` +// Implemented on `IsoDateRecord` + +/// 3.5.3 `CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] )` +pub(crate) fn create_temporal_date( + iso_date: IsoDateRecord, + calendar: JsValue, + new_target: Option<&JsValue>, + context: &mut Context<'_>, +) -> JsResult { + // 1. If IsValidISODate(isoYear, isoMonth, isoDay) is false, throw a RangeError exception. + if !iso_date.is_valid() { + return Err(JsNativeError::range() + .with_message("Date is not a valid ISO date.") + .into()); + }; + + let iso_date_time = plain_date_time::iso::IsoDateTimeRecord::default() + .with_date(iso_date.year(), iso_date.month(), iso_date.day()) + .with_time(12, 0, 0, 0, 0, 0); + + // 2. If ISODateTimeWithinLimits(isoYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0) is false, throw a RangeError exception. + if iso_date_time.is_valid() { + return Err(JsNativeError::range() + .with_message("Date is not within ISO date time limits.") + .into()); + } + + // 3. If newTarget is not present, set newTarget to %Temporal.PlainDate%. + let new_target = if let Some(new_target) = new_target { + new_target.clone() + } else { + context + .realm() + .intrinsics() + .constructors() + .plain_date() + .constructor() + .into() + }; + + // 4. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainDate.prototype%", « [[InitializedTemporalDate]], [[ISOYear]], [[ISOMonth]], [[ISODay]], [[Calendar]] »). + let prototype = + get_prototype_from_constructor(&new_target, StandardConstructors::plain_date, context)?; + + // 5. Set object.[[ISOYear]] to isoYear. + // 6. Set object.[[ISOMonth]] to isoMonth. + // 7. Set object.[[ISODay]] to isoDay. + // 8. Set object.[[Calendar]] to calendar. + let obj = JsObject::from_proto_and_data( + prototype, + ObjectData::plain_date(PlainDate { + inner: iso_date, + calendar, + }), + ); + + // 9. Return object. + Ok(obj) +} + +/// 3.5.4 `ToTemporalDate ( item [ , options ] )` +/// +/// Converts an ambiguous `JsValue` into a `PlainDate` +pub(crate) fn to_temporal_date( + item: &JsValue, + options: Option, + context: &mut Context<'_>, +) -> JsResult { + // 1. If options is not present, set options to undefined. + let options = options.unwrap_or(JsValue::undefined()); + + // 2. Assert: Type(options) is Object or Undefined. + // 3. If options is not undefined, set options to ? SnapshotOwnProperties(? GetOptionsObject(options), null). + let options_obj = get_options_object(&options)?; + + // 4. If Type(item) is Object, then + if let Some(object) = item.as_object() { + // a. If item has an [[InitializedTemporalDate]] internal slot, then + if object.is_plain_date() { + // i. Return item. + let obj = object.borrow(); + let date = obj.as_plain_date().expect("obj must be a PlainDate."); + return Ok(PlainDate { + inner: date.inner, + calendar: date.calendar.clone(), + }); + // b. If item has an [[InitializedTemporalZonedDateTime]] internal slot, then + } else if object.is_zoned_date_time() { + return Err(JsNativeError::range() + .with_message("ZonedDateTime not yet implemented.") + .into()); + // i. Perform ? ToTemporalOverflow(options). + // ii. Let instant be ! CreateTemporalInstant(item.[[Nanoseconds]]). + // iii. Let plainDateTime be ? GetPlainDateTimeFor(item.[[TimeZone]], instant, item.[[Calendar]]). + // iv. Return ! CreateTemporalDate(plainDateTime.[[ISOYear]], plainDateTime.[[ISOMonth]], plainDateTime.[[ISODay]], plainDateTime.[[Calendar]]). + + // c. If item has an [[InitializedTemporalDateTime]] internal slot, then + } else if object.is_plain_date_time() { + // i. Perform ? ToTemporalOverflow(options). + let _o = get_option(&options_obj, utf16!("overflow"), context)? + .unwrap_or(ArithmeticOverflow::Constrain); + + let obj = object.borrow(); + let date_time = obj + .as_plain_date_time() + .expect("obj must be a PlainDateTime"); + + let iso = date_time.inner.iso_date(); + let calendar = date_time.calendar.clone(); + + drop(obj); + + // ii. Return ! CreateTemporalDate(item.[[ISOYear]], item.[[ISOMonth]], item.[[ISODay]], item.[[Calendar]]). + return Ok(PlainDate { + inner: iso, + calendar, + }); + } + + // d. Let calendar be ? GetTemporalCalendarSlotValueWithISODefault(item). + // 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.") + .into()); + } + + // 5. If item is not a String, throw a TypeError exception. + match item { + JsValue::String(s) => { + // 6. Let result be ? ParseTemporalDateString(item). + let result = TemporalDateTimeString::parse( + false, + &mut IsoCursor::new(&s.to_std_string_escaped()), + ) + .map_err(|err| JsNativeError::range().with_message(err.to_string()))?; + + // 7. Assert: IsValidISODate(result.[[Year]], result.[[Month]], result.[[Day]]) is true. + // 8. Let calendar be result.[[Calendar]]. + // 9. If calendar is undefined, set calendar to "iso8601". + let identifier = result + .date + .calendar + .map_or_else(|| js_string!("iso8601"), JsString::from); + + // 10. If IsBuiltinCalendar(calendar) is false, throw a RangeError exception. + if !super::calendar::is_builtin_calendar(&identifier) { + return Err(JsNativeError::range() + .with_message("not a valid calendar identifier.") + .into()); + } + + // TODO: impl to ASCII-lowercase on JsStirng + // 11. Set calendar to the ASCII-lowercase of calendar. + + // 12. Perform ? ToTemporalOverflow(options). + let _result = get_option(&options_obj, utf16!("overflow"), context)? + .unwrap_or(ArithmeticOverflow::Constrain); + + // 13. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar). + Ok(PlainDate { + inner: IsoDateRecord::new(result.date.year, result.date.month, result.date.day), + calendar: identifier.into(), + }) + } + _ => Err(JsNativeError::typ() + .with_message("ToTemporalDate item must be an object or string.") + .into()), + } +} + +// 3.5.5. DifferenceIsoDate +// Implemented on IsoDateRecord. + +// 3.5.6 RegulateIsoDate +// Implemented on IsoDateRecord. + +// 3.5.7 IsValidIsoDate +// Implemented on IsoDateRecord. + +// 3.5.8 BalanceIsoDate +// Implemented on IsoDateRecord. + +// 3.5.11 AddISODate ( year, month, day, years, months, weeks, days, overflow ) +// Implemented on IsoDateRecord diff --git a/boa_engine/src/builtins/temporal/plain_date_time/iso.rs b/boa_engine/src/builtins/temporal/plain_date_time/iso.rs new file mode 100644 index 00000000000..50b2805a37a --- /dev/null +++ b/boa_engine/src/builtins/temporal/plain_date_time/iso.rs @@ -0,0 +1,100 @@ +use crate::{ + builtins::{ + date::utils, + temporal::{self, plain_date::iso::IsoDateRecord}, + }, + JsBigInt, +}; + +#[derive(Default, Debug, Clone)] +pub(crate) struct IsoDateTimeRecord { + iso_date: IsoDateRecord, + hour: i32, + minute: i32, + second: i32, + millisecond: i32, + microsecond: i32, + nanosecond: i32, +} + +impl IsoDateTimeRecord { + pub(crate) const fn iso_date(&self) -> IsoDateRecord { + self.iso_date + } +} + +// ==== `IsoDateTimeRecord` methods ==== + +impl IsoDateTimeRecord { + pub(crate) const fn with_date(mut self, year: i32, month: i32, day: i32) -> Self { + let iso_date = IsoDateRecord::new(year, month, day); + self.iso_date = iso_date; + self + } + + pub(crate) const fn with_time( + mut self, + hour: i32, + minute: i32, + second: i32, + ms: i32, + mis: i32, + ns: i32, + ) -> Self { + self.hour = hour; + self.minute = minute; + self.second = second; + self.millisecond = ms; + self.microsecond = mis; + self.nanosecond = ns; + self + } + + /// 5.5.1 `ISODateTimeWithinLimits ( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond )` + pub(crate) fn is_valid(&self) -> bool { + self.iso_date.is_valid(); + let ns = self.get_utc_epoch_ns(None).to_f64(); + + if ns <= temporal::ns_min_instant().to_f64() - (temporal::NS_PER_DAY as f64) + || ns >= temporal::ns_max_instant().to_f64() + (temporal::NS_PER_DAY as f64) + { + return false; + } + true + } + + /// 14.8.1 `GetUTCEpochNanoseconds` + pub(crate) fn get_utc_epoch_ns(&self, offset_ns: Option) -> JsBigInt { + let day = utils::make_day( + i64::from(self.iso_date.year()), + i64::from(self.iso_date.month()), + i64::from(self.iso_date.day()), + ) + .unwrap_or_default(); + let time = utils::make_time( + i64::from(self.hour), + i64::from(self.minute), + i64::from(self.second), + i64::from(self.millisecond), + ) + .unwrap_or_default(); + + let ms = utils::make_date(day, time).unwrap_or_default(); + + let epoch_ns = match offset_ns { + Some(offset) if offset != 0 => { + let ns = (ms * 1_000_000_i64) + + (i64::from(self.microsecond) * 1_000_i64) + + i64::from(self.nanosecond); + ns - offset + } + _ => { + (ms * 1_000_000_i64) + + (i64::from(self.microsecond) * 1_000_i64) + + i64::from(self.nanosecond) + } + }; + + JsBigInt::from(epoch_ns) + } +} diff --git a/boa_engine/src/builtins/temporal/plain_date_time/mod.rs b/boa_engine/src/builtins/temporal/plain_date_time/mod.rs new file mode 100644 index 00000000000..e3e5bf938f1 --- /dev/null +++ b/boa_engine/src/builtins/temporal/plain_date_time/mod.rs @@ -0,0 +1,148 @@ +//! Boa's implementation of the ECMAScript `Temporal.PlainDateTime` builtin object. +#![allow(dead_code, unused_variables)] + +use crate::{ + builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + property::Attribute, + realm::Realm, + string::common::StaticJsStrings, + Context, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; + +use self::iso::IsoDateTimeRecord; + +pub(crate) mod iso; + +/// The `Temporal.PlainDateTime` object. +#[derive(Debug, Clone)] +pub struct PlainDateTime { + pub(crate) inner: IsoDateTimeRecord, + pub(crate) calendar: JsValue, +} + +impl BuiltInObject for PlainDateTime { + const NAME: JsString = StaticJsStrings::PLAIN_DATETIME; +} + +impl IntrinsicObject for PlainDateTime { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + BuiltInBuilder::from_standard_constructor::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::CONFIGURABLE, + ) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInConstructor for PlainDateTime { + const LENGTH: usize = 0; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::plain_date_time; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::range() + .with_message("Not yet implemented.") + .into()) + } +} + +// ==== `PlainDateTime` Accessor Properties ==== + +impl PlainDateTime { + fn calendar_id(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn month(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn month_code(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn day(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn hour(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn minute(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn second(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn millisecond(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn microsecond(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn nanosecond(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn era(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } + + fn era_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("calendars not yet implemented.") + .into()) + } +} + +// ==== `PlainDateTime` Abstract Operations` ==== + +// See `IsoDateTimeRecord` diff --git a/boa_engine/src/builtins/temporal/plain_month_day/mod.rs b/boa_engine/src/builtins/temporal/plain_month_day/mod.rs new file mode 100644 index 00000000000..a970fc26f23 --- /dev/null +++ b/boa_engine/src/builtins/temporal/plain_month_day/mod.rs @@ -0,0 +1,122 @@ +//! Boa's implementation of the ECMAScript `Temporal.PlainMonthDay` builtin object. +#![allow(dead_code, unused_variables)] +use crate::{ + builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + object::{internal_methods::get_prototype_from_constructor, ObjectData}, + property::Attribute, + realm::Realm, + string::common::StaticJsStrings, + Context, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; + +use super::{plain_date::iso::IsoDateRecord, plain_date_time::iso::IsoDateTimeRecord}; + +/// The `Temporal.PlainMonthDay` object. +#[derive(Debug, Clone)] +pub struct PlainMonthDay { + pub(crate) inner: IsoDateRecord, + pub(crate) calendar: JsValue, +} + +impl BuiltInObject for PlainMonthDay { + const NAME: JsString = StaticJsStrings::PLAIN_MD; +} + +impl IntrinsicObject for PlainMonthDay { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + BuiltInBuilder::from_standard_constructor::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::CONFIGURABLE, + ) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInConstructor for PlainMonthDay { + const LENGTH: usize = 0; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::plain_month_day; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::range() + .with_message("Not yet implemented.") + .into()) + } +} + +// ==== `PlainMonthDay` Abstract Operations ==== + +pub(crate) fn create_temporal_month_day( + iso: IsoDateRecord, + calendar: JsValue, + new_target: Option<&JsValue>, + context: &mut Context<'_>, +) -> JsResult { + // 1. If IsValidISODate(referenceISOYear, isoMonth, isoDay) is false, throw a RangeError exception. + if iso.is_valid() { + return Err(JsNativeError::range() + .with_message("PlainMonthDay is not a valid ISO date.") + .into()); + } + + // 2. If ISODateTimeWithinLimits(referenceISOYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0) is false, throw a RangeError exception. + let iso_date_time = IsoDateTimeRecord::default() + .with_date(iso.year(), iso.month(), iso.day()) + .with_time(12, 0, 0, 0, 0, 0); + + if !iso_date_time.is_valid() { + return Err(JsNativeError::range() + .with_message("PlainMonthDay is not a valid ISO date time.") + .into()); + } + + // 3. If newTarget is not present, set newTarget to %Temporal.PlainMonthDay%. + let new_target = if let Some(target) = new_target { + target.clone() + } else { + context + .realm() + .intrinsics() + .constructors() + .plain_month_day() + .constructor() + .into() + }; + + // 4. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainMonthDay.prototype%", « [[InitializedTemporalMonthDay]], [[ISOMonth]], [[ISODay]], [[ISOYear]], [[Calendar]] »). + let proto = get_prototype_from_constructor( + &new_target, + StandardConstructors::plain_month_day, + context, + )?; + + // 5. Set object.[[ISOMonth]] to isoMonth. + // 6. Set object.[[ISODay]] to isoDay. + // 7. Set object.[[Calendar]] to calendar. + // 8. Set object.[[ISOYear]] to referenceISOYear. + let obj = JsObject::from_proto_and_data( + proto, + ObjectData::plain_month_day(PlainMonthDay { + inner: iso, + calendar, + }), + ); + + // 9. Return object. + Ok(obj.into()) +} diff --git a/boa_engine/src/builtins/temporal/plain_time/mod.rs b/boa_engine/src/builtins/temporal/plain_time/mod.rs new file mode 100644 index 00000000000..245fbbd0084 --- /dev/null +++ b/boa_engine/src/builtins/temporal/plain_time/mod.rs @@ -0,0 +1,62 @@ +//! Boa's implementation of the ECMAScript `Temporal.PlainTime` builtin object. +#![allow(dead_code, unused_variables)] + +use crate::{ + builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + property::Attribute, + realm::Realm, + string::common::StaticJsStrings, + Context, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; + +/// The `Temporal.PlainTime` object. +#[derive(Debug, Clone, Copy)] +pub struct PlainTime { + iso_hour: i32, // integer between 0-23 + iso_minute: i32, // integer between 0-59 + iso_second: i32, // integer between 0-59 + iso_millisecond: i32, // integer between 0-999 + iso_microsecond: i32, // integer between 0-999 + iso_nanosecond: i32, // integer between 0-999 +} + +impl BuiltInObject for PlainTime { + const NAME: JsString = StaticJsStrings::PLAIN_TIME; +} + +impl IntrinsicObject for PlainTime { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + BuiltInBuilder::from_standard_constructor::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::CONFIGURABLE, + ) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInConstructor for PlainTime { + const LENGTH: usize = 0; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::plain_time; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::range() + .with_message("Not yet implemented.") + .into()) + } +} diff --git a/boa_engine/src/builtins/temporal/plain_year_month/mod.rs b/boa_engine/src/builtins/temporal/plain_year_month/mod.rs new file mode 100644 index 00000000000..d5c6ead7bdf --- /dev/null +++ b/boa_engine/src/builtins/temporal/plain_year_month/mod.rs @@ -0,0 +1,327 @@ +//! Boa's implementation of the `Temporal.PlainYearMonth` builtin object. + +use crate::{ + builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + js_string, + object::{internal_methods::get_prototype_from_constructor, ObjectData}, + property::Attribute, + realm::Realm, + string::{common::StaticJsStrings, utf16}, + Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; + +use super::plain_date::iso::IsoDateRecord; + +/// The `Temporal.PlainYearMonth` object. +#[derive(Debug, Clone)] +pub struct PlainYearMonth { + pub(crate) inner: IsoDateRecord, + pub(crate) calendar: JsValue, +} + +impl BuiltInObject for PlainYearMonth { + const NAME: JsString = StaticJsStrings::PLAIN_YM; +} + +impl IntrinsicObject for PlainYearMonth { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + let get_calendar_id = BuiltInBuilder::callable(realm, Self::get_calendar_id) + .name(js_string!("get calendarId")) + .build(); + + let get_year = BuiltInBuilder::callable(realm, Self::get_year) + .name(js_string!("get year")) + .build(); + + let get_month = BuiltInBuilder::callable(realm, Self::get_month) + .name(js_string!("get month")) + .build(); + + let get_month_code = BuiltInBuilder::callable(realm, Self::get_month_code) + .name(js_string!("get monthCode")) + .build(); + + let get_days_in_month = BuiltInBuilder::callable(realm, Self::get_days_in_month) + .name(js_string!("get daysInMonth")) + .build(); + + let get_days_in_year = BuiltInBuilder::callable(realm, Self::get_days_in_year) + .name(js_string!("get daysInYear")) + .build(); + + let get_months_in_year = BuiltInBuilder::callable(realm, Self::get_months_in_year) + .name(js_string!("get monthsInYear")) + .build(); + + let get_in_leap_year = BuiltInBuilder::callable(realm, Self::get_in_leap_year) + .name(js_string!("get inLeapYear")) + .build(); + + BuiltInBuilder::from_standard_constructor::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::CONFIGURABLE, + ) + .accessor( + utf16!("calendarId"), + Some(get_calendar_id), + None, + Attribute::default(), + ) + .accessor(utf16!("year"), Some(get_year), None, Attribute::default()) + .accessor(utf16!("month"), Some(get_month), None, Attribute::default()) + .accessor( + utf16!("monthCode"), + Some(get_month_code), + None, + Attribute::default(), + ) + .accessor( + utf16!("daysInMonth"), + Some(get_days_in_month), + None, + Attribute::default(), + ) + .accessor( + utf16!("daysInYear"), + Some(get_days_in_year), + None, + Attribute::default(), + ) + .accessor( + utf16!("monthsInYear"), + Some(get_months_in_year), + None, + Attribute::default(), + ) + .accessor( + utf16!("inLeapYear"), + Some(get_in_leap_year), + None, + Attribute::default(), + ) + .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) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInConstructor for PlainYearMonth { + const LENGTH: usize = 0; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::plain_year_month; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. If NewTarget is undefined, then + if new_target.is_undefined() { + // a. Throw a TypeError exception. + return Err(JsNativeError::typ() + .with_message("NewTarget cannot be undefined when constructing a PlainYearMonth.") + .into()); + } + + let day = args.get_or_undefined(3); + // 2. If referenceISODay is undefined, then + let ref_day = if day.is_undefined() { + // a. Set referenceISODay to 1𝔽. + 1 + } else { + // 6. Let ref be ? ToIntegerWithTruncation(referenceISODay). + super::to_integer_with_truncation(day, context)? + }; + + // 3. Let y be ? ToIntegerWithTruncation(isoYear). + let y = super::to_integer_with_truncation(args.get_or_undefined(0), context)?; + // 4. Let m be ? ToIntegerWithTruncation(isoMonth). + let m = super::to_integer_with_truncation(args.get_or_undefined(1), context)?; + + // TODO: calendar handling. + // 5. Let calendar be ? ToTemporalCalendarSlotValue(calendarLike, "iso8601"). + + // 7. Return ? CreateTemporalYearMonth(y, m, calendar, ref, NewTarget). + let record = IsoDateRecord::new(y, m, ref_day); + create_temporal_year_month( + record, + JsValue::from(js_string!("iso8601")), + Some(new_target), + context, + ) + } +} + +// ==== `PlainYearMonth` Accessor Implementations ==== + +impl PlainYearMonth { + fn get_calendar_id(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_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 { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_month_code(_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 { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_days_in_month(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_months_in_year( + _this: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + fn get_in_leap_year(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } +} + +// ==== `PlainYearMonth` Method Implementations ==== + +impl PlainYearMonth { + fn with(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::typ() + .with_message("not yet implemented.") + .into()) + } + + fn add(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::typ() + .with_message("not yet implemented.") + .into()) + } + + fn subtract(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::typ() + .with_message("not yet implemented.") + .into()) + } + + fn until(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::typ() + .with_message("not yet implemented.") + .into()) + } + + fn since(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::typ() + .with_message("not yet implemented.") + .into()) + } + + fn equals(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + Err(JsNativeError::typ() + .with_message("not yet implemented.") + .into()) + } +} + +// ==== Abstract Operations ==== + +// 9.5.2 `RegulateISOYearMonth ( year, month, overflow )` +// Implemented on `TemporalFields`. + +// 9.5.5 `CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , newTarget ] )` +pub(crate) fn create_temporal_year_month( + year_month_record: IsoDateRecord, + calendar: JsValue, + new_target: Option<&JsValue>, + context: &mut Context<'_>, +) -> JsResult { + // 1. If IsValidISODate(isoYear, isoMonth, referenceISODay) is false, throw a RangeError exception. + if !year_month_record.is_valid() { + return Err(JsNativeError::range() + .with_message("PlainYearMonth values are not a valid ISO date.") + .into()); + } + + // 2. If ! ISOYearMonthWithinLimits(isoYear, isoMonth) is false, throw a RangeError exception. + if year_month_record.within_year_month_limits() { + return Err(JsNativeError::range() + .with_message("PlainYearMonth values are not a valid ISO date.") + .into()); + } + + // 3. If newTarget is not present, set newTarget to %Temporal.PlainYearMonth%. + let new_target = if let Some(target) = new_target { + target.clone() + } else { + context + .realm() + .intrinsics() + .constructors() + .plain_year_month() + .constructor() + .into() + }; + + // 4. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainYearMonth.prototype%", « [[InitializedTemporalYearMonth]], [[ISOYear]], [[ISOMonth]], [[ISODay]], [[Calendar]] »). + let proto = get_prototype_from_constructor( + &new_target, + StandardConstructors::plain_year_month, + context, + )?; + + // 5. Set object.[[ISOYear]] to isoYear. + // 6. Set object.[[ISOMonth]] to isoMonth. + // 7. Set object.[[Calendar]] to calendar. + // 8. Set object.[[ISODay]] to referenceISODay. + + let obj = JsObject::from_proto_and_data( + proto, + ObjectData::plain_year_month(PlainYearMonth { + inner: year_month_record, + calendar, + }), + ); + + // 9. Return object. + Ok(obj.into()) +} diff --git a/boa_engine/src/builtins/temporal/tests.rs b/boa_engine/src/builtins/temporal/tests.rs new file mode 100644 index 00000000000..5cd24b57f4b --- /dev/null +++ b/boa_engine/src/builtins/temporal/tests.rs @@ -0,0 +1,52 @@ +use super::date_equations::{epoch_time_to_month_in_year, mathematical_in_leap_year}; +use crate::{js_string, run_test_actions, JsValue, TestAction}; + +// Temporal Object tests. + +#[test] +fn temporal_object() { + // Temporal Builtin tests. + run_test_actions([ + TestAction::assert_eq( + "Object.prototype.toString.call(Temporal)", + js_string!("[object Temporal]"), + ), + TestAction::assert_eq("String(Temporal)", js_string!("[object Temporal]")), + TestAction::assert_eq("Object.keys(Temporal).length === 0", true), + ]); +} + +#[test] +fn now_object() { + // Now Builtin tests. + run_test_actions([ + TestAction::assert_eq("Object.isExtensible(Temporal.Now)", true), + TestAction::assert_eq( + "Object.prototype.toString.call(Temporal.Now)", + js_string!("[object Temporal.Now]"), + ), + TestAction::assert_eq( + "Object.getPrototypeOf(Temporal.Now) === Object.prototype", + true, + ), + TestAction::assert_eq("Temporal.Now.prototype", JsValue::undefined()), + ]); +} + +// Date Equations + +#[test] +fn time_to_month() { + let oct_2023 = 1_696_459_917_000_f64; + let mar_1_2020 = 1_583_020_800_000_f64; + let feb_29_2020 = 1_582_934_400_000_f64; + let mar_1_2021 = 1_614_556_800_000_f64; + + assert_eq!(epoch_time_to_month_in_year(oct_2023), 9); + assert_eq!(epoch_time_to_month_in_year(mar_1_2020), 2); + assert_eq!(mathematical_in_leap_year(mar_1_2020), 1); + assert_eq!(epoch_time_to_month_in_year(feb_29_2020), 1); + assert_eq!(mathematical_in_leap_year(feb_29_2020), 1); + assert_eq!(epoch_time_to_month_in_year(mar_1_2021), 2); + assert_eq!(mathematical_in_leap_year(mar_1_2021), 0); +} diff --git a/boa_engine/src/builtins/temporal/time_zone/mod.rs b/boa_engine/src/builtins/temporal/time_zone/mod.rs new file mode 100644 index 00000000000..1f053ade575 --- /dev/null +++ b/boa_engine/src/builtins/temporal/time_zone/mod.rs @@ -0,0 +1,491 @@ +#![allow(dead_code)] + +use crate::{ + builtins::{ + temporal::to_zero_padded_decimal_string, BuiltInBuilder, BuiltInConstructor, BuiltInObject, + IntrinsicObject, + }, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + js_string, + object::{internal_methods::get_prototype_from_constructor, ObjectData, CONSTRUCTOR}, + property::Attribute, + realm::Realm, + string::{common::StaticJsStrings, utf16}, + Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; + +/// The `Temporal.TimeZone` object. +#[derive(Debug, Clone)] +pub struct TimeZone { + pub(crate) initialized_temporal_time_zone: bool, + pub(crate) identifier: String, + pub(crate) offset_nanoseconds: Option, +} + +impl BuiltInObject for TimeZone { + const NAME: JsString = StaticJsStrings::TIMEZONE; +} + +impl IntrinsicObject for TimeZone { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + let get_id = BuiltInBuilder::callable(realm, Self::get_id) + .name(js_string!("get Id")) + .build(); + + BuiltInBuilder::from_standard_constructor::(realm) + .method( + Self::get_offset_nanoseconds_for, + js_string!("getOffsetNanosecondsFor"), + 1, + ) + .method( + Self::get_offset_string_for, + js_string!("getOffsetStringFor"), + 1, + ) + .method( + Self::get_plain_date_time_for, + js_string!("getPlainDateTimeFor"), + 2, + ) + .method(Self::get_instant_for, js_string!("getInstantFor"), 2) + .method( + Self::get_possible_instants_for, + js_string!("getPossibleInstantFor"), + 1, + ) + .method( + Self::get_next_transition, + js_string!("getNextTransition"), + 1, + ) + .method( + Self::get_previous_transition, + js_string!("getPreviousTransition"), + 1, + ) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_string, js_string!("toJSON"), 0) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + CONSTRUCTOR, + realm.intrinsics().constructors().time_zone().prototype(), + Attribute::default(), + ) + .accessor(utf16!("id"), Some(get_id), None, Attribute::default()) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInConstructor for TimeZone { + const LENGTH: usize = 1; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::time_zone; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. If NewTarget is undefined, then + // 1a. Throw a TypeError exception. + if new_target.is_undefined() { + return Err(JsNativeError::typ() + .with_message("newTarget cannot be undefined for Temporal.TimeZone constructor") + .into()); + }; + + // 2. Set identifier to ? ToString(identifier). + let identifier = args.get_or_undefined(0); + if identifier.is_undefined() { + return Err(JsNativeError::range() + .with_message("Temporal.TimeZone must be called with a valid initializer") + .into()); + } + + // 3. If IsTimeZoneOffsetString(identifier) is false, then + // a. If IsAvailableTimeZoneName(identifier) is false, then + // i. Throw a RangeError exception. + // b. Set identifier to ! CanonicalizeTimeZoneName(identifier). + // 4. Return ? CreateTemporalTimeZone(identifier, NewTarget). + create_temporal_time_zone( + identifier.to_string(context)?.to_std_string_escaped(), + Some(new_target.clone()), + context, + ) + } +} + +impl TimeZone { + // NOTE: id, toJSON, toString currently share the exact same implementation -> Consolidate into one function and define multiple accesors? + pub(crate) fn get_id(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") + })?; + let tz = o.as_time_zone().ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") + })?; + Ok(JsString::from(tz.identifier.clone()).into()) + } + + pub(crate) fn get_offset_nanoseconds_for( + this: &JsValue, + args: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + // 1. Let timeZone be the this value. + // 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]). + let _tz = this + .as_object() + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") + })? + .borrow() + .as_time_zone() + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") + })?; + // 3. Set instant to ? ToTemporalInstant(instant). + let _i = args.get_or_undefined(0); + // TODO: to_temporal_instant is abstract operation for Temporal.Instant objects. + // let instant = to_temporal_instant(i)?; + + // 4. If timeZone.[[OffsetNanoseconds]] is not undefined, return 𝔽(timeZone.[[OffsetNanoseconds]]). + // 5. Return 𝔽(GetNamedTimeZoneOffsetNanoseconds(timeZone.[[Identifier]], instant.[[Nanoseconds]])). + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + pub(crate) fn get_offset_string_for( + this: &JsValue, + args: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + // 1. Let timeZone be the this value. + // 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]). + let _tz = this + .as_object() + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") + })? + .borrow() + .as_time_zone() + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") + })?; + // 3. Set instant to ? ToTemporalInstant(instant). + let _i = args.get_or_undefined(0); + // TODO: to_temporal_instant is abstract operation for Temporal.Instant objects. + // let instant = to_temporal_instant(i)?; + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + + // 4. Return ? GetOffsetStringFor(timeZone, instant). + } + + pub(crate) fn get_plain_date_time_for( + _: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + pub(crate) fn get_instant_for( + _: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + pub(crate) fn get_possible_instants_for( + _: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + pub(crate) fn get_next_transition( + _: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + pub(crate) fn get_previous_transition( + _: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) + } + + pub(crate) fn to_string( + this: &JsValue, + _: &[JsValue], + _: &mut Context<'_>, + ) -> JsResult { + // 1. Let timeZone be the this value. + // 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]). + let o = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") + })?; + let o = o.borrow(); + let tz = o.as_time_zone().ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") + })?; + // 3. Return timeZone.[[Identifier]]. + Ok(JsString::from(tz.identifier.clone()).into()) + } +} + +// -- TimeZone Abstract Operations -- + +/// Abstract operation `DefaultTimeZone ( )` +/// +/// The abstract operation `DefaultTimeZone` takes no arguments. It returns a String value +/// representing the host environment's current time zone, which is either a valid (11.1.1) and +/// canonicalized (11.1.2) time zone name, or an offset conforming to the syntax of a +/// `TimeZoneNumericUTCOffset`. +/// +/// An ECMAScript implementation that includes the ECMA-402 Internationalization API must implement +/// the `DefaultTimeZone` abstract operation as specified in the ECMA-402 specification. +/// +/// More information: +/// - [ECMAScript specififcation][spec] +/// +/// [spec]: https://tc39.es/proposal-temporal/#sec-defaulttimezone +#[allow(unused)] +pub(super) fn default_time_zone(context: &mut Context<'_>) -> String { + // The minimum implementation of DefaultTimeZone for ECMAScript implementations that do not + // include the ECMA-402 API, supporting only the "UTC" time zone, performs the following steps + // when called: + + // 1. Return "UTC". + "UTC".to_owned() + + // TO-DO: full, system-aware implementation (and intl feature) +} + +/// Abstract operation `CreateTemporalTimeZone ( identifier [ , newTarget ] )` +/// +/// More information: +/// - [ECMAScript specififcation][spec] +/// +/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-createtemporaltimezone +#[allow(clippy::needless_pass_by_value, unused)] +pub(super) fn create_temporal_time_zone( + identifier: String, + new_target: Option, + context: &mut Context<'_>, +) -> JsResult { + // 1. If newTarget is not present, set newTarget to %Temporal.TimeZone%. + let new_target = new_target.unwrap_or_else(|| { + context + .realm() + .intrinsics() + .constructors() + .time_zone() + .prototype() + .into() + }); + + // 2. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.TimeZone.prototype%", « [[InitializedTemporalTimeZone]], [[Identifier]], [[OffsetNanoseconds]] »). + let prototype = + get_prototype_from_constructor(&new_target, StandardConstructors::time_zone, context)?; + + // 3. Let offsetNanosecondsResult be Completion(ParseTimeZoneOffsetString(identifier)). + let offset_nanoseconds_result = parse_timezone_offset_string(&identifier, context); + + // 4. If offsetNanosecondsResult is an abrupt completion, then + let (identifier, offset_nanoseconds) = if let Ok(offset_nanoseconds) = offset_nanoseconds_result + { + // Switched conditions for more idiomatic rust code structuring + // 5. Else, + // a. Set object.[[Identifier]] to ! FormatTimeZoneOffsetString(offsetNanosecondsResult.[[Value]]). + // b. Set object.[[OffsetNanoseconds]] to offsetNanosecondsResult.[[Value]]. + ( + format_time_zone_offset_string(offset_nanoseconds), + Some(offset_nanoseconds), + ) + } else { + // a. Assert: ! CanonicalizeTimeZoneName(identifier) is identifier. + assert_eq!(canonicalize_time_zone_name(&identifier), identifier); + + // b. Set object.[[Identifier]] to identifier. + // c. Set object.[[OffsetNanoseconds]] to undefined. + (identifier, None) + }; + + // 6. Return object. + let object = JsObject::from_proto_and_data( + prototype, + ObjectData::time_zone(TimeZone { + initialized_temporal_time_zone: false, + identifier, + offset_nanoseconds, + }), + ); + Ok(object.into()) +} + +/// Abstract operation `ParseTimeZoneOffsetString ( offsetString )` +/// +/// The abstract operation `ParseTimeZoneOffsetString` takes argument `offsetString` (a String). It +/// parses the argument as a numeric UTC offset string and returns a signed integer representing +/// that offset as a number of nanoseconds. +/// +/// More information: +/// - [ECMAScript specififcation][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring +#[allow(clippy::unnecessary_wraps, unused)] +fn parse_timezone_offset_string(offset_string: &str, context: &mut Context<'_>) -> JsResult { + use boa_parser::temporal::{IsoCursor, TemporalTimeZoneString}; + + // 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset). + let parse_result = TemporalTimeZoneString::parse(&mut IsoCursor::new(offset_string))?; + + // 2. Assert: parseResult is not a List of errors. + // 3. Assert: parseResult contains a TemporalSign Parse Node. + let Some(utc_offset) = parse_result.offset else { + return Err(JsNativeError::typ() + .with_message("Offset string was not a valid offset") + .into()); + }; + + // 4. Let parsedSign be the source text matched by the TemporalSign Parse Node contained within + // parseResult. + // 5. If parsedSign is the single code point U+002D (HYPHEN-MINUS) or U+2212 (MINUS SIGN), then + let sign = utc_offset.sign; + // a. Let sign be -1. + // 6. Else, + // a. Let sign be 1. + + // 7. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed + // values is guaranteed to be a sufficiently short string of decimal digits. + // 8. Assert: parseResult contains an Hour Parse Node. + // 9. Let parsedHours be the source text matched by the Hour Parse Node contained within parseResult. + let parsed_hours = utc_offset.hour; + + // 10. Let hours be ℝ(StringToNumber(CodePointsToString(parsedHours))). + // 11. If parseResult does not contain a MinuteSecond Parse Node, then + // a. Let minutes be 0. + // 12. Else, + // a. Let parsedMinutes be the source text matched by the first MinuteSecond Parse Node contained within parseResult. + // b. Let minutes be ℝ(StringToNumber(CodePointsToString(parsedMinutes))). + // 13. If parseResult does not contain two MinuteSecond Parse Nodes, then + // a. Let seconds be 0. + // 14. Else, + // a. Let parsedSeconds be the source text matched by the second MinuteSecond Parse Node contained within parseResult. + // b. Let seconds be ℝ(StringToNumber(CodePointsToString(parsedSeconds))). + // 15. If parseResult does not contain a TemporalDecimalFraction Parse Node, then + // a. Let nanoseconds be 0. + // 16. Else, + // a. Let parsedFraction be the source text matched by the TemporalDecimalFraction Parse Node contained within parseResult. + // b. Let fraction be the string-concatenation of CodePointsToString(parsedFraction) and "000000000". + // c. Let nanosecondsString be the substring of fraction from 1 to 10. + // d. Let nanoseconds be ℝ(StringToNumber(nanosecondsString)). + // 17. Return sign × (((hours × 60 + minutes) × 60 + seconds) × 10^9 + nanoseconds). + + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) +} + +/// Abstract operation `FormatTimeZoneOffsetString ( offsetNanoseconds )` +fn format_time_zone_offset_string(offset_nanoseconds: i64) -> String { + // 1. Assert: offsetNanoseconds is an integer. + + // 2. If offsetNanoseconds ≥ 0, let sign be "+"; otherwise, let sign be "-". + let sign = if offset_nanoseconds >= 0 { "+" } else { "-" }; + + // 3. Let offsetNanoseconds be abs(offsetNanoseconds). + let offset_nanoseconds = offset_nanoseconds.unsigned_abs(); + + // 4. Let nanoseconds be offsetNanoseconds modulo 10^9. + let nanoseconds = offset_nanoseconds % 1_000_000_000; + + // 5. Let seconds be floor(offsetNanoseconds / 10^9) modulo 60. + let seconds = (offset_nanoseconds / 1_000_000_000) % 60; + + // 6. Let minutes be floor(offsetNanoseconds / (6 × 10^10)) modulo 60. + let minutes = (offset_nanoseconds / 60_000_000_000) % 60; + + // 7. Let hours be floor(offsetNanoseconds / (3.6 × 1012)). + let hours = (offset_nanoseconds / 3_600_000_000_000) % 60; + + // 8. Let h be ToZeroPaddedDecimalString(hours, 2). + let h = to_zero_padded_decimal_string(hours, 2); + + // 9. Let m be ToZeroPaddedDecimalString(minutes, 2). + let m = to_zero_padded_decimal_string(minutes, 2); + + // 10. Let s be ToZeroPaddedDecimalString(seconds, 2). + let s = to_zero_padded_decimal_string(seconds, 2); + + // 11. If nanoseconds ≠ 0, then + let post = if nanoseconds != 0 { + // a. Let fraction be ToZeroPaddedDecimalString(nanoseconds, 9). + let fraction = to_zero_padded_decimal_string(nanoseconds, 9); + + // b. Set fraction to the longest possible substring of fraction starting at position 0 and not ending with the code unit 0x0030 (DIGIT ZERO). + let fraction = fraction.trim_end_matches('0'); + + // c. Let post be the string-concatenation of the code unit 0x003A (COLON), s, the code unit 0x002E (FULL STOP), and fraction. + format!(":{s}.{fraction}") + } else if seconds != 0 { + // 12. Else if seconds ≠ 0, then + // a. Let post be the string-concatenation of the code unit 0x003A (COLON) and s. + format!(":{s}") + } else { + // 13. Else, + // a. Let post be the empty String. + String::new() + }; + + // 14. Return the string-concatenation of sign, h, the code unit 0x003A (COLON), m, and post. + format!("{sign}{h}:{m}{post}") +} + +/// Abstract operation `CanonicalizeTimeZoneName ( timeZone )` +/// +/// The abstract operation `CanonicalizeTimeZoneName` takes argument `timeZone` (a String that is a +/// valid time zone name as verified by `IsAvailableTimeZoneName`). It returns the canonical and +/// case-regularized form of `timeZone`. +fn canonicalize_time_zone_name(time_zone: &str) -> String { + // The minimum implementation of CanonicalizeTimeZoneName for ECMAScript implementations that + // do not include local political rules for any time zones performs the following steps when + // called: + // 1. Assert: timeZone is an ASCII-case-insensitive match for "UTC". + assert!(time_zone.to_ascii_uppercase() == "UTC"); + // 2. Return "UTC". + "UTC".to_owned() +} diff --git a/boa_engine/src/builtins/temporal/zoned_date_time/mod.rs b/boa_engine/src/builtins/temporal/zoned_date_time/mod.rs new file mode 100644 index 00000000000..6034dec9f3f --- /dev/null +++ b/boa_engine/src/builtins/temporal/zoned_date_time/mod.rs @@ -0,0 +1,133 @@ +#![allow(dead_code, unused_variables)] +use crate::{ + builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + property::Attribute, + realm::Realm, + string::common::StaticJsStrings, + Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; +use boa_profiler::Profiler; + +/// The `Temporal.ZonedDateTime` object. +#[derive(Debug, Clone)] +pub struct ZonedDateTime { + nanoseconds: JsBigInt, + time_zone: JsObject, + calendar: JsObject, +} + +impl BuiltInObject for ZonedDateTime { + const NAME: JsString = StaticJsStrings::ZONED_DT; +} + +impl IntrinsicObject for ZonedDateTime { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); + + BuiltInBuilder::from_standard_constructor::(realm) + .static_property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::CONFIGURABLE, + ) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInConstructor for ZonedDateTime { + const LENGTH: usize = 0; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::zoned_date_time; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // TODO: Implement ZonedDateTime. + Err(JsNativeError::error() + .with_message("%ZonedDateTime% not yet implemented.") + .into()) + } +} + +// -- ZonedDateTime Abstract Operations -- + +///6.5.5 `AddZonedDateTime ( epochNanoseconds, timeZone, calendar, years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , options ] )` +pub(crate) fn add_zoned_date_time( + epoch_nanos: &JsBigInt, + time_zone: &JsObject, + calendar: &JsObject, + duration: super::duration::DurationRecord, + options: Option<&JsObject>, +) -> JsResult { + // 1. If options is not present, set options to undefined. + // 2. Assert: Type(options) is Object or Undefined. + // 3. If years = 0, months = 0, weeks = 0, and days = 0, then + // a. Return ? AddInstant(epochNanoseconds, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + // 4. Let instant be ! CreateTemporalInstant(epochNanoseconds). + // 5. Let temporalDateTime be ? GetPlainDateTimeFor(timeZone, instant, calendar). + // 6. Let datePart be ! CreateTemporalDate(temporalDateTime.[[ISOYear]], temporalDateTime.[[ISOMonth]], temporalDateTime.[[ISODay]], calendar). + // 7. Let dateDuration be ! CreateTemporalDuration(years, months, weeks, days, 0, 0, 0, 0, 0, 0). + // 8. Let addedDate be ? CalendarDateAdd(calendar, datePart, dateDuration, options). + // 9. Let intermediateDateTime be ? CreateTemporalDateTime(addedDate.[[ISOYear]], addedDate.[[ISOMonth]], addedDate.[[ISODay]], temporalDateTime.[[ISOHour]], temporalDateTime.[[ISOMinute]], temporalDateTime.[[ISOSecond]], temporalDateTime.[[ISOMillisecond]], temporalDateTime.[[ISOMicrosecond]], temporalDateTime.[[ISONanosecond]], calendar). + // 10. Let intermediateInstant be ? GetInstantFor(timeZone, intermediateDateTime, "compatible"). + // 11. Return ? AddInstant(intermediateInstant.[[Nanoseconds]], hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + Err(JsNativeError::error() + .with_message("%ZonedDateTime% not yet implemented.") + .into()) +} + +/// 6.5.7 `NanosecondsToDays ( nanoseconds, relativeTo )` +pub(crate) fn nanoseconds_to_days( + nanoseconds: f64, + relative_to: &JsValue, +) -> JsResult<(i32, i32, i32)> { + // 1. Let dayLengthNs be nsPerDay. + // 2. If nanoseconds = 0, then + // a. Return the Record { [[Days]]: 0, [[Nanoseconds]]: 0, [[DayLength]]: dayLengthNs }. + // 3. If nanoseconds < 0, let sign be -1; else, let sign be 1. + // 4. If Type(relativeTo) is not Object or relativeTo does not have an [[InitializedTemporalZonedDateTime]] internal slot, then + // a. Return the Record { [[Days]]: truncate(nanoseconds / dayLengthNs), [[Nanoseconds]]: (abs(nanoseconds) modulo dayLengthNs) × sign, [[DayLength]]: dayLengthNs }. + // 5. Let startNs be ℝ(relativeTo.[[Nanoseconds]]). + // 6. Let startInstant be ! CreateTemporalInstant(ℤ(startNs)). + // 7. Let startDateTime be ? GetPlainDateTimeFor(relativeTo.[[TimeZone]], startInstant, relativeTo.[[Calendar]]). + // 8. Let endNs be startNs + nanoseconds. + // 9. If ! IsValidEpochNanoseconds(ℤ(endNs)) is false, throw a RangeError exception. + // 10. Let endInstant be ! CreateTemporalInstant(ℤ(endNs)). + // 11. Let endDateTime be ? GetPlainDateTimeFor(relativeTo.[[TimeZone]], endInstant, relativeTo.[[Calendar]]). + // 12. Let dateDifference be ? DifferenceISODateTime(startDateTime.[[ISOYear]], startDateTime.[[ISOMonth]], startDateTime.[[ISODay]], startDateTime.[[ISOHour]], startDateTime.[[ISOMinute]], startDateTime.[[ISOSecond]], startDateTime.[[ISOMillisecond]], startDateTime.[[ISOMicrosecond]], startDateTime.[[ISONanosecond]], endDateTime.[[ISOYear]], endDateTime.[[ISOMonth]], endDateTime.[[ISODay]], endDateTime.[[ISOHour]], endDateTime.[[ISOMinute]], endDateTime.[[ISOSecond]], endDateTime.[[ISOMillisecond]], endDateTime.[[ISOMicrosecond]], endDateTime.[[ISONanosecond]], relativeTo.[[Calendar]], "day", OrdinaryObjectCreate(null)). + // 13. Let days be dateDifference.[[Days]]. + // 14. Let intermediateNs be ℝ(? AddZonedDateTime(ℤ(startNs), relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)). + // 15. If sign is 1, then + // a. Repeat, while days > 0 and intermediateNs > endNs, + // i. Set days to days - 1. + // ii. Set intermediateNs to ℝ(? AddZonedDateTime(ℤ(startNs), relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)). + // 16. Set nanoseconds to endNs - intermediateNs. + // 17. Let done be false. + // 18. Repeat, while done is false, + // a. Let oneDayFartherNs be ℝ(? AddZonedDateTime(ℤ(intermediateNs), relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, sign, 0, 0, 0, 0, 0, 0)). + // b. Set dayLengthNs to oneDayFartherNs - intermediateNs. + // c. If (nanoseconds - dayLengthNs) × sign ≥ 0, then + // i. Set nanoseconds to nanoseconds - dayLengthNs. + // ii. Set intermediateNs to oneDayFartherNs. + // iii. Set days to days + sign. + // d. Else, + // i. Set done to true. + // 19. If days < 0 and sign = 1, throw a RangeError exception. + // 20. If days > 0 and sign = -1, throw a RangeError exception. + // 21. If nanoseconds < 0, then + // a. Assert: sign is -1. + // 22. If nanoseconds > 0 and sign = -1, throw a RangeError exception. + // 23. Assert: The inequality abs(nanoseconds) < abs(dayLengthNs) holds. + // 24. Return the Record { [[Days]]: days, [[Nanoseconds]]: nanoseconds, [[DayLength]]: abs(dayLengthNs) }. + Err(JsNativeError::error() + .with_message("%ZonedDateTime% not yet implemented.") + .into()) +} diff --git a/boa_engine/src/context/intrinsics.rs b/boa_engine/src/context/intrinsics.rs index 70d58198e98..ea52c299225 100644 --- a/boa_engine/src/context/intrinsics.rs +++ b/boa_engine/src/context/intrinsics.rs @@ -166,6 +166,26 @@ pub struct StandardConstructors { segmenter: StandardConstructor, #[cfg(feature = "intl")] plural_rules: StandardConstructor, + #[cfg(feature = "experimental")] + instant: StandardConstructor, + #[cfg(feature = "experimental")] + plain_date_time: StandardConstructor, + #[cfg(feature = "experimental")] + plain_date: StandardConstructor, + #[cfg(feature = "experimental")] + plain_time: StandardConstructor, + #[cfg(feature = "experimental")] + plain_year_month: StandardConstructor, + #[cfg(feature = "experimental")] + plain_month_day: StandardConstructor, + #[cfg(feature = "experimental")] + time_zone: StandardConstructor, + #[cfg(feature = "experimental")] + duration: StandardConstructor, + #[cfg(feature = "experimental")] + zoned_date_time: StandardConstructor, + #[cfg(feature = "experimental")] + calendar: StandardConstructor, } impl Default for StandardConstructors { @@ -242,6 +262,26 @@ impl Default for StandardConstructors { segmenter: StandardConstructor::default(), #[cfg(feature = "intl")] plural_rules: StandardConstructor::default(), + #[cfg(feature = "experimental")] + instant: StandardConstructor::default(), + #[cfg(feature = "experimental")] + plain_date_time: StandardConstructor::default(), + #[cfg(feature = "experimental")] + plain_date: StandardConstructor::default(), + #[cfg(feature = "experimental")] + plain_time: StandardConstructor::default(), + #[cfg(feature = "experimental")] + plain_year_month: StandardConstructor::default(), + #[cfg(feature = "experimental")] + plain_month_day: StandardConstructor::default(), + #[cfg(feature = "experimental")] + time_zone: StandardConstructor::default(), + #[cfg(feature = "experimental")] + duration: StandardConstructor::default(), + #[cfg(feature = "experimental")] + zoned_date_time: StandardConstructor::default(), + #[cfg(feature = "experimental")] + calendar: StandardConstructor::default(), } } } @@ -827,6 +867,136 @@ impl StandardConstructors { pub const fn plural_rules(&self) -> &StandardConstructor { &self.plural_rules } + + /// Returns the `Temporal.Instant` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-instant-constructor + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn instant(&self) -> &StandardConstructor { + &self.instant + } + + /// Returns the `Temporal.PlainDateTime` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-plaindatetime-constructor + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn plain_date_time(&self) -> &StandardConstructor { + &self.plain_date_time + } + + /// Returns the `Temporal.PlainDate` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-plaindate-constructor + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn plain_date(&self) -> &StandardConstructor { + &self.plain_date + } + + /// Returns the `Temporal.PlainTime` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-plaintime-constructor + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn plain_time(&self) -> &StandardConstructor { + &self.plain_time + } + + /// Returns the `Temporal.PlainYearMonth` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-plainyearmonth-constructor + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn plain_year_month(&self) -> &StandardConstructor { + &self.plain_year_month + } + + /// Returns the `Temporal.PlainMonthDay` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-plainmonthday-constructor + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn plain_month_day(&self) -> &StandardConstructor { + &self.plain_month_day + } + + /// Returns the `Temporal.TimeZone` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-timezone-constructor + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn time_zone(&self) -> &StandardConstructor { + &self.time_zone + } + + /// Returns the `Temporal.Duration` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-duration-constructor + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn duration(&self) -> &StandardConstructor { + &self.duration + } + + /// Returns the `Temporal.ZonedDateTime` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-zoneddatetime-constructor + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn zoned_date_time(&self) -> &StandardConstructor { + &self.zoned_date_time + } + + /// Returns the `Temporal.Calendar` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-calendar-constructor + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn calendar(&self) -> &StandardConstructor { + &self.calendar + } } /// Cached intrinsic objects @@ -892,6 +1062,14 @@ pub struct IntrinsicObjects { /// [`%SegmentsPrototype%`](https://tc39.es/ecma402/#sec-%segmentsprototype%-object) #[cfg(feature = "intl")] segments_prototype: JsObject, + + /// [`%Temporal%`](https://tc39.es/proposal-temporal/#sec-temporal-objects) + #[cfg(feature = "experimental")] + temporal: JsObject, + + /// [`%Temporal.Now%`](https://tc39.es/proposal-temporal/#sec-temporal-now-object) + #[cfg(feature = "experimental")] + now: JsObject, } impl Default for IntrinsicObjects { @@ -920,6 +1098,10 @@ impl Default for IntrinsicObjects { intl: JsObject::default(), #[cfg(feature = "intl")] segments_prototype: JsObject::default(), + #[cfg(feature = "experimental")] + temporal: JsObject::default(), + #[cfg(feature = "experimental")] + now: JsObject::default(), } } } @@ -980,12 +1162,14 @@ impl IntrinsicObjects { /// Gets the [`%eval%`][spec] intrinsic function. /// /// [spec]: https://tc39.es/ecma262/#sec-eval-x + #[inline] #[must_use] pub fn eval(&self) -> JsFunction { self.eval.clone() } /// Gets the URI intrinsic functions. + #[inline] #[must_use] pub const fn uri_functions(&self) -> &UriFunctions { &self.uri_functions @@ -994,6 +1178,7 @@ impl IntrinsicObjects { /// Gets the [`%Reflect%`][spec] intrinsic object. /// /// [spec]: https://tc39.es/ecma262/#sec-reflect + #[inline] #[must_use] pub fn reflect(&self) -> JsObject { self.reflect.clone() @@ -1002,6 +1187,7 @@ impl IntrinsicObjects { /// Gets the [`%Math%`][spec] intrinsic object. /// /// [spec]: https://tc39.es/ecma262/#sec-math + #[inline] #[must_use] pub fn math(&self) -> JsObject { self.math.clone() @@ -1010,6 +1196,7 @@ impl IntrinsicObjects { /// Gets the [`%JSON%`][spec] intrinsic object. /// /// [spec]: https://tc39.es/ecma262/#sec-json + #[inline] #[must_use] pub fn json(&self) -> JsObject { self.json.clone() @@ -1018,6 +1205,7 @@ impl IntrinsicObjects { /// Gets the [`%isFinite%`][spec] intrinsic function. /// /// [spec]: https://tc39.es/ecma262/#sec-isfinite-number + #[inline] #[must_use] pub fn is_finite(&self) -> JsFunction { self.is_finite.clone() @@ -1026,6 +1214,7 @@ impl IntrinsicObjects { /// Gets the [`%isNaN%`][spec] intrinsic function. /// /// [spec]: https://tc39.es/ecma262/#sec-isnan-number + #[inline] #[must_use] pub fn is_nan(&self) -> JsFunction { self.is_nan.clone() @@ -1034,6 +1223,7 @@ impl IntrinsicObjects { /// Gets the [`%parseFloat%`][spec] intrinsic function. /// /// [spec]: https://tc39.es/ecma262/#sec-parsefloat-string + #[inline] #[must_use] pub fn parse_float(&self) -> JsFunction { self.parse_float.clone() @@ -1042,6 +1232,7 @@ impl IntrinsicObjects { /// Gets the [`%parseInt%`][spec] intrinsic function. /// /// [spec]: https://tc39.es/ecma262/#sec-parseint-string-radix + #[inline] #[must_use] pub fn parse_int(&self) -> JsFunction { self.parse_int.clone() @@ -1052,6 +1243,7 @@ impl IntrinsicObjects { /// [spec]: https://tc39.es/ecma262/#sec-escape-string #[must_use] #[cfg(feature = "annex-b")] + #[inline] pub fn escape(&self) -> JsFunction { self.escape.clone() } @@ -1061,6 +1253,7 @@ impl IntrinsicObjects { /// [spec]: https://tc39.es/ecma262/#sec-unescape-string #[must_use] #[cfg(feature = "annex-b")] + #[inline] pub fn unescape(&self) -> JsFunction { self.unescape.clone() } @@ -1070,6 +1263,7 @@ impl IntrinsicObjects { /// [spec]: https://tc39.es/ecma402/#intl-object #[must_use] #[cfg(feature = "intl")] + #[inline] pub fn intl(&self) -> JsObject { self.intl.clone() } @@ -1082,6 +1276,26 @@ impl IntrinsicObjects { pub fn segments_prototype(&self) -> JsObject { self.segments_prototype.clone() } + + /// Gets the [`%Temporal%`][spec] intrinsic object. + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-objects + #[cfg(feature = "experimental")] + #[must_use] + #[inline] + pub fn temporal(&self) -> JsObject { + self.temporal.clone() + } + + /// Gets the [`%Temporal.Now%`][spec] intrinsic object. + /// + /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-now-object + #[cfg(feature = "experimental")] + #[must_use] + #[inline] + pub fn now(&self) -> JsObject { + self.now.clone() + } } /// Contains commonly used [`ObjectTemplate`]s. diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index a6a2323d2ac..76ad7840182 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -75,6 +75,7 @@ impl JsObject { }), } } + /// Creates a new ordinary object with its prototype set to the `Object` prototype. /// /// This is equivalent to calling the specification's abstract operation @@ -735,6 +736,110 @@ impl JsObject { self.borrow().is_ordinary() } + /// Checks if current object is a `Temporal.Duration` object. + /// + /// # Panics + /// + /// Panics if the object is currently mutably borrowed. + #[inline] + #[must_use] + #[track_caller] + #[cfg(feature = "experimental")] + pub fn is_duration(&self) -> bool { + self.borrow().is_duration() + } + + /// Checks if current object is a `Temporal.TimeZone` object. + /// + /// # Panics + /// + /// Panics if the object is currently mutably borrowed. + #[inline] + #[must_use] + #[track_caller] + #[cfg(feature = "experimental")] + pub fn is_time_zone(&self) -> bool { + self.borrow().is_time_zone() + } + + /// Checks if current object is a `Temporal.PlainDateTime` object. + /// + /// # Panics + /// + /// Panics if the object is currently mutably borrowed. + #[inline] + #[must_use] + #[track_caller] + #[cfg(feature = "experimental")] + pub fn is_plain_date_time(&self) -> bool { + self.borrow().is_plain_date_time() + } + + /// Checks if current object is a `Temporal.PlainDate` object. + /// + /// # Panics + /// + /// Panics if the object is currently mutably borrowed. + #[inline] + #[must_use] + #[track_caller] + #[cfg(feature = "experimental")] + pub fn is_plain_date(&self) -> bool { + self.borrow().is_plain_date() + } + + /// Checks if current object is a `Temporal.PlainYearMonth` object. + /// + /// # Panics + /// + /// Panics if the object is currently mutably borrowed. + #[inline] + #[must_use] + #[track_caller] + #[cfg(feature = "experimental")] + pub fn is_plain_year_month(&self) -> bool { + self.borrow().is_plain_year_month() + } + + /// Checks if current object is a `Temporal.PlainMonthDay` object. + /// + /// # Panics + /// + /// Panics if the object is currently mutably borrowed. + #[inline] + #[must_use] + #[track_caller] + #[cfg(feature = "experimental")] + pub fn is_plain_month_day(&self) -> bool { + self.borrow().is_plain_month_day() + } + + /// Checks if current object is a `Temporal.ZonedDateTime` object. + /// + /// # Panics + /// + /// Panics if the object is currently mutably borrowed. + #[inline] + #[must_use] + #[track_caller] + #[cfg(feature = "experimental")] + pub fn is_zoned_date_time(&self) -> bool { + self.borrow().is_zoned_date_time() + } + + /// Checks if current object is a `Temporal.Calendar` object. + /// + /// # Panics + /// + /// Panics if the object is currently mutably borrowed. + #[inline] + #[must_use] + #[track_caller] + #[cfg(feature = "experimental")] + pub fn is_calendar(&self) -> bool { + self.borrow().is_calendar() + } + /// Checks if it's a proxy object. /// /// # Panics diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 7d023a0e746..a70e4b5aa95 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -38,6 +38,11 @@ use crate::builtins::intl::{ plural_rules::PluralRules, segmenter::{SegmentIterator, Segmenter, Segments}, }; +#[cfg(feature = "experimental")] +use crate::builtins::temporal::{ + Calendar, Duration, Instant, PlainDate, PlainDateTime, PlainMonthDay, PlainTime, + PlainYearMonth, TimeZone, ZonedDateTime, +}; use crate::{ builtins::{ array::ArrayIterator, @@ -443,10 +448,49 @@ pub enum ObjectKind { /// The `Segment Iterator` object kind. #[cfg(feature = "intl")] SegmentIterator(SegmentIterator), - /// The `PluralRules` object kind. #[cfg(feature = "intl")] PluralRules(PluralRules), + + /// The `Temporal.Instant` object kind. + #[cfg(feature = "experimental")] + Instant(Instant), + + /// The `Temporal.PlainDateTime` object kind. + #[cfg(feature = "experimental")] + PlainDateTime(PlainDateTime), + + /// The `Temporal.PlainDate` object kind. + #[cfg(feature = "experimental")] + PlainDate(PlainDate), + + /// The `Temporal.PlainTime` object kind. + #[cfg(feature = "experimental")] + PlainTime(PlainTime), + + /// The `Temporal.PlainYearMonth` object kind. + #[cfg(feature = "experimental")] + PlainYearMonth(PlainYearMonth), + + /// The `Temporal.PlainMonthDay` object kind. + #[cfg(feature = "experimental")] + PlainMonthDay(PlainMonthDay), + + /// The `Temporal.TimeZone` object kind. + #[cfg(feature = "experimental")] + TimeZone(TimeZone), + + /// The `Temporal.Duration` object kind. + #[cfg(feature = "experimental")] + Duration(Duration), + + /// The `Temporal.ZonedDateTime` object kind. + #[cfg(feature = "experimental")] + ZonedDateTime(ZonedDateTime), + + /// The `Temporal.Calendar` object kind. + #[cfg(feature = "experimental")] + Calendar(Calendar), } unsafe impl Trace for ObjectKind { @@ -504,6 +548,17 @@ unsafe impl Trace for ObjectKind { | Self::Global | Self::Number(_) | Self::Symbol(_) => {} + #[cfg(feature = "experimental")] + Self::Instant(_) + | Self::PlainDateTime(_) + | Self::PlainDate(_) + | Self::PlainTime(_) + | Self::PlainYearMonth(_) + | Self::PlainMonthDay(_) + | Self::TimeZone(_) + | Self::Calendar(_) + | Self::Duration(_) + | Self::ZonedDateTime(_) => {} } }} } @@ -958,6 +1013,105 @@ impl ObjectData { internal_methods: &ORDINARY_INTERNAL_METHODS, } } + + /// Create the `Instant` object data + #[cfg(feature = "experimental")] + #[must_use] + pub fn instant(instant: Instant) -> Self { + Self { + kind: ObjectKind::Instant(instant), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + + /// Create the `PlainDateTime` object data + #[cfg(feature = "experimental")] + #[must_use] + pub fn plain_date_time(date_time: PlainDateTime) -> Self { + Self { + kind: ObjectKind::PlainDateTime(date_time), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + /// Create the `PlainDate` object data + #[cfg(feature = "experimental")] + #[must_use] + pub fn plain_date(date: PlainDate) -> Self { + Self { + kind: ObjectKind::PlainDate(date), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + + /// Create the `PlainTime` object data + #[cfg(feature = "experimental")] + #[must_use] + pub fn plain_time(time: PlainTime) -> Self { + Self { + kind: ObjectKind::PlainTime(time), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + + /// Create the `PlainYearMonth` object data + #[cfg(feature = "experimental")] + #[must_use] + pub fn plain_year_month(year_month: PlainYearMonth) -> Self { + Self { + kind: ObjectKind::PlainYearMonth(year_month), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + + /// Create the `PlainMonthDay` object data + #[cfg(feature = "experimental")] + #[must_use] + pub fn plain_month_day(month_day: PlainMonthDay) -> Self { + Self { + kind: ObjectKind::PlainMonthDay(month_day), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + + /// Create the `TimeZone` object data + #[cfg(feature = "experimental")] + #[must_use] + pub fn time_zone(time_zone: TimeZone) -> Self { + Self { + kind: ObjectKind::TimeZone(time_zone), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + + /// Create the `Duration` object data + #[cfg(feature = "experimental")] + #[must_use] + pub fn duration(duration: Duration) -> Self { + Self { + kind: ObjectKind::Duration(duration), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + + /// Create the `ZonedDateTime` object data. + #[cfg(feature = "experimental")] + #[must_use] + pub fn zoned_date_time(zoned_date_time: ZonedDateTime) -> Self { + Self { + kind: ObjectKind::ZonedDateTime(zoned_date_time), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + + /// Create the `Calendar` object data. + #[cfg(feature = "experimental")] + #[must_use] + pub fn calendar(calendar: Calendar) -> Self { + Self { + kind: ObjectKind::Calendar(calendar), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } } impl Debug for ObjectKind { @@ -1017,6 +1171,26 @@ impl Debug for ObjectKind { Self::SegmentIterator(_) => "SegmentIterator", #[cfg(feature = "intl")] Self::PluralRules(_) => "PluralRules", + #[cfg(feature = "experimental")] + Self::Instant(_) => "Instant", + #[cfg(feature = "experimental")] + Self::PlainDateTime(_) => "PlainDateTime", + #[cfg(feature = "experimental")] + Self::PlainDate(_) => "PlainDate", + #[cfg(feature = "experimental")] + Self::PlainTime(_) => "PlainTime", + #[cfg(feature = "experimental")] + Self::PlainYearMonth(_) => "PlainYearMonth", + #[cfg(feature = "experimental")] + Self::PlainMonthDay(_) => "PlainMonthDay", + #[cfg(feature = "experimental")] + Self::TimeZone(_) => "TimeZone", + #[cfg(feature = "experimental")] + Self::Duration(_) => "Duration", + #[cfg(feature = "experimental")] + Self::ZonedDateTime(_) => "ZonedDateTime", + #[cfg(feature = "experimental")] + Self::Calendar(_) => "Calendar", }) } } @@ -1955,6 +2129,224 @@ impl Object { } } + /// Gets the `TimeZone` data if the object is a `Temporal.TimeZone`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn as_time_zone(&self) -> Option<&TimeZone> { + match self.kind { + ObjectKind::TimeZone(ref tz) => Some(tz), + _ => None, + } + } + + /// Checks if the object is a `TimeZone` object. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn is_time_zone(&self) -> bool { + matches!(self.kind, ObjectKind::TimeZone(_)) + } + + /// Gets a mutable reference to `Instant` data if the object is a `Temporal.Instant`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub fn as_instant_mut(&mut self) -> Option<&mut Instant> { + match &mut self.kind { + ObjectKind::Instant(instant) => Some(instant), + _ => None, + } + } + + /// Gets the `Instant` data if the object is a `Temporal.Instant`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn as_instant(&self) -> Option<&Instant> { + match &self.kind { + ObjectKind::Instant(instant) => Some(instant), + _ => None, + } + } + + /// Checks if the object is a `Duration` object. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn is_duration(&self) -> bool { + matches!(self.kind, ObjectKind::Duration(_)) + } + + /// Gets a mutable reference to `Duration` data if the object is a `Temporal.Duration`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub fn as_duration_mut(&mut self) -> Option<&mut Duration> { + match &mut self.kind { + ObjectKind::Duration(dur) => Some(dur), + _ => None, + } + } + + /// Gets the `Duration` data if the object is a `Temporal.Duration`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn as_duration(&self) -> Option<&Duration> { + match &self.kind { + ObjectKind::Duration(dur) => Some(dur), + _ => None, + } + } + + /// Checks if object is a `PlainDateTime` object. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn is_plain_date_time(&self) -> bool { + matches!(self.kind, ObjectKind::PlainDateTime(_)) + } + + /// Gets a reference to `PlainDateTime` data if the object is a `Temporal.PlainDateTime`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn as_plain_date_time(&self) -> Option<&PlainDateTime> { + match &self.kind { + ObjectKind::PlainDateTime(date) => Some(date), + _ => None, + } + } + + /// Checks if object is a `PlainDate` object. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn is_plain_date(&self) -> bool { + matches!(self.kind, ObjectKind::PlainDate(_)) + } + + /// Gets a mutable reference to `PlainDate` data if the object is a `Temporal.PlainDate`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub fn as_plain_date_mut(&mut self) -> Option<&mut PlainDate> { + match &mut self.kind { + ObjectKind::PlainDate(date) => Some(date), + _ => None, + } + } + + /// Gets the `PlainDate` data if the object is a `Temporal.PlainDate`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn as_plain_date(&self) -> Option<&PlainDate> { + match &self.kind { + ObjectKind::PlainDate(date) => Some(date), + _ => None, + } + } + + /// Checks if object is a `PlainYearMonth` object. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn is_plain_year_month(&self) -> bool { + matches!(self.kind, ObjectKind::PlainYearMonth(_)) + } + + /// Gets a mutable reference to `PlainYearMonth` data if the object is a `Temporal.PlainYearMonth`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub fn as_plain_year_month_mut(&mut self) -> Option<&mut PlainYearMonth> { + match &mut self.kind { + ObjectKind::PlainYearMonth(year_month) => Some(year_month), + _ => None, + } + } + + /// Gets the `PlainYearMonth` data if the object is a `Temporal.PlainYearMonth`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn as_plain_year_month(&self) -> Option<&PlainYearMonth> { + match &self.kind { + ObjectKind::PlainYearMonth(ym) => Some(ym), + _ => None, + } + } + + /// Checks if object is a `PlainMonthDay` object. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn is_plain_month_day(&self) -> bool { + matches!(self.kind, ObjectKind::PlainMonthDay(_)) + } + + /// Gets the `PlainMonthDay` data if the object is a `Temporal.PlainMonthDay`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn as_plain_month_day(&self) -> Option<&PlainMonthDay> { + match &self.kind { + ObjectKind::PlainMonthDay(md) => Some(md), + _ => None, + } + } + + /// Gets a mutable reference to `PlainMonthDay` data if the object is a `Temporal.PlainMonthDay`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub fn as_plain_month_day_mut(&mut self) -> Option<&mut PlainMonthDay> { + match &mut self.kind { + ObjectKind::PlainMonthDay(month_day) => Some(month_day), + _ => None, + } + } + + /// Gets the `PlainDate` data if the object is a `Temporal.PlainDate`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn as_zoned_date_time(&self) -> Option<&ZonedDateTime> { + match &self.kind { + ObjectKind::ZonedDateTime(zdt) => Some(zdt), + _ => None, + } + } + + /// Checks if the object is a `ZonedDateTime` object. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn is_zoned_date_time(&self) -> bool { + matches!(self.kind, ObjectKind::ZonedDateTime(_)) + } + + /// Checks if the object is a `Calendar` object. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn is_calendar(&self) -> bool { + matches!(self.kind, ObjectKind::Calendar(_)) + } + + /// Gets the `Calendar` data if the object is a `Temporal.Calendar`. + #[inline] + #[must_use] + #[cfg(feature = "experimental")] + pub const fn as_calendar(&self) -> Option<&Calendar> { + match &self.kind { + ObjectKind::Calendar(calendar) => Some(calendar), + _ => None, + } + } + /// Return `true` if it is a native object and the native type is `T`. #[must_use] pub fn is(&self) -> bool diff --git a/boa_engine/src/string/common.rs b/boa_engine/src/string/common.rs index 88b21665081..8a81ed910bc 100644 --- a/boa_engine/src/string/common.rs +++ b/boa_engine/src/string/common.rs @@ -173,6 +173,18 @@ impl StaticJsStrings { (WEAK_REF, "WeakRef"), (WEAK_MAP, "WeakMap"), (WEAK_SET, "WeakSet"), + (TEMPORAL, "Temporal"), + (NOW, "Temporal.Now"), + (INSTANT, "Temporal.Instant"), + (DURATION, "Temporal.Duration"), + (PLAIN_DATE, "Temporal.PlainDate"), + (PLAIN_DATETIME, "Temporal.PlainDateTime"), + (PLAIN_TIME, "Temporal.PlainTime"), + (PLAIN_YM, "Temporal.PlainYearMonth"), + (PLAIN_MD, "Temporal.PlainMonthDay"), + (CALENDAR, "Temporal.Calendar"), + (TIMEZONE, "Temporal.TimeZone"), + (ZONED_DT, "Temporal.ZonedDateTime"), } } @@ -301,6 +313,18 @@ const RAW_STATICS: &[&[u16]] = &[ utf16!("WeakRef"), utf16!("WeakMap"), utf16!("WeakSet"), + utf16!("Temporal"), + utf16!("Temporal.Now"), + utf16!("Temporal.Instant"), + utf16!("Temporal.Duration"), + utf16!("Temporal.Calendar"), + utf16!("Temporal.PlainDate"), + utf16!("Temporal.PlainDateTime"), + utf16!("Temporal.PlainMonthDay"), + utf16!("Temporal.PlainYearMonth"), + utf16!("Temporal.PlainTime"), + utf16!("Temporal.TimeZone"), + utf16!("Temporal.ZonedDateTime"), // Misc utf16!(","), utf16!(":"), diff --git a/boa_parser/Cargo.toml b/boa_parser/Cargo.toml index 208fa32174c..28301120a28 100644 --- a/boa_parser/Cargo.toml +++ b/boa_parser/Cargo.toml @@ -25,3 +25,4 @@ icu_properties.workspace = true [features] annex-b = [] +experimental = ["boa_ast/experimental"] diff --git a/boa_parser/src/error/mod.rs b/boa_parser/src/error/mod.rs index 7805be2c668..2e7183e7f97 100644 --- a/boa_parser/src/error/mod.rs +++ b/boa_parser/src/error/mod.rs @@ -10,6 +10,7 @@ use std::fmt; /// Result of a parsing operation. pub type ParseResult = Result; +/// Adds context to a parser error. pub(crate) trait ErrorContext { /// Sets the context of the error, if possible. fn set_context(self, context: &'static str) -> Self; diff --git a/boa_parser/src/lib.rs b/boa_parser/src/lib.rs index fa7433173f3..cb021cf7459 100644 --- a/boa_parser/src/lib.rs +++ b/boa_parser/src/lib.rs @@ -80,6 +80,8 @@ pub mod error; pub mod lexer; pub mod parser; mod source; +#[cfg(feature = "experimental")] +pub mod temporal; pub use error::Error; pub use lexer::Lexer; diff --git a/boa_parser/src/parser/expression/assignment/yield.rs b/boa_parser/src/parser/expression/assignment/yield.rs index 56f8dffc7e4..406fe7f48f9 100644 --- a/boa_parser/src/parser/expression/assignment/yield.rs +++ b/boa_parser/src/parser/expression/assignment/yield.rs @@ -10,7 +10,7 @@ use super::AssignmentExpression; use crate::{ lexer::TokenKind, - parser::{AllowAwait, AllowIn, Cursor, OrAbrupt, ParseResult, TokenParser}, + parser::{cursor::Cursor, AllowAwait, AllowIn, OrAbrupt, ParseResult, TokenParser}, }; use boa_ast::{expression::Yield, Expression, Keyword, Punctuator}; use boa_interner::Interner; diff --git a/boa_parser/src/parser/mod.rs b/boa_parser/src/parser/mod.rs index 0f3e60b589d..b75d13b114a 100644 --- a/boa_parser/src/parser/mod.rs +++ b/boa_parser/src/parser/mod.rs @@ -46,6 +46,10 @@ where /// Parses the token stream using the current parser. /// /// This method needs to be provided by the implementor type. + /// + /// # Errors + /// + /// It will fail if the cursor is not placed at the beginning of the expected non-terminal. fn parse(self, cursor: &mut Cursor, interner: &mut Interner) -> ParseResult; } diff --git a/boa_parser/src/temporal/annotations.rs b/boa_parser/src/temporal/annotations.rs new file mode 100644 index 00000000000..f9c0ae13ce0 --- /dev/null +++ b/boa_parser/src/temporal/annotations.rs @@ -0,0 +1,205 @@ +/// Parsing for Temporal's `Annotations`. +use crate::{ + error::{Error, ParseResult}, + lexer::Error as LexError, + temporal::{ + grammar::{ + is_a_key_char, is_a_key_leading_char, is_annotation_close, + is_annotation_key_value_separator, is_annotation_value_component, is_critical_flag, + }, + time_zone, + time_zone::TimeZoneAnnotation, + IsoCursor, + }, +}; + +use boa_ast::{Position, Span}; + +use super::grammar::{is_annotation_open, is_hyphen}; + +/// A `KeyValueAnnotation` Parse Node. +#[derive(Debug, Clone)] +pub(crate) struct KeyValueAnnotation { + /// An `Annotation`'s Key. + pub(crate) key: String, + /// An `Annotation`'s value. + pub(crate) value: String, + /// Whether the annotation was flagged as critical. + pub(crate) critical: bool, +} + +/// Strictly a Parsing Intermediary for the checking the common annotation backing. +pub(crate) struct AnnotationSet { + pub(crate) tz: Option, + pub(crate) calendar: Option, +} + +/// Parse a `TimeZoneAnnotation` `Annotations` set +pub(crate) fn parse_annotation_set( + zoned: bool, + cursor: &mut IsoCursor, +) -> ParseResult { + // Parse the first annotation. + let tz_annotation = time_zone::parse_ambiguous_tz_annotation(cursor)?; + + if tz_annotation.is_none() && zoned { + return Err(Error::unexpected( + "Annotation", + Span::new( + Position::new(1, cursor.pos() + 1), + Position::new(1, cursor.pos() + 2), + ), + "iso8601 ZonedDateTime requires a TimeZoneAnnotation.", + )); + } + + // Parse any `Annotations` + let annotations = cursor.check_or(false, is_annotation_open); + + if annotations { + let annotations = parse_annotations(cursor)?; + return Ok(AnnotationSet { + tz: tz_annotation, + calendar: annotations.calendar, + }); + } + + Ok(AnnotationSet { + tz: tz_annotation, + calendar: None, + }) +} + +/// An internal crate type to house any recognized annotations that are found. +#[derive(Default)] +pub(crate) struct RecognizedAnnotations { + pub(crate) calendar: Option, +} + +/// Parse any number of `KeyValueAnnotation`s +pub(crate) fn parse_annotations(cursor: &mut IsoCursor) -> ParseResult { + let mut annotations = RecognizedAnnotations::default(); + + let mut calendar_crit = false; + while cursor.check_or(false, is_annotation_open) { + let start = Position::new(1, cursor.pos() + 1); + let kv = parse_kv_annotation(cursor)?; + + if &kv.key == "u-ca" { + if annotations.calendar.is_none() { + annotations.calendar = Some(kv.value); + calendar_crit = kv.critical; + continue; + } + + if calendar_crit || kv.critical { + return Err(Error::general( + "Cannot have critical flag with duplicate calendar annotations", + start, + )); + } + } else if kv.critical { + return Err(Error::general("Unrecognized critical annotation.", start)); + } + } + + Ok(annotations) +} + +/// Parse an annotation with an `AnnotationKey`=`AnnotationValue` pair. +fn parse_kv_annotation(cursor: &mut IsoCursor) -> ParseResult { + debug_assert!(cursor.check_or(false, is_annotation_open)); + + let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?; + let (leading_char, critical) = if is_critical_flag(potential_critical) { + (cursor.next().ok_or_else(|| Error::AbruptEnd)?, true) + } else { + (potential_critical, false) + }; + + if !is_a_key_leading_char(leading_char) { + return Err(LexError::syntax( + "Invalid AnnotationKey leading character", + Position::new(1, cursor.pos() + 1), + ) + .into()); + } + + // Parse AnnotationKey. + let annotation_key = parse_annotation_key(cursor)?; + + debug_assert!(cursor.check_or(false, is_annotation_key_value_separator)); + // Advance past the '=' character. + cursor.advance(); + + // Parse AnnotationValue. + let annotation_value = parse_annotation_value(cursor)?; + + // Assert that we are at the annotation close and advance cursor past annotation to close. + debug_assert!(cursor.check_or(false, is_annotation_close)); + cursor.advance(); + + Ok(KeyValueAnnotation { + key: annotation_key, + value: annotation_value, + critical, + }) +} + +/// Parse an `AnnotationKey`. +fn parse_annotation_key(cursor: &mut IsoCursor) -> ParseResult { + let key_start = cursor.pos(); + while let Some(potential_key_char) = cursor.next() { + // End of key. + if is_annotation_key_value_separator(potential_key_char) { + // Return found key + return Ok(cursor.slice(key_start, cursor.pos())); + } + + if !is_a_key_char(potential_key_char) { + return Err(LexError::syntax( + "Invalid AnnotationKey Character", + Position::new(1, cursor.pos() + 1), + ) + .into()); + } + } + + Err(Error::AbruptEnd) +} + +/// Parse an `AnnotationValue`. +fn parse_annotation_value(cursor: &mut IsoCursor) -> ParseResult { + let value_start = cursor.pos(); + while let Some(potential_value_char) = cursor.next() { + if is_annotation_close(potential_value_char) { + // Return the determined AnnotationValue. + return Ok(cursor.slice(value_start, cursor.pos())); + } + + if is_hyphen(potential_value_char) { + if !cursor + .peek_n(1) + .map_or(false, is_annotation_value_component) + { + return Err(LexError::syntax( + "Missing AttributeValueComponent after '-'", + Position::new(1, cursor.pos() + 1), + ) + .into()); + } + cursor.advance(); + continue; + } + + if !is_annotation_value_component(potential_value_char) { + return Err(LexError::syntax( + "Invalid character in AnnotationValue", + Position::new(1, value_start + cursor.pos() + 1), + ) + .into()); + } + } + + Err(Error::AbruptEnd) +} diff --git a/boa_parser/src/temporal/date_time.rs b/boa_parser/src/temporal/date_time.rs new file mode 100644 index 00000000000..b147a89416f --- /dev/null +++ b/boa_parser/src/temporal/date_time.rs @@ -0,0 +1,373 @@ +//! Parsing for Temporal's ISO8601 `Date` and `DateTime`. + +use crate::{ + error::{Error, ParseResult}, + lexer::Error as LexError, + temporal::{ + annotations, + grammar::{is_date_time_separator, is_sign, is_utc_designator}, + time, + time::TimeSpec, + time_zone, IsoCursor, IsoParseRecord, + }, +}; + +use boa_ast::{temporal::TimeZone, Position, Span}; + +use super::grammar::{is_annotation_open, is_hyphen}; + +#[derive(Debug, Default, Clone)] +/// A `DateTime` Parse Node that contains the date, time, and offset info. +pub(crate) struct DateTimeRecord { + /// Date + pub(crate) date: DateRecord, + /// Time + pub(crate) time: Option, + /// Tz Offset + pub(crate) time_zone: Option, +} + +#[derive(Default, Debug, Clone, Copy)] +/// The record of a parsed date. +pub(crate) struct DateRecord { + /// Date Year + pub(crate) year: i32, + /// Date Month + pub(crate) month: i32, + /// Date Day + pub(crate) day: i32, +} + +/// This function handles parsing for [`AnnotatedDateTime`][datetime], +/// [`AnnotatedDateTimeTimeRequred`][time], and +/// [`TemporalInstantString.`][instant] according to the requirements +/// provided via Spec. +/// +/// [datetime]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTime +/// [time]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTimeTimeRequired +/// [instant]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString +pub(crate) fn parse_annotated_date_time( + zoned: bool, + time_required: bool, + utc_required: bool, + cursor: &mut IsoCursor, +) -> ParseResult { + let date_time = parse_date_time(time_required, utc_required, cursor)?; + + // Peek Annotation presence + // Throw error if annotation does not exist and zoned is true, else return. + let annotation_check = cursor.check_or(false, is_annotation_open); + if !annotation_check { + if zoned { + return Err(Error::expected( + ["TimeZoneAnnotation".into()], + "No Annotation", + Span::new( + Position::new(1, cursor.pos() + 1), + Position::new(1, cursor.pos() + 1), + ), + "iso8601 grammar", + )); + } + + return Ok(IsoParseRecord { + date: date_time.date, + time: date_time.time, + tz: date_time.time_zone, + calendar: None, + }); + } + + let mut tz = TimeZone::default(); + + if let Some(tz_info) = date_time.time_zone { + tz = tz_info; + } + + let annotation_set = annotations::parse_annotation_set(zoned, cursor)?; + + if let Some(annotated_tz) = annotation_set.tz { + tz = annotated_tz.tz; + } + + let tz = if tz.name.is_some() || tz.offset.is_some() { + Some(tz) + } else { + None + }; + + Ok(IsoParseRecord { + date: date_time.date, + time: date_time.time, + tz, + calendar: annotation_set.calendar, + }) +} + +/// Parses a `DateTime` record. +fn parse_date_time( + time_required: bool, + utc_required: bool, + cursor: &mut IsoCursor, +) -> ParseResult { + let date = parse_date(cursor)?; + + // If there is no `DateTimeSeparator`, return date early. + if !cursor.check_or(false, is_date_time_separator) { + if time_required { + return Err(Error::general( + "Missing a required TimeSpec.", + Position::new(1, cursor.pos() + 1), + )); + } + + return Ok(DateTimeRecord { + date, + time: None, + time_zone: None, + }); + } + + cursor.advance(); + + let time = time::parse_time_spec(cursor)?; + + let time_zone = if cursor + .check(|ch| is_sign(ch) || is_utc_designator(ch)) + .unwrap_or(false) + { + Some(time_zone::parse_date_time_utc(cursor)?) + } else { + if utc_required { + return Err(Error::general( + "DateTimeUTCOffset is required.", + Position::new(1, cursor.pos() + 1), + )); + } + None + }; + + Ok(DateTimeRecord { + date, + time: Some(time), + time_zone, + }) +} + +/// Parses `Date` record. +fn parse_date(cursor: &mut IsoCursor) -> ParseResult { + let year = parse_date_year(cursor)?; + let divided = cursor.check(is_hyphen).ok_or_else(|| Error::AbruptEnd)?; + + if divided { + cursor.advance(); + } + + let month = parse_date_month(cursor)?; + + if cursor.check_or(false, is_hyphen) { + if !divided { + return Err(LexError::syntax( + "Invalid date separator", + Position::new(1, cursor.pos() + 1), + ) + .into()); + } + cursor.advance(); + } + + let day = parse_date_day(cursor)?; + + Ok(DateRecord { year, month, day }) +} + +/// Determines if the string can be parsed as a `DateSpecYearMonth`. +pub(crate) fn peek_year_month(cursor: &IsoCursor) -> ParseResult { + let mut ym_peek = if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) { + 7 + } else { + 4 + }; + + if cursor + .peek_n(ym_peek) + .map(is_hyphen) + .ok_or_else(|| Error::AbruptEnd)? + { + ym_peek += 1; + } + + ym_peek += 2; + + if cursor.peek_n(ym_peek).map_or(true, is_annotation_open) { + Ok(true) + } else { + Ok(false) + } +} + +/// Parses a `DateSpecYearMonth` +pub(crate) fn parse_year_month(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> { + let year = parse_date_year(cursor)?; + + if cursor.check_or(false, is_hyphen) { + cursor.advance(); + } + + let month = parse_date_month(cursor)?; + + Ok((year, month)) +} + +/// Determines if the string can be parsed as a `DateSpecYearMonth`. +pub(crate) fn peek_month_day(cursor: &IsoCursor) -> ParseResult { + let mut md_peek = if cursor + .peek_n(1) + .map(is_hyphen) + .ok_or_else(|| Error::AbruptEnd)? + { + 4 + } else { + 2 + }; + + if cursor + .peek_n(md_peek) + .map(is_hyphen) + .ok_or_else(|| Error::AbruptEnd)? + { + md_peek += 1; + } + + md_peek += 2; + + if cursor.peek_n(md_peek).map_or(true, is_annotation_open) { + Ok(true) + } else { + Ok(false) + } +} + +/// Parses a `DateSpecMonthDay` +pub(crate) fn parse_month_day(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> { + let dash_one = cursor.check(is_hyphen).ok_or_else(|| Error::AbruptEnd)?; + let dash_two = cursor + .peek_n(1) + .map(is_hyphen) + .ok_or_else(|| Error::AbruptEnd)?; + + if dash_two && dash_one { + cursor.advance_n(2); + } else if dash_two && !dash_one { + return Err(LexError::syntax( + "MonthDay requires two dashes", + Position::new(1, cursor.pos()), + ) + .into()); + } + + let month = parse_date_month(cursor)?; + if cursor.check_or(false, is_hyphen) { + cursor.advance(); + } + + let day = parse_date_day(cursor)?; + + Ok((month, day)) +} + +// ==== Unit Parsers ==== + +fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult { + if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) { + let year_start = cursor.pos(); + let sign = if cursor.check_or(false, |ch| ch == '+') { + 1 + } else { + -1 + }; + + cursor.advance(); + + for _ in 0..6 { + let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?; + if !year_digit.is_ascii_digit() { + return Err(Error::lex(LexError::syntax( + "DateYear must contain digit", + Position::new(1, cursor.pos() + 1), + ))); + } + cursor.advance(); + } + + let year_string = cursor.slice(year_start + 1, cursor.pos()); + let year_value = year_string + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, year_start + 1)))?; + + // 13.30.1 Static Semantics: Early Errors + // + // It is a Syntax Error if DateYear is "-000000" or "−000000" (U+2212 MINUS SIGN followed by 000000). + if sign == -1 && year_value == 0 { + return Err(Error::lex(LexError::syntax( + "Cannot have negative 0 years.", + Position::new(1, year_start + 1), + ))); + } + + return Ok(sign * year_value); + } + + let year_start = cursor.pos(); + + for _ in 0..4 { + let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?; + if !year_digit.is_ascii_digit() { + return Err(LexError::syntax( + "DateYear must contain digit", + Position::new(1, cursor.pos() + 1), + ) + .into()); + } + cursor.advance(); + } + + let year_string = cursor.slice(year_start, cursor.pos()); + let year_value = year_string + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() + 1)))?; + + Ok(year_value) +} + +fn parse_date_month(cursor: &mut IsoCursor) -> ParseResult { + let month_value = cursor + .slice(cursor.pos(), cursor.pos() + 2) + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() + 1)))?; + if !(1..=12).contains(&month_value) { + return Err(LexError::syntax( + "DateMonth must be in a range of 1-12", + Position::new(1, cursor.pos() + 1), + ) + .into()); + } + cursor.advance_n(2); + Ok(month_value) +} + +fn parse_date_day(cursor: &mut IsoCursor) -> ParseResult { + let day_value = cursor + .slice(cursor.pos(), cursor.pos() + 2) + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?; + if !(1..=31).contains(&day_value) { + return Err(LexError::syntax( + "DateDay must be in a range of 1-31", + Position::new(1, cursor.pos() + 1), + ) + .into()); + } + cursor.advance_n(2); + Ok(day_value) +} diff --git a/boa_parser/src/temporal/duration.rs b/boa_parser/src/temporal/duration.rs new file mode 100644 index 00000000000..e99dd87bc2f --- /dev/null +++ b/boa_parser/src/temporal/duration.rs @@ -0,0 +1,275 @@ +use boa_ast::Position; + +use crate::{ + error::{Error, ParseResult}, + temporal::{ + grammar::{ + is_day_designator, is_decimal_separator, is_duration_designator, is_hour_designator, + is_minute_designator, is_month_designator, is_second_designator, is_sign, + is_time_designator, is_week_designator, is_year_designator, + }, + time::parse_fraction, + IsoCursor, + }, +}; + +/// A ISO8601 `DurationRecord` Parse Node. +#[derive(Debug, Clone, Copy)] +pub(crate) struct DurationParseRecord { + /// Duration Sign + pub(crate) sign: bool, + /// A `DateDuration` record. + pub(crate) date: DateDuration, + /// A `TimeDuration` record. + pub(crate) time: TimeDuration, +} + +/// A `DateDuration` Parse Node. +#[derive(Default, Debug, Clone, Copy)] +pub(crate) struct DateDuration { + /// Years value. + pub(crate) years: i32, + /// Months value. + pub(crate) months: i32, + /// Weeks value. + pub(crate) weeks: i32, + /// Days value. + pub(crate) days: i32, +} + +/// A `TimeDuration` Parse Node +#[derive(Default, Debug, Clone, Copy)] +pub(crate) struct TimeDuration { + /// Hours value. + pub(crate) hours: i32, + /// Hours fraction value. + pub(crate) fhours: f64, + /// Minutes value with fraction. + pub(crate) minutes: i32, + /// Minutes fraction value. + pub(crate) fminutes: f64, + /// Seconds value with fraction. + pub(crate) seconds: i32, + /// Seconds fraction value, + pub(crate) fseconds: f64, +} + +pub(crate) fn parse_duration(cursor: &mut IsoCursor) -> ParseResult { + let sign = if cursor.check(is_sign).ok_or_else(|| Error::AbruptEnd)? { + let sign = cursor.check_or(false, |ch| ch == '+'); + cursor.advance(); + sign + } else { + true + }; + + if !cursor + .check(is_duration_designator) + .ok_or_else(|| Error::AbruptEnd)? + { + return Err(Error::general( + "DurationString missing DurationDesignator.", + Position::new(1, cursor.pos() + 1), + )); + } + + cursor.advance(); + + let date = if cursor.check_or(false, is_time_designator) { + Some(DateDuration::default()) + } else { + Some(parse_date_duration(cursor)?) + }; + + let time = if cursor.check_or(false, is_time_designator) { + cursor.advance(); + Some(parse_time_duration(cursor)?) + } else { + None + }; + + if cursor.peek().is_some() { + return Err(Error::general( + "Unrecognized value in DurationString.", + Position::new(1, cursor.pos()), + )); + } + + Ok(DurationParseRecord { + sign, + date: date.unwrap_or_default(), + time: time.unwrap_or_default(), + }) +} + +#[derive(PartialEq, PartialOrd, Eq, Ord)] +enum DateUnit { + None = 0, + Year, + Month, + Week, + Day, +} + +pub(crate) fn parse_date_duration(cursor: &mut IsoCursor) -> ParseResult { + let mut date = DateDuration::default(); + + let mut previous_unit = DateUnit::None; + while cursor.check_or(false, |ch| ch.is_ascii_digit()) { + let digit_start = cursor.pos(); + + while cursor.check_or(false, |ch| ch.is_ascii_digit()) { + cursor.advance(); + } + + let value = cursor + .slice(digit_start, cursor.pos()) + .parse::() + .map_err(|err| { + Error::general(err.to_string(), Position::new(digit_start, cursor.pos())) + })?; + + match cursor.peek() { + Some(ch) if is_year_designator(ch) => { + if previous_unit > DateUnit::Year { + return Err(Error::general( + "Not a valid DateDuration order", + Position::new(1, cursor.pos()), + )); + } + date.years = value; + previous_unit = DateUnit::Year; + } + Some(ch) if is_month_designator(ch) => { + if previous_unit > DateUnit::Month { + return Err(Error::general( + "Not a valid DateDuration order", + Position::new(1, cursor.pos()), + )); + } + date.months = value; + previous_unit = DateUnit::Month; + } + Some(ch) if is_week_designator(ch) => { + if previous_unit > DateUnit::Week { + return Err(Error::general( + "Not a valid DateDuration order", + Position::new(1, cursor.pos()), + )); + } + date.weeks = value; + previous_unit = DateUnit::Week; + } + Some(ch) if is_day_designator(ch) => { + if previous_unit > DateUnit::Day { + return Err(Error::general( + "Not a valid DateDuration order", + Position::new(1, cursor.pos()), + )); + } + date.days = value; + previous_unit = DateUnit::Day; + } + Some(_) | None => return Err(Error::AbruptEnd), + } + + cursor.advance(); + } + + Ok(date) +} + +#[derive(PartialEq, PartialOrd, Eq, Ord)] +enum TimeUnit { + None = 0, + Hour, + Minute, + Second, +} + +pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult { + let mut time = TimeDuration::default(); + + if !cursor.check_or(false, |ch| ch.is_ascii()) { + return Err(Error::general( + "No time values provided after TimeDesignator.", + Position::new(1, cursor.pos()), + )); + } + + let mut previous_unit = TimeUnit::None; + let mut fraction_present = false; + while cursor.check_or(false, |ch| ch.is_ascii_digit()) { + let digit_start = cursor.pos(); + + while cursor.check_or(false, |ch| ch.is_ascii_digit()) { + cursor.advance(); + } + + let value = cursor + .slice(digit_start, cursor.pos()) + .parse::() + .map_err(|err| { + Error::general(err.to_string(), Position::new(digit_start, cursor.pos())) + })?; + + let fraction = if cursor.check_or(false, is_decimal_separator) { + fraction_present = true; + parse_fraction(cursor)? + } else { + 0.0 + }; + + match cursor.peek() { + Some(ch) if is_hour_designator(ch) => { + if previous_unit > TimeUnit::Hour { + return Err(Error::general( + "Not a valid DateDuration order", + Position::new(1, cursor.pos()), + )); + } + time.hours = value; + time.fhours = fraction; + previous_unit = TimeUnit::Hour; + } + Some(ch) if is_minute_designator(ch) => { + if previous_unit > TimeUnit::Minute { + return Err(Error::general( + "Not a valid DateDuration order", + Position::new(1, cursor.pos()), + )); + } + time.minutes = value; + time.fminutes = fraction; + previous_unit = TimeUnit::Minute; + } + Some(ch) if is_second_designator(ch) => { + if previous_unit > TimeUnit::Second { + return Err(Error::general( + "Not a valid DateDuration order", + Position::new(1, cursor.pos()), + )); + } + time.seconds = value; + time.fseconds = fraction; + previous_unit = TimeUnit::Second; + } + Some(_) | None => return Err(Error::AbruptEnd), + } + + cursor.advance(); + + if fraction_present { + if cursor.check_or(false, |ch| ch.is_ascii_digit()) { + return Err(Error::general( + "Invalid TimeDuration continuation after FractionPart.", + Position::new(1, cursor.pos()), + )); + } + + break; + } + } + + Ok(time) +} diff --git a/boa_parser/src/temporal/grammar.rs b/boa_parser/src/temporal/grammar.rs new file mode 100644 index 00000000000..ae7a28eb8b3 --- /dev/null +++ b/boa_parser/src/temporal/grammar.rs @@ -0,0 +1,136 @@ +//! ISO8601 specific grammar checks. + +/// Checks if char is a `AKeyLeadingChar`. +#[inline] +pub(crate) const fn is_a_key_leading_char(ch: char) -> bool { + ch.is_ascii_lowercase() || ch == '_' +} + +/// Checks if char is an `AKeyChar`. +#[inline] +pub(crate) const fn is_a_key_char(ch: char) -> bool { + is_a_key_leading_char(ch) || ch.is_ascii_digit() || ch == '-' +} + +/// Checks if char is an `AnnotationValueComponent`. +pub(crate) const fn is_annotation_value_component(ch: char) -> bool { + ch.is_ascii_digit() || ch.is_ascii_alphabetic() +} + +/// Checks if char is a `TZLeadingChar`. +#[inline] +pub(crate) const fn is_tz_leading_char(ch: char) -> bool { + ch.is_ascii_alphabetic() || ch == '_' || ch == '.' +} + +/// Checks if char is a `TZChar`. +#[inline] +pub(crate) const fn is_tz_char(ch: char) -> bool { + is_tz_leading_char(ch) || ch.is_ascii_digit() || ch == '-' || ch == '+' +} + +/// Checks if char is a `TimeZoneIANAName` Separator. +pub(crate) const fn is_tz_name_separator(ch: char) -> bool { + ch == '/' +} + +/// Checks if char is an ascii sign. +pub(crate) const fn is_ascii_sign(ch: char) -> bool { + ch == '+' || ch == '-' +} + +/// Checks if char is an ascii sign or U+2212 +pub(crate) const fn is_sign(ch: char) -> bool { + is_ascii_sign(ch) || ch == '\u{2212}' +} + +/// Checks if char is a `TimeSeparator`. +pub(crate) const fn is_time_separator(ch: char) -> bool { + ch == ':' +} + +/// Checks if char is a `TimeDesignator`. +pub(crate) const fn is_time_designator(ch: char) -> bool { + ch == 'T' || ch == 't' +} + +/// Checks if char is a `DateTimeSeparator`. +pub(crate) const fn is_date_time_separator(ch: char) -> bool { + is_time_designator(ch) || ch == '\u{0020}' +} + +/// Checks if char is a `UtcDesignator`. +pub(crate) const fn is_utc_designator(ch: char) -> bool { + ch == 'Z' || ch == 'z' +} + +/// Checks if char is a `DurationDesignator`. +pub(crate) const fn is_duration_designator(ch: char) -> bool { + ch == 'P' || ch == 'p' +} + +/// Checks if char is a `YearDesignator`. +pub(crate) const fn is_year_designator(ch: char) -> bool { + ch == 'Y' || ch == 'y' +} + +/// Checks if char is a `MonthsDesignator`. +pub(crate) const fn is_month_designator(ch: char) -> bool { + ch == 'M' || ch == 'm' +} + +/// Checks if char is a `WeekDesignator`. +pub(crate) const fn is_week_designator(ch: char) -> bool { + ch == 'W' || ch == 'w' +} + +/// Checks if char is a `DayDesignator`. +pub(crate) const fn is_day_designator(ch: char) -> bool { + ch == 'D' || ch == 'd' +} + +/// checks if char is a `DayDesignator`. +pub(crate) const fn is_hour_designator(ch: char) -> bool { + ch == 'H' || ch == 'h' +} + +/// Checks if char is a `MinuteDesignator`. +pub(crate) const fn is_minute_designator(ch: char) -> bool { + is_month_designator(ch) +} + +/// checks if char is a `SecondDesignator`. +pub(crate) const fn is_second_designator(ch: char) -> bool { + ch == 'S' || ch == 's' +} + +/// Checks if char is a `DecimalSeparator`. +pub(crate) const fn is_decimal_separator(ch: char) -> bool { + ch == '.' || ch == ',' +} + +/// Checks if char is an `AnnotationOpen`. +pub(crate) const fn is_annotation_open(ch: char) -> bool { + ch == '[' +} + +/// Checks if char is an `AnnotationClose`. +pub(crate) const fn is_annotation_close(ch: char) -> bool { + ch == ']' +} + +/// Checks if char is an `CriticalFlag`. +pub(crate) const fn is_critical_flag(ch: char) -> bool { + ch == '!' +} + +/// Checks if char is the `AnnotationKeyValueSeparator`. +pub(crate) const fn is_annotation_key_value_separator(ch: char) -> bool { + ch == '=' +} + +/// Checks if char is a hyphen. Hyphens are used as a Date separator +/// and as a `AttributeValueComponent` separator. +pub(crate) const fn is_hyphen(ch: char) -> bool { + ch == '-' +} diff --git a/boa_parser/src/temporal/mod.rs b/boa_parser/src/temporal/mod.rs new file mode 100644 index 00000000000..c9fc55d448e --- /dev/null +++ b/boa_parser/src/temporal/mod.rs @@ -0,0 +1,348 @@ +//! Implementation of Iso8601 grammar lexing/parsing + +use crate::error::ParseResult; + +mod annotations; +mod date_time; +mod duration; +mod grammar; +mod time; +mod time_zone; + +use boa_ast::temporal::{IsoDate, IsoDateTime, IsoDuration, IsoTime, TimeZone}; + +use date_time::DateRecord; +use time::TimeSpec; + +#[cfg(feature = "experimental")] +#[cfg(test)] +mod tests; + +// TODO: optimize where possible. + +/// An `IsoParseRecord` is an intermediary record returned by ISO parsing functions. +/// +/// `IsoParseRecord` is converted into the ISO AST Nodes. +#[derive(Default, Debug)] +pub(crate) struct IsoParseRecord { + /// Parsed Date Record + pub(crate) date: DateRecord, + /// Parsed Time + pub(crate) time: Option, + /// Parsed `TimeZone` data (UTCOffset | IANA name) + pub(crate) tz: Option, + /// The parsed calendar value. + pub(crate) calendar: Option, +} + +/// Parse a [`TemporalDateTimeString`][proposal]. +/// +/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalDateTimeString +#[derive(Debug, Clone, Copy)] +pub struct TemporalDateTimeString; + +impl TemporalDateTimeString { + /// Parses a targeted string as a `DateTime`. + /// + /// # Errors + /// + /// The parse will error if the provided target is not valid + /// Iso8601 grammar. + pub fn parse(zoned: bool, cursor: &mut IsoCursor) -> ParseResult { + let parse_record = date_time::parse_annotated_date_time(zoned, false, false, cursor)?; + + let date = IsoDate { + year: parse_record.date.year, + month: parse_record.date.month, + day: parse_record.date.day, + calendar: parse_record.calendar, + }; + + let time = parse_record.time.map_or_else(IsoTime::default, |time| { + IsoTime::from_components(time.hour, time.minute, time.second, time.fraction) + }); + + Ok(IsoDateTime { + date, + time, + tz: parse_record.tz, + }) + } +} + +/// Parse a [`TemporalTimeZoneString`][proposal]. +/// +/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalTimeZoneString +#[derive(Debug, Clone, Copy)] +pub struct TemporalTimeZoneString; + +impl TemporalTimeZoneString { + /// Parses a targeted string as a `TimeZone`. + /// + /// # Errors + /// + /// The parse will error if the provided target is not valid + /// Iso8601 grammar. + pub fn parse(cursor: &mut IsoCursor) -> ParseResult { + time_zone::parse_time_zone(cursor) + } +} + +/// Parse a [`TemporalYearMonthString`][proposal] +/// +/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalYearMonthString +#[derive(Debug, Clone, Copy)] +pub struct TemporalYearMonthString; + +impl TemporalYearMonthString { + /// Parses a targeted string as a `YearMonth` + /// + /// # Errors + /// + /// The parse will error if the provided target is not valid + /// Iso8601 grammar. + pub fn parse(cursor: &mut IsoCursor) -> ParseResult { + if date_time::peek_year_month(cursor)? { + let ym = date_time::parse_year_month(cursor)?; + + let calendar = if cursor.check_or(false, |ch| ch == '[') { + let set = annotations::parse_annotation_set(false, cursor)?; + set.calendar + } else { + None + }; + + return Ok(IsoDate { + year: ym.0, + month: ym.1, + day: 0, + calendar, + }); + } + + let parse_record = date_time::parse_annotated_date_time(false, false, false, cursor)?; + + Ok(IsoDate { + year: parse_record.date.year, + month: parse_record.date.month, + day: parse_record.date.day, + calendar: parse_record.calendar, + }) + } +} + +/// Parse a [`TemporalMonthDayString`][proposal] +/// +/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalMonthDayString +#[derive(Debug, Clone, Copy)] +pub struct TemporalMonthDayString; + +impl TemporalMonthDayString { + /// Parses a targeted string as a `MonthDay`. + /// + /// # Errors + /// + /// The parse will error if the provided target is not valid + /// Iso8601 grammar. + pub fn parse(cursor: &mut IsoCursor) -> ParseResult { + if date_time::peek_month_day(cursor)? { + let md = date_time::parse_month_day(cursor)?; + + let calendar = if cursor.check_or(false, |ch| ch == '[') { + let set = annotations::parse_annotation_set(false, cursor)?; + set.calendar + } else { + None + }; + + return Ok(IsoDate { + year: 0, + month: md.0, + day: md.1, + calendar, + }); + } + + let parse_record = date_time::parse_annotated_date_time(false, false, false, cursor)?; + + Ok(IsoDate { + year: parse_record.date.year, + month: parse_record.date.month, + day: parse_record.date.day, + calendar: parse_record.calendar, + }) + } +} + +/// Parser for a [`TemporalInstantString`][proposal]. +/// +/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString +#[derive(Debug, Clone, Copy)] +pub struct TemporalInstantString; + +impl TemporalInstantString { + /// Parses a targeted string as an `Instant`. + /// + /// # Errors + /// + /// The parse will error if the provided target is not valid + /// Iso8601 grammar. + pub fn parse(cursor: &mut IsoCursor) -> ParseResult { + let parse_record = date_time::parse_annotated_date_time(false, true, true, cursor)?; + + let date = IsoDate { + year: parse_record.date.year, + month: parse_record.date.month, + day: parse_record.date.day, + calendar: parse_record.calendar, + }; + + let time = parse_record.time.map_or_else(IsoTime::default, |time| { + IsoTime::from_components(time.hour, time.minute, time.second, time.fraction) + }); + + Ok(IsoDateTime { + date, + time, + tz: parse_record.tz, + }) + } +} + +// TODO: implement TemporalTimeString. + +/// Parser for a [`TemporalDurationString`][proposal]. +/// +/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalDurationString +#[derive(Debug, Clone, Copy)] +pub struct TemporalDurationString; + +impl TemporalDurationString { + /// Parses a targeted string as a `Duration`. + /// + /// # Errors + /// + /// The parse will error if the provided target is not valid + /// Iso8601 grammar. + pub fn parse(cursor: &mut IsoCursor) -> ParseResult { + let parse_record = duration::parse_duration(cursor)?; + + let minutes = if parse_record.time.fhours > 0.0 { + parse_record.time.fhours * 60.0 + } else { + f64::from(parse_record.time.minutes) + }; + + let seconds = if parse_record.time.fminutes > 0.0 { + parse_record.time.fminutes * 60.0 + } else if parse_record.time.seconds > 0 { + f64::from(parse_record.time.seconds) + } else { + minutes.rem_euclid(1.0) * 60.0 + }; + + let milliseconds = if parse_record.time.fseconds > 0.0 { + parse_record.time.fseconds * 1000.0 + } else { + seconds.rem_euclid(1.0) * 1000.0 + }; + + let micro = milliseconds.rem_euclid(1.0) * 1000.0; + let nano = micro.rem_euclid(1.0) * 1000.0; + + let sign = if parse_record.sign { 1 } else { -1 }; + + Ok(IsoDuration { + years: parse_record.date.years * sign, + months: parse_record.date.months * sign, + weeks: parse_record.date.weeks * sign, + days: parse_record.date.days * sign, + hours: parse_record.time.hours * sign, + minutes: minutes.floor() * f64::from(sign), + seconds: seconds.floor() * f64::from(sign), + milliseconds: milliseconds.floor() * f64::from(sign), + microseconds: micro.floor() * f64::from(sign), + nanoseconds: nano.floor() * f64::from(sign), + }) + } +} + +// ==== Mini cursor implementation for Iso8601 targets ==== + +/// `IsoCursor` is a small cursor implementation for parsing Iso8601 grammar. +#[derive(Debug)] +pub struct IsoCursor { + pos: u32, + source: Vec, +} + +impl IsoCursor { + /// Create a new cursor from a source `String` value. + #[must_use] + pub fn new(source: &str) -> Self { + Self { + pos: 0, + source: source.chars().collect(), + } + } + + /// Returns a string value from a slice of the cursor. + fn slice(&self, start: u32, end: u32) -> String { + self.source[start as usize..end as usize].iter().collect() + } + + /// Get current position + const fn pos(&self) -> u32 { + self.pos + } + + /// Peek the value at the current position. + fn peek(&self) -> Option { + if (self.pos as usize) < self.source.len() { + Some(self.source[self.pos as usize]) + } else { + None + } + } + + /// Peek the value at n len from current. + fn peek_n(&self, n: u32) -> Option { + let target = (self.pos + n) as usize; + if target < self.source.len() { + Some(self.source[target]) + } else { + None + } + } + + /// Returns boolean if current position passes check. + fn check(&self, f: F) -> Option + where + F: FnOnce(char) -> bool, + { + self.peek().map(f) + } + + /// Returns boolean if current position passes check or default if None. + fn check_or(&self, default: bool, f: F) -> bool + where + F: FnOnce(char) -> bool, + { + self.peek().map_or(default, f) + } + /// Advances the cursor's position and returns the new character. + fn next(&mut self) -> Option { + self.advance(); + self.peek() + } + + /// Advances the cursor's position by 1. + fn advance(&mut self) { + self.pos += 1; + } + + /// Advances the cursor's position by `n`. + fn advance_n(&mut self, n: u32) { + self.pos += n; + } +} diff --git a/boa_parser/src/temporal/tests.rs b/boa_parser/src/temporal/tests.rs new file mode 100644 index 00000000000..20d91e1edae --- /dev/null +++ b/boa_parser/src/temporal/tests.rs @@ -0,0 +1,190 @@ +use super::{ + IsoCursor, TemporalDateTimeString, TemporalDurationString, TemporalInstantString, + TemporalMonthDayString, TemporalYearMonthString, +}; + +#[test] +fn temporal_parser_basic() { + let basic = "20201108"; + let basic_separated = "2020-11-08"; + + let basic_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic)).unwrap(); + + let sep_result = + TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic_separated)).unwrap(); + + assert_eq!(basic_result.date.year, 2020); + assert_eq!(basic_result.date.month, 11); + assert_eq!(basic_result.date.day, 8); + assert_eq!(basic_result.date.year, sep_result.date.year); + assert_eq!(basic_result.date.month, sep_result.date.month); + assert_eq!(basic_result.date.day, sep_result.date.day); +} + +#[test] +#[allow(clippy::cast_possible_truncation)] +fn temporal_date_time_max() { + // Fractions not accurate, but for testing purposes. + let date_time = + "+002020-11-08T12:28:32.329402834[!America/Argentina/ComodRivadavia][!u-ca=iso8601]"; + + let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(date_time)).unwrap(); + + let time_results = &result.time; + + assert_eq!(time_results.hour, 12); + assert_eq!(time_results.minute, 28); + assert_eq!(time_results.second, 32); + assert_eq!(time_results.millisecond, 329); + assert_eq!(time_results.microsecond, 402); + assert_eq!(time_results.nanosecond, 834); + + let tz = &result.tz.unwrap(); + + // OffsetSubMinute is Empty when TimeZoneIdentifier is present. + assert!(&tz.offset.is_none()); + + let tz_name = &tz.name.clone().unwrap(); + + assert_eq!(tz_name, "America/Argentina/ComodRivadavia"); + + assert_eq!(&result.date.calendar, &Some("iso8601".to_string())); +} + +#[test] +fn temporal_year_parsing() { + let long = "+002020-11-08"; + let bad_year = "-000000-11-08"; + + let result_good = TemporalDateTimeString::parse(false, &mut IsoCursor::new(long)).unwrap(); + assert_eq!(result_good.date.year, 2020); + + let err_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(bad_year)); + assert!(err_result.is_err()); +} + +#[test] +fn temporal_annotated_date_time() { + let basic = "2020-11-08[America/Argentina/ComodRivadavia][u-ca=iso8601][foo=bar]"; + let omitted = "+0020201108[u-ca=iso8601][f-1a2b=a0sa-2l4s]"; + + let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic)).unwrap(); + + let tz = &result.tz.unwrap().name.unwrap(); + + assert_eq!(tz, "America/Argentina/ComodRivadavia"); + + assert_eq!(&result.date.calendar, &Some("iso8601".to_string())); + + let omit_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(omitted)).unwrap(); + + assert!(&omit_result.tz.is_none()); + + assert_eq!(&omit_result.date.calendar, &Some("iso8601".to_string())); +} + +#[test] +fn temporal_year_month() { + let possible_year_months = &[ + "+002020-11", + "2020-11[u-ca=iso8601]", + "+00202011", + "202011[u-ca=iso8601]", + ]; + + for ym in possible_year_months { + let result = TemporalYearMonthString::parse(&mut IsoCursor::new(ym)).unwrap(); + + assert_eq!(result.year, 2020); + assert_eq!(result.month, 11); + + if let Some(calendar) = result.calendar { + assert_eq!(calendar, "iso8601"); + } + } +} + +#[test] +fn temporal_month_day() { + let possible_month_day = ["11-07", "1107[+04:00]", "--11-07", "--1107[+04:00]"]; + + for md in possible_month_day { + let result = TemporalMonthDayString::parse(&mut IsoCursor::new(md)).unwrap(); + + assert_eq!(result.month, 11); + assert_eq!(result.day, 7); + } +} + +#[test] +fn temporal_invalid_annotations() { + let invalid_annotations = [ + "2020-11-11[!u-ca=iso8601][u-ca=iso8601]", + "2020-11-11[u-ca=iso8601][!u-ca=iso8601]", + "2020-11-11[u-ca=iso8601][!rip=this-invalid-annotation]", + ]; + + for invalid in invalid_annotations { + let err_result = TemporalMonthDayString::parse(&mut IsoCursor::new(invalid)); + assert!(err_result.is_err()); + } +} + +#[test] +fn temporal_valid_instant_strings() { + let instants = [ + "1970-01-01T00:00+00:00[!Africa/Abidjan]", + "1970-01-01T00:00+00:00[UTC]", + "1970-01-01T00:00Z[!Europe/Vienna]", + ]; + + for test in instants { + let result = TemporalInstantString::parse(&mut IsoCursor::new(test)); + assert!(result.is_ok()); + } +} + +#[test] +#[allow(clippy::cast_possible_truncation)] +fn temporal_duration_parsing() { + let durations = [ + "p1y1m1dt1h1m1s", + "P1Y1M1W1DT1H1M1.1S", + "-P1Y1M1W1DT1H1M1.123456789S", + "-P1Y3wT0,5H", + ]; + + for dur in durations { + let ok_result = TemporalDurationString::parse(&mut IsoCursor::new(dur)); + assert!(ok_result.is_ok()); + } + + let sub = durations[2]; + let sub_second = TemporalDurationString::parse(&mut IsoCursor::new(sub)).unwrap(); + + assert_eq!(sub_second.milliseconds, -123.0); + assert_eq!(sub_second.microseconds, -456.0); + assert_eq!(sub_second.nanoseconds, -789.0); + + let dur = durations[3]; + let test_result = TemporalDurationString::parse(&mut IsoCursor::new(dur)).unwrap(); + + assert_eq!(test_result.years, -1); + assert_eq!(test_result.weeks, -3); + assert_eq!(test_result.minutes, -30.0); +} + +#[test] +fn temporal_invalid_durations() { + let invalids = [ + "P1Y1M1W0,5D", + "P1Y1M1W1DT1H1M1.123456789123S", + "+PT", + "P1Y1M1W1DT1H0.5M0.5S", + ]; + + for test in invalids { + let err = TemporalDurationString::parse(&mut IsoCursor::new(test)); + assert!(err.is_err()); + } +} diff --git a/boa_parser/src/temporal/time.rs b/boa_parser/src/temporal/time.rs new file mode 100644 index 00000000000..f8b8075f646 --- /dev/null +++ b/boa_parser/src/temporal/time.rs @@ -0,0 +1,146 @@ +//! Parsing of ISO8601 Time Values + +use super::{ + grammar::{is_decimal_separator, is_time_separator}, + IsoCursor, +}; +use crate::{ + error::{Error, ParseResult}, + lexer::Error as LexError, +}; + +/// Parsed Time info +#[derive(Debug, Default, Clone, Copy)] +pub(crate) struct TimeSpec { + /// An hour + pub(crate) hour: u8, + /// A minute value + pub(crate) minute: u8, + /// A second value. + pub(crate) second: u8, + /// A floating point number representing the sub-second values + pub(crate) fraction: f64, +} + +use boa_ast::Position; + +/// Parse `TimeSpec` +pub(crate) fn parse_time_spec(cursor: &mut IsoCursor) -> ParseResult { + let hour = parse_hour(cursor)?; + let mut separator = false; + + if cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit()) { + if cursor.check_or(false, is_time_separator) { + separator = true; + cursor.advance(); + } + } else { + return Ok(TimeSpec { + hour, + minute: 0, + second: 0, + fraction: 0.0, + }); + } + + let minute = parse_minute_second(cursor, false)?; + + if cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit()) { + let is_time_separator = cursor.check_or(false, is_time_separator); + if separator && is_time_separator { + cursor.advance(); + } else if is_time_separator { + return Err( + LexError::syntax("Invalid TimeSeparator", Position::new(1, cursor.pos())).into(), + ); + } + } else { + return Ok(TimeSpec { + hour, + minute, + second: 0, + fraction: 0.0, + }); + } + + let second = parse_minute_second(cursor, true)?; + + let fraction = if cursor.check_or(false, is_decimal_separator) { + parse_fraction(cursor)? + } else { + 0.0 + }; + + Ok(TimeSpec { + hour, + minute, + second, + fraction, + }) +} + +pub(crate) fn parse_hour(cursor: &mut IsoCursor) -> ParseResult { + let hour_value = cursor + .slice(cursor.pos(), cursor.pos() + 2) + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?; + if !(0..=23).contains(&hour_value) { + return Err(LexError::syntax( + "Hour must be in a range of 0-23", + Position::new(1, cursor.pos() + 1), + ) + .into()); + } + cursor.advance_n(2); + Ok(hour_value) +} + +// NOTE: `TimeSecond` is a 60 inclusive `MinuteSecond`. +/// Parse `MinuteSecond` +pub(crate) fn parse_minute_second(cursor: &mut IsoCursor, inclusive: bool) -> ParseResult { + let min_sec_value = cursor + .slice(cursor.pos(), cursor.pos() + 2) + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?; + + let valid_range = if inclusive { 0..=60 } else { 0..=59 }; + if !valid_range.contains(&min_sec_value) { + return Err(LexError::syntax( + "MinuteSecond must be in a range of 0-59", + Position::new(1, cursor.pos() + 1), + ) + .into()); + } + + cursor.advance_n(2); + Ok(min_sec_value) +} + +/// Parse a `Fraction` value +/// +/// This is primarily used in ISO8601 to add percision past +/// a second. +pub(crate) fn parse_fraction(cursor: &mut IsoCursor) -> ParseResult { + // Decimal is skipped by next call. + let mut fraction_components = Vec::from(['.']); + while let Some(ch) = cursor.next() { + if !ch.is_ascii_digit() { + if fraction_components.len() > 10 { + return Err(Error::general( + "Fraction exceeds 9 DecimalDigits", + Position::new(1, cursor.pos() - 1), + )); + } + + let fraction_value = fraction_components + .iter() + .collect::() + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() - 1)))?; + return Ok(fraction_value); + } + fraction_components.push(ch); + } + + Err(Error::AbruptEnd) +} diff --git a/boa_parser/src/temporal/time_zone.rs b/boa_parser/src/temporal/time_zone.rs new file mode 100644 index 00000000000..1905eb1f10f --- /dev/null +++ b/boa_parser/src/temporal/time_zone.rs @@ -0,0 +1,263 @@ +//! ISO8601 parsing for Time Zone and Offset data. + +use super::{ + grammar::{ + is_a_key_char, is_a_key_leading_char, is_annotation_close, + is_annotation_key_value_separator, is_annotation_open, is_critical_flag, + is_decimal_separator, is_sign, is_time_separator, is_tz_char, is_tz_leading_char, + is_tz_name_separator, is_utc_designator, + }, + time::{parse_fraction, parse_hour, parse_minute_second}, + IsoCursor, +}; +use crate::{ + error::{Error, ParseResult}, + lexer::Error as LexError, +}; + +use boa_ast::{ + temporal::{TimeZone, UTCOffset}, + Position, +}; + +/// A `TimeZoneAnnotation`. +#[derive(Debug, Clone)] +#[allow(unused)] +pub(crate) struct TimeZoneAnnotation { + /// Critical Flag for the annotation. + pub(crate) critical: bool, + /// TimeZone Data + pub(crate) tz: TimeZone, +} + +// ==== Time Zone Annotation Parsing ==== + +pub(crate) fn parse_ambiguous_tz_annotation( + cursor: &mut IsoCursor, +) -> ParseResult> { + // Peek position + 1 to check for critical flag. + let mut current_peek = 1; + let critical = cursor + .peek_n(current_peek) + .map(is_critical_flag) + .ok_or_else(|| Error::AbruptEnd)?; + + // Advance cursor if critical flag present. + if critical { + current_peek += 1; + } + + let leading_char = cursor + .peek_n(current_peek) + .ok_or_else(|| Error::AbruptEnd)?; + + if is_tz_leading_char(leading_char) || is_sign(leading_char) { + // Ambigious start values when lowercase alpha that is shared between `TzLeadingChar` and `KeyLeadingChar`. + if is_a_key_leading_char(leading_char) { + let mut peek_pos = current_peek + 1; + while let Some(ch) = cursor.peek_n(peek_pos) { + if is_tz_name_separator(ch) || (is_tz_char(ch) && !is_a_key_char(ch)) { + let tz = parse_tz_annotation(cursor)?; + return Ok(Some(tz)); + } else if is_annotation_key_value_separator(ch) + || (is_a_key_char(ch) && !is_tz_char(ch)) + { + return Ok(None); + } else if is_annotation_close(ch) { + return Err(LexError::syntax( + "Invalid Annotation", + Position::new(1, peek_pos + 1), + ) + .into()); + } + + peek_pos += 1; + } + return Err(Error::AbruptEnd); + } + let tz = parse_tz_annotation(cursor)?; + return Ok(Some(tz)); + } + + if is_a_key_leading_char(leading_char) { + return Ok(None); + }; + + Err(Error::lex(LexError::syntax( + "Unexpected character in ambiguous annotation.", + Position::new(1, cursor.pos() + 1), + ))) +} + +fn parse_tz_annotation(cursor: &mut IsoCursor) -> ParseResult { + debug_assert!(is_annotation_open(cursor.peek().expect("annotation start"))); + + let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?; + let critical = is_critical_flag(potential_critical); + + if critical { + cursor.advance(); + } + + let tz = parse_time_zone(cursor)?; + + if !cursor.check_or(false, is_annotation_close) { + return Err(LexError::syntax( + "Invalid TimeZoneAnnotation.", + Position::new(1, cursor.pos() + 1), + ) + .into()); + } + + cursor.advance(); + + Ok(TimeZoneAnnotation { critical, tz }) +} + +/// Parses the [`TimeZoneIdentifier`][tz] node. +/// +/// [tz]: https://tc39.es/proposal-temporal/#prod-TimeZoneIdentifier +pub(crate) fn parse_time_zone(cursor: &mut IsoCursor) -> ParseResult { + let is_iana = cursor + .check(is_tz_leading_char) + .ok_or_else(|| Error::AbruptEnd)?; + let is_offset = cursor.check_or(false, is_sign); + + if is_iana { + return parse_tz_iana_name(cursor); + } else if is_offset { + let offset = parse_utc_offset_minute_precision(cursor)?; + return Ok(TimeZone { + name: None, + offset: Some(offset), + }); + } + + Err(LexError::syntax( + "Invalid leading character for a TimeZoneIdentifier", + Position::new(1, cursor.pos() + 1), + ) + .into()) +} + +/// Parse a `TimeZoneIANAName` Parse Node +fn parse_tz_iana_name(cursor: &mut IsoCursor) -> ParseResult { + let tz_name_start = cursor.pos(); + while let Some(potential_value_char) = cursor.next() { + if is_tz_name_separator(potential_value_char) { + if !cursor.peek_n(1).map_or(false, is_tz_char) { + return Err(LexError::syntax( + "Missing TimeZoneIANANameComponent after '/'", + Position::new(1, cursor.pos() + 2), + ) + .into()); + } + continue; + } + + if !is_tz_char(potential_value_char) { + // Return the valid TimeZoneIANAName + return Ok(TimeZone { + name: Some(cursor.slice(tz_name_start, cursor.pos())), + offset: None, + }); + } + } + + Err(Error::AbruptEnd) +} + +// ==== Utc Offset Parsing ==== + +/// Parse a full precision `UtcOffset` +pub(crate) fn parse_date_time_utc(cursor: &mut IsoCursor) -> ParseResult { + if cursor.check_or(false, is_utc_designator) { + cursor.advance(); + return Ok(TimeZone { + name: Some("UTC".to_owned()), + offset: None, + }); + } + + let separated = cursor.peek_n(3).map_or(false, is_time_separator); + + let mut utc_to_minute = parse_utc_offset_minute_precision(cursor)?; + + if cursor.check_or(false, is_time_separator) { + if !separated { + return Err(LexError::syntax( + "Unexpected TimeSeparator", + Position::new(1, cursor.pos()), + ) + .into()); + } + cursor.advance(); + } + + // Return early on None or next char an AnnotationOpen. + if cursor.check_or(true, is_annotation_open) { + return Ok(TimeZone { + name: None, + offset: Some(utc_to_minute), + }); + } + + // If `UtcOffsetWithSubMinuteComponents`, continue parsing. + utc_to_minute.second = parse_minute_second(cursor, true)?; + + let sub_second = if cursor.check_or(false, is_decimal_separator) { + parse_fraction(cursor)? + } else { + 0.0 + }; + + utc_to_minute.fraction = sub_second; + + Ok(TimeZone { + name: None, + offset: Some(utc_to_minute), + }) +} + +/// Parse an `UtcOffsetMinutePrecision` node +pub(crate) fn parse_utc_offset_minute_precision(cursor: &mut IsoCursor) -> ParseResult { + let sign = if let Some(ch) = cursor.next() { + if ch == '+' { + 1_i8 + } else { + -1_i8 + } + } else { + return Err(Error::AbruptEnd); + }; + let hour = parse_hour(cursor)?; + + // If at the end of the utc, then return. + if cursor + .check(|ch| !(ch.is_ascii_digit() || is_time_separator(ch))) + .ok_or_else(|| Error::AbruptEnd)? + { + return Ok(UTCOffset { + sign, + hour, + minute: 0, + second: 0, + fraction: 0.0, + }); + } + + // Advance cursor beyond any TimeSeparator + if cursor.check_or(false, is_time_separator) { + cursor.advance(); + } + + let minute = parse_minute_second(cursor, false)?; + + Ok(UTCOffset { + sign, + hour, + minute, + second: 0, + fraction: 0.0, + }) +}