Bug 1289882 - Implement an experimental, off-by-default Intl.NumberFormat.prototype.formatToParts function. r=anba
authorJeff Walden <jwalden@mit.edu>
Mon, 08 Aug 2016 13:34:37 -0700
changeset 354553 f849271896d3fbde28471c8e362aa7f7da300b50
parent 354552 d33f8588693cacaa94c3b1c3536d7042295dbdc7
child 354554 00fde28274fa2afb2bdaacc9152046269c7e9db8
push id10621
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 16:02:43 +0000
treeherdermozilla-aurora@dca7b42e6c67 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersanba
bugs1289882
milestone53.0a1
Bug 1289882 - Implement an experimental, off-by-default Intl.NumberFormat.prototype.formatToParts function. r=anba
intl/icu-patches/unum_formatDoubleForFields.diff
intl/icu/source/i18n/unicode/unum.h
intl/icu/source/i18n/unum.cpp
intl/update-icu.sh
js/src/builtin/Intl.cpp
js/src/builtin/Intl.js
js/src/builtin/Number.js
js/src/jsapi.h
js/src/shell/js.cpp
js/src/tests/Intl/NumberFormat/formatToParts.js
js/src/vm/CommonPropertyNames.h
new file mode 100644
--- /dev/null
+++ b/intl/icu-patches/unum_formatDoubleForFields.diff
@@ -0,0 +1,134 @@
+Add an ICU API for formatting a number into constituent parts (sign, integer, grouping separator, decimal, fraction, &c.) for use in implementing Intl.NumberFormat.prototype.formatToParts.
+
+https://ssl.icu-project.org/trac/ticket/12684
+
+diff --git a/intl/icu/source/i18n/unicode/unum.h b/intl/icu/source/i18n/unicode/unum.h
+--- a/intl/icu/source/i18n/unicode/unum.h
++++ b/intl/icu/source/i18n/unicode/unum.h
+@@ -20,16 +20,17 @@
+ 
+ #include "unicode/localpointer.h"
+ #include "unicode/uloc.h"
+ #include "unicode/ucurr.h"
+ #include "unicode/umisc.h"
+ #include "unicode/parseerr.h"
+ #include "unicode/uformattable.h"
+ #include "unicode/udisplaycontext.h"
++#include "unicode/ufieldpositer.h"
+ 
+ /**
+  * \file
+  * \brief C API: NumberFormat
+  *
+  * <h2> Number Format C API </h2>
+  *
+  * Number Format C API  Provides functions for
+@@ -647,16 +648,67 @@ U_STABLE int32_t U_EXPORT2
+ unum_formatUFormattable(const UNumberFormat* fmt,
+                         const UFormattable *number,
+                         UChar *result,
+                         int32_t resultLength,
+                         UFieldPosition *pos,
+                         UErrorCode *status);
+ 
+ /**
++* Format a double using a UNumberFormat according to the UNumberFormat's locale,
++* and initialize a UFieldPositionIterator that enumerates the subcomponents of
++* the resulting string.
++*
++* @param format
++*          The formatter to use.
++* @param number
++*          The number to format.
++* @param result
++*          A pointer to a buffer to receive the NULL-terminated formatted
++*          number. If the formatted number fits into dest but cannot be
++*          NULL-terminated (length == resultLength) then the error code is set
++*          to U_STRING_NOT_TERMINATED_WARNING. If the formatted number doesn't
++*          fit into result then the error code is set to
++*          U_BUFFER_OVERFLOW_ERROR.
++* @param resultLength
++*          The maximum size of result.
++* @param fpositer
++*          A pointer to a UFieldPositionIterator created by {@link #ufieldpositer_open}
++*          (may be NULL if field position information is not needed, but in this
++*          case it's preferable to use {@link #unum_formatDouble}). Iteration
++*          information already present in the UFieldPositionIterator is deleted,
++*          and the iterator is reset to apply to the fields in the formatted
++*          string created by this function call. The field values and indexes
++*          returned by {@link #ufieldpositer_next} represent fields denoted by
++*          the UNumberFormatFields enum. Fields are not returned in a guaranteed
++*          order. Fields cannot overlap, but they may nest. For example, 1234
++*          could format as "1,234" which might consist of a grouping separator
++*          field for ',' and an integer field encompassing the entire string.
++* @param status
++*          A pointer to an UErrorCode to receive any errors
++* @return
++*          The total buffer size needed; if greater than resultLength, the
++*          output was truncated.
++* @see unum_formatDouble
++* @see unum_parse
++* @see unum_parseDouble
++* @see UFieldPositionIterator
++* @see UNumberFormatFields
++* @draft ICU 59
++*/
++U_DRAFT int32_t U_EXPORT2
++unum_formatDoubleForFields(const UNumberFormat* format,
++                           double number,
++                           UChar* result,
++                           int32_t resultLength,
++                           UFieldPositionIterator* fpositer,
++                           UErrorCode* status);
++#define ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS
++
++/**
+ * Parse a string into an integer using a UNumberFormat.
+ * The string will be parsed according to the UNumberFormat's locale.
+ * Note: parsing is not supported for styles UNUM_DECIMAL_COMPACT_SHORT
+ * and UNUM_DECIMAL_COMPACT_LONG.
+ * @param fmt The formatter to use.
+ * @param text The text to parse.
+ * @param textLength The length of text, or -1 if null-terminated.
+ * @param parsePos If not NULL, on input a pointer to an integer specifying the offset at which
+diff --git a/intl/icu/source/i18n/unum.cpp b/intl/icu/source/i18n/unum.cpp
+--- a/intl/icu/source/i18n/unum.cpp
++++ b/intl/icu/source/i18n/unum.cpp
+@@ -870,9 +870,37 @@ unum_formatUFormattable(const UNumberFor
+     if(pos != 0) {
+         pos->beginIndex = fp.getBeginIndex();
+         pos->endIndex = fp.getEndIndex();
+     }
+ 
+     return res.extract(result, resultLength, *status);
+ }
+ 
++U_CAPI int32_t U_EXPORT2
++unum_formatDoubleForFields(const UNumberFormat* format,
++                           double number,
++                           UChar* result,
++                           int32_t resultLength,
++                           UFieldPositionIterator* fpositer,
++                           UErrorCode* status)
++{
++    if (U_FAILURE(*status))
++        return -1;
++
++    if (result == NULL ? resultLength != 0 : resultLength < 0) {
++        *status = U_ILLEGAL_ARGUMENT_ERROR;
++        return -1;
++    }
++
++    UnicodeString res;
++    if (result != NULL) {
++        // NULL destination for pure preflighting: empty dummy string
++        // otherwise, alias the destination buffer
++        res.setTo(result, 0, resultLength);
++    }
++
++    ((const NumberFormat*)format)->format(number, res, (FieldPositionIterator*)fpositer, *status);
++
++    return res.extract(result, resultLength, *status);
++}
++
+ #endif /* #if !UCONFIG_NO_FORMATTING */
--- a/intl/icu/source/i18n/unicode/unum.h
+++ b/intl/icu/source/i18n/unicode/unum.h
@@ -20,16 +20,17 @@
 
 #include "unicode/localpointer.h"
 #include "unicode/uloc.h"
 #include "unicode/ucurr.h"
 #include "unicode/umisc.h"
 #include "unicode/parseerr.h"
 #include "unicode/uformattable.h"
 #include "unicode/udisplaycontext.h"
+#include "unicode/ufieldpositer.h"
 
 /**
  * \file
  * \brief C API: NumberFormat
  *
  * <h2> Number Format C API </h2>
  *
  * Number Format C API  Provides functions for
@@ -647,16 +648,67 @@ U_STABLE int32_t U_EXPORT2
 unum_formatUFormattable(const UNumberFormat* fmt,
                         const UFormattable *number,
                         UChar *result,
                         int32_t resultLength,
                         UFieldPosition *pos,
                         UErrorCode *status);
 
 /**
+* Format a double using a UNumberFormat according to the UNumberFormat's locale,
+* and initialize a UFieldPositionIterator that enumerates the subcomponents of
+* the resulting string.
+*
+* @param format
+*          The formatter to use.
+* @param number
+*          The number to format.
+* @param result
+*          A pointer to a buffer to receive the NULL-terminated formatted
+*          number. If the formatted number fits into dest but cannot be
+*          NULL-terminated (length == resultLength) then the error code is set
+*          to U_STRING_NOT_TERMINATED_WARNING. If the formatted number doesn't
+*          fit into result then the error code is set to
+*          U_BUFFER_OVERFLOW_ERROR.
+* @param resultLength
+*          The maximum size of result.
+* @param fpositer
+*          A pointer to a UFieldPositionIterator created by {@link #ufieldpositer_open}
+*          (may be NULL if field position information is not needed, but in this
+*          case it's preferable to use {@link #unum_formatDouble}). Iteration
+*          information already present in the UFieldPositionIterator is deleted,
+*          and the iterator is reset to apply to the fields in the formatted
+*          string created by this function call. The field values and indexes
+*          returned by {@link #ufieldpositer_next} represent fields denoted by
+*          the UNumberFormatFields enum. Fields are not returned in a guaranteed
+*          order. Fields cannot overlap, but they may nest. For example, 1234
+*          could format as "1,234" which might consist of a grouping separator
+*          field for ',' and an integer field encompassing the entire string.
+* @param status
+*          A pointer to an UErrorCode to receive any errors
+* @return
+*          The total buffer size needed; if greater than resultLength, the
+*          output was truncated.
+* @see unum_formatDouble
+* @see unum_parse
+* @see unum_parseDouble
+* @see UFieldPositionIterator
+* @see UNumberFormatFields
+* @draft ICU 59
+*/
+U_DRAFT int32_t U_EXPORT2
+unum_formatDoubleForFields(const UNumberFormat* format,
+                           double number,
+                           UChar* result,
+                           int32_t resultLength,
+                           UFieldPositionIterator* fpositer,
+                           UErrorCode* status);
+#define ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS
+
+/**
 * Parse a string into an integer using a UNumberFormat.
 * The string will be parsed according to the UNumberFormat's locale.
 * Note: parsing is not supported for styles UNUM_DECIMAL_COMPACT_SHORT
 * and UNUM_DECIMAL_COMPACT_LONG.
 * @param fmt The formatter to use.
 * @param text The text to parse.
 * @param textLength The length of text, or -1 if null-terminated.
 * @param parsePos If not NULL, on input a pointer to an integer specifying the offset at which
--- a/intl/icu/source/i18n/unum.cpp
+++ b/intl/icu/source/i18n/unum.cpp
@@ -870,9 +870,37 @@ unum_formatUFormattable(const UNumberFor
     if(pos != 0) {
         pos->beginIndex = fp.getBeginIndex();
         pos->endIndex = fp.getEndIndex();
     }
 
     return res.extract(result, resultLength, *status);
 }
 
+U_CAPI int32_t U_EXPORT2
+unum_formatDoubleForFields(const UNumberFormat* format,
+                           double number,
+                           UChar* result,
+                           int32_t resultLength,
+                           UFieldPositionIterator* fpositer,
+                           UErrorCode* status)
+{
+    if (U_FAILURE(*status))
+        return -1;
+
+    if (result == NULL ? resultLength != 0 : resultLength < 0) {
+        *status = U_ILLEGAL_ARGUMENT_ERROR;
+        return -1;
+    }
+
+    UnicodeString res;
+    if (result != NULL) {
+        // NULL destination for pure preflighting: empty dummy string
+        // otherwise, alias the destination buffer
+        res.setTo(result, 0, resultLength);
+    }
+
+    ((const NumberFormat*)format)->format(number, res, (FieldPositionIterator*)fpositer, *status);
+
+    return res.extract(result, resultLength, *status);
+}
+
 #endif /* #if !UCONFIG_NO_FORMATTING */
--- a/intl/update-icu.sh
+++ b/intl/update-icu.sh
@@ -57,16 +57,17 @@ svn info $1 | grep -v '^Revision: [[:dig
 
 for patch in \
  bug-915735 \
  suppress-warnings.diff \
  bug-1172609-timezone-recreateDefault.diff \
  bug-1198952-workaround-make-3.82-bug.diff \
  bug-1228227-bug-1263325-libc++-gcc_hidden.diff \
  ucol_getKeywordValuesForLocale-ulist_resetList.diff \
+ unum_formatDoubleForFields.diff \
 ; do
   echo "Applying local patch $patch"
   patch -d ${icu_dir}/../../ -p1 --no-backup-if-mismatch < ${icu_dir}/../icu-patches/$patch
 done
 
 topsrcdir=`dirname $0`/../
 python ${topsrcdir}/js/src/tests/ecma_6/String/make-normalize-generateddata-input.py $topsrcdir
 
--- a/js/src/builtin/Intl.cpp
+++ b/js/src/builtin/Intl.cpp
@@ -8,27 +8,27 @@
  * The Intl module specified by standard ECMA-402,
  * ECMAScript Internationalization API Specification.
  */
 
 #include "builtin/Intl.h"
 
 #include "mozilla/PodOperations.h"
 #include "mozilla/Range.h"
-#include "mozilla/ScopeExit.h"
 
 #include <string.h>
 
 #include "jsapi.h"
 #include "jsatom.h"
 #include "jscntxt.h"
 #include "jsobj.h"
 #include "jsstr.h"
 
 #include "builtin/IntlTimeZoneData.h"
+#include "ds/Sort.h"
 #if ENABLE_INTL_API
 #include "unicode/ucal.h"
 #include "unicode/ucol.h"
 #include "unicode/udat.h"
 #include "unicode/udatpg.h"
 #include "unicode/uenum.h"
 #include "unicode/unum.h"
 #include "unicode/unumsys.h"
@@ -43,18 +43,18 @@
 
 #include "jsobjinlines.h"
 
 #include "vm/NativeObject-inl.h"
 
 using namespace js;
 
 using mozilla::IsFinite;
+using mozilla::IsNaN;
 using mozilla::IsNegativeZero;
-using mozilla::MakeScopeExit;
 using mozilla::PodCopy;
 using mozilla::Range;
 using mozilla::RangedPtr;
 
 /*
  * Pervasive note: ICU functions taking a UErrorCode in/out parameter always
  * test that parameter before doing anything, and will return immediately if
  * the value indicates that a failure occurred in a prior ICU call,
@@ -263,23 +263,52 @@ unum_open(UNumberFormatStyle style, cons
 }
 
 void
 unum_setAttribute(UNumberFormat* fmt, UNumberFormatAttribute  attr, int32_t newValue)
 {
     MOZ_CRASH("unum_setAttribute: Intl API disabled");
 }
 
+#if defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+
+int32_t
+unum_formatDoubleForFields(const UNumberFormat* fmt, double number, UChar* result,
+                           int32_t resultLength, UFieldPositionIterator* fpositer,
+                           UErrorCode* status)
+{
+    MOZ_CRASH("unum_formatDoubleForFields: Intl API disabled");
+}
+
+enum UNumberFormatFields {
+    UNUM_INTEGER_FIELD,
+    UNUM_GROUPING_SEPARATOR_FIELD,
+    UNUM_DECIMAL_SEPARATOR_FIELD,
+    UNUM_FRACTION_FIELD,
+    UNUM_SIGN_FIELD,
+    UNUM_PERCENT_FIELD,
+    UNUM_CURRENCY_FIELD,
+    UNUM_PERMILL_FIELD,
+    UNUM_EXPONENT_SYMBOL_FIELD,
+    UNUM_EXPONENT_SIGN_FIELD,
+    UNUM_EXPONENT_FIELD,
+    UNUM_FIELD_COUNT,
+};
+
+#else
+
 int32_t
 unum_formatDouble(const UNumberFormat* fmt, double number, UChar* result,
                   int32_t resultLength, UFieldPosition* pos, UErrorCode* status)
 {
     MOZ_CRASH("unum_formatDouble: Intl API disabled");
 }
 
+#endif // defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+
 void
 unum_close(UNumberFormat* fmt)
 {
     MOZ_CRASH("unum_close: Intl API disabled");
 }
 
 void
 unum_setTextAttribute(UNumberFormat* fmt, UNumberFormatTextAttribute tag, const UChar* newValue,
@@ -1499,16 +1528,34 @@ CreateNumberFormatPrototype(JSContext* c
     }
     if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue,
                         JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()),
                         nullptr, JSPROP_GETTER | JSPROP_SHARED))
     {
         return nullptr;
     }
 
+#if defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+    // If the still-experimental NumberFormat.prototype.formatToParts method is
+    // enabled, also add it.
+    if (cx->compartment()->creationOptions().experimentalNumberFormatFormatToPartsEnabled()) {
+        RootedValue ftp(cx);
+        HandlePropertyName name = cx->names().formatToParts;
+        if (!GlobalObject::getSelfHostedFunction(cx, cx->global(),
+                                                 cx->names().NumberFormatFormatToParts,
+                                                 name, 1, &ftp))
+        {
+            return nullptr;
+        }
+
+        if (!DefineProperty(cx, proto, cx->names().formatToParts, ftp, nullptr, nullptr, 0))
+            return nullptr;
+    }
+#endif // defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+
     RootedValue options(cx);
     if (!CreateDefaultOptions(cx, &options))
         return nullptr;
 
     // 11.2.1 and 11.3
     if (!IntlInitialize(cx, proto, cx->names().InitializeNumberFormat, UndefinedHandleValue,
                         options))
     {
@@ -1705,57 +1752,497 @@ NewUNumberFormat(JSContext* cx, HandleOb
         unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits);
     }
     unum_setAttribute(nf, UNUM_GROUPING_USED, uUseGrouping);
     unum_setAttribute(nf, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP);
 
     return toClose.forget();
 }
 
+using FormattedNumberChars = Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE>;
+
 static bool
-intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result)
+PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, double* x,
+                       UFieldPositionIterator* fpositer, FormattedNumberChars& formattedChars)
 {
-    // FormatNumber doesn't consider -0.0 to be negative.
-    if (IsNegativeZero(x))
-        x = 0.0;
-
-    Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE> chars(cx);
-    if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE))
-        return false;
+    // PartitionNumberPattern doesn't consider -0.0 to be negative.
+    if (IsNegativeZero(*x))
+        *x = 0.0;
+
+    MOZ_ASSERT(formattedChars.length() == 0,
+               "formattedChars must initially be empty");
+    MOZ_ALWAYS_TRUE(formattedChars.resize(INITIAL_CHAR_BUFFER_SIZE));
+
     UErrorCode status = U_ZERO_ERROR;
-    int32_t size = unum_formatDouble(nf, x, Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE,
-                                     nullptr, &status);
+
+#if !defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+    MOZ_ASSERT(fpositer == nullptr,
+               "shouldn't be requesting field information from an ICU that "
+               "can't provide it");
+#endif
+
+    int32_t resultSize;
+#if defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+    resultSize =
+        unum_formatDoubleForFields(nf, *x,
+                                   Char16ToUChar(formattedChars.begin()), INITIAL_CHAR_BUFFER_SIZE,
+                                   fpositer, &status);
+#else
+    resultSize =
+        unum_formatDouble(nf, *x, Char16ToUChar(formattedChars.begin()), INITIAL_CHAR_BUFFER_SIZE,
+                          nullptr, &status);
+#endif // defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
     if (status == U_BUFFER_OVERFLOW_ERROR) {
-        MOZ_ASSERT(size >= 0);
-        if (!chars.resize(size))
+        MOZ_ASSERT(resultSize >= 0);
+        if (!formattedChars.resize(size_t(resultSize)))
             return false;
         status = U_ZERO_ERROR;
-        unum_formatDouble(nf, x, Char16ToUChar(chars.begin()), size, nullptr, &status);
+#ifdef DEBUG
+        int32_t size =
+#endif
+#if defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+            unum_formatDoubleForFields(nf, *x, Char16ToUChar(formattedChars.begin()), resultSize,
+                                       fpositer, &status);
+#else
+            unum_formatDouble(nf, *x, Char16ToUChar(formattedChars.begin()), resultSize,
+                              nullptr, &status);
+#endif // defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+        MOZ_ASSERT(size == resultSize);
     }
     if (U_FAILURE(status)) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
         return false;
     }
 
-    MOZ_ASSERT(size >= 0);
-    JSString* str = NewStringCopyN<CanGC>(cx, chars.begin(), size);
+    MOZ_ASSERT(resultSize >= 0);
+    return formattedChars.resize(size_t(resultSize));
+}
+
+static bool
+intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result)
+{
+    // Passing null for |fpositer| will just not compute partition information,
+    // letting us common up all ICU number-formatting code.
+    FormattedNumberChars chars(cx);
+    if (!PartitionNumberPattern(cx, nf, &x, nullptr, chars))
+        return false;
+
+    JSString* str = NewStringCopyN<CanGC>(cx, chars.begin(), chars.length());
     if (!str)
         return false;
 
     result.setString(str);
     return true;
 }
 
+using FieldType = ImmutablePropertyNamePtr JSAtomState::*;
+
+static FieldType
+GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d)
+{
+    // 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 (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: {
+        MOZ_ASSERT(!IsNegativeZero(d),
+                   "-0 should have been excluded by PartitionNumberPattern");
+
+        // Manual trawling through the ICU call graph appears to indicate that
+        // the basic formatting we request will never include a positive sign.
+        // But this analysis may be mistaken, so don't absolutely trust it.
+        return d < 0 ? &JSAtomState::minusSign : &JSAtomState::plusSign;
+      }
+
+      case UNUM_PERCENT_FIELD:
+        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");
+
+      case UNUM_EXPONENT_SYMBOL_FIELD:
+      case UNUM_EXPONENT_SIGN_FIELD:
+      case UNUM_EXPONENT_FIELD:
+        MOZ_ASSERT_UNREACHABLE("exponent field unexpectedly found in "
+                               "formatted number, even though UNUM_SCIENTIFIC "
+                               "and scientific notation were never requested");
+
+      case UNUM_FIELD_COUNT:
+        MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by "
+                               "iterator!");
+    }
+
+    MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned "
+                           "by iterator");
+    return nullptr;
+}
+
+#if defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+
+static bool
+intl_FormatNumberToParts(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result)
+{
+    UErrorCode status = U_ZERO_ERROR;
+
+    UFieldPositionIterator* fpositer = ufieldpositer_open(&status);
+    if (U_FAILURE(status)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
+        return false;
+    }
+
+    MOZ_ASSERT(fpositer);
+    ScopedICUObject<UFieldPositionIterator, ufieldpositer_close> toClose(fpositer);
+
+    FormattedNumberChars chars(cx);
+    if (!PartitionNumberPattern(cx, nf, &x, fpositer, chars))
+        return false;
+
+    RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx));
+    if (!partsArray)
+        return false;
+
+    RootedString overallResult(cx, NewStringCopyN<CanGC>(cx, chars.begin(), chars.length()));
+    if (!overallResult)
+        return false;
+
+    // First, vacuum up fields in the overall formatted string.
+
+    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)
+        {}
+    };
+
+    using FieldsVector = Vector<Field, 16>;
+    FieldsVector fields(cx);
+
+    int32_t fieldInt, beginIndexInt, endIndexInt;
+    while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) {
+        MOZ_ASSERT(beginIndexInt >= 0);
+        MOZ_ASSERT(endIndexInt >= 0);
+        MOZ_ASSERT(beginIndexInt < endIndexInt,
+                   "erm, aren't fields always non-empty?");
+
+        FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(fieldInt), x);
+        if (!fields.emplaceBack(uint32_t(beginIndexInt), uint32_t(endIndexInt), type))
+            return false;
+    }
+
+    // Second, merge sort the fields vector.  Expand the vector to have scratch
+    // space for performing the sort.
+    size_t fieldsLen = fields.length();
+    if (!fields.resizeUninitialized(fieldsLen * 2))
+        return false;
+
+    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;
+                              }));
+
+    // Deallocate the scratch space.
+    if (!fields.resize(fieldsLen))
+        return false;
+
+    // Third, 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;
+    uint32_t partIndex = 0;
+    RootedObject singlePart(cx);
+    RootedValue propVal(cx);
+
+    PartGenerator gen(cx, fields, chars.length());
+    do {
+        bool hasPart;
+        Part part;
+        if (!gen.nextPart(&hasPart, &part))
+            return false;
+
+        if (!hasPart)
+            break;
+
+        FieldType type = part.type;
+        size_t endIndex = part.end;
+
+        MOZ_ASSERT(lastEndIndex < endIndex);
+
+        singlePart = NewBuiltinClassInstance<PlainObject>(cx);
+        if (!singlePart)
+            return false;
+
+        propVal.setString(cx->names().*type);
+        if (!DefineProperty(cx, singlePart, cx->names().type, propVal))
+            return false;
+
+        JSLinearString* partSubstr =
+            NewDependentString(cx, overallResult, lastEndIndex, endIndex - lastEndIndex);
+        if (!partSubstr)
+            return false;
+
+        propVal.setString(partSubstr);
+        if (!DefineProperty(cx, singlePart, cx->names().value, propVal))
+            return false;
+
+        propVal.setObject(*singlePart);
+        if (!DefineElement(cx, partsArray, partIndex, propVal))
+            return false;
+
+        lastEndIndex = endIndex;
+        partIndex++;
+    } while (true);
+
+    MOZ_ASSERT(lastEndIndex == chars.length(),
+               "result array must partition the entire string");
+
+    result.setObject(*partsArray);
+    return true;
+}
+
+#endif // defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+
 bool
 js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
-    MOZ_ASSERT(args.length() == 2);
+    MOZ_ASSERT(args.length() == 3);
     MOZ_ASSERT(args[0].isObject());
     MOZ_ASSERT(args[1].isNumber());
+    MOZ_ASSERT(args[2].isBoolean());
 
     RootedObject numberFormat(cx, &args[0].toObject());
 
     // Obtain a UNumberFormat object, cached if possible.
     bool isNumberFormatInstance = numberFormat->getClass() == &NumberFormatClass;
     UNumberFormat* nf;
     if (isNumberFormatInstance) {
         void* priv =
@@ -1773,18 +2260,31 @@ js::intl_FormatNumber(JSContext* cx, uns
         // NumberFormat instance. One possibility might be to add a
         // NumberFormat instance as an internal property to each such object.
         nf = NewUNumberFormat(cx, numberFormat);
         if (!nf)
             return false;
     }
 
     // Use the UNumberFormat to actually format the number.
+    double d = args[1].toNumber();
     RootedValue result(cx);
-    bool success = intl_FormatNumber(cx, nf, args[1].toNumber(), &result);
+
+    bool success;
+#if defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+    if (args[2].toBoolean()) {
+        success = intl_FormatNumberToParts(cx, nf, d, &result);
+    } else
+#endif // defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS)
+    {
+        MOZ_ASSERT(!args[2].toBoolean(),
+                   "shouldn't be doing formatToParts without an ICU that "
+                   "supports it");
+        success = intl_FormatNumber(cx, nf, d, &result);
+    }
 
     if (!isNumberFormatInstance)
         unum_close(nf);
     if (!success)
         return false;
     args.rval().set(result);
     return true;
 }
@@ -2663,18 +3163,16 @@ intl_FormatDateTime(JSContext* cx, UDate
     if (!str)
         return false;
 
     result.setString(str);
 
     return true;
 }
 
-using FieldType = ImmutablePropertyNamePtr JSAtomState::*;
-
 static FieldType
 GetFieldTypeForFormatField(UDateFormatField fieldName)
 {
     // See intl/icu/source/i18n/unicode/udat.h for a detailed field list.  This
     // switch is deliberately exhaustive: cases might have to be added/removed
     // if this code is compiled with a different ICU with more
     // UDateFormatField enum initializers.  Please guard such cases with
     // appropriate ICU version-testing #ifdefs, should cross-version divergence
@@ -2769,17 +3267,17 @@ intl_FormatToPartsDateTime(JSContext* cx
         return false;
 
     UErrorCode status = U_ZERO_ERROR;
     UFieldPositionIterator* fpositer = ufieldpositer_open(&status);
     if (U_FAILURE(status)) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
         return false;
     }
-    auto closeFieldPosIter = MakeScopeExit([&]() { ufieldpositer_close(fpositer); });
+    ScopedICUObject<UFieldPositionIterator, ufieldpositer_close> toClose(fpositer);
 
     int32_t resultSize =
         udat_formatForFields(df, x, Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE,
                              fpositer, &status);
     if (status == U_BUFFER_OVERFLOW_ERROR) {
         MOZ_ASSERT(resultSize >= 0);
         if (!chars.resize(resultSize))
             return false;
@@ -2806,29 +3304,29 @@ intl_FormatToPartsDateTime(JSContext* cx
     if (!overallResult)
         return false;
 
     size_t lastEndIndex = 0;
 
     uint32_t partIndex = 0;
     RootedObject singlePart(cx);
     RootedValue partType(cx);
-    RootedString partSubstr(cx);
     RootedValue val(cx);
 
     auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) {
         singlePart = NewBuiltinClassInstance<PlainObject>(cx);
         if (!singlePart)
             return false;
 
         partType = StringValue(cx->names().*type);
         if (!DefineProperty(cx, singlePart, cx->names().type, partType))
             return false;
 
-        partSubstr = SubstringKernel(cx, overallResult, beginIndex, endIndex - beginIndex);
+        JSLinearString* partSubstr =
+            NewDependentString(cx, overallResult, beginIndex, endIndex - beginIndex);
         if (!partSubstr)
             return false;
 
         val = StringValue(partSubstr);
         if (!DefineProperty(cx, singlePart, cx->names().value, val))
             return false;
 
         val = ObjectValue(*singlePart);
--- a/js/src/builtin/Intl.js
+++ b/js/src/builtin/Intl.js
@@ -2096,17 +2096,17 @@ function numberFormatLocaleData(locale) 
  * Spec: ECMAScript Internationalization API Specification, 11.3.2.
  */
 function numberFormatFormatToBind(value) {
     // Steps 1.a.i implemented by ECMAScript declaration binding instantiation,
     // ES5.1 10.5, step 4.d.ii.
 
     // Step 1.a.ii-iii.
     var x = ToNumber(value);
-    return intl_FormatNumber(this, x);
+    return intl_FormatNumber(this, x, /* formatToParts = */ false);
 }
 
 
 /**
  * Returns a function bound to this NumberFormat that returns a String value
  * representing the result of calling ToNumber(value) according to the
  * effective locale and the formatting options of this NumberFormat.
  *
@@ -2125,16 +2125,31 @@ function Intl_NumberFormat_format_get() 
         var bf = callFunction(FunctionBind, F, this);
         internals.boundFormat = bf;
     }
     // Step 2.
     return internals.boundFormat;
 }
 
 
+function Intl_NumberFormat_formatToParts(value) {
+    // Step 1.
+    var nf = this;
+
+    // Steps 2-3.
+    getNumberFormatInternals(nf, "formatToParts");
+
+    // Step 4.
+    var x = ToNumber(value);
+
+    // Step 5.
+    return intl_FormatNumber(nf, x, /* formatToParts = */ true);
+}
+
+
 /**
  * Returns the resolved options for a NumberFormat object.
  *
  * Spec: ECMAScript Internationalization API Specification, 11.3.3 and 11.4.
  */
 function Intl_NumberFormat_resolvedOptions() {
     // Check "this NumberFormat object" per introduction of section 11.3.
     var internals = getNumberFormatInternals(this, "resolvedOptions");
--- a/js/src/builtin/Number.js
+++ b/js/src/builtin/Number.js
@@ -31,17 +31,17 @@ function Number_toLocaleString() {
         if (numberFormatCache.numberFormat === undefined)
             numberFormatCache.numberFormat = intl_NumberFormat(locales, options);
         numberFormat = numberFormatCache.numberFormat;
     } else {
         numberFormat = intl_NumberFormat(locales, options);
     }
 
     // Step 5.
-    return intl_FormatNumber(numberFormat, x);
+    return intl_FormatNumber(numberFormat, x, /* formatToParts = */ false);
 }
 
 // ES6 draft ES6 20.1.2.4
 function Number_isFinite(num) {
     if (typeof num !== "number")
         return false;
     return num - num === 0;
 }
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -2190,16 +2190,17 @@ class JS_PUBLIC_API(CompartmentCreationO
   public:
     CompartmentCreationOptions()
       : addonId_(nullptr),
         traceGlobal_(nullptr),
         invisibleToDebugger_(false),
         mergeable_(false),
         preserveJitCode_(false),
         cloneSingletons_(false),
+        experimentalNumberFormatFormatToPartsEnabled_(false),
         sharedMemoryAndAtomics_(false),
         secureContext_(false)
     {
         zone_.spec = JS::FreshZone;
     }
 
     // A null add-on ID means that the compartment is not associated with an
     // add-on.
@@ -2254,16 +2255,33 @@ class JS_PUBLIC_API(CompartmentCreationO
     }
 
     bool cloneSingletons() const { return cloneSingletons_; }
     CompartmentCreationOptions& setCloneSingletons(bool flag) {
         cloneSingletons_ = flag;
         return *this;
     }
 
+    // ECMA-402 is considering adding a "formatToParts" NumberFormat method,
+    // that exposes not just a formatted string but its subcomponents.  The
+    // method, its semantics, and its name aren't finalized, so for now it's
+    // exposed *only* if requested.
+    //
+    // Until "formatToParts" is included in a final specification edition, it's
+    // subject to change or removal at any time.  Do *not* rely on it in
+    // mission-critical code that can't be changed if ECMA-402 decides not to
+    // accept the method in its current form.
+    bool experimentalNumberFormatFormatToPartsEnabled() const {
+        return experimentalNumberFormatFormatToPartsEnabled_;
+    }
+    CompartmentCreationOptions& setExperimentalNumberFormatFormatToPartsEnabled(bool flag) {
+        experimentalNumberFormatFormatToPartsEnabled_ = flag;
+        return *this;
+    }
+
     bool getSharedMemoryAndAtomicsEnabled() const;
     CompartmentCreationOptions& setSharedMemoryAndAtomicsEnabled(bool flag);
 
     // This flag doesn't affect JS engine behavior.  It is used by Gecko to
     // mark whether content windows and workers are "Secure Context"s. See
     // https://w3c.github.io/webappsec-secure-contexts/
     // https://bugzilla.mozilla.org/show_bug.cgi?id=1162772#c34
     bool secureContext() const { return secureContext_; }
@@ -2278,16 +2296,17 @@ class JS_PUBLIC_API(CompartmentCreationO
     union {
         ZoneSpecifier spec;
         void* pointer; // js::Zone* is not exposed in the API.
     } zone_;
     bool invisibleToDebugger_;
     bool mergeable_;
     bool preserveJitCode_;
     bool cloneSingletons_;
+    bool experimentalNumberFormatFormatToPartsEnabled_;
     bool sharedMemoryAndAtomics_;
     bool secureContext_;
 };
 
 /**
  * CompartmentBehaviors specifies behaviors of a compartment that can be
  * changed after the compartment's been created.
  */
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -4628,16 +4628,21 @@ NewGlobal(JSContext* cx, unsigned argc, 
         if (v.isBoolean())
             creationOptions.setInvisibleToDebugger(v.toBoolean());
 
         if (!JS_GetProperty(cx, opts, "cloneSingletons", &v))
             return false;
         if (v.isBoolean())
             creationOptions.setCloneSingletons(v.toBoolean());
 
+        if (!JS_GetProperty(cx, opts, "experimentalNumberFormatFormatToPartsEnabled", &v))
+            return false;
+        if (v.isBoolean())
+            creationOptions.setExperimentalNumberFormatFormatToPartsEnabled(v.toBoolean());
+
         if (!JS_GetProperty(cx, opts, "sameZoneAs", &v))
             return false;
         if (v.isObject())
             creationOptions.setSameZoneAs(UncheckedUnwrap(&v.toObject()));
 
         if (!JS_GetProperty(cx, opts, "disableLazyParsing", &v))
             return false;
         if (v.isBoolean())
new file mode 100644
--- /dev/null
+++ b/js/src/tests/Intl/NumberFormat/formatToParts.js
@@ -0,0 +1,349 @@
+// |reftest| skip-if(!this.hasOwnProperty("Intl")||typeof(newGlobal)!=="function"||!newGlobal({experimentalNumberFormatFormatToPartsEnabled:true}).Intl.NumberFormat().formatToParts)
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/licenses/publicdomain/
+
+//-----------------------------------------------------------------------------
+var BUGNUMBER = 1289882;
+var summary = "Implement Intl.NumberFormat.prototype.formatToParts";
+
+print(BUGNUMBER + ": " + summary);
+
+//-----------------------------------------------------------------------------
+
+// Once Intl.NumberFormat.prototype.formatToParts isn't feature-guarded, remove
+// this bit.
+assertEq(typeof newGlobal, "function");
+assertEq("formatToParts" in Intl.NumberFormat(), false);
+Intl = newGlobal({ experimentalNumberFormatFormatToPartsEnabled: true }).Intl;
+assertEq("formatToParts" in Intl.NumberFormat(), true);
+
+// NOTE: Some of these tests exercise standard behavior (e.g. that format and
+//       formatToParts expose the same formatted string).  But much of this,
+//       like the exact-formatted-string expectations, is technically
+//       implementation-dependent.  This is necessary as a practical matter to
+//       properly test the conversion from ICU's nested-field exposure to
+//       ECMA-402's sequential-parts exposure.
+
+function GenericPartCreator(type)
+{
+  return function(str) { return { type, value: str }; };
+}
+
+var Nan = GenericPartCreator("nan");
+var Inf = GenericPartCreator("infinity");
+var Integer = GenericPartCreator("integer");
+var Group = GenericPartCreator("group");
+var Decimal = GenericPartCreator("decimal");
+var Fraction = GenericPartCreator("fraction");
+var MinusSign = GenericPartCreator("minusSign");
+var PlusSign = GenericPartCreator("plusSign");
+var PercentSign = GenericPartCreator("percentSign");
+var Currency = GenericPartCreator("currency");
+var Literal = GenericPartCreator("literal");
+
+function assertParts(nf, x, expected)
+{
+  var parts = nf.formatToParts(x);
+  assertEq(parts.map(part => part.value).join(""), nf.format(x),
+           "formatToParts and format must agree");
+
+  var len = parts.length;
+  assertEq(len, expected.length, "parts count mismatch");
+  for (var i = 0; i < len; i++)
+  {
+    assertEq(parts[i].type, expected[i].type, "type mismatch at " + i);
+    assertEq(parts[i].value, expected[i].value, "value mismatch at " + i);
+  }
+}
+
+//-----------------------------------------------------------------------------
+
+// Test behavior of a currency with code formatting.
+var usdCodeOptions =
+  {
+    style: "currency",
+    currency: "USD",
+    currencyDisplay: "code",
+    minimumFractionDigits: 0,
+    maximumFractionDigits: 0,
+  };
+var usDollarsCode = new Intl.NumberFormat("en-US", usdCodeOptions);
+
+assertParts(usDollarsCode, 25,
+            [Currency("USD"), Integer("25")]);
+
+// ISO 4217 currency codes are formed from an ISO 3166-1 alpha-2 country code
+// followed by a third letter.  ISO 3166 guarantees that no country code
+// starting with "X" will ever be assigned.  Stepping carefully around a few
+// 4217-designated special "currencies", XQQ will never have a representation.
+// Thus, yes: this really is specified to work, as unrecognized or unsupported
+// codes pass into the string unmodified.
+var xqqCodeOptions =
+  {
+    style: "currency",
+    currency: "XQQ",
+    currencyDisplay: "code",
+    minimumFractionDigits: 0,
+    maximumFractionDigits: 0,
+  };
+var xqqMoneyCode = new Intl.NumberFormat("en-US", xqqCodeOptions);
+
+assertParts(xqqMoneyCode, 25,
+            [Currency("XQQ"), Integer("25")]);
+
+// Test currencyDisplay: "name".
+var usdNameOptions =
+  {
+    style: "currency",
+    currency: "USD",
+    currencyDisplay: "name",
+    minimumFractionDigits: 0,
+    maximumFractionDigits: 0,
+  };
+var usDollarsName = new Intl.NumberFormat("en-US", usdNameOptions);
+
+assertParts(usDollarsName, 25,
+            [Integer("25"), Literal(" "), Currency("US dollars")]);
+
+var usdNameGroupingOptions =
+  {
+    style: "currency",
+    currency: "USD",
+    currencyDisplay: "name",
+    minimumFractionDigits: 0,
+    maximumFractionDigits: 0,
+  };
+var usDollarsNameGrouping =
+  new Intl.NumberFormat("en-US", usdNameGroupingOptions);
+
+assertParts(usDollarsNameGrouping, 12345678,
+            [Integer("12"),
+             Group(","),
+             Integer("345"),
+             Group(","),
+             Integer("678"),
+             Literal(" "),
+             Currency("US dollars")]);
+
+// But if the implementation doesn't recognize the currency, the provided code
+// is used in place of a proper name, unmolested.
+var xqqNameOptions =
+  {
+    style: "currency",
+    currency: "XQQ",
+    currencyDisplay: "name",
+    minimumFractionDigits: 0,
+    maximumFractionDigits: 0,
+  };
+var xqqMoneyName = new Intl.NumberFormat("en-US", xqqNameOptions);
+
+assertParts(xqqMoneyName, 25,
+            [Integer("25"), Literal(" "), Currency("XQQ")]);
+
+// Test some currencies with fractional components.
+
+var usdNameFractionOptions =
+  {
+    style: "currency",
+    currency: "USD",
+    currencyDisplay: "name",
+    minimumFractionDigits: 2,
+    maximumFractionDigits: 2,
+  };
+var usdNameFractionFormatter =
+  new Intl.NumberFormat("en-US", usdNameFractionOptions);
+
+// The US national surplus (i.e. debt) as of October 18, 2016.
+// (Replicating data from a comment in Intl.cpp.)
+var usNationalSurplus = -19766580028249.41;
+
+assertParts(usdNameFractionFormatter, usNationalSurplus,
+            [MinusSign("-"),
+             Integer("19"),
+             Group(","),
+             Integer("766"),
+             Group(","),
+             Integer("580"),
+             Group(","),
+             Integer("028"),
+             Group(","),
+             Integer("249"),
+             Decimal("."),
+             Fraction("41"),
+             Literal(" "),
+             Currency("US dollars")]);
+
+// Percents in various forms.
+
+var usPercentOptions =
+  {
+    style: "percent",
+    minimumFractionDigits: 1,
+    maximumFractionDigits: 1,
+  };
+var usPercentFormatter =
+  new Intl.NumberFormat("en-US", usPercentOptions);
+
+assertParts(usPercentFormatter, 0.375,
+            [Integer("37"), Decimal("."), Fraction("5"), PercentSign("%")]);
+
+assertParts(usPercentFormatter, -1284.375,
+            [MinusSign("-"),
+             Integer("128"),
+             Group(","),
+             Integer("437"),
+             Decimal("."),
+             Fraction("5"),
+             PercentSign("%")]);
+
+assertParts(usPercentFormatter, NaN,
+            [Nan("NaN")]);
+
+assertParts(usPercentFormatter, Infinity,
+            [Inf("∞"), PercentSign("%")]);
+
+assertParts(usPercentFormatter, -Infinity,
+            [MinusSign("-"), Inf("∞"), PercentSign("%")]);
+
+var arPercentOptions =
+  {
+    style: "percent",
+    minimumFractionDigits: 2,
+  };
+var arPercentFormatter =
+  new Intl.NumberFormat("ar-IQ", arPercentOptions);
+
+assertParts(arPercentFormatter, -135.32,
+            [MinusSign("\u{061C}-"),
+             Integer("١٣"),
+             Group("٬"),
+             Integer("٥٣٢"),
+             Decimal("٫"),
+             Fraction("٠٠"),
+             Literal("\xA0"),
+             PercentSign("٪\u{061C}")]);
+
+// Decimals.
+
+var usDecimalOptions =
+  {
+    style: "decimal",
+    maximumFractionDigits: 7 // minimum defaults to 0
+  };
+var usDecimalFormatter =
+  new Intl.NumberFormat("en-US", usDecimalOptions);
+
+assertParts(usDecimalFormatter, 42,
+            [Integer("42")]);
+
+assertParts(usDecimalFormatter, 1337,
+            [Integer("1"), Group(","), Integer("337")]);
+
+assertParts(usDecimalFormatter, -6.25,
+            [MinusSign("-"), Integer("6"), Decimal("."), Fraction("25")]);
+
+assertParts(usDecimalFormatter, -1376.25,
+            [MinusSign("-"),
+             Integer("1"),
+             Group(","),
+             Integer("376"),
+             Decimal("."),
+             Fraction("25")]);
+
+assertParts(usDecimalFormatter, 124816.8359375,
+            [Integer("124"),
+             Group(","),
+             Integer("816"),
+             Decimal("."),
+             Fraction("8359375")]);
+
+var usNoGroupingDecimalOptions =
+  {
+    style: "decimal",
+    useGrouping: false,
+    maximumFractionDigits: 7 // minimum defaults to 0
+  };
+var usNoGroupingDecimalFormatter =
+  new Intl.NumberFormat("en-US", usNoGroupingDecimalOptions);
+
+assertParts(usNoGroupingDecimalFormatter, 1337,
+            [Integer("1337")]);
+
+assertParts(usNoGroupingDecimalFormatter, -6.25,
+            [MinusSign("-"), Integer("6"), Decimal("."), Fraction("25")]);
+
+assertParts(usNoGroupingDecimalFormatter, -1376.25,
+            [MinusSign("-"),
+             Integer("1376"),
+             Decimal("."),
+             Fraction("25")]);
+
+assertParts(usNoGroupingDecimalFormatter, 124816.8359375,
+            [Integer("124816"),
+             Decimal("."),
+             Fraction("8359375")]);
+
+var deDecimalOptions =
+  {
+    style: "decimal",
+    maximumFractionDigits: 7 // minimum defaults to 0
+  };
+var deDecimalFormatter =
+  new Intl.NumberFormat("de-DE", deDecimalOptions);
+
+assertParts(deDecimalFormatter, 42,
+            [Integer("42")]);
+
+assertParts(deDecimalFormatter, 1337,
+            [Integer("1"), Group("."), Integer("337")]);
+
+assertParts(deDecimalFormatter, -6.25,
+            [MinusSign("-"), Integer("6"), Decimal(","), Fraction("25")]);
+
+assertParts(deDecimalFormatter, -1376.25,
+            [MinusSign("-"),
+             Integer("1"),
+             Group("."),
+             Integer("376"),
+             Decimal(","),
+             Fraction("25")]);
+
+assertParts(deDecimalFormatter, 124816.8359375,
+            [Integer("124"),
+             Group("."),
+             Integer("816"),
+             Decimal(","),
+             Fraction("8359375")]);
+
+var deNoGroupingDecimalOptions =
+  {
+    style: "decimal",
+    useGrouping: false,
+    maximumFractionDigits: 7 // minimum defaults to 0
+  };
+var deNoGroupingDecimalFormatter =
+  new Intl.NumberFormat("de-DE", deNoGroupingDecimalOptions);
+
+assertParts(deNoGroupingDecimalFormatter, 1337,
+            [Integer("1337")]);
+
+assertParts(deNoGroupingDecimalFormatter, -6.25,
+            [MinusSign("-"), Integer("6"), Decimal(","), Fraction("25")]);
+
+assertParts(deNoGroupingDecimalFormatter, -1376.25,
+            [MinusSign("-"),
+             Integer("1376"),
+             Decimal(","),
+             Fraction("25")]);
+
+assertParts(deNoGroupingDecimalFormatter, 124816.8359375,
+            [Integer("124816"),
+             Decimal(","),
+             Fraction("8359375")]);
+
+//-----------------------------------------------------------------------------
+
+if (typeof reportCompare === "function")
+    reportCompare(0, 0, 'ok');
+
+print("Tests complete");
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -68,19 +68,19 @@
     macro(ConvertAndCopyTo, ConvertAndCopyTo, "ConvertAndCopyTo") \
     macro(copyWithin, copyWithin, "copyWithin") \
     macro(count, count, "count") \
     macro(CreateResolvingFunctions, CreateResolvingFunctions, "CreateResolvingFunctions") \
     macro(currency, currency, "currency") \
     macro(currencyDisplay, currencyDisplay, "currencyDisplay") \
     macro(DateTimeFormat, DateTimeFormat, "DateTimeFormat") \
     macro(DateTimeFormatFormatGet, DateTimeFormatFormatGet, "Intl_DateTimeFormat_format_get") \
-    macro(DateTimeFormatFormatToParts, DateTimeFormatFormatToParts, "Intl_DateTimeFormat_formatToParts") \
     macro(day, day, "day") \
     macro(dayPeriod, dayPeriod, "dayPeriod") \
+    macro(decimal, decimal, "decimal") \
     macro(decodeURI, decodeURI, "decodeURI") \
     macro(decodeURIComponent, decodeURIComponent, "decodeURIComponent") \
     macro(DefaultBaseClassConstructor, DefaultBaseClassConstructor, "DefaultBaseClassConstructor") \
     macro(DefaultDerivedClassConstructor, DefaultDerivedClassConstructor, "DefaultDerivedClassConstructor") \
     macro(default_, default_, "default") \
     macro(defineGetter, defineGetter, "__defineGetter__") \
     macro(defineProperty, defineProperty, "defineProperty") \
     macro(defineSetter, defineSetter, "__defineSetter__") \
@@ -117,16 +117,18 @@
     macro(flags, flags, "flags") \
     macro(float32, float32, "float32") \
     macro(Float32x4, Float32x4, "Float32x4") \
     macro(float64, float64, "float64") \
     macro(Float64x2, Float64x2, "Float64x2") \
     macro(forceInterpreter, forceInterpreter, "forceInterpreter") \
     macro(forEach, forEach, "forEach") \
     macro(format, format, "format") \
+    macro(formatToParts, formatToParts, "formatToParts") \
+    macro(fraction, fraction, "fraction") \
     macro(frame, frame, "frame") \
     macro(from, from, "from") \
     macro(fulfilled, fulfilled, "fulfilled") \
     macro(futexNotEqual, futexNotEqual, "not-equal") \
     macro(futexOK, futexOK, "ok") \
     macro(futexTimedOut, futexTimedOut, "timed-out") \
     macro(gcCycleNumber, gcCycleNumber, "gcCycleNumber") \
     macro(Generator, Generator, "Generator") \
@@ -134,39 +136,42 @@
     macro(get, get, "get") \
     macro(getInternals, getInternals, "getInternals") \
     macro(getOwnPropertyDescriptor, getOwnPropertyDescriptor, "getOwnPropertyDescriptor") \
     macro(getOwnPropertyNames, getOwnPropertyNames, "getOwnPropertyNames") \
     macro(getPrefix, getPrefix, "get ") \
     macro(getPropertyDescriptor, getPropertyDescriptor, "getPropertyDescriptor") \
     macro(getPrototypeOf, getPrototypeOf, "getPrototypeOf") \
     macro(global, global, "global") \
+    macro(group, group, "group") \
     macro(Handle, Handle, "Handle") \
     macro(has, has, "has") \
     macro(hasOwn, hasOwn, "hasOwn") \
     macro(hasOwnProperty, hasOwnProperty, "hasOwnProperty") \
     macro(hour, hour, "hour") \
     macro(ignoreCase, ignoreCase, "ignoreCase") \
     macro(ignorePunctuation, ignorePunctuation, "ignorePunctuation") \
     macro(includes, includes, "includes") \
     macro(incumbentGlobal, incumbentGlobal, "incumbentGlobal") \
     macro(index, index, "index") \
+    macro(infinity, infinity, "infinity") \
     macro(Infinity, Infinity, "Infinity") \
     macro(InitializeCollator, InitializeCollator, "InitializeCollator") \
     macro(InitializeDateTimeFormat, InitializeDateTimeFormat, "InitializeDateTimeFormat") \
     macro(InitializeNumberFormat, InitializeNumberFormat, "InitializeNumberFormat") \
     macro(innermost, innermost, "innermost") \
     macro(inNursery, inNursery, "inNursery") \
     macro(input, input, "input") \
     macro(int8, int8, "int8") \
     macro(int16, int16, "int16") \
     macro(int32, int32, "int32") \
     macro(Int8x16, Int8x16, "Int8x16") \
     macro(Int16x8, Int16x8, "Int16x8") \
     macro(Int32x4, Int32x4, "Int32x4") \
+    macro(integer, integer, "integer") \
     macro(InterpretGeneratorResume, InterpretGeneratorResume, "InterpretGeneratorResume") \
     macro(isEntryPoint, isEntryPoint, "isEntryPoint") \
     macro(isExtensible, isExtensible, "isExtensible") \
     macro(isFinite, isFinite, "isFinite") \
     macro(isNaN, isNaN, "isNaN") \
     macro(isPrototypeOf, isPrototypeOf, "isPrototypeOf") \
     macro(IterableToList, IterableToList, "IterableToList") \
     macro(iterate, iterate, "iterate") \
@@ -190,38 +195,41 @@
     macro(MapIterator, MapIterator, "Map Iterator") \
     macro(maximumFractionDigits, maximumFractionDigits, "maximumFractionDigits") \
     macro(maximumSignificantDigits, maximumSignificantDigits, "maximumSignificantDigits") \
     macro(message, message, "message") \
     macro(minDays, minDays, "minDays") \
     macro(minimumFractionDigits, minimumFractionDigits, "minimumFractionDigits") \
     macro(minimumIntegerDigits, minimumIntegerDigits, "minimumIntegerDigits") \
     macro(minimumSignificantDigits, minimumSignificantDigits, "minimumSignificantDigits") \
+    macro(minusSign, minusSign, "minusSign") \
     macro(minute, minute, "minute") \
     macro(missingArguments, missingArguments, "missingArguments") \
     macro(module, module, "module") \
     macro(Module, Module, "Module") \
     macro(ModuleDeclarationInstantiation, ModuleDeclarationInstantiation, "ModuleDeclarationInstantiation") \
     macro(ModuleEvaluation, ModuleEvaluation, "ModuleEvaluation") \
     macro(month, month, "month") \
     macro(multiline, multiline, "multiline") \
     macro(name, name, "name") \
+    macro(nan, nan, "nan") \
     macro(NaN, NaN, "NaN") \
     macro(NegativeInfinity, NegativeInfinity, "-Infinity") \
     macro(new, new_, "new") \
     macro(next, next, "next") \
     macro(NFC, NFC, "NFC") \
     macro(NFD, NFD, "NFD") \
     macro(NFKC, NFKC, "NFKC") \
     macro(NFKD, NFKD, "NFKD") \
     macro(noFilename, noFilename, "noFilename") \
     macro(nonincrementalReason, nonincrementalReason, "nonincrementalReason") \
     macro(noStack, noStack, "noStack") \
     macro(NumberFormat, NumberFormat, "NumberFormat") \
     macro(NumberFormatFormatGet, NumberFormatFormatGet, "Intl_NumberFormat_format_get") \
+    macro(NumberFormatFormatToParts, NumberFormatFormatToParts, "Intl_NumberFormat_formatToParts") \
     macro(numeric, numeric, "numeric") \
     macro(objectArguments, objectArguments, "[object Arguments]") \
     macro(objectArray, objectArray, "[object Array]") \
     macro(objectBoolean, objectBoolean, "[object Boolean]") \
     macro(objectDate, objectDate, "[object Date]") \
     macro(objectError, objectError, "[object Error]") \
     macro(objectFunction, objectFunction, "[object Function]") \
     macro(objectNull, objectNull, "[object Null]") \
@@ -235,16 +243,18 @@
     macro(optimizedOut, optimizedOut, "optimizedOut") \
     macro(other, other, "other") \
     macro(outOfMemory, outOfMemory, "out of memory") \
     macro(ownKeys, ownKeys, "ownKeys") \
     macro(parseFloat, parseFloat, "parseFloat") \
     macro(parseInt, parseInt, "parseInt") \
     macro(pattern, pattern, "pattern") \
     macro(pending, pending, "pending") \
+    macro(percentSign, percentSign, "percentSign") \
+    macro(plusSign, plusSign, "plusSign") \
     macro(preventExtensions, preventExtensions, "preventExtensions") \
     macro(promise, promise, "promise") \
     macro(propertyIsEnumerable, propertyIsEnumerable, "propertyIsEnumerable") \
     macro(proto, proto, "__proto__") \
     macro(prototype, prototype, "prototype") \
     macro(proxy, proxy, "proxy") \
     macro(raw, raw, "raw") \
     macro(reason, reason, "reason") \