Skip to content

Commit bf3282f

Browse files
committed
refactor(intl): return DateTimeFormat from create_date_time_format, avoid JS alloc in Date toLocaleString path
1 parent 4e05780 commit bf3282f

File tree

1 file changed

+67
-49
lines changed
  • core/engine/src/builtins/intl/date_time_format

1 file changed

+67
-49
lines changed

core/engine/src/builtins/intl/date_time_format/mod.rs

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,19 @@ impl BuiltInConstructor for DateTimeFormat {
170170
let options = args.get_or_undefined(1);
171171

172172
// 2. Let dateTimeFormat be ? CreateDateTimeFormat(newTarget, locales, options, any, date).
173-
let date_time_format = create_date_time_format(
174-
new_target_inner,
173+
let dtf = create_date_time_format(
175174
locales,
176175
options,
177176
FormatType::Any,
178177
FormatDefaults::Date,
179178
context,
180179
)?;
180+
let prototype = get_prototype_from_constructor(
181+
new_target_inner,
182+
StandardConstructors::date_time_format,
183+
context,
184+
)?;
185+
let date_time_format = JsObject::from_proto_and_data(prototype, dtf);
181186

182187
// 3. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then
183188
// a. Let this be the this value.
@@ -551,23 +556,16 @@ impl ToLocalTime {
551556

552557
// ==== Abstract Operations ====
553558

559+
/// Creates a [`DateTimeFormat`] struct (internal slots only). The constructor wraps this in a
560+
/// `JsObject` with the correct prototype; Date.prototype.toLocaleString (and friends) use it
561+
/// directly with [`format_timestamp_with_dtf`] without allocating a JS object.
554562
pub(crate) fn create_date_time_format(
555-
new_target: &JsValue,
556563
locales: &JsValue,
557564
options: &JsValue,
558565
date_time_format_type: FormatType,
559566
defaults: FormatDefaults,
560567
context: &mut Context,
561-
) -> JsResult<JsObject> {
562-
// 1. Let dateTimeFormat be ? OrdinaryCreateFromConstructor(newTarget, "%Intl.DateTimeFormat.prototype%",
563-
// « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]],
564-
// [[HourCycle]], [[DateStyle]], [[TimeStyle]], [[DateTimeFormat]], [[BoundFormat]] »).
565-
let prototype = get_prototype_from_constructor(
566-
new_target,
567-
StandardConstructors::date_time_format,
568-
context,
569-
)?;
570-
568+
) -> JsResult<DateTimeFormat> {
571569
// 2. Let hour12 be undefined. <- TODO
572570
// 3. Let modifyResolutionOptions be a new Abstract Closure with parameters (options) that captures hour12 and performs the following steps when called:
573571
// a. Set hour12 to options.[[hour12]].
@@ -843,21 +841,57 @@ pub(crate) fn create_date_time_format(
843841
)
844842
.map_err(|e| JsNativeError::range().with_message(format!("failed to load formatter: {e}")))?;
845843

846-
Ok(JsObject::from_proto_and_data(
847-
prototype,
848-
DateTimeFormat {
849-
locale: resolved_locale,
850-
calendar_algorithm: intl_options.preferences.calendar_algorithm,
851-
numbering_system: intl_options.preferences.numbering_system,
852-
hour_cycle: intl_options.preferences.hour_cycle,
853-
date_style,
854-
time_style,
855-
time_zone,
856-
fieldset,
857-
formatter,
858-
bound_format: None,
859-
},
860-
))
844+
Ok(DateTimeFormat {
845+
locale: resolved_locale,
846+
calendar_algorithm: intl_options.preferences.calendar_algorithm,
847+
numbering_system: intl_options.preferences.numbering_system,
848+
hour_cycle: intl_options.preferences.hour_cycle,
849+
date_style,
850+
time_style,
851+
time_zone,
852+
fieldset,
853+
formatter,
854+
bound_format: None,
855+
})
856+
}
857+
858+
/// Formats a timestamp (epoch milliseconds) using the given [`DateTimeFormat`] internals.
859+
/// Used by the bound `format` function and by [`format_date_time_locale`] without creating a JS object.
860+
fn format_timestamp_with_dtf(
861+
dtf: &DateTimeFormat,
862+
timestamp: f64,
863+
context: &mut Context,
864+
) -> JsResult<JsString> {
865+
// FormatDateTime / PartitionDateTimePattern: TimeClip, then ToLocalTime, then format.
866+
let x = time_clip(timestamp);
867+
if x.is_nan() {
868+
return Err(js_error!(RangeError: "formatted date cannot be NaN"));
869+
}
870+
let time_zone_offset = match dtf.time_zone {
871+
FormatTimeZone::UtcOffset(offset) => offset.to_seconds(),
872+
FormatTimeZone::Identifier((_, time_zone_id)) => {
873+
let epoch_ns = x as i128 * 1_000_000;
874+
let offset_seconds = context
875+
.timezone_provider()
876+
.transition_nanoseconds_for_utc_epoch_nanoseconds(time_zone_id, epoch_ns)
877+
.map_err(
878+
|_e| js_error!(RangeError: "unable to determine transition nanoseconds"),
879+
)?;
880+
offset_seconds.0 as i32
881+
}
882+
};
883+
let tz = x + f64::from(time_zone_offset * 1_000);
884+
let fields = ToLocalTime::from_local_epoch_milliseconds(tz)?;
885+
let dt = fields.to_formattable_datetime()?;
886+
let tz_info = dtf.time_zone.to_time_zone_info();
887+
let tz_info_at_time = tz_info.at_date_time_iso(dt);
888+
let zdt = ZonedDateTime {
889+
date: dt.date,
890+
time: dt.time,
891+
zone: tz_info_at_time,
892+
};
893+
let result = dtf.formatter.format(&zdt).to_string();
894+
Ok(JsString::from(result))
861895
}
862896

863897
fn date_time_style_format(
@@ -974,8 +1008,8 @@ fn unwrap_date_time_format(
9741008

9751009
/// Shared helper used by Date.prototype.toLocaleString,
9761010
/// Date.prototype.toLocaleDateString, and Date.prototype.toLocaleTimeString.
977-
/// Applies `ToDateTimeOptions` defaults, constructs `Intl.DateTimeFormat`,
978-
/// and formats the provided timestamp.
1011+
/// Applies `ToDateTimeOptions` defaults, calls [`create_date_time_format`], and formats
1012+
/// the timestamp via [`format_timestamp_with_dtf`] without allocating a JS object.
9791013
#[allow(clippy::too_many_arguments)]
9801014
pub(crate) fn format_date_time_locale(
9811015
locales: &JsValue,
@@ -1004,24 +1038,8 @@ pub(crate) fn format_date_time_locale(
10041038
context,
10051039
)?;
10061040
}
1007-
let new_target = context
1008-
.intrinsics()
1009-
.constructors()
1010-
.date_time_format()
1011-
.constructor()
1012-
.into();
10131041
let options_value = options.into();
1014-
let dtf = create_date_time_format(
1015-
&new_target,
1016-
locales,
1017-
&options_value,
1018-
format_type,
1019-
defaults,
1020-
context,
1021-
)?;
1022-
let format_val = dtf.get(js_string!("format"), context)?;
1023-
let format_fn = format_val
1024-
.as_callable()
1025-
.ok_or_else(|| JsNativeError::typ().with_message("format is not callable"))?;
1026-
format_fn.call(&dtf.into(), &[JsValue::from(timestamp)], context)
1042+
let dtf = create_date_time_format(locales, &options_value, format_type, defaults, context)?;
1043+
let result = format_timestamp_with_dtf(&dtf, timestamp, context)?;
1044+
Ok(JsValue::from(result))
10271045
}

0 commit comments

Comments
 (0)