From ac557bb00fa815946c87732c7b81174a73cde5be Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 22 Nov 2024 13:39:27 -0800 Subject: [PATCH] Revert "Disable DateTimeFormat::formatToParts for Apple platform (#1155)" (#1567) Summary: This reverts commit c5a633f6452902cd0b5c9392a1e4f51a33933284. That commit removed the Intl.DateTimeFormat.formatToParts for Apple platforms from Hermes. However, the community started depending on it, and we ended up manually reverting that commit in the release branch for the past 3 releases. This is like reverting the commit in main, so we are proceeding with that approach to simplify the release process of React Native and Hermes to the OSS. Pull Request resolved: https://github.com/facebook/hermes/pull/1567 Test Plan: CI Reviewed By: blakef Differential Revision: D66099176 Pulled By: cipolleschi fbshipit-source-id: fe42bab7308bbd2294911be694f0bef8d1652566 --- doc/IntlAPIs.md | 1 - lib/Platform/Intl/PlatformIntlApple.mm | 72 +++++++++++++++++++++- lib/VM/JSLib/Intl.cpp | 2 - test/hermes/intl/date-time-format-apple.js | 19 ++++++ test/hermes/intl/intl.js | 4 +- utils/testsuite/testsuite_skiplist.py | 9 ++- 6 files changed, 99 insertions(+), 8 deletions(-) diff --git a/doc/IntlAPIs.md b/doc/IntlAPIs.md index 6798a0f1f97..5f3a4158756 100644 --- a/doc/IntlAPIs.md +++ b/doc/IntlAPIs.md @@ -50,7 +50,6 @@ One popular implementation strategy followed by other engines, is to bundle an i ## Supported on Android only - `Intl.NumberFormat` - `Intl.NumberFormat.prototype.formatToParts` - - `Intl.DateTimeFormat.prototype.formatToParts` ## * Limitations on property support diff --git a/lib/Platform/Intl/PlatformIntlApple.mm b/lib/Platform/Intl/PlatformIntlApple.mm index fbeef3011ba..090bb952350 100644 --- a/lib/Platform/Intl/PlatformIntlApple.mm +++ b/lib/Platform/Intl/PlatformIntlApple.mm @@ -1262,6 +1262,8 @@ uint8_t getCurrencyDigits(std::u16string_view code) { std::u16string format(double jsTimeValue) noexcept; + std::vector formatToParts(double x) noexcept; + private: void initializeNSDateFormatter(NSLocale *nsLocale) noexcept; @@ -1932,8 +1934,76 @@ uint8_t getCurrencyDigits(std::u16string_view code) { return static_cast(this)->format(jsTimeValue); } +static std::u16string returnTypeOfDate(const char16_t &c16) { + if (c16 == u'a') + return u"dayPeriod"; + if (c16 == u'z' || c16 == u'v' || c16 == u'O') + return u"timeZoneName"; + if (c16 == u'G') + return u"era"; + if (c16 == u'y') + return u"year"; + if (c16 == u'M') + return u"month"; + if (c16 == u'E') + return u"weekday"; + if (c16 == u'd') + return u"day"; + if (c16 == u'h' || c16 == u'k' || c16 == u'K' || c16 == u'H') + return u"hour"; + if (c16 == u'm') + return u"minute"; + if (c16 == u's') + return u"second"; + if (c16 == u'S') + return u"fractionalSecond"; + return u"literal"; +} + +// Implementer note: This method corresponds roughly to +// https://402.ecma-international.org/8.0/#sec-formatdatetimetoparts +std::vector DateTimeFormatApple::formatToParts(double x) noexcept { + // NOTE: We dont have access to localeData.patterns. Instead we use + // NSDateFormatter's foramt string, and break it into components. + // 1. Let parts be ? PartitionDateTimePattern(dateTimeFormat, x). + auto fmt = nsStringToU16String(nsDateFormatter_.dateFormat); + std::unique(fmt.begin(), fmt.end()); + auto formattedDate = format(x); + // 2. Let result be ArrayCreate(0). + std::vector result; + // 3. Let n be 0. + // 4. For each Record { [[Type]], [[Value]] } part in parts, do + // a. Let O be OrdinaryObjectCreate(%Object.prototype%). + // b. Perform ! CreateDataPropertyOrThrow(O, "type", part.[[Type]]). + // c. Perform ! CreateDataPropertyOrThrow(O, "value", part.[[Value]]). + // d. Perform ! CreateDataProperty(result, ! ToString(n), O). + // e. Increment n by 1. + std::u16string currentPart; + unsigned n = 0; + static auto alphanumerics = NSCharacterSet.alphanumericCharacterSet; + for (char16_t c16 : formattedDate) { + if ([alphanumerics characterIsMember:c16]) { + currentPart += c16; + continue; + } + if (currentPart != u"") { + result.push_back( + {{u"type", returnTypeOfDate(fmt[n])}, {u"value", currentPart}}); + currentPart = u""; + n++; + } + result.push_back({{u"type", u"literal"}, {u"value", {c16}}}); + n++; + } + // Last format string component. + result.push_back( + {{u"type", returnTypeOfDate(fmt[n])}, {u"value", currentPart}}); + // 5. Return result. + return result; +} + std::vector DateTimeFormat::formatToParts(double x) noexcept { - llvm_unreachable("formatToParts is unimplemented on Apple platforms"); + return static_cast(this)->formatToParts(x); } class NumberFormatApple : public NumberFormat { diff --git a/lib/VM/JSLib/Intl.cpp b/lib/VM/JSLib/Intl.cpp index 87ada8cab06..83c116e614b 100644 --- a/lib/VM/JSLib/Intl.cpp +++ b/lib/VM/JSLib/Intl.cpp @@ -929,7 +929,6 @@ void defineIntlDateTimeFormat(Runtime &runtime, Handle intl) { false, true); -#ifndef __APPLE__ defineMethod( runtime, prototype, @@ -937,7 +936,6 @@ void defineIntlDateTimeFormat(Runtime &runtime, Handle intl) { nullptr, intlDateTimeFormatPrototypeFormatToParts, 1); -#endif defineMethod( runtime, diff --git a/test/hermes/intl/date-time-format-apple.js b/test/hermes/intl/date-time-format-apple.js index 941305c45f6..83e1c28e50b 100644 --- a/test/hermes/intl/date-time-format-apple.js +++ b/test/hermes/intl/date-time-format-apple.js @@ -150,5 +150,24 @@ print(new Intl.DateTimeFormat('en-US').resolvedOptions().numberingSystem); print(new Intl.DateTimeFormat('en-US', { timeZone: 'SGT'}).resolvedOptions().timeZone); // CHECK-NEXT: SGT +print(JSON.stringify(new Intl.DateTimeFormat('en-US').formatToParts(date))); +// CHECK-NEXT: [{"value":"1","type":"month"},{"value":"/","type":"literal"},{"value":"2","type":"day"},{"value":"/","type":"literal"},{"value":"2020","type":"year"}] + +print(JSON.stringify(new Intl.DateTimeFormat('en-GB').formatToParts(date))); +// CHECK-NEXT: [{"value":"02","type":"day"},{"value":"/","type":"literal"},{"value":"01","type":"month"},{"value":"/","type":"literal"},{"value":"2020","type":"year"}] + +print(JSON.stringify(new Intl.DateTimeFormat('en-US', {weekday: 'long', + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + fractionalSecondDigits: 3, + hour12: true, + timeZone: 'UTC' +}).formatToParts(new Date(Date.UTC(2020, 0, 2, 3, 45, 00, 30))))); +// CHECK-NEXT: [{"value":"Thursday","type":"weekday"},{"value":",","type":"literal"},{"value":" ","type":"literal"},{"value":"1","type":"month"},{"value":"/","type":"literal"},{"value":"2","type":"day"},{"value":"/","type":"literal"},{"value":"2020","type":"year"},{"value":",","type":"literal"},{"value":" ","type":"literal"},{"value":"3","type":"hour"},{"value":":","type":"literal"},{"value":"45","type":"minute"},{"value":":","type":"literal"},{"value":"00","type":"second"},{"value":".","type":"literal"},{"value":"030","type":"fractionalSecond"},{"value":" ","type":"literal"},{"value":"AM","type":"dayPeriod"}] + print(new Date(Date.UTC(2020, 0, 2)).toLocaleString("en-US", {weekday: "short", timeZone: "UTC"})) // CHECK-NEXT: Thu diff --git a/test/hermes/intl/intl.js b/test/hermes/intl/intl.js index c5eb5086ad6..8e13358dee2 100644 --- a/test/hermes/intl/intl.js +++ b/test/hermes/intl/intl.js @@ -83,9 +83,7 @@ testServiceGetterTypes(Intl.DateTimeFormat, 'format'); testServiceMethodTypes(Intl.DateTimeFormat, 'formatToParts'); testServiceMethodTypes(Intl.DateTimeFormat, 'resolvedOptions'); assert(typeof Intl.DateTimeFormat().format() === 'string'); -if(Intl.DateTimeFormat.prototype.formatToParts) { - testParts(Intl.DateTimeFormat().formatToParts()); -} +testParts(Intl.DateTimeFormat().formatToParts()); testServiceTypes(Intl.NumberFormat); testServiceGetterTypes(Intl.NumberFormat, 'format'); diff --git a/utils/testsuite/testsuite_skiplist.py b/utils/testsuite/testsuite_skiplist.py index a2b759ec4dc..8d995187c6d 100644 --- a/utils/testsuite/testsuite_skiplist.py +++ b/utils/testsuite/testsuite_skiplist.py @@ -1113,7 +1113,11 @@ "test262/test/intl402/DateTimeFormat/prototype/resolvedOptions/order-dayPeriod.js", "test262/test/intl402/DateTimeFormat/prototype/resolvedOptions/hourCycle-timeStyle.js", "test262/test/intl402/DateTimeFormat/prototype/resolvedOptions/order-style.js", - "test262/test/intl402/DateTimeFormat/prototype/formatToParts", + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/related-year-zh.js", + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/dayPeriod-narrow-en.js", + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/dayPeriod-long-en.js", + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/dayPeriod-short-en.js", + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/fractionalSecondDigits.js", "test262/test/intl402/DateTimeFormat/prototype/format/timedatestyle-en.js", "test262/test/intl402/DateTimeFormat/prototype/format/dayPeriod-long-en.js", "test262/test/intl402/DateTimeFormat/prototype/format/dayPeriod-narrow-en.js", @@ -1123,6 +1127,7 @@ # This test assumes that "year" has some default value. That is an implementation-defined behavior. # In our case it remains undefined, which causes this test to fail. "test262/test/intl402/DateTimeFormat/default-options-object-prototype.js", + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/related-year.js", "test262/test/intl402/DateTimeFormat/prototype/format/proleptic-gregorian-calendar.js", "test262/test/intl402/DateTimeFormat/prototype/formatRange", "test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts", @@ -1954,11 +1959,13 @@ "test262/test/intl402/NumberFormat", "test262/test/intl402/String/prototype/toLocaleLowerCase", "test262/test/intl402/String/prototype/toLocaleUpperCase", + "test262/test/intl402/DateTimeFormat/prototype/formatToParts", ], "darwin": [ # Intl implementation issues on Apple. "test262/test/intl402/Collator/ignore-invalid-unicode-ext-values.js", "test262/test/intl402/Collator/unicode-ext-value-collation.js", + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/offset-timezone-correct.js", ], }