QDateTime: disambiguate times in a zone transition

Previously, requesting a time that got repeated - on the given date,
due to a fall-back transition - would get one of the two repeats,
giving the caller (no hint that there was a choice and) no way to
select the other. Add a flags parameter that captures the available
ways to resolve such ambiguity or select a suitable time near a gap.

Add such a parameter to relevant QDateTime methods, including
constructors, to enable callers to indicate their preference in the
same way. This replaces DST-hint parameters in various internal
functions, including QTimeZonePrivate's dataForLocalTime(). Adapted
tst_QDateTime to test the new feature.

Adapt to gap-times no longer being invalid (by default; or, when they
are, no longer having a useful toMSecsSinceEpoch() value). Instead,
they don't match what was asked for. Amend documentation to reflect
that. Most of the code change for this is to QDTParser and QDTEdit.

[ChangeLog][QtCore][QDateTime] Added a TransitionResolution parameter
to various QDateTime methods to enable the caller to indicate, when
the indicated datetime falls in a time-zone transition, which side of
the transition to fall or whether to produce an invalid result.

[ChangeLog][QtCore][Possibly Significant Behavior Change] When
QDateTime is instantiated for a combination of date and time that was
skipped, by local time or a time-zone, for example during a
spring-forward DST transition, the result is no longer marked invalid.
Whether the selected nearby date-time is before or after the skipped
interval may have changed on some platforms; unless overridden by an
explicit TransitionResolution, it is now a date-time as long after the
previous day's noon as a naive reading of the requested date and time
would expect. This was the prior behavior at least on Linux.

Fixes: QTBUG-79923
Change-Id: I11d5339abef9e7125c4e0dc95a09a7cd4f169dab
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Edward Welbourne 2022-08-31 15:43:48 +02:00
parent 38994ab9ac
commit a49ccc08c3
12 changed files with 833 additions and 439 deletions

View File

@ -618,6 +618,19 @@ QStringView QXmlStreamAttributes::value(QLatin1StringView qualifiedName) const
#if QT_CORE_REMOVED_SINCE(6, 7)
#include "qdatetime.h"
QDateTime::QDateTime(QDate date, QTime time, const QTimeZone &timeZone)
: QDateTime(date, time, timeZone, TransitionResolution::LegacyBehavior) {}
QDateTime::QDateTime(QDate date, QTime time)
: QDateTime(date, time, TransitionResolution::LegacyBehavior) {}
void QDateTime::setDate(QDate date) { setDate(date, TransitionResolution::LegacyBehavior); }
void QDateTime::setTime(QTime time) { setTime(time, TransitionResolution::LegacyBehavior); }
void QDateTime::setTimeZone(const QTimeZone &toZone)
{
setTimeZone(toZone, TransitionResolution::LegacyBehavior);
}
#if defined(Q_OS_ANDROID)
#include "qjniobject.h"

View File

@ -853,7 +853,10 @@ static bool inDateTimeRange(qint64 jd, DaySide side)
static QDateTime toEarliest(QDate day, const QTimeZone &zone)
{
Q_ASSERT(!zone.isUtcOrFixedOffset());
const auto moment = [=](QTime time) { return QDateTime(day, time, zone); };
// And the day starts in a gap. First find a moment not in that gap.
const auto moment = [=](QTime time) {
return QDateTime(day, time, zone, QDateTime::TransitionResolution::Reject);
};
// Longest routine time-zone transition is 2 hours:
QDateTime when = moment(QTime(2, 0));
if (!when.isValid()) {
@ -871,7 +874,8 @@ static QDateTime toEarliest(QDate day, const QTimeZone &zone)
// Binary chop to the right minute
while (high > low + 1) {
const int mid = (high + low) / 2;
const QDateTime probe = moment(QTime(mid / 60, mid % 60));
const QDateTime probe = QDateTime(day, QTime(mid / 60, mid % 60), zone,
QDateTime::TransitionResolution::PreferBefore);
if (probe.isValid() && probe.date() == day) {
high = mid;
when = probe;
@ -933,24 +937,26 @@ QDateTime QDate::startOfDay(const QTimeZone &zone) const
if (!inDateTimeRange(jd, DaySide::Start) || !zone.isValid())
return QDateTime();
QDateTime when(*this, QTime(0, 0), zone);
if (Q_LIKELY(when.isValid()))
return when;
QDateTime when(*this, QTime(0, 0), zone,
QDateTime::TransitionResolution::RelativeToBefore);
if (Q_UNLIKELY(!when.isValid() || when.date() != *this)) {
#if QT_CONFIG(timezone)
// The start of the day must have fallen in a spring-forward's gap; find the spring-forward:
if (zone.timeSpec() == Qt::TimeZone && zone.hasTransitions()) {
QTimeZone::OffsetData tran
// There's unlikely to be another transition before noon tomorrow.
// However, the whole of today may have been skipped !
= zone.previousTransition(QDateTime(addDays(1), QTime(12, 0), zone));
const QDateTime &at = tran.atUtc.toTimeZone(zone);
if (at.isValid() && at.date() == *this)
return at;
}
// The start of the day must have fallen in a spring-forward's gap; find the spring-forward:
if (zone.timeSpec() == Qt::TimeZone && zone.hasTransitions()) {
QTimeZone::OffsetData tran
// There's unlikely to be another transition before noon tomorrow.
// However, the whole of today may have been skipped !
= zone.previousTransition(QDateTime(addDays(1), QTime(12, 0), zone));
const QDateTime &at = tran.atUtc.toTimeZone(zone);
if (at.isValid() && at.date() == *this)
return at;
}
#endif
return toEarliest(*this, zone);
when = toEarliest(*this, zone);
}
return when;
}
/*!
@ -1002,7 +1008,10 @@ QDateTime QDate::startOfDay(Qt::TimeSpec spec, int offsetSeconds) const
static QDateTime toLatest(QDate day, const QTimeZone &zone)
{
Q_ASSERT(!zone.isUtcOrFixedOffset());
const auto moment = [=](QTime time) { return QDateTime(day, time, zone); };
// And the day ends in a gap. First find a moment not in that gap:
const auto moment = [=](QTime time) {
return QDateTime(day, time, zone, QDateTime::TransitionResolution::Reject);
};
// Longest routine time-zone transition is 2 hours:
QDateTime when = moment(QTime(21, 59, 59, 999));
if (!when.isValid()) {
@ -1020,7 +1029,8 @@ static QDateTime toLatest(QDate day, const QTimeZone &zone)
// Binary chop to the right minute
while (high > low + 1) {
const int mid = (high + low) / 2;
const QDateTime probe = moment(QTime(mid / 60, mid % 60, 59, 999));
const QDateTime probe = QDateTime(day, QTime(mid / 60, mid % 60, 59, 999), zone,
QDateTime::TransitionResolution::PreferAfter);
if (probe.isValid() && probe.date() == day) {
low = mid;
when = probe;
@ -1083,24 +1093,25 @@ QDateTime QDate::endOfDay(const QTimeZone &zone) const
if (!inDateTimeRange(jd, DaySide::End) || !zone.isValid())
return QDateTime();
QDateTime when(*this, QTime(23, 59, 59, 999), zone);
if (Q_LIKELY(when.isValid()))
return when;
QDateTime when(*this, QTime(23, 59, 59, 999), zone,
QDateTime::TransitionResolution::RelativeToAfter);
if (Q_UNLIKELY(!when.isValid() || when.date() != *this)) {
#if QT_CONFIG(timezone)
// The end of the day must have fallen in a spring-forward's gap; find the spring-forward:
if (zone.timeSpec() == Qt::TimeZone && zone.hasTransitions()) {
QTimeZone::OffsetData tran
// It's unlikely there's been another transition since yesterday noon.
// However, the whole of today may have been skipped !
= zone.nextTransition(QDateTime(addDays(-1), QTime(12, 0), zone));
const QDateTime &at = tran.atUtc.toTimeZone(zone);
if (at.isValid() && at.date() == *this)
return at;
}
// The end of the day must have fallen in a spring-forward's gap; find the spring-forward:
if (zone.timeSpec() == Qt::TimeZone && zone.hasTransitions()) {
QTimeZone::OffsetData tran
// It's unlikely there's been another transition since yesterday noon.
// However, the whole of today may have been skipped !
= zone.nextTransition(QDateTime(addDays(-1), QTime(12, 0), zone));
const QDateTime &at = tran.atUtc.toTimeZone(zone);
if (at.isValid() && at.date() == *this)
return at;
}
#endif
return toLatest(*this, zone);
when = toLatest(*this, zone);
}
return when;
}
/*!
@ -2728,11 +2739,99 @@ static auto millisToWithinRange(qint64 millis)
return result;
}
/*!
\internal
\enum QDateTimePrivate::TransitionOption
This enumeration is used to resolve datetime combinations which fall in \l
{Timezone transitions}. The transition is described as a "gap" if there are
time representations skipped over by the zone, as is common in the "spring
forward" transitions in many zones on entering daylight-saving time. The
transition is described as a "fold" if there are time representations
repeated in the zone, as in a "fall back" transition out of daylight-saving
time.
When the options specified do not determine a resolution for a datetime, it
is marked invalid.
The prepared option sets above are in fact composed from low-level atomic
options. For each of gap and fold you can chose between two candidate times,
one before or after the transition, based on the time requested; or you can
pick the moment of transition, or the start or end of the transition
interval. For a gap, the start and end of the interval are the moment of the
transition, but for a repeated interval the start of the first pass is the
start of the transition interval, the end of the second pass is the end of
the transition interval and the moment of the transition itself is both the
end of the first pass and the start of the second.
\value GapUseBefore For a time in a gap, use a time before the transition,
as if stepping back from a later time.
\value GapUseAfter For a time in a gap, use a time after the transition, as
if stepping forward from an earlier time.
\value FoldUseBefore For a repeated time, use the first candidate, which is
before the transition.
\value FoldUseAfter For a repeated time, use the second candidate, which is
after the transition.
\value FlipForReverseDst For "reversed" DST, this reverses the preceding
four options (see below).
The last has no effect unless the "daylight-saving" time side of the
transition is known to have a lower offset from UTC than the standard time
side. (This is the "reversed" DST case of \l {Timezone transitions}.) In
that case, if other options would select a time after the transition, a time
before is used instead, and vice versa. This effectively turns a preference
for the side with lower offset into a preference for the side that is
officially standard time, even if it has higher offset; and conversely a
preference for higher offset into a preference for daylight-saving time,
even if it has a lower offset. This option has no effect on a resolution
that selects the moment of transition or the start or end of the transition
interval.
The result of combining more than one of the \c GapUse* options is
undefined; likewise for the \c FoldUse*. Each of QDateTime's
TransitionResolution values, aside from Reject, maps to a combination that
incorporates one from each of these sets.
*/
constexpr static QDateTimePrivate::TransitionOptions
toTransitionOptions(QDateTime::TransitionResolution res)
{
switch (res) {
case QDateTime::TransitionResolution::RelativeToBefore:
return QDateTimePrivate::GapUseAfter | QDateTimePrivate::FoldUseBefore;
case QDateTime::TransitionResolution::RelativeToAfter:
return QDateTimePrivate::GapUseBefore | QDateTimePrivate::FoldUseAfter;
case QDateTime::TransitionResolution::PreferBefore:
return QDateTimePrivate::GapUseBefore | QDateTimePrivate::FoldUseBefore;
case QDateTime::TransitionResolution::PreferAfter:
return QDateTimePrivate::GapUseAfter | QDateTimePrivate::FoldUseAfter;
case QDateTime::TransitionResolution::PreferStandard:
return QDateTimePrivate::GapUseBefore
| QDateTimePrivate::FoldUseAfter
| QDateTimePrivate::FlipForReverseDst;
case QDateTime::TransitionResolution::PreferDaylightSaving:
return QDateTimePrivate::GapUseAfter
| QDateTimePrivate::FoldUseBefore
| QDateTimePrivate::FlipForReverseDst;
case QDateTime::TransitionResolution::Reject: break;
}
return {};
}
constexpr static QDateTimePrivate::TransitionOptions
toTransitionOptions(QDateTimePrivate::DaylightStatus dst)
{
return toTransitionOptions(dst == QDateTimePrivate::DaylightTime
? QDateTime::TransitionResolution::PreferDaylightSaving
: QDateTime::TransitionResolution::PreferStandard);
}
QString QDateTimePrivate::localNameAtMillis(qint64 millis, DaylightStatus dst)
{
const QDateTimePrivate::TransitionOptions resolve = toTransitionOptions(dst);
QString abbreviation;
if (millisInSystemRange(millis, MSECS_PER_DAY)) {
abbreviation = QLocalTime::localTimeAbbbreviationAt(millis, dst);
abbreviation = QLocalTime::localTimeAbbbreviationAt(millis, resolve);
if (!abbreviation.isEmpty())
return abbreviation;
}
@ -2742,7 +2841,7 @@ QString QDateTimePrivate::localNameAtMillis(qint64 millis, DaylightStatus dst)
// Use the system zone:
const auto sys = QTimeZone::systemTimeZone();
if (sys.isValid()) {
ZoneState state = zoneStateAtMillis(sys, millis, dst);
ZoneState state = zoneStateAtMillis(sys, millis, resolve);
if (state.valid)
return sys.d->abbreviation(state.when - state.offset * MSECS_PER_SEC);
}
@ -2752,19 +2851,20 @@ QString QDateTimePrivate::localNameAtMillis(qint64 millis, DaylightStatus dst)
// Use a time in the system range with the same day-of-week pattern to its year:
auto fake = millisToWithinRange(millis);
if (Q_LIKELY(fake.good))
return QLocalTime::localTimeAbbbreviationAt(fake.shifted, dst);
return QLocalTime::localTimeAbbbreviationAt(fake.shifted, resolve);
// Overflow, apparently.
return {};
}
// Determine the offset from UTC at the given local time as millis.
QDateTimePrivate::ZoneState QDateTimePrivate::localStateAtMillis(qint64 millis, DaylightStatus dst)
QDateTimePrivate::ZoneState QDateTimePrivate::localStateAtMillis(
qint64 millis, QDateTimePrivate::TransitionOptions resolve)
{
// First, if millis is within a day of the viable range, try mktime() in
// case it does fall in the range and gets useful information:
if (millisInSystemRange(millis, MSECS_PER_DAY)) {
auto result = QLocalTime::mapLocalTime(millis, dst);
auto result = QLocalTime::mapLocalTime(millis, resolve);
if (result.valid)
return result;
}
@ -2774,14 +2874,14 @@ QDateTimePrivate::ZoneState QDateTimePrivate::localStateAtMillis(qint64 millis,
// Use the system zone:
const auto sys = QTimeZone::systemTimeZone();
if (sys.isValid())
return zoneStateAtMillis(sys, millis, dst);
return zoneStateAtMillis(sys, millis, resolve);
#endif // timezone
// Kludge
// Use a time in the system range with the same day-of-week pattern to its year:
auto fake = millisToWithinRange(millis);
if (Q_LIKELY(fake.good)) {
auto result = QLocalTime::mapLocalTime(fake.shifted, dst);
auto result = QLocalTime::mapLocalTime(fake.shifted, resolve);
if (result.valid) {
qint64 adjusted;
if (Q_UNLIKELY(qAddOverflow(result.when, millis - fake.shifted, &adjusted))) {
@ -2799,33 +2899,26 @@ QDateTimePrivate::ZoneState QDateTimePrivate::localStateAtMillis(qint64 millis,
}
#if QT_CONFIG(timezone)
// For a TimeZone and a time expressed in zone msecs encoding, possibly with a
// hint to DST-ness, compute the actual DST-ness and offset, adjusting the time
// if needed to escape a spring-forward.
QDateTimePrivate::ZoneState QDateTimePrivate::zoneStateAtMillis(const QTimeZone &zone,
qint64 millis, DaylightStatus dst)
// For a TimeZone and a time expressed in zone msecs encoding, compute the
// actual DST-ness and offset, adjusting the time if needed to escape a
// spring-forward.
QDateTimePrivate::ZoneState QDateTimePrivate::zoneStateAtMillis(
const QTimeZone &zone, qint64 millis, QDateTimePrivate::TransitionOptions resolve)
{
Q_ASSERT(zone.isValid());
Q_ASSERT(zone.timeSpec() == Qt::TimeZone);
// Get the effective data from QTimeZone
QTimeZonePrivate::Data data = zone.d->dataForLocalTime(millis, int(dst));
if (data.offsetFromUtc == QTimeZonePrivate::invalidSeconds())
return {millis};
Q_ASSERT(zone.d->offsetFromUtc(data.atMSecsSinceEpoch) == data.offsetFromUtc);
return ZoneState(data.atMSecsSinceEpoch + data.offsetFromUtc * MSECS_PER_SEC,
data.offsetFromUtc,
data.daylightTimeOffset ? DaylightTime : StandardTime);
return zone.d->stateAtZoneTime(millis, resolve);
}
#endif // timezone
static inline QDateTimePrivate::ZoneState stateAtMillis(const QTimeZone &zone, qint64 millis,
QDateTimePrivate::DaylightStatus dst)
QDateTimePrivate::TransitionOptions resolve)
{
if (zone.timeSpec() == Qt::LocalTime)
return QDateTimePrivate::localStateAtMillis(millis, dst);
return QDateTimePrivate::localStateAtMillis(millis, resolve);
#if QT_CONFIG(timezone)
if (zone.timeSpec() == Qt::TimeZone && zone.isValid())
return QDateTimePrivate::zoneStateAtMillis(zone, millis, dst);
return QDateTimePrivate::zoneStateAtMillis(zone, millis, resolve);
#endif
return {millis};
}
@ -2938,7 +3031,8 @@ static inline bool usesSameOffset(const QDateTimeData &a, const QDateTimeData &b
}
// Refresh the LocalTime or TimeZone validity and offset
static void refreshZonedDateTime(QDateTimeData &d, const QTimeZone &zone)
static void refreshZonedDateTime(QDateTimeData &d, const QTimeZone &zone,
QDateTimePrivate::TransitionOptions resolve)
{
Q_ASSERT(zone.timeSpec() == Qt::TimeZone || zone.timeSpec() == Qt::LocalTime);
auto status = getStatus(d);
@ -2958,27 +3052,19 @@ static void refreshZonedDateTime(QDateTimeData &d, const QTimeZone &zone)
if (!status.testFlags(QDateTimePrivate::ValidDate | QDateTimePrivate::ValidTime)) {
status.setFlag(QDateTimePrivate::ValidDateTime, false);
} else {
// We have a valid date and time and a Qt::LocalTime or Qt::TimeZone that needs calculating
// LocalTime and TimeZone might fall into a "missing" DST transition hour
// Calling toEpochMSecs will adjust the returned date/time if it does
// We have a valid date and time and a Qt::LocalTime or Qt::TimeZone
// that might fall into a "missing" DST transition hour.
qint64 msecs = getMSecs(d);
QDateTimePrivate::ZoneState state = stateAtMillis(zone, msecs,
extractDaylightStatus(status));
// Save the offset to use in offsetFromUtc() &c., even if the next check
// marks invalid; this lets toMSecsSinceEpoch() give a useful fallback
// for times in spring-forward gaps.
offsetFromUtc = state.offset;
QDateTimePrivate::ZoneState state = stateAtMillis(zone, msecs, resolve);
Q_ASSERT(!state.valid || (state.offset >= -SECS_PER_DAY && state.offset <= SECS_PER_DAY));
if (Q_LIKELY(state.valid && msecs == state.when)) {
status = mergeDaylightStatus(status | QDateTimePrivate::ValidDateTime, state.dst);
} else { // msecs changed: gap, or failed to convert (e.g. overflow)
if (state.dst == QDateTimePrivate::UnknownDaylightTime) { // Overflow
status.setFlag(QDateTimePrivate::ValidDateTime, false);
if (state.valid) { // gap
/* Make sure our offset and msecs do produce the selected UTC
secs, if queried. When d isn't short, we record offset, so
need msecs to match; when d is short, consistency demands we
also update msecs, which will at least mean we don't hit the
gap again, if we ever recompute offset. */
} else if (state.valid) {
status = mergeDaylightStatus(status, state.dst);
offsetFromUtc = state.offset;
status.setFlag(QDateTimePrivate::ValidDateTime, true);
if (Q_UNLIKELY(msecs != state.when)) {
// Update msecs to the resolution:
if (status.testFlag(QDateTimePrivate::ShortData)) {
if (msecsCanBeSmall(state.when)) {
d.data.msecs = qintptr(state.when);
@ -2991,6 +3077,8 @@ static void refreshZonedDateTime(QDateTimeData &d, const QTimeZone &zone)
if (!status.testFlag(QDateTimePrivate::ShortData))
d->m_msecs = state.when;
}
} else {
status.setFlag(QDateTimePrivate::ValidDateTime, false);
}
}
@ -3017,7 +3105,7 @@ static void refreshSimpleDateTime(QDateTimeData &d)
}
// Clean up and set status after assorted set-up or reworking:
static void checkValidDateTime(QDateTimeData &d)
static void checkValidDateTime(QDateTimeData &d, QDateTime::TransitionResolution resolve)
{
auto spec = extractSpec(getStatus(d));
switch (spec) {
@ -3030,12 +3118,13 @@ static void checkValidDateTime(QDateTimeData &d)
case Qt::LocalTime:
// For these, we need to check whether (the zone is valid and) the time
// is valid for the zone. Expensive, but we have no other option.
refreshZonedDateTime(d, d.timeZone());
refreshZonedDateTime(d, d.timeZone(), toTransitionOptions(resolve));
break;
}
}
static void reviseTimeZone(QDateTimeData &d, QTimeZone zone)
static void reviseTimeZone(QDateTimeData &d, QTimeZone zone,
QDateTime::TransitionResolution resolve)
{
Qt::TimeSpec spec = zone.timeSpec();
auto status = mergeSpec(getStatus(d), spec);
@ -3074,7 +3163,7 @@ static void reviseTimeZone(QDateTimeData &d, QTimeZone zone)
if (QTimeZone::isUtcOrFixedOffset(spec))
refreshSimpleDateTime(d);
else
refreshZonedDateTime(d, zone);
refreshZonedDateTime(d, zone, toTransitionOptions(resolve));
}
static void setDateTime(QDateTimeData &d, QDate date, QTime time)
@ -3319,14 +3408,15 @@ inline QDateTimePrivate *QDateTime::Data::operator->()
*****************************************************************************/
Q_NEVER_INLINE
QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTimeZone &zone)
QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTimeZone &zone,
QDateTime::TransitionResolution resolve)
{
QDateTime::Data result(zone);
setDateTime(result, toDate, toTime);
if (zone.isUtcOrFixedOffset())
refreshSimpleDateTime(result);
else
refreshZonedDateTime(result, zone);
refreshZonedDateTime(result, zone, toTransitionOptions(resolve));
return result;
}
@ -3356,17 +3446,17 @@ QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTime
UTC of +3600 seconds is one hour ahead of UTC (usually written in ISO
standard notation as "UTC+01:00"), with no daylight-saving
complications. When using either local time or a specified time zone,
time-zone transitions (see \l {Daylight-Saving Time (DST)}{below}) are taken
into account. A QDateTime's timeSpec() will tell you which of the four types
of time representation is in use; its timeRepresentation() provides a full
representation of that time representation, as a QTimeZone.
time-zone transitions (see \l {Timezone transitions}{below}) are taken into
account. A QDateTime's timeSpec() will tell you which of the four types of
time representation is in use; its timeRepresentation() provides a full
description of that time representation, as a QTimeZone.
A QDateTime object is typically created either by giving a date and time
explicitly in the constructor, or by using a static function such as
currentDateTime() or fromMSecsSinceEpoch(). The date and time can be changed
with setDate() and setTime(). A datetime can also be set using the
setMSecsSinceEpoch() function that takes the time, in milliseconds, since
the start, in UTC of the year 1970. The fromString() function returns a
the start, in UTC, of the year 1970. The fromString() function returns a
QDateTime, given a string and a date format used to interpret the date
within the string.
@ -3400,10 +3490,10 @@ QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTime
(whose \l {QTimeZone::timeSpec()}{timeSpec()} is \c {Qt::TimeZone}) to use
that instead.
\note QDateTime does not account for leap seconds.
\section1 Remarks
\note QDateTime does not account for leap seconds.
\note All conversion to and from string formats is done using the C locale.
For localized conversions, see QLocale.
@ -3411,6 +3501,12 @@ QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTime
considered invalid. The year -1 is the year "1 before Christ" or "1 before
common era." The day before 1 January 1 CE is 31 December 1 BCE.
\note Using local time (the default) or a specified time zone implies a need
to resolve any issues around \l {Timezone transitions}{transitions}. As a
result, operations on such QDateTime instances (notably including
constructing them) may be more expensive than the equivalent when using UTC
or a fixed offset from it.
\section2 Range of Valid Dates
The range of values that QDateTime can represent is dependent on the
@ -3440,23 +3536,67 @@ QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTime
library will equipe QTimeZone with the same timezone database as is used on
Unix.
\section2 Daylight-Saving Time (DST)
\section2 Timezone transitions
QDateTime takes into account transitions between Standard Time and
Daylight-Saving Time. For example, if the transition is at 2am and the clock
goes forward to 3am, then there is a "missing" hour from 02:00:00 to
02:59:59.999 which QDateTime considers to be invalid. Any date arithmetic
performed will take this missing hour into account and return a valid
result. For example, adding one second to 01:59:59 will get 03:00:00.
QDateTime takes into account timezone transitions, both the transitions
between Standard Time and Daylight-Saving Time (DST) and the transitions
that arise when a zone changes its standard offset. For example, if the
transition is at 2am and the clock goes forward to 3am, then there is a
"missing" hour from 02:00:00 to 02:59:59.999. Such a transition is known as
a "spring forward" and the times skipped over have no meaning. When a
transition goes the other way, known as a "fall back", a time interval is
repeated, first in the old zone (usually DST), then in the new zone (usually
Standard Time), so times in this interval are ambiguous.
Some zones use "reversed" DST, using standard time in summer and
daylight-saving time (with a lowered offset) in winter. For such zones, the
spring forward still happens in spring and skips an hour, but is a
transition \e{out of} daylight-saving time, while the fall back still
repeats an autumn hour but is a transition \e to daylight-saving time.
When converting from a UTC time (or a time at fixed offset from UTC), there
is always an unambiguous valid result in any timezone. However, when
combining a date and time to make a datetime, expressed with respect to
local time or a specific time-zone, the nominal result may fall in a
transition, making it either invalid or ambiguous. Methods where this
situation may arise take a \c resolve parameter: this is always ignored if
the requested datetime is valid and unambiguous. See \l TransitionResolution
for the options it lets you control. Prior to Qt 6.7, the equivalent of its
\l LegacyBehavior was selected.
For a spring forward's skipped interval, interpreting the requested time
with either offset yields an actual time at which the other offset was in
use; so passing \c TransitionResolution::RelativeToBefore for \c resolve
will actually result in a time after the transition, that would have had the
requested representation had the transition not happened. Likewise, \c
TransitionResolution::RelativeToAfter for \c resolve results in a time
before the transition, that would have had the requested representation, had
the transition happened earlier.
When QDateTime performs arithmetic, as with addDay() or addSecs(), it takes
care to produce a valid result. For example, on a day when there is a spring
forward from 02:00 to 03:00, adding one second to 01:59:59 will get
03:00:00. Adding one day to 02:30 on the preceding day will get 03:30 on the
day of the transition, while subtracting one day, by calling \c{addDay(-1)},
to 02:30 on the following day will get 01:30 on the day of the transition.
While addSecs() will deliver a time offset by the given number of seconds,
addDays() adjusts the date and only adjusts time if it would otherwise get
an invalid result. Applying \c{addDays(1)} to 03:00 on the day before the
spring-forward will simply get 03:00 on the day of the transition, even
though the latter is only 23 hours after the former; but \c{addSecs(24 * 60
* 60)} will get 04:00 on the day of the transition, since that's 24 hours
later. Typical transitions make some days 23 or 25 hours long.
For datetimes that the system \c time_t can represent (from 1901-12-14 to
2038-01-18 on systems with 32-bit \c time_t; for the full range QDateTime
can represent if the type is 64-bit), the standard system APIs are used to
determine local time's offset from UTC. For datetimes not handled by these
system APIs, QTimeZone::systemTimeZone() is used. In either case, the offset
information used depends on the system and may be incomplete or, for past
times, historically inaccurate. In any case, for future dates, the local
time zone's offsets and DST rules may change before that date comes around.
system APIs (potentially including some within the \c time_t range),
QTimeZone::systemTimeZone() is used, if available, or a best effort is made
to estimate. In any case, the offset information used depends on the system
and may be incomplete or, for past times, historically
inaccurate. Furthermore, for future dates, the local time zone's offsets and
DST rules may change before that date comes around.
\section2 Offsets From UTC
@ -3471,7 +3611,8 @@ QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTime
which use a ±hh:mm format, effectively limiting the range to ± 99 hours and
59 minutes and whole minutes only. Note that currently no time zone has an
offset outside the range of ±14 hours and all known offsets are multiples of
five minutes.
five minutes. Historical time zones have a wider range and may have offsets
including seconds; these last cannot be faithfully represented in strings.
\sa QDate, QTime, QDateTimeEdit, QTimeZone
*/
@ -3496,6 +3637,148 @@ QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTime
\sa isValid(), QDate
*/
/*!
\since 6.7
\enum QDateTime::TransitionResolution
This enumeration is used to resolve datetime combinations which fall in \l
{Timezone transitions}.
When constructing a datetime, specified in terms of local time or a
time-zone that has daylight-saving time, or revising one with setDate(),
setTime() or setTimeZone(), the given parameters may imply a time
representation that either has no meaning or has two meanings in the
zone. Such time representations are described as being in the transition. In
either case, we can simply return an invalid datetime, to indicate that the
operation is ill-defined. In the ambiguous case, we can alternatively select
one of the two times that could be meant. When there is no meaning, we can
select a time either side of it that might plausibly have been meant. For
example, when advancing from an earlier time, we can select the time after
the transition that is actually the specified amount of time after the
earlier time in question. The options specified here configure how such
selection is performed.
\value Reject
Treat any time in a transition as invalid. Either it really is, or it
is ambiguous.
\value RelativeToBefore
Selects a time as if stepping forward from a time before the
transition. This interprets the requested time using the offset in
effect before the transition and, if necessary, converts the result
to the offset in effect at the resulting time.
\value RelativeToAfter
Select a time as if stepping backward from a time after the
transition. This interprets the requested time using the offset in
effect after the transition and, if necessary, converts the result to
the offset in effect at the resulting time.
\value PreferBefore
Selects a time before the transition,
\value PreferAfter
Selects a time after the transition.
\value PreferStandard
Selects a time on the standard time side of the transition.
\value PreferDaylightSaving
Selects a time on the daylight-saving-time side of the transition.
\value LegacyBehavior
An alias for RelativeToBefore, which is used as default for
TransitionResolution parameters, as this most closely matches the
behavior prior to Qt 6.7.
For \l addDays(), \l addMonths() or \l addYears(), the behavior is and
(mostly) was to use \c RelativeToBefore if adding a positive adjustment and \c
RelativeToAfter if adding a negative adjustment.
\note In time zones where daylight-saving increases the offset from UTC in
summer (known as "positive DST"), PreferStandard is an alias for
RelativeToAfter and PreferDaylightSaving for RelativeToBefore. In time zones
where the daylight-saving mechanism is a decrease in offset from UTC in
winter (known as "negative DST"), the reverse applies, provided the
operating system reports - as it does on most platforms - whether a datetime
is in DST or standard time. For some platforms, where transition times are
unavailable even for Qt::TimeZone datetimes, QTimeZone is obliged to presume
that the side with lower offset from UTC is standard time, effectively
assuming positive DST.
The following tables illustrate how a QDateTime constructor resolves a
request for 02:30 on a day when local time has a transition between 02:00
and 03:00, with a nominal standard time LST and daylight-saving time LDT on
the two sides, in the various possible cases. The transition type may be to
skip an hour or repeat it. The type of transition and value of a parameter
\c resolve determine which actual time on the given date is selected. First,
the common case of positive daylight-saving, where:
\table
\header \li Before \li 02:00--03:00 \li After \li \c resolve \li selected
\row \li LST \li skip \li LDT \li RelativeToBefore \li 03:30 LDT
\row \li LST \li skip \li LDT \li RelativeToAfter \li 01:30 LST
\row \li LST \li skip \li LDT \li PreferBefore \li 01:30 LST
\row \li LST \li skip \li LDT \li PreferAfter \li 03:30 LDT
\row \li LST \li skip \li LDT \li PreferStandard \li 01:30 LST
\row \li LST \li skip \li LDT \li PreferDaylightSaving \li 03:30 LDT
\row \li LDT \li repeat \li LST \li RelativeToBefore \li 02:30 LDT
\row \li LDT \li repeat \li LST \li RelativeToAfter \li 02:30 LST
\row \li LDT \li repeat \li LST \li PreferBefore \li 02:30 LDT
\row \li LDT \li repeat \li LST \li PreferAfter \li 02:30 LST
\row \li LDT \li repeat \li LST \li PreferStandard \li 02:30 LST
\row \li LDT \li repeat \li LST \li PreferDaylightSaving \li 02:30 LDT
\endtable
Second, the case for negative daylight-saving, using LDT in winter and
skipping an hour to transition to LST in summer, then repeating an hour at
the transition back to winter:
\table
\row \li LDT \li skip \li LST \li RelativeToBefore \li 03:30 LST
\row \li LDT \li skip \li LST \li RelativeToAfter \li 01:30 LDT
\row \li LDT \li skip \li LST \li PreferBefore \li 01:30 LDT
\row \li LDT \li skip \li LST \li PreferAfter \li 03:30 LST
\row \li LDT \li skip \li LST \li PreferStandard \li 03:30 LST
\row \li LDT \li skip \li LST \li PreferDaylightSaving \li 01:30 LDT
\row \li LST \li repeat \li LDT \li RelativeToBefore \li 02:30 LST
\row \li LST \li repeat \li LDT \li RelativeToAfter \li 02:30 LDT
\row \li LST \li repeat \li LDT \li PreferBefore \li 02:30 LST
\row \li LST \li repeat \li LDT \li PreferAfter \li 02:30 LDT
\row \li LST \li repeat \li LDT \li PreferStandard \li 02:30 LST
\row \li LST \li repeat \li LDT \li PreferDaylightSaving \li 02:30 LDT
\endtable
Reject can be used to prompt relevant QDateTime APIs to return an invalid
datetime object so that your code can deal with transitions for itself, for
example by alerting a user to the fact that the datetime they have selected
is in a transition interval, to offer them the opportunity to resolve a
conflict or ambiguity. Code using this may well find the other options above
useful to determine relevant information to use in its own (or the user's)
resolution. If the start or end of the transition, or the moment of the
transition itself, is the right resolution, QTimeZone's transition APIs can
be used to obtain that information. You can determine whether the transition
is a repeated or skipped interval by using \l secsTo() to measure the actual
time between noon on the previous and following days. The result will be
less than 48 hours for a skipped interval (such as a spring-forward) and
more than 48 hours for a repeated interval (such as a fall-back).
\note When a resolution other than Reject is specified, a valid QDateTime
object is returned, if possible. If the requested date-time falls in a gap,
the returned date-time will not have the time() requested - or, in some
cases, the date(), if a whole day was skipped. You can thus detect when a
gap is hit by comparing date() and time() to what was requested.
\section2 Relation to other datetime software
The Python programming language's datetime APIs have a \c fold parameter
that corresponds to \c RelativeToBefore (\c{fold = True}) and \c
RelativeToAfter (\c{fold = False}).
The \c Temporal proposal to replace JavaScript's \c Date offers four options
for how to resolve a transition, as value for a \c disambiguation
parameter. Its \c{'reject'} raises an exception, which roughly corresponds
to \c Reject producing an invalid result. Its \c{'earlier'} and \c{'later'}
options correspond to \c PreferBefore and \c PreferAfter. Its
\c{'compatible'} option corresponds to \c RelativeToBefore (and Python's
\c{fold = True}).
\sa {Timezone transitions}, QDateTime::TransitionResolution
*/
/*!
Constructs a null datetime, nominally using local time.
@ -3534,7 +3817,8 @@ QDateTime::QDateTime() noexcept
skipped over the given date and time, the result is invalid.
*/
QDateTime::QDateTime(QDate date, QTime time, Qt::TimeSpec spec, int offsetSeconds)
: d(QDateTimePrivate::create(date, time, asTimeZone(spec, offsetSeconds, "QDateTime")))
: d(QDateTimePrivate::create(date, time, asTimeZone(spec, offsetSeconds, "QDateTime"),
TransitionResolution::LegacyBehavior))
{
}
#endif // 6.9 deprecation
@ -3546,24 +3830,36 @@ QDateTime::QDateTime(QDate date, QTime time, Qt::TimeSpec spec, int offsetSecond
representation described by \a timeZone.
If \a date is valid and \a time is not, the time will be set to midnight.
If \a timeZone is invalid then the datetime will be invalid.
If \a timeZone is invalid then the datetime will be invalid. If \a date and
\a time describe a moment close to a transition for \a timeZone, \a resolve
controls how that situation is resolved.
//! [pre-resolve-note]
\note Prior to Qt 6.7, the version of this function lacked the \a resolve
parameter so had no way to resolve the ambiguities related to transitions.
//! [pre-resolve-note]
*/
QDateTime::QDateTime(QDate date, QTime time, const QTimeZone &timeZone)
: d(QDateTimePrivate::create(date, time, timeZone))
QDateTime::QDateTime(QDate date, QTime time, const QTimeZone &timeZone, TransitionResolution resolve)
: d(QDateTimePrivate::create(date, time, timeZone, resolve))
{
}
/*!
\since 6.5
\overload
Constructs a datetime with the given \a date and \a time, using local time.
If \a date is valid and \a time is not, midnight will be used as the time.
If \a date is valid and \a time is not, midnight will be used as the
time. If \a date and \a time describe a moment close to a transition for
local time, \a resolve controls how that situation is resolved.
\include qdatetime.cpp pre-resolve-note
*/
QDateTime::QDateTime(QDate date, QTime time)
: d(QDateTimePrivate::create(date, time, QTimeZone::LocalTime))
QDateTime::QDateTime(QDate date, QTime time, TransitionResolution resolve)
: d(QDateTimePrivate::create(date, time, QTimeZone::LocalTime, resolve))
{
}
@ -3754,8 +4050,8 @@ int QDateTime::offsetFromUtc() const
auto spec = extractSpec(status);
if (spec == Qt::LocalTime) {
// We didn't cache the value, so we need to calculate it:
auto dst = extractDaylightStatus(status);
return QDateTimePrivate::localStateAtMillis(getMSecs(d), dst).offset;
const auto resolve = toTransitionOptions(extractDaylightStatus(status));
return QDateTimePrivate::localStateAtMillis(getMSecs(d), resolve).offset;
}
Q_ASSERT(spec == Qt::UTC);
@ -3840,8 +4136,10 @@ bool QDateTime::isDaylightTime() const
#endif // timezone
case Qt::LocalTime: {
auto dst = extractDaylightStatus(getStatus(d));
if (dst == QDateTimePrivate::UnknownDaylightTime)
dst = QDateTimePrivate::localStateAtMillis(getMSecs(d), dst).dst;
if (dst == QDateTimePrivate::UnknownDaylightTime) {
dst = QDateTimePrivate::localStateAtMillis(
getMSecs(d), toTransitionOptions(TransitionResolution::LegacyBehavior)).dst;
}
return dst == QDateTimePrivate::DaylightTime;
}
}
@ -3849,16 +4147,24 @@ bool QDateTime::isDaylightTime() const
}
/*!
Sets the date part of this datetime to \a date. If no time is set yet, it
is set to midnight. If \a date is invalid, this QDateTime becomes invalid.
Sets the date part of this datetime to \a date.
If no time is set yet, it is set to midnight. If \a date is invalid, this
QDateTime becomes invalid.
If \a date and time() describe a moment close to a transition for this
datetime's time representation, \a resolve controls how that situation is
resolved.
\include qdatetime.cpp pre-resolve-note
\sa date(), setTime(), setTimeZone()
*/
void QDateTime::setDate(QDate date)
void QDateTime::setDate(QDate date, TransitionResolution resolve)
{
setDateTime(d, date, time());
checkValidDateTime(d);
checkValidDateTime(d, resolve);
}
/*!
@ -3871,13 +4177,19 @@ void QDateTime::setDate(QDate date)
dt.setTime(QTime());
\endcode
If date() and \a time describe a moment close to a transition for this
datetime's time representation, \a resolve controls how that situation is
resolved.
\include qdatetime.cpp pre-resolve-note
\sa time(), setDate(), setTimeZone()
*/
void QDateTime::setTime(QTime time)
void QDateTime::setTime(QTime time, TransitionResolution resolve)
{
setDateTime(d, date(), time);
checkValidDateTime(d);
checkValidDateTime(d, resolve);
}
#if QT_DEPRECATED_SINCE(6, 9)
@ -3901,7 +4213,8 @@ void QDateTime::setTime(QTime time)
void QDateTime::setTimeSpec(Qt::TimeSpec spec)
{
reviseTimeZone(d, asTimeZone(spec, 0, "QDateTime::setTimeSpec"));
reviseTimeZone(d, asTimeZone(spec, 0, "QDateTime::setTimeSpec"),
TransitionResolution::LegacyBehavior);
}
/*!
@ -3922,7 +4235,8 @@ void QDateTime::setTimeSpec(Qt::TimeSpec spec)
void QDateTime::setOffsetFromUtc(int offsetSeconds)
{
reviseTimeZone(d, QTimeZone::fromSecondsAheadOfUtc(offsetSeconds));
reviseTimeZone(d, QTimeZone::fromSecondsAheadOfUtc(offsetSeconds),
TransitionResolution::Reject);
}
#endif // 6.9 deprecations
@ -3938,12 +4252,17 @@ void QDateTime::setOffsetFromUtc(int offsetSeconds)
If \a toZone is invalid then the datetime will be invalid. Otherwise, this
datetime's timeSpec() after the call will match \c{toZone.timeSpec()}.
If date() and time() describe a moment close to a transition for \a toZone,
\a resolve controls how that situation is resolved.
\include qdatetime.cpp pre-resolve-note
\sa timeRepresentation(), timeZone(), Qt::TimeSpec
*/
void QDateTime::setTimeZone(const QTimeZone &toZone)
void QDateTime::setTimeZone(const QTimeZone &toZone, TransitionResolution resolve)
{
reviseTimeZone(d, toZone);
reviseTimeZone(d, toZone, resolve);
}
/*!
@ -3983,8 +4302,8 @@ qint64 QDateTime::toMSecsSinceEpoch() const
case Qt::LocalTime:
if (status.testFlag(QDateTimePrivate::ShortData)) {
// Short form has nowhere to cache the offset, so recompute.
auto dst = extractDaylightStatus(status);
auto state = QDateTimePrivate::localStateAtMillis(getMSecs(d), dst);
const auto resolve = toTransitionOptions(extractDaylightStatus(getStatus(d)));
const auto state = QDateTimePrivate::localStateAtMillis(getMSecs(d), resolve);
return state.when - state.offset * MSECS_PER_SEC;
}
// Use the offset saved by refreshZonedDateTime() on creation.
@ -4248,19 +4567,11 @@ QString QDateTime::toString(QStringView format, QCalendar cal) const
}
#endif // datestring
static inline void massageAdjustedDateTime(QDateTimeData &d, QDate date, QTime time)
static inline void massageAdjustedDateTime(QDateTimeData &d, QDate date, QTime time, bool forward)
{
/*
If we have just adjusted to a day with a DST transition, our given time
may lie in the transition hour (either missing or duplicated). For any
other time, telling mktime() or QTimeZone what we know about DST-ness, of
the time we adjusted from, will make no difference; it'll just tell us the
actual DST-ness of the given time. When landing in a transition that
repeats an hour, passing the prior DST-ness - when known - will get us the
indicated side of the duplicate (either local or zone). When landing in a
gap, the zone gives us the other side of the gap and mktime() is wrapped
to coax it into doing the same (which it does by default on Unix).
*/
const QDateTimePrivate::TransitionOptions resolve = toTransitionOptions(
forward ? QDateTime::TransitionResolution::RelativeToBefore
: QDateTime::TransitionResolution::RelativeToAfter);
auto status = getStatus(d);
Q_ASSERT(status.testFlags(QDateTimePrivate::ValidDate | QDateTimePrivate::ValidTime
| QDateTimePrivate::ValidDateTime));
@ -4270,13 +4581,13 @@ static inline void massageAdjustedDateTime(QDateTimeData &d, QDate date, QTime t
refreshSimpleDateTime(d);
return;
}
auto dst = extractDaylightStatus(status);
qint64 local = timeToMSecs(date, time);
const QDateTimePrivate::ZoneState state = stateAtMillis(d.timeZone(), local, dst);
if (state.valid)
status = mergeDaylightStatus(status | QDateTimePrivate::ValidDateTime, state.dst);
else
const QDateTimePrivate::ZoneState state = stateAtMillis(d.timeZone(), local, resolve);
Q_ASSERT(state.valid || state.dst == QDateTimePrivate::UnknownDaylightTime);
if (state.dst == QDateTimePrivate::UnknownDaylightTime)
status.setFlag(QDateTimePrivate::ValidDateTime, false);
else
status = mergeDaylightStatus(status | QDateTimePrivate::ValidDateTime, state.dst);
if (status & QDateTimePrivate::ShortData) {
d.data.msecs = state.when;
@ -4303,7 +4614,7 @@ static inline void massageAdjustedDateTime(QDateTimeData &d, QDate date, QTime t
aiming between 2am and 3am will be adjusted to fall before 2am (if \c{ndays
< 0}) or after 3am (otherwise).
\sa daysTo(), addMonths(), addYears(), addSecs()
\sa daysTo(), addMonths(), addYears(), addSecs(), {Timezone transitions}
*/
QDateTime QDateTime::addDays(qint64 ndays) const
@ -4313,7 +4624,7 @@ QDateTime QDateTime::addDays(qint64 ndays) const
QDateTime dt(*this);
QPair<QDate, QTime> p = getDateTime(d);
massageAdjustedDateTime(dt.d, p.first.addDays(ndays), p.second);
massageAdjustedDateTime(dt.d, p.first.addDays(ndays), p.second, ndays >= 0);
return dt;
}
@ -4329,7 +4640,7 @@ QDateTime QDateTime::addDays(qint64 ndays) const
aiming between 2am and 3am will be adjusted to fall before 2am (if
\c{nmonths < 0}) or after 3am (otherwise).
\sa daysTo(), addDays(), addYears(), addSecs()
\sa daysTo(), addDays(), addYears(), addSecs(), {Timezone transitions}
*/
QDateTime QDateTime::addMonths(int nmonths) const
@ -4339,7 +4650,7 @@ QDateTime QDateTime::addMonths(int nmonths) const
QDateTime dt(*this);
QPair<QDate, QTime> p = getDateTime(d);
massageAdjustedDateTime(dt.d, p.first.addMonths(nmonths), p.second);
massageAdjustedDateTime(dt.d, p.first.addMonths(nmonths), p.second, nmonths >= 0);
return dt;
}
@ -4355,7 +4666,7 @@ QDateTime QDateTime::addMonths(int nmonths) const
aiming between 2am and 3am will be adjusted to fall before 2am (if \c{nyears
< 0}) or after 3am (otherwise).
\sa daysTo(), addDays(), addMonths(), addSecs()
\sa daysTo(), addDays(), addMonths(), addSecs(), {Timezone transitions}
*/
QDateTime QDateTime::addYears(int nyears) const
@ -4365,7 +4676,7 @@ QDateTime QDateTime::addYears(int nyears) const
QDateTime dt(*this);
QPair<QDate, QTime> p = getDateTime(d);
massageAdjustedDateTime(dt.d, p.first.addYears(nyears), p.second);
massageAdjustedDateTime(dt.d, p.first.addYears(nyears), p.second, nyears >= 0);
return dt;
}
@ -5109,7 +5420,7 @@ QDateTime QDateTime::fromSecsSinceEpoch(qint64 secs, Qt::TimeSpec spec, int offs
QDateTime QDateTime::fromMSecsSinceEpoch(qint64 msecs, const QTimeZone &timeZone)
{
QDateTime dt;
reviseTimeZone(dt.d, timeZone);
reviseTimeZone(dt.d, timeZone, TransitionResolution::Reject);
if (timeZone.isValid())
dt.setMSecsSinceEpoch(msecs);
return dt;
@ -5141,7 +5452,7 @@ QDateTime QDateTime::fromMSecsSinceEpoch(qint64 msecs)
QDateTime QDateTime::fromSecsSinceEpoch(qint64 secs, const QTimeZone &timeZone)
{
QDateTime dt;
reviseTimeZone(dt.d, timeZone);
reviseTimeZone(dt.d, timeZone, TransitionResolution::Reject);
if (timeZone.isValid())
dt.setSecsSinceEpoch(secs);
return dt;
@ -5355,10 +5666,8 @@ QDateTime QDateTime::fromString(QStringView string, Qt::DateFormat format)
If the format is not satisfied, an invalid QDateTime is returned. If the
format is satisfied but \a string represents an invalid datetime (e.g. in a
gap skipped by a time-zone transition), an invalid QDateTime is returned,
whose toMSecsSinceEpoch() represents a near-by datetime that is
valid. Passing that to fromMSecsSinceEpoch() will produce a valid datetime
that isn't faithfully represented by the string parsed.
gap skipped by a time-zone transition), an valid QDateTime is returned, that
represents a near-by datetime that is valid.
The expressions that don't have leading zeroes (d, M, h, m, s, z) will be
greedy. This means that they will use two digits (or three, for z) even if this will
@ -5609,6 +5918,7 @@ QDataStream &operator>>(QDataStream &in, QDateTime &dateTime)
in >> zone;
break;
}
// Note: no way to resolve transition ambiguity, when relevant; use default.
dateTime = QDateTime(dt, tm, zone);
} else if (in.version() == QDataStream::Qt_5_0) {

View File

@ -311,12 +311,31 @@ class Q_CORE_EXPORT QDateTime
public:
QDateTime() noexcept;
enum class TransitionResolution {
Reject = 0,
RelativeToBefore,
RelativeToAfter,
PreferBefore,
PreferAfter,
PreferStandard,
PreferDaylightSaving,
// Closest match to behavior prior to introducing TransitionResolution:
LegacyBehavior = RelativeToBefore
};
#if QT_DEPRECATED_SINCE(6, 9)
QT_DEPRECATED_VERSION_X_6_9("Pass QTimeZone instead")
QDateTime(QDate date, QTime time, Qt::TimeSpec spec, int offsetSeconds = 0);
#endif
#if QT_CORE_REMOVED_SINCE(6, 7)
QDateTime(QDate date, QTime time, const QTimeZone &timeZone);
QDateTime(QDate date, QTime time);
#endif
QDateTime(QDate date, QTime time, const QTimeZone &timeZone,
TransitionResolution resolve = TransitionResolution::LegacyBehavior);
QDateTime(QDate date, QTime time,
TransitionResolution resolve = TransitionResolution::LegacyBehavior);
QDateTime(const QDateTime &other) noexcept;
QDateTime(QDateTime &&other) noexcept;
~QDateTime();
@ -343,15 +362,24 @@ public:
qint64 toMSecsSinceEpoch() const;
qint64 toSecsSinceEpoch() const;
#if QT_CORE_REMOVED_SINCE(6, 7)
void setDate(QDate date);
void setTime(QTime time);
#endif
void setDate(QDate date, TransitionResolution resolve = TransitionResolution::LegacyBehavior);
void setTime(QTime time, TransitionResolution resolve = TransitionResolution::LegacyBehavior);
#if QT_DEPRECATED_SINCE(6, 9)
QT_DEPRECATED_VERSION_X_6_9("Use setTimeZone() instead")
void setTimeSpec(Qt::TimeSpec spec);
QT_DEPRECATED_VERSION_X_6_9("Use setTimeZone() instead")
void setOffsetFromUtc(int offsetSeconds);
#endif
#if QT_CORE_REMOVED_SINCE(6, 7)
void setTimeZone(const QTimeZone &toZone);
#endif
void setTimeZone(const QTimeZone &toZone,
TransitionResolution resolve = TransitionResolution::LegacyBehavior);
void setMSecsSinceEpoch(qint64 msecs);
void setSecsSinceEpoch(qint64 secs);

View File

@ -73,6 +73,22 @@ public:
};
Q_DECLARE_FLAGS(StatusFlags, StatusFlag)
enum TransitionOption {
// Handling of a spring-forward (or other gap):
GapUseBefore = 2,
GapUseAfter = 4,
// Handling of a fall-back (or other repeated period):
FoldUseBefore = 0x20,
FoldUseAfter = 0x40,
// Quirk for negative DST:
FlipForReverseDst = 0x400,
GapMask = GapUseBefore | GapUseAfter,
FoldMask = FoldUseBefore | FoldUseAfter,
};
Q_DECLARE_FLAGS(TransitionOptions, TransitionOption)
enum {
TimeSpecShift = 4,
};
@ -89,14 +105,16 @@ public:
: when(w), offset(o), dst(d), valid(v) {}
};
static QDateTime::Data create(QDate toDate, QTime toTime, const QTimeZone &timeZone);
static QDateTime::Data create(QDate toDate, QTime toTime, const QTimeZone &timeZone,
QDateTime::TransitionResolution resolve);
#if QT_CONFIG(timezone)
static ZoneState zoneStateAtMillis(const QTimeZone &zone, qint64 millis, DaylightStatus dst);
static ZoneState zoneStateAtMillis(const QTimeZone &zone, qint64 millis,
TransitionOptions resolve);
#endif // timezone
static ZoneState expressUtcAsLocal(qint64 utcMSecs);
static ZoneState localStateAtMillis(qint64 millis, DaylightStatus dst);
static ZoneState localStateAtMillis(qint64 millis, TransitionOptions resolve);
static QString localNameAtMillis(qint64 millis, DaylightStatus dst); // empty if unknown
StatusFlags m_status = StatusFlag(Qt::LocalTime << TimeSpecShift);
@ -106,6 +124,7 @@ public:
};
Q_DECLARE_OPERATORS_FOR_FLAGS(QDateTimePrivate::StatusFlags)
Q_DECLARE_OPERATORS_FOR_FLAGS(QDateTimePrivate::TransitionOptions)
namespace QtPrivate {
namespace DateTimeConstants {

View File

@ -798,11 +798,6 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
&& m_text.at(offset) == u'-');
const int negativeYearOffset = negate ? 1 : 0;
// If the fields we've read thus far imply a time in a spring-forward,
// coerce to a nearby valid time:
const QDateTime defaultValue = currentValue.isValid() ? currentValue
: QDateTime::fromMSecsSinceEpoch(currentValue.toMSecsSinceEpoch());
QStringView sectionTextRef =
QStringView { m_text }.mid(offset + negativeYearOffset, sectionmaxsize);
@ -838,7 +833,7 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
m_text.replace(offset, used, sectiontext.constData(), used);
break; }
case TimeZoneSection:
result = findTimeZone(sectionTextRef, defaultValue,
result = findTimeZone(sectionTextRef, currentValue,
absoluteMax(sectionIndex),
absoluteMin(sectionIndex), sn.count);
break;
@ -850,7 +845,7 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
int num = 0, used = 0;
if (sn.type == MonthSection) {
const QDate minDate = getMinimum().date();
const int year = defaultValue.date().year(calendar);
const int year = currentValue.date().year(calendar);
const int min = (year == minDate.year(calendar)) ? minDate.month(calendar) : 1;
num = findMonth(sectiontext.toLower(), min, sectionIndex, year, &sectiontext, &used);
} else {
@ -955,7 +950,7 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
}
} else if (unfilled && (fi & (FixedWidth | Numeric)) == (FixedWidth | Numeric)) {
if (skipToNextSection(sectionIndex, defaultValue, digitsStr)) {
if (skipToNextSection(sectionIndex, currentValue, digitsStr)) {
const int missingZeroes = sectionmaxsize - digitsStr.size();
result = ParsedSection(Acceptable, lastVal, sectionmaxsize, missingZeroes);
m_text.insert(offset, QString(missingZeroes, u'0'));
@ -1432,31 +1427,40 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
const QTime time(hour, minute, second, msec);
const QDateTime when = QDateTime(date, time, timeZone);
// If hour wasn't specified, check the default we're using exists on the
// given date (which might be a spring-forward, skipping an hour).
if (!(isSet & HourSectionMask) && !when.isValid()) {
switch (parserType) {
case QMetaType::QDateTime: {
qint64 msecs = when.toMSecsSinceEpoch();
// Fortunately, that gets a useful answer, even though when is invalid ...
const QDateTime replace = QDateTime::fromMSecsSinceEpoch(msecs, timeZone);
const QTime tick = replace.time();
if (replace.date() == date
&& (!(isSet & MinuteSection) || tick.minute() == minute)
&& (!(isSet & SecondSection) || tick.second() == second)
&& (!(isSet & MSecSection) || tick.msec() == msec)) {
return StateNode(replace, state, padding, conflicts);
if (when.time() != time || when.date() != date) {
// In a spring-forward, if we hit the skipped hour, we may have been
// shunted out of it.
// If hour wasn't specified, so we're using our default, changing it may
// fix that.
if (!(isSet & HourSectionMask)) {
switch (parserType) {
case QMetaType::QDateTime: {
qint64 msecs = when.toMSecsSinceEpoch();
// Fortunately, that gets a useful answer, even though when is invalid ...
const QDateTime replace = QDateTime::fromMSecsSinceEpoch(msecs, timeZone);
const QTime tick = replace.time();
if (replace.date() == date
&& (!(isSet & MinuteSection) || tick.minute() == minute)
&& (!(isSet & SecondSection) || tick.second() == second)
&& (!(isSet & MSecSection) || tick.msec() == msec)) {
return StateNode(replace, state, padding, conflicts);
}
} break;
case QMetaType::QDate:
// Don't care about time, so just use start of day (and ignore spec):
return StateNode(date.startOfDay(QTimeZone::UTC),
state, padding, conflicts);
break;
case QMetaType::QTime:
// Don't care about date or representation, so pick a safe representation:
return StateNode(QDateTime(date, time, QTimeZone::UTC),
state, padding, conflicts);
default:
Q_UNREACHABLE_RETURN(StateNode());
}
} break;
case QMetaType::QDate:
// Don't care about time, so just use start of day (and ignore spec):
return StateNode(date.startOfDay(QTimeZone::UTC), state, padding, conflicts);
break;
case QMetaType::QTime:
// Don't care about date or representation, so pick a safe representation:
return StateNode(QDateTime(date, time, QTimeZone::UTC), state, padding, conflicts);
default:
Q_UNREACHABLE_RETURN(StateNode());
} else if (state > Intermediate) {
state = Intermediate;
}
}
@ -1607,12 +1611,8 @@ QDateTimeParser::parse(const QString &input, int position,
}
}
/*
We might have ended up with an invalid datetime: the non-existent hour
during dst changes, for instance.
*/
if (!scan.value.isValid() && scan.state == Acceptable)
scan.state = Intermediate;
// An invalid time should only arise if we set the state to less than acceptable:
Q_ASSERT(scan.value.isValid() || scan.state != Acceptable);
return scan;
}
@ -2233,7 +2233,7 @@ bool QDateTimeParser::fromString(const QString &t, QDateTime *datetime) const
const StateNode tmp = parse(t, -1, defaultLocalTime, false);
if (datetime)
*datetime = tmp.value;
return tmp.state == Acceptable && !tmp.conflicts && tmp.value.isValid();
return tmp.state >= Intermediate && !tmp.conflicts && tmp.value.isValid();
}
QDateTime QDateTimeParser::getMinimum() const

View File

@ -274,13 +274,17 @@ MkTimeResult hopAcrossGap(const MkTimeResult &outside, const struct tm &base)
Q_DECL_COLD_FUNCTION
MkTimeResult resolveRejected(struct tm base, MkTimeResult result,
QDateTimePrivate::DaylightStatus dst)
QDateTimePrivate::TransitionOptions resolve)
{
// May result from a time outside the supported range of system time_t
// functions, or from a gap (on a platform where mktime() rejects them).
// QDateTime filters on times well outside the supported range, but may
// pass values only slightly outside the range.
// The easy case - no need to find a resolution anyway:
if (!resolve.testAnyFlags(QDateTimePrivate::GapMask))
return {};
constexpr time_t twoDaysInSeconds = 2 * 24 * 60 * 60;
// Bracket base, one day each side (in case the zone skipped a whole day):
MkTimeResult early(adjacentDay(base, -1));
@ -291,32 +295,15 @@ MkTimeResult resolveRejected(struct tm base, MkTimeResult result,
// OK, looks like a gap.
Q_ASSERT(twoDaysInSeconds + early.utcSecs > later.utcSecs);
result.adjusted = true;
// When simply constructing a gap-time, dst is unknown and construction will
// leave us with a time outside the gap, so later calls to rediscover its
// offset won't hit the gap. So if we've hit a gap and think we know dst,
// it's because addDays() or similar has moved us from the side we think
// we're on, which means we should over-shoot and get the opposite DST.
// A gap is usually followed by DST - except for "negative DST", where
// early's tm_isdst is 1 and later's isn't. Default to using 24h after
// early (which shall fall after the gap).
enum { AfterEarly, BeforeLater } choice = AfterEarly;
switch (dst) {
case QDateTimePrivate::UnknownDaylightTime:
break;
case QDateTimePrivate::StandardTime:
// Aiming for DST, so AfterEarly is OK, unless DST is reversed:
if (early.local.tm_isdst == 1 && later.local.tm_isdst != 1)
choice = BeforeLater;
break;
case QDateTimePrivate::DaylightTime:
// Aiming for standard, so only retain AfterEarly if DST is reversed:
if (early.local.tm_isdst != 1 || later.local.tm_isdst == 1)
choice = BeforeLater;
break;
// Extrapolate backwards from later if this option is set:
QDateTimePrivate::TransitionOption beforeLater = QDateTimePrivate::GapUseBefore;
if (resolve.testFlag(QDateTimePrivate::FlipForReverseDst)) {
// Reverse DST has DST before a gap and not after:
if (early.local.tm_isdst == 1 && !later.local.tm_isdst)
beforeLater = QDateTimePrivate::GapUseAfter;
}
if (choice == BeforeLater) // Result will be before the gap:
if (resolve.testFlag(beforeLater)) // Result will be before the gap:
result.utcSecs = later.utcSecs - secondsBetween(base, later.local);
else // Result will be after the gap:
result.utcSecs = early.utcSecs + secondsBetween(early.local, base);
@ -328,7 +315,7 @@ MkTimeResult resolveRejected(struct tm base, MkTimeResult result,
}
Q_DECL_COLD_FUNCTION
bool preferAlternative(QDateTimePrivate::DaylightStatus dst,
bool preferAlternative(QDateTimePrivate::TransitionOptions resolve,
// is_dst flags of incumbent and an alternative:
int gotDst, int altDst,
// True precisely if alternative selects a later UTC time:
@ -336,35 +323,31 @@ bool preferAlternative(QDateTimePrivate::DaylightStatus dst,
// True for a gap, false for a fold:
bool inGap)
{
if (dst == QDateTimePrivate::UnknownDaylightTime)
return altIsLater; // Prefer later candidate
// gotDst and altDst are {-1: unknown, 0: standard, 1: daylight-saving}
// So gotDst ^ altDst is 1 precisely if exactly one candidate thinks it's DST.
if ((gotDst ^ altDst) != 1) {
// Both or neither think they're DST - pretend one is: around a gap, the
// later candidate is DST; around a fold, the earlier.
if (altIsLater == inGap) {
altDst = 1;
gotDst = 0;
} else {
gotDst = 1;
altDst = 0;
}
// If resolve has this option set, prefer the later candidate, else the earlier:
QDateTimePrivate::TransitionOption preferLater = inGap ? QDateTimePrivate::GapUseAfter
: QDateTimePrivate::FoldUseAfter;
if (resolve.testFlag(QDateTimePrivate::FlipForReverseDst)) {
// gotDst and altDst are {-1: unknown, 0: standard, 1: daylight-saving}
// So gotDst ^ altDst is 1 precisely if exactly one candidate thinks it's DST.
if ((altDst ^ gotDst) == 1) {
// In this case, we can tell whether we have reversed DST: that's a
// gap with DST before it or a fold with DST after it.
#if 1
const bool isReversed = (altDst == 1) != (altIsLater == inGap);
#else // Pedagogic version of the same thing:
bool isReversed;
if (altIsLater == inGap) // alt is after a gap or before a fold, so summer-time
isReversed = altDst != 1; // flip if summer-time isn't DST
else // alt is before a gap or after a fold, so winter-time
isReversed = altDst == 1; // flip if winter-time is DST
#endif
if (isReversed) {
preferLater = inGap ? QDateTimePrivate::GapUseBefore
: QDateTimePrivate::FoldUseBefore;
}
} // Otherwise, we can't tell, so assume not.
}
// When we create a time in a gap, it comes here with UnknownDST, so has
// already been handled; so a gep only gets here if we've previously
// resolved a non-gap and are now adjusting into the gap. For setTime(),
// setDate() or setTimeZone() we've no strong reason to prefer either
// resolution, but addDays(), addSecs() and friends all want to overshoot
// the gap, to the side beyond where they started; that'll typically be the
// side with the *opposite* state to the one specified.
// If we want standard, switch to the alternative iff what we have is DST
if ((dst == QDateTimePrivate::StandardTime) != inGap)
return gotDst == 1;
// Otherwise we wanted DST, so switch iff alternative is DST
return altDst == 1;
return resolve.testFlag(preferLater) == altIsLater;
}
/*
@ -372,12 +355,12 @@ bool preferAlternative(QDateTimePrivate::DaylightStatus dst,
The local time is specified as a number of seconds since the epoch (so, in
effect, a time_t, albeit delivered as qint64). If the specified local time
falls in a transition, dst determines what to do.
falls in a transition, resolve determines what to do.
If the specified local time is outside what the system time_t APIs will
handle, this fails.
*/
MkTimeResult resolveLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst)
MkTimeResult resolveLocalTime(qint64 local, QDateTimePrivate::TransitionOptions resolve)
{
const auto localDaySecs = QRoundingDown::qDivMod<SECS_PER_DAY>(local);
struct tm base = timeToTm(localDaySecs.quotient, localDaySecs.remainder);
@ -392,19 +375,23 @@ MkTimeResult resolveLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst
// that we hit a gap, although we have to handle these cases differently:
if (!result.good) {
// Rejected. The tricky case: maybe mktime() doesn't resolve gaps.
return resolveRejected(base, result, dst);
return resolveRejected(base, result, resolve);
} else if (result.local.tm_isdst < 0) {
// Apparently success without knowledge of whether this is DST or not.
// Should not happen, but that means our usual understanding of what the
// system is up to has gone out the window. So just let it be.
} else if (result.adjusted) {
// Shunted out of a gap.
if (!resolve.testAnyFlags(QDateTimePrivate::GapMask)) {
result = {};
return result;
}
// Try to obtain a matching point on the other side of the gap:
const MkTimeResult flipped = hopAcrossGap(result, base);
// Even if that failed, result may be the correct resolution
if (preferAlternative(dst, result.local.tm_isdst, flipped.local.tm_isdst,
if (preferAlternative(resolve, result.local.tm_isdst, flipped.local.tm_isdst,
flipped.utcSecs > result.utcSecs, true)) {
// If hopAcrossGap() failed and we do need its answer, give up.
if (!flipped.good || flipped.adjusted)
@ -414,10 +401,11 @@ MkTimeResult resolveLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst
result = flipped;
result.adjusted = true;
}
} else if (dst != QDateTimePrivate::UnknownDaylightTime
// We may not need to check whether we're in a transition:
// Does DST-ness match what we were asked for ?
&& result.local.tm_isdst == (dst == QDateTimePrivate::StandardTime ? 0 : 1)) {
} else if (resolve.testFlag(QDateTimePrivate::FlipForReverseDst)
// In fold, DST counts as before and standard as after -
// we may not need to check whether we're in a transition:
&& resolve.testFlag(result.local.tm_isdst ? QDateTimePrivate::FoldUseBefore
: QDateTimePrivate::FoldUseAfter)) {
// We prefer DST or standard and got what we wanted, so we're good.
// As below, but we don't need to check, because we're on the side of
// the transition that it would select as valid, if we were near one.
@ -432,7 +420,13 @@ MkTimeResult resolveLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst
const MkTimeResult flipped(copy);
if (flipped.good && !flipped.adjusted) {
// We're in a fall-back
if (preferAlternative(dst, result.local.tm_isdst, flipped.local.tm_isdst,
if (!resolve.testAnyFlags(QDateTimePrivate::FoldMask)) {
result = {};
return result;
}
// Work out which repeat to use:
if (preferAlternative(resolve, result.local.tm_isdst, flipped.local.tm_isdst,
flipped.utcSecs > result.utcSecs, false)) {
result = flipped;
}
@ -563,9 +557,9 @@ QDateTimePrivate::ZoneState utcToLocal(qint64 utcMillis)
return { localMillis, int(localSeconds - epochSeconds), dst };
}
QString localTimeAbbbreviationAt(qint64 local, QDateTimePrivate::DaylightStatus dst)
QString localTimeAbbbreviationAt(qint64 local, QDateTimePrivate::TransitionOptions resolve)
{
auto use = resolveLocalTime(QRoundingDown::qDiv<MSECS_PER_SEC>(local), dst);
auto use = resolveLocalTime(QRoundingDown::qDiv<MSECS_PER_SEC>(local), resolve);
if (!use.good)
return {};
#ifdef HAVE_TM_ZONE
@ -575,11 +569,11 @@ QString localTimeAbbbreviationAt(qint64 local, QDateTimePrivate::DaylightStatus
return qTzName(use.local.tm_isdst > 0 ? 1 : 0);
}
QDateTimePrivate::ZoneState mapLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst)
QDateTimePrivate::ZoneState mapLocalTime(qint64 local, QDateTimePrivate::TransitionOptions resolve)
{
// Revised later to match what use.local tells us:
qint64 localSecs = local / MSECS_PER_SEC;
auto use = resolveLocalTime(localSecs, dst);
auto use = resolveLocalTime(localSecs, resolve);
if (!use.good)
return {local};
@ -588,8 +582,9 @@ QDateTimePrivate::ZoneState mapLocalTime(qint64 local, QDateTimePrivate::Dayligh
Q_ASSERT(local < 0 ? (millis <= 0 && millis > -MSECS_PER_SEC)
: (millis >= 0 && millis < MSECS_PER_SEC));
// Revise our original hint-dst to what it resolved to:
dst = use.local.tm_isdst > 0 ? QDateTimePrivate::DaylightTime : QDateTimePrivate::StandardTime;
QDateTimePrivate::DaylightStatus dst =
use.local.tm_isdst > 0 ? QDateTimePrivate::DaylightTime : QDateTimePrivate::StandardTime;
#ifdef HAVE_TM_GMTOFF
const int offset = use.local.tm_gmtoff;
localSecs = offset + use.utcSecs;

View File

@ -34,8 +34,8 @@ Q_CORE_EXPORT int getUtcOffset(qint64 atMSecsSinceEpoch);
// Support for QDateTime
QDateTimePrivate::ZoneState utcToLocal(qint64 utcMillis);
QString localTimeAbbbreviationAt(qint64 local, QDateTimePrivate::DaylightStatus dst);
QDateTimePrivate::ZoneState mapLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst);
QString localTimeAbbbreviationAt(qint64 local, QDateTimePrivate::TransitionOptions resolve);
QDateTimePrivate::ZoneState mapLocalTime(qint64 local, QDateTimePrivate::TransitionOptions resolve);
struct SystemMillisRange { qint64 min, max; bool minClip, maxClip; };
SystemMillisRange computeSystemMillisRange();

View File

@ -7,11 +7,13 @@
#include "qtimezoneprivate_p.h"
#include "qtimezoneprivate_data_p.h"
#include <private/qnumeric_p.h>
#include <private/qtools_p.h>
#include <qdatastream.h>
#include <qdebug.h>
#include <private/qcalendarmath_p.h>
#include <private/qnumeric_p.h>
#include <private/qtools_p.h>
#include <algorithm>
QT_BEGIN_NAMESPACE
@ -170,8 +172,16 @@ QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
}
// Private only method for use by QDateTime to convert local msecs to epoch msecs
QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs, int hint) const
QDateTimePrivate::ZoneState QTimeZonePrivate::stateAtZoneTime(
qint64 forLocalMSecs, QDateTimePrivate::TransitionOptions resolve) const
{
auto dataToState = [](QTimeZonePrivate::Data d) {
return QDateTimePrivate::ZoneState(d.atMSecsSinceEpoch + d.offsetFromUtc * 1000,
d.offsetFromUtc,
d.daylightTimeOffset ? QDateTimePrivate::DaylightTime
: QDateTimePrivate::StandardTime);
};
/*
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.
@ -194,11 +204,24 @@ QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs,
? maxMSecs() : millis; // Necessarily >= forLocalMSecs
// At most one of those was clipped to its boundary value:
Q_ASSERT(recent < imminent && seventeenHoursInMSecs < imminent - recent + 1);
const Data past = data(recent), future = data(imminent);
// > 99% of the time, past and future will agree:
if (Q_LIKELY(past.offsetFromUtc == future.offsetFromUtc
&& past.standardTimeOffset == future.standardTimeOffset
// Those two imply same daylightTimeOffset.
&& past.abbreviation == future.abbreviation)) {
Data data = future;
data.atMSecsSinceEpoch = forLocalMSecs - future.offsetFromUtc * 1000;
return dataToState(data);
}
/*
Offsets are Local - UTC, positive to the east of Greenwich, negative to
the west; DST offset always exceeds standard offset, when DST applies.
the west; DST offset normally 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.
standard, the higher is DST, unless we have data telling us it's the other
way round.
Non-DST transitions (jurisdictions changing time-zone and time-zones
changing their standard offset, typically) are described below as if they
@ -210,63 +233,26 @@ QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs,
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.
reasoning that applies to DST applies just the same.
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.)
The resolution of transitions, specified by \a resolve, may be lead astray
if (as happens on Windows) the backend has been obliged to guess whether a
transition is in fact a DST one or a change to standard offset; or to
guess that the higher-offset side is the DST one (the reverse of this is
true for Ireland, using negative DST). There's not much we can do about
that, though.
*/
const Data past = data(recent), future = data(imminent);
// > 99% of the time, past and future will agree:
if (Q_LIKELY(past.offsetFromUtc == future.offsetFromUtc
&& past.standardTimeOffset == future.standardTimeOffset
// Those two imply same daylightTimeOffset.
&& past.abbreviation == future.abbreviation)) {
Data data = future;
data.atMSecsSinceEpoch = forLocalMSecs - future.offsetFromUtc * 1000;
return data;
}
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.
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 resolve to disambiguate.
*/
// Get a transition definitely before the local MSecs; usually all we need.
@ -306,50 +292,75 @@ QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs,
// If we know of no transition after it, the answer is easy:
const qint64 nextStart = nextTran.atMSecsSinceEpoch;
if (nextStart == invalidMSecs())
return tran;
return dataToState(tran); // Last valid transition.
/*
... and nextTran is either after or only slightly before. We're
going to interpret one as standard time, the other as DST
(although the transition might in fact be a change in standard
offset, or a change in DST offset, e.g. to/from double-DST). 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.
offset, or a change in DST offset, e.g. to/from double-DST).
Usually exactly one of those shall be relevant and we'll use it;
but if we're close to nextTran we may be in a transition, to be
settled according to resolve's rules.
*/
// Work out the UTC value it would make sense to return if using nextTran:
nextTran.atMSecsSinceEpoch = forLocalMSecs - nextTran.offsetFromUtc * 1000;
// If both or neither have zero DST, treat the one with lower offset as standard:
const bool nextIsDst = !nextTran.daylightTimeOffset == !tran.daylightTimeOffset
? tran.offsetFromUtc < nextTran.offsetFromUtc : nextTran.daylightTimeOffset;
// If that agrees with hint > 0, our first guess is to use nextTran; else tran.
const bool nextFirst = nextIsDst == (hint > 0);
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 (nextFirst ? i == 0 : i) {
if (nextStart <= nextTran.atMSecsSinceEpoch)
return nextTran;
} else {
// If next is invalid, nextFirst is false, to route us here first:
if (nextStart > tran.atMSecsSinceEpoch)
return tran;
}
}
bool fallBack = false;
if (nextStart > nextTran.atMSecsSinceEpoch) {
// If both UTC values are before nextTran's offset applies, use tran:
if (nextStart > tran.atMSecsSinceEpoch)
return dataToState(tran);
/*
Neither is valid (e.g. in a spring-forward's gap) and
nextTran.atMSecsSinceEpoch < nextStart <= tran.atMSecsSinceEpoch;
swap their atMSecsSinceEpoch to give each a moment on its side of
the transition; and pick the reverse of what hint asked for:
*/
std::swap(tran.atMSecsSinceEpoch, nextTran.atMSecsSinceEpoch);
return nextFirst ? tran : nextTran;
Q_ASSERT(tran.offsetFromUtc < nextTran.offsetFromUtc);
// We're in a spring-forward.
} else if (nextStart <= tran.atMSecsSinceEpoch) {
// Both UTC values say we should be using nextTran:
return dataToState(nextTran);
} else {
Q_ASSERT(nextTran.offsetFromUtc < tran.offsetFromUtc);
fallBack = true; // We're in a fall-back.
}
// (forLocalMSecs - nextStart) / 1000 lies between the two offsets.
// Apply resolve:
// Determine whether FlipForReverseDst affects the outcome:
const bool flipped
= resolve.testFlag(QDateTimePrivate::FlipForReverseDst)
&& (fallBack ? !tran.daylightTimeOffset && nextTran.daylightTimeOffset
: tran.daylightTimeOffset && !nextTran.daylightTimeOffset);
if (fallBack) {
if (resolve.testFlag(flipped
? QDateTimePrivate::FoldUseBefore
: QDateTimePrivate::FoldUseAfter)) {
return dataToState(nextTran);
}
if (resolve.testFlag(flipped
? QDateTimePrivate::FoldUseAfter
: QDateTimePrivate::FoldUseBefore)) {
return dataToState(tran);
}
} else {
/* Neither is valid (e.g. in a spring-forward's gap) and
nextTran.atMSecsSinceEpoch < nextStart <= tran.atMSecsSinceEpoch.
So swap their atMSecsSinceEpoch to give each a moment on the
side of the transition that it describes, then select the one
after or before according to the option set:
*/
std::swap(tran.atMSecsSinceEpoch, nextTran.atMSecsSinceEpoch);
if (resolve.testFlag(flipped
? QDateTimePrivate::GapUseBefore
: QDateTimePrivate::GapUseAfter))
return dataToState(nextTran);
if (resolve.testFlag(flipped
? QDateTimePrivate::GapUseAfter
: QDateTimePrivate::GapUseBefore))
return dataToState(tran);
}
// Reject
return {forLocalMSecs};
}
// Before first transition, or system has transitions but not for this zone.
// Try falling back to offsetFromUtc (works for before first transition, at least).
@ -358,40 +369,54 @@ QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs,
/* Bracket and refine to discover offset. */
qint64 utcEpochMSecs;
// We don't have true data on DST-ness, so can't apply FlipForReverseDst.
int early = past.offsetFromUtc;
int late = future.offsetFromUtc;
if (early == late || late == invalidSeconds()) {
if (early == invalidSeconds()
|| qSubOverflow(forLocalMSecs, early * qint64(1000), &utcEpochMSecs)) {
return invalidData(); // Outside representable range
return {forLocalMSecs}; // Outside representable range
}
} 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 (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;
const qint64 forEarly = forLocalMSecs - early * 1000;
const qint64 forLate = forLocalMSecs - late * 1000;
// If either of those doesn't have the offset we got it from, it's on
// the wrong side of the transition (and both may be, for a gap):
const bool earlyOk = offsetFromUtc(forEarly) == early;
const bool lateOk = offsetFromUtc(forLate) == late;
if (earlyOk) {
if (lateOk) {
Q_ASSERT(early > late);
// fall-back's repeated interval
if (resolve.testFlag(QDateTimePrivate::FoldUseBefore))
utcEpochMSecs = forEarly;
else if (resolve.testFlag(QDateTimePrivate::FoldUseAfter))
utcEpochMSecs = forLate;
else
return {forLocalMSecs};
} else {
// Before and clear of the transition:
utcEpochMSecs = forEarly;
}
} else if (lateOk) {
// After and clear of the transition:
utcEpochMSecs = forLate;
} else {
// Invalid forLocalMSecs: in spring-forward gap.
const int dstStep = (offsetInDst - offsetInStd) * 1000;
// That'll typically be the DST offset at imminent, but changes to
// standard time have zero DST offset both before and after.
Q_ASSERT(dstStep > 0); // There can't be a gap without it !
utcEpochMSecs = (hint > 0) ? forStd - dstStep : forDst + dstStep;
// forLate <= gap < forEarly
Q_ASSERT(late > early);
const int dstStep = (late - early) * 1000;
if (resolve.testFlag(QDateTimePrivate::GapUseBefore))
utcEpochMSecs = forEarly - dstStep;
else if (resolve.testFlag(QDateTimePrivate::GapUseAfter))
utcEpochMSecs = forLate + dstStep;
else
return {forLocalMSecs};
}
}
return data(utcEpochMSecs);
return dataToState(data(utcEpochMSecs));
}
bool QTimeZonePrivate::hasTransitions() const

View File

@ -20,6 +20,7 @@
#include "qlist.h"
#include "qtimezone.h"
#include "private/qlocale_p.h"
#include "private/qdatetime_p.h"
#if QT_CONFIG(icu)
#include <unicode/ucal.h>
@ -84,7 +85,8 @@ public:
virtual bool isDaylightTime(qint64 atMSecsSinceEpoch) const;
virtual Data data(qint64 forMSecsSinceEpoch) const;
Data dataForLocalTime(qint64 forLocalMSecs, int hint) const;
QDateTimePrivate::ZoneState stateAtZoneTime(qint64 forLocalMSecs,
QDateTimePrivate::TransitionOptions resolve) const;
virtual bool hasTransitions() const;
virtual Data nextTransition(qint64 afterMSecsSinceEpoch) const;

View File

@ -1450,16 +1450,11 @@ void QDateTimeEdit::fixup(QString &input) const
int copy = d->edit->cursorPosition();
QDateTime value = d->validateAndInterpret(input, copy, state, true);
/*
String was valid, but the datetime still is not; use the time that
has the same distance from epoch.
CorrectToPreviousValue correction is handled by QAbstractSpinBox.
*/
if (!value.isValid() && d->correctionMode == QAbstractSpinBox::CorrectToNearestValue) {
value = QDateTime::fromMSecsSinceEpoch(value.toMSecsSinceEpoch(),
value.timeRepresentation());
// CorrectToPreviousValue correction is handled by QAbstractSpinBox.
// The value might not match the input if the input represents a date-time
// skipped over by its time representation, such as a spring-forward.
if (d->correctionMode == QAbstractSpinBox::CorrectToNearestValue)
input = textFromDateTime(value);
}
}
@ -1727,11 +1722,7 @@ QDateTime QDateTimeEditPrivate::convertTimeZone(const QDateTime &datetime)
QDateTime QDateTimeEditPrivate::dateTimeValue(QDate date, QTime time) const
{
QDateTime when = QDateTime(date, time, timeZone);
if (when.isValid())
return when;
// Hit a spring-forward gap
return QDateTime::fromMSecsSinceEpoch(when.toMSecsSinceEpoch(), timeZone);
return QDateTime(date, time, timeZone);
}
void QDateTimeEditPrivate::updateTimeZone()
@ -2135,11 +2126,10 @@ QDateTime QDateTimeEditPrivate::stepBy(int sectionIndex, int steps, bool test) c
true when date and time are valid, even if the date-time returned
isn't), so use the time that has the same distance from epoch.
*/
if (setDigit(v, sectionIndex, val) && !v.isValid()) {
auto msecsSinceEpoch = v.toMSecsSinceEpoch();
if (setDigit(v, sectionIndex, val) && getDigit(v, sectionIndex) != val
&& sn.type & HourSectionMask && steps < 0) {
// decreasing from e.g 3am to 2am would get us back to 3am, but we want 1am
if (steps < 0 && sn.type & HourSectionMask)
msecsSinceEpoch -= 3600 * 1000;
auto msecsSinceEpoch = v.toMSecsSinceEpoch() - 3600 * 1000;
v = QDateTime::fromMSecsSinceEpoch(msecsSinceEpoch, v.timeRepresentation());
}
// if this sets year or month it will make

View File

@ -627,14 +627,18 @@ void tst_QDate::startOfDay_endOfDay()
QCOMPARE(front.date(), date);
UNLESSKLUDGE(IgnoreStart) QCOMPARE(front.time(), start);
} else UNLESSKLUDGE(IgnoreStart) {
auto report = qScopeGuard([front]() { qDebug() << "Start of day:" << front; });
QVERIFY(!front.isValid());
report.dismiss();
}
if (end.isValid()) {
QVERIFY(back.isValid());
QCOMPARE(back.date(), date);
UNLESSKLUDGE(IgnoreEnd) QCOMPARE(back.time(), end);
} else UNLESSKLUDGE(IgnoreEnd) {
auto report = qScopeGuard([back]() { qDebug() << "End of day:" << back; });
QVERIFY(!back.isValid());
report.dismiss();
}
#undef UNLESSKLUDGE
}

View File

@ -2286,15 +2286,14 @@ void tst_QDateTime::springForward()
QFETCH(int, adjust);
QDateTime direct = QDateTime(day.addDays(-step), time, zone).addDays(step);
if (direct.isValid()) { // mktime() may deem a time in the gap invalid
QCOMPARE(direct.date(), day);
QCOMPARE(direct.time().minute(), time.minute());
QCOMPARE(direct.time().second(), time.second());
const int off = step < 0 ? -1 : 1;
QCOMPARE(direct.time().hour() - time.hour(), off);
// adjust is the offset on the other side of the gap:
QCOMPARE(direct.offsetFromUtc(), (adjust + off * 60) * 60);
}
QVERIFY(direct.isValid());
QCOMPARE(direct.date(), day);
QCOMPARE(direct.time().minute(), time.minute());
QCOMPARE(direct.time().second(), time.second());
const int off = step < 0 ? -1 : 1;
QCOMPARE(direct.time().hour() - time.hour(), off);
// adjust is the offset on the other side of the gap:
QCOMPARE(direct.offsetFromUtc(), (adjust + off * 60) * 60);
// Repeat, but getting there via .toTimeZone(). Apply adjust to datetime,
// not time, as the time wraps round if the adjustment crosses midnight.
@ -2303,12 +2302,8 @@ void tst_QDateTime::springForward()
QCOMPARE(detour.time(), time);
detour = detour.addDays(step);
// Insist on consistency:
if (direct.isValid()) {
QCOMPARE(detour, direct);
QCOMPARE(detour.offsetFromUtc(), direct.offsetFromUtc());
} else {
QVERIFY(!detour.isValid());
}
QCOMPARE(detour, direct);
QCOMPARE(detour.offsetFromUtc(), direct.offsetFromUtc());
}
void tst_QDateTime::operator_eqeq_data()
@ -3267,11 +3262,10 @@ void tst_QDateTime::fromStringStringFormat_localTimeZone_data()
QTimeZone helsinki("Europe/Helsinki");
if (helsinki.isValid()) {
lacksRows = false;
// QTBUG-96861: QAsn1Element::toDateTime() tripped over an assert in
// QTimeZonePrivate::dataForLocalTime() on macOS and iOS.
// The first 20m 11s of 1921-05-01 were skipped, so the parser's attempt
// to construct a local time after scanning yyMM tripped up on the start
// of the day, when the zone backend lacked transition data.
// QTBUG-96861: QAsn1Element::toDateTime() tripped over an assert due to
// the first 20m 11s of 1921-05-01 being skipped, so the parser's
// attempt to construct a local time after scanning yyMM tripped up on
// the start of the day, when the zone backend lacked transition data.
QTest::newRow("Helsinki-joins-EET")
<< QByteArrayLiteral("Europe/Helsinki")
<< QString("210506000000Z") << QString("yyMMddHHmmsst")
@ -3702,12 +3696,23 @@ void tst_QDateTime::daylightTransitions() const
QCOMPARE(before.time(), QTime(1, 59, 59, 999));
QCOMPARE(before.toMSecsSinceEpoch(), spring2012 - 1);
QDateTime missing(QDate(2012, 3, 25), QTime(2, 0));
QVERIFY(!missing.isValid());
QCOMPARE(missing.date(), QDate(2012, 3, 25));
QCOMPARE(missing.time(), QTime(3, 0));
// datetimeparser relies on toMSecsSinceEpoch to still work:
QCOMPARE(missing.toMSecsSinceEpoch(), spring2012);
QDateTime entering(QDate(2012, 3, 25), QTime(2, 0),
QDateTime::TransitionResolution::PreferBefore);
QVERIFY(entering.isValid());
QVERIFY(!entering.isDaylightTime());
QCOMPARE(entering.date(), QDate(2012, 3, 25));
QCOMPARE(entering.time(), QTime(1, 0));
// QDateTimeParser relies on toMSecsSinceEpoch() to still work:
QCOMPARE(entering.toMSecsSinceEpoch(), spring2012 - msecsOneHour);
QDateTime leaving(QDate(2012, 3, 25), QTime(2, 0),
QDateTime::TransitionResolution::PreferAfter);
QVERIFY(leaving.isValid());
QVERIFY(leaving.isDaylightTime());
QCOMPARE(leaving.date(), QDate(2012, 3, 25));
QCOMPARE(leaving.time(), QTime(3, 0));
// QDateTimeParser relies on toMSecsSinceEpoch to still work:
QCOMPARE(leaving.toMSecsSinceEpoch(), spring2012);
QDateTime after(QDate(2012, 3, 25), QTime(3, 0));
QVERIFY(after.isValid());
@ -3735,11 +3740,11 @@ void tst_QDateTime::daylightTransitions() const
QVERIFY(utc.isValid());
QCOMPARE(utc.date(), QDate(2012, 3, 25));
QCOMPARE(utc.time(), QTime(2, 0));
utc.setTimeZone(QTimeZone::LocalTime);
QVERIFY(!utc.isValid());
utc.setTimeZone(QTimeZone::LocalTime); // Resolved to RelativeToBefore.
QVERIFY(utc.isValid());
QCOMPARE(utc.date(), QDate(2012, 3, 25));
QCOMPARE(utc.time(), QTime(3, 0));
utc.setTimeZone(UTC);
utc.setTimeZone(UTC); // Preserves the changed time().
QVERIFY(utc.isValid());
QCOMPARE(utc.date(), QDate(2012, 3, 25));
QCOMPARE(utc.time(), QTime(3, 0));
@ -3780,19 +3785,17 @@ void tst_QDateTime::daylightTransitions() const
#undef CHECK_SPRING_FORWARD
// Test for correct behviour for DaylightTime -> StandardTime transition, fall-back
// TODO (QTBUG-79923): Compare to results of direct QDateTime(date, time, fold)
// construction; see Prior/Post commented-out tests.
QDateTime autumnMidnight = QDate(2012, 10, 28).startOfDay();
QVERIFY(autumnMidnight.isValid());
// QCOMPARE(autumnMidnight, QDateTime(QDate(2012, 10, 28), QTime(2, 0), Prior));
QCOMPARE(autumnMidnight.date(), QDate(2012, 10, 28));
QCOMPARE(autumnMidnight.time(), QTime(0, 0));
QCOMPARE(autumnMidnight.toMSecsSinceEpoch(), autumn2012 - 3 * msecsOneHour);
QDateTime startFirst = autumnMidnight.addMSecs(2 * msecsOneHour);
QVERIFY(startFirst.isValid());
// QCOMPARE(startFirst, QDateTime(QDate(2012, 10, 28), QTime(2, 0), Prior));
QCOMPARE(startFirst, QDateTime(QDate(2012, 10, 28), QTime(2, 0),
QDateTime::TransitionResolution::PreferBefore));
QCOMPARE(startFirst.date(), QDate(2012, 10, 28));
QCOMPARE(startFirst.time(), QTime(2, 0));
QCOMPARE(startFirst.toMSecsSinceEpoch(), autumn2012 - msecsOneHour);
@ -3800,7 +3803,9 @@ void tst_QDateTime::daylightTransitions() const
// 1 msec before transition is 2:59:59.999 FirstOccurrence
QDateTime endFirst = startFirst.addMSecs(msecsOneHour - 1);
QVERIFY(endFirst.isValid());
// QCOMPARE(endFirst, QDateTime(QDate(2012, 10, 28), QTime(2, 59, 59, 999), Prior));
QCOMPARE(endFirst,
QDateTime(QDate(2012, 10, 28), QTime(2, 59, 59, 999),
QDateTime::TransitionResolution::PreferBefore));
QCOMPARE(endFirst.date(), QDate(2012, 10, 28));
QCOMPARE(endFirst.time(), QTime(2, 59, 59, 999));
QCOMPARE(endFirst.toMSecsSinceEpoch(), autumn2012 - 1);
@ -3808,7 +3813,8 @@ void tst_QDateTime::daylightTransitions() const
// At the transition, starting the second pass
QDateTime startRepeat = endFirst.addMSecs(1);
QVERIFY(startRepeat.isValid());
// QCOMPARE(startRepeat, QDateTime(QDate(2012, 10, 28), QTime(2, 0), Post));
QCOMPARE(startRepeat, QDateTime(QDate(2012, 10, 28), QTime(2, 0),
QDateTime::TransitionResolution::PreferAfter));
QCOMPARE(startRepeat.date(), QDate(2012, 10, 28));
QCOMPARE(startRepeat.time(), QTime(2, 0));
QCOMPARE(startRepeat.toMSecsSinceEpoch(), autumn2012);
@ -3816,7 +3822,9 @@ void tst_QDateTime::daylightTransitions() const
// 59:59.999 after transition is 2:59:59.999 SecondOccurrence
QDateTime endRepeat = endFirst.addMSecs(msecsOneHour);
QVERIFY(endRepeat.isValid());
// QCOMPARE(endRepeat, QDateTime(QDate(2012, 10, 28), QTime(2, 59, 59, 999), Post));
QCOMPARE(endRepeat,
QDateTime(QDate(2012, 10, 28), QTime(2, 59, 59, 999),
QDateTime::TransitionResolution::PreferAfter));
QCOMPARE(endRepeat.date(), QDate(2012, 10, 28));
QCOMPARE(endRepeat.time(), QTime(2, 59, 59, 999));
QCOMPARE(endRepeat.toMSecsSinceEpoch(), autumn2012 + msecsOneHour - 1);
@ -4209,20 +4217,20 @@ void tst_QDateTime::timeZones() const
QCOMPARE(atGap.toMSecsSinceEpoch(), gapMSecs);
// - Test transition hole, setting 02:00:00 is invalid
QDateTime inGap = QDateTime(QDate(2013, 3, 31), QTime(2, 0), cet);
QVERIFY(!inGap.isValid());
QVERIFY(inGap.isValid());
QCOMPARE(inGap.date(), QDate(2013, 3, 31));
QCOMPARE(inGap.time(), QTime(3, 0));
QCOMPARE(inGap.offsetFromUtc(), 7200);
// - Test transition hole, setting 02:59:59.999 is invalid
// - Test transition hole, 02:59:59.999 was skipped:
inGap = QDateTime(QDate(2013, 3, 31), QTime(2, 59, 59, 999), cet);
QVERIFY(!inGap.isValid());
QVERIFY(inGap.isValid());
QCOMPARE(inGap.date(), QDate(2013, 3, 31));
QCOMPARE(inGap.time(), QTime(3, 59, 59, 999));
QCOMPARE(inGap.offsetFromUtc(), 7200);
// Test similar for local time, if it's CET:
if (zoneIsCET) {
inGap = QDateTime(QDate(2013, 3, 31), QTime(2, 30));
QVERIFY(!inGap.isValid());
QVERIFY(inGap.isValid());
QCOMPARE(inGap.date(), QDate(2013, 3, 31));
QCOMPARE(inGap.offsetFromUtc(), 7200);
QCOMPARE(inGap.time(), QTime(3, 30));
@ -4238,7 +4246,7 @@ void tst_QDateTime::timeZones() const
if (QDateTime(QDate(longYear, 3, 24), QTime(12, 0), cet).msecsTo(
QDateTime(QDate(longYear, 3, 31), QTime(12, 0), cet)) < millisInWeek) {
inGap = QDateTime(QDate(longYear, 3, 27), QTime(2, 30), cet);
QVERIFY(!inGap.isValid());
QVERIFY(inGap.isValid());
QCOMPARE(inGap.date(), QDate(longYear, 3, 27));
QCOMPARE(inGap.time(), QTime(3, 30));
QCOMPARE(inGap.offsetFromUtc(), 7200);
@ -4248,7 +4256,7 @@ void tst_QDateTime::timeZones() const
if (zoneIsCET && QDateTime(QDate(longYear, 3, 24), QTime(12, 0)).msecsTo(
QDateTime(QDate(longYear, 3, 31), QTime(12, 0))) < millisInWeek) {
inGap = QDateTime(QDate(longYear, 3, 27), QTime(2, 30));
QVERIFY(!inGap.isValid());
QVERIFY(inGap.isValid());
QCOMPARE(inGap.date(), QDate(longYear, 3, 27));
QCOMPARE(inGap.offsetFromUtc(), 7200);
QCOMPARE(inGap.time(), QTime(3, 30));