| // Copyright 2022 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-duration-format.h" |
| |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| |
| #include "src/execution/isolate.h" |
| #include "src/heap/factory.h" |
| #include "src/objects/intl-objects.h" |
| #include "src/objects/js-duration-format-inl.h" |
| #include "src/objects/js-number-format.h" |
| #include "src/objects/js-temporal-objects.h" |
| #include "src/objects/managed-inl.h" |
| #include "src/objects/objects-inl.h" |
| #include "src/objects/option-utils.h" |
| #include "unicode/dtfmtsym.h" |
| #include "unicode/listformatter.h" |
| #include "unicode/locid.h" |
| #include "unicode/numberformatter.h" |
| #include "unicode/ulistformatter.h" |
| #include "unicode/unumberformatter.h" |
| |
| namespace v8 { |
| namespace internal { |
| |
| using temporal::DurationRecord; |
| |
| namespace { |
| |
| // #sec-getdurationunitoptions |
| enum class StylesList { k3Styles, k4Styles, k5Styles }; |
| enum class UnitKind { kMinutesOrSeconds, kOthers }; |
| struct DurationUnitOptions { |
| JSDurationFormat::FieldStyle style; |
| JSDurationFormat::Display display; |
| }; |
| Maybe<DurationUnitOptions> GetDurationUnitOptions( |
| Isolate* isolate, const char* unit, const char* display_field, |
| Handle<JSReceiver> options, JSDurationFormat::Style base_style, |
| StylesList styles_list, JSDurationFormat::FieldStyle prev_style, |
| UnitKind unit_kind, const char* method_name) { |
| JSDurationFormat::FieldStyle style; |
| JSDurationFormat::FieldStyle digital_base; |
| // 1. Let style be ? GetOption(options, unit, "string", stylesList, |
| // undefined). |
| switch (styles_list) { |
| case StylesList::k3Styles: |
| // For years, months, weeks, days |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, style, |
| GetStringOption<JSDurationFormat::FieldStyle>( |
| isolate, options, unit, method_name, {"long", "short", "narrow"}, |
| {JSDurationFormat::FieldStyle::kLong, |
| JSDurationFormat::FieldStyle::kShort, |
| JSDurationFormat::FieldStyle::kNarrow}, |
| JSDurationFormat::FieldStyle::kUndefined), |
| Nothing<DurationUnitOptions>()); |
| digital_base = JSDurationFormat::FieldStyle::kShort; |
| break; |
| case StylesList::k4Styles: |
| // For milliseconds, microseconds, nanoseconds |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, style, |
| GetStringOption<JSDurationFormat::FieldStyle>( |
| isolate, options, unit, method_name, |
| {"long", "short", "narrow", "numeric"}, |
| {JSDurationFormat::FieldStyle::kLong, |
| JSDurationFormat::FieldStyle::kShort, |
| JSDurationFormat::FieldStyle::kNarrow, |
| JSDurationFormat::FieldStyle::kNumeric}, |
| JSDurationFormat::FieldStyle::kUndefined), |
| Nothing<DurationUnitOptions>()); |
| digital_base = JSDurationFormat::FieldStyle::kNumeric; |
| break; |
| case StylesList::k5Styles: |
| // For hours, minutes, seconds |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, style, |
| GetStringOption<JSDurationFormat::FieldStyle>( |
| isolate, options, unit, method_name, |
| {"long", "short", "narrow", "numeric", "2-digit"}, |
| {JSDurationFormat::FieldStyle::kLong, |
| JSDurationFormat::FieldStyle::kShort, |
| JSDurationFormat::FieldStyle::kNarrow, |
| JSDurationFormat::FieldStyle::kNumeric, |
| JSDurationFormat::FieldStyle::k2Digit}, |
| JSDurationFormat::FieldStyle::kUndefined), |
| Nothing<DurationUnitOptions>()); |
| digital_base = JSDurationFormat::FieldStyle::kNumeric; |
| break; |
| } |
| |
| // 2. Let displayDefault be "always". |
| JSDurationFormat::Display display_default = |
| JSDurationFormat::Display::kAlways; |
| // 3. If style is undefined, then |
| if (style == JSDurationFormat::FieldStyle::kUndefined) { |
| // a. If baseStyle is "digital", then |
| if (base_style == JSDurationFormat::Style::kDigital) { |
| // i. If unit is not one of "hours", "minutes", or "seconds", then |
| if (styles_list != StylesList::k5Styles) { |
| DCHECK_NE(0, strcmp(unit, "hours")); |
| DCHECK_NE(0, strcmp(unit, "minutes")); |
| DCHECK_NE(0, strcmp(unit, "seconds")); |
| // a. Set displayDefault to "auto". |
| display_default = JSDurationFormat::Display::kAuto; |
| } |
| // ii. Set style to digitalBase. |
| style = digital_base; |
| // b. Else |
| } else { |
| // i. Set displayDefault to "auto". |
| display_default = JSDurationFormat::Display::kAuto; |
| // ii. if prevStyle is "numeric" or "2-digit", then |
| if (prev_style == JSDurationFormat::FieldStyle::kNumeric || |
| prev_style == JSDurationFormat::FieldStyle::k2Digit) { |
| // 1. Set style to "numeric". |
| style = JSDurationFormat::FieldStyle::kNumeric; |
| // iii. Else, |
| } else { |
| // 1. Set style to baseStyle. |
| switch (base_style) { |
| case JSDurationFormat::Style::kLong: |
| style = JSDurationFormat::FieldStyle::kLong; |
| break; |
| case JSDurationFormat::Style::kShort: |
| style = JSDurationFormat::FieldStyle::kShort; |
| break; |
| case JSDurationFormat::Style::kNarrow: |
| style = JSDurationFormat::FieldStyle::kNarrow; |
| break; |
| default: |
| UNREACHABLE(); |
| } |
| } |
| } |
| } |
| // 4. Let displayField be the string-concatenation of unit and "Display". |
| // 5. Let display be ? GetOption(options, displayField, "string", « "auto", |
| // "always" », displayDefault). |
| JSDurationFormat::Display display; |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, display, |
| GetStringOption<JSDurationFormat::Display>( |
| isolate, options, display_field, method_name, {"auto", "always"}, |
| {JSDurationFormat::Display::kAuto, |
| JSDurationFormat::Display::kAlways}, |
| display_default), |
| Nothing<DurationUnitOptions>()); |
| // 6. If prevStyle is "numeric" or "2-digit", then |
| if (prev_style == JSDurationFormat::FieldStyle::kNumeric || |
| prev_style == JSDurationFormat::FieldStyle::k2Digit) { |
| // a. If style is not "numeric" or "2-digit", then |
| if (style != JSDurationFormat::FieldStyle::kNumeric && |
| style != JSDurationFormat::FieldStyle::k2Digit) { |
| // i. Throw a RangeError exception. |
| // b. Else if unit is "minutes" or "seconds", then |
| } else if (unit_kind == UnitKind::kMinutesOrSeconds) { |
| CHECK(strcmp(unit, "minutes") == 0 || strcmp(unit, "seconds") == 0); |
| // i. Set style to "2-digit". |
| style = JSDurationFormat::FieldStyle::k2Digit; |
| } |
| } |
| // 7. Return the Record { [[Style]]: style, [[Display]]: display }. |
| return Just(DurationUnitOptions({style, display})); |
| } |
| |
| JSDurationFormat::Separator GetSeparator(const icu::Locale& l) { |
| UErrorCode status = U_ZERO_ERROR; |
| icu::DateFormatSymbols sym(l, status); |
| if (U_FAILURE(status)) return JSDurationFormat::Separator::kColon; |
| icu::UnicodeString sep; |
| sym.getTimeSeparatorString(sep); |
| if (sep.length() != 1) return JSDurationFormat::Separator::kColon; |
| switch (sep.charAt(0)) { |
| case u'.': |
| return JSDurationFormat::Separator::kFullStop; |
| case u'\uFF1A': |
| return JSDurationFormat::Separator::kFullwidthColon; |
| case u'\u066B': |
| return JSDurationFormat::Separator::kArabicDecimalSeparator; |
| // By default, or if we get anything else, just use ':'. |
| default: |
| return JSDurationFormat::Separator::kColon; |
| } |
| } |
| |
| } // namespace |
| MaybeHandle<JSDurationFormat> JSDurationFormat::New( |
| Isolate* isolate, Handle<Map> map, Handle<Object> locales, |
| Handle<Object> input_options) { |
| Factory* factory = isolate->factory(); |
| const char* method_name = "Intl.DurationFormat"; |
| |
| // 3. Let requestedLocales be ? CanonicalizeLocaleList(locales). |
| std::vector<std::string> requested_locales; |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, requested_locales, |
| Intl::CanonicalizeLocaleList(isolate, locales), |
| Handle<JSDurationFormat>()); |
| |
| // 4. Let options be ? GetOptionsObject(options). |
| Handle<JSReceiver> options; |
| ASSIGN_RETURN_ON_EXCEPTION( |
| isolate, options, GetOptionsObject(isolate, input_options, method_name), |
| JSDurationFormat); |
| |
| // 5. Let matcher be ? GetOption(options, "localeMatcher", "string", « |
| // "lookup", "best fit" », "best fit"). |
| Intl::MatcherOption matcher; |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, matcher, Intl::GetLocaleMatcher(isolate, options, method_name), |
| Handle<JSDurationFormat>()); |
| |
| // 6. Let numberingSystem be ? GetOption(options, "numberingSystem", "string", |
| // undefined, undefined). |
| // |
| // 7. If numberingSystem is not undefined, then |
| // |
| // a. If numberingSystem does not match the Unicode Locale Identifier type |
| // nonterminal, throw a RangeError exception. |
| // Note: The matching test and throw in Step 7-a is throw inside |
| // Intl::GetNumberingSystem. |
| std::unique_ptr<char[]> numbering_system_str = nullptr; |
| bool get; |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, get, |
| Intl::GetNumberingSystem(isolate, options, method_name, |
| &numbering_system_str), |
| Handle<JSDurationFormat>()); |
| |
| // 8. Let opt be the Record { [[localeMatcher]]: matcher, [[nu]]: |
| // numberingSystem }. |
| // 9. Let r be ResolveLocale(%DurationFormat%.[[AvailableLocales]], |
| // requestedLocales, opt, %DurationFormat%.[[RelevantExtensionKeys]], |
| // %DurationFormat%.[[LocaleData]]). |
| std::set<std::string> relevant_extension_keys{"nu"}; |
| Intl::ResolvedLocale r; |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, r, |
| Intl::ResolveLocale(isolate, JSDurationFormat::GetAvailableLocales(), |
| requested_locales, matcher, relevant_extension_keys), |
| Handle<JSDurationFormat>()); |
| |
| // 10. Let locale be r.[[locale]]. |
| icu::Locale r_locale = r.icu_locale; |
| UErrorCode status = U_ZERO_ERROR; |
| // 11. Set durationFormat.[[Locale]] to locale. |
| // 12. Set durationFormat.[[NumberingSystem]] to r.[[nu]]. |
| 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()) { |
| r_locale.setUnicodeKeywordValue("nu", nullptr, status); |
| DCHECK(U_SUCCESS(status)); |
| } |
| } |
| icu::Locale icu_locale = r_locale; |
| if (numbering_system_str != nullptr && |
| Intl::IsValidNumberingSystem(numbering_system_str.get())) { |
| r_locale.setUnicodeKeywordValue("nu", numbering_system_str.get(), status); |
| DCHECK(U_SUCCESS(status)); |
| } |
| std::string numbering_system = Intl::GetNumberingSystem(r_locale); |
| Separator separator = GetSeparator(r_locale); |
| |
| // 13. Let style be ? GetOption(options, "style", "string", « "long", "short", |
| // "narrow", "digital" », "long"). |
| Style style; |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, style, |
| GetStringOption<Style>( |
| isolate, options, "style", method_name, |
| {"long", "short", "narrow", "digital"}, |
| {Style::kLong, Style::kShort, Style::kNarrow, Style::kDigital}, |
| Style::kShort), |
| Handle<JSDurationFormat>()); |
| |
| // 14. Set durationFormat.[[Style]] to style. |
| // 15. Set durationFormat.[[DataLocale]] to r.[[dataLocale]]. |
| Handle<Managed<icu::Locale>> managed_locale = |
| Managed<icu::Locale>::FromRawPtr(isolate, 0, icu_locale.clone()); |
| // 16. Let prevStyle be the empty String. |
| FieldStyle prev_style = FieldStyle::kUndefined; |
| // 17. For each row of Table 1, except the header row, in table order, do |
| // a. Let styleSlot be the Style Slot value of the current row. |
| // b. Let displaySlot be the Display Slot value of the current row. |
| // c. Let unit be the Unit value. |
| // d. Let valueList be the Values value. |
| // e. Let digitalBase be the Digital Default value. |
| // f. Let unitOptions be ? GetDurationUnitOptions(unit, options, style, |
| // valueList, digitalBase, prevStyle). |
| // of durationFormat to unitOptions.[[Style]]. |
| // h. Set the value of the |
| // displaySlot slot of durationFormat to unitOptions.[[Display]]. |
| // i. If unit is one of "hours", "minutes", "seconds", "milliseconds", |
| // or "microseconds", then |
| // i. Set prevStyle to unitOptions.[[Style]]. |
| // g. Set the value of the styleSlot slot |
| DurationUnitOptions years_option; |
| DurationUnitOptions months_option; |
| DurationUnitOptions weeks_option; |
| DurationUnitOptions days_option; |
| DurationUnitOptions hours_option; |
| DurationUnitOptions minutes_option; |
| DurationUnitOptions seconds_option; |
| DurationUnitOptions milliseconds_option; |
| DurationUnitOptions microseconds_option; |
| DurationUnitOptions nanoseconds_option; |
| |
| #define CALL_GET_DURATION_UNIT_OPTIONS(u, sl, uk) \ |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( \ |
| isolate, u##_option, \ |
| GetDurationUnitOptions(isolate, #u, #u "Display", options, style, sl, \ |
| prev_style, uk, method_name), \ |
| Handle<JSDurationFormat>()); |
| CALL_GET_DURATION_UNIT_OPTIONS(years, StylesList::k3Styles, UnitKind::kOthers) |
| CALL_GET_DURATION_UNIT_OPTIONS(months, StylesList::k3Styles, |
| UnitKind::kOthers) |
| CALL_GET_DURATION_UNIT_OPTIONS(weeks, StylesList::k3Styles, UnitKind::kOthers) |
| CALL_GET_DURATION_UNIT_OPTIONS(days, StylesList::k3Styles, UnitKind::kOthers) |
| CALL_GET_DURATION_UNIT_OPTIONS(hours, StylesList::k5Styles, UnitKind::kOthers) |
| prev_style = hours_option.style; |
| CALL_GET_DURATION_UNIT_OPTIONS(minutes, StylesList::k5Styles, |
| UnitKind::kMinutesOrSeconds) |
| prev_style = minutes_option.style; |
| CALL_GET_DURATION_UNIT_OPTIONS(seconds, StylesList::k5Styles, |
| UnitKind::kMinutesOrSeconds) |
| prev_style = seconds_option.style; |
| CALL_GET_DURATION_UNIT_OPTIONS(milliseconds, StylesList::k4Styles, |
| UnitKind::kOthers) |
| prev_style = milliseconds_option.style; |
| CALL_GET_DURATION_UNIT_OPTIONS(microseconds, StylesList::k4Styles, |
| UnitKind::kOthers) |
| prev_style = microseconds_option.style; |
| CALL_GET_DURATION_UNIT_OPTIONS(nanoseconds, StylesList::k4Styles, |
| UnitKind::kOthers) |
| #undef CALL_GET_DURATION_UNIT_OPTIONS |
| // 18. Set durationFormat.[[FractionalDigits]] to ? GetNumberOption(options, |
| // "fractionalDigits", 0, 9, undefined). |
| int fractional_digits; |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, fractional_digits, |
| GetNumberOption(isolate, options, factory->fractionalDigits_string(), 0, |
| 9, kUndefinedFractionalDigits), |
| Handle<JSDurationFormat>()); |
| |
| icu::number::LocalizedNumberFormatter fmt = |
| icu::number::UnlocalizedNumberFormatter() |
| .roundingMode(UNUM_ROUND_HALFUP) |
| .locale(icu_locale); |
| if (!numbering_system.empty() && numbering_system != "latn") { |
| fmt = fmt.adoptSymbols(icu::NumberingSystem::createInstanceByName( |
| numbering_system.c_str(), status)); |
| DCHECK(U_SUCCESS(status)); |
| } |
| Handle<Managed<icu::number::LocalizedNumberFormatter>> |
| managed_number_formatter = |
| Managed<icu::number::LocalizedNumberFormatter>::FromRawPtr( |
| isolate, 0, new icu::number::LocalizedNumberFormatter(fmt)); |
| |
| // 19. Return durationFormat. |
| Handle<JSDurationFormat> duration_format = Handle<JSDurationFormat>::cast( |
| factory->NewFastOrSlowJSObjectFromMap(map)); |
| duration_format->set_style_flags(0); |
| duration_format->set_display_flags(0); |
| duration_format->set_style(style); |
| duration_format->set_years_style(years_option.style); |
| duration_format->set_months_style(months_option.style); |
| duration_format->set_weeks_style(weeks_option.style); |
| duration_format->set_days_style(days_option.style); |
| duration_format->set_hours_style(hours_option.style); |
| duration_format->set_minutes_style(minutes_option.style); |
| duration_format->set_seconds_style(seconds_option.style); |
| duration_format->set_milliseconds_style(milliseconds_option.style); |
| duration_format->set_microseconds_style(microseconds_option.style); |
| duration_format->set_nanoseconds_style(nanoseconds_option.style); |
| duration_format->set_separator(separator); |
| |
| duration_format->set_years_display(years_option.display); |
| duration_format->set_months_display(months_option.display); |
| duration_format->set_weeks_display(weeks_option.display); |
| duration_format->set_days_display(days_option.display); |
| duration_format->set_hours_display(hours_option.display); |
| duration_format->set_minutes_display(minutes_option.display); |
| duration_format->set_seconds_display(seconds_option.display); |
| duration_format->set_milliseconds_display(milliseconds_option.display); |
| duration_format->set_microseconds_display(microseconds_option.display); |
| duration_format->set_nanoseconds_display(nanoseconds_option.display); |
| |
| duration_format->set_fractional_digits(fractional_digits); |
| |
| duration_format->set_icu_locale(*managed_locale); |
| duration_format->set_icu_number_formatter(*managed_number_formatter); |
| |
| return duration_format; |
| } |
| |
| namespace { |
| |
| Handle<String> StyleToString(Isolate* isolate, JSDurationFormat::Style style) { |
| switch (style) { |
| case JSDurationFormat::Style::kLong: |
| return ReadOnlyRoots(isolate).long_string_handle(); |
| case JSDurationFormat::Style::kShort: |
| return ReadOnlyRoots(isolate).short_string_handle(); |
| case JSDurationFormat::Style::kNarrow: |
| return ReadOnlyRoots(isolate).narrow_string_handle(); |
| case JSDurationFormat::Style::kDigital: |
| return ReadOnlyRoots(isolate).digital_string_handle(); |
| } |
| } |
| |
| Handle<String> StyleToString(Isolate* isolate, |
| JSDurationFormat::FieldStyle style) { |
| switch (style) { |
| case JSDurationFormat::FieldStyle::kLong: |
| return ReadOnlyRoots(isolate).long_string_handle(); |
| case JSDurationFormat::FieldStyle::kShort: |
| return ReadOnlyRoots(isolate).short_string_handle(); |
| case JSDurationFormat::FieldStyle::kNarrow: |
| return ReadOnlyRoots(isolate).narrow_string_handle(); |
| case JSDurationFormat::FieldStyle::kNumeric: |
| return ReadOnlyRoots(isolate).numeric_string_handle(); |
| case JSDurationFormat::FieldStyle::k2Digit: |
| return ReadOnlyRoots(isolate).two_digit_string_handle(); |
| case JSDurationFormat::FieldStyle::kUndefined: |
| UNREACHABLE(); |
| } |
| } |
| |
| Handle<String> DisplayToString(Isolate* isolate, |
| JSDurationFormat::Display display) { |
| switch (display) { |
| case JSDurationFormat::Display::kAuto: |
| return ReadOnlyRoots(isolate).auto_string_handle(); |
| case JSDurationFormat::Display::kAlways: |
| return ReadOnlyRoots(isolate).always_string_handle(); |
| } |
| } |
| |
| } // namespace |
| |
| Handle<JSObject> JSDurationFormat::ResolvedOptions( |
| Isolate* isolate, Handle<JSDurationFormat> format) { |
| Factory* factory = isolate->factory(); |
| Handle<JSObject> options = factory->NewJSObject(isolate->object_function()); |
| |
| Handle<String> locale = factory->NewStringFromAsciiChecked( |
| Intl::ToLanguageTag(*format->icu_locale()->raw()).FromJust().c_str()); |
| UErrorCode status = U_ZERO_ERROR; |
| icu::UnicodeString skeleton = |
| format->icu_number_formatter()->raw()->toSkeleton(status); |
| DCHECK(U_SUCCESS(status)); |
| |
| Handle<String> numbering_system; |
| CHECK(Intl::ToString(isolate, |
| JSNumberFormat::NumberingSystemFromSkeleton(skeleton)) |
| .ToHandle(&numbering_system)); |
| |
| bool created; |
| |
| #define OUTPUT_PROPERTY(s, f) \ |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( \ |
| isolate, created, \ |
| JSReceiver::CreateDataProperty(isolate, options, factory->s(), f, \ |
| Just(kDontThrow)), \ |
| Handle<JSObject>()); \ |
| CHECK(created); |
| #define OUTPUT_STYLE_PROPERTY(p) \ |
| OUTPUT_PROPERTY(p##_string, StyleToString(isolate, format->p##_style())) |
| #define OUTPUT_DISPLAY_PROPERTY(p) \ |
| OUTPUT_PROPERTY(p##Display_string, \ |
| DisplayToString(isolate, format->p##_display())) |
| #define OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(p) \ |
| OUTPUT_STYLE_PROPERTY(p); \ |
| OUTPUT_DISPLAY_PROPERTY(p); |
| |
| OUTPUT_PROPERTY(locale_string, locale); |
| OUTPUT_PROPERTY(style_string, StyleToString(isolate, format->style())); |
| |
| OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(years); |
| OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(months); |
| OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(weeks); |
| OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(days); |
| OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(hours); |
| OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(minutes); |
| OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(seconds); |
| OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(milliseconds); |
| OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(microseconds); |
| OUTPUT_STYLE_AND_DISPLAY_PROPERTIES(nanoseconds); |
| |
| int32_t fractional_digits = format->fractional_digits(); |
| if (kUndefinedFractionalDigits == fractional_digits) { |
| OUTPUT_PROPERTY(fractionalDigits_string, factory->undefined_value()); |
| } else { |
| Handle<Smi> fractional_digits_obj = |
| handle(Smi::FromInt(fractional_digits), isolate); |
| OUTPUT_PROPERTY(fractionalDigits_string, fractional_digits_obj); |
| } |
| OUTPUT_PROPERTY(numberingSystem_string, numbering_system); |
| #undef OUTPUT_PROPERTY |
| #undef OUTPUT_STYLE_PROPERTY |
| #undef OUTPUT_DISPLAY_PROPERTY |
| #undef OUTPUT_STYLE_AND_DISPLAY_PROPERTIES |
| |
| return options; |
| } |
| |
| namespace { |
| |
| UNumberUnitWidth ToUNumberUnitWidth(JSDurationFormat::FieldStyle style) { |
| switch (style) { |
| case JSDurationFormat::FieldStyle::kShort: |
| return UNumberUnitWidth::UNUM_UNIT_WIDTH_SHORT; |
| case JSDurationFormat::FieldStyle::kLong: |
| return UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME; |
| case JSDurationFormat::FieldStyle::kNarrow: |
| return UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW; |
| default: |
| UNREACHABLE(); |
| } |
| } |
| |
| struct Part { |
| enum Type { kFormatted, kSeparator }; |
| Type part_type; |
| std::string type; |
| icu::number::FormattedNumber formatted; |
| }; |
| |
| char16_t SeparatorToChar(JSDurationFormat::Separator separator) { |
| switch (separator) { |
| case JSDurationFormat::Separator::kColon: |
| return u':'; |
| case JSDurationFormat::Separator::kFullStop: |
| return u'.'; |
| case JSDurationFormat::Separator::kFullwidthColon: |
| return u'\uFF1A'; |
| case JSDurationFormat::Separator::kArabicDecimalSeparator: |
| return u'\u066B'; |
| } |
| } |
| |
| void Output(const char* type, double value, |
| const icu::number::LocalizedNumberFormatter& fmt, bool addToLast, |
| JSDurationFormat::Separator separator, |
| std::vector<std::vector<Part>>* parts, |
| std::vector<icu::UnicodeString>* strings) { |
| UErrorCode status = U_ZERO_ERROR; |
| icu::number::FormattedNumber formatted = fmt.formatDouble(value, status); |
| icu::UnicodeString unit_string = formatted.toString(status); |
| CHECK(U_SUCCESS(status)); |
| Part p = {Part::Type::kFormatted, std::string(type), std::move(formatted)}; |
| if (addToLast && !strings->empty()) { |
| strings->back().append(SeparatorToChar(separator)); |
| strings->back() += unit_string; |
| |
| if (parts != nullptr) { |
| icu::number::FormattedNumber dummy; |
| Part s = {Part::Type::kSeparator, std::string(), std::move(dummy)}; |
| parts->back().push_back(std::move(s)); |
| parts->back().push_back(std::move(p)); |
| } |
| return; |
| } |
| strings->push_back(unit_string); |
| if (parts != nullptr) { |
| std::vector<Part> v; |
| v.push_back(std::move(p)); |
| parts->push_back(std::move(v)); |
| } |
| } |
| |
| void Output3Styles(const char* type, double value, |
| JSDurationFormat::Display display, |
| const icu::number::LocalizedNumberFormatter& fmt, |
| bool addToLast, JSDurationFormat::Separator separator, |
| std::vector<std::vector<Part>>* parts, |
| std::vector<icu::UnicodeString>* strings) { |
| if (value == 0 && display == JSDurationFormat::Display::kAuto) return; |
| Output(type, value, fmt, addToLast, separator, parts, strings); |
| } |
| |
| void Output4Styles(const char* type, double value, |
| JSDurationFormat::Display display, |
| JSDurationFormat::FieldStyle style, |
| const icu::number::LocalizedNumberFormatter& fmt, |
| icu::MeasureUnit unit, bool addToLast, |
| JSDurationFormat::Separator separator, |
| std::vector<std::vector<Part>>* parts, |
| std::vector<icu::UnicodeString>* strings) { |
| if (value == 0 && display == JSDurationFormat::Display::kAuto) return; |
| if (style == JSDurationFormat::FieldStyle::kNumeric) { |
| return Output(type, value, fmt, addToLast, separator, parts, strings); |
| } |
| Output3Styles(type, value, display, |
| fmt.unit(unit).unitWidth(ToUNumberUnitWidth(style)), addToLast, |
| separator, parts, strings); |
| } |
| void Output5Styles(const char* type, double value, |
| JSDurationFormat::Display display, |
| JSDurationFormat::FieldStyle style, |
| const icu::number::LocalizedNumberFormatter& fmt, |
| icu::MeasureUnit unit, bool maybeAddToLast, |
| JSDurationFormat::Separator separator, |
| std::vector<std::vector<Part>>* parts, |
| std::vector<icu::UnicodeString>* strings) { |
| if (value == 0 && display == JSDurationFormat::Display::kAuto) return; |
| if (style == JSDurationFormat::FieldStyle::k2Digit) { |
| return Output(type, value, |
| fmt.integerWidth(icu::number::IntegerWidth::zeroFillTo(2)), |
| maybeAddToLast, separator, parts, strings); |
| } |
| Output4Styles( |
| type, value, display, style, fmt, unit, |
| (maybeAddToLast && JSDurationFormat::FieldStyle::kNumeric == style), |
| separator, parts, strings); |
| } |
| |
| void DurationRecordToListOfFormattedNumber( |
| Handle<JSDurationFormat> df, |
| const icu::number::LocalizedNumberFormatter& fmt, |
| const DurationRecord& record, std::vector<std::vector<Part>>* parts, |
| std::vector<icu::UnicodeString>* strings) { |
| JSDurationFormat::Separator separator = df->separator(); |
| Output3Styles("year", record.years, df->years_display(), |
| fmt.unit(icu::MeasureUnit::getYear()) |
| .unitWidth(ToUNumberUnitWidth(df->years_style())), |
| false, separator, parts, strings); |
| Output3Styles("month", record.months, df->months_display(), |
| fmt.unit(icu::MeasureUnit::getMonth()) |
| .unitWidth(ToUNumberUnitWidth(df->months_style())), |
| false, separator, parts, strings); |
| Output3Styles("week", record.weeks, df->weeks_display(), |
| fmt.unit(icu::MeasureUnit::getWeek()) |
| .unitWidth(ToUNumberUnitWidth(df->weeks_style())), |
| false, separator, parts, strings); |
| Output3Styles("day", record.time_duration.days, df->days_display(), |
| fmt.unit(icu::MeasureUnit::getDay()) |
| .unitWidth(ToUNumberUnitWidth(df->days_style())), |
| false, separator, parts, strings); |
| Output5Styles("hour", record.time_duration.hours, df->hours_display(), |
| df->hours_style(), fmt, icu::MeasureUnit::getHour(), false, |
| separator, parts, strings); |
| Output5Styles("minute", record.time_duration.minutes, df->minutes_display(), |
| df->minutes_style(), fmt, icu::MeasureUnit::getMinute(), true, |
| separator, parts, strings); |
| int32_t fractional_digits = df->fractional_digits(); |
| int32_t maximumFractionDigits = |
| (fractional_digits == JSDurationFormat::kUndefinedFractionalDigits) |
| ? 9 |
| : fractional_digits; |
| int32_t minimumFractionDigits = |
| (fractional_digits == JSDurationFormat::kUndefinedFractionalDigits) |
| ? 0 |
| : fractional_digits; |
| |
| if (df->milliseconds_style() == JSDurationFormat::FieldStyle::kNumeric) { |
| // a. Set value to value + duration.[[Milliseconds]] / 10^3 + |
| // duration.[[Microseconds]] / 10^6 + duration.[[Nanoseconds]] / 10^9. |
| double value = record.time_duration.seconds + |
| record.time_duration.milliseconds / 1e3 + |
| record.time_duration.microseconds / 1e6 + |
| record.time_duration.nanoseconds / 1e9; |
| Output5Styles("second", value, df->seconds_display(), df->seconds_style(), |
| fmt.precision(icu::number::Precision::minMaxFraction( |
| minimumFractionDigits, maximumFractionDigits)), |
| icu::MeasureUnit::getSecond(), true, separator, parts, |
| strings); |
| return; |
| } |
| Output5Styles("second", record.time_duration.seconds, df->seconds_display(), |
| df->seconds_style(), fmt, icu::MeasureUnit::getSecond(), true, |
| separator, parts, strings); |
| |
| if (df->microseconds_style() == JSDurationFormat::FieldStyle::kNumeric) { |
| // a. Set value to value + duration.[[Microseconds]] / 10^3 + |
| // duration.[[Nanoseconds]] / 10^6. |
| double value = record.time_duration.milliseconds + |
| record.time_duration.microseconds / 1e3 + |
| record.time_duration.nanoseconds / 1e6; |
| Output4Styles("millisecond", value, df->milliseconds_display(), |
| df->milliseconds_style(), |
| fmt.precision(icu::number::Precision::minMaxFraction( |
| minimumFractionDigits, maximumFractionDigits)), |
| icu::MeasureUnit::getMillisecond(), false, separator, parts, |
| strings); |
| return; |
| } |
| Output4Styles("millisecond", record.time_duration.milliseconds, |
| df->milliseconds_display(), df->milliseconds_style(), fmt, |
| icu::MeasureUnit::getMillisecond(), false, separator, parts, |
| strings); |
| |
| if (df->nanoseconds_style() == JSDurationFormat::FieldStyle::kNumeric) { |
| // a. Set value to value + duration.[[Nanoseconds]] / 10^3. |
| double value = record.time_duration.microseconds + |
| record.time_duration.nanoseconds / 1e3; |
| Output4Styles("microsecond", value, df->microseconds_display(), |
| df->microseconds_style(), |
| fmt.precision(icu::number::Precision::minMaxFraction( |
| minimumFractionDigits, maximumFractionDigits)), |
| icu::MeasureUnit::getMicrosecond(), false, separator, parts, |
| strings); |
| return; |
| } |
| Output4Styles("microsecond", record.time_duration.microseconds, |
| df->microseconds_display(), df->microseconds_style(), fmt, |
| icu::MeasureUnit::getMicrosecond(), false, separator, parts, |
| strings); |
| |
| Output4Styles("nanosecond", record.time_duration.nanoseconds, |
| df->nanoseconds_display(), df->nanoseconds_style(), fmt, |
| icu::MeasureUnit::getNanosecond(), false, separator, parts, |
| strings); |
| } |
| |
| UListFormatterWidth StyleToWidth(JSDurationFormat::Style style) { |
| switch (style) { |
| case JSDurationFormat::Style::kLong: |
| return ULISTFMT_WIDTH_WIDE; |
| case JSDurationFormat::Style::kNarrow: |
| return ULISTFMT_WIDTH_NARROW; |
| case JSDurationFormat::Style::kShort: |
| case JSDurationFormat::Style::kDigital: |
| return ULISTFMT_WIDTH_SHORT; |
| } |
| UNREACHABLE(); |
| } |
| |
| // The last two arguments passed to the Format function is only needed |
| // for Format function to output detail structure and not needed if the |
| // Format only needs to output a String. |
| template <typename T, bool Details, |
| MaybeHandle<T> (*Format)(Isolate*, const icu::FormattedValue&, |
| const std::vector<std::vector<Part>>*, |
| JSDurationFormat::Separator separator)> |
| MaybeHandle<T> PartitionDurationFormatPattern(Isolate* isolate, |
| Handle<JSDurationFormat> df, |
| const DurationRecord& record, |
| const char* method_name) { |
| // 4. Let lfOpts be ! OrdinaryObjectCreate(null). |
| // 5. Perform ! CreateDataPropertyOrThrow(lfOpts, "type", "unit"). |
| UListFormatterType type = ULISTFMT_TYPE_UNITS; |
| // 6. Let listStyle be durationFormat.[[Style]]. |
| // 7. If listStyle is "digital", then |
| // a. Set listStyle to "narrow". |
| // 8. Perform ! CreateDataPropertyOrThrow(lfOpts, "style", listStyle). |
| UListFormatterWidth list_style = StyleToWidth(df->style()); |
| // 9. Let lf be ! Construct(%ListFormat%, « durationFormat.[[Locale]], lfOpts |
| // »). |
| UErrorCode status = U_ZERO_ERROR; |
| icu::Locale icu_locale = *df->icu_locale()->raw(); |
| std::unique_ptr<icu::ListFormatter> formatter( |
| icu::ListFormatter::createInstance(icu_locale, type, list_style, status)); |
| CHECK(U_SUCCESS(status)); |
| |
| std::vector<std::vector<Part>> list; |
| std::vector<std::vector<Part>>* parts = Details ? &list : nullptr; |
| std::vector<icu::UnicodeString> string_list; |
| |
| DurationRecordToListOfFormattedNumber( |
| df, *(df->icu_number_formatter()->raw()), record, parts, &string_list); |
| |
| icu::FormattedList formatted = formatter->formatStringsToValue( |
| string_list.data(), static_cast<int32_t>(string_list.size()), status); |
| CHECK(U_SUCCESS(status)); |
| return Format(isolate, formatted, parts, df->separator()); |
| } |
| |
| // #sec-todurationrecord |
| // ToDurationRecord is almost the same as temporal::ToPartialDuration |
| // except: |
| // 1) In the beginning it will throw RangeError if the type of input is String, |
| // 2) In the end it will throw RangeError if IsValidDurationRecord return false. |
| Maybe<DurationRecord> ToDurationRecord(Isolate* isolate, Handle<Object> input, |
| const DurationRecord& default_value) { |
| // 1-a. If Type(input) is String, throw a RangeError exception. |
| if (IsString(*input)) { |
| THROW_NEW_ERROR_RETURN_VALUE( |
| isolate, |
| NewRangeError(MessageTemplate::kInvalid, |
| isolate->factory()->object_string(), input), |
| Nothing<DurationRecord>()); |
| } |
| // Step 1-b - 23. Same as ToTemporalPartialDurationRecord. |
| DurationRecord record; |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, record, |
| temporal::ToPartialDuration(isolate, input, default_value), |
| Nothing<DurationRecord>()); |
| // 24. If IsValidDurationRecord(result) is false, throw a RangeError |
| // exception. |
| if (!temporal::IsValidDuration(isolate, record)) { |
| THROW_NEW_ERROR_RETURN_VALUE( |
| isolate, |
| NewRangeError(MessageTemplate::kInvalid, |
| isolate->factory()->object_string(), input), |
| Nothing<DurationRecord>()); |
| } |
| return Just(record); |
| } |
| |
| template <typename T, bool Details, |
| MaybeHandle<T> (*Format)(Isolate*, const icu::FormattedValue&, |
| const std::vector<std::vector<Part>>*, |
| JSDurationFormat::Separator)> |
| MaybeHandle<T> FormatCommon(Isolate* isolate, Handle<JSDurationFormat> df, |
| Handle<Object> duration, const char* method_name) { |
| // 1. Let df be this value. |
| // 2. Perform ? RequireInternalSlot(df, [[InitializedDurationFormat]]). |
| // 3. Let record be ? ToDurationRecord(duration). |
| DurationRecord record; |
| MAYBE_ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, record, |
| ToDurationRecord(isolate, duration, {0, 0, 0, {0, 0, 0, 0, 0, 0, 0}}), |
| Handle<T>()); |
| // 5. Let parts be ! PartitionDurationFormatPattern(df, record). |
| return PartitionDurationFormatPattern<T, Details, Format>(isolate, df, record, |
| method_name); |
| } |
| |
| } // namespace |
| |
| MaybeHandle<String> FormattedToString( |
| Isolate* isolate, const icu::FormattedValue& formatted, |
| const std::vector<std::vector<Part>>* parts, JSDurationFormat::Separator) { |
| DCHECK_NULL(parts); |
| return Intl::FormattedToString(isolate, formatted); |
| } |
| |
| MaybeHandle<JSArray> FormattedListToJSArray( |
| Isolate* isolate, const icu::FormattedValue& formatted, |
| const std::vector<std::vector<Part>>* parts, |
| JSDurationFormat::Separator separator) { |
| DCHECK_NOT_NULL(parts); |
| Factory* factory = isolate->factory(); |
| Handle<JSArray> array = factory->NewJSArray(0); |
| icu::ConstrainedFieldPosition cfpos; |
| cfpos.constrainCategory(UFIELD_CATEGORY_LIST); |
| int index = 0; |
| int part_index = 0; |
| UErrorCode status = U_ZERO_ERROR; |
| icu::UnicodeString string = formatted.toString(status); |
| while (formatted.nextPosition(cfpos, status) && U_SUCCESS(status)) { |
| if (cfpos.getField() == ULISTFMT_ELEMENT_FIELD) { |
| for (auto& it : parts->at(part_index++)) { |
| switch (it.part_type) { |
| case Part::Type::kSeparator: { |
| icu::UnicodeString sep(SeparatorToChar(separator)); |
| Handle<String> separator_string; |
| ASSIGN_RETURN_ON_EXCEPTION(isolate, separator_string, |
| Intl::ToString(isolate, sep), JSArray); |
| Intl::AddElement(isolate, array, index++, factory->literal_string(), |
| separator_string); |
| } break; |
| case Part::Type::kFormatted: |
| Handle<String> type_string = |
| factory->NewStringFromAsciiChecked(it.type.c_str()); |
| Maybe<int> index_after_add = Intl::AddNumberElements( |
| isolate, it.formatted, array, index, type_string); |
| MAYBE_RETURN(index_after_add, MaybeHandle<JSArray>()); |
| index = index_after_add.FromJust(); |
| break; |
| } |
| } |
| } else { |
| Handle<String> substring; |
| ASSIGN_RETURN_ON_EXCEPTION( |
| isolate, substring, |
| Intl::ToString(isolate, string, cfpos.getStart(), cfpos.getLimit()), |
| JSArray); |
| Intl::AddElement(isolate, array, index++, factory->literal_string(), |
| substring); |
| } |
| } |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kIcuError), JSArray); |
| } |
| JSObject::ValidateElements(*array); |
| return array; |
| } |
| |
| MaybeHandle<String> JSDurationFormat::Format(Isolate* isolate, |
| Handle<JSDurationFormat> df, |
| Handle<Object> duration) { |
| const char* method_name = "Intl.DurationFormat.prototype.format"; |
| return FormatCommon<String, false, FormattedToString>(isolate, df, duration, |
| method_name); |
| } |
| |
| MaybeHandle<JSArray> JSDurationFormat::FormatToParts( |
| Isolate* isolate, Handle<JSDurationFormat> df, Handle<Object> duration) { |
| const char* method_name = "Intl.DurationFormat.prototype.formatToParts"; |
| return FormatCommon<JSArray, true, FormattedListToJSArray>( |
| isolate, df, duration, method_name); |
| } |
| |
| const std::set<std::string>& JSDurationFormat::GetAvailableLocales() { |
| return JSNumberFormat::GetAvailableLocales(); |
| } |
| |
| } // namespace internal |
| } // namespace v8 |