diff --git a/doc/IntlAPIs.md b/doc/IntlAPIs.md index 4e8ad3ba704..f0b6b56fcf1 100644 --- a/doc/IntlAPIs.md +++ b/doc/IntlAPIs.md @@ -3,16 +3,18 @@ id: intl title: Internationalization APIs --- -This document describes the current state of Android implementation of the [ECMAScript Internationalization API Specification](https://tc39.es/ecma402/) (ECMA-402, or `Intl`). ECMA-402 is still evolving and the latest iteration is [7th edition](https://402.ecma-international.org/7.0/) which was published in June 2020. Each new edition is built on top of the last one and adds new capabilities typically as, +This document describes the current state of Android/Windows implementation of the [ECMAScript Internationalization API Specification](https://tc39.es/ecma402/) (ECMA-402, or `Intl`). ECMA-402 is still evolving and the latest iteration is [7th edition](https://402.ecma-international.org/7.0/) which was published in June 2020. Each new edition is built on top of the last one and adds new capabilities typically as, - New `Intl` service constructors (e.g. `Intl.Collator`, `Intl.NumberFormat` etc.) or extending existing ones by accepting more parameters - New functions or properties in `Intl` objects (e.g. `Intl.Collator.prototype.compare`) - New locale aware functions in standard Javascript object prototypes (e.g. `String.prototype.localeCompare`) +# Android + One popular implementation strategy followed by other engines, is to bundle an internationalization framework (typically [ICU](http://site.icu-project.org/)) along with the application package. This guarantees deterministic behaviours at the cost of applications package bloat. We decided to consume the Android platform provided facilities for space efficiency, but at the cost of some variance in behaviours across Android platforms. -# ECMA-402 Compliance +## ECMA-402 Compliance -## Supported +### Supported - `Intl.Collator` - `Intl.Collator.supportedLocalesOf` @@ -49,7 +51,7 @@ One popular implementation strategy followed by other engines, is to bundle an i - `toLocaleDateString` - `toLocaleTimeString` -## Not yet supported +### Not yet supported - [`Intl.PluralRules`](https://tc39.es/ecma402/#pluralrules-objects) @@ -67,13 +69,13 @@ One popular implementation strategy followed by other engines, is to bundle an i - [`fractionalSecondDigits`](https://github.com/tc39/ecma402/pull/347) - [`BigInt.prototype.toLocaleString`](https://tc39.es/ecma402/#sup-bigint.prototype.tolocalestring) -## Excluded +### Excluded - `Intl.DateTimeFormat`: [`formatMatcher`](https://tc39.es/ecma402/#sec-basicformatmatcher) parameter is not respected. The parameter enables the implementation to pick the best display format when it supports only a subset of all possible formats. ICU library in Android platform and hence our implementation allows all subsets and formats which makes this `formatMatcher` property unnecessary. -## Limitations across Android SDKs +### Limitations across Android SDKs -### Android 11 +#### Android 11 - The keys of the object returned by `resolvedOptions` function in all `Intl` services are not deterministically ordered as prescribed by spec. - DateFormat: ECMAScript [beginning of time](https://www.ecma-international.org/ecma-262/11.0/index.html#sec-time-values-and-time-range) (-8,640,000,000,000,000), is formatted as `November 271817`, instead of expected `April 271822`. @@ -83,28 +85,28 @@ One popular implementation strategy followed by other engines, is to bundle an i - `signDisplay` - `currencyFormat` -### Android 10 and older (SDK < 30) +#### Android 10 and older (SDK < 30) - `Intl.NumberFormat`: Scientific notation formatting has issues on some cases. e.g. `-Infinity` may get formatted as '-∞E0' instead of expected '-∞'. Another manifestation of the issues is that the formatToParts may return 4 parts instead of 2. - `Intl.NumberFormat`: Compact notation `formatToParts` doesn't identify unit, hence we report unit as 'literal'. For e.g. the second part of "100ac" gets reported as "literal" instead of "compact" -### Android 9 and older (SDK < 29) +#### Android 9 and older (SDK < 29) - There are some failures likely due to older Unicode and CLDR version, which are hard to generalize. Some examples are, - `Intl.NumberFormat`: 'Percent' is not accepted as a unit. - `Intl.NumberFormat`: unit symbols difference, kph vs km/h - Some issue in significant digit precision, which is not yet looked into the details. -### Android 8.0 – 8.1 and older (SDK < 28) +#### Android 8.0 – 8.1 and older (SDK < 28) - `Intl.getCanonicalLocales`: Unicode/CLDR version differences results in some variances. e.g. und-u-tz-utc vs. und-u-tz-gmt. - `Intl.NumberFormat`: CompactFormatter doesn't respect the precision inputs. -### Android 7.0 - 7.1 and older (SDK < 26) +#### Android 7.0 - 7.1 and older (SDK < 26) - `Intl.getCanonicalLocales`: Unicode/CLDR version differences results in some variances. e.g. und-u-ms-imperial vs. und-u-ms-uksystem. -### Android 7.0 - 7.1 and older (SDK < 24) +#### Android 7.0 - 7.1 and older (SDK < 24) - `Intl.Collator`: Doesn't canonically decompose the input strings. Canonically equivalent string with non-identical code points may not match. - `Intl.getCanonicalLocales`: Unicode/CLDR version differences results in some variances. e.g. und-u-ca-ethiopic-amete-alem vs. und-u-ca-ethioaa, und-u-ks-primary vs. und-u-ks-level1. @@ -112,11 +114,11 @@ One popular implementation strategy followed by other engines, is to bundle an i - `Intl.NumberFormat`: There are issues in the precision configuration due to lack of APIs. - `Intl.DateFormat`: There are issues with the calendar configuration which needs to be dug into. -### SDK < 21 and older +#### SDK < 21 and older On platforms before 21, `Locale.forLanguageTag()` is not available, hence we can't construct `java.util.Locale` object from locale tag. Hence, we fallback to English for any locale input. -# Internationalization framework in Android Platform +## Internationalization framework in Android Platform Our implementation is essentially projecting the Android platform provided internationalization facilities through the ECMA-402 specified services. It implies that the results of running the same code may vary between devices running different versions of Android. @@ -124,7 +126,7 @@ Android platform internationalization libraries have been based on [ICU4j projec The following table summarizes ICU, CLDR and Unicode versions available on the Android platforms. -### Platform 24+ where ICU4j APIs are available. +#### Platform 24+ where ICU4j APIs are available. | Android Platform Version | ICU | Unicode | CLDR | --- | --- | --- | --- | @@ -135,7 +137,7 @@ The following table summarizes ICU, CLDR and Unicode versions available on the A | Android 7.0 - 7.1 (API levels 24 - 25) | ICU4j 56 ([ref](https://developer.android.com/guide/topics/resources/internationalization))| CLDR 28 | Unicode 8.0 | -### Pre-24 platforms +#### Pre-24 platforms | Android Platform Version | ICU | Unicode | CLDR | --- | --- | --- | --- | @@ -157,7 +159,7 @@ In summary, 4. Platform 30 has introduced classes under [`android.icu.number`](https://developer.android.com/reference/android/icu/util/package-summary) namespace which will majorly improve our `Intl.NumberFormat` implementation -# Impact on Application Size +## Impact on Application Size The following numbers are measured using a test application which takes dependency on the Hermes library to evaluate a JavaScript snippet. Essentially, enabling Intl APIs adds 57-62K per ABI. @@ -193,3 +195,65 @@ And finally, this is the increase in the final npm package, | **NPM Package** | **NOINTL** | **INTL** | **DIFF** | **PERC** | | --- | --- | --- | --- | --- | | hermes | 214447973 | 219291220 | 4,843,247 | 2.26% | + +# Windows + +The Windows Intl API's are a work in progress and currently very limited in support. We'll keep track of the status of API's here as we work through them. + +## ECMA-402 Compliance +### Supported +- `Intl.DateTimeFormat` + - `Intl.DateTimeFormat.supportedLocalesOf` + - `Intl.DateTimeFormat.prototype.format` + - `Intl.DateTimeFormat.prototype.resolvedOptions` + +- `Intl.getCanonicalLocales` + +### Not yet supported + +- `Intl.DateTimeFormat` + - `Intl.DateTimeFormat.prototype.formatToParts` + +- `Intl.DateTimeFormat` properties + - [`dayPeriod`] + - [`fractionalSecondDigits`] + - [`formatMatcher`] + +- `Intl.Collator` + - `Intl.Collator.supportedLocalesOf` + - `Intl.Collator.prototype.compare` + - `Intl.Collator.prototype.resolvedOptions` + +- `Intl.NumberFormat` + - `Intl.NumberFormat.supportedLocalesOf` + - `Intl.NumberFormat.prototype.format` + - `Intl.NumberFormat.prototype.formatToParts` + - `Intl.NumberFormat.prototype.resolvedOptions` + +- `String.prototype` + - `localeCompare` + - `toLocaleLowerCase` + - `toLocaleUpperCase` + +- `Array.prototype` + - `toLocaleString` + +- `Number.prototype` + - `toLocaleString` + +- `Date.prototype` + - `toLocaleString` + - `toLocaleDateString` + - `toLocaleTimeString` + +- [`Intl.PluralRules`](https://tc39.es/ecma402/#pluralrules-objects) + +- [`Intl.RelativeTimeFormat`](https://tc39.es/ecma402/#relativetimeformat-objects) + +- [`Intl.DisplayNames`](https://tc39.es/proposal-intl-displaynames/#sec-intl-displaynames-constructor) + +- [`Intl.ListFormat`](https://tc39.es/proposal-intl-list-format/#sec-intl-listformat-constructor) + +- [`Intl.Locale`](https://tc39.es/ecma402/#sec-intl-locale-constructor) + +- [`BigInt.prototype.toLocaleString`](https://tc39.es/ecma402/#sup-bigint.prototype.tolocalestring) diff --git a/lib/Platform/Intl/PlatformIntlShared.cpp b/lib/Platform/Intl/PlatformIntlShared.cpp new file mode 100644 index 00000000000..f380f78c31c --- /dev/null +++ b/lib/Platform/Intl/PlatformIntlShared.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file includes shared code between Apple and Windows implementation of Intl APIs +#include "hermes/Platform/Intl/PlatformIntl.h" + +using namespace ::hermes; + +namespace hermes { +namespace platform_intl { + +// https://402.ecma-international.org/8.0/#sec-bestavailablelocale +std::optional bestAvailableLocale( + const std::vector &availableLocales, + const std::u16string &locale) { + // 1. Let candidate be locale + std::u16string candidate = locale; + + // 2. Repeat + while (true) { + // a. If availableLocales contains an element equal to candidate, return + // candidate. + if (llvh::find(availableLocales, candidate) != availableLocales.end()) + return candidate; + // b. Let pos be the character index of the last occurrence of "-" (U+002D) + // within candidate. + size_t pos = candidate.rfind(u'-'); + + // ...If that character does not occur, return undefined. + if (pos == std::u16string::npos) + return std::nullopt; + + // c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, + // decrease pos by 2. + if (pos >= 2 && candidate[pos - 2] == '-') + pos -= 2; + + // d. Let candidate be the substring of candidate from position 0, + // inclusive, to position pos, exclusive. + candidate.resize(pos); + } +} + +// https://402.ecma-international.org/8.0/#sec-lookupsupportedlocales +std::vector lookupSupportedLocales( + const std::vector &availableLocales, + const std::vector &requestedLocales) { + // 1. Let subset be a new empty List. + std::vector subset; + // 2. For each element locale of requestedLocales in List order, do + for (const std::u16string &locale : requestedLocales) { + // a. Let noExtensionsLocale be the String value that is locale with all + // Unicode locale extension sequences removed. + // We can skip this step, see the comment in lookupMatcher. + // b. Let availableLocale be BestAvailableLocale(availableLocales, + // noExtensionsLocale). + std::optional availableLocale = + bestAvailableLocale(availableLocales, locale); + // c. If availableLocale is not undefined, append locale to the end of + // subset. + if (availableLocale) { + subset.push_back(locale); + } + } + // 3. Return subset. + return subset; +} + +std::optional getOptionBool( + vm::Runtime &runtime, + const Options &options, + const std::u16string &property, + std::optional fallback) { + // 1. Assert: Type(options) is Object. + // 2. Let value be ? Get(options, property). + auto value = options.find(property); + // 3. If value is undefined, return fallback. + if (value == options.end()) { + return fallback; + } + // 8. Return value. + return value->second.getBool(); +} + +// Implementation of +// https://402.ecma-international.org/8.0/#sec-todatetimeoptions +vm::CallResult toDateTimeOptions( + vm::Runtime &runtime, + Options options, + std::u16string_view required, + std::u16string_view defaults) { + // 1. If options is undefined, let options be null; otherwise let options be ? + // ToObject(options). + // 2. Let options be OrdinaryObjectCreate(options). + // 3. Let needDefaults be true. + bool needDefaults = true; + // 4. If required is "date" or "any", then + if (required == u"date" || required == u"any") { + // a. For each property name prop of « "weekday", "year", "month", "day" », + // do + // TODO(T116352920): Make this a std::u16string props[] once we have + // constexpr std::u16string. + static constexpr std::u16string_view props[] = { + u"weekday", u"year", u"month", u"day"}; + for (const auto &prop : props) { + // i. Let value be ? Get(options, prop). + if (options.find(std::u16string(prop)) != options.end()) { + // ii. If value is not undefined, let needDefaults be false. + needDefaults = false; + } + } + } + // 5. If required is "time" or "any", then + if (required == u"time" || required == u"any") { + // a. For each property name prop of « "dayPeriod", "hour", "minute", + // "second", "fractionalSecondDigits" », do + static constexpr std::u16string_view props[] = { + u"dayPeriod", u"hour", u"minute", u"second", u"fractionalSecondDigits"}; + for (const auto &prop : props) { + // i. Let value be ? Get(options, prop). + if (options.find(std::u16string(prop)) != options.end()) { + // ii. If value is not undefined, let needDefaults be false. + needDefaults = false; + } + } + } + // 6. Let dateStyle be ? Get(options, "dateStyle"). + auto dateStyle = options.find(u"dateStyle"); + // 7. Let timeStyle be ? Get(options, "timeStyle"). + auto timeStyle = options.find(u"timeStyle"); + // 8. If dateStyle is not undefined or timeStyle is not undefined, let + // needDefaults be false. + if (dateStyle != options.end() || timeStyle != options.end()) { + needDefaults = false; + } + // 9. If required is "date" and timeStyle is not undefined, then + if (required == u"date" && timeStyle != options.end()) { + // a. Throw a TypeError exception. + return runtime.raiseTypeError( + "Unexpectedly found timeStyle option for \"date\" property"); + } + // 10. If required is "time" and dateStyle is not undefined, then + if (required == u"time" && dateStyle != options.end()) { + // a. Throw a TypeError exception. + return runtime.raiseTypeError( + "Unexpectedly found dateStyle option for \"time\" property"); + } + // 11. If needDefaults is true and defaults is either "date" or "all", then + if (needDefaults && (defaults == u"date" || defaults == u"all")) { + // a. For each property name prop of « "year", "month", "day" », do + static constexpr std::u16string_view props[] = {u"year", u"month", u"day"}; + for (const auto &prop : props) { + // i. Perform ? CreateDataPropertyOrThrow(options, prop, "numeric"). + options.emplace(prop, Option(std::u16string(u"numeric"))); + } + } + // 12. If needDefaults is true and defaults is either "time" or "all", then + if (needDefaults && (defaults == u"time" || defaults == u"all")) { + // a. For each property name prop of « "hour", "minute", "second" », do + static constexpr std::u16string_view props[] = { + u"hour", u"minute", u"second"}; + for (const auto &prop : props) { + // i. Perform ? CreateDataPropertyOrThrow(options, prop, "numeric"). + options.emplace(prop, Option(std::u16string(u"numeric"))); + } + } + // 13. return options + return options; +} + +} // namespace platform_intl +} // namespace hermes \ No newline at end of file diff --git a/lib/Platform/Intl/PlatformIntlWindows.cpp b/lib/Platform/Intl/PlatformIntlWindows.cpp index dc287aaffec..a6a84f9e8b3 100644 --- a/lib/Platform/Intl/PlatformIntlWindows.cpp +++ b/lib/Platform/Intl/PlatformIntlWindows.cpp @@ -3,10 +3,12 @@ * Licensed under the MIT License. */ +#include "./PlatformIntlShared.cpp" #include "hermes/Platform/Intl/PlatformIntl.h" #include #include +#include #include #include #include "llvh/Support/ConvertUTF.h" @@ -16,6 +18,8 @@ using namespace ::hermes; namespace hermes { namespace platform_intl { +namespace { + // convert utf8 string to utf16 vm::CallResult UTF8toUTF16( vm::Runtime &runtime, @@ -116,7 +120,7 @@ vm::CallResult NormalizeLanguageTag( } // https://tc39.es/ecma402/#sec-canonicalizelocalelist -vm::CallResult> CanonicalizeLocaleList( +vm::CallResult> canonicalizeLocaleList( vm::Runtime &runtime, const std::vector &locales) { // 1. If locales is undefined, then a. Return a new empty list @@ -155,11 +159,62 @@ vm::CallResult> CanonicalizeLocaleList( return seen; } +/// https://402.ecma-international.org/8.0/#sec-getoption +vm::CallResult getOptionString( + vm::Runtime &runtime, + const Options &options, + const std::u16string &property, + const std::vector &validValues, + const std::u16string &fallback) { + // 1. Assert type(options) is object + // 2. Let value be ? Get(options, property). + auto valueIt = options.find(property); + // 3. If value is undefined, return fallback. + if (valueIt == options.end()) + return std::u16string(fallback); + + const auto &value = valueIt->second.getString(); + // 4. Assert: type is "boolean" or "string". + // 5. If type is "boolean", then + // a. Set value to ! ToBoolean(value). + // 6. If type is "string", then + // a. Set value to ? ToString(value). + // 7. If values is not undefined and values does not contain an element equal + // to value, throw a RangeError exception. + if (!validValues.empty() && llvh::find(validValues, value) == validValues.end()) + return runtime.raiseRangeError( + vm::TwineChar16(property.c_str()) + + vm::TwineChar16("Value is invalid.")); + // 8. Return value. + return std::u16string(value); +} + +/// https://402.ecma-international.org/8.0/#sec-supportedlocales +std::vector supportedLocales( + const std::vector &availableLocales, + const std::vector &requestedLocales, + const Options &options) { + // 1. Set options to ? CoerceOptionsToObject(options). + // 2. Let matcher be ? GetOption(options, "localeMatcher", "string", « + // "lookup", "best fit" », "best fit"). + // 3. If matcher is "best fit", then + // a. Let supportedLocales be BestFitSupportedLocales(availableLocales, + // requestedLocales). + // 4. Else, + // a. Let supportedLocales be LookupSupportedLocales(availableLocales, + // requestedLocales). + // 5. Return CreateArrayFromList(supportedLocales). + + // We do not implement a BestFitMatcher, so we can just use LookupMatcher. + return lookupSupportedLocales(availableLocales, requestedLocales); +} +} // namespace + // https://tc39.es/ecma402/#sec-intl.getcanonicallocales vm::CallResult> getCanonicalLocales( vm::Runtime &runtime, const std::vector &locales) { - return CanonicalizeLocaleList(runtime, locales); + return canonicalizeLocaleList(runtime, locales); } // Not yet implemented. Tracked by @@ -182,12 +237,15 @@ vm::CallResult toLocaleUpperCase( // Collator - Not yet implemented. Tracked by // https://github.com/microsoft/hermes-windows/issues/87 -struct Collator::Impl { +namespace { +struct CollatorWindows : Collator { + CollatorWindows(const char16_t *l) : locale(l) {} std::u16string locale; }; +} // namespace -Collator::Collator() : impl_(std::make_unique()) {} -Collator::~Collator() {} +Collator::Collator() = default; +Collator::~Collator() = default; vm::CallResult> Collator::supportedLocalesOf( vm::Runtime &runtime, @@ -196,17 +254,17 @@ vm::CallResult> Collator::supportedLocalesOf( return std::vector{u"en-CA", u"de-DE"}; } -vm::ExecutionStatus Collator::initialize( +vm::CallResult> Collator::create( vm::Runtime &runtime, const std::vector &locales, const Options &options) noexcept { - impl_->locale = u"en-US"; - return vm::ExecutionStatus::RETURNED; + return std::make_unique(u"en-US"); } Options Collator::resolvedOptions() noexcept { Options options; - options.emplace(u"locale", Option(impl_->locale)); + options.emplace( + u"locale", Option(static_cast(this)->locale)); options.emplace(u"numeric", Option(false)); return options; } @@ -217,44 +275,709 @@ double Collator::compare( return x.compare(y); } -// DateTimeFormat - Not yet implemented. Tracked by -// https://github.com/microsoft/hermes-windows/issues/87 -struct DateTimeFormat::Impl { - std::u16string locale; +namespace { +// Implementation of +// https://402.ecma-international.org/8.0/#datetimeformat-objects +class DateTimeFormatWindows : public DateTimeFormat { + public: + DateTimeFormatWindows() = default; + ~DateTimeFormatWindows() { + udat_close(dtf_); + }; + + vm::ExecutionStatus initialize( + vm::Runtime &runtime, + const std::vector &locales, + const Options &inputOptions) noexcept; + + Options resolvedOptions() noexcept; + + std::u16string format(double jsTimeValue) noexcept; + + std::vector formatToParts(double x) noexcept; + + private: + // Options used with DateTimeFormat + std::u16string locale_; + std::u16string timeZone_; + std::u16string weekday_; + std::u16string era_; + std::u16string year_; + std::u16string month_; + std::u16string day_; + std::u16string dayPeriod_; // Not yet supported + std::u16string hour_; + std::u16string minute_; + std::u16string second_; + std::u16string timeZoneName_; + std::u16string dateStyle_; + std::u16string timeStyle_; + std::u16string hourCycle_; + // Internal use + UDateFormat *dtf_; + std::string locale8_; + UDateFormat *getUDateFormatter(vm::Runtime &runtime); + vm::CallResult getDefaultHourCycle(vm::Runtime &runtime); }; +} // namespace -DateTimeFormat::DateTimeFormat() : impl_(std::make_unique()) {} -DateTimeFormat::~DateTimeFormat() {} +DateTimeFormat::DateTimeFormat() = default; +DateTimeFormat::~DateTimeFormat() = default; +// Implementation of +// https://402.ecma-international.org/8.0/#sec-intl.datetimeformat.supportedlocalesof +// without options vm::CallResult> DateTimeFormat::supportedLocalesOf( vm::Runtime &runtime, const std::vector &locales, const Options &options) noexcept { - return std::vector{u"en-CA", u"de-DE"}; + // 1. Let availableLocales be %DateTimeFormat%.[[AvailableLocales]]. + std::vector availableLocales = {}; + for (int32_t i = 0, count = uloc_countAvailable(); i < count; i++) { + auto locale = uloc_getAvailable(i); + availableLocales.push_back(UTF8toUTF16(runtime, locale).getValue()); + } + + // 2. Let requestedLocales be ? CanonicalizeLocaleList(locales). + auto requestedLocales = getCanonicalLocales(runtime, locales).getValue(); + + // 3. Return ? SupportedLocales(availableLocales, requestedLocales, options). + return supportedLocales(availableLocales, requestedLocales, options); } -vm::ExecutionStatus DateTimeFormat::initialize( +// Implementation of +// https://402.ecma-international.org/8.0/#sec-initializedatetimeformat +vm::ExecutionStatus DateTimeFormatWindows::initialize( vm::Runtime &runtime, const std::vector &locales, - const Options &options) noexcept { - impl_->locale = u"en-US"; + const Options &inputOptions) noexcept { + auto requestedLocalesRes = canonicalizeLocaleList(runtime, locales); + if (requestedLocalesRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return requestedLocalesRes.getStatus(); + } + locale_ = locales.front(); + + auto conversion = UTF16toUTF8(runtime, locale_); + if (conversion.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return conversion.getStatus(); + } + locale8_ = conversion.getValue(); // store the UTF8 version of locale since it + // is used in almost all other functions + + // 2. Let options be ? ToDateTimeOptions(options, "any", "date"). + Options options = + toDateTimeOptions(runtime, inputOptions, u"any", u"date").getValue(); + // 3. Let opt be a new Record. + std::unordered_map opt; + // 4. Let matcher be ? GetOption(options, "localeMatcher", "string", + // «"lookup", "best fit" », "best fit"). + auto matcher = getOptionString( + runtime, options, u"localeMatcher", {u"lookup", u"best fit"}, u"lookup"); + // 5. Set opt.[[localeMatcher]] to matcher. + opt.emplace(u"localeMatcher", matcher.getValue()); + // 6. Let calendar be ? GetOption(options, "calendar", "string", undefined, + // undefined). + auto calendar = getOptionString(runtime, options, u"calendar", {}, {}); + opt.emplace(u"ca", calendar.getValue()); + + // 9. Let numberingSystem be ? GetOption(options, "numberingSystem", + // "string", undefined, undefined). + // 10. If numberingSystem is not undefined, then + // a. If numberingSystem does not match the Unicode Locale Identifier + // type nonterminal, throw a RangeError exception. + + // 11. Set opt.[[nu]] to numberingSystem. + opt.emplace(u"nu", u""); + + // 12. Let hour12 be ? GetOption(options, "hour12", "boolean", undefined, + // undefined). + auto hour12 = getOptionBool(runtime, options, u"hour12", {}); + + // 13. Let hourCycle be ? GetOption(options, "hourCycle", "string", «"h11", + // "h12", "h23", "h24" », undefined). + static const std::vector hourCycles = { + u"h11", u"h12", u"h23", u"h24"}; + auto hourCycleRes = + getOptionString(runtime, options, u"hourCycle", hourCycles, {}); + std::u16string hourCycle = hourCycleRes.getValue(); + + // 14. If hour12 is not undefined, then a. Set hourCycle to null. + if (hour12.has_value()) { + hourCycle = u""; + } + // 15. Set opt.[[hc]] to hourCycle. + opt.emplace(u"hc", hourCycle); + hourCycle_ = hourCycle; + + // 16. Let localeData be %DateTimeFormat%.[[LocaleData]]. + // 17. Let r be ResolveLocale(%DateTimeFormat%.[[AvailableLocales]], + // requestedLocales, opt, %DateTimeFormat%.[[RelevantExtensionKeys]], + // localeData). + // 18. Set dateTimeFormat.[[Locale]] to r.[[locale]]. + // 19. Let calendar be r.[[ca] + // 20. Set dateTimeFormat.[[Calendar]] to calendar. + // 21. Set dateTimeFormat.[[HourCycle]] to r.[[hc]]. + // 22. Set dateTimeFormat.[[NumberingSystem]] to r.[[nu]]. + // 23. Let dataLocale be r.[[dataLocale]]. + + // 24. Let timeZone be ? Get(options, "timeZone"). + auto timeZoneRes = options.find(u"timeZone"); + // 25. If timeZone is undefined, then + if (timeZoneRes == options.end()) { + // a. Let timeZone be DefaultTimeZone(). + // 26. Else, + } else { + // a. Let timeZone be ? ToString(timeZone). + std::u16string timeZone = std::u16string(timeZoneRes->second.getString()); + // b. If the result of IsValidTimeZoneName(timeZone) is false, then + // i. Throw a RangeError exception. + // c. Let timeZone be CanonicalizeTimeZoneName(timeZone). + // 27. Set dateTimeFormat.[[TimeZone]] to timeZone. + timeZone_ = timeZone; + } + + // 28. Let opt be a new Record. + // 29. For each row of Table 4, except the header row, in table order, do + // a. Let prop be the name given in the Property column of the row. + // b. If prop is "fractionalSecondDigits", then + // i. Let value be ? GetNumberOption(options, "fractionalSecondDigits", 1, + // 3, undefined). + // d. Set opt.[[]] to value. + // c. Else, + // i. Let value be ? GetOption(options, prop, "string", « the strings + // given in the Values column of the row », undefined). + // d. Set opt.[[]] to value. + // 30. Let dataLocaleData be localeData.[[]]. + // 31. Let matcher be ? GetOption(options, "formatMatcher", "string", « + // "basic", "best fit" », "best fit"). + + // 32. Let dateStyle be ? GetOption(options, "dateStyle", "string", « "full", + // "long", "medium", "short" », undefined). + static const std::vector dateStyles = { + u"full", u"long", u"medium", u"short"}; + auto dateStyleRes = + getOptionString(runtime, options, u"dateStyle", dateStyles, {}); + if (dateStyleRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return dateStyleRes.getStatus(); + } + // 33. Set dateTimeFormat.[[DateStyle]] to dateStyle. + dateStyle_ = dateStyleRes.getValue(); + + // 34. Let timeStyle be ? GetOption(options, "timeStyle", "string", « "full", + // "long", "medium", "short" », undefined). + static const std::vector timeStyles = { + u"full", u"long", u"medium", u"short"}; + auto timeStyleRes = + getOptionString(runtime, options, u"timeStyle", timeStyles, {}); + if (timeStyleRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return timeStyleRes.getStatus(); + } + // 35. Set dateTimeFormat.[[TimeStyle]] to timeStyle. + timeStyle_ = timeStyleRes.getValue(); + + // Initialize properties using values from the options. + static const std::vector weekdayValues = { + u"narrow", u"short", u"long"}; + auto weekdayRes = + getOptionString(runtime, options, u"weekday", weekdayValues, {}); + if (weekdayRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return weekdayRes.getStatus(); + } + weekday_ = weekdayRes.getValue(); + + static const std::vector eraValues = { + u"narrow", u"short", u"long"}; + auto eraRes = getOptionString(runtime, options, u"era", eraValues, {}); + if (eraRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return eraRes.getStatus(); + } + era_ = *eraRes; + + static const std::vector yearValues = { + u"2-digit", u"numeric"}; + auto yearRes = getOptionString(runtime, options, u"year", yearValues, {}); + if (yearRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return yearRes.getStatus(); + } + year_ = *yearRes; + + static const std::vector monthValues = { + u"2-digit", u"numeric", u"narrow", u"short", u"long"}; + auto monthRes = getOptionString(runtime, options, u"month", monthValues, {}); + if (monthRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return monthRes.getStatus(); + } + month_ = *monthRes; + + static const std::vector dayValues = {u"2-digit", u"numeric"}; + auto dayRes = getOptionString(runtime, options, u"day", dayValues, {}); + if (dayRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return dayRes.getStatus(); + } + day_ = *dayRes; + + static const std::vector dayPeriodValues = { + u"narrow", u"short", u"long"}; + auto dayPeriodRes = + getOptionString(runtime, options, u"dayPeriod", dayPeriodValues, {}); + if (dayPeriodRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return dayPeriodRes.getStatus(); + } + dayPeriod_ = *dayPeriodRes; + + static const std::vector hourValues = { + u"2-digit", u"numeric"}; + auto hourRes = getOptionString(runtime, options, u"hour", hourValues, {}); + if (hourRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return hourRes.getStatus(); + } + hour_ = *hourRes; + + static const std::vector minuteValues = { + u"2-digit", u"numeric"}; + auto minuteRes = + getOptionString(runtime, options, u"minute", minuteValues, {}); + if (minuteRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return minuteRes.getStatus(); + } + minute_ = *minuteRes; + + static const std::vector secondValues = { + u"2-digit", u"numeric"}; + auto secondRes = + getOptionString(runtime, options, u"second", secondValues, {}); + if (secondRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return secondRes.getStatus(); + } + second_ = *secondRes; + + static const std::vector timeZoneNameValues = { + u"short", + u"long", + u"shortOffset", + u"longOffset", + u"shortGeneric", + u"longGeneric"}; + auto timeZoneNameRes = getOptionString( + runtime, options, u"timeZoneName", timeZoneNameValues, {}); + if (timeZoneNameRes.getStatus() == vm::ExecutionStatus::EXCEPTION) { + return timeZoneNameRes.getStatus(); + } + timeZoneName_ = *timeZoneNameRes; + + // 36. If dateStyle is not undefined or timeStyle is not undefined, then + // a. For each row in Table 4, except the header row, do + // i. Let prop be the name given in the Property column of the row. + // ii. Let p be opt.[[]]. + // iii. If p is not undefined, then + // 1. Throw a TypeError exception. + // b. Let styles be dataLocaleData.[[styles]].[[]]. + // c. Let bestFormat be DateTimeStyleFormat(dateStyle, timeStyle, styles). + // 37. Else, + // a. Let formats be dataLocaleData.[[formats]].[[]]. + // b. If matcher is "basic", then + // i. Let bestFormat be BasicFormatMatcher(opt, formats). + // c. Else, + // i. Let bestFormat be BestFitFormatMatcher(opt, formats). + // 38. For each row in Table 4, except the header row, in table order, do + // for (auto const &row : table4) { + // a. Let prop be the name given in the Property column of the row. + // auto prop = row.first; + // b. If bestFormat has a field [[]], then + // i. Let p be bestFormat.[[]]. + // ii. Set dateTimeFormat's internal slot whose name is the Internal + // Slot column of the row to p. + + // 39. If dateTimeFormat.[[Hour]] is undefined, then + if (hour_.empty()) { + // a. Set dateTimeFormat.[[HourCycle]] to undefined. + hourCycle_ = u""; + // b. Let pattern be bestFormat.[[pattern]]. + // c. Let rangePatterns be bestFormat.[[rangePatterns]]. + // 40. Else, + } else { + // a. Let hcDefault be dataLocaleData.[[hourCycle]]. + std::u16string hcDefault = getDefaultHourCycle(runtime).getValue(); + // b. Let hc be dateTimeFormat.[[HourCycle]]. + auto hc = hourCycle_; + // c. If hc is null, then + if (hc.empty()) + // i. Set hc to hcDefault. + hc = hcDefault; + // d. If hour12 is not undefined, then + if (hour12.has_value()) { + // i. If hour12 is true, then + if (*hour12 == true) { + // 1. If hcDefault is "h11" or "h23", then + if (hcDefault == u"h11" || hcDefault == u"h23") { + // a. Set hc to "h11". + hc = u"h11"; + // 2. Else, + } else { + // a. Set hc to "h12". + hc = u"h12"; + } + // ii. Else, + } else { + // 1. Assert: hour12 is false. + // 2. If hcDefault is "h11" or "h23", then + if (hcDefault == u"h11" || hcDefault == u"h23") { + // a. Set hc to "h23". + hc = u"h23"; + // 3. Else, + } else { + // a. Set hc to "h24". + hc = u"h24"; + } + } + } + // e. Set dateTimeFormat.[[HourCycle]] to hc. + hourCycle_ = hc; + // f. If dateTimeformat.[[HourCycle]] is "h11" or "h12", then + // i. Let pattern be bestFormat.[[pattern12]]. + // ii. Let rangePatterns be bestFormat.[[rangePatterns12]]. + // g. Else, + // i. Let pattern be bestFormat.[[pattern]]. + // ii. Let rangePatterns be bestFormat.[[rangePatterns]]. + } + // 41. Set dateTimeFormat.[[Pattern]] to pattern. + // 42. Set dateTimeFormat.[[RangePatterns]] to rangePatterns. + // 43. Return dateTimeFormat + + dtf_ = getUDateFormatter(runtime); return vm::ExecutionStatus::RETURNED; } -Options DateTimeFormat::resolvedOptions() noexcept { +vm::CallResult> DateTimeFormat::create( + vm::Runtime &runtime, + const std::vector &locales, + const Options &inputOptions) noexcept { + auto instance = std::make_unique(); + if (LLVM_UNLIKELY( + instance->initialize(runtime, locales, inputOptions) == + vm::ExecutionStatus::EXCEPTION)) { + return vm::ExecutionStatus::EXCEPTION; + } + return instance; +} + +Options DateTimeFormatWindows::resolvedOptions() noexcept { Options options; - options.emplace(u"locale", Option(impl_->locale)); - options.emplace(u"numeric", Option(false)); + options.emplace(u"locale", Option(locale_)); + options.emplace(u"timeZone", Option(timeZone_)); + options.emplace(u"weekday", weekday_); + options.emplace(u"era", era_); + options.emplace(u"year", year_); + options.emplace(u"month", month_); + options.emplace(u"day", day_); + options.emplace(u"hour", hour_); + options.emplace(u"minute", minute_); + options.emplace(u"second", second_); + options.emplace(u"timeZoneName", timeZoneName_); + options.emplace(u"timeZone", timeZone_); + options.emplace(u"dateStyle", dateStyle_); + options.emplace(u"timeStyle", timeStyle_); return options; } +Options DateTimeFormat::resolvedOptions() noexcept { + return static_cast(this)->resolvedOptions(); +} + +std::u16string DateTimeFormatWindows::format(double jsTimeValue) noexcept { + auto timeInSeconds = jsTimeValue; + UDate date = UDate(timeInSeconds); + UErrorCode status = U_ZERO_ERROR; + std::u16string myString; + int32_t myStrlen = 0; + + myStrlen = udat_format(dtf_, date, nullptr, myStrlen, nullptr, &status); + if (status == U_BUFFER_OVERFLOW_ERROR) { + status = U_ZERO_ERROR; + myString.resize(myStrlen); + udat_format(dtf_, date, &myString[0], myStrlen, nullptr, &status); + } + + assert(status <= 0); // Check for errors + return myString; +} + +vm::CallResult DateTimeFormatWindows::getDefaultHourCycle( + vm::Runtime &runtime) { + UErrorCode status = U_ZERO_ERROR; + std::u16string myString; + // open the default UDateFormat and Pattern of locale + UDateFormat *defaultDTF = udat_open( + UDAT_DEFAULT, + UDAT_DEFAULT, + &locale8_[0], + nullptr, + -1, + nullptr, + -1, + &status); + int32_t size = udat_toPattern(defaultDTF, true, nullptr, 0, &status); + if (status == U_BUFFER_OVERFLOW_ERROR) { + status = U_ZERO_ERROR; + myString.resize(size + 1); + udat_toPattern(defaultDTF, true, &myString[0], 40, &status); + assert(status <= 0); // Check for errors + udat_close(defaultDTF); + // find the default hour cycle and return it + for (int32_t i = 0; i < size; i++) { + char16_t ch = myString[i]; + switch (ch) { + case 'K': + return u"h11"; + break; + case 'h': + return u"h12"; + break; + case 'H': + return u"h23"; + break; + case 'k': + return u"h24"; + break; + } + } + } + + // There should always be a default hour cycle, return an exception if not + return vm::ExecutionStatus::EXCEPTION; +} + +// gets the UDateFormat with options set in initialize +UDateFormat *DateTimeFormatWindows::getUDateFormatter(vm::Runtime &runtime) { + static std::u16string eLong = u"long", eShort = u"short", eNarrow = u"narrow", + eMedium = u"medium", eFull = u"full", + eNumeric = u"numeric", eTwoDigit = u"2-digit", + eShortOffset = u"shortOffset", + eLongOffset = u"longOffset", + eShortGeneric = u"shortGeneric", + eLongGeneric = u"longGeneric"; + + // timeStyle and dateStyle cannot be used in conjunction with the other + // options. + if (!timeStyle_.empty() || !dateStyle_.empty()) { + UDateFormatStyle dateStyleRes = UDAT_DEFAULT; + UDateFormatStyle timeStyleRes = UDAT_DEFAULT; + + if (!dateStyle_.empty()) { + if (dateStyle_ == eFull) + dateStyleRes = UDAT_FULL; + else if (dateStyle_ == eLong) + dateStyleRes = UDAT_LONG; + else if (dateStyle_ == eMedium) + dateStyleRes = UDAT_MEDIUM; + else if (dateStyle_ == eShort) + dateStyleRes = UDAT_SHORT; + } + + if (!timeStyle_.empty()) { + if (timeStyle_ == eFull) + timeStyleRes = UDAT_FULL; + else if (timeStyle_ == eLong) + timeStyleRes = UDAT_LONG; + else if (timeStyle_ == eMedium) + timeStyleRes = UDAT_MEDIUM; + else if (timeStyle_ == eShort) + timeStyleRes = UDAT_SHORT; + } + + UErrorCode status = U_ZERO_ERROR; + UDateFormat *dtf; + // if timezone is specified, use that instead, else use default + if (!timeZone_.empty()) { + const UChar *timeZoneRes = + reinterpret_cast(timeZone_.c_str()); + int32_t timeZoneLength = timeZone_.length(); + dtf = udat_open( + timeStyleRes, + dateStyleRes, + &locale8_[0], + timeZoneRes, + timeZoneLength, + nullptr, + -1, + &status); + } else { + dtf = udat_open( + timeStyleRes, + dateStyleRes, + &locale8_[0], + nullptr, + -1, + nullptr, + -1, + &status); + } + assert(status == U_ZERO_ERROR); + return dtf; + } + + // Else: lets create the skeleton + std::u16string skeleton = u""; + if (!weekday_.empty()) { + if (weekday_ == eNarrow) + skeleton += u"EEEEE"; + else if (weekday_ == eLong) + skeleton += u"EEEE"; + else if (weekday_ == eShort) + skeleton += u"EEE"; + } + + if (!timeZoneName_.empty()) { + if (timeZoneName_ == eShort) + skeleton += u"z"; + else if (timeZoneName_ == eLong) + skeleton += u"zzzz"; + else if (timeZoneName_ == eShortOffset) + skeleton += u"O"; + else if (timeZoneName_ == eLongOffset) + skeleton += u"OOOO"; + else if (timeZoneName_ == eShortGeneric) + skeleton += u"v"; + else if (timeZoneName_ == eLongGeneric) + skeleton += u"vvvv"; + } + + if (!era_.empty()) { + if (era_ == eNarrow) + skeleton += u"GGGGG"; + else if (era_ == eShort) + skeleton += u"G"; + else if (era_ == eLong) + skeleton += u"GGGG"; + } + + if (!year_.empty()) { + if (year_ == eNumeric) + skeleton += u"y"; + else if (year_ == eTwoDigit) + skeleton += u"yy"; + } + + if (!month_.empty()) { + if (month_ == eTwoDigit) + skeleton += u"MM"; + else if (month_ == eNumeric) + skeleton += u'M'; + else if (month_ == eNarrow) + skeleton += u"MMMMM"; + else if (month_ == eShort) + skeleton += u"MMM"; + else if (month_ == eLong) + skeleton += u"MMMM"; + } + + if (!day_.empty()) { + if (day_ == eNumeric) + skeleton += u"d"; + else if (day_ == eTwoDigit) + skeleton += u"dd"; + } + + if (!hour_.empty()) { + if (hourCycle_ == u"h12") { + if (hour_ == eNumeric) + skeleton += u"h"; + else if (hour_ == eTwoDigit) + skeleton += u"hh"; + } else if (hourCycle_ == u"h24") { + if (hour_ == eNumeric) + skeleton += u"k"; + else if (hour_ == eTwoDigit) + skeleton += u"kk"; + } else if (hourCycle_ == u"h23") { + if (hour_ == eNumeric) + skeleton += u"k"; + else if (hour_ == eTwoDigit) + skeleton += u"KK"; + } else { + if (hour_ == eNumeric) + skeleton += u"h"; + else if (hour_ == eTwoDigit) + skeleton += u"HH"; + } + } + + if (!minute_.empty()) { + if (minute_ == eNumeric) + skeleton += u"m"; + else if (minute_ == eTwoDigit) + skeleton += u"mm"; + } + + if (!second_.empty()) { + if (second_ == eNumeric) + skeleton += u"s"; + else if (second_ == eTwoDigit) + skeleton += u"ss"; + } + + UErrorCode status = U_ZERO_ERROR; + std::u16string bestpattern; + int32_t patternLength; + + UDateTimePatternGenerator *dtpGenerator = udatpg_open(&locale8_[0], &status); + patternLength = udatpg_getBestPatternWithOptions( + dtpGenerator, + &skeleton[0], + -1, + UDATPG_MATCH_ALL_FIELDS_LENGTH, + nullptr, + 0, + &status); + + if (status == U_BUFFER_OVERFLOW_ERROR) { + status = U_ZERO_ERROR; + bestpattern.resize(patternLength); + udatpg_getBestPatternWithOptions( + dtpGenerator, + &skeleton[0], + skeleton.length(), + UDATPG_MATCH_ALL_FIELDS_LENGTH, + &bestpattern[0], + patternLength, + &status); + } + + // if timezone is specified, use that instead, else use default + if (!timeZone_.empty()) { + const UChar *timeZoneRes = + reinterpret_cast(timeZone_.c_str()); + int32_t timeZoneLength = timeZone_.length(); + return udat_open( + UDAT_PATTERN, + UDAT_PATTERN, + &locale8_[0], + timeZoneRes, + timeZoneLength, + &bestpattern[0], + patternLength, + &status); + } else { + return udat_open( + UDAT_PATTERN, + UDAT_PATTERN, + &locale8_[0], + nullptr, + -1, + &bestpattern[0], + patternLength, + &status); + } +} + std::u16string DateTimeFormat::format(double jsTimeValue) noexcept { - auto s = std::to_string(jsTimeValue); - return std::u16string(s.begin(), s.end()); + return static_cast(this)->format(jsTimeValue); } +// Not yet implemented. Tracked by +// https://github.com/microsoft/hermes-windows/issues/87 std::vector> -DateTimeFormat::formatToParts(double jsTimeValue) noexcept { +DateTimeFormatWindows::formatToParts(double jsTimeValue) noexcept { std::unordered_map part; part[u"type"] = u"integer"; // This isn't right, but I didn't want to do more work for a stub. @@ -263,14 +986,23 @@ DateTimeFormat::formatToParts(double jsTimeValue) noexcept { return std::vector>{part}; } +// Not yet implemented. Tracked by +// https://github.com/microsoft/hermes-windows/issues/87 +std::vector DateTimeFormat::formatToParts(double x) noexcept { + return static_cast(this)->formatToParts(x); +} + // NumberFormat - Not yet implemented. Tracked by // https://github.com/microsoft/hermes-windows/issues/87 -struct NumberFormat::Impl { +namespace { +struct NumberFormatDummy : NumberFormat { + NumberFormatDummy(const char16_t *l) : locale(l) {} std::u16string locale; }; +} // namespace -NumberFormat::NumberFormat() : impl_(std::make_unique()) {} -NumberFormat::~NumberFormat() {} +NumberFormat::NumberFormat() = default; +NumberFormat::~NumberFormat() = default; vm::CallResult> NumberFormat::supportedLocalesOf( vm::Runtime &runtime, @@ -279,17 +1011,17 @@ vm::CallResult> NumberFormat::supportedLocalesOf( return std::vector{u"en-CA", u"de-DE"}; } -vm::ExecutionStatus NumberFormat::initialize( +vm::CallResult> NumberFormat::create( vm::Runtime &runtime, const std::vector &locales, const Options &options) noexcept { - impl_->locale = u"en-US"; - return vm::ExecutionStatus::RETURNED; + return std::make_unique(u"en-US"); } Options NumberFormat::resolvedOptions() noexcept { Options options; - options.emplace(u"locale", Option(impl_->locale)); + options.emplace( + u"locale", Option(static_cast(this)->locale)); options.emplace(u"numeric", Option(false)); return options; } diff --git a/unittests/PlatformIntl/CMakeLists.txt b/unittests/PlatformIntl/CMakeLists.txt index c72bce596f7..c3c42acd27e 100644 --- a/unittests/PlatformIntl/CMakeLists.txt +++ b/unittests/PlatformIntl/CMakeLists.txt @@ -4,7 +4,7 @@ # LICENSE file in the root directory of this source tree. if(${HERMES_ENABLE_INTL}) - set(PlatformIntlSources getCanonicalLocales.cpp) + set(PlatformIntlSources getCanonicalLocales.cpp DateTimeFormat.cpp) add_hermes_unittest(PlatformIntlTests ${PlatformIntlSources} diff --git a/unittests/PlatformIntl/DateTimeFormat.cpp b/unittests/PlatformIntl/DateTimeFormat.cpp new file mode 100644 index 00000000000..ff620420118 --- /dev/null +++ b/unittests/PlatformIntl/DateTimeFormat.cpp @@ -0,0 +1,361 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include +#include "hermes/Platform/Intl/PlatformIntl.h" +#include "hermes/VM/Runtime.h" + +#include "gtest/gtest.h" + +namespace { +using namespace hermes; +using namespace hermes::platform_intl; + +// simplest of testcases, tests one locale without any options +TEST(DateTimeFormat, DatesWithoutOptions) { + std::vector AmericanEnglish = + std::vector{u"en-us"}; + std::vector KoreanKorea = + std::vector{u"ko-KR"}; + std::vector french = std::vector{u"fr"}; + std::shared_ptr runtime = hermes::vm::Runtime::create( + hermes::vm::RuntimeConfig::Builder().withIntl(true).build()); + platform_intl::Options testOptions = {}; + + auto dtf = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions); + auto result = dtf.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result, u"5/2/2021"); + + auto dtf2 = platform_intl::DateTimeFormat::create( + *runtime.get(), KoreanKorea, testOptions); + auto result2 = dtf2.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result2, u"2021. 5. 2."); + + auto dtf3 = platform_intl::DateTimeFormat::create( + *runtime.get(), french, testOptions); + std::u16string result3 = dtf3.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result3, u"02/05/2021"); +} + +// tests dateStyle and timeStyle options (full, long, medium, short) +TEST(DateTimeFormat, DatesWithTimeDateStyles) { + std::vector AmericanEnglish = + std::vector{u"en-us"}; + std::vector SpanishPeru = + std::vector{u"es-PE"}; + std::vector french = std::vector{u"fr"}; + std::shared_ptr runtime = hermes::vm::Runtime::create( + hermes::vm::RuntimeConfig::Builder().withIntl(true).build()); + + // dateStyle = full and timeStye = full + Options testOptions = { + {u"dateStyle", Option(std::u16string(u"full"))}, + {u"timeStyle", Option(std::u16string(u"full"))}}; + auto dtf = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions); + auto result = dtf.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result, u"Sunday, May 2, 2021 at 5:00:00 PM Pacific Daylight Time"); + + auto dtf2 = platform_intl::DateTimeFormat::create( + *runtime.get(), french, testOptions); + auto result2 = dtf2.getValue().get()->format(1620000000000.00); + EXPECT_EQ( + result2, + u"dimanche 2 mai 2021 \u00E0 17:00:00 heure d\u2019\u00E9t\u00E9 du Pacifique"); // L"dimanche 2 mai 2021 à 17:00:00 heure d’été du Pacifique" + + auto dtf3 = platform_intl::DateTimeFormat::create( + *runtime.get(), SpanishPeru, testOptions); + auto result3 = dtf3.getValue().get()->format(1620000000000.00); + EXPECT_EQ( + result3, + u"domingo, 2 de mayo de 2021, 17:00:00 hora de verano del Pac\u00EDfico"); // L"domingo, 2 de mayo de 2021, 17:00:00 hora de verano del Pacífico" + + // dateStyle = short and timeStyle = short + Options testOptions2 = { + {u"dateStyle", Option(std::u16string(u"short"))}, + {u"timeStyle", Option(std::u16string(u"short"))}}; + auto dtf4 = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions2); + auto result4 = dtf4.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result4, u"5/2/21, 5:00 PM"); + + auto dtf5 = platform_intl::DateTimeFormat::create( + *runtime.get(), french, testOptions2); + auto result5 = dtf5.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result5, u"02/05/2021 17:00"); + + auto dtf6 = platform_intl::DateTimeFormat::create( + *runtime.get(), SpanishPeru, testOptions2); + auto result6 = dtf6.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result6, u"2/05/21 17:00"); + + // dateStyle = long and timeStyle = medium + Options testOptions3 = { + {u"dateStyle", Option(std::u16string(u"long"))}, + {u"timeStyle", Option(std::u16string(u"medium"))}}; + auto dtf7 = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions3); + auto result7 = dtf7.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result7, u"May 2, 2021 at 5:00:00 PM"); + + auto dtf8 = platform_intl::DateTimeFormat::create( + *runtime.get(), french, testOptions3); + auto result8 = dtf8.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result8, u"2 mai 2021 \u00E0 17:00:00"); // L"2 mai 2021 à 17:00:00" + + auto dtf9 = platform_intl::DateTimeFormat::create( + *runtime.get(), SpanishPeru, testOptions3); + auto result9 = dtf9.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result9, u"2 de mayo de 2021, 17:00:00"); +} + +// Tests Date with Month (2-digit, numeric, narrow, short, long), Day (2-digit, +// numeric), and Year (2-digit, numeric) options +TEST(DateTimeFormat, DatesWithMonthDayYearOptions) { + std::vector DutchBelgium = + std::vector{u"nl-BE"}; + std::shared_ptr runtime = hermes::vm::Runtime::create( + hermes::vm::RuntimeConfig::Builder().withIntl(true).build()); + + Options testOptions = { + {u"day", Option(std::u16string(u"numeric"))}, + {u"month", Option(std::u16string(u"long"))}, + {u"year", Option(std::u16string(u"numeric"))}}; + auto dtf = platform_intl::DateTimeFormat::create( + *runtime.get(), DutchBelgium, testOptions); + auto result = dtf.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result, u"2 mei 2021"); + + Options testOptions2 = { + {u"day", Option(std::u16string(u"2-digit"))}, + {u"month", Option(std::u16string(u"narrow"))}, + {u"year", Option(std::u16string(u"2-digit"))}}; + auto dtf2 = platform_intl::DateTimeFormat::create( + *runtime.get(), DutchBelgium, testOptions2); + auto result2 = dtf2.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result2, u"02 M 21"); + + Options testOptions3 = { + {u"month", Option(std::u16string(u"numeric"))}, + {u"year", Option(std::u16string(u"2-digit"))}}; + auto dtf3 = platform_intl::DateTimeFormat::create( + *runtime.get(), DutchBelgium, testOptions3); + auto result3 = dtf3.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result3, u"5/21"); +} + +// Tests Date with Weekday ( narrow, short, long), era ( narrow, +// short, long), TimeZoneName (short, long, shortOffset, longOffset, +// shortGeneric, longGeneric) +TEST(DateTimeFormat, DatesWithWeekdayEraTimeZoneNameOptions) { + std::vector ItalianItaly = + std::vector{u"it-IT"}; // it-IT + std::shared_ptr runtime = hermes::vm::Runtime::create( + hermes::vm::RuntimeConfig::Builder().withIntl(true).build()); + + Options testOptions = { + {u"weekday", Option(std::u16string(u"long"))}, + {u"era", Option(std::u16string(u"long"))}, + {u"timeZoneName", Option(std::u16string(u"long"))}}; + auto dtf = platform_intl::DateTimeFormat::create( + *runtime.get(), ItalianItaly, testOptions); + auto result = dtf.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result, u"dopo Cristo domenica, Ora legale del Pacifico USA"); + + Options testOptions2 = { + {u"weekday", Option(std::u16string(u"short"))}, + {u"era", Option(std::u16string(u"narrow"))}, + {u"timeZoneName", Option(std::u16string(u"shortOffset"))}}; + auto dtf2 = platform_intl::DateTimeFormat::create( + *runtime.get(), ItalianItaly, testOptions2); + auto result2 = dtf2.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result2, u"dC dom, GMT-7"); + + Options testOptions3 = { + {u"weekday", Option(std::u16string(u"narrow"))}, + {u"era", Option(std::u16string(u"short"))}, + {u"timeZoneName", Option(std::u16string(u"longGeneric"))}}; + auto dtf3 = platform_intl::DateTimeFormat::create( + *runtime.get(), ItalianItaly, testOptions3); + auto result3 = dtf3.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result3, u"d.C. D, Ora del Pacifico USA"); +} + +// Tests Date with Hour (2-digit, numeric), Minute (2-digit, numeric), Second +// (2-digit, numeric) +TEST(DateTimeFormat, DatesWithHourMinuteSecondOptions) { + std::vector AmericanEnglish = + std::vector{u"en-US"}; + std::shared_ptr runtime = hermes::vm::Runtime::create( + hermes::vm::RuntimeConfig::Builder().withIntl(true).build()); + + Options testOptions = { + {u"hour", Option(std::u16string(u"2-digit"))}, + {u"minute", Option(std::u16string(u"2-digit"))}, + {u"second", Option(std::u16string(u"2-digit"))}}; + auto dtf = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions); + auto result = dtf.getValue().get()->format(1620000303000); + EXPECT_EQ(result, u"05:05:03 PM"); + + Options testOptions2 = { + {u"hour", Option(std::u16string(u"numeric"))}, + {u"minute", Option(std::u16string(u"numeric"))}, + {u"second", Option(std::u16string(u"numeric"))}}; + auto dtf2 = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions2); + auto result2 = dtf2.getValue().get()->format(1620000303000); + EXPECT_EQ(result2, u"5:05:03 PM"); + + Options testOptions3 = {{u"minute", Option(std::u16string(u"2-digit"))}}; + auto dtf3 = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions3); + auto result3 = dtf3.getValue().get()->format(1620000303000); + EXPECT_EQ(result3, u"05"); + + Options testOptions4 = {{u"hour", Option(std::u16string(u"2-digit"))}}; + auto dtf4 = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions4); + auto result4 = dtf4.getValue().get()->format(1620000303000); + EXPECT_EQ(result4, u"05 PM"); + + Options testOptions5 = { + {u"hour", Option(std::u16string(u"2-digit"))}, + {u"second", Option(std::u16string(u"numeric"))}}; + auto dtf5 = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions5); + auto result5 = dtf5.getValue().get()->format(1620000303000); + EXPECT_EQ(result5, u"05 PM (second: 3)"); +} + +// Tests Date with HourCycle (h11, h12, h23, h24) +TEST(DateTimeFormat, DatesWithHourCyclesOptions) { + std::vector AmericanEnglish = + std::vector{u"en-US"}; + std::shared_ptr runtime = hermes::vm::Runtime::create( + hermes::vm::RuntimeConfig::Builder().withIntl(true).build()); + + Options testOptions = { + {u"hour", Option(std::u16string(u"numeric"))}, + {u"minute", Option(std::u16string(u"numeric"))}, + {u"hourCycle", Option(std::u16string(u"h12"))}}; + auto dtf = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions); + auto result = dtf.getValue().get()->format(1620008000000); + EXPECT_EQ(result, u"7:13 PM"); + + Options testOptions2 = { + {u"hour", Option(std::u16string(u"numeric"))}, + {u"minute", Option(std::u16string(u"numeric"))}, + {u"hourCycle", Option(std::u16string(u"h24"))}}; + auto dtf2 = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions2); + auto result2 = dtf2.getValue().get()->format(1620008000000); + EXPECT_EQ(result2, u"19:13"); + + Options testOptions3 = { + {u"hour", Option(std::u16string(u"numeric"))}, + {u"minute", Option(std::u16string(u"numeric"))}, + {u"hourCycle", Option(std::u16string(u"h11"))}}; + auto dtf3 = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions3); + auto result3 = dtf3.getValue().get()->format(1620008000000); + EXPECT_EQ(result3, u"7:13 PM"); +} + +// Tests Date with specified TimeZone +TEST(DateTimeFormat, DatesWithTimeZone) { + std::vector AmericanEnglish = + std::vector{u"en-US"}; + std::shared_ptr runtime = hermes::vm::Runtime::create( + hermes::vm::RuntimeConfig::Builder().withIntl(true).build()); + + Options testOptions = { + {u"dateStyle", Option(std::u16string(u"long"))}, + {u"timeStyle", Option(std::u16string(u"long"))}, + {u"timeZone", Option(std::u16string(u"UTC"))}}; + auto dtf = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions); + auto result = dtf.getValue().get()->format(1620000000000.00); + EXPECT_EQ(result, u"May 3, 2021 at 12:00:00 AM UTC"); + + Options testOptions2 = { + {u"dateStyle", Option(std::u16string(u"full"))}, + {u"timeStyle", Option(std::u16string(u"full"))}, + {u"timeZone", Option(std::u16string(u"Australia/Sydney"))}}; + auto dtf2 = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions2); + auto result2 = dtf2.getValue().get()->format(1620000000000.00); + EXPECT_EQ( + result2, + u"Monday, May 3, 2021 at 10:00:00 AM Australian Eastern Standard Time"); +} + +// Tests Date with all options +TEST(DateTimeFormat, DatesWithAllOptions) { + std::vector AmericanEnglish = + std::vector{u"en-US"}; + std::shared_ptr runtime = hermes::vm::Runtime::create( + hermes::vm::RuntimeConfig::Builder().withIntl(true).build()); + + Options testOptions = { + {u"day", Option(std::u16string(u"numeric"))}, + {u"month", Option(std::u16string(u"long"))}, + {u"year", Option(std::u16string(u"numeric"))}, + {u"weekday", Option(std::u16string(u"long"))}, + {u"era", Option(std::u16string(u"long"))}, + {u"timeZoneName", Option(std::u16string(u"long"))}, + {u"hour", Option(std::u16string(u"2-digit"))}, + {u"minute", Option(std::u16string(u"2-digit"))}, + {u"second", Option(std::u16string(u"2-digit"))}, + {u"hourCycle", Option(std::u16string(u"h12"))}, + {u"timeZone", Option(std::u16string(u"UTC"))}}; + auto dtf = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions); + auto result = dtf.getValue().get()->format(1620008000000.00); + EXPECT_EQ( + result, + u"Monday, May 3, 2021 Anno Domini, 02:13:20 AM Coordinated Universal Time"); + + Options testOptions2 = { + {u"day", Option(std::u16string(u"2-digit"))}, + {u"month", Option(std::u16string(u"short"))}, + {u"year", Option(std::u16string(u"2-digit"))}, + {u"weekday", Option(std::u16string(u"short"))}, + {u"era", Option(std::u16string(u"narrow"))}, + {u"timeZoneName", Option(std::u16string(u"longGeneric"))}, + {u"hour", Option(std::u16string(u"2-digit"))}, + {u"minute", Option(std::u16string(u"2-digit"))}, + {u"second", Option(std::u16string(u"2-digit"))}, + {u"hourCycle", Option(std::u16string(u"h24"))}, + {u"timeZone", Option(std::u16string(u"Europe/Madrid"))}}; + auto dtf2 = platform_intl::DateTimeFormat::create( + *runtime.get(), AmericanEnglish, testOptions2); + auto result2 = dtf2.getValue().get()->format(1620008000000.00); + EXPECT_EQ(result2, u"Mon, May 03, 21 A, 04:13:20 Central European Time"); +} + +// Tests DateTimeFormat.supportedLocalesOf +TEST(DateTimeFormat, SupportedLocales) { + std::shared_ptr runtime = hermes::vm::Runtime::create( + hermes::vm::RuntimeConfig::Builder().withIntl(true).build()); + + Options testOptions = {}; + std::vector expected = + std::vector{u"en-US", u"fr"}; + auto result = platform_intl::DateTimeFormat::supportedLocalesOf( + *runtime.get(), {u"en-us", u"fr"}, testOptions); + auto value = result.getValue(); + EXPECT_EQ(value, expected); + + std::vector expected2 = + std::vector{u"en-US", u"fr", u"it-IT"}; + auto result2 = platform_intl::DateTimeFormat::supportedLocalesOf( + *runtime.get(), {u"en-us", u"fr", u"bans", u"it-it"}, testOptions); + auto value2 = result2.getValue(); + EXPECT_EQ(value2, expected2); +} +} // end anonymous namespace \ No newline at end of file