js/src/builtin/intl/NumberFormat.cpp
author Csoregi Natalia <ncsoregi@mozilla.com>
Thu, 04 Jun 2020 22:27:11 +0300
changeset 533959 e33aea19d0c50b6b5eec8a45a8b21abcfdc79c85
parent 533878 fd4400f508cc99e96d6a1a129480d5e59b667128
permissions -rw-r--r--
Backed out 4 changesets (bug 1642006, bug 1628653) for failures on browser_storageAccessWithDynamicFpiHeuristics.js. CLOSED TREE Backed out changeset c2b234b60396 (bug 1642006) Backed out changeset 07e596b8a81b (bug 1642006) Backed out changeset 10a61355cf44 (bug 1642006) Backed out changeset d2edf82befde (bug 1628653)

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* Intl.NumberFormat implementation. */

#include "builtin/intl/NumberFormat.h"

#include "mozilla/ArrayUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/Casting.h"
#include "mozilla/FloatingPoint.h"
#include "mozilla/UniquePtr.h"

#include <algorithm>
#include <cstring>
#include <iterator>
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <type_traits>

#include "builtin/Array.h"
#include "builtin/intl/CommonFunctions.h"
#include "builtin/intl/LanguageTag.h"
#include "builtin/intl/MeasureUnitGenerated.h"
#include "builtin/intl/RelativeTimeFormat.h"
#include "builtin/intl/ScopedICUObject.h"
#include "ds/Sort.h"
#include "gc/FreeOp.h"
#include "js/CharacterEncoding.h"
#include "js/PropertySpec.h"
#include "js/RootingAPI.h"
#include "js/TypeDecls.h"
#include "js/Vector.h"
#include "unicode/udata.h"
#include "unicode/ufieldpositer.h"
#include "unicode/uformattedvalue.h"
#include "unicode/unum.h"
#include "unicode/unumberformatter.h"
#include "unicode/unumsys.h"
#include "unicode/ures.h"
#include "unicode/utypes.h"
#include "vm/BigIntType.h"
#include "vm/GlobalObject.h"
#include "vm/JSContext.h"
#include "vm/PlainObject.h"  // js::PlainObject
#include "vm/SelfHosting.h"
#include "vm/Stack.h"
#include "vm/StringType.h"

#include "vm/JSObject-inl.h"

using namespace js;

using mozilla::AssertedCast;
using mozilla::IsFinite;
using mozilla::IsNaN;
using mozilla::IsNegative;
using mozilla::SpecificNaN;

using js::intl::CallICU;
using js::intl::DateTimeFormatOptions;
using js::intl::FieldType;
using js::intl::IcuLocale;

const JSClassOps NumberFormatObject::classOps_ = {
    nullptr,                       // addProperty
    nullptr,                       // delProperty
    nullptr,                       // enumerate
    nullptr,                       // newEnumerate
    nullptr,                       // resolve
    nullptr,                       // mayResolve
    NumberFormatObject::finalize,  // finalize
    nullptr,                       // call
    nullptr,                       // hasInstance
    nullptr,                       // construct
    nullptr,                       // trace
};

const JSClass NumberFormatObject::class_ = {
    js_Object_str,
    JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) |
        JSCLASS_HAS_CACHED_PROTO(JSProto_NumberFormat) |
        JSCLASS_FOREGROUND_FINALIZE,
    &NumberFormatObject::classOps_, &NumberFormatObject::classSpec_};

const JSClass& NumberFormatObject::protoClass_ = PlainObject::class_;

static bool numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  args.rval().setString(cx->names().NumberFormat);
  return true;
}

static const JSFunctionSpec numberFormat_static_methods[] = {
    JS_SELF_HOSTED_FN("supportedLocalesOf",
                      "Intl_NumberFormat_supportedLocalesOf", 1, 0),
    JS_FS_END};

static const JSFunctionSpec numberFormat_methods[] = {
    JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0,
                      0),
    JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0),
    JS_FN(js_toSource_str, numberFormat_toSource, 0, 0), JS_FS_END};

static const JSPropertySpec numberFormat_properties[] = {
    JS_SELF_HOSTED_GET("format", "$Intl_NumberFormat_format_get", 0),
    JS_STRING_SYM_PS(toStringTag, "Intl.NumberFormat", JSPROP_READONLY),
    JS_PS_END};

static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp);

const ClassSpec NumberFormatObject::classSpec_ = {
    GenericCreateConstructor<NumberFormat, 0, gc::AllocKind::FUNCTION>,
    GenericCreatePrototype<NumberFormatObject>,
    numberFormat_static_methods,
    nullptr,
    numberFormat_methods,
    numberFormat_properties,
    nullptr,
    ClassSpec::DontDefineConstructor};

/**
 * 11.2.1 Intl.NumberFormat([ locales [, options]])
 *
 * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
 */
static bool NumberFormat(JSContext* cx, const CallArgs& args, bool construct) {
  // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).

  // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
  RootedObject proto(cx);
  if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_NumberFormat,
                                          &proto)) {
    return false;
  }

  Rooted<NumberFormatObject*> numberFormat(cx);
  numberFormat = NewObjectWithClassProto<NumberFormatObject>(cx, proto);
  if (!numberFormat) {
    return false;
  }

  RootedValue thisValue(cx,
                        construct ? ObjectValue(*numberFormat) : args.thisv());
  HandleValue locales = args.get(0);
  HandleValue options = args.get(1);

  // Step 3.
  return intl::LegacyInitializeObject(
      cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, locales,
      options, DateTimeFormatOptions::Standard, args.rval());
}

static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  return NumberFormat(cx, args, args.isConstructing());
}

bool js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  MOZ_ASSERT(args.length() == 2);
  MOZ_ASSERT(!args.isConstructing());
  // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it
  // cannot be used with "new", but it still has to be treated as a
  // constructor.
  return NumberFormat(cx, args, true);
}

void js::NumberFormatObject::finalize(JSFreeOp* fop, JSObject* obj) {
  MOZ_ASSERT(fop->onMainThread());

  auto* numberFormat = &obj->as<NumberFormatObject>();
  UNumberFormatter* nf = numberFormat->getNumberFormatter();
  UFormattedNumber* formatted = numberFormat->getFormattedNumber();

  if (nf) {
    intl::RemoveICUCellMemory(fop, obj, NumberFormatObject::EstimatedMemoryUse);

    unumf_close(nf);
  }
  if (formatted) {
    // UFormattedNumber memory tracked as part of UNumberFormatter.

    unumf_closeResult(formatted);
  }
}

bool js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  MOZ_ASSERT(args.length() == 1);
  MOZ_ASSERT(args[0].isString());

  UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
  if (!locale) {
    return false;
  }

  UErrorCode status = U_ZERO_ERROR;
  UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.get()), &status);
  if (U_FAILURE(status)) {
    intl::ReportInternalError(cx);
    return false;
  }

  ScopedICUObject<UNumberingSystem, unumsys_close> toClose(numbers);

  const char* name = unumsys_getName(numbers);
  if (!name) {
    intl::ReportInternalError(cx);
    return false;
  }

  JSString* jsname = NewStringCopyZ<CanGC>(cx, name);
  if (!jsname) {
    return false;
  }

  args.rval().setString(jsname);
  return true;
}

#if DEBUG || MOZ_SYSTEM_ICU
class UResourceBundleDeleter {
 public:
  void operator()(UResourceBundle* aPtr) { ures_close(aPtr); }
};

using UniqueUResourceBundle =
    mozilla::UniquePtr<UResourceBundle, UResourceBundleDeleter>;

bool js::intl_availableMeasurementUnits(JSContext* cx, unsigned argc,
                                        Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  MOZ_ASSERT(args.length() == 0);

  RootedObject measurementUnits(
      cx, NewObjectWithGivenProto<PlainObject>(cx, nullptr));
  if (!measurementUnits) {
    return false;
  }

  // Lookup the available measurement units in the resource boundle of the root
  // locale.

  static const char packageName[] =
      U_ICUDATA_NAME U_TREE_SEPARATOR_STRING "unit";
  static const char rootLocale[] = "";

  UErrorCode status = U_ZERO_ERROR;
  UResourceBundle* rawRes = ures_open(packageName, rootLocale, &status);
  if (U_FAILURE(status)) {
    intl::ReportInternalError(cx);
    return false;
  }
  UniqueUResourceBundle res(rawRes);

  UResourceBundle* rawUnits =
      ures_getByKey(res.get(), "units", nullptr, &status);
  if (U_FAILURE(status)) {
    intl::ReportInternalError(cx);
    return false;
  }
  UniqueUResourceBundle units(rawUnits);

  RootedAtom unitAtom(cx);

  int32_t unitsSize = ures_getSize(units.get());
  for (int32_t i = 0; i < unitsSize; i++) {
    UResourceBundle* rawType =
        ures_getByIndex(units.get(), i, nullptr, &status);
    if (U_FAILURE(status)) {
      intl::ReportInternalError(cx);
      return false;
    }
    UniqueUResourceBundle type(rawType);

    int32_t typeSize = ures_getSize(type.get());
    for (int32_t j = 0; j < typeSize; j++) {
      UResourceBundle* rawSubtype =
          ures_getByIndex(type.get(), j, nullptr, &status);
      if (U_FAILURE(status)) {
        intl::ReportInternalError(cx);
        return false;
      }
      UniqueUResourceBundle subtype(rawSubtype);

      const char* unitIdentifier = ures_getKey(subtype.get());

      unitAtom = Atomize(cx, unitIdentifier, strlen(unitIdentifier));
      if (!unitAtom) {
        return false;
      }
      if (!DefineDataProperty(cx, measurementUnits, unitAtom->asPropertyName(),
                              TrueHandleValue)) {
        return false;
      }
    }
  }

  args.rval().setObject(*measurementUnits);
  return true;
}
#endif

bool js::intl::NumberFormatterSkeleton::currency(JSLinearString* currency) {
  MOZ_ASSERT(currency->length() == 3,
             "IsWellFormedCurrencyCode permits only length-3 strings");

  char16_t currencyChars[] = {currency->latin1OrTwoByteChar(0),
                              currency->latin1OrTwoByteChar(1),
                              currency->latin1OrTwoByteChar(2), '\0'};
  return append(u"currency/") && append(currencyChars) && append(' ');
}

bool js::intl::NumberFormatterSkeleton::currencyDisplay(
    CurrencyDisplay display) {
  switch (display) {
    case CurrencyDisplay::Code:
      return appendToken(u"unit-width-iso-code");
    case CurrencyDisplay::Name:
      return appendToken(u"unit-width-full-name");
    case CurrencyDisplay::Symbol:
      // Default, no additional tokens needed.
      return true;
    case CurrencyDisplay::NarrowSymbol:
      return appendToken(u"unit-width-narrow");
  }
  MOZ_CRASH("unexpected currency display type");
}

static const MeasureUnit& FindSimpleMeasureUnit(const char* name) {
  auto measureUnit = std::lower_bound(
      std::begin(simpleMeasureUnits), std::end(simpleMeasureUnits), name,
      [](const auto& measureUnit, const char* name) {
        return strcmp(measureUnit.name, name) < 0;
      });
  MOZ_ASSERT(measureUnit != std::end(simpleMeasureUnits),
             "unexpected unit identifier: unit not found");
  MOZ_ASSERT(strcmp(measureUnit->name, name) == 0,
             "unexpected unit identifier: wrong unit found");
  return *measureUnit;
}

static constexpr size_t MaxUnitLength() {
  // Enable by default when libstdc++ 7 is the minimal version expected
#if _GLIBCXX_RELEASE >= 7
  size_t length = 0;
  for (const auto& unit : simpleMeasureUnits) {
    length = std::max(length, std::char_traits<char>::length(unit.name));
  }
  return length * 2 + std::char_traits<char>::length("-per-");
#else
  return mozilla::ArrayLength("mile-scandinavian-per-mile-scandinavian") - 1;
#endif
}

bool js::intl::NumberFormatterSkeleton::unit(JSLinearString* unit) {
  MOZ_RELEASE_ASSERT(unit->length() <= MaxUnitLength());

  char unitChars[MaxUnitLength() + 1] = {};
  CopyChars(reinterpret_cast<Latin1Char*>(unitChars), *unit);

  auto appendUnit = [this](const MeasureUnit& unit) {
    return append(unit.type, strlen(unit.type)) && append('-') &&
           append(unit.name, strlen(unit.name));
  };

  // |unit| can be a compound unit identifier, separated by "-per-".

  static constexpr char separator[] = "-per-";
  if (char* p = strstr(unitChars, separator)) {
    // Split into two strings.
    p[0] = '\0';

    auto& numerator = FindSimpleMeasureUnit(unitChars);
    if (!append(u"measure-unit/") || !appendUnit(numerator) || !append(' ')) {
      return false;
    }

    auto& denominator = FindSimpleMeasureUnit(p + strlen(separator));
    if (!append(u"per-measure-unit/") || !appendUnit(denominator) ||
        !append(' ')) {
      return false;
    }
  } else {
    auto& simple = FindSimpleMeasureUnit(unitChars);
    if (!append(u"measure-unit/") || !appendUnit(simple) || !append(' ')) {
      return false;
    }
  }
  return true;
}

bool js::intl::NumberFormatterSkeleton::unitDisplay(UnitDisplay display) {
  switch (display) {
    case UnitDisplay::Short:
      return appendToken(u"unit-width-short");
    case UnitDisplay::Narrow:
      return appendToken(u"unit-width-narrow");
    case UnitDisplay::Long:
      return appendToken(u"unit-width-full-name");
  }
  MOZ_CRASH("unexpected unit display type");
}

bool js::intl::NumberFormatterSkeleton::percent() {
  return appendToken(u"percent scale/100");
}

bool js::intl::NumberFormatterSkeleton::fractionDigits(uint32_t min,
                                                       uint32_t max) {
  // Note: |min| can be zero here.
  MOZ_ASSERT(min <= max);
  return append('.') && appendN('0', min) && appendN('#', max - min) &&
         append(' ');
}

bool js::intl::NumberFormatterSkeleton::integerWidth(uint32_t min) {
  MOZ_ASSERT(min > 0);
  return append(u"integer-width/+") && appendN('0', min) && append(' ');
}

bool js::intl::NumberFormatterSkeleton::significantDigits(uint32_t min,
                                                          uint32_t max) {
  MOZ_ASSERT(min > 0);
  MOZ_ASSERT(min <= max);
  return appendN('@', min) && appendN('#', max - min) && append(' ');
}

bool js::intl::NumberFormatterSkeleton::useGrouping(bool on) {
  return on || appendToken(u"group-off");
}

bool js::intl::NumberFormatterSkeleton::notation(Notation style) {
  switch (style) {
    case Notation::Standard:
      // Default, no additional tokens needed.
      return true;
    case Notation::Scientific:
      return appendToken(u"scientific");
    case Notation::Engineering:
      return appendToken(u"engineering");
    case Notation::CompactShort:
      return appendToken(u"compact-short");
    case Notation::CompactLong:
      return appendToken(u"compact-long");
  }
  MOZ_CRASH("unexpected notation style");
}

bool js::intl::NumberFormatterSkeleton::signDisplay(SignDisplay display) {
  switch (display) {
    case SignDisplay::Auto:
      // Default, no additional tokens needed.
      return true;
    case SignDisplay::Always:
      return appendToken(u"sign-always");
    case SignDisplay::Never:
      return appendToken(u"sign-never");
    case SignDisplay::ExceptZero:
      return appendToken(u"sign-except-zero");
    case SignDisplay::Accounting:
      return appendToken(u"sign-accounting");
    case SignDisplay::AccountingAlways:
      return appendToken(u"sign-accounting-always");
    case SignDisplay::AccountingExceptZero:
      return appendToken(u"sign-accounting-except-zero");
  }
  MOZ_CRASH("unexpected sign display type");
}

bool js::intl::NumberFormatterSkeleton::roundingModeHalfUp() {
  return appendToken(u"rounding-mode-half-up");
}

UNumberFormatter* js::intl::NumberFormatterSkeleton::toFormatter(
    JSContext* cx, const char* locale) {
  UErrorCode status = U_ZERO_ERROR;
  UNumberFormatter* nf = unumf_openForSkeletonAndLocale(
      vector_.begin(), vector_.length(), locale, &status);
  if (U_FAILURE(status)) {
    intl::ReportInternalError(cx);
    return nullptr;
  }
  return nf;
}

/**
 * Returns a new UNumberFormatter with the locale and number formatting options
 * of the given NumberFormat.
 */
static UNumberFormatter* NewUNumberFormatter(
    JSContext* cx, Handle<NumberFormatObject*> numberFormat) {
  RootedValue value(cx);

  RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat));
  if (!internals) {
    return nullptr;
  }

  if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
    return nullptr;
  }

  // ICU expects numberingSystem as a Unicode locale extensions on locale.

  intl::LanguageTag tag(cx);
  {
    JSLinearString* locale = value.toString()->ensureLinear(cx);
    if (!locale) {
      return nullptr;
    }

    if (!intl::LanguageTagParser::parse(cx, locale, tag)) {
      return nullptr;
    }
  }

  JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);

  if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
                   &value)) {
    return nullptr;
  }

  {
    JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
    if (!numberingSystem) {
      return nullptr;
    }

    if (!keywords.emplaceBack("nu", numberingSystem)) {
      return nullptr;
    }
  }

  // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of
  // the Unicode extension subtag. We're then relying on ICU to follow RFC
  // 6067, which states that any trailing keywords using the same key
  // should be ignored.
  if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
    return nullptr;
  }

  UniqueChars locale = tag.toStringZ(cx);
  if (!locale) {
    return nullptr;
  }

  intl::NumberFormatterSkeleton skeleton(cx);

  if (!GetProperty(cx, internals, internals, cx->names().style, &value)) {
    return nullptr;
  }

  bool accountingSign = false;
  {
    JSLinearString* style = value.toString()->ensureLinear(cx);
    if (!style) {
      return nullptr;
    }

    if (StringEqualsLiteral(style, "currency")) {
      if (!GetProperty(cx, internals, internals, cx->names().currency,
                       &value)) {
        return nullptr;
      }
      JSLinearString* currency = value.toString()->ensureLinear(cx);
      if (!currency) {
        return nullptr;
      }

      if (!skeleton.currency(currency)) {
        return nullptr;
      }

      if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay,
                       &value)) {
        return nullptr;
      }
      JSLinearString* currencyDisplay = value.toString()->ensureLinear(cx);
      if (!currencyDisplay) {
        return nullptr;
      }

      using CurrencyDisplay = intl::NumberFormatterSkeleton::CurrencyDisplay;

      CurrencyDisplay display;
      if (StringEqualsLiteral(currencyDisplay, "code")) {
        display = CurrencyDisplay::Code;
      } else if (StringEqualsLiteral(currencyDisplay, "symbol")) {
        display = CurrencyDisplay::Symbol;
      } else if (StringEqualsLiteral(currencyDisplay, "narrowSymbol")) {
        display = CurrencyDisplay::NarrowSymbol;
      } else {
        MOZ_ASSERT(StringEqualsLiteral(currencyDisplay, "name"));
        display = CurrencyDisplay::Name;
      }

      if (!skeleton.currencyDisplay(display)) {
        return nullptr;
      }

      if (!GetProperty(cx, internals, internals, cx->names().currencySign,
                       &value)) {
        return nullptr;
      }
      JSLinearString* currencySign = value.toString()->ensureLinear(cx);
      if (!currencySign) {
        return nullptr;
      }

      if (StringEqualsLiteral(currencySign, "accounting")) {
        accountingSign = true;
      } else {
        MOZ_ASSERT(StringEqualsLiteral(currencySign, "standard"));
      }
    } else if (StringEqualsLiteral(style, "percent")) {
      if (!skeleton.percent()) {
        return nullptr;
      }
    } else if (StringEqualsLiteral(style, "unit")) {
      if (!GetProperty(cx, internals, internals, cx->names().unit, &value)) {
        return nullptr;
      }
      JSLinearString* unit = value.toString()->ensureLinear(cx);
      if (!unit) {
        return nullptr;
      }

      if (!skeleton.unit(unit)) {
        return nullptr;
      }

      if (!GetProperty(cx, internals, internals, cx->names().unitDisplay,
                       &value)) {
        return nullptr;
      }
      JSLinearString* unitDisplay = value.toString()->ensureLinear(cx);
      if (!unitDisplay) {
        return nullptr;
      }

      using UnitDisplay = intl::NumberFormatterSkeleton::UnitDisplay;

      UnitDisplay display;
      if (StringEqualsLiteral(unitDisplay, "short")) {
        display = UnitDisplay::Short;
      } else if (StringEqualsLiteral(unitDisplay, "narrow")) {
        display = UnitDisplay::Narrow;
      } else {
        MOZ_ASSERT(StringEqualsLiteral(unitDisplay, "long"));
        display = UnitDisplay::Long;
      }

      if (!skeleton.unitDisplay(display)) {
        return nullptr;
      }
    } else {
      MOZ_ASSERT(StringEqualsLiteral(style, "decimal"));
    }
  }

  bool hasMinimumSignificantDigits;
  if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits,
                   &hasMinimumSignificantDigits)) {
    return nullptr;
  }

  if (hasMinimumSignificantDigits) {
    if (!GetProperty(cx, internals, internals,
                     cx->names().minimumSignificantDigits, &value)) {
      return nullptr;
    }
    uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());

    if (!GetProperty(cx, internals, internals,
                     cx->names().maximumSignificantDigits, &value)) {
      return nullptr;
    }
    uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());

    if (!skeleton.significantDigits(minimumSignificantDigits,
                                    maximumSignificantDigits)) {
      return nullptr;
    }
  }

  bool hasMinimumFractionDigits;
  if (!HasProperty(cx, internals, cx->names().minimumFractionDigits,
                   &hasMinimumFractionDigits)) {
    return nullptr;
  }

  if (hasMinimumFractionDigits) {
    if (!GetProperty(cx, internals, internals,
                     cx->names().minimumFractionDigits, &value)) {
      return nullptr;
    }
    uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());

    if (!GetProperty(cx, internals, internals,
                     cx->names().maximumFractionDigits, &value)) {
      return nullptr;
    }
    uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());

    if (!skeleton.fractionDigits(minimumFractionDigits,
                                 maximumFractionDigits)) {
      return nullptr;
    }
  }

  if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
                   &value)) {
    return nullptr;
  }
  uint32_t minimumIntegerDigits = AssertedCast<uint32_t>(value.toInt32());

  if (!skeleton.integerWidth(minimumIntegerDigits)) {
    return nullptr;
  }

  if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) {
    return nullptr;
  }
  if (!skeleton.useGrouping(value.toBoolean())) {
    return nullptr;
  }

  if (!GetProperty(cx, internals, internals, cx->names().notation, &value)) {
    return nullptr;
  }

  {
    JSLinearString* notation = value.toString()->ensureLinear(cx);
    if (!notation) {
      return nullptr;
    }

    using Notation = intl::NumberFormatterSkeleton::Notation;

    Notation style;
    if (StringEqualsLiteral(notation, "standard")) {
      style = Notation::Standard;
    } else if (StringEqualsLiteral(notation, "scientific")) {
      style = Notation::Scientific;
    } else if (StringEqualsLiteral(notation, "engineering")) {
      style = Notation::Engineering;
    } else {
      MOZ_ASSERT(StringEqualsLiteral(notation, "compact"));

      if (!GetProperty(cx, internals, internals, cx->names().compactDisplay,
                       &value)) {
        return nullptr;
      }

      JSLinearString* compactDisplay = value.toString()->ensureLinear(cx);
      if (!compactDisplay) {
        return nullptr;
      }

      if (StringEqualsLiteral(compactDisplay, "short")) {
        style = Notation::CompactShort;
      } else {
        MOZ_ASSERT(StringEqualsLiteral(compactDisplay, "long"));
        style = Notation::CompactLong;
      }
    }

    if (!skeleton.notation(style)) {
      return nullptr;
    }
  }

  if (!GetProperty(cx, internals, internals, cx->names().signDisplay, &value)) {
    return nullptr;
  }

  {
    JSLinearString* signDisplay = value.toString()->ensureLinear(cx);
    if (!signDisplay) {
      return nullptr;
    }

    using SignDisplay = intl::NumberFormatterSkeleton::SignDisplay;

    SignDisplay display;
    if (StringEqualsLiteral(signDisplay, "auto")) {
      if (accountingSign) {
        display = SignDisplay::Accounting;
      } else {
        display = SignDisplay::Auto;
      }
    } else if (StringEqualsLiteral(signDisplay, "never")) {
      display = SignDisplay::Never;
    } else if (StringEqualsLiteral(signDisplay, "always")) {
      if (accountingSign) {
        display = SignDisplay::AccountingAlways;
      } else {
        display = SignDisplay::Always;
      }
    } else {
      MOZ_ASSERT(StringEqualsLiteral(signDisplay, "exceptZero"));
      if (accountingSign) {
        display = SignDisplay::AccountingExceptZero;
      } else {
        display = SignDisplay::ExceptZero;
      }
    }

    if (!skeleton.signDisplay(display)) {
      return nullptr;
    }
  }

  if (!skeleton.roundingModeHalfUp()) {
    return nullptr;
  }

  return skeleton.toFormatter(cx, locale.get());
}

static UFormattedNumber* NewUFormattedNumber(JSContext* cx) {
  UErrorCode status = U_ZERO_ERROR;
  UFormattedNumber* formatted = unumf_openResult(&status);
  if (U_FAILURE(status)) {
    intl::ReportInternalError(cx);
    return nullptr;
  }
  return formatted;
}

static const UFormattedValue* PartitionNumberPattern(
    JSContext* cx, const UNumberFormatter* nf, UFormattedNumber* formatted,
    HandleValue x) {
  UErrorCode status = U_ZERO_ERROR;
  if (x.isNumber()) {
    double num = x.toNumber();

    // ICU incorrectly formats NaN values with the sign bit set, as if they
    // were negative.  Replace all NaNs with a single pattern with sign bit
    // unset ("positive", that is) until ICU is fixed.
    if (MOZ_UNLIKELY(IsNaN(num))) {
      num = SpecificNaN<double>(0, 1);
    }

    unumf_formatDouble(nf, num, formatted, &status);
  } else {
    RootedBigInt bi(cx, x.toBigInt());

    int64_t num;
    if (BigInt::isInt64(bi, &num)) {
      unumf_formatInt(nf, num, formatted, &status);
    } else {
      JSLinearString* str = BigInt::toString<CanGC>(cx, bi, 10);
      if (!str) {
        return nullptr;
      }
      MOZ_ASSERT(str->hasLatin1Chars());

      // Tell the analysis the |unumf_formatDecimal| function can't GC.
      JS::AutoSuppressGCAnalysis nogc;

      const char* chars = reinterpret_cast<const char*>(str->latin1Chars(nogc));
      unumf_formatDecimal(nf, chars, str->length(), formatted, &status);
    }
  }
  if (U_FAILURE(status)) {
    intl::ReportInternalError(cx);
    return nullptr;
  }

  const UFormattedValue* formattedValue =
      unumf_resultAsValue(formatted, &status);
  if (U_FAILURE(status)) {
    intl::ReportInternalError(cx);
    return nullptr;
  }
  return formattedValue;
}

static JSString* FormattedNumberToString(
    JSContext* cx, const UFormattedValue* formattedValue) {
  UErrorCode status = U_ZERO_ERROR;
  int32_t strLength;
  const char16_t* str = ufmtval_getString(formattedValue, &strLength, &status);
  if (U_FAILURE(status)) {
    intl::ReportInternalError(cx);
    return nullptr;
  }

  return NewStringCopyN<CanGC>(cx, str, AssertedCast<uint32_t>(strLength));
}

static bool FormatNumeric(JSContext* cx, const UNumberFormatter* nf,
                          UFormattedNumber* formatted, HandleValue x,
                          MutableHandleValue result) {
  const UFormattedValue* formattedValue =
      PartitionNumberPattern(cx, nf, formatted, x);
  if (!formattedValue) {
    return false;
  }

  JSString* str = FormattedNumberToString(cx, formattedValue);
  if (!str) {
    return false;
  }

  result.setString(str);
  return true;
}

enum class FormattingType { ForUnit, NotForUnit };

static FieldType GetFieldTypeForNumberField(UNumberFormatFields fieldName,
                                            HandleValue x,
                                            FormattingType formattingType) {
  // See intl/icu/source/i18n/unicode/unum.h for a detailed field list.  This
  // list is deliberately exhaustive: cases might have to be added/removed if
  // this code is compiled with a different ICU with more UNumberFormatFields
  // enum initializers.  Please guard such cases with appropriate ICU
  // version-testing #ifdefs, should cross-version divergence occur.
  switch (fieldName) {
    case UNUM_INTEGER_FIELD:
      if (x.isNumber()) {
        double d = x.toNumber();
        if (IsNaN(d)) {
          return &JSAtomState::nan;
        }
        if (!IsFinite(d)) {
          return &JSAtomState::infinity;
        }
      }
      return &JSAtomState::integer;

    case UNUM_GROUPING_SEPARATOR_FIELD:
      return &JSAtomState::group;

    case UNUM_DECIMAL_SEPARATOR_FIELD:
      return &JSAtomState::decimal;

    case UNUM_FRACTION_FIELD:
      return &JSAtomState::fraction;

    case UNUM_SIGN_FIELD: {
      // We coerce all NaNs to one with the sign bit unset, so all NaNs are
      // positive in our implementation.
      bool isNegative = x.isNumber()
                            ? !IsNaN(x.toNumber()) && IsNegative(x.toNumber())
                            : x.toBigInt()->isNegative();
      return isNegative ? &JSAtomState::minusSign : &JSAtomState::plusSign;
    }

    case UNUM_PERCENT_FIELD:
      // Percent fields are returned as "unit" elements when the number
      // formatter's style is "unit".
      if (formattingType == FormattingType::ForUnit) {
        return &JSAtomState::unit;
      }
      return &JSAtomState::percentSign;

    case UNUM_CURRENCY_FIELD:
      return &JSAtomState::currency;

    case UNUM_PERMILL_FIELD:
      MOZ_ASSERT_UNREACHABLE(
          "unexpected permill field found, even though "
          "we don't use any user-defined patterns that "
          "would require a permill field");
      break;

    case UNUM_EXPONENT_SYMBOL_FIELD:
      return &JSAtomState::exponentSeparator;

    case UNUM_EXPONENT_SIGN_FIELD:
      return &JSAtomState::exponentMinusSign;

    case UNUM_EXPONENT_FIELD:
      return &JSAtomState::exponentInteger;

    case UNUM_MEASURE_UNIT_FIELD:
      return &JSAtomState::unit;

    case UNUM_COMPACT_FIELD:
      return &JSAtomState::compact;

#ifndef U_HIDE_DEPRECATED_API
    case UNUM_FIELD_COUNT:
      MOZ_ASSERT_UNREACHABLE(
          "format field sentinel value returned by iterator!");
      break;
#endif
  }

  MOZ_ASSERT_UNREACHABLE(
      "unenumerated, undocumented format field returned by iterator");
  return nullptr;
}

struct Field {
  uint32_t begin;
  uint32_t end;
  FieldType type;

  // Needed for vector-resizing scratch space.
  Field() = default;

  Field(uint32_t begin, uint32_t end, FieldType type)
      : begin(begin), end(end), type(type) {}
};

class NumberFormatFields {
  using FieldsVector = Vector<Field, 16>;

  FieldsVector fields_;

 public:
  explicit NumberFormatFields(JSContext* cx) : fields_(cx) {}

  MOZ_MUST_USE bool append(FieldType type, int32_t begin, int32_t end);

  MOZ_MUST_USE ArrayObject* toArray(JSContext* cx,
                                    JS::HandleString overallResult,
                                    FieldType unitType);
};

bool NumberFormatFields::append(FieldType type, int32_t begin, int32_t end) {
  MOZ_ASSERT(begin >= 0);
  MOZ_ASSERT(end >= 0);
  MOZ_ASSERT(begin < end, "erm, aren't fields always non-empty?");

  return fields_.emplaceBack(uint32_t(begin), uint32_t(end), type);
}

ArrayObject* NumberFormatFields::toArray(JSContext* cx,
                                         HandleString overallResult,
                                         FieldType unitType) {
  // Merge sort the fields vector.  Expand the vector to have scratch space for
  // performing the sort.
  size_t fieldsLen = fields_.length();
  if (!fields_.growByUninitialized(fieldsLen)) {
    return nullptr;
  }

  MOZ_ALWAYS_TRUE(MergeSort(
      fields_.begin(), fieldsLen, fields_.begin() + fieldsLen,
      [](const Field& left, const Field& right, bool* lessOrEqual) {
        // Sort first by begin index, then to place
        // enclosing fields before nested fields.
        *lessOrEqual = left.begin < right.begin ||
                       (left.begin == right.begin && left.end > right.end);
        return true;
      }));

  // Delete the elements in the scratch space.
  fields_.shrinkBy(fieldsLen);

  // Then iterate over the sorted field list to generate a sequence of parts
  // (what ECMA-402 actually exposes).  A part is a maximal character sequence
  // entirely within no field or a single most-nested field.
  //
  // Diagrams may be helpful to illustrate how fields map to parts.  Consider
  // formatting -19,766,580,028,249.41, the US national surplus (negative
  // because it's actually a debt) on October 18, 2016.
  //
  //    var options =
  //      { style: "currency", currency: "USD", currencyDisplay: "name" };
  //    var usdFormatter = new Intl.NumberFormat("en-US", options);
  //    usdFormatter.format(-19766580028249.41);
  //
  // The formatted result is "-19,766,580,028,249.41 US dollars".  ICU
  // identifies these fields in the string:
  //
  //     UNUM_GROUPING_SEPARATOR_FIELD
  //                   |
  //   UNUM_SIGN_FIELD |  UNUM_DECIMAL_SEPARATOR_FIELD
  //    |   __________/|   |
  //    |  /   |   |   |   |
  //   "-19,766,580,028,249.41 US dollars"
  //     \________________/ |/ \_______/
  //             |          |      |
  //    UNUM_INTEGER_FIELD  |  UNUM_CURRENCY_FIELD
  //                        |
  //               UNUM_FRACTION_FIELD
  //
  // These fields map to parts as follows:
  //
  //         integer     decimal
  //       _____|________  |
  //      /  /| |\  |\  |\ |  literal
  //     /| / | | \ | \ | \|  |
  //   "-19,766,580,028,249.41 US dollars"
  //    |  \___|___|___/    |/ \________/
  //    |        |          |       |
  //    |      group        |   currency
  //    |                   |
  //   minusSign        fraction
  //
  // The sign is a part.  Each comma is a part, splitting the integer field
  // into parts for trillions/billions/&c. digits.  The decimal point is a
  // part.  Cents are a part.  The space between cents and currency is a part
  // (outside any field).  Last, the currency field is a part.
  //
  // Because parts fully partition the formatted string, we only track the
  // end of each part -- the beginning is implicitly the last part's end.
  struct Part {
    uint32_t end;
    FieldType type;
  };

  class PartGenerator {
    // The fields in order from start to end, then least to most nested.
    const FieldsVector& fields;

    // Index of the current field, in |fields|, being considered to
    // determine part boundaries.  |lastEnd <= fields[index].begin| is an
    // invariant.
    size_t index;

    // The end index of the last part produced, always less than or equal
    // to |limit|, strictly increasing.
    uint32_t lastEnd;

    // The length of the overall formatted string.
    const uint32_t limit;

    Vector<size_t, 4> enclosingFields;

    void popEnclosingFieldsEndingAt(uint32_t end) {
      MOZ_ASSERT_IF(enclosingFields.length() > 0,
                    fields[enclosingFields.back()].end >= end);

      while (enclosingFields.length() > 0 &&
             fields[enclosingFields.back()].end == end) {
        enclosingFields.popBack();
      }
    }

    bool nextPartInternal(Part* part) {
      size_t len = fields.length();
      MOZ_ASSERT(index <= len);

      // If we're out of fields, all that remains are part(s) consisting
      // of trailing portions of enclosing fields, and maybe a final
      // literal part.
      if (index == len) {
        if (enclosingFields.length() > 0) {
          const auto& enclosing = fields[enclosingFields.popCopy()];
          part->end = enclosing.end;
          part->type = enclosing.type;

          // If additional enclosing fields end where this part ends,
          // pop them as well.
          popEnclosingFieldsEndingAt(part->end);
        } else {
          part->end = limit;
          part->type = &JSAtomState::literal;
        }

        return true;
      }

      // Otherwise we still have a field to process.
      const Field* current = &fields[index];
      MOZ_ASSERT(lastEnd <= current->begin);
      MOZ_ASSERT(current->begin < current->end);

      // But first, deal with inter-field space.
      if (lastEnd < current->begin) {
        if (enclosingFields.length() > 0) {
          // Space between fields, within an enclosing field, is part
          // of that enclosing field, until the start of the current
          // field or the end of the enclosing field, whichever is
          // earlier.
          const auto& enclosing = fields[enclosingFields.back()];
          part->end = std::min(enclosing.end, current->begin);
          part->type = enclosing.type;
          popEnclosingFieldsEndingAt(part->end);
        } else {
          // If there's no enclosing field, the space is a literal.
          part->end = current->begin;
          part->type = &JSAtomState::literal;
        }

        return true;
      }

      // Otherwise, the part spans a prefix of the current field.  Find
      // the most-nested field containing that prefix.
      const Field* next;
      do {
        current = &fields[index];

        // If the current field is last, the part extends to its end.
        if (++index == len) {
          part->end = current->end;
          part->type = current->type;
          return true;
        }

        next = &fields[index];
        MOZ_ASSERT(current->begin <= next->begin);
        MOZ_ASSERT(current->begin < next->end);

        // If the next field nests within the current field, push an
        // enclosing field.  (If there are no nested fields, don't
        // bother pushing a field that'd be immediately popped.)
        if (current->end > next->begin) {
          if (!enclosingFields.append(index - 1)) {
            return false;
          }
        }

        // Do so until the next field begins after this one.
      } while (current->begin == next->begin);

      part->type = current->type;

      if (current->end <= next->begin) {
        // The next field begins after the current field ends.  Therefore
        // the current part ends at the end of the current field.
        part->end = current->end;
        popEnclosingFieldsEndingAt(part->end);
      } else {
        // The current field encloses the next one.  The current part
        // ends where the next field/part will start.
        part->end = next->begin;
      }

      return true;
    }

   public:
    PartGenerator(JSContext* cx, const FieldsVector& vec, uint32_t limit)
        : fields(vec),
          index(0),
          lastEnd(0),
          limit(limit),
          enclosingFields(cx) {}

    bool nextPart(bool* hasPart, Part* part) {
      // There are no parts left if we've partitioned the entire string.
      if (lastEnd == limit) {
        MOZ_ASSERT(enclosingFields.length() == 0);
        *hasPart = false;
        return true;
      }

      if (!nextPartInternal(part)) {
        return false;
      }

      *hasPart = true;
      lastEnd = part->end;
      return true;
    }
  };

  // Finally, generate the result array.
  size_t lastEndIndex = 0;
  RootedObject singlePart(cx);
  RootedValue propVal(cx);

  RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx));
  if (!partsArray) {
    return nullptr;
  }

  PartGenerator gen(cx, fields_, overallResult->length());
  do {
    bool hasPart;
    Part part;
    if (!gen.nextPart(&hasPart, &part)) {
      return nullptr;
    }

    if (!hasPart) {
      break;
    }

    FieldType type = part.type;
    size_t endIndex = part.end;

    MOZ_ASSERT(lastEndIndex < endIndex);

    singlePart = NewBuiltinClassInstance<PlainObject>(cx);
    if (!singlePart) {
      return nullptr;
    }

    propVal.setString(cx->names().*type);
    if (!DefineDataProperty(cx, singlePart, cx->names().type, propVal)) {
      return nullptr;
    }

    JSLinearString* partSubstr = NewDependentString(
        cx, overallResult, lastEndIndex, endIndex - lastEndIndex);
    if (!partSubstr) {
      return nullptr;
    }

    propVal.setString(partSubstr);
    if (!DefineDataProperty(cx, singlePart, cx->names().value, propVal)) {
      return nullptr;
    }

    if (unitType != nullptr && type != &JSAtomState::literal) {
      propVal.setString(cx->names().*unitType);
      if (!DefineDataProperty(cx, singlePart, cx->names().unit, propVal)) {
        return nullptr;
      }
    }

    if (!NewbornArrayPush(cx, partsArray, ObjectValue(*singlePart))) {
      return nullptr;
    }

    lastEndIndex = endIndex;
  } while (true);

  MOZ_ASSERT(lastEndIndex == overallResult->length(),
             "result array must partition the entire string");

  return partsArray;
}

static bool FormattedNumberToParts(JSContext* cx,
                                   const UFormattedValue* formattedValue,
                                   HandleValue number,
                                   FieldType relativeTimeUnit,
                                   FormattingType formattingType,
                                   MutableHandleValue result) {
  MOZ_ASSERT(number.isNumeric());

  RootedString overallResult(cx, FormattedNumberToString(cx, formattedValue));
  if (!overallResult) {
    return false;
  }

  UErrorCode status = U_ZERO_ERROR;
  UConstrainedFieldPosition* fpos = ucfpos_open(&status);
  if (U_FAILURE(status)) {
    intl::ReportInternalError(cx);
    return false;
  }
  ScopedICUObject<UConstrainedFieldPosition, ucfpos_close> toCloseFpos(fpos);

  // We're only interested in UFIELD_CATEGORY_NUMBER fields.
  ucfpos_constrainCategory(fpos, UFIELD_CATEGORY_NUMBER, &status);
  if (U_FAILURE(status)) {
    intl::ReportInternalError(cx);
    return false;
  }

  // Vacuum up fields in the overall formatted string.

  NumberFormatFields fields(cx);

  while (true) {
    bool hasMore = ufmtval_nextPosition(formattedValue, fpos, &status);
    if (U_FAILURE(status)) {
      intl::ReportInternalError(cx);
      return false;
    }
    if (!hasMore) {
      break;
    }

    int32_t field = ucfpos_getField(fpos, &status);
    if (U_FAILURE(status)) {
      intl::ReportInternalError(cx);
      return false;
    }

    int32_t beginIndex, endIndex;
    ucfpos_getIndexes(fpos, &beginIndex, &endIndex, &status);
    if (U_FAILURE(status)) {
      intl::ReportInternalError(cx);
      return false;
    }

    FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(field),
                                                number, formattingType);

    if (!fields.append(type, beginIndex, endIndex)) {
      return false;
    }
  }

  ArrayObject* array = fields.toArray(cx, overallResult, relativeTimeUnit);
  if (!array) {
    return false;
  }

  result.setObject(*array);
  return true;
}

bool js::intl::FormattedRelativeTimeToParts(
    JSContext* cx, const UFormattedValue* formattedValue, double timeValue,
    FieldType relativeTimeUnit, MutableHandleValue result) {
  Value tval = DoubleValue(timeValue);
  return FormattedNumberToParts(
      cx, formattedValue, HandleValue::fromMarkedLocation(&tval),
      relativeTimeUnit, FormattingType::NotForUnit, result);
}

static bool FormatNumericToParts(JSContext* cx, const UNumberFormatter* nf,
                                 UFormattedNumber* formatted, HandleValue x,
                                 FormattingType formattingType,
                                 MutableHandleValue result) {
  const UFormattedValue* formattedValue =
      PartitionNumberPattern(cx, nf, formatted, x);
  if (!formattedValue) {
    return false;
  }

  return FormattedNumberToParts(cx, formattedValue, x, nullptr, formattingType,
                                result);
}

bool js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  MOZ_ASSERT(args.length() == 4);
  MOZ_ASSERT(args[0].isObject());
  MOZ_ASSERT(args[1].isNumeric());
  MOZ_ASSERT(args[2].isBoolean());
  MOZ_ASSERT(args[3].isBoolean());

  Rooted<NumberFormatObject*> numberFormat(
      cx, &args[0].toObject().as<NumberFormatObject>());

  // Obtain a cached UNumberFormatter object.
  UNumberFormatter* nf = numberFormat->getNumberFormatter();
  if (!nf) {
    nf = NewUNumberFormatter(cx, numberFormat);
    if (!nf) {
      return false;
    }
    numberFormat->setNumberFormatter(nf);

    intl::AddICUCellMemory(numberFormat,
                           NumberFormatObject::EstimatedMemoryUse);
  }

  // Obtain a cached UFormattedNumber object.
  UFormattedNumber* formatted = numberFormat->getFormattedNumber();
  if (!formatted) {
    formatted = NewUFormattedNumber(cx);
    if (!formatted) {
      return false;
    }
    numberFormat->setFormattedNumber(formatted);

    // UFormattedNumber memory tracked as part of UNumberFormatter.
  }

  // Use the UNumberFormatter to actually format the number.
  if (args[2].toBoolean()) {
    FormattingType formattingType = args[3].toBoolean()
                                        ? FormattingType::ForUnit
                                        : FormattingType::NotForUnit;
    return FormatNumericToParts(cx, nf, formatted, args[1], formattingType,
                                args.rval());
  }

  return FormatNumeric(cx, nf, formatted, args[1], args.rval());
}