diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/corelib/text/qlocale.cpp | 11 | ||||
-rw-r--r-- | src/corelib/time/qdatetime.cpp | 21 | ||||
-rw-r--r-- | src/corelib/time/qdatetimeparser.cpp | 46 | ||||
-rw-r--r-- | src/corelib/time/qtimezonelocale.cpp | 376 | ||||
-rw-r--r-- | src/corelib/time/qtimezoneprivate.cpp | 200 | ||||
-rw-r--r-- | src/corelib/time/qtimezoneprivate_p.h | 16 | ||||
-rw-r--r-- | src/network/doc/src/examples.qdoc | 2 |
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. |