Add startOfDay() and endOfDay() methods to QDate

These methods give the first and last QDateTime values in the given
day, for a given time-zone or time-spec.  These are usually the
relevant midnight, or the millisecond before, except when time-zone
transitions (typically DST changes) skip it, when care is needed to
select the right moment.  Adapted some code to make use of the new
API, eliminating some old cruft from qdatetimeparser_p.h in the
process.

[ChangeLog][QtCore][QDate] Added startOfDay() and endOfDay() methods
to provide a QDateTime at the start and end of a given date, taking
account of any time skipped by transitions, e.g. a DST spring-forward,
which can lead to a day starting at 01:00 or ending just before 23:00.

Task-number: QTBUG-64485
Change-Id: I3dd7a34bedfbec8f8af00c43d13f50f99346ecd0
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Edward Welbourne 2018-08-23 18:04:07 +02:00
parent 3abfa4dfff
commit 80853afd73
9 changed files with 462 additions and 29 deletions

View File

@ -128,7 +128,7 @@ qDebug("Time elapsed: %d ms", t.elapsed());
//! [11]
QDateTime now = QDateTime::currentDateTime();
QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0));
QDateTime xmas(QDate(now.date().year(), 12, 25).startOfDay());
qDebug("There are %d seconds to Christmas", now.secsTo(xmas));
//! [11]

View File

@ -1,6 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2016 Intel Corporation.
** Contact: https://www.qt.io/licensing/
**
@ -617,6 +617,268 @@ int QDate::weekNumber(int *yearNumber) const
return week;
}
static bool inDateTimeRange(qint64 jd, bool start)
{
using Bounds = std::numeric_limits<qint64>;
if (jd < Bounds::min() + JULIAN_DAY_FOR_EPOCH)
return false;
jd -= JULIAN_DAY_FOR_EPOCH;
const qint64 maxDay = Bounds::max() / MSECS_PER_DAY;
const qint64 minDay = Bounds::min() / MSECS_PER_DAY - 1;
// (Divisions rounded towards zero, as MSECS_PER_DAY has factors other than two.)
// Range includes start of last day and end of first:
if (start)
return jd > minDay && jd <= maxDay;
return jd >= minDay && jd < maxDay;
}
static QDateTime toEarliest(const QDate &day, const QDateTime &form)
{
const Qt::TimeSpec spec = form.timeSpec();
const int offset = (spec == Qt::OffsetFromUTC) ? form.offsetFromUtc() : 0;
#if QT_CONFIG(timezone)
QTimeZone zone;
if (spec == Qt::TimeZone)
zone = form.timeZone();
#endif
auto moment = [=](QTime time) {
switch (spec) {
case Qt::OffsetFromUTC: return QDateTime(day, time, spec, offset);
#if QT_CONFIG(timezone)
case Qt::TimeZone: return QDateTime(day, time, zone);
#endif
default: return QDateTime(day, time, spec);
}
};
// Longest routine time-zone transition is 2 hours:
QDateTime when = moment(QTime(2, 0));
if (!when.isValid()) {
// Noon should be safe ...
when = moment(QTime(12, 0));
if (!when.isValid()) {
// ... unless it's a 24-hour jump (moving the date-line)
when = moment(QTime(23, 59, 59, 999));
if (!when.isValid())
return QDateTime();
}
}
int high = when.time().msecsSinceStartOfDay() / 60000;
int low = 0;
// Binary chop to the right minute
while (high > low + 1) {
int mid = (high + low) / 2;
QDateTime probe = moment(QTime(mid / 60, mid % 60));
if (probe.isValid() && probe.date() == day) {
high = mid;
when = probe;
} else {
low = mid;
}
}
return when;
}
/*!
\since 5.14
\fn QDateTime QDate::startOfDay(Qt::TimeSpec spec, int offsetSeconds) const
\fn QDateTime QDate::startOfDay(const QTimeZone &zone) const
Returns the start-moment of the day. Usually, this shall be midnight at the
start of the day: however, if a time-zone transition causes the given date
to skip over that midnight (e.g. a DST spring-forward skipping from the end
of the previous day to 01:00 of the new day), the actual earliest time in
the day is returned. This can only arise when the start-moment is specified
in terms of a time-zone (by passing its QTimeZone as \a zone) or in terms of
local time (by passing Qt::LocalTime as \a spec; this is its default).
The \a offsetSeconds is ignored unless \a spec is Qt::OffsetFromUTC, when it
gives the implied zone's offset from UTC. As UTC and such zones have no
transitions, the start of the day is QTime(0, 0) in these cases.
In the rare case of a date that was entirely skipped (this happens when a
zone east of the international date-line switches to being west of it), the
return shall be invalid. Passing Qt::TimeZone as \a spec (instead of
passing a QTimeZone) or passing an invalid time-zone as \a zone will also
produce an invalid result, as shall dates that start outside the range
representable by QDateTime.
\sa endOfDay()
*/
QDateTime QDate::startOfDay(Qt::TimeSpec spec, int offsetSeconds) const
{
if (!inDateTimeRange(jd, true))
return QDateTime();
switch (spec) {
case Qt::TimeZone: // should pass a QTimeZone instead of Qt::TimeZone
qWarning() << "Called QDate::startOfDay(Qt::TimeZone) on" << *this;
return QDateTime();
case Qt::OffsetFromUTC:
case Qt::UTC:
return QDateTime(*this, QTime(0, 0), spec, offsetSeconds);
case Qt::LocalTime:
if (offsetSeconds)
qWarning("Ignoring offset (%d seconds) passed with Qt::LocalTime", offsetSeconds);
break;
}
QDateTime when(*this, QTime(0, 0), spec);
if (!when.isValid())
when = toEarliest(*this, when);
return when.isValid() ? when : QDateTime();
}
#if QT_CONFIG(timezone)
/*!
\overload
\since 5.14
*/
QDateTime QDate::startOfDay(const QTimeZone &zone) const
{
if (!inDateTimeRange(jd, true) || !zone.isValid())
return QDateTime();
QDateTime when(*this, QTime(0, 0), zone);
if (when.isValid())
return when;
// The start of the day must have fallen in a spring-forward's gap; find the spring-forward:
if (zone.hasTransitions()) {
QTimeZone::OffsetData tran = zone.previousTransition(QDateTime(*this, QTime(23, 59, 59, 999), zone));
const QDateTime &at = tran.atUtc.toTimeZone(zone);
if (at.isValid() && at.date() == *this)
return at;
}
when = toEarliest(*this, when);
return when.isValid() ? when : QDateTime();
}
#endif // timezone
static QDateTime toLatest(const QDate &day, const QDateTime &form)
{
const Qt::TimeSpec spec = form.timeSpec();
const int offset = (spec == Qt::OffsetFromUTC) ? form.offsetFromUtc() : 0;
#if QT_CONFIG(timezone)
QTimeZone zone;
if (spec == Qt::TimeZone)
zone = form.timeZone();
#endif
auto moment = [=](QTime time) {
switch (spec) {
case Qt::OffsetFromUTC: return QDateTime(day, time, spec, offset);
#if QT_CONFIG(timezone)
case Qt::TimeZone: return QDateTime(day, time, zone);
#endif
default: return QDateTime(day, time, spec);
}
};
// Longest routine time-zone transition is 2 hours:
QDateTime when = moment(QTime(21, 59, 59, 999));
if (!when.isValid()) {
// Noon should be safe ...
when = moment(QTime(12, 0));
if (!when.isValid()) {
// ... unless it's a 24-hour jump (moving the date-line)
when = moment(QTime(0, 0));
if (!when.isValid())
return QDateTime();
}
}
int high = 24 * 60;
int low = when.time().msecsSinceStartOfDay() / 60000;
// Binary chop to the right minute
while (high > low + 1) {
int mid = (high + low) / 2;
QDateTime probe = moment(QTime(mid / 60, mid % 60, 59, 999));
if (probe.isValid() && probe.date() == day) {
low = mid;
when = probe;
} else {
high = mid;
}
}
return when;
}
/*!
\since 5.14
\fn QDateTime QDate::endOfDay(Qt::TimeSpec spec, int offsetSeconds) const
\fn QDateTime QDate::endOfDay(const QTimeZone &zone) const
Returns the end-moment of the day. Usually, this is one millisecond before
the midnight at the end of the day: however, if a time-zone transition
causes the given date to skip over that midnight (e.g. a DST spring-forward
skipping from just before 23:00 to the start of the next day), the actual
latest time in the day is returned. This can only arise when the
start-moment is specified in terms of a time-zone (by passing its QTimeZone
as \a zone) or in terms of local time (by passing Qt::LocalTime as \a spec;
this is its default).
The \a offsetSeconds is ignored unless \a spec is Qt::OffsetFromUTC, when it
gives the implied zone's offset from UTC. As UTC and such zones have no
transitions, the end of the day is QTime(23, 59, 59, 999) in these cases.
In the rare case of a date that was entirely skipped (this happens when a
zone east of the international date-line switches to being west of it), the
return shall be invalid. Passing Qt::TimeZone as \a spec (instead of
passing a QTimeZone) will also produce an invalid result, as shall dates
that end outside the range representable by QDateTime.
\sa startOfDay()
*/
QDateTime QDate::endOfDay(Qt::TimeSpec spec, int offsetSeconds) const
{
if (!inDateTimeRange(jd, false))
return QDateTime();
switch (spec) {
case Qt::TimeZone: // should pass a QTimeZone instead of Qt::TimeZone
qWarning() << "Called QDate::endOfDay(Qt::TimeZone) on" << *this;
return QDateTime();
case Qt::UTC:
case Qt::OffsetFromUTC:
return QDateTime(*this, QTime(23, 59, 59, 999), spec, offsetSeconds);
case Qt::LocalTime:
if (offsetSeconds)
qWarning("Ignoring offset (%d seconds) passed with Qt::LocalTime", offsetSeconds);
break;
}
QDateTime when(*this, QTime(23, 59, 59, 999), spec);
if (!when.isValid())
when = toLatest(*this, when);
return when.isValid() ? when : QDateTime();
}
#if QT_CONFIG(timezone)
/*!
\overload
\since 5.14
*/
QDateTime QDate::endOfDay(const QTimeZone &zone) const
{
if (!inDateTimeRange(jd, false) || !zone.isValid())
return QDateTime();
QDateTime when(*this, QTime(23, 59, 59, 999), zone);
if (when.isValid())
return when;
// The end of the day must have fallen in a spring-forward's gap; find the spring-forward:
if (zone.hasTransitions()) {
QTimeZone::OffsetData tran = zone.nextTransition(QDateTime(*this, QTime(0, 0), zone));
const QDateTime &at = tran.atUtc.toTimeZone(zone);
if (at.isValid() && at.date() == *this)
return at;
}
when = toLatest(*this, when);
return when.isValid() ? when : QDateTime();
}
#endif // timezone
#if QT_DEPRECATED_SINCE(5, 11) && QT_CONFIG(textdate)
/*!
\since 4.5
@ -1468,7 +1730,8 @@ bool QDate::isLeapYear(int y)
\fn QTime::QTime()
Constructs a null time object. For a null time, isNull() returns \c true and
isValid() returns \c false. If you need a zero time, use QTime(0, 0).
isValid() returns \c false. If you need a zero time, use QTime(0, 0). For
the start of a day, see QDate::startOfDay().
\sa isNull(), isValid()
*/
@ -2392,8 +2655,8 @@ static void msecsToTime(qint64 msecs, QDate *date, QTime *time)
qint64 jd = JULIAN_DAY_FOR_EPOCH;
qint64 ds = 0;
if (qAbs(msecs) >= MSECS_PER_DAY) {
jd += (msecs / MSECS_PER_DAY);
if (msecs >= MSECS_PER_DAY || msecs <= -MSECS_PER_DAY) {
jd += msecs / MSECS_PER_DAY;
msecs %= MSECS_PER_DAY;
}

View File

@ -1,6 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2016 Intel Corporation.
** Contact: https://www.qt.io/licensing/
**
@ -55,6 +55,7 @@ Q_FORWARD_DECLARE_OBJC_CLASS(NSDate);
QT_BEGIN_NAMESPACE
class QTimeZone;
class QDateTime;
class Q_CORE_EXPORT QDate
{
@ -81,6 +82,13 @@ public:
int daysInYear() const;
int weekNumber(int *yearNum = nullptr) const;
QDateTime startOfDay(Qt::TimeSpec spec = Qt::LocalTime, int offsetSeconds = 0) const;
QDateTime endOfDay(Qt::TimeSpec spec = Qt::LocalTime, int offsetSeconds = 0) const;
#if QT_CONFIG(timezone)
QDateTime startOfDay(const QTimeZone &zone) const;
QDateTime endOfDay(const QTimeZone &zone) const;
#endif
#if QT_DEPRECATED_SINCE(5, 10) && QT_CONFIG(textdate)
QT_DEPRECATED_X("Use QLocale::monthName or QLocale::standaloneMonthName")
static QString shortMonthName(int month, MonthNameType type = DateFormat);

View File

@ -1982,7 +1982,7 @@ QString QDateTimeParser::stateName(State s) const
#if QT_CONFIG(datestring)
bool QDateTimeParser::fromString(const QString &t, QDate *date, QTime *time) const
{
QDateTime val(QDate(1900, 1, 1), QDATETIMEEDIT_TIME_MIN);
QDateTime val(QDate(1900, 1, 1).startOfDay());
const StateNode tmp = parse(t, -1, val, false);
if (tmp.state != Acceptable || tmp.conflicts) {
return false;
@ -2010,20 +2010,20 @@ QDateTime QDateTimeParser::getMinimum() const
{
// Cache the most common case
if (spec == Qt::LocalTime) {
static const QDateTime localTimeMin(QDATETIMEEDIT_DATE_MIN, QDATETIMEEDIT_TIME_MIN, Qt::LocalTime);
static const QDateTime localTimeMin(QDATETIMEEDIT_DATE_MIN.startOfDay(Qt::LocalTime));
return localTimeMin;
}
return QDateTime(QDATETIMEEDIT_DATE_MIN, QDATETIMEEDIT_TIME_MIN, spec);
return QDateTime(QDATETIMEEDIT_DATE_MIN.startOfDay(spec));
}
QDateTime QDateTimeParser::getMaximum() const
{
// Cache the most common case
if (spec == Qt::LocalTime) {
static const QDateTime localTimeMax(QDATETIMEEDIT_DATE_MAX, QDATETIMEEDIT_TIME_MAX, Qt::LocalTime);
static const QDateTime localTimeMax(QDATETIMEEDIT_DATE_MAX.endOfDay(Qt::LocalTime));
return localTimeMax;
}
return QDateTime(QDATETIMEEDIT_DATE_MAX, QDATETIMEEDIT_TIME_MAX, spec);
return QDateTime(QDATETIMEEDIT_DATE_MAX.endOfDay(spec));
}
QString QDateTimeParser::getAmPmText(AmPm ap, Case cs) const

View File

@ -65,14 +65,11 @@
QT_REQUIRE_CONFIG(datetimeparser);
#define QDATETIMEEDIT_TIME_MIN QTime(0, 0, 0, 0)
#define QDATETIMEEDIT_TIME_MAX QTime(23, 59, 59, 999)
#define QDATETIMEEDIT_TIME_MIN QTime(0, 0) // Prefer QDate::startOfDay()
#define QDATETIMEEDIT_TIME_MAX QTime(23, 59, 59, 999) // Prefer QDate::endOfDay()
#define QDATETIMEEDIT_DATE_MIN QDate(100, 1, 1)
#define QDATETIMEEDIT_COMPAT_DATE_MIN QDate(1752, 9, 14)
#define QDATETIMEEDIT_DATE_MAX QDate(9999, 12, 31)
#define QDATETIMEEDIT_DATETIME_MIN QDateTime(QDATETIMEEDIT_DATE_MIN, QDATETIMEEDIT_TIME_MIN)
#define QDATETIMEEDIT_COMPAT_DATETIME_MIN QDateTime(QDATETIMEEDIT_COMPAT_DATE_MIN, QDATETIMEEDIT_TIME_MIN)
#define QDATETIMEEDIT_DATETIME_MAX QDateTime(QDATETIMEEDIT_DATE_MAX, QDATETIMEEDIT_TIME_MAX)
#define QDATETIMEEDIT_DATE_INITIAL QDate(2000, 1, 1)
QT_BEGIN_NAMESPACE

View File

@ -2097,7 +2097,7 @@ QVariant operator*(const QVariant &arg1, double multiplier)
days -= daysInt;
qint64 msecs = qint64(arg1.toDateTime().time().msecsSinceStartOfDay() * multiplier
+ days * (24 * 3600 * 1000));
ret = QDateTime(QDATETIMEEDIT_DATE_MIN.addDays(daysInt), QTime::fromMSecsSinceStartOfDay(msecs));
ret = QDATETIMEEDIT_DATE_MIN.addDays(daysInt).startOfDay().addMSecs(msecs);
break;
}
#endif // datetimeparser

View File

@ -153,7 +153,7 @@ QDateTimeEdit::QDateTimeEdit(QWidget *parent)
: QAbstractSpinBox(*new QDateTimeEditPrivate, parent)
{
Q_D(QDateTimeEdit);
d->init(QDateTime(QDATETIMEEDIT_DATE_INITIAL, QDATETIMEEDIT_TIME_MIN));
d->init(QDATETIMEEDIT_DATE_INITIAL.startOfDay());
}
/*!
@ -165,8 +165,7 @@ QDateTimeEdit::QDateTimeEdit(const QDateTime &datetime, QWidget *parent)
: QAbstractSpinBox(*new QDateTimeEditPrivate, parent)
{
Q_D(QDateTimeEdit);
d->init(datetime.isValid() ? datetime : QDateTime(QDATETIMEEDIT_DATE_INITIAL,
QDATETIMEEDIT_TIME_MIN));
d->init(datetime.isValid() ? datetime : QDATETIMEEDIT_DATE_INITIAL.startOfDay());
}
/*!
@ -342,7 +341,7 @@ QDateTime QDateTimeEdit::minimumDateTime() const
void QDateTimeEdit::clearMinimumDateTime()
{
setMinimumDateTime(QDateTime(QDATETIMEEDIT_COMPAT_DATE_MIN, QDATETIMEEDIT_TIME_MIN));
setMinimumDateTime(QDATETIMEEDIT_COMPAT_DATE_MIN.startOfDay());
}
void QDateTimeEdit::setMinimumDateTime(const QDateTime &dt)
@ -385,7 +384,7 @@ QDateTime QDateTimeEdit::maximumDateTime() const
void QDateTimeEdit::clearMaximumDateTime()
{
setMaximumDateTime(QDATETIMEEDIT_DATETIME_MAX);
setMaximumDateTime(QDATETIMEEDIT_DATE_MAX.endOfDay());
}
void QDateTimeEdit::setMaximumDateTime(const QDateTime &dt)
@ -1658,8 +1657,8 @@ QDateTimeEditPrivate::QDateTimeEditPrivate()
first.pos = 0;
sections = 0;
calendarPopup = false;
minimum = QDATETIMEEDIT_COMPAT_DATETIME_MIN;
maximum = QDATETIMEEDIT_DATETIME_MAX;
minimum = QDATETIMEEDIT_COMPAT_DATE_MIN.startOfDay();
maximum = QDATETIMEEDIT_DATE_MAX.endOfDay();
arrowState = QStyle::State_None;
monthCalendar = 0;
readLocaleSettings();
@ -1683,8 +1682,8 @@ void QDateTimeEditPrivate::updateTimeSpec()
const bool dateShown = (sections & QDateTimeEdit::DateSections_Mask);
if (!dateShown) {
if (minimum.toTime() >= maximum.toTime()){
minimum = QDateTime(value.toDate(), QDATETIMEEDIT_TIME_MIN, spec);
maximum = QDateTime(value.toDate(), QDATETIMEEDIT_TIME_MAX, spec);
minimum = value.toDate().startOfDay(spec);
maximum = value.toDate().endOfDay(spec);
}
}
}
@ -2382,7 +2381,7 @@ void QDateTimeEditPrivate::init(const QVariant &var)
Q_Q(QDateTimeEdit);
switch (var.type()) {
case QVariant::Date:
value = QDateTime(var.toDate(), QDATETIMEEDIT_TIME_MIN);
value = var.toDate().startOfDay();
updateTimeSpec();
q->setDisplayFormat(defaultDateFormat);
if (sectionNodes.isEmpty()) // ### safeguard for broken locale

View File

@ -1,4 +1,4 @@
CONFIG += testcase
TARGET = tst_qdate
QT = core testlib
QT = core-private testlib
SOURCES = tst_qdate.cpp

View File

@ -1,6 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2016 Intel Corporation.
** Contact: https://www.qt.io/licensing/
**
@ -27,6 +27,7 @@
**
****************************************************************************/
#include <private/qglobal_p.h> // for the icu feature test
#include <QtTest/QtTest>
#include <qdatetime.h>
#include <qlocale.h>
@ -54,6 +55,13 @@ private slots:
void weekNumber_invalid();
void weekNumber_data();
void weekNumber();
#if QT_CONFIG(timezone)
void startOfDay_endOfDay_data();
void startOfDay_endOfDay();
#endif
void startOfDay_endOfDay_fixed_data();
void startOfDay_endOfDay_fixed();
void startOfDay_endOfDay_bounds();
void julianDaysLimits();
void addDays_data();
void addDays();
@ -458,6 +466,164 @@ void tst_QDate::weekNumber_invalid()
QCOMPARE( dt.weekNumber( &yearNumber ), 0 );
}
#if QT_CONFIG(timezone)
void tst_QDate::startOfDay_endOfDay_data()
{
QTest::addColumn<QDate>("date"); // Typically a spring-forward.
// A zone in which that date's start and end are worth checking:
QTest::addColumn<QByteArray>("zoneName");
// The start and end times in that zone:
QTest::addColumn<QTime>("start");
QTest::addColumn<QTime>("end");
const QTime initial(0, 0), final(23, 59, 59, 999), invalid(QDateTime().time());
QTest::newRow("epoch")
<< QDate(1970, 1, 1) << QByteArray("UTC")
<< initial << final;
QTest::newRow("Brazil")
<< QDate(2008, 10, 19) << QByteArray("America/Sao_Paulo")
<< QTime(1, 0) << final;
#if QT_CONFIG(icu) || !defined(Q_OS_WIN) // MS's TZ APIs lack data
QTest::newRow("Sofia")
<< QDate(1994, 3, 27) << QByteArray("Europe/Sofia")
<< QTime(1, 0) << final;
#endif
QTest::newRow("Kiritimati")
<< QDate(1994, 12, 31) << QByteArray("Pacific/Kiritimati")
<< invalid << invalid;
QTest::newRow("Samoa")
<< QDate(2011, 12, 30) << QByteArray("Pacific/Apia")
<< invalid << invalid;
// TODO: find other zones with transitions at/crossing midnight.
}
void tst_QDate::startOfDay_endOfDay()
{
QFETCH(QDate, date);
QFETCH(QByteArray, zoneName);
QFETCH(QTime, start);
QFETCH(QTime, end);
const QTimeZone zone(zoneName);
const bool isSystem = QTimeZone::systemTimeZone() == zone;
QDateTime front(date.startOfDay(zone)), back(date.endOfDay(zone));
if (end.isValid())
QCOMPARE(date.addDays(1).startOfDay(zone).addMSecs(-1), back);
if (start.isValid())
QCOMPARE(date.addDays(-1).endOfDay(zone).addMSecs(1), front);
do { // Avoids duplicating these tests for local-time when it *is* zone:
if (start.isValid()) {
QCOMPARE(front.date(), date);
QCOMPARE(front.time(), start);
}
if (end.isValid()) {
QCOMPARE(back.date(), date);
QCOMPARE(back.time(), end);
}
if (front.timeSpec() == Qt::LocalTime)
break;
front = date.startOfDay(Qt::LocalTime);
back = date.endOfDay(Qt::LocalTime);
} while (isSystem);
if (end.isValid())
QCOMPARE(date.addDays(1).startOfDay(Qt::LocalTime).addMSecs(-1), back);
if (start.isValid())
QCOMPARE(date.addDays(-1).endOfDay(Qt::LocalTime).addMSecs(1), front);
if (!isSystem) {
// These might fail if system zone coincides with zone; but only if it
// did something similarly unusual on the date picked for this test.
if (start.isValid()) {
QCOMPARE(front.date(), date);
QCOMPARE(front.time(), QTime(0, 0));
}
if (end.isValid()) {
QCOMPARE(back.date(), date);
QCOMPARE(back.time(), QTime(23, 59, 59, 999));
}
}
}
#endif // timezone
void tst_QDate::startOfDay_endOfDay_fixed_data()
{
const qint64 kilo(1000);
using Bounds = std::numeric_limits<qint64>;
const QDateTime
first(QDateTime::fromMSecsSinceEpoch(Bounds::min() + 1, Qt::UTC)),
start32sign(QDateTime::fromMSecsSinceEpoch(-0x80000000L * kilo, Qt::UTC)),
end32sign(QDateTime::fromMSecsSinceEpoch(0x80000000L * kilo, Qt::UTC)),
end32unsign(QDateTime::fromMSecsSinceEpoch(0x100000000L * kilo, Qt::UTC)),
last(QDateTime::fromMSecsSinceEpoch(Bounds::max(), Qt::UTC));
const struct {
const char *name;
QDate date;
} data[] = {
{ "epoch", QDate(1970, 1, 1) },
{ "y2k-leap-day", QDate(2000, 2, 29) },
// Just outside the start and end of 32-bit time_t:
{ "pre-sign32", QDate(start32sign.date().year(), 1, 1) },
{ "post-sign32", QDate(end32sign.date().year(), 12, 31) },
{ "post-uint32", QDate(end32unsign.date().year(), 12, 31) },
// Just inside the start and end of QDateTime's range:
{ "first-full", first.date().addDays(1) },
{ "last-full", last.date().addDays(-1) }
};
QTest::addColumn<QDate>("date");
for (const auto &r : data)
QTest::newRow(r.name) << r.date;
}
void tst_QDate::startOfDay_endOfDay_fixed()
{
const QTime early(0, 0), late(23, 59, 59, 999);
QFETCH(QDate, date);
QDateTime start(date.startOfDay(Qt::UTC));
QDateTime end(date.endOfDay(Qt::UTC));
QCOMPARE(start.date(), date);
QCOMPARE(end.date(), date);
QCOMPARE(start.time(), early);
QCOMPARE(end.time(), late);
QCOMPARE(date.addDays(1).startOfDay(Qt::UTC).addMSecs(-1), end);
QCOMPARE(date.addDays(-1).endOfDay(Qt::UTC).addMSecs(1), start);
for (int offset = -60 * 16; offset <= 60 * 16; offset += 65) {
start = date.startOfDay(Qt::OffsetFromUTC, offset);
end = date.endOfDay(Qt::OffsetFromUTC, offset);
QCOMPARE(start.date(), date);
QCOMPARE(end.date(), date);
QCOMPARE(start.time(), early);
QCOMPARE(end.time(), late);
QCOMPARE(date.addDays(1).startOfDay(Qt::OffsetFromUTC, offset).addMSecs(-1), end);
QCOMPARE(date.addDays(-1).endOfDay(Qt::OffsetFromUTC, offset).addMSecs(1), start);
}
}
void tst_QDate::startOfDay_endOfDay_bounds()
{
// Check the days in which QDateTime's range starts and ends:
using Bounds = std::numeric_limits<qint64>;
const QDateTime
first(QDateTime::fromMSecsSinceEpoch(Bounds::min(), Qt::UTC)),
last(QDateTime::fromMSecsSinceEpoch(Bounds::max(), Qt::UTC)),
epoch(QDateTime::fromMSecsSinceEpoch(0, Qt::UTC));
// First, check these *are* the start and end of QDateTime's range:
QVERIFY(first.isValid());
QVERIFY(last.isValid());
QVERIFY(first < epoch);
QVERIFY(last > epoch);
// QDateTime's addMSecs doesn't check against {und,ov}erflow ...
QVERIFY(!first.addMSecs(-1).isValid() || first.addMSecs(-1) > first);
QVERIFY(!last.addMSecs(1).isValid() || last.addMSecs(1) < last);
// Now test start/end methods with them:
QCOMPARE(first.date().endOfDay(Qt::UTC).time(), QTime(23, 59, 59, 999));
QCOMPARE(last.date().startOfDay(Qt::UTC).time(), QTime(0, 0));
QVERIFY(!first.date().startOfDay(Qt::UTC).isValid());
QVERIFY(!last.date().endOfDay(Qt::UTC).isValid());
}
void tst_QDate::julianDaysLimits()
{
qint64 min = std::numeric_limits<qint64>::min();