Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtimezonelocale.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <private/qtimezonelocale_p.h>
5#include <private/qtimezoneprivate_p.h>
6
7#if !QT_CONFIG(icu)
8# include <QtCore/qspan.h>
9# include <private/qdatetime_p.h>
10// Use data generated from CLDR:
11# include "qtimezonelocale_data_p.h"
12# include "qtimezoneprivate_data_p.h"
13# ifdef QT_CLDR_ZONE_DEBUG
14# include "../text/qlocale_data_p.h"
15QT_BEGIN_NAMESPACE
16static_assert(std::size(locale_data) == std::size(QtTimeZoneLocale::localeZoneData));
17// Size includes terminal rows: for now, they do match in tag IDs, but they needn't.
18static_assert([]() {
19 for (std::size_t i = 0; i < std::size(locale_data); ++i) {
20 const auto &loc = locale_data[i];
21 const auto &zone = QtTimeZoneLocale::localeZoneData[i];
22 if (loc.m_language_id != zone.m_language_id
23 || loc.m_script_id != zone.m_script_id
24 || loc.m_territory_id != zone.m_territory_id) {
25 return false;
26 }
27 }
28 return true;
29}());
30QT_END_NAMESPACE
31# endif
32#endif
33
34QT_BEGIN_NAMESPACE
35
36using namespace Qt::StringLiterals;
37
38#if QT_CONFIG(icu) // Get data from ICU:
39namespace {
40
41// Convert TimeType and NameType into ICU UCalendarDisplayNameType
42UCalendarDisplayNameType ucalDisplayNameType(QTimeZone::TimeType timeType,
43 QTimeZone::NameType nameType)
44{
45 // TODO ICU C UCalendarDisplayNameType does not support full set of C++ TimeZone::EDisplayType
46 // For now, treat Generic as Standard
47 switch (nameType) {
48 case QTimeZone::ShortName:
49 return timeType == QTimeZone::DaylightTime ? UCAL_SHORT_DST : UCAL_SHORT_STANDARD;
50 case QTimeZone::DefaultName:
51 case QTimeZone::LongName:
52 return timeType == QTimeZone::DaylightTime ? UCAL_DST : UCAL_STANDARD;
53 case QTimeZone::OffsetName:
54 Q_UNREACHABLE(); // Callers of ucalTimeZoneDisplayName() should take care of OffsetName.
55 }
56 Q_UNREACHABLE_RETURN(UCAL_STANDARD);
57}
58
59} // nameless namespace
60
61namespace QtTimeZoneLocale {
62
63// Qt wrapper around ucal_getTimeZoneDisplayName()
64// Used directly by ICU backend; indirectly by TZ (see below).
65QString ucalTimeZoneDisplayName(UCalendar *ucal,
66 QTimeZone::TimeType timeType,
67 QTimeZone::NameType nameType,
68 const QByteArray &localeCode)
69{
70 constexpr int32_t BigNameLength = 50;
71 int32_t size = BigNameLength;
72 QString result(size, Qt::Uninitialized);
73 auto dst = [&result]() { return reinterpret_cast<UChar *>(result.data()); };
74 UErrorCode status = U_ZERO_ERROR;
75 const UCalendarDisplayNameType utype = ucalDisplayNameType(timeType, nameType);
76
77 // size = ucal_getTimeZoneDisplayName(cal, type, locale, result, resultLength, status)
78 size = ucal_getTimeZoneDisplayName(ucal, utype, localeCode.constData(),
79 dst(), size, &status);
80
81 // If overflow, then resize and retry
82 if (size > BigNameLength || status == U_BUFFER_OVERFLOW_ERROR) {
83 result.resize(size);
84 status = U_ZERO_ERROR;
85 size = ucal_getTimeZoneDisplayName(ucal, utype, localeCode.constData(),
86 dst(), size, &status);
87 }
88
89 if (!U_SUCCESS(status))
90 return QString();
91
92 // Resize and return:
93 result.resize(size);
94 return result;
95}
96
97} // QtTimeZoneLocale
98
99// Used by TZ backends when ICU is available:
100QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc,
101 QTimeZone::TimeType timeType,
102 QTimeZone::NameType nameType,
103 const QLocale &locale) const
104{
105 Q_UNUSED(atMSecsSinceEpoch);
106 // TODO: use CLDR data for the offset name.
107 // No ICU API for offset formats, so fall back to our ISO one, even if
108 // locale isn't C:
109 if (nameType == QTimeZone::OffsetName)
110 return isoOffsetFormat(offsetFromUtc);
111
112 const QString id = QString::fromUtf8(m_id);
113 const QByteArray loc = locale.name().toUtf8();
114 UErrorCode status = U_ZERO_ERROR;
115 // TODO: QTBUG-124271 can we cache any of this ?
116 UCalendar *ucal = ucal_open(reinterpret_cast<const UChar *>(id.data()), id.size(),
117 loc.constData(), UCAL_DEFAULT, &status);
118 if (ucal && U_SUCCESS(status)) {
119 auto tidier = qScopeGuard([ucal]() { ucal_close(ucal); });
120 return QtTimeZoneLocale::ucalTimeZoneDisplayName(ucal, timeType, nameType, loc);
121 }
122 return QString();
123}
124#else // No ICU, use QTZ[LP]_data_p.h data for feature timezone_locale.
125namespace QtTimeZoneLocale {
126// Inline methods promised in QTZL_p.h
127using namespace QtTimeZoneCldr; // QTZP_data_p.h
128constexpr QByteArrayView LocaleZoneExemplar::ianaId() const { return ianaIdData + ianaIdIndex; }
129constexpr QByteArrayView LocaleZoneNames::ianaId() const { return ianaIdData + ianaIdIndex; }
130} // QtTimeZoneLocale
131
132namespace {
133using namespace QtTimeZoneLocale; // QTZL_p.h QTZL_data_p.h
134using namespace QtTimeZoneCldr; // QTZP_data_p.h
135// Accessors for the QTZL_data_p.h
136
137template <typename Row, typename Sought, typename Condition>
138const Row *findTableEntryFor(const QSpan<Row> data, Sought value, Condition test)
139{
140 // We have the present locale's data (if any). Its rows are sorted on
141 // (localeIndex and) a field for which we want the Sought value. The test()
142 // compares that field.
143 auto begin = data.begin(), end = data.end();
144 Q_ASSERT(begin == end || end->localeIndex > begin->localeIndex);
145 Q_ASSERT(begin == end || end[-1].localeIndex == begin->localeIndex);
146 auto row = std::lower_bound(begin, end, value, test);
147 return row == end ? nullptr : row;
148}
149
150QString exemplarCityFor(const LocaleZoneData &locale, const LocaleZoneData &next,
151 QByteArrayView iana)
152{
153 auto xct = findTableEntryFor(
154 QSpan(localeZoneExemplarTable).first(next.m_exemplarTableStart
155 ).sliced(locale.m_exemplarTableStart),
156 iana, [](auto &row, QByteArrayView key) { return row.ianaId() < key; });
157 if (xct && xct->ianaId() == iana)
158 return xct->exemplarCity().getData(exemplarCityTable);
159 return {};
160}
161
162// Accessors for the QTZP_data_p.h
163quint32 clipEpochMinute(qint64 epochMinute)
164{
165 // ZoneMetaHistory's quint32 UTC epoch minutes.
166 // Dates from 1970-01-01 to 10136-02-16 (at 04:14) are representable.
167 constexpr quint32 epoch = 0;
168 // Since the .end value of an interval that does end is the first epoch
169 // minutes *after* the interval, intervalEndsBefore() uses a <= test. The
170 // value ~epoch (0xffffffff) is used as a sentinel value to mean "there is
171 // no end", so we need a value strictly less than it for "epoch minutes too
172 // big to represent" so that this value is less than "no end". So the value
173 // 1 ^ ~epoch (0xfffffffe) is reserved as this "unrepresentably late time"
174 // and the scripts to generate data assert that no actual interval ends then
175 // or later.
176 constexpr quint32 ragnarok = 1 ^ ~epoch;
177 return epochMinute + 1 >= ragnarok ? ragnarok : quint32(epochMinute);
178}
179
180constexpr bool intervalEndsBefore(const ZoneMetaHistory &record, quint32 dt) noexcept
181{
182 // See clipEpochMinute()'s explanation of ragnarok for why this is <=
183 return record.end <= dt;
184}
185
186/* The metaZoneKey of the ZoneMetaHistory entry whose ianaId() is equal to the
187 given zoneId, for which atMSecsSinceEpoch refers to an instant between its
188 begin and end. Returns zero if there is no such ZoneMetaHistory entry.
189*/
190quint16 metaZoneAt(QByteArrayView zoneId, qint64 atMSecsSinceEpoch)
191{
192 using namespace QtPrivate::DateTimeConstants;
193 auto it = std::lower_bound(std::begin(zoneHistoryTable), std::end(zoneHistoryTable), zoneId,
194 [](const ZoneMetaHistory &record, QByteArrayView id) {
195 return record.ianaId().compare(id, Qt::CaseInsensitive) < 0;
196 });
197 if (it == std::end(zoneHistoryTable) || it->ianaId().compare(zoneId, Qt::CaseInsensitive) > 0)
198 return 0;
199 const auto stop =
200 std::upper_bound(it, std::end(zoneHistoryTable), zoneId,
201 [](QByteArrayView id, const ZoneMetaHistory &record) {
202 return id.compare(record.ianaId(), Qt::CaseInsensitive) < 0;
203 });
204 const quint32 dt = clipEpochMinute(atMSecsSinceEpoch / MSECS_PER_MIN);
205 it = std::lower_bound(it, stop, dt, intervalEndsBefore);
206 return it != stop && it->begin <= dt ? it->metaZoneKey : 0;
207}
208
209constexpr bool dataBeforeMeta(const MetaZoneData &row, quint16 metaKey) noexcept
210{
211 return row.metaZoneKey < metaKey;
212}
213
214constexpr bool metaDataBeforeTerritory(const MetaZoneData &row, qint16 territory) noexcept
215{
216 return row.territory < territory;
217}
218
219const MetaZoneData *metaZoneStart(quint16 metaKey)
220{
221 const MetaZoneData *const from =
222 std::lower_bound(std::begin(metaZoneTable), std::end(metaZoneTable),
223 metaKey, dataBeforeMeta);
224 if (from == std::end(metaZoneTable) || from->metaZoneKey != metaKey) {
225 qWarning("No metazone data found for metazone key %d", metaKey);
226 return nullptr;
227 }
228 return from;
229}
230
231const MetaZoneData *metaZoneDataFor(const MetaZoneData *from, QLocale::Territory territory)
232{
233 const quint16 metaKey = from->metaZoneKey;
234 const MetaZoneData *const end =
235 std::lower_bound(from, std::end(metaZoneTable), metaKey + 1, dataBeforeMeta);
236 Q_ASSERT(end != from && end[-1].metaZoneKey == metaKey);
237 QLocale::Territory land = territory;
238 do {
239 const MetaZoneData *row =
240 std::lower_bound(from, end, qint16(land), metaDataBeforeTerritory);
241 if (row != end && QLocale::Territory(row->territory) == land) {
242 Q_ASSERT(row->metaZoneKey == metaKey);
243 return row;
244 }
245 // Fall back to World (if territory itself isn't World).
246 } while (std::exchange(land, QLocale::World) != QLocale::World);
247
248 qWarning("Metazone %s lacks World data for %ls",
249 from->metaZoneId().constData(),
250 qUtf16Printable(QLocale::territoryToString(territory)));
251 return nullptr;
252}
253
254QString addPadded(qsizetype width, const QString &zero, const QString &number, QString &&onto)
255{
256 // TODO (QTBUG-122834): QLocale::toString() should support zero-padding directly.
257 width -= number.size() / zero.size();
258 while (width > 0) {
259 onto += zero;
260 --width;
261 }
262 return std::move(onto) + number;
263}
264
265QString formatOffset(QStringView format, int offsetMinutes, const QLocale &locale,
266 QLocale::FormatType form)
267{
268 Q_ASSERT(offsetMinutes >= 0);
269 const QString hour = locale.toString(offsetMinutes / 60);
270 const QString mins = locale.toString(offsetMinutes % 60);
271 // If zero.size() > 1, digits are surrogate pairs; each only counts one
272 // towards width of the field, even if it contributes more to result.size().
273 const QString zero = locale.zeroDigit();
274 QStringView tail = format;
275 QString result;
276 while (!tail.isEmpty()) {
277 if (tail.startsWith(u'\'')) {
278 qsizetype end = tail.indexOf(u'\'', 1);
279 if (end < 0) {
280 qWarning("Unbalanced quote in offset format string: %s",
281 format.toUtf8().constData());
282 return result + tail; // Include the quote; format is bogus.
283 } else if (end == 1) {
284 // Special case: adjacent quotes signify a simple quote.
285 result += u'\'';
286 tail = tail.sliced(2);
287 } else {
288 Q_ASSERT(end > 1); // We searched from index 1.
289 while (end + 1 < tail.size() && tail[end + 1] == u'\'') {
290 // Special case: adjacent quotes inside a quoted string also
291 // signify a simple quote.
292 result += tail.sliced(1, end); // Include a quote at the end
293 tail = tail.sliced(end + 1); // Still starts with a quote
294 end = tail.indexOf(u'\'', 1); // Where's the next ?
295 if (end < 0) {
296 qWarning("Unbalanced quoted quote in offset format string: %s",
297 format.toUtf8().constData());
298 return result + tail;
299 }
300 Q_ASSERT(end > 0);
301 }
302 // Skip leading and trailng quotes:
303 result += tail.sliced(1, end - 1);
304 tail = tail.sliced(end + 1);
305 }
306 } else if (tail.startsWith(u'H')) {
307 qsizetype width = 1;
308 while (width < tail.size() && tail[width] == u'H')
309 ++width;
310 tail = tail.sliced(width);
311 if (form != QLocale::NarrowFormat)
312 result = addPadded(width, zero, hour, std::move(result));
313 else
314 result += hour;
315 } else if (tail.startsWith(u'm')) {
316 qsizetype width = 1;
317 while (width < tail.size() && tail[width] == u'm')
318 ++width;
319 // (At CLDR v45, all locales use two-digit minutes.)
320 // (No known zone has single-digit non-zero minutes.)
321 tail = tail.sliced(width);
322 if (form != QLocale::NarrowFormat)
323 result = addPadded(width, zero, mins, std::move(result));
324 else if (offsetMinutes % 60)
325 result += mins;
326 else if (result.endsWith(u':') || result.endsWith(u'.'))
327 result.chop(1);
328 // (At CLDR v45, mm follows H either immediately or after a colon or dot.)
329 } else if (tail[0].isHighSurrogate() && tail.size() > 1
330 && tail[1].isLowSurrogate()) {
331 result += tail.first(2);
332 tail = tail.sliced(2);
333 } else {
334 result += tail.front();
335 tail = tail.sliced(1);
336 }
337 }
338 return result;
339}
340
341} // nameless namespace
342
343namespace QtTimeZoneLocale {
344
345QList<QByteArrayView> ianaIdsForTerritory(QLocale::Territory territory)
346{
347 QList<QByteArrayView> result;
348 {
349 const TerritoryZone *row =
350 std::lower_bound(std::begin(territoryZoneMap), std::end(territoryZoneMap),
351 qint16(territory),
352 [](const TerritoryZone &row, qint16 territory) {
353 return row.territory < territory;
354 });
355 if (row != std::end(territoryZoneMap) && QLocale::Territory(row->territory) == territory)
356 result << row->ianaId();
357 }
358 for (const MetaZoneData &row : metaZoneTable) {
359 if (QLocale::Territory(row.territory) == territory)
360 result << row.ianaId();
361 }
362 return result;
363}
364
365// The QDateTime is only needed by the fall-back implementation in qlocale.cpp;
366// the calls below don't need to pass a valid QDateTime (based on its
367// atMSecsSinceEpoch); an invalid QDateTime() will suffice and be ignored.
368QString zoneOffsetFormat(const QLocale &locale, qsizetype locInd, QLocale::FormatType width,
369 const QDateTime &, int offsetSeconds)
370{
371 // QLocale::LongFormat gets the full GMT-prefix plus hour offset.
372 // QLocale::ShortFormat gets just the hour offset (with full with).
373 // QLocale::NarrowFormat gets the GMT-prefix plus the pruned hour format.
374 // The last drops :00 for zero minutes and removes leading 0 from the hour.
375 const LocaleZoneData &locData = localeZoneData[locInd];
376
377 auto hourFormatR = offsetSeconds < 0 ? locData.negHourFormat() : locData.posHourFormat();
378 QStringView hourFormat = hourFormatR.viewData(hourFormatTable);
379 Q_ASSERT(!hourFormat.isEmpty());
380 // Sign is already handled by choice of the hourFormat:
381 offsetSeconds = qAbs(offsetSeconds);
382 // Offsets are only displayed in minutes - round seconds (if any) to nearest
383 // minute (prefering an even minute when rounding an exact half):
384 const int offsetMinutes = (offsetSeconds + 29 + (1 & (offsetSeconds / 60))) / 60;
385
386 const QString hourOffset = formatOffset(hourFormat, offsetMinutes, locale, width);
387 if (width == QLocale::ShortFormat)
388 return hourOffset;
389
390 QStringView offsetFormat = locData.offsetGmtFormat().viewData(gmtFormatTable);
391 Q_ASSERT(!offsetFormat.isEmpty());
392 return offsetFormat.arg(hourOffset);
393}
394
395} // QtTimeZoneLocale
396
397QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc,
398 QTimeZone::TimeType timeType,
399 QTimeZone::NameType nameType,
400 const QLocale &locale) const
401{
402 if (nameType == QTimeZone::OffsetName) {
403 // Doesn't need fallbacks, since every locale has hour and offset formats.
404 return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::LongFormat,
405 QDateTime(), offsetFromUtc);
406 }
407
408 // An IANA ID may give clues to fall back on for abbreviation or exemplar city:
409 QByteArray ianaAbbrev, ianaTail;
410 const auto scanIana = [&](QByteArrayView iana) {
411 // Scan the name of each zone whose data we consider using and, if the
412 // name gives us a clue to a fallback for which we have nothing better
413 // yet, remember it (and ignore later clues for that fallback).
414 if (!ianaAbbrev.isEmpty() && !ianaTail.isEmpty())
415 return;
416 qsizetype cut = iana.lastIndexOf('/');
417 QByteArrayView tail = cut < 0 ? iana : iana.sliced(cut + 1);
418 // Deal with a couple of special cases
419 if (tail == "McMurdo") { // Exceptional lowercase-uppercase sequence without space
420 if (ianaTail.isEmpty())
421 ianaTail = "McMurdo"_ba;
422 return;
423 } else if (tail == "DumontDUrville") { // Chopped to fit into IANA's 14-char limit
424 if (ianaTail.isEmpty())
425 ianaTail = "Dumont d'Urville"_ba;
426 return;
427 } else if (tail.isEmpty()) {
428 // Custom zone with perverse m_id ?
429 return;
430 }
431
432 // Even if it is abbr or city name, we don't care if we've found one before.
433 bool maybeAbbr = ianaAbbrev.isEmpty(), maybeCityName = ianaTail.isEmpty(), inword = false;
434 char sign = '\0';
435 for (char ch : tail) {
436 if (ch == '+' || ch == '-') {
437 if (ch == '+' || !inword)
438 maybeCityName = false;
439 inword = false;
440 if (maybeAbbr) {
441 if (sign)
442 maybeAbbr = false; // two signs: no
443 else
444 sign = ch;
445 }
446 } else if (ch == '_') {
447 maybeAbbr = false;
448 if (!inword) // No double-underscore, or leading underscore
449 maybeCityName = false;
450 inword = false;
451 } else if (QChar::isLower(ch)) {
452 maybeAbbr = false;
453 // Dar_es_Salaam shows both cases as word starts
454 inword = true;
455 } else if (QChar::isUpper(ch)) {
456 if (sign)
457 maybeAbbr = false;
458 if (inword)
459 maybeCityName = false;
460 inword = true;
461 } else if (QChar::isDigit(ch)) {
462 if (!sign)
463 maybeAbbr = false;
464 maybeCityName = false;
465 inword = false;
466 }
467
468 if (!maybeAbbr && !maybeCityName)
469 break;
470 }
471 if (maybeAbbr && maybeCityName) // No real IANA ID matches both
472 return;
473
474 if (maybeAbbr) {
475 if (tail.endsWith("-0") || tail.endsWith("+0"))
476 tail = tail.chopped(2);
477 ianaAbbrev = tail.toByteArray();
478 if (sign && iana.startsWith("Etc/")) { // Reverse convention for offsets
479 if (sign == '-')
480 ianaAbbrev = ianaAbbrev.replace('-', '+');
481 else if (sign == '+')
482 ianaAbbrev = ianaAbbrev.replace('+', '-');
483 }
484 }
485 if (maybeCityName)
486 ianaTail = tail.toByteArray().replace('_', ' ');
487 }; // end scanIana
488
489 scanIana(m_id);
490 if (QByteArray iana = aliasToIana(m_id); !iana.isEmpty() && iana != m_id)
491 scanIana(iana);
492
493 // Requires locData, nextData set suitably - save repetition of member:
494#define tableLookup(table, member, sought, test)
495 findTableEntryFor(QSpan(table).first(nextData.member).sliced(locData.member), sought, test)
496 // Note: any commas in test need to be within parentheses; but the only
497 // comma a comparison should need is in its (parenthesised) parameter list.
498
499 const QList<qsizetype> indices = fallbackLocalesFor(locale.d->m_index);
500 QString exemplarCity; // In case we need it.
501 const auto metaIdBefore = [](auto &row, quint16 key) { return row.metaIdIndex < key; };
502
503 // First try for an actual name:
504 for (const qsizetype locInd : indices) {
505 const LocaleZoneData &locData = localeZoneData[locInd];
506 // After the row for the last actual locale, there's a terminal row:
507 Q_ASSERT(std::size_t(locInd) < std::size(localeZoneData) - 1);
508 const LocaleZoneData &nextData = localeZoneData[locInd + 1];
509
510 QByteArrayView iana{m_id};
511 if (quint16 metaKey = metaZoneAt(iana, atMSecsSinceEpoch)) {
512 if (const MetaZoneData *metaFrom = metaZoneStart(metaKey)) {
513 quint16 metaIdIndex = metaFrom->metaIdIndex;
514 QLocaleData::DataRange range{0, 0};
515 const char16_t *strings = nullptr;
516 if (nameType == QTimeZone::ShortName) {
517 auto row = tableLookup(localeMetaZoneShortNameTable, m_metaShortTableStart,
518 metaIdIndex, metaIdBefore);
519 if (row && row->metaIdIndex == metaIdIndex) {
520 range = row->shortName(timeType);
521 strings = shortMetaZoneNameTable;
522 }
523 } else { // LongName or DefaultName
524 auto row = tableLookup(localeMetaZoneLongNameTable, m_metaLongTableStart,
525 metaIdIndex, metaIdBefore);
526 if (row && row->metaIdIndex == metaIdIndex) {
527 range = row->longName(timeType);
528 strings = longMetaZoneNameTable;
529 }
530 }
531 Q_ASSERT(strings || !range.size);
532
533 if (range.size)
534 return range.getData(strings);
535
536 if (const auto *metaRow = metaZoneDataFor(metaFrom, locale.territory()))
537 iana = metaRow->ianaId(); // Use IANA ID of zone in use at that time
538 }
539 }
540
541 // Use exemplar city from closest match to locale, m_id:
542 if (exemplarCity.isEmpty()) {
543 exemplarCity = exemplarCityFor(locData, nextData, m_id);
544 if (exemplarCity.isEmpty())
545 exemplarCity = exemplarCityFor(locData, nextData, iana);
546 }
547 if (iana != m_id) // Check for hints to abbreviation and exemplar city:
548 scanIana(iana);
549
550 // That may give us a revised IANA ID; if the first search fails, fall back
551 // to m_id, if different.
552 do {
553 auto row = tableLookup(
554 localeZoneNameTable, m_zoneTableStart,
555 iana, [](auto &row, QByteArrayView key) { return row.ianaId() < key; });
556 if (row && row->ianaId() == iana) {
557 QLocaleData::DataRange range = row->name(nameType, timeType);
558 if (range.size) {
559 auto table = nameType == QTimeZone::ShortName
560 ? shortZoneNameTable
561 : longZoneNameTable;
562 return range.getData(table);
563 }
564 }
565 } while (std::exchange(iana, QByteArrayView{m_id}) != m_id);
566 }
567 // Most zones should now have ianaAbbrev or ianaTail set, maybe even both.
568 // We've now tried all the candidates we'll see for those.
569 // If an IANA ID's last component looked like a city name, use it.
570 if (exemplarCity.isEmpty() && !ianaTail.isEmpty())
571 exemplarCity = QString::fromLatin1(ianaTail); // It's ASCII
572
573 switch (nameType) {
574 case QTimeZone::DefaultName:
575 case QTimeZone::LongName:
576 for (const qsizetype locInd : indices) {
577 const LocaleZoneData &locData = localeZoneData[locInd];
578 QStringView regionFormat
579 = locData.regionFormatRange(timeType).viewData(regionFormatTable);
580 if (!regionFormat.isEmpty()) {
581 QString where = exemplarCity;
582 // TODO: if empty, use territory name
583 if (!where.isEmpty())
584 return regionFormat.arg(where);
585 }
586 }
587#if 0 // See comment within.
588 for (const qsizetype locInd : indices) {
589 const LocaleZoneData &locData = localeZoneData[locInd];
590 QStringView fallbackFormat = locData.fallbackFormat().viewData(fallbackFormatTable);
591 // Use fallbackFormat - probably never needed, as regionFormat is
592 // never empty, and this also needs city or territory name (along
593 // with metazone name).
594 }
595#endif
596 break;
597
598 case QTimeZone::ShortName:
599 // If an IANA ID's last component looked like an abbreviation (UTC, EST, ...) use it.
600 if (!ianaAbbrev.isEmpty())
601 return QString::fromLatin1(ianaAbbrev); // It's ASCII
602 break;
603
604 case QTimeZone::OffsetName:
605 Q_UNREACHABLE_RETURN(QString());
606 }
607
608#undef tableLookup
609
610 // Final fall-back: ICU seems to use a compact form of offset time for
611 // short-forms it doesn't know. This seems to correspond to the short form
612 // of LDML's Localized GMT format.
613 return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::NarrowFormat,
614 QDateTime(), offsetFromUtc);
615}
616#endif // ICU or not
617
618QT_END_NAMESPACE
QList< QByteArrayView > ianaIdsForTerritory(QLocale::Territory territory)
#define tableLookup(table, member, sought, test)