summaryrefslogtreecommitdiffstats
path: root/src
diff options
Diffstat (limited to 'src')
-rw-r--r--src/corelib/text/qlocale.cpp11
-rw-r--r--src/corelib/time/qdatetime.cpp21
-rw-r--r--src/corelib/time/qdatetimeparser.cpp46
-rw-r--r--src/corelib/time/qtimezonelocale.cpp376
-rw-r--r--src/corelib/time/qtimezoneprivate.cpp200
-rw-r--r--src/corelib/time/qtimezoneprivate_p.h16
-rw-r--r--src/network/doc/src/examples.qdoc2
7 files changed, 653 insertions, 19 deletions
diff --git a/src/corelib/text/qlocale.cpp b/src/corelib/text/qlocale.cpp
index d2fdcb6dc81..c6d3c150c19 100644
--- a/src/corelib/text/qlocale.cpp
+++ b/src/corelib/text/qlocale.cpp
@@ -4001,8 +4001,15 @@ QString QCalendarBackend::dateTimeToString(QStringView format, const QDateTime &
text = when.timeRepresentation().displayName(when, mode, locale);
if (!text.isEmpty())
return text;
- // else fall back to an unlocalized one if we can manage it:
- } // else: prefer QDateTime's abbreviation, for backwards-compatibility.
+ // else fall back to an unlocalized one if we can find one.
+ }
+ 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:
// Absent timezone_locale data, Offset might still reach here:
if (type == Offset) // Our prior failure might not have tried this:
diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp
index 5c3c88e92ee..a5ac987c701 100644
--- a/src/corelib/time/qdatetime.cpp
+++ b/src/corelib/time/qdatetime.cpp
@@ -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
minutes (for example "+02:00").
\row \li tttt
- \li The timezone name (for example "Europe/Berlin"). Note that this
- gives no indication of whether the datetime was in daylight-saving
- time or standard time, which may lead to ambiguity if the datetime
- falls in an hour repeated by a transition between the two. The name
- used is the one provided by \l QTimeZone::displayName() with the \l
- QTimeZone::LongName type. This may depend on the operating system
- in use.
+ \li The timezone name, as provided by \l QTimeZone::displayName() with
+ the \l QTimeZone::LongName type. This may depend on the operating
+ system in use. If no such name is available, the IANA ID of the
+ zone (such as "Europe/Berlin") may be used. It may give no
+ indication of whether the datetime was in daylight-saving time or
+ standard time, which may lead to ambiguity if the datetime falls in
+ an hour repeated by a transition between the two.
\endtable
\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
minutes (for example "+02:00")
\row \li tttt
- \li the timezone name (for example "Europe/Berlin"). The name
- recognized are those known to \l QTimeZone, which may depend on the
- operating system in use.
+ \li the timezone name, either what \l QTimeZone::displayName() reports
+ for \l QTimeZone::LongName or the IANA ID of the zone (for example
+ "Europe/Berlin"). The names recognized are those known to \l
+ QTimeZone, which may depend on the operating system in use.
\endtable
If no 't' format specifier is present, the system's local time-zone is used.
diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp
index 31e010de74d..3e8e6ac77ea 100644
--- a/src/corelib/time/qdatetimeparser.cpp
+++ b/src/corelib/time/qdatetimeparser.cpp
@@ -13,6 +13,9 @@
#include "qtimezone.h"
#include "qvarlengtharray.h"
#include "private/qlocale_p.h"
+#if QT_CONFIG(timezone)
+#include "private/qtimezoneprivate_p.h"
+#endif
#include "private/qstringiterator_p.h"
#include "private/qtenvironmentvariables_p.h"
@@ -1228,6 +1231,27 @@ static int startsWithLocalTimeZone(QStringView name, const QDateTime &when, cons
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
*/
@@ -1313,9 +1337,14 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
timeZone = QTimeZone::fromSecondsAheadOfUtc(sect.value);
#if QT_CONFIG(timezone)
} else if (startsWithLocalTimeZone(zoneName, usedDateTime, locale()) != sect.used) {
- QTimeZone namedZone = QTimeZone(zoneName.toLatin1());
- Q_ASSERT(namedZone.isValid());
- timeZone = namedZone;
+ if (QTimeZone namedZone = QTimeZone(zoneName.toLatin1()); namedZone.isValid()) {
+ timeZone = namedZone;
+ } else {
+ auto found = findZoneByLongName(zoneName, locale(), usedDateTime);
+ Q_ASSERT(found.isValid());
+ Q_ASSERT(found.nameLength == zoneName.length());
+ timeZone = found.zone;
+ }
#endif
} else {
timeZone = QTimeZone::LocalTime;
@@ -1853,12 +1882,17 @@ QDateTimeParser::findTimeZoneName(QStringView str, const QDateTime &when) const
lastSlash = slash;
}
- for (; index > systemLength; --index) { // Find longest match
- str.truncate(index);
- QTimeZone zone(str.toLatin1());
+ // Find longest IANA ID match:
+ for (QStringView copy = str; index > systemLength; --index) {
+ copy.truncate(index);
+ QTimeZone zone(copy.toLatin1());
if (zone.isValid())
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
if (systemLength > 0) // won't actually use the offset, but need it to be valid
return ParsedSection(Acceptable, when.toLocalTime().offsetFromUtc(), systemLength);
diff --git a/src/corelib/time/qtimezonelocale.cpp b/src/corelib/time/qtimezonelocale.cpp
index dd22e68c5e6..c27050648f6 100644
--- a/src/corelib/time/qtimezonelocale.cpp
+++ b/src/corelib/time/qtimezonelocale.cpp
@@ -7,6 +7,7 @@
#if !QT_CONFIG(icu)
# include <QtCore/qspan.h>
# include <private/qdatetime_p.h>
+# include <private/qtools_p.h>
// Use data generated from CLDR:
# include "qtimezonelocale_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;
}
+// 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
{
return row.metaZoneKey < metaKey;
@@ -361,6 +378,147 @@ QString formatOffset(QStringView format, int offsetMinutes, const QLocale &local
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
namespace QtTimeZoneLocale {
@@ -427,6 +585,7 @@ QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc
return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::LongFormat,
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:
QByteArray ianaAbbrev, ianaTail;
@@ -636,6 +795,223 @@ QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc
return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::NarrowFormat,
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
QT_END_NAMESPACE
diff --git a/src/corelib/time/qtimezoneprivate.cpp b/src/corelib/time/qtimezoneprivate.cpp
index 491fba84e3b..4ef990bde7c 100644
--- a/src/corelib/time/qtimezoneprivate.cpp
+++ b/src/corelib/time/qtimezoneprivate.cpp
@@ -16,6 +16,9 @@
#include <private/qcalendarmath_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 <algorithm>
@@ -798,6 +801,162 @@ QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType
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)
{
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,
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(locale);
+#endif
if (nameType == QTimeZone::ShortName)
return m_abbreviation;
if (nameType == QTimeZone::OffsetName)
diff --git a/src/corelib/time/qtimezoneprivate_p.h b/src/corelib/time/qtimezoneprivate_p.h
index e599fcec37f..af1ff741b2a 100644
--- a/src/corelib/time/qtimezoneprivate_p.h
+++ b/src/corelib/time/qtimezoneprivate_p.h
@@ -25,6 +25,7 @@
#if QT_CONFIG(timezone_tzdb)
#include <chrono>
#endif
+#include <optional>
#if QT_CONFIG(icu)
#include <unicode/ucal.h>
@@ -156,6 +157,14 @@ public:
static QList<QByteArray> windowsIdToIanaIds(const QByteArray &windowsId);
static QList<QByteArray> windowsIdToIanaIds(const QByteArray &windowsId,
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
[[nodiscard]] static inline QString utcQString()
@@ -170,6 +179,13 @@ public:
[[nodiscard]] static QTimeZone utcQTimeZone();
+#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:
// Zones CLDR data says match a condition.
// Use to filter what the backend has available.
diff --git a/src/network/doc/src/examples.qdoc b/src/network/doc/src/examples.qdoc
index ee9084c74cb..8f0dd3bfd2f 100644
--- a/src/network/doc/src/examples.qdoc
+++ b/src/network/doc/src/examples.qdoc
@@ -7,7 +7,7 @@
\title Network Examples
\brief How to do network programming in Qt.
- \image network-examples.webp
+ \image network-examples.webp {There are many examples that demonstrate the capabilities of Qt Network}
Qt is provided with an extensive set of network classes to support both
client-based and server side network programming.
close