11#include <private/qnumeric_p.h>
12#include <private/wcharhelpers_win_p.h>
16#include <private/qwinregistry_p.h>
22using namespace Qt::StringLiterals;
25
26
27
28
30#define MAX_KEY_LENGTH 255
40static const wchar_t tzRegPath[] = LR"(SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones)";
41static const wchar_t currTzRegPath[] = LR"(SYSTEM\CurrentControlSet\Control\TimeZoneInformation)";
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
87QDate msecsToDate(qint64 msecs)
89 qint64 jd = JULIAN_DAY_FOR_EPOCH;
91 if (msecs >= MSECS_PER_DAY || msecs <= -MSECS_PER_DAY) {
92 jd += msecs / MSECS_PER_DAY;
93 msecs %= MSECS_PER_DAY;
97 Q_ASSERT(msecs > -MSECS_PER_DAY);
101 return QDate::fromJulianDay(jd);
104bool equalSystemtime(
const SYSTEMTIME &t1,
const SYSTEMTIME &t2)
106 return (t1.wYear == t2.wYear
107 && t1.wMonth == t2.wMonth
108 && t1.wDay == t2.wDay
109 && t1.wDayOfWeek == t2.wDayOfWeek
110 && t1.wHour == t2.wHour
111 && t1.wMinute == t2.wMinute
112 && t1.wSecond == t2.wSecond
113 && t1.wMilliseconds == t2.wMilliseconds);
116bool equalTzi(
const TIME_ZONE_INFORMATION &tzi1,
const TIME_ZONE_INFORMATION &tzi2)
118 return(tzi1.Bias == tzi2.Bias
119 && tzi1.StandardBias == tzi2.StandardBias
120 && equalSystemtime(tzi1.StandardDate, tzi2.StandardDate)
121 && wcscmp(tzi1.StandardName, tzi2.StandardName) == 0
122 && tzi1.DaylightBias == tzi2.DaylightBias
123 && equalSystemtime(tzi1.DaylightDate, tzi2.DaylightDate)
124 && wcscmp(tzi1.DaylightName, tzi2.DaylightName) == 0);
127QWinTimeZonePrivate::QWinTransitionRule readRegistryRule(
const HKEY &key,
128 const wchar_t *value,
bool *ok)
131 QWinTimeZonePrivate::QWinTransitionRule rule;
133 DWORD tziSize =
sizeof(tzi);
134 if (RegQueryValueEx(key, value,
nullptr,
nullptr,
reinterpret_cast<BYTE *>(&tzi), &tziSize)
137 rule.standardTimeBias = tzi.Bias + tzi.StandardBias;
138 rule.daylightTimeBias = tzi.Bias + tzi.DaylightBias - rule.standardTimeBias;
139 rule.standardTimeRule = tzi.StandardDate;
140 rule.daylightTimeRule = tzi.DaylightDate;
146TIME_ZONE_INFORMATION getRegistryTzi(
const QByteArray &windowsId,
bool *ok)
149 TIME_ZONE_INFORMATION tzi;
150 REG_TZI_FORMAT regTzi;
151 DWORD regTziSize =
sizeof(regTzi);
152 const QString tziKeyPath = QString::fromWCharArray(tzRegPath) + u'\\'
153 + QString::fromUtf8(windowsId);
155 QWinRegistryKey key(HKEY_LOCAL_MACHINE, tziKeyPath);
157 DWORD size =
sizeof(tzi.DaylightName);
158 RegQueryValueEx(key, L"Dlt",
nullptr,
nullptr,
reinterpret_cast<LPBYTE>(tzi.DaylightName), &size);
160 size =
sizeof(tzi.StandardName);
161 RegQueryValueEx(key, L"Std",
nullptr,
nullptr,
reinterpret_cast<LPBYTE>(tzi.StandardName), &size);
163 if (RegQueryValueEx(key, L"TZI",
nullptr,
nullptr,
reinterpret_cast<BYTE *>(®Tzi), ®TziSize)
165 tzi.Bias = regTzi.Bias;
166 tzi.StandardBias = regTzi.StandardBias;
167 tzi.DaylightBias = regTzi.DaylightBias;
168 tzi.StandardDate = regTzi.StandardDate;
169 tzi.DaylightDate = regTzi.DaylightDate;
177bool isSameRule(
const QWinTimeZonePrivate::QWinTransitionRule &last,
178 const QWinTimeZonePrivate::QWinTransitionRule &rule)
185 return equalSystemtime(last.standardTimeRule, rule.standardTimeRule)
186 && equalSystemtime(last.daylightTimeRule, rule.daylightTimeRule)
187 && last.standardTimeBias == rule.standardTimeBias
188 && last.daylightTimeBias == rule.daylightTimeBias;
191QList<QByteArray> availableWindowsIds()
193 static const QList<QByteArray> cache = [] {
194 QList<QByteArray> list;
195 QWinRegistryKey key(HKEY_LOCAL_MACHINE, tzRegPath);
198 if (RegQueryInfoKey(key, 0, 0, 0, &idCount, 0, 0, 0, 0, 0, 0, 0) == ERROR_SUCCESS
200 for (DWORD i = 0; i < idCount; ++i) {
203 if (RegEnumKeyEx(key, i, buffer, &maxLen, 0, 0, 0, 0) == ERROR_SUCCESS)
204 list.append(QString::fromWCharArray(buffer).toUtf8());
213QByteArray windowsSystemZoneId()
216 const QString id = QWinRegistryKey(HKEY_LOCAL_MACHINE,
currTzRegPath)
217 .stringValue(L"TimeZoneKeyName");
223 TIME_ZONE_INFORMATION sysTzi;
224 GetTimeZoneInformation(&sysTzi);
226 const auto winIds = availableWindowsIds();
227 for (
const QByteArray &winId : winIds) {
228 if (equalTzi(getRegistryTzi(winId, &ok), sysTzi))
233 return QTimeZonePrivate::utcQByteArray();
236QDate calculateTransitionLocalDate(
const SYSTEMTIME &rule,
int year)
239 if (rule.wMonth == 0)
247 return QDate(rule.wYear, rule.wMonth, rule.wDay);
250 const int dayOfWeek = rule.wDayOfWeek == 0 ? 7 : rule.wDayOfWeek;
251 QDate date(year, rule.wMonth, 1);
252 Q_ASSERT(date.isValid());
254 int adjust = dayOfWeek - date.dayOfWeek();
259 adjust += (rule.wDay < 1 ? 1 : rule.wDay > 4 ? 5 : rule.wDay) * 7;
260 date = date.addDays(adjust);
262 if (date.month() != rule.wMonth) {
263 Q_ASSERT(rule.wDay > 4);
266 date = date.addDays(-7);
267 Q_ASSERT(date.month() == rule.wMonth);
273inline bool timeToMSecs(QDate date, QTime time, qint64 *msecs)
276 qint64 daySinceEpoch = date.toJulianDay() - JULIAN_DAY_FOR_EPOCH;
277 qint64 msInDay = time.msecsSinceStartOfDay();
278 if (daySinceEpoch < 0 && msInDay > 0) {
282 msInDay -= MSECS_PER_DAY;
284 return qMulOverflow(daySinceEpoch, std::integral_constant<qint64, MSECS_PER_DAY>(), &dayms)
285 || qAddOverflow(dayms, msInDay, msecs);
288qint64 calculateTransitionForYear(
const SYSTEMTIME &rule,
int year,
int bias)
293 const QDate date = calculateTransitionLocalDate(rule, year);
294 const QTime time = QTime(rule.wHour, rule.wMinute, rule.wSecond);
296 if (date.isValid() && time.isValid() && !timeToMSecs(date, time, &msecs)) {
300 return bias && qAddOverflow(msecs, qint64(bias) * 60000, &msecs)
301 ? (bias < 0 ? QTimeZonePrivate::minMSecs() : QTimeZonePrivate::maxMSecs())
302 : qMax(QTimeZonePrivate::minMSecs(), msecs);
304 return QTimeZonePrivate::invalidMSecs();
308bool isAtStartOfYear(
const SYSTEMTIME &transition,
int year)
311
312
313
314
315
316
317
318
319 return transition.wMonth == 1 && transition.wDay == 1
320 && (
QDate(year, 1, 1).dayOfWeek() - transition.wDayOfWeek) % 7 == 0
321 && transition.wHour == 0 && transition.wMinute == 0 && transition.wSecond == 0;
324struct TransitionTimePair
330 bool fakesDst =
false;
332 TransitionTimePair(
const QWinTimeZonePrivate::QWinTransitionRule &rule,
333 int year,
int oldYearOffset)
335 : std(calculateTransitionForYear(rule.standardTimeRule, year,
336 rule.standardTimeBias + rule.daylightTimeBias)),
338 dst(calculateTransitionForYear(rule.daylightTimeRule, year, rule.standardTimeBias))
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385 if (rule.standardTimeBias + rule.daylightTimeBias == oldYearOffset
386 && isAtStartOfYear(rule.daylightTimeRule, year)) {
387 dst = QTimeZonePrivate::invalidMSecs();
390 if (rule.standardTimeBias == oldYearOffset
391 && isAtStartOfYear(rule.standardTimeRule, year)) {
392 Q_ASSERT_X(!fakesDst,
"TransitionTimePair",
393 "Year with (DST bias zero and) both transitions fake !");
394 std = QTimeZonePrivate::invalidMSecs();
399 bool startsInDst()
const
403 return std != QTimeZonePrivate::invalidMSecs()
404 && (std < dst || dst == QTimeZonePrivate::invalidMSecs());
411 bool beforeInitialDst(
int year, qint64 millis)
const
413 return !fakesDst && (year == FIRST_DST_YEAR ? millis < dst : year < FIRST_DST_YEAR);
416 QTimeZonePrivate::Data ruleToData(
const QWinTimeZonePrivate::QWinTransitionRule &rule,
417 const QWinTimeZonePrivate *tzp,
bool isDst)
const
419 const auto type = isDst ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
420 auto time = isDst ? dst : std;
423 if (fakesDst && time == QTimeZonePrivate::invalidMSecs())
424 time = isDst ? std : dst;
425 return tzp->ruleToData(rule, time, type, fakesDst);
429int yearEndOffset(
const QWinTimeZonePrivate::QWinTransitionRule &rule,
int year)
432 int offset = rule.standardTimeBias;
436 TransitionTimePair pair(rule, year, offset);
437 if (pair.dst > pair.std)
438 offset += rule.daylightTimeBias;
442QLocale::Territory userTerritory()
444 const GEOID id = GetUserGeoID(GEOCLASS_NATION);
446 const int size = GetGeoInfo(id, GEO_ISO2, code, 3, 0);
447 return (size == 3) ? QLocalePrivate::codeToTerritory(QStringView(code, size))
448 : QLocale::AnyTerritory;
452int ruleIndexForYear(
const QList<QWinTimeZonePrivate::QWinTransitionRule> &rules,
int year)
454 if (rules.last().startYear <= year)
455 return rules.count() - 1;
457 if (rules.first().startYear > year)
461 int lo = 0, hi = rules.count();
464 while (lo + 1 < hi) {
465 const int mid = (lo + hi) / 2;
468 const int midYear = rules.at(mid).startYear;
471 else if (midYear < year)
482QWinTimeZonePrivate::QWinTimeZonePrivate()
489QWinTimeZonePrivate::QWinTimeZonePrivate(
const QByteArray &ianaId)
495QWinTimeZonePrivate::~QWinTimeZonePrivate()
499QWinTimeZonePrivate *QWinTimeZonePrivate::clone()
const
501 return new QWinTimeZonePrivate(*
this);
504void QWinTimeZonePrivate::init(
const QByteArray &ianaId)
506 if (ianaId.isEmpty()) {
507 m_windowsId = windowsSystemZoneId();
508 m_id = systemTimeZoneId();
510 m_windowsId = ianaIdToWindowsId(ianaId).toByteArray();
513 const auto initialYear = [](
const QWinTransitionRule &rule) {
518 return (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0
519 ? FIRST_DST_YEAR :
int(QDateTime::YearRange::First));
522 bool badMonth =
false;
523 if (!m_windowsId.isEmpty()) {
525 const QString baseKeyPath = QString::fromWCharArray(tzRegPath) + u'\\'
526 + QString::fromUtf8(m_windowsId);
527 QWinRegistryKey baseKey(HKEY_LOCAL_MACHINE, baseKeyPath);
528 if (baseKey.isValid()) {
530 m_displayName = baseKey.stringValue(L"Display");
531 m_standardName = baseKey.stringValue(L"Std");
532 m_daylightName = baseKey.stringValue(L"Dlt");
534 const QString dynamicKeyPath = baseKeyPath +
"\\Dynamic DST"_L1;
535 QWinRegistryKey dynamicKey(HKEY_LOCAL_MACHINE, dynamicKeyPath);
536 if (dynamicKey.isValid()) {
538 const int startYear = dynamicKey.value<
int>(L"FirstEntry").value_or(0);
539 const int endYear = dynamicKey.value<
int>(L"LastEntry").value_or(0);
540 for (
int year = startYear; year <= endYear; ++year) {
542 QWinTransitionRule rule = readRegistryRule(dynamicKey,
543 qt_castToWchar(QString::number(year)),
547 && (m_tranRules.isEmpty()
548 || !isSameRule(m_tranRules.last(), rule))) {
550 && (rule.standardTimeRule.wMonth == 0)
551 != (rule.daylightTimeRule.wMonth == 0)) {
553 qWarning(
"MS registry TZ API violated its wMonth constraint;"
554 "this may cause mistakes for %s from %d",
555 ianaId.constData(), year);
557 const TransitionTimePair pair(rule, year, rule.standardTimeBias);
560 = m_tranRules.size() || pair.fakesDst ? year : initialYear(rule);
561 m_tranRules.append(rule);
567 QWinTransitionRule rule = readRegistryRule(baseKey, L"TZI", &ruleOk);
569 rule.startYear = initialYear(rule);
570 m_tranRules.append(rule);
577 if (m_tranRules.size() == 0) {
580 m_displayName.clear();
581 }
else if (m_id.isEmpty()) {
582 m_id = m_standardName.toUtf8();
586QString QWinTimeZonePrivate::comment()
const
588 return m_displayName;
591QString QWinTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
592 QTimeZone::NameType nameType,
593 const QLocale &locale)
const
596 if (nameType == QTimeZone::LongName && locale == QLocale::system()) {
598 case QTimeZone::DaylightTime :
599 return m_daylightName;
600 case QTimeZone::GenericTime :
601 return m_displayName;
602 case QTimeZone::StandardTime :
603 return m_standardName;
607 return QTimeZonePrivate::displayName(timeType, nameType, locale);;
610QString QWinTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch)
const
612 return data(atMSecsSinceEpoch).abbreviation;
615int QWinTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch)
const
617 return data(atMSecsSinceEpoch).offsetFromUtc;
620int QWinTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch)
const
622 return data(atMSecsSinceEpoch).standardTimeOffset;
625int QWinTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch)
const
627 return data(atMSecsSinceEpoch).daylightTimeOffset;
630bool QWinTimeZonePrivate::hasDaylightTime()
const
637 for (
const QWinTransitionRule &rule : m_tranRules) {
638 if (rule.standardTimeRule.wMonth > 0 && rule.daylightTimeRule.wMonth > 0)
644bool QWinTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch)
const
646 return (data(atMSecsSinceEpoch).daylightTimeOffset != 0);
649QTimeZonePrivate::Data QWinTimeZonePrivate::data(qint64 forMSecsSinceEpoch)
const
651 int year = msecsToDate(forMSecsSinceEpoch).year();
652 for (
int ruleIndex = ruleIndexForYear(m_tranRules, year);
653 ruleIndex >= 0; --ruleIndex) {
654 const QWinTransitionRule &rule = m_tranRules.at(ruleIndex);
655 Q_ASSERT(ruleIndex == 0 || year >= rule.startYear);
656 if (year < rule.startYear
657 || !(rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0)) {
659 return ruleToData(rule, forMSecsSinceEpoch, QTimeZone::StandardTime);
662 int prior = year == 1 ? -1 : year - 1;
663 const int endYear = qMax(rule.startYear, prior);
664 while (year >= endYear) {
665 const int newYearOffset = (prior < rule.startYear && ruleIndex > 0)
666 ? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior)
667 : yearEndOffset(rule, prior);
668 const TransitionTimePair pair(rule, year, newYearOffset);
670 if (ruleIndex == 0 && pair.beforeInitialDst(year, forMSecsSinceEpoch)) {
674 }
else if (pair.std != invalidMSecs() && pair.std <= forMSecsSinceEpoch) {
675 isDst = pair.std < pair.dst && pair.dst <= forMSecsSinceEpoch;
676 }
else if (pair.dst != invalidMSecs() && pair.dst <= forMSecsSinceEpoch) {
680 prior = year == 1 ? -1 : year - 1;
683 return ruleToData(rule, forMSecsSinceEpoch,
684 isDst ? QTimeZone::DaylightTime : QTimeZone::StandardTime,
688 Q_ASSERT(year < rule.startYear);
695bool QWinTimeZonePrivate::hasTransitions()
const
701QTimeZonePrivate::Data QWinTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch)
const
703 int year = msecsToDate(afterMSecsSinceEpoch).year();
704 int newYearOffset = invalidSeconds();
705 for (
int ruleIndex = ruleIndexForYear(m_tranRules, year);
706 ruleIndex < m_tranRules.count(); ++ruleIndex) {
707 const QWinTransitionRule &rule = m_tranRules.at(ruleIndex);
709 if (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0) {
710 int prior = year == 1 ? -1 : year - 1;
711 if (newYearOffset == invalidSeconds()) {
714 newYearOffset = (prior < rule.startYear && ruleIndex > 0)
715 ? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior)
716 : yearEndOffset(rule, prior);
718 if (year < rule.startYear) {
722 TransitionTimePair pair(rule, rule.startYear, newYearOffset);
726 return pair.ruleToData(rule,
this, !(year > FIRST_DST_YEAR && pair.startsInDst()));
728 const int endYear = ruleIndex + 1 < m_tranRules.count()
729 ? qMin(m_tranRules.at(ruleIndex + 1).startYear, year + 2) : (year + 2);
730 while (year < endYear) {
731 const TransitionTimePair pair(rule, year, newYearOffset);
733 Q_ASSERT(invalidMSecs() <= afterMSecsSinceEpoch);
734 if (ruleIndex == 0 && pair.beforeInitialDst(year, afterMSecsSinceEpoch)) {
737 Q_ASSERT(year == FIRST_DST_YEAR);
743 }
else if (pair.std > afterMSecsSinceEpoch) {
744 isDst = pair.std > pair.dst && pair.dst > afterMSecsSinceEpoch;
745 }
else if (pair.dst > afterMSecsSinceEpoch) {
748 newYearOffset = rule.standardTimeBias;
749 if (pair.dst > pair.std)
750 newYearOffset += rule.daylightTimeBias;
753 year = year == -1 ? 1 : year + 1;
757 return pair.ruleToData(rule,
this, isDst);
764 if (newYearOffset == invalidSeconds())
765 newYearOffset = rule.standardTimeBias;
767 Q_ASSERT(newYearOffset == rule.standardTimeBias);
774QTimeZonePrivate::Data QWinTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch)
const
776 if (beforeMSecsSinceEpoch <= minMSecs())
779 int year = msecsToDate(beforeMSecsSinceEpoch).year();
780 for (
int ruleIndex = ruleIndexForYear(m_tranRules, year);
781 ruleIndex >= 0; --ruleIndex) {
782 const QWinTransitionRule &rule = m_tranRules.at(ruleIndex);
783 Q_ASSERT(ruleIndex == 0 || year >= rule.startYear);
785 if (year >= rule.startYear
786 && (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0)) {
787 int prior = year == 1 ? -1 : year - 1;
788 const int endYear = qMax(rule.startYear, prior);
789 while (year >= endYear) {
790 const int newYearOffset = (prior < rule.startYear && ruleIndex > 0)
791 ? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior)
792 : yearEndOffset(rule, prior);
793 const TransitionTimePair pair(rule, year, newYearOffset);
798 if (ruleIndex == 0 && pair.beforeInitialDst(year, beforeMSecsSinceEpoch - 1))
799 return ruleToData(rule, minMSecs(), QTimeZone::StandardTime,
false);
802 if (pair.std != invalidMSecs() && pair.std < beforeMSecsSinceEpoch) {
803 isDst = pair.std < pair.dst && pair.dst < beforeMSecsSinceEpoch;
804 }
else if (pair.dst != invalidMSecs() && pair.dst < beforeMSecsSinceEpoch) {
808 prior = year == 1 ? -1 : year - 1;
811 return pair.ruleToData(rule,
this, isDst);
814 }
else if (ruleIndex == 0) {
818 return ruleToData(rule, minMSecs(), QTimeZone::StandardTime,
false);
820 if (year >= rule.startYear) {
821 year = rule.startYear - 1;
830QByteArray QWinTimeZonePrivate::systemTimeZoneId()
const
832 const QLocale::Territory territory = userTerritory();
833 const QByteArray windowsId = windowsSystemZoneId();
834 QByteArrayView ianaId;
836 if (territory != QLocale::AnyTerritory)
837 ianaId = windowsIdToDefaultIanaId(windowsId, territory);
839 if (ianaId.isEmpty())
840 ianaId = windowsIdToDefaultIanaId(windowsId);
841 return ianaId.toByteArray();
844QList<QByteArray> QWinTimeZonePrivate::availableTimeZoneIds()
const
846 static const QList<QByteArray> cache = [] {
847 QList<QByteArray> result;
848 const auto winIds = availableWindowsIds();
849 for (
const QByteArray &winId : winIds)
850 result += windowsIdToIanaIds(winId);
851 return QTimeZonePrivate::uniqueSortedAliasPadded(std::move(result));
856QTimeZonePrivate::Data QWinTimeZonePrivate::ruleToData(
const QWinTransitionRule &rule,
857 qint64 atMSecsSinceEpoch,
858 QTimeZone::TimeType type,
862 tran.atMSecsSinceEpoch = atMSecsSinceEpoch;
863 tran.standardTimeOffset = rule.standardTimeBias * -60;
865 tran.daylightTimeOffset = 0;
867 if (type == QTimeZone::DaylightTime)
868 tran.standardTimeOffset += rule.daylightTimeBias * -60;
869 }
else if (type == QTimeZone::DaylightTime) {
870 tran.daylightTimeOffset = rule.daylightTimeBias * -60;
872 tran.daylightTimeOffset = 0;
874 tran.offsetFromUtc = tran.standardTimeOffset + tran.daylightTimeOffset;
875 tran.abbreviation = localeName(atMSecsSinceEpoch, tran.offsetFromUtc,
876 type, QTimeZone::ShortName, QLocale::system());
\inmodule QtCore \reentrant
QT_REQUIRE_CONFIG(timezone_locale)
constexpr qint64 JULIAN_DAY_FOR_EPOCH
constexpr int FIRST_DST_YEAR
static const wchar_t tzRegPath[]
static const wchar_t currTzRegPath[]
constexpr qint64 MSECS_PER_DAY