From 409231315eb3d05aa3401c93313100110ec3a7f2 Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:10:46 +0300 Subject: [PATCH 01/16] Prepare ZonedDateTime::to_locale_string for refactoring --- .../builtins/temporal/zoneddatetime/mod.rs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index 2d0019c98db..5541d4568ee 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -1614,7 +1614,11 @@ impl ZonedDateTime { /// /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.tolocalestring /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toLocaleString - fn to_locale_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + fn to_locale_string( + this: &JsValue, + _args: &[JsValue], + context: &mut Context, + ) -> JsResult { // TODO: Update for ECMA-402 compliance let object = this.as_object(); let zdt = object @@ -1624,15 +1628,18 @@ impl ZonedDateTime { JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") })?; - let ixdtf = zdt.inner.to_ixdtf_string_with_provider( - DisplayOffset::Auto, - DisplayTimeZone::Auto, - DisplayCalendar::Auto, - ToStringRoundingOptions::default(), - context.timezone_provider(), - )?; + #[cfg(not(feature = "intl"))] + { + let ixdtf = zdt.inner.to_ixdtf_string_with_provider( + DisplayOffset::Auto, + DisplayTimeZone::Auto, + DisplayCalendar::Auto, + ToStringRoundingOptions::default(), + context.timezone_provider(), + )?; - Ok(JsString::from(ixdtf).into()) + Ok(JsString::from(ixdtf).into()) + } } /// 6.3.43 `Temporal.ZonedDateTime.prototype.toJSON ( )` From f4bdcf77cbe3aad5af93a84a82a2a59359b514dc Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:19:27 +0300 Subject: [PATCH 02/16] Create DateTimeFormat --- .../src/builtins/temporal/zoneddatetime/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index 5541d4568ee..bde840a81cb 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -7,6 +7,7 @@ use crate::{ JsSymbol, JsValue, JsVariant, builtins::{ BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + intl::date_time_format::{FormatDefaults, FormatType, create_date_time_format}, options::{get_option, get_options_object}, temporal::{calendar::to_temporal_calendar_identifier, options::get_digits_option}, }, @@ -1628,6 +1629,22 @@ impl ZonedDateTime { JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") })?; + #[cfg(feature = "intl")] + { + let locales = _args.get_or_undefined(0); + let options = _args.get_or_undefined(1); + + let dtf = create_date_time_format( + locales, + options, + FormatType::Any, + FormatDefaults::All, + context, + )?; + + Ok(JsString::from(String::new()).into()) + } + #[cfg(not(feature = "intl"))] { let ixdtf = zdt.inner.to_ixdtf_string_with_provider( From 76581a6371b820c169c43677252abdb96aaa43dd Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:23:25 +0300 Subject: [PATCH 03/16] Check calendars for equality --- core/engine/src/builtins/intl/date_time_format/mod.rs | 7 +++++++ .../engine/src/builtins/temporal/zoneddatetime/mod.rs | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) 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 cc692602c79..d24ff3fd9f7 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -242,6 +242,13 @@ impl BuiltInConstructor for DateTimeFormat { } impl DateTimeFormat { + #[inline] + #[must_use] + pub(crate) fn calendar_algorithm(&self) -> CalendarAlgorithm { + self.calendar_algorithm + .unwrap_or(CalendarAlgorithm::Iso8601) + } + fn get_format(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { // 1. Let dtf be the this value. // 2. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index bde840a81cb..1069ebde1a4 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -12,7 +12,7 @@ use crate::{ temporal::{calendar::to_temporal_calendar_identifier, options::get_digits_option}, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, - js_string, + js_error, js_string, object::internal_methods::get_prototype_from_constructor, property::Attribute, realm::Realm, @@ -1642,6 +1642,15 @@ impl ZonedDateTime { context, )?; + let cal_1 = zdt.inner.calendar(); + let cal_2 = dtf.calendar_algorithm(); + + if !cal_1.is_iso() && cal_1.identifier() != cal_2.as_str() { + return Err( + js_error!(RangeError: "calendars {} and {} aren't compatible", cal_1.identifier(), cal_2.as_str()), + ); + } + Ok(JsString::from(String::new()).into()) } From 41522fdc392f1efc5611d3d55135860e416381d1 Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:19:21 +0300 Subject: [PATCH 04/16] First draft implementation --- .../src/builtins/temporal/zoneddatetime/mod.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index 1069ebde1a4..50d5c5f485b 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -7,7 +7,9 @@ use crate::{ JsSymbol, JsValue, JsVariant, builtins::{ BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, - intl::date_time_format::{FormatDefaults, FormatType, create_date_time_format}, + intl::date_time_format::{ + FormatDefaults, FormatType, create_date_time_format, format_date_time_locale, + }, options::{get_option, get_options_object}, temporal::{calendar::to_temporal_calendar_identifier, options::get_digits_option}, }, @@ -1651,7 +1653,15 @@ impl ZonedDateTime { ); } - Ok(JsString::from(String::new()).into()) + let inst = create_temporal_instant(zdt.inner.to_instant(), None, context)?; + format_date_time_locale( + locales, + options, + FormatType::Any, + FormatDefaults::All, + zdt.inner.to_instant().epoch_milliseconds() as f64, + context, + ) } #[cfg(not(feature = "intl"))] From e0acac9c6aad6b657c578d5783b1abb310b39199 Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:08:41 +0300 Subject: [PATCH 05/16] Refactor `format_date_time_locale()` --- .../src/builtins/intl/date_time_format/mod.rs | 22 +------------------ 1 file changed, 1 insertion(+), 21 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 d24ff3fd9f7..a9c4b332d91 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -1031,27 +1031,7 @@ pub(crate) fn format_date_time_locale( 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 options_value = options.into(); - let dtf = create_date_time_format(locales, &options_value, format_type, defaults, context)?; + let dtf = create_date_time_format(locales, options, 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() { From 3c4c99303a60f06061a8a2099dff5b889b8b7dea Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Sun, 22 Mar 2026 06:21:19 +0300 Subject: [PATCH 06/16] Gate `intl` specific imports after appropriate flag --- core/engine/src/builtins/temporal/zoneddatetime/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index 50d5c5f485b..7b0b53dc942 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -7,9 +7,6 @@ use crate::{ JsSymbol, JsValue, JsVariant, builtins::{ BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, - intl::date_time_format::{ - FormatDefaults, FormatType, create_date_time_format, format_date_time_locale, - }, options::{get_option, get_options_object}, temporal::{calendar::to_temporal_calendar_identifier, options::get_digits_option}, }, @@ -44,6 +41,11 @@ use super::{ to_temporal_duration, to_temporal_time, }; +#[cfg(feature = "intl")] +use crate::builtins::intl::date_time_format::{ + FormatDefaults, FormatType, create_date_time_format, format_date_time_locale, +}; + /// The `Temporal.ZonedDateTime` built-in implementation /// /// More information: From 07b2513f3a93d58efcbea6230aef8e4af365eb88 Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:03:21 +0300 Subject: [PATCH 07/16] Update `toLocaleString` --- .../engine/src/builtins/temporal/zoneddatetime/mod.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index 7b0b53dc942..e7442740b2c 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -43,7 +43,7 @@ use super::{ #[cfg(feature = "intl")] use crate::builtins::intl::date_time_format::{ - FormatDefaults, FormatType, create_date_time_format, format_date_time_locale, + FormatDefaults, FormatType, create_date_time_format, format_date_time, }; /// The `Temporal.ZonedDateTime` built-in implementation @@ -1656,14 +1656,7 @@ impl ZonedDateTime { } let inst = create_temporal_instant(zdt.inner.to_instant(), None, context)?; - format_date_time_locale( - locales, - options, - FormatType::Any, - FormatDefaults::All, - zdt.inner.to_instant().epoch_milliseconds() as f64, - context, - ) + format_date_time(&dtf, &inst.as_object().unwrap(), context) } #[cfg(not(feature = "intl"))] From 3fe5b4fc52677ebf7bf5febd05a2cbff160a744c Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:14:27 +0300 Subject: [PATCH 08/16] Rename `format_date_time_locale` to `format_date_time` --- .../src/builtins/intl/date_time_format/mod.rs | 125 ++++-------------- .../builtins/temporal/zoneddatetime/mod.rs | 10 +- 2 files changed, 29 insertions(+), 106 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 a9c4b332d91..b55d997854f 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -51,7 +51,7 @@ use icu_decimal::preferences::NumberingSystem; use icu_decimal::provider::DecimalSymbolsV1; use icu_locale::{Locale, extensions::unicode::Value}; use icu_time::{ - TimeZoneInfo, ZonedDateTime, + TimeZoneInfo, zone::{IanaParser, models::Base}, }; use timezone_provider::provider::TimeZoneId; @@ -276,21 +276,25 @@ impl DateTimeFormat { // NOTE (nekevss) i64 should be sufficient for a millisecond // representation. // a. Let x be ! Call(%Date.now%, undefined). - context.clock().system_time_millis() as f64 + JsValue::from(context.clock().system_time_millis() as f64) // 4. Else, } else { // NOTE (nekevss) The i64 covers all MAX_SAFE_INTEGER values. - // a. Let x be ? ToNumber(date). - date.to_number(context)? + // a. Let x be ? ToDateTimeFormattable(date). + to_date_time_formattable(date, context)? }; // 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)?; + // let x = time_clip(x); + // if x.is_nan() { + // return Err(js_error!(RangeError: "formatted date cannot be NaN")); + // } + let result = format_date_time( + dtf.borrow().data(), + &x.as_object().unwrap(), + context, + )?; Ok(JsValue::from(result)) }, dtf_clone, @@ -831,80 +835,6 @@ 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 -/// [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 { - // 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 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 - } - }; - - // 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 = 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( date_style: Option, time_style: Option, @@ -1018,25 +948,16 @@ fn unwrap_date_time_format( .into()) } -/// Shared helper used by Date.prototype.toLocaleString, -/// Date.prototype.toLocaleDateString, and Date.prototype.toLocaleTimeString. -/// 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, - options: &JsValue, - format_type: FormatType, - defaults: FormatDefaults, - timestamp: f64, +/// 15.6.6 FormatDateTime ( dateTimeFormat, x ) +pub(crate) fn format_date_time( + dtf: &DateTimeFormat, + x: &JsObject, context: &mut Context, ) -> JsResult { - let dtf = create_date_time_format(locales, options, 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)) + todo!() +} + +/// 15.6.11 ToDateTimeFormattable ( value ) +fn to_date_time_formattable(value: &JsValue, context: &mut Context) -> JsResult { + todo!() } diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index e7442740b2c..45830169eb7 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -1624,7 +1624,8 @@ impl ZonedDateTime { _args: &[JsValue], context: &mut Context, ) -> JsResult { - // TODO: Update for ECMA-402 compliance + // 1. Let zonedDateTime be the this value. + // 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]). let object = this.as_object(); let zdt = object .as_ref() @@ -1637,7 +1638,7 @@ impl ZonedDateTime { { let locales = _args.get_or_undefined(0); let options = _args.get_or_undefined(1); - + // 3. Let dateTimeFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, any, all, zonedDateTime.[[TimeZone]]). let dtf = create_date_time_format( locales, options, @@ -1648,13 +1649,14 @@ impl ZonedDateTime { let cal_1 = zdt.inner.calendar(); let cal_2 = dtf.calendar_algorithm(); - + // 4. If zonedDateTime.[[Calendar]] is not "iso8601" and CalendarEquals(zonedDateTime.[[Calendar]], dateTimeFormat.[[Calendar]]) is false, throw a RangeError exception. if !cal_1.is_iso() && cal_1.identifier() != cal_2.as_str() { return Err( js_error!(RangeError: "calendars {} and {} aren't compatible", cal_1.identifier(), cal_2.as_str()), ); } - + // 5. Let instant be ! CreateTemporalInstant(zonedDateTime.[[EpochNanoseconds]]). + // 6. Return ? FormatDateTime(dateTimeFormat, instant). let inst = create_temporal_instant(zdt.inner.to_instant(), None, context)?; format_date_time(&dtf, &inst.as_object().unwrap(), context) } From 7663683768d27337ecf175078274ee5621152904 Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:08:29 +0300 Subject: [PATCH 09/16] Implement `to_date_time_formattable` --- core/engine/src/builtins/intl/date_time_format/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) 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 b55d997854f..31e6c517a99 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -959,5 +959,13 @@ pub(crate) fn format_date_time( /// 15.6.11 ToDateTimeFormattable ( value ) fn to_date_time_formattable(value: &JsValue, context: &mut Context) -> JsResult { + if is_temporal_object(value) { + return Ok(value.clone()); + } + Ok(JsValue::from(value.to_number(context)?)) +} + +/// 15.6.12 IsTemporalObject ( value ) +fn is_temporal_object(value: &JsValue) -> bool { todo!() } From 2efe9c885372e2eea6f33a4c78f07c697dbcd6e6 Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:21:00 +0300 Subject: [PATCH 10/16] Implement `is_temporal_object` --- .../src/builtins/intl/date_time_format/mod.rs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 31e6c517a99..d5ceef0558e 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -22,6 +22,10 @@ use crate::{ options::{IntlOptions, coerce_options_to_object}, }, options::get_option, + temporal::{ + Instant, PlainDate, PlainDateTime, PlainMonthDay, PlainTime, PlainYearMonth, + ZonedDateTime, + }, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, error::JsNativeError, @@ -959,13 +963,34 @@ pub(crate) fn format_date_time( /// 15.6.11 ToDateTimeFormattable ( value ) fn to_date_time_formattable(value: &JsValue, context: &mut Context) -> JsResult { + // 1. If IsTemporalObject(value) is true, return value. if is_temporal_object(value) { return Ok(value.clone()); } + // 2. Return ? ToNumber(value). Ok(JsValue::from(value.to_number(context)?)) } /// 15.6.12 IsTemporalObject ( value ) fn is_temporal_object(value: &JsValue) -> bool { - todo!() + // 1. If value is not an Object, return false. + let Some(obj) = value.as_object() else { + return false; + }; + // 2. If value has an [[InitializedTemporalDate]], [[InitializedTemporalTime]], + // [[InitializedTemporalDateTime]], [[InitializedTemporalZonedDateTime]] + // [[InitializedTemporalYearMonth]], [[InitializedTemporalMonthDay]], + // or [[InitializedTemporalInstant]] internal slot, return false. + if obj.is::() + || obj.is::() + || obj.is::() + || obj.is::() + || obj.is::() + || obj.is::() + || obj.is::() + { + return false; + } + // 3. Return true. + true } From 41666c99acf33fba537ced594c3049039e3cf52c Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:16:17 +0300 Subject: [PATCH 11/16] Implement `format_date_time` --- .../src/builtins/intl/date_time_format/mod.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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 d5ceef0558e..c47a1f96dcd 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -952,13 +952,27 @@ fn unwrap_date_time_format( .into()) } +///15.6.5 PartitionDateTimePattern ( dateTimeFormat, x ) +fn partition_date_time_pattern( + dtf: &DateTimeFormat, + x: &JsObject, + context: &mut Context, +) -> JsResult { + todo!() +} + /// 15.6.6 FormatDateTime ( dateTimeFormat, x ) pub(crate) fn format_date_time( dtf: &DateTimeFormat, x: &JsObject, context: &mut Context, ) -> JsResult { - todo!() + let parts = partition_date_time_pattern(dtf, x, context)?; + let mut result = String::new(); + for part in parts { + result += part; + } + Ok(JsString::from(result).into()) } /// 15.6.11 ToDateTimeFormattable ( value ) From 67ec3b0b72988974c28e2d6548bc61a0bebf82ef Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:34:03 +0300 Subject: [PATCH 12/16] Implement `handle_date_time_value` --- .../src/builtins/intl/date_time_format/mod.rs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 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 c47a1f96dcd..5ade63d5482 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -8,9 +8,9 @@ //! [spec]: https://tc39.es/ecma402/#datetimeformat-objects use crate::{ - Context, JsArgs, JsData, JsExpect, JsResult, JsString, JsValue, NativeFunction, + Context, JsArgs, JsBigInt, JsData, JsExpect, JsResult, JsString, JsValue, NativeFunction, builtins::{ - BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + Boolean, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, date::utils::{ date_from_time, hour_from_time, min_from_time, month_from_time, ms_from_time, sec_from_time, time_clip, year_from_time, @@ -958,6 +958,7 @@ fn partition_date_time_pattern( x: &JsObject, context: &mut Context, ) -> JsResult { + let format_record = handle_date_time_value(&dtf, x, context)?; todo!() } @@ -1008,3 +1009,29 @@ fn is_temporal_object(value: &JsValue) -> bool { // 3. Return true. true } + +/// 15.6.22 HandleDateTimeValue +fn handle_date_time_value( + dtf: &DateTimeFormat, + x: &JsObject, + context: &mut Context, +) -> JsResult { + // if JsValue::from(x.clone()).is_number() { + // } else + if x.is::() { + // 15.6.20 HandleDateTimeTemporalInstant ( dateTimeFormat, instant ) + let format = dtf.temporal_instant_format; + return Ok(ValueFormatRecord { + format, + epoch_nanoseconds: JsBigInt::from( + x.downcast_ref::() + .unwrap() + .inner + .epoch_nanoseconds() + .as_i128(), + ), + is_plain: Boolean::from(false), + }); + } + Err(js_error!(TypeError: "Object is ZonedDateTime")) +} From 3e933088b4169509560a0379f026559a3d298143 Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:57:18 +0300 Subject: [PATCH 13/16] Add `ValueFormatRecord` --- core/engine/src/builtins/intl/date_time_format/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 5ade63d5482..6ea4ca7e705 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -1010,6 +1010,13 @@ fn is_temporal_object(value: &JsValue) -> bool { true } +/// 15.6.14 Value Format Records +struct ValueFormatRecord { + format: DateTimeFormatRecord, + epoch_nanoseconds: JsBigInt, + is_plain: bool, +} + /// 15.6.22 HandleDateTimeValue fn handle_date_time_value( dtf: &DateTimeFormat, @@ -1030,7 +1037,7 @@ fn handle_date_time_value( .epoch_nanoseconds() .as_i128(), ), - is_plain: Boolean::from(false), + is_plain: false, }); } Err(js_error!(TypeError: "Object is ZonedDateTime")) From e09320210751709aef623f17ae6654f981a2092a Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:26:44 +0300 Subject: [PATCH 14/16] Add `DateTimeFormatRecord` --- .../src/builtins/intl/date_time_format/mod.rs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 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 6ea4ca7e705..ebd2879df3d 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -10,14 +10,17 @@ use crate::{ Context, JsArgs, JsBigInt, JsData, JsExpect, JsResult, JsString, JsValue, NativeFunction, builtins::{ - Boolean, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, date::utils::{ date_from_time, hour_from_time, min_from_time, month_from_time, ms_from_time, sec_from_time, time_clip, year_from_time, }, intl::{ Service, - date_time_format::options::{DateStyle, FormatMatcher, FormatOptions, TimeStyle}, + date_time_format::options::{ + DateStyle, Day, DayPeriod, Era, FormatMatcher, FormatOptions, Hour, Minute, Month, + Second, SubsecondDigits, TimeStyle, TimeZoneName, WeekDay, Year, + }, locale::{canonicalize_locale_list, filter_locales, resolve_locale}, options::{IntlOptions, coerce_options_to_object}, }, @@ -96,6 +99,7 @@ pub(crate) struct DateTimeFormat { formatter: DateTimeFormatter, bound_format: Option, resolved_options: Option, + temporal_instant_format: DateTimeFormatRecord, } impl Service for DateTimeFormat { @@ -1010,7 +1014,23 @@ fn is_temporal_object(value: &JsValue) -> bool { true } -/// 15.6.14 Value Format Records +#[derive(Debug, Clone)] +struct DateTimeFormatRecord { + week_day: Option, + era: Option, + year: Option, + month: Option, + day: Option, + day_period: Option, + hour: Option, + minute: Option, + second: Option, + fractional_second_digits: Option, + time_zone_name: Option, + pattern: JsString, + pattern12: JsString, +} + struct ValueFormatRecord { format: DateTimeFormatRecord, epoch_nanoseconds: JsBigInt, @@ -1027,7 +1047,7 @@ fn handle_date_time_value( // } else if x.is::() { // 15.6.20 HandleDateTimeTemporalInstant ( dateTimeFormat, instant ) - let format = dtf.temporal_instant_format; + let format = dtf.temporal_instant_format.clone(); return Ok(ValueFormatRecord { format, epoch_nanoseconds: JsBigInt::from( From 10251caa9ffd81f923d2b9690c4cca594b7044e0 Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:13:51 +0300 Subject: [PATCH 15/16] Implement `partition_date_time_pattern` --- .../src/builtins/intl/date_time_format/mod.rs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 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 ebd2879df3d..2d184b8ec8d 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -961,9 +961,25 @@ fn partition_date_time_pattern( dtf: &DateTimeFormat, x: &JsObject, context: &mut Context, -) -> JsResult { +) -> JsResult> { let format_record = handle_date_time_value(&dtf, x, context)?; - todo!() + let epoch_ns = format_record.epoch_nanoseconds; + let format = format_record.format; + + let pattern = + if format.hour.is_some() && dtf.hour_cycle.is_some_and(|hc| hc != IcuHourCycle::H23) { + format.pattern12 + } else { + format.pattern + }; + + Ok(format_date_time_pattern( + dtf, + format, + pattern, + epoch_ns, + format_record.is_plain, + )) } /// 15.6.6 FormatDateTime ( dateTimeFormat, x ) @@ -975,7 +991,7 @@ pub(crate) fn format_date_time( let parts = partition_date_time_pattern(dtf, x, context)?; let mut result = String::new(); for part in parts { - result += part; + result += &part.1; } Ok(JsString::from(result).into()) } From 0c1e64282787c49cd3902aa6e39c2c68a93d74bd Mon Sep 17 00:00:00 2001 From: Vellumic <161718748+Vellumic@users.noreply.github.com> Date: Mon, 30 Mar 2026 04:42:24 +0300 Subject: [PATCH 16/16] Add comments with steps from the spec --- .../src/builtins/intl/date_time_format/mod.rs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 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 2d184b8ec8d..7591a171e0c 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -956,23 +956,30 @@ fn unwrap_date_time_format( .into()) } -///15.6.5 PartitionDateTimePattern ( dateTimeFormat, x ) +/// 15.6.5 PartitionDateTimePattern ( dateTimeFormat, x ) fn partition_date_time_pattern( dtf: &DateTimeFormat, x: &JsObject, context: &mut Context, ) -> JsResult> { + // 1. Let formatRecord be ? HandleDateTimeValue(dateTimeFormat, x). + // 2. Let epochNanoseconds be formatRecord.[[EpochNanoseconds]]. + // 3. Let format be formatRecord.[[Format]]. let format_record = handle_date_time_value(&dtf, x, context)?; let epoch_ns = format_record.epoch_nanoseconds; let format = format_record.format; - + // 4. If format has a field [[hour]] and dateTimeFormat.[[HourCycle]] is "h11" or "h12", then let pattern = if format.hour.is_some() && dtf.hour_cycle.is_some_and(|hc| hc != IcuHourCycle::H23) { + // a. Let pattern be format.[[pattern12]]. format.pattern12 + // 5. Else, } else { + // a. Let pattern be format.[[pattern]]. format.pattern }; - + // 5. Let result be FormatDateTimePattern(dateTimeFormat, format, pattern, epochNanoseconds, formatRecord.[[IsPlain]]). + // 6. Return result. Ok(format_date_time_pattern( dtf, format, @@ -988,11 +995,16 @@ pub(crate) fn format_date_time( x: &JsObject, context: &mut Context, ) -> JsResult { + // 1. Let parts be ? PartitionDateTimePattern(dateTimeFormat, x). + // 2. Let result be the empty String. let parts = partition_date_time_pattern(dtf, x, context)?; let mut result = String::new(); + // 3. For each Record { [[Type]], [[Value]] } part of parts, do for part in parts { + // a. Set result to the string-concatenation of result and part.[[Value]]. result += &part.1; } + // 4. Return result. Ok(JsString::from(result).into()) } @@ -1061,9 +1073,12 @@ fn handle_date_time_value( ) -> JsResult { // if JsValue::from(x.clone()).is_number() { // } else + // 7. If x has an [[InitializedTemporalInstant]] internal slot, return HandleDateTimeTemporalInstant(dateTimeFormat, x). if x.is::() { // 15.6.20 HandleDateTimeTemporalInstant ( dateTimeFormat, instant ) + // 1. Let format be dateTimeFormat.[[TemporalInstantFormat]]. let format = dtf.temporal_instant_format.clone(); + // 2. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: instant.[[EpochNanoseconds]], [[IsPlain]]: false }. return Ok(ValueFormatRecord { format, epoch_nanoseconds: JsBigInt::from( @@ -1076,5 +1091,7 @@ fn handle_date_time_value( is_plain: false, }); } + // 8. Assert: x has an [[InitializedTemporalZonedDateTime]] internal slot. + // 9. Throw a TypeError exception. Err(js_error!(TypeError: "Object is ZonedDateTime")) }