From 00c5cb52a8e5c259385ea904032d27c460bd5818 Mon Sep 17 00:00:00 2001 From: alienx5499 Date: Sun, 15 Mar 2026 05:56:03 +0530 Subject: [PATCH 1/4] feat(intl): implement Date.prototype.toLocaleString, toLocaleDateString, and toLocaleTimeString --- core/engine/src/builtins/date/mod.rs | 146 +++++++++++++++--- core/engine/src/builtins/date/tests.rs | 49 ++++++ .../src/builtins/intl/date_time_format/mod.rs | 64 +++++++- 3 files changed, 234 insertions(+), 25 deletions(-) diff --git a/core/engine/src/builtins/date/mod.rs b/core/engine/src/builtins/date/mod.rs index f730ba82724..76a865c0293 100644 --- a/core/engine/src/builtins/date/mod.rs +++ b/core/engine/src/builtins/date/mod.rs @@ -8,7 +8,7 @@ //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date use crate::{ - Context, JsArgs, JsData, JsError, JsResult, JsString, + Context, JsArgs, JsData, JsResult, JsString, builtins::{ BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, date::utils::{ @@ -1608,6 +1608,15 @@ impl Date { func.call(this, &[], context) } + /// Returns the `[[DateValue]]` internal slot (RequireInternalSlot(dateObject, `[[DateValue]]`)). + fn this_date_value(this: &JsValue) -> JsResult { + this.as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date")) + .map(|d| d.0) + .map_err(Into::into) + } + /// [`Date.prototype.toLocaleDateString()`][spec]. /// /// The `toLocaleDateString()` method returns the date portion of the given Date instance according @@ -1616,16 +1625,47 @@ impl Date { /// More information: /// - [MDN documentation][mdn] /// - /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.tolocaledatestring + /// [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], - _context: &mut Context, + this: &JsValue, + args: &[JsValue], + context: &mut Context, ) -> JsResult { - Err(JsError::from_opaque(JsValue::new(js_string!( - "Function Unimplemented" - )))) + // Let dateObject be the this value. + // Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // Let x be dateObject.[[DateValue]]. + let t = Self::this_date_value(this)?; + // If x is NaN, return "Invalid Date". + if t.is_nan() { + return Ok(JsValue::new(js_string!("Invalid Date"))); + } + // Let dateFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, date, date). + // Return ! FormatDateTime(dateFormat, x). + #[cfg(feature = "intl")] + { + use crate::builtins::intl::date_time_format::{ + FormatDefaults, FormatType, format_date_time_locale, + }; + 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, + ) + } + #[cfg(not(feature = "intl"))] + { + Self::to_string(this, &[], context) + } } /// [`Date.prototype.toLocaleString()`][spec]. @@ -1635,16 +1675,47 @@ impl Date { /// More information: /// - [MDN documentation][mdn] /// - /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.tolocalestring + /// [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, - _: &[JsValue], - _context: &mut Context, + this: &JsValue, + args: &[JsValue], + context: &mut Context, ) -> JsResult { - Err(JsError::from_opaque(JsValue::new(js_string!( - "Function Unimplemented]" - )))) + // Let dateObject be the this value. + // Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // Let x be dateObject.[[DateValue]]. + let t = Self::this_date_value(this)?; + // If x is NaN, return "Invalid Date". + if t.is_nan() { + return Ok(JsValue::new(js_string!("Invalid Date"))); + } + // Let dateFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, any, all). + // Return ! FormatDateTime(dateFormat, x). + #[cfg(feature = "intl")] + { + use crate::builtins::intl::date_time_format::{ + FormatDefaults, FormatType, format_date_time_locale, + }; + 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, + ) + } + #[cfg(not(feature = "intl"))] + { + Self::to_string(this, &[], context) + } } /// [`Date.prototype.toLocaleTimeString()`][spec]. @@ -1655,16 +1726,47 @@ impl Date { /// More information: /// - [MDN documentation][mdn] /// - /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.tolocaletimestring + /// [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], - _context: &mut Context, + this: &JsValue, + args: &[JsValue], + context: &mut Context, ) -> JsResult { - Err(JsError::from_opaque(JsValue::new(js_string!( - "Function Unimplemented]" - )))) + // Let dateObject be the this value. + // Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // Let x be dateObject.[[DateValue]]. + let t = Self::this_date_value(this)?; + // If x is NaN, return "Invalid Date". + if t.is_nan() { + return Ok(JsValue::new(js_string!("Invalid Date"))); + } + // Let timeFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, time, time). + // Return ! FormatDateTime(timeFormat, x). + #[cfg(feature = "intl")] + { + use crate::builtins::intl::date_time_format::{ + FormatDefaults, FormatType, format_date_time_locale, + }; + 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, + ) + } + #[cfg(not(feature = "intl"))] + { + Self::to_string(this, &[], context) + } } /// [`Date.prototype.toString()`][spec]. diff --git a/core/engine/src/builtins/date/tests.rs b/core/engine/src/builtins/date/tests.rs index 52366832a82..a6b6aed6d2e 100644 --- a/core/engine/src/builtins/date/tests.rs +++ b/core/engine/src/builtins/date/tests.rs @@ -943,3 +943,52 @@ fn date_parse_hour24_validation() { TestAction::assert("isNaN(Date.parse('2024-01-01T24:00:00.001Z'))"), ]); } + +#[test] +#[cfg(feature = "intl")] +fn date_proto_to_locale_string_intl() { + run_test_actions([ + // Invalid receiver: spec requires TypeError + TestAction::assert_native_error( + "Date.prototype.toLocaleString.call({})", + JsNativeErrorKind::Type, + "'this' is not a Date", + ), + TestAction::assert_native_error( + "Date.prototype.toLocaleDateString.call({})", + JsNativeErrorKind::Type, + "'this' is not a Date", + ), + TestAction::assert_native_error( + "Date.prototype.toLocaleTimeString.call({})", + JsNativeErrorKind::Type, + "'this' is not a Date", + ), + TestAction::assert_eq("new Date(NaN).toLocaleString()", js_str!("Invalid Date")), + TestAction::assert("typeof new Date(2020, 6, 8).toLocaleString() === 'string'"), + TestAction::assert("typeof new Date(2020, 6, 8).toLocaleDateString() === 'string'"), + TestAction::assert("typeof new Date(2020, 6, 8).toLocaleTimeString() === 'string'"), + TestAction::assert("typeof new Date(0).toLocaleString('en-US') === 'string'"), + TestAction::assert("typeof new Date(0).toLocaleDateString('en-US') === 'string'"), + TestAction::assert("typeof new Date(0).toLocaleDateString('de-DE') === 'string'"), + TestAction::assert("typeof new Date(0).toLocaleTimeString('en-US') === 'string'"), + // Prove locale pipeline: different locales produce different output + TestAction::assert( + "new Date(0).toLocaleDateString('en-US') !== new Date(0).toLocaleDateString('de-DE')", + ), + TestAction::assert( + "new Date(0).toLocaleString('en-US') !== new Date(0).toLocaleString('de-DE')", + ), + TestAction::assert( + "new Date(0).toLocaleTimeString('en-US') !== new Date(0).toLocaleTimeString('de-DE')", + ), + // Prove ToDateTimeOptions pipeline: options affect output + TestAction::assert( + "typeof new Date(0).toLocaleDateString('en-US', { dateStyle: 'short' }) === 'string'", + ), + // Prove output is a string and not empty + TestAction::assert( + "new Date(0).toLocaleDateString('en-US', { dateStyle: 'short' }).length > 0", + ), + ]); +} 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 4e1460aa4ff..0d46552bd1b 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -551,7 +551,7 @@ impl ToLocalTime { // ==== Abstract Operations ==== -fn create_date_time_format( +pub(crate) fn create_date_time_format( new_target: &JsValue, locales: &JsValue, options: &JsValue, @@ -807,13 +807,13 @@ fn create_date_time_format( // 2. If value is not undefined, set needDefaults to false. let needs_defaults = format_options.check_dtf_type(date_time_format_type); // d. If needDefaults is true and defaults is either date or all, then - if needs_defaults && defaults != FormatDefaults::Time { + if needs_defaults && (defaults == FormatDefaults::All || defaults == FormatDefaults::Date) { // i. For each property name prop of « "year", "month", "day" », do // 1. Set formatOptions.[[]] to "numeric". format_options.set_date_defaults(); } // e. If needDefaults is true and defaults is either time or all, then - if needs_defaults && defaults != FormatDefaults::Date { + if needs_defaults && (defaults == FormatDefaults::All || defaults == FormatDefaults::Time) { // i. For each property name prop of « "hour", "minute", "second" », do // 1. Set formatOptions.[[]] to "numeric". format_options.set_time_defaults(); @@ -911,10 +911,14 @@ pub(crate) enum FormatType { Any, } +/// Indicates which default fields should be applied when `ToDateTimeOptions` +/// determines defaults are needed. `All` applies both date and time defaults. #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) enum FormatDefaults { Date, Time, + /// Apply both date and time defaults (e.g. for `toLocaleString`). + All, } /// Abstract operation [`UnwrapDateTimeFormat ( dtf )`][spec]. @@ -967,3 +971,57 @@ fn unwrap_date_time_format( .with_message("object was not an `Intl.DateTimeFormat` object") .into()) } + +/// Shared helper used by Date.prototype.toLocaleString, +/// Date.prototype.toLocaleDateString, and Date.prototype.toLocaleTimeString. +/// Applies `ToDateTimeOptions` defaults, constructs `Intl.DateTimeFormat`, +/// and formats the provided timestamp. +#[allow(clippy::too_many_arguments)] +pub(crate) fn format_date_time_locale( + locales: &JsValue, + options: &JsValue, + format_type: FormatType, + defaults: FormatDefaults, + timestamp: f64, + context: &mut Context, +) -> JsResult { + let options = coerce_options_to_object(options, context)?; + if format_type != FormatType::Time + && get_option::(&options, js_string!("dateStyle"), context)?.is_none() + { + options.create_data_property_or_throw( + js_string!("dateStyle"), + JsValue::from(js_string!("long")), + context, + )?; + } + if format_type != FormatType::Date + && get_option::(&options, js_string!("timeStyle"), context)?.is_none() + { + options.create_data_property_or_throw( + js_string!("timeStyle"), + JsValue::from(js_string!("long")), + context, + )?; + } + let new_target = context + .intrinsics() + .constructors() + .date_time_format() + .constructor() + .into(); + let options_value = options.into(); + let dtf = create_date_time_format( + &new_target, + locales, + &options_value, + format_type, + defaults, + context, + )?; + let format_val = dtf.get(js_string!("format"), context)?; + let format_fn = format_val + .as_callable() + .ok_or_else(|| JsNativeError::typ().with_message("format is not callable"))?; + format_fn.call(&dtf.into(), &[JsValue::from(timestamp)], context) +} From 725441eaa504cfa0b7c7c6bbe278c72e8b4dfba6 Mon Sep 17 00:00:00 2001 From: alienx5499 Date: Sun, 15 Mar 2026 07:49:50 +0530 Subject: [PATCH 2/4] refactor(intl): return DateTimeFormat from create_date_time_format, avoid JS alloc in Date toLocaleString path --- .../src/builtins/intl/date_time_format/mod.rs | 116 ++++++++++-------- 1 file changed, 67 insertions(+), 49 deletions(-) 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 0d46552bd1b..9e600bf291c 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -170,14 +170,19 @@ impl BuiltInConstructor for DateTimeFormat { let options = args.get_or_undefined(1); // 2. Let dateTimeFormat be ? CreateDateTimeFormat(newTarget, locales, options, any, date). - let date_time_format = create_date_time_format( - new_target_inner, + let dtf = create_date_time_format( locales, options, FormatType::Any, FormatDefaults::Date, context, )?; + let prototype = get_prototype_from_constructor( + new_target_inner, + StandardConstructors::date_time_format, + context, + )?; + let date_time_format = JsObject::from_proto_and_data(prototype, dtf); // 3. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then // a. Let this be the this value. @@ -551,23 +556,16 @@ impl ToLocalTime { // ==== Abstract Operations ==== +/// Creates a [`DateTimeFormat`] struct (internal slots only). The constructor wraps this in a +/// `JsObject` with the correct prototype; Date.prototype.toLocaleString (and friends) use it +/// directly with [`format_timestamp_with_dtf`] without allocating a JS object. pub(crate) fn create_date_time_format( - new_target: &JsValue, locales: &JsValue, options: &JsValue, date_time_format_type: FormatType, defaults: FormatDefaults, context: &mut Context, -) -> JsResult { - // 1. Let dateTimeFormat be ? OrdinaryCreateFromConstructor(newTarget, "%Intl.DateTimeFormat.prototype%", - // « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], - // [[HourCycle]], [[DateStyle]], [[TimeStyle]], [[DateTimeFormat]], [[BoundFormat]] »). - let prototype = get_prototype_from_constructor( - new_target, - StandardConstructors::date_time_format, - context, - )?; - +) -> JsResult { // 2. Let hour12 be undefined. <- TODO // 3. Let modifyResolutionOptions be a new Abstract Closure with parameters (options) that captures hour12 and performs the following steps when called: // a. Set hour12 to options.[[hour12]]. @@ -843,21 +841,57 @@ pub(crate) fn create_date_time_format( ) .map_err(|e| JsNativeError::range().with_message(format!("failed to load formatter: {e}")))?; - Ok(JsObject::from_proto_and_data( - prototype, - DateTimeFormat { - locale: resolved_locale, - calendar_algorithm: intl_options.preferences.calendar_algorithm, - numbering_system: intl_options.preferences.numbering_system, - hour_cycle: intl_options.preferences.hour_cycle, - date_style, - time_style, - time_zone, - fieldset, - formatter, - bound_format: None, - }, - )) + Ok(DateTimeFormat { + locale: resolved_locale, + calendar_algorithm: intl_options.preferences.calendar_algorithm, + numbering_system: intl_options.preferences.numbering_system, + hour_cycle: intl_options.preferences.hour_cycle, + date_style, + time_style, + time_zone, + fieldset, + formatter, + bound_format: None, + }) +} + +/// Formats a timestamp (epoch milliseconds) using the given [`DateTimeFormat`] internals. +/// Used by the bound `format` function and by [`format_date_time_locale`] without creating a JS object. +fn format_timestamp_with_dtf( + dtf: &DateTimeFormat, + timestamp: f64, + context: &mut Context, +) -> JsResult { + // FormatDateTime / PartitionDateTimePattern: TimeClip, then ToLocalTime, then format. + let x = time_clip(timestamp); + if x.is_nan() { + return Err(js_error!(RangeError: "formatted date cannot be NaN")); + } + let time_zone_offset = match dtf.time_zone { + FormatTimeZone::UtcOffset(offset) => offset.to_seconds(), + FormatTimeZone::Identifier((_, time_zone_id)) => { + let epoch_ns = x as i128 * 1_000_000; + let offset_seconds = context + .timezone_provider() + .transition_nanoseconds_for_utc_epoch_nanoseconds(time_zone_id, epoch_ns) + .map_err( + |_e| js_error!(RangeError: "unable to determine transition nanoseconds"), + )?; + offset_seconds.0 as i32 + } + }; + let tz = x + f64::from(time_zone_offset * 1_000); + let fields = ToLocalTime::from_local_epoch_milliseconds(tz)?; + let dt = fields.to_formattable_datetime()?; + let tz_info = dtf.time_zone.to_time_zone_info(); + let tz_info_at_time = tz_info.at_date_time_iso(dt); + let zdt = ZonedDateTime { + date: dt.date, + time: dt.time, + zone: tz_info_at_time, + }; + let result = dtf.formatter.format(&zdt).to_string(); + Ok(JsString::from(result)) } fn date_time_style_format( @@ -974,8 +1008,8 @@ fn unwrap_date_time_format( /// Shared helper used by Date.prototype.toLocaleString, /// Date.prototype.toLocaleDateString, and Date.prototype.toLocaleTimeString. -/// Applies `ToDateTimeOptions` defaults, constructs `Intl.DateTimeFormat`, -/// and formats the provided timestamp. +/// Applies `ToDateTimeOptions` defaults, calls [`create_date_time_format`], and formats +/// the timestamp via [`format_timestamp_with_dtf`] without allocating a JS object. #[allow(clippy::too_many_arguments)] pub(crate) fn format_date_time_locale( locales: &JsValue, @@ -1004,24 +1038,8 @@ pub(crate) fn format_date_time_locale( context, )?; } - let new_target = context - .intrinsics() - .constructors() - .date_time_format() - .constructor() - .into(); let options_value = options.into(); - let dtf = create_date_time_format( - &new_target, - locales, - &options_value, - format_type, - defaults, - context, - )?; - let format_val = dtf.get(js_string!("format"), context)?; - let format_fn = format_val - .as_callable() - .ok_or_else(|| JsNativeError::typ().with_message("format is not callable"))?; - format_fn.call(&dtf.into(), &[JsValue::from(timestamp)], context) + let dtf = create_date_time_format(locales, &options_value, format_type, defaults, context)?; + let result = format_timestamp_with_dtf(&dtf, timestamp, context)?; + Ok(JsValue::from(result)) } From 755481e9dac8aa567a1642d658a1537444b27515 Mon Sep 17 00:00:00 2001 From: alienx5499 Date: Mon, 16 Mar 2026 03:18:23 +0530 Subject: [PATCH 3/4] refactor(intl): improve Date toLocaleString and DateTimeFormat implementation --- core/engine/src/builtins/date/mod.rs | 81 ++++++++++--------- .../src/builtins/intl/date_time_format/mod.rs | 77 ++++-------------- 2 files changed, 57 insertions(+), 101 deletions(-) diff --git a/core/engine/src/builtins/date/mod.rs b/core/engine/src/builtins/date/mod.rs index 76a865c0293..1751fb10841 100644 --- a/core/engine/src/builtins/date/mod.rs +++ b/core/engine/src/builtins/date/mod.rs @@ -1608,15 +1608,6 @@ impl Date { func.call(this, &[], context) } - /// Returns the `[[DateValue]]` internal slot (RequireInternalSlot(dateObject, `[[DateValue]]`)). - fn this_date_value(this: &JsValue) -> JsResult { - this.as_object() - .and_then(|obj| obj.downcast_ref::().as_deref().copied()) - .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date")) - .map(|d| d.0) - .map_err(Into::into) - } - /// [`Date.prototype.toLocaleDateString()`][spec]. /// /// The `toLocaleDateString()` method returns the date portion of the given Date instance according @@ -1636,21 +1627,25 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // Let dateObject be the this value. - // Perform ? RequireInternalSlot(dateObject, [[DateValue]]). - // Let x be dateObject.[[DateValue]]. - let t = Self::this_date_value(this)?; - // If x is NaN, return "Invalid Date". - if t.is_nan() { - return Ok(JsValue::new(js_string!("Invalid Date"))); - } - // Let dateFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, date, date). - // Return ! FormatDateTime(dateFormat, x). #[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( @@ -1686,21 +1681,25 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // Let dateObject be the this value. - // Perform ? RequireInternalSlot(dateObject, [[DateValue]]). - // Let x be dateObject.[[DateValue]]. - let t = Self::this_date_value(this)?; - // If x is NaN, return "Invalid Date". - if t.is_nan() { - return Ok(JsValue::new(js_string!("Invalid Date"))); - } - // Let dateFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, any, all). - // Return ! FormatDateTime(dateFormat, x). #[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( @@ -1737,21 +1736,25 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // Let dateObject be the this value. - // Perform ? RequireInternalSlot(dateObject, [[DateValue]]). - // Let x be dateObject.[[DateValue]]. - let t = Self::this_date_value(this)?; - // If x is NaN, return "Invalid Date". - if t.is_nan() { - return Ok(JsValue::new(js_string!("Invalid Date"))); - } - // Let timeFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, time, time). - // Return ! FormatDateTime(timeFormat, x). #[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( 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 9e600bf291c..25cfc38684d 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -271,61 +271,13 @@ impl DateTimeFormat { }; // 5. Return ? FormatDateTime(dtf, x). - - // A.O 11.5.6 PartitionDateTimePattern - - // 1. Let x be TimeClip(x). - // 2. If x is NaN, throw a RangeError exception. + // 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")); } - - // A.O 11.5.12 ToLocalTime - let time_zone_offset = match dtf.borrow().data().time_zone { - // 1. If IsTimeZoneOffsetString(timeZoneIdentifier) is true, then - // a. Let offsetNs be ParseTimeZoneOffsetString(timeZoneIdentifier). - FormatTimeZone::UtcOffset(offset) => offset.to_seconds(), - // 2. Else, - FormatTimeZone::Identifier((_, time_zone_id)) => { - // Shift x in epoch milliseconds to epoch nanoseconds - let epoch_ns = x as i128 * 1_000_000; - // a. Assert: GetAvailableNamedTimeZoneIdentifier(timeZoneIdentifier) is not empty. - // b. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(timeZoneIdentifier, epochNs). - let offset_seconds = context - .timezone_provider() - .transition_nanoseconds_for_utc_epoch_nanoseconds(time_zone_id, epoch_ns) - .map_err(|_e| js_error!(RangeError: "unable to determine transition nanoseconds"))?; - offset_seconds.0 as i32 - } - }; - - // 3. Let tz be ℝ(epochNs) + offsetNs. - let tz = x + f64::from(time_zone_offset * 1_000); - - // TODO: Non-gregorian calendar support? - // 4. If calendar is "gregory", then - // a. Return a ToLocalTime Record with fields calculated from tz according to Table 17. - // 5. Else, - // a. Return a ToLocalTime Record with the fields calculated from tz for - // the given calendar. The calculations should use best available - // information about the specified calendar. - let fields = ToLocalTime::from_local_epoch_milliseconds(tz)?; - - let formatter = dtf.borrow().data().formatter.clone(); - - let dt = fields.to_formattable_datetime()?; - let tz_info = dtf.borrow().data().time_zone.to_time_zone_info(); - let tz_info_at_time = tz_info.at_date_time_iso(dt); - - let zdt = ZonedDateTime { - date: dt.date, - time: dt.time, - zone: tz_info_at_time, - }; - let result = formatter.format(&zdt).to_string(); - - Ok(JsString::from(result).into()) + let result = format_timestamp_with_dtf(dtf.borrow().data(), x, context)?; + Ok(JsValue::from(result)) }, dtf_clone, ), @@ -805,13 +757,13 @@ pub(crate) fn create_date_time_format( // 2. If value is not undefined, set needDefaults to false. let needs_defaults = format_options.check_dtf_type(date_time_format_type); // d. If needDefaults is true and defaults is either date or all, then - if needs_defaults && (defaults == FormatDefaults::All || defaults == FormatDefaults::Date) { + if needs_defaults && defaults != FormatDefaults::Time { // i. For each property name prop of « "year", "month", "day" », do // 1. Set formatOptions.[[]] to "numeric". format_options.set_date_defaults(); } // e. If needDefaults is true and defaults is either time or all, then - if needs_defaults && (defaults == FormatDefaults::All || defaults == FormatDefaults::Time) { + if needs_defaults && defaults != FormatDefaults::Date { // i. For each property name prop of « "hour", "minute", "second" », do // 1. Set formatOptions.[[]] to "numeric". format_options.set_time_defaults(); @@ -856,21 +808,17 @@ pub(crate) fn create_date_time_format( } /// Formats a timestamp (epoch milliseconds) using the given [`DateTimeFormat`] internals. -/// Used by the bound `format` function and by [`format_date_time_locale`] without creating a JS object. +/// Callers must have already applied `TimeClip` and `NaN` check (`FormatDateTime` steps 1–2). +/// This performs `ToLocalTime` and format only. fn format_timestamp_with_dtf( dtf: &DateTimeFormat, timestamp: f64, context: &mut Context, ) -> JsResult { - // FormatDateTime / PartitionDateTimePattern: TimeClip, then ToLocalTime, then format. - let x = time_clip(timestamp); - if x.is_nan() { - return Err(js_error!(RangeError: "formatted date cannot be NaN")); - } let time_zone_offset = match dtf.time_zone { FormatTimeZone::UtcOffset(offset) => offset.to_seconds(), FormatTimeZone::Identifier((_, time_zone_id)) => { - let epoch_ns = x as i128 * 1_000_000; + let epoch_ns = timestamp as i128 * 1_000_000; let offset_seconds = context .timezone_provider() .transition_nanoseconds_for_utc_epoch_nanoseconds(time_zone_id, epoch_ns) @@ -880,7 +828,7 @@ fn format_timestamp_with_dtf( offset_seconds.0 as i32 } }; - let tz = x + f64::from(time_zone_offset * 1_000); + let tz = timestamp + f64::from(time_zone_offset * 1_000); let fields = ToLocalTime::from_local_epoch_milliseconds(tz)?; let dt = fields.to_formattable_datetime()?; let tz_info = dtf.time_zone.to_time_zone_info(); @@ -1040,6 +988,11 @@ pub(crate) fn format_date_time_locale( } let options_value = options.into(); let dtf = create_date_time_format(locales, &options_value, format_type, defaults, context)?; - let result = format_timestamp_with_dtf(&dtf, timestamp, 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)) } From 6b270bf3ff959b3f8d2c25735b8d7d1e4aeb5a6a Mon Sep 17 00:00:00 2001 From: alienx5499 Date: Tue, 17 Mar 2026 11:58:19 +0530 Subject: [PATCH 4/4] feat(intl): align DateTimeFormat timestamp formatting docs with spec steps --- .../src/builtins/intl/date_time_format/mod.rs | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) 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 25cfc38684d..bed9856a2c6 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -808,30 +808,69 @@ pub(crate) fn create_date_time_format( } /// Formats a timestamp (epoch milliseconds) using the given [`DateTimeFormat`] internals. -/// Callers must have already applied `TimeClip` and `NaN` check (`FormatDateTime` steps 1–2). -/// This performs `ToLocalTime` and format only. +/// +/// 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 +/// [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: +/// +/// 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) +/// 3. Let epochNanoseconds be ℤ(ℝ(x) × 10^6). +/// 4. Let timeZone be dtf.[[`TimeZone`]]. +/// 5. Let offsetNs be GetOffsetNanosecondsFor(timeZone, epochNanoseconds). +/// 6. Let tz be 𝔽(ℝ(x) + ℝ(offsetNs) / 10^6). +/// +/// Then calls `ToLocalTime::from_local_epoch_milliseconds` to obtain calendar fields, +/// and formats the resulting `ZonedDateTime` with ICU4X. fn format_timestamp_with_dtf( dtf: &DateTimeFormat, timestamp: f64, context: &mut Context, ) -> JsResult { - let time_zone_offset = match dtf.time_zone { + // 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; + + // PartitionDateTimePattern ( dtf, x ) step 4: + // Let timeZone be dtf.[[`TimeZone`]]. + let time_zone = &dtf.time_zone; + + // PartitionDateTimePattern ( dtf, x ) step 5: + // Let offsetNs be GetOffsetNanosecondsFor(timeZone, epochNanoseconds). + // + // NOTE: the spec describes the offset in *nanoseconds*. Internally, we obtain/normalize it to + // seconds (and then milliseconds) for use with `ToLocalTime::from_local_epoch_milliseconds`. + let time_zone_offset_seconds = match time_zone { FormatTimeZone::UtcOffset(offset) => offset.to_seconds(), FormatTimeZone::Identifier((_, time_zone_id)) => { - let epoch_ns = timestamp as i128 * 1_000_000; let offset_seconds = context .timezone_provider() - .transition_nanoseconds_for_utc_epoch_nanoseconds(time_zone_id, epoch_ns) + .transition_nanoseconds_for_utc_epoch_nanoseconds(*time_zone_id, epoch_ns) .map_err( |_e| js_error!(RangeError: "unable to determine transition nanoseconds"), )?; offset_seconds.0 as i32 } }; - let tz = timestamp + f64::from(time_zone_offset * 1_000); + + // PartitionDateTimePattern ( dtf, x ) step 6: + // Let tz be 𝔽(ℝ(x) + ℝ(offsetNs) / 10^6). + let tz = timestamp + 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 = dtf.time_zone.to_time_zone_info(); + let tz_info = time_zone.to_time_zone_info(); let tz_info_at_time = tz_info.at_date_time_iso(dt); let zdt = ZonedDateTime { date: dt.date,