common: Unify Date/Time formatting, UTC offset
authorShane Synan <digitalcircuit36939@gmail.com>
Thu, 23 Aug 2018 00:34:40 +0000 (19:34 -0500)
committerManuel Nickschas <sputnick@quassel-irc.org>
Tue, 28 Aug 2018 20:21:45 +0000 (22:21 +0200)
Unify date/time formatting for the client UI, using
Qt::DateFormat::DefaultLocaleShortDate to respect the operating
system's configured locale.  This avoids Qt's kind-of-weird default
date/time format that mixes numbers and named short months/days.

See https://doc.qt.io/qt-5/qt.html#DateFormat-enum

Modify AM/PM detection to use application locale, which currently
defaults to system locale.  This avoids any inconsistencies should
Quassel later allow specifying an application-wide locale different
from the system locale.

In CTCP VERSION replies, always use UTC time in ISO8601 date format,
avoiding locale-specific replies and timezone differences.  This
fixes client version statistic-tracking software from treating
different languages and different timezones as unique versions of
Quassel IRC.

In CTCP TIME replies, use local time in ISO8601 date format with
timezone offset.  This provides a consistent format for others to
parse and still provides the timezone information.

Work around a Qt bug for the latter.

See https://bugreports.qt.io/browse/QTBUG-26161
And https://stackoverflow.com/questions/18750569/qdatetime-isodate-with-timezone

src/common/util.cpp
src/common/util.h
src/core/coresessioneventprocessor.cpp
src/qtui/aboutdlg.cpp
src/qtui/coreinfodlg.cpp
src/qtui/coresessionwidget.cpp
src/uisupport/uistyle.cpp
src/uisupport/uistyle.h

index a2c4e25..d7e83d8 100644 (file)
@@ -365,7 +365,7 @@ bool scopeMatch(const QString &string, const QString &scopeRule, const bool &isR
 }
 
 
-QString tryFormatUnixEpoch(const QString &possibleEpochDate)
+QString tryFormatUnixEpoch(const QString &possibleEpochDate, Qt::DateFormat dateFormat, bool useUTC)
 {
     // Does the string resemble a Unix epoch?  Parse as 64-bit time
     qint64 secsSinceEpoch = possibleEpochDate.toLongLong();
@@ -387,5 +387,50 @@ QString tryFormatUnixEpoch(const QString &possibleEpochDate)
 #endif
 
     // Return the localized date/time
-    return date.toString();
+    if (useUTC) {
+        // Return UTC time
+        return date.toUTC().toString(dateFormat);
+    } else if (dateFormat == Qt::DateFormat::ISODate) {
+        // Add in ISO local timezone information via special handling below
+        return formatDateTimeToOffsetISO(date);
+    } else {
+        // Return local time
+        return date.toString(dateFormat);
+    }
+}
+
+
+QString formatDateTimeToOffsetISO(const QDateTime &dateTime)
+{
+    if (!dateTime.isValid()) {
+        // Don't try to do anything with invalid date/time
+        return "formatDateTimeToISO() invalid date/time";
+    }
+
+#if 0
+    // The expected way to get a UTC offset on ISO8601 dates
+    return dateTime.toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate);
+#else
+    // Work around Qt bug that converts to UTC instead of including timezone information
+    // See https://bugreports.qt.io/browse/QTBUG-26161
+    //
+    // NOTE: Despite the bug report marking as fixed in Qt 5.2.0 (QT_VERSION >= 0x050200), this
+    // still appears broken in Qt 5.5.1.
+    //
+    // Credit to "user362638" for the solution below, modified to fit Quassel's needs
+    // https://stackoverflow.com/questions/18750569/qdatetime-isodate-with-timezone
+
+    // Get the local and UTC time
+    QDateTime local = QDateTime(dateTime);
+    QDateTime utc = local.toUTC();
+    utc.setTimeSpec(Qt::LocalTime);
+
+    // Find the UTC offset
+    int utcOffset = utc.secsTo(local);
+
+    // Force the local time to follow this offset
+    local.setUtcOffset(utcOffset);
+    // Now the output should be correct
+    return local.toString(Qt::ISODate);
+#endif
 }
index d72dd8e..514c564 100644 (file)
@@ -109,6 +109,19 @@ bool scopeMatch(const QString &string, const QString &scopeRule,
  * localizing if possible, leaving alone if not.
  *
  * @param possibleEpochDate Date/time that might be in seconds since Unix epoch format
+ * @param dateFormat        Desired format of the date/time string
+ * @param useUTC            If true, use UTC timezone, otherwise use local time
  * @return Localized date/time if parse succeeded, otherwise the source string
  */
-QString tryFormatUnixEpoch(const QString &possibleEpochDate);
+QString tryFormatUnixEpoch(const QString &possibleEpochDate,
+                           Qt::DateFormat dateFormat = Qt::DateFormat::TextDate,
+                           bool useUTC = false);
+
+
+/**
+ * Format the given date/time in ISO 8601 format with timezone offset
+ *
+ * @param dateTime Date/time of interest
+ * @return Date/time in ISO 8601 format with timezone offset
+ */
+QString formatDateTimeToOffsetISO(const QDateTime &dateTime);
index 2a3dfc2..a461491 100644 (file)
@@ -1613,30 +1613,22 @@ void CoreSessionEventProcessor::handleCtcpPing(CtcpEvent *e)
 
 void CoreSessionEventProcessor::handleCtcpTime(CtcpEvent *e)
 {
-    // Explicitly specify the Qt default DateTime format string to allow for modification
-    // Qt::TextDate default roughly corresponds to...
-    // > ddd MMM d yyyy HH:mm:ss
-    //
-    // See https://doc.qt.io/qt-5/qdatetime.html#toString
-    // And https://doc.qt.io/qt-5/qt.html#DateFormat-enum
-#if QT_VERSION > 0x050000
-    // Append the timezone identifier "t", so other other IRC users have a frame of reference for
-    // the current timezone.  This could be figured out before by manually comparing to UTC, so this
-    // is just convenience.
-
-    // Alas, "t" was only added in Qt 5
-    e->setReply(QDateTime::currentDateTime().toString("ddd MMM d yyyy HH:mm:ss t"));
-#else
-    e->setReply(QDateTime::currentDateTime().toString("ddd MMM d yyyy HH:mm:ss"));
-#endif
+    // Use the ISO standard to avoid locale-specific translated names
+    // Include timezone offset data to show which timezone a user's in, otherwise we're providing
+    // NTP-over-IRC with terrible accuracy.
+    e->setReply(formatDateTimeToOffsetISO(QDateTime::currentDateTime()));
 }
 
 
 void CoreSessionEventProcessor::handleCtcpVersion(CtcpEvent *e)
 {
     // Deliberately do not translate project name
+    // Use the ISO standard to avoid locale-specific translated names
+    // Use UTC time to provide a consistent string regardless of timezone
+    // (Statistics tracking tools usually only group client versions by exact string matching)
     e->setReply(QString("Quassel IRC %1 (version date %2) -- https://www.quassel-irc.org")
                 .arg(Quassel::buildInfo().plainVersionString)
                 .arg(Quassel::buildInfo().commitDate.isEmpty() ?
-                      "unknown" : tryFormatUnixEpoch(Quassel::buildInfo().commitDate)));
+                      "unknown" : tryFormatUnixEpoch(Quassel::buildInfo().commitDate,
+                                                     Qt::DateFormat::ISODate, true)));
 }
index 1468488..840da62 100644 (file)
@@ -44,7 +44,8 @@ AboutDlg::AboutDlg(QWidget *parent)
         versionDate = QString("<i>%1</i>").arg(tr("Unknown date"));
     }
     else {
-        versionDate = tryFormatUnixEpoch(Quassel::buildInfo().commitDate);
+        versionDate = tryFormatUnixEpoch(Quassel::buildInfo().commitDate,
+                                         Qt::DateFormat::DefaultLocaleShortDate);
     }
     ui.versionLabel->setText(QString(tr("<b>Version:</b> %1<br>"
                                         "<b>Version date:</b> %2<br>"
index b175977..47ab758 100644 (file)
@@ -100,7 +100,8 @@ void CoreInfoDlg::coreInfoChanged(const QVariantMap &coreInfo) {
         }
         else {
             ui.labelCoreVersionDate->setText(
-                        tryFormatUnixEpoch(coreInfo["quasselBuildDate"].toString()));
+                        tryFormatUnixEpoch(coreInfo["quasselBuildDate"].toString(),
+                        Qt::DateFormat::DefaultLocaleShortDate));
         }
         ui.labelClientCount->setNum(coreInfo["sessionConnectedClients"].toInt());
     }
@@ -174,7 +175,8 @@ void CoreInfoDlg::updateUptime() {
                                      .arg(uphours, 2, 10, QChar('0'))
                                      .arg(upmins, 2, 10, QChar('0'))
                                      .arg(uptime, 2, 10, QChar('0'))
-                                     .arg(startTime.toLocalTime().toString(Qt::TextDate));
+                                     .arg(startTime.toLocalTime()
+                                          .toString(Qt::DefaultLocaleShortDate));
         ui.labelUptime->setText(uptimeText);
     }
 }
index fbbb39b..29f323e 100644 (file)
@@ -41,9 +41,11 @@ void CoreSessionWidget::setData(QMap<QString, QVariant> map)
         ui.labelVersionDate->setText(QString("<i>%1</i>").arg(tr("Unknown date")));
     }
     else {
-        ui.labelVersionDate->setText(tryFormatUnixEpoch(map["clientVersionDate"].toString()));
+        ui.labelVersionDate->setText(tryFormatUnixEpoch(map["clientVersionDate"].toString(),
+                                     Qt::DateFormat::DefaultLocaleShortDate));
     }
-    ui.labelUptime->setText(map["connectedSince"].toDateTime().toLocalTime().toString(Qt::DateFormat::SystemLocaleShortDate));
+    ui.labelUptime->setText(map["connectedSince"].toDateTime()
+            .toLocalTime().toString(Qt::DateFormat::DefaultLocaleShortDate));
     if (map["location"].toString().isEmpty()) {
         ui.labelLocation->hide();
         ui.labelLocationTitle->hide();
index cbae7e6..038dc8a 100644 (file)
@@ -222,7 +222,7 @@ void UiStyle::updateSystemTimestampFormat()
     // Helpful interactive website for debugging and explaining:  https://regex101.com/
     const QRegExp regExpMatchAMPM(".*(\\b|_)(A|AP)(\\b|_).*", Qt::CaseInsensitive);
 
-    if (regExpMatchAMPM.exactMatch(QLocale::system().timeFormat(QLocale::ShortFormat))) {
+    if (regExpMatchAMPM.exactMatch(QLocale().timeFormat(QLocale::ShortFormat))) {
         // AM/PM style used
         _systemTimestampFormatString = " h:mm:ss ap";
     } else {
index 6ea9fa8..acf6597 100644 (file)
@@ -300,8 +300,8 @@ protected:
     /**
      * Cache the system locale timestamp format string
      *
-     * Based on whether or not AM/PM designators are used in the QLocale::system().timeFormat(),
-     * this extends the system locale timestamp format string to include seconds.
+     * Based on whether or not AM/PM designators are used in the QLocale.timeFormat(), this extends
+     * the application locale timestamp format string to include seconds.
      *
      * @see UiStyle::systemTimestampFormatString()
      */