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 326453 f849271896d3fbde28471c8e362aa7f7da300b50
parent 326452 d33f8588693cacaa94c3b1c3536d7042295dbdc7
child 326454 00fde28274fa2afb2bdaacc9152046269c7e9db8
push id84962
push userjwalden@mit.edu
push dateMon, 19 Dec 2016 20:43:43 +0000
treeherdermozilla-inbound@00fde28274fa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersanba
bugs1289882
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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") \