From: Shane Synan Date: Thu, 23 Aug 2018 00:34:40 +0000 (-0500) Subject: common: Unify Date/Time formatting, UTC offset X-Git-Tag: 0.13-rc2~69 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=41a136e99bffde8e203fb1abff7c9affbbb16a8b common: Unify Date/Time formatting, UTC offset 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 --- diff --git a/src/common/util.cpp b/src/common/util.cpp index a2c4e25e..d7e83d8f 100644 --- a/src/common/util.cpp +++ b/src/common/util.cpp @@ -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 } diff --git a/src/common/util.h b/src/common/util.h index d72dd8ed..514c564f 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -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); diff --git a/src/core/coresessioneventprocessor.cpp b/src/core/coresessioneventprocessor.cpp index 2a3dfc2e..a461491c 100644 --- a/src/core/coresessioneventprocessor.cpp +++ b/src/core/coresessioneventprocessor.cpp @@ -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))); } diff --git a/src/qtui/aboutdlg.cpp b/src/qtui/aboutdlg.cpp index 14684888..840da62f 100644 --- a/src/qtui/aboutdlg.cpp +++ b/src/qtui/aboutdlg.cpp @@ -44,7 +44,8 @@ AboutDlg::AboutDlg(QWidget *parent) versionDate = QString("%1").arg(tr("Unknown date")); } else { - versionDate = tryFormatUnixEpoch(Quassel::buildInfo().commitDate); + versionDate = tryFormatUnixEpoch(Quassel::buildInfo().commitDate, + Qt::DateFormat::DefaultLocaleShortDate); } ui.versionLabel->setText(QString(tr("Version: %1
" "Version date: %2
" diff --git a/src/qtui/coreinfodlg.cpp b/src/qtui/coreinfodlg.cpp index b1759776..47ab7589 100644 --- a/src/qtui/coreinfodlg.cpp +++ b/src/qtui/coreinfodlg.cpp @@ -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); } } diff --git a/src/qtui/coresessionwidget.cpp b/src/qtui/coresessionwidget.cpp index fbbb39b8..29f323e1 100644 --- a/src/qtui/coresessionwidget.cpp +++ b/src/qtui/coresessionwidget.cpp @@ -41,9 +41,11 @@ void CoreSessionWidget::setData(QMap map) ui.labelVersionDate->setText(QString("%1").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(); diff --git a/src/uisupport/uistyle.cpp b/src/uisupport/uistyle.cpp index cbae7e68..038dc8ab 100644 --- a/src/uisupport/uistyle.cpp +++ b/src/uisupport/uistyle.cpp @@ -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 { diff --git a/src/uisupport/uistyle.h b/src/uisupport/uistyle.h index 6ea9fa8c..acf6597b 100644 --- a/src/uisupport/uistyle.h +++ b/src/uisupport/uistyle.h @@ -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() */