Bug 830304 - Compute correct time zone offsets for Date methods. r=Waldo
authorAndré Bargull <andre.bargull@gmail.com>
Wed, 11 Oct 2017 06:54:44 -0700
changeset 428210 b7ef07909cc4277230ea5ea70399f1ad5e581778
parent 428209 fdb1abbe808a069d2b6ca4ae7ed98969a3fb7314
child 428211 5aa7980268cb3c335fb7d44090e925563be9d0a2
push id97
push userfmarier@mozilla.com
push dateSat, 14 Oct 2017 01:12:59 +0000
reviewersWaldo
bugs830304
milestone58.0a1
Bug 830304 - Compute correct time zone offsets for Date methods. r=Waldo
js/src/builtin/TestingFunctions.cpp
js/src/jsdate.cpp
js/src/tests/ecma_6/Date/browser.js
js/src/tests/ecma_6/Date/time-zone-pst.js
js/src/tests/ecma_6/Date/time-zones-pedantic.js
js/src/tests/ecma_6/Date/time-zones-posix.js
js/src/tests/ecma_6/Date/time-zones.js
js/src/vm/DateTime.cpp
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -8,16 +8,18 @@
 
 #include "mozilla/Atomics.h"
 #include "mozilla/FloatingPoint.h"
 #include "mozilla/Move.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/Unused.h"
 
 #include <cmath>
+#include <cstdlib>
+#include <ctime>
 
 #include "jsapi.h"
 #include "jscntxt.h"
 #include "jsfriendapi.h"
 #include "jsgc.h"
 #include "jsiter.h"
 #include "jsobj.h"
 #include "jsprf.h"
@@ -4732,16 +4734,128 @@ static bool
 DisableForEach(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     JS::ContextOptionsRef(cx).setForEachStatement(false);
     args.rval().setUndefined();
     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<std::time_t>(-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)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     uint32_t max_cnt;
     if (!ToUint32(cx, args.get(0), &max_cnt))
@@ -5409,16 +5523,20 @@ gc::ZealModeHelpText),
     JS_FN_HELP("isConstructor", IsConstructor, 1, 0,
 "isConstructor(value)",
 "  Returns whether the value is considered IsConstructor.\n"),
 
     JS_FN_HELP("isLegacyIterator", IsLegacyIterator, 1, 0,
 "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
 };
 
 static const JSFunctionSpecWithHelp FuzzingUnsafeTestingFunctions[] = {
 #ifdef DEBUG
     JS_FN_HELP("parseRegExp", ParseRegExp, 3, 0,
 "parseRegExp(pattern[, flags[, match_only])",
 "  Parses a RegExp pattern and returns a tree, potentially throwing."),
@@ -5427,16 +5545,22 @@ static const JSFunctionSpecWithHelp Fuzz
 "disRegExp(regexp[, match_only[, input]])",
 "  Dumps RegExp bytecode."),
 #endif
 
     JS_FN_HELP("getErrorNotes", GetErrorNotes, 1, 0,
 "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
 };
 
 bool
 js::DefineTestingFunctions(JSContext* cx, HandleObject obj, bool fuzzingSafe_,
                            bool disableOOMFunctions_)
 {
     fuzzingSafe = fuzzingSafe_;
--- a/js/src/jsdate.cpp
+++ b/js/src/jsdate.cpp
@@ -478,17 +478,24 @@ static double
 LocalTime(double t)
 {
     return t + AdjustTime(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. */
 static double
 HourFromTime(double t)
 {
     double result = fmod(floor(t/msPerHour), HoursPerDay);
     if (result < 0)
@@ -2579,33 +2586,31 @@ date_toJSON(JSContext* cx, unsigned argc
     }
 
     /* Step 6. */
     return Call(cx, toISO, obj, args.rval());
 }
 
 /* Interface to PRMJTime date struct. */
 static PRMJTime
-ToPRMJTime(double localTime)
+ToPRMJTime(double localTime, double utcTime)
 {
     double year = YearFromTime(localTime);
 
     PRMJTime prtm;
     prtm.tm_usec = int32_t(msFromTime(localTime)) * 1000;
     prtm.tm_sec = int8_t(SecFromTime(localTime));
     prtm.tm_min = int8_t(MinFromTime(localTime));
     prtm.tm_hour = int8_t(HourFromTime(localTime));
     prtm.tm_mday = int8_t(DateFromTime(localTime));
     prtm.tm_mon = int8_t(MonthFromTime(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;
 }
 
 enum class FormatSpec {
     DateTime,
     Date,
     Time
@@ -2642,17 +2647,17 @@ FormatDate(JSContext* cx, double utcTime
              * operating-system dependence on strftime (which PRMJ_FormatTime
              * calls, for %Z only.)  win32 prints PST as
              * 'Pacific Standard Time.'  This way we always know what we're
              * getting, and can parse it if we produce it.  The OS time zone
              * string is included as a comment.
              */
 
             /* 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) {
                 /*
                  * Decide whether to use the resulting time zone string.
                  *
                  * Reject it if it contains any non-ASCII, non-alphanumeric
                  * characters.  It's then likely in some other character
                  * encoding, and we probably won't display it correctly.
@@ -2722,17 +2727,17 @@ ToLocaleFormatHelper(JSContext* cx, Hand
 {
     double utcTime = obj->as<DateObject>().UTCTime().toNumber();
 
     char buf[100];
     if (!IsFinite(utcTime)) {
         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);
 
         /* If it failed, default to toString. */
         if (result_len == 0)
             return FormatDate(cx, utcTime, FormatSpec::DateTime, rval);
 
--- 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;
+}
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
--- a/js/src/vm/DateTime.cpp
+++ b/js/src/vm/DateTime.cpp
@@ -85,26 +85,29 @@ UTCToLocalStandardOffsetSeconds()
 
     // Compute a |time_t| corresponding to |local| interpreted without DST.
     time_t currentNoDST;
     if (local.tm_isdst == 0) {
         // If |local| wasn't DST, we can use the same time.
         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
         // a DST offset) near the time a location moves to a different time
         // 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;
     }
 
     // Break down the time corresponding to the no-DST |local| into UTC-based
     // components.
     struct tm utc;
     if (!ComputeUTCTime(currentNoDST, &utc))
@@ -172,23 +175,27 @@ js::DateTimeInfo::computeDSTOffsetMillis
 {
     MOZ_ASSERT(utcSeconds >= 0);
     MOZ_ASSERT(utcSeconds <= MaxUnixTimeT);
 
     struct tm tm;
     if (!ComputeLocalTime(static_cast<time_t>(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);
 
     int32_t diff = tmoff - dayoff;
 
     if (diff < 0)
         diff += SecondsPerDay;
+    else if (uint32_t(diff) >= SecondsPerDay)
+        diff -= SecondsPerDay;
 
     return diff * msPerSecond;
 }
 
 int64_t
 js::DateTimeInfo::internalGetDSTOffsetMilliseconds(int64_t utcMilliseconds)
 {
     sanityCheck();