908 lines
33 KiB
C++
908 lines
33 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2013 John Layt <jlayt@kde.org>
|
|
** 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 "qtimezoneprivate_data_p.h"
|
|
|
|
#include <qdatastream.h>
|
|
#include <qdebug.h>
|
|
|
|
#include <algorithm>
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
|
|
/*
|
|
Static utilities for looking up Windows ID tables
|
|
*/
|
|
|
|
static const int windowsDataTableSize = sizeof(windowsDataTable) / sizeof(QWindowsData) - 1;
|
|
static const int zoneDataTableSize = sizeof(zoneDataTable) / sizeof(QZoneData) - 1;
|
|
static const int utcDataTableSize = sizeof(utcDataTable) / sizeof(QUtcData) - 1;
|
|
|
|
|
|
static const QZoneData *zoneData(quint16 index)
|
|
{
|
|
Q_ASSERT(index < zoneDataTableSize);
|
|
return &zoneDataTable[index];
|
|
}
|
|
|
|
static const QWindowsData *windowsData(quint16 index)
|
|
{
|
|
Q_ASSERT(index < windowsDataTableSize);
|
|
return &windowsDataTable[index];
|
|
}
|
|
|
|
static const QUtcData *utcData(quint16 index)
|
|
{
|
|
Q_ASSERT(index < utcDataTableSize);
|
|
return &utcDataTable[index];
|
|
}
|
|
|
|
// Return the Windows ID literal for a given QWindowsData
|
|
static QByteArray windowsId(const QWindowsData *windowsData)
|
|
{
|
|
return (windowsIdData + windowsData->windowsIdIndex);
|
|
}
|
|
|
|
// Return the IANA ID literal for a given QWindowsData
|
|
static QByteArray ianaId(const QWindowsData *windowsData)
|
|
{
|
|
return (ianaIdData + windowsData->ianaIdIndex);
|
|
}
|
|
|
|
// Return the IANA ID literal for a given QZoneData
|
|
static QByteArray ianaId(const QZoneData *zoneData)
|
|
{
|
|
return (ianaIdData + zoneData->ianaIdIndex);
|
|
}
|
|
|
|
static QByteArray utcId(const QUtcData *utcData)
|
|
{
|
|
return (ianaIdData + utcData->ianaIdIndex);
|
|
}
|
|
|
|
static quint16 toWindowsIdKey(const QByteArray &winId)
|
|
{
|
|
for (quint16 i = 0; i < windowsDataTableSize; ++i) {
|
|
const QWindowsData *data = windowsData(i);
|
|
if (windowsId(data) == winId)
|
|
return data->windowsIdKey;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static QByteArray toWindowsIdLiteral(quint16 windowsIdKey)
|
|
{
|
|
for (quint16 i = 0; i < windowsDataTableSize; ++i) {
|
|
const QWindowsData *data = windowsData(i);
|
|
if (data->windowsIdKey == windowsIdKey)
|
|
return windowsId(data);
|
|
}
|
|
return QByteArray();
|
|
}
|
|
|
|
/*
|
|
Base class implementing common utility routines, only intantiate for a null tz.
|
|
*/
|
|
|
|
QTimeZonePrivate::QTimeZonePrivate()
|
|
{
|
|
}
|
|
|
|
QTimeZonePrivate::QTimeZonePrivate(const QTimeZonePrivate &other)
|
|
: QSharedData(other), m_id(other.m_id)
|
|
{
|
|
}
|
|
|
|
QTimeZonePrivate::~QTimeZonePrivate()
|
|
{
|
|
}
|
|
|
|
QTimeZonePrivate *QTimeZonePrivate::clone() const
|
|
{
|
|
return new QTimeZonePrivate(*this);
|
|
}
|
|
|
|
bool QTimeZonePrivate::operator==(const QTimeZonePrivate &other) const
|
|
{
|
|
// TODO Too simple, but need to solve problem of comparing different derived classes
|
|
// Should work for all System and ICU classes as names guaranteed unique, but not for Simple.
|
|
// Perhaps once all classes have working transitions can compare full list?
|
|
return (m_id == other.m_id);
|
|
}
|
|
|
|
bool QTimeZonePrivate::operator!=(const QTimeZonePrivate &other) const
|
|
{
|
|
return !(*this == other);
|
|
}
|
|
|
|
bool QTimeZonePrivate::isValid() const
|
|
{
|
|
return !m_id.isEmpty();
|
|
}
|
|
|
|
QByteArray QTimeZonePrivate::id() const
|
|
{
|
|
return m_id;
|
|
}
|
|
|
|
QLocale::Country QTimeZonePrivate::country() const
|
|
{
|
|
// Default fall-back mode, use the zoneTable to find Region of known Zones
|
|
for (int i = 0; i < zoneDataTableSize; ++i) {
|
|
const QZoneData *data = zoneData(i);
|
|
if (ianaId(data).split(' ').contains(m_id))
|
|
return (QLocale::Country)data->country;
|
|
}
|
|
return QLocale::AnyCountry;
|
|
}
|
|
|
|
QString QTimeZonePrivate::comment() const
|
|
{
|
|
return QString();
|
|
}
|
|
|
|
QString QTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
|
|
QTimeZone::NameType nameType,
|
|
const QLocale &locale) const
|
|
{
|
|
if (nameType == QTimeZone::OffsetName)
|
|
return isoOffsetFormat(offsetFromUtc(atMSecsSinceEpoch));
|
|
|
|
if (isDaylightTime(atMSecsSinceEpoch))
|
|
return displayName(QTimeZone::DaylightTime, nameType, locale);
|
|
else
|
|
return displayName(QTimeZone::StandardTime, nameType, locale);
|
|
}
|
|
|
|
QString QTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
|
|
QTimeZone::NameType nameType,
|
|
const QLocale &locale) const
|
|
{
|
|
Q_UNUSED(timeType)
|
|
Q_UNUSED(nameType)
|
|
Q_UNUSED(locale)
|
|
return QString();
|
|
}
|
|
|
|
QString QTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
|
|
{
|
|
Q_UNUSED(atMSecsSinceEpoch)
|
|
return QString();
|
|
}
|
|
|
|
int QTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
|
|
{
|
|
return standardTimeOffset(atMSecsSinceEpoch) + daylightTimeOffset(atMSecsSinceEpoch);
|
|
}
|
|
|
|
int QTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
|
|
{
|
|
Q_UNUSED(atMSecsSinceEpoch)
|
|
return invalidSeconds();
|
|
}
|
|
|
|
int QTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
|
|
{
|
|
Q_UNUSED(atMSecsSinceEpoch)
|
|
return invalidSeconds();
|
|
}
|
|
|
|
bool QTimeZonePrivate::hasDaylightTime() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool QTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
|
|
{
|
|
Q_UNUSED(atMSecsSinceEpoch)
|
|
return false;
|
|
}
|
|
|
|
QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
|
|
{
|
|
Q_UNUSED(forMSecsSinceEpoch)
|
|
return invalidData();
|
|
}
|
|
|
|
// Private only method for use by QDateTime to convert local msecs to epoch msecs
|
|
QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs, int hint) const
|
|
{
|
|
if (!hasDaylightTime()) // No DST means same offset for all local msecs
|
|
return data(forLocalMSecs - standardTimeOffset(forLocalMSecs) * 1000);
|
|
|
|
/*
|
|
We need a UTC time at which to ask for the offset, in order to be able to
|
|
add that offset to forLocalMSecs, to get the UTC time we
|
|
need. Fortunately, no time-zone offset is more than 14 hours; and DST
|
|
transitions happen (much) more than thirty-two hours apart. So sampling
|
|
offset sixteen hours each side gives us information we can be sure
|
|
brackets the correct time and at most one DST transition.
|
|
*/
|
|
const qint64 sixteenHoursInMSecs(16 * 3600 * 1000);
|
|
Q_STATIC_ASSERT(-sixteenHoursInMSecs / 1000 < QTimeZone::MinUtcOffsetSecs
|
|
&& sixteenHoursInMSecs / 1000 > QTimeZone::MaxUtcOffsetSecs);
|
|
/*
|
|
Offsets are Local - UTC, positive to the east of Greenwich, negative to
|
|
the west; DST offset always exceeds standard offset, when DST applies.
|
|
When we have offsets on either side of a transition, the lower one is
|
|
standard, the higher is DST.
|
|
|
|
Non-DST transitions (jurisdictions changing time-zone and time-zones
|
|
changing their standard offset, typically) are described below as if they
|
|
were DST transitions (since these are more usual and familiar); the code
|
|
mostly concerns itself with offsets from UTC, described in terms of the
|
|
common case for changes in that. If there is no actual change in offset
|
|
(e.g. a DST transition cancelled by a standard offset change), this code
|
|
should handle it gracefully; without transitions, it'll see early == late
|
|
and take the easy path; with transitions, tran and nextTran get the
|
|
correct UTC time as atMSecsSinceEpoch so comparing to nextStart selects
|
|
the right one. In all other cases, the transition changes offset and the
|
|
reasoning that applies to DST applies just the same. Aside from hinting,
|
|
the only thing that looks at DST-ness at all, other than inferred from
|
|
offset changes, is the case without transition data handling an invalid
|
|
time in the gap that a transition passed over.
|
|
|
|
The handling of hint (see below) is apt to go wrong in non-DST
|
|
transitions. There isn't really a great deal we can hope to do about that
|
|
without adding yet more unreliable complexity to the heuristics in use for
|
|
already obscure corner-cases.
|
|
*/
|
|
|
|
/*
|
|
The hint (really a QDateTimePrivate::DaylightStatus) is > 0 if caller
|
|
thinks we're in DST, 0 if in standard. A value of -2 means never-DST, so
|
|
should have been handled above; if it slips through, it's wrong but we
|
|
should probably treat it as standard anyway (never-DST means
|
|
always-standard, after all). If the hint turns out to be wrong, fall back
|
|
on trying the other possibility: which makes it harmless to treat -1
|
|
(meaning unknown) as standard (i.e. try standard first, then try DST). In
|
|
practice, away from a transition, the only difference hint makes is to
|
|
which candidate we try first: if the hint is wrong (or unknown and
|
|
standard fails), we'll try the other candidate and it'll work.
|
|
|
|
For the obscure (and invalid) case where forLocalMSecs falls in a
|
|
spring-forward's missing hour, a common case is that we started with a
|
|
date/time for which the hint was valid and adjusted it naively; for that
|
|
case, we should correct the adjustment by shunting across the transition
|
|
into where hint is wrong. So half-way through the gap, arrived at from
|
|
the DST side, should be read as an hour earlier, in standard time; but, if
|
|
arrived at from the standard side, should be read as an hour later, in
|
|
DST. (This shall be wrong in some cases; for example, when a country
|
|
changes its transition dates and changing a date/time by more than six
|
|
months lands it on a transition. However, these cases are even more
|
|
obscure than those where the heuristic is good.)
|
|
*/
|
|
|
|
if (hasTransitions()) {
|
|
/*
|
|
We have transitions.
|
|
|
|
Each transition gives the offsets to use until the next; so we need the
|
|
most recent transition before the time forLocalMSecs describes. If it
|
|
describes a time *in* a transition, we'll need both that transition and
|
|
the one before it. So find one transition that's probably after (and not
|
|
much before, otherwise) and another that's definitely before, then work
|
|
out which one to use. When both or neither work on forLocalMSecs, use
|
|
hint to disambiguate.
|
|
*/
|
|
|
|
// Get a transition definitely before the local MSecs; usually all we need.
|
|
// Only around the transition times might we need another.
|
|
Data tran = previousTransition(forLocalMSecs - sixteenHoursInMSecs);
|
|
Q_ASSERT(forLocalMSecs < 0 || // Pre-epoch TZ info may be unavailable
|
|
forLocalMSecs >= tran.atMSecsSinceEpoch + tran.offsetFromUtc * 1000);
|
|
Data nextTran = nextTransition(tran.atMSecsSinceEpoch);
|
|
/*
|
|
Now walk those forward until they bracket forLocalMSecs with transitions.
|
|
|
|
One of the transitions should then be telling us the right offset to use.
|
|
In a transition, we need the transition before it (to describe the run-up
|
|
to the transition) and the transition itself; so we need to stop when
|
|
nextTran is that transition.
|
|
*/
|
|
while (nextTran.atMSecsSinceEpoch != invalidMSecs()
|
|
&& forLocalMSecs > nextTran.atMSecsSinceEpoch + nextTran.offsetFromUtc * 1000) {
|
|
Data newTran = nextTransition(nextTran.atMSecsSinceEpoch);
|
|
if (newTran.atMSecsSinceEpoch == invalidMSecs()
|
|
|| newTran.atMSecsSinceEpoch + newTran.offsetFromUtc * 1000
|
|
> forLocalMSecs + sixteenHoursInMSecs) {
|
|
// Definitely not a relevant tansition: too far in the future.
|
|
break;
|
|
}
|
|
tran = nextTran;
|
|
nextTran = newTran;
|
|
}
|
|
|
|
// Check we do *really* have transitions for this zone:
|
|
if (tran.atMSecsSinceEpoch != invalidMSecs()) {
|
|
|
|
/*
|
|
So now tran is definitely before and nextTran is either after or only
|
|
slightly before. The one with the larger offset is in DST; the other in
|
|
standard time. Our hint tells us which of those to use (defaulting to
|
|
standard if no hint): try it first; if that fails, try the other; if both
|
|
fail life's tricky.
|
|
*/
|
|
Q_ASSERT(forLocalMSecs < 0
|
|
|| forLocalMSecs > tran.atMSecsSinceEpoch + tran.offsetFromUtc * 1000);
|
|
const qint64 nextStart = nextTran.atMSecsSinceEpoch;
|
|
// Work out the UTC values it might make sense to return:
|
|
nextTran.atMSecsSinceEpoch = forLocalMSecs - nextTran.offsetFromUtc * 1000;
|
|
tran.atMSecsSinceEpoch = forLocalMSecs - tran.offsetFromUtc * 1000;
|
|
|
|
const bool nextIsDst = tran.offsetFromUtc < nextTran.offsetFromUtc;
|
|
// If that agrees with hint > 0, our first guess is to use nextTran; else tran.
|
|
const bool nextFirst = nextIsDst == (hint > 0) && nextStart != invalidMSecs();
|
|
for (int i = 0; i < 2; i++) {
|
|
/*
|
|
On the first pass, the case we consider is what hint told us to expect
|
|
(except when hint was -1 and didn't actually tell us what to expect),
|
|
so it's likely right. We only get a second pass if the first failed,
|
|
by which time the second case, that we're trying, is likely right. If
|
|
an overwhelming majority of calls have hint == -1, the Q_LIKELY here
|
|
shall be wrong half the time; otherwise, its errors shall be rarer
|
|
than that.
|
|
*/
|
|
if (nextFirst ? i == 0 : i) {
|
|
Q_ASSERT(nextStart != invalidMSecs());
|
|
if (Q_LIKELY(nextStart <= nextTran.atMSecsSinceEpoch))
|
|
return nextTran;
|
|
} else {
|
|
// If next is invalid, nextFirst is false, to route us here first:
|
|
if (nextStart == invalidMSecs() || Q_LIKELY(nextStart > tran.atMSecsSinceEpoch))
|
|
return tran;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Neither is valid (e.g. in a spring-forward's gap) and
|
|
nextTran.atMSecsSinceEpoch < nextStart <= tran.atMSecsSinceEpoch, so
|
|
0 < tran.atMSecsSinceEpoch - nextTran.atMSecsSinceEpoch
|
|
= (nextTran.offsetFromUtc - tran.offsetFromUtc) * 1000
|
|
*/
|
|
int dstStep = nextTran.offsetFromUtc - tran.offsetFromUtc;
|
|
Q_ASSERT(dstStep > 0); // How else could we get here ?
|
|
if (nextFirst) { // hint thought we needed nextTran, so use tran
|
|
tran.atMSecsSinceEpoch -= dstStep;
|
|
return tran;
|
|
}
|
|
nextTran.atMSecsSinceEpoch += dstStep;
|
|
return nextTran;
|
|
}
|
|
// System has transitions but not for this zone.
|
|
// Try falling back to offsetFromUtc
|
|
}
|
|
|
|
/* Bracket and refine to discover offset. */
|
|
qint64 utcEpochMSecs;
|
|
|
|
int early = offsetFromUtc(forLocalMSecs - sixteenHoursInMSecs);
|
|
int late = offsetFromUtc(forLocalMSecs + sixteenHoursInMSecs);
|
|
if (Q_LIKELY(early == late)) { // > 99% of the time
|
|
utcEpochMSecs = forLocalMSecs - early * 1000;
|
|
} else {
|
|
// Close to a DST transition: early > late is near a fall-back,
|
|
// early < late is near a spring-forward.
|
|
const int offsetInDst = qMax(early, late);
|
|
const int offsetInStd = qMin(early, late);
|
|
// Candidate values for utcEpochMSecs (if forLocalMSecs is valid):
|
|
const qint64 forDst = forLocalMSecs - offsetInDst * 1000;
|
|
const qint64 forStd = forLocalMSecs - offsetInStd * 1000;
|
|
// Best guess at the answer:
|
|
const qint64 hinted = hint > 0 ? forDst : forStd;
|
|
if (Q_LIKELY(offsetFromUtc(hinted) == (hint > 0 ? offsetInDst : offsetInStd))) {
|
|
utcEpochMSecs = hinted;
|
|
} else if (hint <= 0 && offsetFromUtc(forDst) == offsetInDst) {
|
|
utcEpochMSecs = forDst;
|
|
} else if (hint > 0 && offsetFromUtc(forStd) == offsetInStd) {
|
|
utcEpochMSecs = forStd;
|
|
} else {
|
|
// Invalid forLocalMSecs: in spring-forward gap.
|
|
const int dstStep = daylightTimeOffset(early < late ?
|
|
forLocalMSecs + sixteenHoursInMSecs :
|
|
forLocalMSecs - sixteenHoursInMSecs);
|
|
Q_ASSERT(dstStep); // There can't be a transition without it !
|
|
utcEpochMSecs = (hint > 0) ? forStd - dstStep : forDst + dstStep;
|
|
}
|
|
}
|
|
|
|
return data(utcEpochMSecs);
|
|
}
|
|
|
|
bool QTimeZonePrivate::hasTransitions() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
QTimeZonePrivate::Data QTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
|
|
{
|
|
Q_UNUSED(afterMSecsSinceEpoch)
|
|
return invalidData();
|
|
}
|
|
|
|
QTimeZonePrivate::Data QTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
|
|
{
|
|
Q_UNUSED(beforeMSecsSinceEpoch)
|
|
return invalidData();
|
|
}
|
|
|
|
QTimeZonePrivate::DataList QTimeZonePrivate::transitions(qint64 fromMSecsSinceEpoch,
|
|
qint64 toMSecsSinceEpoch) const
|
|
{
|
|
DataList list;
|
|
if (toMSecsSinceEpoch >= fromMSecsSinceEpoch) {
|
|
// fromMSecsSinceEpoch is inclusive but nextTransitionTime() is exclusive so go back 1 msec
|
|
Data next = nextTransition(fromMSecsSinceEpoch - 1);
|
|
while (next.atMSecsSinceEpoch != invalidMSecs()
|
|
&& next.atMSecsSinceEpoch <= toMSecsSinceEpoch) {
|
|
list.append(next);
|
|
next = nextTransition(next.atMSecsSinceEpoch);
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
|
|
QByteArray QTimeZonePrivate::systemTimeZoneId() const
|
|
{
|
|
return QByteArray();
|
|
}
|
|
|
|
QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds() const
|
|
{
|
|
return QList<QByteArray>();
|
|
}
|
|
|
|
QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(QLocale::Country country) const
|
|
{
|
|
// Default fall-back mode, use the zoneTable to find Region of know Zones
|
|
QList<QByteArray> regions;
|
|
|
|
// First get all Zones in the Zones table belonging to the Region
|
|
for (int i = 0; i < zoneDataTableSize; ++i) {
|
|
if (zoneData(i)->country == country)
|
|
regions += ianaId(zoneData(i)).split(' ');
|
|
}
|
|
|
|
std::sort(regions.begin(), regions.end());
|
|
regions.erase(std::unique(regions.begin(), regions.end()), regions.end());
|
|
|
|
// Then select just those that are available
|
|
const QList<QByteArray> all = availableTimeZoneIds();
|
|
QList<QByteArray> result;
|
|
result.reserve(qMin(all.size(), regions.size()));
|
|
std::set_intersection(all.begin(), all.end(), regions.cbegin(), regions.cend(),
|
|
std::back_inserter(result));
|
|
return result;
|
|
}
|
|
|
|
QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const
|
|
{
|
|
// Default fall-back mode, use the zoneTable to find Offset of know Zones
|
|
QList<QByteArray> offsets;
|
|
// First get all Zones in the table using the Offset
|
|
for (int i = 0; i < windowsDataTableSize; ++i) {
|
|
const QWindowsData *winData = windowsData(i);
|
|
if (winData->offsetFromUtc == offsetFromUtc) {
|
|
for (int j = 0; j < zoneDataTableSize; ++j) {
|
|
const QZoneData *data = zoneData(j);
|
|
if (data->windowsIdKey == winData->windowsIdKey)
|
|
offsets += ianaId(data).split(' ');
|
|
}
|
|
}
|
|
}
|
|
|
|
std::sort(offsets.begin(), offsets.end());
|
|
offsets.erase(std::unique(offsets.begin(), offsets.end()), offsets.end());
|
|
|
|
// Then select just those that are available
|
|
const QList<QByteArray> all = availableTimeZoneIds();
|
|
QList<QByteArray> result;
|
|
result.reserve(qMin(all.size(), offsets.size()));
|
|
std::set_intersection(all.begin(), all.end(), offsets.cbegin(), offsets.cend(),
|
|
std::back_inserter(result));
|
|
return result;
|
|
}
|
|
|
|
#ifndef QT_NO_DATASTREAM
|
|
void QTimeZonePrivate::serialize(QDataStream &ds) const
|
|
{
|
|
ds << QString::fromUtf8(m_id);
|
|
}
|
|
#endif // QT_NO_DATASTREAM
|
|
|
|
// Static Utility Methods
|
|
|
|
QTimeZonePrivate::Data QTimeZonePrivate::invalidData()
|
|
{
|
|
Data data;
|
|
data.atMSecsSinceEpoch = invalidMSecs();
|
|
data.offsetFromUtc = invalidSeconds();
|
|
data.standardTimeOffset = invalidSeconds();
|
|
data.daylightTimeOffset = invalidSeconds();
|
|
return data;
|
|
}
|
|
|
|
QTimeZone::OffsetData QTimeZonePrivate::invalidOffsetData()
|
|
{
|
|
QTimeZone::OffsetData offsetData;
|
|
offsetData.atUtc = QDateTime();
|
|
offsetData.offsetFromUtc = invalidSeconds();
|
|
offsetData.standardTimeOffset = invalidSeconds();
|
|
offsetData.daylightTimeOffset = invalidSeconds();
|
|
return offsetData;
|
|
}
|
|
|
|
QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Data &data)
|
|
{
|
|
QTimeZone::OffsetData offsetData = invalidOffsetData();
|
|
if (data.atMSecsSinceEpoch != invalidMSecs()) {
|
|
offsetData.atUtc = QDateTime::fromMSecsSinceEpoch(data.atMSecsSinceEpoch, Qt::UTC);
|
|
offsetData.offsetFromUtc = data.offsetFromUtc;
|
|
offsetData.standardTimeOffset = data.standardTimeOffset;
|
|
offsetData.daylightTimeOffset = data.daylightTimeOffset;
|
|
offsetData.abbreviation = data.abbreviation;
|
|
}
|
|
return offsetData;
|
|
}
|
|
|
|
// Is the format of the ID valid ?
|
|
bool QTimeZonePrivate::isValidId(const QByteArray &ianaId)
|
|
{
|
|
/*
|
|
Main rules for defining TZ/IANA names as per ftp://ftp.iana.org/tz/code/Theory
|
|
1. Use only valid POSIX file name components
|
|
2. Within a file name component, use only ASCII letters, `.', `-' and `_'.
|
|
3. Do not use digits (except in a [+-]\d+ suffix, when used).
|
|
4. A file name component must not exceed 14 characters or start with `-'
|
|
However, the rules are really guidelines - a later one says
|
|
- Do not change established names if they only marginally violate the
|
|
above rules.
|
|
We may, therefore, need to be a bit slack in our check here, if we hit
|
|
legitimate exceptions in real time-zone databases.
|
|
|
|
In particular, aliases such as "Etc/GMT+7" and "SystemV/EST5EDT" are valid
|
|
so we need to accept digits, ':', and '+'; aliases typically have the form
|
|
of POSIX TZ strings, which allow a suffix to a proper IANA name. A POSIX
|
|
suffix starts with an offset (as in GMT+7) and may continue with another
|
|
name (as in EST5EDT, giving the DST name of the zone); a further offset is
|
|
allowed (for DST). The ("hard to describe and [...] error-prone in
|
|
practice") POSIX form even allows a suffix giving the dates (and
|
|
optionally times) of the annual DST transitions. Hopefully, no TZ aliases
|
|
go that far, but we at least need to accept an offset and (single
|
|
fragment) DST-name.
|
|
|
|
But for the legacy complications, the following would be preferable if
|
|
QRegExp would work on QByteArrays directly:
|
|
const QRegExp rx(QStringLiteral("[a-z+._][a-z+._-]{,13}"
|
|
"(?:/[a-z+._][a-z+._-]{,13})*"
|
|
// Optional suffix:
|
|
"(?:[+-]?\d{1,2}(?::\d{1,2}){,2}" // offset
|
|
// one name fragment (DST):
|
|
"(?:[a-z+._][a-z+._-]{,13})?)"),
|
|
Qt::CaseInsensitive);
|
|
return rx.exactMatch(ianaId);
|
|
*/
|
|
|
|
// Somewhat slack hand-rolled version:
|
|
const int MinSectionLength = 1;
|
|
const int MaxSectionLength = 14;
|
|
int sectionLength = 0;
|
|
for (const char *it = ianaId.begin(), * const end = ianaId.end(); it != end; ++it, ++sectionLength) {
|
|
const char ch = *it;
|
|
if (ch == '/') {
|
|
if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
|
|
return false; // violates (4)
|
|
sectionLength = -1;
|
|
} else if (ch == '-') {
|
|
if (sectionLength == 0)
|
|
return false; // violates (4)
|
|
} else if (!(ch >= 'a' && ch <= 'z')
|
|
&& !(ch >= 'A' && ch <= 'Z')
|
|
&& !(ch == '_')
|
|
&& !(ch == '.')
|
|
// Should ideally check these only happen as an offset:
|
|
&& !(ch >= '0' && ch <= '9')
|
|
&& !(ch == '+')
|
|
&& !(ch == ':')) {
|
|
return false; // violates (2)
|
|
}
|
|
}
|
|
if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
|
|
return false; // violates (4)
|
|
return true;
|
|
}
|
|
|
|
QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc)
|
|
{
|
|
const int mins = offsetFromUtc / 60;
|
|
return QString::fromUtf8("UTC%1%2:%3").arg(mins >= 0 ? QLatin1Char('+') : QLatin1Char('-'))
|
|
.arg(qAbs(mins) / 60, 2, 10, QLatin1Char('0'))
|
|
.arg(qAbs(mins) % 60, 2, 10, QLatin1Char('0'));
|
|
}
|
|
|
|
QByteArray QTimeZonePrivate::ianaIdToWindowsId(const QByteArray &id)
|
|
{
|
|
for (int i = 0; i < zoneDataTableSize; ++i) {
|
|
const QZoneData *data = zoneData(i);
|
|
if (ianaId(data).split(' ').contains(id))
|
|
return toWindowsIdLiteral(data->windowsIdKey);
|
|
}
|
|
return QByteArray();
|
|
}
|
|
|
|
QByteArray QTimeZonePrivate::windowsIdToDefaultIanaId(const QByteArray &windowsId)
|
|
{
|
|
const quint16 windowsIdKey = toWindowsIdKey(windowsId);
|
|
for (int i = 0; i < windowsDataTableSize; ++i) {
|
|
const QWindowsData *data = windowsData(i);
|
|
if (data->windowsIdKey == windowsIdKey)
|
|
return ianaId(data);
|
|
}
|
|
return QByteArray();
|
|
}
|
|
|
|
QByteArray QTimeZonePrivate::windowsIdToDefaultIanaId(const QByteArray &windowsId,
|
|
QLocale::Country country)
|
|
{
|
|
const QList<QByteArray> list = windowsIdToIanaIds(windowsId, country);
|
|
if (list.count() > 0)
|
|
return list.first();
|
|
else
|
|
return QByteArray();
|
|
}
|
|
|
|
QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId)
|
|
{
|
|
const quint16 windowsIdKey = toWindowsIdKey(windowsId);
|
|
QList<QByteArray> list;
|
|
|
|
for (int i = 0; i < zoneDataTableSize; ++i) {
|
|
const QZoneData *data = zoneData(i);
|
|
if (data->windowsIdKey == windowsIdKey)
|
|
list << ianaId(data).split(' ');
|
|
}
|
|
|
|
// Return the full list in alpha order
|
|
std::sort(list.begin(), list.end());
|
|
return list;
|
|
}
|
|
|
|
QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId,
|
|
QLocale::Country country)
|
|
{
|
|
const quint16 windowsIdKey = toWindowsIdKey(windowsId);
|
|
for (int i = 0; i < zoneDataTableSize; ++i) {
|
|
const QZoneData *data = zoneData(i);
|
|
// Return the region matches in preference order
|
|
if (data->windowsIdKey == windowsIdKey && data->country == (quint16) country)
|
|
return ianaId(data).split(' ');
|
|
}
|
|
|
|
return QList<QByteArray>();
|
|
}
|
|
|
|
// Define template for derived classes to reimplement so QSharedDataPointer clone() works correctly
|
|
template<> QTimeZonePrivate *QSharedDataPointer<QTimeZonePrivate>::clone()
|
|
{
|
|
return d->clone();
|
|
}
|
|
|
|
/*
|
|
UTC Offset implementation, used when QT_NO_SYSTEMLOCALE set and ICU is not being used,
|
|
or for QDateTimes with a Qt:Spec of Qt::OffsetFromUtc.
|
|
*/
|
|
|
|
// Create default UTC time zone
|
|
QUtcTimeZonePrivate::QUtcTimeZonePrivate()
|
|
{
|
|
const QString name = utcQString();
|
|
init(utcQByteArray(), 0, name, name, QLocale::AnyCountry, name);
|
|
}
|
|
|
|
// Create a named UTC time zone
|
|
QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id)
|
|
{
|
|
// Look for the name in the UTC list, if found set the values
|
|
for (int i = 0; i < utcDataTableSize; ++i) {
|
|
const QUtcData *data = utcData(i);
|
|
const QByteArray uid = utcId(data);
|
|
if (uid == id) {
|
|
QString name = QString::fromUtf8(id);
|
|
init(id, data->offsetFromUtc, name, name, QLocale::AnyCountry, name);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create offset from UTC
|
|
QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
|
|
{
|
|
QString utcId;
|
|
|
|
if (offsetSeconds == 0)
|
|
utcId = utcQString();
|
|
else
|
|
utcId = isoOffsetFormat(offsetSeconds);
|
|
|
|
init(utcId.toUtf8(), offsetSeconds, utcId, utcId, QLocale::AnyCountry, utcId);
|
|
}
|
|
|
|
QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,
|
|
const QString &name, const QString &abbreviation,
|
|
QLocale::Country country, const QString &comment)
|
|
{
|
|
init(zoneId, offsetSeconds, name, abbreviation, country, comment);
|
|
}
|
|
|
|
QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other)
|
|
: QTimeZonePrivate(other), m_name(other.m_name),
|
|
m_abbreviation(other.m_abbreviation),
|
|
m_comment(other.m_comment),
|
|
m_country(other.m_country),
|
|
m_offsetFromUtc(other.m_offsetFromUtc)
|
|
{
|
|
}
|
|
|
|
QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
|
|
{
|
|
}
|
|
|
|
QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
|
|
{
|
|
return new QUtcTimeZonePrivate(*this);
|
|
}
|
|
|
|
QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
|
|
{
|
|
Data d;
|
|
d.abbreviation = m_abbreviation;
|
|
d.atMSecsSinceEpoch = forMSecsSinceEpoch;
|
|
d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
|
|
d.daylightTimeOffset = 0;
|
|
return d;
|
|
}
|
|
|
|
void QUtcTimeZonePrivate::init(const QByteArray &zoneId)
|
|
{
|
|
m_id = zoneId;
|
|
}
|
|
|
|
void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
|
|
const QString &abbreviation, QLocale::Country country,
|
|
const QString &comment)
|
|
{
|
|
m_id = zoneId;
|
|
m_offsetFromUtc = offsetSeconds;
|
|
m_name = name;
|
|
m_abbreviation = abbreviation;
|
|
m_country = country;
|
|
m_comment = comment;
|
|
}
|
|
|
|
QLocale::Country QUtcTimeZonePrivate::country() const
|
|
{
|
|
return m_country;
|
|
}
|
|
|
|
QString QUtcTimeZonePrivate::comment() const
|
|
{
|
|
return m_comment;
|
|
}
|
|
|
|
QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
|
|
QTimeZone::NameType nameType,
|
|
const QLocale &locale) const
|
|
{
|
|
Q_UNUSED(timeType)
|
|
Q_UNUSED(locale)
|
|
if (nameType == QTimeZone::ShortName)
|
|
return m_abbreviation;
|
|
else if (nameType == QTimeZone::OffsetName)
|
|
return isoOffsetFormat(m_offsetFromUtc);
|
|
return m_name;
|
|
}
|
|
|
|
QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
|
|
{
|
|
Q_UNUSED(atMSecsSinceEpoch)
|
|
return m_abbreviation;
|
|
}
|
|
|
|
qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
|
|
{
|
|
Q_UNUSED(atMSecsSinceEpoch)
|
|
return m_offsetFromUtc;
|
|
}
|
|
|
|
qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
|
|
{
|
|
Q_UNUSED(atMSecsSinceEpoch)
|
|
return 0;
|
|
}
|
|
|
|
QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
|
|
{
|
|
return utcQByteArray();
|
|
}
|
|
|
|
QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
|
|
{
|
|
QList<QByteArray> result;
|
|
result.reserve(utcDataTableSize);
|
|
for (int i = 0; i < utcDataTableSize; ++i)
|
|
result << utcId(utcData(i));
|
|
std::sort(result.begin(), result.end()); // ### or already sorted??
|
|
// ### assuming no duplicates
|
|
return result;
|
|
}
|
|
|
|
QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Country country) const
|
|
{
|
|
// If AnyCountry then is request for all non-region offset codes
|
|
if (country == QLocale::AnyCountry)
|
|
return availableTimeZoneIds();
|
|
return QList<QByteArray>();
|
|
}
|
|
|
|
QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
|
|
{
|
|
QList<QByteArray> result;
|
|
for (int i = 0; i < utcDataTableSize; ++i) {
|
|
const QUtcData *data = utcData(i);
|
|
if (data->offsetFromUtc == offsetSeconds)
|
|
result << utcId(data);
|
|
}
|
|
std::sort(result.begin(), result.end()); // ### or already sorted??
|
|
// ### assuming no duplicates
|
|
return result;
|
|
}
|
|
|
|
#ifndef QT_NO_DATASTREAM
|
|
void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
|
|
{
|
|
ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(m_id) << m_offsetFromUtc << m_name
|
|
<< m_abbreviation << (qint32) m_country << m_comment;
|
|
}
|
|
#endif // QT_NO_DATASTREAM
|
|
|
|
QT_END_NAMESPACE
|