/**************************************************************************** ** ** Copyright (C) 2013 John Layt ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtCore module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qtimezone.h" #include "qtimezoneprivate_p.h" #include "qdatetime.h" #include "qdebug.h" #include QT_BEGIN_NAMESPACE #ifndef Q_OS_WINRT // The registry-based timezone backend is not available on WinRT, which falls back to equivalent APIs. #define QT_USE_REGISTRY_TIMEZONE 1 #endif /* Private Windows system implementation */ #define MAX_KEY_LENGTH 255 #define FILETIME_UNIX_EPOCH Q_UINT64_C(116444736000000000) // MSDN home page for Time support // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724962%28v=vs.85%29.aspx // For Windows XP and later refer to MSDN docs on TIME_ZONE_INFORMATION structure // http://msdn.microsoft.com/en-gb/library/windows/desktop/ms725481%28v=vs.85%29.aspx // Vista introduced support for historic data, see MSDN docs on DYNAMIC_TIME_ZONE_INFORMATION // http://msdn.microsoft.com/en-gb/library/windows/desktop/ms724253%28v=vs.85%29.aspx #ifdef QT_USE_REGISTRY_TIMEZONE static const char tzRegPath[] = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones"; static const char currTzRegPath[] = "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation"; #endif enum { MIN_YEAR = -292275056, MAX_YEAR = 292278994, MSECS_PER_DAY = 86400000, TIME_T_MAX = 2145916799, // int maximum 2037-12-31T23:59:59 UTC JULIAN_DAY_FOR_EPOCH = 2440588 // result of julianDayFromDate(1970, 1, 1) }; // Copied from MSDN, see above for link typedef struct _REG_TZI_FORMAT { LONG Bias; LONG StandardBias; LONG DaylightBias; SYSTEMTIME StandardDate; SYSTEMTIME DaylightDate; } REG_TZI_FORMAT; namespace { // Fast and reliable conversion from msecs to date for all values // Adapted from QDateTime msecsToDate QDate msecsToDate(qint64 msecs) { qint64 jd = JULIAN_DAY_FOR_EPOCH; if (qAbs(msecs) >= MSECS_PER_DAY) { jd += (msecs / MSECS_PER_DAY); msecs %= MSECS_PER_DAY; } if (msecs < 0) { qint64 ds = MSECS_PER_DAY - msecs - 1; jd -= ds / MSECS_PER_DAY; } return QDate::fromJulianDay(jd); } bool equalSystemtime(const SYSTEMTIME &t1, const SYSTEMTIME &t2) { return (t1.wYear == t2.wYear && t1.wMonth == t2.wMonth && t1.wDay == t2.wDay && t1.wDayOfWeek == t2.wDayOfWeek && t1.wHour == t2.wHour && t1.wMinute == t2.wMinute && t1.wSecond == t2.wSecond && t1.wMilliseconds == t2.wMilliseconds); } bool equalTzi(const TIME_ZONE_INFORMATION &tzi1, const TIME_ZONE_INFORMATION &tzi2) { return(tzi1.Bias == tzi2.Bias && tzi1.StandardBias == tzi2.StandardBias && equalSystemtime(tzi1.StandardDate, tzi2.StandardDate) && wcscmp(tzi1.StandardName, tzi2.StandardName) == 0 && tzi1.DaylightBias == tzi2.DaylightBias && equalSystemtime(tzi1.DaylightDate, tzi2.DaylightDate) && wcscmp(tzi1.DaylightName, tzi2.DaylightName) == 0); } #ifdef QT_USE_REGISTRY_TIMEZONE bool openRegistryKey(const QString &keyPath, HKEY *key) { return RegOpenKeyEx(HKEY_LOCAL_MACHINE, reinterpret_cast(keyPath.utf16()), 0, KEY_READ, key) == ERROR_SUCCESS; } QString readRegistryString(const HKEY &key, const wchar_t *value) { wchar_t buffer[MAX_PATH] = {0}; DWORD size = sizeof(wchar_t) * MAX_PATH; RegQueryValueEx(key, value, nullptr, nullptr, reinterpret_cast(buffer), &size); return QString::fromWCharArray(buffer); } int readRegistryValue(const HKEY &key, const wchar_t *value) { DWORD buffer; DWORD size = sizeof(buffer); RegQueryValueEx(key, value, nullptr, nullptr, reinterpret_cast(&buffer), &size); return buffer; } QWinTimeZonePrivate::QWinTransitionRule readRegistryRule(const HKEY &key, const wchar_t *value, bool *ok) { *ok = false; QWinTimeZonePrivate::QWinTransitionRule rule; REG_TZI_FORMAT tzi; DWORD tziSize = sizeof(tzi); if (RegQueryValueEx(key, value, nullptr, nullptr, reinterpret_cast(&tzi), &tziSize) == ERROR_SUCCESS) { rule.startYear = 0; rule.standardTimeBias = tzi.Bias + tzi.StandardBias; rule.daylightTimeBias = tzi.Bias + tzi.DaylightBias - rule.standardTimeBias; rule.standardTimeRule = tzi.StandardDate; rule.daylightTimeRule = tzi.DaylightDate; *ok = true; } return rule; } TIME_ZONE_INFORMATION getRegistryTzi(const QByteArray &windowsId, bool *ok) { *ok = false; TIME_ZONE_INFORMATION tzi; REG_TZI_FORMAT regTzi; DWORD regTziSize = sizeof(regTzi); HKEY key = NULL; const QString tziKeyPath = QString::fromUtf8(tzRegPath) + QLatin1Char('\\') + QString::fromUtf8(windowsId); if (openRegistryKey(tziKeyPath, &key)) { DWORD size = sizeof(tzi.DaylightName); RegQueryValueEx(key, L"Dlt", nullptr, nullptr, reinterpret_cast(tzi.DaylightName), &size); size = sizeof(tzi.StandardName); RegQueryValueEx(key, L"Std", nullptr, nullptr, reinterpret_cast(tzi.StandardName), &size); if (RegQueryValueEx(key, L"TZI", nullptr, nullptr, reinterpret_cast(®Tzi), ®TziSize) == ERROR_SUCCESS) { tzi.Bias = regTzi.Bias; tzi.StandardBias = regTzi.StandardBias; tzi.DaylightBias = regTzi.DaylightBias; tzi.StandardDate = regTzi.StandardDate; tzi.DaylightDate = regTzi.DaylightDate; *ok = true; } RegCloseKey(key); } return tzi; } #else // QT_USE_REGISTRY_TIMEZONE struct QWinDynamicTimeZone { QString standardName; QString daylightName; QString timezoneName; qint32 bias; bool daylightTime; }; typedef QHash QWinRTTimeZoneHash; Q_GLOBAL_STATIC(QWinRTTimeZoneHash, gTimeZones) void enumerateTimeZones() { DYNAMIC_TIME_ZONE_INFORMATION dtzInfo; quint32 index = 0; QString prevTimeZoneKeyName; while (SUCCEEDED(EnumDynamicTimeZoneInformation(index++, &dtzInfo))) { QWinDynamicTimeZone item; item.timezoneName = QString::fromWCharArray(dtzInfo.TimeZoneKeyName); // As soon as key name repeats, break. Some systems continue to always // return the last item independent of index being out of range if (item.timezoneName == prevTimeZoneKeyName) break; item.standardName = QString::fromWCharArray(dtzInfo.StandardName); item.daylightName = QString::fromWCharArray(dtzInfo.DaylightName); item.daylightTime = !dtzInfo.DynamicDaylightTimeDisabled; item.bias = dtzInfo.Bias; gTimeZones->insert(item.timezoneName.toUtf8(), item); prevTimeZoneKeyName = item.timezoneName; } } DYNAMIC_TIME_ZONE_INFORMATION dynamicInfoForId(const QByteArray &windowsId) { DYNAMIC_TIME_ZONE_INFORMATION dtzInfo; quint32 index = 0; QString prevTimeZoneKeyName; while (SUCCEEDED(EnumDynamicTimeZoneInformation(index++, &dtzInfo))) { const QString timeZoneName = QString::fromWCharArray(dtzInfo.TimeZoneKeyName); if (timeZoneName == QLatin1String(windowsId)) break; if (timeZoneName == prevTimeZoneKeyName) break; prevTimeZoneKeyName = timeZoneName; } return dtzInfo; } QWinTimeZonePrivate::QWinTransitionRule readDynamicRule(DYNAMIC_TIME_ZONE_INFORMATION &dtzi, int year, bool *ok) { TIME_ZONE_INFORMATION tzi; QWinTimeZonePrivate::QWinTransitionRule rule; *ok = GetTimeZoneInformationForYear(year, &dtzi, &tzi); if (*ok) { rule.startYear = 0; rule.standardTimeBias = tzi.Bias + tzi.StandardBias; rule.daylightTimeBias = tzi.Bias + tzi.DaylightBias - rule.standardTimeBias; rule.standardTimeRule = tzi.StandardDate; rule.daylightTimeRule = tzi.DaylightDate; } return rule; } #endif // QT_USE_REGISTRY_TIMEZONE bool isSameRule(const QWinTimeZonePrivate::QWinTransitionRule &last, const QWinTimeZonePrivate::QWinTransitionRule &rule) { // In particular, when this is true and either wYear is 0, so is the other; // so if one rule is recurrent and they're equal, so is the other. If // either rule *isn't* recurrent, it has non-0 wYear which shall be // different from the other's. Note that we don't compare .startYear, since // that will always be different. return equalSystemtime(last.standardTimeRule, rule.standardTimeRule) && equalSystemtime(last.daylightTimeRule, rule.daylightTimeRule) && last.standardTimeBias == rule.standardTimeBias && last.daylightTimeBias == rule.daylightTimeBias; } QList availableWindowsIds() { #ifdef QT_USE_REGISTRY_TIMEZONE // TODO Consider caching results in a global static, very unlikely to change. QList list; HKEY key = NULL; if (openRegistryKey(QString::fromUtf8(tzRegPath), &key)) { DWORD idCount = 0; if (RegQueryInfoKey(key, 0, 0, 0, &idCount, 0, 0, 0, 0, 0, 0, 0) == ERROR_SUCCESS && idCount > 0) { for (DWORD i = 0; i < idCount; ++i) { DWORD maxLen = MAX_KEY_LENGTH; TCHAR buffer[MAX_KEY_LENGTH]; if (RegEnumKeyEx(key, i, buffer, &maxLen, 0, 0, 0, 0) == ERROR_SUCCESS) list.append(QString::fromWCharArray(buffer).toUtf8()); } } RegCloseKey(key); } return list; #else // QT_USE_REGISTRY_TIMEZONE if (gTimeZones->isEmpty()) enumerateTimeZones(); return gTimeZones->keys(); #endif // QT_USE_REGISTRY_TIMEZONE } QByteArray windowsSystemZoneId() { #ifdef QT_USE_REGISTRY_TIMEZONE // On Vista and later is held in the value TimeZoneKeyName in key currTzRegPath QString id; HKEY key = NULL; QString tziKeyPath = QString::fromUtf8(currTzRegPath); if (openRegistryKey(tziKeyPath, &key)) { id = readRegistryString(key, L"TimeZoneKeyName"); RegCloseKey(key); if (!id.isEmpty()) return std::move(id).toUtf8(); } // On XP we have to iterate over the zones until we find a match on // names/offsets with the current data TIME_ZONE_INFORMATION sysTzi; GetTimeZoneInformation(&sysTzi); bool ok = false; const auto winIds = availableWindowsIds(); for (const QByteArray &winId : winIds) { if (equalTzi(getRegistryTzi(winId, &ok), sysTzi)) return winId; } #else // QT_USE_REGISTRY_TIMEZONE DYNAMIC_TIME_ZONE_INFORMATION dtzi; if (SUCCEEDED(GetDynamicTimeZoneInformation(&dtzi))) return QString::fromWCharArray(dtzi.TimeZoneKeyName).toLocal8Bit(); #endif // QT_USE_REGISTRY_TIMEZONE // If we can't determine the current ID use UTC return QTimeZonePrivate::utcQByteArray(); } QDate calculateTransitionLocalDate(const SYSTEMTIME &rule, int year) { // If month is 0 then there is no date if (rule.wMonth == 0) return QDate(); // Interpret SYSTEMTIME according to the slightly quirky rules in: // https://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx // If the year is set, the rule gives an absolute date: if (rule.wYear) return QDate(rule.wYear, rule.wMonth, rule.wDay); // Otherwise, the rule date is annual and relative: const int dayOfWeek = rule.wDayOfWeek == 0 ? 7 : rule.wDayOfWeek; QDate date(year, rule.wMonth, 1); // How many days before was last dayOfWeek before target month ? int adjust = dayOfWeek - date.dayOfWeek(); // -6 <= adjust < 7 if (adjust >= 0) // Ensure -7 <= adjust < 0: adjust -= 7; // Normally, wDay is day-within-month; but here it is 1 for the first // of the given dayOfWeek in the month, through 4 for the fourth or ... adjust += (rule.wDay < 1 ? 1 : rule.wDay > 4 ? 5 : rule.wDay) * 7; date = date.addDays(adjust); // ... 5 for the last; so back up by weeks to get within the month: if (date.month() != rule.wMonth) { Q_ASSERT(rule.wDay > 4); // (Note that, with adjust < 0, date <= 28th of our target month // is guaranteed when wDay <= 4, or after our first -7 here.) date = date.addDays(-7); Q_ASSERT(date.month() == rule.wMonth); } return date; } // Converts a date/time value into msecs inline qint64 timeToMSecs(const QDate &date, const QTime &time) { return ((date.toJulianDay() - JULIAN_DAY_FOR_EPOCH) * MSECS_PER_DAY) + time.msecsSinceStartOfDay(); } qint64 calculateTransitionForYear(const SYSTEMTIME &rule, int year, int bias) { // TODO Consider caching the calculated values - i.e. replace SYSTEMTIME in // WinTransitionRule; do this in init() once and store the results. const QDate date = calculateTransitionLocalDate(rule, year); const QTime time = QTime(rule.wHour, rule.wMinute, rule.wSecond); if (date.isValid() && time.isValid()) return timeToMSecs(date, time) + bias * 60000; return QTimeZonePrivate::invalidMSecs(); } struct TransitionTimePair { // Transition times after the epoch, in ms: qint64 std, dst; // If either is invalidMSecs(), which shall then be < the other, there is no // DST and the other describes a change in actual standard offset. TransitionTimePair(const QWinTimeZonePrivate::QWinTransitionRule &rule, int year, int oldYearOffset) // The local time in Daylight Time of the switch to Standard Time : std(calculateTransitionForYear(rule.standardTimeRule, year, rule.standardTimeBias + rule.daylightTimeBias)), // The local time in Standard Time of the switch to Daylight Time dst(calculateTransitionForYear(rule.daylightTimeRule, year, rule.standardTimeBias)) { /* Check for potential "fake DST", used by MS's APIs because the TIME_ZONE_INFORMATION spec either expresses no transitions in the year, or expresses a transition of each kind, even if standard time did change in a year with no DST. We've seen year-start fake-DST (whose offset matches prior standard offset, in which the previous year ended); and conjecture that similar might be used at a year-end. (This might be used for a southern-hemisphere zone, where the start of the year usually is in DST, when applicable.) Note that, here, wDay identifies an instance of a given day-of-week in the month, with 5 meaning last. Either the alleged standardTimeRule or the alleged daylightTimeRule may be faked; either way, the transition is actually a change to the current standard offset; but the unfaked half of the rule contains the useful bias data, so we have to go along with its lies. Example: Russia/Moscow Format: -bias +( -stdBias, stdDate | -dstBias, dstDate ) notes Last year of DST, 2010: 180 +( 0, 0-10-5 3:0 | 60, 0-3-5 2:0 ) normal DST Zone change in 2011: 180 +( 0, 0-1-1 0:0 | 60 0-3-5 2:0 ) fake DST at transition Fixed standard in 2012: 240 +( 0, 0-0-0 0:0 | 60, 0-0-0 0:0 ) standard time years Zone change in 2014: 180 +( 0, 0-10-5 2:0 | 60, 0-1-1 0:0 ) fake DST at year-start The last of these is missing on Win7 VMs (too old to know about it). */ if (rule.daylightTimeRule.wMonth == 1 && rule.daylightTimeRule.wDay == 1) { // Fake "DST transition" at start of year producing the same offset as // previous year ended in. if (rule.standardTimeBias + rule.daylightTimeBias == oldYearOffset) dst = QTimeZonePrivate::invalidMSecs(); } else if (rule.daylightTimeRule.wMonth == 12 && rule.daylightTimeRule.wDay > 3) { // Similar, conjectured, for end of year, not changing offset. if (rule.daylightTimeBias == 0) dst = QTimeZonePrivate::invalidMSecs(); } if (rule.standardTimeRule.wMonth == 1 && rule.standardTimeRule.wDay == 1) { // Fake "transition out of DST" at start of year producing the same // offset as previous year ended in. if (rule.standardTimeBias == oldYearOffset) std = QTimeZonePrivate::invalidMSecs(); } else if (rule.standardTimeRule.wMonth == 12 && rule.standardTimeRule.wDay > 3) { // Similar, conjectured, for end of year, not changing offset. if (rule.daylightTimeBias == 0) std = QTimeZonePrivate::invalidMSecs(); } } bool fakesDst() const { return std == QTimeZonePrivate::invalidMSecs() || dst == QTimeZonePrivate::invalidMSecs(); } }; int yearEndOffset(const QWinTimeZonePrivate::QWinTransitionRule &rule, int year) { int offset = rule.standardTimeBias; // Only needed to help another TransitionTimePair work out year + 1's start // offset; and the oldYearOffset we use only affects an alleged transition // at the *start* of this year, so it doesn't matter if we guess wrong here: TransitionTimePair pair(rule, year, offset); if (pair.dst > pair.std) offset += rule.daylightTimeBias; return offset; } QLocale::Country userCountry() { const GEOID id = GetUserGeoID(GEOCLASS_NATION); wchar_t code[3]; const int size = GetGeoInfo(id, GEO_ISO2, code, 3, 0); return (size == 3) ? QLocalePrivate::codeToCountry(QStringView(code, size)) : QLocale::AnyCountry; } // Index of last rule in rules with .startYear <= year: int ruleIndexForYear(const QList &rules, int year) { if (rules.last().startYear <= year) return rules.count() - 1; // We don't have a rule for before the first, but the first is the best we can offer: if (rules.first().startYear > year) return 0; // Otherwise, use binary chop: int lo = 0, hi = rules.count(); // invariant: rules[i].startYear <= year < rules[hi].startYear // subject to treating rules[rules.count()] as "off the end of time" while (lo + 1 < hi) { const int mid = (lo + hi) / 2; // lo + 2 <= hi, so lo + 1 <= mid <= hi - 1, so lo < mid < hi // In particular, mid < rules.count() const int midYear = rules.at(mid).startYear; if (midYear > year) hi = mid; else if (midYear < year) lo = mid; else // No two rules have the same startYear: return mid; } return lo; } } // anonymous namespace // Create the system default time zone QWinTimeZonePrivate::QWinTimeZonePrivate() : QTimeZonePrivate() { init(QByteArray()); } // Create a named time zone QWinTimeZonePrivate::QWinTimeZonePrivate(const QByteArray &ianaId) : QTimeZonePrivate() { init(ianaId); } QWinTimeZonePrivate::QWinTimeZonePrivate(const QWinTimeZonePrivate &other) : QTimeZonePrivate(other), m_windowsId(other.m_windowsId), m_displayName(other.m_displayName), m_standardName(other.m_standardName), m_daylightName(other.m_daylightName), m_tranRules(other.m_tranRules) { } QWinTimeZonePrivate::~QWinTimeZonePrivate() { } QWinTimeZonePrivate *QWinTimeZonePrivate::clone() const { return new QWinTimeZonePrivate(*this); } void QWinTimeZonePrivate::init(const QByteArray &ianaId) { if (ianaId.isEmpty()) { m_windowsId = windowsSystemZoneId(); m_id = systemTimeZoneId(); } else { m_windowsId = ianaIdToWindowsId(ianaId); m_id = ianaId; } bool badMonth = false; // Only warn once per zone, if at all. if (!m_windowsId.isEmpty()) { #ifdef QT_USE_REGISTRY_TIMEZONE // Open the base TZI for the time zone HKEY baseKey = NULL; const QString baseKeyPath = QString::fromUtf8(tzRegPath) + QLatin1Char('\\') + QString::fromUtf8(m_windowsId); if (openRegistryKey(baseKeyPath, &baseKey)) { // Load the localized names m_displayName = readRegistryString(baseKey, L"Display"); m_standardName = readRegistryString(baseKey, L"Std"); m_daylightName = readRegistryString(baseKey, L"Dlt"); // On Vista and later the optional dynamic key holds historic data const QString dynamicKeyPath = baseKeyPath + QLatin1String("\\Dynamic DST"); HKEY dynamicKey = NULL; if (openRegistryKey(dynamicKeyPath, &dynamicKey)) { // Find out the start and end years stored, then iterate over them int startYear = readRegistryValue(dynamicKey, L"FirstEntry"); int endYear = readRegistryValue(dynamicKey, L"LastEntry"); for (int year = startYear; year <= endYear; ++year) { bool ruleOk; QWinTransitionRule rule = readRegistryRule(dynamicKey, reinterpret_cast(QString::number(year).utf16()), &ruleOk); if (ruleOk // Don't repeat a recurrent rule: && (m_tranRules.isEmpty() || !isSameRule(m_tranRules.last(), rule))) { if (!badMonth && (rule.standardTimeRule.wMonth == 0) != (rule.daylightTimeRule.wMonth == 0)) { badMonth = true; qWarning("MS registry TZ API violated its wMonth constraint;" "this may cause mistakes for %s from %d", ianaId.constData(), year); } rule.startYear = m_tranRules.isEmpty() ? MIN_YEAR : year; m_tranRules.append(rule); } } RegCloseKey(dynamicKey); } else { // No dynamic data so use the base data bool ruleOk; QWinTransitionRule rule = readRegistryRule(baseKey, L"TZI", &ruleOk); rule.startYear = MIN_YEAR; if (ruleOk) m_tranRules.append(rule); } RegCloseKey(baseKey); } #else // QT_USE_REGISTRY_TIMEZONE if (gTimeZones->isEmpty()) enumerateTimeZones(); QWinRTTimeZoneHash::const_iterator it = gTimeZones->find(m_windowsId); if (it != gTimeZones->constEnd()) { m_displayName = it->timezoneName; m_standardName = it->standardName; m_daylightName = it->daylightName; DWORD firstYear = 0; DWORD lastYear = 0; DYNAMIC_TIME_ZONE_INFORMATION dtzi = dynamicInfoForId(m_windowsId); if (GetDynamicTimeZoneInformationEffectiveYears(&dtzi, &firstYear, &lastYear) == ERROR_SUCCESS && firstYear < lastYear) { for (DWORD year = firstYear; year <= lastYear; ++year) { bool ok = false; QWinTransitionRule rule = readDynamicRule(dtzi, year, &ok); if (ok // Don't repeat a recurrent rule && (m_tranRules.isEmpty() || !isSameRule(m_tranRules.last(), rule))) { if (!badMonth && (rule.standardTimeRule.wMonth == 0) != (rule.daylightTimeRule.wMonth == 0)) { badMonth = true; qWarning("MS dynamic TZ API violated its wMonth constraint;" "this may cause mistakes for %s from %d", ianaId.constData(), year); } rule.startYear = m_tranRules.isEmpty() ? MIN_YEAR : year; m_tranRules.append(rule); } } } else { // At least try to get the non-dynamic data: dtzi.DynamicDaylightTimeDisabled = false; bool ok = false; QWinTransitionRule rule = readDynamicRule(dtzi, 1970, &ok); if (ok) { rule.startYear = MIN_YEAR; m_tranRules.append(rule); } } } #endif // QT_USE_REGISTRY_TIMEZONE } // If there are no rules then we failed to find a windowsId or any tzi info if (m_tranRules.size() == 0) { m_id.clear(); m_windowsId.clear(); m_displayName.clear(); } } QString QWinTimeZonePrivate::comment() const { return m_displayName; } QString QWinTimeZonePrivate::displayName(QTimeZone::TimeType timeType, QTimeZone::NameType nameType, const QLocale &locale) const { // TODO Registry holds MUI keys, should be able to look up translations? Q_UNUSED(locale); if (nameType == QTimeZone::OffsetName) { const QWinTransitionRule &rule = m_tranRules.at(ruleIndexForYear(m_tranRules, QDate::currentDate().year())); if (timeType == QTimeZone::DaylightTime) return isoOffsetFormat((rule.standardTimeBias + rule.daylightTimeBias) * -60); else return isoOffsetFormat((rule.standardTimeBias) * -60); } switch (timeType) { case QTimeZone::DaylightTime : return m_daylightName; case QTimeZone::GenericTime : return m_displayName; case QTimeZone::StandardTime : return m_standardName; } return m_standardName; } QString QWinTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const { return data(atMSecsSinceEpoch).abbreviation; } int QWinTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const { return data(atMSecsSinceEpoch).offsetFromUtc; } int QWinTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const { return data(atMSecsSinceEpoch).standardTimeOffset; } int QWinTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const { return data(atMSecsSinceEpoch).daylightTimeOffset; } bool QWinTimeZonePrivate::hasDaylightTime() const { return hasTransitions(); } bool QWinTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const { return (data(atMSecsSinceEpoch).daylightTimeOffset != 0); } QTimeZonePrivate::Data QWinTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const { int year = msecsToDate(forMSecsSinceEpoch).year(); for (int ruleIndex = ruleIndexForYear(m_tranRules, year); ruleIndex >= 0; --ruleIndex) { const QWinTransitionRule &rule = m_tranRules.at(ruleIndex); // Does this rule's period include any transition at all ? if (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0) { const int endYear = qMax(rule.startYear, year - 1); while (year >= endYear) { const int newYearOffset = (year <= rule.startYear && ruleIndex > 0) ? yearEndOffset(m_tranRules.at(ruleIndex - 1), year - 1) : yearEndOffset(rule, year - 1); const TransitionTimePair pair(rule, year, newYearOffset); bool isDst = false; if (pair.std != invalidMSecs() && pair.std <= forMSecsSinceEpoch) { isDst = pair.std < pair.dst && pair.dst <= forMSecsSinceEpoch; } else if (pair.dst != invalidMSecs() && pair.dst <= forMSecsSinceEpoch) { isDst = true; } else { --year; // Try an earlier year for this rule (once). continue; } return ruleToData(rule, forMSecsSinceEpoch, isDst ? QTimeZone::DaylightTime : QTimeZone::StandardTime, pair.fakesDst()); } // Fell off start of rule, try previous rule. } else { // No transition, no DST, use the year's standard time. return ruleToData(rule, forMSecsSinceEpoch, QTimeZone::StandardTime); } if (year >= rule.startYear) year = rule.startYear - 1; // Seek last transition in new rule. } // We don't have relevant data :-( return invalidData(); } bool QWinTimeZonePrivate::hasTransitions() const { for (const QWinTransitionRule &rule : m_tranRules) { if (rule.standardTimeRule.wMonth > 0 && rule.daylightTimeRule.wMonth > 0) return true; } return false; } QTimeZonePrivate::Data QWinTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const { int year = msecsToDate(afterMSecsSinceEpoch).year(); for (int ruleIndex = ruleIndexForYear(m_tranRules, year); ruleIndex < m_tranRules.count(); ++ruleIndex) { const QWinTransitionRule &rule = m_tranRules.at(ruleIndex); // Does this rule's period include any transition at all ? if (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0) { if (year < rule.startYear) year = rule.startYear; // Seek first transition in this rule. const int endYear = ruleIndex + 1 < m_tranRules.count() ? qMin(m_tranRules.at(ruleIndex + 1).startYear, year + 2) : (year + 2); int newYearOffset = (year <= rule.startYear && ruleIndex > 0) ? yearEndOffset(m_tranRules.at(ruleIndex - 1), year - 1) : yearEndOffset(rule, year - 1); while (year < endYear) { const TransitionTimePair pair(rule, year, newYearOffset); bool isDst = false; Q_ASSERT(invalidMSecs() <= afterMSecsSinceEpoch); // invalid is min qint64 if (pair.std > afterMSecsSinceEpoch) { isDst = pair.std > pair.dst && pair.dst > afterMSecsSinceEpoch; } else if (pair.dst > afterMSecsSinceEpoch) { isDst = true; } else { newYearOffset = rule.standardTimeBias; if (pair.dst > pair.std) newYearOffset += rule.daylightTimeBias; ++year; // Try a later year for this rule (once). continue; } if (isDst) return ruleToData(rule, pair.dst, QTimeZone::DaylightTime, pair.fakesDst()); return ruleToData(rule, pair.std, QTimeZone::StandardTime, pair.fakesDst()); } // Fell off end of rule, try next rule. } // else: no transition during rule's period } // Apparently no transition after the given time: return invalidData(); } QTimeZonePrivate::Data QWinTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const { const qint64 startOfTime = invalidMSecs() + 1; if (beforeMSecsSinceEpoch <= startOfTime) return invalidData(); int year = msecsToDate(beforeMSecsSinceEpoch).year(); for (int ruleIndex = ruleIndexForYear(m_tranRules, year); ruleIndex >= 0; --ruleIndex) { const QWinTransitionRule &rule = m_tranRules.at(ruleIndex); // Does this rule's period include any transition at all ? if (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0) { const int endYear = qMax(rule.startYear, year - 1); while (year >= endYear) { const int newYearOffset = (year <= rule.startYear && ruleIndex > 0) ? yearEndOffset(m_tranRules.at(ruleIndex - 1), year - 1) : yearEndOffset(rule, year - 1); const TransitionTimePair pair(rule, year, newYearOffset); bool isDst = false; if (pair.std != invalidMSecs() && pair.std < beforeMSecsSinceEpoch) { isDst = pair.std < pair.dst && pair.dst < beforeMSecsSinceEpoch; } else if (pair.dst != invalidMSecs() && pair.dst < beforeMSecsSinceEpoch) { isDst = true; } else { --year; // Try an earlier year for this rule (once). continue; } if (isDst) return ruleToData(rule, pair.dst, QTimeZone::DaylightTime, pair.fakesDst()); return ruleToData(rule, pair.std, QTimeZone::StandardTime, pair.fakesDst()); } // Fell off start of rule, try previous rule. } else if (ruleIndex == 0) { // Treat a no-transition first rule as a transition at the start of // time, so that a scan through all rules *does* see it as the first // rule: return ruleToData(rule, startOfTime, QTimeZone::StandardTime, false); } // else: no transition during rule's period if (year >= rule.startYear) year = rule.startYear - 1; // Seek last transition in new rule } // Apparently no transition before the given time: return invalidData(); } QByteArray QWinTimeZonePrivate::systemTimeZoneId() const { const QLocale::Country country = userCountry(); const QByteArray windowsId = windowsSystemZoneId(); QByteArray ianaId; // If we have a real country, then try get a specific match for that country if (country != QLocale::AnyCountry) ianaId = windowsIdToDefaultIanaId(windowsId, country); // If we don't have a real country, or there wasn't a specific match, try the global default if (ianaId.isEmpty()) { ianaId = windowsIdToDefaultIanaId(windowsId); // If no global default then probably an unknown Windows ID so return UTC if (ianaId.isEmpty()) return utcQByteArray(); } return ianaId; } QList QWinTimeZonePrivate::availableTimeZoneIds() const { QList result; const auto winIds = availableWindowsIds(); for (const QByteArray &winId : winIds) result += windowsIdToIanaIds(winId); std::sort(result.begin(), result.end()); result.erase(std::unique(result.begin(), result.end()), result.end()); return result; } QTimeZonePrivate::Data QWinTimeZonePrivate::ruleToData(const QWinTransitionRule &rule, qint64 atMSecsSinceEpoch, QTimeZone::TimeType type, bool fakeDst) const { Data tran = invalidData(); tran.atMSecsSinceEpoch = atMSecsSinceEpoch; tran.standardTimeOffset = rule.standardTimeBias * -60; if (fakeDst) { tran.daylightTimeOffset = 0; tran.abbreviation = m_standardName; // Rule may claim we're in DST when it's actually a standard time change: if (type == QTimeZone::DaylightTime) tran.standardTimeOffset += rule.daylightTimeBias * -60; } else if (type == QTimeZone::DaylightTime) { tran.daylightTimeOffset = rule.daylightTimeBias * -60; tran.abbreviation = m_daylightName; } else { tran.daylightTimeOffset = 0; tran.abbreviation = m_standardName; } tran.offsetFromUtc = tran.standardTimeOffset + tran.daylightTimeOffset; return tran; } QT_END_NAMESPACE