/ runtime / IntlDateTimeFormat.cpp
IntlDateTimeFormat.cpp
   1  /*
   2   * Copyright (C) 2015 Andy VanWagoner (andy@vanwagoner.family)
   3   * Copyright (C) 2016-2020 Apple Inc. All rights reserved.
   4   *
   5   * Redistribution and use in source and binary forms, with or without
   6   * modification, are permitted provided that the following conditions
   7   * are met:
   8   * 1. Redistributions of source code must retain the above copyright
   9   *    notice, this list of conditions and the following disclaimer.
  10   * 2. Redistributions in binary form must reproduce the above copyright
  11   *    notice, this list of conditions and the following disclaimer in the
  12   *    documentation and/or other materials provided with the distribution.
  13   *
  14   * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
  15   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  16   * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  17   * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
  18   * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  19   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  20   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  21   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  22   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  23   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
  24   * THE POSSIBILITY OF SUCH DAMAGE.
  25   */
  26  
  27  #include "config.h"
  28  #include "IntlDateTimeFormat.h"
  29  
  30  #include "IntlCache.h"
  31  #include "IntlObjectInlines.h"
  32  #include "JSBoundFunction.h"
  33  #include "JSCInlines.h"
  34  #include "ObjectConstructor.h"
  35  #include <unicode/ucal.h>
  36  #include <unicode/uenum.h>
  37  #include <wtf/Range.h>
  38  #include <wtf/text/StringBuilder.h>
  39  #include <wtf/unicode/icu/ICUHelpers.h>
  40  
  41  #if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
  42  #include <unicode/uformattedvalue.h>
  43  #ifdef U_HIDE_DRAFT_API
  44  #undef U_HIDE_DRAFT_API
  45  #endif
  46  #endif // HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
  47  #include <unicode/udateintervalformat.h>
  48  #if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
  49  #define U_HIDE_DRAFT_API 1
  50  #endif // HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
  51  
  52  namespace JSC {
  53  
  54  // We do not use ICUDeleter<udtitvfmt_close> because we do not want to include udateintervalformat.h in IntlDateTimeFormat.h.
  55  // udateintervalformat.h needs to be included with #undef U_HIDE_DRAFT_API, and we would like to minimize this effect in IntlDateTimeFormat.cpp.
  56  void UDateIntervalFormatDeleter::operator()(UDateIntervalFormat* formatter)
  57  {
  58      if (formatter)
  59          udtitvfmt_close(formatter);
  60  }
  61  
  62  static constexpr double minECMAScriptTime = -8.64E15;
  63  
  64  const ClassInfo IntlDateTimeFormat::s_info = { "Object", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(IntlDateTimeFormat) };
  65  
  66  namespace IntlDateTimeFormatInternal {
  67  static constexpr bool verbose = false;
  68  }
  69  
  70  IntlDateTimeFormat* IntlDateTimeFormat::create(VM& vm, Structure* structure)
  71  {
  72      IntlDateTimeFormat* format = new (NotNull, allocateCell<IntlDateTimeFormat>(vm.heap)) IntlDateTimeFormat(vm, structure);
  73      format->finishCreation(vm);
  74      return format;
  75  }
  76  
  77  Structure* IntlDateTimeFormat::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
  78  {
  79      return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
  80  }
  81  
  82  IntlDateTimeFormat::IntlDateTimeFormat(VM& vm, Structure* structure)
  83      : Base(vm, structure)
  84  {
  85  }
  86  
  87  void IntlDateTimeFormat::finishCreation(VM& vm)
  88  {
  89      Base::finishCreation(vm);
  90      ASSERT(inherits(vm, info()));
  91  }
  92  
  93  void IntlDateTimeFormat::visitChildren(JSCell* cell, SlotVisitor& visitor)
  94  {
  95      IntlDateTimeFormat* thisObject = jsCast<IntlDateTimeFormat*>(cell);
  96      ASSERT_GC_OBJECT_INHERITS(thisObject, info());
  97  
  98      Base::visitChildren(thisObject, visitor);
  99  
 100      visitor.append(thisObject->m_boundFormat);
 101  }
 102  
 103  void IntlDateTimeFormat::setBoundFormat(VM& vm, JSBoundFunction* format)
 104  {
 105      m_boundFormat.set(vm, this, format);
 106  }
 107  
 108  static String canonicalizeTimeZoneName(const String& timeZoneName)
 109  {
 110      // 6.4.1 IsValidTimeZoneName (timeZone)
 111      // The abstract operation returns true if timeZone, converted to upper case as described in 6.1, is equal to one of the Zone or Link names of the IANA Time Zone Database, converted to upper case as described in 6.1. It returns false otherwise.
 112      UErrorCode status = U_ZERO_ERROR;
 113      UEnumeration* timeZones = ucal_openTimeZones(&status);
 114      ASSERT(U_SUCCESS(status));
 115  
 116      String canonical;
 117      do {
 118          status = U_ZERO_ERROR;
 119          int32_t ianaTimeZoneLength;
 120          // Time zone names are represented as UChar[] in all related ICU APIs.
 121          const UChar* ianaTimeZone = uenum_unext(timeZones, &ianaTimeZoneLength, &status);
 122          ASSERT(U_SUCCESS(status));
 123  
 124          // End of enumeration.
 125          if (!ianaTimeZone)
 126              break;
 127  
 128          StringView ianaTimeZoneView(ianaTimeZone, ianaTimeZoneLength);
 129          if (!equalIgnoringASCIICase(timeZoneName, ianaTimeZoneView))
 130              continue;
 131  
 132          // Found a match, now canonicalize.
 133          // 6.4.2 CanonicalizeTimeZoneName (timeZone) (ECMA-402 2.0)
 134          // 1. Let ianaTimeZone be the Zone or Link name of the IANA Time Zone Database such that timeZone, converted to upper case as described in 6.1, is equal to ianaTimeZone, converted to upper case as described in 6.1.
 135          // 2. If ianaTimeZone is a Link name, then let ianaTimeZone be the corresponding Zone name as specified in the “backward” file of the IANA Time Zone Database.
 136  
 137          Vector<UChar, 32> buffer;
 138          auto status = callBufferProducingFunction(ucal_getCanonicalTimeZoneID, ianaTimeZone, ianaTimeZoneLength, buffer, nullptr);
 139          ASSERT_UNUSED(status, U_SUCCESS(status));
 140          canonical = String(buffer);
 141      } while (canonical.isNull());
 142      uenum_close(timeZones);
 143  
 144      // 3. If ianaTimeZone is "Etc/UTC" or "Etc/GMT", then return "UTC".
 145      if (isUTCEquivalent(canonical))
 146          return "UTC"_s;
 147  
 148      // 4. Return ianaTimeZone.
 149      return canonical;
 150  }
 151  
 152  Vector<String> IntlDateTimeFormat::localeData(const String& locale, RelevantExtensionKey key)
 153  {
 154      Vector<String> keyLocaleData;
 155      switch (key) {
 156      case RelevantExtensionKey::Ca: {
 157          UErrorCode status = U_ZERO_ERROR;
 158          UEnumeration* calendars = ucal_getKeywordValuesForLocale("calendar", locale.utf8().data(), false, &status);
 159          ASSERT(U_SUCCESS(status));
 160  
 161          int32_t nameLength;
 162          while (const char* availableName = uenum_next(calendars, &nameLength, &status)) {
 163              ASSERT(U_SUCCESS(status));
 164              String calendar = String(availableName, nameLength);
 165              keyLocaleData.append(calendar);
 166              // Ensure aliases used in language tag are allowed.
 167              if (calendar == "gregorian")
 168                  keyLocaleData.append("gregory"_s);
 169              else if (calendar == "islamic-civil")
 170                  keyLocaleData.append("islamicc"_s);
 171              else if (calendar == "ethiopic-amete-alem")
 172                  keyLocaleData.append("ethioaa"_s);
 173          }
 174          uenum_close(calendars);
 175          break;
 176      }
 177      case RelevantExtensionKey::Hc:
 178          // Null default so we know to use 'j' in pattern.
 179          keyLocaleData.append(String());
 180          keyLocaleData.append("h11"_s);
 181          keyLocaleData.append("h12"_s);
 182          keyLocaleData.append("h23"_s);
 183          keyLocaleData.append("h24"_s);
 184          break;
 185      case RelevantExtensionKey::Nu:
 186          keyLocaleData = numberingSystemsForLocale(locale);
 187          break;
 188      default:
 189          ASSERT_NOT_REACHED();
 190      }
 191      return keyLocaleData;
 192  }
 193  
 194  static JSObject* toDateTimeOptionsAnyDate(JSGlobalObject* globalObject, JSValue originalOptions)
 195  {
 196      // 12.1.1 ToDateTimeOptions abstract operation (ECMA-402 2.0)
 197      VM& vm = globalObject->vm();
 198      auto scope = DECLARE_THROW_SCOPE(vm);
 199  
 200      // 1. If options is undefined, then let options be null, else let options be ToObject(options).
 201      // 2. ReturnIfAbrupt(options).
 202      // 3. Let options be ObjectCreate(options).
 203      JSObject* options;
 204      if (originalOptions.isUndefined())
 205          options = constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure());
 206      else {
 207          JSObject* originalToObject = originalOptions.toObject(globalObject);
 208          RETURN_IF_EXCEPTION(scope, nullptr);
 209          options = constructEmptyObject(globalObject, originalToObject);
 210      }
 211  
 212      // 4. Let needDefaults be true.
 213      bool needDefaults = true;
 214  
 215      // 5. If required is "date" or "any",
 216      // Always "any".
 217  
 218      // a. For each of the property names "weekday", "year", "month", "day":
 219      // i. Let prop be the property name.
 220      // ii. Let value be Get(options, prop).
 221      // iii. ReturnIfAbrupt(value).
 222      // iv. If value is not undefined, then let needDefaults be false.
 223      JSValue weekday = options->get(globalObject, vm.propertyNames->weekday);
 224      RETURN_IF_EXCEPTION(scope, nullptr);
 225      if (!weekday.isUndefined())
 226          needDefaults = false;
 227  
 228      JSValue year = options->get(globalObject, vm.propertyNames->year);
 229      RETURN_IF_EXCEPTION(scope, nullptr);
 230      if (!year.isUndefined())
 231          needDefaults = false;
 232  
 233      JSValue month = options->get(globalObject, vm.propertyNames->month);
 234      RETURN_IF_EXCEPTION(scope, nullptr);
 235      if (!month.isUndefined())
 236          needDefaults = false;
 237  
 238      JSValue day = options->get(globalObject, vm.propertyNames->day);
 239      RETURN_IF_EXCEPTION(scope, nullptr);
 240      if (!day.isUndefined())
 241          needDefaults = false;
 242  
 243      // 6. If required is "time" or "any",
 244      // Always "any".
 245  
 246      // a. For each of the property names ""dayPeriod", hour", "minute", "second", "fractionalSecondDigits":
 247      // i. Let prop be the property name.
 248      // ii. Let value be Get(options, prop).
 249      // iii. ReturnIfAbrupt(value).
 250      // iv. If value is not undefined, then let needDefaults be false.
 251      if (Options::useIntlDateTimeFormatDayPeriod()) {
 252          JSValue dayPeriod = options->get(globalObject, vm.propertyNames->dayPeriod);
 253          RETURN_IF_EXCEPTION(scope, nullptr);
 254          if (!dayPeriod.isUndefined())
 255              needDefaults = false;
 256      }
 257  
 258      JSValue hour = options->get(globalObject, vm.propertyNames->hour);
 259      RETURN_IF_EXCEPTION(scope, nullptr);
 260      if (!hour.isUndefined())
 261          needDefaults = false;
 262  
 263      JSValue minute = options->get(globalObject, vm.propertyNames->minute);
 264      RETURN_IF_EXCEPTION(scope, nullptr);
 265      if (!minute.isUndefined())
 266          needDefaults = false;
 267  
 268      JSValue second = options->get(globalObject, vm.propertyNames->second);
 269      RETURN_IF_EXCEPTION(scope, nullptr);
 270      if (!second.isUndefined())
 271          needDefaults = false;
 272  
 273      JSValue fractionalSecondDigits = options->get(globalObject, vm.propertyNames->fractionalSecondDigits);
 274      RETURN_IF_EXCEPTION(scope, nullptr);
 275      if (!fractionalSecondDigits.isUndefined())
 276          needDefaults = false;
 277  
 278      JSValue dateStyle = options->get(globalObject, vm.propertyNames->dateStyle);
 279      RETURN_IF_EXCEPTION(scope, nullptr);
 280      JSValue timeStyle = options->get(globalObject, vm.propertyNames->timeStyle);
 281      RETURN_IF_EXCEPTION(scope, nullptr);
 282  
 283      if (!dateStyle.isUndefined() || !timeStyle.isUndefined())
 284          needDefaults = false;
 285  
 286      // 7. If needDefaults is true and defaults is either "date" or "all", then
 287      // Defaults is always "date".
 288      if (needDefaults) {
 289          // a. For each of the property names "year", "month", "day":
 290          // i. Let status be CreateDatePropertyOrThrow(options, prop, "numeric").
 291          // ii. ReturnIfAbrupt(status).
 292          JSString* numeric = jsNontrivialString(vm, "numeric"_s);
 293  
 294          options->putDirect(vm, vm.propertyNames->year, numeric);
 295          RETURN_IF_EXCEPTION(scope, nullptr);
 296  
 297          options->putDirect(vm, vm.propertyNames->month, numeric);
 298          RETURN_IF_EXCEPTION(scope, nullptr);
 299  
 300          options->putDirect(vm, vm.propertyNames->day, numeric);
 301          RETURN_IF_EXCEPTION(scope, nullptr);
 302      }
 303  
 304      // 8. If needDefaults is true and defaults is either "time" or "all", then
 305      // Defaults is always "date". Ignore this branch.
 306  
 307      // 9. Return options.
 308      return options;
 309  }
 310  
 311  void IntlDateTimeFormat::setFormatsFromPattern(const StringView& pattern)
 312  {
 313      // Get all symbols from the pattern, and set format fields accordingly.
 314      // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
 315      unsigned length = pattern.length();
 316      for (unsigned i = 0; i < length; ++i) {
 317          UChar currentCharacter = pattern[i];
 318          if (!isASCIIAlpha(currentCharacter))
 319              continue;
 320  
 321          unsigned count = 1;
 322          while (i + 1 < length && pattern[i + 1] == currentCharacter) {
 323              ++count;
 324              ++i;
 325          }
 326  
 327          switch (currentCharacter) {
 328          case 'G':
 329              if (count <= 3)
 330                  m_era = Era::Short;
 331              else if (count == 4)
 332                  m_era = Era::Long;
 333              else if (count == 5)
 334                  m_era = Era::Narrow;
 335              break;
 336          case 'y':
 337              if (count == 1)
 338                  m_year = Year::Numeric;
 339              else if (count == 2)
 340                  m_year = Year::TwoDigit;
 341              break;
 342          case 'M':
 343          case 'L':
 344              if (count == 1)
 345                  m_month = Month::Numeric;
 346              else if (count == 2)
 347                  m_month = Month::TwoDigit;
 348              else if (count == 3)
 349                  m_month = Month::Short;
 350              else if (count == 4)
 351                  m_month = Month::Long;
 352              else if (count == 5)
 353                  m_month = Month::Narrow;
 354              break;
 355          case 'E':
 356          case 'e':
 357          case 'c':
 358              if (count <= 3)
 359                  m_weekday = Weekday::Short;
 360              else if (count == 4)
 361                  m_weekday = Weekday::Long;
 362              else if (count == 5)
 363                  m_weekday = Weekday::Narrow;
 364              break;
 365          case 'd':
 366              if (count == 1)
 367                  m_day = Day::Numeric;
 368              else if (count == 2)
 369                  m_day = Day::TwoDigit;
 370              break;
 371          case 'a':
 372          case 'b':
 373          case 'B':
 374              if (count <= 3)
 375                  m_dayPeriod = DayPeriod::Short;
 376              else if (count == 4)
 377                  m_dayPeriod = DayPeriod::Long;
 378              else if (count == 5)
 379                  m_dayPeriod = DayPeriod::Narrow;
 380              break;
 381          case 'h':
 382          case 'H':
 383          case 'k':
 384          case 'K': {
 385              // Populate hourCycle from actually generated patterns. It is possible that locale or option is specifying hourCycle explicitly,
 386              // but the generated pattern does not include related part since the pattern does not include hours.
 387              // This is tested in test262/test/intl402/DateTimeFormat/prototype/resolvedOptions/hourCycle-dateStyle.js and our stress tests.
 388              // Example:
 389              //     new Intl.DateTimeFormat(`de-u-hc-h11`, {
 390              //         dateStyle: "full"
 391              //     }).resolvedOptions().hourCycle === undefined
 392              m_hourCycle = hourCycleFromSymbol(currentCharacter);
 393              if (count == 1)
 394                  m_hour = Hour::Numeric;
 395              else if (count == 2)
 396                  m_hour = Hour::TwoDigit;
 397              break;
 398          }
 399          case 'm':
 400              if (count == 1)
 401                  m_minute = Minute::Numeric;
 402              else if (count == 2)
 403                  m_minute = Minute::TwoDigit;
 404              break;
 405          case 's':
 406              if (count == 1)
 407                  m_second = Second::Numeric;
 408              else if (count == 2)
 409                  m_second = Second::TwoDigit;
 410              break;
 411          case 'z':
 412          case 'v':
 413          case 'V':
 414              if (count == 1)
 415                  m_timeZoneName = TimeZoneName::Short;
 416              else if (count == 4)
 417                  m_timeZoneName = TimeZoneName::Long;
 418              break;
 419          case 'S':
 420              m_fractionalSecondDigits = count;
 421              break;
 422          }
 423      }
 424  }
 425  
 426  IntlDateTimeFormat::HourCycle IntlDateTimeFormat::parseHourCycle(const String& hourCycle)
 427  {
 428      if (hourCycle == "h11"_s)
 429          return HourCycle::H11;
 430      if (hourCycle == "h12"_s)
 431          return HourCycle::H12;
 432      if (hourCycle == "h23"_s)
 433          return HourCycle::H23;
 434      if (hourCycle == "h24"_s)
 435          return HourCycle::H24;
 436      return HourCycle::None;
 437  }
 438  
 439  inline IntlDateTimeFormat::HourCycle IntlDateTimeFormat::hourCycleFromSymbol(UChar symbol)
 440  {
 441      switch (symbol) {
 442      case 'K':
 443          return HourCycle::H11;
 444      case 'h':
 445          return HourCycle::H12;
 446      case 'H':
 447          return HourCycle::H23;
 448      case 'k':
 449          return HourCycle::H24;
 450      }
 451      return HourCycle::None;
 452  }
 453  
 454  inline IntlDateTimeFormat::HourCycle IntlDateTimeFormat::hourCycleFromPattern(const Vector<UChar, 32>& pattern)
 455  {
 456      for (auto character : pattern) {
 457          switch (character) {
 458          case 'K':
 459          case 'h':
 460          case 'H':
 461          case 'k':
 462              return hourCycleFromSymbol(character);
 463          }
 464      }
 465      return HourCycle::None;
 466  }
 467  
 468  inline void IntlDateTimeFormat::replaceHourCycleInSkeleton(Vector<UChar, 32>& skeleton, bool isHour12)
 469  {
 470      UChar skeletonCharacter = 'H';
 471      if (isHour12)
 472          skeletonCharacter = 'h';
 473      for (auto& character : skeleton) {
 474          switch (character) {
 475          case 'h':
 476          case 'H':
 477          case 'j':
 478              character = skeletonCharacter;
 479              break;
 480          }
 481      }
 482  }
 483  
 484  inline void IntlDateTimeFormat::replaceHourCycleInPattern(Vector<UChar, 32>& pattern, HourCycle hourCycle)
 485  {
 486      UChar hourFromHourCycle = 'H';
 487      switch (hourCycle) {
 488      case HourCycle::H11:
 489          hourFromHourCycle = 'K';
 490          break;
 491      case HourCycle::H12:
 492          hourFromHourCycle = 'h';
 493          break;
 494      case HourCycle::H23:
 495          hourFromHourCycle = 'H';
 496          break;
 497      case HourCycle::H24:
 498          hourFromHourCycle = 'k';
 499          break;
 500      case HourCycle::None:
 501          return;
 502      }
 503  
 504      for (auto& character : pattern) {
 505          switch (character) {
 506          case 'K':
 507          case 'h':
 508          case 'H':
 509          case 'k':
 510              character = hourFromHourCycle;
 511              break;
 512          }
 513      }
 514  }
 515  
 516  // https://tc39.github.io/ecma402/#sec-initializedatetimeformat
 517  void IntlDateTimeFormat::initializeDateTimeFormat(JSGlobalObject* globalObject, JSValue locales, JSValue originalOptions)
 518  {
 519      VM& vm = globalObject->vm();
 520      auto scope = DECLARE_THROW_SCOPE(vm);
 521  
 522      Vector<String> requestedLocales = canonicalizeLocaleList(globalObject, locales);
 523      RETURN_IF_EXCEPTION(scope, void());
 524  
 525      JSObject* options = toDateTimeOptionsAnyDate(globalObject, originalOptions);
 526      RETURN_IF_EXCEPTION(scope, void());
 527  
 528      ResolveLocaleOptions localeOptions;
 529  
 530      LocaleMatcher localeMatcher = intlOption<LocaleMatcher>(globalObject, options, vm.propertyNames->localeMatcher, { { "lookup"_s, LocaleMatcher::Lookup }, { "best fit"_s, LocaleMatcher::BestFit } }, "localeMatcher must be either \"lookup\" or \"best fit\""_s, LocaleMatcher::BestFit);
 531      RETURN_IF_EXCEPTION(scope, void());
 532  
 533      String calendar = intlStringOption(globalObject, options, vm.propertyNames->calendar, { }, nullptr, nullptr);
 534      RETURN_IF_EXCEPTION(scope, void());
 535      if (!calendar.isNull()) {
 536          if (!isUnicodeLocaleIdentifierType(calendar)) {
 537              throwRangeError(globalObject, scope, "calendar is not a well-formed calendar value"_s);
 538              return;
 539          }
 540          localeOptions[static_cast<unsigned>(RelevantExtensionKey::Ca)] = calendar;
 541      }
 542  
 543      String numberingSystem = intlStringOption(globalObject, options, vm.propertyNames->numberingSystem, { }, nullptr, nullptr);
 544      RETURN_IF_EXCEPTION(scope, void());
 545      if (!numberingSystem.isNull()) {
 546          if (!isUnicodeLocaleIdentifierType(numberingSystem)) {
 547              throwRangeError(globalObject, scope, "numberingSystem is not a well-formed numbering system value"_s);
 548              return;
 549          }
 550          localeOptions[static_cast<unsigned>(RelevantExtensionKey::Nu)] = numberingSystem;
 551      }
 552  
 553      TriState hour12 = intlBooleanOption(globalObject, options, vm.propertyNames->hour12);
 554      RETURN_IF_EXCEPTION(scope, void());
 555  
 556      HourCycle hourCycle = intlOption<HourCycle>(globalObject, options, vm.propertyNames->hourCycle, { { "h11"_s, HourCycle::H11 }, { "h12"_s, HourCycle::H12 }, { "h23"_s, HourCycle::H23 }, { "h24"_s, HourCycle::H24 } }, "hourCycle must be \"h11\", \"h12\", \"h23\", or \"h24\""_s, HourCycle::None);
 557      RETURN_IF_EXCEPTION(scope, void());
 558      if (hour12 == TriState::Indeterminate) {
 559          if (hourCycle != HourCycle::None)
 560              localeOptions[static_cast<unsigned>(RelevantExtensionKey::Hc)] = String(hourCycleString(hourCycle));
 561      } else {
 562          // If there is hour12, hourCycle is ignored.
 563          // We are setting null String explicitly here (localeOptions' entries are Optional<String>). This leads us to use HourCycle::None later.
 564          localeOptions[static_cast<unsigned>(RelevantExtensionKey::Hc)] = String();
 565      }
 566  
 567      const HashSet<String>& availableLocales = intlDateTimeFormatAvailableLocales();
 568      auto resolved = resolveLocale(globalObject, availableLocales, requestedLocales, localeMatcher, localeOptions, { RelevantExtensionKey::Ca, RelevantExtensionKey::Hc, RelevantExtensionKey::Nu }, localeData);
 569  
 570      m_locale = resolved.locale;
 571      if (m_locale.isEmpty()) {
 572          throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat due to invalid locale"_s);
 573          return;
 574      }
 575  
 576      m_calendar = resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Ca)];
 577      if (m_calendar == "gregorian")
 578          m_calendar = "gregory"_s;
 579      else if (m_calendar == "islamicc")
 580          m_calendar = "islamic-civil"_s;
 581      else if (m_calendar == "ethioaa")
 582          m_calendar = "ethiopic-amete-alem"_s;
 583  
 584      hourCycle = parseHourCycle(resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Hc)]);
 585      m_numberingSystem = resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Nu)];
 586      m_dataLocale = resolved.dataLocale;
 587      CString dataLocaleWithExtensions = makeString(m_dataLocale, "-u-ca-", m_calendar, "-nu-", m_numberingSystem).utf8();
 588  
 589      JSValue tzValue = options->get(globalObject, vm.propertyNames->timeZone);
 590      RETURN_IF_EXCEPTION(scope, void());
 591      String tz;
 592      if (!tzValue.isUndefined()) {
 593          String originalTz = tzValue.toWTFString(globalObject);
 594          RETURN_IF_EXCEPTION(scope, void());
 595          tz = canonicalizeTimeZoneName(originalTz);
 596          if (tz.isNull()) {
 597              throwRangeError(globalObject, scope, "invalid time zone: " + originalTz);
 598              return;
 599          }
 600      } else
 601          tz = vm.dateCache.defaultTimeZone();
 602      m_timeZone = tz;
 603  
 604      StringBuilder skeletonBuilder;
 605  
 606      Weekday weekday = intlOption<Weekday>(globalObject, options, vm.propertyNames->weekday, { { "narrow"_s, Weekday::Narrow }, { "short"_s, Weekday::Short }, { "long"_s, Weekday::Long } }, "weekday must be \"narrow\", \"short\", or \"long\""_s, Weekday::None);
 607      RETURN_IF_EXCEPTION(scope, void());
 608      switch (weekday) {
 609      case Weekday::Narrow:
 610          skeletonBuilder.appendLiteral("EEEEE");
 611          break;
 612      case Weekday::Short:
 613          skeletonBuilder.appendLiteral("EEE");
 614          break;
 615      case Weekday::Long:
 616          skeletonBuilder.appendLiteral("EEEE");
 617          break;
 618      case Weekday::None:
 619          break;
 620      }
 621  
 622      Era era = intlOption<Era>(globalObject, options, vm.propertyNames->era, { { "narrow"_s, Era::Narrow }, { "short"_s, Era::Short }, { "long"_s, Era::Long } }, "era must be \"narrow\", \"short\", or \"long\""_s, Era::None);
 623      RETURN_IF_EXCEPTION(scope, void());
 624      switch (era) {
 625      case Era::Narrow:
 626          skeletonBuilder.appendLiteral("GGGGG");
 627          break;
 628      case Era::Short:
 629          skeletonBuilder.appendLiteral("GGG");
 630          break;
 631      case Era::Long:
 632          skeletonBuilder.appendLiteral("GGGG");
 633          break;
 634      case Era::None:
 635          break;
 636      }
 637  
 638      Year year = intlOption<Year>(globalObject, options, vm.propertyNames->year, { { "2-digit"_s, Year::TwoDigit }, { "numeric"_s, Year::Numeric } }, "year must be \"2-digit\" or \"numeric\""_s, Year::None);
 639      RETURN_IF_EXCEPTION(scope, void());
 640      switch (year) {
 641      case Year::TwoDigit:
 642          skeletonBuilder.appendLiteral("yy");
 643          break;
 644      case Year::Numeric:
 645          skeletonBuilder.append('y');
 646          break;
 647      case Year::None:
 648          break;
 649      }
 650  
 651      Month month = intlOption<Month>(globalObject, options, vm.propertyNames->month, { { "2-digit"_s, Month::TwoDigit }, { "numeric"_s, Month::Numeric }, { "narrow"_s, Month::Narrow }, { "short"_s, Month::Short }, { "long"_s, Month::Long } }, "month must be \"2-digit\", \"numeric\", \"narrow\", \"short\", or \"long\""_s, Month::None);
 652      RETURN_IF_EXCEPTION(scope, void());
 653      switch (month) {
 654      case Month::TwoDigit:
 655          skeletonBuilder.appendLiteral("MM");
 656          break;
 657      case Month::Numeric:
 658          skeletonBuilder.append('M');
 659          break;
 660      case Month::Narrow:
 661          skeletonBuilder.appendLiteral("MMMMM");
 662          break;
 663      case Month::Short:
 664          skeletonBuilder.appendLiteral("MMM");
 665          break;
 666      case Month::Long:
 667          skeletonBuilder.appendLiteral("MMMM");
 668          break;
 669      case Month::None:
 670          break;
 671      }
 672  
 673      Day day = intlOption<Day>(globalObject, options, vm.propertyNames->day, { { "2-digit"_s, Day::TwoDigit }, { "numeric"_s, Day::Numeric } }, "day must be \"2-digit\" or \"numeric\""_s, Day::None);
 674      RETURN_IF_EXCEPTION(scope, void());
 675      switch (day) {
 676      case Day::TwoDigit:
 677          skeletonBuilder.appendLiteral("dd");
 678          break;
 679      case Day::Numeric:
 680          skeletonBuilder.append('d');
 681          break;
 682      case Day::None:
 683          break;
 684      }
 685  
 686      DayPeriod dayPeriod = DayPeriod::None;
 687      if (Options::useIntlDateTimeFormatDayPeriod()) {
 688          dayPeriod = intlOption<DayPeriod>(globalObject, options, vm.propertyNames->dayPeriod, { { "narrow"_s, DayPeriod::Narrow }, { "short"_s, DayPeriod::Short }, { "long"_s, DayPeriod::Long } }, "dayPeriod must be \"narrow\", \"short\", or \"long\""_s, DayPeriod::None);
 689          RETURN_IF_EXCEPTION(scope, void());
 690      }
 691  
 692      Hour hour = intlOption<Hour>(globalObject, options, vm.propertyNames->hour, { { "2-digit"_s, Hour::TwoDigit }, { "numeric"_s, Hour::Numeric } }, "hour must be \"2-digit\" or \"numeric\""_s, Hour::None);
 693      RETURN_IF_EXCEPTION(scope, void());
 694      {
 695          // Specifically, this hour-cycle / hour12 behavior is slightly different from the spec.
 696          // But the spec behavior is known to cause surprising behaviors, and the spec change is ongoing.
 697          // We implement SpiderMonkey's behavior.
 698          //
 699          //     > No option present: "j"
 700          //     > hour12 = true: "h"
 701          //     > hour12 = false: "H"
 702          //     > hourCycle = h11: "h", plus modifying the resolved pattern to use the hour symbol "K".
 703          //     > hourCycle = h12: "h", plus modifying the resolved pattern to use the hour symbol "h".
 704          //     > hourCycle = h23: "H", plus modifying the resolved pattern to use the hour symbol "H".
 705          //     > hourCycle = h24: "H", plus modifying the resolved pattern to use the hour symbol "k".
 706          //
 707          UChar skeletonCharacter = 'j';
 708          if (hour12 == TriState::Indeterminate) {
 709              switch (hourCycle) {
 710              case HourCycle::None:
 711                  break;
 712              case HourCycle::H11:
 713              case HourCycle::H12:
 714                  skeletonCharacter = 'h';
 715                  break;
 716              case HourCycle::H23:
 717              case HourCycle::H24:
 718                  skeletonCharacter = 'H';
 719                  break;
 720              }
 721          } else {
 722              if (hour12 == TriState::True)
 723                  skeletonCharacter = 'h';
 724              else
 725                  skeletonCharacter = 'H';
 726          }
 727  
 728          switch (hour) {
 729          case Hour::TwoDigit:
 730              skeletonBuilder.append(skeletonCharacter);
 731              skeletonBuilder.append(skeletonCharacter);
 732              break;
 733          case Hour::Numeric:
 734              skeletonBuilder.append(skeletonCharacter);
 735              break;
 736          case Hour::None:
 737              break;
 738          }
 739      }
 740  
 741      if (Options::useIntlDateTimeFormatDayPeriod()) {
 742          // dayPeriod must be set after setting hour.
 743          // https://unicode-org.atlassian.net/browse/ICU-20731
 744          switch (dayPeriod) {
 745          case DayPeriod::Narrow:
 746              skeletonBuilder.appendLiteral("BBBBB");
 747              break;
 748          case DayPeriod::Short:
 749              skeletonBuilder.append('B');
 750              break;
 751          case DayPeriod::Long:
 752              skeletonBuilder.appendLiteral("BBBB");
 753              break;
 754          case DayPeriod::None:
 755              break;
 756          }
 757      }
 758  
 759      Minute minute = intlOption<Minute>(globalObject, options, vm.propertyNames->minute, { { "2-digit"_s, Minute::TwoDigit }, { "numeric"_s, Minute::Numeric } }, "minute must be \"2-digit\" or \"numeric\""_s, Minute::None);
 760      RETURN_IF_EXCEPTION(scope, void());
 761      switch (minute) {
 762      case Minute::TwoDigit:
 763          skeletonBuilder.appendLiteral("mm");
 764          break;
 765      case Minute::Numeric:
 766          skeletonBuilder.append('m');
 767          break;
 768      case Minute::None:
 769          break;
 770      }
 771  
 772      Second second = intlOption<Second>(globalObject, options, vm.propertyNames->second, { { "2-digit"_s, Second::TwoDigit }, { "numeric"_s, Second::Numeric } }, "second must be \"2-digit\" or \"numeric\""_s, Second::None);
 773      RETURN_IF_EXCEPTION(scope, void());
 774      switch (second) {
 775      case Second::TwoDigit:
 776          skeletonBuilder.appendLiteral("ss");
 777          break;
 778      case Second::Numeric:
 779          skeletonBuilder.append('s');
 780          break;
 781      case Second::None:
 782          break;
 783      }
 784  
 785      unsigned fractionalSecondDigits = intlNumberOption(globalObject, options, vm.propertyNames->fractionalSecondDigits, 1, 3, 0);
 786      RETURN_IF_EXCEPTION(scope, void());
 787      for (unsigned i = 0; i < fractionalSecondDigits; ++i)
 788          skeletonBuilder.append('S');
 789  
 790      TimeZoneName timeZoneName = intlOption<TimeZoneName>(globalObject, options, vm.propertyNames->timeZoneName, { { "short"_s, TimeZoneName::Short }, { "long"_s, TimeZoneName::Long } }, "timeZoneName must be \"short\" or \"long\""_s, TimeZoneName::None);
 791      RETURN_IF_EXCEPTION(scope, void());
 792      switch (timeZoneName) {
 793      case TimeZoneName::Short:
 794          skeletonBuilder.append('z');
 795          break;
 796      case TimeZoneName::Long:
 797          skeletonBuilder.appendLiteral("zzzz");
 798          break;
 799      case TimeZoneName::None:
 800          break;
 801      }
 802  
 803      intlStringOption(globalObject, options, vm.propertyNames->formatMatcher, { "basic", "best fit" }, "formatMatcher must be either \"basic\" or \"best fit\"", "best fit");
 804      RETURN_IF_EXCEPTION(scope, void());
 805  
 806      m_dateStyle = intlOption<DateTimeStyle>(globalObject, options, vm.propertyNames->dateStyle, { { "full"_s, DateTimeStyle::Full }, { "long"_s, DateTimeStyle::Long }, { "medium"_s, DateTimeStyle::Medium }, { "short"_s, DateTimeStyle::Short } }, "dateStyle must be \"full\", \"long\", \"medium\", or \"short\""_s, DateTimeStyle::None);
 807      RETURN_IF_EXCEPTION(scope, void());
 808  
 809      m_timeStyle = intlOption<DateTimeStyle>(globalObject, options, vm.propertyNames->timeStyle, { { "full"_s, DateTimeStyle::Full }, { "long"_s, DateTimeStyle::Long }, { "medium"_s, DateTimeStyle::Medium }, { "short"_s, DateTimeStyle::Short } }, "timeStyle must be \"full\", \"long\", \"medium\", or \"short\""_s, DateTimeStyle::None);
 810      RETURN_IF_EXCEPTION(scope, void());
 811  
 812      Vector<UChar, 32> patternBuffer;
 813      if (m_dateStyle != DateTimeStyle::None || m_timeStyle != DateTimeStyle::None) {
 814          // 30. For each row in Table 1, except the header row, do
 815          //     i. Let prop be the name given in the Property column of the row.
 816          //     ii. Let p be opt.[[<prop>]].
 817          //     iii. If p is not undefined, then
 818          //         1. Throw a TypeError exception.
 819          if (weekday != Weekday::None || era != Era::None || year != Year::None || month != Month::None || day != Day::None || dayPeriod != DayPeriod::None || hour != Hour::None || minute != Minute::None || second != Second::None || fractionalSecondDigits != 0 || timeZoneName != TimeZoneName::None) {
 820              throwTypeError(globalObject, scope, "dateStyle and timeStyle may not be used with other DateTimeFormat options"_s);
 821              return;
 822          }
 823  
 824          auto parseUDateFormatStyle = [](DateTimeStyle style) {
 825              switch (style) {
 826              case DateTimeStyle::Full:
 827                  return UDAT_FULL;
 828              case DateTimeStyle::Long:
 829                  return UDAT_LONG;
 830              case DateTimeStyle::Medium:
 831                  return UDAT_MEDIUM;
 832              case DateTimeStyle::Short:
 833                  return UDAT_SHORT;
 834              case DateTimeStyle::None:
 835                  return UDAT_NONE;
 836              }
 837              return UDAT_NONE;
 838          };
 839  
 840          // We cannot use this UDateFormat directly yet because we need to enforce specified hourCycle.
 841          // First, we create UDateFormat via dateStyle and timeStyle. And then convert it to pattern string.
 842          // After updating this pattern string with hourCycle, we create a final UDateFormat with the updated pattern string.
 843          UErrorCode status = U_ZERO_ERROR;
 844          StringView timeZoneView(m_timeZone);
 845          auto dateFormatFromStyle = std::unique_ptr<UDateFormat, UDateFormatDeleter>(udat_open(parseUDateFormatStyle(m_timeStyle), parseUDateFormatStyle(m_dateStyle), dataLocaleWithExtensions.data(), timeZoneView.upconvertedCharacters(), timeZoneView.length(), nullptr, -1, &status));
 846          if (U_FAILURE(status)) {
 847              throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
 848              return;
 849          }
 850          constexpr bool localized = false; // Aligned with how ICU SimpleDateTimeFormat::format works. We do not need to translate this to localized pattern.
 851          status = callBufferProducingFunction(udat_toPattern, dateFormatFromStyle.get(), localized, patternBuffer);
 852          if (U_FAILURE(status)) {
 853              throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
 854              return;
 855          }
 856  
 857          // It is possible that timeStyle includes dayPeriod, which is sensitive to hour-cycle.
 858          // If dayPeriod is included, just replacing hour based on hourCycle / hour12 produces strange results.
 859          // Let's consider about the example. The formatted result looks like "02:12:47 PM Coordinated Universal Time"
 860          // If we simply replace 02 to 14, this becomes "14:12:47 PM Coordinated Universal Time", this looks strange since "PM" is unnecessary!
 861          //
 862          // If the generated pattern's hour12 does not match against the option's one, we retrieve skeleton from the pattern, enforcing hour-cycle,
 863          // and re-generating the best pattern from the modified skeleton. ICU will look into the generated skeleton, and pick the best format for the request.
 864          // We do not care about h11 vs. h12 and h23 vs. h24 difference here since this will be later adjusted by replaceHourCycleInPattern.
 865          //
 866          // test262/test/intl402/DateTimeFormat/prototype/format/timedatestyle-en.js includes the test for this behavior.
 867          if (m_timeStyle != DateTimeStyle::None && (hourCycle != HourCycle::None || hour12 != TriState::Indeterminate)) {
 868              auto isHour12 = [](HourCycle hourCycle) {
 869                  return hourCycle == HourCycle::H11 || hourCycle == HourCycle::H12;
 870              };
 871              bool specifiedHour12 = false;
 872              // If hour12 is specified, we prefer it and ignore hourCycle.
 873              if (hour12 != TriState::Indeterminate)
 874                  specifiedHour12 = hour12 == TriState::True;
 875              else
 876                  specifiedHour12 = isHour12(hourCycle);
 877              HourCycle extractedHourCycle = hourCycleFromPattern(patternBuffer);
 878              if (extractedHourCycle != HourCycle::None && isHour12(extractedHourCycle) != specifiedHour12) {
 879                  Vector<UChar, 32> skeleton;
 880                  auto status = callBufferProducingFunction(udatpg_getSkeleton, nullptr, patternBuffer.data(), patternBuffer.size(), skeleton);
 881                  if (U_FAILURE(status)) {
 882                      throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
 883                      return;
 884                  }
 885                  replaceHourCycleInSkeleton(skeleton, specifiedHour12);
 886                  dataLogLnIf(IntlDateTimeFormatInternal::verbose, "replaced:(", StringView(skeleton.data(), skeleton.size()), ")");
 887  
 888                  patternBuffer = vm.intlCache().getBestDateTimePattern(dataLocaleWithExtensions, skeleton.data(), skeleton.size(), status);
 889                  if (U_FAILURE(status)) {
 890                      throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
 891                      return;
 892                  }
 893              }
 894          }
 895      } else {
 896          UErrorCode status = U_ZERO_ERROR;
 897          String skeleton = skeletonBuilder.toString();
 898          patternBuffer = vm.intlCache().getBestDateTimePattern(dataLocaleWithExtensions, StringView(skeleton).upconvertedCharacters().get(), skeleton.length(), status);
 899          if (U_FAILURE(status)) {
 900              throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
 901              return;
 902          }
 903      }
 904  
 905      // After generating pattern from skeleton, we need to change h11 vs. h12 and h23 vs. h24 if hourCycle is specified.
 906      if (hourCycle != HourCycle::None)
 907          replaceHourCycleInPattern(patternBuffer, hourCycle);
 908  
 909      StringView pattern(patternBuffer.data(), patternBuffer.size());
 910      setFormatsFromPattern(pattern);
 911  
 912      dataLogLnIf(IntlDateTimeFormatInternal::verbose, "locale:(", m_locale, "),dataLocale:(", dataLocaleWithExtensions, "),pattern:(", pattern, ")");
 913  
 914      UErrorCode status = U_ZERO_ERROR;
 915      StringView timeZoneView(m_timeZone);
 916      m_dateFormat = std::unique_ptr<UDateFormat, UDateFormatDeleter>(udat_open(UDAT_PATTERN, UDAT_PATTERN, dataLocaleWithExtensions.data(), timeZoneView.upconvertedCharacters(), timeZoneView.length(), pattern.upconvertedCharacters(), pattern.length(), &status));
 917      if (U_FAILURE(status)) {
 918          throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
 919          return;
 920      }
 921  
 922      // Gregorian calendar should be used from the beginning of ECMAScript time.
 923      // Failure here means unsupported calendar, and can safely be ignored.
 924      UCalendar* cal = const_cast<UCalendar*>(udat_getCalendar(m_dateFormat.get()));
 925      ucal_setGregorianChange(cal, minECMAScriptTime, &status);
 926  }
 927  
 928  ASCIILiteral IntlDateTimeFormat::hourCycleString(HourCycle hourCycle)
 929  {
 930      switch (hourCycle) {
 931      case HourCycle::H11:
 932          return "h11"_s;
 933      case HourCycle::H12:
 934          return "h12"_s;
 935      case HourCycle::H23:
 936          return "h23"_s;
 937      case HourCycle::H24:
 938          return "h24"_s;
 939      case HourCycle::None:
 940          ASSERT_NOT_REACHED();
 941          return ASCIILiteral::null();
 942      }
 943      ASSERT_NOT_REACHED();
 944      return ASCIILiteral::null();
 945  }
 946  
 947  ASCIILiteral IntlDateTimeFormat::weekdayString(Weekday weekday)
 948  {
 949      switch (weekday) {
 950      case Weekday::Narrow:
 951          return "narrow"_s;
 952      case Weekday::Short:
 953          return "short"_s;
 954      case Weekday::Long:
 955          return "long"_s;
 956      case Weekday::None:
 957          ASSERT_NOT_REACHED();
 958          return ASCIILiteral::null();
 959      }
 960      ASSERT_NOT_REACHED();
 961      return ASCIILiteral::null();
 962  }
 963  
 964  ASCIILiteral IntlDateTimeFormat::eraString(Era era)
 965  {
 966      switch (era) {
 967      case Era::Narrow:
 968          return "narrow"_s;
 969      case Era::Short:
 970          return "short"_s;
 971      case Era::Long:
 972          return "long"_s;
 973      case Era::None:
 974          ASSERT_NOT_REACHED();
 975          return ASCIILiteral::null();
 976      }
 977      ASSERT_NOT_REACHED();
 978      return ASCIILiteral::null();
 979  }
 980  
 981  ASCIILiteral IntlDateTimeFormat::yearString(Year year)
 982  {
 983      switch (year) {
 984      case Year::TwoDigit:
 985          return "2-digit"_s;
 986      case Year::Numeric:
 987          return "numeric"_s;
 988      case Year::None:
 989          ASSERT_NOT_REACHED();
 990          return ASCIILiteral::null();
 991      }
 992      ASSERT_NOT_REACHED();
 993      return ASCIILiteral::null();
 994  }
 995  
 996  ASCIILiteral IntlDateTimeFormat::monthString(Month month)
 997  {
 998      switch (month) {
 999      case Month::TwoDigit:
1000          return "2-digit"_s;
1001      case Month::Numeric:
1002          return "numeric"_s;
1003      case Month::Narrow:
1004          return "narrow"_s;
1005      case Month::Short:
1006          return "short"_s;
1007      case Month::Long:
1008          return "long"_s;
1009      case Month::None:
1010          ASSERT_NOT_REACHED();
1011          return ASCIILiteral::null();
1012      }
1013      ASSERT_NOT_REACHED();
1014      return ASCIILiteral::null();
1015  }
1016  
1017  ASCIILiteral IntlDateTimeFormat::dayString(Day day)
1018  {
1019      switch (day) {
1020      case Day::TwoDigit:
1021          return "2-digit"_s;
1022      case Day::Numeric:
1023          return "numeric"_s;
1024      case Day::None:
1025          ASSERT_NOT_REACHED();
1026          return ASCIILiteral::null();
1027      }
1028      ASSERT_NOT_REACHED();
1029      return ASCIILiteral::null();
1030  }
1031  
1032  ASCIILiteral IntlDateTimeFormat::dayPeriodString(DayPeriod dayPeriod)
1033  {
1034      switch (dayPeriod) {
1035      case DayPeriod::Narrow:
1036          return "narrow"_s;
1037      case DayPeriod::Short:
1038          return "short"_s;
1039      case DayPeriod::Long:
1040          return "long"_s;
1041      case DayPeriod::None:
1042          ASSERT_NOT_REACHED();
1043          return ASCIILiteral::null();
1044      }
1045      ASSERT_NOT_REACHED();
1046      return ASCIILiteral::null();
1047  }
1048  
1049  ASCIILiteral IntlDateTimeFormat::hourString(Hour hour)
1050  {
1051      switch (hour) {
1052      case Hour::TwoDigit:
1053          return "2-digit"_s;
1054      case Hour::Numeric:
1055          return "numeric"_s;
1056      case Hour::None:
1057          ASSERT_NOT_REACHED();
1058          return ASCIILiteral::null();
1059      }
1060      ASSERT_NOT_REACHED();
1061      return ASCIILiteral::null();
1062  }
1063  
1064  ASCIILiteral IntlDateTimeFormat::minuteString(Minute minute)
1065  {
1066      switch (minute) {
1067      case Minute::TwoDigit:
1068          return "2-digit"_s;
1069      case Minute::Numeric:
1070          return "numeric"_s;
1071      case Minute::None:
1072          ASSERT_NOT_REACHED();
1073          return ASCIILiteral::null();
1074      }
1075      ASSERT_NOT_REACHED();
1076      return ASCIILiteral::null();
1077  }
1078  
1079  ASCIILiteral IntlDateTimeFormat::secondString(Second second)
1080  {
1081      switch (second) {
1082      case Second::TwoDigit:
1083          return "2-digit"_s;
1084      case Second::Numeric:
1085          return "numeric"_s;
1086      case Second::None:
1087          ASSERT_NOT_REACHED();
1088          return ASCIILiteral::null();
1089      }
1090      ASSERT_NOT_REACHED();
1091      return ASCIILiteral::null();
1092  }
1093  
1094  ASCIILiteral IntlDateTimeFormat::timeZoneNameString(TimeZoneName timeZoneName)
1095  {
1096      switch (timeZoneName) {
1097      case TimeZoneName::Short:
1098          return "short"_s;
1099      case TimeZoneName::Long:
1100          return "long"_s;
1101      case TimeZoneName::None:
1102          ASSERT_NOT_REACHED();
1103          return ASCIILiteral::null();
1104      }
1105      ASSERT_NOT_REACHED();
1106      return ASCIILiteral::null();
1107  }
1108  
1109  ASCIILiteral IntlDateTimeFormat::formatStyleString(DateTimeStyle style)
1110  {
1111      switch (style) {
1112      case DateTimeStyle::Full:
1113          return "full"_s;
1114      case DateTimeStyle::Long:
1115          return "long"_s;
1116      case DateTimeStyle::Medium:
1117          return "medium"_s;
1118      case DateTimeStyle::Short:
1119          return "short"_s;
1120      case DateTimeStyle::None:
1121          ASSERT_NOT_REACHED();
1122          return ASCIILiteral::null();
1123      }
1124      ASSERT_NOT_REACHED();
1125      return ASCIILiteral::null();
1126  }
1127  
1128  // https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions
1129  JSObject* IntlDateTimeFormat::resolvedOptions(JSGlobalObject* globalObject) const
1130  {
1131      VM& vm = globalObject->vm();
1132  
1133      JSObject* options = constructEmptyObject(globalObject);
1134      options->putDirect(vm, vm.propertyNames->locale, jsNontrivialString(vm, m_locale));
1135      options->putDirect(vm, vm.propertyNames->calendar, jsNontrivialString(vm, m_calendar));
1136      options->putDirect(vm, vm.propertyNames->numberingSystem, jsNontrivialString(vm, m_numberingSystem));
1137      options->putDirect(vm, vm.propertyNames->timeZone, jsNontrivialString(vm, m_timeZone));
1138  
1139      if (m_hourCycle != HourCycle::None) {
1140          options->putDirect(vm, vm.propertyNames->hourCycle, jsNontrivialString(vm, hourCycleString(m_hourCycle)));
1141          options->putDirect(vm, vm.propertyNames->hour12, jsBoolean(m_hourCycle == HourCycle::H11 || m_hourCycle == HourCycle::H12));
1142      }
1143  
1144      if (m_weekday != Weekday::None)
1145          options->putDirect(vm, vm.propertyNames->weekday, jsNontrivialString(vm, weekdayString(m_weekday)));
1146  
1147      if (m_era != Era::None)
1148          options->putDirect(vm, vm.propertyNames->era, jsNontrivialString(vm, eraString(m_era)));
1149  
1150      if (m_year != Year::None)
1151          options->putDirect(vm, vm.propertyNames->year, jsNontrivialString(vm, yearString(m_year)));
1152  
1153      if (m_month != Month::None)
1154          options->putDirect(vm, vm.propertyNames->month, jsNontrivialString(vm, monthString(m_month)));
1155  
1156      if (m_day != Day::None)
1157          options->putDirect(vm, vm.propertyNames->day, jsNontrivialString(vm, dayString(m_day)));
1158  
1159      if (Options::useIntlDateTimeFormatDayPeriod()) {
1160          if (m_dayPeriod != DayPeriod::None)
1161              options->putDirect(vm, vm.propertyNames->dayPeriod, jsNontrivialString(vm, dayPeriodString(m_dayPeriod)));
1162      }
1163  
1164      if (m_hour != Hour::None)
1165          options->putDirect(vm, vm.propertyNames->hour, jsNontrivialString(vm, hourString(m_hour)));
1166  
1167      if (m_minute != Minute::None)
1168          options->putDirect(vm, vm.propertyNames->minute, jsNontrivialString(vm, minuteString(m_minute)));
1169  
1170      if (m_second != Second::None)
1171          options->putDirect(vm, vm.propertyNames->second, jsNontrivialString(vm, secondString(m_second)));
1172  
1173      if (m_fractionalSecondDigits)
1174          options->putDirect(vm, vm.propertyNames->fractionalSecondDigits, jsNumber(m_fractionalSecondDigits));
1175  
1176      if (m_timeZoneName != TimeZoneName::None)
1177          options->putDirect(vm, vm.propertyNames->timeZoneName, jsNontrivialString(vm, timeZoneNameString(m_timeZoneName)));
1178  
1179      if (m_dateStyle != DateTimeStyle::None)
1180          options->putDirect(vm, vm.propertyNames->dateStyle, jsNontrivialString(vm, formatStyleString(m_dateStyle)));
1181  
1182      if (m_timeStyle != DateTimeStyle::None)
1183          options->putDirect(vm, vm.propertyNames->timeStyle, jsNontrivialString(vm, formatStyleString(m_timeStyle)));
1184  
1185      return options;
1186  }
1187  
1188  // https://tc39.es/ecma402/#sec-formatdatetime
1189  JSValue IntlDateTimeFormat::format(JSGlobalObject* globalObject, double value) const
1190  {
1191      ASSERT(m_dateFormat);
1192  
1193      VM& vm = globalObject->vm();
1194      auto scope = DECLARE_THROW_SCOPE(vm);
1195  
1196      if (!std::isfinite(value))
1197          return throwRangeError(globalObject, scope, "date value is not finite in DateTimeFormat format()"_s);
1198  
1199      Vector<UChar, 32> result;
1200      auto status = callBufferProducingFunction(udat_format, m_dateFormat.get(), value, result, nullptr);
1201      if (U_FAILURE(status))
1202          return throwTypeError(globalObject, scope, "failed to format date value"_s);
1203  
1204      return jsString(vm, String(result));
1205  }
1206  
1207  static ASCIILiteral partTypeString(UDateFormatField field)
1208  {
1209      switch (field) {
1210      case UDAT_ERA_FIELD:
1211          return "era"_s;
1212      case UDAT_YEAR_FIELD:
1213      case UDAT_EXTENDED_YEAR_FIELD:
1214          return "year"_s;
1215      case UDAT_YEAR_NAME_FIELD:
1216          return "yearName"_s;
1217      case UDAT_MONTH_FIELD:
1218      case UDAT_STANDALONE_MONTH_FIELD:
1219          return "month"_s;
1220      case UDAT_DATE_FIELD:
1221          return "day"_s;
1222      case UDAT_HOUR_OF_DAY1_FIELD:
1223      case UDAT_HOUR_OF_DAY0_FIELD:
1224      case UDAT_HOUR1_FIELD:
1225      case UDAT_HOUR0_FIELD:
1226          return "hour"_s;
1227      case UDAT_MINUTE_FIELD:
1228          return "minute"_s;
1229      case UDAT_SECOND_FIELD:
1230          return "second"_s;
1231      case UDAT_FRACTIONAL_SECOND_FIELD:
1232          return "fractionalSecond"_s;
1233      case UDAT_DAY_OF_WEEK_FIELD:
1234      case UDAT_DOW_LOCAL_FIELD:
1235      case UDAT_STANDALONE_DAY_FIELD:
1236          return "weekday"_s;
1237      case UDAT_AM_PM_FIELD:
1238      case UDAT_AM_PM_MIDNIGHT_NOON_FIELD:
1239      case UDAT_FLEXIBLE_DAY_PERIOD_FIELD:
1240          return "dayPeriod"_s;
1241      case UDAT_TIMEZONE_FIELD:
1242      case UDAT_TIMEZONE_RFC_FIELD:
1243      case UDAT_TIMEZONE_GENERIC_FIELD:
1244      case UDAT_TIMEZONE_SPECIAL_FIELD:
1245      case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD:
1246      case UDAT_TIMEZONE_ISO_FIELD:
1247      case UDAT_TIMEZONE_ISO_LOCAL_FIELD:
1248          return "timeZoneName"_s;
1249      case UDAT_RELATED_YEAR_FIELD:
1250          return "relatedYear"_s;
1251      // These should not show up because there is no way to specify them in DateTimeFormat options.
1252      // If they do, they don't fit well into any of known part types, so consider it an "unknown".
1253      case UDAT_DAY_OF_YEAR_FIELD:
1254      case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD:
1255      case UDAT_WEEK_OF_YEAR_FIELD:
1256      case UDAT_WEEK_OF_MONTH_FIELD:
1257      case UDAT_YEAR_WOY_FIELD:
1258      case UDAT_JULIAN_DAY_FIELD:
1259      case UDAT_MILLISECONDS_IN_DAY_FIELD:
1260      case UDAT_QUARTER_FIELD:
1261      case UDAT_STANDALONE_QUARTER_FIELD:
1262      case UDAT_TIME_SEPARATOR_FIELD:
1263      // Any newer additions to the UDateFormatField enum should just be considered an "unknown" part.
1264      default:
1265          return "unknown"_s;
1266      }
1267      return "unknown"_s;
1268  }
1269  
1270  // https://tc39.es/ecma402/#sec-formatdatetimetoparts
1271  JSValue IntlDateTimeFormat::formatToParts(JSGlobalObject* globalObject, double value, JSString* sourceType) const
1272  {
1273      ASSERT(m_dateFormat);
1274  
1275      VM& vm = globalObject->vm();
1276      auto scope = DECLARE_THROW_SCOPE(vm);
1277  
1278      if (!std::isfinite(value))
1279          return throwRangeError(globalObject, scope, "date value is not finite in DateTimeFormat formatToParts()"_s);
1280  
1281      UErrorCode status = U_ZERO_ERROR;
1282      auto fields = std::unique_ptr<UFieldPositionIterator, UFieldPositionIteratorDeleter>(ufieldpositer_open(&status));
1283      if (U_FAILURE(status))
1284          return throwTypeError(globalObject, scope, "failed to open field position iterator"_s);
1285  
1286      Vector<UChar, 32> result;
1287      status = callBufferProducingFunction(udat_formatForFields, m_dateFormat.get(), value, result, fields.get());
1288      if (U_FAILURE(status))
1289          return throwTypeError(globalObject, scope, "failed to format date value"_s);
1290  
1291      JSArray* parts = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 0);
1292      if (!parts)
1293          return throwOutOfMemoryError(globalObject, scope);
1294  
1295      auto resultString = String(result);
1296      auto literalString = jsNontrivialString(vm, "literal"_s);
1297  
1298      int32_t resultLength = result.size();
1299      int32_t previousEndIndex = 0;
1300      int32_t beginIndex = 0;
1301      int32_t endIndex = 0;
1302      while (previousEndIndex < resultLength) {
1303          auto fieldType = ufieldpositer_next(fields.get(), &beginIndex, &endIndex);
1304          if (fieldType < 0)
1305              beginIndex = endIndex = resultLength;
1306  
1307          if (previousEndIndex < beginIndex) {
1308              auto value = jsString(vm, resultString.substring(previousEndIndex, beginIndex - previousEndIndex));
1309              JSObject* part = constructEmptyObject(globalObject);
1310              part->putDirect(vm, vm.propertyNames->type, literalString);
1311              part->putDirect(vm, vm.propertyNames->value, value);
1312              if (sourceType)
1313                  part->putDirect(vm, vm.propertyNames->source, sourceType);
1314              parts->push(globalObject, part);
1315              RETURN_IF_EXCEPTION(scope, { });
1316          }
1317          previousEndIndex = endIndex;
1318  
1319          if (fieldType >= 0) {
1320              auto type = jsString(vm, partTypeString(UDateFormatField(fieldType)));
1321              auto value = jsString(vm, resultString.substring(beginIndex, endIndex - beginIndex));
1322              JSObject* part = constructEmptyObject(globalObject);
1323              part->putDirect(vm, vm.propertyNames->type, type);
1324              part->putDirect(vm, vm.propertyNames->value, value);
1325              if (sourceType)
1326                  part->putDirect(vm, vm.propertyNames->source, sourceType);
1327              parts->push(globalObject, part);
1328              RETURN_IF_EXCEPTION(scope, { });
1329          }
1330      }
1331  
1332      return parts;
1333  }
1334  
1335  UDateIntervalFormat* IntlDateTimeFormat::createDateIntervalFormatIfNecessary(JSGlobalObject* globalObject)
1336  {
1337      ASSERT(m_dateFormat);
1338  
1339      VM& vm = globalObject->vm();
1340      auto scope = DECLARE_THROW_SCOPE(vm);
1341  
1342      if (m_dateIntervalFormat)
1343          return m_dateIntervalFormat.get();
1344  
1345      Vector<UChar, 32> pattern;
1346      {
1347          auto status = callBufferProducingFunction(udat_toPattern, m_dateFormat.get(), false, pattern);
1348          if (U_FAILURE(status)) {
1349              throwTypeError(globalObject, scope, "failed to initialize DateIntervalFormat"_s);
1350              return nullptr;
1351          }
1352      }
1353  
1354      Vector<UChar, 32> skeleton;
1355      {
1356          auto status = callBufferProducingFunction(udatpg_getSkeleton, nullptr, pattern.data(), pattern.size(), skeleton);
1357          if (U_FAILURE(status)) {
1358              throwTypeError(globalObject, scope, "failed to initialize DateIntervalFormat"_s);
1359              return nullptr;
1360          }
1361      }
1362  
1363      dataLogLnIf(IntlDateTimeFormatInternal::verbose, "interval format pattern:(", String(pattern), "),skeleton:(", String(skeleton), ")");
1364  
1365      // While the pattern is including right HourCycle patterns, UDateIntervalFormat does not follow.
1366      // We need to enforce HourCycle by setting "hc" extension if it is specified.
1367      StringBuilder localeBuilder;
1368      localeBuilder.append(m_dataLocale, "-u-ca-", m_calendar, "-nu-", m_numberingSystem);
1369      if (m_hourCycle != HourCycle::None)
1370          localeBuilder.append("-hc-", hourCycleString(m_hourCycle));
1371      CString dataLocaleWithExtensions = localeBuilder.toString().utf8();
1372  
1373      UErrorCode status = U_ZERO_ERROR;
1374      StringView timeZoneView(m_timeZone);
1375      m_dateIntervalFormat = std::unique_ptr<UDateIntervalFormat, UDateIntervalFormatDeleter>(udtitvfmt_open(dataLocaleWithExtensions.data(), skeleton.data(), skeleton.size(), timeZoneView.upconvertedCharacters(), timeZoneView.length(), &status));
1376      if (U_FAILURE(status)) {
1377          throwTypeError(globalObject, scope, "failed to initialize DateIntervalFormat"_s);
1378          return nullptr;
1379      }
1380      return m_dateIntervalFormat.get();
1381  }
1382  
1383  #if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
1384  
1385  static std::unique_ptr<UFormattedDateInterval, ICUDeleter<udtitvfmt_closeResult>> formattedValueFromDateRange(UDateIntervalFormat& dateIntervalFormat, UDateFormat& dateFormat, double startDate, double endDate, UErrorCode& status)
1386  {
1387      auto result = std::unique_ptr<UFormattedDateInterval, ICUDeleter<udtitvfmt_closeResult>>(udtitvfmt_openResult(&status));
1388      if (U_FAILURE(status))
1389          return nullptr;
1390  
1391      // After ICU 67, udtitvfmt_formatToResult's signature is changed.
1392  #if U_ICU_VERSION_MAJOR_NUM >= 67
1393      // If a date is after Oct 15, 1582, the configuration of gregorian calendar change date in UCalendar does not affect
1394      // on the formatted string. To ensure that it is after Oct 15 in all timezones, we add one day to gregorian calendar
1395      // change date in UTC, so that this check can conservatively answer whether the date is definitely after gregorian
1396      // calendar change date.
1397      auto definitelyAfterGregorianCalendarChangeDate = [](double millisecondsFromEpoch) {
1398          constexpr double gregorianCalendarReformDateInUTC = -12219292800000.0;
1399          return millisecondsFromEpoch >= (gregorianCalendarReformDateInUTC + msPerDay);
1400      };
1401  
1402      // UFormattedDateInterval does not have a way to configure gregorian calendar change date while ECMAScript requires that
1403      // gregorian calendar change should not have effect (we are setting ucal_setGregorianChange(cal, minECMAScriptTime, &status) explicitly).
1404      // As a result, if the input date is older than gregorian calendar change date (Oct 15, 1582), the formatted string becomes
1405      // julian calendar date.
1406      // udtitvfmt_formatCalendarToResult API offers the way to set calendar to each date of the input, so that we can use UDateFormat's
1407      // calendar which is already configured to meet ECMAScript's requirement (effectively clearing gregorian calendar change date).
1408      //
1409      // If we can ensure that startDate is after gregorian calendar change date, we can just use udtitvfmt_formatToResult since gregorian
1410      // calendar change date does not affect on the formatted string.
1411      //
1412      // https://unicode-org.atlassian.net/browse/ICU-20705
1413      if (definitelyAfterGregorianCalendarChangeDate(startDate))
1414          udtitvfmt_formatToResult(&dateIntervalFormat, startDate, endDate, result.get(), &status);
1415      else {
1416          auto createCalendarForDate = [](const UCalendar* calendar, double date, UErrorCode& status) -> std::unique_ptr<UCalendar, ICUDeleter<ucal_close>> {
1417              auto result = std::unique_ptr<UCalendar, ICUDeleter<ucal_close>>(ucal_clone(calendar, &status));
1418              if (U_FAILURE(status))
1419                  return nullptr;
1420              ucal_setMillis(result.get(), date, &status);
1421              if (U_FAILURE(status))
1422                  return nullptr;
1423              return result;
1424          };
1425  
1426          auto calendar = udat_getCalendar(&dateFormat);
1427  
1428          auto startCalendar = createCalendarForDate(calendar, startDate, status);
1429          if (U_FAILURE(status))
1430              return nullptr;
1431  
1432          auto endCalendar = createCalendarForDate(calendar, endDate, status);
1433          if (U_FAILURE(status))
1434              return nullptr;
1435  
1436          udtitvfmt_formatCalendarToResult(&dateIntervalFormat, startCalendar.get(), endCalendar.get(), result.get(), &status);
1437      }
1438  #else
1439      UNUSED_PARAM(dateFormat);
1440      udtitvfmt_formatToResult(&dateIntervalFormat, result.get(), startDate, endDate, &status);
1441  #endif
1442      return result;
1443  }
1444  
1445  static bool dateFieldsPracticallyEqual(const UFormattedValue* formattedValue, UErrorCode& status)
1446  {
1447      auto iterator = std::unique_ptr<UConstrainedFieldPosition, ICUDeleter<ucfpos_close>>(ucfpos_open(&status));
1448      if (U_FAILURE(status))
1449          return false;
1450  
1451      // We only care about UFIELD_CATEGORY_DATE_INTERVAL_SPAN category.
1452      ucfpos_constrainCategory(iterator.get(), UFIELD_CATEGORY_DATE_INTERVAL_SPAN, &status);
1453      if (U_FAILURE(status))
1454          return false;
1455  
1456      bool hasSpan = ufmtval_nextPosition(formattedValue, iterator.get(), &status);
1457      if (U_FAILURE(status))
1458          return false;
1459  
1460      return !hasSpan;
1461  }
1462  
1463  #endif // HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
1464  
1465  JSValue IntlDateTimeFormat::formatRange(JSGlobalObject* globalObject, double startDate, double endDate)
1466  {
1467      ASSERT(m_dateFormat);
1468  
1469      VM& vm = globalObject->vm();
1470      auto scope = DECLARE_THROW_SCOPE(vm);
1471  
1472      // http://tc39.es/proposal-intl-DateTimeFormat-formatRange/#sec-partitiondatetimerangepattern
1473      startDate = timeClip(startDate);
1474      endDate = timeClip(endDate);
1475      if (std::isnan(startDate) || std::isnan(endDate)) {
1476          throwRangeError(globalObject, scope, "Passed date is out of range"_s);
1477          return { };
1478      }
1479  
1480      auto* dateIntervalFormat = createDateIntervalFormatIfNecessary(globalObject);
1481      RETURN_IF_EXCEPTION(scope, { });
1482  
1483  #if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
1484      UErrorCode status = U_ZERO_ERROR;
1485      auto result = formattedValueFromDateRange(*dateIntervalFormat, *m_dateFormat, startDate, endDate, status);
1486      if (U_FAILURE(status)) {
1487          throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1488          return { };
1489      }
1490  
1491      // UFormattedValue is owned by UFormattedDateInterval. We do not need to close it.
1492      auto formattedValue = udtitvfmt_resultAsValue(result.get(), &status);
1493      if (U_FAILURE(status)) {
1494          throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1495          return { };
1496      }
1497  
1498      // If the formatted parts of startDate and endDate are the same, it is possible that the resulted string does not look like range.
1499      // For example, if the requested format only includes "year" and startDate and endDate are the same year, the result just contains one year.
1500      // In that case, startDate and endDate are *practically-equal* (spec term), and we generate parts as we call `formatToParts(startDate)` with
1501      // `source: "shared"` additional fields.
1502      bool equal = dateFieldsPracticallyEqual(formattedValue, status);
1503      if (U_FAILURE(status)) {
1504          throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1505          return { };
1506      }
1507  
1508      if (equal)
1509          RELEASE_AND_RETURN(scope, format(globalObject, startDate));
1510  
1511      int32_t formattedStringLength = 0;
1512      const UChar* formattedStringPointer = ufmtval_getString(formattedValue, &formattedStringLength, &status);
1513      if (U_FAILURE(status)) {
1514          throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1515          return { };
1516      }
1517  
1518      return jsString(vm, String(formattedStringPointer, formattedStringLength));
1519  #else
1520      Vector<UChar, 32> buffer;
1521      auto status = callBufferProducingFunction(udtitvfmt_format, dateIntervalFormat, startDate, endDate, buffer, nullptr);
1522      if (U_FAILURE(status)) {
1523          throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1524          return { };
1525      }
1526  
1527      return jsString(vm, String(buffer));
1528  #endif
1529  }
1530  
1531  JSValue IntlDateTimeFormat::formatRangeToParts(JSGlobalObject* globalObject, double startDate, double endDate)
1532  {
1533      ASSERT(m_dateFormat);
1534  
1535      VM& vm = globalObject->vm();
1536      auto scope = DECLARE_THROW_SCOPE(vm);
1537  
1538  #if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
1539      // http://tc39.es/proposal-intl-DateTimeFormat-formatRange/#sec-partitiondatetimerangepattern
1540      startDate = timeClip(startDate);
1541      endDate = timeClip(endDate);
1542      if (std::isnan(startDate) || std::isnan(endDate)) {
1543          throwRangeError(globalObject, scope, "Passed date is out of range"_s);
1544          return { };
1545      }
1546  
1547      auto* dateIntervalFormat = createDateIntervalFormatIfNecessary(globalObject);
1548      RETURN_IF_EXCEPTION(scope, { });
1549  
1550      UErrorCode status = U_ZERO_ERROR;
1551      auto result = formattedValueFromDateRange(*dateIntervalFormat, *m_dateFormat, startDate, endDate, status);
1552      if (U_FAILURE(status)) {
1553          throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1554          return { };
1555      }
1556  
1557      // UFormattedValue is owned by UFormattedDateInterval. We do not need to close it.
1558      auto formattedValue = udtitvfmt_resultAsValue(result.get(), &status);
1559      if (U_FAILURE(status)) {
1560          throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1561          return { };
1562      }
1563  
1564      auto sharedString = jsNontrivialString(vm, "shared"_s);
1565  
1566      // If the formatted parts of startDate and endDate are the same, it is possible that the resulted string does not look like range.
1567      // For example, if the requested format only includes "year" and startDate and endDate are the same year, the result just contains one year.
1568      // In that case, startDate and endDate are *practically-equal* (spec term), and we generate parts as we call `formatToParts(startDate)` with
1569      // `source: "shared"` additional fields.
1570      bool equal = dateFieldsPracticallyEqual(formattedValue, status);
1571      if (U_FAILURE(status)) {
1572          throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1573          return { };
1574      }
1575  
1576      if (equal)
1577          RELEASE_AND_RETURN(scope, formatToParts(globalObject, startDate, sharedString));
1578  
1579      // ICU produces ranges for the formatted string, and we construct parts array from that.
1580      // For example, startDate = Jan 3, 2019, endDate = Jan 5, 2019 with en-US locale is,
1581      //
1582      // Formatted string: "1/3/2019 – 1/5/2019"
1583      //                    | | |  |   | | |  |
1584      //                    B C |  |   F G |  |
1585      //                    |   +-D+   |   +-H+
1586      //                    |      |   |      |
1587      //                    +--A---+   +--E---+
1588      //
1589      // Ranges ICU generates:
1590      //     A:    (0, 8)   UFIELD_CATEGORY_DATE_INTERVAL_SPAN startRange
1591      //     B:    (0, 1)   UFIELD_CATEGORY_DATE month
1592      //     C:    (2, 3)   UFIELD_CATEGORY_DATE day
1593      //     D:    (4, 8)   UFIELD_CATEGORY_DATE year
1594      //     E:    (11, 19) UFIELD_CATEGORY_DATE_INTERVAL_SPAN endRange
1595      //     F:    (11, 12) UFIELD_CATEGORY_DATE month
1596      //     G:    (13, 14) UFIELD_CATEGORY_DATE day
1597      //     H:    (15, 19) UFIELD_CATEGORY_DATE year
1598      //
1599      //  We use UFIELD_CATEGORY_DATE_INTERVAL_SPAN range to determine each part is either "startRange", "endRange", or "shared".
1600      //  It is gurarnteed that UFIELD_CATEGORY_DATE_INTERVAL_SPAN comes first before any other parts including that range.
1601      //  For example, in the above formatted string, " – " is "shared" part. For UFIELD_CATEGORY_DATE ranges, we generate corresponding
1602      //  part object with types such as "month". And non populated parts (e.g. "/") become "literal" parts.
1603      //  In the above case, expected parts are,
1604      //
1605      //     { type: "month", value: "1", source: "startRange" },
1606      //     { type: "literal", value: "/", source: "startRange" },
1607      //     { type: "day", value: "3", source: "startRange" },
1608      //     { type: "literal", value: "/", source: "startRange" },
1609      //     { type: "year", value: "2019", source: "startRange" },
1610      //     { type: "literal", value: " - ", source: "shared" },
1611      //     { type: "month", value: "1", source: "endRange" },
1612      //     { type: "literal", value: "/", source: "endRange" },
1613      //     { type: "day", value: "5", source: "endRange" },
1614      //     { type: "literal", value: "/", source: "endRange" },
1615      //     { type: "year", value: "2019", source: "endRange" },
1616      //
1617  
1618      JSArray* parts = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 0);
1619      if (!parts) {
1620          throwOutOfMemoryError(globalObject, scope);
1621          return { };
1622      }
1623  
1624      int32_t formattedStringLength = 0;
1625      const UChar* formattedStringPointer = ufmtval_getString(formattedValue, &formattedStringLength, &status);
1626      if (U_FAILURE(status)) {
1627          throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1628          return { };
1629      }
1630      String resultString(formattedStringPointer, formattedStringLength);
1631  
1632      // We care multiple categories (UFIELD_CATEGORY_DATE and UFIELD_CATEGORY_DATE_INTERVAL_SPAN).
1633      // So we do not constraint iterator.
1634      auto iterator = std::unique_ptr<UConstrainedFieldPosition, ICUDeleter<ucfpos_close>>(ucfpos_open(&status));
1635      if (U_FAILURE(status)) {
1636          throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1637          return { };
1638      }
1639  
1640      auto startRangeString = jsNontrivialString(vm, "startRange"_s);
1641      auto endRangeString = jsNontrivialString(vm, "endRange"_s);
1642      auto literalString = jsNontrivialString(vm, "literal"_s);
1643  
1644      WTF::Range<int32_t> startRange { -1, -1 };
1645      WTF::Range<int32_t> endRange { -1, -1 };
1646  
1647      auto createPart = [&] (JSString* type, int32_t beginIndex, int32_t length) {
1648          auto sourceType = [&](int32_t index) -> JSString* {
1649              if (startRange.contains(index))
1650                  return startRangeString;
1651              if (endRange.contains(index))
1652                  return endRangeString;
1653              return sharedString;
1654          };
1655  
1656          auto value = jsString(vm, resultString.substring(beginIndex, length));
1657          JSObject* part = constructEmptyObject(globalObject);
1658          part->putDirect(vm, vm.propertyNames->type, type);
1659          part->putDirect(vm, vm.propertyNames->value, value);
1660          part->putDirect(vm, vm.propertyNames->source, sourceType(beginIndex));
1661          return part;
1662      };
1663  
1664      int32_t resultLength = resultString.length();
1665      int32_t previousEndIndex = 0;
1666      while (true) {
1667          bool next = ufmtval_nextPosition(formattedValue, iterator.get(), &status);
1668          if (U_FAILURE(status)) {
1669              throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1670              return { };
1671          }
1672          if (!next)
1673              break;
1674  
1675          int32_t category = ucfpos_getCategory(iterator.get(), &status);
1676          if (U_FAILURE(status)) {
1677              throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1678              return { };
1679          }
1680  
1681          int32_t fieldType = ucfpos_getField(iterator.get(), &status);
1682          if (U_FAILURE(status)) {
1683              throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1684              return { };
1685          }
1686  
1687          int32_t beginIndex = 0;
1688          int32_t endIndex = 0;
1689          ucfpos_getIndexes(iterator.get(), &beginIndex, &endIndex, &status);
1690          if (U_FAILURE(status)) {
1691              throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1692              return { };
1693          }
1694  
1695          dataLogLnIf(IntlDateTimeFormatInternal::verbose, category, " ", fieldType, " (", beginIndex, ", ", endIndex, ")");
1696  
1697          if (category != UFIELD_CATEGORY_DATE && category != UFIELD_CATEGORY_DATE_INTERVAL_SPAN)
1698              continue;
1699          if (category == UFIELD_CATEGORY_DATE && fieldType < 0)
1700              continue;
1701  
1702          if (previousEndIndex < beginIndex) {
1703              JSObject* part = createPart(literalString, previousEndIndex, beginIndex - previousEndIndex);
1704              parts->push(globalObject, part);
1705              RETURN_IF_EXCEPTION(scope, { });
1706              previousEndIndex = beginIndex;
1707          }
1708  
1709          if (category == UFIELD_CATEGORY_DATE_INTERVAL_SPAN) {
1710              // > The special field category UFIELD_CATEGORY_DATE_INTERVAL_SPAN is used to indicate which datetime
1711              // > primitives came from which arguments: 0 means fromCalendar, and 1 means toCalendar. The span category
1712              // > will always occur before the corresponding fields in UFIELD_CATEGORY_DATE in the nextPosition() iterator.
1713              // from ICU comment. So, field 0 is startRange, field 1 is endRange.
1714              if (!fieldType)
1715                  startRange = WTF::Range<int32_t>(beginIndex, endIndex);
1716              else {
1717                  ASSERT(fieldType == 1);
1718                  endRange = WTF::Range<int32_t>(beginIndex, endIndex);
1719              }
1720              continue;
1721          }
1722  
1723          ASSERT(category == UFIELD_CATEGORY_DATE);
1724  
1725          auto type = jsString(vm, partTypeString(UDateFormatField(fieldType)));
1726          JSObject* part = createPart(type, beginIndex, endIndex - beginIndex);
1727          parts->push(globalObject, part);
1728          RETURN_IF_EXCEPTION(scope, { });
1729          previousEndIndex = endIndex;
1730      }
1731  
1732      if (previousEndIndex < resultLength) {
1733          JSObject* part = createPart(literalString, previousEndIndex, resultLength - previousEndIndex);
1734          parts->push(globalObject, part);
1735          RETURN_IF_EXCEPTION(scope, { });
1736      }
1737  
1738      return parts;
1739  #else
1740      UNUSED_PARAM(startDate);
1741      UNUSED_PARAM(endDate);
1742      throwTypeError(globalObject, scope, "Failed to format date interval"_s);
1743      return { };
1744  #endif
1745  }
1746  
1747  
1748  } // namespace JSC