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// Qt-Security score:critical reason:data-parser
4
5#include <private/qtimezonelocale_p.h>
6#include <private/qtimezoneprivate_p.h>
7
8#if !QT_CONFIG(icu)
9# include <QtCore/qspan.h>
10# include <private/qdatetime_p.h>
11# include <private/qtools_p.h>
12// Use data generated from CLDR:
13# include "qtimezonelocale_data_p.h"
14# include "qtimezoneprivate_data_p.h"
15# ifdef QT_CLDR_ZONE_DEBUG
16# include "../text/qlocale_data_p.h"
17QT_BEGIN_NAMESPACE
18static_assert(std::size(locale_data) == std::size(QtTimeZoneLocale::localeZoneData));
19// Size includes terminal rows: for now, they do match in tag IDs, but they needn't.
20static_assert([]() {
21 for (std::size_t i = 0; i < std::size(locale_data); ++i) {
22 const auto &loc = locale_data[i];
23 const auto &zone = QtTimeZoneLocale::localeZoneData[i];
24 if (loc.m_language_id != zone.m_language_id
25 || loc.m_script_id != zone.m_script_id
26 || loc.m_territory_id != zone.m_territory_id) {
27 return false;
28 }
29 }
30 return true;
31}());
32QT_END_NAMESPACE
33# endif
34#endif
35
36QT_BEGIN_NAMESPACE
37
38using namespace Qt::StringLiterals;
39
40#if QT_CONFIG(icu) // Get data from ICU:
41namespace {
42
43// Convert TimeType and NameType into ICU UCalendarDisplayNameType
44UCalendarDisplayNameType ucalDisplayNameType(QTimeZone::TimeType timeType,
45 QTimeZone::NameType nameType)
46{
47 // TODO ICU C UCalendarDisplayNameType does not support full set of C++ TimeZone::EDisplayType
48 // For now, treat Generic as Standard
49 switch (nameType) {
50 case QTimeZone::ShortName:
51 return timeType == QTimeZone::DaylightTime ? UCAL_SHORT_DST : UCAL_SHORT_STANDARD;
52 case QTimeZone::DefaultName:
53 case QTimeZone::LongName:
54 return timeType == QTimeZone::DaylightTime ? UCAL_DST : UCAL_STANDARD;
55 case QTimeZone::OffsetName:
56 Q_UNREACHABLE(); // Callers of ucalTimeZoneDisplayName() should take care of OffsetName.
57 }
58 Q_UNREACHABLE_RETURN(UCAL_STANDARD);
59}
60
61} // nameless namespace
62
63namespace QtTimeZoneLocale {
64
65// Qt wrapper around ucal_getTimeZoneDisplayName()
66// Used directly by ICU backend; indirectly by TZ (see below).
67QString ucalTimeZoneDisplayName(UCalendar *ucal,
68 QTimeZone::TimeType timeType,
69 QTimeZone::NameType nameType,
70 const QByteArray &localeCode)
71{
72 constexpr int32_t BigNameLength = 50;
73 int32_t size = BigNameLength;
74 QString result(size, Qt::Uninitialized);
75 auto dst = [&result]() { return reinterpret_cast<UChar *>(result.data()); };
76 UErrorCode status = U_ZERO_ERROR;
77 const UCalendarDisplayNameType utype = ucalDisplayNameType(timeType, nameType);
78
79 // size = ucal_getTimeZoneDisplayName(cal, type, locale, result, resultLength, status)
80 size = ucal_getTimeZoneDisplayName(ucal, utype, localeCode.constData(),
81 dst(), size, &status);
82
83 // If overflow, then resize and retry
84 if (size > BigNameLength || status == U_BUFFER_OVERFLOW_ERROR) {
85 result.resize(size);
86 status = U_ZERO_ERROR;
87 size = ucal_getTimeZoneDisplayName(ucal, utype, localeCode.constData(),
88 dst(), size, &status);
89 }
90
91 if (!U_SUCCESS(status))
92 return QString();
93
94 // Resize and return:
95 result.resize(size);
96 return result;
97}
98
99bool ucalKnownTimeZoneId(const QString &ianaStr)
100{
101 const UChar *const name = reinterpret_cast<const UChar *>(ianaStr.constData());
102 // We are not interested in the value, but we have to pass something.
103 // No known IANA zone name is (up to 2023) longer than 30 characters.
104 constexpr size_t size = 64;
105 UChar buffer[size];
106
107 // TODO: convert to ucal_getIanaTimeZoneID(), new draft in ICU 74, once we
108 // can rely on its availability, assuming it works the same once not draft.
109 UErrorCode status = U_ZERO_ERROR;
110 UBool isSys = false;
111 // Returns the length of the IANA zone name (but we don't care):
112 ucal_getCanonicalTimeZoneID(name, ianaStr.size(), buffer, size, &isSys, &status);
113 // We're only interested if the result is a "system" (i.e. IANA) ID:
114 return isSys;
115}
116
117} // QtTimeZoneLocale
118
119// Used by TZ backends when ICU is available:
120QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc,
121 QTimeZone::TimeType timeType,
122 QTimeZone::NameType nameType,
123 const QLocale &locale) const
124{
125 Q_UNUSED(atMSecsSinceEpoch);
126 // TODO: use CLDR data for the offset name.
127 // No ICU API for offset formats, so fall back to our ISO one, even if
128 // locale isn't C:
129 if (nameType == QTimeZone::OffsetName)
130 return isoOffsetFormat(offsetFromUtc);
131
132 const QString id = QString::fromUtf8(m_id);
133 // Need to check id is known to ICU, since ucal_open() will return a
134 // misleading "valid" GMT ucal when it doesn't recognise id.
135 if (!QtTimeZoneLocale::ucalKnownTimeZoneId(id))
136 return QString();
137
138 const QByteArray loc = locale.name().toUtf8();
139 UErrorCode status = U_ZERO_ERROR;
140 // TODO: QTBUG-124271 can we cache any of this ?
141 UCalendar *ucal = ucal_open(reinterpret_cast<const UChar *>(id.data()), id.size(),
142 loc.constData(), UCAL_DEFAULT, &status);
143 if (ucal && U_SUCCESS(status)) {
144 auto tidier = qScopeGuard([ucal]() { ucal_close(ucal); });
145 return QtTimeZoneLocale::ucalTimeZoneDisplayName(ucal, timeType, nameType, loc);
146 }
147 return QString();
148}
149#else // No ICU, use QTZ[LP]_data_p.h data for feature timezone_locale.
150namespace QtTimeZoneLocale {
151// Inline methods promised in QTZL_p.h
152using namespace QtTimeZoneCldr; // QTZP_data_p.h
153constexpr QByteArrayView LocaleZoneExemplar::ianaId() const { return ianaIdData + ianaIdIndex; }
154constexpr QByteArrayView LocaleZoneNames::ianaId() const { return ianaIdData + ianaIdIndex; }
155} // QtTimeZoneLocale
156
157namespace {
158using namespace QtTimeZoneLocale; // QTZL_p.h QTZL_data_p.h
159using namespace QtTimeZoneCldr; // QTZP_data_p.h
160// Accessors for the QTZL_data_p.h
161
162template <typename Row, typename Sought, typename Condition>
163const Row *findTableEntryFor(const QSpan<Row> data, Sought value, Condition test)
164{
165 // We have the present locale's data (if any). Its rows are sorted on
166 // (localeIndex and) a field for which we want the Sought value. The test()
167 // compares that field.
168 auto begin = data.begin(), end = data.end();
169 Q_ASSERT(begin == end || end->localeIndex > begin->localeIndex);
170 Q_ASSERT(begin == end || end[-1].localeIndex == begin->localeIndex);
171 auto row = std::lower_bound(begin, end, value, test);
172 return row == end ? nullptr : row;
173}
174
175QString exemplarCityFor(const LocaleZoneData &locale, const LocaleZoneData &next,
176 QByteArrayView iana)
177{
178 auto xct = findTableEntryFor(
179 QSpan(localeZoneExemplarTable).first(next.m_exemplarTableStart
180 ).sliced(locale.m_exemplarTableStart),
181 iana, [](auto &row, QByteArrayView key) { return row.ianaId() < key; });
182 if (xct && xct->ianaId() == iana)
183 return xct->exemplarCity().getData(exemplarCityTable);
184 return {};
185}
186
187// Accessors for the QTZP_data_p.h
188quint32 clipEpochMinute(qint64 epochMinute)
189{
190 // ZoneMetaHistory's quint32 UTC epoch minutes.
191 // Dates from 1970-01-01 to 10136-02-16 (at 04:14) are representable.
192 constexpr quint32 epoch = 0;
193 // Since the .end value of an interval that does end is the first epoch
194 // minutes *after* the interval, intervalEndsBefore() uses a <= test. The
195 // value ~epoch (0xffffffff) is used as a sentinel value to mean "there is
196 // no end", so we need a value strictly less than it for "epoch minutes too
197 // big to represent" so that this value is less than "no end". So the value
198 // 1 ^ ~epoch (0xfffffffe) is reserved as this "unrepresentably late time"
199 // and the scripts to generate data assert that no actual interval ends then
200 // or later.
201 constexpr quint32 ragnarok = 1 ^ ~epoch;
202 return epochMinute + 1 >= ragnarok ? ragnarok : quint32(epochMinute);
203}
204
205constexpr bool intervalEndsBefore(const ZoneMetaHistory &record, quint32 dt) noexcept
206{
207 // See clipEpochMinute()'s explanation of ragnarok for why this is <=
208 return record.end <= dt;
209}
210
211/* The metaZoneKey of the ZoneMetaHistory entry whose ianaId() is equal to the
212 given zoneId, for which atMSecsSinceEpoch refers to an instant between its
213 begin and end. Returns zero if there is no such ZoneMetaHistory entry.
214*/
215quint16 metaZoneAt(QByteArrayView zoneId, qint64 atMSecsSinceEpoch)
216{
217 using namespace QtPrivate::DateTimeConstants;
218 auto it = std::lower_bound(std::begin(zoneHistoryTable), std::end(zoneHistoryTable), zoneId,
219 [](const ZoneMetaHistory &record, QByteArrayView id) {
220 return record.ianaId().compare(id, Qt::CaseInsensitive) < 0;
221 });
222 if (it == std::end(zoneHistoryTable) || it->ianaId().compare(zoneId, Qt::CaseInsensitive) > 0)
223 return 0;
224 const auto stop =
225 std::upper_bound(it, std::end(zoneHistoryTable), zoneId,
226 [](QByteArrayView id, const ZoneMetaHistory &record) {
227 return id.compare(record.ianaId(), Qt::CaseInsensitive) < 0;
228 });
229 const quint32 dt = clipEpochMinute(atMSecsSinceEpoch / MSECS_PER_MIN);
230 it = std::lower_bound(it, stop, dt, intervalEndsBefore);
231 return it != stop && it->begin <= dt ? it->metaZoneKey : 0;
232}
233
234// True if the named zone is ever part of the specified metazone:
235bool zoneEverInMeta(QByteArrayView zoneId, quint16 metaKey)
236{
237 for (auto it = std::lower_bound(std::begin(zoneHistoryTable), std::end(zoneHistoryTable),
238 zoneId,
239 [](const ZoneMetaHistory &record, QByteArrayView id) {
240 return record.ianaId().compare(id, Qt::CaseInsensitive) < 0;
241 });
242 it != std::end(zoneHistoryTable) && it->ianaId().compare(zoneId, Qt::CaseInsensitive) == 0;
243 ++it) {
244 if (it->metaZoneKey == metaKey)
245 return true;
246 }
247 return false;
248}
249
250constexpr bool dataBeforeMeta(const MetaZoneData &row, quint16 metaKey) noexcept
251{
252 return row.metaZoneKey < metaKey;
253}
254
255constexpr bool metaDataBeforeTerritory(const MetaZoneData &row, qint16 territory) noexcept
256{
257 return row.territory < territory;
258}
259
260const MetaZoneData *metaZoneStart(quint16 metaKey)
261{
262 const MetaZoneData *const from =
263 std::lower_bound(std::begin(metaZoneTable), std::end(metaZoneTable),
264 metaKey, dataBeforeMeta);
265 if (from == std::end(metaZoneTable) || from->metaZoneKey != metaKey) {
266 qWarning("No metazone data found for metazone key %d", metaKey);
267 return nullptr;
268 }
269 return from;
270}
271
272const MetaZoneData *metaZoneDataFor(const MetaZoneData *from, QLocale::Territory territory)
273{
274 const quint16 metaKey = from->metaZoneKey;
275 const MetaZoneData *const end =
276 std::lower_bound(from, std::end(metaZoneTable), metaKey + 1, dataBeforeMeta);
277 Q_ASSERT(end != from && end[-1].metaZoneKey == metaKey);
278 QLocale::Territory land = territory;
279 do {
280 const MetaZoneData *row =
281 std::lower_bound(from, end, qint16(land), metaDataBeforeTerritory);
282 if (row != end && QLocale::Territory(row->territory) == land) {
283 Q_ASSERT(row->metaZoneKey == metaKey);
284 return row;
285 }
286 // Fall back to World (if territory itself isn't World).
287 } while (std::exchange(land, QLocale::World) != QLocale::World);
288
289 qWarning("Metazone %s lacks World data for %ls",
290 from->metaZoneId().constData(),
291 qUtf16Printable(QLocale::territoryToString(territory)));
292 return nullptr;
293}
294
295QString addPadded(qsizetype width, const QString &zero, const QString &number, QString &&onto)
296{
297 // TODO (QTBUG-122834): QLocale::toString() should support zero-padding directly.
298 width -= number.size() / zero.size();
299 while (width > 0) {
300 onto += zero;
301 --width;
302 }
303 return std::move(onto) + number;
304}
305
306QString formatOffset(QStringView format, int offsetMinutes, const QLocale &locale,
307 QLocale::FormatType form)
308{
309 Q_ASSERT(offsetMinutes >= 0);
310 const QString hour = locale.toString(offsetMinutes / 60);
311 const QString mins = locale.toString(offsetMinutes % 60);
312 // If zero.size() > 1, digits are surrogate pairs; each only counts one
313 // towards width of the field, even if it contributes more to result.size().
314 const QString zero = locale.zeroDigit();
315 QStringView tail = format;
316 QString result;
317 while (!tail.isEmpty()) {
318 if (tail.startsWith(u'\'')) {
319 qsizetype end = tail.indexOf(u'\'', 1);
320 if (end < 0) {
321 qWarning("Unbalanced quote in offset format string: %s",
322 format.toUtf8().constData());
323 return result + tail; // Include the quote; format is bogus.
324 } else if (end == 1) {
325 // Special case: adjacent quotes signify a simple quote.
326 result += u'\'';
327 tail = tail.sliced(2);
328 } else {
329 Q_ASSERT(end > 1); // We searched from index 1.
330 while (end + 1 < tail.size() && tail[end + 1] == u'\'') {
331 // Special case: adjacent quotes inside a quoted string also
332 // signify a simple quote.
333 result += tail.sliced(1, end); // Include a quote at the end
334 tail = tail.sliced(end + 1); // Still starts with a quote
335 end = tail.indexOf(u'\'', 1); // Where's the next ?
336 if (end < 0) {
337 qWarning("Unbalanced quoted quote in offset format string: %s",
338 format.toUtf8().constData());
339 return result + tail;
340 }
341 Q_ASSERT(end > 0);
342 }
343 // Skip leading and trailng quotes:
344 result += tail.sliced(1, end - 1);
345 tail = tail.sliced(end + 1);
346 }
347 } else if (tail.startsWith(u'H')) {
348 qsizetype width = 1;
349 while (width < tail.size() && tail[width] == u'H')
350 ++width;
351 tail = tail.sliced(width);
352 if (form != QLocale::NarrowFormat)
353 result = addPadded(width, zero, hour, std::move(result));
354 else
355 result += hour;
356 } else if (tail.startsWith(u'm')) {
357 qsizetype width = 1;
358 while (width < tail.size() && tail[width] == u'm')
359 ++width;
360 // (At CLDR v45, all locales use two-digit minutes.)
361 // (No known zone has single-digit non-zero minutes.)
362 tail = tail.sliced(width);
363 if (form != QLocale::NarrowFormat)
364 result = addPadded(width, zero, mins, std::move(result));
365 else if (offsetMinutes % 60)
366 result += mins;
367 else if (result.endsWith(u':') || result.endsWith(u'.'))
368 result.chop(1);
369 // (At CLDR v45, mm follows H either immediately or after a colon or dot.)
370 } else if (tail[0].isHighSurrogate() && tail.size() > 1
371 && tail[1].isLowSurrogate()) {
372 result += tail.first(2);
373 tail = tail.sliced(2);
374 } else {
375 result += tail.front();
376 tail = tail.sliced(1);
377 }
378 }
379 return result;
380}
381
382struct OffsetFormatMatch
383{
384 qsizetype size = 0;
385 int offset = 0;
386 operator bool() { return size > 0; }
387};
388
389OffsetFormatMatch matchOffsetText(QStringView text, QStringView format, const QLocale &locale,
390 QLocale::FormatType scale)
391{
392 // Sign is taken care of by caller.
393 // TODO (QTBUG-77948): rework in terms of text pattern matchers.
394 // For now, don't try to be general, it gets too tricky.
395 OffsetFormatMatch res;
396 // At least at CLDR v46:
397 // Amharic in Ethiopia has ±HHmm formats; all others use separators.
398 // None have single m. All have H or HH before mm. (None has anything after mm.)
399 // In narrow format, mm and its preceding separator are elided for 0
400 // minutes; and hour may be single digit even if the format says HH.
401 qsizetype cut = format.indexOf(u'H');
402 if (cut < 0 || !text.startsWith(format.first(cut)) || !format.endsWith(u"mm"))
403 return res;
404 text = text.sliced(cut);
405 QStringView sep = format.sliced(cut).chopped(2); // Prune prefix and "mm".
406 int hlen = 1; // We already know we have one 'H' at the start of sep.
407 while (hlen < sep.size() && sep[hlen] == u'H')
408 ++hlen;
409 sep = sep.sliced(hlen);
410
411 int digits = 0;
412 while (digits < text.size() && digits < 4 && text[digits].isDigit())
413 ++digits;
414
415 // See zoneOffsetFormat() for the eccentric meaning of scale.
416 QStringView minStr;
417 if (sep.isEmpty()) {
418 if (digits > hlen) {
419 // Long and Short formats allow two-digit match when hlen < 2.
420 if (scale == QLocale::NarrowFormat || (hlen < 2 && text[0] != u'0'))
421 hlen = digits - 2;
422 else if (digits < hlen + 2)
423 return res;
424 minStr = text.sliced(hlen).first(2);
425 } else if (scale == QLocale::NarrowFormat) {
426 hlen = digits;
427 } else if (hlen != digits) {
428 return res;
429 }
430 } else {
431 const qsizetype sepAt = text.indexOf(sep); // May be -1; digits isn't < -1.
432 if (digits < sepAt) // Separator doesn't immediately follow hour.
433 return res;
434 if (scale == QLocale::NarrowFormat || (hlen < 2 && text[0] != u'0'))
435 hlen = digits;
436 else if (digits != hlen)
437 return res;
438 if (sepAt >= 0 && text.size() >= sepAt + sep.size() + 2)
439 minStr = text.sliced(sepAt + sep.size()).first(2);
440 else if (scale != QLocale::NarrowFormat)
441 return res;
442 else if (sepAt >= 0) // Allow minutes without zero-padding in narrow format.
443 minStr = text.sliced(sepAt + sep.size());
444 }
445 if (hlen < 1)
446 return res;
447
448 bool ok = true;
449 uint minute = minStr.isEmpty() ? 0 : locale.toUInt(minStr, &ok);
450 if (!ok && scale == QLocale::NarrowFormat) {
451 // Fall back to matching hour-only form:
452 minStr = {};
453 ok = true;
454 }
455 if (ok && minute < 60) {
456 uint hour = locale.toUInt(text.first(hlen), &ok);
457 if (ok) {
458 res.offset = (hour * 60 + minute) * 60;
459 res.size = cut + hlen;
460 if (!minStr.isEmpty())
461 res.size += sep.size() + minStr.size();
462 }
463 }
464 return res;
465}
466
467OffsetFormatMatch matchOffsetFormat(QStringView text, const QLocale &locale, qsizetype locInd,
468 QLocale::FormatType scale)
469{
470 const LocaleZoneData &locData = localeZoneData[locInd];
471 const QStringView posHourForm = locData.posHourFormat().viewData(hourFormatTable);
472 const QStringView negHourForm = locData.negHourFormat().viewData(hourFormatTable);
473 // For the negative format, allow U+002d to match U+2212 or locale.negativeSign();
474 const bool mapNeg = text.contains(u'-')
475 && (negHourForm.contains(u'\u2212') || negHourForm.contains(locale.negativeSign()));
476 // See zoneOffsetFormat() for the eccentric meaning of scale.
477 if (scale == QLocale::ShortFormat) {
478 if (auto match = matchOffsetText(text, posHourForm, locale, scale))
479 return match;
480 if (auto match = matchOffsetText(text, negHourForm, locale, scale)) {
481 return { match.size, -match.offset };
482 } else if (mapNeg) {
483 const QString mapped = negHourForm.toString()
484 .replace(u'\u2212', u'-').replace(locale.negativeSign(), "-"_L1);
485 if (auto match = matchOffsetText(text, mapped, locale, scale))
486 return { match.size, -match.offset };
487 }
488 } else {
489 const QStringView offsetFormat = locData.offsetGmtFormat().viewData(gmtFormatTable);
490 qsizetype cut = offsetFormat.indexOf(u"%0"); // Should be present
491 if (cut >= 0) {
492 const QStringView gmtPrefix = offsetFormat.first(cut);
493 const QStringView gmtSuffix = offsetFormat.sliced(cut + 2); // After %0
494 const qsizetype gmtSize = cut + gmtSuffix.size();
495 // Cheap pre-test: check suffix does appear after prefix, albeit we must
496 // later check it actually appears right after the offset text:
497 if ((gmtPrefix.isEmpty() || text.startsWith(gmtPrefix))
498 && (gmtSuffix.isEmpty() || text.sliced(cut).indexOf(gmtSuffix) >= 0)) {
499 if (auto match = matchOffsetText(text.sliced(cut), posHourForm, locale, scale)) {
500 if (text.sliced(cut + match.size).startsWith(gmtSuffix)) // too sliced ?
501 return { gmtSize + match.size, match.offset };
502 }
503 if (auto match = matchOffsetText(text.sliced(cut), negHourForm, locale, scale)) {
504 if (text.sliced(cut + match.size).startsWith(gmtSuffix))
505 return { gmtSize + match.size, -match.offset };
506 } else if (mapNeg) {
507 const QString mapped = negHourForm.toString()
508 .replace(u'\u2212', u'-').replace(locale.negativeSign(), "-"_L1);
509 if (auto match = matchOffsetText(text.sliced(cut), mapped, locale, scale)) {
510 if (text.sliced(cut + match.size).startsWith(gmtSuffix))
511 return { gmtSize + match.size, -match.offset };
512 }
513 }
514 // Match empty offset as UTC (unless that'd be an empty match):
515 if (gmtSize > 0 && text.sliced(cut).startsWith(gmtSuffix))
516 return { gmtSize, 0 };
517 }
518 }
519 }
520 return {};
521}
522
523} // nameless namespace
524
525namespace QtTimeZoneLocale {
526
527QList<QByteArrayView> ianaIdsForTerritory(QLocale::Territory territory)
528{
529 QList<QByteArrayView> result;
530 {
531 const TerritoryZone *row =
532 std::lower_bound(std::begin(territoryZoneMap), std::end(territoryZoneMap),
533 qint16(territory),
534 [](const TerritoryZone &row, qint16 territory) {
535 return row.territory < territory;
536 });
537 if (row != std::end(territoryZoneMap) && QLocale::Territory(row->territory) == territory)
538 result << row->ianaId();
539 }
540 for (const MetaZoneData &row : metaZoneTable) {
541 if (QLocale::Territory(row.territory) == territory)
542 result << row.ianaId();
543 }
544 return result;
545}
546
547// The QDateTime is only needed by the fall-back implementation in qlocale.cpp;
548// the calls below don't need to pass a valid QDateTime (based on its
549// atMSecsSinceEpoch); an invalid QDateTime() will suffice and be ignored.
550QString zoneOffsetFormat(const QLocale &locale, qsizetype locInd, QLocale::FormatType width,
551 const QDateTime &, int offsetSeconds)
552{
553 // QLocale::LongFormat gets the full GMT-prefix plus hour offset.
554 // QLocale::ShortFormat gets just the hour offset (with full with).
555 // QLocale::NarrowFormat gets the GMT-prefix plus the pruned hour format.
556 // The last drops :00 for zero minutes and removes leading 0 from the hour.
557 const LocaleZoneData &locData = localeZoneData[locInd];
558
559 auto hourFormatR = offsetSeconds < 0 ? locData.negHourFormat() : locData.posHourFormat();
560 QStringView hourFormat = hourFormatR.viewData(hourFormatTable);
561 Q_ASSERT(!hourFormat.isEmpty());
562 // Sign is already handled by choice of the hourFormat:
563 offsetSeconds = qAbs(offsetSeconds);
564 // Offsets are only displayed in minutes - round seconds (if any) to nearest
565 // minute (prefering an even minute when rounding an exact half):
566 const int offsetMinutes = (offsetSeconds + 29 + (1 & (offsetSeconds / 60))) / 60;
567
568 const QString hourOffset = formatOffset(hourFormat, offsetMinutes, locale, width);
569 if (width == QLocale::ShortFormat)
570 return hourOffset;
571
572 QStringView offsetFormat = locData.offsetGmtFormat().viewData(gmtFormatTable);
573 Q_ASSERT(!offsetFormat.isEmpty());
574 return offsetFormat.arg(hourOffset);
575}
576
577} // QtTimeZoneLocale
578
579QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc,
580 QTimeZone::TimeType timeType,
581 QTimeZone::NameType nameType,
582 const QLocale &locale) const
583{
584 if (nameType == QTimeZone::OffsetName) {
585 // Doesn't need fallbacks, since every locale has hour and offset formats.
586 return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::LongFormat,
587 QDateTime(), offsetFromUtc);
588 }
589 // Handling of long names must stay in sync with findLongNamePrefix(), below.
590
591 // An IANA ID may give clues to fall back on for abbreviation or exemplar city:
592 QByteArray ianaAbbrev, ianaTail;
593 const auto scanIana = [&](QByteArrayView iana) {
594 // Scan the name of each zone whose data we consider using and, if the
595 // name gives us a clue to a fallback for which we have nothing better
596 // yet, remember it (and ignore later clues for that fallback).
597 if (!ianaAbbrev.isEmpty() && !ianaTail.isEmpty())
598 return;
599 qsizetype cut = iana.lastIndexOf('/');
600 QByteArrayView tail = cut < 0 ? iana : iana.sliced(cut + 1);
601 // Deal with a couple of special cases
602 if (tail == "McMurdo") { // Exceptional lowercase-uppercase sequence without space
603 if (ianaTail.isEmpty())
604 ianaTail = "McMurdo"_ba;
605 return;
606 } else if (tail == "DumontDUrville") { // Chopped to fit into IANA's 14-char limit
607 if (ianaTail.isEmpty())
608 ianaTail = "Dumont d'Urville"_ba;
609 return;
610 } else if (tail.isEmpty()) {
611 // Custom zone with perverse m_id ?
612 return;
613 }
614
615 // Even if it is abbr or city name, we don't care if we've found one before.
616 bool maybeAbbr = ianaAbbrev.isEmpty(), maybeCityName = ianaTail.isEmpty(), inword = false;
617 char sign = '\0';
618 for (char ch : tail) {
619 if (ch == '+' || ch == '-') {
620 if (ch == '+' || !inword)
621 maybeCityName = false;
622 inword = false;
623 if (maybeAbbr) {
624 if (sign)
625 maybeAbbr = false; // two signs: no
626 else
627 sign = ch;
628 }
629 } else if (ch == '_') {
630 maybeAbbr = false;
631 if (!inword) // No double-underscore, or leading underscore
632 maybeCityName = false;
633 inword = false;
634 } else if (QChar::isLower(ch)) {
635 maybeAbbr = false;
636 // Dar_es_Salaam shows both cases as word starts
637 inword = true;
638 } else if (QChar::isUpper(ch)) {
639 if (sign)
640 maybeAbbr = false;
641 if (inword)
642 maybeCityName = false;
643 inword = true;
644 } else if (QChar::isDigit(ch)) {
645 if (!sign)
646 maybeAbbr = false;
647 maybeCityName = false;
648 inword = false;
649 }
650
651 if (!maybeAbbr && !maybeCityName)
652 break;
653 }
654 if (maybeAbbr && maybeCityName) // No real IANA ID matches both
655 return;
656
657 if (maybeAbbr) {
658 if (tail.endsWith("-0") || tail.endsWith("+0"))
659 tail = tail.chopped(2);
660 ianaAbbrev = tail.toByteArray();
661 if (sign && iana.startsWith("Etc/")) { // Reverse convention for offsets
662 if (sign == '-')
663 ianaAbbrev = ianaAbbrev.replace('-', '+');
664 else if (sign == '+')
665 ianaAbbrev = ianaAbbrev.replace('+', '-');
666 }
667 }
668 if (maybeCityName)
669 ianaTail = tail.toByteArray().replace('_', ' ');
670 }; // end scanIana
671
672 scanIana(m_id);
673 if (QByteArray iana = aliasToIana(m_id); !iana.isEmpty() && iana != m_id)
674 scanIana(iana);
675
676 // Requires locData, nextData set suitably - save repetition of member:
677#define tableLookup(table, member, sought, test)
678 findTableEntryFor(QSpan(table).first(nextData.member).sliced(locData.member), sought, test)
679 // Note: any commas in test need to be within parentheses; but the only
680 // comma a comparison should need is in its (parenthesised) parameter list.
681
682 const QList<qsizetype> indices = fallbackLocalesFor(locale.d->m_index);
683 QString exemplarCity; // In case we need it.
684 const auto metaIdBefore = [](auto &row, quint16 key) { return row.metaIdIndex < key; };
685
686 // First try for an actual name:
687 for (const qsizetype locInd : indices) {
688 const LocaleZoneData &locData = localeZoneData[locInd];
689 // After the row for the last actual locale, there's a terminal row:
690 Q_ASSERT(std::size_t(locInd) < std::size(localeZoneData) - 1);
691 const LocaleZoneData &nextData = localeZoneData[locInd + 1];
692
693 QByteArrayView iana{m_id};
694 if (quint16 metaKey = metaZoneAt(iana, atMSecsSinceEpoch)) {
695 if (const MetaZoneData *metaFrom = metaZoneStart(metaKey)) {
696 quint16 metaIdIndex = metaFrom->metaIdIndex;
697 QLocaleData::DataRange range{0, 0};
698 const char16_t *strings = nullptr;
699 if (nameType == QTimeZone::ShortName) {
700 auto row = tableLookup(localeMetaZoneShortNameTable, m_metaShortTableStart,
701 metaIdIndex, metaIdBefore);
702 if (row && row->metaIdIndex == metaIdIndex) {
703 range = row->shortName(timeType);
704 strings = shortMetaZoneNameTable;
705 }
706 } else { // LongName or DefaultName
707 auto row = tableLookup(localeMetaZoneLongNameTable, m_metaLongTableStart,
708 metaIdIndex, metaIdBefore);
709 if (row && row->metaIdIndex == metaIdIndex) {
710 range = row->longName(timeType);
711 strings = longMetaZoneNameTable;
712 }
713 }
714 Q_ASSERT(strings || !range.size);
715
716 if (range.size)
717 return range.getData(strings);
718
719 if (const auto *metaRow = metaZoneDataFor(metaFrom, locale.territory()))
720 iana = metaRow->ianaId(); // Use IANA ID of zone in use at that time
721 }
722 }
723
724 // Use exemplar city from closest match to locale, m_id:
725 if (exemplarCity.isEmpty()) {
726 exemplarCity = exemplarCityFor(locData, nextData, m_id);
727 if (exemplarCity.isEmpty())
728 exemplarCity = exemplarCityFor(locData, nextData, iana);
729 }
730 if (iana != m_id) // Check for hints to abbreviation and exemplar city:
731 scanIana(iana);
732
733 // That may give us a revised IANA ID; if the first search fails, fall back
734 // to m_id, if different.
735 do {
736 auto row = tableLookup(
737 localeZoneNameTable, m_zoneTableStart,
738 iana, [](auto &row, QByteArrayView key) { return row.ianaId() < key; });
739 if (row && row->ianaId() == iana) {
740 QLocaleData::DataRange range = row->name(nameType, timeType);
741 if (range.size) {
742 auto table = nameType == QTimeZone::ShortName
743 ? shortZoneNameTable
744 : longZoneNameTable;
745 return range.getData(table);
746 }
747 }
748 } while (std::exchange(iana, QByteArrayView{m_id}) != m_id);
749 }
750 // Most zones should now have ianaAbbrev or ianaTail set, maybe even both.
751 // We've now tried all the candidates we'll see for those.
752 // If an IANA ID's last component looked like a city name, use it.
753 if (exemplarCity.isEmpty() && !ianaTail.isEmpty())
754 exemplarCity = QString::fromLatin1(ianaTail); // It's ASCII
755
756 switch (nameType) {
757 case QTimeZone::DefaultName:
758 case QTimeZone::LongName:
759 for (const qsizetype locInd : indices) {
760 const LocaleZoneData &locData = localeZoneData[locInd];
761 QStringView regionFormat
762 = locData.regionFormatRange(timeType).viewData(regionFormatTable);
763 if (!regionFormat.isEmpty()) {
764 QString where = exemplarCity;
765 // TODO: if empty, use territory name
766 if (!where.isEmpty())
767 return regionFormat.arg(where);
768 }
769 }
770#if 0 // See comment within.
771 for (const qsizetype locInd : indices) {
772 const LocaleZoneData &locData = localeZoneData[locInd];
773 QStringView fallbackFormat = locData.fallbackFormat().viewData(fallbackFormatTable);
774 // Use fallbackFormat - probably never needed, as regionFormat is
775 // never empty, and this also needs city or territory name (along
776 // with metazone name).
777 }
778#endif
779 break;
780
781 case QTimeZone::ShortName:
782 // If an IANA ID's last component looked like an abbreviation (UTC, EST, ...) use it.
783 if (!ianaAbbrev.isEmpty())
784 return QString::fromLatin1(ianaAbbrev); // It's ASCII
785 break;
786
787 case QTimeZone::OffsetName:
788 Q_UNREACHABLE_RETURN(QString());
789 }
790
791#undef tableLookup
792
793 // Final fall-back: ICU seems to use a compact form of offset time for
794 // short-forms it doesn't know. This seems to correspond to the short form
795 // of LDML's Localized GMT format.
796 return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::NarrowFormat,
797 QDateTime(), offsetFromUtc);
798}
799
800// Match what the above might return at the start of a text (usually a tail of a
801// datetime string).
802QTimeZonePrivate::NamePrefixMatch
803QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale,
804 std::optional<qint64> atEpochMillis)
805{
806 constexpr std::size_t invalidMetaId = std::size(metaIdData);
807 constexpr std::size_t invalidIanaId = std::size(ianaIdData);
808 constexpr QTimeZone::TimeType timeTypes[] = {
809 // In preference order, should more than one match:
810 QTimeZone::GenericTime,
811 QTimeZone::StandardTime,
812 QTimeZone::DaylightTime,
813 };
814 struct {
815 qsizetype nameLength = 0;
816 QTimeZone::TimeType timeType = QTimeZone::GenericTime;
817 quint16 ianaIdIndex = invalidIanaId;
818 quint16 metaIdIndex = invalidMetaId;
819 QLocale::Territory where = QLocale::AnyTerritory;
820 } best;
821#define localeRows(table, member) QSpan(table).first(nextData.member).sliced(locData.member)
822
823 const QList<qsizetype> indices = fallbackLocalesFor(locale.d->m_index);
824 for (const qsizetype locInd : indices) {
825 const LocaleZoneData &locData = localeZoneData[locInd];
826 // After the row for the last actual locale, there's a terminal row:
827 Q_ASSERT(std::size_t(locInd) < std::size(localeZoneData) - 1);
828 const LocaleZoneData &nextData = localeZoneData[locInd + 1];
829
830 const auto metaRows = localeRows(localeMetaZoneLongNameTable, m_metaLongTableStart);
831 for (const LocaleMetaZoneLongNames &row : metaRows) {
832 for (const QTimeZone::TimeType type : timeTypes) {
833 QLocaleData::DataRange range = row.longName(type);
834 if (range.size > best.nameLength) {
835 QStringView name = range.viewData(longMetaZoneNameTable);
836 if (text.startsWith(name)) {
837 best = { static_cast<qsizetype>(range.size), type,
838 invalidIanaId, row.metaIdIndex };
839 if (best.nameLength >= text.size())
840 break;
841 }
842 }
843 }
844 if (best.nameLength >= text.size())
845 break;
846 }
847
848 const auto ianaRows = localeRows(localeZoneNameTable, m_zoneTableStart);
849 for (const LocaleZoneNames &row : ianaRows) {
850 for (const QTimeZone::TimeType type : timeTypes) {
851 QLocaleData::DataRange range = row.longName(type);
852 if (range.size > best.nameLength) {
853 QStringView name = range.viewData(longZoneNameTable);
854 // Save potentially expensive "zone is supported" check when possible:
855 bool gotZone = row.ianaIdIndex == best.ianaIdIndex
856 || QTimeZone::isTimeZoneIdAvailable(row.ianaId().toByteArray());
857 if (text.startsWith(name) && gotZone)
858 best = { static_cast<qsizetype>(range.size), type, row.ianaIdIndex };
859 }
860 }
861 }
862 }
863 // That's found us our best match, possibly as a meta-zone
864 if (best.metaIdIndex != invalidMetaId) {
865 const auto metaIdBefore = [](auto &row, quint16 key) { return row.metaIdIndex < key; };
866 // Find the standard IANA ID for this meta-zone (or one for another
867 // supported zone using the meta-zone at the specified time).
868 const MetaZoneData *metaRow =
869 std::lower_bound(std::begin(metaZoneTable), std::end(metaZoneTable),
870 best.metaIdIndex, metaIdBefore);
871 // Table is sorted by metazone, then territory.
872 for (; metaRow < std::end(metaZoneTable)
873 && metaRow->metaIdIndex == best.metaIdIndex; ++metaRow) {
874 auto metaLand = QLocale::Territory(metaRow->territory);
875 // World entry is the "standard" zone for this metazone, so always
876 // prefer it over any territory-specific one (from an earlier row):
877 if ((best.where == QLocale::AnyTerritory || metaLand == QLocale::World)
878 && (atEpochMillis
879 ? metaRow->metaZoneKey == metaZoneAt(metaRow->ianaId(), *atEpochMillis)
880 : zoneEverInMeta(metaRow->ianaId(), metaRow->metaZoneKey))) {
881 if (metaRow->ianaIdIndex == best.ianaIdIndex
882 || QTimeZone::isTimeZoneIdAvailable(metaRow->ianaId().toByteArray())) {
883 best.ianaIdIndex = metaRow->ianaIdIndex;
884 best.where = metaLand;
885 if (best.where == QLocale::World)
886 break;
887 }
888 }
889 }
890 }
891 if (best.ianaIdIndex != invalidIanaId)
892 return { QByteArray(ianaIdData + best.ianaIdIndex), best.nameLength, best.timeType };
893
894 // Now try for a region format:
895 best = {};
896 for (const qsizetype locInd : indices) {
897 const LocaleZoneData &locData = localeZoneData[locInd];
898 const LocaleZoneData &nextData = localeZoneData[locInd + 1];
899 for (const QTimeZone::TimeType timeType : timeTypes) {
900 QStringView regionFormat
901 = locData.regionFormatRange(timeType).viewData(regionFormatTable);
902 // "%0 [Season] Time", "Time in %0 [during Season]" &c.
903 const qsizetype cut = regionFormat.indexOf(u"%0");
904 if (cut < 0) // Shouldn't happen unless empty.
905 continue;
906
907 QStringView prefix = regionFormat.first(cut);
908 // Any text before %0 must appear verbatim at the start of our text:
909 if (cut > 0 && !text.startsWith(prefix))
910 continue;
911 QStringView suffix = regionFormat.sliced(cut + 2); // after %0
912 // This must start with an exemplar city or territory, followed by suffix:
913 QStringView tail = text.sliced(cut);
914
915 // Cheap pretest - any text after %0 must appear *somewhere* in our text:
916 if (suffix.size() && tail.indexOf(suffix) < 0)
917 continue; // No match possible
918
919 // Of course, particularly if just punctuation, a copy of our suffix
920 // might appear within the city or territory name.
921 const auto textMatches = [tail, suffix](QStringView where) {
922 return (where.isEmpty() || tail.startsWith(where))
923 && (suffix.isEmpty() || tail.sliced(where.size()).startsWith(suffix));
924 };
925
926 const auto cityRows = localeRows(localeZoneExemplarTable, m_exemplarTableStart);
927 for (const LocaleZoneExemplar &row : cityRows) {
928 QStringView city = row.exemplarCity().viewData(exemplarCityTable);
929 if (textMatches(city)) {
930 qsizetype length = cut + city.size() + suffix.size();
931 if (length > best.nameLength) {
932 bool gotZone = row.ianaIdIndex == best.ianaIdIndex
933 || QTimeZone::isTimeZoneIdAvailable(row.ianaId().toByteArray());
934 if (gotZone)
935 best = { length, timeType, row.ianaIdIndex };
936 }
937 }
938 }
939 // In localeName() we fall back to the last part of the IANA ID:
940 const QList<QByteArray> allZones = QTimeZone::availableTimeZoneIds();
941 for (const auto &iana : allZones) {
942 Q_ASSERT(!iana.isEmpty());
943 qsizetype slash = iana.lastIndexOf('/');
944 QByteArray local = slash > 0 ? iana.sliced(slash + 1) : iana;
945 QString city = QString::fromLatin1(local.replace('_', ' '));
946 if (textMatches(city)) {
947 qsizetype length = cut + city.size() + suffix.size();
948 if (length > best.nameLength) {
949 // Have to find iana in ianaIdData. Although its entries
950 // from locale-independent data are nicely sorted, the
951 // rest are (sadly) not.
952 QByteArrayView run(ianaIdData, qstrlen(ianaIdData));
953 // std::size includes the trailing '\0', so subtract one:
954 const char *stop = ianaIdData + std::size(ianaIdData) - 1;
955 while (run != iana) {
956 if (run.end() < stop) { // Step to the next:
957 run = QByteArrayView(run.end() + 1);
958 } else {
959 run = QByteArrayView();
960 break;
961 }
962 }
963 if (!run.isEmpty()) {
964 Q_ASSERT(run == iana);
965 const auto ianaIdIndex = run.begin() - ianaIdData;
966 Q_ASSERT(ianaIdIndex <= (std::numeric_limits<quint16>::max)());
967 best = { length, timeType, quint16(ianaIdIndex) };
968 }
969 }
970 }
971 }
972 // TODO: similar for territories, at least once localeName() does so.
973 }
974 }
975 if (best.ianaIdIndex != invalidIanaId)
976 return { QByteArray(ianaIdData + best.ianaIdIndex), best.nameLength, best.timeType };
977#undef localeRows
978
979 return {}; // No match found.
980}
981
982QTimeZonePrivate::NamePrefixMatch
983QTimeZonePrivate::findNarrowOffsetPrefix(QStringView text, const QLocale &locale,
984 QLocale::FormatType scale)
985{
986 if (auto match = matchOffsetFormat(text, locale, locale.d->m_index, scale)) {
987 // Check offset is sane:
988 if (QTimeZone::MinUtcOffsetSecs <= match.offset
989 && match.offset <= QTimeZone::MaxUtcOffsetSecs) {
990
991 // Although we don't have an IANA ID, the ISO offset format text
992 // should match what the QLocale(ianaId) constructor accepts, which
993 // is good enough for our purposes.
994 return { isoOffsetFormat(match.offset, QTimeZone::OffsetName).toLatin1(),
995 match.size, QTimeZone::GenericTime };
996 }
997 }
998 return {};
999}
1000#endif // ICU or not
1001
1002QT_END_NAMESPACE
QList< QByteArrayView > ianaIdsForTerritory(QLocale::Territory territory)
#define tableLookup(table, member, sought, test)
#define localeRows(table, member)