From f5c8496af47d0182dea1ebe8a2aad998125d0360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bargull?= Date: Wed, 11 Oct 2017 06:54:44 -0700 Subject: [PATCH] Bug 830304 - Compute correct time zone offsets for Date methods. r=Waldo --HG-- extra : rebase_source : 480e087c1e19771f18ccb7146b55f063f347b817 --- js/src/builtin/TestingFunctions.cpp | 124 +++++++ js/src/jsdate.cpp | 19 +- js/src/tests/ecma_6/Date/browser.js | 3 + js/src/tests/ecma_6/Date/time-zone-pst.js | 134 ++++++++ .../tests/ecma_6/Date/time-zones-pedantic.js | 64 ++++ js/src/tests/ecma_6/Date/time-zones-posix.js | 198 +++++++++++ js/src/tests/ecma_6/Date/time-zones.js | 310 ++++++++++++++++++ js/src/vm/DateTime.cpp | 13 +- 8 files changed, 855 insertions(+), 10 deletions(-) create mode 100644 js/src/tests/ecma_6/Date/time-zone-pst.js create mode 100644 js/src/tests/ecma_6/Date/time-zones-pedantic.js create mode 100644 js/src/tests/ecma_6/Date/time-zones-posix.js create mode 100644 js/src/tests/ecma_6/Date/time-zones.js diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index dba4585b7ed9..c026273e3de0 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -13,6 +13,8 @@ #include "mozilla/Unused.h" #include +#include +#include #include "jsapi.h" #include "jscntxt.h" @@ -4737,6 +4739,118 @@ DisableForEach(JSContext* cx, unsigned argc, Value* vp) return true; } +static bool +GetTimeZone(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 0) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + auto getTimeZone = [](std::time_t* now) -> const char* { + std::tm local{}; +#if defined(_WIN32) + _tzset(); + if (localtime_s(&local, now) == 0) + return _tzname[local.tm_isdst > 0]; +#else + tzset(); +#if defined(HAVE_LOCALTIME_R) + if (localtime_r(now, &local)) { +#else + std::tm* localtm = std::localtime(now); + if (localtm) { + *local = *localtm; +#endif /* HAVE_LOCALTIME_R */ + +#if defined(HAVE_TM_ZONE_TM_GMTOFF) + return local.tm_zone; +#else + return tzname[local.tm_isdst > 0]; +#endif /* HAVE_TM_ZONE_TM_GMTOFF */ + } +#endif /* _WIN32 */ + return nullptr; + }; + + std::time_t now = std::time(nullptr); + if (now != static_cast(-1)) { + if (const char* tz = getTimeZone(&now)) { + JSString* str = JS_NewStringCopyZ(cx, tz); + if (!str) + return false; + args.rval().setString(str); + return true; + } + } + + args.rval().setUndefined(); + return true; +} + +static bool +SetTimeZone(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 1) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + if (!args[0].isString() && !args[0].isUndefined()) { + ReportUsageErrorASCII(cx, callee, "First argument should be a string or undefined"); + return false; + } + + auto setTimeZone = [](const char* value) { +#if defined(_WIN32) + return _putenv_s("TZ", value) == 0; +#else + return setenv("TZ", value, true) == 0; +#endif /* _WIN32 */ + }; + + auto unsetTimeZone = []() { +#if defined(_WIN32) + return _putenv_s("TZ", "") == 0; +#else + return unsetenv("TZ") == 0; +#endif /* _WIN32 */ + }; + + if (args[0].isString() && !args[0].toString()->empty()) { + JSAutoByteString timeZone; + if (!timeZone.encodeLatin1(cx, args[0].toString())) + return false; + + if (!setTimeZone(timeZone.ptr())) { + JS_ReportErrorASCII(cx, "Failed to set 'TZ' environment variable"); + return false; + } + } else { + if (!unsetTimeZone()) { + JS_ReportErrorASCII(cx, "Failed to unset 'TZ' environment variable"); + return false; + } + } + +#if defined(_WIN32) + _tzset(); +#else + tzset(); +#endif /* _WIN32 */ + + JS::ResetTimeZone(); + + args.rval().setUndefined(); + return true; +} + #if defined(FUZZING) && defined(__AFL_COMPILER) static bool AflLoop(JSContext* cx, unsigned argc, Value* vp) @@ -5414,6 +5528,10 @@ gc::ZealModeHelpText), "isLegacyIterator(value)", " Returns whether the value is considered is a legacy iterator.\n"), + JS_FN_HELP("getTimeZone", GetTimeZone, 0, 0, +"getTimeZone()", +" Get the current time zone.\n"), + JS_FS_HELP_END }; @@ -5432,6 +5550,12 @@ static const JSFunctionSpecWithHelp FuzzingUnsafeTestingFunctions[] = { "getErrorNotes(error)", " Returns an array of error notes."), + JS_FN_HELP("setTimeZone", SetTimeZone, 1, 0, +"setTimeZone(tzname)", +" Set the 'TZ' environment variable to the given time zone and applies the new time zone.\n" +" An empty string or undefined resets the time zone to its default value.\n" +" NOTE: The input string is not validated and will be passed verbatim to setenv()."), + JS_FS_HELP_END }; diff --git a/js/src/jsdate.cpp b/js/src/jsdate.cpp index 76f93686ed89..5e5b50edb363 100644 --- a/js/src/jsdate.cpp +++ b/js/src/jsdate.cpp @@ -483,7 +483,14 @@ LocalTime(double t) static double UTC(double t) { - return t - AdjustTime(t - DateTimeInfo::localTZA()); + // Following the ES2017 specification creates undesirable results at DST + // transitions. For example when transitioning from PST to PDT, + // |new Date(2016,2,13,2,0,0).toTimeString()| returns the string value + // "01:00:00 GMT-0800 (PST)" instead of "03:00:00 GMT-0700 (PDT)". Follow + // V8 and subtract one hour before computing the offset. + // Spec bug: https://bugs.ecmascript.org/show_bug.cgi?id=4007 + + return t - AdjustTime(t - DateTimeInfo::localTZA() - msPerHour); } /* ES5 15.9.1.10. */ @@ -2584,7 +2591,7 @@ date_toJSON(JSContext* cx, unsigned argc, Value* vp) /* Interface to PRMJTime date struct. */ static PRMJTime -ToPRMJTime(double localTime) +ToPRMJTime(double localTime, double utcTime) { double year = YearFromTime(localTime); @@ -2598,9 +2605,7 @@ ToPRMJTime(double localTime) prtm.tm_wday = int8_t(WeekDay(localTime)); prtm.tm_year = year; prtm.tm_yday = int16_t(DayWithinYear(localTime, year)); - - // XXX: DaylightSavingTA expects utc-time argument. - prtm.tm_isdst = (DaylightSavingTA(localTime) != 0); + prtm.tm_isdst = (DaylightSavingTA(utcTime) != 0); return prtm; } @@ -2647,7 +2652,7 @@ FormatDate(JSContext* cx, double utcTime, FormatSpec format, MutableHandleValue */ /* get a time zone string from the OS to include as a comment. */ - PRMJTime prtm = ToPRMJTime(utcTime); + PRMJTime prtm = ToPRMJTime(localTime, utcTime); size_t tzlen = PRMJ_FormatTime(tzbuf, sizeof tzbuf, "(%Z)", &prtm); if (tzlen != 0) { /* @@ -2727,7 +2732,7 @@ ToLocaleFormatHelper(JSContext* cx, HandleObject obj, const char* format, Mutabl strcpy(buf, js_NaN_date_str); } else { double localTime = LocalTime(utcTime); - PRMJTime prtm = ToPRMJTime(localTime); + PRMJTime prtm = ToPRMJTime(localTime, utcTime); /* Let PRMJTime format it. */ size_t result_len = PRMJ_FormatTime(buf, sizeof buf, format, &prtm); diff --git a/js/src/tests/ecma_6/Date/browser.js b/js/src/tests/ecma_6/Date/browser.js index e69de29bb2d1..5665e7ed448b 100644 --- a/js/src/tests/ecma_6/Date/browser.js +++ b/js/src/tests/ecma_6/Date/browser.js @@ -0,0 +1,3 @@ +if (typeof setTimeZone === "undefined") { + var setTimeZone = SpecialPowers.Cu.getJSTestingFunctions().setTimeZone; +} diff --git a/js/src/tests/ecma_6/Date/time-zone-pst.js b/js/src/tests/ecma_6/Date/time-zone-pst.js new file mode 100644 index 000000000000..ae890ea3ab4f --- /dev/null +++ b/js/src/tests/ecma_6/Date/time-zone-pst.js @@ -0,0 +1,134 @@ +// |reftest| skip-if(!xulRuntime.shell) + +// Note: The default time zone is set to PST8PDT for all jstests (when run in the shell). + +assertEq(/^(PST|PDT)$/.test(getTimeZone()), true); + +const msPerMinute = 60 * 1000; +const msPerHour = 60 * 60 * 1000; + +const Month = { + January: 0, + February: 1, + March: 2, + April: 3, + May: 4, + June: 5, + July: 6, + August: 7, + September: 8, + October: 9, + November: 10, + December: 11, +}; + +// PDT -> PST, using milliseconds from epoch. +{ + let midnight = new Date(2016, Month.November, 6, 0, 0, 0, 0); + let midnightUTC = Date.UTC(2016, Month.November, 6, 0, 0, 0, 0); + + // Ensure midnight time is correct. + assertEq(midnightUTC - midnight.getTime(), -7 * msPerHour); + + let tests = [ + { offset: 0 * 60, date: "Sun Nov 06 2016", time: "00:00:00 GMT-0700 (PDT)" }, + { offset: 0 * 60 + 30, date: "Sun Nov 06 2016", time: "00:30:00 GMT-0700 (PDT)" }, + { offset: 1 * 60, date: "Sun Nov 06 2016", time: "01:00:00 GMT-0700 (PDT)" }, + { offset: 1 * 60 + 30, date: "Sun Nov 06 2016", time: "01:30:00 GMT-0700 (PDT)" }, + { offset: 2 * 60, date: "Sun Nov 06 2016", time: "01:00:00 GMT-0800 (PST)" }, + { offset: 2 * 60 + 30, date: "Sun Nov 06 2016", time: "01:30:00 GMT-0800 (PST)" }, + { offset: 3 * 60, date: "Sun Nov 06 2016", time: "02:00:00 GMT-0800 (PST)" }, + { offset: 3 * 60 + 30, date: "Sun Nov 06 2016", time: "02:30:00 GMT-0800 (PST)" }, + { offset: 4 * 60, date: "Sun Nov 06 2016", time: "03:00:00 GMT-0800 (PST)" }, + { offset: 4 * 60 + 30, date: "Sun Nov 06 2016", time: "03:30:00 GMT-0800 (PST)" }, + ]; + + for (let {offset, date, time} of tests) { + let dt = new Date(midnight.getTime() + offset * msPerMinute); + assertEq(dt.toString(), `${date} ${time}`); + assertEq(dt.toDateString(), date); + assertEq(dt.toTimeString(), time); + } +} + + +// PDT -> PST, using local date-time. +{ + let tests = [ + { offset: 0 * 60, date: "Sun Nov 06 2016", time: "00:00:00 GMT-0700 (PDT)" }, + { offset: 0 * 60 + 30, date: "Sun Nov 06 2016", time: "00:30:00 GMT-0700 (PDT)" }, + { offset: 1 * 60, date: "Sun Nov 06 2016", time: "01:00:00 GMT-0700 (PDT)" }, + { offset: 1 * 60 + 30, date: "Sun Nov 06 2016", time: "01:30:00 GMT-0700 (PDT)" }, + { offset: 2 * 60, date: "Sun Nov 06 2016", time: "02:00:00 GMT-0800 (PST)" }, + { offset: 2 * 60 + 30, date: "Sun Nov 06 2016", time: "02:30:00 GMT-0800 (PST)" }, + { offset: 3 * 60, date: "Sun Nov 06 2016", time: "03:00:00 GMT-0800 (PST)" }, + { offset: 3 * 60 + 30, date: "Sun Nov 06 2016", time: "03:30:00 GMT-0800 (PST)" }, + { offset: 4 * 60, date: "Sun Nov 06 2016", time: "04:00:00 GMT-0800 (PST)" }, + { offset: 4 * 60 + 30, date: "Sun Nov 06 2016", time: "04:30:00 GMT-0800 (PST)" }, + ]; + + for (let {offset, date, time} of tests) { + let dt = new Date(2016, Month.November, 6, (offset / 60)|0, (offset % 60), 0, 0); + assertEq(dt.toString(), `${date} ${time}`); + assertEq(dt.toDateString(), date); + assertEq(dt.toTimeString(), time); + } +} + + +// PST -> PDT, using milliseconds from epoch. +{ + let midnight = new Date(2016, Month.March, 13, 0, 0, 0, 0); + let midnightUTC = Date.UTC(2016, Month.March, 13, 0, 0, 0, 0); + + // Ensure midnight time is correct. + assertEq(midnightUTC - midnight.getTime(), -8 * msPerHour); + + let tests = [ + { offset: 0 * 60, date: "Sun Mar 13 2016", time: "00:00:00 GMT-0800 (PST)" }, + { offset: 0 * 60 + 30, date: "Sun Mar 13 2016", time: "00:30:00 GMT-0800 (PST)" }, + { offset: 1 * 60, date: "Sun Mar 13 2016", time: "01:00:00 GMT-0800 (PST)" }, + { offset: 1 * 60 + 30, date: "Sun Mar 13 2016", time: "01:30:00 GMT-0800 (PST)" }, + { offset: 2 * 60, date: "Sun Mar 13 2016", time: "03:00:00 GMT-0700 (PDT)" }, + { offset: 2 * 60 + 30, date: "Sun Mar 13 2016", time: "03:30:00 GMT-0700 (PDT)" }, + { offset: 3 * 60, date: "Sun Mar 13 2016", time: "04:00:00 GMT-0700 (PDT)" }, + { offset: 3 * 60 + 30, date: "Sun Mar 13 2016", time: "04:30:00 GMT-0700 (PDT)" }, + { offset: 4 * 60, date: "Sun Mar 13 2016", time: "05:00:00 GMT-0700 (PDT)" }, + { offset: 4 * 60 + 30, date: "Sun Mar 13 2016", time: "05:30:00 GMT-0700 (PDT)" }, + ]; + + for (let {offset, date, time} of tests) { + let dt = new Date(midnight.getTime() + offset * msPerMinute); + assertEq(dt.toString(), `${date} ${time}`); + assertEq(dt.toDateString(), date); + assertEq(dt.toTimeString(), time); + } +} + + +// PST -> PDT, using local date-time. +{ + let tests = [ + { offset: 0 * 60, date: "Sun Mar 13 2016", time: "00:00:00 GMT-0800 (PST)" }, + { offset: 0 * 60 + 30, date: "Sun Mar 13 2016", time: "00:30:00 GMT-0800 (PST)" }, + { offset: 1 * 60, date: "Sun Mar 13 2016", time: "01:00:00 GMT-0800 (PST)" }, + { offset: 1 * 60 + 30, date: "Sun Mar 13 2016", time: "01:30:00 GMT-0800 (PST)" }, + { offset: 2 * 60, date: "Sun Mar 13 2016", time: "03:00:00 GMT-0700 (PDT)" }, + { offset: 2 * 60 + 30, date: "Sun Mar 13 2016", time: "03:30:00 GMT-0700 (PDT)" }, + { offset: 3 * 60, date: "Sun Mar 13 2016", time: "03:00:00 GMT-0700 (PDT)" }, + { offset: 3 * 60 + 30, date: "Sun Mar 13 2016", time: "03:30:00 GMT-0700 (PDT)" }, + { offset: 4 * 60, date: "Sun Mar 13 2016", time: "04:00:00 GMT-0700 (PDT)" }, + { offset: 4 * 60 + 30, date: "Sun Mar 13 2016", time: "04:30:00 GMT-0700 (PDT)" }, + ]; + + for (let {offset, date, time} of tests) { + let dt = new Date(2016, Month.March, 13, (offset / 60)|0, (offset % 60), 0, 0); + assertEq(dt.toString(), `${date} ${time}`); + assertEq(dt.toDateString(), date); + assertEq(dt.toTimeString(), time); + } +} + + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/ecma_6/Date/time-zones-pedantic.js b/js/src/tests/ecma_6/Date/time-zones-pedantic.js new file mode 100644 index 000000000000..a1aca65b70e2 --- /dev/null +++ b/js/src/tests/ecma_6/Date/time-zones-pedantic.js @@ -0,0 +1,64 @@ +// |reftest| skip-if(xulRuntime.OS=="WINNT"||xulRuntime.OS=="Darwin") -- Skip on OS X in addition to Windows + +// Contains the tests from "time-zones.js" which fail on OS X. + +const msPerHour = 60 * 60 * 1000; + +const Month = { + January: 0, + February: 1, + March: 2, + April: 3, + May: 4, + June: 5, + July: 6, + August: 7, + September: 8, + October: 9, + November: 10, + December: 11, +}; + +function inTimeZone(tzname, fn) { + setTimeZone(tzname); + try { + fn(); + } finally { + setTimeZone(undefined); + } +} + +const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].join("|"); +const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].join("|"); +const datePart = String.raw `(?:${weekdays}) (?:${months}) \d{2}`; +const timePart = String.raw `\d{4,6} \d{2}:\d{2}:\d{2} GMT[+-]\d{4}`; +const dateTimeRE = new RegExp(String.raw `^(${datePart} ${timePart})(?: \((.+)\))?$`); + +function assertDateTime(date, expected) { + let actual = date.toString(); + assertEq(dateTimeRE.test(expected), true, `${expected}`); + assertEq(dateTimeRE.test(actual), true, `${actual}`); + + let [, expectedDateTime, expectedTimeZone] = dateTimeRE.exec(expected); + let [, actualDateTime, actualTimeZone] = dateTimeRE.exec(actual); + + assertEq(actualDateTime, expectedDateTime); + + // The time zone identifier is optional, so only compare its value if it's + // present in |actual| and |expected|. + if (expectedTimeZone !== undefined && actualTimeZone !== undefined) { + assertEq(actualTimeZone, expectedTimeZone); + } +} + +// bug 637244 +inTimeZone("Asia/Novosibirsk", () => { + let dt1 = new Date(1984, Month.April, 1, -1); + assertDateTime(dt1, "Sat Mar 31 1984 23:00:00 GMT+0700 (NOVT)"); + + let dt2 = new Date(1984, Month.April, 1); + assertDateTime(dt2, "Sun Apr 01 1984 01:00:00 GMT+0800 (NOVST)"); +}); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/ecma_6/Date/time-zones-posix.js b/js/src/tests/ecma_6/Date/time-zones-posix.js new file mode 100644 index 000000000000..f194dd34effa --- /dev/null +++ b/js/src/tests/ecma_6/Date/time-zones-posix.js @@ -0,0 +1,198 @@ +// |reftest| skip-if(xulRuntime.OS=="WINNT"&&!xulRuntime.shell) -- Windows browser in automation doesn't pick up new time zones correctly + +// Repeats the test from "time-zones.js", but uses POSIX instead of IANA names +// for the time zones. This allows to run these tests on Windows, too. + +// From bug 1330149: +// +// Windows only supports a very limited set of IANA time zone names for the TZ +// environment variable. +// +// TZ format supported by Windows: "TZ=tzn[+|-]hh[:mm[:ss]][dzn]". +// +// Complete list of all IANA time zone ids matching that format. +// +// From tzdata's "northamerica" file: +// EST5EDT +// CST6CDT +// MST7MDT +// PST8PDT +// +// From tzdata's "backward" file: +// GMT+0 +// GMT-0 +// GMT0 + +// Perform the following replacements: +// America/New_York -> EST5EDT +// America/Chicago -> CST6CDT +// America/Denver -> MST7MDT +// America/Los_Angeles -> PST8PDT +// +// And remove any tests not matching one of the four time zones from above. + +const msPerHour = 60 * 60 * 1000; + +const Month = { + January: 0, + February: 1, + March: 2, + April: 3, + May: 4, + June: 5, + July: 6, + August: 7, + September: 8, + October: 9, + November: 10, + December: 11, +}; + +function inTimeZone(tzname, fn) { + setTimeZone(tzname); + try { + fn(); + } finally { + setTimeZone("PST8PDT"); + } +} + +const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].join("|"); +const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].join("|"); +const datePart = String.raw `(?:${weekdays}) (?:${months}) \d{2}`; +const timePart = String.raw `\d{4,6} \d{2}:\d{2}:\d{2} GMT[+-]\d{4}`; +const dateTimeRE = new RegExp(String.raw `^(${datePart} ${timePart})(?: \((.+)\))?$`); + +function assertDateTime(date, expected) { + let actual = date.toString(); + assertEq(dateTimeRE.test(expected), true, `${expected}`); + assertEq(dateTimeRE.test(actual), true, `${actual}`); + + let [, expectedDateTime, expectedTimeZone] = dateTimeRE.exec(expected); + let [, actualDateTime, actualTimeZone] = dateTimeRE.exec(actual); + + assertEq(actualDateTime, expectedDateTime); + + // The time zone identifier is optional, so only compare its value if it's + // present in |actual| and |expected|. + if (expectedTimeZone !== undefined && actualTimeZone !== undefined) { + assertEq(actualTimeZone, expectedTimeZone); + } +} + +// bug 294908 +inTimeZone("EST5EDT", () => { + let dt = new Date(2003, Month.April, 6, 2, 30, 00); + assertDateTime(dt, "Sun Apr 06 2003 03:30:00 GMT-0400 (EDT)"); +}); + +// bug 610183 +inTimeZone("PST8PDT", () => { + let dt = new Date(2014, Month.November, 2, 1, 47, 42); + assertDateTime(dt, "Sun Nov 02 2014 01:47:42 GMT-0700 (PDT)"); +}); + +// bug 629465 +inTimeZone("MST7MDT", () => { + let dt1 = new Date(Date.UTC(2015, Month.November, 1, 0, 0, 0) + 6 * msPerHour); + assertDateTime(dt1, "Sun Nov 01 2015 00:00:00 GMT-0600 (MDT)"); + + let dt2 = new Date(Date.UTC(2015, Month.November, 1, 1, 0, 0) + 6 * msPerHour); + assertDateTime(dt2, "Sun Nov 01 2015 01:00:00 GMT-0600 (MDT)"); + + let dt3 = new Date(Date.UTC(2015, Month.November, 1, 1, 0, 0) + 7 * msPerHour); + assertDateTime(dt3, "Sun Nov 01 2015 01:00:00 GMT-0700 (MST)"); +}); + +// bug 742427 +inTimeZone("EST5EDT", () => { + let dt = new Date(2009, Month.March, 8, 1, 0, 0); + assertDateTime(dt, "Sun Mar 08 2009 01:00:00 GMT-0500 (EST)"); + dt.setHours(dt.getHours() + 1); + assertDateTime(dt, "Sun Mar 08 2009 03:00:00 GMT-0400 (EDT)"); +}); +inTimeZone("MST7MDT", () => { + let dt = new Date(2009, Month.March, 8, 1, 0, 0); + assertDateTime(dt, "Sun Mar 08 2009 01:00:00 GMT-0700 (MST)"); + dt.setHours(dt.getHours() + 1); + assertDateTime(dt, "Sun Mar 08 2009 03:00:00 GMT-0600 (MDT)"); +}); +inTimeZone("EST5EDT", () => { + let dt1 = new Date(Date.UTC(2008, Month.March, 9, 0, 0, 0) + 5 * msPerHour); + assertDateTime(dt1, "Sun Mar 09 2008 00:00:00 GMT-0500 (EST)"); + + let dt2 = new Date(Date.UTC(2008, Month.March, 9, 1, 0, 0) + 5 * msPerHour); + assertDateTime(dt2, "Sun Mar 09 2008 01:00:00 GMT-0500 (EST)"); + + let dt3 = new Date(Date.UTC(2008, Month.March, 9, 4, 0, 0) + 4 * msPerHour); + assertDateTime(dt3, "Sun Mar 09 2008 04:00:00 GMT-0400 (EDT)"); +}); + +// bug 802627 +inTimeZone("EST5EDT", () => { + let dt = new Date(0); + assertDateTime(dt, "Wed Dec 31 1969 19:00:00 GMT-0500 (EST)"); +}); + +// bug 879261 +inTimeZone("EST5EDT", () => { + let dt1 = new Date(1362891600000); + assertDateTime(dt1, "Sun Mar 10 2013 00:00:00 GMT-0500 (EST)"); + + let dt2 = new Date(dt1.setHours(dt1.getHours() + 24)); + assertDateTime(dt2, "Mon Mar 11 2013 00:00:00 GMT-0400 (EDT)"); +}); +inTimeZone("PST8PDT", () => { + let dt1 = new Date(2014, Month.January, 1); + assertDateTime(dt1, "Wed Jan 01 2014 00:00:00 GMT-0800 (PST)"); + + let dt2 = new Date(2014, Month.August, 1); + assertDateTime(dt2, "Fri Aug 01 2014 00:00:00 GMT-0700 (PDT)"); +}); +inTimeZone("EST5EDT", () => { + let dt1 = new Date(2016, Month.October, 14, 3, 5, 9); + assertDateTime(dt1, "Fri Oct 14 2016 03:05:09 GMT-0400 (EDT)"); + + let dt2 = new Date(2016, Month.January, 9, 23, 26, 40); + assertDateTime(dt2, "Sat Jan 09 2016 23:26:40 GMT-0500 (EST)"); +}); + +// bug 1084547 +inTimeZone("EST5EDT", () => { + let dt = new Date(Date.parse("2014-11-02T02:00:00-04:00")); + assertDateTime(dt, "Sun Nov 02 2014 01:00:00 GMT-0500 (EST)"); + + dt.setMilliseconds(0); + assertDateTime(dt, "Sun Nov 02 2014 01:00:00 GMT-0400 (EDT)"); +}); + +// bug 1303306 +inTimeZone("EST5EDT", () => { + let dt = new Date(2016, Month.September, 15, 16, 14, 48); + assertDateTime(dt, "Thu Sep 15 2016 16:14:48 GMT-0400 (EDT)"); +}); + +// bug 1317364 +inTimeZone("PST8PDT", () => { + let dt = new Date(2016, Month.March, 13, 2, 30, 0, 0); + assertDateTime(dt, "Sun Mar 13 2016 03:30:00 GMT-0700 (PDT)"); + + let dt2 = new Date(2016, Month.January, 5, 0, 30, 30, 500); + assertDateTime(dt2, "Tue Jan 05 2016 00:30:30 GMT-0800 (PST)"); + + let dt3 = new Date(dt2.getTime()); + dt3.setMonth(dt2.getMonth() + 2); + dt3.setDate(dt2.getDate() + 7 + 1); + dt3.setHours(dt2.getHours() + 2); + + assertEq(dt3.getHours(), 3); +}); + +// bug 1355272 +inTimeZone("PST8PDT", () => { + let dt = new Date(2017, Month.April, 10, 17, 25, 07); + assertDateTime(dt, "Mon Apr 10 2017 17:25:07 GMT-0700 (PDT)"); +}); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/ecma_6/Date/time-zones.js b/js/src/tests/ecma_6/Date/time-zones.js new file mode 100644 index 000000000000..1fb01e483ca4 --- /dev/null +++ b/js/src/tests/ecma_6/Date/time-zones.js @@ -0,0 +1,310 @@ +// |reftest| skip-if(xulRuntime.OS=="WINNT") -- Windows doesn't accept IANA names for the TZ env variable + +const msPerHour = 60 * 60 * 1000; + +const Month = { + January: 0, + February: 1, + March: 2, + April: 3, + May: 4, + June: 5, + July: 6, + August: 7, + September: 8, + October: 9, + November: 10, + December: 11, +}; + +function inTimeZone(tzname, fn) { + setTimeZone(tzname); + try { + fn(); + } finally { + setTimeZone(undefined); + } +} + +const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].join("|"); +const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].join("|"); +const datePart = String.raw `(?:${weekdays}) (?:${months}) \d{2}`; +const timePart = String.raw `\d{4,6} \d{2}:\d{2}:\d{2} GMT[+-]\d{4}`; +const dateTimeRE = new RegExp(String.raw `^(${datePart} ${timePart})(?: \((.+)\))?$`); + +function assertDateTime(date, expected) { + let actual = date.toString(); + assertEq(dateTimeRE.test(expected), true, `${expected}`); + assertEq(dateTimeRE.test(actual), true, `${actual}`); + + let [, expectedDateTime, expectedTimeZone] = dateTimeRE.exec(expected); + let [, actualDateTime, actualTimeZone] = dateTimeRE.exec(actual); + + assertEq(actualDateTime, expectedDateTime); + + // The time zone identifier is optional, so only compare its value if it's + // present in |actual| and |expected|. + if (expectedTimeZone !== undefined && actualTimeZone !== undefined) { + assertEq(actualTimeZone, expectedTimeZone); + } +} + +// bug 158328 +inTimeZone("Europe/London", () => { + let dt1 = new Date(2002, Month.July, 19, 16, 10, 55); + assertDateTime(dt1, "Fri Jul 19 2002 16:10:55 GMT+0100 (BST)"); + + let dt2 = new Date(2009, Month.December, 24, 13, 44, 52); + assertDateTime(dt2, "Thu Dec 24 2009 13:44:52 GMT+0000 (GMT)"); +}); + +// bug 294908 +inTimeZone("America/New_York", () => { + let dt = new Date(2003, Month.April, 6, 2, 30, 00); + assertDateTime(dt, "Sun Apr 06 2003 03:30:00 GMT-0400 (EDT)"); +}); + +// bug 610183 +inTimeZone("America/Los_Angeles", () => { + let dt = new Date(2014, Month.November, 2, 1, 47, 42); + assertDateTime(dt, "Sun Nov 02 2014 01:47:42 GMT-0700 (PDT)"); +}); + +// bug 629465 +inTimeZone("America/Denver", () => { + let dt1 = new Date(Date.UTC(2015, Month.November, 1, 0, 0, 0) + 6 * msPerHour); + assertDateTime(dt1, "Sun Nov 01 2015 00:00:00 GMT-0600 (MDT)"); + + let dt2 = new Date(Date.UTC(2015, Month.November, 1, 1, 0, 0) + 6 * msPerHour); + assertDateTime(dt2, "Sun Nov 01 2015 01:00:00 GMT-0600 (MDT)"); + + let dt3 = new Date(Date.UTC(2015, Month.November, 1, 1, 0, 0) + 7 * msPerHour); + assertDateTime(dt3, "Sun Nov 01 2015 01:00:00 GMT-0700 (MST)"); +}); + +// bug 637244 +inTimeZone("Europe/Helsinki", () => { + let dt1 = new Date(2016, Month.March, 27, 2, 59); + assertDateTime(dt1, "Sun Mar 27 2016 02:59:00 GMT+0200 (EET)"); + + let dt2 = new Date(2016, Month.March, 27, 3, 0); + assertDateTime(dt2, "Sun Mar 27 2016 04:00:00 GMT+0300 (EEST)"); +}); + +// bug 718175 +inTimeZone("Europe/London", () => { + let dt = new Date(0); + assertEq(dt.getHours(), 1); +}); + +// bug 719274 +inTimeZone("Pacific/Auckland", () => { + let dt = new Date(2012, Month.January, 19, 12, 54, 27); + assertDateTime(dt, "Thu Jan 19 2012 12:54:27 GMT+1300 (NZDT)"); +}); + +// bug 742427 +inTimeZone("Europe/Paris", () => { + let dt1 = new Date(2009, Month.March, 29, 1, 0, 0); + assertDateTime(dt1, "Sun Mar 29 2009 01:00:00 GMT+0100 (CET)"); + dt1.setHours(dt1.getHours() + 1); + assertDateTime(dt1, "Sun Mar 29 2009 03:00:00 GMT+0200 (CEST)"); + + let dt2 = new Date(2010, Month.March, 29, 1, 0, 0); + assertDateTime(dt2, "Mon Mar 29 2010 01:00:00 GMT+0200 (CEST)"); + dt2.setHours(dt2.getHours() + 1); + assertDateTime(dt2, "Mon Mar 29 2010 02:00:00 GMT+0200 (CEST)"); +}); +inTimeZone("America/New_York", () => { + let dt = new Date(2009, Month.March, 8, 1, 0, 0); + assertDateTime(dt, "Sun Mar 08 2009 01:00:00 GMT-0500 (EST)"); + dt.setHours(dt.getHours() + 1); + assertDateTime(dt, "Sun Mar 08 2009 03:00:00 GMT-0400 (EDT)"); +}); +inTimeZone("America/Denver", () => { + let dt = new Date(2009, Month.March, 8, 1, 0, 0); + assertDateTime(dt, "Sun Mar 08 2009 01:00:00 GMT-0700 (MST)"); + dt.setHours(dt.getHours() + 1); + assertDateTime(dt, "Sun Mar 08 2009 03:00:00 GMT-0600 (MDT)"); +}); +inTimeZone("America/New_York", () => { + let dt1 = new Date(Date.UTC(2008, Month.March, 9, 0, 0, 0) + 5 * msPerHour); + assertDateTime(dt1, "Sun Mar 09 2008 00:00:00 GMT-0500 (EST)"); + + let dt2 = new Date(Date.UTC(2008, Month.March, 9, 1, 0, 0) + 5 * msPerHour); + assertDateTime(dt2, "Sun Mar 09 2008 01:00:00 GMT-0500 (EST)"); + + let dt3 = new Date(Date.UTC(2008, Month.March, 9, 4, 0, 0) + 4 * msPerHour); + assertDateTime(dt3, "Sun Mar 09 2008 04:00:00 GMT-0400 (EDT)"); +}); +inTimeZone("Europe/Paris", () => { + let dt1 = new Date(Date.UTC(2008, Month.March, 30, 0, 0, 0) - 1 * msPerHour); + assertDateTime(dt1, "Sun Mar 30 2008 00:00:00 GMT+0100 (CET)"); + + let dt2 = new Date(Date.UTC(2008, Month.March, 30, 1, 0, 0) - 1 * msPerHour); + assertDateTime(dt2, "Sun Mar 30 2008 01:00:00 GMT+0100 (CET)"); + + let dt3 = new Date(Date.UTC(2008, Month.March, 30, 3, 0, 0) - 2 * msPerHour); + assertDateTime(dt3, "Sun Mar 30 2008 03:00:00 GMT+0200 (CEST)"); + + let dt4 = new Date(Date.UTC(2008, Month.March, 30, 4, 0, 0) - 2 * msPerHour); + assertDateTime(dt4, "Sun Mar 30 2008 04:00:00 GMT+0200 (CEST)"); +}); + +// bug 802627 +inTimeZone("America/New_York", () => { + let dt = new Date(0); + assertDateTime(dt, "Wed Dec 31 1969 19:00:00 GMT-0500 (EST)"); +}); + +// bug 819820 +inTimeZone("Europe/London", () => { + let dt1 = new Date(Date.UTC(2012, Month.October, 28, 0, 59, 59)); + assertDateTime(dt1, "Sun Oct 28 2012 01:59:59 GMT+0100 (BST)"); + + let dt2 = new Date(Date.UTC(2012, Month.October, 28, 1, 0, 0)); + assertDateTime(dt2, "Sun Oct 28 2012 01:00:00 GMT+0000 (GMT)"); + + let dt3 = new Date(Date.UTC(2012, Month.October, 28, 1, 59, 59)); + assertDateTime(dt3, "Sun Oct 28 2012 01:59:59 GMT+0000 (GMT)"); + + let dt4 = new Date(Date.UTC(2012, Month.October, 28, 2, 0, 0)); + assertDateTime(dt4, "Sun Oct 28 2012 02:00:00 GMT+0000 (GMT)"); +}); + +// bug 879261 +inTimeZone("America/New_York", () => { + let dt1 = new Date(1362891600000); + assertDateTime(dt1, "Sun Mar 10 2013 00:00:00 GMT-0500 (EST)"); + + let dt2 = new Date(dt1.setHours(dt1.getHours() + 24)); + assertDateTime(dt2, "Mon Mar 11 2013 00:00:00 GMT-0400 (EDT)"); +}); +inTimeZone("America/Los_Angeles", () => { + let dt1 = new Date(2014, Month.January, 1); + assertDateTime(dt1, "Wed Jan 01 2014 00:00:00 GMT-0800 (PST)"); + + let dt2 = new Date(2014, Month.August, 1); + assertDateTime(dt2, "Fri Aug 01 2014 00:00:00 GMT-0700 (PDT)"); +}); +inTimeZone("America/New_York", () => { + let dt1 = new Date(2016, Month.October, 14, 3, 5, 9); + assertDateTime(dt1, "Fri Oct 14 2016 03:05:09 GMT-0400 (EDT)"); + + let dt2 = new Date(2016, Month.January, 9, 23, 26, 40); + assertDateTime(dt2, "Sat Jan 09 2016 23:26:40 GMT-0500 (EST)"); +}); + +// bug 994086 +inTimeZone("Europe/Vienna", () => { + let dt1 = new Date(2014, Month.March, 30, 2, 0); + assertDateTime(dt1, "Sun Mar 30 2014 03:00:00 GMT+0200 (CEST)"); + + let dt2 = new Date(2014, Month.March, 30, 3, 0); + assertDateTime(dt2, "Sun Mar 30 2014 03:00:00 GMT+0200 (CEST)"); + + let dt3 = new Date(2014, Month.March, 30, 4, 0); + assertDateTime(dt3, "Sun Mar 30 2014 04:00:00 GMT+0200 (CEST)"); +}); + +// bug 1084434 +inTimeZone("America/Sao_Paulo", () => { + let dt = new Date(2014, Month.October, 19); + assertEq(dt.getDate(), 19); + assertEq(dt.getHours(), 1); + assertDateTime(dt, "Sun Oct 19 2014 01:00:00 GMT-0200 (BRST)"); +}); + +// bug 1084547 +inTimeZone("America/New_York", () => { + let dt = new Date(Date.parse("2014-11-02T02:00:00-04:00")); + assertDateTime(dt, "Sun Nov 02 2014 01:00:00 GMT-0500 (EST)"); + + dt.setMilliseconds(0); + assertDateTime(dt, "Sun Nov 02 2014 01:00:00 GMT-0400 (EDT)"); +}); + +// bug 1118690 +inTimeZone("Europe/London", () => { + let dt = new Date(1965, Month.January, 1); + assertEq(dt.getFullYear(), 1965); +}); + +// bug 1155096 +inTimeZone("Europe/Moscow", () => { + let dt1 = new Date(1981, Month.March, 32); + assertEq(dt1.getDate(), 1); + + let dt2 = new Date(1982, Month.March, 32); + assertEq(dt2.getDate(), 1); + + let dt3 = new Date(1983, Month.March, 32); + assertEq(dt3.getDate(), 1); + + let dt4 = new Date(1984, Month.March, 32); + assertEq(dt4.getDate(), 1); +}); + +// bug 1284507 +inTimeZone("Atlantic/Azores", () => { + let dt1 = new Date(2017, Month.March, 25, 0, 0, 0); + assertDateTime(dt1, "Sat Mar 25 2017 00:00:00 GMT-0100 (AZOT)"); + + let dt2 = new Date(2016, Month.October, 30, 0, 0, 0); + assertDateTime(dt2, "Sun Oct 30 2016 00:00:00 GMT+0000 (AZOST)"); + + let dt3 = new Date(2016, Month.October, 30, 23, 0, 0); + assertDateTime(dt3, "Sun Oct 30 2016 23:00:00 GMT-0100 (AZOT)"); +}); + +// bug 1303306 +inTimeZone("America/New_York", () => { + let dt = new Date(2016, Month.September, 15, 16, 14, 48); + assertDateTime(dt, "Thu Sep 15 2016 16:14:48 GMT-0400 (EDT)"); +}); + +// bug 1317364 +inTimeZone("America/Los_Angeles", () => { + let dt = new Date(2016, Month.March, 13, 2, 30, 0, 0); + assertDateTime(dt, "Sun Mar 13 2016 03:30:00 GMT-0700 (PDT)"); + + let dt2 = new Date(2016, Month.January, 5, 0, 30, 30, 500); + assertDateTime(dt2, "Tue Jan 05 2016 00:30:30 GMT-0800 (PST)"); + + let dt3 = new Date(dt2.getTime()); + dt3.setMonth(dt2.getMonth() + 2); + dt3.setDate(dt2.getDate() + 7 + 1); + dt3.setHours(dt2.getHours() + 2); + + assertEq(dt3.getHours(), 3); +}); + +// bug 1335818 +inTimeZone("Asia/Jerusalem", () => { + let dt1 = new Date(2013, Month.March, 22, 1, 0, 0, 0); + assertDateTime(dt1, "Fri Mar 22 2013 01:00:00 GMT+0200 (IST)"); + + let dt2 = new Date(2013, Month.March, 22, 2, 0, 0, 0); + assertDateTime(dt2, "Fri Mar 22 2013 02:00:00 GMT+0200 (IST)"); + + let dt3 = new Date(2013, Month.March, 22, 3, 0, 0, 0); + assertDateTime(dt3, "Fri Mar 22 2013 03:00:00 GMT+0200 (IST)"); + + let dt4 = new Date(2013, Month.March, 29, 1, 0, 0, 0); + assertDateTime(dt4, "Fri Mar 29 2013 01:00:00 GMT+0200 (IST)"); + + let dt5 = new Date(2013, Month.March, 29, 2, 0, 0, 0); + assertDateTime(dt5, "Fri Mar 29 2013 03:00:00 GMT+0300 (IDT)"); + + let dt6 = new Date(2013, Month.March, 29, 3, 0, 0, 0); + assertDateTime(dt6, "Fri Mar 29 2013 03:00:00 GMT+0300 (IDT)"); +}); + +// bug 1355272 +inTimeZone("America/Los_Angeles", () => { + let dt = new Date(2017, Month.April, 10, 17, 25, 07); + assertDateTime(dt, "Mon Apr 10 2017 17:25:07 GMT-0700 (PDT)"); +}); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/vm/DateTime.cpp b/js/src/vm/DateTime.cpp index 3225165e5eb4..bc35dd5dd081 100644 --- a/js/src/vm/DateTime.cpp +++ b/js/src/vm/DateTime.cpp @@ -90,8 +90,11 @@ UTCToLocalStandardOffsetSeconds() currentNoDST = currentMaybeWithDST; } else { // If |local| respected DST, we need a time broken down into components - // ignoring DST. Turn off DST in the broken-down time. - local.tm_isdst = 0; + // ignoring DST. Turn off DST in the broken-down time. Create a fresh + // copy of |local|, because mktime() will reset tm_isdst = 1 and will + // adjust tm_hour and tm_hour accordingly. + struct tm localNoDST = local; + localNoDST.tm_isdst = 0; // Compute a |time_t t| corresponding to the broken-down time with DST // off. This has boundary-condition issues (for about the duration of @@ -99,7 +102,7 @@ UTCToLocalStandardOffsetSeconds() // zone. But 1) errors will be transient; 2) locations rarely change // time zone; and 3) in the absence of an API that provides the time // zone offset directly, this may be the best we can do. - currentNoDST = mktime(&local); + currentNoDST = mktime(&localNoDST); if (currentNoDST == time_t(-1)) return 0; } @@ -177,6 +180,8 @@ js::DateTimeInfo::computeDSTOffsetMilliseconds(int64_t utcSeconds) if (!ComputeLocalTime(static_cast(utcSeconds), &tm)) return 0; + // NB: The offset isn't computed correctly when the standard local offset + // at |utcSeconds| is different from |utcToLocalStandardOffsetSeconds|. int32_t dayoff = int32_t((utcSeconds + utcToLocalStandardOffsetSeconds) % SecondsPerDay); int32_t tmoff = tm.tm_sec + (tm.tm_min * SecondsPerMinute) + (tm.tm_hour * SecondsPerHour); @@ -184,6 +189,8 @@ js::DateTimeInfo::computeDSTOffsetMilliseconds(int64_t utcSeconds) if (diff < 0) diff += SecondsPerDay; + else if (uint32_t(diff) >= SecondsPerDay) + diff -= SecondsPerDay; return diff * msPerSecond; }