From 99c74445d3c8a62cde4cf70f3d7c8d238c4d9884 Mon Sep 17 00:00:00 2001 From: duonglaiquang Date: Tue, 17 Mar 2026 14:44:16 +0900 Subject: [PATCH] NativeDate: make toLocaleString() behave more like real browsers --- .../org/mozilla/javascript/NativeDate.java | 89 +++++++++------ .../javascript/tests/es6/NativeDateTest.java | 102 ++++++++---------- 2 files changed, 102 insertions(+), 89 deletions(-) diff --git a/rhino/src/main/java/org/mozilla/javascript/NativeDate.java b/rhino/src/main/java/org/mozilla/javascript/NativeDate.java index 426258fc25..bb6d405548 100644 --- a/rhino/src/main/java/org/mozilla/javascript/NativeDate.java +++ b/rhino/src/main/java/org/mozilla/javascript/NativeDate.java @@ -12,7 +12,9 @@ import java.time.Instant; import java.time.ZoneId; +import java.time.chrono.IsoChronology; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Arrays; @@ -24,6 +26,7 @@ * This class implements the Date native object. See ECMA 15.9. * * @author Mike McCabe + * @author Lai Quang Duong *

Significant parts of this code are adapted from the venerable jsdate.cpp (also Mozilla): * jsdate.cpp */ @@ -1647,30 +1650,6 @@ private static NativeDate jsConstructor(Context cx, Object[] args) { } private static String toLocale_helper(Context cx, double t, int methodId, Object[] args) { - DateTimeFormatter formatter; - switch (methodId) { - case Id_toLocaleString: - formatter = - cx.getLanguageVersion() >= Context.VERSION_ES6 - ? localeDateTimeFormatterES6 - : localeDateTimeFormatter; - break; - case Id_toLocaleTimeString: - formatter = - cx.getLanguageVersion() >= Context.VERSION_ES6 - ? localeTimeFormatterES6 - : localeTimeFormatter; - break; - case Id_toLocaleDateString: - formatter = - cx.getLanguageVersion() >= Context.VERSION_ES6 - ? localeDateFormatterES6 - : localeDateFormatter; - break; - default: - throw new AssertionError(); // unreachable - } - final List languageTags = new ArrayList<>(); if (args.length != 0) { // we use the 'locales' argument but ignore the second 'options' argument as per spec of @@ -1686,15 +1665,67 @@ private static String toLocale_helper(Context cx, double t, int methodId, Object } } + Locale firstSupportedLocale = null; final List availableLocales = Arrays.asList(Locale.getAvailableLocales()); for (String languageTag : languageTags) { Locale locale = Locale.forLanguageTag(languageTag); if (availableLocales.contains(locale)) { - formatter = formatter.withLocale(locale); + firstSupportedLocale = locale; break; } } + DateTimeFormatter formatter; + switch (methodId) { + case Id_toLocaleString: + if (cx.getLanguageVersion() >= Context.VERSION_ES6) { + final String pattern = + DateTimeFormatterBuilder.getLocalizedDateTimePattern( + FormatStyle.SHORT, + FormatStyle.MEDIUM, + IsoChronology.INSTANCE, + firstSupportedLocale != null + ? firstSupportedLocale + : Locale.getDefault()); + formatter = DateTimeFormatter.ofPattern(pattern.replaceAll("y+", "yyyy")); + } else { + formatter = localeDateTimeFormatter; + } + break; + case Id_toLocaleTimeString: + if (cx.getLanguageVersion() >= Context.VERSION_ES6) { + final String pattern = + DateTimeFormatterBuilder.getLocalizedDateTimePattern( + null, + FormatStyle.MEDIUM, + IsoChronology.INSTANCE, + firstSupportedLocale != null + ? firstSupportedLocale + : Locale.getDefault()); + formatter = DateTimeFormatter.ofPattern(pattern); + } else { + formatter = localeTimeFormatter; + } + break; + case Id_toLocaleDateString: + if (cx.getLanguageVersion() >= Context.VERSION_ES6) { + final String pattern = + DateTimeFormatterBuilder.getLocalizedDateTimePattern( + FormatStyle.SHORT, + null, + IsoChronology.INSTANCE, + firstSupportedLocale != null + ? firstSupportedLocale + : Locale.getDefault()); + formatter = DateTimeFormatter.ofPattern(pattern.replaceAll("y+", "yyyy")); + } else { + formatter = localeDateFormatter; + } + break; + default: + throw new AssertionError(); // unreachable + } + final ZoneId zoneid = cx.getTimeZone().toZoneId(); final String formatted = formatter.format(Instant.ofEpochMilli((long) t).atZone(zoneid)); // jdk 21 uses a nnbsp in front of 'PM' @@ -2052,13 +2083,5 @@ private static double makeDate(Context cx, double date, Object[] args, int metho private static final DateTimeFormatter localeTimeFormatter = DateTimeFormatter.ofPattern("h:mm:ss a z"); - // use FormatStyle.SHORT for these as per spec of an implementation that has no - // Intl.DateTimeFormat support - private static final DateTimeFormatter localeDateTimeFormatterES6 = - DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT); - private static final DateTimeFormatter localeDateFormatterES6 = - DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT); - private static final DateTimeFormatter localeTimeFormatterES6 = - DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); private double date; } diff --git a/tests/src/test/java/org/mozilla/javascript/tests/es6/NativeDateTest.java b/tests/src/test/java/org/mozilla/javascript/tests/es6/NativeDateTest.java index 0c250e6f96..f9682a2d18 100644 --- a/tests/src/test/java/org/mozilla/javascript/tests/es6/NativeDateTest.java +++ b/tests/src/test/java/org/mozilla/javascript/tests/es6/NativeDateTest.java @@ -581,76 +581,45 @@ public void ctorDouble() { @Test public void toLocaleEnUs() { - // real browser toLocale("12/18/2021, 10:23:00 PM", "new - // Date('2021-12-18T22:23').toLocaleString('en-US')"); - // toLocale("12/18/21 10:23 PM", "new Date('2021-12-18T22:23').toLocaleString('en-US')"); - toLocale("12/18/21, 10:23 PM", "new Date('2021-12-18T22:23').toLocaleString('en-US')"); - - // real browser toLocale("12/18/2021", "new - // Date('2021-12-18T22:23').toLocaleDateString('en-US')"); - toLocale("12/18/21", "new Date('2021-12-18T22:23').toLocaleDateString('en-US')"); - - // real browser toLocale("10:23:00 PM", "new - // Date('2021-12-18T22:23').toLocaleTimeString('en-US')"); - toLocale("10:23 PM", "new Date('2021-12-18T22:23').toLocaleTimeString('en-US')"); + toLocale("12/18/2021, 10:23:00 PM", "new Date('2021-12-18T22:23').toLocaleString('en-US')"); + toLocale("12/18/2021", "new Date('2021-12-18T22:23').toLocaleDateString('en-US')"); + toLocale("10:23:00 PM", "new Date('2021-12-18T22:23').toLocaleTimeString('en-US')"); } @Test public void toLocaleDeDe() { - // real browser toLocale("18.12.2021, 22:23:00", "new - // Date('2021-12-18T22:23').toLocaleString('de-DE')"); - // toLocale("18.12.21 22:23", "new Date('2021-12-18T22:23').toLocaleString('de-DE')"); - toLocale("18.12.21, 22:23", "new Date('2021-12-18T22:23').toLocaleString('de-DE')"); - - // real browser toLocale("18.12.2021", "new - // Date('2021-12-18T22:23').toLocaleDateString('de-DE')"); - toLocale("18.12.21", "new Date('2021-12-18T22:23').toLocaleDateString('de-DE')"); - - // real browser toLocale("22:23:00", "new - // Date('2021-12-18T22:23').toLocaleTimeString('de-DE')"); - toLocale("22:23", "new Date('2021-12-18T22:23').toLocaleTimeString('de-DE')"); + toLocale("18.12.2021, 22:23:00", "new Date('2021-12-18T22:23').toLocaleString('de-DE')"); + toLocale("18.12.2021", "new Date('2021-12-18T22:23').toLocaleDateString('de-DE')"); + toLocale("22:23:00", "new Date('2021-12-18T22:23').toLocaleTimeString('de-DE')"); } @Test public void toLocaleJaJp() { - // real browser toLocale("2021/12/18 22:23:00", "new - // Date('2021-12-18T22:23').toLocaleString('ja-JP')"); - // toLocale("21/12/18 22:23", "new Date('2021-12-18T22:23').toLocaleString('ja-JP')"); - toLocale("2021/12/18 22:23", "new Date('2021-12-18T22:23').toLocaleString('ja-JP')"); - - // real browser toLocale("2021/12/18", "new - // Date('2021-12-18T22:23').toLocaleDateString('ja-JP')"); - // toLocale("21/12/18", "new Date('2021-12-18T22:23').toLocaleDateString('ja-JP')"); + toLocale("2021/12/18 22:23:00", "new Date('2021-12-18T22:23').toLocaleString('ja-JP')"); toLocale("2021/12/18", "new Date('2021-12-18T22:23').toLocaleDateString('ja-JP')"); + toLocale("22:23:00", "new Date('2021-12-18T22:23').toLocaleTimeString('ja-JP')"); + } - // real browser toLocale("22:23:00", "new - // Date('2021-12-18T22:23').toLocaleTimeString('ja-JP')"); - toLocale("22:23", "new Date('2021-12-18T22:23').toLocaleTimeString('ja-JP')"); + @Test + public void toLocaleFrFr() { + toLocale("18/12/2021 22:23:00", "new Date('2021-12-18T22:23').toLocaleString('fr-FR')"); + toLocale("18/12/2021", "new Date('2021-12-18T22:23').toLocaleDateString('fr-FR')"); + toLocale("22:23:00", "new Date('2021-12-18T22:23').toLocaleTimeString('fr-FR')"); + } + + @Test + public void toLocaleFiFi() { + // real browser: "18.12.2021 klo 22.23.00" (includes "klo" between date and time) + toLocale("18.12.2021 22.23.00", "new Date('2021-12-18T22:23').toLocaleString('fi-FI')"); + toLocale("18.12.2021", "new Date('2021-12-18T22:23').toLocaleDateString('fi-FI')"); + toLocale("22.23.00", "new Date('2021-12-18T22:23').toLocaleTimeString('fi-FI')"); } @Test public void toLocaleArray() { - // real browser toLocale("2021/12/18 22:23:00", "new - // Date('2021-12-18T22:23').toLocaleString(['foo', 'ja-JP', 'en-US'])"); - // toLocale("21/12/18 22:23", "new Date('2021-12-18T22:23').toLocaleString(['foo', 'ja-JP', - // 'en-US'])"); - toLocale( - "2021/12/18 22:23", - "new Date('2021-12-18T22:23').toLocaleString(['foo', 'ja-JP', 'en-US'])"); - - // real browser toLocale("2021/12/18", "new - // Date('2021-12-18T22:23').toLocaleDateString(['foo', 'ja-JP', 'en-US'])"); - // toLocale("21/12/18", "new Date('2021-12-18T22:23').toLocaleDateString(['foo', 'ja-JP', - // 'en-US'])"); - toLocale( - "2021/12/18", - "new Date('2021-12-18T22:23').toLocaleDateString(['foo', 'ja-JP', 'en-US'])"); - - // real browser toLocale("22:23:00", "new - // Date('2021-12-18T22:23').toLocaleTimeString(['foo', 'ja-JP', 'en-US'])"); - toLocale( - "22:23", - "new Date('2021-12-18T22:23').toLocaleTimeString(['foo', 'ja-JP', 'en-US'])"); + toLocale("2021/12/18 22:23:00", "new Date('2021-12-18T22:23').toLocaleString(['foo', 'ja-JP', 'en-US'])"); + toLocale("2021/12/18", "new Date('2021-12-18T22:23').toLocaleDateString(['foo', 'ja-JP', 'en-US'])"); + toLocale("22:23:00", "new Date('2021-12-18T22:23').toLocaleTimeString(['foo', 'ja-JP', 'en-US'])"); } private static void toLocale(final String expected, final String js) { @@ -666,6 +635,27 @@ private static void toLocale(final String expected, final String js) { }); } + @Test + public void toLocaleEpochDate() { + toLocale("1/1/1970, 12:00:00 AM", "new Date(0).toLocaleString('en-US')"); + // real browser: "1.1.1970, 00:00:00" (without zero-padding) + toLocale("01.01.1970, 00:00:00", "new Date(0).toLocaleString('de-DE')"); + // real browser: "1970/1/1 0:00:00" (without zero-padding) + toLocale("1970/01/01 0:00:00", "new Date(0).toLocaleString('ja-JP')"); + toLocale("1/1/1970", "new Date(0).toLocaleDateString('en-US')"); + // real browser: "1.1.1970" (without zero-padding) + toLocale("01.01.1970", "new Date(0).toLocaleDateString('de-DE')"); + toLocale("12:00:00 AM", "new Date(0).toLocaleTimeString('en-US')"); + toLocale("00:00:00", "new Date(0).toLocaleTimeString('de-DE')"); + } + + @Test + public void toLocaleWithSeconds() { + toLocale("12/18/2021, 10:23:45 PM", "new Date('2021-12-18T22:23:45').toLocaleString('en-US')"); + toLocale("10:23:45 PM", "new Date('2021-12-18T22:23:45').toLocaleTimeString('en-US')"); + toLocale("22:23:45", "new Date('2021-12-18T22:23:45').toLocaleTimeString('ja-JP')"); + } + @Test public void toDateStringGMT() { toDateString("Sat Dec 18 2021", "GMT");