Skip to content

Commit 4e05780

Browse files
committed
feat(intl): implement Date.prototype.toLocaleString, toLocaleDateString, and toLocaleTimeString
1 parent 3016f49 commit 4e05780

File tree

3 files changed

+234
-25
lines changed

3 files changed

+234
-25
lines changed

core/engine/src/builtins/date/mod.rs

Lines changed: 124 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
99
1010
use crate::{
11-
Context, JsArgs, JsData, JsError, JsResult, JsString,
11+
Context, JsArgs, JsData, JsResult, JsString,
1212
builtins::{
1313
BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject,
1414
date::utils::{
@@ -1608,6 +1608,15 @@ impl Date {
16081608
func.call(this, &[], context)
16091609
}
16101610

1611+
/// Returns the `[[DateValue]]` internal slot (RequireInternalSlot(dateObject, `[[DateValue]]`)).
1612+
fn this_date_value(this: &JsValue) -> JsResult<f64> {
1613+
this.as_object()
1614+
.and_then(|obj| obj.downcast_ref::<Date>().as_deref().copied())
1615+
.ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))
1616+
.map(|d| d.0)
1617+
.map_err(Into::into)
1618+
}
1619+
16111620
/// [`Date.prototype.toLocaleDateString()`][spec].
16121621
///
16131622
/// The `toLocaleDateString()` method returns the date portion of the given Date instance according
@@ -1616,16 +1625,47 @@ impl Date {
16161625
/// More information:
16171626
/// - [MDN documentation][mdn]
16181627
///
1619-
/// [spec]: https://tc39.es/ecma262/#sec-date.prototype.tolocaledatestring
1628+
/// [spec]: https://tc39.es/ecma402/#sup-date.prototype.tolocaledatestring
16201629
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString
1630+
#[allow(
1631+
unused_variables,
1632+
reason = "`args` and `context` are used when the `intl` feature is enabled"
1633+
)]
16211634
pub(crate) fn to_locale_date_string(
1622-
_this: &JsValue,
1623-
_args: &[JsValue],
1624-
_context: &mut Context,
1635+
this: &JsValue,
1636+
args: &[JsValue],
1637+
context: &mut Context,
16251638
) -> JsResult<JsValue> {
1626-
Err(JsError::from_opaque(JsValue::new(js_string!(
1627-
"Function Unimplemented"
1628-
))))
1639+
// Let dateObject be the this value.
1640+
// Perform ? RequireInternalSlot(dateObject, [[DateValue]]).
1641+
// Let x be dateObject.[[DateValue]].
1642+
let t = Self::this_date_value(this)?;
1643+
// If x is NaN, return "Invalid Date".
1644+
if t.is_nan() {
1645+
return Ok(JsValue::new(js_string!("Invalid Date")));
1646+
}
1647+
// Let dateFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, date, date).
1648+
// Return ! FormatDateTime(dateFormat, x).
1649+
#[cfg(feature = "intl")]
1650+
{
1651+
use crate::builtins::intl::date_time_format::{
1652+
FormatDefaults, FormatType, format_date_time_locale,
1653+
};
1654+
let locales = args.get_or_undefined(0);
1655+
let options = args.get_or_undefined(1);
1656+
format_date_time_locale(
1657+
locales,
1658+
options,
1659+
FormatType::Date,
1660+
FormatDefaults::Date,
1661+
t,
1662+
context,
1663+
)
1664+
}
1665+
#[cfg(not(feature = "intl"))]
1666+
{
1667+
Self::to_string(this, &[], context)
1668+
}
16291669
}
16301670

16311671
/// [`Date.prototype.toLocaleString()`][spec].
@@ -1635,16 +1675,47 @@ impl Date {
16351675
/// More information:
16361676
/// - [MDN documentation][mdn]
16371677
///
1638-
/// [spec]: https://tc39.es/ecma262/#sec-date.prototype.tolocalestring
1678+
/// [spec]: https://tc39.es/ecma402/#sup-date.prototype.tolocalestring
16391679
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString
1680+
#[allow(
1681+
unused_variables,
1682+
reason = "`args` and `context` are used when the `intl` feature is enabled"
1683+
)]
16401684
pub(crate) fn to_locale_string(
1641-
_this: &JsValue,
1642-
_: &[JsValue],
1643-
_context: &mut Context,
1685+
this: &JsValue,
1686+
args: &[JsValue],
1687+
context: &mut Context,
16441688
) -> JsResult<JsValue> {
1645-
Err(JsError::from_opaque(JsValue::new(js_string!(
1646-
"Function Unimplemented]"
1647-
))))
1689+
// Let dateObject be the this value.
1690+
// Perform ? RequireInternalSlot(dateObject, [[DateValue]]).
1691+
// Let x be dateObject.[[DateValue]].
1692+
let t = Self::this_date_value(this)?;
1693+
// If x is NaN, return "Invalid Date".
1694+
if t.is_nan() {
1695+
return Ok(JsValue::new(js_string!("Invalid Date")));
1696+
}
1697+
// Let dateFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, any, all).
1698+
// Return ! FormatDateTime(dateFormat, x).
1699+
#[cfg(feature = "intl")]
1700+
{
1701+
use crate::builtins::intl::date_time_format::{
1702+
FormatDefaults, FormatType, format_date_time_locale,
1703+
};
1704+
let locales = args.get_or_undefined(0);
1705+
let options = args.get_or_undefined(1);
1706+
format_date_time_locale(
1707+
locales,
1708+
options,
1709+
FormatType::Any,
1710+
FormatDefaults::All,
1711+
t,
1712+
context,
1713+
)
1714+
}
1715+
#[cfg(not(feature = "intl"))]
1716+
{
1717+
Self::to_string(this, &[], context)
1718+
}
16481719
}
16491720

16501721
/// [`Date.prototype.toLocaleTimeString()`][spec].
@@ -1655,16 +1726,47 @@ impl Date {
16551726
/// More information:
16561727
/// - [MDN documentation][mdn]
16571728
///
1658-
/// [spec]: https://tc39.es/ecma262/#sec-date.prototype.tolocaletimestring
1729+
/// [spec]: https://tc39.es/ecma402/#sup-date.prototype.tolocaletimestring
16591730
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString
1731+
#[allow(
1732+
unused_variables,
1733+
reason = "`args` and `context` are used when the `intl` feature is enabled"
1734+
)]
16601735
pub(crate) fn to_locale_time_string(
1661-
_this: &JsValue,
1662-
_args: &[JsValue],
1663-
_context: &mut Context,
1736+
this: &JsValue,
1737+
args: &[JsValue],
1738+
context: &mut Context,
16641739
) -> JsResult<JsValue> {
1665-
Err(JsError::from_opaque(JsValue::new(js_string!(
1666-
"Function Unimplemented]"
1667-
))))
1740+
// Let dateObject be the this value.
1741+
// Perform ? RequireInternalSlot(dateObject, [[DateValue]]).
1742+
// Let x be dateObject.[[DateValue]].
1743+
let t = Self::this_date_value(this)?;
1744+
// If x is NaN, return "Invalid Date".
1745+
if t.is_nan() {
1746+
return Ok(JsValue::new(js_string!("Invalid Date")));
1747+
}
1748+
// Let timeFormat be ? CreateDateTimeFormat(%Intl.DateTimeFormat%, locales, options, time, time).
1749+
// Return ! FormatDateTime(timeFormat, x).
1750+
#[cfg(feature = "intl")]
1751+
{
1752+
use crate::builtins::intl::date_time_format::{
1753+
FormatDefaults, FormatType, format_date_time_locale,
1754+
};
1755+
let locales = args.get_or_undefined(0);
1756+
let options = args.get_or_undefined(1);
1757+
format_date_time_locale(
1758+
locales,
1759+
options,
1760+
FormatType::Time,
1761+
FormatDefaults::Time,
1762+
t,
1763+
context,
1764+
)
1765+
}
1766+
#[cfg(not(feature = "intl"))]
1767+
{
1768+
Self::to_string(this, &[], context)
1769+
}
16681770
}
16691771

16701772
/// [`Date.prototype.toString()`][spec].

core/engine/src/builtins/date/tests.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,3 +943,52 @@ fn date_parse_hour24_validation() {
943943
TestAction::assert("isNaN(Date.parse('2024-01-01T24:00:00.001Z'))"),
944944
]);
945945
}
946+
947+
#[test]
948+
#[cfg(feature = "intl")]
949+
fn date_proto_to_locale_string_intl() {
950+
run_test_actions([
951+
// Invalid receiver: spec requires TypeError
952+
TestAction::assert_native_error(
953+
"Date.prototype.toLocaleString.call({})",
954+
JsNativeErrorKind::Type,
955+
"'this' is not a Date",
956+
),
957+
TestAction::assert_native_error(
958+
"Date.prototype.toLocaleDateString.call({})",
959+
JsNativeErrorKind::Type,
960+
"'this' is not a Date",
961+
),
962+
TestAction::assert_native_error(
963+
"Date.prototype.toLocaleTimeString.call({})",
964+
JsNativeErrorKind::Type,
965+
"'this' is not a Date",
966+
),
967+
TestAction::assert_eq("new Date(NaN).toLocaleString()", js_str!("Invalid Date")),
968+
TestAction::assert("typeof new Date(2020, 6, 8).toLocaleString() === 'string'"),
969+
TestAction::assert("typeof new Date(2020, 6, 8).toLocaleDateString() === 'string'"),
970+
TestAction::assert("typeof new Date(2020, 6, 8).toLocaleTimeString() === 'string'"),
971+
TestAction::assert("typeof new Date(0).toLocaleString('en-US') === 'string'"),
972+
TestAction::assert("typeof new Date(0).toLocaleDateString('en-US') === 'string'"),
973+
TestAction::assert("typeof new Date(0).toLocaleDateString('de-DE') === 'string'"),
974+
TestAction::assert("typeof new Date(0).toLocaleTimeString('en-US') === 'string'"),
975+
// Prove locale pipeline: different locales produce different output
976+
TestAction::assert(
977+
"new Date(0).toLocaleDateString('en-US') !== new Date(0).toLocaleDateString('de-DE')",
978+
),
979+
TestAction::assert(
980+
"new Date(0).toLocaleString('en-US') !== new Date(0).toLocaleString('de-DE')",
981+
),
982+
TestAction::assert(
983+
"new Date(0).toLocaleTimeString('en-US') !== new Date(0).toLocaleTimeString('de-DE')",
984+
),
985+
// Prove ToDateTimeOptions pipeline: options affect output
986+
TestAction::assert(
987+
"typeof new Date(0).toLocaleDateString('en-US', { dateStyle: 'short' }) === 'string'",
988+
),
989+
// Prove output is a string and not empty
990+
TestAction::assert(
991+
"new Date(0).toLocaleDateString('en-US', { dateStyle: 'short' }).length > 0",
992+
),
993+
]);
994+
}

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

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ impl ToLocalTime {
551551

552552
// ==== Abstract Operations ====
553553

554-
fn create_date_time_format(
554+
pub(crate) fn create_date_time_format(
555555
new_target: &JsValue,
556556
locales: &JsValue,
557557
options: &JsValue,
@@ -807,13 +807,13 @@ fn create_date_time_format(
807807
// 2. If value is not undefined, set needDefaults to false.
808808
let needs_defaults = format_options.check_dtf_type(date_time_format_type);
809809
// d. If needDefaults is true and defaults is either date or all, then
810-
if needs_defaults && defaults != FormatDefaults::Time {
810+
if needs_defaults && (defaults == FormatDefaults::All || defaults == FormatDefaults::Date) {
811811
// i. For each property name prop of « "year", "month", "day" », do
812812
// 1. Set formatOptions.[[<prop>]] to "numeric".
813813
format_options.set_date_defaults();
814814
}
815815
// e. If needDefaults is true and defaults is either time or all, then
816-
if needs_defaults && defaults != FormatDefaults::Date {
816+
if needs_defaults && (defaults == FormatDefaults::All || defaults == FormatDefaults::Time) {
817817
// i. For each property name prop of « "hour", "minute", "second" », do
818818
// 1. Set formatOptions.[[<prop>]] to "numeric".
819819
format_options.set_time_defaults();
@@ -911,10 +911,14 @@ pub(crate) enum FormatType {
911911
Any,
912912
}
913913

914+
/// Indicates which default fields should be applied when `ToDateTimeOptions`
915+
/// determines defaults are needed. `All` applies both date and time defaults.
914916
#[derive(Debug, Clone, Copy, PartialEq)]
915917
pub(crate) enum FormatDefaults {
916918
Date,
917919
Time,
920+
/// Apply both date and time defaults (e.g. for `toLocaleString`).
921+
All,
918922
}
919923

920924
/// Abstract operation [`UnwrapDateTimeFormat ( dtf )`][spec].
@@ -967,3 +971,57 @@ fn unwrap_date_time_format(
967971
.with_message("object was not an `Intl.DateTimeFormat` object")
968972
.into())
969973
}
974+
975+
/// Shared helper used by Date.prototype.toLocaleString,
976+
/// Date.prototype.toLocaleDateString, and Date.prototype.toLocaleTimeString.
977+
/// Applies `ToDateTimeOptions` defaults, constructs `Intl.DateTimeFormat`,
978+
/// and formats the provided timestamp.
979+
#[allow(clippy::too_many_arguments)]
980+
pub(crate) fn format_date_time_locale(
981+
locales: &JsValue,
982+
options: &JsValue,
983+
format_type: FormatType,
984+
defaults: FormatDefaults,
985+
timestamp: f64,
986+
context: &mut Context,
987+
) -> JsResult<JsValue> {
988+
let options = coerce_options_to_object(options, context)?;
989+
if format_type != FormatType::Time
990+
&& get_option::<DateStyle>(&options, js_string!("dateStyle"), context)?.is_none()
991+
{
992+
options.create_data_property_or_throw(
993+
js_string!("dateStyle"),
994+
JsValue::from(js_string!("long")),
995+
context,
996+
)?;
997+
}
998+
if format_type != FormatType::Date
999+
&& get_option::<TimeStyle>(&options, js_string!("timeStyle"), context)?.is_none()
1000+
{
1001+
options.create_data_property_or_throw(
1002+
js_string!("timeStyle"),
1003+
JsValue::from(js_string!("long")),
1004+
context,
1005+
)?;
1006+
}
1007+
let new_target = context
1008+
.intrinsics()
1009+
.constructors()
1010+
.date_time_format()
1011+
.constructor()
1012+
.into();
1013+
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)
1027+
}

0 commit comments

Comments
 (0)