summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEdward Welbourne <edward.welbourne@qt.io>2022-08-31 15:43:48 +0200
committerEdward Welbourne <edward.welbourne@qt.io>2023-10-19 14:45:56 +0200
commita49ccc08c307b7c7e1acc34752b81dd38ea43bfa (patch)
treeba4194b766a80a9a687d13337158585e309753c6
parent38994ab9accc9aecf1139eb02f7e5fc75fccceec (diff)
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>
-rw-r--r--src/corelib/compat/removed_api.cpp13
-rw-r--r--src/corelib/time/qdatetime.cpp620
-rw-r--r--src/corelib/time/qdatetime.h28
-rw-r--r--src/corelib/time/qdatetime_p.h25
-rw-r--r--src/corelib/time/qdatetimeparser.cpp78
-rw-r--r--src/corelib/time/qlocaltime.cpp135
-rw-r--r--src/corelib/time/qlocaltime_p.h4
-rw-r--r--src/corelib/time/qtimezoneprivate.cpp249
-rw-r--r--src/corelib/time/qtimezoneprivate_p.h4
-rw-r--r--src/widgets/widgets/qdatetimeedit.cpp26
-rw-r--r--tests/auto/corelib/time/qdate/tst_qdate.cpp4
-rw-r--r--tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp92
12 files changed, 836 insertions, 442 deletions
diff --git a/src/corelib/compat/removed_api.cpp b/src/corelib/compat/removed_api.cpp
index 04af737add..bb3dca62a1 100644
--- a/src/corelib/compat/removed_api.cpp
+++ b/src/corelib/compat/removed_api.cpp
@@ -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"
diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp
index e59681ea34..53e0987302 100644
--- a/src/corelib/time/qdatetime.cpp
+++ b/src/corelib/time/qdatetime.cpp
@@ -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)
-
- 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.
+ \section2 Timezone transitions
+
+ 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
*/
@@ -3497,6 +3638,148 @@ QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTime
*/
/*!
+ \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.
A null datetime is invalid, since its date and time are invalid.
@@ -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)
-{
- /*
- 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).
- */
+static inline void massageAdjustedDateTime(QDateTimeData &d, QDate date, QTime time, bool forward)
+{
+ 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) {
diff --git a/src/corelib/time/qdatetime.h b/src/corelib/time/qdatetime.h
index 3ca51b60df..7ef869ae9f 100644
--- a/src/corelib/time/qdatetime.h
+++ b/src/corelib/time/qdatetime.h
@@ -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);
diff --git a/src/corelib/time/qdatetime_p.h b/src/corelib/time/qdatetime_p.h
index bd4e1e33d0..02b047dd73 100644
--- a/src/corelib/time/qdatetime_p.h
+++ b/src/corelib/time/qdatetime_p.h
@@ -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 {
diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp
index e7878a4958..59ee639644 100644
--- a/src/corelib/time/qdatetimeparser.cpp
+++ b/src/corelib/time/qdatetimeparser.cpp
@@ -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
diff --git a/src/corelib/time/qlocaltime.cpp b/src/corelib/time/qlocaltime.cpp
index 3e134fd180..609a5a4b37 100644
--- a/src/corelib/time/qlocaltime.cpp
+++ b/src/corelib/time/qlocaltime.cpp
@@ -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;
- }
- if (choice == BeforeLater) // Result will be before the gap:
+ // 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 (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;
diff --git a/src/corelib/time/qlocaltime_p.h b/src/corelib/time/qlocaltime_p.h
index 027b1bc05b..5e31d5e9cc 100644
--- a/src/corelib/time/qlocaltime_p.h
+++ b/src/corelib/time/qlocaltime_p.h
@@ -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();
diff --git a/src/corelib/time/qtimezoneprivate.cpp b/src/corelib/time/qtimezoneprivate.cpp
index 9032f2cf79..f007b05d58 100644
--- a/src/corelib/time/qtimezoneprivate.cpp
+++ b/src/corelib/time/qtimezoneprivate.cpp
@@ -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.
-
- 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.)
+ reasoning that applies to DST applies just the same.
+
+ 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);
+
+ 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);
}
-
- /*
- 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;
+ // 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
diff --git a/src/corelib/time/qtimezoneprivate_p.h b/src/corelib/time/qtimezoneprivate_p.h
index cb045ab5c3..51e093e002 100644
--- a/src/corelib/time/qtimezoneprivate_p.h
+++ b/src/corelib/time/qtimezoneprivate_p.h
@@ -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;
diff --git a/src/widgets/widgets/qdatetimeedit.cpp b/src/widgets/widgets/qdatetimeedit.cpp
index 61cf20891b..5b14457b0d 100644
--- a/src/widgets/widgets/qdatetimeedit.cpp
+++ b/src/widgets/widgets/qdatetimeedit.cpp
@@ -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
diff --git a/tests/auto/corelib/time/qdate/tst_qdate.cpp b/tests/auto/corelib/time/qdate/tst_qdate.cpp
index ab27af0dd8..8da22918a7 100644
--- a/tests/auto/corelib/time/qdate/tst_qdate.cpp
+++ b/tests/auto/corelib/time/qdate/tst_qdate.cpp
@@ -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
}
diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp
index 4cf8c3d5f7..17473d4af9 100644
--- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp
+++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp
@@ -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));