js-relative-time-format.cc 15.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// 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"
19
#include "src/objects/js-number-format.h"
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
#include "src/objects/js-relative-time-format-inl.h"
#include "unicode/numfmt.h"
#include "unicode/reldatefmt.h"

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();
  }
}
41
}  // namespace
42

43
JSRelativeTimeFormat::Style JSRelativeTimeFormat::getStyle(const char* str) {
44 45 46 47 48 49
  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();
}

50 51
JSRelativeTimeFormat::Numeric JSRelativeTimeFormat::getNumeric(
    const char* str) {
52 53 54 55 56
  if (strcmp(str, "auto") == 0) return JSRelativeTimeFormat::Numeric::AUTO;
  if (strcmp(str, "always") == 0) return JSRelativeTimeFormat::Numeric::ALWAYS;
  UNREACHABLE();
}

57
MaybeHandle<JSRelativeTimeFormat> JSRelativeTimeFormat::Initialize(
58
    Isolate* isolate, Handle<JSRelativeTimeFormat> relative_time_format_holder,
59
    Handle<Object> locales, Handle<Object> input_options) {
60
  relative_time_format_holder->set_flags(0);
61 62 63 64 65 66 67 68 69

  // 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
70 71
  Handle<JSReceiver> options;
  if (input_options->IsUndefined(isolate)) {
72
    // 2. a. Let options be ObjectCreate(null).
73
    options = isolate->factory()->NewJSObjectWithNullProto();
74
    // 3. Else
75
  } else {
76
    // 3. a. Let options be ? ToObject(options).
77 78 79 80 81
    ASSIGN_RETURN_ON_EXCEPTION(isolate, options,
                               Object::ToObject(isolate, input_options),
                               JSRelativeTimeFormat);
  }

82 83 84 85
  // 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.
86 87 88 89
  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();
90 91 92 93 94 95

  // 7. Let localeData be %RelativeTimeFormat%.[[LocaleData]].
  // 8. Let r be
  // ResolveLocale(%RelativeTimeFormat%.[[AvailableLocales]],
  //               requestedLocales, opt,
  //               %RelativeTimeFormat%.[[RelevantExtensionKeys]], localeData).
96 97
  Intl::ResolvedLocale r =
      Intl::ResolveLocale(isolate, JSRelativeTimeFormat::GetAvailableLocales(),
98
                          requested_locales, matcher, {"nu"});
99 100 101 102 103 104 105 106 107

  // 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",
108
  //                          «"long", "short", "narrow"», "long").
109 110 111 112 113 114
  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();
115

116
  // 13. Set relativeTimeFormat.[[Style]] to s.
117 118
  relative_time_format_holder->set_style(style_enum);

119
  // 14. Let numeric be ? GetOption(options, "numeric", "string",
120
  //                                «"always", "auto"», "always").
121 122 123 124 125
  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();
126

127
  // 15. Set relativeTimeFormat.[[Numeric]] to numeric.
128 129
  relative_time_format_holder->set_numeric(numeric_enum);

130
  icu::Locale icu_locale = r.icu_locale;
131 132
  UErrorCode status = U_ZERO_ERROR;

133
  // 19. Let relativeTimeFormat.[[NumberFormat]] be
134 135 136
  //     ? Construct(%NumberFormat%, « nfLocale, nfOptions »).
  icu::NumberFormat* number_format =
      icu::NumberFormat::createInstance(icu_locale, UNUM_DECIMAL, status);
137 138 139
  if (U_FAILURE(status)) {
    delete number_format;
    FATAL("Failed to create ICU number format, are ICU data files missing?");
140
  }
141 142
  CHECK_NOT_NULL(number_format);

143 144 145 146 147 148 149
  // 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);
150 151 152 153 154
  if (U_FAILURE(status)) {
    delete icu_formatter;
    FATAL(
        "Failed to create ICU relative date time formatter, are ICU data files "
        "missing?");
155
  }
156 157
  CHECK_NOT_NULL(icu_formatter);

158 159 160 161
  Handle<Managed<icu::RelativeDateTimeFormatter>> managed_formatter =
      Managed<icu::RelativeDateTimeFormatter>::FromRawPtr(isolate, 0,
                                                          icu_formatter);

162
  // 21. Set relativeTimeFormat.[[InitializedRelativeTimeFormat]] to true.
163
  relative_time_format_holder->set_icu_formatter(*managed_formatter);
164 165

  // 22. Return relativeTimeFormat.
166 167 168 169 170 171 172 173 174 175 176
  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(),
177
                        format_holder->StyleAsString(), NONE);
178
  JSObject::AddProperty(isolate, result, factory->numeric_string(),
179
                        format_holder->NumericAsString(), NONE);
180 181 182 183 184 185
  std::string locale_str(format_holder->locale()->ToCString().get());
  icu::Locale icu_locale = Intl::CreateICULocale(locale_str);
  std::string numbering_system = Intl::GetNumberingSystem(icu_locale);
  JSObject::AddProperty(
      isolate, result, factory->numberingSystem_string(),
      factory->NewStringFromAsciiChecked(numbering_system.c_str()), NONE);
186 187 188
  return result;
}

189
Handle<String> JSRelativeTimeFormat::StyleAsString() const {
190 191
  switch (style()) {
    case Style::LONG:
192
      return GetReadOnlyRoots().long_string_handle();
193
    case Style::SHORT:
194
      return GetReadOnlyRoots().short_string_handle();
195
    case Style::NARROW:
196
      return GetReadOnlyRoots().narrow_string_handle();
197 198 199 200 201
    case Style::COUNT:
      UNREACHABLE();
  }
}

202
Handle<String> JSRelativeTimeFormat::NumericAsString() const {
203 204
  switch (numeric()) {
    case Numeric::ALWAYS:
205
      return GetReadOnlyRoots().always_string_handle();
206
    case Numeric::AUTO:
207
      return GetReadOnlyRoots().auto_string_handle();
208 209 210 211 212
    case Numeric::COUNT:
      UNREACHABLE();
  }
}

213 214
namespace {

215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
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();
  }
}

239
MaybeHandle<JSArray> GenerateRelativeTimeFormatParts(
240
    Isolate* isolate, const icu::UnicodeString& formatted,
241 242
    const icu::UnicodeString& integer_part, URelativeDateTimeUnit unit_enum,
    double number, const icu::NumberFormat& nf) {
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
  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);
    }

273
    Handle<String> unit = UnitAsString(isolate, unit_enum);
274

275 276 277
    Handle<Object> number_obj = factory->NewNumber(number);
    Maybe<int> maybe_format_to_parts = JSNumberFormat::FormatToParts(
        isolate, array, index, nf, number_obj, unit);
278 279
    MAYBE_RETURN(maybe_format_to_parts, Handle<JSArray>());
    index = maybe_format_to_parts.FromJust();
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359

    // 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 =
360
      format_holder->icu_formatter()->raw();
361 362 363 364 365 366 367 368 369 370 371 372 373 374
  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;
375

376 377 378 379 380
  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);
381 382 383 384 385 386 387
  }

  if (U_FAILURE(status)) {
    THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kIcuError), Object);
  }

  if (to_parts) {
388
    icu::UnicodeString number_str;
389
    icu::FieldPosition pos;
390 391
    double abs_number = std::abs(number);
    formatter->getNumberFormat().format(abs_number, number_str, pos, status);
392 393 394 395 396 397
    if (U_FAILURE(status)) {
      THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kIcuError),
                      Object);
    }

    Handle<JSArray> elements;
398 399 400 401 402
    ASSIGN_RETURN_ON_EXCEPTION(isolate, elements,
                               GenerateRelativeTimeFormatParts(
                                   isolate, formatted, number_str, unit_enum,
                                   abs_number, formatter->getNumberFormat()),
                               Object);
403 404 405 406 407 408 409 410
    return elements;
  }

  return factory->NewStringFromTwoByte(Vector<const uint16_t>(
      reinterpret_cast<const uint16_t*>(formatted.getBuffer()),
      formatted.length()));
}

411
const std::set<std::string>& JSRelativeTimeFormat::GetAvailableLocales() {
412 413 414
  // Since RelativeTimeFormatter does not have a method to list all
  // available locales, work around by calling the DateFormat.
  return Intl::GetAvailableLocalesForDateFormat();
415 416
}

417 418
}  // namespace internal
}  // namespace v8