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 const auto isMixedCaseAbbrev = [tail](char ch) {
615 // cv-RU and en-GU abbreviate Chamorro as ChST
616 // scn-IT abbreviates Cuba as CuT/CuST/CuDT
617 // blo-BJ abbreviates GMT as Gk
618 switch (tail.size()) {
619 case 2: return tail == "Gk";
620 case 3: return tail == "CuT";
621 case 4:
622 if (tail[0] == 'C' && tail[1] == ch && tail[3] == 'T') {
623 switch (ch) {
624 case 'h': return tail[2] == 'S';
625 case 'u': return tail[2] == 'S' || tail[2] == 'D';
626 default: break;
627 }
628 }
629 return false;
630 default:
631 break;
632 }
633 return false;
634 };
635
636 // Even if it is abbr or city name, we don't care if we've found one before.
637 bool maybeAbbr = ianaAbbrev.isEmpty(), maybeCityName = ianaTail.isEmpty(), inword = false;
638 char sign = '\0';
639 for (char ch : tail) {
640 if (ch == '+' || ch == '-') {
641 if (ch == '+' || !inword)
642 maybeCityName = false;
643 inword = false;
644 if (maybeAbbr) {
645 if (sign)
646 maybeAbbr = false; // two signs: no
647 else
648 sign = ch;
649 }
650 } else if (ch == '_') {
651 maybeAbbr = false;
652 if (!inword) // No double-underscore, or leading underscore
653 maybeCityName = false;
654 inword = false;
655 } else if (QChar::isLower(ch)) {
656 maybeAbbr = isMixedCaseAbbrev(ch);
657 // Dar_es_Salaam shows both cases as word starts
658 inword = true;
659 } else if (QChar::isUpper(ch)) {
660 if (sign)
661 maybeAbbr = false;
662 if (inword)
663 maybeCityName = false;
664 inword = true;
665 } else if (QChar::isDigit(ch)) {
666 if (!sign)
667 maybeAbbr = false;
668 maybeCityName = false;
669 inword = false;
670 }
671
672 if (!maybeAbbr && !maybeCityName)
673 break;
674 }
675 if (maybeAbbr && maybeCityName) // No real IANA ID matches both
676 return;
677
678 if (maybeAbbr) {
679 if (tail.endsWith("-0") || tail.endsWith("+0"))
680 tail = tail.chopped(2);
681 ianaAbbrev = tail.toByteArray();
682 if (sign && iana.startsWith("Etc/")) { // Reverse convention for offsets
683 if (sign == '-')
684 ianaAbbrev = ianaAbbrev.replace('-', '+');
685 else if (sign == '+')
686 ianaAbbrev = ianaAbbrev.replace('+', '-');
687 }
688 }
689 if (maybeCityName)
690 ianaTail = tail.toByteArray().replace('_', ' ');
691 }; // end scanIana
692
693 scanIana(m_id);
694 if (QByteArray iana = aliasToIana(m_id); !iana.isEmpty() && iana != m_id)
695 scanIana(iana);
696
697 // Requires locData, nextData set suitably - save repetition of member:
698#define tableLookup(table, member, sought, test)
699 findTableEntryFor(QSpan(table).first(nextData.member).sliced(locData.member), sought, test)
700 // Note: any commas in test need to be within parentheses; but the only
701 // comma a comparison should need is in its (parenthesised) parameter list.
702
703 const QList<qsizetype> indices = fallbackLocalesFor(locale.d->m_index);
704 QString exemplarCity; // In case we need it.
705 const auto metaIdBefore = [](auto &row, quint16 key) { return row.metaIdIndex < key; };
706
707 // First try for an actual name:
708 for (const qsizetype locInd : indices) {
709 const LocaleZoneData &locData = localeZoneData[locInd];
710 // After the row for the last actual locale, there's a terminal row:
711 Q_ASSERT(std::size_t(locInd) < std::size(localeZoneData) - 1);
712 const LocaleZoneData &nextData = localeZoneData[locInd + 1];
713
714 QByteArrayView iana{m_id};
715 if (quint16 metaKey = metaZoneAt(iana, atMSecsSinceEpoch)) {
716 if (const MetaZoneData *metaFrom = metaZoneStart(metaKey)) {
717 quint16 metaIdIndex = metaFrom->metaIdIndex;
718 QLocaleData::DataRange range{0, 0};
719 const char16_t *strings = nullptr;
720 if (nameType == QTimeZone::ShortName) {
721 auto row = tableLookup(localeMetaZoneShortNameTable, m_metaShortTableStart,
722 metaIdIndex, metaIdBefore);
723 if (row && row->metaIdIndex == metaIdIndex) {
724 range = row->shortName(timeType);
725 strings = shortMetaZoneNameTable;
726 }
727 } else { // LongName or DefaultName
728 auto row = tableLookup(localeMetaZoneLongNameTable, m_metaLongTableStart,
729 metaIdIndex, metaIdBefore);
730 if (row && row->metaIdIndex == metaIdIndex) {
731 range = row->longName(timeType);
732 strings = longMetaZoneNameTable;
733 }
734 }
735 Q_ASSERT(strings || !range.size);
736
737 if (range.size)
738 return range.getData(strings);
739
740 if (const auto *metaRow = metaZoneDataFor(metaFrom, locale.territory()))
741 iana = metaRow->ianaId(); // Use IANA ID of zone in use at that time
742 }
743 }
744
745 // Use exemplar city from closest match to locale, m_id:
746 if (exemplarCity.isEmpty()) {
747 exemplarCity = exemplarCityFor(locData, nextData, m_id);
748 if (exemplarCity.isEmpty())
749 exemplarCity = exemplarCityFor(locData, nextData, iana);
750 }
751 if (iana != m_id) // Check for hints to abbreviation and exemplar city:
752 scanIana(iana);
753
754 // That may give us a revised IANA ID; if the first search fails, fall back
755 // to m_id, if different.
756 do {
757 auto row = tableLookup(
758 localeZoneNameTable, m_zoneTableStart,
759 iana, [](auto &row, QByteArrayView key) { return row.ianaId() < key; });
760 if (row && row->ianaId() == iana) {
761 QLocaleData::DataRange range = row->name(nameType, timeType);
762 if (range.size) {
763 auto table = nameType == QTimeZone::ShortName
764 ? shortZoneNameTable
765 : longZoneNameTable;
766 return range.getData(table);
767 }
768 }
769 } while (std::exchange(iana, QByteArrayView{m_id}) != m_id);
770 }
771 // Most zones should now have ianaAbbrev or ianaTail set, maybe even both.
772 // We've now tried all the candidates we'll see for those.
773 // If an IANA ID's last component looked like a city name, use it.
774 if (exemplarCity.isEmpty() && !ianaTail.isEmpty())
775 exemplarCity = QString::fromLatin1(ianaTail); // It's ASCII
776
777 switch (nameType) {
778 case QTimeZone::DefaultName:
779 case QTimeZone::LongName:
780 for (const qsizetype locInd : indices) {
781 const LocaleZoneData &locData = localeZoneData[locInd];
782 QStringView regionFormat
783 = locData.regionFormatRange(timeType).viewData(regionFormatTable);
784 if (!regionFormat.isEmpty()) {
785 QString where = exemplarCity;
786 // TODO: if empty, use territory name
787 if (!where.isEmpty())
788 return regionFormat.arg(where);
789 }
790 }
791#if 0 // See comment within.
792 for (const qsizetype locInd : indices) {
793 const LocaleZoneData &locData = localeZoneData[locInd];
794 QStringView fallbackFormat = locData.fallbackFormat().viewData(fallbackFormatTable);
795 // Use fallbackFormat - probably never needed, as regionFormat is
796 // never empty, and this also needs city or territory name (along
797 // with metazone name).
798 }
799#endif
800 break;
801
802 case QTimeZone::ShortName:
803 // If an IANA ID's last component looked like an abbreviation (UTC, EST, ...) use it.
804 if (!ianaAbbrev.isEmpty())
805 return QString::fromLatin1(ianaAbbrev); // It's ASCII
806 break;
807
808 case QTimeZone::OffsetName:
809 Q_UNREACHABLE_RETURN(QString());
810 }
811
812#undef tableLookup
813
814 // Final fall-back: ICU seems to use a compact form of offset time for
815 // short-forms it doesn't know. This seems to correspond to the short form
816 // of LDML's Localized GMT format.
817 return QtTimeZoneLocale::zoneOffsetFormat(locale, locale.d->m_index, QLocale::NarrowFormat,
818 QDateTime(), offsetFromUtc);
819}
820
821// Match what the above might return at the start of a text (usually a tail of a
822// datetime string).
823QTimeZonePrivate::NamePrefixMatch
824QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale,
825 std::optional<qint64> atEpochMillis)
826{
827 constexpr std::size_t invalidMetaId = std::size(metaIdData);
828 constexpr std::size_t invalidIanaId = std::size(ianaIdData);
829 constexpr QTimeZone::TimeType timeTypes[] = {
830 // In preference order, should more than one match:
831 QTimeZone::GenericTime,
832 QTimeZone::StandardTime,
833 QTimeZone::DaylightTime,
834 };
835 struct {
836 qsizetype nameLength = 0;
837 QTimeZone::TimeType timeType = QTimeZone::GenericTime;
838 quint16 ianaIdIndex = invalidIanaId;
839 quint16 metaIdIndex = invalidMetaId;
840 QLocale::Territory where = QLocale::AnyTerritory;
841 } best;
842#define localeRows(table, member) QSpan(table).first(nextData.member).sliced(locData.member)
843
844 const QList<qsizetype> indices = fallbackLocalesFor(locale.d->m_index);
845 for (const qsizetype locInd : indices) {
846 const LocaleZoneData &locData = localeZoneData[locInd];
847 // After the row for the last actual locale, there's a terminal row:
848 Q_ASSERT(std::size_t(locInd) < std::size(localeZoneData) - 1);
849 const LocaleZoneData &nextData = localeZoneData[locInd + 1];
850
851 const auto metaRows = localeRows(localeMetaZoneLongNameTable, m_metaLongTableStart);
852 for (const LocaleMetaZoneLongNames &row : metaRows) {
853 for (const QTimeZone::TimeType type : timeTypes) {
854 QLocaleData::DataRange range = row.longName(type);
855 if (range.size > best.nameLength) {
856 QStringView name = range.viewData(longMetaZoneNameTable);
857 if (text.startsWith(name)) {
858 best = { static_cast<qsizetype>(range.size), type,
859 invalidIanaId, row.metaIdIndex };
860 if (best.nameLength >= text.size())
861 break;
862 }
863 }
864 }
865 if (best.nameLength >= text.size())
866 break;
867 }
868
869 const auto ianaRows = localeRows(localeZoneNameTable, m_zoneTableStart);
870 for (const LocaleZoneNames &row : ianaRows) {
871 for (const QTimeZone::TimeType type : timeTypes) {
872 QLocaleData::DataRange range = row.longName(type);
873 if (range.size > best.nameLength) {
874 QStringView name = range.viewData(longZoneNameTable);
875 // Save potentially expensive "zone is supported" check when possible:
876 bool gotZone = row.ianaIdIndex == best.ianaIdIndex
877 || QTimeZone::isTimeZoneIdAvailable(row.ianaId().toByteArray());
878 if (text.startsWith(name) && gotZone)
879 best = { static_cast<qsizetype>(range.size), type, row.ianaIdIndex };
880 }
881 }
882 }
883 }
884 // That's found us our best match, possibly as a meta-zone
885 if (best.metaIdIndex != invalidMetaId) {
886 const auto metaIdBefore = [](auto &row, quint16 key) { return row.metaIdIndex < key; };
887 // Find the standard IANA ID for this meta-zone (or one for another
888 // supported zone using the meta-zone at the specified time).
889 const MetaZoneData *metaRow =
890 std::lower_bound(std::begin(metaZoneTable), std::end(metaZoneTable),
891 best.metaIdIndex, metaIdBefore);
892 // Table is sorted by metazone, then territory.
893 for (; metaRow < std::end(metaZoneTable)
894 && metaRow->metaIdIndex == best.metaIdIndex; ++metaRow) {
895 auto metaLand = QLocale::Territory(metaRow->territory);
896 // World entry is the "standard" zone for this metazone, so always
897 // prefer it over any territory-specific one (from an earlier row):
898 if ((best.where == QLocale::AnyTerritory || metaLand == QLocale::World)
899 && (atEpochMillis
900 ? metaRow->metaZoneKey == metaZoneAt(metaRow->ianaId(), *atEpochMillis)
901 : zoneEverInMeta(metaRow->ianaId(), metaRow->metaZoneKey))) {
902 if (metaRow->ianaIdIndex == best.ianaIdIndex
903 || QTimeZone::isTimeZoneIdAvailable(metaRow->ianaId().toByteArray())) {
904 best.ianaIdIndex = metaRow->ianaIdIndex;
905 best.where = metaLand;
906 if (best.where == QLocale::World)
907 break;
908 }
909 }
910 }
911 }
912 if (best.ianaIdIndex != invalidIanaId)
913 return { QByteArray(ianaIdData + best.ianaIdIndex), best.nameLength, best.timeType };
914
915 // Now try for a region format.
916 // Since we may get the IANA ID directly from a zone, we may not need an
917 // ianaIdIndex from CLDR-derived tables: and the active backend may know
918 // some zones newer than our latest CLDR.
919 NamePrefixMatch found;
920 for (const qsizetype locInd : indices) {
921 const LocaleZoneData &locData = localeZoneData[locInd];
922 const LocaleZoneData &nextData = localeZoneData[locInd + 1];
923 for (const QTimeZone::TimeType timeType : timeTypes) {
924 QStringView regionFormat
925 = locData.regionFormatRange(timeType).viewData(regionFormatTable);
926 // "%0 [Season] Time", "Time in %0 [during Season]" &c.
927 const qsizetype cut = regionFormat.indexOf(u"%0");
928 if (cut < 0) // Shouldn't happen unless empty.
929 continue;
930
931 QStringView prefix = regionFormat.first(cut);
932 // Any text before %0 must appear verbatim at the start of our text:
933 if (cut > 0 && !text.startsWith(prefix))
934 continue;
935 QStringView suffix = regionFormat.sliced(cut + 2); // after %0
936 // This must start with an exemplar city or territory, followed by suffix:
937 QStringView tail = text.sliced(cut);
938
939 // Cheap pretest - any text after %0 must appear *somewhere* in our text:
940 if (suffix.size() && tail.indexOf(suffix) < 0)
941 continue; // No match possible
942
943 // Of course, particularly if just punctuation, a copy of our suffix
944 // might appear within the city or territory name.
945 const auto textMatches = [tail, suffix](QStringView where) {
946 return (where.isEmpty() || tail.startsWith(where))
947 && (suffix.isEmpty() || tail.sliced(where.size()).startsWith(suffix));
948 };
949
950 const auto cityRows = localeRows(localeZoneExemplarTable, m_exemplarTableStart);
951 for (const LocaleZoneExemplar &row : cityRows) {
952 QStringView city = row.exemplarCity().viewData(exemplarCityTable);
953 if (textMatches(city)) {
954 qsizetype length = cut + city.size() + suffix.size();
955 if (length > found.nameLength) {
956 bool gotZone = row.ianaId() == found.ianaId // (cheap pre-test)
957 || QTimeZone::isTimeZoneIdAvailable(row.ianaId().toByteArray());
958 if (gotZone)
959 found = { row.ianaId().toByteArray(), length, timeType };
960 }
961 }
962 }
963 // In localeName() we fall back to the last part of the IANA ID:
964 const QList<QByteArray> allZones = QTimeZone::availableTimeZoneIds();
965 for (const auto &iana : allZones) {
966 Q_ASSERT(!iana.isEmpty());
967 qsizetype slash = iana.lastIndexOf('/');
968 QByteArray local = slash > 0 ? iana.sliced(slash + 1) : iana;
969 QString city = QString::fromLatin1(local.replace('_', ' '));
970 if (textMatches(city)) {
971 qsizetype length = cut + city.size() + suffix.size();
972 if (length > found.nameLength)
973 found = { iana, length, timeType };
974 }
975 }
976 // TODO: similar for territories, at least once localeName() does so.
977 }
978 }
979#undef localeRows
980
981 return found;
982}
983
984QTimeZonePrivate::NamePrefixMatch
985QTimeZonePrivate::findNarrowOffsetPrefix(QStringView text, const QLocale &locale)
986{
987 // NB: uses QLocale::FormatType with non-canonical meaning !
988 if (auto match = matchOffsetFormat(text, locale, locale.d->m_index, QLocale::NarrowFormat)) {
989 // Check offset is sane:
990 if (QTimeZone::MinUtcOffsetSecs <= match.offset
991 && match.offset <= QTimeZone::MaxUtcOffsetSecs) {
992
993 // Although we don't have an IANA ID, the ISO offset format text
994 // should match what the QLocale(ianaId) constructor accepts, which
995 // is good enough for our purposes.
996 return { isoOffsetFormat(match.offset, QTimeZone::OffsetName).toLatin1(),
997 match.size, QTimeZone::GenericTime };
998 }
999 }
1000 return {};
1001}
1002#endif // ICU or not
1003
1004QT_END_NAMESPACE
QList< QByteArrayView > ianaIdsForTerritory(QLocale::Territory territory)
#define tableLookup(table, member, sought, test)
#define localeRows(table, member)