| // 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/heap/factory.h" |
| #include "src/isolate.h" |
| #include "src/objects-inl.h" |
| #include "src/objects/intl-objects.h" |
| #include "src/objects/js-relative-time-format-inl.h" |
| #include "unicode/numfmt.h" |
| #include "unicode/reldatefmt.h" |
| #include "unicode/uvernum.h" // for U_ICU_VERSION_MAJOR_NUM |
| |
| namespace v8 { |
| namespace internal { |
| |
| namespace { |
| UDateRelativeDateTimeFormatterStyle getIcuStyle( |
| JSRelativeTimeFormat::Style style) { |
| switch (style) { |
| case JSRelativeTimeFormat::Style::LONG: |
| return UDAT_STYLE_LONG; |
| case JSRelativeTimeFormat::Style::SHORT: |
| return UDAT_STYLE_SHORT; |
| case JSRelativeTimeFormat::Style::NARROW: |
| return UDAT_STYLE_NARROW; |
| case JSRelativeTimeFormat::Style::COUNT: |
| UNREACHABLE(); |
| } |
| } |
| } // namespace |
| |
| JSRelativeTimeFormat::Style JSRelativeTimeFormat::getStyle(const char* str) { |
| if (strcmp(str, "long") == 0) return JSRelativeTimeFormat::Style::LONG; |
| if (strcmp(str, "short") == 0) return JSRelativeTimeFormat::Style::SHORT; |
| if (strcmp(str, "narrow") == 0) return JSRelativeTimeFormat::Style::NARROW; |
| UNREACHABLE(); |
| } |
| |
| JSRelativeTimeFormat::Numeric JSRelativeTimeFormat::getNumeric( |
| const char* str) { |
| if (strcmp(str, "auto") == 0) return JSRelativeTimeFormat::Numeric::AUTO; |
| if (strcmp(str, "always") == 0) return JSRelativeTimeFormat::Numeric::ALWAYS; |
| UNREACHABLE(); |
| } |
| |
| MaybeHandle<JSRelativeTimeFormat> JSRelativeTimeFormat::Initialize( |
| Isolate* isolate, Handle<JSRelativeTimeFormat> relative_time_format_holder, |
| Handle<Object> locales, Handle<Object> input_options) { |
| relative_time_format_holder->set_flags(0); |
| |
| // 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 localeData be %RelativeTimeFormat%.[[LocaleData]]. |
| // 8. Let r be |
| // ResolveLocale(%RelativeTimeFormat%.[[AvailableLocales]], |
| // requestedLocales, opt, |
| // %RelativeTimeFormat%.[[RelevantExtensionKeys]], localeData). |
| std::set<std::string> available_locales = |
| Intl::GetAvailableLocales(Intl::ICUService::kRelativeDateTimeFormatter); |
| Intl::ResolvedLocale r = Intl::ResolveLocale(isolate, available_locales, |
| requested_locales, matcher, {}); |
| |
| // 9. Let locale be r.[[Locale]]. |
| // 10. Set relativeTimeFormat.[[Locale]] to locale. |
| // 11. Let dataLocale be r.[[DataLocale]]. |
| Handle<String> locale_str = |
| isolate->factory()->NewStringFromAsciiChecked(r.locale.c_str()); |
| relative_time_format_holder->set_locale(*locale_str); |
| |
| // 12. Let s be ? GetOption(options, "style", "string", |
| // «"long", "short", "narrow"», "long"). |
| std::unique_ptr<char[]> style_str = nullptr; |
| std::vector<const char*> style_values = {"long", "short", "narrow"}; |
| Maybe<bool> maybe_found_style = |
| Intl::GetStringOption(isolate, options, "style", style_values, |
| "Intl.RelativeTimeFormat", &style_str); |
| Style style_enum = Style::LONG; |
| MAYBE_RETURN(maybe_found_style, MaybeHandle<JSRelativeTimeFormat>()); |
| if (maybe_found_style.FromJust()) { |
| DCHECK_NOT_NULL(style_str.get()); |
| style_enum = getStyle(style_str.get()); |
| } |
| |
| // 13. Set relativeTimeFormat.[[Style]] to s. |
| relative_time_format_holder->set_style(style_enum); |
| |
| // 14. Let numeric be ? GetOption(options, "numeric", "string", |
| // «"always", "auto"», "always"). |
| std::unique_ptr<char[]> numeric_str = nullptr; |
| std::vector<const char*> numeric_values = {"always", "auto"}; |
| Maybe<bool> maybe_found_numeric = |
| Intl::GetStringOption(isolate, options, "numeric", numeric_values, |
| "Intl.RelativeTimeFormat", &numeric_str); |
| Numeric numeric_enum = Numeric::ALWAYS; |
| MAYBE_RETURN(maybe_found_numeric, MaybeHandle<JSRelativeTimeFormat>()); |
| if (maybe_found_numeric.FromJust()) { |
| DCHECK_NOT_NULL(numeric_str.get()); |
| numeric_enum = getNumeric(numeric_str.get()); |
| } |
| |
| // 15. Set relativeTimeFormat.[[Numeric]] to numeric. |
| relative_time_format_holder->set_numeric(numeric_enum); |
| |
| icu::Locale icu_locale = r.icu_locale; |
| UErrorCode status = U_ZERO_ERROR; |
| |
| // 19. Let relativeTimeFormat.[[NumberFormat]] be |
| // ? Construct(%NumberFormat%, « nfLocale, nfOptions »). |
| icu::NumberFormat* number_format = |
| icu::NumberFormat::createInstance(icu_locale, UNUM_DECIMAL, status); |
| if (U_FAILURE(status)) { |
| delete number_format; |
| FATAL("Failed to create ICU number format, are ICU data files missing?"); |
| } |
| CHECK_NOT_NULL(number_format); |
| |
| // 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, |
| getIcuStyle(style_enum), |
| UDISPCTX_CAPITALIZATION_NONE, status); |
| if (U_FAILURE(status)) { |
| delete icu_formatter; |
| FATAL( |
| "Failed to create ICU relative date time formatter, are ICU data files " |
| "missing?"); |
| } |
| CHECK_NOT_NULL(icu_formatter); |
| |
| Handle<Managed<icu::RelativeDateTimeFormatter>> managed_formatter = |
| Managed<icu::RelativeDateTimeFormatter>::FromRawPtr(isolate, 0, |
| icu_formatter); |
| |
| // 21. Set relativeTimeFormat.[[InitializedRelativeTimeFormat]] to true. |
| relative_time_format_holder->set_icu_formatter(*managed_formatter); |
| |
| // 22. Return relativeTimeFormat. |
| return relative_time_format_holder; |
| } |
| |
| Handle<JSObject> JSRelativeTimeFormat::ResolvedOptions( |
| Isolate* isolate, Handle<JSRelativeTimeFormat> format_holder) { |
| Factory* factory = isolate->factory(); |
| Handle<JSObject> result = factory->NewJSObject(isolate->object_function()); |
| Handle<String> locale(format_holder->locale(), isolate); |
| JSObject::AddProperty(isolate, result, factory->locale_string(), locale, |
| NONE); |
| JSObject::AddProperty(isolate, result, factory->style_string(), |
| format_holder->StyleAsString(), NONE); |
| JSObject::AddProperty(isolate, result, factory->numeric_string(), |
| format_holder->NumericAsString(), NONE); |
| return result; |
| } |
| |
| Handle<String> JSRelativeTimeFormat::StyleAsString() const { |
| switch (style()) { |
| case Style::LONG: |
| return GetReadOnlyRoots().long_string_handle(); |
| case Style::SHORT: |
| return GetReadOnlyRoots().short_string_handle(); |
| case Style::NARROW: |
| return GetReadOnlyRoots().narrow_string_handle(); |
| case Style::COUNT: |
| UNREACHABLE(); |
| } |
| } |
| |
| Handle<String> JSRelativeTimeFormat::NumericAsString() const { |
| switch (numeric()) { |
| case Numeric::ALWAYS: |
| return GetReadOnlyRoots().always_string_handle(); |
| case Numeric::AUTO: |
| return GetReadOnlyRoots().auto_string_handle(); |
| case Numeric::COUNT: |
| 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(); |
| } |
| } |
| |
| MaybeHandle<JSArray> GenerateRelativeTimeFormatParts( |
| Isolate* isolate, const icu::UnicodeString& formatted, |
| const icu::UnicodeString& integer_part, URelativeDateTimeUnit unit_enum) { |
| Factory* factory = isolate->factory(); |
| Handle<JSArray> array = factory->NewJSArray(0); |
| int32_t found = formatted.indexOf(integer_part); |
| |
| Handle<String> substring; |
| if (found < 0) { |
| // Cannot find the integer_part in the formatted. |
| // Return [{'type': 'literal', 'value': formatted}] |
| ASSIGN_RETURN_ON_EXCEPTION(isolate, substring, |
| Intl::ToString(isolate, formatted), JSArray); |
| Intl::AddElement(isolate, array, |
| 0, // index |
| factory->literal_string(), // field_type_string |
| substring); |
| } else { |
| // Found the formatted integer in the result. |
| int index = 0; |
| |
| // array.push({ |
| // 'type': 'literal', |
| // 'value': formatted.substring(0, found)}) |
| if (found > 0) { |
| ASSIGN_RETURN_ON_EXCEPTION(isolate, substring, |
| Intl::ToString(isolate, formatted, 0, found), |
| JSArray); |
| Intl::AddElement(isolate, array, index++, |
| factory->literal_string(), // field_type_string |
| substring); |
| } |
| |
| // array.push({ |
| // 'type': 'integer', |
| // 'value': formatted.substring(found, found + integer_part.length), |
| // 'unit': unit}) |
| ASSIGN_RETURN_ON_EXCEPTION(isolate, substring, |
| Intl::ToString(isolate, formatted, found, |
| found + integer_part.length()), |
| JSArray); |
| Handle<String> unit = UnitAsString(isolate, unit_enum); |
| Intl::AddElement(isolate, array, index++, |
| factory->integer_string(), // field_type_string |
| substring, factory->unit_string(), unit); |
| |
| // array.push({ |
| // 'type': 'literal', |
| // 'value': formatted.substring( |
| // found + integer_part.length, formatted.length)}) |
| if (found + integer_part.length() < formatted.length()) { |
| ASSIGN_RETURN_ON_EXCEPTION( |
| isolate, substring, |
| Intl::ToString(isolate, formatted, found + integer_part.length(), |
| formatted.length()), |
| JSArray); |
| Intl::AddElement(isolate, array, index, |
| factory->literal_string(), // field_type_string |
| substring); |
| } |
| } |
| return array; |
| } |
| |
| 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; |
| } |
| |
| } // namespace |
| |
| MaybeHandle<Object> JSRelativeTimeFormat::Format( |
| Isolate* isolate, Handle<Object> value_obj, Handle<Object> unit_obj, |
| Handle<JSRelativeTimeFormat> format_holder, const char* func_name, |
| bool to_parts) { |
| Factory* factory = isolate->factory(); |
| |
| // 3. Let value be ? ToNumber(value). |
| Handle<Object> value; |
| ASSIGN_RETURN_ON_EXCEPTION(isolate, value, |
| Object::ToNumber(isolate, value_obj), Object); |
| double number = value->Number(); |
| // 4. Let unit be ? ToString(unit). |
| Handle<String> unit; |
| ASSIGN_RETURN_ON_EXCEPTION(isolate, unit, Object::ToString(isolate, unit_obj), |
| Object); |
| |
| // 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)), |
| Object); |
| } |
| |
| icu::RelativeDateTimeFormatter* formatter = |
| format_holder->icu_formatter()->raw(); |
| CHECK_NOT_NULL(formatter); |
| |
| URelativeDateTimeUnit unit_enum; |
| if (!GetURelativeDateTimeUnit(unit, &unit_enum)) { |
| THROW_NEW_ERROR( |
| isolate, |
| NewRangeError(MessageTemplate::kInvalidUnit, |
| isolate->factory()->NewStringFromAsciiChecked(func_name), |
| unit), |
| Object); |
| } |
| |
| UErrorCode status = U_ZERO_ERROR; |
| icu::UnicodeString formatted; |
| |
| #if USE_CHROMIUM_ICU != 1 && U_ICU_VERSION_MAJOR_NUM < 63 |
| if (unit_enum != UDAT_REL_UNIT_QUARTER) { // ICU did not implement |
| // UDAT_REL_UNIT_QUARTER < 63 |
| #endif // USE_CHROMIUM_ICU != 1 && U_ICU_VERSION_MAJOR_NUM < 63 |
| if (format_holder->numeric() == JSRelativeTimeFormat::Numeric::ALWAYS) { |
| formatter->formatNumeric(number, unit_enum, formatted, status); |
| } else { |
| DCHECK_EQ(JSRelativeTimeFormat::Numeric::AUTO, format_holder->numeric()); |
| formatter->format(number, unit_enum, formatted, status); |
| } |
| #if USE_CHROMIUM_ICU != 1 && U_ICU_VERSION_MAJOR_NUM < 63 |
| } |
| #endif // USE_CHROMIUM_ICU != 1 && U_ICU_VERSION_MAJOR_NUM < 63 |
| |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kIcuError), Object); |
| } |
| |
| if (to_parts) { |
| icu::UnicodeString integer; |
| icu::FieldPosition pos; |
| formatter->getNumberFormat().format(std::abs(number), integer, pos, status); |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kIcuError), |
| Object); |
| } |
| |
| Handle<JSArray> elements; |
| ASSIGN_RETURN_ON_EXCEPTION( |
| isolate, elements, |
| GenerateRelativeTimeFormatParts(isolate, formatted, integer, unit_enum), |
| Object); |
| return elements; |
| } |
| |
| return factory->NewStringFromTwoByte(Vector<const uint16_t>( |
| reinterpret_cast<const uint16_t*>(formatted.getBuffer()), |
| formatted.length())); |
| } |
| |
| } // namespace internal |
| } // namespace v8 |