From b2a0384662b5633458bbaae09f135e59004e8623 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 1/4] Prepare `ZonedDateTime::to_locale_string` for an update --- .../builtins/temporal/zoneddatetime/mod.rs | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index 7f186c87878..595b79c21c9 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -11,7 +11,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, @@ -1612,25 +1612,38 @@ 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 { - // TODO: Update for ECMA-402 compliance + fn to_locale_string( + this: &JsValue, + _args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let zonedDateTime be the this value. + // 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]). let object = this.as_object(); let zdt = object .as_ref() .and_then(JsObject::downcast_ref::) - .ok_or_else(|| { - JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") - })?; + .ok_or_else( + || js_error!(TypeError: "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(feature = "intl")] + { + Ok(JsValue::null()) + } - Ok(JsString::from(ixdtf).into()) + #[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()) + } } /// 6.3.43 `Temporal.ZonedDateTime.prototype.toJSON ( )` From c124b08f8d0f4cf575ab0030e6b480dc7e25ae1f 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 2/4] Create `DateTimeFormat` --- .../src/builtins/temporal/zoneddatetime/mod.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index 595b79c21c9..42f111c5bd8 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -1629,6 +1629,21 @@ impl ZonedDateTime { #[cfg(feature = "intl")] { + use crate::builtins::intl::date_time_format::{ + FormatDefaults, FormatType, create_date_time_format, + }; + // 3. Let dateTimeFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, any, all, zonedDateTime.[[TimeZone]]). + 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(JsValue::null()) } From 77eead5603cafec33026b08f08c561aa9f062fd9 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 3/4] Check calendars for equality --- core/engine/src/builtins/intl/date_time_format/mod.rs | 7 +++++++ core/engine/src/builtins/temporal/zoneddatetime/mod.rs | 10 ++++++++++ 2 files changed, 17 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 de90a7fb999..fd3f784e9a3 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -252,6 +252,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 42f111c5bd8..3a502b46513 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -1643,6 +1643,16 @@ impl ZonedDateTime { FormatDefaults::All, context, )?; + // 4. If zonedDateTime.[[Calendar]] is not "iso8601" and CalendarEquals(zonedDateTime.[[Calendar]], dateTimeFormat.[[Calendar]]) is false, + // throw a RangeError exception. + let zdt_cal = zdt.inner.calendar(); + let dtf_cal = dtf.calendar_algorithm(); + + if !zdt_cal.is_iso() && zdt_cal.identifier() != dtf_cal.as_str() { + return Err( + js_error!(RangeError: "calendars {} and {} aren't compatible", zdt_cal.identifier(), dtf_cal.as_str()), + ); + } Ok(JsValue::null()) } From 14bdea49c0e97cf943131c33f396b3e737225b00 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 4/4] Implement `handle_date_time_value` --- core/engine/src/builtins/date/mod.rs | 2 +- .../src/builtins/intl/date_time_format/mod.rs | 90 +++++++++++++++---- .../builtins/temporal/zoneddatetime/mod.rs | 14 +-- 3 files changed, 81 insertions(+), 25 deletions(-) diff --git a/core/engine/src/builtins/date/mod.rs b/core/engine/src/builtins/date/mod.rs index 05e28775e1b..1294b055271 100644 --- a/core/engine/src/builtins/date/mod.rs +++ b/core/engine/src/builtins/date/mod.rs @@ -1556,7 +1556,7 @@ impl Date { 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_date_time(&dtf, x, context) + format_date_time(&dtf, &x.into(), context) } /// [`Date.prototype.toLocaleDateString()`][spec]. 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 fd3f784e9a3..292c4e8e677 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -26,6 +26,7 @@ use crate::{ options::{IntlOptions, coerce_options_to_object}, }, options::get_option, + temporal, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, error::JsNativeError, @@ -295,7 +296,7 @@ impl DateTimeFormat { }; // 5. Return ? FormatDateTime(dtf, x). - format_date_time(dtf.borrow().data(), x, context) + format_date_time(dtf.borrow().data(), &x.into(), context) }, dtf_clone, ), @@ -341,7 +342,7 @@ impl DateTimeFormat { date.to_number(context)? }; // 5. Return ? FormatDateTimeToParts(dtf, x). - format_date_time_to_parts(dtf.borrow().data(), x, context).map(JsValue::from) + format_date_time_to_parts(dtf.borrow().data(), &x.into(), context).map(JsValue::from) } // [`Intl.DateTimeFormat.prototype.formatRange ( startDate, endDate )`][spec] @@ -931,11 +932,11 @@ pub(crate) fn create_date_time_format( /// [spec]: https://tc39.es/ecma402/#sec-formatdatetime pub(crate) fn format_date_time( dtf: &DateTimeFormat, - timestamp: f64, + x: &JsValue, context: &mut Context, ) -> JsResult { // 1. Let parts be ? PartitionDateTimePattern(dateTimeFormat, x). - let parts = partition_date_time_pattern(dtf, timestamp, context)?; + let parts = partition_date_time_pattern(dtf, x, context)?; // 2. Let result be the empty String. // 3. For each Record { [[Type]], [[Value]] } part of parts, do @@ -946,7 +947,7 @@ pub(crate) fn format_date_time( pub(crate) fn format_date_time_to_parts( dtf: &DateTimeFormat, - timestamp: f64, + x: &JsValue, context: &mut Context, ) -> JsResult { #[derive(Debug, Clone)] @@ -981,7 +982,7 @@ pub(crate) fn format_date_time_to_parts( } // 1. Let parts be ? PartitionDateTimePattern(dateTimeFormat, x). - let parts = partition_date_time_pattern(dtf, timestamp, context)?; + let parts = partition_date_time_pattern(dtf, x, context)?; let mut collector = PartsCollector(Vec::new()); parts @@ -1023,25 +1024,21 @@ pub(crate) fn format_date_time_to_parts( /// [spec]: https://tc39.es/ecma402/#sec-partitiondatetimepattern fn partition_date_time_pattern<'a>( dtf: &'a DateTimeFormat, - timestamp: f64, + x: &JsValue, context: &mut Context, ) -> JsResult> { // 1. Set x to TimeClip(x). - let x = time_clip(timestamp); + let format_record = handle_date_time_value(dtf, x, context)?; - // 2. If x is `NaN`, throw a `RangeError` exception. - if x.is_nan() { - return Err(js_error!(RangeError: "formatted date cannot be NaN")); - } - // Let epochNanoseconds be ℤ(ℝ(x) × 10^6). - let epoch_ns = x as i128 * 1_000_000; + // 2. Let epochNanoseconds be formatRecord.[[EpochNanoseconds]]. + let epoch_ns = format_record.epoch_nanoseconds; - // 4. Let format be dateTimeFormat.[[DateTimeFormat]]. - // 5. If dateTimeFormat.[[HourCycle]] is "h11" or "h12", then + // 3. Let format be dateTimeFormat.[[DateTimeFormat]]. + // 4. If dateTimeFormat.[[HourCycle]] is "h11" or "h12", then // a. Let pattern be format.[[pattern12]]. - // 6. Else, + // 5. Else, // a. Let pattern be format.[[pattern]]. - // 7. Let result be FormatDateTimePattern(dateTimeFormat, format, pattern, epochNanoseconds). + // 6. Let result be FormatDateTimePattern(dateTimeFormat, format, pattern, epochNanoseconds). // // NOTE: `FormatDateTimePattern` is entirely handled in icu_datetime, and the only step // we really do is @@ -1261,3 +1258,60 @@ fn unwrap_date_time_format( .with_message("object was not an initialized `Intl.DateTimeFormat` object") .into()) } + +struct ValueFormatRecord<'a> { + _format: &'a DateTimeFormat, + epoch_nanoseconds: i128, + _is_plain: bool, +} + +/// [`HandleDateTimeValue`](https://tc39.es/proposal-temporal/#sec-temporal-handledatetimevalue) +fn handle_date_time_value<'a>( + dtf: &'a DateTimeFormat, + x: &JsValue, + _: &mut Context, +) -> JsResult> { + let o = x.as_object().expect("value is a Temporal object"); + + // 1. If x is a Number, return ? HandleDateTimeOthers(dateTimeFormat, x). + let (epoch_nanoseconds, is_plain) = if let Some(timestamp) = x.as_number() { + // 1. Set x to TimeClip(x). + // 2. If x is `NaN`, throw a `RangeError` exception. + // 3. Let epochNanoseconds be ℤ(ℝ(x) × 10^6). + // 4. Let format be dateTimeFormat.[[DateTimeFormat]]. + let x = time_clip(timestamp); + if x.is_nan() { + return Err(js_error!(RangeError: "formatted date cannot be NaN")); + } + (x as i128 * 1_000_000, false) + // 2. If x has an [[InitializedTemporalDate]] internal slot, return ? HandleDateTimeTemporalDate(dateTimeFormat, x). + } else if let Some(_to) = o.downcast_ref::() { + unimplemented!() + // 3. If x has an [[InitializedTemporalYearMonth]] internal slot, return ? HandleDateTimeTemporalYearMonth(dateTimeFormat, x). + } else if let Some(_to) = o.downcast_ref::() { + unimplemented!() + // 4. If x has an [[InitializedTemporalMonthDay]] internal slot, return ? HandleDateTimeTemporalMonthDay(dateTimeFormat, x). + } else if let Some(_to) = o.downcast_ref::() { + unimplemented!() + // 5. If x has an [[InitializedTemporalTime]] internal slot, return ? HandleDateTimeTemporalTime(dateTimeFormat, x). + } else if let Some(_to) = o.downcast_ref::() { + unimplemented!() + // 6. If x has an [[InitializedTemporalDateTime]] internal slot, return ? HandleDateTimeTemporalDateTime(dateTimeFormat, x). + } else if let Some(_to) = o.downcast_ref::() { + unimplemented!() + // 7. If x has an [[InitializedTemporalInstant]] internal slot, return HandleDateTimeTemporalInstant(dateTimeFormat, x). + } else if let Some(to) = o.downcast_ref::() { + // 1. Let format be dateTimeFormat.[[TemporalInstantFormat]]. + (to.inner.epoch_nanoseconds().as_i128(), false) + // 8. Assert: x has an [[InitializedTemporalZonedDateTime]] internal slot. + // 9. Throw a TypeError exception. + } else { + return Err(js_error!(TypeError: "Object is ZonedDateTime")); + }; + + Ok(ValueFormatRecord { + _format: dtf, + epoch_nanoseconds, + _is_plain: is_plain, + }) +} diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index 3a502b46513..bd56656e870 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -1614,7 +1614,7 @@ impl ZonedDateTime { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toLocaleString fn to_locale_string( this: &JsValue, - _args: &[JsValue], + args: &[JsValue], context: &mut Context, ) -> JsResult { // 1. Let zonedDateTime be the this value. @@ -1630,11 +1630,11 @@ impl ZonedDateTime { #[cfg(feature = "intl")] { use crate::builtins::intl::date_time_format::{ - FormatDefaults, FormatType, create_date_time_format, + FormatDefaults, FormatType, create_date_time_format, format_date_time, }; // 3. Let dateTimeFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, any, all, zonedDateTime.[[TimeZone]]). - let locales = _args.get_or_undefined(0); - let options = _args.get_or_undefined(1); + let locales = args.get_or_undefined(0); + let options = args.get_or_undefined(1); let dtf = create_date_time_format( locales, @@ -1653,8 +1653,10 @@ impl ZonedDateTime { js_error!(RangeError: "calendars {} and {} aren't compatible", zdt_cal.identifier(), dtf_cal.as_str()), ); } - - Ok(JsValue::null()) + // 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, context) } #[cfg(not(feature = "intl"))]