Fix long-form zone parts in date-time strings
Recent changes to date-time formatting arranged for time-zone names to be localized appropriately. Teach the datetime parser to recognize the long names that now implies, as well as IANA IDs, and ensure these last are also used as a fallback when no localized long name is available. Discrepancies between the platform time-zone backend and our CLDR-derived name L10n (where needed, due to the backend lacking it) make the naive approach to this (find any zone that matches, expect them all to have the same offset history) fail, so take care in this case to select as canonical an IANA ID for the named zone as we can find. Add a round-trip test and adapt an existing test to round-trip when it can. [ChangeLog][QtCore][QDateTime] The tttt format specifier now uses the full long name of the zone, falling back to its IANA ID only if this cannot be determined. Both forms are now recognized when reading a datetime from a string. [ChangeLog][QtCore][QLocale] When serializing a datetime, the tttt specifier now uses the localized full long name of the zone, falling back to its IANA ID only if this cannot be determined. Both forms are now recognized when reading a datetime from a string. Pick-to: 6.8 Fixes: QTBUG-130278 Change-Id: Ia28529790c0f600930d55c92a606adbcdfa852b9 Reviewed-by: Ivan Solovev <ivan.solovev@qt.io> (cherry picked from commit 2edd9286cf386675be76032424248e60216f6331)
This commit is contained in:
parent
a55c9dbabc
commit
1e486c14d4
@ -3882,8 +3882,15 @@ QString QCalendarBackend::dateTimeToString(QStringView format, const QDateTime &
|
|||||||
text = when.timeRepresentation().displayName(when, mode, locale);
|
text = when.timeRepresentation().displayName(when, mode, locale);
|
||||||
if (!text.isEmpty())
|
if (!text.isEmpty())
|
||||||
return text;
|
return text;
|
||||||
// else fall back to an unlocalized one if we can manage it:
|
// else fall back to an unlocalized one if we can find one.
|
||||||
} // else: prefer QDateTime's abbreviation, for backwards-compatibility.
|
}
|
||||||
|
if (type == Long) {
|
||||||
|
// If no long name found, use IANA ID:
|
||||||
|
text = QString::fromLatin1(when.timeZone().id());
|
||||||
|
if (!text.isEmpty())
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
// else: prefer QDateTime's abbreviation, for backwards-compatibility.
|
||||||
#endif // else, make do with non-localized abbreviation:
|
#endif // else, make do with non-localized abbreviation:
|
||||||
// Absent timezone_locale data, Offset might still reach here:
|
// Absent timezone_locale data, Offset might still reach here:
|
||||||
if (type == Offset) // Our prior failure might not have tried this:
|
if (type == Offset) // Our prior failure might not have tried this:
|
||||||
|
@ -2210,13 +2210,13 @@ QString QTime::toString(Qt::DateFormat format) const
|
|||||||
\li The timezone's offset from UTC with a colon between the hours and
|
\li The timezone's offset from UTC with a colon between the hours and
|
||||||
minutes (for example "+02:00").
|
minutes (for example "+02:00").
|
||||||
\row \li tttt
|
\row \li tttt
|
||||||
\li The timezone name (for example "Europe/Berlin"). Note that this
|
\li The timezone name, as provided by \l QTimeZone::displayName() with
|
||||||
gives no indication of whether the datetime was in daylight-saving
|
the \l QTimeZone::LongName type. This may depend on the operating
|
||||||
time or standard time, which may lead to ambiguity if the datetime
|
system in use. If no such name is available, the IANA ID of the
|
||||||
falls in an hour repeated by a transition between the two. The name
|
zone (such as "Europe/Berlin") may be used. It may give no
|
||||||
used is the one provided by \l QTimeZone::displayName() with the \l
|
indication of whether the datetime was in daylight-saving time or
|
||||||
QTimeZone::LongName type. This may depend on the operating system
|
standard time, which may lead to ambiguity if the datetime falls in
|
||||||
in use.
|
an hour repeated by a transition between the two.
|
||||||
\endtable
|
\endtable
|
||||||
|
|
||||||
\note To get localized forms of AM or PM (the \c{AP}, \c{ap}, \c{A}, \c{a},
|
\note To get localized forms of AM or PM (the \c{AP}, \c{ap}, \c{A}, \c{a},
|
||||||
@ -5879,9 +5879,10 @@ QDateTime QDateTime::fromString(QStringView string, Qt::DateFormat format)
|
|||||||
\li the timezone in offset format with a colon between hours and
|
\li the timezone in offset format with a colon between hours and
|
||||||
minutes (for example "+02:00")
|
minutes (for example "+02:00")
|
||||||
\row \li tttt
|
\row \li tttt
|
||||||
\li the timezone name (for example "Europe/Berlin"). The name
|
\li the timezone name, either what \l QTimeZone::displayName() reports
|
||||||
recognized are those known to \l QTimeZone, which may depend on the
|
for \l QTimeZone::LongName or the IANA ID of the zone (for example
|
||||||
operating system in use.
|
"Europe/Berlin"). The names recognized are those known to \l
|
||||||
|
QTimeZone, which may depend on the operating system in use.
|
||||||
\endtable
|
\endtable
|
||||||
|
|
||||||
If no 't' format specifier is present, the system's local time-zone is used.
|
If no 't' format specifier is present, the system's local time-zone is used.
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
#include "qtimezone.h"
|
#include "qtimezone.h"
|
||||||
#include "qvarlengtharray.h"
|
#include "qvarlengtharray.h"
|
||||||
#include "private/qlocale_p.h"
|
#include "private/qlocale_p.h"
|
||||||
|
#if QT_CONFIG(timezone)
|
||||||
|
#include "private/qtimezoneprivate_p.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "private/qstringiterator_p.h"
|
#include "private/qstringiterator_p.h"
|
||||||
#include "private/qtenvironmentvariables_p.h"
|
#include "private/qtenvironmentvariables_p.h"
|
||||||
@ -1216,6 +1219,27 @@ static int startsWithLocalTimeZone(QStringView name, const QDateTime &when, cons
|
|||||||
return int(longest);
|
return int(longest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if QT_CONFIG(timezone)
|
||||||
|
static auto findZoneByLongName(QStringView str, const QLocale &locale, const QDateTime &when)
|
||||||
|
{
|
||||||
|
struct R
|
||||||
|
{
|
||||||
|
QTimeZone zone;
|
||||||
|
qsizetype nameLength = 0;
|
||||||
|
bool isValid() const { return nameLength > 0 && zone.isValid(); }
|
||||||
|
} result;
|
||||||
|
auto pfx = QTimeZonePrivate::findLongNamePrefix(str, locale, when.toMSecsSinceEpoch());
|
||||||
|
if (!pfx.nameLength) // Incomplete data in when: try without time-point.
|
||||||
|
pfx = QTimeZonePrivate::findLongNamePrefix(str, locale);
|
||||||
|
if (pfx.nameLength > 0) {
|
||||||
|
result = R{ QTimeZone(pfx.ianaId), pfx.nameLength };
|
||||||
|
Q_ASSERT(result.zone.isValid());
|
||||||
|
// TODO: we should be able to take pfx.timeType into account.
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#endif // timezone
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
\internal
|
\internal
|
||||||
*/
|
*/
|
||||||
@ -1301,9 +1325,14 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
|
|||||||
timeZone = QTimeZone::fromSecondsAheadOfUtc(sect.value);
|
timeZone = QTimeZone::fromSecondsAheadOfUtc(sect.value);
|
||||||
#if QT_CONFIG(timezone)
|
#if QT_CONFIG(timezone)
|
||||||
} else if (startsWithLocalTimeZone(zoneName, usedDateTime, locale()) != sect.used) {
|
} else if (startsWithLocalTimeZone(zoneName, usedDateTime, locale()) != sect.used) {
|
||||||
QTimeZone namedZone = QTimeZone(zoneName.toLatin1());
|
if (QTimeZone namedZone = QTimeZone(zoneName.toLatin1()); namedZone.isValid()) {
|
||||||
Q_ASSERT(namedZone.isValid());
|
timeZone = namedZone;
|
||||||
timeZone = namedZone;
|
} else {
|
||||||
|
auto found = findZoneByLongName(zoneName, locale(), usedDateTime);
|
||||||
|
Q_ASSERT(found.isValid());
|
||||||
|
Q_ASSERT(found.nameLength == zoneName.length());
|
||||||
|
timeZone = found.zone;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
timeZone = QTimeZone::LocalTime;
|
timeZone = QTimeZone::LocalTime;
|
||||||
@ -1841,12 +1870,17 @@ QDateTimeParser::findTimeZoneName(QStringView str, const QDateTime &when) const
|
|||||||
lastSlash = slash;
|
lastSlash = slash;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (; index > systemLength; --index) { // Find longest match
|
// Find longest IANA ID match:
|
||||||
str.truncate(index);
|
for (QStringView copy = str; index > systemLength; --index) {
|
||||||
QTimeZone zone(str.toLatin1());
|
copy.truncate(index);
|
||||||
|
QTimeZone zone(copy.toLatin1());
|
||||||
if (zone.isValid())
|
if (zone.isValid())
|
||||||
return ParsedSection(Acceptable, zone.offsetFromUtc(when), index);
|
return ParsedSection(Acceptable, zone.offsetFromUtc(when), index);
|
||||||
}
|
}
|
||||||
|
// Not a known IANA ID.
|
||||||
|
|
||||||
|
if (auto found = findZoneByLongName(str, locale(), when); found.isValid())
|
||||||
|
return ParsedSection(Acceptable, found.zone.offsetFromUtc(when), found.nameLength);
|
||||||
#endif
|
#endif
|
||||||
if (systemLength > 0) // won't actually use the offset, but need it to be valid
|
if (systemLength > 0) // won't actually use the offset, but need it to be valid
|
||||||
return ParsedSection(Acceptable, when.toLocalTime().offsetFromUtc(), systemLength);
|
return ParsedSection(Acceptable, when.toLocalTime().offsetFromUtc(), systemLength);
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
#if !QT_CONFIG(icu)
|
#if !QT_CONFIG(icu)
|
||||||
# include <QtCore/qspan.h>
|
# include <QtCore/qspan.h>
|
||||||
# include <private/qdatetime_p.h>
|
# include <private/qdatetime_p.h>
|
||||||
|
# include <private/qtools_p.h>
|
||||||
// Use data generated from CLDR:
|
// Use data generated from CLDR:
|
||||||
# include "qtimezonelocale_data_p.h"
|
# include "qtimezonelocale_data_p.h"
|
||||||
# include "qtimezoneprivate_data_p.h"
|
# include "qtimezoneprivate_data_p.h"
|
||||||
@ -229,6 +230,22 @@ quint16 metaZoneAt(QByteArrayView zoneId, qint64 atMSecsSinceEpoch)
|
|||||||
return it != stop && it->begin <= dt ? it->metaZoneKey : 0;
|
return it != stop && it->begin <= dt ? it->metaZoneKey : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True if the named zone is ever part of the specified metazone:
|
||||||
|
bool zoneEverInMeta(QByteArrayView zoneId, quint16 metaKey)
|
||||||
|
{
|
||||||
|
for (auto it = std::lower_bound(std::begin(zoneHistoryTable), std::end(zoneHistoryTable),
|
||||||
|
zoneId,
|
||||||
|
[](const ZoneMetaHistory &record, QByteArrayView id) {
|
||||||
|
return record.ianaId().compare(id, Qt::CaseInsensitive) < 0;
|
||||||
|
});
|
||||||
|
it != std::end(zoneHistoryTable) && it->ianaId().compare(zoneId, Qt::CaseInsensitive) == 0;
|
||||||
|
++it) {
|
||||||
|
if (it->metaZoneKey == metaKey)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
constexpr bool dataBeforeMeta(const MetaZoneData &row, quint16 metaKey) noexcept
|
constexpr bool dataBeforeMeta(const MetaZoneData &row, quint16 metaKey) noexcept
|
||||||
{
|
{
|
||||||
return row.metaZoneKey < metaKey;
|
return row.metaZoneKey < metaKey;
|
||||||
@ -361,6 +378,147 @@ QString formatOffset(QStringView format, int offsetMinutes, const QLocale &local
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct OffsetFormatMatch
|
||||||
|
{
|
||||||
|
qsizetype size = 0;
|
||||||
|
int offset = 0;
|
||||||
|
operator bool() { return size != 0; }
|
||||||
|
};
|
||||||
|
|
||||||
|
OffsetFormatMatch matchOffsetText(QStringView text, QStringView format, const QLocale &locale,
|
||||||
|
QLocale::FormatType scale)
|
||||||
|
{
|
||||||
|
// Sign is taken care of by caller.
|
||||||
|
// TODO (QTBUG-77948): rework in terms of text pattern matchers.
|
||||||
|
// For now, don't try to be general, it gets too tricky.
|
||||||
|
OffsetFormatMatch res;
|
||||||
|
// At least at CLDR v46:
|
||||||
|
// Amharic in Ethiopia has ±HHmm formats; all others use separators.
|
||||||
|
// None have single m. All have H or HH before mm. (None has anything after mm.)
|
||||||
|
// In narrow format, mm and its preceding separator are elided for 0
|
||||||
|
// minutes; and hour may be single digit even if the format says HH.
|
||||||
|
qsizetype cut = format.indexOf(u'H');
|
||||||
|
if (cut < 0 || !text.startsWith(format.first(cut)) || !format.endsWith(u"mm"))
|
||||||
|
return res;
|
||||||
|
text = text.sliced(cut);
|
||||||
|
QStringView sep = format.sliced(cut).chopped(2); // Prune prefix and "mm".
|
||||||
|
int hlen = 1; // We already know we have one 'H' at the start of sep.
|
||||||
|
while (hlen < sep.size() && sep[hlen] == u'H')
|
||||||
|
++hlen;
|
||||||
|
sep = sep.sliced(hlen);
|
||||||
|
|
||||||
|
int digits = 0;
|
||||||
|
while (digits < text.size() && digits < 4 && text[digits].isDigit())
|
||||||
|
++digits;
|
||||||
|
|
||||||
|
// See zoneOffsetFormat() for the eccentric meaning of scale.
|
||||||
|
QStringView minStr;
|
||||||
|
if (sep.isEmpty()) {
|
||||||
|
if (digits > hlen) {
|
||||||
|
// Long and Short formats allow two-digit match when hlen < 2.
|
||||||
|
if (scale == QLocale::NarrowFormat || (hlen < 2 && text[0] != u'0'))
|
||||||
|
hlen = digits - 2;
|
||||||
|
else if (digits < hlen + 2)
|
||||||
|
return res;
|
||||||
|
minStr = text.sliced(hlen).first(2);
|
||||||
|
} else if (scale == QLocale::NarrowFormat) {
|
||||||
|
hlen = digits;
|
||||||
|
} else if (hlen != digits) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const qsizetype sepAt = text.indexOf(sep); // May be -1; digits isn't < -1.
|
||||||
|
if (digits < sepAt) // Separator doesn't immediately follow hour.
|
||||||
|
return res;
|
||||||
|
if (scale == QLocale::NarrowFormat || (hlen < 2 && text[0] != u'0'))
|
||||||
|
hlen = digits;
|
||||||
|
else if (digits != hlen)
|
||||||
|
return res;
|
||||||
|
if (sepAt >= 0 && text.size() >= sepAt + sep.size() + 2)
|
||||||
|
minStr = text.sliced(sepAt + sep.size()).first(2);
|
||||||
|
else if (scale != QLocale::NarrowFormat)
|
||||||
|
return res;
|
||||||
|
else if (sepAt >= 0) // Allow minutes without zero-padding in narrow format.
|
||||||
|
minStr = text.sliced(sepAt + sep.size());
|
||||||
|
}
|
||||||
|
if (hlen < 1)
|
||||||
|
return res;
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
uint minute = minStr.isEmpty() ? 0 : locale.toUInt(minStr, &ok);
|
||||||
|
if (!ok && scale == QLocale::NarrowFormat) {
|
||||||
|
// Fall back to matching hour-only form:
|
||||||
|
minStr = {};
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
if (ok && minute < 60) {
|
||||||
|
uint hour = locale.toUInt(text.first(hlen), &ok);
|
||||||
|
if (ok) {
|
||||||
|
res.offset = (hour * 60 + minute) * 60;
|
||||||
|
res.size = cut + hlen;
|
||||||
|
if (!minStr.isEmpty())
|
||||||
|
res.size += sep.size() + minStr.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
OffsetFormatMatch matchOffsetFormat(QStringView text, const QLocale &locale, qsizetype locInd,
|
||||||
|
QLocale::FormatType scale)
|
||||||
|
{
|
||||||
|
const LocaleZoneData &locData = localeZoneData[locInd];
|
||||||
|
const QStringView posHourForm = locData.posHourFormat().viewData(hourFormatTable);
|
||||||
|
const QStringView negHourForm = locData.negHourFormat().viewData(hourFormatTable);
|
||||||
|
// For the negative format, allow U+002d to match U+2212 or locale.negativeSign();
|
||||||
|
const bool mapNeg = text.contains(u'-')
|
||||||
|
&& (negHourForm.contains(u'\u2212') || negHourForm.contains(locale.negativeSign()));
|
||||||
|
// See zoneOffsetFormat() for the eccentric meaning of scale.
|
||||||
|
if (scale == QLocale::ShortFormat) {
|
||||||
|
if (auto match = matchOffsetText(text, posHourForm, locale, scale))
|
||||||
|
return match;
|
||||||
|
if (auto match = matchOffsetText(text, negHourForm, locale, scale)) {
|
||||||
|
return { match.size, -match.offset };
|
||||||
|
} else if (mapNeg) {
|
||||||
|
const QString mapped = negHourForm.toString()
|
||||||
|
.replace(u'\u2212', u'-').replace(locale.negativeSign(), "-"_L1);
|
||||||
|
if (auto match = matchOffsetText(text, mapped, locale, scale))
|
||||||
|
return { match.size, -match.offset };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const QStringView offsetFormat = locData.offsetGmtFormat().viewData(gmtFormatTable);
|
||||||
|
qsizetype cut = offsetFormat.indexOf(u"%0"); // Should be present
|
||||||
|
if (cut >= 0) {
|
||||||
|
const QStringView gmtPrefix = offsetFormat.first(cut);
|
||||||
|
const QStringView gmtSuffix = offsetFormat.sliced(cut + 2); // After %0
|
||||||
|
const qsizetype gmtSize = cut + gmtSuffix.size();
|
||||||
|
// Cheap pre-test: check suffix does appear after prefix, albeit we must
|
||||||
|
// later check it actually appears right after the offset text:
|
||||||
|
if ((gmtPrefix.isEmpty() || text.startsWith(gmtPrefix))
|
||||||
|
&& (gmtSuffix.isEmpty() || text.sliced(cut).indexOf(gmtSuffix) >= 0)) {
|
||||||
|
if (auto match = matchOffsetText(text.sliced(cut), posHourForm, locale, scale)) {
|
||||||
|
if (text.sliced(cut + match.size).startsWith(gmtSuffix)) // too sliced ?
|
||||||
|
return { gmtSize + match.size, match.offset };
|
||||||
|
}
|
||||||
|
if (auto match = matchOffsetText(text.sliced(cut), negHourForm, locale, scale)) {
|
||||||
|
if (text.sliced(cut + match.size).startsWith(gmtSuffix))
|
||||||
|
return { gmtSize + match.size, -match.offset };
|
||||||
|
} else if (mapNeg) {
|
||||||
|
const QString mapped = negHourForm.toString()
|
||||||
|
.replace(u'\u2212', u'-').replace(locale.negativeSign(), "-"_L1);
|
||||||
|
if (auto match = matchOffsetText(text.sliced(cut), mapped, locale, scale)) {
|
||||||
|
if (text.sliced(cut + match.size).startsWith(gmtSuffix))
|
||||||
|
return { gmtSize + match.size, -match.offset };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Match empty offset as UTC (unless that'd be an empty match):
|
||||||
|
if (gmtSize > 0 && text.sliced(cut).startsWith(gmtSuffix))
|
||||||
|
return { gmtSize, 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
} // nameless namespace
|
} // nameless namespace
|
||||||
|
|
||||||
namespace QtTimeZoneLocale {
|
namespace QtTimeZoneLocale {
|
||||||
@ -427,6 +585,7 @@ QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc
|
|||||||
return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::LongFormat,
|
return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::LongFormat,
|
||||||
QDateTime(), offsetFromUtc);
|
QDateTime(), offsetFromUtc);
|
||||||
}
|
}
|
||||||
|
// Handling of long names must stay in sync with findLongNamePrefix(), below.
|
||||||
|
|
||||||
// An IANA ID may give clues to fall back on for abbreviation or exemplar city:
|
// An IANA ID may give clues to fall back on for abbreviation or exemplar city:
|
||||||
QByteArray ianaAbbrev, ianaTail;
|
QByteArray ianaAbbrev, ianaTail;
|
||||||
@ -636,6 +795,223 @@ QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc
|
|||||||
return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::NarrowFormat,
|
return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::NarrowFormat,
|
||||||
QDateTime(), offsetFromUtc);
|
QDateTime(), offsetFromUtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Match what the above might return at the start of a text (usually a tail of a
|
||||||
|
// datetime string).
|
||||||
|
QTimeZonePrivate::NamePrefixMatch
|
||||||
|
QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale,
|
||||||
|
std::optional<qint64> atEpochMillis)
|
||||||
|
{
|
||||||
|
constexpr std::size_t invalidMetaId = std::size(metaIdData);
|
||||||
|
constexpr std::size_t invalidIanaId = std::size(ianaIdData);
|
||||||
|
constexpr QTimeZone::TimeType timeTypes[] = {
|
||||||
|
// In preference order, should more than one match:
|
||||||
|
QTimeZone::GenericTime,
|
||||||
|
QTimeZone::StandardTime,
|
||||||
|
QTimeZone::DaylightTime,
|
||||||
|
};
|
||||||
|
struct {
|
||||||
|
qsizetype nameLength = 0;
|
||||||
|
QTimeZone::TimeType timeType = QTimeZone::GenericTime;
|
||||||
|
quint16 ianaIdIndex = invalidIanaId;
|
||||||
|
quint16 metaIdIndex = invalidMetaId;
|
||||||
|
QLocale::Territory where = QLocale::AnyTerritory;
|
||||||
|
} best;
|
||||||
|
#define localeRows(table, member) QSpan(table).first(nextData.member).sliced(locData.member)
|
||||||
|
|
||||||
|
const QList<qsizetype> indices = fallbackLocalesFor(locale.d->m_index);
|
||||||
|
for (const qsizetype locInd : indices) {
|
||||||
|
const LocaleZoneData &locData = localeZoneData[locInd];
|
||||||
|
// After the row for the last actual locale, there's a terminal row:
|
||||||
|
Q_ASSERT(std::size_t(locInd) < std::size(localeZoneData) - 1);
|
||||||
|
const LocaleZoneData &nextData = localeZoneData[locInd + 1];
|
||||||
|
|
||||||
|
const auto metaRows = localeRows(localeMetaZoneLongNameTable, m_metaLongTableStart);
|
||||||
|
for (const LocaleMetaZoneLongNames &row : metaRows) {
|
||||||
|
for (const QTimeZone::TimeType type : timeTypes) {
|
||||||
|
QLocaleData::DataRange range = row.longName(type);
|
||||||
|
if (range.size > best.nameLength) {
|
||||||
|
QStringView name = range.viewData(longMetaZoneNameTable);
|
||||||
|
if (text.startsWith(name)) {
|
||||||
|
best = { range.size, type, invalidIanaId, row.metaIdIndex };
|
||||||
|
if (best.nameLength >= text.size())
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best.nameLength >= text.size())
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ianaRows = localeRows(localeZoneNameTable, m_zoneTableStart);
|
||||||
|
for (const LocaleZoneNames &row : ianaRows) {
|
||||||
|
for (const QTimeZone::TimeType type : timeTypes) {
|
||||||
|
QLocaleData::DataRange range = row.longName(type);
|
||||||
|
if (range.size > best.nameLength) {
|
||||||
|
QStringView name = range.viewData(longZoneNameTable);
|
||||||
|
// Save potentially expensive "zone is supported" check when possible:
|
||||||
|
bool gotZone = row.ianaIdIndex == best.ianaIdIndex
|
||||||
|
|| QTimeZone::isTimeZoneIdAvailable(row.ianaId().toByteArray());
|
||||||
|
if (text.startsWith(name) && gotZone)
|
||||||
|
best = { range.size, type, row.ianaIdIndex };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// That's found us our best match, possibly as a meta-zone
|
||||||
|
if (best.metaIdIndex != invalidMetaId) {
|
||||||
|
const auto metaIdBefore = [](auto &row, quint16 key) { return row.metaIdIndex < key; };
|
||||||
|
// Find the standard IANA ID for this meta-zone (or one for another
|
||||||
|
// supported zone using the meta-zone at the specified time).
|
||||||
|
const MetaZoneData *metaRow =
|
||||||
|
std::lower_bound(std::begin(metaZoneTable), std::end(metaZoneTable),
|
||||||
|
best.metaIdIndex, metaIdBefore);
|
||||||
|
// Table is sorted by metazone, then territory.
|
||||||
|
for (; metaRow < std::end(metaZoneTable)
|
||||||
|
&& metaRow->metaIdIndex == best.metaIdIndex; ++metaRow) {
|
||||||
|
auto metaLand = QLocale::Territory(metaRow->territory);
|
||||||
|
// World entry is the "standard" zone for this metazone, so always
|
||||||
|
// prefer it over any territory-specific one (from an earlier row):
|
||||||
|
if ((best.where == QLocale::AnyTerritory || metaLand == QLocale::World)
|
||||||
|
&& (atEpochMillis
|
||||||
|
? metaRow->metaZoneKey == metaZoneAt(metaRow->ianaId(), *atEpochMillis)
|
||||||
|
: zoneEverInMeta(metaRow->ianaId(), metaRow->metaZoneKey))) {
|
||||||
|
if (metaRow->ianaIdIndex == best.ianaIdIndex
|
||||||
|
|| QTimeZone::isTimeZoneIdAvailable(metaRow->ianaId().toByteArray())) {
|
||||||
|
best.ianaIdIndex = metaRow->ianaIdIndex;
|
||||||
|
best.where = metaLand;
|
||||||
|
if (best.where == QLocale::World)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best.ianaIdIndex != invalidIanaId)
|
||||||
|
return { QByteArray(ianaIdData + best.ianaIdIndex), best.nameLength, best.timeType };
|
||||||
|
|
||||||
|
// Now try for a region format:
|
||||||
|
best = {};
|
||||||
|
for (const qsizetype locInd : indices) {
|
||||||
|
const LocaleZoneData &locData = localeZoneData[locInd];
|
||||||
|
const LocaleZoneData &nextData = localeZoneData[locInd + 1];
|
||||||
|
for (const QTimeZone::TimeType timeType : timeTypes) {
|
||||||
|
QStringView regionFormat
|
||||||
|
= locData.regionFormatRange(timeType).viewData(regionFormatTable);
|
||||||
|
// "%0 [Season] Time", "Time in %0 [during Season]" &c.
|
||||||
|
const qsizetype cut = regionFormat.indexOf(u"%0");
|
||||||
|
if (cut < 0) // Shouldn't happen unless empty.
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QStringView prefix = regionFormat.first(cut);
|
||||||
|
// Any text before %0 must appear verbatim at the start of our text:
|
||||||
|
if (cut > 0 && !text.startsWith(prefix))
|
||||||
|
continue;
|
||||||
|
QStringView suffix = regionFormat.sliced(cut + 2); // after %0
|
||||||
|
// This must start with an exemplar city or territory, followed by suffix:
|
||||||
|
QStringView tail = text.sliced(cut);
|
||||||
|
|
||||||
|
// Cheap pretest - any text after %0 must appear *somewhere* in our text:
|
||||||
|
if (suffix.size() && tail.indexOf(suffix) < 0)
|
||||||
|
continue; // No match possible
|
||||||
|
|
||||||
|
// Of course, particularly if just punctuation, a copy of our suffix
|
||||||
|
// might appear within the city or territory name.
|
||||||
|
const auto textMatches = [tail, suffix](QStringView where) {
|
||||||
|
return (where.isEmpty() || tail.startsWith(where))
|
||||||
|
&& (suffix.isEmpty() || tail.sliced(where.size()).startsWith(suffix));
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto cityRows = localeRows(localeZoneExemplarTable, m_exemplarTableStart);
|
||||||
|
for (const LocaleZoneExemplar &row : cityRows) {
|
||||||
|
QStringView city = row.exemplarCity().viewData(exemplarCityTable);
|
||||||
|
if (textMatches(city)) {
|
||||||
|
qsizetype length = cut + city.size() + suffix.size();
|
||||||
|
if (length > best.nameLength) {
|
||||||
|
bool gotZone = row.ianaIdIndex == best.ianaIdIndex
|
||||||
|
|| QTimeZone::isTimeZoneIdAvailable(row.ianaId().toByteArray());
|
||||||
|
if (gotZone)
|
||||||
|
best = { length, timeType, row.ianaIdIndex };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// In localeName() we fall back to the last part of the IANA ID:
|
||||||
|
const QList<QByteArray> allZones = QTimeZone::availableTimeZoneIds();
|
||||||
|
for (const auto &iana : allZones) {
|
||||||
|
Q_ASSERT(!iana.isEmpty());
|
||||||
|
qsizetype slash = iana.lastIndexOf('/');
|
||||||
|
QByteArray local = slash > 0 ? iana.sliced(slash + 1) : iana;
|
||||||
|
QString city = QString::fromLatin1(local.replace('_', ' '));
|
||||||
|
if (textMatches(city)) {
|
||||||
|
qsizetype length = cut + city.size() + suffix.size();
|
||||||
|
if (length > best.nameLength) {
|
||||||
|
// Have to find iana in ianaIdData. Although its entries
|
||||||
|
// from locale-independent data are nicely sorted, the
|
||||||
|
// rest are (sadly) not.
|
||||||
|
QByteArrayView run(ianaIdData, qstrlen(ianaIdData));
|
||||||
|
// std::size includes the trailing '\0', so subtract one:
|
||||||
|
const char *stop = ianaIdData + std::size(ianaIdData) - 1;
|
||||||
|
while (run != iana) {
|
||||||
|
if (run.end() < stop) { // Step to the next:
|
||||||
|
run = QByteArrayView(run.end() + 1);
|
||||||
|
} else {
|
||||||
|
run = QByteArrayView();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!run.isEmpty()) {
|
||||||
|
Q_ASSERT(run == iana);
|
||||||
|
const auto ianaIdIndex = run.begin() - ianaIdData;
|
||||||
|
Q_ASSERT(ianaIdIndex <= (std::numeric_limits<quint16>::max)());
|
||||||
|
best = { length, timeType, quint16(ianaIdIndex) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: similar for territories, at least once localeName() does so.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best.ianaIdIndex != invalidIanaId)
|
||||||
|
return { QByteArray(ianaIdData + best.ianaIdIndex), best.nameLength, best.timeType };
|
||||||
|
#undef localeRows
|
||||||
|
|
||||||
|
// (We don't want offset format to match 'tttt', so do need to limit this.)
|
||||||
|
// The final fall-back for localeName() is a zoneOffsetFormat(,,NarrowFormat,,):
|
||||||
|
if (auto match = matchOffsetFormat(text, locale, locale.d->m_index, QLocale::NarrowFormat)) {
|
||||||
|
// Check offset is sane:
|
||||||
|
if (QTimeZone::MinUtcOffsetSecs <= match.offset
|
||||||
|
&& match.offset <= QTimeZone::MaxUtcOffsetSecs) {
|
||||||
|
|
||||||
|
// Although we don't have an IANA ID, the ISO offset format text
|
||||||
|
// should match what the QLocale(ianaId) constructor accepts, which
|
||||||
|
// is good enough for our purposes.
|
||||||
|
return { isoOffsetFormat(match.offset, QTimeZone::OffsetName).toLatin1(),
|
||||||
|
match.size, QTimeZone::GenericTime };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the unlocalized long form of QUtcTimeZonePrivate:
|
||||||
|
if (text.startsWith(u"UTC")) {
|
||||||
|
if (text.size() > 4 && (text[3] == u'+' || text[3] == u'-')) {
|
||||||
|
// Compare QUtcTimeZonePrivate::offsetFromUtcString()
|
||||||
|
using QtMiscUtils::isAsciiDigit;
|
||||||
|
qsizetype length = 3;
|
||||||
|
int groups = 0; // Number of groups of digits seen (allow up to three).
|
||||||
|
do {
|
||||||
|
// text[length] is sign or the colon after last digit-group.
|
||||||
|
Q_ASSERT(length < text.size());
|
||||||
|
if (length + 1 >= text.size() || !isAsciiDigit(text[length + 1].unicode()))
|
||||||
|
break;
|
||||||
|
length +=
|
||||||
|
(length + 2 < text.size() && isAsciiDigit(text[length + 2].unicode())) ? 3 : 2;
|
||||||
|
} while (++groups < 3 && length < text.size() && text[length] == u':');
|
||||||
|
if (length > 4)
|
||||||
|
return { text.sliced(length).toLatin1(), length, QTimeZone::GenericTime };
|
||||||
|
}
|
||||||
|
return { utcQByteArray(), 3, QTimeZone::GenericTime };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}; // No match found.
|
||||||
|
}
|
||||||
#endif // ICU or not
|
#endif // ICU or not
|
||||||
|
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
|
|
||||||
#include <private/qcalendarmath_p.h>
|
#include <private/qcalendarmath_p.h>
|
||||||
#include <private/qnumeric_p.h>
|
#include <private/qnumeric_p.h>
|
||||||
|
#if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
|
||||||
|
# include <private/qstringiterator_p.h>
|
||||||
|
#endif
|
||||||
#include <private/qtools_p.h>
|
#include <private/qtools_p.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@ -798,6 +801,162 @@ QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
|
||||||
|
static QTimeZonePrivate::NamePrefixMatch
|
||||||
|
findUtcOffsetPrefix(QStringView text, const QLocale &locale)
|
||||||
|
{
|
||||||
|
// First, see if we have a {UTC,GMT}+offset. This would ideally use
|
||||||
|
// locale-appropriate versions of the offset format, but we don't know those.
|
||||||
|
qsizetype signLen = 0;
|
||||||
|
char sign = '\0';
|
||||||
|
auto signStart = [&signLen, &sign, locale](QStringView str) {
|
||||||
|
QString signStr = locale.negativeSign();
|
||||||
|
if (str.startsWith(signStr)) {
|
||||||
|
sign = '-';
|
||||||
|
signLen = signStr.size();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Special case: U+2212 MINUS SIGN (cf. qlocale.cpp's NumericTokenizer)
|
||||||
|
if (str.startsWith(u'\u2212')) {
|
||||||
|
sign = '-';
|
||||||
|
signLen = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
signStr = locale.positiveSign();
|
||||||
|
if (str.startsWith(signStr)) {
|
||||||
|
sign = '+';
|
||||||
|
signLen = signStr.size();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
// Should really use locale-appropriate
|
||||||
|
if (!((text.startsWith(u"UTC") || text.startsWith(u"GMT")) && signStart(text.sliced(3))))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
QStringView offset = text.sliced(3 + signLen);
|
||||||
|
QStringIterator iter(offset);
|
||||||
|
qsizetype hourEnd = 0, hmMid = 0, minEnd = 0;
|
||||||
|
int digits = 0;
|
||||||
|
char32_t ch;
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
ch = iter.next();
|
||||||
|
if (!QChar::isDigit(ch))
|
||||||
|
break;
|
||||||
|
|
||||||
|
++digits;
|
||||||
|
// Have hourEnd keep track of the end of the last-but-two digit, if
|
||||||
|
// we have that many; use hmMid to hold the last-but-one.
|
||||||
|
hourEnd = std::exchange(hmMid, std::exchange(minEnd, iter.index()));
|
||||||
|
}
|
||||||
|
if (digits < 1 || digits > 4) // No offset or something other than an offset.
|
||||||
|
return {};
|
||||||
|
|
||||||
|
QStringView hourStr, minStr;
|
||||||
|
if (digits < 3 && iter.hasNext() && QChar::isPunct(ch)) {
|
||||||
|
hourEnd = minEnd; // Use all digits seen thus far for hour.
|
||||||
|
hmMid = iter.index(); // Reuse as minStart, in effect.
|
||||||
|
int mindig = 0;
|
||||||
|
while (mindig < 2 && iter.hasNext() && QChar::isDigit(iter.next())) {
|
||||||
|
++mindig;
|
||||||
|
minEnd = iter.index();
|
||||||
|
}
|
||||||
|
if (mindig == 2)
|
||||||
|
minStr = offset.first(minEnd).sliced(hmMid);
|
||||||
|
else
|
||||||
|
minEnd = hourEnd; // Ignore punctuator and beyond
|
||||||
|
} else {
|
||||||
|
minStr = offset.first(minEnd).sliced(hourEnd);
|
||||||
|
}
|
||||||
|
hourStr = offset.first(hourEnd);
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
uint hour = 0, minute = 0;
|
||||||
|
if (!hourStr.isEmpty())
|
||||||
|
hour = locale.toUInt(hourStr, &ok);
|
||||||
|
if (ok && !minStr.isEmpty()) {
|
||||||
|
minute = locale.toUInt(minStr, &ok);
|
||||||
|
// If the part after a punctuator is bad, pretend we never saw it:
|
||||||
|
if ((!ok || minute >= 60) && minEnd > hourEnd + minStr.size()) {
|
||||||
|
minEnd = hourEnd;
|
||||||
|
minute = 0;
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
// but if we had too many digits for just an hour, and its tail
|
||||||
|
// isn't minutes, then this isn't an offset form.
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int MaxOffsetSeconds
|
||||||
|
= qMax(QTimeZone::MaxUtcOffsetSecs, -QTimeZone::MinUtcOffsetSecs);
|
||||||
|
if (!ok || (hour * 60 + minute) * 60 > MaxOffsetSeconds)
|
||||||
|
return {}; // Let the zone-name scan find UTC or GMT prefix as a zone name.
|
||||||
|
|
||||||
|
// Transform offset into the form the QTimeZone constructor prefers:
|
||||||
|
char buffer[26];
|
||||||
|
// We need: 3 for "UTC", 1 for sign, 2+2 for digits, 1 for colon between, 1
|
||||||
|
// for '\0'; but gcc [-Werror=format-truncation=] doesn't know the %02u
|
||||||
|
// fields can't be longer than 2 digits, so complains if we don't have space
|
||||||
|
// for 10 digits in each.
|
||||||
|
if (minute)
|
||||||
|
std::snprintf(buffer, sizeof(buffer), "UTC%c%02u:%02u", sign, hour, minute);
|
||||||
|
else
|
||||||
|
std::snprintf(buffer, sizeof(buffer), "UTC%c%02u", sign, hour);
|
||||||
|
|
||||||
|
return { QByteArray(buffer, qstrnlen(buffer, sizeof(buffer))),
|
||||||
|
3 + signLen + minEnd,
|
||||||
|
QTimeZone::GenericTime };
|
||||||
|
}
|
||||||
|
|
||||||
|
QTimeZonePrivate::NamePrefixMatch
|
||||||
|
QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale,
|
||||||
|
std::optional<qint64> atEpochMillis)
|
||||||
|
{
|
||||||
|
// Search all known zones for one that matches a prefix of text in our locale.
|
||||||
|
const auto when = atEpochMillis
|
||||||
|
? QDateTime::fromMSecsSinceEpoch(*atEpochMillis, QTimeZone::UTC)
|
||||||
|
: QDateTime();
|
||||||
|
const auto typeFor = [when](QTimeZone zone) {
|
||||||
|
if (when.isValid() && zone.isDaylightTime(when))
|
||||||
|
return QTimeZone::DaylightTime;
|
||||||
|
// Assume standard time name applies equally as generic:
|
||||||
|
return QTimeZone::GenericTime;
|
||||||
|
};
|
||||||
|
QTimeZonePrivate::NamePrefixMatch best = findUtcOffsetPrefix(text, locale);
|
||||||
|
constexpr QTimeZone::TimeType types[]
|
||||||
|
= { QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime };
|
||||||
|
const auto improves = [text, &best](const QString &name) {
|
||||||
|
return text.startsWith(name, Qt::CaseInsensitive) && name.size() > best.nameLength;
|
||||||
|
};
|
||||||
|
const QList<QByteArray> allZones = QTimeZone::availableTimeZoneIds();
|
||||||
|
for (const QByteArray &iana : allZones) {
|
||||||
|
QTimeZone zone(iana);
|
||||||
|
if (!zone.isValid())
|
||||||
|
continue;
|
||||||
|
if (when.isValid()) {
|
||||||
|
QString name = zone.displayName(when, QTimeZone::LongName, locale);
|
||||||
|
if (improves(name))
|
||||||
|
best = { iana, name.size(), typeFor(zone) };
|
||||||
|
} else {
|
||||||
|
for (const QTimeZone::TimeType type : types) {
|
||||||
|
QString name = zone.displayName(type, QTimeZone::LongName, locale);
|
||||||
|
if (improves(name))
|
||||||
|
best = { iana, name.size(), type };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we have a match for all of text, we can't get any better:
|
||||||
|
if (best.nameLength >= text.size())
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// This has the problem of selecting the first IANA ID of a zone with a
|
||||||
|
// match; where several IANA IDs share a long name, this may not be the
|
||||||
|
// natural one to pick. Hopefully a backend that does its own name L10n will
|
||||||
|
// at least produce one with the same offsets as the most natural choice.
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Implemented in qtimezonelocale.cpp
|
||||||
|
#endif // icu || !timezone_locale
|
||||||
|
|
||||||
QByteArray QTimeZonePrivate::aliasToIana(QByteArrayView alias)
|
QByteArray QTimeZonePrivate::aliasToIana(QByteArrayView alias)
|
||||||
{
|
{
|
||||||
const auto data = std::lower_bound(std::begin(aliasMappingTable), std::end(aliasMappingTable),
|
const auto data = std::lower_bound(std::begin(aliasMappingTable), std::end(aliasMappingTable),
|
||||||
@ -1080,8 +1239,49 @@ QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
|
|||||||
QTimeZone::NameType nameType,
|
QTimeZone::NameType nameType,
|
||||||
const QLocale &locale) const
|
const QLocale &locale) const
|
||||||
{
|
{
|
||||||
|
#if QT_CONFIG(timezone_locale)
|
||||||
|
QString name = QTimeZonePrivate::displayName(timeType, nameType, locale);
|
||||||
|
// That may fall back to standard offset format, in which case we'd sooner
|
||||||
|
// use m_name if it's non-empty (for the benefit of custom zones).
|
||||||
|
// However, a localized fallback is better than ignoring the locale, so only
|
||||||
|
// consider the fallback a match if it matches modulo reading GMT as UTC,
|
||||||
|
// U+2212 as MINUS SIGN and the narrow form of offset the fallback uses.
|
||||||
|
const auto matchesFallback = [](int offset, QStringView name) {
|
||||||
|
// Fallback rounds offset to nearest minute:
|
||||||
|
int seconds = offset % 60;
|
||||||
|
int rounded = offset
|
||||||
|
+ (seconds > 30 || (seconds == 30 && (offset / 60) % 2)
|
||||||
|
? 60 - seconds // Round up to next minute
|
||||||
|
: (seconds < -30 || (seconds == -30 && (offset / 60) % 2)
|
||||||
|
? -(60 + seconds) // Round down to previous minute
|
||||||
|
: -seconds));
|
||||||
|
const QString avoid = isoOffsetFormat(rounded);
|
||||||
|
if (name == avoid)
|
||||||
|
return true;
|
||||||
|
Q_ASSERT(avoid.startsWith("UTC"_L1));
|
||||||
|
Q_ASSERT(avoid.size() == 9);
|
||||||
|
// Fallback may use GMT in place of UTC, but always has sign plus at
|
||||||
|
// least one hour digit, even for +0:
|
||||||
|
if (!(name.startsWith("GMT"_L1) || name.startsWith("UTC"_L1)) || name.size() < 5)
|
||||||
|
return false;
|
||||||
|
// Fallback drops trailing ":00" minute:
|
||||||
|
QStringView tail{avoid};
|
||||||
|
tail = tail.sliced(3);
|
||||||
|
if (tail.endsWith(":00"_L1))
|
||||||
|
tail = tail.chopped(3);
|
||||||
|
if (name.sliced(3) == tail)
|
||||||
|
return true;
|
||||||
|
// Accept U+2212 as minus sign:
|
||||||
|
const QChar sign = name[3] == u'\u2212' ? u'-' : name[3];
|
||||||
|
// Fallback doesn't zero-pad hour:
|
||||||
|
return sign == tail[0] && tail.sliced(tail[1] == u'0' ? 2 : 1) == name.sliced(4);
|
||||||
|
};
|
||||||
|
if (!name.isEmpty() && (m_name.isEmpty() || !matchesFallback(m_offsetFromUtc, name)))
|
||||||
|
return name;
|
||||||
|
#else // No L10N :-(
|
||||||
Q_UNUSED(timeType);
|
Q_UNUSED(timeType);
|
||||||
Q_UNUSED(locale);
|
Q_UNUSED(locale);
|
||||||
|
#endif
|
||||||
if (nameType == QTimeZone::ShortName)
|
if (nameType == QTimeZone::ShortName)
|
||||||
return m_abbreviation;
|
return m_abbreviation;
|
||||||
if (nameType == QTimeZone::OffsetName)
|
if (nameType == QTimeZone::OffsetName)
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
#if QT_CONFIG(timezone_tzdb)
|
#if QT_CONFIG(timezone_tzdb)
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#endif
|
#endif
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#if QT_CONFIG(icu)
|
#if QT_CONFIG(icu)
|
||||||
#include <unicode/ucal.h>
|
#include <unicode/ucal.h>
|
||||||
@ -156,6 +157,14 @@ public:
|
|||||||
static QList<QByteArray> windowsIdToIanaIds(const QByteArray &windowsId);
|
static QList<QByteArray> windowsIdToIanaIds(const QByteArray &windowsId);
|
||||||
static QList<QByteArray> windowsIdToIanaIds(const QByteArray &windowsId,
|
static QList<QByteArray> windowsIdToIanaIds(const QByteArray &windowsId,
|
||||||
QLocale::Territory territory);
|
QLocale::Territory territory);
|
||||||
|
struct NamePrefixMatch
|
||||||
|
{
|
||||||
|
QByteArray ianaId;
|
||||||
|
qsizetype nameLength = 0;
|
||||||
|
QTimeZone::TimeType timeType = QTimeZone::GenericTime;
|
||||||
|
};
|
||||||
|
static NamePrefixMatch findLongNamePrefix(QStringView text, const QLocale &locale,
|
||||||
|
std::optional<qint64> atEpochMillis = std::nullopt);
|
||||||
|
|
||||||
// returns "UTC" QString and QByteArray
|
// returns "UTC" QString and QByteArray
|
||||||
[[nodiscard]] static inline QString utcQString()
|
[[nodiscard]] static inline QString utcQString()
|
||||||
@ -168,6 +177,14 @@ public:
|
|||||||
return QByteArrayLiteral("UTC");
|
return QByteArrayLiteral("UTC");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef QT_BUILD_INTERNAL // For the benefit of a test
|
||||||
|
[[nodiscard]] static inline const QTimeZonePrivate *extractPrivate(const QTimeZone &zone)
|
||||||
|
{
|
||||||
|
return zone.d.operator->();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Zones CLDR data says match a condition.
|
// Zones CLDR data says match a condition.
|
||||||
// Use to filter what the backend has available.
|
// Use to filter what the backend has available.
|
||||||
|
@ -85,6 +85,8 @@ private slots:
|
|||||||
void formatTimeZone();
|
void formatTimeZone();
|
||||||
void toDateTime_data();
|
void toDateTime_data();
|
||||||
void toDateTime();
|
void toDateTime();
|
||||||
|
void roundtripDateTimeFormat_data();
|
||||||
|
void roundtripDateTimeFormat();
|
||||||
void toDate_data();
|
void toDate_data();
|
||||||
void toDate();
|
void toDate();
|
||||||
void toTime_data();
|
void toTime_data();
|
||||||
@ -2387,6 +2389,79 @@ void tst_QLocale::toDateTime()
|
|||||||
QCOMPARE(l.toDateTime(string, QLocale::ShortFormat), result);
|
QCOMPARE(l.toDateTime(string, QLocale::ShortFormat), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void tst_QLocale::roundtripDateTimeFormat_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QLocale>("locale");
|
||||||
|
QTest::addColumn<QDateTime>("when");
|
||||||
|
QTest::addColumn<QCalendar>("cal");
|
||||||
|
QTest::addColumn<QLocale::FormatType>("format");
|
||||||
|
QTest::addColumn<int>("baseYear");
|
||||||
|
const QCalendar greg;
|
||||||
|
|
||||||
|
#if QT_CONFIG(timezone)
|
||||||
|
qsizetype count = 0;
|
||||||
|
const QTimeZone westOz("Australia/Perth");
|
||||||
|
if (westOz.isValid()) {
|
||||||
|
QTest::newRow("de_DE/LongFormat/2024-05-06T12:34/AWT") // QTBUG-130278
|
||||||
|
<< QLocale(QLocale::German, QLocale::Germany)
|
||||||
|
<< QDateTime(QDate(2024, 5, 6, greg), QTime(12, 34), westOz)
|
||||||
|
<< greg << QLocale::LongFormat << 2000;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QTimeZone nepal("Asia/Katmandu");
|
||||||
|
if (nepal.isValid()) {
|
||||||
|
// Triggers the region-format code-path:
|
||||||
|
QTest::newRow("en_US/LongFormat/2025-02-06T20:20/Katmandu")
|
||||||
|
<< QLocale(QLocale::English, QLocale::UnitedStates)
|
||||||
|
<< QDateTime(QDate(2025, 2, 6, greg), QTime(20, 20), nepal)
|
||||||
|
<< greg << QLocale::LongFormat << 2000;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!count)
|
||||||
|
QSKIP("Missing zones for both test-cases");
|
||||||
|
#else
|
||||||
|
QSKIP("The only test-case depends on feature timezone");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void tst_QLocale::roundtripDateTimeFormat()
|
||||||
|
{
|
||||||
|
QFETCH(const QLocale, locale);
|
||||||
|
QFETCH(const QDateTime, when);
|
||||||
|
QFETCH(const QCalendar, cal);
|
||||||
|
QFETCH(const QLocale::FormatType, format);
|
||||||
|
QFETCH(const int, baseYear);
|
||||||
|
|
||||||
|
const QString text = locale.toString(when, format, cal);
|
||||||
|
auto report = qScopeGuard([=]() {
|
||||||
|
qDebug() << "Went via:" << text;
|
||||||
|
qDebug() << "Used format:" << locale.dateTimeFormat(format);
|
||||||
|
QDateTime parsed = locale.toDateTime(text, format, cal, baseYear);
|
||||||
|
if (parsed.isValid()) {
|
||||||
|
switch (parsed.timeSpec()) {
|
||||||
|
#if QT_CONFIG(timezone)
|
||||||
|
case Qt::TimeZone:
|
||||||
|
qDebug() << "Used zone:" << parsed.timeZone().id();
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
case Qt::OffsetFromUTC:
|
||||||
|
qDebug() << "Used fixed UTC offset:" << parsed.offsetFromUtc();
|
||||||
|
break;
|
||||||
|
case Qt::LocalTime:
|
||||||
|
qDebug("Used local time");
|
||||||
|
break;
|
||||||
|
case Qt::UTC:
|
||||||
|
qDebug("Used plain UTC");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
QCOMPARE(locale.toDateTime(text, format, cal, baseYear), when);
|
||||||
|
report.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
void tst_QLocale::toDate_data()
|
void tst_QLocale::toDate_data()
|
||||||
{
|
{
|
||||||
QTest::addColumn<QLocale>("locale");
|
QTest::addColumn<QLocale>("locale");
|
||||||
|
@ -1261,7 +1261,15 @@ void tst_QDateTime::toString_strformat()
|
|||||||
QCOMPARE(testDateTime.toString("yyyy-MM-dd hh:mm:ss t"), QString("2013-01-01 01:02:03 UTC"));
|
QCOMPARE(testDateTime.toString("yyyy-MM-dd hh:mm:ss t"), QString("2013-01-01 01:02:03 UTC"));
|
||||||
QCOMPARE(testDateTime.toString("yyyy-MM-dd hh:mm:ss tt"), QString("2013-01-01 01:02:03 +0000"));
|
QCOMPARE(testDateTime.toString("yyyy-MM-dd hh:mm:ss tt"), QString("2013-01-01 01:02:03 +0000"));
|
||||||
QCOMPARE(testDateTime.toString("yyyy-MM-dd hh:mm:ss ttt"), QString("2013-01-01 01:02:03 +00:00"));
|
QCOMPARE(testDateTime.toString("yyyy-MM-dd hh:mm:ss ttt"), QString("2013-01-01 01:02:03 +00:00"));
|
||||||
QCOMPARE(testDateTime.toString("yyyy-MM-dd hh:mm:ss tttt"), QString("2013-01-01 01:02:03 UTC"));
|
|
||||||
|
#if QT_CONFIG(icu) && !defined(Q_STL_DINKUMWARE)
|
||||||
|
// The Dinkum (VxWorks) exception may just be because it has an old ICU.
|
||||||
|
// Hopefully other timezone backends shall eventually agree with this:
|
||||||
|
const QString longForm = u"2013-01-01 01:02:03 Coordinated Universal Time"_s;
|
||||||
|
#else
|
||||||
|
const QString longForm = u"2013-01-01 01:02:03 UTC"_s;
|
||||||
|
#endif // (Note: #if-ery is not allowed within a macro's parameter list.)
|
||||||
|
QCOMPARE(testDateTime.toString(u"yyyy-MM-dd hh:mm:ss tttt"_s), longForm);
|
||||||
}
|
}
|
||||||
#endif // datestring
|
#endif // datestring
|
||||||
|
|
||||||
|
@ -64,6 +64,8 @@ private Q_SLOTS:
|
|||||||
void winTest();
|
void winTest();
|
||||||
void localeSpecificDisplayName_data();
|
void localeSpecificDisplayName_data();
|
||||||
void localeSpecificDisplayName();
|
void localeSpecificDisplayName();
|
||||||
|
void roundtripDisplayNames_data();
|
||||||
|
void roundtripDisplayNames();
|
||||||
void stdCompatibility_data();
|
void stdCompatibility_data();
|
||||||
void stdCompatibility();
|
void stdCompatibility();
|
||||||
#endif // timezone backends
|
#endif // timezone backends
|
||||||
@ -1315,6 +1317,11 @@ void tst_QTimeZone::malformed()
|
|||||||
|
|
||||||
void tst_QTimeZone::utcTest()
|
void tst_QTimeZone::utcTest()
|
||||||
{
|
{
|
||||||
|
#if QT_CONFIG(icu) // || hopefully various other cases, eventually
|
||||||
|
const QString utcLongName = u"Coordinated Universal Time"_s;
|
||||||
|
#else
|
||||||
|
const QString utcLongName = u"UTC"_s;
|
||||||
|
#endif
|
||||||
#ifdef QT_BUILD_INTERNAL
|
#ifdef QT_BUILD_INTERNAL
|
||||||
// Test default UTC constructor
|
// Test default UTC constructor
|
||||||
QUtcTimeZonePrivate tzp;
|
QUtcTimeZonePrivate tzp;
|
||||||
@ -1322,7 +1329,7 @@ void tst_QTimeZone::utcTest()
|
|||||||
QCOMPARE(tzp.id(), QByteArray("UTC"));
|
QCOMPARE(tzp.id(), QByteArray("UTC"));
|
||||||
QCOMPARE(tzp.territory(), QLocale::AnyTerritory);
|
QCOMPARE(tzp.territory(), QLocale::AnyTerritory);
|
||||||
QCOMPARE(tzp.abbreviation(0), QString("UTC"));
|
QCOMPARE(tzp.abbreviation(0), QString("UTC"));
|
||||||
QCOMPARE(tzp.displayName(QTimeZone::StandardTime, QTimeZone::LongName, QLocale()), QString("UTC"));
|
QCOMPARE(tzp.displayName(QTimeZone::StandardTime, QTimeZone::LongName, QLocale()), utcLongName);
|
||||||
QCOMPARE(tzp.offsetFromUtc(0), 0);
|
QCOMPARE(tzp.offsetFromUtc(0), 0);
|
||||||
QCOMPARE(tzp.standardTimeOffset(0), 0);
|
QCOMPARE(tzp.standardTimeOffset(0), 0);
|
||||||
QCOMPARE(tzp.daylightTimeOffset(0), 0);
|
QCOMPARE(tzp.daylightTimeOffset(0), 0);
|
||||||
@ -1337,7 +1344,7 @@ void tst_QTimeZone::utcTest()
|
|||||||
QCOMPARE(tz.id(), QByteArrayLiteral("UTC"));
|
QCOMPARE(tz.id(), QByteArrayLiteral("UTC"));
|
||||||
QCOMPARE(tz.territory(), QLocale::AnyTerritory);
|
QCOMPARE(tz.territory(), QLocale::AnyTerritory);
|
||||||
QCOMPARE(tz.abbreviation(now), QStringLiteral("UTC"));
|
QCOMPARE(tz.abbreviation(now), QStringLiteral("UTC"));
|
||||||
QCOMPARE(tz.displayName(QTimeZone::StandardTime, QTimeZone::LongName, QLocale()), QStringLiteral("UTC"));
|
QCOMPARE(tz.displayName(QTimeZone::StandardTime, QTimeZone::LongName, QLocale()), utcLongName);
|
||||||
QCOMPARE(tz.offsetFromUtc(now), 0);
|
QCOMPARE(tz.offsetFromUtc(now), 0);
|
||||||
QCOMPARE(tz.standardTimeOffset(now), 0);
|
QCOMPARE(tz.standardTimeOffset(now), 0);
|
||||||
QCOMPARE(tz.daylightTimeOffset(now), 0);
|
QCOMPARE(tz.daylightTimeOffset(now), 0);
|
||||||
@ -1770,6 +1777,7 @@ void tst_QTimeZone::localeSpecificDisplayName_data()
|
|||||||
QTest::addColumn<QLocale>("locale");
|
QTest::addColumn<QLocale>("locale");
|
||||||
QTest::addColumn<QTimeZone::TimeType>("timeType");
|
QTest::addColumn<QTimeZone::TimeType>("timeType");
|
||||||
QTest::addColumn<QString>("expectedName");
|
QTest::addColumn<QString>("expectedName");
|
||||||
|
QTest::addColumn<QDateTime>("when");
|
||||||
|
|
||||||
QStringList names;
|
QStringList names;
|
||||||
QLocale locale;
|
QLocale locale;
|
||||||
@ -1786,10 +1794,12 @@ void tst_QTimeZone::localeSpecificDisplayName_data()
|
|||||||
|
|
||||||
qsizetype index = 0;
|
qsizetype index = 0;
|
||||||
QTest::newRow("Berlin, standard time")
|
QTest::newRow("Berlin, standard time")
|
||||||
<< "Europe/Berlin"_ba << locale << QTimeZone::StandardTime << names.at(index++);
|
<< "Europe/Berlin"_ba << locale << QTimeZone::StandardTime << names.at(index++)
|
||||||
|
<< QDateTime(QDate(2024, 1, 1), QTime(12, 0));
|
||||||
|
|
||||||
QTest::newRow("Berlin, summer time")
|
QTest::newRow("Berlin, summer time")
|
||||||
<< "Europe/Berlin"_ba << locale << QTimeZone::DaylightTime << names.at(index++);
|
<< "Europe/Berlin"_ba << locale << QTimeZone::DaylightTime << names.at(index++)
|
||||||
|
<< QDateTime(QDate(2024, 7, 1), QTime(12, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
void tst_QTimeZone::localeSpecificDisplayName()
|
void tst_QTimeZone::localeSpecificDisplayName()
|
||||||
@ -1806,6 +1816,163 @@ void tst_QTimeZone::localeSpecificDisplayName()
|
|||||||
|
|
||||||
const QString localeName = zone.displayName(timeType, QTimeZone::LongName, locale);
|
const QString localeName = zone.displayName(timeType, QTimeZone::LongName, locale);
|
||||||
QCOMPARE(localeName, expectedName);
|
QCOMPARE(localeName, expectedName);
|
||||||
|
#ifdef QT_BUILD_INTERNAL
|
||||||
|
QFETCH(QDateTime, when);
|
||||||
|
// Check that round-trips:
|
||||||
|
auto match = QTimeZonePrivate::findLongNamePrefix(localeName, locale, when.toMSecsSinceEpoch());
|
||||||
|
QCOMPARE(match.nameLength, localeName.size());
|
||||||
|
auto report = qScopeGuard([=]() {
|
||||||
|
auto typeName = [](QTimeZone::TimeType type) {
|
||||||
|
return (type == QTimeZone::StandardTime ? "std"
|
||||||
|
: type == QTimeZone::GenericTime ? "gen" : "dst");
|
||||||
|
};
|
||||||
|
qDebug("Long name round-tripped %s (%s) to %s (%s) via %s",
|
||||||
|
zoneName.constData(), typeName(timeType),
|
||||||
|
match.ianaId.constData(), typeName(match.timeType),
|
||||||
|
localeName.toUtf8().constData());
|
||||||
|
});
|
||||||
|
// We may have found a different zone in the same metazone.
|
||||||
|
// Ideally prefer canonical, but the ICU-based version doesn't.
|
||||||
|
// At least check offsets match:
|
||||||
|
const QTimeZone actual(match.ianaId);
|
||||||
|
if (when.isValid() && actual.isValid())
|
||||||
|
QCOMPARE(actual.offsetFromUtc(when), zone.offsetFromUtc(when));
|
||||||
|
// GenericTime gets preferred and may be a synonym for StandardTime:
|
||||||
|
if (timeType != QTimeZone::StandardTime || match.timeType != QTimeZone::GenericTime)
|
||||||
|
QCOMPARE(match.timeType, timeType);
|
||||||
|
|
||||||
|
// Let report happen when names don't match:
|
||||||
|
if (match.ianaId == zoneName)
|
||||||
|
report.dismiss();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void tst_QTimeZone::roundtripDisplayNames_data()
|
||||||
|
{
|
||||||
|
#ifdef QT_BUILD_INTERNAL
|
||||||
|
QTest::addColumn<QTimeZone>("zone");
|
||||||
|
QTest::addColumn<QLocale>("locale");
|
||||||
|
QTest::addColumn<QTimeZone::TimeType>("type");
|
||||||
|
|
||||||
|
constexpr QTimeZone::TimeType types[] = {
|
||||||
|
QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime
|
||||||
|
};
|
||||||
|
const auto typeName = [](QTimeZone::TimeType type) {
|
||||||
|
switch (type) {
|
||||||
|
case QTimeZone::GenericTime: return "Gen";
|
||||||
|
case QTimeZone::StandardTime: return "Std";
|
||||||
|
case QTimeZone::DaylightTime: return "DST";
|
||||||
|
}
|
||||||
|
Q_UNREACHABLE_RETURN("Unrecognised");
|
||||||
|
};
|
||||||
|
const QList<QByteArray> allList = (QTimeZone::availableTimeZoneIds() << "Vulcan/ShiKahr"_ba);
|
||||||
|
#ifdef EXHAUSTIVE_ZONE_DISPLAY
|
||||||
|
const QList<QByteArray> idList = allList;
|
||||||
|
#else
|
||||||
|
const QList<QByteArray> idList = {
|
||||||
|
"Africa/Casablanca"_ba, "Africa/Lagos"_ba, "Africa/Tunis"_ba,
|
||||||
|
"America/Caracas"_ba, "America/Indiana/Tell_City"_ba, "America/Managua"_ba,
|
||||||
|
"Asia/Bangkok"_ba, "Asia/Colombo"_ba, "Asia/Tokyo"_ba,
|
||||||
|
"Atlantic/Bermuda"_ba, "Atlantic/Faroe"_ba, "Atlantic/Madeira"_ba,
|
||||||
|
"Australia/Broken_Hill"_ba, "Australia/NSW"_ba, "Australia/Tasmania"_ba,
|
||||||
|
"Brazil/Acre"_ba, "CST6CDT"_ba, "Canada/Atlantic"_ba,
|
||||||
|
"Chile/EasterIsland"_ba, "Etc/Greenwich"_ba, "Etc/Universal"_ba,
|
||||||
|
"Europe/Guernsey"_ba, "Europe/Kaliningrad"_ba, "Europe/Kyiv"_ba,
|
||||||
|
"Europe/Prague"_ba, "Europe/Vatican"_ba,
|
||||||
|
"Indian/Comoro"_ba, "Mexico/BajaSur"_ba,
|
||||||
|
"Pacific/Bougainville"_ba, "Pacific/Midway"_ba, "Pacific/Wallis"_ba,
|
||||||
|
"US/Aleutian"_ba,
|
||||||
|
"UTC"_ba,
|
||||||
|
// Those named overtly in tst_QDateTime - special cases first:
|
||||||
|
"UTC-02:00"_ba, "UTC+02:00"_ba, "UTC+12:00"_ba,
|
||||||
|
"Etc/GMT+3"_ba, "GMT-2"_ba, "GMT"_ba,
|
||||||
|
// ... then ordinary names in alphabetic order:
|
||||||
|
"America/New_York"_ba, "America/Sao_Paulo"_ba, "America/Vancouver"_ba,
|
||||||
|
"Asia/Kathmandu"_ba, "Asia/Singapore"_ba,
|
||||||
|
"Australia/Brisbane"_ba, "Australia/Eucla"_ba, "Australia/Sydney"_ba,
|
||||||
|
"Europe/Berlin"_ba, "Europe/Helsinki"_ba, "Europe/Rome"_ba, "Europe/Oslo"_ba,
|
||||||
|
"Pacific/Apia"_ba, "Pacific/Auckland"_ba, "Pacific/Kiritimati"_ba,
|
||||||
|
"Vulcan/ShiKahr"_ba // Invalid: also worth testing.
|
||||||
|
};
|
||||||
|
// Some valid zones in that list may be absent from the platform's
|
||||||
|
// availableTimeZoneIds(), yet in fact work when used as it's asked to
|
||||||
|
// instantiate them (e.g. Etc/Universal on macOS). This can give them a
|
||||||
|
// displayName() that we fail to decode, without timezone_locale, due to
|
||||||
|
// only trying the availableTimeZoneIds() in findLongNamePrefix(). So we
|
||||||
|
// have to filter on membership of allList when creating rows.
|
||||||
|
#endif // Exhaustive
|
||||||
|
const QLocale fr(QLocale::French, QLocale::France);
|
||||||
|
const QLocale hi(QLocale::Hindi, QLocale::India);
|
||||||
|
for (const QByteArray &id : idList) {
|
||||||
|
if (id == "localtime"_ba || id == "posixrules"_ba || !allList.contains(id))
|
||||||
|
continue;
|
||||||
|
QTimeZone zone = QTimeZone(id);
|
||||||
|
if (!zone.isValid())
|
||||||
|
continue;
|
||||||
|
for (const auto type : types) {
|
||||||
|
QTest::addRow("%s@fr_FR/%s", id.constData(), typeName(type))
|
||||||
|
<< zone << fr << type;
|
||||||
|
QTest::addRow("%s@hi_IN/%s", id.constData(), typeName(type))
|
||||||
|
<< zone << hi << type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
QSKIP("Test needs access to internal APIs");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void tst_QTimeZone::roundtripDisplayNames()
|
||||||
|
{
|
||||||
|
#ifdef QT_BUILD_INTERNAL
|
||||||
|
QFETCH(const QTimeZone, zone);
|
||||||
|
QFETCH(const QLocale, locale);
|
||||||
|
QFETCH(const QTimeZone::TimeType, type);
|
||||||
|
static const QDateTime jan = QDateTime(QDate(2015, 1, 1), QTime(12, 0), QTimeZone::UTC);
|
||||||
|
static const QDateTime jul = QDateTime(QDate(2015, 7, 1), QTime(12, 0), QTimeZone::UTC);
|
||||||
|
const QDateTime dt = zone.isDaylightTime(jul) == (type == QTimeZone::DaylightTime) ? jul : jan;
|
||||||
|
|
||||||
|
// Some zones exercise region format.
|
||||||
|
const QString name = zone.displayName(type, QTimeZone::LongName, locale);
|
||||||
|
if (!name.isEmpty()) {
|
||||||
|
const auto tran = QTimeZonePrivate::extractPrivate(zone)->data(type);
|
||||||
|
const qint64 when = tran.atMSecsSinceEpoch == QTimeZonePrivate::invalidMSecs()
|
||||||
|
? dt.toMSecsSinceEpoch() : tran.atMSecsSinceEpoch;
|
||||||
|
const QString extended = name + "some spurious cruft"_L1;
|
||||||
|
auto match =
|
||||||
|
QTimeZonePrivate::findLongNamePrefix(extended, locale, when);
|
||||||
|
if (!match.nameLength)
|
||||||
|
match = QTimeZonePrivate::findLongNamePrefix(extended, locale);
|
||||||
|
auto report = qScopeGuard([=]() {
|
||||||
|
qDebug() << "At" << QDateTime::fromMSecsSinceEpoch(when, QTimeZone::UTC)
|
||||||
|
<< "via" << name;
|
||||||
|
});
|
||||||
|
QCOMPARE(match.nameLength, name.size());
|
||||||
|
report.dismiss();
|
||||||
|
#if 0
|
||||||
|
if (match.ianaId != zone.id()) {
|
||||||
|
const QTimeZone found = QTimeZone(match.ianaId);
|
||||||
|
if (QTimeZonePrivate::extractPrivate(found)->offsetFromUtc(when)
|
||||||
|
!= QTimeZonePrivate::extractPrivate(zone)->offsetFromUtc(when)) {
|
||||||
|
// For DST, some zones haven't done it in ages, so tran may be ancient.
|
||||||
|
// Meanwhile, match.ianaId is typically the canonical zone for a metazone.
|
||||||
|
// That, in turn, may not have been doing DST when zone was.
|
||||||
|
// So we can't rely on a match, but can report the mismatches.
|
||||||
|
qDebug() << "Long name" << name << "on"
|
||||||
|
<< QTimeZonePrivate::extractPrivate(zone)->offsetFromUtc(when)
|
||||||
|
<< "at" << QDateTime::fromMSecsSinceEpoch(when, QTimeZone::UTC)
|
||||||
|
<< "got" << match.ianaId << "on"
|
||||||
|
<< QTimeZonePrivate::extractPrivate(found)->offsetFromUtc(when);
|
||||||
|
// There are also some absurdly over-generic names, that lead to
|
||||||
|
// ambiguities, e.g. "heure : West"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // Debug code
|
||||||
|
} else if (type != QTimeZone::DaylightTime) { /* Zones with no DST have no DST-name */
|
||||||
|
qDebug("Empty display name");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
Q_ASSERT(!"Should be skipped when building data table");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef QT_BUILD_INTERNAL
|
#ifdef QT_BUILD_INTERNAL
|
||||||
|
Loading…
x
Reference in New Issue
Block a user