From 15a126ec685151fed7ecc610bbdffb1cb49dff06 Mon Sep 17 00:00:00 2001 From: Norbert Lindenberg Date: Thu, 21 Mar 2013 16:38:25 -0700 Subject: [PATCH] Bug 769871 - Reimplement Date.toLocaleString per ECMA-402. r=jwalden --HG-- extra : rebase_source : 77b0023432502bc4e110143d06c10825fb020190 --- js/src/Makefile.in | 1 + js/src/builtin/Date.js | 122 ++++++++++++++++++ js/src/builtin/Intl.cpp | 22 +++- js/src/builtin/Intl.h | 10 ++ js/src/builtin/Utilities.js | 1 + js/src/jsdate.cpp | 70 +++++++--- js/src/jsdate.h | 9 ++ js/src/tests/ecma_3/Date/15.9.5.5-02.js | 19 ++- .../js1_5/extensions/toLocaleFormat-01.js | 7 +- js/src/vm/SelfHosting.cpp | 15 ++- 10 files changed, 241 insertions(+), 35 deletions(-) create mode 100644 js/src/builtin/Date.js diff --git a/js/src/Makefile.in b/js/src/Makefile.in index 2460692dff52..3175475bb5b7 100644 --- a/js/src/Makefile.in +++ b/js/src/Makefile.in @@ -961,6 +961,7 @@ selfhosting:: selfhosted.out.h selfhosting_srcs := \ $(srcdir)/builtin/Utilities.js \ $(srcdir)/builtin/Array.js \ + $(srcdir)/builtin/Date.js \ $(srcdir)/builtin/Intl.js \ $(srcdir)/builtin/IntlData.js \ $(srcdir)/builtin/Number.js \ diff --git a/js/src/builtin/Date.js b/js/src/builtin/Date.js new file mode 100644 index 000000000000..5bab1d8756ee --- /dev/null +++ b/js/src/builtin/Date.js @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/*global date_CheckThisDate: false, intl_DateTimeFormat: false, */ + + +var dateTimeFormatCache = new Record(); + + +/** + * Format this Date object into a date and time string, using the locale and + * formatting options provided. + * + * Spec: ECMAScript Language Specification, 5.1 edition, 15.9.5.5. + * Spec: ECMAScript Internationalization API Specification, 13.3.1. + */ +function Date_toLocaleString() { + // Steps 1-2. + callFunction(date_CheckThisDate, this); + var x = callFunction(std_Date_valueOf, this); + if (std_isNaN(x)) + return "Invalid Date"; + + // Steps 3-4. + var locales = arguments.length > 0 ? arguments[0] : undefined; + var options = arguments.length > 1 ? arguments[1] : undefined; + + // Step 5-6. + var dateTimeFormat; + if (locales === undefined && options === undefined) { + // This cache only optimizes for the old ES5 toLocaleString without + // locales and options. + if (dateTimeFormatCache.dateTimeFormat === undefined) { + options = ToDateTimeOptions(options, "any", "all"); + dateTimeFormatCache.dateTimeFormat = intl_DateTimeFormat(locales, options); + } + dateTimeFormat = dateTimeFormatCache.dateTimeFormat; + } else { + options = ToDateTimeOptions(options, "any", "all"); + dateTimeFormat = intl_DateTimeFormat(locales, options); + } + + // Step 7. + return intl_FormatDateTime(dateTimeFormat, x); +} + + +/** + * 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. + * Spec: ECMAScript Internationalization API Specification, 13.3.2. + */ +function Date_toLocaleDateString() { + // Steps 1-2. + callFunction(date_CheckThisDate, this); + var x = callFunction(std_Date_valueOf, this); + if (std_isNaN(x)) + return "Invalid Date"; + + // Steps 3-4. + var locales = arguments.length > 0 ? arguments[0] : undefined; + var options = arguments.length > 1 ? arguments[1] : undefined; + + // Step 5-6. + var dateTimeFormat; + if (locales === undefined && options === undefined) { + // This cache only optimizes for the old ES5 toLocaleDateString without + // locales and options. + if (dateTimeFormatCache.dateFormat === undefined) { + options = ToDateTimeOptions(options, "date", "date"); + dateTimeFormatCache.dateFormat = intl_DateTimeFormat(locales, options); + } + dateTimeFormat = dateTimeFormatCache.dateFormat; + } else { + options = ToDateTimeOptions(options, "date", "date"); + dateTimeFormat = intl_DateTimeFormat(locales, options); + } + + // Step 7. + return intl_FormatDateTime(dateTimeFormat, x); +} + + +/** + * 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. + * Spec: ECMAScript Internationalization API Specification, 13.3.3. + */ +function Date_toLocaleTimeString() { + // Steps 1-2. + callFunction(date_CheckThisDate, this); + var x = callFunction(std_Date_valueOf, this); + if (std_isNaN(x)) + return "Invalid Date"; + + // Steps 3-4. + var locales = arguments.length > 0 ? arguments[0] : undefined; + var options = arguments.length > 1 ? arguments[1] : undefined; + + // Step 5-6. + var dateTimeFormat; + if (locales === undefined && options === undefined) { + // This cache only optimizes for the old ES5 toLocaleTimeString without + // locales and options. + if (dateTimeFormatCache.timeFormat === undefined) { + options = ToDateTimeOptions(options, "time", "time"); + dateTimeFormatCache.timeFormat = intl_DateTimeFormat(locales, options); + } + dateTimeFormat = dateTimeFormatCache.timeFormat; + } else { + options = ToDateTimeOptions(options, "time", "time"); + dateTimeFormat = intl_DateTimeFormat(locales, options); + } + + // Step 7. + return intl_FormatDateTime(dateTimeFormat, x); +} diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 95d0d2da4605..29c26d90a52e 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -1546,14 +1546,11 @@ static JSFunctionSpec dateTimeFormat_methods[] = { * DateTimeFormat constructor. * Spec: ECMAScript Internationalization API Specification, 12.1 */ -static JSBool -DateTimeFormat(JSContext *cx, unsigned argc, Value *vp) +static bool +DateTimeFormat(JSContext *cx, CallArgs args, bool construct) { - CallArgs args = CallArgsFromVp(argc, vp); - RootedObject obj(cx); - bool construct = IsConstructing(args); if (!construct) { // 12.1.2.1 step 3 JSObject *intl = cx->global()->getOrCreateIntlObject(cx); @@ -1599,6 +1596,21 @@ DateTimeFormat(JSContext *cx, unsigned argc, Value *vp) return true; } +static JSBool +DateTimeFormat(JSContext *cx, unsigned argc, Value *vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return DateTimeFormat(cx, args, IsConstructing(args)); +} + +JSBool +js::intl_DateTimeFormat(JSContext *cx, unsigned argc, Value *vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + JS_ASSERT(args.length() == 2); + return DateTimeFormat(cx, args, true); +} + static void dateTimeFormat_finalize(FreeOp *fop, JSObject *obj) { diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index c4668c8592e6..3db27dc63767 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -127,6 +127,16 @@ intl_FormatNumber(JSContext *cx, unsigned argc, Value *vp); /******************** DateTimeFormat ********************/ +/** + * Returns a new instance of the standard built-in DateTimeFormat constructor. + * Self-hosted code cannot cache this constructor (as it does for others in + * Utilities.js) because it is initialized after self-hosted code is compiled. + * + * Usage: dateTimeFormat = intl_DateTimeFormat(locales, options) + */ +extern JSBool +intl_DateTimeFormat(JSContext *cx, unsigned argc, Value *vp); + /** * Returns an object indicating the supported locales for date and time * formatting by having a true-valued property for each such locale with the diff --git a/js/src/builtin/Utilities.js b/js/src/builtin/Utilities.js index ee4599f776c4..78f74be3b7d2 100644 --- a/js/src/builtin/Utilities.js +++ b/js/src/builtin/Utilities.js @@ -39,6 +39,7 @@ var std_Array_unshift = Array.prototype.unshift; var std_Boolean_toString = Boolean.prototype.toString; var Std_Date = Date; var std_Date_now = Date.now; +var std_Date_valueOf = Date.prototype.valueOf; var std_Function_bind = Function.prototype.bind; var std_Function_apply = Function.prototype.apply; var std_Math_floor = Math.floor; diff --git a/js/src/jsdate.cpp b/js/src/jsdate.cpp index 3fbe3cf73753..132e37bf96ba 100644 --- a/js/src/jsdate.cpp +++ b/js/src/jsdate.cpp @@ -59,7 +59,7 @@ using mozilla::ArrayLength; /* * The JS 'Date' object is patterned after the Java 'Date' object. - * Here is an script: + * Here is a script: * * today = new Date(); * @@ -1392,6 +1392,26 @@ IsDate(const Value &v) return v.isObject() && v.toObject().hasClass(&DateClass); } +JS_ALWAYS_INLINE bool +date_nop(JSContext *cx, CallArgs args) +{ + JS_ASSERT(IsDate(args.thisv())); + args.rval().setUndefined(); + return true; +} + +JSBool +date_CheckThisDate(JSContext *cx, unsigned argc, Value *vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // CallNonGenericMethod will handle proxies correctly and throw exceptions + // in the right circumstances, but will report date_CheckThisDate as the + // function name in the message. We need a better solution: + // https://bugzilla.mozilla.org/show_bug.cgi?id=844677 + return CallNonGenericMethod(cx, args); +} + /* * See ECMA 15.9.5.4 thru 15.9.5.23 */ @@ -2584,7 +2604,7 @@ date_toJSON(JSContext *cx, unsigned argc, Value *vp) return true; } -/* for Date.toLocaleString; interface to PRMJTime date struct. +/* for Date.toLocaleFormat; interface to PRMJTime date struct. */ static void new_explode(double timeval, PRMJTime *split, DateTimeInfo *dtInfo) @@ -2726,7 +2746,7 @@ date_format(JSContext *cx, double date, formatspec format, MutableHandleValue rv } static bool -ToLocaleHelper(JSContext *cx, HandleObject obj, const char *format, MutableHandleValue rval) +ToLocaleFormatHelper(JSContext *cx, HandleObject obj, const char *format, MutableHandleValue rval) { double utctime = obj->getDateUTCTime().toNumber(); @@ -2771,6 +2791,7 @@ ToLocaleHelper(JSContext *cx, HandleObject obj, const char *format, MutableHandl return true; } +#if !ENABLE_INTL_API static bool ToLocaleStringHelper(JSContext *cx, HandleObject thisObj, MutableHandleValue rval) { @@ -2778,7 +2799,7 @@ ToLocaleStringHelper(JSContext *cx, HandleObject thisObj, MutableHandleValue rva * Use '%#c' for windows, because '%c' is backward-compatible and non-y2k * with msvc; '%#c' requests that a full year be used in the result string. */ - return ToLocaleHelper(cx, thisObj, + return ToLocaleFormatHelper(cx, thisObj, #if defined(_WIN32) && !defined(__MWERKS__) "%#c" #else @@ -2823,7 +2844,7 @@ date_toLocaleDateString_impl(JSContext *cx, CallArgs args) ; RootedObject thisObj(cx, &args.thisv().toObject()); - return ToLocaleHelper(cx, thisObj, format, args.rval()); + return ToLocaleFormatHelper(cx, thisObj, format, args.rval()); } static JSBool @@ -2840,7 +2861,7 @@ date_toLocaleTimeString_impl(JSContext *cx, CallArgs args) JS_ASSERT(IsDate(args.thisv())); RootedObject thisObj(cx, &args.thisv().toObject()); - return ToLocaleHelper(cx, thisObj, "%X", args.rval()); + return ToLocaleFormatHelper(cx, thisObj, "%X", args.rval()); } static JSBool @@ -2849,6 +2870,7 @@ date_toLocaleTimeString(JSContext *cx, unsigned argc, Value *vp) CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } +#endif JS_ALWAYS_INLINE bool date_toLocaleFormat_impl(JSContext *cx, CallArgs args) @@ -2857,8 +2879,19 @@ date_toLocaleFormat_impl(JSContext *cx, CallArgs args) RootedObject thisObj(cx, &args.thisv().toObject()); - if (args.length() == 0) - return ToLocaleStringHelper(cx, thisObj, args.rval()); + if (args.length() == 0) { + /* + * Use '%#c' for windows, because '%c' is backward-compatible and non-y2k + * with msvc; '%#c' requests that a full year be used in the result string. + */ + return ToLocaleFormatHelper(cx, thisObj, +#if defined(_WIN32) && !defined(__MWERKS__) + "%#c" +#else + "%c" +#endif + , args.rval()); + } RootedString fmt(cx, ToString(cx, args[0])); if (!fmt) @@ -2868,7 +2901,7 @@ date_toLocaleFormat_impl(JSContext *cx, CallArgs args) if (!fmtbytes) return false; - return ToLocaleHelper(cx, thisObj, fmtbytes.ptr(), args.rval()); + return ToLocaleFormatHelper(cx, thisObj, fmtbytes.ptr(), args.rval()); } static JSBool @@ -3018,9 +3051,6 @@ static JSFunctionSpec date_methods[] = { JS_FN("setMilliseconds", date_setMilliseconds, 1,0), JS_FN("setUTCMilliseconds", date_setUTCMilliseconds, 1,0), JS_FN("toUTCString", date_toGMTString, 0,0), - JS_FN(js_toLocaleString_str, date_toLocaleString, 0,0), - JS_FN("toLocaleDateString", date_toLocaleDateString, 0,0), - JS_FN("toLocaleTimeString", date_toLocaleTimeString, 0,0), JS_FN("toLocaleFormat", date_toLocaleFormat, 0,0), JS_FN("toDateString", date_toDateString, 0,0), JS_FN("toTimeString", date_toTimeString, 0,0), @@ -3031,6 +3061,19 @@ static JSFunctionSpec date_methods[] = { #endif JS_FN(js_toString_str, date_toString, 0,0), JS_FN(js_valueOf_str, date_valueOf, 0,0), + + // This must be at the end because of bug 853075: functions listed after + // self-hosted methods aren't available in self-hosted code. +#if ENABLE_INTL_API + {js_toLocaleString_str, {NULL, NULL}, 0,0, "Date_toLocaleString"}, + {"toLocaleDateString", {NULL, NULL}, 0,0, "Date_toLocaleDateString"}, + {"toLocaleTimeString", {NULL, NULL}, 0,0, "Date_toLocaleTimeString"}, +#else + JS_FN(js_toLocaleString_str, date_toLocaleString, 0,0), + JS_FN("toLocaleDateString", date_toLocaleDateString, 0,0), + JS_FN("toLocaleTimeString", date_toLocaleTimeString, 0,0), +#endif + JS_FS_END }; @@ -3261,9 +3304,6 @@ static const NativeImpl sReadOnlyDateMethods[] = { date_getTimezoneOffset_impl, date_toGMTString_impl, date_toISOString_impl, - date_toLocaleString_impl, - date_toLocaleDateString_impl, - date_toLocaleTimeString_impl, date_toLocaleFormat_impl, date_toTimeString_impl, date_toDateString_impl, diff --git a/js/src/jsdate.h b/js/src/jsdate.h index 514160ecd634..f156e8a6aaae 100644 --- a/js/src/jsdate.h +++ b/js/src/jsdate.h @@ -65,6 +65,15 @@ js_DateGetMinutes(JSContext *cx, JSRawObject obj); extern JS_FRIEND_API(int) js_DateGetSeconds(JSRawObject obj); +/** + * Checks that the this value provided meets the requirements for "this Date + * object" in ES5.1, 15.9.5, and throws a TypeError if not. + * + * Usage: callFunction(date_CheckThisDate, this) + */ +extern JSBool +date_CheckThisDate(JSContext *cx, unsigned argc, js::Value *vp); + /* Date constructor native. Exposed only so the JIT can know its address. */ JSBool js_Date(JSContext *cx, unsigned argc, js::Value *vp); diff --git a/js/src/tests/ecma_3/Date/15.9.5.5-02.js b/js/src/tests/ecma_3/Date/15.9.5.5-02.js index fb46924357e8..fb3498972433 100644 --- a/js/src/tests/ecma_3/Date/15.9.5.5-02.js +++ b/js/src/tests/ecma_3/Date/15.9.5.5-02.js @@ -27,15 +27,26 @@ function test() d = new Date(-maxms ); y = d.getFullYear(); - l = d.toLocaleString(); - print(l); actual = y; expect = -271821; reportCompare(expect, actual, summary + ': check year'); - actual = l.match(new RegExp(y)) + ''; - expect = y + ''; + l = d.toLocaleString(); + print(l); + if (this.hasOwnProperty("Intl")) { + // ECMA-402 specifies that toLocaleString uses a proleptic Gregorian + // calender without year 0. + // Also, localized strings usually use era indicators such as "BC" + // instead of minus signs. + expect = Math.abs(y - 1) + ''; + } else { + // ECMA-262 up to edition 5.1 didn't specify toLocaleString; + // the previous implementation assumed a calendar with year 0 and used + // minus sign. + expect = y + ''; + } + actual = l.match(/-?[0-9]{3,}/) + ''; reportCompare(expect, actual, summary + ': check toLocaleString'); d = new Date(maxms ); diff --git a/js/src/tests/js1_5/extensions/toLocaleFormat-01.js b/js/src/tests/js1_5/extensions/toLocaleFormat-01.js index 648d9e5c6fe6..ae053b061424 100644 --- a/js/src/tests/js1_5/extensions/toLocaleFormat-01.js +++ b/js/src/tests/js1_5/extensions/toLocaleFormat-01.js @@ -20,7 +20,7 @@ var temp; enterFunc ('test'); printBugNumber(BUGNUMBER); printStatus (summary); - + var date = new Date("06/05/2005 00:00:00 GMT-0000"); expect = date.getTimezoneOffset() > 0 ? 'Sat' : 'Sun'; @@ -86,11 +86,6 @@ expect = '22'; actual = date.toLocaleFormat('%W'); reportCompare(expect, actual, 'Date.toLocaleFormat("%W")'); -expect = date.toLocaleTimeString(); -actual = date.toLocaleFormat('%X'); -reportCompare(expect, actual, 'Date.toLocaleTimeString() == ' + - 'Date.toLocaleFormat("%X")'); - expect = '05'; actual = date.toLocaleFormat('%y'); reportCompare(expect, actual, 'Date.toLocaleFormat("%y")'); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index c741043bc598..16ff8e63abd9 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -7,6 +7,7 @@ #include "jscntxt.h" #include "jscompartment.h" +#include "jsdate.h" #include "jsinterp.h" #include "jsnum.h" #include "jsobj.h" @@ -470,19 +471,23 @@ JSFunctionSpec intrinsic_functions[] = { JS_FN("ShouldForceSequential", intrinsic_ShouldForceSequential, 0,0), JS_FN("ParallelTestsShouldPass", intrinsic_ParallelTestsShouldPass, 0,0), + // See jsdate.h for descriptions of the date_* functions. + JS_FN("date_CheckThisDate", date_CheckThisDate, 2,0), + // See builtin/Intl.h for descriptions of the intl_* functions. + JS_FN("intl_availableCalendars", intl_availableCalendars, 1,0), + JS_FN("intl_availableCollations", intl_availableCollations, 1,0), JS_FN("intl_Collator", intl_Collator, 2,0), JS_FN("intl_Collator_availableLocales", intl_Collator_availableLocales, 0,0), - JS_FN("intl_availableCollations", intl_availableCollations, 1,0), JS_FN("intl_CompareStrings", intl_CompareStrings, 3,0), + JS_FN("intl_DateTimeFormat", intl_DateTimeFormat, 2,0), + JS_FN("intl_DateTimeFormat_availableLocales", intl_DateTimeFormat_availableLocales, 0,0), + JS_FN("intl_FormatDateTime", intl_FormatDateTime, 2,0), + JS_FN("intl_FormatNumber", intl_FormatNumber, 2,0), JS_FN("intl_NumberFormat", intl_NumberFormat, 2,0), JS_FN("intl_NumberFormat_availableLocales", intl_NumberFormat_availableLocales, 0,0), JS_FN("intl_numberingSystem", intl_numberingSystem, 1,0), - JS_FN("intl_FormatNumber", intl_FormatNumber, 2,0), - JS_FN("intl_DateTimeFormat_availableLocales", intl_DateTimeFormat_availableLocales, 0,0), - JS_FN("intl_availableCalendars", intl_availableCalendars, 1,0), JS_FN("intl_patternForSkeleton", intl_patternForSkeleton, 2,0), - JS_FN("intl_FormatDateTime", intl_FormatDateTime, 2,0), // See jsnum.h for descriptions of the num_* functions. JS_FN("num_CheckThisNumber", num_CheckThisNumber, 2,0),