Bug 1406993 - Add a new equivalent year mapping table for years after 2037. r=Waldo
authorAndré Bargull <andre.bargull@gmail.com>
Fri, 03 Nov 2017 03:12:47 -0700
changeset 390012 45a2ee7d468eb695608f095d66b4981dbf7f4574
parent 390011 eabded1b884a4e6d803abe1cfe0fef4df97a2044
child 390013 80a1b262d7a74e32d32f5aac097125f8c230dc89
push id96978
push userryanvm@gmail.com
push dateFri, 03 Nov 2017 17:47:59 +0000
treeherdermozilla-inbound@45a2ee7d468e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersWaldo
bugs1406993
milestone58.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 1406993 - Add a new equivalent year mapping table for years after 2037. r=Waldo
js/src/jsdate.cpp
js/src/tests/ecma_6/Date/time-zone-2038-pst.js
js/src/vm/Time.cpp
js/src/vm/Time.h
--- a/js/src/jsdate.cpp
+++ b/js/src/jsdate.cpp
@@ -424,41 +424,57 @@ EquivalentYearForDST(int year)
     /*
      * Years and leap years on which Jan 1 is a Sunday, Monday, etc.
      *
      * yearStartingWith[0][i] is an example non-leap year where
      * Jan 1 appears on Sunday (i == 0), Monday (i == 1), etc.
      *
      * yearStartingWith[1][i] is an example leap year where
      * Jan 1 appears on Sunday (i == 0), Monday (i == 1), etc.
+     *
+     * Keep two different mappings, one for past years (< 1970), and a
+     * different one for future years (> 2037).
      */
-    static const int yearStartingWith[2][7] = {
+    static const int pastYearStartingWith[2][7] = {
         {1978, 1973, 1974, 1975, 1981, 1971, 1977},
         {1984, 1996, 1980, 1992, 1976, 1988, 1972}
     };
+    static const int futureYearStartingWith[2][7] = {
+        {2034, 2035, 2030, 2031, 2037, 2027, 2033},
+        {2012, 2024, 2036, 2020, 2032, 2016, 2028}
+    };
 
     int day = int(DayFromYear(year) + 4) % 7;
     if (day < 0)
         day += 7;
 
+    const auto& yearStartingWith = year < 1970 ? pastYearStartingWith : futureYearStartingWith;
     return yearStartingWith[IsLeapYear(year)][day];
 }
 
+// Return true if |t| is representable as a 32-bit time_t variable, that means
+// the year is in [1970, 2038).
+static bool
+IsRepresentableAsTime32(double t)
+{
+    return 0.0 <= t && t < 2145916800000.0;
+}
+
 /* ES5 15.9.1.8. */
 static double
 DaylightSavingTA(double t)
 {
     if (!IsFinite(t))
         return GenericNaN();
 
     /*
      * If earlier than 1970 or after 2038, potentially beyond the ken of
      * many OSes, map it to an equivalent year before asking.
      */
-    if (t < 0.0 || t > 2145916800000.0) {
+    if (!IsRepresentableAsTime32(t)) {
         int year = EquivalentYearForDST(int(YearFromTime(t)));
         double day = MakeDay(year, MonthFromTime(t), DateFromTime(t));
         t = MakeDate(day, TimeWithinDay(t));
     }
 
     int64_t utcMilliseconds = static_cast<int64_t>(t);
     int64_t offsetMilliseconds = DateTimeInfo::getDSTOffsetMilliseconds(utcMilliseconds);
     return static_cast<double>(offsetMilliseconds);
@@ -2616,16 +2632,28 @@ ToPRMJTime(double localTime, double utcT
     prtm.tm_wday = int8_t(WeekDay(localTime));
     prtm.tm_year = year;
     prtm.tm_yday = int16_t(DayWithinYear(localTime, year));
     prtm.tm_isdst = (DaylightSavingTA(utcTime) != 0);
 
     return prtm;
 }
 
+static size_t
+FormatTime(char* buf, int buflen, const char* fmt, double utcTime, double localTime)
+{
+    PRMJTime prtm = ToPRMJTime(localTime, utcTime);
+    int eqivalentYear = IsRepresentableAsTime32(utcTime)
+                        ? prtm.tm_year
+                        : EquivalentYearForDST(prtm.tm_year);
+    int offsetInSeconds = (int) floor((localTime - utcTime) / msPerSecond);
+
+    return PRMJ_FormatTime(buf, buflen, fmt, &prtm, eqivalentYear, offsetInSeconds);
+}
+
 enum class FormatSpec {
     DateTime,
     Date,
     Time
 };
 
 static bool
 FormatDate(JSContext* cx, double utcTime, FormatSpec format, MutableHandleValue rval)
@@ -2658,18 +2686,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(localTime, utcTime);
-            size_t tzlen = PRMJ_FormatTime(tzbuf, sizeof tzbuf, "(%Z)", &prtm);
+            size_t tzlen = FormatTime(tzbuf, sizeof tzbuf, "(%Z)", utcTime, localTime);
             if (tzlen != 0) {
                 /*
                  * Decide whether to use the resulting time zone string.
                  *
                  * Reject it if it contains any non-ASCII or non-printable
                  * characters.  It's then likely in some other character
                  * encoding, and we probably won't display it correctly.
                  */
@@ -2739,20 +2766,19 @@ 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, utcTime);
 
         /* Let PRMJTime format it. */
-        size_t result_len = PRMJ_FormatTime(buf, sizeof buf, format, &prtm);
+        size_t result_len = FormatTime(buf, sizeof buf, format, utcTime, localTime);
 
         /* If it failed, default to toString. */
         if (result_len == 0)
             return FormatDate(cx, utcTime, FormatSpec::DateTime, rval);
 
         /* Hacked check against undesired 2-digit year 00/00/00 form. */
         if (strcmp(format, "%x") == 0 && result_len >= 6 &&
             /* Format %x means use OS settings, which may have 2-digit yr, so
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Date/time-zone-2038-pst.js
@@ -0,0 +1,59 @@
+// |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 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,
+};
+
+// U.S. daylight saving rules changed in 2007, excerpt from tzdata's
+// northamerica file:
+// NAME  FROM  TO    IN   ON       AT    SAVE  LETTER/S   
+// US    1967  2006  Oct  lastSun  2:00  0     S
+// US    1967  1973  Apr  lastSun  2:00  1:00  D
+// US    1974  only  Jan  6        2:00  1:00  D
+// US    1975  only  Feb  23       2:00  1:00  D
+// US    1976  1986  Apr  lastSun  2:00  1:00  D
+// US    1987  2006  Apr  Sun>=1   2:00  1:00  D
+// US    2007  max   Mar  Sun>=8   2:00  1:00  D
+// US    2007  max   Nov  Sun>=1   2:00  0     S
+
+
+// When 2040 is mapped to 1984, the old U.S. rules are applied, i.e. DST isn't
+// yet observed on March 31. If mapped to 2012, the new U.S. rules are applied
+// and DST is already observed, which is the expected behaviour.
+// A similar effect is visible in November.
+// NOTE: This test expects that 2012 and 2040 use the same DST rules. If this
+//       ever changes, the test needs to be updated accordingly.
+{
+    let dt1 = new Date(2040, Month.March, 31);
+    assertEq(dt1.toString(), "Sat Mar 31 2040 00:00:00 GMT-0700 (PDT)");
+
+    let dt2 = new Date(2040, Month.November, 1);
+    assertEq(dt2.toString(), "Thu Nov 01 2040 00:00:00 GMT-0700 (PDT)");
+}
+
+// 2038 is mapped to 2027 instead of 1971.
+{
+    let dt1 = new Date(2038, Month.March, 31);
+    assertEq(dt1.toString(), "Wed Mar 31 2038 00:00:00 GMT-0700 (PDT)");
+
+    let dt2 = new Date(2038, Month.November, 1);
+    assertEq(dt2.toString(), "Mon Nov 01 2038 00:00:00 GMT-0700 (PDT)");
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
--- a/js/src/vm/Time.cpp
+++ b/js/src/vm/Time.cpp
@@ -256,22 +256,22 @@ PRMJ_InvalidParameterHandler(const wchar
                              uintptr_t      pReserved)
 {
     /* empty */
 }
 #endif
 
 /* Format a time value into a buffer. Same semantics as strftime() */
 size_t
-PRMJ_FormatTime(char* buf, int buflen, const char* fmt, PRMJTime* prtm)
+PRMJ_FormatTime(char* buf, int buflen, const char* fmt, const PRMJTime* prtm,
+                int equivalentYear, int offsetInSeconds)
 {
     size_t result = 0;
 #if defined(XP_UNIX) || defined(XP_WIN)
     struct tm a;
-    int fake_tm_year = 0;
 #ifdef XP_WIN
     _invalid_parameter_handler oldHandler;
 #ifndef __MINGW32__
     int oldReportMode;
 #endif // __MINGW32__
 #endif //XP_WIN
 
     memset(&a, 0, sizeof(struct tm));
@@ -284,16 +284,17 @@ PRMJ_FormatTime(char* buf, int buflen, c
     a.tm_wday = prtm->tm_wday;
 
     /*
      * On systems where |struct tm| has members tm_gmtoff and tm_zone, we
      * must fill in those values, or else strftime will return wrong results
      * (e.g., bug 511726, bug 554338).
      */
 #if defined(HAVE_LOCALTIME_R) && defined(HAVE_TM_ZONE_TM_GMTOFF)
+    char emptyTimeZoneId[] = "";
     {
         /*
          * Fill out |td| to the time represented by |prtm|, leaving the
          * timezone fields zeroed out. localtime_r will then fill in the
          * timezone fields for that local time according to the system's
          * timezone parameters.
          */
         struct tm td;
@@ -302,33 +303,49 @@ PRMJ_FormatTime(char* buf, int buflen, c
         td.tm_min = prtm->tm_min;
         td.tm_hour = prtm->tm_hour;
         td.tm_mday = prtm->tm_mday;
         td.tm_mon = prtm->tm_mon;
         td.tm_wday = prtm->tm_wday;
         td.tm_year = prtm->tm_year - 1900;
         td.tm_yday = prtm->tm_yday;
         td.tm_isdst = prtm->tm_isdst;
+
         time_t t = mktime(&td);
-        localtime_r(&t, &td);
+
+        // If |prtm| cannot be represented in |time_t| the year is probably
+        // out of range, try again with the DST equivalent year.
+        if (t == static_cast<time_t>(-1)) {
+            td.tm_year = equivalentYear - 1900;
+            t = mktime(&td);
+        }
 
-        a.tm_gmtoff = td.tm_gmtoff;
-        a.tm_zone = td.tm_zone;
+        // If either mktime or localtime_r failed, fill in the fallback time
+        // zone offset |offsetInSeconds| and set the time zone identifier to
+        // the empty string.
+        if (t != static_cast<time_t>(-1) && localtime_r(&t, &td)) {
+            a.tm_gmtoff = td.tm_gmtoff;
+            a.tm_zone = td.tm_zone;
+        } else {
+            a.tm_gmtoff = offsetInSeconds;
+            a.tm_zone = emptyTimeZoneId;
+        }
     }
 #endif
 
     /*
      * Years before 1900 and after 9999 cause strftime() to abort on Windows.
      * To avoid that we replace it with FAKE_YEAR_BASE + year % 100 and then
      * replace matching substrings in the strftime() result with the real year.
      * Note that FAKE_YEAR_BASE should be a multiple of 100 to make 2-digit
      * year formats (%y) work correctly (since we won't find the fake year
      * in that case).
      */
-#define FAKE_YEAR_BASE 9900
+    constexpr int FAKE_YEAR_BASE = 9900;
+    int fake_tm_year = 0;
     if (prtm->tm_year < 1900 || prtm->tm_year > 9999) {
         fake_tm_year = FAKE_YEAR_BASE + prtm->tm_year % 100;
         a.tm_year = fake_tm_year - 1900;
     }
     else {
         a.tm_year = prtm->tm_year - 1900;
     }
     a.tm_yday = prtm->tm_yday;
--- a/js/src/vm/Time.h
+++ b/js/src/vm/Time.h
@@ -49,17 +49,18 @@ extern void
 PRMJ_NowShutdown();
 #else
 inline void
 PRMJ_NowShutdown() {}
 #endif
 
 /* Format a time value into a buffer. Same semantics as strftime() */
 extern size_t
-PRMJ_FormatTime(char* buf, int buflen, const char* fmt, PRMJTime* tm);
+PRMJ_FormatTime(char* buf, int buflen, const char* fmt, const PRMJTime* tm,
+                int equivalentYear, int offsetInSeconds);
 
 
 /**
  * Requesting the number of cycles from the CPU.
  *
  * `rdtsc`, or Read TimeStamp Cycle, is an instruction provided by
  * x86-compatible CPUs that lets processes request the number of
  * cycles spent by the CPU executing instructions since the CPU was