diff --git a/core/engine/src/builtins/date/mod.rs b/core/engine/src/builtins/date/mod.rs index 0c82b5b570f..0e9c56e79d1 100644 --- a/core/engine/src/builtins/date/mod.rs +++ b/core/engine/src/builtins/date/mod.rs @@ -32,6 +32,11 @@ use crate::{ use boa_gc::{Finalize, Trace}; use boa_macros::js_str; +#[cfg(feature = "intl")] +use crate::builtins::intl::date_time_format::{ + FormatDefaults, FormatType, create_date_time_format, format_timestamp_with_dtf, +}; + pub(crate) mod utils; #[cfg(test)] @@ -1636,6 +1641,40 @@ impl Date { func.call(this, &[], context) } + /// Shared implementation of `Date.prototype.toLocaleString`, + /// `Date.prototype.toLocaleDateString`, and `Date.prototype.toLocaleTimeString` + /// methods with the corresponding formatting params + #[cfg(feature = "intl")] + #[inline] + fn to_locale_string_with( + this: &JsValue, + args: &[JsValue], + required: FormatType, + defaults: FormatDefaults, + context: &mut Context, + ) -> JsResult { + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let x be dateObject.[[DateValue]]. + let x = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. If x is NaN, return "Invalid Date". + if x.is_nan() { + return Ok(JsValue::new(js_string!("Invalid Date"))); + } + + // 5. Let dateFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, required, defaults). + // 6. Return ! FormatDateTime(dateFormat, x). + let locales = args.get_or_undefined(0); + let options = args.get_or_undefined(1); + let dtf = create_date_time_format(locales, options, required, defaults, context)?; + format_timestamp_with_dtf(&dtf, x, context) + } + /// [`Date.prototype.toLocaleDateString()`][spec]. /// /// The `toLocaleDateString()` method returns the date portion of the given Date instance according @@ -1646,10 +1685,6 @@ impl Date { /// /// [spec]: https://tc39.es/ecma402/#sup-date.prototype.tolocaledatestring /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString - #[allow( - unused_variables, - reason = "`args` and `context` are used when the `intl` feature is enabled" - )] pub(crate) fn to_locale_date_string( this: &JsValue, args: &[JsValue], @@ -1657,37 +1692,11 @@ impl Date { ) -> JsResult { #[cfg(feature = "intl")] { - use crate::builtins::intl::date_time_format::{ - FormatDefaults, FormatType, format_date_time_locale, - }; - // 1. Let dateObject be the this value. - // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). - // 3. Let x be dateObject.[[DateValue]]. - let t = this - .as_object() - .and_then(|obj| obj.downcast_ref::().as_deref().copied()) - .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? - .0; - // 4. If x is NaN, return "Invalid Date". - if t.is_nan() { - return Ok(JsValue::new(js_string!("Invalid Date"))); - } - // 5. Let dateFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, date, date). - // 6. Return ! FormatDateTime(dateFormat, x). - let locales = args.get_or_undefined(0); - let options = args.get_or_undefined(1); - format_date_time_locale( - locales, - options, - FormatType::Date, - FormatDefaults::Date, - t, - context, - ) + Self::to_locale_string_with(this, args, FormatType::Date, FormatDefaults::Date, context) } #[cfg(not(feature = "intl"))] { - Self::to_string(this, &[], context) + Self::to_string(this, args, context) } } @@ -1700,10 +1709,6 @@ impl Date { /// /// [spec]: https://tc39.es/ecma402/#sup-date.prototype.tolocalestring /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString - #[allow( - unused_variables, - reason = "`args` and `context` are used when the `intl` feature is enabled" - )] pub(crate) fn to_locale_string( this: &JsValue, args: &[JsValue], @@ -1711,37 +1716,11 @@ impl Date { ) -> JsResult { #[cfg(feature = "intl")] { - use crate::builtins::intl::date_time_format::{ - FormatDefaults, FormatType, format_date_time_locale, - }; - // 1. Let dateObject be the this value. - // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). - // 3. Let x be dateObject.[[DateValue]]. - let t = this - .as_object() - .and_then(|obj| obj.downcast_ref::().as_deref().copied()) - .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? - .0; - // 4. If x is NaN, return "Invalid Date". - if t.is_nan() { - return Ok(JsValue::new(js_string!("Invalid Date"))); - } - // 5. Let dateFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, any, all). - // 6. Return ! FormatDateTime(dateFormat, x). - let locales = args.get_or_undefined(0); - let options = args.get_or_undefined(1); - format_date_time_locale( - locales, - options, - FormatType::Any, - FormatDefaults::All, - t, - context, - ) + Self::to_locale_string_with(this, args, FormatType::Any, FormatDefaults::All, context) } #[cfg(not(feature = "intl"))] { - Self::to_string(this, &[], context) + Self::to_string(this, args, context) } } @@ -1755,10 +1734,6 @@ impl Date { /// /// [spec]: https://tc39.es/ecma402/#sup-date.prototype.tolocaletimestring /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString - #[allow( - unused_variables, - reason = "`args` and `context` are used when the `intl` feature is enabled" - )] pub(crate) fn to_locale_time_string( this: &JsValue, args: &[JsValue], @@ -1766,37 +1741,11 @@ impl Date { ) -> JsResult { #[cfg(feature = "intl")] { - use crate::builtins::intl::date_time_format::{ - FormatDefaults, FormatType, format_date_time_locale, - }; - // 1. Let dateObject be the this value. - // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). - // 3. Let x be dateObject.[[DateValue]]. - let t = this - .as_object() - .and_then(|obj| obj.downcast_ref::().as_deref().copied()) - .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? - .0; - // 4. If x is NaN, return "Invalid Date". - if t.is_nan() { - return Ok(JsValue::new(js_string!("Invalid Date"))); - } - // 5. Let timeFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, time, time). - // 6. Return ! FormatDateTime(timeFormat, x). - let locales = args.get_or_undefined(0); - let options = args.get_or_undefined(1); - format_date_time_locale( - locales, - options, - FormatType::Time, - FormatDefaults::Time, - t, - context, - ) + Self::to_locale_string_with(this, args, FormatType::Time, FormatDefaults::Time, context) } #[cfg(not(feature = "intl"))] { - Self::to_string(this, &[], context) + Self::to_string(this, args, context) } } diff --git a/core/engine/src/builtins/intl/date_time_format/mod.rs b/core/engine/src/builtins/intl/date_time_format/mod.rs index b888eebbe21..05b5827e232 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -278,13 +278,7 @@ impl DateTimeFormat { }; // 5. Return ? FormatDateTime(dtf, x). - // A.O 11.5.6 PartitionDateTimePattern: 1. TimeClip(x). 2. If NaN throw. Then ToLocalTime and format. - let x = time_clip(x); - if x.is_nan() { - return Err(js_error!(RangeError: "formatted date cannot be NaN")); - } - let result = format_timestamp_with_dtf(dtf.borrow().data(), x, context)?; - Ok(JsValue::from(result)) + format_timestamp_with_dtf(dtf.borrow().data(), x, context) }, dtf_clone, ), @@ -826,22 +820,15 @@ pub(crate) fn create_date_time_format( /// Formats a timestamp (epoch milliseconds) using the given [`DateTimeFormat`] internals. /// -/// This is the shared implementation used by: -/// - the bound `format` function created in `get_format`, and -/// - [`format_date_time_locale`] used by `Date.prototype.toLocaleString` (and friends). -/// -/// It corresponds to the *post*-`TimeClip` portion of -/// [`FormatDateTime(dtf, x)`](https://tc39.es/ecma402/#sec-formatdatetime), -/// and the `ToLocalTime` / `PartitionDateTimePattern` logic from +/// It corresponds the `ToLocalTime` / `PartitionDateTimePattern` logic from /// [11.5.6](https://tc39.es/ecma402/#sec-partitiondatetimepattern) and /// [11.5.12](https://tc39.es/ecma402/#sec-tolocaltime). /// -/// Callers must have already applied `TimeClip` and `NaN` check -/// (`FormatDateTime` steps 1–2). This helper implements: +/// This helper implements: /// /// 11.5.6 `PartitionDateTimePattern` ( dtf, x ) -/// 1. Let x be TimeClip(x). (Done by caller) -/// 2. If x is `NaN`, throw a `RangeError` exception. (Done by caller) +/// 1. Let x be TimeClip(x). +/// 2. If x is `NaN`, throw a `RangeError` exception. /// 3. Let epochNanoseconds be ℤ(ℝ(x) × 10^6). /// 4. Let timeZone be dtf.[[`TimeZone`]]. /// 5. Let offsetNs be GetOffsetNanosecondsFor(timeZone, epochNanoseconds). @@ -849,16 +836,23 @@ pub(crate) fn create_date_time_format( /// /// Then calls `ToLocalTime::from_local_epoch_milliseconds` to obtain calendar fields, /// and formats the resulting `ZonedDateTime` with ICU4X. -fn format_timestamp_with_dtf( +pub(crate) fn format_timestamp_with_dtf( dtf: &DateTimeFormat, timestamp: f64, context: &mut Context, -) -> JsResult { +) -> JsResult { + // PartitionDateTimePattern ( dtf, x ) step 1: + // 1. Let x be TimeClip(x). + let x = time_clip(timestamp); + + // PartitionDateTimePattern ( dtf, x ) step 2: + // 2. If x is `NaN`, throw a `RangeError` exception. + if x.is_nan() { + return Err(js_error!(RangeError: "formatted date cannot be NaN")); + } // PartitionDateTimePattern ( dtf, x ) step 3: // Let epochNanoseconds be ℤ(ℝ(x) × 10^6). - // - // NOTE: `timestamp` is already `TimeClip`'d by the caller and represents *UTC epoch milliseconds*. - let epoch_ns = timestamp as i128 * 1_000_000; + let epoch_ns = x as i128 * 1_000_000; // PartitionDateTimePattern ( dtf, x ) step 4: // Let timeZone be dtf.[[`TimeZone`]]. @@ -884,7 +878,7 @@ fn format_timestamp_with_dtf( // PartitionDateTimePattern ( dtf, x ) step 6: // Let tz be 𝔽(ℝ(x) + ℝ(offsetNs) / 10^6). - let tz = timestamp + f64::from(time_zone_offset_seconds * 1_000); + let tz = x + f64::from(time_zone_offset_seconds * 1_000); let fields = ToLocalTime::from_local_epoch_milliseconds(tz)?; let dt = fields.to_formattable_datetime()?; let tz_info = time_zone.to_time_zone_info(); @@ -895,7 +889,7 @@ fn format_timestamp_with_dtf( zone: tz_info_at_time, }; let result = dtf.formatter.format(&zdt).to_string(); - Ok(JsString::from(result)) + Ok(JsString::from(result).into()) } fn date_time_style_format( @@ -940,8 +934,7 @@ fn best_fit_date_time_format(format_options: &FormatOptions) -> JsResult JsResult { - let options = coerce_options_to_object(options, context)?; - let options_value = options.into(); - let dtf = create_date_time_format(locales, &options_value, format_type, defaults, context)?; - // FormatDateTime steps 1–2: TimeClip and NaN check (format_timestamp_with_dtf does ToLocalTime + format only). - let x = time_clip(timestamp); - if x.is_nan() { - return Err(js_error!(RangeError: "formatted date cannot be NaN")); - } - let result = format_timestamp_with_dtf(&dtf, x, context)?; - Ok(JsValue::from(result)) -}