From 80ca1dafa6f90817cea64c16113ae277e089f1a2 Mon Sep 17 00:00:00 2001
From: Edward Welbourne <edward.welbourne@qt.io>
Date: Fri, 28 Oct 2016 17:24:46 +0200
Subject: [PATCH] V4 Date: fix what we can within ECMA 262's limitations

Use QDateTime and QTimeZone to simplify code (when we can).
Note which defective methods are so because the ECMAScript spec
requires those defects, reference my bug report against those defects.
Fix currentTime to return a UTC time.

Fix getLocalTZA to actually deliver the standard offset, without
daylight-saving corrections.

Fix DaylightSavingTA(t) to return the difference between current
standard time offset and pertinent total offset at time t (explaining
why that's the least broken answer), rather than assuming DST is one
hour when active.  (In some places it's half an hour, in some places
two hours; and the bugs in the spec break UTC() and LocalTime() if we
only return actual DST offset, without adjusting for historical
changes to standard offset.)

Fix tests to use actual IANA IDs for zones.

Change QTime-to-Time conversion to use a modernish date, for which
time-zone data is likely to at least be consistently handled, instead
of MakeDay(0, 0, 0); that's (nominal proleptic Gregorian) 2 BC,
December 31st (represented denormally) for which any time-zone data we
have is artificial and contrived.  I chose Cassini's third centennary,
as it's not likely to be near any zone transitions and is within the
era of sane time-zone data.

Task-number: QTBUG-56787
Change-Id: I5f21ee2010070a5c1798134fdd2915a723208fd9
Reviewed-by: Lars Knoll <lars.knoll@qt.io>
---
 src/qml/jsruntime/qv4dateobject.cpp          | 119 +++++++++++++------
 tests/auto/qml/ecmascripttests/test262.py    |   4 +-
 tests/auto/qml/qqmllocale/tst_qqmllocale.cpp |   8 +-
 3 files changed, 86 insertions(+), 45 deletions(-)

diff --git a/src/qml/jsruntime/qv4dateobject.cpp b/src/qml/jsruntime/qv4dateobject.cpp
index b90c335b1..324c4059b 100644
--- a/src/qml/jsruntime/qv4dateobject.cpp
+++ b/src/qml/jsruntime/qv4dateobject.cpp
@@ -54,16 +54,29 @@
 
 #include <wtf/MathExtras.h>
 
-#ifdef Q_OS_WIN
-#  include <windows.h>
+#ifdef Q_OS_LINUX
+/*
+  See QTBUG-56899.  Although we don't (yet) have a proper way to reset the
+  system zone, the code below, that uses QTimeZone::systemTimeZone(), works
+  adequately on Linux, when the TZ environment variable is changed.
+ */
+#define USE_QTZ_SYSTEM_ZONE
+#endif
+
+#ifdef USE_QTZ_SYSTEM_ZONE
+#include <QtCore/QTimeZone>
 #else
-#  ifndef Q_OS_VXWORKS
-#    include <sys/time.h>
+#  ifdef Q_OS_WIN
+#    include <windows.h>
 #  else
-#    include "qplatformdefs.h"
+#    ifndef Q_OS_VXWORKS
+#      include <sys/time.h>
+#    else
+#      include "qplatformdefs.h"
+#    endif
+#    include <unistd.h> // for _POSIX_THREAD_SAFE_FUNCTIONS
 #  endif
-#  include <unistd.h> // for _POSIX_THREAD_SAFE_FUNCTIONS
-#endif
+#endif // USE_QTZ_SYSTEM_ZONE
 
 using namespace QV4;
 
@@ -75,6 +88,7 @@ static const double msPerMinute = 60000.0;
 static const double msPerHour = 3600000.0;
 static const double msPerDay = 86400000.0;
 
+// The current *standard* time offset, regardless of DST:
 static double LocalTZA = 0.0; // initialized at startup
 
 static inline double TimeWithinDay(double t)
@@ -285,6 +299,31 @@ static inline double MakeDate(double day, double time)
     return day * msPerDay + time;
 }
 
+#ifdef USE_QTZ_SYSTEM_ZONE
+/*
+  ECMAScript specifies use of a fixed (current, standard) time-zone offset,
+  LocalTZA; and LocalTZA + DaylightSavingTA(t) is taken to be (see LocalTime and
+  UTC, following) local time's offset from UTC at time t.  For simple zones,
+  DaylightSavingTA(t) is thus the DST offset applicable at date/time t; however,
+  if a zone has changed its standard offset, the only way to make LocalTime and
+  UTC (if implemented in accord with the spec) perform correct transformations
+  is to have DaylightSavingTA(t) correct for the zone's standard offset change
+  as well as its actual DST offset.
+
+  This means we have to treat any historical changes in the zone's standard
+  offset as DST perturbations, regardless of historical reality.  (This shall
+  mean a whole day of DST offset for some zones, that have crossed the
+  international date line.  This shall confuse client code.)  The bug report
+  against the ECMAScript spec is https://github.com/tc39/ecma262/issues/725
+*/
+
+static inline double DaylightSavingTA(double t) // t is a UTC time
+{
+    return QTimeZone::systemTimeZone().offsetFromUtc(
+        QDateTime::fromMSecsSinceEpoch(qint64(t), Qt::UTC)) * 1e3 - LocalTZA;
+}
+#else
+// This implementation fails to take account of past changes in standard offset.
 static inline double DaylightSavingTA(double t)
 {
     struct tm tmtm;
@@ -306,34 +345,26 @@ static inline double DaylightSavingTA(double t)
         return 0;
     return (tmtm.tm_isdst > 0) ? msPerHour : 0;
 }
+#endif // USE_QTZ_SYSTEM_ZONE
 
 static inline double LocalTime(double t)
 {
+    // Flawed, yet verbatim from the spec:
     return t + LocalTZA + DaylightSavingTA(t);
 }
 
+// The spec does note [*] that UTC and LocalTime are not quite mutually inverse.
+// [*] http://www.ecma-international.org/ecma-262/7.0/index.html#sec-utc-t
+
 static inline double UTC(double t)
 {
+    // Flawed, yet verbatim from the spec:
     return t - LocalTZA - DaylightSavingTA(t - LocalTZA);
 }
 
 static inline double currentTime()
 {
-#ifndef Q_OS_WIN
-    struct timeval tv;
-
-    gettimeofday(&tv, 0);
-    return ::floor(tv.tv_sec * msPerSecond + (tv.tv_usec / 1000.0));
-#else
-    SYSTEMTIME st;
-    GetSystemTime(&st);
-    FILETIME ft;
-    SystemTimeToFileTime(&st, &ft);
-    LARGE_INTEGER li;
-    li.LowPart = ft.dwLowDateTime;
-    li.HighPart = ft.dwHighDateTime;
-    return double(li.QuadPart - Q_INT64_C(116444736000000000)) / 10000.0;
-#endif
+    return QDateTime::currentDateTimeUtc().toMSecsSinceEpoch();
 }
 
 static inline double TimeClip(double t)
@@ -616,20 +647,28 @@ static inline QString ToLocaleTimeString(double t)
 static double getLocalTZA()
 {
 #ifndef Q_OS_WIN
+    tzset();
+#endif
+#ifdef USE_QTZ_SYSTEM_ZONE
+    // TODO: QTimeZone::resetSystemTimeZone(), see QTBUG-56899 and comment above.
+    // Standard offset, with no daylight-savings adjustment, in ms:
+    return QTimeZone::systemTimeZone().standardTimeOffset(QDateTime::currentDateTime()) * 1e3;
+#else
+#  ifdef Q_OS_WIN
+    TIME_ZONE_INFORMATION tzInfo;
+    GetTimeZoneInformation(&tzInfo);
+    return -tzInfo.Bias * 60.0 * 1000.0;
+#  else
     struct tm t;
     time_t curr;
-    tzset();
     time(&curr);
-    localtime_r(&curr, &t);
+    localtime_r(&curr, &t); // Wrong: includes DST offset
     time_t locl = mktime(&t);
     gmtime_r(&curr, &t);
     time_t globl = mktime(&t);
     return (double(locl) - double(globl)) * 1000.0;
-#else
-    TIME_ZONE_INFORMATION tzInfo;
-    GetTimeZoneInformation(&tzInfo);
-    return -tzInfo.Bias * 60.0 * 1000.0;
-#endif
+#  endif
+#endif // USE_QTZ_SYSTEM_ZONE
 }
 
 DEFINE_OBJECT_VTABLE(DateObject);
@@ -648,17 +687,19 @@ void Heap::DateObject::init(const QTime &time)
         return;
     }
 
-    /* All programmers know that stuff starts at 0. Whatever that may mean in this context (and
-     * local timezone), it's before the epoch, so there is defenitely no DST problem. Specifically:
-     * you can't start with a date before the epoch, add some[*] hours, and end up with a date
-     * after. That's a problem for timezones where new year happens during DST, like
-     * Australia/Hobart, because we have to ignore DST before the epoch (but honor it after the
-     * epoch).
-     *
-     * [*] Well, when "some" is in the range 0-24. If you add something like 1M then this might
-     *     still happen.
+    /* We have to chose a date on which to instantiate this time.  All we really
+     * care about is that it round-trips back to the same time if we extract the
+     * time from it, which shall (via toQDateTime(), below) discard the date
+     * part.  We need a date for which time-zone data is likely to be sane (so
+     * MakeDay(0, 0, 0) was a bad choice; 2 BC, December 31st is before
+     * time-zones were standardized), with no transition nearby in date.  We
+     * ignore DST transitions before 1970, but even then zone transitions did
+     * happen.  Some do happen at new year, others on DST transitions in spring
+     * and autumn; so pick the three hundredth anniversary of the birth of
+     * Giovanni Domenico Cassini (1625-06-08), whose work first let us
+     * synchronize clocks tolerably accurately at distant locations.
      */
-    static const double d = MakeDay(0, 0, 0);
+    static const double d = MakeDay(1925, 5, 8);
     double t = MakeTime(time.hour(), time.minute(), time.second(), time.msec());
     date = TimeClip(UTC(MakeDate(d, t)));
 }
diff --git a/tests/auto/qml/ecmascripttests/test262.py b/tests/auto/qml/ecmascripttests/test262.py
index 437cd1b27..ae4c54df9 100755
--- a/tests/auto/qml/ecmascripttests/test262.py
+++ b/tests/auto/qml/ecmascripttests/test262.py
@@ -552,10 +552,10 @@ class TestSuite(object):
 
 
 def Main():
-  # Some date tests rely on being run in pacific time.
   # Uncomment the next line for more logging info.
   #logging.basicConfig(level=logging.DEBUG)
-  os.environ["TZ"] = "PST8PDT"
+  # Some date tests rely on being run in pacific time and the USA's locale:
+  os.environ["TZ"] = "America/Vancouver"
   os.environ["LANG"] = "en_US.UTF-8"
   os.environ["LC_TIME"] = "en_US.UTF-8"
   parser = BuildOptions()
diff --git a/tests/auto/qml/qqmllocale/tst_qqmllocale.cpp b/tests/auto/qml/qqmllocale/tst_qqmllocale.cpp
index d0ce83b99..4f4deaafe 100644
--- a/tests/auto/qml/qqmllocale/tst_qqmllocale.cpp
+++ b/tests/auto/qml/qqmllocale/tst_qqmllocale.cpp
@@ -1268,8 +1268,8 @@ void tst_qqmllocale::timeZoneUpdated()
 {
     QByteArray original(qgetenv("TZ"));
 
-    // Set the timezone to Brisbane time
-    setTimeZone(QByteArray("AEST-10:00"));
+    // Set the timezone to Brisbane time, AEST-10:00
+    setTimeZone(QByteArray("Australia/Brisbane"));
 
     DateFormatter formatter;
 
@@ -1281,8 +1281,8 @@ void tst_qqmllocale::timeZoneUpdated()
     QVERIFY(obj);
     QVERIFY(obj->property("success").toBool());
 
-    // Change to Indian time
-    setTimeZone(QByteArray("IST-05:30"));
+    // Change to Indian time, IST-05:30
+    setTimeZone(QByteArray("Asia/Kolkata"));
 
     QMetaObject::invokeMethod(obj.data(), "check");
 
-- 
2.19.1

