diff --git a/Cargo.lock b/Cargo.lock index 4dc3f91218..1f8732ceca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,7 @@ dependencies = [ "boa_macros", "boa_parser", "boa_profiler", + "boa_temporal", "bytemuck", "cfg-if", "chrono", @@ -606,6 +607,18 @@ dependencies = [ "textwrap", ] +[[package]] +name = "boa_temporal" +version = "0.17.0" +dependencies = [ + "bitflags 2.4.1", + "icu_calendar", + "num-bigint", + "num-traits", + "rustc-hash", + "tinystr", +] + [[package]] name = "boa_tester" version = "0.17.0" diff --git a/Cargo.toml b/Cargo.toml index 2d934f7824..43730a4ae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "boa_parser", "boa_profiler", "boa_runtime", + "boa_temporal", "boa_tester", "boa_wasm", ] @@ -38,6 +39,7 @@ boa_macros = { version = "~0.17.0", path = "boa_macros" } boa_parser = { version = "~0.17.0", path = "boa_parser" } boa_profiler = { version = "~0.17.0", path = "boa_profiler" } boa_runtime = { version = "~0.17.0", path = "boa_runtime" } +boa_temporal = {version = "~0.17.0", path = "boa_temporal" } # Shared deps arbitrary = "1" diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index b8321f9881..73f02c0027 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -62,6 +62,7 @@ boa_profiler.workspace = true boa_macros.workspace = true boa_ast.workspace = true boa_parser.workspace = true +boa_temporal = { workspace = true } serde = { workspace = true, features = ["derive", "rc"] } serde_json.workspace = true rand = "0.8.5" diff --git a/boa_engine/src/builtins/temporal/calendar/iso.rs b/boa_engine/src/builtins/temporal/calendar/iso.rs deleted file mode 100644 index 3abb440c77..0000000000 --- a/boa_engine/src/builtins/temporal/calendar/iso.rs +++ /dev/null @@ -1,343 +0,0 @@ -//! Implementation of the "iso8601" `BuiltinCalendar`. - -use crate::{ - builtins::temporal::{ - self, - date_equations::mathematical_days_in_year, - duration::DurationRecord, - options::{ArithmeticOverflow, TemporalUnit}, - plain_date::iso::IsoDateRecord, - }, - js_string, JsNativeError, JsResult, JsString, -}; - -use super::{BuiltinCalendar, FieldsType}; - -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, - ) -> 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(IsoDateRecord::from_date_iso(date)) - } - - /// 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, - ) -> 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]]). - Ok(IsoDateRecord::from_date_iso(result)) - } - - /// 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, - ) -> 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]]). - Ok(IsoDateRecord::from_date_iso(result)) - } - - /// 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: &DurationRecord, - _overflow: ArithmeticOverflow, - ) -> 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, - ) -> 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) -> JsResult> { - // Returns undefined on iso8601. - Ok(None) - } - - /// `Temporal.Calendar.prototype.eraYear( dateLike )` for iso8601 calendar. - fn era_year(&self, _: &IsoDateRecord) -> JsResult> { - // Returns undefined on iso8601. - Ok(None) - } - - /// Returns the `year` for the `Iso` calendar. - fn year(&self, date_like: &IsoDateRecord) -> 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) - } - - /// Returns the `month` for the `Iso` calendar. - fn month(&self, date_like: &IsoDateRecord) -> 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 as i32) - } - - /// Returns the `monthCode` for the `Iso` calendar. - fn month_code(&self, date_like: &IsoDateRecord) -> 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())) - } - - /// Returns the `day` for the `Iso` calendar. - fn day(&self, date_like: &IsoDateRecord) -> 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 as i32) - } - - /// Returns the `dayOfWeek` for the `Iso` calendar. - fn day_of_week(&self, date_like: &IsoDateRecord) -> 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 i32) - } - - /// Returns the `dayOfYear` for the `Iso` calendar. - fn day_of_year(&self, date_like: &IsoDateRecord) -> 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)) - } - - /// Returns the `weekOfYear` for the `Iso` calendar. - fn week_of_year(&self, date_like: &IsoDateRecord) -> 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(i32::from(week_of.week)) - } - - /// Returns the `yearOfWeek` for the `Iso` calendar. - fn year_of_week(&self, date_like: &IsoDateRecord) -> 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), - RelativeUnit::Current => Ok(date.year().number), - RelativeUnit::Next => Ok(date.year().number + 1), - } - } - - /// Returns the `daysInWeek` value for the `Iso` calendar. - fn days_in_week(&self, _: &IsoDateRecord) -> JsResult { - Ok(7) - } - - /// Returns the `daysInMonth` value for the `Iso` calendar. - fn days_in_month(&self, date_like: &IsoDateRecord) -> 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.days_in_month())) - } - - /// Returns the `daysInYear` value for the `Iso` calendar. - fn days_in_year(&self, date_like: &IsoDateRecord) -> 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.days_in_year())) - } - - /// Return the amount of months in an ISO Calendar. - fn months_in_year(&self, _: &IsoDateRecord) -> JsResult { - Ok(12) - } - - /// Returns whether provided date is in a leap year according to this calendar. - fn in_leap_year(&self, date_like: &IsoDateRecord) -> JsResult { - // `ICU4X`'s `CalendarArithmetic` is currently private. - if mathematical_days_in_year(date_like.year()) == 366 { - return Ok(true); - } - Ok(false) - } - - // Resolve the fields for the iso calendar. - fn resolve_fields(&self, fields: &mut temporal::TemporalFields, _: FieldsType) -> JsResult<()> { - fields.iso_resolve_month()?; - Ok(()) - } - - /// Returns the ISO field descriptors, which is not called for the iso8601 calendar. - fn field_descriptors(&self, _: FieldsType) -> Vec<(JsString, 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 { - result.push(key.clone()); - if key.to_std_string_escaped().as_str() == "month" { - result.push(js_string!("monthCode")); - } else if key.to_std_string_escaped().as_str() == "monthCode" { - result.push(js_string!("month")); - } - } - 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 index f1e39422fc..c9de740c08 100644 --- a/boa_engine/src/builtins/temporal/calendar/mod.rs +++ b/boa_engine/src/builtins/temporal/calendar/mod.rs @@ -1,13 +1,10 @@ //! An implementation of the `Temporal` proposal's Calendar builtin. -use self::iso::IsoCalendar; +use std::str::FromStr; use super::{ create_temporal_date, create_temporal_duration, create_temporal_month_day, - create_temporal_year_month, - options::{ArithmeticOverflow, TemporalUnit, TemporalUnitGroup}, - plain_date::iso::IsoDateRecord, - DurationRecord, PlainDate, TemporalFields, + create_temporal_year_month, fields, options::TemporalUnitGroup, }; use crate::{ builtins::{ @@ -24,147 +21,32 @@ use crate::{ Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, }; use boa_profiler::Profiler; -use rustc_hash::FxHashMap; +use boa_temporal::{ + calendar::{ + AvailableCalendars, CalendarDateLike, CalendarFieldsType, CalendarSlot, + CALENDAR_PROTOCOL_METHODS, + }, + options::{ArithmeticOverflow, TemporalUnit}, +}; -mod iso; -pub(crate) mod utils; +mod object; +use object::CustomRuntimeCalendar; + +#[cfg(feature = "experimental")] #[cfg(test)] mod tests; -pub(crate) enum FieldsType { - Date, - YearMonth, - MonthDay, -} - -impl From<&[JsString]> for FieldsType { - fn from(value: &[JsString]) -> Self { - let year_present = value.contains(&js_string!("year")); - let day_present = value.contains(&js_string!("day")); - - if year_present && day_present { - FieldsType::Date - } else if year_present { - FieldsType::YearMonth - } else { - FieldsType::MonthDay - } - } -} - -// 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, - ) -> JsResult; - /// Creates a `Temporal.PlainYearMonth` object from the provided fields. - fn year_month_from_fields( - &self, - fields: &mut TemporalFields, - overflow: ArithmeticOverflow, - ) -> JsResult; - /// Creates a `Temporal.PlainMonthDay` object from the provided fields. - fn month_day_from_fields( - &self, - fields: &mut TemporalFields, - overflow: ArithmeticOverflow, - ) -> JsResult; - /// Returns a `Temporal.PlainDate` based off an added date. - fn date_add( - &self, - date: &PlainDate, - duration: &DurationRecord, - overflow: ArithmeticOverflow, - ) -> JsResult; - /// Returns a `Temporal.Duration` representing the duration between two dates. - fn date_until( - &self, - one: &PlainDate, - two: &PlainDate, - largest_unit: TemporalUnit, - ) -> JsResult; - /// Returns the era for a given `temporaldatelike`. - fn era(&self, date_like: &IsoDateRecord) -> JsResult>; - /// Returns the era year for a given `temporaldatelike` - fn era_year(&self, date_like: &IsoDateRecord) -> JsResult>; - /// Returns the `year` for a given `temporaldatelike` - fn year(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns the `month` for a given `temporaldatelike` - fn month(&self, date_like: &IsoDateRecord) -> JsResult; - // Note: Best practice would probably be to switch to a MonthCode enum after extraction. - /// Returns the `monthCode` for a given `temporaldatelike` - fn month_code(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns the `day` for a given `temporaldatelike` - fn day(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns a value representing the day of the week for a date. - fn day_of_week(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns a value representing the day of the year for a given calendar. - fn day_of_year(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns a value representing the week of the year for a given calendar. - fn week_of_year(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns the year of a given week. - fn year_of_week(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns the days in a week for a given calendar. - fn days_in_week(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns the days in a month for a given calendar. - fn days_in_month(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns the days in a year for a given calendar. - fn days_in_year(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns the months in a year for a given calendar. - fn months_in_year(&self, date_like: &IsoDateRecord) -> JsResult; - /// Returns whether a value is within a leap year according to the designated calendar. - fn in_leap_year(&self, date_like: &IsoDateRecord) -> JsResult; - /// Resolve the `TemporalFields` for the implemented Calendar - fn resolve_fields(&self, fields: &mut TemporalFields, r#type: FieldsType) -> JsResult<()>; - /// Return this calendar's a fieldName and whether it is required depending on type (date, day-month). - fn field_descriptors(&self, r#type: FieldsType) -> Vec<(JsString, 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, + slot: CalendarSlot, +} + +impl Calendar { + pub(crate) fn new(slot: CalendarSlot) -> Self { + Self { slot } + } } impl BuiltInObject for Calendar { @@ -186,6 +68,7 @@ impl IntrinsicObject for Calendar { Attribute::CONFIGURABLE, ) .accessor(utf16!("id"), Some(get_id), None, Attribute::default()) + .static_method(Self::from, js_string!("from"), 1) .method(Self::date_from_fields, js_string!("dateFromFields"), 2) .method( Self::year_month_from_fields, @@ -257,20 +140,26 @@ impl BuiltInConstructor for Calendar { }; // 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()); - } + // a. Throw a RangeError exception. + let _ = AvailableCalendars::from_str(&id.to_std_string_escaped())?; // 4. Return ? CreateTemporalCalendar(id, NewTarget). - create_temporal_calendar(id, Some(new_target.clone()), context) + create_temporal_calendar( + CalendarSlot::Identifier(id.to_std_string_escaped()), + Some(new_target.clone()), + context, + ) } } impl Calendar { - fn get_id(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + fn from(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let calendar_like = args.get_or_undefined(0); + let slot = to_temporal_calendar_slot_value(calendar_like, context)?; + create_temporal_calendar(slot, None, context) + } + + fn get_id(this: &JsValue, _: &[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.") })?; @@ -280,7 +169,12 @@ impl Calendar { .with_message("the this value of Calendar must be a Calendar object.") })?; - Ok(calendar.identifier.clone().into()) + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; + + Ok(JsString::from(protocol.identifier(context)?.as_str()).into()) } /// 15.8.2.1 `Temporal.Calendar.prototype.dateFromFields ( fields [ , options ] )` - Supercedes 12.5.4 @@ -300,10 +194,10 @@ impl Calendar { })?; // Retrieve the current CalendarProtocol. - let available_calendars = available_calendars(); - let this_calendar = available_calendars - .get(calendar.identifier.as_slice()) - .expect("builtin must exist"); + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; // 3. If Type(fields) is not Object, throw a TypeError exception. let fields = args.get_or_undefined(0); @@ -323,10 +217,10 @@ impl Calendar { ]); // 6. If calendar.[[Identifier]] is "iso8601", then - let mut fields = if calendar.identifier.as_slice() == ISO { + let mut fields = if protocol.identifier(context)?.as_str() == "iso8601" { // a. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « "year", "day" »). let mut required_fields = Vec::from([js_string!("year"), js_string!("day")]); - temporal::TemporalFields::from_js_object( + fields::prepare_temporal_fields( fields_obj, &mut relevant_field_names, &mut required_fields, @@ -338,9 +232,9 @@ impl Calendar { // 7. Else, } else { // a. Let calendarRelevantFieldDescriptors be CalendarFieldDescriptors(calendar.[[Identifier]], date). - let calendar_relevant_fields = this_calendar.field_descriptors(FieldsType::Date); + let calendar_relevant_fields = protocol.field_descriptors(CalendarFieldsType::Date); // b. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « », calendarRelevantFieldDescriptors). - temporal::TemporalFields::from_js_object( + fields::prepare_temporal_fields( fields_obj, &mut relevant_field_names, &mut Vec::new(), @@ -363,10 +257,9 @@ impl Calendar { // a. Perform ? CalendarResolveFields(calendar.[[Identifier]], fields, date). // b. Let result be ? CalendarDateToISO(calendar.[[Identifier]], fields, overflow). - let result = this_calendar.date_from_fields(&mut fields, overflow)?; + let result = protocol.date_from_fields(&mut fields, overflow, context)?; - create_temporal_date(result, calendar.identifier.clone().into(), None, context) - .map(Into::into) + create_temporal_date(result, None, context).map(Into::into) } /// 15.8.2.2 `Temporal.Calendar.prototype.yearMonthFromFields ( fields [ , options ] )` - Supercedes 12.5.5 @@ -383,11 +276,11 @@ impl Calendar { .with_message("the this value of Calendar must be a Calendar object.") })?; - let available_calendars = available_calendars(); + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; - 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.") @@ -403,10 +296,10 @@ impl Calendar { ]); // 6. Set fields to ? PrepareTemporalFields(fields, « "month", "monthCode", "year" », « "year" »). - let mut fields = if calendar.identifier.as_slice() == ISO { + let mut fields = if protocol.identifier(context)?.as_str() == "iso8601" { // a. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « "year" »). let mut required_fields = Vec::from([js_string!("year")]); - temporal::TemporalFields::from_js_object( + fields::prepare_temporal_fields( fields_obj, &mut relevant_field_names, &mut required_fields, @@ -419,8 +312,9 @@ impl Calendar { // 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(FieldsType::YearMonth); - temporal::TemporalFields::from_js_object( + let calendar_relevant_fields = + protocol.field_descriptors(CalendarFieldsType::YearMonth); + fields::prepare_temporal_fields( fields_obj, &mut relevant_field_names, &mut Vec::new(), @@ -429,6 +323,7 @@ impl Calendar { 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)). @@ -438,10 +333,9 @@ impl Calendar { let overflow = get_option::(&options, utf16!("overflow"), context)? .unwrap_or(ArithmeticOverflow::Constrain); - let result = this_calendar.year_month_from_fields(&mut fields, overflow)?; + let result = protocol.year_month_from_fields(&mut fields, overflow, context)?; - create_temporal_year_month(result, calendar.identifier.clone().into(), None, context) - .map(Into::into) + create_temporal_year_month(result, None, context) } /// 15.8.2.3 `Temporal.Calendar.prototype.monthDayFromFields ( fields [ , options ] )` - Supercedes 12.5.6 @@ -460,11 +354,10 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; // 3. If Type(fields) is not Object, throw a TypeError exception. let fields = args.get_or_undefined(0); @@ -484,10 +377,10 @@ impl Calendar { ]); // 6. If calendar.[[Identifier]] is "iso8601", then - let mut fields = if calendar.identifier.as_slice() == ISO { + let mut fields = if protocol.identifier(context)?.as_str() == "iso8601" { // a. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « "day" »). let mut required_fields = Vec::from([js_string!("day")]); - temporal::TemporalFields::from_js_object( + fields::prepare_temporal_fields( fields_obj, &mut relevant_field_names, &mut required_fields, @@ -499,9 +392,9 @@ impl Calendar { // 7. Else, } else { // a. Let calendarRelevantFieldDescriptors be CalendarFieldDescriptors(calendar.[[Identifier]], month-day). - let calendar_relevant_fields = this_calendar.field_descriptors(FieldsType::MonthDay); + let calendar_relevant_fields = protocol.field_descriptors(CalendarFieldsType::MonthDay); // b. Set fields to ? PrepareTemporalFields(fields, relevantFieldNames, « », calendarRelevantFieldDescriptors). - temporal::TemporalFields::from_js_object( + fields::prepare_temporal_fields( fields_obj, &mut relevant_field_names, &mut Vec::new(), @@ -516,10 +409,9 @@ impl Calendar { let overflow = get_option(&options, utf16!("overflow"), context)? .unwrap_or(ArithmeticOverflow::Constrain); - let result = this_calendar.month_day_from_fields(&mut fields, overflow)?; + let result = protocol.month_day_from_fields(&mut fields, overflow, context)?; - create_temporal_month_day(result, calendar.identifier.clone().into(), None, context) - .map(Into::into) + create_temporal_month_day(result, None, context) } /// 15.8.2.4 `Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] )` - supercedes 12.5.7 @@ -535,11 +427,10 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; // 4. Set date to ? ToTemporalDate(date). let date_like = args.get_or_undefined(0); @@ -547,7 +438,7 @@ impl Calendar { // 5. Set duration to ? ToTemporalDuration(duration). let duration_like = args.get_or_undefined(1); - let mut duration = temporal::duration::to_temporal_duration(duration_like)?; + let duration = temporal::duration::to_temporal_duration(duration_like)?; // 6. Set options to ? GetOptionsObject(options). let options = args.get_or_undefined(2); @@ -558,12 +449,11 @@ impl Calendar { .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)?; + duration.balance_time_duration(TemporalUnit::Day)?; - let result = this_calendar.date_add(&date, &duration, overflow)?; + let result = protocol.date_add(&date.inner, &duration, overflow, context)?; - create_temporal_date(result, calendar.identifier.clone().into(), None, context) - .map(Into::into) + create_temporal_date(result, None, context).map(Into::into) } ///15.8.2.5 `Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] )` - Supercedes 12.5.8 @@ -579,11 +469,10 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; // 4. Set one to ? ToTemporalDate(one). let one = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; @@ -604,7 +493,7 @@ impl Calendar { )? .unwrap_or(TemporalUnit::Day); - let result = this_calendar.date_until(&one, &two, largest_unit)?; + let result = protocol.date_until(&one.inner, &two.inner, largest_unit, context)?; create_temporal_duration(result, None, context).map(Into::into) } @@ -619,42 +508,18 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; - 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."); + let date_like = to_calendar_date_like(args.get_or_undefined(0), context)?; - 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"); + let result = protocol + .era(&date_like, context)? + .map_or(JsValue::undefined(), |r| JsString::from(r.as_str()).into()); - 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) - .map(|r| r.map_or(JsValue::undefined(), Into::into)) + Ok(result) } /// 15.8.2.7 `Temporal.Calendar.prototype.eraYear ( temporalDateLike )` @@ -667,42 +532,18 @@ impl Calendar { .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"); + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; - 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."); + let date_like = to_calendar_date_like(args.get_or_undefined(0), context)?; - ym.inner - } - _ => { - let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; - date.inner - } - }; + let result = protocol + .era_year(&date_like, context)? + .map_or(JsValue::undefined(), JsValue::from); - this_calendar - .era_year(&date_info) - .map(|r| r.map_or(JsValue::undefined(), JsValue::from)) + Ok(result) } /// 15.8.2.8 `Temporal.Calendar.prototype.year ( temporalDateLike )` @@ -715,40 +556,16 @@ impl Calendar { .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"); + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; - 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."); + let date_like = to_calendar_date_like(args.get_or_undefined(0), context)?; - ym.inner - } - _ => { - let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; - date.inner - } - }; + let result = protocol.year(&date_like, context)?; - this_calendar.year(&date_record).map(Into::into) + Ok(result.into()) } /// 15.8.2.9 `Temporal.Calendar.prototype.month ( temporalDateLike )` @@ -761,49 +578,21 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; - let date_like = args.get_or_undefined(0); + let date_like = to_calendar_date_like(args.get_or_undefined(0), context)?; // 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 - } - }; + let result = protocol.month(&date_like, context)?; - this_calendar.month(&date_record).map(Into::into) + Ok(result.into()) } /// 15.8.2.10 `Temporal.Calendar.prototype.monthCode ( temporalDateLike )` @@ -816,46 +605,16 @@ impl Calendar { .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."); + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; - 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."); + let date_like = to_calendar_date_like(args.get_or_undefined(0), context)?; - md.inner - } - _ => { - let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; - date.inner - } - }; + let result = protocol.month_code(&date_like, context)?; - this_calendar.month_code(&date_record).map(Into::into) + Ok(JsString::from(result.as_str()).into()) } /// 15.8.2.11 `Temporal.Calendar.prototype.day ( temporalDateLike )` @@ -868,40 +627,16 @@ impl Calendar { .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."); + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; - 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"); + let date_like = to_calendar_date_like(args.get_or_undefined(0), context)?; - 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."); + let result = protocol.day(&date_like, context)?; - md.inner - } - _ => { - let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; - date.inner - } - }; - - this_calendar.day(&date_record).map(Into::into) + Ok(result.into()) } /// 15.8.2.12 `Temporal.Calendar.prototype.dayOfWeek ( dateOrDateTime )` @@ -916,18 +651,17 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; // 3. Let temporalDate be ? ToTemporalDate(temporalDateLike). let date = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; - let result = this_calendar.day_of_week(&date.inner); + let result = protocol.day_of_week(&CalendarDateLike::Date(date.inner.clone()), context)?; - result.map(Into::into) + Ok(result.into()) } /// 15.8.2.13 `Temporal.Calendar.prototype.dayOfYear ( temporalDateLike )` @@ -941,18 +675,17 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; // 3. Let temporalDate be ? ToTemporalDate(temporalDateLike). let date = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; - let result = this_calendar.day_of_year(&date.inner); + let result = protocol.day_of_year(&CalendarDateLike::Date(date.inner.clone()), context)?; - result.map(Into::into) + Ok(result.into()) } /// 15.8.2.14 `Temporal.Calendar.prototype.weekOfYear ( temporalDateLike )` @@ -965,18 +698,17 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; // 3. Let temporalDate be ? ToTemporalDate(temporalDateLike). let date = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; - let result = this_calendar.week_of_year(&date.inner); + let result = protocol.week_of_year(&CalendarDateLike::Date(date.inner.clone()), context)?; - result.map(Into::into) + Ok(result.into()) } /// 15.8.2.15 `Temporal.Calendar.prototype.yearOfWeek ( temporalDateLike )` @@ -989,18 +721,17 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; // 3. Let temporalDate be ? ToTemporalDate(temporalDateLike). let date = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; - let result = this_calendar.year_of_week(&date.inner); + let result = protocol.year_of_week(&CalendarDateLike::Date(date.inner.clone()), context)?; - result.map(Into::into) + Ok(result.into()) } /// 15.8.2.16 `Temporal.Calendar.prototype.daysInWeek ( temporalDateLike )` @@ -1013,18 +744,17 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; // 3. Let temporalDate be ? ToTemporalDate(temporalDateLike). let date = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; - let result = this_calendar.days_in_week(&date.inner); + let result = protocol.days_in_week(&CalendarDateLike::Date(date.inner.clone()), context)?; - result.map(Into::into) + Ok(result.into()) } /// 15.8.2.17 `Temporal.Calendar.prototype.daysInMonth ( temporalDateLike )` @@ -1037,42 +767,16 @@ impl Calendar { .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 - } + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), }; - let result = this_calendar.days_in_month(&date_record); + let date_like = to_calendar_date_like(args.get_or_undefined(0), context)?; + + let result = protocol.days_in_month(&date_like, context)?; - result.map(Into::into) + Ok(result.into()) } /// 15.8.2.18 `Temporal.Calendar.prototype.daysInYear ( temporalDateLike )` @@ -1085,42 +789,15 @@ impl Calendar { .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 - } + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), }; - let result = this_calendar.days_in_year(&date_record); + let date_like = to_calendar_date_like(args.get_or_undefined(0), context)?; + let result = protocol.days_in_year(&date_like, context)?; - result.map(Into::into) + Ok(result.into()) } /// 15.8.2.19 `Temporal.Calendar.prototype.monthsInYear ( temporalDateLike )` @@ -1137,42 +814,16 @@ impl Calendar { .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 - } + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), }; - let result = this_calendar.months_in_year(&date_record); + let date_like = to_calendar_date_like(args.get_or_undefined(0), context)?; + + let result = protocol.months_in_year(&date_like, context)?; - result.map(Into::into) + Ok(result.into()) } /// 15.8.2.20 `Temporal.Calendar.prototype.inLeapYear ( temporalDateLike )` @@ -1185,42 +836,16 @@ impl Calendar { .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 - } + let protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), }; - let result = this_calendar.in_leap_year(&date_record); + let date_like = to_calendar_date_like(args.get_or_undefined(0), context)?; + + let result = protocol.in_leap_year(&date_like, context)?; - result.map(Into::into) + Ok(result.into()) } /// 15.8.2.21 `Temporal.Calendar.prototype.fields ( fields )` @@ -1235,11 +860,10 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; // 3. Let iteratorRecord be ? GetIterator(fields, sync). let mut iterator_record = @@ -1266,9 +890,12 @@ impl Calendar { // 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. - match value.to_std_string_escaped().as_str() { - "year" | "month" | "monthCode" | "day" if !fields_names.contains(&value) => { - fields_names.push(value); + let this_field = value.to_std_string_escaped(); + match this_field.as_str() { + "year" | "month" | "monthCode" | "day" + if !fields_names.contains(&this_field) => + { + fields_names.push(this_field); } _ => { let completion = Err(JsNativeError::range() @@ -1289,11 +916,11 @@ impl Calendar { // 7. Let result be fieldNames. // 8. If calendar.[[Identifier]] is not "iso8601", then - if calendar.identifier.as_slice() != ISO { + if protocol.identifier(context)?.as_str() != "iso8601" { // 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(FieldsType::from(&fields_names[..])); + protocol.field_descriptors(CalendarFieldsType::from(&fields_names[..])); // c. For each Calendar Field Descriptor Record desc of extraFieldDescriptors, do for descriptor in extended_fields { // i. Append desc.[[Property]] to result. @@ -1302,10 +929,13 @@ impl Calendar { } // 9. Return CreateArrayFromList(result). - Ok( - Array::create_array_from_list(fields_names.iter().map(|s| s.clone().into()), context) - .into(), + 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 )` @@ -1320,11 +950,10 @@ impl Calendar { .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 protocol = match &calendar.slot { + CalendarSlot::Identifier(s) => AvailableCalendars::from_str(s)?.to_protocol(), + CalendarSlot::Protocol(proto) => proto.clone(), + }; let fields = args.get_or_undefined(0).to_object(context)?; let additional_fields = args.get_or_undefined(1).to_object(context)?; @@ -1350,14 +979,14 @@ impl Calendar { let add_keys = additional_fields_copy .__own_property_keys__(context)? .iter() - .map(|k| JsString::from(k.to_string())) + .map(ToString::to_string) .collect::>(); // 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); + let overridden_keys = protocol.field_keys_to_ignore(add_keys); // 9. Let merged be OrdinaryObjectCreate(null). let merged = JsObject::with_null_proto(); @@ -1376,7 +1005,7 @@ impl Calendar { for key in field_keys { // a. Let propValue be undefined. // b. If overriddenKeys contains key, then - let prop_value = if overridden_keys.contains(&key) { + let prop_value = if overridden_keys.contains(&key.to_std_string_escaped()) { // i. Set propValue to ! Get(additionalFieldsCopy, key). additional_fields_copy.get(key.as_slice(), context)? // c. Else, @@ -1409,16 +1038,11 @@ impl Calendar { /// 12.2.1 `CreateTemporalCalendar ( identifier [ , newTarget ] )` pub(crate) fn create_temporal_calendar( - identifier: &JsString, + identifier: CalendarSlot, 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 @@ -1434,7 +1058,7 @@ pub(crate) fn create_temporal_calendar( let proto = get_prototype_from_constructor(&new_target, StandardConstructors::calendar, context)?; - let obj = JsObject::from_proto_and_data(proto, ObjectData::calendar(calendar)); + let obj = JsObject::from_proto_and_data(proto, ObjectData::calendar(Calendar::new(identifier))); // 4. Set object.[[Identifier]] to the ASCII-lowercase of identifier. // 5. Return object. @@ -1446,14 +1070,14 @@ pub(crate) fn create_temporal_calendar( pub(crate) fn get_temporal_calendar_slot_value_with_default( item: &JsObject, context: &mut Context, -) -> JsResult { +) -> 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(); + let calendar = date.inner.calendar().clone(); drop(obj); return Ok(calendar); } @@ -1461,7 +1085,7 @@ pub(crate) fn get_temporal_calendar_slot_value_with_default( let obj = item.borrow(); let date_time = obj.as_plain_date_time(); if let Some(dt) = date_time { - let calendar = dt.calendar.clone(); + let calendar = dt.inner.calendar().clone(); drop(obj); return Ok(calendar); } @@ -1469,7 +1093,7 @@ pub(crate) fn get_temporal_calendar_slot_value_with_default( let obj = item.borrow(); let year_month = obj.as_plain_year_month(); if let Some(ym) = year_month { - let calendar = ym.calendar.clone(); + let calendar = ym.inner.calendar().clone(); drop(obj); return Ok(calendar); } @@ -1477,7 +1101,7 @@ pub(crate) fn get_temporal_calendar_slot_value_with_default( let obj = item.borrow(); let month_day = obj.as_plain_month_day(); if let Some(md) = month_day { - let calendar = md.calendar.clone(); + let calendar = md.inner.calendar().clone(); drop(obj); return Ok(calendar); } @@ -1491,23 +1115,19 @@ pub(crate) fn get_temporal_calendar_slot_value_with_default( let calendar_like = item.get(utf16!("calendar"), context)?; // 3. Return ? ToTemporalCalendarSlotValue(calendarLike, "iso8601"). - to_temporal_calendar_slot_value(&calendar_like, Some(ISO.into())) + to_temporal_calendar_slot_value(&calendar_like, context) } -#[allow(unused)] -fn to_temporal_calendar_slot_value( +/// `12.2.20 ToTemporalCalendarSlotValue ( temporalCalendarLike [ , default ] )` +pub(crate) fn to_temporal_calendar_slot_value( calendar_like: &JsValue, - default: Option, -) -> JsResult { + context: &mut Context, +) -> JsResult { // 1. If temporalCalendarLike is undefined and default is present, then + // a. Assert: IsBuiltinCalendar(default) is true. + // b. Return default. 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()); - } - } + return Ok(CalendarSlot::Identifier("iso8601".to_owned())); // 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 @@ -1516,31 +1136,15 @@ fn to_temporal_calendar_slot_value( 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(); + let calendar = date.inner.calendar().clone(); return Ok(calendar); } - } else if calendar_like.is_zoned_date_time() { + } else if calendar_like.is_plain_date_time() + || calendar_like.is_plain_year_month() + || calendar_like.is_plain_month_day() + || calendar_like.is_zoned_date_time() + { + // TODO(nekevss): Separate out and reimplement the handling of different branches. return Err(JsNativeError::range() .with_message("Not yet implemented.") .into()); @@ -1548,8 +1152,16 @@ fn to_temporal_calendar_slot_value( // TODO: implement ObjectImplementsTemporalCalendarProtocol // b. If ? ObjectImplementsTemporalCalendarProtocol(temporalCalendarLike) is false, throw a TypeError exception. + if !object_implements_calendar_protocol(calendar_like, context) { + return Err(JsNativeError::typ() + .with_message("CalendarLike does not implement the CalendarProtocol.") + .into()); + } + + // Types: Box <- UserCalendar + let protocol = Box::new(CustomRuntimeCalendar::new(calendar_like)); // c. Return temporalCalendarLike. - return Ok(calendar_like.clone().into()); + return Ok(CalendarSlot::Protocol(protocol)); } // 3. If temporalCalendarLike is not a String, throw a TypeError exception. @@ -1563,746 +1175,41 @@ fn to_temporal_calendar_slot_value( // 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()) + Ok(CalendarSlot::Identifier("iso8601".to_owned())) } -// ---------------------------- Native Abstract Calendar 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) +fn object_implements_calendar_protocol(calendar_like: &JsObject, context: &mut Context) -> bool { + CALENDAR_PROTOCOL_METHODS.into_iter().all(|method| { + calendar_like + .__has_property__(&JsString::from(method).into(), context) + .unwrap_or(false) + }) } -/// 12.2.2 `CalendarFields ( calendar, fieldNames )` -/// -/// `CalendarFields` takes the input fields and adds the `extraFieldDescriptors` for -/// that specific calendar. -#[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()), - } -} +/// Utility function for taking a `JsValue` and converting it to a temporal library `CalendarDateLike` enum. +fn to_calendar_date_like(date_like: &JsValue, context: &mut Context) -> JsResult { + 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."); -/// 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: &PlainDate, - duration: &DurationRecord, - options: &JsValue, - context: &mut Context, -) -> JsResult { - // NOTE: The specification never calls CalendarDateAdd without an options argument provided. - // 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.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.as_object(context)?.into(), - duration.as_object(context)?.into(), - options.clone(), - ], - context, - )?; - - // 5. Perform ? RequireInternalSlot(addedDate, [[InitializedTemporalDate]]). - // 6. Return addedDate. - match added_date { + Ok(CalendarDateLike::DateTime(date_time.inner.clone())) + } JsValue::Object(o) if o.is_plain_date() => { let obj = o.borrow(); - let result = obj.as_plain_date().expect("must be a plain date"); - Ok(result.clone()) - } - _ => Err(JsNativeError::typ() - .with_message("dateAdd returned a value other than a Temoporal.PlainDate") - .into()), - } -} + let date = obj.as_plain_date().expect("Must be a Date"); -/// 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: &PlainDate, - two: &PlainDate, - 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.as_object(context)?.into(), - two.as_object(context)?.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) + Ok(CalendarDateLike::Date(date.inner.clone())) } - _ => 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). - let identifier = match calendar { - JsValue::String(s) => s.clone(), - JsValue::Object(o) if o.is_calendar() => { + JsValue::Object(o) if o.is_plain_year_month() => { let obj = o.borrow(); - let calendar = obj.as_calendar().expect("value must be a calendar"); - calendar.identifier.clone() - } - _ => unreachable!( - "A calendar slot value not being a calendar obj or string is an implementation error." - ), - }; - - let calendars = available_calendars(); - let this = calendars.get(identifier.as_slice()).ok_or_else(|| { - JsNativeError::range().with_message("calendar value was not an implemented 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.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()); - }; + let ym = obj.as_plain_year_month().expect("must be a YearMonth."); - // 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.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.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.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()), + Ok(CalendarDateLike::YearMonth(ym.inner.clone())) + } + _ => { + let date = temporal::plain_date::to_temporal_date(date_like, None, context)?; + Ok(CalendarDateLike::Date(date.inner.clone())) + } } } - -/// 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/object.rs b/boa_engine/src/builtins/temporal/calendar/object.rs new file mode 100644 index 0000000000..291a3d5925 --- /dev/null +++ b/boa_engine/src/builtins/temporal/calendar/object.rs @@ -0,0 +1,883 @@ +//! Boa's implementation of a user-defined Anonymous Calendar. + +use crate::{ + builtins::temporal::{plain_date, plain_month_day, plain_year_month}, + property::PropertyKey, + Context, JsObject, JsString, JsValue, +}; +use std::any::Any; + +use boa_macros::utf16; +use boa_temporal::{ + calendar::{CalendarDateLike, CalendarProtocol}, + date::Date, + duration::Duration, + error::TemporalError, + fields::TemporalFields, + month_day::MonthDay, + options::ArithmeticOverflow, + year_month::YearMonth, + TemporalResult, TinyAsciiStr, +}; +use num_traits::ToPrimitive; + +/// A user-defined, custom calendar that is only known at runtime +/// and executed at runtime. +/// +/// A user-defined calendar implements all of the `CalendarProtocolMethods` +/// and therefore satisfies the requirements to be used as a calendar. +#[derive(Debug, Clone)] +pub(crate) struct CustomRuntimeCalendar { + calendar: JsObject, +} + +impl CustomRuntimeCalendar { + pub(crate) fn new(calendar: &JsObject) -> Self { + Self { + calendar: calendar.clone(), + } + } +} + +impl CalendarProtocol for CustomRuntimeCalendar { + fn date_from_fields( + &self, + fields: &mut TemporalFields, + overflow: ArithmeticOverflow, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let method = self + .calendar + .get(utf16!("dateFromFields"), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let fields = JsObject::from_temporal_fields(fields, context) + .map_err(|e| TemporalError::general(e.to_string()))?; + + let overflow_obj = JsObject::with_null_proto(); + + overflow_obj + .create_data_property_or_throw( + utf16!("overflow"), + JsString::from(overflow.to_string()), + context, + ) + .map_err(|e| TemporalError::general(e.to_string()))?; + + let value = method + .as_callable() + .ok_or_else(|| { + TemporalError::general("dateFromFields must be implemented as a callable method.") + })? + .call( + &self.calendar.clone().into(), + &[fields.into(), overflow_obj.into()], + context, + ) + .map_err(|e| TemporalError::general(e.to_string()))?; + + let obj = value.as_object().map(JsObject::borrow).ok_or_else(|| { + TemporalError::r#type() + .with_message("datefromFields must return a valid PlainDate object.") + })?; + + let pd = obj.as_plain_date().ok_or_else(|| { + TemporalError::r#type().with_message("Object returned was not a PlainDate") + })?; + + Ok(pd.inner.clone()) + } + + fn year_month_from_fields( + &self, + fields: &mut TemporalFields, + overflow: ArithmeticOverflow, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let method = self + .calendar + .get(utf16!("yearMonthFromFields"), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let fields = JsObject::from_temporal_fields(fields, context) + .map_err(|e| TemporalError::general(e.to_string()))?; + + let overflow_obj = JsObject::with_null_proto(); + + overflow_obj + .create_data_property_or_throw( + utf16!("overflow"), + JsString::from(overflow.to_string()), + context, + ) + .map_err(|e| TemporalError::general(e.to_string()))?; + + let value = method + .as_callable() + .ok_or_else(|| { + TemporalError::general( + "yearMonthFromFields must be implemented as a callable method.", + ) + })? + .call( + &self.calendar.clone().into(), + &[fields.into(), overflow_obj.into()], + context, + ) + .map_err(|e| TemporalError::general(e.to_string()))?; + + let obj = value.as_object().map(JsObject::borrow).ok_or_else(|| { + TemporalError::r#type() + .with_message("yearMonthFromFields must return a valid PlainYearMonth object.") + })?; + + let ym = obj.as_plain_year_month().ok_or_else(|| { + TemporalError::r#type().with_message("Object returned was not a PlainDate") + })?; + + Ok(ym.inner.clone()) + } + + fn month_day_from_fields( + &self, + fields: &mut TemporalFields, + overflow: ArithmeticOverflow, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let method = self + .calendar + .get(utf16!("yearMonthFromFields"), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let fields = JsObject::from_temporal_fields(fields, context) + .map_err(|e| TemporalError::general(e.to_string()))?; + + let overflow_obj = JsObject::with_null_proto(); + + overflow_obj + .create_data_property_or_throw( + utf16!("overflow"), + JsString::from(overflow.to_string()), + context, + ) + .map_err(|e| TemporalError::general(e.to_string()))?; + + let value = method + .as_callable() + .ok_or_else(|| { + TemporalError::general( + "yearMonthFromFields must be implemented as a callable method.", + ) + })? + .call( + &self.calendar.clone().into(), + &[fields.into(), overflow_obj.into()], + context, + ) + .map_err(|e| TemporalError::general(e.to_string()))?; + + let obj = value.as_object().map(JsObject::borrow).ok_or_else(|| { + TemporalError::r#type() + .with_message("yearMonthFromFields must return a valid PlainYearMonth object.") + })?; + + let md = obj.as_plain_month_day().ok_or_else(|| { + TemporalError::r#type().with_message("Object returned was not a PlainDate") + })?; + + Ok(md.inner.clone()) + } + + fn date_add( + &self, + _date: &Date, + _duration: &Duration, + _overflow: ArithmeticOverflow, + _context: &mut dyn Any, + ) -> TemporalResult { + // TODO + Err(TemporalError::general("Not yet implemented.")) + } + + fn date_until( + &self, + _one: &Date, + _two: &Date, + _largest_unit: boa_temporal::options::TemporalUnit, + _context: &mut dyn Any, + ) -> TemporalResult { + // TODO + Err(TemporalError::general("Not yet implemented.")) + } + + fn era( + &self, + _: &CalendarDateLike, + _: &mut dyn Any, + ) -> TemporalResult>> { + // Return undefined as custom calendars do not implement -> Currently. + Ok(None) + } + + fn era_year(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult> { + // Return undefined as custom calendars do not implement -> Currently. + Ok(None) + } + + fn year(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("year")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. If result < 1𝔽, throw a RangeError exception. + // 6. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("year must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err(TemporalError::r#type().with_message("year return must be integral.")); + } + + if number < 1f64 { + return Err(TemporalError::r#type().with_message("year return must be larger than 1.")); + } + + let result = number + .to_i32() + .ok_or_else(|| TemporalError::range().with_message("year exceeded a valid range."))?; + + Ok(result) + } + + fn month(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("month")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. If result < 1𝔽, throw a RangeError exception. + // 6. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("month must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err(TemporalError::r#type().with_message("month return must be integral.")); + } + + if number < 1f64 { + return Err(TemporalError::r#type().with_message("month return must be larger than 1.")); + } + + let result = number + .to_u8() + .ok_or_else(|| TemporalError::range().with_message("month exceeded a valid range."))?; + + Ok(result) + } + + fn month_code( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult> { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("monthCode")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + let JsValue::String(result) = val else { + return Err(TemporalError::r#type().with_message("monthCode return must be a String.")); + }; + + let result = TinyAsciiStr::<4>::from_str(&result.to_std_string_escaped()) + .map_err(|_| TemporalError::general("Unexpected monthCode value."))?; + + Ok(result) + } + + fn day(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("day")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. If result < 1𝔽, throw a RangeError exception. + // 6. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("day must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err(TemporalError::r#type().with_message("day return must be integral.")); + } + + if number < 1f64 { + return Err(TemporalError::r#type().with_message("day return must be larger than 1.")); + } + + let result = number + .to_u8() + .ok_or_else(|| TemporalError::range().with_message("day exceeded a valid range."))?; + + Ok(result) + } + + fn day_of_week( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("dayOfWeek")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. If result < 1𝔽, throw a RangeError exception. + // 6. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("DayOfWeek must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err(TemporalError::r#type().with_message("DayOfWeek return must be integral.")); + } + + if number < 1f64 { + return Err( + TemporalError::r#type().with_message("DayOfWeek return must be larger than 1.") + ); + } + + let result = number.to_u16().ok_or_else(|| { + TemporalError::range().with_message("DayOfWeek exceeded valid range.") + })?; + + Ok(result) + } + + fn day_of_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("dayOfYear")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. If result < 1𝔽, throw a RangeError exception. + // 6. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("dayOfYear must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err(TemporalError::r#type().with_message("dayOfYear return must be integral.")); + } + + if number < 1f64 { + return Err( + TemporalError::r#type().with_message("dayOfYear return must be larger than 1.") + ); + } + + let result = number.to_u16().ok_or_else(|| { + TemporalError::range().with_message("dayOfYear exceeded valid range.") + })?; + + Ok(result) + } + + fn week_of_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("weekOfYear")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. If result < 1𝔽, throw a RangeError exception. + // 6. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("weekOfYear must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err(TemporalError::r#type().with_message("weekOfYear return must be integral.")); + } + + if number < 1f64 { + return Err( + TemporalError::r#type().with_message("weekOfYear return must be larger than 1.") + ); + } + + let result = number.to_u16().ok_or_else(|| { + TemporalError::range().with_message("weekOfYear exceeded valid range.") + })?; + + Ok(result) + } + + fn year_of_week( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("yearOfWeek")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("yearOfWeek must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err(TemporalError::r#type().with_message("yearOfWeek return must be integral.")); + } + + let result = number.to_i32().ok_or_else(|| { + TemporalError::range().with_message("yearOfWeek exceeded valid range.") + })?; + + Ok(result) + } + + fn days_in_week( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("daysInWeek")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. If result < 1𝔽, throw a RangeError exception. + // 6. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("daysInWeek must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err(TemporalError::r#type().with_message("daysInWeek return must be integral.")); + } + + if number < 1f64 { + return Err( + TemporalError::r#type().with_message("daysInWeek return must be larger than 1.") + ); + } + + let result = number.to_u16().ok_or_else(|| { + TemporalError::range().with_message("daysInWeek exceeded valid range.") + })?; + + Ok(result) + } + + fn days_in_month( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("daysInMonth")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. If result < 1𝔽, throw a RangeError exception. + // 6. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("daysInMonth must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err( + TemporalError::r#type().with_message("daysInMonth return must be integral.") + ); + } + + if number < 1f64 { + return Err( + TemporalError::r#type().with_message("daysInMonth return must be larger than 1.") + ); + } + + let result = number.to_u16().ok_or_else(|| { + TemporalError::range().with_message("daysInMonth exceeded valid range.") + })?; + + Ok(result) + } + + fn days_in_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("daysInYear")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. If result < 1𝔽, throw a RangeError exception. + // 6. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("daysInYear must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err(TemporalError::r#type().with_message("daysInYear return must be integral.")); + } + + if number < 1f64 { + return Err( + TemporalError::r#type().with_message("daysInYear return must be larger than 1.") + ); + } + + let result = number.to_u16().ok_or_else(|| { + TemporalError::range().with_message("monthsInYear exceeded valid range.") + })?; + + Ok(result) + } + + fn months_in_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("monthsInYear")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + // Validate the return value. + // 3. If Type(result) is not Number, throw a TypeError exception. + // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. + // 5. If result < 1𝔽, throw a RangeError exception. + // 6. Return ℝ(result). + + let Some(number) = val.as_number() else { + return Err(TemporalError::r#type().with_message("monthsInYear must return a number.")); + }; + + if !number.is_finite() || number.fract() != 0.0 { + return Err( + TemporalError::r#type().with_message("monthsInYear return must be integral.") + ); + } + + if number < 1f64 { + return Err( + TemporalError::r#type().with_message("monthsInYear return must be larger than 1.") + ); + } + + let result = number.to_u16().ok_or_else(|| { + TemporalError::range().with_message("monthsInYear exceeded valid range.") + })?; + + Ok(result) + } + + fn in_leap_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let date_like = date_like_to_object(date_like, context)?; + + let method = self + .calendar + .get(PropertyKey::from(utf16!("inLeapYear")), context) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let val = method + .as_callable() + .expect("is method") + .call(&method, &[date_like], context) + .map_err(|err| TemporalError::general(err.to_string()))?; + + let JsValue::Boolean(result) = val else { + return Err( + TemporalError::r#type().with_message("inLeapYear must return a valid boolean.") + ); + }; + + Ok(result) + } + + // TODO: Determine fate of fn fields() + + fn field_descriptors( + &self, + _: boa_temporal::calendar::CalendarFieldsType, + ) -> Vec<(String, bool)> { + Vec::default() + } + + fn field_keys_to_ignore(&self, _: Vec) -> Vec { + Vec::default() + } + + fn resolve_fields( + &self, + _: &mut TemporalFields, + _: boa_temporal::calendar::CalendarFieldsType, + ) -> TemporalResult<()> { + Ok(()) + } + + fn identifier(&self, context: &mut dyn Any) -> TemporalResult { + let context = context + .downcast_mut::() + .expect("Context was not provided for a CustomCalendar."); + + let identifier = self + .calendar + .__get__( + &PropertyKey::from(utf16!("id")), + JsValue::undefined(), + context, + ) + .expect("method must exist on a object that implements the CalendarProtocol."); + + let JsValue::String(s) = identifier else { + return Err(TemporalError::range().with_message("Identifier was not a string")); + }; + + Ok(s.to_std_string_escaped()) + } +} + +/// Utility function for converting `Temporal`'s `CalendarDateLike` to it's `Boa` specific `JsObject`. +pub(crate) fn date_like_to_object( + date_like: &CalendarDateLike, + context: &mut Context, +) -> TemporalResult { + match date_like { + CalendarDateLike::Date(d) => plain_date::create_temporal_date(d.clone(), None, context) + .map_err(|e| TemporalError::general(e.to_string())) + .map(Into::into), + CalendarDateLike::DateTime(_dt) => { + todo!() + } + CalendarDateLike::MonthDay(md) => { + plain_month_day::create_temporal_month_day(md.clone(), None, context) + .map_err(|e| TemporalError::general(e.to_string())) + } + CalendarDateLike::YearMonth(ym) => { + plain_year_month::create_temporal_year_month(ym.clone(), None, context) + .map_err(|e| TemporalError::general(e.to_string())) + } + } +} diff --git a/boa_engine/src/builtins/temporal/calendar/tests.rs b/boa_engine/src/builtins/temporal/calendar/tests.rs index de24f03efc..7618d924f2 100644 --- a/boa_engine/src/builtins/temporal/calendar/tests.rs +++ b/boa_engine/src/builtins/temporal/calendar/tests.rs @@ -20,3 +20,42 @@ fn calendar_methods() { TestAction::assert_eq("iso.daysInWeek('2021-11-20')", 7), ]); } + +#[test] +fn run_custom_calendar() { + run_test_actions([ + TestAction::run( + r#"const custom = { + dateAdd() {}, + dateFromFields() {}, + dateUntil() {}, + day() {}, + dayOfWeek() {}, + dayOfYear() {}, + daysInMonth() { return 14 }, + daysInWeek() {return 6}, + daysInYear() {return 360}, + fields() {}, + id: "custom-calendar", + inLeapYear() {}, + mergeFields() {}, + month() {}, + monthCode() {}, + monthDayFromFields() {}, + monthsInYear() {}, + weekOfYear() {}, + year() {}, + yearMonthFromFields() {}, + yearOfWeek() {}, + }; + + let cal = Temporal.Calendar.from(custom); + let date = "1972-05-01"; + "#, + ), + TestAction::assert_eq("cal.id", js_string!("custom-calendar")), + TestAction::assert_eq("cal.daysInMonth(date)", 14), + TestAction::assert_eq("cal.daysInWeek(date)", 6), + TestAction::assert_eq("cal.daysInYear(date)", 360), + ]); +} diff --git a/boa_engine/src/builtins/temporal/calendar/utils.rs b/boa_engine/src/builtins/temporal/calendar/utils.rs deleted file mode 100644 index 2e7b6f0eba..0000000000 --- a/boa_engine/src/builtins/temporal/calendar/utils.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! 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 deleted file mode 100644 index 201a90386d..0000000000 --- a/boa_engine/src/builtins/temporal/date_equations.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! 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 index cf67023e51..f0d4069515 100644 --- a/boa_engine/src/builtins/temporal/duration/mod.rs +++ b/boa_engine/src/builtins/temporal/duration/mod.rs @@ -15,22 +15,17 @@ use crate::{ Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, }; use boa_profiler::Profiler; +use boa_temporal::{duration::Duration as InnerDuration, options::TemporalUnit}; use super::{ - options::{ - get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup, - }, - plain_date::{self, PlainDate}, + options::{get_temporal_rounding_increment, get_temporal_unit, TemporalUnitGroup}, + plain_date::PlainDate, to_integer_if_integral, DateTimeValues, PlainDateTime, }; -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 @@ -38,7 +33,13 @@ pub(crate) use record::{DateDuration, DurationRecord, TimeDuration}; /// [spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances #[derive(Debug, Clone, Copy)] pub struct Duration { - pub(crate) inner: DurationRecord, + pub(crate) inner: InnerDuration, +} + +impl Duration { + pub(crate) fn new(inner: InnerDuration) -> Self { + Self { inner } + } } impl BuiltInObject for Duration { @@ -241,17 +242,18 @@ impl BuiltInConstructor for Duration { .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, - ), - ); + let record = InnerDuration::new( + years, + months, + weeks, + days, + 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) @@ -270,17 +272,19 @@ impl Duration { JsNativeError::typ().with_message("the this object must be a Duration object.") })?; + let inner = &duration.inner; + 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::Year => Ok(JsValue::Rational(inner.date().years())), + DateTimeValues::Month => Ok(JsValue::Rational(inner.date().months())), + DateTimeValues::Week => Ok(JsValue::Rational(inner.date().weeks())), + DateTimeValues::Day => Ok(JsValue::Rational(inner.date().days())), + DateTimeValues::Hour => Ok(JsValue::Rational(inner.time().hours())), + DateTimeValues::Minute => Ok(JsValue::Rational(inner.time().minutes())), + DateTimeValues::Second => Ok(JsValue::Rational(inner.time().seconds())), + DateTimeValues::Millisecond => Ok(JsValue::Rational(inner.time().milliseconds())), + DateTimeValues::Microsecond => Ok(JsValue::Rational(inner.time().microseconds())), + DateTimeValues::Nanosecond => Ok(JsValue::Rational(inner.time().nanoseconds())), DateTimeValues::MonthCode => unreachable!( "Any other DateTimeValue fields on Duration would be an implementation error." ), @@ -399,122 +403,123 @@ impl Duration { // 3. Let temporalDurationLike be ? ToTemporalPartialDurationRecord(temporalDurationLike). let temporal_duration_like = - DurationRecord::from_partial_js_object(args.get_or_undefined(0), context)?; + to_temporal_partial_duration(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() + let years = if temporal_duration_like.date().years().is_nan() { + duration.inner.date().years() } else { - temporal_duration_like.years() + temporal_duration_like.date().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() + let months = if temporal_duration_like.date().months().is_nan() { + duration.inner.date().months() } else { - temporal_duration_like.months() + temporal_duration_like.date().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() + let weeks = if temporal_duration_like.date().weeks().is_nan() { + duration.inner.date().weeks() } else { - temporal_duration_like.weeks() + temporal_duration_like.date().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() + let days = if temporal_duration_like.date().days().is_nan() { + duration.inner.date().days() } else { - temporal_duration_like.days() + temporal_duration_like.date().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() + let hours = if temporal_duration_like.time().hours().is_nan() { + duration.inner.time().hours() } else { - temporal_duration_like.hours() + temporal_duration_like.time().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() + let minutes = if temporal_duration_like.time().minutes().is_nan() { + duration.inner.time().minutes() } else { - temporal_duration_like.minutes() + temporal_duration_like.time().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() + let seconds = if temporal_duration_like.time().seconds().is_nan() { + duration.inner.time().seconds() } else { - temporal_duration_like.seconds() + temporal_duration_like.time().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() + let milliseconds = if temporal_duration_like.time().milliseconds().is_nan() { + duration.inner.time().milliseconds() } else { - temporal_duration_like.milliseconds() + temporal_duration_like.time().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() + let microseconds = if temporal_duration_like.time().microseconds().is_nan() { + duration.inner.time().microseconds() } else { - temporal_duration_like.microseconds() + temporal_duration_like.time().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() + let nanoseconds = if temporal_duration_like.time().nanoseconds().is_nan() { + duration.inner.time().nanoseconds() } else { - temporal_duration_like.nanoseconds() + temporal_duration_like.time().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, - ), - ); + let new_duration = InnerDuration::new( + years, + months, + weeks, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + )?; - new_duration.as_object(context).map(Into::into) + create_temporal_duration(new_duration, None, context).map(Into::into) } /// 7.3.16 `Temporal.Duration.prototype.negated ( )` @@ -544,7 +549,7 @@ impl Duration { let abs = duration.inner.abs(); - abs.as_object(context).map(Into::into) + create_temporal_duration(abs, None, context).map(Into::into) } /// 7.3.18 `Temporal.Duration.prototype.add ( other [ , options ] )` @@ -630,7 +635,7 @@ impl Duration { let rounding_increment = get_temporal_rounding_increment(&round_to, context)?; // 14. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand"). - let rounding_mode = get_option(&round_to, utf16!("roundingMode"), context)? + let _rounding_mode = get_option(&round_to, utf16!("roundingMode"), context)? .unwrap_or(RoundingMode::HalfExpand); // 15. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit", datetime, undefined). @@ -697,19 +702,20 @@ impl Duration { // 25. Let hoursToDaysConversionMayOccur be false. // 26. If duration.[[Days]] ≠ 0 and zonedRelativeTo is not undefined, set hoursToDaysConversionMayOccur to true. // 27. Else if abs(duration.[[Hours]]) ≥ 24, set hoursToDaysConversionMayOccur to true. - let conversion_may_occur = if duration.inner.days() != 0.0 && zoned_relative_to.is_some() { - true - } else { - 24f64 <= duration.inner.hours().abs() - }; + let conversion_may_occur = + if duration.inner.date().days() != 0.0 && zoned_relative_to.is_some() { + true + } else { + 24f64 <= duration.inner.time().hours().abs() + }; // 28. If smallestUnit is "nanosecond" and roundingIncrement = 1, let roundingGranularityIsNoop be true; else let roundingGranularityIsNoop be false. let is_noop = smallest_unit == TemporalUnit::Nanosecond && rounding_increment == 1; // 29. If duration.[[Years]] = 0 and duration.[[Months]] = 0 and duration.[[Weeks]] = 0, let calendarUnitsPresent be false; else let calendarUnitsPresent be true. - let calendar_units_present = !(duration.inner.years() == 0f64 - || duration.inner.months() == 0f64 - || duration.inner.weeks() == 0f64); + let calendar_units_present = !(duration.inner.date().years() == 0f64 + || duration.inner.date().months() == 0f64 + || duration.inner.date().weeks() == 0f64); // 30. If roundingGranularityIsNoop is true, and largestUnit is existingLargestUnit, // and calendarUnitsPresent is false, and hoursToDaysConversionMayOccur is false, @@ -740,10 +746,10 @@ impl Duration { || largest_unit == TemporalUnit::Week || largest_unit == TemporalUnit::Day || calendar_units_present - || duration.inner.days() != 0f64; + || duration.inner.date().days() != 0f64; // 33. If zonedRelativeTo is not undefined and plainDateTimeOrRelativeToWillBeUsed is true, then - let (plain_relative_to, precalc_pdt) = if zoned_relative_to.is_some() + let (_plain_relative_to, _precalc_pdt) = if zoned_relative_to.is_some() && pdt_or_rel_will_be_used { // TODO(TimeZone): Implement GetPlainDateTimeFor @@ -763,29 +769,10 @@ impl Duration { }; // 34. Let unbalanceResult be ? UnbalanceDateDurationRelative(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], largestUnit, plainRelativeTo). - let unbalance_result = duration.inner.unbalance_duration_relative( - largest_unit, - plain_relative_to.as_ref(), - context, - )?; - // 35. Let roundRecord 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, plainRelativeTo, zonedRelativeTo, precalculatedPlainDateTime). - let (_round_result, _) = duration.inner.round_duration( - unbalance_result, - rounding_increment.into(), - smallest_unit, - rounding_mode, - ( - plain_relative_to.as_ref(), - zoned_relative_to.as_ref(), - precalc_pdt.as_ref(), - ), - context, - )?; - // 36. Let roundResult be roundRecord.[[DurationRecord]]. // 37. If zonedRelativeTo is not undefined, then // 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, zonedRelativeTo, precalculatedPlainDateTime). @@ -887,7 +874,7 @@ impl Duration { // -- Duration Abstract Operations -- /// 7.5.8 `ToTemporalDuration ( item )` -pub(crate) fn to_temporal_duration(item: &JsValue) -> JsResult { +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 @@ -911,7 +898,7 @@ pub(crate) fn to_temporal_duration(item: &JsValue) -> JsResult { /// 7.5.9 `ToTemporalDurationRecord ( temporalDurationLike )` pub(crate) fn to_temporal_duration_record( _temporal_duration_like: &JsValue, -) -> JsResult { +) -> JsResult { Err(JsNativeError::range() .with_message("Duration Parsing is not yet complete.") .into()) @@ -919,16 +906,11 @@ pub(crate) fn to_temporal_duration_record( /// 7.5.14 `CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , newTarget ] )` pub(crate) fn create_temporal_duration( - record: DurationRecord, + inner: InnerDuration, 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 { @@ -958,38 +940,105 @@ pub(crate) fn create_temporal_duration( // 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 })); + let obj = JsObject::from_proto_and_data(prototype, ObjectData::duration(Duration::new(inner))); // 14. Return object. Ok(obj) } -/// 7.5.23 `DaysUntil ( earlier, later )` -pub(crate) fn days_until(earlier: &PlainDate, later: &PlainDate) -> i32 { - // 1. Let epochDays1 be ISODateToEpochDays(earlier.[[ISOYear]], earlier.[[ISOMonth]] - 1, earlier.[[ISODay]]). - let epoch_days_one = earlier.inner.as_epoch_days(); +/// Equivalent to 7.5.13 `ToTemporalPartialDurationRecord ( temporalDurationLike )` +pub(crate) fn to_temporal_partial_duration( + 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 epochDays2 be ISODateToEpochDays(later.[[ISOYear]], later.[[ISOMonth]] - 1, later.[[ISODay]]). - let epoch_days_two = later.inner.as_epoch_days(); + // 2. Let result be a new partial Duration Record with each field set to undefined. + let mut result = InnerDuration::partial(); - // 3. Return epochDays2 - epochDays1. - epoch_days_two - epoch_days_one -} + // 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)?)); + } -/// Abstract Operation 7.5.24 `MoveRelativeDate ( calendar, relativeTo, duration, dateAdd )` -fn move_relative_date( - calendar: &JsValue, - relative_to: &PlainDate, - duration: &DurationRecord, - context: &mut Context, -) -> JsResult<(PlainDate, f64)> { - let new_date = plain_date::add_date( - calendar, - relative_to, - duration, - &JsValue::undefined(), - context, - )?; - let days = days_until(relative_to, &new_date); - Ok((new_date, f64::from(days))) + // 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) } diff --git a/boa_engine/src/builtins/temporal/error.rs b/boa_engine/src/builtins/temporal/error.rs new file mode 100644 index 0000000000..3b86b21a17 --- /dev/null +++ b/boa_engine/src/builtins/temporal/error.rs @@ -0,0 +1,20 @@ +use boa_temporal::error::{ErrorKind, TemporalError}; + +use crate::{JsError, JsNativeError}; + +impl From for JsNativeError { + fn from(value: TemporalError) -> Self { + match value.kind() { + ErrorKind::Range => JsNativeError::range().with_message(value.message()), + ErrorKind::Type => JsNativeError::typ().with_message(value.message()), + ErrorKind::Generic => JsNativeError::error().with_message(value.message()), + } + } +} + +impl From for JsError { + fn from(value: TemporalError) -> Self { + let native: JsNativeError = value.into(); + native.into() + } +} diff --git a/boa_engine/src/builtins/temporal/fields.rs b/boa_engine/src/builtins/temporal/fields.rs index e3fc21e76d..c842816c07 100644 --- a/boa_engine/src/builtins/temporal/fields.rs +++ b/boa_engine/src/builtins/temporal/fields.rs @@ -1,587 +1,174 @@ //! A Rust native implementation of the `fields` object used in `Temporal`. +use std::str::FromStr; + 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" | `None` | 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: &JsString, - value: &JsValue, - context: &mut Context, - ) -> JsResult<()> { - match field.to_std_string_escaped().as_str() { - "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()); +use boa_temporal::fields::{FieldConversion, FieldValue, TemporalFields}; + +use super::{to_integer_with_truncation, to_positive_integer_with_trunc}; + +// TODO: Move extended and required fields into the temporal library? +/// `PrepareTemporalFeilds` +pub(crate) fn prepare_temporal_fields( + fields: &JsObject, + field_names: &mut Vec, + required_fields: &mut Vec, + 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 = TemporalFields::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(JsString::from(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(JsString::from(field_name)); + } } - - 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); + // 5. Let sortedFieldNames be SortStringListByCodeUnit(fieldNames). + // 6. Let previousProperty be undefined. + let mut dups_map = FxHashSet::default(); - 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.") + // 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.to_std_string_escaped().as_str() == "constructor" + || field.to_std_string_escaped().as_str() == "__proto__" + { + // i. Throw a RangeError exception. + return Err(JsNativeError::range() + .with_message("constructor or proto is out of field range.") .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.to_std_string_escaped().as_str() == "constructor" - || field.to_std_string_escaped().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(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). + 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(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. + + // TODO: Conversion from TemporalError -> JsError + let conversion = FieldConversion::from_str(field.to_std_string_escaped().as_str()) + .map_err(|_| JsNativeError::range().with_message("wrong field value"))?; + // b. If Conversion is ToIntegerWithTruncation, then + let converted_value = match conversion { + FieldConversion::ToIntegerWithTruncation => { + // i. Set value to ? ToIntegerWithTruncation(value). + let v = to_integer_with_truncation(&value, context)?; + // ii. Set value to 𝔽(value). + FieldValue::Integer(v) + } // c. Else if Conversion is ToPositiveIntegerWithTruncation, then - // i. Set value to ? ToPositiveIntegerWithTruncation(value). - // ii. Set value to 𝔽(value). + FieldConversion::ToPositiveIntegerWithTruncation => { + // i. Set value to ? ToPositiveIntegerWithTruncation(value). + let v = to_positive_integer_with_trunc(&value, context)?; + // ii. Set value to 𝔽(value). + FieldValue::Integer(v) + } // 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()); + FieldConversion::ToPrimativeAndRequireString => { + // 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). + let primitive = value.to_primitive(context, PreferredType::String)?; + // iv. If value is not a String, throw a TypeError exception. + FieldValue::String(primitive.to_string(context)?.to_std_string_escaped()) } - - // 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()); + FieldConversion::None => { + unreachable!("todo need to implement conversion handling for tz.") + } + }; + + // 3. Perform ! CreateDataPropertyOrThrow(result, property, value). + result + .set_field_value(&field.to_std_string_escaped(), &converted_value) + .expect("FieldConversion enforces the appropriate type"); + // 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: flag that the value is active and the default should be used. + // 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). + result.require_field(&field.to_std_string_escaped()); } - // d. Set previousProperty to property. - } - - // 8. If requiredFields is partial and any is false, then - if partial && !any { - // a. Throw a TypeError exception. + // 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("requiredFields cannot be partial when any is false") + .with_message("Cannot have a duplicate field") .into()); } - - // 9. Return result. - Ok(result) + // d. Set previousProperty to property. } - /// 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) + // 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()); } - // 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(()) - } + // 9. Return result. + Ok(result) +} - 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); - } - _ => {} - } - } +impl JsObject { + pub(crate) fn from_temporal_fields( + fields: &TemporalFields, + context: &mut Context, + ) -> JsResult { + let obj = JsObject::with_null_proto(); - /// 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(()); - } + for (key, value) in fields.active_kvs() { + let js_value = match value { + FieldValue::Undefined => JsValue::undefined(), + FieldValue::Integer(x) => JsValue::Integer(x), + FieldValue::String(s) => JsValue::String(s.into()), + }; - return Err(JsNativeError::range() - .with_message("month and MonthCode values cannot both be undefined.") - .into()); + obj.create_data_property_or_throw(JsString::from(key), js_value, context)?; } - 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()), + Ok(obj) } } diff --git a/boa_engine/src/builtins/temporal/instant/mod.rs b/boa_engine/src/builtins/temporal/instant/mod.rs index 4d3e8b6e65..498496dd26 100644 --- a/boa_engine/src/builtins/temporal/instant/mod.rs +++ b/boa_engine/src/builtins/temporal/instant/mod.rs @@ -4,11 +4,8 @@ use crate::{ builtins::{ options::{get_option, get_options_object, RoundingMode}, - temporal::{ - duration::{DateDuration, DurationRecord, TimeDuration}, - options::{ - get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup, - }, + temporal::options::{ + get_temporal_rounding_increment, get_temporal_unit, TemporalUnitGroup, }, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, }, @@ -21,8 +18,9 @@ use crate::{ Context, JsArgs, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, }; use boa_profiler::Profiler; +use boa_temporal::{duration::Duration, options::TemporalUnit}; -use super::{duration, ns_max_instant, ns_min_instant, MIS_PER_DAY, MS_PER_DAY, NS_PER_DAY}; +use super::{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; @@ -567,70 +565,41 @@ fn add_instant( fn diff_instant( ns1: &JsBigInt, ns2: &JsBigInt, - rounding_increment: f64, - smallest_unit: TemporalUnit, - largest_unit: TemporalUnit, - rounding_mode: RoundingMode, - context: &mut Context, -) -> JsResult { + _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)); + 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)); + 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)); + let _milliseconds = JsBigInt::rem(&truncated_milli, &JsBigInt::from(1000)); // 5. Let seconds be truncate(difference / 10^9). - let seconds = (&difference.to_f64() / 1_000_000_000_f64).trunc(); - - // Create TimeDuration - let mut time_duration = DurationRecord::from_day_and_time( - 0f64, - TimeDuration::new( - 0f64, - 0f64, - seconds, - milliseconds.to_f64(), - microseconds.to_f64(), - nanoseconds.to_f64(), - ), - ); + let _seconds = (&difference.to_f64() / 1_000_000_000_f64).trunc(); + // TODO: Update to new Temporal library // 6. If smallestUnit is "nanosecond" and roundingIncrement is 1, then - if smallest_unit == TemporalUnit::Nanosecond && (rounding_increment - 1f64).abs() < f64::EPSILON - { - // a. Return ! BalanceTimeDuration(0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, largestUnit). - time_duration.balance_time_duration(largest_unit, None)?; - return Ok(time_duration); - } - + // a. Return ! BalanceTimeDuration(0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, largestUnit). // 7. Let roundResult be ! RoundDuration(0, 0, 0, 0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, roundingIncrement, smallestUnit, largestUnit, roundingMode). - - let (mut round_result, _total) = time_duration.round_duration( - DateDuration::default(), - rounding_increment, - smallest_unit, - rounding_mode, - (None, None, None), - context, - )?; - // 8. Assert: roundResult.[[Days]] is 0. - assert_eq!(round_result.days() as i32, 0); - // 9. Return ! BalanceTimeDuration(0, roundResult.[[Hours]], roundResult.[[Minutes]], // roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], // roundResult.[[Nanoseconds]], largestUnit). - round_result.balance_time_duration(largest_unit, None)?; - Ok(round_result) + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) } /// 8.5.8 `RoundTemporalInstant ( ns, increment, unit, roundingMode )` @@ -691,7 +660,7 @@ fn diff_temporal_instant( 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 }; + 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). @@ -710,7 +679,7 @@ fn diff_temporal_instant( )?; // 5. Let result be DifferenceInstant(instant.[[Nanoseconds]], other.[[Nanoseconds]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[LargestUnit]], settings.[[RoundingMode]]). - let result = diff_instant( + let _result = diff_instant( &instant.nanoseconds, &other.nanoseconds, settings.3, @@ -720,23 +689,9 @@ fn diff_temporal_instant( context, )?; + // TODO: diff_instant will error so this shouldn't run. + unimplemented!(); // 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 )` @@ -752,25 +707,25 @@ fn add_or_subtract_duration_from_instant( // 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 { + if duration.date().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 { + if duration.date().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 { + if duration.date().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 { + if duration.date().years() != 0_f64 { return Err(JsNativeError::range() .with_message("DurationYears cannot be 0") .into()); @@ -780,12 +735,12 @@ fn add_or_subtract_duration_from_instant( // 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, + sign * duration.time().hours() as i32, + sign * duration.time().minutes() as i32, + sign * duration.time().seconds() as i32, + sign * duration.time().milliseconds() as i32, + sign * duration.time().microseconds() as i32, + sign * duration.time().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 index ecc28d6019..a4955f3540 100644 --- a/boa_engine/src/builtins/temporal/mod.rs +++ b/boa_engine/src/builtins/temporal/mod.rs @@ -5,8 +5,8 @@ //! [spec]: https://tc39.es/proposal-temporal/ mod calendar; -mod date_equations; mod duration; +mod error; mod fields; mod instant; mod now; @@ -22,11 +22,7 @@ mod zoned_date_time; #[cfg(test)] mod tests; -pub(crate) use fields::TemporalFields; - -use self::options::{ - get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup, -}; +use self::options::{get_temporal_rounding_increment, get_temporal_unit, 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::*, @@ -47,6 +43,7 @@ use crate::{ Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, }; use boa_profiler::Profiler; +use boa_temporal::options::TemporalUnit; // Relavant numeric constants /// Nanoseconds per day constant: 8.64e+13 @@ -192,7 +189,7 @@ fn to_zero_padded_decimal_string(n: u64, min_length: usize) -> String { /// Abstract Operation 13.1 [`IteratorToListOfType`][proposal] /// /// [proposal]: https://tc39.es/proposal-temporal/#sec-iteratortolistoftype -pub(crate) fn iterator_to_list_of_types( +pub(crate) fn _iterator_to_list_of_types( iterator: &mut IteratorRecord, element_types: &[Type], context: &mut Context, @@ -229,7 +226,7 @@ pub(crate) fn iterator_to_list_of_types( // Note: implemented on IsoDateRecord. // Abstract Operation 13.3 `EpochDaysToEpochMs` -pub(crate) fn epoch_days_to_epoch_ms(day: i32, time: i32) -> f64 { +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)) } @@ -370,7 +367,7 @@ fn apply_unsigned_rounding_mode( } /// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )` -pub(crate) fn round_number_to_increment( +pub(crate) fn _round_number_to_increment( x: f64, increment: f64, rounding_mode: RoundingMode, diff --git a/boa_engine/src/builtins/temporal/options.rs b/boa_engine/src/builtins/temporal/options.rs index 92f1cf45f2..b881cdbbf7 100644 --- a/boa_engine/src/builtins/temporal/options.rs +++ b/boa_engine/src/builtins/temporal/options.rs @@ -12,7 +12,9 @@ use crate::{ builtins::options::{get_option, ParsableOptionType}, js_string, Context, JsNativeError, JsObject, JsResult, }; -use std::{fmt, str::FromStr}; +use boa_temporal::options::{ + ArithmeticOverflow, DurationOverflow, InstantDisambiguation, OffsetDisambiguation, TemporalUnit, +}; // TODO: Expand docs on the below options. @@ -123,262 +125,9 @@ fn date_units() -> impl Iterator { 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 deleted file mode 100644 index 52eccc4fb1..0000000000 --- a/boa_engine/src/builtins/temporal/plain_date/iso.rs +++ /dev/null @@ -1,236 +0,0 @@ -//! An `IsoDateRecord` that represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots. - -use crate::{ - builtins::temporal::{self, options::ArithmeticOverflow, DateDuration, 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: ArithmeticOverflow, - ) -> JsResult { - match overflow { - ArithmeticOverflow::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)) - } - ArithmeticOverflow::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) - } - } - } - - /// 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: ArithmeticOverflow, - ) -> 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: ArithmeticOverflow, - ) -> 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, - date_duration: DateDuration, - overflow: ArithmeticOverflow, - ) -> 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 + date_duration.years() as i32, - self.month + date_duration.months() as i32, - 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 = date_duration.days() as i32 + (date_duration.weeks() as i32 * 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 index d43245cec4..517e02f162 100644 --- a/boa_engine/src/builtins/temporal/plain_date/mod.rs +++ b/boa_engine/src/builtins/temporal/plain_date/mod.rs @@ -1,10 +1,11 @@ //! Boa's implementation of the ECMAScript `Temporal.PlainDate` builtin object. #![allow(dead_code, unused_variables)] +use std::str::FromStr; + use crate::{ builtins::{ options::{get_option, get_options_object}, - temporal::options::TemporalUnit, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, @@ -17,27 +18,24 @@ use crate::{ }; use boa_parser::temporal::{IsoCursor, TemporalDateTimeString}; use boa_profiler::Profiler; - -use super::{ - calendar, duration::DurationRecord, options::ArithmeticOverflow, - plain_date::iso::IsoDateRecord, plain_date_time, DateDuration, TimeDuration, +use boa_temporal::{ + calendar::{AvailableCalendars, CalendarSlot}, + date::Date as InnerDate, + datetime::DateTime, + options::ArithmeticOverflow, }; -pub(crate) mod iso; +use super::calendar; /// 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. + pub(crate) inner: InnerDate, } impl PlainDate { - pub(crate) fn new(record: IsoDateRecord, calendar: JsValue) -> Self { - Self { - inner: record, - calendar, - } + pub(crate) fn new(inner: InnerDate) -> Self { + Self { inner } } } @@ -219,12 +217,18 @@ impl BuiltInConstructor for PlainDate { 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 calendar_slot = + calendar::to_temporal_calendar_slot_value(args.get_or_undefined(3), context)?; - let iso = IsoDateRecord::new(iso_year, iso_month, iso_day); + let date = InnerDate::new( + iso_year, + iso_month, + iso_day, + calendar_slot, + ArithmeticOverflow::Reject, + )?; - Ok(create_temporal_date(iso, calendar_like.clone(), Some(new_target), context)?.into()) + Ok(create_temporal_date(date, Some(new_target), context)?.into()) } } @@ -390,7 +394,7 @@ impl PlainDate { impl PlainDate { /// Utitily function for translating a `Temporal.PlainDate` into a `JsObject`. pub(crate) fn as_object(&self, context: &mut Context) -> JsResult { - create_temporal_date(self.inner, self.calendar.clone(), None, context) + create_temporal_date(self.inner.clone(), None, context) } } @@ -399,24 +403,19 @@ impl PlainDate { /// 3.5.3 `CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] )` pub(crate) fn create_temporal_date( - iso_date: IsoDateRecord, - calendar: JsValue, + inner: InnerDate, 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() { + if !inner.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() { + if !DateTime::validate(&inner) { return Err(JsNativeError::range() .with_message("Date is not within ISO date time limits.") .into()); @@ -443,10 +442,8 @@ pub(crate) fn create_temporal_date( // 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::new(iso_date, calendar)), - ); + let obj = + JsObject::from_proto_and_data(prototype, ObjectData::plain_date(PlainDate::new(inner))); // 9. Return object. Ok(obj) @@ -474,10 +471,7 @@ pub(crate) fn to_temporal_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(), - }); + return Ok(PlainDate::new(date.inner.clone())); // b. If item has an [[InitializedTemporalZonedDateTime]] internal slot, then } else if object.is_zoned_date_time() { return Err(JsNativeError::range() @@ -499,16 +493,11 @@ pub(crate) fn to_temporal_date( .as_plain_date_time() .expect("obj must be a PlainDateTime"); - let iso = date_time.inner.iso_date(); - let calendar = date_time.calendar.clone(); - + let date = InnerDate::from_datetime(date_time.inner()); drop(obj); // ii. Return ! CreateTemporalDate(item.[[ISOYear]], item.[[ISOMonth]], item.[[ISODay]], item.[[Calendar]]). - return Ok(PlainDate { - inner: iso, - calendar, - }); + return Ok(PlainDate::new(date)); } // d. Let calendar be ? GetTemporalCalendarSlotValueWithISODefault(item). @@ -521,140 +510,39 @@ pub(crate) fn to_temporal_date( } // 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() + let JsValue::String(date_like_string) = item else { + return Err(JsNativeError::typ() .with_message("ToTemporalDate item must be an object or string.") - .into()), - } -} - -// 3.5.5. DifferenceIsoDate -// Implemented on IsoDateRecord. - -/// 3.5.6 `DifferenceDate ( calendar, one, two, options )` -pub(crate) fn difference_date( - calendar: &JsValue, - one: &PlainDate, - two: &PlainDate, - largest_unit: TemporalUnit, - context: &mut Context, -) -> JsResult { - // 1. Assert: one.[[Calendar]] and two.[[Calendar]] have been determined to be equivalent as with CalendarEquals. - // 2. Assert: options is an ordinary Object. - // 3. Assert: options.[[Prototype]] is null. - // 4. Assert: options has a "largestUnit" data property. - // 5. If one.[[ISOYear]] = two.[[ISOYear]] and one.[[ISOMonth]] = two.[[ISOMonth]] and one.[[ISODay]] = two.[[ISODay]], then - if one.inner.year() == two.inner.year() - && one.inner.month() == two.inner.month() - && one.inner.day() == two.inner.day() - { - // a. Return ! CreateTemporalDuration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0). - return Ok(DurationRecord::default()); - } - // 6. If ! Get(options, "largestUnit") is "day", then - if largest_unit == TemporalUnit::Day { - // a. Let days be DaysUntil(one, two). - let days = super::duration::days_until(one, two); - // b. Return ! CreateTemporalDuration(0, 0, 0, days, 0, 0, 0, 0, 0, 0). - return Ok(DurationRecord::new( - DateDuration::new(0.0, 0.0, 0.0, f64::from(days)), - TimeDuration::default(), - )); - } - - // Create the options object prior to sending it to the calendars. - let options_obj = JsObject::with_null_proto(); - - options_obj.create_data_property_or_throw( - utf16!("largestUnit"), - JsString::from(largest_unit.to_string()), - context, - )?; - - // 7. Return ? CalendarDateUntil(calendar, one, two, options). - calendar::calendar_date_until(calendar, one, two, &options_obj.into(), context) -} - -// 3.5.7 RegulateIsoDate -// Implemented on IsoDateRecord. - -// 3.5.8 IsValidIsoDate -// Implemented on IsoDateRecord. - -// 3.5.9 BalanceIsoDate -// Implemented on IsoDateRecord. - -// 3.5.12 AddISODate ( year, month, day, years, months, weeks, days, overflow ) -// Implemented on IsoDateRecord + .into()); + }; -/// 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ]] )` -pub(crate) fn add_date( - calendar: &JsValue, - plain_date: &PlainDate, - duration: &DurationRecord, - options: &JsValue, - context: &mut Context, -) -> JsResult { - // 1. If options is not present, set options to undefined. - // 2. If duration.[[Years]] ≠ 0, or duration.[[Months]] ≠ 0, or duration.[[Weeks]] ≠ 0, then - if duration.years() != 0.0 || duration.months() != 0.0 || duration.weeks() != 0.0 { - // a. If dateAdd is not present, then - // i. Set dateAdd to unused. - // ii. If calendar is an Object, set dateAdd to ? GetMethod(calendar, "dateAdd"). - // b. Return ? CalendarDateAdd(calendar, plainDate, duration, options, dateAdd). - return calendar::calendar_date_add(calendar, plain_date, duration, options, context); - } - - // 3. Let overflow be ? ToTemporalOverflow(options). - let options_obj = get_options_object(options)?; - let overflow = get_option(&options_obj, utf16!("overflow"), context)? - .unwrap_or(ArithmeticOverflow::Constrain); - - let mut intermediate = *duration; - // 4. Let days be ? BalanceTimeDuration(duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], "day").[[Days]]. - intermediate.balance_time_duration(TemporalUnit::Day, None)?; - - // 5. Let result be ? AddISODate(plainDate.[[ISOYear]], plainDate.[[ISOMonth]], plainDate.[[ISODay]], 0, 0, 0, days, overflow). - let result = plain_date - .inner - .add_iso_date(intermediate.date(), overflow)?; - - // 6. Return ! CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar). - Ok(PlainDate::new(result, plain_date.calendar.clone())) + // 6. Let result be ? ParseTemporalDateString(item). + let result = TemporalDateTimeString::parse( + false, + &mut IsoCursor::new(&date_like_string.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.unwrap_or("iso8601".to_string()); + + // 10. If IsBuiltinCalendar(calendar) is false, throw a RangeError exception. + let _ = AvailableCalendars::from_str(identifier.to_ascii_lowercase().as_str())?; + + // 11. Set calendar to the ASCII-lowercase of calendar. + let calendar = CalendarSlot::Identifier(identifier.to_ascii_lowercase()); + + // 12. Perform ? ToTemporalOverflow(options). + let _ = get_option::(&options_obj, utf16!("overflow"), context)?; + + // 13. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar). + Ok(PlainDate::new(InnerDate::new( + result.date.year, + result.date.month, + result.date.day, + calendar, + ArithmeticOverflow::Reject, + )?)) } diff --git a/boa_engine/src/builtins/temporal/plain_date_time/iso.rs b/boa_engine/src/builtins/temporal/plain_date_time/iso.rs deleted file mode 100644 index 50b2805a37..0000000000 --- a/boa_engine/src/builtins/temporal/plain_date_time/iso.rs +++ /dev/null @@ -1,100 +0,0 @@ -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 index 83aa5b4c80..5d658e24f6 100644 --- a/boa_engine/src/builtins/temporal/plain_date_time/mod.rs +++ b/boa_engine/src/builtins/temporal/plain_date_time/mod.rs @@ -11,15 +11,22 @@ use crate::{ }; use boa_profiler::Profiler; -use self::iso::IsoDateTimeRecord; - -pub(crate) mod iso; +use boa_temporal::datetime::DateTime as InnerDateTime; /// The `Temporal.PlainDateTime` object. #[derive(Debug, Clone)] pub struct PlainDateTime { - pub(crate) inner: IsoDateTimeRecord, - pub(crate) calendar: JsValue, + pub(crate) inner: InnerDateTime, +} + +impl PlainDateTime { + fn new(inner: InnerDateTime) -> Self { + Self { inner } + } + + pub(crate) fn inner(&self) -> &InnerDateTime { + &self.inner + } } impl BuiltInObject for PlainDateTime { diff --git a/boa_engine/src/builtins/temporal/plain_month_day/mod.rs b/boa_engine/src/builtins/temporal/plain_month_day/mod.rs index 4571914311..8acd997b0e 100644 --- a/boa_engine/src/builtins/temporal/plain_month_day/mod.rs +++ b/boa_engine/src/builtins/temporal/plain_month_day/mod.rs @@ -11,13 +11,18 @@ use crate::{ }; use boa_profiler::Profiler; -use super::{plain_date::iso::IsoDateRecord, plain_date_time::iso::IsoDateTimeRecord}; +use boa_temporal::{datetime::DateTime, month_day::MonthDay as InnerMonthDay}; /// The `Temporal.PlainMonthDay` object. #[derive(Debug, Clone)] pub struct PlainMonthDay { - pub(crate) inner: IsoDateRecord, - pub(crate) calendar: JsValue, + pub(crate) inner: InnerMonthDay, +} + +impl PlainMonthDay { + fn new(inner: InnerMonthDay) -> Self { + Self { inner } + } } impl BuiltInObject for PlainMonthDay { @@ -62,24 +67,13 @@ impl BuiltInConstructor for PlainMonthDay { // ==== `PlainMonthDay` Abstract Operations ==== pub(crate) fn create_temporal_month_day( - iso: IsoDateRecord, - calendar: JsValue, + inner: InnerMonthDay, 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() { + if DateTime::validate(&inner) { return Err(JsNativeError::range() .with_message("PlainMonthDay is not a valid ISO date time.") .into()); @@ -111,10 +105,7 @@ pub(crate) fn create_temporal_month_day( // 8. Set object.[[ISOYear]] to referenceISOYear. let obj = JsObject::from_proto_and_data( proto, - ObjectData::plain_month_day(PlainMonthDay { - inner: iso, - calendar, - }), + ObjectData::plain_month_day(PlainMonthDay::new(inner)), ); // 9. Return object. diff --git a/boa_engine/src/builtins/temporal/plain_year_month/mod.rs b/boa_engine/src/builtins/temporal/plain_year_month/mod.rs index 885cf1a817..c3c6479caa 100644 --- a/boa_engine/src/builtins/temporal/plain_year_month/mod.rs +++ b/boa_engine/src/builtins/temporal/plain_year_month/mod.rs @@ -12,13 +12,19 @@ use crate::{ }; use boa_profiler::Profiler; -use super::plain_date::iso::IsoDateRecord; +use super::calendar::to_temporal_calendar_slot_value; +use boa_temporal::{options::ArithmeticOverflow, year_month::YearMonth as InnerYearMonth}; /// The `Temporal.PlainYearMonth` object. #[derive(Debug, Clone)] pub struct PlainYearMonth { - pub(crate) inner: IsoDateRecord, - pub(crate) calendar: JsValue, + pub(crate) inner: InnerYearMonth, +} + +impl PlainYearMonth { + pub(crate) fn new(inner: InnerYearMonth) -> Self { + Self { inner } + } } impl BuiltInObject for PlainYearMonth { @@ -142,28 +148,23 @@ impl BuiltInConstructor for PlainYearMonth { // 2. If referenceISODay is undefined, then let ref_day = if day.is_undefined() { // a. Set referenceISODay to 1𝔽. - 1 + None } else { // 6. Let ref be ? ToIntegerWithTruncation(referenceISODay). - super::to_integer_with_truncation(day, context)? + Some(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"). + let calendar = to_temporal_calendar_slot_value(args.get_or_undefined(2), context)?; // 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, - ) + let inner = InnerYearMonth::new(y, m, ref_day, calendar, ArithmeticOverflow::Reject)?; + + create_temporal_year_month(inner, Some(new_target), context) } } @@ -266,24 +267,12 @@ impl PlainYearMonth { // 9.5.5 `CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , newTarget ] )` pub(crate) fn create_temporal_year_month( - year_month_record: IsoDateRecord, - calendar: JsValue, + ym: InnerYearMonth, 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 { @@ -310,13 +299,8 @@ pub(crate) fn create_temporal_year_month( // 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, - }), - ); + let obj = + JsObject::from_proto_and_data(proto, ObjectData::plain_year_month(PlainYearMonth::new(ym))); // 9. Return object. Ok(obj.into()) diff --git a/boa_engine/src/builtins/temporal/tests.rs b/boa_engine/src/builtins/temporal/tests.rs index 5cd24b57f4..58dde9b608 100644 --- a/boa_engine/src/builtins/temporal/tests.rs +++ b/boa_engine/src/builtins/temporal/tests.rs @@ -1,4 +1,3 @@ -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. @@ -34,19 +33,3 @@ fn now_object() { } // 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/zoned_date_time/mod.rs b/boa_engine/src/builtins/temporal/zoned_date_time/mod.rs index 98be2226e0..8988c6e79e 100644 --- a/boa_engine/src/builtins/temporal/zoned_date_time/mod.rs +++ b/boa_engine/src/builtins/temporal/zoned_date_time/mod.rs @@ -8,6 +8,7 @@ use crate::{ Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, }; use boa_profiler::Profiler; +use boa_temporal::duration::Duration as TemporalDuration; /// The `Temporal.ZonedDateTime` object. #[derive(Debug, Clone)] @@ -64,7 +65,7 @@ pub(crate) fn add_zoned_date_time( epoch_nanos: &JsBigInt, time_zone: &JsObject, calendar: &JsObject, - duration: super::duration::DurationRecord, + duration: TemporalDuration, options: Option<&JsObject>, ) -> JsResult { // 1. If options is not present, set options to undefined. diff --git a/boa_temporal/Cargo.toml b/boa_temporal/Cargo.toml new file mode 100644 index 0000000000..2c3a128620 --- /dev/null +++ b/boa_temporal/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "boa_temporal" +keywords = ["javascript", "js", "compiler", "temporal", "calendar", "date", "time"] +categories = ["date", "time", "calendars"] +readme = "./README.md" +description.workspace = true +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +tinystr = "0.7.4" +icu_calendar = { workspace = true, default-features = false } +rustc-hash = { workspace = true, features = ["std"] } +num-bigint = { workspace = true, features = ["serde"] } +bitflags.workspace = true +num-traits.workspace = true + +[lints] +workspace = true diff --git a/boa_temporal/README.md b/boa_temporal/README.md new file mode 100644 index 0000000000..2211542e0f --- /dev/null +++ b/boa_temporal/README.md @@ -0,0 +1,11 @@ +# Temporal in Rust + +Provides a standard API for working with dates and time. + +IMPORTANT NOTE: The Temporal Proposal is still in Stage 3. As such, this crate should be viewed +as highly experimental until the proposal has been completely standardized and released. + +## Goal + +The intended goal of this crate is to provide an engine agnostic +implementation of `ECMAScript`'s Temporal algorithms. diff --git a/boa_temporal/src/calendar.rs b/boa_temporal/src/calendar.rs new file mode 100644 index 0000000000..5d9e112f83 --- /dev/null +++ b/boa_temporal/src/calendar.rs @@ -0,0 +1,579 @@ +//! Temporal calendar traits and implementations. +//! +//! The goal of the calendar module of `boa_temporal` is to provide +//! Temporal compatible calendar implementations. +//! +//! The implementation will only be of calendar's prexisting calendars. This library +//! does not come with a pre-existing `CustomCalendar` (i.e., an object that implements +//! the calendar protocol), but it does aim to provide the necessary tools and API for +//! implementing one. + +use std::{any::Any, str::FromStr}; + +use crate::{ + date::Date, + datetime::DateTime, + duration::Duration, + fields::TemporalFields, + iso::{IsoDate, IsoDateSlots}, + month_day::MonthDay, + options::{ArithmeticOverflow, TemporalUnit}, + year_month::YearMonth, + TemporalError, TemporalResult, +}; + +use tinystr::TinyAsciiStr; + +use self::iso::IsoCalendar; + +pub mod iso; + +/// The ECMAScript defined protocol methods +pub const CALENDAR_PROTOCOL_METHODS: [&str; 21] = [ + "dateAdd", + "dateFromFields", + "dateUntil", + "day", + "dayOfWeek", + "dayOfYear", + "daysInMonth", + "daysInWeek", + "daysInYear", + "fields", + "id", + "inLeapYear", + "mergeFields", + "month", + "monthCode", + "monthDayFromFields", + "monthsInYear", + "weekOfYear", + "year", + "yearMonthFromFields", + "yearOfWeek", +]; + +/// Designate the type of `CalendarFields` needed +#[derive(Debug, Clone, Copy)] +pub enum CalendarFieldsType { + /// Whether the Fields should return for a Date. + Date, + /// Whether the Fields should return for a YearMonth. + YearMonth, + /// Whether the Fields should return for a MonthDay. + MonthDay, +} + +// TODO: Optimize to TinyStr or &str. +impl From<&[String]> for CalendarFieldsType { + fn from(value: &[String]) -> Self { + let year_present = value.contains(&"year".to_owned()); + let day_present = value.contains(&"day".to_owned()); + + if year_present && day_present { + CalendarFieldsType::Date + } else if year_present { + CalendarFieldsType::YearMonth + } else { + CalendarFieldsType::MonthDay + } + } +} + +/// `AvailableCalendars` lists the currently implemented `CalendarProtocols` +#[derive(Debug, Clone, Copy)] +pub enum AvailableCalendars { + /// The ISO8601 calendar. + Iso, +} + +// NOTE: Should `s` be forced to lowercase or should the user be expected to provide the lowercase. +impl FromStr for AvailableCalendars { + type Err = TemporalError; + fn from_str(s: &str) -> Result { + match s { + "iso8601" => Ok(Self::Iso), + _ => { + Err(TemporalError::range().with_message("CalendarId is not an available Calendar")) + } + } + } +} + +impl AvailableCalendars { + /// Returns the `CalendarProtocol` for the `AvailableCalendar` + #[must_use] + pub fn to_protocol(&self) -> Box { + match self { + Self::Iso => Box::new(IsoCalendar), + } + } +} + +/// The `DateLike` objects that can be provided to the `CalendarProtocol`. +#[derive(Debug)] +pub enum CalendarDateLike { + /// Represents a `Date` datelike + Date(Date), + /// Represents a `DateTime` datelike + DateTime(DateTime), + /// Represents a `YearMonth` datelike + YearMonth(YearMonth), + /// Represents a `MonthDay` datelike + MonthDay(MonthDay), +} + +impl CalendarDateLike { + /// Retrieves the internal `IsoDate` field. + #[inline] + #[must_use] + pub fn as_iso_date(&self) -> IsoDate { + match self { + CalendarDateLike::Date(d) => d.iso_date(), + CalendarDateLike::DateTime(dt) => dt.iso_date(), + CalendarDateLike::MonthDay(md) => md.iso_date(), + CalendarDateLike::YearMonth(ym) => ym.iso_date(), + } + } +} + +// ==== CalendarProtocol trait ==== + +/// The `CalendarProtocol`'s Clone supertrait. +pub trait CalendarProtocolClone { + /// Clone's the current `CalendarProtocol` + fn clone_box(&self) -> Box; +} + +impl

CalendarProtocolClone for P +where + P: 'static + CalendarProtocol + Clone, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +// TODO: Split further into `CalendarProtocol` and `BuiltinCalendar` to better handle +// fields and mergeFields. +/// A trait for implementing a Builtin Calendar's Calendar Protocol in Rust. +pub trait CalendarProtocol: CalendarProtocolClone { + /// Creates a `Temporal.PlainDate` object from provided fields. + fn date_from_fields( + &self, + fields: &mut TemporalFields, + overflow: ArithmeticOverflow, + context: &mut dyn Any, + ) -> TemporalResult; + /// Creates a `Temporal.PlainYearMonth` object from the provided fields. + fn year_month_from_fields( + &self, + fields: &mut TemporalFields, + overflow: ArithmeticOverflow, + context: &mut dyn Any, + ) -> TemporalResult; + /// Creates a `Temporal.PlainMonthDay` object from the provided fields. + fn month_day_from_fields( + &self, + fields: &mut TemporalFields, + overflow: ArithmeticOverflow, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns a `Temporal.PlainDate` based off an added date. + fn date_add( + &self, + date: &Date, + duration: &Duration, + overflow: ArithmeticOverflow, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns a `Temporal.Duration` representing the duration between two dates. + fn date_until( + &self, + one: &Date, + two: &Date, + largest_unit: TemporalUnit, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns the era for a given `temporaldatelike`. + fn era( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult>>; + /// Returns the era year for a given `temporaldatelike` + fn era_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult>; + /// Returns the `year` for a given `temporaldatelike` + fn year(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult; + /// Returns the `month` for a given `temporaldatelike` + fn month(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult; + // Note: Best practice would probably be to switch to a MonthCode enum after extraction. + /// Returns the `monthCode` for a given `temporaldatelike` + fn month_code( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult>; + /// Returns the `day` for a given `temporaldatelike` + fn day(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult; + /// Returns a value representing the day of the week for a date. + fn day_of_week( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns a value representing the day of the year for a given calendar. + fn day_of_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns a value representing the week of the year for a given calendar. + fn week_of_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns the year of a given week. + fn year_of_week( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns the days in a week for a given calendar. + fn days_in_week( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns the days in a month for a given calendar. + fn days_in_month( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns the days in a year for a given calendar. + fn days_in_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns the months in a year for a given calendar. + fn months_in_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult; + /// Returns whether a value is within a leap year according to the designated calendar. + fn in_leap_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult; + /// Resolve the `TemporalFields` for the implemented Calendar + fn resolve_fields( + &self, + fields: &mut TemporalFields, + r#type: CalendarFieldsType, + ) -> TemporalResult<()>; + /// Return this calendar's a fieldName and whether it is required depending on type (date, day-month). + fn field_descriptors(&self, r#type: CalendarFieldsType) -> 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 identifier(&self, context: &mut dyn Any) -> TemporalResult; +} + +impl core::fmt::Debug for dyn CalendarProtocol { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{}", + self.identifier(&mut ()).unwrap_or_default().as_str() + ) + } +} + +/// The `[[Calendar]]` field slot of a Temporal Object. +#[derive(Debug)] +pub enum CalendarSlot { + /// The calendar identifier string. + Identifier(String), + /// A `CalendarProtocol` implementation. + Protocol(Box), +} + +impl Clone for CalendarSlot { + fn clone(&self) -> Self { + match self { + Self::Identifier(s) => Self::Identifier(s.clone()), + Self::Protocol(b) => Self::Protocol(b.clone_box()), + } + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_box() + } +} + +impl Default for CalendarSlot { + fn default() -> Self { + Self::Identifier("iso8601".to_owned()) + } +} + +// TODO: Handle `CalendarFields` and `CalendarMergeFields` +impl CalendarSlot { + /// `CalendarDateAdd` + /// + /// TODO: More Docs + pub fn date_add( + &self, + date: &Date, + duration: &Duration, + overflow: ArithmeticOverflow, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.date_add(date, duration, overflow, context) + } + Self::Protocol(protocol) => protocol.date_add(date, duration, overflow, context), + } + } + + /// `CalendarDateUntil` + /// + /// TODO: More Docs + pub fn date_until( + &self, + one: &Date, + two: &Date, + largest_unit: TemporalUnit, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.date_until(one, two, largest_unit, context) + } + Self::Protocol(protocol) => protocol.date_until(one, two, largest_unit, context), + } + } + + /// `CalendarYear` + /// + /// TODO: More docs. + pub fn year(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.year(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.year(date_like, context), + } + } + + /// `CalendarMonth` + /// + /// TODO: More docs. + pub fn month(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.month(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.month(date_like, context), + } + } + + /// `CalendarMonthCode` + /// + /// TODO: More docs. + pub fn month_code( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult> { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.month_code(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.month_code(date_like, context), + } + } + + /// `CalendarDay` + /// + /// TODO: More docs. + pub fn day(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.day(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.day(date_like, context), + } + } + + /// `CalendarDayOfWeek` + /// + /// TODO: More docs. + pub fn day_of_week( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.day_of_week(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.day_of_week(date_like, context), + } + } + + /// `CalendarDayOfYear` + /// + /// TODO: More docs. + pub fn day_of_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.day_of_year(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.day_of_year(date_like, context), + } + } + + /// `CalendarWeekOfYear` + /// + /// TODO: More docs. + pub fn week_of_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.week_of_year(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.week_of_year(date_like, context), + } + } + + /// `CalendarYearOfWeek` + /// + /// TODO: More docs. + pub fn year_of_week( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.year_of_week(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.year_of_week(date_like, context), + } + } + + /// `CalendarDaysInWeek` + /// + /// TODO: More docs. + pub fn days_in_week( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.days_in_week(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.days_in_week(date_like, context), + } + } + + /// `CalendarDaysInMonth` + /// + /// TODO: More docs. + pub fn days_in_month( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.days_in_month(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.days_in_month(date_like, context), + } + } + + /// `CalendarDaysInYear` + /// + /// TODO: More docs. + pub fn days_in_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.days_in_year(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.days_in_year(date_like, context), + } + } + + /// `CalendarMonthsInYear` + /// + /// TODO: More docs. + pub fn months_in_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.months_in_year(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.months_in_year(date_like, context), + } + } + + /// `CalendarInLeapYear` + /// + /// TODO: More docs. + pub fn in_leap_year( + &self, + date_like: &CalendarDateLike, + context: &mut dyn Any, + ) -> TemporalResult { + match self { + Self::Identifier(id) => { + let protocol = AvailableCalendars::from_str(id)?.to_protocol(); + protocol.in_leap_year(date_like, &mut ()) + } + Self::Protocol(protocol) => protocol.in_leap_year(date_like, context), + } + } +} diff --git a/boa_temporal/src/calendar/iso.rs b/boa_temporal/src/calendar/iso.rs new file mode 100644 index 0000000000..1ac7283fb2 --- /dev/null +++ b/boa_temporal/src/calendar/iso.rs @@ -0,0 +1,285 @@ +//! Implementation of the "iso8601" calendar. + +use crate::{ + date::Date, + duration::Duration, + error::TemporalError, + fields::TemporalFields, + month_day::MonthDay, + options::{ArithmeticOverflow, TemporalUnit}, + utils, + year_month::YearMonth, + TemporalResult, +}; +use std::any::Any; + +use tinystr::TinyAsciiStr; + +use super::{CalendarDateLike, CalendarFieldsType, CalendarProtocol, CalendarSlot}; + +use icu_calendar::week::{RelativeUnit, WeekCalculator}; + +/// This represents the implementation of the `ISO8601` +/// calendar for Temporal. +#[derive(Debug, Clone, Copy)] +pub struct IsoCalendar; + +impl CalendarProtocol 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 TemporalFields, + overflow: ArithmeticOverflow, + _: &mut dyn Any, + ) -> TemporalResult { + // NOTE: we are in ISO by default here. + // a. Perform ? ISOResolveMonth(fields). + // b. Let result be ? ISODateFromFields(fields, overflow). + fields.iso_resolve_month()?; + + // 9. Return ? CreateDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601"). + Date::new( + fields.year().unwrap_or(0), + fields.month().unwrap_or(0), + fields.day().unwrap_or(0), + CalendarSlot::Identifier("iso8601".to_string()), + overflow, + ) + } + + /// 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 TemporalFields, + overflow: ArithmeticOverflow, + _: &mut dyn Any, + ) -> TemporalResult { + // 9. If calendar.[[Identifier]] is "iso8601", then + // a. Perform ? ISOResolveMonth(fields). + fields.iso_resolve_month()?; + + // TODO: Do we even need ISOYearMonthFromFields? YearMonth would should pass as a valid date + // b. Let result be ? ISOYearMonthFromFields(fields, overflow). + // 10. Return ? CreateYearMonth(result.[[Year]], result.[[Month]], "iso8601", result.[[ReferenceISODay]]). + YearMonth::new( + fields.year().unwrap_or(0), + fields.month().unwrap_or(0), + fields.day(), + CalendarSlot::Identifier("iso8601".to_string()), + overflow, + ) + } + + /// 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 TemporalFields, + overflow: ArithmeticOverflow, + _: &mut dyn Any, + ) -> TemporalResult { + // 8. Perform ? ISOResolveMonth(fields). + fields.iso_resolve_month()?; + + // TODO: double check error mapping is correct for specifcation/test262. + // 9. Let result be ? ISOMonthDayFromFields(fields, overflow). + // 10. Return ? CreateMonthDay(result.[[Month]], result.[[Day]], "iso8601", result.[[ReferenceISOYear]]). + MonthDay::new( + fields.month().unwrap_or(0), + fields.month().unwrap_or(0), + CalendarSlot::Identifier("iso8601".to_string()), + overflow, + ) + } + + /// 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: &Date, + _duration: &Duration, + _overflow: ArithmeticOverflow, + _: &mut dyn Any, + ) -> TemporalResult { + // TODO: Not stable on `ICU4X`. Implement once completed. + Err(TemporalError::range().with_message("feature not implemented.")) + + // 9. Let result be ? AddISODate(date.[[ISOYear]], date.[[ISOMonth]], date.[[ISODay]], duration.[[Years]], duration.[[Months]], duration.[[Weeks]], balanceResult.[[Days]], overflow). + // 10. Return ? CreateDate(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: &Date, + _two: &Date, + _largest_unit: TemporalUnit, + _: &mut dyn Any, + ) -> TemporalResult { + // TODO: Not stable on `ICU4X`. Implement once completed. + Err(TemporalError::range().with_message("Feature not yet implemented.")) + + // 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, + _: &CalendarDateLike, + _: &mut dyn Any, + ) -> TemporalResult>> { + // Returns undefined on iso8601. + Ok(None) + } + + /// `Temporal.Calendar.prototype.eraYear( dateLike )` for iso8601 calendar. + fn era_year(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult> { + // Returns undefined on iso8601. + Ok(None) + } + + /// Returns the `year` for the `Iso` calendar. + fn year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + Ok(date_like.as_iso_date().year()) + } + + /// Returns the `month` for the `Iso` calendar. + fn month(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + Ok(date_like.as_iso_date().month()) + } + + /// Returns the `monthCode` for the `Iso` calendar. + fn month_code( + &self, + date_like: &CalendarDateLike, + _: &mut dyn Any, + ) -> TemporalResult> { + let date = date_like.as_iso_date().as_icu4x()?; + Ok(date.month().code.0) + } + + /// Returns the `day` for the `Iso` calendar. + fn day(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + Ok(date_like.as_iso_date().day()) + } + + /// Returns the `dayOfWeek` for the `Iso` calendar. + fn day_of_week(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + let date = date_like.as_iso_date().as_icu4x()?; + Ok(date.day_of_week() as u16) + } + + /// Returns the `dayOfYear` for the `Iso` calendar. + fn day_of_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + let date = date_like.as_iso_date().as_icu4x()?; + Ok(date.day_of_year_info().day_of_year) + } + + /// Returns the `weekOfYear` for the `Iso` calendar. + fn week_of_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + let date = date_like.as_iso_date().as_icu4x()?; + + let week_calculator = WeekCalculator::default(); + + let week_of = date + .week_of_year(&week_calculator) + .map_err(|err| TemporalError::range().with_message(err.to_string()))?; + + Ok(week_of.week) + } + + /// Returns the `yearOfWeek` for the `Iso` calendar. + fn year_of_week(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + let date = date_like.as_iso_date().as_icu4x()?; + + let week_calculator = WeekCalculator::default(); + + let week_of = date + .week_of_year(&week_calculator) + .map_err(|err| TemporalError::range().with_message(err.to_string()))?; + + // TODO: Reach out and see about RelativeUnit starting at -1 + // Ok(date.year().number - week_of.unit) + match week_of.unit { + RelativeUnit::Previous => Ok(date.year().number - 1), + RelativeUnit::Current => Ok(date.year().number), + RelativeUnit::Next => Ok(date.year().number + 1), + } + } + + /// Returns the `daysInWeek` value for the `Iso` calendar. + fn days_in_week(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + Ok(7) + } + + /// Returns the `daysInMonth` value for the `Iso` calendar. + fn days_in_month(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + let date = date_like.as_iso_date().as_icu4x()?; + Ok(u16::from(date.days_in_month())) + } + + /// Returns the `daysInYear` value for the `Iso` calendar. + fn days_in_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + let date = date_like.as_iso_date().as_icu4x()?; + Ok(date.days_in_year()) + } + + /// Return the amount of months in an ISO Calendar. + fn months_in_year(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + Ok(12) + } + + /// Returns whether provided date is in a leap year according to this calendar. + fn in_leap_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult { + // `ICU4X`'s `CalendarArithmetic` is currently private. + Ok(utils::mathematical_days_in_year(date_like.as_iso_date().year()) == 366) + } + + // Resolve the fields for the iso calendar. + fn resolve_fields( + &self, + fields: &mut TemporalFields, + _: CalendarFieldsType, + ) -> TemporalResult<()> { + fields.iso_resolve_month()?; + Ok(()) + } + + /// Returns the ISO field descriptors, which is not called for the iso8601 calendar. + fn field_descriptors(&self, _: CalendarFieldsType) -> 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 { + result.push(key.clone()); + if key.as_str() == "month" { + result.push("monthCode".to_string()); + } else if key.as_str() == "monthCode" { + result.push("month".to_string()); + } + } + result + } + + // NOTE: This is currently not a name that is compliant with + // the Temporal proposal. For debugging purposes only. + /// Returns the debug name. + fn identifier(&self, _: &mut dyn Any) -> TemporalResult { + Ok("iso8601".to_string()) + } +} diff --git a/boa_temporal/src/date.rs b/boa_temporal/src/date.rs new file mode 100644 index 0000000000..054940790c --- /dev/null +++ b/boa_temporal/src/date.rs @@ -0,0 +1,220 @@ +//! The `PlainDate` representation. + +use crate::{ + calendar::CalendarSlot, + datetime::DateTime, + duration::{DateDuration, Duration}, + iso::{IsoDate, IsoDateSlots}, + options::{ArithmeticOverflow, TemporalUnit}, + TemporalResult, +}; +use std::any::Any; + +/// The `Temporal.PlainDate` equivalent +#[derive(Debug, Default, Clone)] +pub struct Date { + iso: IsoDate, + calendar: CalendarSlot, +} + +// ==== Private API ==== + +impl Date { + /// Create a new `Date` with the date values and calendar slot. + #[inline] + #[must_use] + pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot) -> Self { + Self { iso, calendar } + } + + #[inline] + /// Returns a new moved date and the days associated with that adjustment + pub(crate) fn move_relative_date( + &self, + duration: &Duration, + context: &mut dyn Any, + ) -> TemporalResult<(Self, f64)> { + let new_date = + self.contextual_add_date(duration, ArithmeticOverflow::Constrain, context)?; + let days = f64::from(self.days_until(&new_date)); + Ok((new_date, days)) + } +} + +// ==== Public API ==== + +impl Date { + /// Creates a new `Date` while checking for validity. + pub fn new( + year: i32, + month: i32, + day: i32, + calendar: CalendarSlot, + overflow: ArithmeticOverflow, + ) -> TemporalResult { + let iso = IsoDate::new(year, month, day, overflow)?; + Ok(Self::new_unchecked(iso, calendar)) + } + + #[must_use] + /// Creates a `Date` from a `DateTime`. + pub fn from_datetime(dt: &DateTime) -> Self { + Self { + iso: dt.iso_date(), + calendar: dt.calendar().clone(), + } + } + + #[inline] + #[must_use] + /// Returns this `Date`'s year value. + pub const fn year(&self) -> i32 { + self.iso.year() + } + + #[inline] + #[must_use] + /// Returns this `Date`'s month value. + pub const fn month(&self) -> u8 { + self.iso.month() + } + + #[inline] + #[must_use] + /// Returns this `Date`'s day value. + pub const fn day(&self) -> u8 { + self.iso.day() + } + + #[inline] + #[must_use] + /// Returns the `Date`'s inner `IsoDate` record. + pub const fn iso_date(&self) -> IsoDate { + self.iso + } + + #[inline] + #[must_use] + /// Returns a reference to this `Date`'s calendar slot. + pub fn calendar(&self) -> &CalendarSlot { + &self.calendar + } + + /// 3.5.7 `IsValidISODate` + /// + /// Checks if the current date is a valid `ISODate`. + #[must_use] + pub fn is_valid(&self) -> bool { + self.iso.is_valid() + } + + /// `DaysUntil` + /// + /// Calculates the epoch days between two `Date`s + #[inline] + #[must_use] + pub fn days_until(&self, other: &Self) -> i32 { + other.iso.to_epoch_days() - self.iso.to_epoch_days() + } +} + +impl IsoDateSlots for Date { + /// Returns the structs `IsoDate` + fn iso_date(&self) -> IsoDate { + self.iso + } +} + +// ==== Context based API ==== + +impl Date { + /// Returns the date after adding the given duration to date with a provided context. + /// + /// Temporal Equivalent: 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ] ] )` + #[inline] + pub fn contextual_add_date( + &self, + duration: &Duration, + overflow: ArithmeticOverflow, + context: &mut dyn Any, + ) -> TemporalResult { + // 1. If options is not present, set options to undefined. + // 2. If duration.[[Years]] ≠ 0, or duration.[[Months]] ≠ 0, or duration.[[Weeks]] ≠ 0, then + if duration.date().years() != 0.0 + || duration.date().months() != 0.0 + || duration.date().weeks() != 0.0 + { + // a. If dateAdd is not present, then + // i. Set dateAdd to unused. + // ii. If calendar is an Object, set dateAdd to ? GetMethod(calendar, "dateAdd"). + // b. Return ? CalendarDateAdd(calendar, plainDate, duration, options, dateAdd). + return self.calendar().date_add(self, duration, overflow, context); + } + + // 3. Let overflow be ? ToTemporalOverflow(options). + // 4. Let days be ? BalanceTimeDuration(duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], "day").[[Days]]. + let (days, _) = duration.balance_time_duration(TemporalUnit::Day)?; + + // 5. Let result be ? AddISODate(plainDate.[[ISOYear]], plainDate.[[ISOMonth]], plainDate.[[ISODay]], 0, 0, 0, days, overflow). + let result = self + .iso + .add_iso_date(&DateDuration::new(0f64, 0f64, 0f64, days), overflow)?; + + Ok(Self::new_unchecked(result, self.calendar().clone())) + } + + /// Returns the date after adding the given duration to date. + /// + /// Temporal Equivalent: 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ] ] )` + #[inline] + pub fn add_date( + &self, + duration: &Duration, + overflow: ArithmeticOverflow, + ) -> TemporalResult { + self.contextual_add_date(duration, overflow, &mut ()) + } + + /// Returns a duration representing the difference between the dates one and two with a provided context. + /// + /// Temporal Equivalent: 3.5.6 `DifferenceDate ( calendar, one, two, options )` + #[inline] + pub fn contextual_difference_date( + &self, + other: &Self, + largest_unit: TemporalUnit, + context: &mut dyn Any, + ) -> TemporalResult { + if self.iso.year() == other.iso.year() + && self.iso.month() == other.iso.month() + && self.iso.day() == other.iso.day() + { + return Ok(Duration::default()); + } + + if largest_unit == TemporalUnit::Day { + let days = self.days_until(other); + return Ok(Duration::from_date_duration(DateDuration::new( + 0f64, + 0f64, + 0f64, + f64::from(days), + ))); + } + + self.calendar() + .date_until(self, other, largest_unit, context) + } + + /// Returns a duration representing the difference between the dates one and two. + /// + /// Temporal Equivalent: 3.5.6 `DifferenceDate ( calendar, one, two, options )` + #[inline] + pub fn difference_date( + &self, + other: &Self, + largest_unit: TemporalUnit, + ) -> TemporalResult { + self.contextual_difference_date(other, largest_unit, &mut ()) + } +} diff --git a/boa_temporal/src/datetime.rs b/boa_temporal/src/datetime.rs new file mode 100644 index 0000000000..cca2164929 --- /dev/null +++ b/boa_temporal/src/datetime.rs @@ -0,0 +1,95 @@ +//! Temporal implementation of `DateTime` + +use crate::{ + calendar::CalendarSlot, + iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime}, + options::ArithmeticOverflow, + TemporalResult, +}; + +/// The `DateTime` struct. +#[derive(Debug, Default, Clone)] +pub struct DateTime { + iso: IsoDateTime, + calendar: CalendarSlot, +} + +// ==== Private DateTime API ==== + +impl DateTime { + /// Creates a new unchecked `DateTime`. + #[inline] + #[must_use] + pub(crate) fn new_unchecked(date: IsoDate, time: IsoTime, calendar: CalendarSlot) -> Self { + Self { + iso: IsoDateTime::new_unchecked(date, time), + calendar, + } + } + + #[inline] + #[must_use] + /// Utility function for validating `IsoDate`s + fn validate_iso(iso: IsoDate) -> bool { + IsoDateTime::new_unchecked(iso, IsoTime::noon()).is_within_limits() + } +} + +// ==== Public DateTime API ==== + +impl DateTime { + /// Creates a new validated `DateTime`. + #[inline] + #[allow(clippy::too_many_arguments)] + pub fn new( + year: i32, + month: i32, + day: i32, + hour: i32, + minute: i32, + second: i32, + millisecond: i32, + microsecond: i32, + nanosecond: i32, + calendar: CalendarSlot, + ) -> TemporalResult { + let iso_date = IsoDate::new(year, month, day, ArithmeticOverflow::Reject)?; + let iso_time = IsoTime::new( + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + ArithmeticOverflow::Reject, + )?; + Ok(Self::new_unchecked(iso_date, iso_time, calendar)) + } + + /// Validates whether ISO date slots are within iso limits at noon. + #[inline] + pub fn validate(target: &T) -> bool { + Self::validate_iso(target.iso_date()) + } + + /// Returns the inner `IsoDate` value. + #[inline] + #[must_use] + pub fn iso_date(&self) -> IsoDate { + self.iso.iso_date() + } + + /// Returns the inner `IsoTime` value. + #[inline] + #[must_use] + pub fn iso_time(&self) -> IsoTime { + self.iso.iso_time() + } + + /// Returns the Calendar value. + #[inline] + #[must_use] + pub fn calendar(&self) -> &CalendarSlot { + &self.calendar + } +} diff --git a/boa_engine/src/builtins/temporal/duration/record.rs b/boa_temporal/src/duration.rs similarity index 53% rename from boa_engine/src/builtins/temporal/duration/record.rs rename to boa_temporal/src/duration.rs index cd29c089a6..fa2c9f4962 100644 --- a/boa_engine/src/builtins/temporal/duration/record.rs +++ b/boa_temporal/src/duration.rs @@ -1,33 +1,27 @@ -//! The `DurationRecord` implements the internal representation of a Temporal Duration. +//! The Temporal Duration. +//! +//! TODO: Docs use crate::{ - builtins::{ - options::RoundingMode, - temporal::{ - self, - options::{ArithmeticOverflow, TemporalUnit}, - round_number_to_increment, to_temporal_date, NS_PER_DAY, - }, - }, - js_string, - string::utf16, - Context, JsNativeError, JsObject, JsResult, JsValue, -}; - -use super::super::{ - calendar, plain_date, to_integer_if_integral, PlainDate, PlainDateTime, ZonedDateTime, + date::Date, + datetime::DateTime, + options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit}, + utils, + zoneddatetime::ZonedDateTime, + TemporalError, TemporalResult, NS_PER_DAY, }; +use std::any::Any; // ==== `DateDuration` ==== -/// `DateDuration` represents the [date duration record][spec] of the `DurationRecord.` +/// `DateDuration` represents the [date duration record][spec] of the `Duration.` /// /// 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 { +pub struct DateDuration { years: f64, months: f64, weeks: f64, @@ -35,7 +29,9 @@ pub(crate) struct DateDuration { } impl DateDuration { - pub(crate) const fn new(years: f64, months: f64, weeks: f64, days: f64) -> Self { + /// Creates a new `DateDuration` with provided values. + #[must_use] + pub const fn new(years: f64, months: f64, weeks: f64, days: f64) -> Self { Self { years, months, @@ -44,7 +40,9 @@ impl DateDuration { } } - pub(crate) const fn partial() -> Self { + /// Returns a `PartialDateDuration` with all fields set to `NaN`. + #[must_use] + pub const fn partial() -> Self { Self { years: f64::NAN, months: f64::NAN, @@ -53,19 +51,27 @@ impl DateDuration { } } - pub(crate) const fn years(&self) -> f64 { + /// Returns the `[[years]]` value. + #[must_use] + pub const fn years(&self) -> f64 { self.years } - pub(crate) const fn months(&self) -> f64 { + /// Returns the `[[months]]` value. + #[must_use] + pub const fn months(&self) -> f64 { self.months } - pub(crate) const fn weeks(&self) -> f64 { + /// Returns the `[[weeks]]` value. + #[must_use] + pub const fn weeks(&self) -> f64 { self.weeks } - pub(crate) const fn days(&self) -> f64 { + /// Returns the `[[days]]` value. + #[must_use] + pub const fn days(&self) -> f64 { self.days } } @@ -82,7 +88,9 @@ impl<'a> IntoIterator for &'a DateDuration { } } -pub(crate) struct DateIter<'a> { +/// An iterator over the `DateDuration` +#[derive(Debug)] +pub struct DateIter<'a> { date: &'a DateDuration, index: usize, } @@ -105,14 +113,14 @@ impl Iterator for DateIter<'_> { // ==== `TimeDuration` ==== -/// `TimeDuration` represents the [Time Duration record][spec] of the `DurationRecord.` +/// `TimeDuration` represents the [Time Duration record][spec] of the `Duration.` /// /// 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 { +pub struct TimeDuration { hours: f64, minutes: f64, seconds: f64, @@ -122,7 +130,9 @@ pub(crate) struct TimeDuration { } impl TimeDuration { - pub(crate) const fn new( + /// Creates a new `TimeDuration`. + #[must_use] + pub const fn new( hours: f64, minutes: f64, seconds: f64, @@ -140,7 +150,9 @@ impl TimeDuration { } } - pub(crate) const fn partial() -> Self { + /// Creates a partial `TimeDuration` with all values set to `NaN`. + #[must_use] + pub const fn partial() -> Self { Self { hours: f64::NAN, minutes: f64::NAN, @@ -153,7 +165,8 @@ impl TimeDuration { /// Utility function for returning if values in a valid range. #[inline] - pub(crate) fn is_within_range(&self) -> bool { + #[must_use] + pub fn is_within_range(&self) -> bool { self.hours.abs() < 24f64 && self.minutes.abs() < 60f64 && self.seconds.abs() < 60f64 @@ -161,6 +174,42 @@ impl TimeDuration { && self.milliseconds.abs() < 1000f64 && self.milliseconds.abs() < 1000f64 } + + /// Returns the `[[hours]]` value. + #[must_use] + pub const fn hours(&self) -> f64 { + self.hours + } + + /// Returns the `[[minutes]]` value. + #[must_use] + pub const fn minutes(&self) -> f64 { + self.minutes + } + + /// Returns the `[[seconds]]` value. + #[must_use] + pub const fn seconds(&self) -> f64 { + self.seconds + } + + /// Returns the `[[milliseconds]]` value. + #[must_use] + pub const fn milliseconds(&self) -> f64 { + self.milliseconds + } + + /// Returns the `[[microseconds]]` value. + #[must_use] + pub const fn microseconds(&self) -> f64 { + self.microseconds + } + + /// Returns the `[[nanoseconds]]` value. + #[must_use] + pub const fn nanoseconds(&self) -> f64 { + self.nanoseconds + } } impl<'a> IntoIterator for &'a TimeDuration { @@ -175,7 +224,9 @@ impl<'a> IntoIterator for &'a TimeDuration { } } -pub(crate) struct TimeIter<'a> { +/// An iterator over a `TimeDuration`. +#[derive(Debug)] +pub struct TimeIter<'a> { time: &'a TimeDuration, index: usize, } @@ -198,328 +249,205 @@ impl Iterator for TimeIter<'_> { } } -// ==== `DurationRecord` ==== +// ==== `Duration` ==== -/// The `DurationRecord` is a native Rust implementation of the `Duration` builtin +/// The `Duration` 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 { +pub struct Duration { date: DateDuration, time: TimeDuration, } -impl DurationRecord { - pub(crate) const fn new(date: DateDuration, time: TimeDuration) -> Self { +// NOTE(nekevss): Structure of the below is going to be a little convoluted, +// but intended to section everything based on the below +// +// Notation - [section](sub-section(s)). +// +// Sections: +// - Creation (private/public) +// - Getters/Setters +// - Methods (private/public/feature) +// + +// ==== Private Creation methods ==== + +impl Duration { + /// Creates a new `Duration` from a `DateDuration` and `TimeDuration`. + pub(crate) const fn new_unchecked(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, - } - } - - /// Utility function to create a one year duration. + /// Utility function to create a year duration. pub(crate) fn one_year(year_value: f64) -> Self { Self::from_date_duration(DateDuration::new(year_value, 0f64, 0f64, 0f64)) } - /// Utility function to create a one month duration. + /// Utility function to create a month duration. pub(crate) fn one_month(month_value: f64) -> Self { Self::from_date_duration(DateDuration::new(0f64, month_value, 0f64, 0f64)) } - /// Utility function to create a one week duration. + /// Utility function to create a week duration. pub(crate) fn one_week(week_value: f64) -> Self { Self::from_date_duration(DateDuration::new(0f64, 0f64, week_value, 0f64)) } - - /// Utility function to return if the Durations values are within their valid ranges. - #[inline] - pub(crate) fn is_time_within_range(&self) -> bool { - self.time.is_within_range() - } - - /// 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); +impl Duration { + /// Creates a new validated `Duration`. + #[allow(clippy::too_many_arguments)] + pub fn new( + years: f64, + months: f64, + weeks: f64, + days: f64, + hours: f64, + minutes: f64, + seconds: f64, + milliseconds: f64, + microseconds: f64, + nanoseconds: f64, + ) -> TemporalResult { + let duration = Self::new_unchecked( + DateDuration::new(years, months, weeks, days), + TimeDuration::new( + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + ), + ); + if !duration.is_valid() { + return Err(TemporalError::range().with_message("Duration was not valid.")); + } + Ok(duration) } - /// 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); + /// Creates a partial `Duration` with all fields set to `NaN`. + #[must_use] + pub const fn partial() -> Self { + Self { + date: DateDuration::partial(), + time: TimeDuration::partial(), + } } - /// 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); + /// Creates a `Duration` from only a `DateDuration`. + #[must_use] + pub fn from_date_duration(date: DateDuration) -> Self { + Self { + date, + time: TimeDuration::default(), + } } - /// 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); + /// Creates a `Duration` from a provided a day and a `TimeDuration`. + /// + /// Note: `TimeDuration` records can store a day value to deal with overflow. + #[must_use] + pub const fn from_day_and_time(day: f64, time: TimeDuration) -> Self { + Self { + date: DateDuration::new(0.0, 0.0, 0.0, day), + time, + } } - /// 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); + /// Return if the Durations values are within their valid ranges. + #[inline] + #[must_use] + pub fn is_time_within_range(&self) -> bool { + self.time.is_within_range() } } -// ==== `DurationRecord` getter/setter methods ==== +// ==== Public `Duration` Getters/Setters ==== -impl DurationRecord { - /// Return this `DurationRecord`'s `DateDuration` - pub(crate) const fn date(&self) -> DateDuration { - self.date +impl Duration { + /// Returns a reference to the inner `TimeDuration` + #[inline] + #[must_use] + pub fn time(&self) -> &TimeDuration { + &self.time } - /// Return this `DurationRecord`'s `TimeDuration` - pub(crate) const fn time(&self) -> TimeDuration { - self.time + /// Returns a reference to the inner `DateDuration` + #[inline] + #[must_use] + pub fn date(&self) -> &DateDuration { + &self.date } /// Set this `DurationRecord`'s `TimeDuration`. - pub(crate) fn set_time_duration(&mut self, time: TimeDuration) { + #[inline] + pub 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) { + #[inline] + pub 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) { + #[inline] + pub 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) { + #[inline] + pub 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) { + #[inline] + pub 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) { + #[inline] + pub 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) { + #[inline] + pub 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) { + #[inline] + pub 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) { + #[inline] + pub 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) { + #[inline] + pub 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) { + #[inline] + pub 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 { +impl<'a> IntoIterator for &'a Duration { type Item = f64; type IntoIter = DurationIter<'a>; @@ -531,8 +459,10 @@ impl<'a> IntoIterator for &'a DurationRecord { } } -pub(crate) struct DurationIter<'a> { - duration: &'a DurationRecord, +/// A Duration iterator that iterates through all duration fields. +#[derive(Debug)] +pub struct DurationIter<'a> { + duration: &'a Duration, index: usize, } @@ -541,16 +471,16 @@ impl Iterator for DurationIter<'_> { 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()), + 0 => Some(self.duration.date.years()), + 1 => Some(self.duration.date.months()), + 2 => Some(self.duration.date.weeks()), + 3 => Some(self.duration.date.days()), + 4 => Some(self.duration.time.hours()), + 5 => Some(self.duration.time.minutes()), + 6 => Some(self.duration.time.seconds()), + 7 => Some(self.duration.time.milliseconds()), + 8 => Some(self.duration.time.microseconds()), + 9 => Some(self.duration.time.nanoseconds()), _ => None, }; self.index += 1; @@ -558,88 +488,20 @@ impl Iterator for DurationIter<'_> { } } -// ==== 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) - } +// ==== Private Duration methods ==== +impl Duration { /// 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] - #[allow(unused)] - pub(crate) fn is_positive_overflow(&self) -> bool { - self.years().is_infinite() && self.years().is_sign_positive() - } - - #[inline] - #[allow(unused)] - 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 + pub(crate) fn time_values(&self) -> Vec { + let mut values = Vec::from([self.date.days]); + values.extend(&self.time); + values } /// 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 { + pub(crate) fn is_valid(&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 @@ -661,127 +523,44 @@ impl DurationRecord { 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() + pub(crate) fn total_duration_nanoseconds(&self, offset_shift: f64) -> f64 { + let nanoseconds = if self.date.days == 0_f64 { + self.time.nanoseconds } else { - self.nanoseconds() - offset_shift + self.time.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()) + self.date + .days + .mul_add(24_f64, self.time.hours) + .mul_add(60_f64, self.time.minutes) + .mul_add(60_f64, self.time.seconds) + .mul_add(1_000_f64, self.time.milliseconds) + .mul_add(1_000_f64, self.time.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, + /// Abstract Operation 7.5.18 `BalancePossiblyInfiniteDuration ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit )` + pub(crate) fn balance_possibly_infinite_time_duration( + &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(()) - } + ) -> TemporalResult> { + let mut result = TimeDuration::default(); + let mut result_days = 0f64; - /// 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() - }; + // 1. Set hours to hours + days × 24. + result.hours = self.time.hours + (self.date.days * 24f64); - // 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)); - } + // 2. Set nanoseconds to TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + result.nanoseconds = self.total_duration_nanoseconds(0f64); - 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), - } + // 3. Set days, hours, minutes, seconds, milliseconds, and microseconds to 0. - // 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()); + // 4. If nanoseconds < 0, let sign be -1; else, let sign be 1. + let sign = if result.nanoseconds < 0f64 { -1 } else { 1 }; + // 5. Set nanoseconds to abs(nanoseconds). + result.nanoseconds = result.nanoseconds.abs(); match largest_unit { // 9. If largestUnit is "year", "month", "week", "day", or "hour", then @@ -791,76 +570,96 @@ impl DurationRecord { | TemporalUnit::Day | TemporalUnit::Hour => { // a. Set microseconds to floor(nanoseconds / 1000). + result.microseconds = (result.nanoseconds / 1000f64).floor(); // b. Set nanoseconds to nanoseconds modulo 1000. - self.balance_microseconds(); + result.nanoseconds %= 1000f64; // c. Set milliseconds to floor(microseconds / 1000). + result.milliseconds = (result.microseconds / 1000f64).floor(); // d. Set microseconds to microseconds modulo 1000. - self.balance_milliseconds(); + result.microseconds %= 1000f64; // e. Set seconds to floor(milliseconds / 1000). + result.seconds = (result.milliseconds / 1000f64).floor(); // f. Set milliseconds to milliseconds modulo 1000. - self.balance_minutes(); + result.milliseconds %= 1000f64; // g. Set minutes to floor(seconds / 60). + result.minutes = (result.seconds / 60f64).floor(); // h. Set seconds to seconds modulo 60. - self.balance_minutes(); + result.seconds %= 60f64; // i. Set hours to floor(minutes / 60). + result.hours = (result.minutes / 60f64).floor(); // j. Set minutes to minutes modulo 60. - self.balance_hours(); + result.minutes %= 60f64; + + // k. Set days to floor(hours / 24). + result_days = (result.hours / 24f64).floor(); + // l. Set hours to hours modulo 24. + result.hours %= 24f64; } // 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(); + result.microseconds = (result.nanoseconds / 1000f64).floor(); + result.nanoseconds %= 1000f64; // c. Set milliseconds to floor(microseconds / 1000). // d. Set microseconds to microseconds modulo 1000. - self.balance_milliseconds(); + result.milliseconds = (result.microseconds / 1000f64).floor(); + result.microseconds %= 1000f64; // e. Set seconds to floor(milliseconds / 1000). // f. Set milliseconds to milliseconds modulo 1000. - self.balance_seconds(); + result.minutes = (result.seconds / 60f64).floor(); + result.seconds %= 60f64; // g. Set minutes to floor(seconds / 60). // h. Set seconds to seconds modulo 60. - self.balance_minutes(); + result.minutes = (result.seconds / 60f64).floor(); + result.seconds %= 60f64; } // 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(); + result.microseconds = (result.nanoseconds / 1000f64).floor(); + result.nanoseconds %= 1000f64; // c. Set milliseconds to floor(microseconds / 1000). // d. Set microseconds to microseconds modulo 1000. - self.balance_milliseconds(); + result.milliseconds = (result.microseconds / 1000f64).floor(); + result.microseconds %= 1000f64; // e. Set seconds to floor(milliseconds / 1000). // f. Set milliseconds to milliseconds modulo 1000. - self.balance_seconds(); + result.minutes = (result.seconds / 60f64).floor(); + result.seconds %= 60f64; } // 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(); + result.microseconds = (result.nanoseconds / 1000f64).floor(); + result.nanoseconds %= 1000f64; // c. Set milliseconds to floor(microseconds / 1000). // d. Set microseconds to microseconds modulo 1000. - self.balance_milliseconds(); + result.milliseconds = (result.microseconds / 1000f64).floor(); + result.microseconds %= 1000f64; } // 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(); + result.microseconds = (result.nanoseconds / 1000f64).floor(); + result.nanoseconds %= 1000f64; } // 14. Else, // a. Assert: largestUnit is "nanosecond". - _ => assert!(largest_unit == TemporalUnit::Nanosecond), + _ => debug_assert!(largest_unit == TemporalUnit::Nanosecond), } // 15. For each value v of « days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds », do @@ -868,35 +667,34 @@ impl DurationRecord { // a. If 𝔽(v) is not finite, then if !value.is_finite() { // i. If sign = 1, then - if sign as i32 == 1 { + if sign == 1 { // 1. Return positive overflow. - self.set_years(f64::INFINITY); - return Ok(()); + return Ok(None); } // ii. Else if sign = -1, then // 1. Return negative overflow. - self.set_years(f64::NEG_INFINITY); - return Ok(()); + return Ok(None); } } - // NOTE (nekevss): diviate from spec here as the current implementation with `DurationRecord` means that we create the record and than mutate values. + let sign = f64::from(sign); + // 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); + result.hours *= sign; + result.minutes *= sign; + result.seconds *= sign; + result.milliseconds *= sign; + result.microseconds *= sign; + result.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()); + if !self.is_valid() { + return Err( + TemporalError::range().with_message("TimeDurationRecord is not a valid duration.") + ); } - Ok(()) + Ok(Some((result_days, result))) } /// 7.5.21 `UnbalanceDateDurationRelative ( years, months, weeks, days, largestUnit, plainRelativeTo )` @@ -904,20 +702,20 @@ impl DurationRecord { pub(crate) fn unbalance_duration_relative( &self, largest_unit: TemporalUnit, - plain_relative_to: Option<&PlainDate>, - context: &mut Context, - ) -> JsResult { + plain_relative_to: Option<&Date>, + context: &mut dyn Any, + ) -> TemporalResult { // 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; + let all_zero = self.date.years == 0_f64 + && self.date.months == 0_f64 + && self.date.weeks == 0_f64 + && self.date.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(self.date()); + return Ok(self.date); }; // 4. Let sign be ! DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0). @@ -937,28 +735,20 @@ impl DurationRecord { // 11. If largestUnit is "month", then if largest_unit == TemporalUnit::Month { // a. If years = 0, return ! CreateDateDurationRecord(0, months, weeks, days). - if self.years() == 0f64 { + if self.date.years == 0f64 { return Ok(DateDuration::new( 0f64, - self.months(), - self.weeks(), - self.days(), + self.date.months, + self.date.weeks, + self.date.days, )); } // b. If calendar is undefined, then - let (mut plain_relative_to, calendar) = - if let Some(plain_relative_to) = plain_relative_to { - ( - PlainDate::new(plain_relative_to.inner, plain_relative_to.calendar.clone()), - plain_relative_to.calendar.clone(), - ) - } else { - // i. Throw a RangeError exception. - return Err(JsNativeError::range() - .with_message("Calendar cannot be undefined.") - .into()); - }; + let Some(mut plain_relative_to) = plain_relative_to.map(Clone::clone) else { + // i. Throw a RangeError exception. + return Err(TemporalError::range().with_message("Calendar cannot be undefined.")); + }; // c. If calendar is an Object, then // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). @@ -967,39 +757,30 @@ impl DurationRecord { // i. Let dateAdd be unused. // ii. Let dateUntil be unused. - let mut years = self.years(); - let mut months = self.months(); + let mut years = self.date.years; + let mut months = self.date.months; // e. Repeat, while years ≠ 0, while years != 0f64 { // i. Let newRelativeTo be ? CalendarDateAdd(calendar, plainRelativeTo, oneYear, undefined, dateAdd). - let new_relative_to = calendar::calendar_date_add( - &calendar, + let new_relative_to = plain_relative_to.calendar().date_add( &plain_relative_to, &one_year, - &JsValue::undefined(), + ArithmeticOverflow::Constrain, context, )?; // ii. Let untilOptions be OrdinaryObjectCreate(null). - let until_options = JsObject::with_null_proto(); // iii. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). - until_options.create_data_property_or_throw( - utf16!("largestUnit"), - js_string!("month"), - context, - )?; - // iv. Let untilResult be ? CalendarDateUntil(calendar, plainRelativeTo, newRelativeTo, untilOptions, dateUntil). - let until_result = calendar::calendar_date_until( - &calendar, + let until_result = plain_relative_to.calendar().date_until( &plain_relative_to, &new_relative_to, - &until_options.into(), + TemporalUnit::Month, context, )?; // v. Let oneYearMonths be untilResult.[[Months]]. - let one_year_months = until_result.months(); + let one_year_months = until_result.date.months; // vi. Set plainRelativeTo to newRelativeTo. plain_relative_to = new_relative_to; @@ -1010,41 +791,42 @@ impl DurationRecord { months += one_year_months; } // f. Return ? CreateDateDurationRecord(0, months, weeks, days). - return Ok(DateDuration::new(years, months, self.weeks(), self.days())); + return Ok(DateDuration::new( + years, + months, + self.date.weeks, + self.date.days, + )); // 12. If largestUnit is "week", then } else if largest_unit == TemporalUnit::Week { // a. If years = 0 and months = 0, return ! CreateDateDurationRecord(0, 0, weeks, days). - if self.years() == 0f64 && self.months() == 0f64 { - return Ok(DateDuration::new(0f64, 0f64, self.weeks(), self.days())); + if self.date.years == 0f64 && self.date.months == 0f64 { + return Ok(DateDuration::new( + 0f64, + 0f64, + self.date.weeks, + self.date.days, + )); } // b. If calendar is undefined, then - let (mut plain_relative_to, calendar) = - if let Some(plain_relative_to) = plain_relative_to { - ( - PlainDate::new(plain_relative_to.inner, plain_relative_to.calendar.clone()), - plain_relative_to.calendar.clone(), - ) - } else { - // i. Throw a RangeError exception. - return Err(JsNativeError::range() - .with_message("Calendar cannot be undefined.") - .into()); - }; + let Some(mut plain_relative_to) = plain_relative_to.map(Clone::clone) else { + // i. Throw a RangeError exception. + return Err(TemporalError::range().with_message("Calendar cannot be undefined.")); + }; // c. If calendar is an Object, then // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). // d. Else, // i. Let dateAdd be unused. - let mut years = self.years(); - let mut days = self.days(); + let mut years = self.date.years; + let mut days = self.date.days; // e. Repeat, while years ≠ 0, while years != 0f64 { // i. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneYear, dateAdd). - let move_result = - super::move_relative_date(&calendar, &plain_relative_to, &one_year, context)?; + let move_result = plain_relative_to.move_relative_date(&one_year, context)?; // ii. Set plainRelativeTo to moveResult.[[RelativeTo]]. plain_relative_to = move_result.0; @@ -1054,12 +836,11 @@ impl DurationRecord { years -= sign; } - let mut months = self.months(); + let mut months = self.date.months; // f. Repeat, while months ≠ 0, while months != 0f64 { // i. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneMonth, dateAdd). - let move_result = - super::move_relative_date(&calendar, &plain_relative_to, &one_month, context)?; + let move_result = plain_relative_to.move_relative_date(&one_month, context)?; // ii. Set plainRelativeTo to moveResult.[[RelativeTo]]. plain_relative_to = move_result.0; // iii. Set days to days + moveResult.[[Days]]. @@ -1068,12 +849,12 @@ impl DurationRecord { months -= sign; } // g. Return ? CreateDateDurationRecord(0, 0, weeks, days). - return Ok(DateDuration::new(0f64, 0f64, self.weeks(), days)); + return Ok(DateDuration::new(0f64, 0f64, self.date.weeks(), days)); } // 13. If years = 0, and months = 0, and weeks = 0, return ! CreateDateDurationRecord(0, 0, 0, days). - if self.years() == 0f64 && self.months() == 0f64 && self.weeks() == 0f64 { - return Ok(DateDuration::new(0f64, 0f64, 0f64, self.days())); + if self.date.years == 0f64 && self.date.months == 0f64 && self.date.weeks == 0f64 { + return Ok(DateDuration::new(0f64, 0f64, 0f64, self.date.days)); } // NOTE: Move 8 down to past 13 as we only use one_week after making it past 13. @@ -1081,16 +862,9 @@ impl DurationRecord { let one_week = Self::one_week(sign); // 14. If calendar is undefined, then - let (mut plain_relative_to, calendar) = if let Some(plain_relative_to) = plain_relative_to { - ( - PlainDate::new(plain_relative_to.inner, plain_relative_to.calendar.clone()), - plain_relative_to.calendar.clone(), - ) - } else { + let Some(mut plain_relative_to) = plain_relative_to.map(Clone::clone) else { // a. Throw a RangeError exception. - return Err(JsNativeError::range() - .with_message("Calendar cannot be undefined.") - .into()); + return Err(TemporalError::range().with_message("Calendar cannot be undefined.")); }; // 15. If calendar is an Object, then @@ -1098,13 +872,12 @@ impl DurationRecord { // 16. Else, // a. Let dateAdd be unused. - let mut years = self.years(); - let mut days = self.days(); + let mut years = self.date.years; + let mut days = self.date.days; // a. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneYear, dateAdd). while years != 0f64 { // a. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneYear, dateAdd). - let move_result = - super::move_relative_date(&calendar, &plain_relative_to, &one_year, context)?; + let move_result = plain_relative_to.move_relative_date(&one_year, context)?; // b. Set plainRelativeTo to moveResult.[[RelativeTo]]. plain_relative_to = move_result.0; @@ -1114,12 +887,11 @@ impl DurationRecord { years -= sign; } - let mut months = self.months(); + let mut months = self.date.months; // 18. Repeat, while months ≠ 0, while months != 0f64 { // a. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneMonth, dateAdd). - let move_result = - super::move_relative_date(&calendar, &plain_relative_to, &one_month, context)?; + let move_result = plain_relative_to.move_relative_date(&one_month, context)?; // b. Set plainRelativeTo to moveResult.[[RelativeTo]]. plain_relative_to = move_result.0; // c. Set days to days +moveResult.[[Days]]. @@ -1128,12 +900,11 @@ impl DurationRecord { months -= sign; } - let mut weeks = self.weeks(); + let mut weeks = self.date.weeks; // 19. Repeat, while weeks ≠ 0, while weeks != 0f64 { // a. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneWeek, dateAdd). - let move_result = - super::move_relative_date(&calendar, &plain_relative_to, &one_week, context)?; + let move_result = plain_relative_to.move_relative_date(&one_week, context)?; // b. Set plainRelativeTo to moveResult.[[RelativeTo]]. plain_relative_to = move_result.0; // c. Set days to days + moveResult.[[Days]]. @@ -1146,37 +917,38 @@ impl DurationRecord { Ok(DateDuration::new(0f64, 0f64, 0f64, days)) } + // TODO: Move to DateDuration /// `BalanceDateDurationRelative` #[allow(unused)] - pub(crate) fn balance_date_duration_relative( - &mut self, + pub fn balance_date_duration_relative( + &self, largest_unit: TemporalUnit, - relative_to: &JsValue, - context: &mut Context, - ) -> JsResult<()> { + plain_relative_to: Option<&Date>, + context: &mut dyn Any, + ) -> TemporalResult { + let mut result = self.date; + // 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; + let all_zero = self.date.years == 0.0 + && self.date.months == 0.0 + && self.date.weeks == 0.0 + && self.date.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(()); + return Ok(result); } } - // 4. If relativeTo is undefined, then - if relative_to.is_undefined() { + // 4. If plainRelativeTo is undefined, then + let Some(mut plain_relative_to) = plain_relative_to.map(Clone::clone) else { // a. Throw a RangeError exception. - return Err(JsNativeError::range() - .with_message("relativeTo cannot be undefined.") - .into()); - } + return Err(TemporalError::range().with_message("relativeTo cannot be undefined.")); + }; // 5. Let sign be ! DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0). // 6. Assert: sign ≠ 0. @@ -1189,11 +961,7 @@ impl DurationRecord { // 9. Let oneWeek be ! CreateTemporalDuration(0, 0, sign, 0, 0, 0, 0, 0, 0, 0). let one_week = Self::one_week(sign); - // 10. Set relativeTo to ? ToTemporalDate(relativeTo). - let mut relative_to = to_temporal_date(relative_to, None, context)?; - - // 11. Let calendar be relativeTo.[[Calendar]]. - let calendar = &relative_to.calendar.clone(); + // 10. Let calendar be relativeTo.[[Calendar]]. match largest_unit { // 12. If largestUnit is "year", then @@ -1202,31 +970,28 @@ impl DurationRecord { // 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, context)?; + // c. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd). // 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; + let (mut new_relative_to, mut one_year_days) = + plain_relative_to.move_relative_date(&one_year, context)?; // f. Repeat, while abs(days) ≥ abs(oneYearDays), - while self.days().abs() >= one_year_days.abs() { + while result.days().abs() >= one_year_days.abs() { // i. Set days to days - oneYearDays. - self.set_days(self.days() - one_year_days); + result.days -= one_year_days; // ii. Set years to years + sign. - self.set_years(self.years() + sign); + result.years += sign; // iii. Set relativeTo to newRelativeTo. - let relative_to = new_relative; + plain_relative_to = new_relative_to; // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd). - let move_result = - super::move_relative_date(calendar, &relative_to, &one_year, context)?; + let move_result = plain_relative_to.move_relative_date(&one_year, context)?; // v. Set newRelativeTo to moveResult.[[RelativeTo]]. - new_relative = move_result.0; + new_relative_to = move_result.0; // vi. Set oneYearDays to moveResult.[[Days]]. one_year_days = move_result.1; } @@ -1235,20 +1000,19 @@ impl DurationRecord { // h. Set newRelativeTo to moveResult.[[RelativeTo]]. // i. Let oneMonthDays be moveResult.[[Days]]. let (mut new_relative_to, mut one_month_days) = - super::move_relative_date(calendar, &relative_to, &one_month, context)?; + plain_relative_to.move_relative_date(&one_month, context)?; // j. Repeat, while abs(days) ≥ abs(oneMonthDays), - while self.days().abs() >= one_month_days.abs() { + while result.days().abs() >= one_month_days.abs() { // i. Set days to days - oneMonthDays. - self.set_days(self.days() - one_month_days); + result.days -= one_month_days; // ii. Set months to months + sign. - self.set_months(self.months() + sign); - // iii. Set relativeTo to newRelativeTo. + result.months += sign; - let relative_to = new_relative.clone(); + // iii. Set relativeTo to newRelativeTo. + plain_relative_to = new_relative_to; // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). - let move_result = - super::move_relative_date(calendar, &relative_to, &one_month, context)?; + let move_result = plain_relative_to.move_relative_date(&one_month, context)?; // v. Set newRelativeTo to moveResult.[[RelativeTo]]. new_relative_to = move_result.0; @@ -1257,11 +1021,10 @@ impl DurationRecord { } // k. Set newRelativeTo to ? CalendarDateAdd(calendar, relativeTo, oneYear, undefined, dateAdd). - new_relative_to = calendar::calendar_date_add( - calendar, - &relative_to, + new_relative_to = plain_relative_to.calendar().date_add( + &plain_relative_to, &one_year, - &JsValue::undefined(), + ArithmeticOverflow::Constrain, context, )?; @@ -1269,65 +1032,50 @@ impl DurationRecord { // i. Let dateUntil be ? GetMethod(calendar, "dateUntil"). // m. Else, // i. Let dateUntil be unused. - // n. Let untilOptions be OrdinaryObjectCreate(null). - let until_options = JsObject::with_null_proto(); // o. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). - until_options.create_data_property_or_throw( - utf16!("largestUnit"), - js_string!("month"), - context, - )?; // p. Let untilResult be ? CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil). - let until_result = calendar::calendar_date_until( - calendar, - &relative_to, + let mut until_result = plain_relative_to.calendar().date_until( + &plain_relative_to, &new_relative_to, - &until_options.into(), + TemporalUnit::Month, context, )?; // q. Let oneYearMonths be untilResult.[[Months]]. - let mut one_year_months = until_result.months(); + let mut one_year_months = until_result.date.months(); // r. Repeat, while abs(months) ≥ abs(oneYearMonths), - while self.months().abs() >= one_year_months.abs() { + while result.months().abs() >= one_year_months.abs() { // i. Set months to months - oneYearMonths. - self.set_months(self.months() - one_year_months); + result.months -= one_year_months; // ii. Set years to years + sign. - self.set_years(self.years() + sign); + result.years += sign; // iii. Set relativeTo to newRelativeTo. - relative_to = new_relative_to; + plain_relative_to = new_relative_to; // iv. Set newRelativeTo to ? CalendarDateAdd(calendar, relativeTo, oneYear, undefined, dateAdd). - new_relative_to = calendar::calendar_date_add( - calendar, - &relative_to, + new_relative_to = plain_relative_to.calendar().date_add( + &plain_relative_to, &one_year, - &JsValue::undefined(), + ArithmeticOverflow::Constrain, context, )?; // v. Set untilOptions to OrdinaryObjectCreate(null). - let until_options = JsObject::with_null_proto(); // vi. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). - until_options.create_data_property_or_throw( - utf16!("largestUnit"), - js_string!("month"), - context, - )?; // vii. Set untilResult to ? CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil). - let until_result = calendar::calendar_date_until( - calendar, - &relative_to, + until_result = plain_relative_to.calendar().date_until( + &plain_relative_to, &new_relative_to, - &until_options.into(), + TemporalUnit::Month, context, )?; + // viii. Set oneYearMonths to untilResult.[[Months]]. - one_year_months = until_result.months(); + one_year_months = until_result.date.months(); } } // 13. Else if largestUnit is "month", then @@ -1341,22 +1089,22 @@ impl DurationRecord { // d. Let newRelativeTo be moveResult.[[RelativeTo]]. // e. Let oneMonthDays be moveResult.[[Days]]. let (mut new_relative_to, mut one_month_days) = - super::move_relative_date(calendar, &relative_to, &one_month, context)?; + plain_relative_to.move_relative_date(&one_month, context)?; // f. Repeat, while abs(days) ≥ abs(oneMonthDays), - while self.days().abs() >= one_month_days.abs() { + while result.days().abs() >= one_month_days.abs() { // i. Set days to days - oneMonthDays. - self.set_days(self.days() - one_month_days); + result.days -= one_month_days; // ii. Set months to months + sign. - self.set_months(self.months() + sign); + result.months += sign; // iii. Set relativeTo to newRelativeTo. - relative_to = new_relative_to; + plain_relative_to = new_relative_to; // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). - let move_result = - super::move_relative_date(calendar, &relative_to, &one_month, context)?; + let move_result = plain_relative_to.move_relative_date(&one_month, context)?; + // v. Set newRelativeTo to moveResult.[[RelativeTo]]. new_relative_to = move_result.0; // vi. Set oneMonthDays to moveResult.[[Days]]. @@ -1375,19 +1123,19 @@ impl DurationRecord { // e. Let newRelativeTo be moveResult.[[RelativeTo]]. // f. Let oneWeekDays be moveResult.[[Days]]. let (mut new_relative_to, mut one_week_days) = - super::move_relative_date(calendar, &relative_to, &one_week, context)?; + plain_relative_to.move_relative_date(&one_week, context)?; // g. Repeat, while abs(days) ≥ abs(oneWeekDays), - while self.days().abs() >= one_week_days.abs() { + while result.days().abs() >= one_week_days.abs() { // i. Set days to days - oneWeekDays. - self.set_days(self.days() - one_week_days); + result.days -= one_week_days; // ii. Set weeks to weeks + sign. - self.set_weeks(self.weeks() + sign); + result.weeks += sign; // iii. Set relativeTo to newRelativeTo. - relative_to = new_relative_to; + plain_relative_to = new_relative_to; // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd). - let move_result = - super::move_relative_date(calendar, &relative_to, &one_week, context)?; + let move_result = plain_relative_to.move_relative_date(&one_week, context)?; + // v. Set newRelativeTo to moveResult.[[RelativeTo]]. new_relative_to = move_result.0; // vi. Set oneWeekDays to moveResult.[[Days]]. @@ -1398,27 +1146,23 @@ impl DurationRecord { } // 15. Return ! CreateDateDurationRecord(years, months, weeks, days). - Ok(()) + Ok(result) } // TODO: Refactor relative_to's into a RelativeTo struct? /// Abstract Operation 7.5.26 `RoundDuration ( years, months, weeks, days, hours, minutes, /// seconds, milliseconds, microseconds, nanoseconds, increment, unit, /// roundingMode [ , plainRelativeTo [, zonedRelativeTo [, precalculatedDateTime]]] )` - pub(crate) fn round_duration( + pub fn round_duration( &self, unbalance_date_duration: DateDuration, increment: f64, unit: TemporalUnit, - rounding_mode: RoundingMode, - relative_targets: ( - Option<&PlainDate>, - Option<&ZonedDateTime>, - Option<&PlainDateTime>, - ), - context: &mut Context, - ) -> JsResult<(Self, f64)> { - let mut result = DurationRecord::new(unbalance_date_duration, self.time()); + rounding_mode: TemporalRoundingMode, + relative_targets: (Option<&Date>, Option<&ZonedDateTime>, Option<&DateTime>), + context: &mut dyn Any, + ) -> TemporalResult<(Self, f64)> { + let mut result = Duration::new_unchecked(unbalance_date_duration, self.time); // 1. If plainRelativeTo is not present, set plainRelativeTo to undefined. let plain_relative_to = relative_targets.0; @@ -1433,15 +1177,14 @@ impl DurationRecord { if plain_relative_to.is_none() => { // a. Throw a RangeError exception. - return Err(JsNativeError::range() - .with_message("plainRelativeTo canot be undefined with given TemporalUnit") - .into()); + return Err(TemporalError::range() + .with_message("plainRelativeTo canot be undefined with given TemporalUnit")); } // 5. If unit is one of "year", "month", "week", or "day", then TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week | TemporalUnit::Day => { // a. Let nanoseconds be TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds). let nanos = - Self::from_day_and_time(0.0, result.time()).total_duration_nanoseconds(0.0); + Self::from_day_and_time(0.0, *result.time()).total_duration_nanoseconds(0.0); // b. If zonedRelativeTo is not undefined, then // i. Let intermediate be ? MoveRelativeZonedDateTime(zonedRelativeTo, years, months, weeks, days, precalculatedPlainDateTime). @@ -1450,27 +1193,28 @@ impl DurationRecord { // c. Else, // i. Let fractionalDays be days + nanoseconds / nsPerDay. let frac_days = if zoned_relative_to.is_none() { - result.days() + nanos / NS_PER_DAY as f64 + result.date.days + nanos / NS_PER_DAY as f64 } else { // implementation of b: i-iii needed. - return Err(JsNativeError::range() - .with_message("Not yet implemented.") - .into()); + return Err(TemporalError::range().with_message("Not yet implemented.")); }; // d. Set days, hours, minutes, seconds, milliseconds, microseconds, and nanoseconds to 0. - result.set_days(0.0); - result.set_time_duration(TimeDuration::default()); + result.date.days = 0f64; + result.time = TimeDuration::default(); // e. Assert: fractionalSeconds is not used below. (Some(frac_days), None) } // 6. Else, _ => { // a. Let fractionalSeconds be nanoseconds × 10-9 + microseconds × 10-6 + milliseconds × 10-3 + seconds. - let frac_secs = result.nanoseconds().mul_add( + let frac_secs = result.time.nanoseconds.mul_add( 1_000_000_000f64, - result.microseconds().mul_add( + result.time.microseconds.mul_add( 1_000_000f64, - result.milliseconds().mul_add(1_000f64, result.seconds()), + result + .time + .milliseconds + .mul_add(1_000f64, result.time.seconds), ), ); @@ -1489,11 +1233,11 @@ impl DurationRecord { let plain_relative_to = plain_relative_to.expect("this must exist."); // a. Let calendar be plainRelativeTo.[[Calendar]]. - let calendar = &plain_relative_to.calendar; + let calendar = plain_relative_to.calendar(); // b. Let yearsDuration be ! CreateTemporalDuration(years, 0, 0, 0, 0, 0, 0, 0, 0, 0). - let years = DateDuration::new(result.years(), 0.0, 0.0, 0.0); - let years_duration = DurationRecord::new(years, TimeDuration::default()); + let years = DateDuration::new(result.date.years, 0.0, 0.0, 0.0); + let years_duration = Duration::new_unchecked(years, TimeDuration::default()); // c. If calendar is an Object, then // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). @@ -1501,32 +1245,32 @@ impl DurationRecord { // i. Let dateAdd be unused. // e. Let yearsLater be ? AddDate(calendar, plainRelativeTo, yearsDuration, undefined, dateAdd). - let years_later = plain_date::add_date( - calendar, - plain_relative_to, + let years_later = plain_relative_to.contextual_add_date( &years_duration, - &JsValue::undefined(), + ArithmeticOverflow::Constrain, context, )?; // f. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0). - let years_months_weeks = Self::new( - DateDuration::new(result.years(), result.months(), result.weeks(), 0.0), + let years_months_weeks = Self::new_unchecked( + DateDuration::new( + result.date.years, + result.date.months, + result.date.weeks, + 0.0, + ), TimeDuration::default(), ); // g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd). - let years_months_weeks_later = plain_date::add_date( - calendar, - plain_relative_to, + let years_months_weeks_later = plain_relative_to.contextual_add_date( &years_months_weeks, - &JsValue::undefined(), + ArithmeticOverflow::Constrain, context, )?; // h. Let monthsWeeksInDays be DaysUntil(yearsLater, yearsMonthsWeeksLater). - let months_weeks_in_days = - super::days_until(&years_later, &years_months_weeks_later); + let months_weeks_in_days = years_later.days_until(&years_months_weeks_later); // i. Set plainRelativeTo to yearsLater. let plain_relative_to = years_later; @@ -1535,30 +1279,28 @@ impl DurationRecord { frac_days += f64::from(months_weeks_in_days); // k. Let isoResult be ! AddISODate(plainRelativeTo.[[ISOYear]]. plainRelativeTo.[[ISOMonth]], plainRelativeTo.[[ISODay]], 0, 0, 0, truncate(fractionalDays), "constrain"). - let iso_result = plain_relative_to.inner.add_iso_date( - DateDuration::new(0.0, 0.0, 0.0, frac_days.trunc()), + let iso_result = plain_relative_to.iso_date().add_iso_date( + &DateDuration::new(0.0, 0.0, 0.0, frac_days.trunc()), ArithmeticOverflow::Constrain, )?; - // l. Let wholeDaysLater be ? CreateTemporalDate(isoResult.[[Year]], isoResult.[[Month]], isoResult.[[Day]], calendar). - let whole_days_later = PlainDate::new(iso_result, calendar.clone()); + // l. Let wholeDaysLater be ? CreateDate(isoResult.[[Year]], isoResult.[[Month]], isoResult.[[Day]], calendar). + let whole_days_later = Date::new_unchecked(iso_result, calendar.clone()); // m. Let untilOptions be OrdinaryObjectCreate(null). // n. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "year"). // o. Let timePassed be ? DifferenceDate(calendar, plainRelativeTo, wholeDaysLater, untilOptions). - let time_passed = plain_date::difference_date( - calendar, - &plain_relative_to, + let time_passed = plain_relative_to.contextual_difference_date( &whole_days_later, TemporalUnit::Year, context, )?; // p. Let yearsPassed be timePassed.[[Years]]. - let years_passed = time_passed.years(); + let years_passed = time_passed.date.years(); // q. Set years to years + yearsPassed. - result.set_years(result.years() + years_passed); + result.date.years += years_passed; // r. Let yearsDuration be ! CreateTemporalDuration(yearsPassed, 0, 0, 0, 0, 0, 0, 0, 0, 0). let years_duration = Self::one_year(years_passed); @@ -1566,12 +1308,8 @@ impl DurationRecord { // s. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, yearsDuration, dateAdd). // t. Set plainRelativeTo to moveResult.[[RelativeTo]]. // u. Let daysPassed be moveResult.[[Days]]. - let (plain_relative_to, days_passed) = super::move_relative_date( - calendar, - &plain_relative_to, - &years_duration, - context, - )?; + let (plain_relative_to, days_passed) = + plain_relative_to.move_relative_date(&years_duration, context)?; // v. Set fractionalDays to fractionalDays - daysPassed. frac_days -= days_passed; @@ -1585,22 +1323,19 @@ impl DurationRecord { // y. Set moveResult to ? MoveRelativeDate(calendar, plainRelativeTo, oneYear, dateAdd). // z. Let oneYearDays be moveResult.[[Days]]. let (_, one_year_days) = - super::move_relative_date(calendar, &plain_relative_to, &one_year, context)?; + plain_relative_to.move_relative_date(&one_year, context)?; // aa. Let fractionalYears be years + fractionalDays / abs(oneYearDays). - let frac_years = result.years() + (frac_days / one_year_days.abs()); + let frac_years = result.date.years() + (frac_days / one_year_days.abs()); // ab. Set years to RoundNumberToIncrement(fractionalYears, increment, roundingMode). - result.set_years(round_number_to_increment( - frac_years, - increment, - rounding_mode, - )); + result.date.years = + utils::round_number_to_increment(frac_years, increment, rounding_mode); // ac. Set total to fractionalYears. // ad. Set months and weeks to 0. - result.set_months(0.0); - result.set_weeks(0.0); + result.date.months = 0f64; + result.date.weeks = 0f64; frac_years } @@ -1611,12 +1346,11 @@ impl DurationRecord { // a. Let calendar be plainRelativeTo.[[Calendar]]. let plain_relative_to = plain_relative_to.expect("this must exist."); - let calendar = &plain_relative_to.calendar; // b. Let yearsMonths be ! CreateTemporalDuration(years, months, 0, 0, 0, 0, 0, 0, 0, 0). let years_months = Self::from_date_duration(DateDuration::new( - result.years(), - result.months(), + result.date.years(), + result.date.months(), 0.0, 0.0, )); @@ -1627,34 +1361,29 @@ impl DurationRecord { // i. Let dateAdd be unused. // e. Let yearsMonthsLater be ? AddDate(calendar, plainRelativeTo, yearsMonths, undefined, dateAdd). - let years_months_later = plain_date::add_date( - calendar, - plain_relative_to, + let years_months_later = plain_relative_to.contextual_add_date( &years_months, - &JsValue::undefined(), + ArithmeticOverflow::Constrain, context, )?; // f. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0). let years_months_weeks = Self::from_date_duration(DateDuration::new( - result.years(), - result.months(), - result.weeks(), + result.date.years(), + result.date.months(), + result.date.weeks(), 0.0, )); // g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd). - let years_months_weeks_later = plain_date::add_date( - calendar, - plain_relative_to, + let years_months_weeks_later = plain_relative_to.contextual_add_date( &years_months_weeks, - &JsValue::undefined(), + ArithmeticOverflow::Constrain, context, )?; // h. Let weeksInDays be DaysUntil(yearsMonthsLater, yearsMonthsWeeksLater). - let weeks_in_days = - super::days_until(&years_months_later, &years_months_weeks_later); + let weeks_in_days = years_months_later.days_until(&years_months_weeks_later); // i. Set plainRelativeTo to yearsMonthsLater. let plain_relative_to = years_months_later; @@ -1672,23 +1401,19 @@ impl DurationRecord { // n. Set plainRelativeTo to moveResult.[[RelativeTo]]. // o. Let oneMonthDays be moveResult.[[Days]]. let (mut plain_relative_to, mut one_month_days) = - super::move_relative_date(calendar, &plain_relative_to, &one_month, context)?; + plain_relative_to.move_relative_date(&one_month, context)?; // p. Repeat, while abs(fractionalDays) ≥ abs(oneMonthDays), while frac_days.abs() >= one_month_days.abs() { // i. Set months to months + sign. - result.set_months(result.months() + sign); + result.date.months += sign; // ii. Set fractionalDays to fractionalDays - oneMonthDays. frac_days -= one_month_days; // iii. Set moveResult to ? MoveRelativeDate(calendar, plainRelativeTo, oneMonth, dateAdd). - let move_result = super::move_relative_date( - calendar, - &plain_relative_to, - &one_month, - context, - )?; + let move_result = plain_relative_to.move_relative_date(&one_month, context)?; + // iv. Set plainRelativeTo to moveResult.[[RelativeTo]]. plain_relative_to = move_result.0; // v. Set oneMonthDays to moveResult.[[Days]]. @@ -1696,18 +1421,15 @@ impl DurationRecord { } // q. Let fractionalMonths be months + fractionalDays / abs(oneMonthDays). - let frac_months = result.months() + frac_days / one_month_days.abs(); + let frac_months = result.date.months() + frac_days / one_month_days.abs(); // r. Set months to RoundNumberToIncrement(fractionalMonths, increment, roundingMode). - result.set_months(round_number_to_increment( - frac_months, - increment, - rounding_mode, - )); + result.date.months = + utils::round_number_to_increment(frac_months, increment, rounding_mode); // s. Set total to fractionalMonths. // t. Set weeks to 0. - result.set_weeks(0.0); + result.date.weeks = 0.0; frac_months } // 10. Else if unit is "week", then @@ -1716,7 +1438,6 @@ impl DurationRecord { frac_days.expect("assert that fractionalDays exists for TemporalUnit::Month"); // a. Let calendar be plainRelativeTo.[[Calendar]]. let plain_relative_to = plain_relative_to.expect("date must exist given Week"); - let calendar = &plain_relative_to.calendar; // b. If fractionalDays < 0, let sign be -1; else, let sign be 1. let sign = if frac_days < 0.0 { -1f64 } else { 1f64 }; @@ -1733,23 +1454,18 @@ impl DurationRecord { // g. Set plainRelativeTo to moveResult.[[RelativeTo]]. // h. Let oneWeekDays be moveResult.[[Days]]. let (mut plain_relative_to, mut one_week_days) = - super::move_relative_date(calendar, plain_relative_to, &one_week, context)?; + plain_relative_to.move_relative_date(&one_week, context)?; // i. Repeat, while abs(fractionalDays) ≥ abs(oneWeekDays), while frac_days.abs() >= one_week_days.abs() { // i. Set weeks to weeks + sign. - result.set_weeks(result.weeks() + sign); + result.date.weeks += sign; // ii. Set fractionalDays to fractionalDays - oneWeekDays. frac_days -= one_week_days; // iii. Set moveResult to ? MoveRelativeDate(calendar, plainRelativeTo, oneWeek, dateAdd). - let move_result = super::move_relative_date( - calendar, - &plain_relative_to, - &one_week, - context, - )?; + let move_result = plain_relative_to.move_relative_date(&one_week, context)?; // iv. Set plainRelativeTo to moveResult.[[RelativeTo]]. plain_relative_to = move_result.0; @@ -1758,14 +1474,11 @@ impl DurationRecord { } // j. Let fractionalWeeks be weeks + fractionalDays / abs(oneWeekDays). - let frac_weeks = result.weeks() + frac_days / one_week_days.abs(); + let frac_weeks = result.date.weeks() + frac_days / one_week_days.abs(); // k. Set weeks to RoundNumberToIncrement(fractionalWeeks, increment, roundingMode). - result.set_weeks(round_number_to_increment( - frac_weeks, - increment, - rounding_mode, - )); + result.date.weeks = + utils::round_number_to_increment(frac_weeks, increment, rounding_mode); // l. Set total to fractionalWeeks. frac_weeks } @@ -1775,11 +1488,8 @@ impl DurationRecord { frac_days.expect("assert that fractionalDays exists for TemporalUnit::Day"); // a. Set days to RoundNumberToIncrement(fractionalDays, increment, roundingMode). - result.set_days(round_number_to_increment( - frac_days, - increment, - rounding_mode, - )); + result.date.days = + utils::round_number_to_increment(frac_days, increment, rounding_mode); // b. Set total to fractionalDays. frac_days } @@ -1788,12 +1498,14 @@ impl DurationRecord { let frac_secs = frac_secs.expect("Assert fractionSeconds exists for Temporal::Hour"); // a. Let fractionalHours be (fractionalSeconds / 60 + minutes) / 60 + hours. - let frac_hours = (frac_secs / 60f64 + result.minutes()) / 60f64 + result.hours(); + let frac_hours = + (frac_secs / 60f64 + result.time.minutes) / 60f64 + result.time.hours; // b. Set hours to RoundNumberToIncrement(fractionalHours, increment, roundingMode). - let rounded_hours = round_number_to_increment(frac_hours, increment, rounding_mode); + let rounded_hours = + utils::round_number_to_increment(frac_hours, increment, rounding_mode); // c. Set total to fractionalHours. // d. Set minutes, seconds, milliseconds, microseconds, and nanoseconds to 0. - result.set_time_duration(TimeDuration::new(rounded_hours, 0.0, 0.0, 0.0, 0.0, 0.0)); + result.time = TimeDuration::new(rounded_hours, 0.0, 0.0, 0.0, 0.0, 0.0); frac_hours } // 13. Else if unit is "minute", then @@ -1801,20 +1513,14 @@ impl DurationRecord { let frac_secs = frac_secs.expect("Assert fractionSeconds exists for Temporal::Hour"); // a. Let fractionalMinutes be fractionalSeconds / 60 + minutes. - let frac_minutes = frac_secs / 60f64 + result.minutes(); + let frac_minutes = frac_secs / 60f64 + result.time.minutes; // b. Set minutes to RoundNumberToIncrement(fractionalMinutes, increment, roundingMode). let rounded_minutes = - round_number_to_increment(frac_minutes, increment, rounding_mode); + utils::round_number_to_increment(frac_minutes, increment, rounding_mode); // c. Set total to fractionalMinutes. // d. Set seconds, milliseconds, microseconds, and nanoseconds to 0. - result.set_time_duration(TimeDuration::new( - result.hours(), - rounded_minutes, - 0.0, - 0.0, - 0.0, - 0.0, - )); + result.time = + TimeDuration::new(result.time.hours, rounded_minutes, 0.0, 0.0, 0.0, 0.0); frac_minutes } @@ -1823,72 +1529,65 @@ impl DurationRecord { let frac_secs = frac_secs.expect("Assert fractionSeconds exists for Temporal::Second"); // a. Set seconds to RoundNumberToIncrement(fractionalSeconds, increment, roundingMode). - result.set_seconds(round_number_to_increment( - frac_secs, - increment, - rounding_mode, - )); + result.time.seconds = + utils::round_number_to_increment(frac_secs, increment, rounding_mode); // b. Set total to fractionalSeconds. // c. Set milliseconds, microseconds, and nanoseconds to 0. - result.set_milliseconds(0.0); - result.set_microseconds(0.0); - result.set_nanoseconds(0.0); + result.time.milliseconds = 0.0; + result.time.microseconds = 0.0; + result.time.nanoseconds = 0.0; frac_secs } // 15. Else if unit is "millisecond", then TemporalUnit::Millisecond => { // a. Let fractionalMilliseconds be nanoseconds × 10-6 + microseconds × 10-3 + milliseconds. - let fraction_millis = result.nanoseconds().mul_add( + let fraction_millis = result.time.nanoseconds.mul_add( 1_000_000f64, result - .microseconds() - .mul_add(1_000f64, result.milliseconds()), + .time + .microseconds + .mul_add(1_000f64, result.time.milliseconds), ); // b. Set milliseconds to RoundNumberToIncrement(fractionalMilliseconds, increment, roundingMode). - result.set_milliseconds(round_number_to_increment( - fraction_millis, - increment, - rounding_mode, - )); + result.time.milliseconds = + utils::round_number_to_increment(fraction_millis, increment, rounding_mode); // c. Set total to fractionalMilliseconds. // d. Set microseconds and nanoseconds to 0. - result.set_microseconds(0.0); - result.set_nanoseconds(0.0); + result.time.microseconds = 0.0; + result.time.nanoseconds = 0.0; fraction_millis } // 16. Else if unit is "microsecond", then TemporalUnit::Microsecond => { // a. Let fractionalMicroseconds be nanoseconds × 10-3 + microseconds. let frac_micros = result - .nanoseconds() - .mul_add(1_000f64, result.microseconds()); + .time + .nanoseconds + .mul_add(1_000f64, result.time.microseconds); // b. Set microseconds to RoundNumberToIncrement(fractionalMicroseconds, increment, roundingMode). - result.set_microseconds(round_number_to_increment( - frac_micros, - increment, - rounding_mode, - )); + result.time.microseconds = + utils::round_number_to_increment(frac_micros, increment, rounding_mode); // c. Set total to fractionalMicroseconds. // d. Set nanoseconds to 0. - result.set_nanoseconds(0.0); + result.time.nanoseconds = 0.0; frac_micros } // 17. Else, TemporalUnit::Nanosecond => { // a. Assert: unit is "nanosecond". // b. Set total to nanoseconds. - let total = result.nanoseconds(); + let total = result.time.nanoseconds; // c. Set nanoseconds to RoundNumberToIncrement(nanoseconds, increment, roundingMode). - result.set_nanoseconds(round_number_to_increment( - result.nanoseconds(), + result.time.nanoseconds = utils::round_number_to_increment( + result.time.nanoseconds, increment, rounding_mode, - )); + ); total } @@ -1899,74 +1598,93 @@ impl DurationRecord { // 19. Return the Record { [[DurationRecord]]: duration, [[Total]]: total }. Ok((result, total)) } +} - /// 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() +// ==== Public Duration methods ==== + +impl Duration { + /// Returns the absolute value of `Duration`. + #[inline] + #[must_use] + pub fn abs(&self) -> Self { + Self { + date: DateDuration::new( + self.date.years.abs(), + self.date.months.abs(), + self.date.weeks.abs(), + self.date.days.abs(), + ), + time: TimeDuration::new( + self.time.hours.abs(), + self.time.minutes.abs(), + self.time.seconds.abs(), + self.time.milliseconds.abs(), + self.time.microseconds.abs(), + self.time.nanoseconds.abs(), + ), + } + } + + /// 7.5.10 `DurationSign ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds )` + /// + /// Determines the sign for the current self. + #[inline] + #[must_use] + pub 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; } - _ => return Ok(()), - }; + } + // 2. Return 0. + 0 + } - 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(()) + /// 7.5.12 `DefaultTemporalLargestUnit ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds )` + #[inline] + #[must_use] + pub 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, + _ => {} + } } - // 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 - }; + TemporalUnit::Nanosecond + } - // 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()) + /// Abstract Operation 7.5.17 `BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit )` + #[inline] + pub fn balance_time_duration( + &self, + largest_unit: TemporalUnit, + ) -> TemporalResult<(f64, TimeDuration)> { + // 1. Let balanceResult be ? BalancePossiblyInfiniteDuration(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit, relativeTo). + let balance_result = self.balance_possibly_infinite_time_duration(largest_unit)?; + + // 2. If balanceResult is positive overflow or negative overflow, then + let Some(result) = balance_result else { + // a. Throw a RangeError exception. + return Err(TemporalError::range().with_message("duration overflowed viable range.")); + }; + // 3. Else, + // a. Return balanceResult. + Ok(result) } } diff --git a/boa_temporal/src/error.rs b/boa_temporal/src/error.rs new file mode 100644 index 0000000000..75957a2520 --- /dev/null +++ b/boa_temporal/src/error.rs @@ -0,0 +1,98 @@ +//! An error type for Temporal Errors. + +use core::fmt; + +/// `TemporalError`'s error type. +#[derive(Debug, Default, Clone, Copy)] +pub enum ErrorKind { + /// Error. + #[default] + Generic, + /// TypeError + Type, + /// RangeError + Range, +} + +impl fmt::Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Generic => "Error", + Self::Type => "TypeError", + Self::Range => "RangeError", + } + .fmt(f) + } +} + +/// The error type for `boa_temporal`. +#[derive(Debug, Clone)] +pub struct TemporalError { + kind: ErrorKind, + msg: Box, +} + +impl TemporalError { + fn new(kind: ErrorKind) -> Self { + Self { + kind, + msg: Box::default(), + } + } + + /// Create a generic error + #[must_use] + pub fn general(msg: S) -> Self + where + S: Into>, + { + Self::new(ErrorKind::Generic).with_message(msg) + } + + /// Create a range error. + #[must_use] + pub fn range() -> Self { + Self::new(ErrorKind::Range) + } + + /// Create a type error. + #[must_use] + pub fn r#type() -> Self { + Self::new(ErrorKind::Type) + } + + /// Add a message to the error. + #[must_use] + pub fn with_message(mut self, msg: S) -> Self + where + S: Into>, + { + self.msg = msg.into(); + self + } + + /// Returns this error's kind. + #[must_use] + pub fn kind(&self) -> ErrorKind { + self.kind + } + + /// Returns the error message. + #[must_use] + pub fn message(&self) -> &str { + &self.msg + } +} + +impl fmt::Display for TemporalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.kind)?; + + let msg = self.msg.trim(); + if !msg.is_empty() { + write!(f, ": {msg}")?; + } + + Ok(()) + } +} diff --git a/boa_temporal/src/fields.rs b/boa_temporal/src/fields.rs new file mode 100644 index 0000000000..1c6d2a9411 --- /dev/null +++ b/boa_temporal/src/fields.rs @@ -0,0 +1,487 @@ +//! `TemporalFields` native Rust representation. + +use std::str::FromStr; + +use crate::{error::TemporalError, TemporalResult}; + +use bitflags::bitflags; +// use rustc_hash::FxHashSet; +use tinystr::{TinyAsciiStr, TinyStr16, TinyStr4}; + +bitflags! { + /// FieldMap maps the currently active fields on the `TemporalField` + #[derive(Debug, PartialEq, Eq)] + pub struct FieldMap: u16 { + /// Represents an active `year` field + const YEAR = 0b0000_0000_0000_0001; + /// Represents an active `month` field + const MONTH = 0b0000_0000_0000_0010; + /// Represents an active `monthCode` field + const MONTH_CODE = 0b0000_0000_0000_0100; + /// Represents an active `day` field + const DAY = 0b0000_0000_0000_1000; + /// Represents an active `hour` field + const HOUR = 0b0000_0000_0001_0000; + /// Represents an active `minute` field + const MINUTE = 0b0000_0000_0010_0000; + /// Represents an active `second` field + const SECOND = 0b0000_0000_0100_0000; + /// Represents an active `millisecond` field + const MILLISECOND = 0b0000_0000_1000_0000; + /// Represents an active `microsecond` field + const MICROSECOND = 0b0000_0001_0000_0000; + /// Represents an active `nanosecond` field + const NANOSECOND = 0b0000_0010_0000_0000; + /// Represents an active `offset` field + const OFFSET = 0b0000_0100_0000_0000; + /// Represents an active `era` field + const ERA = 0b0000_1000_0000_0000; + /// Represents an active `eraYear` field + const ERA_YEAR = 0b0001_0000_0000_0000; + /// Represents an active `timeZone` field + const TIME_ZONE = 0b0010_0000_0000_0000; + // NOTE(nekevss): Two bits preserved if needed. + } +} + +/// The post conversion field value. +#[derive(Debug)] +#[allow(variant_size_differences)] +pub enum FieldValue { + /// Designates the values as an integer. + Integer(i32), + /// Designates that the value is undefined. + Undefined, + /// Designates the value as a string. + String(String), +} + +/// The Conversion type of a field. +#[derive(Debug, Clone, Copy)] +pub enum FieldConversion { + /// Designates the Conversion type is `ToIntegerWithTruncation` + ToIntegerWithTruncation, + /// Designates the Conversion type is `ToPositiveIntegerWithTruncation` + ToPositiveIntegerWithTruncation, + /// Designates the Conversion type is `ToPrimitiveRequireString` + ToPrimativeAndRequireString, + /// Designates the Conversion type is nothing + None, +} + +impl FromStr for FieldConversion { + type Err = TemporalError; + + fn from_str(s: &str) -> Result { + match s { + "year" | "hour" | "minute" | "second" | "millisecond" | "microsecond" + | "nanosecond" => Ok(Self::ToIntegerWithTruncation), + "month" | "day" => Ok(Self::ToPositiveIntegerWithTruncation), + "monthCode" | "offset" | "eraYear" => Ok(Self::ToPrimativeAndRequireString), + _ => Err(TemporalError::range() + .with_message(format!("{s} is not a valid TemporalField Property"))), + } + } +} + +/// 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" | `None` | undefined | +#[derive(Debug)] +pub 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, // TODO: Switch to tinystr? + 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 + } +} + +// TODO: Update the below. +impl TemporalFields { + /// Flags a field as being required. + #[inline] + pub fn require_field(&mut self, field: &str) { + match field { + "year" => self.bit_map.set(FieldMap::YEAR, true), + "month" => self.bit_map.set(FieldMap::MONTH, true), + "monthCode" => self.bit_map.set(FieldMap::MONTH_CODE, true), + "day" => self.bit_map.set(FieldMap::DAY, true), + "hour" => self.bit_map.set(FieldMap::HOUR, true), + "minute" => self.bit_map.set(FieldMap::MINUTE, true), + "second" => self.bit_map.set(FieldMap::SECOND, true), + "millisecond" => self.bit_map.set(FieldMap::MILLISECOND, true), + "microsecond" => self.bit_map.set(FieldMap::MICROSECOND, true), + "nanosecond" => self.bit_map.set(FieldMap::NANOSECOND, true), + "offset" => self.bit_map.set(FieldMap::OFFSET, true), + "era" => self.bit_map.set(FieldMap::ERA, true), + "eraYear" => self.bit_map.set(FieldMap::ERA_YEAR, true), + "timeZone" => self.bit_map.set(FieldMap::TIME_ZONE, true), + _ => {} + } + } + + #[inline] + /// A generic field setter for `TemporalFields` + /// + /// This method will not run any `JsValue` conversion. `FieldValue` is + /// expected to contain a preconverted value. + pub fn set_field_value(&mut self, field: &str, value: &FieldValue) -> TemporalResult<()> { + match field { + "year" => self.set_year(value)?, + "month" => self.set_month(value)?, + "monthCode" => self.set_month_code(value)?, + "day" => self.set_day(value)?, + "hour" => self.set_hour(value)?, + "minute" => self.set_minute(value)?, + "second" => self.set_second(value)?, + "millisecond" => self.set_milli(value)?, + "microsecond" => self.set_micro(value)?, + "nanosecond" => self.set_nano(value)?, + "offset" => self.set_offset(value)?, + "era" => self.set_era(value)?, + "eraYear" => self.set_era_year(value)?, + "timeZone" => self.set_time_zone(value)?, + _ => unreachable!(), + } + + Ok(()) + } + + #[inline] + fn set_year(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::Integer(y) = value else { + return Err(TemporalError::r#type().with_message("Year must be an integer.")); + }; + self.year = Some(*y); + self.bit_map.set(FieldMap::YEAR, true); + Ok(()) + } + + #[inline] + fn set_month(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::Integer(mo) = value else { + return Err(TemporalError::r#type().with_message("Month must be an integer.")); + }; + self.year = Some(*mo); + self.bit_map.set(FieldMap::MONTH, true); + Ok(()) + } + + #[inline] + fn set_month_code(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::String(mc) = value else { + return Err(TemporalError::r#type().with_message("monthCode must be string.")); + }; + self.month_code = + Some(TinyStr4::from_bytes(mc.as_bytes()).expect("monthCode must be less than 4 chars")); + self.bit_map.set(FieldMap::MONTH_CODE, true); + Ok(()) + } + + #[inline] + fn set_day(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::Integer(d) = value else { + return Err(TemporalError::r#type().with_message("day must be an integer.")); + }; + self.day = Some(*d); + self.bit_map.set(FieldMap::DAY, true); + Ok(()) + } + + #[inline] + fn set_hour(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::Integer(h) = value else { + return Err(TemporalError::r#type().with_message("hour must be an integer.")); + }; + self.hour = *h; + self.bit_map.set(FieldMap::HOUR, true); + Ok(()) + } + + #[inline] + fn set_minute(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::Integer(min) = value else { + return Err(TemporalError::r#type().with_message("minute must be an integer.")); + }; + self.minute = *min; + self.bit_map.set(FieldMap::MINUTE, true); + Ok(()) + } + + #[inline] + fn set_second(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::Integer(sec) = value else { + return Err(TemporalError::r#type().with_message("Second must be an integer.")); + }; + self.second = *sec; + self.bit_map.set(FieldMap::SECOND, true); + Ok(()) + } + + #[inline] + fn set_milli(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::Integer(milli) = value else { + return Err(TemporalError::r#type().with_message("Second must be an integer.")); + }; + self.millisecond = *milli; + self.bit_map.set(FieldMap::MILLISECOND, true); + Ok(()) + } + + #[inline] + fn set_micro(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::Integer(micro) = value else { + return Err(TemporalError::r#type().with_message("microsecond must be an integer.")); + }; + self.microsecond = *micro; + self.bit_map.set(FieldMap::MICROSECOND, true); + Ok(()) + } + + #[inline] + fn set_nano(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::Integer(nano) = value else { + return Err(TemporalError::r#type().with_message("nanosecond must be an integer.")); + }; + self.nanosecond = *nano; + self.bit_map.set(FieldMap::NANOSECOND, true); + Ok(()) + } + + #[inline] + fn set_offset(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::String(offset) = value else { + return Err(TemporalError::r#type().with_message("offset must be string.")); + }; + self.offset = Some(offset.to_string()); + self.bit_map.set(FieldMap::OFFSET, true); + + Ok(()) + } + + #[inline] + fn set_era(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::String(era) = value else { + return Err(TemporalError::r#type().with_message("era must be string.")); + }; + self.era = + Some(TinyStr16::from_bytes(era.as_bytes()).expect("era should not exceed 16 bytes.")); + self.bit_map.set(FieldMap::ERA, true); + + Ok(()) + } + + #[inline] + fn set_era_year(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::Integer(era_year) = value else { + return Err(TemporalError::r#type().with_message("eraYear must be an integer.")); + }; + self.era_year = Some(*era_year); + self.bit_map.set(FieldMap::ERA_YEAR, true); + Ok(()) + } + + #[inline] + fn set_time_zone(&mut self, value: &FieldValue) -> TemporalResult<()> { + let FieldValue::String(tz) = value else { + return Err(TemporalError::r#type().with_message("tz must be string.")); + }; + self.time_zone = Some(tz.to_string()); + self.bit_map.set(FieldMap::TIME_ZONE, true); + Ok(()) + } +} + +// TODO: optimize into iter. +impl TemporalFields { + /// Returns a vector filled with the key-value pairs marked as active. + pub fn active_kvs(&self) -> Vec<(String, FieldValue)> { + let mut result = Vec::default(); + + for field in self.bit_map.iter() { + match field { + FieldMap::YEAR => result.push(( + "year".to_owned(), + self.year.map_or(FieldValue::Undefined, FieldValue::Integer), + )), + FieldMap::MONTH => result.push(( + "month".to_owned(), + self.month + .map_or(FieldValue::Undefined, FieldValue::Integer), + )), + FieldMap::MONTH_CODE => result.push(( + "monthCode".to_owned(), + self.month_code + .map_or(FieldValue::Undefined, |s| FieldValue::String(s.to_string())), + )), + FieldMap::DAY => result.push(( + "day".to_owned(), + self.day.map_or(FieldValue::Undefined, FieldValue::Integer), + )), + FieldMap::HOUR => result.push(("hour".to_owned(), FieldValue::Integer(self.hour))), + FieldMap::MINUTE => { + result.push(("minute".to_owned(), FieldValue::Integer(self.minute))); + } + FieldMap::SECOND => { + result.push(("second".to_owned(), FieldValue::Integer(self.second))); + } + FieldMap::MILLISECOND => result.push(( + "millisecond".to_owned(), + FieldValue::Integer(self.millisecond), + )), + FieldMap::MICROSECOND => result.push(( + "microsecond".to_owned(), + FieldValue::Integer(self.microsecond), + )), + FieldMap::NANOSECOND => result.push(( + "nanosecond".to_owned(), + FieldValue::Integer(self.nanosecond), + )), + FieldMap::OFFSET => result.push(( + "offset".to_owned(), + self.offset + .clone() + .map_or(FieldValue::Undefined, FieldValue::String), + )), + FieldMap::ERA => result.push(( + "era".to_owned(), + self.era + .map_or(FieldValue::Undefined, |s| FieldValue::String(s.to_string())), + )), + FieldMap::ERA_YEAR => result.push(( + "eraYear".to_owned(), + self.era_year + .map_or(FieldValue::Undefined, FieldValue::Integer), + )), + FieldMap::TIME_ZONE => result.push(( + "timeZone".to_owned(), + self.time_zone + .clone() + .map_or(FieldValue::Undefined, FieldValue::String), + )), + _ => {} + } + } + + result + } + + /// Resolve `TemporalFields` month and monthCode fields. + pub(crate) fn iso_resolve_month(&mut self) -> TemporalResult<()> { + if self.month_code.is_none() { + if self.month.is_some() { + return Ok(()); + } + + return Err(TemporalError::range() + .with_message("month and MonthCode values cannot both be undefined.")); + } + + 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( + TemporalError::range().with_message("month and monthCode cannot be resolved.") + ) + } + _ => month_code_integer, + }; + + self.month = Some(new_month); + + Ok(()) + } +} + +fn month_code_to_integer(mc: TinyAsciiStr<4>) -> TemporalResult { + match mc.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(TemporalError::range().with_message("monthCode is not within the valid values.")), + } +} diff --git a/boa_temporal/src/iso.rs b/boa_temporal/src/iso.rs new file mode 100644 index 0000000000..113730b340 --- /dev/null +++ b/boa_temporal/src/iso.rs @@ -0,0 +1,341 @@ +//! The `ISO` module implements the internal ISO field slots. +//! +//! The three main types of slots are: +//! - `IsoDateTime` +//! - `IsoDate` +//! - `IsoTime` +//! +//! An `IsoDate` that represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots. +//! An `IsoTime` that represents the `[[ISOHour]]`, `[[ISOMinute]]`, `[[ISOsecond]]`, `[[ISOmillisecond]]`, +//! `[[ISOmicrosecond]]`, and `[[ISOnanosecond]]` internal slots. +//! An `IsoDateTime` has the internal slots of both an `IsoDate` and `IsoTime`. + +use crate::{ + duration::DateDuration, error::TemporalError, options::ArithmeticOverflow, utils, + TemporalResult, +}; +use icu_calendar::{Date as IcuDate, Iso}; +use num_bigint::BigInt; +use num_traits::cast::FromPrimitive; + +/// `IsoDateTime` is the Temporal internal representation of +/// a `DateTime` record +#[derive(Debug, Default, Clone, Copy)] +pub struct IsoDateTime { + date: IsoDate, + time: IsoTime, +} + +impl IsoDateTime { + /// Creates a new `IsoDateTime` without any validaiton. + pub(crate) fn new_unchecked(date: IsoDate, time: IsoTime) -> Self { + Self { date, time } + } + + /// Returns whether the `IsoDateTime` is within valid limits. + pub(crate) fn is_within_limits(&self) -> bool { + let Some(ns) = self.to_utc_epoch_nanoseconds(0f64) else { + return false; + }; + + let max = BigInt::from(crate::NS_MAX_INSTANT + i128::from(crate::NS_PER_DAY)); + let min = BigInt::from(crate::NS_MIN_INSTANT - i128::from(crate::NS_PER_DAY)); + + min < ns && max > ns + } + + /// Returns the UTC epoch nanoseconds for this `IsoDateTime`. + pub(crate) fn to_utc_epoch_nanoseconds(self, offset: f64) -> Option { + let day = self.date.to_epoch_days(); + let time = self.time.to_epoch_ms(); + let epoch_ms = utils::epoch_days_to_epoch_ms(day, time); + + let epoch_nanos = epoch_ms.mul_add( + 1_000_000f64, + f64::from(self.time.microsecond).mul_add(1_000f64, f64::from(self.time.nanosecond)), + ); + + BigInt::from_f64(epoch_nanos - offset) + } + + pub(crate) fn iso_date(&self) -> IsoDate { + self.date + } + + pub(crate) fn iso_time(&self) -> IsoTime { + self.time + } +} + +// ==== `IsoDate` section ==== + +// TODO: Figure out `ICU4X` interop / replacement? + +/// A trait for accessing the `IsoDate` across the various Temporal objects +pub trait IsoDateSlots { + /// Returns the target's internal `IsoDate`. + fn iso_date(&self) -> IsoDate; +} + +/// `IsoDate` serves as a 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 struct IsoDate { + year: i32, + month: u8, + day: u8, +} + +impl IsoDate { + /// Creates a new `IsoDate` without determining the validity. + pub(crate) const fn new_unchecked(year: i32, month: u8, day: u8) -> Self { + Self { year, month, day } + } + + pub(crate) fn new( + year: i32, + month: i32, + day: i32, + overflow: ArithmeticOverflow, + ) -> TemporalResult { + match overflow { + ArithmeticOverflow::Constrain => { + let m = month.clamp(1, 12); + let days_in_month = utils::iso_days_in_month(year, month); + let d = day.clamp(1, days_in_month); + Ok(Self::new_unchecked(year, m as u8, d as u8)) + } + ArithmeticOverflow::Reject => { + if !is_valid_date(year, month, day) { + return Err(TemporalError::range().with_message("not a valid ISO date.")); + } + // NOTE: Values have been verified to be in a u8 range. + Ok(Self::new_unchecked(year, month as u8, day as u8)) + } + } + } + + /// Create a balanced `IsoDate` + /// + /// Equivalent to `BalanceISODate`. + fn balance(year: i32, month: i32, day: i32) -> Self { + let epoch_days = iso_date_to_epoch_days(year, month - 1, day); + let ms = utils::epoch_days_to_epoch_ms(epoch_days, 0f64); + Self::new_unchecked( + utils::epoch_time_to_epoch_year(ms), + utils::epoch_time_to_month_in_year(ms) + 1, + utils::epoch_time_to_date(ms), + ) + } + + /// Returns the year field + pub(crate) const fn year(self) -> i32 { + self.year + } + + /// Returns the month field + pub(crate) const fn month(self) -> u8 { + self.month + } + + /// Returns the day field + pub(crate) const fn day(self) -> u8 { + self.day + } + + /// Functionally the same as Date's abstract operation `MakeDay` + /// + /// Equivalent to `IsoDateToEpochDays` + pub(crate) fn to_epoch_days(self) -> i32 { + iso_date_to_epoch_days(self.year, self.month.into(), self.day.into()) + } + + /// Returns if the current `IsoDate` is valid. + pub(crate) fn is_valid(self) -> bool { + is_valid_date(self.year, self.month.into(), self.day.into()) + } + + /// Returns the resulting `IsoDate` from adding a provided `Duration` to this `IsoDate` + pub(crate) fn add_iso_date( + self, + duration: &DateDuration, + overflow: ArithmeticOverflow, + ) -> TemporalResult { + // 1. Assert: year, month, day, years, months, weeks, and days are integers. + // 2. Assert: overflow is either "constrain" or "reject". + // 3. Let intermediate be ! BalanceISOYearMonth(year + years, month + months). + let mut intermediate_year = self.year + duration.years() as i32; + let mut intermediate_month = i32::from(self.month) + duration.months() as i32; + + intermediate_year += (intermediate_month - 1) / 12; + intermediate_month = (intermediate_month - 1) % 12 + 1; + + // 4. Let intermediate be ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], day, overflow). + let intermediate = Self::new( + intermediate_year, + intermediate_month, + i32::from(self.day), + overflow, + )?; + + // 5. Set days to days + 7 × weeks. + // 6. Let d be intermediate.[[Day]] + days. + let additional_days = duration.days() as i32 + (duration.weeks() as i32 * 7); + let d = i32::from(intermediate.day) + additional_days; + + // 7. Return BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d). + Ok(Self::balance( + intermediate.year, + intermediate.month.into(), + d, + )) + } +} + +impl IsoDate { + /// Creates `[[ISOYear]]`, `[[isoMonth]]`, `[[isoDay]]` fields from `ICU4X`'s `Date` struct. + pub(crate) fn as_icu4x(self) -> TemporalResult> { + IcuDate::try_new_iso_date(self.year, self.month, self.day) + .map_err(|e| TemporalError::range().with_message(e.to_string())) + } +} + +// ==== `IsoTime` section ==== + +/// An `IsoTime` record that contains `Temporal`'s +/// time slots. +#[derive(Debug, Default, Clone, Copy)] +pub struct IsoTime { + hour: i32, // 0..=23 + minute: i32, // 0..=59 + second: i32, // 0..=59 + millisecond: i32, // 0..=999 + microsecond: i32, // 0..=999 + nanosecond: i32, // 0..=999 +} + +impl IsoTime { + /// Creates a new `IsoTime` without any validation. + pub(crate) fn new_unchecked( + hour: i32, + minute: i32, + second: i32, + millisecond: i32, + microsecond: i32, + nanosecond: i32, + ) -> Self { + Self { + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + } + } + + /// Creates a new regulated `IsoTime`. + pub fn new( + hour: i32, + minute: i32, + second: i32, + millisecond: i32, + microsecond: i32, + nanosecond: i32, + overflow: ArithmeticOverflow, + ) -> TemporalResult { + match overflow { + ArithmeticOverflow::Constrain => { + let h = hour.clamp(0, 23); + let min = minute.clamp(0, 59); + let sec = second.clamp(0, 59); + let milli = millisecond.clamp(0, 999); + let micro = microsecond.clamp(0, 999); + let nano = nanosecond.clamp(0, 999); + Ok(Self::new_unchecked(h, min, sec, milli, micro, nano)) + } + ArithmeticOverflow::Reject => { + // TODO: Invert structure validation and update fields to u16. + let time = + Self::new_unchecked(hour, minute, second, millisecond, microsecond, nanosecond); + if !time.is_valid() { + return Err(TemporalError::range().with_message("IsoTime is not valid")); + } + Ok(time) + } + } + } + + /// Returns an `IsoTime` set to 12:00:00 + pub(crate) const fn noon() -> Self { + Self { + hour: 12, + minute: 0, + second: 0, + millisecond: 0, + microsecond: 0, + nanosecond: 0, + } + } + + /// Checks if the time is a valid `IsoTime` + pub(crate) fn is_valid(&self) -> bool { + if !(0..=23).contains(&self.hour) { + return false; + } + + let min_sec = 0..=59; + if !min_sec.contains(&self.minute) || !min_sec.contains(&self.second) { + return false; + } + + let sub_second = 0..=999; + sub_second.contains(&self.millisecond) + && sub_second.contains(&self.microsecond) + && sub_second.contains(&self.nanosecond) + } + + /// `IsoTimeToEpochMs` + /// + /// Note: This method is library specific and not in spec + /// + /// Functionally the same as Date's `MakeTime` + pub(crate) fn to_epoch_ms(self) -> f64 { + f64::from(self.hour).mul_add( + utils::MS_PER_HOUR, + f64::from(self.minute) * utils::MS_PER_MINUTE, + ) + f64::from(self.second).mul_add(1000f64, f64::from(self.millisecond)) + } +} + +// ==== `IsoDate` specific utiltiy functions ==== + +/// Returns the Epoch days based off the given year, month, and day. +#[inline] +fn iso_date_to_epoch_days(year: i32, month: i32, day: i32) -> i32 { + // 1. Let resolvedYear be year + floor(month / 12). + let resolved_year = year + (f64::from(month) / 12_f64).floor() as i32; + // 2. Let resolvedMonth be month modulo 12. + let resolved_month = 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 = utils::epoch_time_for_year(resolved_year); + let month_t = utils::epoch_time_for_month_given_year(resolved_month, resolved_year); + + // 4. Return EpochTimeToDayNumber(t) + date - 1. + utils::epoch_time_to_day_number(year_t + month_t) + day - 1 +} + +#[inline] +// Determines if the month and day are valid for the given year. +fn is_valid_date(year: i32, month: i32, day: i32) -> bool { + if !(1..=12).contains(&month) { + return false; + } + + let days_in_month = utils::iso_days_in_month(year, month); + (1..=days_in_month).contains(&day) +} diff --git a/boa_temporal/src/lib.rs b/boa_temporal/src/lib.rs new file mode 100644 index 0000000000..fb5149aac0 --- /dev/null +++ b/boa_temporal/src/lib.rs @@ -0,0 +1,61 @@ +//! Boa's `boa_temporal` crate is intended to serve as an engine agnostic +//! implementation the ECMAScript's Temporal builtin and algorithm. +#![doc = include_str!("../../ABOUT.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg", + html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg" +)] +#![cfg_attr(not(test), forbid(clippy::unwrap_used))] +#![allow( + // Currently throws a false positive regarding dependencies that are only used in benchmarks. + unused_crate_dependencies, + clippy::module_name_repetitions, + clippy::redundant_pub_crate, + clippy::too_many_lines, + clippy::cognitive_complexity, + clippy::missing_errors_doc, + clippy::let_unit_value, + clippy::option_if_let_else, + + // It may be worth to look if we can fix the issues highlighted by these lints. + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss, + clippy::cast_possible_wrap, + + // Add temporarily - Needs addressing + clippy::missing_panics_doc, +)] + +pub mod calendar; +pub mod date; +pub mod datetime; +pub mod duration; +pub mod error; +pub mod fields; +pub mod iso; +pub mod month_day; +pub mod options; +pub mod time; +pub(crate) mod utils; +pub mod year_month; +pub mod zoneddatetime; + +// TODO: evaluate positives and negatives of using tinystr. +// Re-exporting tinystr as a convenience, as it is currently tied into the API. +pub use tinystr::TinyAsciiStr; + +pub use error::TemporalError; + +/// The `Temporal` result type +pub type TemporalResult = Result; + +// Relevant numeric constants +/// Nanoseconds per day constant: 8.64e+13 +pub(crate) const NS_PER_DAY: i64 = 86_400_000_000_000; +/// Milliseconds per day constant: 8.64e+7 +pub(crate) const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000; +/// Max Instant nanosecond constant +pub(crate) const NS_MAX_INSTANT: i128 = NS_PER_DAY as i128 * 100_000_000i128; +/// Min Instant nanosecond constant +pub(crate) const NS_MIN_INSTANT: i128 = -NS_MAX_INSTANT; diff --git a/boa_temporal/src/month_day.rs b/boa_temporal/src/month_day.rs new file mode 100644 index 0000000000..0d9b7875c3 --- /dev/null +++ b/boa_temporal/src/month_day.rs @@ -0,0 +1,51 @@ +//! `MonthDay` + +use crate::{ + calendar::CalendarSlot, + iso::{IsoDate, IsoDateSlots}, + options::ArithmeticOverflow, + TemporalResult, +}; + +/// The `MonthDay` struct +#[derive(Debug, Default, Clone)] +pub struct MonthDay { + iso: IsoDate, + calendar: CalendarSlot, +} + +impl MonthDay { + /// Creates a new unchecked `MonthDay` + #[inline] + #[must_use] + pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot) -> Self { + Self { iso, calendar } + } + + #[inline] + /// Creates a new valid `MonthDay`. + pub fn new( + month: i32, + day: i32, + calendar: CalendarSlot, + overflow: ArithmeticOverflow, + ) -> TemporalResult { + let iso = IsoDate::new(1972, month, day, overflow)?; + Ok(Self::new_unchecked(iso, calendar)) + } + + #[inline] + #[must_use] + /// Returns a reference to `MonthDay`'s `CalendarSlot` + pub fn calendar(&self) -> &CalendarSlot { + &self.calendar + } +} + +impl IsoDateSlots for MonthDay { + #[inline] + /// Returns this structs `IsoDate`. + fn iso_date(&self) -> IsoDate { + self.iso + } +} diff --git a/boa_temporal/src/options.rs b/boa_temporal/src/options.rs new file mode 100644 index 0000000000..6f6ccd3cff --- /dev/null +++ b/boa_temporal/src/options.rs @@ -0,0 +1,413 @@ +//! Temporal Options + +use core::{fmt, str::FromStr}; + +use crate::TemporalError; + +/// The relevant unit that should be used for the operation that +/// this option is provided as a value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum TemporalUnit { + /// The `Auto` unit + Auto = 0, + /// The `Nanosecond` unit + Nanosecond, + /// The `Microsecond` unit + Microsecond, + /// The `Millisecond` unit + Millisecond, + /// The `Second` unit + Second, + /// The `Minute` unit + Minute, + /// The `Hour` unit + Hour, + /// The `Day` unit + Day, + /// The `Week` unit + Week, + /// The `Month` unit + Month, + /// The `Year` unit + Year, +} + +impl TemporalUnit { + #[inline] + #[must_use] + /// Returns the `MaximumRoundingIncrement` for the current `TemporalUnit`. + pub 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!(), + } + } +} + +/// A parsing error for `TemporalUnit` +#[derive(Debug, Clone, Copy)] +pub 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 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 enum ArithmeticOverflow { + /// Constrain option + Constrain, + /// Constrain option + Reject, +} + +/// A parsing error for `ArithemeticOverflow` +#[derive(Debug, Clone, Copy)] +pub 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 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. +#[derive(Debug, Clone, Copy)] +pub enum DurationOverflow { + /// Constrain option + Constrain, + /// Balance option + Balance, +} + +/// A parsing error for `DurationOverflow`. +#[derive(Debug, Clone, Copy)] +pub 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 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. +#[derive(Debug, Clone, Copy)] +pub enum InstantDisambiguation { + /// Compatible option + Compatible, + /// Earlier option + Earlier, + /// Later option + Later, + /// Reject option + Reject, +} + +/// A parsing error on `InstantDisambiguation` options. +#[derive(Debug, Clone, Copy)] +pub 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 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. +#[derive(Debug, Clone, Copy)] +pub enum OffsetDisambiguation { + /// Use option + Use, + /// Prefer option + Prefer, + /// Ignore option + Ignore, + /// Reject option + Reject, +} + +/// A parsing error for `OffsetDisambiguation` parsing. +#[derive(Debug, Clone, Copy)] +pub 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 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) + } +} + +// TODO: Figure out what to do with intl's RoundingMode + +/// Declares the specified `RoundingMode` for the operation. +#[derive(Debug, Copy, Clone, Default)] +pub enum TemporalRoundingMode { + /// Ceil RoundingMode + Ceil, + /// Floor RoundingMode + Floor, + /// Expand RoundingMode + Expand, + /// Truncate RoundingMode + Trunc, + /// HalfCeil RoundingMode + HalfCeil, + /// HalfFloor RoundingMode + HalfFloor, + /// HalfExpand RoundingMode - Default + #[default] + HalfExpand, + /// HalfTruncate RoundingMode + HalfTrunc, + /// HalfEven RoundingMode + HalfEven, +} + +/// The `UnsignedRoundingMode` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TemporalUnsignedRoundingMode { + /// `Infinity` `RoundingMode` + Infinity, + /// `Zero` `RoundingMode` + Zero, + /// `HalfInfinity` `RoundingMode` + HalfInfinity, + /// `HalfZero` `RoundingMode` + HalfZero, + /// `HalfEven` `RoundingMode` + HalfEven, +} + +impl TemporalRoundingMode { + #[inline] + #[must_use] + /// Negates the current `RoundingMode`. + pub const fn negate(self) -> Self { + use TemporalRoundingMode::{ + 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, + } + } + + #[inline] + #[must_use] + /// Returns the `UnsignedRoundingMode` + pub const fn get_unsigned_round_mode(self, is_negative: bool) -> TemporalUnsignedRoundingMode { + use TemporalRoundingMode::{ + Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc, + }; + + match self { + Ceil if !is_negative => TemporalUnsignedRoundingMode::Infinity, + Ceil => TemporalUnsignedRoundingMode::Zero, + Floor if !is_negative => TemporalUnsignedRoundingMode::Zero, + Floor | Trunc | Expand => TemporalUnsignedRoundingMode::Infinity, + HalfCeil if !is_negative => TemporalUnsignedRoundingMode::HalfInfinity, + HalfCeil | HalfTrunc => TemporalUnsignedRoundingMode::HalfZero, + HalfFloor if !is_negative => TemporalUnsignedRoundingMode::HalfZero, + HalfFloor | HalfExpand => TemporalUnsignedRoundingMode::HalfInfinity, + HalfEven => TemporalUnsignedRoundingMode::HalfEven, + } + } +} + +impl FromStr for TemporalRoundingMode { + type Err = TemporalError; + + fn from_str(s: &str) -> Result { + match s { + "ceil" => Ok(Self::Ceil), + "floor" => Ok(Self::Floor), + "expand" => Ok(Self::Expand), + "trunc" => Ok(Self::Trunc), + "halfCeil" => Ok(Self::HalfCeil), + "halfFloor" => Ok(Self::HalfFloor), + "halfExpand" => Ok(Self::HalfExpand), + "halfTrunc" => Ok(Self::HalfTrunc), + "halfEven" => Ok(Self::HalfEven), + _ => Err(TemporalError::range().with_message("RoundingMode not an accepted value.")), + } + } +} + +impl fmt::Display for TemporalRoundingMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Ceil => "ceil", + Self::Floor => "floor", + Self::Expand => "expand", + Self::Trunc => "trunc", + Self::HalfCeil => "halfCeil", + Self::HalfFloor => "halfFloor", + Self::HalfExpand => "halfExpand", + Self::HalfTrunc => "halfTrunc", + Self::HalfEven => "halfEven", + } + .fmt(f) + } +} diff --git a/boa_temporal/src/time.rs b/boa_temporal/src/time.rs new file mode 100644 index 0000000000..f618c3ed0b --- /dev/null +++ b/boa_temporal/src/time.rs @@ -0,0 +1,34 @@ +//! Temporal Time Representation. + +use crate::iso::IsoTime; + +/// The Temporal `PlainTime` object. +#[derive(Debug, Default, Clone, Copy)] +#[allow(dead_code)] +pub struct Time { + iso: IsoTime, +} + +// ==== Private API ==== + +impl Time { + #[allow(dead_code)] + pub(crate) fn new_unchecked( + hour: i32, + minute: i32, + second: i32, + millisecond: i32, + microsecond: i32, + nanosecond: i32, + ) -> Self { + Self { + iso: IsoTime::new_unchecked(hour, minute, second, millisecond, microsecond, nanosecond), + } + } + + /// Returns true if a valid `Time`. + #[allow(dead_code)] + pub(crate) fn is_valid(&self) -> bool { + self.iso.is_valid() + } +} diff --git a/boa_temporal/src/utils.rs b/boa_temporal/src/utils.rs new file mode 100644 index 0000000000..09ed3b71c2 --- /dev/null +++ b/boa_temporal/src/utils.rs @@ -0,0 +1,284 @@ +//! Utility equations for Temporal + +use crate::{ + options::{TemporalRoundingMode, TemporalUnsignedRoundingMode}, + MS_PER_DAY, +}; + +use std::ops::Mul; + +// NOTE: Review the below for optimizations and add ALOT of tests. + +fn apply_unsigned_rounding_mode( + x: f64, + r1: f64, + r2: f64, + unsigned_rounding_mode: TemporalUnsignedRoundingMode, +) -> 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 == TemporalUnsignedRoundingMode::Zero { + return r1; + }; + // 5. If unsignedRoundingMode is infinity, return r2. + if unsigned_rounding_mode == TemporalUnsignedRoundingMode::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 == TemporalUnsignedRoundingMode::HalfZero { + return r1; + }; + // 12. If unsignedRoundingMode is half-infinity, return r2. + if unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfInfinity { + return r2; + }; + // 13. Assert: unsignedRoundingMode is half-even. + assert!(unsigned_rounding_mode == TemporalUnsignedRoundingMode::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: TemporalRoundingMode, +) -> 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 +} + +// ==== Begin Date Equations ==== + +pub(crate) const MS_PER_HOUR: f64 = 3_600_000f64; +pub(crate) const MS_PER_MINUTE: f64 = 60_000f64; + +/// `EpochDaysToEpochMS` +/// +/// Functionally the same as Date's abstract operation `MakeDate` +pub(crate) fn epoch_days_to_epoch_ms(day: i32, time: f64) -> f64 { + f64::from(day).mul_add(f64::from(MS_PER_DAY), time).floor() +} + +/// `EpochTimeToDayNumber` +/// +/// This equation is the equivalent to `ECMAScript`'s `Date(t)` +pub(crate) fn epoch_time_to_day_number(t: f64) -> i32 { + (t / f64::from(MS_PER_DAY)).floor() as i32 +} + +/// Mathematically determine the days in a year. +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 + } +} + +/// Returns the epoch day number for a given year. +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(MS_PER_DAY) * epoch_day_number_for_year(f64::from(y)) +} + +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) + 1970; + loop { + if epoch_time_for_year(year) <= t { + break; + } + year -= 1; + } + + year +} + +/// 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) -> u8 { + 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 u8, + } +} + +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!(), + }; + + f64::from(MS_PER_DAY).mul(f64::from(days)) +} + +pub(crate) fn epoch_time_to_date(t: f64) -> u8 { + 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; + } + + // This return of date should be < 31. + date as u8 +} + +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) +} + +// EpochTimeTOWeekDay -> REMOVED + +// ==== End Date Equations ==== + +// ==== Begin Calendar Equations ==== + +// NOTE: below was the iso methods in temporal::calendar -> Need to be reassessed. + +/// 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 + mathematical_in_leap_year(epoch_time_for_year(year)), + _ => unreachable!("an invalid month value is an implementation error."), + } +} + +// The below calendar abstract equations/utilities were removed for being unused. +// 12.2.32 `ToISOWeekOfYear ( year, month, day )` +// 12.2.33 `ISOMonthCode ( month )` +// 12.2.39 `ToISODayOfYear ( year, month, day )` +// 12.2.40 `ToISODayOfWeek ( year, month, day )` + +// ==== End Calendar Equations ==== + +// ==== Tests ===== + +// TODO(nekevss): Add way more to the below. + +#[cfg(test)] +mod tests { + use super::*; + + #[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_temporal/src/year_month.rs b/boa_temporal/src/year_month.rs new file mode 100644 index 0000000000..c7283cb7b0 --- /dev/null +++ b/boa_temporal/src/year_month.rs @@ -0,0 +1,53 @@ +//! `YearMonth` + +use crate::{ + calendar::CalendarSlot, + iso::{IsoDate, IsoDateSlots}, + options::ArithmeticOverflow, + TemporalResult, +}; + +/// The `YearMonth` struct +#[derive(Debug, Default, Clone)] +pub struct YearMonth { + iso: IsoDate, + calendar: CalendarSlot, +} + +impl YearMonth { + /// Creates an unvalidated `YearMonth`. + #[inline] + #[must_use] + pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot) -> Self { + Self { iso, calendar } + } + + /// Creates a new valid `YearMonth`. + #[inline] + pub fn new( + year: i32, + month: i32, + reference_day: Option, + calendar: CalendarSlot, + overflow: ArithmeticOverflow, + ) -> TemporalResult { + let day = reference_day.unwrap_or(1); + let iso = IsoDate::new(year, month, day, overflow)?; + Ok(Self::new_unchecked(iso, calendar)) + } + + #[inline] + #[must_use] + /// Returns a reference to `YearMonth`'s `CalendarSlot` + pub fn calendar(&self) -> &CalendarSlot { + &self.calendar + } +} + +impl IsoDateSlots for YearMonth { + #[inline] + /// Returns this `YearMonth`'s `IsoDate` + fn iso_date(&self) -> IsoDate { + self.iso + } +} diff --git a/boa_temporal/src/zoneddatetime.rs b/boa_temporal/src/zoneddatetime.rs new file mode 100644 index 0000000000..6fd3f2c94a --- /dev/null +++ b/boa_temporal/src/zoneddatetime.rs @@ -0,0 +1,7 @@ +//! The `ZonedDateTime` module. + +// NOTE: Mostly serves as a placeholder currently +// until the rest can be implemented. +/// `TemporalZoneDateTime` +#[derive(Debug, Clone, Copy)] +pub struct ZonedDateTime;