Bug 1216150 - Implement ECMA 402 DateTimeFormat formatToParts
authorZibi Braniecki <gandalf@mozilla.com>
Thu, 31 Dec 2015 19:07:45 -0800
changeset 278231 15bd594b598247dc4e651bd2b8d9b7666817f2ba
parent 278230 235dcd7acb6cf43fed439c1d49afe57fbce34354
child 278232 b0d4b1c817a4434906f102a300fa0cc860889287
push id29841
push userryanvm@gmail.com
push dateSat, 02 Jan 2016 00:29:52 +0000
treeherdermozilla-central@f7fbc524f9f3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1216150
milestone46.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 1216150 - Implement ECMA 402 DateTimeFormat formatToParts
js/src/builtin/Date.js
js/src/builtin/Intl.cpp
js/src/builtin/Intl.js
js/src/jsapi.h
js/src/shell/js.cpp
js/src/tests/Intl/DateTimeFormat/formatToParts.js
js/src/vm/CommonPropertyNames.h
--- a/js/src/builtin/Date.js
+++ b/js/src/builtin/Date.js
@@ -96,17 +96,17 @@ function Date_toLocaleString() {
         // locales and options.
         dateTimeFormat = GetCachedFormat("dateTimeFormat", "any", "all");
     } else {
         options = ToDateTimeOptions(options, "any", "all");
         dateTimeFormat = intl_DateTimeFormat(locales, options);
     }
 
     // Step 7.
-    return intl_FormatDateTime(dateTimeFormat, x);
+    return intl_FormatDateTime(dateTimeFormat, x, false);
 }
 
 
 /**
  * Format this Date object into a date string, using the locale and formatting
  * options provided.
  *
  * Spec: ECMAScript Language Specification, 5.1 edition, 15.9.5.6.
@@ -129,17 +129,17 @@ function Date_toLocaleDateString() {
         // locales and options.
         dateTimeFormat = GetCachedFormat("dateFormat", "date", "date");
     } else {
         options = ToDateTimeOptions(options, "date", "date");
         dateTimeFormat = intl_DateTimeFormat(locales, options);
     }
 
     // Step 7.
-    return intl_FormatDateTime(dateTimeFormat, x);
+    return intl_FormatDateTime(dateTimeFormat, x, false);
 }
 
 
 /**
  * Format this Date object into a time string, using the locale and formatting
  * options provided.
  *
  * Spec: ECMAScript Language Specification, 5.1 edition, 15.9.5.7.
@@ -162,10 +162,10 @@ function Date_toLocaleTimeString() {
         // locales and options.
         dateTimeFormat = GetCachedFormat("timeFormat", "time", "time");
     } else {
         options = ToDateTimeOptions(options, "time", "time");
         dateTimeFormat = intl_DateTimeFormat(locales, options);
     }
 
     // Step 7.
-    return intl_FormatDateTime(dateTimeFormat, x);
+    return intl_FormatDateTime(dateTimeFormat, x, false);
 }
--- a/js/src/builtin/Intl.cpp
+++ b/js/src/builtin/Intl.cpp
@@ -7,16 +7,17 @@
 /*
  * The Intl module specified by standard ECMA-402,
  * ECMAScript Internationalization API Specification.
  */
 
 #include "builtin/Intl.h"
 
 #include "mozilla/Range.h"
+#include "mozilla/ScopeExit.h"
 
 #include <string.h>
 
 #include "jsapi.h"
 #include "jsatom.h"
 #include "jscntxt.h"
 #include "jsobj.h"
 
@@ -40,16 +41,17 @@
 #include "jsobjinlines.h"
 
 #include "vm/NativeObject-inl.h"
 
 using namespace js;
 
 using mozilla::IsFinite;
 using mozilla::IsNegativeZero;
+using mozilla::MakeScopeExit;
 
 #if ENABLE_INTL_API
 using icu::Locale;
 using icu::NumberingSystem;
 #endif
 
 
 /*
@@ -171,16 +173,17 @@ static UEnumeration*
 ucol_getKeywordValuesForLocale(const char* key, const char* locale, UBool commonlyUsed,
                                UErrorCode* status)
 {
     MOZ_CRASH("ucol_getKeywordValuesForLocale: Intl API disabled");
 }
 
 struct UParseError;
 struct UFieldPosition;
+struct UFieldPositionIterator;
 typedef void* UNumberFormat;
 
 enum UNumberFormatStyle {
     UNUM_DECIMAL = 1,
     UNUM_CURRENCY,
     UNUM_PERCENT,
     UNUM_CURRENCY_ISO,
     UNUM_CURRENCY_PLURAL,
@@ -334,16 +337,56 @@ static void
 udatpg_close(UDateTimePatternGenerator* dtpg)
 {
     MOZ_CRASH("udatpg_close: Intl API disabled");
 }
 
 typedef void* UCalendar;
 typedef void* UDateFormat;
 
+enum UDateFormatField {
+  UDAT_ERA_FIELD = 0,
+  UDAT_YEAR_FIELD = 1,
+  UDAT_MONTH_FIELD = 2,
+  UDAT_DATE_FIELD = 3,
+  UDAT_HOUR_OF_DAY1_FIELD = 4,
+  UDAT_HOUR_OF_DAY0_FIELD = 5,
+  UDAT_MINUTE_FIELD = 6,
+  UDAT_SECOND_FIELD = 7,
+  UDAT_FRACTIONAL_SECOND_FIELD = 8,
+  UDAT_DAY_OF_WEEK_FIELD = 9,
+  UDAT_DAY_OF_YEAR_FIELD = 10,
+  UDAT_DAY_OF_WEEK_IN_MONTH_FIELD = 11,
+  UDAT_WEEK_OF_YEAR_FIELD = 12,
+  UDAT_WEEK_OF_MONTH_FIELD = 13,
+  UDAT_AM_PM_FIELD = 14,
+  UDAT_HOUR1_FIELD = 15,
+  UDAT_HOUR0_FIELD = 16,
+  UDAT_TIMEZONE_FIELD = 17,
+  UDAT_YEAR_WOY_FIELD = 18,
+  UDAT_DOW_LOCAL_FIELD = 19,
+  UDAT_EXTENDED_YEAR_FIELD = 20,
+  UDAT_JULIAN_DAY_FIELD = 21,
+  UDAT_MILLISECONDS_IN_DAY_FIELD = 22,
+  UDAT_TIMEZONE_RFC_FIELD = 23,
+  UDAT_TIMEZONE_GENERIC_FIELD = 24,
+  UDAT_STANDALONE_DAY_FIELD = 25,
+  UDAT_STANDALONE_MONTH_FIELD = 26,
+  UDAT_QUARTER_FIELD = 27,
+  UDAT_STANDALONE_QUARTER_FIELD = 28,
+  UDAT_TIMEZONE_SPECIAL_FIELD = 29,
+  UDAT_YEAR_NAME_FIELD = 30,
+  UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD = 31,
+  UDAT_TIMEZONE_ISO_FIELD = 32,
+  UDAT_TIMEZONE_ISO_LOCAL_FIELD = 33,
+  UDAT_RELATED_YEAR_FIELD = 34,
+  UDAT_TIME_SEPARATOR_FIELD = 35,
+  UDAT_FIELD_COUNT = 36 
+};
+
 enum UDateFormatStyle {
     UDAT_PATTERN = -2,
     UDAT_IGNORE = UDAT_PATTERN
 };
 
 static int32_t
 udat_countAvailable()
 {
@@ -378,16 +421,42 @@ ucal_setGregorianChange(UCalendar* cal, 
 
 static int32_t
 udat_format(const UDateFormat* format, UDate dateToFormat, UChar* result,
             int32_t resultLength, UFieldPosition* position, UErrorCode* status)
 {
     MOZ_CRASH("udat_format: Intl API disabled");
 }
 
+static int32_t
+udat_formatForFields(const UDateFormat* format, UDate dateToFormat,
+                     UChar* result, int32_t resultLength, UFieldPositionIterator* fpositer,
+                     UErrorCode* status)
+{
+    MOZ_CRASH("udat_formatForFields: Intl API disabled");
+}
+
+static UFieldPositionIterator*
+ufieldpositer_open(UErrorCode* status)
+{
+    MOZ_CRASH("ufieldpositer_open: Intl API disabled");
+}
+
+static void
+ufieldpositer_close(UFieldPositionIterator* fpositer)
+{
+    MOZ_CRASH("ufieldpositer_close: Intl API disabled");
+}
+
+static int32_t
+ufieldpositer_next(UFieldPositionIterator* fpositer, int32_t* beginIndex, int32_t* endIndex)
+{
+    MOZ_CRASH("ufieldpositer_next: Intl API disabled");
+}
+
 static void
 udat_close(UDateFormat* format)
 {
     MOZ_CRASH("udat_close: Intl API disabled");
 }
 
 #endif
 
@@ -1664,34 +1733,48 @@ InitDateTimeFormatClass(JSContext* cx, H
     // 12.2.2
     if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods))
         return nullptr;
 
     // 12.3.2 and 12.3.3
     if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods))
         return nullptr;
 
-    /*
-     * Install the getter for DateTimeFormat.prototype.format, which returns a
-     * bound formatting function for the specified DateTimeFormat object
-     * (suitable for passing to methods like Array.prototype.map).
-     */
+    // Install a getter for DateTimeFormat.prototype.format that returns a
+    // formatting function bound to a specified DateTimeFormat object (suitable
+    // for passing to methods like Array.prototype.map).
     RootedValue getter(cx);
     if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().DateTimeFormatFormatGet,
                                          &getter))
     {
         return nullptr;
     }
     if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue,
                         JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()),
                         nullptr, JSPROP_GETTER | JSPROP_SHARED))
     {
         return nullptr;
     }
 
+    // If the still-experimental DateTimeFormat.prototype.formatToParts method
+    // is enabled, also add its getter.
+    if (cx->compartment()->creationOptions().experimentalDateTimeFormatFormatToPartsEnabled()) {
+        if (!GlobalObject::getIntrinsicValue(cx, cx->global(),
+                                             cx->names().DateTimeFormatFormatToPartsGet, &getter))
+        {
+            return nullptr;
+        }
+        if (!DefineProperty(cx, proto, cx->names().formatToParts, UndefinedHandleValue,
+                            JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()),
+                            nullptr, JSPROP_GETTER | JSPROP_SHARED))
+        {
+            return nullptr;
+        }
+    }
+
     RootedValue options(cx);
     if (!CreateDefaultOptions(cx, &options))
         return nullptr;
 
     // 12.2.1 and 12.3
     if (!IntlInitialize(cx, proto, cx->names().InitializeDateTimeFormat, UndefinedHandleValue,
                         options))
     {
@@ -1981,26 +2064,231 @@ intl_FormatDateTime(JSContext* cx, UDate
         return false;
     }
 
     JSString* str = NewStringCopyN<CanGC>(cx, chars.begin(), size);
     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
+    // occur.
+    switch (fieldName) {
+      case UDAT_ERA_FIELD:
+        return &JSAtomState::era;
+      case UDAT_YEAR_FIELD:
+      case UDAT_YEAR_WOY_FIELD:
+      case UDAT_EXTENDED_YEAR_FIELD:
+      case UDAT_YEAR_NAME_FIELD:
+        return &JSAtomState::year;
+
+      case UDAT_MONTH_FIELD:
+      case UDAT_STANDALONE_MONTH_FIELD:
+        return &JSAtomState::month;
+
+      case UDAT_DATE_FIELD:
+      case UDAT_JULIAN_DAY_FIELD:
+        return &JSAtomState::day;
+
+      case UDAT_HOUR_OF_DAY1_FIELD:
+      case UDAT_HOUR_OF_DAY0_FIELD:
+      case UDAT_HOUR1_FIELD:
+      case UDAT_HOUR0_FIELD:
+        return &JSAtomState::hour;
+
+      case UDAT_MINUTE_FIELD:
+        return &JSAtomState::minute;
+
+      case UDAT_SECOND_FIELD:
+        return &JSAtomState::second;
+
+      case UDAT_DAY_OF_WEEK_FIELD:
+      case UDAT_STANDALONE_DAY_FIELD:
+      case UDAT_DOW_LOCAL_FIELD:
+      case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD:
+        return &JSAtomState::weekday;
+
+      case UDAT_AM_PM_FIELD:
+        return &JSAtomState::dayperiod;
+
+      case UDAT_TIMEZONE_FIELD:
+        return &JSAtomState::timeZoneName;
+
+      case UDAT_FRACTIONAL_SECOND_FIELD:
+      case UDAT_DAY_OF_YEAR_FIELD:
+      case UDAT_WEEK_OF_YEAR_FIELD:
+      case UDAT_WEEK_OF_MONTH_FIELD:
+      case UDAT_MILLISECONDS_IN_DAY_FIELD:
+      case UDAT_TIMEZONE_RFC_FIELD:
+      case UDAT_TIMEZONE_GENERIC_FIELD:
+      case UDAT_QUARTER_FIELD:
+      case UDAT_STANDALONE_QUARTER_FIELD:
+      case UDAT_TIMEZONE_SPECIAL_FIELD:
+      case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD:
+      case UDAT_TIMEZONE_ISO_FIELD:
+      case UDAT_TIMEZONE_ISO_LOCAL_FIELD:
+#ifndef U_HIDE_INTERNAL_API
+      case UDAT_RELATED_YEAR_FIELD:
+#endif
+#ifndef U_HIDE_DRAFT_API
+      case UDAT_TIME_SEPARATOR_FIELD:
+#endif
+        // These fields are all unsupported.
+        return nullptr;
+
+      case UDAT_FIELD_COUNT:
+        MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by "
+                               "iterator!");
+    }
+
+    MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned "
+                           "by iterator");
+    return nullptr;
+}
+
+static bool
+intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result)
+{
+    if (!IsFinite(x)) {
+        JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE);
+        return false;
+    }
+
+    Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE> chars(cx);
+    if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE))
+        return false;
+
+    UErrorCode status = U_ZERO_ERROR;
+    UFieldPositionIterator* fpositer = ufieldpositer_open(&status);
+    if (U_FAILURE(status)) {
+        JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
+        return false;
+    }
+    auto closeFieldPosIter = MakeScopeExit([&]() { ufieldpositer_close(fpositer); });
+
+    int resultSize =
+        udat_formatForFields(df, x, Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE,
+                             fpositer, &status);
+    if (status == U_BUFFER_OVERFLOW_ERROR) {
+        if (!chars.resize(resultSize))
+            return false;
+        status = U_ZERO_ERROR;
+        udat_formatForFields(df, x, Char16ToUChar(chars.begin()), resultSize, fpositer, &status);
+    }
+    if (U_FAILURE(status)) {
+        JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
+        return false;
+    }
+
+    RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx));
+    if (!partsArray)
+        return false;
+    if (resultSize == 0) {
+        // An empty string contains no parts, so avoid extra work below.
+        result.setObject(*partsArray);
+        return true;
+    }
+
+    RootedString overallResult(cx, NewStringCopyN<CanGC>(cx, chars.begin(), resultSize));
+    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);
+        if (!partSubstr)
+            return false;
+
+        val = StringValue(partSubstr);
+        if (!DefineProperty(cx, singlePart, cx->names().value, val))
+            return false;
+
+        val = ObjectValue(*singlePart);
+        if (!DefineElement(cx, partsArray, partIndex, val))
+            return false;
+
+        lastEndIndex = endIndex;
+        partIndex++;
+        return true;
+    };
+
+    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,
+                   "field iterator returning invalid range");
+
+        size_t beginIndex(beginIndexInt);
+        size_t endIndex(endIndexInt);
+
+        // Technically this isn't guaranteed.  But it appears true in pratice,
+        // and http://bugs.icu-project.org/trac/ticket/12024 is expected to
+        // correct the documentation lapse.
+        MOZ_ASSERT(lastEndIndex <= beginIndex,
+                   "field iteration didn't return fields in order start to "
+                   "finish as expected");
+
+        if (FieldType type = GetFieldTypeForFormatField(static_cast<UDateFormatField>(fieldInt))) {
+            if (lastEndIndex < beginIndex) {
+                if (!AppendPart(&JSAtomState::separator, lastEndIndex, beginIndex))
+                    return false;
+            }
+
+            if (!AppendPart(type, beginIndex, endIndex))
+                return false;
+        }
+    }
+
+    // Append any final separator.
+    if (lastEndIndex < overallResult->length()) {
+        if (!AppendPart(&JSAtomState::separator, lastEndIndex, overallResult->length()))
+            return false;
+    }
+
+    result.setObject(*partsArray);
     return true;
 }
 
 bool
 js::intl_FormatDateTime(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 dateTimeFormat(cx, &args[0].toObject());
 
     // Obtain a UDateFormat object, cached if possible.
     bool isDateTimeFormatInstance = dateTimeFormat->getClass() == &DateTimeFormatClass;
     UDateFormat* df;
     if (isDateTimeFormatInstance) {
         void* priv =
@@ -2019,17 +2307,19 @@ js::intl_FormatDateTime(JSContext* cx, u
         // DateTimeFormat instance as an internal property to each such object.
         df = NewUDateFormat(cx, dateTimeFormat);
         if (!df)
             return false;
     }
 
     // Use the UDateFormat to actually format the time stamp.
     RootedValue result(cx);
-    bool success = intl_FormatDateTime(cx, df, args[1].toNumber(), &result);
+    bool success = args[2].toBoolean()
+                   ? intl_FormatToPartsDateTime(cx, df, args[1].toNumber(), &result)
+                   : intl_FormatDateTime(cx, df, args[1].toNumber(), &result);
 
     if (!isDateTimeFormatInstance)
         udat_close(df);
     if (!success)
         return false;
     args.rval().set(result);
     return true;
 }
--- a/js/src/builtin/Intl.js
+++ b/js/src/builtin/Intl.js
@@ -2707,20 +2707,19 @@ function dateTimeFormatLocaleData(locale
  * Spec: ECMAScript Internationalization API Specification, 12.3.2.
  */
 function dateTimeFormatFormatToBind() {
     // Steps 1.a.i-ii
     var date = arguments.length > 0 ? arguments[0] : undefined;
     var x = (date === undefined) ? std_Date_now() : ToNumber(date);
 
     // Step 1.a.iii.
-    return intl_FormatDateTime(this, x);
+    return intl_FormatDateTime(this, x, false);
 }
 
-
 /**
  * Returns a function bound to this DateTimeFormat that returns a String value
  * representing the result of calling ToNumber(date) according to the
  * effective locale and the formatting options of this DateTimeFormat.
  *
  * Spec: ECMAScript Internationalization API Specification, 12.3.2.
  */
 function Intl_DateTimeFormat_format_get() {
@@ -2737,16 +2736,45 @@ function Intl_DateTimeFormat_format_get(
         internals.boundFormat = bf;
     }
 
     // Step 2.
     return internals.boundFormat;
 }
 
 
+function dateTimeFormatFormatToPartsToBind() {
+    // Steps 1.a.i-ii
+    var date = arguments.length > 0 ? arguments[0] : undefined;
+    var x = (date === undefined) ? std_Date_now() : ToNumber(date);
+
+    // Step 1.a.iii.
+    return intl_FormatDateTime(this, x, true);
+}
+
+
+function Intl_DateTimeFormat_formatToParts_get() {
+    // Check "this DateTimeFormat object" per introduction of section 12.3.
+    var internals = getDateTimeFormatInternals(this, "formatToParts");
+
+    // Step 1.
+    if (internals.boundFormatToParts === undefined) {
+        // Step 1.a.
+        var F = dateTimeFormatFormatToPartsToBind;
+
+        // Step 1.b-d.
+        var bf = callFunction(std_Function_bind, F, this);
+        internals.boundFormatToParts = bf;
+    }
+
+    // Step 2.
+    return internals.boundFormatToParts;
+}
+
+
 /**
  * Returns the resolved options for a DateTimeFormat object.
  *
  * Spec: ECMAScript Internationalization API Specification, 12.3.3 and 12.4.
  */
 function Intl_DateTimeFormat_resolvedOptions() {
     // Check "this DateTimeFormat object" per introduction of section 12.3.
     var internals = getDateTimeFormatInternals(this, "resolvedOptions");
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -2175,17 +2175,18 @@ class JS_PUBLIC_API(CompartmentCreationO
 {
   public:
     CompartmentCreationOptions()
       : addonId_(nullptr),
         traceGlobal_(nullptr),
         invisibleToDebugger_(false),
         mergeable_(false),
         preserveJitCode_(false),
-        cloneSingletons_(false)
+        cloneSingletons_(false),
+        experimentalDateTimeFormatFormatToPartsEnabled_(false)
     {
         zone_.spec = JS::FreshZone;
     }
 
     // A null add-on ID means that the compartment is not associated with an
     // add-on.
     JSAddonId* addonIdOrNull() const { return addonId_; }
     CompartmentCreationOptions& setAddonId(JSAddonId* id) {
@@ -2238,27 +2239,46 @@ 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" DateTimeFormat method,
+    // that exposes not just a formatted string but its ordered subcomponents.
+    // The method, its semantics, and its name are all well short of being
+    // 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 experimentalDateTimeFormatFormatToPartsEnabled() const {
+        return experimentalDateTimeFormatFormatToPartsEnabled_;
+    }
+    CompartmentCreationOptions& setExperimentalDateTimeFormatFormatToPartsEnabled(bool flag) {
+        experimentalDateTimeFormatFormatToPartsEnabled_ = flag;
+        return *this;
+    }
+
+
   private:
     JSAddonId* addonId_;
     JSTraceOp traceGlobal_;
     union {
         ZoneSpecifier spec;
         void* pointer; // js::Zone* is not exposed in the API.
     } zone_;
     bool invisibleToDebugger_;
     bool mergeable_;
     bool preserveJitCode_;
     bool cloneSingletons_;
+    bool experimentalDateTimeFormatFormatToPartsEnabled_;
 };
 
 /**
  * CompartmentBehaviors specifies behaviors of a compartment that can be
  * changed after the compartment's been created.
  */
 class JS_PUBLIC_API(CompartmentBehaviors)
 {
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -3972,16 +3972,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, "experimentalDateTimeFormatFormatToPartsEnabled", &v))
+            return true;
+        if (v.isBoolean())
+            creationOptions.setExperimentalDateTimeFormatFormatToPartsEnabled(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/DateTimeFormat/formatToParts.js
@@ -0,0 +1,176 @@
+// |reftest| skip-if(!this.hasOwnProperty("Intl")||!this.newGlobal||!newGlobal({experimentalDateTimeFormatFormatToPartsEnabled:true}).Intl.DateTimeFormat().formatToParts)
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/licenses/publicdomain/
+
+// Tests the format function with a diverse set of locales and options.
+// Always use UTC to avoid dependencies on test environment.
+
+/*
+ * Return true if A is equal to B, where equality on arrays and objects
+ * means that they have the same set of enumerable properties, the values
+ * of each property are deep_equal, and their 'length' properties are
+ * equal. Equality on other types is ==.
+ */
+function deepEqual(a, b) {
+    if (typeof a !== typeof b)
+        return false;
+
+    if (a === null)
+        return b === null;
+
+    if (typeof a === 'object') {
+        // For every property of a, does b have that property with an equal value?
+        var props = {};
+        for (var prop in a) {
+            if (!deepEqual(a[prop], b[prop]))
+                return false;
+            props[prop] = true;
+        }
+
+        // Are all of b's properties present on a?
+        for (var prop in b)
+            if (!props[prop])
+                return false;
+
+        // length isn't enumerable, but we want to check it, too.
+        return a.length === b.length;
+    }
+
+    return Object.is(a, b);
+}
+
+function composeDate(parts) {
+  return parts.map(({value}) => value)
+              .reduce((string, part) => string + part);
+}
+
+var format;
+var date = Date.UTC(2012, 11, 17, 3, 0, 42);
+
+// The experimental formatToParts method is only exposed if specifically
+// requested.  Perform all tests using DateTimeFormat instances from a global
+// object with this method enabled.
+var DateTimeFormat =
+  newGlobal({experimentalDateTimeFormatFormatToPartsEnabled:true}).Intl.DateTimeFormat;
+
+// Locale en-US; default options.
+format = new DateTimeFormat("en-us", {timeZone: "UTC"});
+assertEq(deepEqual(format.formatToParts(date), [
+  { type: 'month', value: '12' },
+  { type: 'separator', value: '/' },
+  { type: 'day', value: '17' },
+  { type: 'separator', value: '/' },
+  { type: 'year', value: '2012' }
+]), true);
+
+// Just date
+format = new DateTimeFormat("en-us", {
+  year: 'numeric',
+  month: 'numeric',
+  day: 'numeric',
+  timeZone: "UTC"});
+assertEq(deepEqual(format.formatToParts(date), [
+  { type: 'month', value: '12' },
+  { type: 'separator', value: '/' },
+  { type: 'day', value: '17' },
+  { type: 'separator', value: '/' },
+  { type: 'year', value: '2012' }
+]), true);
+assertEq(composeDate(format.formatToParts(date)), format.format(date));
+
+// Just time in hour24
+format = new DateTimeFormat("en-us", {
+  hour: 'numeric',
+  minute: 'numeric',
+  second: 'numeric',
+  hour12: false,
+  timeZone: "UTC"});
+assertEq(deepEqual(format.formatToParts(date), [
+  { type: 'hour', value: '03' },
+  { type: 'separator', value: ':' },
+  { type: 'minute', value: '00' },
+  { type: 'separator', value: ':' },
+  { type: 'second', value: '42' }
+]), true);
+assertEq(composeDate(format.formatToParts(date)), format.format(date));
+
+// Just time in hour12
+format = new DateTimeFormat("en-us", {
+  hour: 'numeric',
+  minute: 'numeric',
+  second: 'numeric',
+  hour12: true,
+  timeZone: "UTC"});
+assertEq(deepEqual(format.formatToParts(date), [
+  { type: 'hour', value: '3' },
+  { type: 'separator', value: ':' },
+  { type: 'minute', value: '00' },
+  { type: 'separator', value: ':' },
+  { type: 'second', value: '42' },
+  { type: 'separator', value: ' ' },
+  { type: 'dayperiod', value: 'AM' }
+]), true);
+assertEq(composeDate(format.formatToParts(date)), format.format(date));
+
+// Just month.
+format = new DateTimeFormat("en-us", {
+  month: "narrow",
+  timeZone: "UTC"});
+assertEq(deepEqual(format.formatToParts(date), [
+  { type: 'month', value: 'D' }
+]), true);
+assertEq(composeDate(format.formatToParts(date)), format.format(date));
+
+// Just weekday.
+format = new DateTimeFormat("en-us", {
+  weekday: "narrow",
+  timeZone: "UTC"});
+assertEq(deepEqual(format.formatToParts(date), [
+  { type: 'weekday', value: 'M' }
+]), true);
+assertEq(composeDate(format.formatToParts(date)), format.format(date));
+
+// Year and era.
+format = new DateTimeFormat("en-us", {
+  year: "numeric",
+  era: "short",
+  timeZone: "UTC"});
+assertEq(deepEqual(format.formatToParts(date), [
+  { type: 'year', value: '2012' },
+  { type: 'separator', value: ' ' },
+  { type: 'era', value: 'AD' }
+]), true);
+assertEq(composeDate(format.formatToParts(date)), format.format(date));
+
+// Time and date
+format = new DateTimeFormat("en-us", {
+  weekday: 'long',
+  year: 'numeric',
+  month: 'numeric',
+  day: 'numeric',
+  hour: 'numeric',
+  minute: 'numeric',
+  second: 'numeric',
+  hour12: true,
+  timeZone: "UTC"});
+assertEq(deepEqual(format.formatToParts(date), [
+  { type: 'weekday', value: 'Monday' },
+  { type: 'separator', value: ', ' },
+  { type: 'month', value: '12' },
+  { type: 'separator', value: '/' },
+  { type: 'day', value: '17' },
+  { type: 'separator', value: '/' },
+  { type: 'year', value: '2012' },
+  { type: 'separator', value: ', ' },
+  { type: 'hour', value: '3' },
+  { type: 'separator', value: ':' },
+  { type: 'minute', value: '00' },
+  { type: 'separator', value: ':' },
+  { type: 'second', value: '42' },
+  { type: 'separator', value: ' ' },
+  { type: 'dayperiod', value: 'AM' }
+]), true);
+assertEq(composeDate(format.formatToParts(date)), format.format(date));
+
+if (typeof reportCompare === "function")
+    reportCompare(0, 0, 'ok');
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -54,16 +54,19 @@
     macro(construct, construct, "construct") \
     macro(constructor, constructor, "constructor") \
     macro(ConvertAndCopyTo, ConvertAndCopyTo, "ConvertAndCopyTo") \
     macro(count, count, "count") \
     macro(currency, currency, "currency") \
     macro(currencyDisplay, currencyDisplay, "currencyDisplay") \
     macro(DateTimeFormat, DateTimeFormat, "DateTimeFormat") \
     macro(DateTimeFormatFormatGet, DateTimeFormatFormatGet, "Intl_DateTimeFormat_format_get") \
+    macro(DateTimeFormatFormatToPartsGet, DateTimeFormatFormatToPartsGet, "Intl_DateTimeFormat_formatToParts_get") \
+    macro(day, day, "day") \
+    macro(dayperiod, dayperiod, "dayperiod") \
     macro(decodeURI, decodeURI, "decodeURI") \
     macro(decodeURIComponent, decodeURIComponent, "decodeURIComponent") \
     macro(default_, default_, "default") \
     macro(defineProperty, defineProperty, "defineProperty") \
     macro(defineGetter, defineGetter, "__defineGetter__") \
     macro(defineSetter, defineSetter, "__defineSetter__") \
     macro(delete, delete_, "delete") \
     macro(deleteProperty, deleteProperty, "deleteProperty") \
@@ -75,45 +78,48 @@
     macro(elementType, elementType, "elementType") \
     macro(empty, empty, "") \
     macro(emptyRegExp, emptyRegExp, "(?:)") \
     macro(encodeURI, encodeURI, "encodeURI") \
     macro(encodeURIComponent, encodeURIComponent, "encodeURIComponent") \
     macro(endTimestamp, endTimestamp, "endTimestamp") \
     macro(enumerable, enumerable, "enumerable") \
     macro(enumerate, enumerate, "enumerate") \
+    macro(era, era, "era") \
     macro(escape, escape, "escape") \
     macro(eval, eval, "eval") \
     macro(false, false_, "false") \
     macro(fieldOffsets, fieldOffsets, "fieldOffsets") \
     macro(fieldTypes, fieldTypes, "fieldTypes") \
     macro(fileName, fileName, "fileName") \
     macro(fix, fix, "fix") \
     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(frame, frame, "frame") \
     macro(from, from, "from") \
     macro(gcCycleNumber, gcCycleNumber, "gcCycleNumber") \
     macro(GeneratorFunction, GeneratorFunction, "GeneratorFunction") \
     macro(get, get, "get") \
     macro(getInternals, getInternals, "getInternals") \
     macro(getOwnPropertyDescriptor, getOwnPropertyDescriptor, "getOwnPropertyDescriptor") \
     macro(getOwnPropertyNames, getOwnPropertyNames, "getOwnPropertyNames") \
     macro(getPropertyDescriptor, getPropertyDescriptor, "getPropertyDescriptor") \
     macro(global, global, "global") \
     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(index, index, "index") \
     macro(InitializeCollator, InitializeCollator, "InitializeCollator") \
     macro(InitializeDateTimeFormat, InitializeDateTimeFormat, "InitializeDateTimeFormat") \
     macro(InitializeNumberFormat, InitializeNumberFormat, "InitializeNumberFormat") \
     macro(inNursery, inNursery, "inNursery") \
     macro(innermost, innermost, "innermost") \
@@ -147,18 +153,20 @@
     macro(lookupGetter, lookupGetter, "__lookupGetter__") \
     macro(lookupSetter, lookupSetter, "__lookupSetter__") \
     macro(maximumFractionDigits, maximumFractionDigits, "maximumFractionDigits") \
     macro(maximumSignificantDigits, maximumSignificantDigits, "maximumSignificantDigits") \
     macro(message, message, "message") \
     macro(minimumFractionDigits, minimumFractionDigits, "minimumFractionDigits") \
     macro(minimumIntegerDigits, minimumIntegerDigits, "minimumIntegerDigits") \
     macro(minimumSignificantDigits, minimumSignificantDigits, "minimumSignificantDigits") \
+    macro(minute, minute, "minute") \
     macro(missingArguments, missingArguments, "missingArguments") \
     macro(module, module, "module") \
+    macro(month, month, "month") \
     macro(multiline, multiline, "multiline") \
     macro(name, name, "name") \
     macro(NaN, NaN, "NaN") \
     macro(new, new_, "new") \
     macro(next, next, "next") \
     macro(NFC, NFC, "NFC") \
     macro(NFD, NFD, "NFD") \
     macro(NFKC, NFKC, "NFKC") \
@@ -195,17 +203,19 @@
     macro(reason, reason, "reason") \
     macro(Reify, Reify, "Reify") \
     macro(RequireObjectCoercible, RequireObjectCoercible, "RequireObjectCoercible") \
     macro(resumeGenerator, resumeGenerator, "resumeGenerator") \
     macro(return, return_, "return") \
     macro(revoke, revoke, "revoke") \
     macro(script, script, "script") \
     macro(scripts, scripts, "scripts") \
+    macro(second, second, "second") \
     macro(sensitivity, sensitivity, "sensitivity") \
+    macro(separator, separator, "separator") \
     macro(set, set, "set") \
     macro(shape, shape, "shape") \
     macro(size, size, "size") \
     macro(source, source, "source") \
     macro(stack, stack, "stack") \
     macro(star, star, "*") \
     macro(starDefaultStar, starDefaultStar, "*default*") \
     macro(startTimestamp, startTimestamp, "startTimestamp") \
@@ -216,24 +226,26 @@
     macro(style, style, "style") \
     macro(super, super, "super") \
     macro(target, target, "target") \
     macro(test, test, "test") \
     macro(then, then, "then") \
     macro(throw, throw_, "throw") \
     macro(timestamp, timestamp, "timestamp") \
     macro(timeZone, timeZone, "timeZone") \
+    macro(timeZoneName, timeZoneName, "timeZoneName") \
     macro(toGMTString, toGMTString, "toGMTString") \
     macro(toISOString, toISOString, "toISOString") \
     macro(toJSON, toJSON, "toJSON") \
     macro(toLocaleString, toLocaleString, "toLocaleString") \
     macro(toSource, toSource, "toSource") \
     macro(toString, toString, "toString") \
     macro(toUTCString, toUTCString, "toUTCString") \
     macro(true, true_, "true") \
+    macro(type, type, "type") \
     macro(unescape, unescape, "unescape") \
     macro(uneval, uneval, "uneval") \
     macro(unicode, unicode, "unicode") \
     macro(uninitialized, uninitialized, "uninitialized") \
     macro(uint8, uint8, "uint8") \
     macro(uint8Clamped, uint8Clamped, "uint8Clamped") \
     macro(uint16, uint16, "uint16") \
     macro(uint32, uint32, "uint32") \
@@ -250,17 +262,19 @@
     macro(value, value, "value") \
     macro(values, values, "values") \
     macro(valueOf, valueOf, "valueOf") \
     macro(var, var, "var") \
     macro(variable, variable, "variable") \
     macro(void0, void0, "(void 0)") \
     macro(watch, watch, "watch") \
     macro(WeakSet_add, WeakSet_add, "WeakSet_add") \
+    macro(weekday, weekday, "weekday") \
     macro(writable, writable, "writable") \
+    macro(year, year, "year") \
     macro(yield, yield, "yield") \
     macro(raw, raw, "raw") \
     /* Type names must be contiguous and ordered; see js::TypeName. */ \
     macro(undefined, undefined, "undefined") \
     macro(object, object, "object") \
     macro(function, function, "function") \
     macro(string, string, "string") \
     macro(number, number, "number") \