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", ], }