| // Copyright 2018 the V8 project authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef V8_INTL_SUPPORT |
| #error Internationalization is expected to be enabled. |
| #endif // V8_INTL_SUPPORT |
| |
| #include "src/objects/js-relative-time-format.h" |
| |
| #include <map> |
| #include <memory> |
| #include <string> |
| |
| #include "src/execution/isolate.h" |
| #include "src/heap/factory.h" |
| #include "src/objects/intl-objects.h" |
| #include "src/objects/js-number-format.h" |
| #include "src/objects/js-relative-time-format-inl.h" |
| #include "src/objects/objects-inl.h" |
| #include "unicode/decimfmt.h" |
| #include "unicode/numfmt.h" |
| #include "unicode/reldatefmt.h" |
| #include "unicode/unum.h" |
| |
| namespace v8 { |
| namespace internal { |
| |
| namespace { |
| // Style: identifying the relative time format style used. |
| // |
| // ecma402/#sec-properties-of-intl-relativetimeformat-instances |
| |
| enum class Style { |
| LONG, // Everything spelled out. |
| SHORT, // Abbreviations used when possible. |
| NARROW // Use the shortest possible form. |
| }; |
| |
| UDateRelativeDateTimeFormatterStyle toIcuStyle(Style style) { |
| switch (style) { |
| case Style::LONG: |
| return UDAT_STYLE_LONG; |
| case Style::SHORT: |
| return UDAT_STYLE_SHORT; |
| case Style::NARROW: |
| return UDAT_STYLE_NARROW; |
| } |
| UNREACHABLE(); |
| } |
| |
| Style fromIcuStyle(UDateRelativeDateTimeFormatterStyle icu_style) { |
| switch (icu_style) { |
| case UDAT_STYLE_LONG: |
| return Style::LONG; |
| case UDAT_STYLE_SHORT: |
| return Style::SHORT; |
| case UDAT_STYLE_NARROW: |
| return Style::NARROW; |
| case UDAT_STYLE_COUNT: |
| UNREACHABLE(); |
| } |
| UNREACHABLE(); |
| } |
| } // namespace |
| |
| MaybeHandle<JSRelativeTimeFormat> JSRelativeTimeFormat::New( |
| Isolate* isolate, Handle<Map> map, Handle<Object> locales, |
| Handle<Object> input_options) { |
| // 1. Let requestedLocales be ? CanonicalizeLocaleList(locales). |
| Maybe<std::vector<std::string>> maybe_requested_locales = |
| Intl::CanonicalizeLocaleList(isolate, locales); |
| MAYBE_RETURN(maybe_requested_locales, Handle<JSRelativeTimeFormat>()); |
| std::vector<std::string> requested_locales = |
| maybe_requested_locales.FromJust(); |
| |
| // 2. If options is undefined, then |
| Handle<JSReceiver> options; |
| if (input_options->IsUndefined(isolate)) { |
| // 2. a. Let options be ObjectCreate(null). |
| options = isolate->factory()->NewJSObjectWithNullProto(); |
| // 3. Else |
| } else { |
| // 3. a. Let options be ? ToObject(options). |
| ASSIGN_RETURN_ON_EXCEPTION(isolate, options, |
| Object::ToObject(isolate, input_options), |
| JSRelativeTimeFormat); |
| } |
| |
| // 4. Let opt be a new Record. |
| // 5. Let matcher be ? GetOption(options, "localeMatcher", "string", « |
| // "lookup", "best fit" », "best fit"). |
| // 6. Set opt.[[localeMatcher]] to matcher. |
| Maybe<Intl::MatcherOption> maybe_locale_matcher = |
| Intl::GetLocaleMatcher(isolate, options, "Intl.RelativeTimeFormat"); |
| MAYBE_RETURN(maybe_locale_matcher, MaybeHandle<JSRelativeTimeFormat>()); |
| Intl::MatcherOption matcher = maybe_locale_matcher.FromJust(); |
| |
| // 7. Let _numberingSystem_ be ? GetOption(_options_, `"numberingSystem"`, |
| // `"string"`, *undefined*, *undefined*). |
| std::unique_ptr<char[]> numbering_system_str = nullptr; |
| Maybe<bool> maybe_numberingSystem = Intl::GetNumberingSystem( |
| isolate, options, "Intl.RelativeTimeFormat", &numbering_system_str); |
| // 8. If _numberingSystem_ is not *undefined*, then |
| // a. If _numberingSystem_ does not match the |
| // `(3*8alphanum) *("-" (3*8alphanum))` sequence, throw a *RangeError* |
| // exception. |
| MAYBE_RETURN(maybe_numberingSystem, MaybeHandle<JSRelativeTimeFormat>()); |
| |
| // 9. Set _opt_.[[nu]] to _numberingSystem_. |
| |
| // 10. Let localeData be %RelativeTimeFormat%.[[LocaleData]]. |
| // 11. Let r be |
| // ResolveLocale(%RelativeTimeFormat%.[[AvailableLocales]], |
| // requestedLocales, opt, |
| // %RelativeTimeFormat%.[[RelevantExtensionKeys]], localeData). |
| Maybe<Intl::ResolvedLocale> maybe_resolve_locale = |
| Intl::ResolveLocale(isolate, JSRelativeTimeFormat::GetAvailableLocales(), |
| requested_locales, matcher, {"nu"}); |
| if (maybe_resolve_locale.IsNothing()) { |
| THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError), |
| JSRelativeTimeFormat); |
| } |
| Intl::ResolvedLocale r = maybe_resolve_locale.FromJust(); |
| |
| UErrorCode status = U_ZERO_ERROR; |
| |
| icu::Locale icu_locale = r.icu_locale; |
| if (numbering_system_str != nullptr) { |
| auto nu_extension_it = r.extensions.find("nu"); |
| if (nu_extension_it != r.extensions.end() && |
| nu_extension_it->second != numbering_system_str.get()) { |
| icu_locale.setUnicodeKeywordValue("nu", nullptr, status); |
| DCHECK(U_SUCCESS(status)); |
| } |
| } |
| // 12. Let locale be r.[[Locale]]. |
| Maybe<std::string> maybe_locale_str = Intl::ToLanguageTag(icu_locale); |
| MAYBE_RETURN(maybe_locale_str, MaybeHandle<JSRelativeTimeFormat>()); |
| |
| // 13. Set relativeTimeFormat.[[Locale]] to locale. |
| Handle<String> locale_str = isolate->factory()->NewStringFromAsciiChecked( |
| maybe_locale_str.FromJust().c_str()); |
| |
| // 14. Set relativeTimeFormat.[[NumberingSystem]] to r.[[nu]]. |
| if (numbering_system_str != nullptr && |
| Intl::IsValidNumberingSystem(numbering_system_str.get())) { |
| icu_locale.setUnicodeKeywordValue("nu", numbering_system_str.get(), status); |
| DCHECK(U_SUCCESS(status)); |
| } |
| // 15. Let dataLocale be r.[[DataLocale]]. |
| |
| // 16. Let s be ? GetOption(options, "style", "string", |
| // «"long", "short", "narrow"», "long"). |
| Maybe<Style> maybe_style = Intl::GetStringOption<Style>( |
| isolate, options, "style", "Intl.RelativeTimeFormat", |
| {"long", "short", "narrow"}, {Style::LONG, Style::SHORT, Style::NARROW}, |
| Style::LONG); |
| MAYBE_RETURN(maybe_style, MaybeHandle<JSRelativeTimeFormat>()); |
| Style style_enum = maybe_style.FromJust(); |
| |
| // 17. Set relativeTimeFormat.[[Style]] to s. |
| |
| // 18. Let numeric be ? GetOption(options, "numeric", "string", |
| // «"always", "auto"», "always"). |
| Maybe<Numeric> maybe_numeric = Intl::GetStringOption<Numeric>( |
| isolate, options, "numeric", "Intl.RelativeTimeFormat", |
| {"always", "auto"}, {Numeric::ALWAYS, Numeric::AUTO}, Numeric::ALWAYS); |
| MAYBE_RETURN(maybe_numeric, MaybeHandle<JSRelativeTimeFormat>()); |
| Numeric numeric_enum = maybe_numeric.FromJust(); |
| |
| // 19. Set relativeTimeFormat.[[Numeric]] to numeric. |
| |
| // 23. Let relativeTimeFormat.[[NumberFormat]] be |
| // ? Construct(%NumberFormat%, « nfLocale, nfOptions »). |
| icu::NumberFormat* number_format = |
| icu::NumberFormat::createInstance(icu_locale, UNUM_DECIMAL, status); |
| if (U_FAILURE(status)) { |
| // Data build filter files excluded data in "rbnf_tree" since ECMA402 does |
| // not support "algorithmic" numbering systems. Therefore we may get the |
| // U_MISSING_RESOURCE_ERROR here. Fallback to locale without the numbering |
| // system and create the object again. |
| if (status == U_MISSING_RESOURCE_ERROR) { |
| delete number_format; |
| status = U_ZERO_ERROR; |
| icu_locale.setUnicodeKeywordValue("nu", nullptr, status); |
| DCHECK(U_SUCCESS(status)); |
| number_format = |
| icu::NumberFormat::createInstance(icu_locale, UNUM_DECIMAL, status); |
| } |
| if (U_FAILURE(status) || number_format == nullptr) { |
| delete number_format; |
| THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError), |
| JSRelativeTimeFormat); |
| } |
| } |
| |
| if (number_format->getDynamicClassID() == |
| icu::DecimalFormat::getStaticClassID()) { |
| icu::DecimalFormat* decimal_format = |
| static_cast<icu::DecimalFormat*>(number_format); |
| decimal_format->setMinimumGroupingDigits(-2); |
| } |
| |
| // Change UDISPCTX_CAPITALIZATION_NONE to other values if |
| // ECMA402 later include option to change capitalization. |
| // Ref: https://github.com/tc39/proposal-intl-relative-time/issues/11 |
| icu::RelativeDateTimeFormatter* icu_formatter = |
| new icu::RelativeDateTimeFormatter(icu_locale, number_format, |
| toIcuStyle(style_enum), |
| UDISPCTX_CAPITALIZATION_NONE, status); |
| if (U_FAILURE(status) || icu_formatter == nullptr) { |
| delete icu_formatter; |
| THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError), |
| JSRelativeTimeFormat); |
| } |
| |
| Handle<String> numbering_system_string = |
| isolate->factory()->NewStringFromAsciiChecked( |
| Intl::GetNumberingSystem(icu_locale).c_str()); |
| |
| Handle<Managed<icu::RelativeDateTimeFormatter>> managed_formatter = |
| Managed<icu::RelativeDateTimeFormatter>::FromRawPtr(isolate, 0, |
| icu_formatter); |
| |
| Handle<JSRelativeTimeFormat> relative_time_format_holder = |
| Handle<JSRelativeTimeFormat>::cast( |
| isolate->factory()->NewFastOrSlowJSObjectFromMap(map)); |
| |
| DisallowHeapAllocation no_gc; |
| relative_time_format_holder->set_flags(0); |
| relative_time_format_holder->set_locale(*locale_str); |
| relative_time_format_holder->set_numberingSystem(*numbering_system_string); |
| relative_time_format_holder->set_numeric(numeric_enum); |
| relative_time_format_holder->set_icu_formatter(*managed_formatter); |
| |
| // 25. Return relativeTimeFormat. |
| return relative_time_format_holder; |
| } |
| |
| namespace { |
| |
| Handle<String> StyleAsString(Isolate* isolate, Style style) { |
| switch (style) { |
| case Style::LONG: |
| return ReadOnlyRoots(isolate).long_string_handle(); |
| case Style::SHORT: |
| return ReadOnlyRoots(isolate).short_string_handle(); |
| case Style::NARROW: |
| return ReadOnlyRoots(isolate).narrow_string_handle(); |
| } |
| UNREACHABLE(); |
| } |
| |
| } // namespace |
| |
| Handle<JSObject> JSRelativeTimeFormat::ResolvedOptions( |
| Isolate* isolate, Handle<JSRelativeTimeFormat> format_holder) { |
| Factory* factory = isolate->factory(); |
| icu::RelativeDateTimeFormatter* formatter = |
| format_holder->icu_formatter().raw(); |
| DCHECK_NOT_NULL(formatter); |
| Handle<JSObject> result = factory->NewJSObject(isolate->object_function()); |
| Handle<String> locale(format_holder->locale(), isolate); |
| Handle<String> numberingSystem(format_holder->numberingSystem(), isolate); |
| JSObject::AddProperty(isolate, result, factory->locale_string(), locale, |
| NONE); |
| JSObject::AddProperty( |
| isolate, result, factory->style_string(), |
| StyleAsString(isolate, fromIcuStyle(formatter->getFormatStyle())), NONE); |
| JSObject::AddProperty(isolate, result, factory->numeric_string(), |
| format_holder->NumericAsString(), NONE); |
| JSObject::AddProperty(isolate, result, factory->numberingSystem_string(), |
| numberingSystem, NONE); |
| return result; |
| } |
| |
| Handle<String> JSRelativeTimeFormat::NumericAsString() const { |
| switch (numeric()) { |
| case Numeric::ALWAYS: |
| return GetReadOnlyRoots().always_string_handle(); |
| case Numeric::AUTO: |
| return GetReadOnlyRoots().auto_string_handle(); |
| } |
| UNREACHABLE(); |
| } |
| |
| namespace { |
| |
| Handle<String> UnitAsString(Isolate* isolate, URelativeDateTimeUnit unit_enum) { |
| Factory* factory = isolate->factory(); |
| switch (unit_enum) { |
| case UDAT_REL_UNIT_SECOND: |
| return factory->second_string(); |
| case UDAT_REL_UNIT_MINUTE: |
| return factory->minute_string(); |
| case UDAT_REL_UNIT_HOUR: |
| return factory->hour_string(); |
| case UDAT_REL_UNIT_DAY: |
| return factory->day_string(); |
| case UDAT_REL_UNIT_WEEK: |
| return factory->week_string(); |
| case UDAT_REL_UNIT_MONTH: |
| return factory->month_string(); |
| case UDAT_REL_UNIT_QUARTER: |
| return factory->quarter_string(); |
| case UDAT_REL_UNIT_YEAR: |
| return factory->year_string(); |
| default: |
| UNREACHABLE(); |
| } |
| } |
| |
| bool GetURelativeDateTimeUnit(Handle<String> unit, |
| URelativeDateTimeUnit* unit_enum) { |
| std::unique_ptr<char[]> unit_str = unit->ToCString(); |
| if ((strcmp("second", unit_str.get()) == 0) || |
| (strcmp("seconds", unit_str.get()) == 0)) { |
| *unit_enum = UDAT_REL_UNIT_SECOND; |
| } else if ((strcmp("minute", unit_str.get()) == 0) || |
| (strcmp("minutes", unit_str.get()) == 0)) { |
| *unit_enum = UDAT_REL_UNIT_MINUTE; |
| } else if ((strcmp("hour", unit_str.get()) == 0) || |
| (strcmp("hours", unit_str.get()) == 0)) { |
| *unit_enum = UDAT_REL_UNIT_HOUR; |
| } else if ((strcmp("day", unit_str.get()) == 0) || |
| (strcmp("days", unit_str.get()) == 0)) { |
| *unit_enum = UDAT_REL_UNIT_DAY; |
| } else if ((strcmp("week", unit_str.get()) == 0) || |
| (strcmp("weeks", unit_str.get()) == 0)) { |
| *unit_enum = UDAT_REL_UNIT_WEEK; |
| } else if ((strcmp("month", unit_str.get()) == 0) || |
| (strcmp("months", unit_str.get()) == 0)) { |
| *unit_enum = UDAT_REL_UNIT_MONTH; |
| } else if ((strcmp("quarter", unit_str.get()) == 0) || |
| (strcmp("quarters", unit_str.get()) == 0)) { |
| *unit_enum = UDAT_REL_UNIT_QUARTER; |
| } else if ((strcmp("year", unit_str.get()) == 0) || |
| (strcmp("years", unit_str.get()) == 0)) { |
| *unit_enum = UDAT_REL_UNIT_YEAR; |
| } else { |
| return false; |
| } |
| return true; |
| } |
| |
| template <typename T> |
| MaybeHandle<T> FormatCommon( |
| Isolate* isolate, Handle<JSRelativeTimeFormat> format, |
| Handle<Object> value_obj, Handle<Object> unit_obj, const char* func_name, |
| MaybeHandle<T> (*formatToResult)(Isolate*, |
| const icu::FormattedRelativeDateTime&, |
| Handle<Object>, Handle<String>)) { |
| // 3. Let value be ? ToNumber(value). |
| Handle<Object> value; |
| ASSIGN_RETURN_ON_EXCEPTION(isolate, value, |
| Object::ToNumber(isolate, value_obj), T); |
| double number = value->Number(); |
| // 4. Let unit be ? ToString(unit). |
| Handle<String> unit; |
| ASSIGN_RETURN_ON_EXCEPTION(isolate, unit, Object::ToString(isolate, unit_obj), |
| T); |
| // 4. If isFinite(value) is false, then throw a RangeError exception. |
| if (!std::isfinite(number)) { |
| THROW_NEW_ERROR( |
| isolate, |
| NewRangeError(MessageTemplate::kNotFiniteNumber, |
| isolate->factory()->NewStringFromAsciiChecked(func_name)), |
| T); |
| } |
| icu::RelativeDateTimeFormatter* formatter = format->icu_formatter().raw(); |
| DCHECK_NOT_NULL(formatter); |
| URelativeDateTimeUnit unit_enum; |
| if (!GetURelativeDateTimeUnit(unit, &unit_enum)) { |
| THROW_NEW_ERROR( |
| isolate, |
| NewRangeError(MessageTemplate::kInvalidUnit, |
| isolate->factory()->NewStringFromAsciiChecked(func_name), |
| unit), |
| T); |
| } |
| UErrorCode status = U_ZERO_ERROR; |
| icu::FormattedRelativeDateTime formatted = |
| (format->numeric() == JSRelativeTimeFormat::Numeric::ALWAYS) |
| ? formatter->formatNumericToValue(number, unit_enum, status) |
| : formatter->formatToValue(number, unit_enum, status); |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kIcuError), T); |
| } |
| return formatToResult(isolate, formatted, value, |
| UnitAsString(isolate, unit_enum)); |
| } |
| |
| MaybeHandle<String> FormatToString( |
| Isolate* isolate, const icu::FormattedRelativeDateTime& formatted, |
| Handle<Object> value, Handle<String> unit) { |
| UErrorCode status = U_ZERO_ERROR; |
| icu::UnicodeString result = formatted.toString(status); |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kIcuError), String); |
| } |
| return Intl::ToString(isolate, result); |
| } |
| |
| Maybe<bool> AddLiteral(Isolate* isolate, Handle<JSArray> array, |
| const icu::UnicodeString& string, int32_t index, |
| int32_t start, int32_t limit) { |
| Handle<String> substring; |
| ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, substring, Intl::ToString(isolate, string, start, limit), |
| Nothing<bool>()); |
| Intl::AddElement(isolate, array, index, isolate->factory()->literal_string(), |
| substring); |
| return Just(true); |
| } |
| |
| Maybe<bool> AddUnit(Isolate* isolate, Handle<JSArray> array, |
| const icu::UnicodeString& string, int32_t index, |
| int32_t start, int32_t limit, int32_t field_id, |
| Handle<Object> value, Handle<String> unit) { |
| Handle<String> substring; |
| ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, substring, Intl::ToString(isolate, string, start, limit), |
| Nothing<bool>()); |
| Intl::AddElement(isolate, array, index, |
| Intl::NumberFieldToType(isolate, value, field_id), substring, |
| isolate->factory()->unit_string(), unit); |
| return Just(true); |
| } |
| |
| MaybeHandle<JSArray> FormatToJSArray( |
| Isolate* isolate, const icu::FormattedRelativeDateTime& formatted, |
| Handle<Object> value, Handle<String> unit) { |
| UErrorCode status = U_ZERO_ERROR; |
| icu::UnicodeString string = formatted.toString(status); |
| |
| Factory* factory = isolate->factory(); |
| Handle<JSArray> array = factory->NewJSArray(0); |
| icu::ConstrainedFieldPosition cfpos; |
| cfpos.constrainCategory(UFIELD_CATEGORY_NUMBER); |
| int32_t index = 0; |
| |
| int32_t previous_end = 0; |
| Handle<String> substring; |
| std::vector<std::pair<int32_t, int32_t>> groups; |
| while (formatted.nextPosition(cfpos, status) && U_SUCCESS(status)) { |
| int32_t category = cfpos.getCategory(); |
| int32_t field = cfpos.getField(); |
| int32_t start = cfpos.getStart(); |
| int32_t limit = cfpos.getLimit(); |
| if (category == UFIELD_CATEGORY_NUMBER) { |
| if (field == UNUM_GROUPING_SEPARATOR_FIELD) { |
| groups.push_back(std::pair<int32_t, int32_t>(start, limit)); |
| continue; |
| } |
| if (start > previous_end) { |
| Maybe<bool> maybe_added = |
| AddLiteral(isolate, array, string, index++, previous_end, start); |
| MAYBE_RETURN(maybe_added, Handle<JSArray>()); |
| } |
| if (field == UNUM_INTEGER_FIELD) { |
| for (auto start_limit : groups) { |
| if (start_limit.first > start) { |
| Maybe<bool> maybe_added = |
| AddUnit(isolate, array, string, index++, start, |
| start_limit.first, field, value, unit); |
| MAYBE_RETURN(maybe_added, Handle<JSArray>()); |
| maybe_added = AddUnit(isolate, array, string, index++, |
| start_limit.first, start_limit.second, |
| UNUM_GROUPING_SEPARATOR_FIELD, value, unit); |
| MAYBE_RETURN(maybe_added, Handle<JSArray>()); |
| start = start_limit.second; |
| } |
| } |
| } |
| Maybe<bool> maybe_added = AddUnit(isolate, array, string, index++, start, |
| limit, field, value, unit); |
| MAYBE_RETURN(maybe_added, Handle<JSArray>()); |
| previous_end = limit; |
| } |
| } |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kIcuError), JSArray); |
| } |
| if (string.length() > previous_end) { |
| Maybe<bool> maybe_added = AddLiteral(isolate, array, string, index, |
| previous_end, string.length()); |
| MAYBE_RETURN(maybe_added, Handle<JSArray>()); |
| } |
| |
| JSObject::ValidateElements(*array); |
| return array; |
| } |
| |
| } // namespace |
| |
| MaybeHandle<String> JSRelativeTimeFormat::Format( |
| Isolate* isolate, Handle<Object> value_obj, Handle<Object> unit_obj, |
| Handle<JSRelativeTimeFormat> format) { |
| return FormatCommon<String>(isolate, format, value_obj, unit_obj, |
| "Intl.RelativeTimeFormat.prototype.format", |
| FormatToString); |
| } |
| |
| MaybeHandle<JSArray> JSRelativeTimeFormat::FormatToParts( |
| Isolate* isolate, Handle<Object> value_obj, Handle<Object> unit_obj, |
| Handle<JSRelativeTimeFormat> format) { |
| return FormatCommon<JSArray>( |
| isolate, format, value_obj, unit_obj, |
| "Intl.RelativeTimeFormat.prototype.formatToParts", FormatToJSArray); |
| } |
| |
| const std::set<std::string>& JSRelativeTimeFormat::GetAvailableLocales() { |
| // Since RelativeTimeFormatter does not have a method to list all |
| // available locales, work around by calling the DateFormat. |
| return Intl::GetAvailableLocalesForDateFormat(); |
| } |
| |
| } // namespace internal |
| } // namespace v8 |