Clarify the behavior of QDateTime around 24-hour transitions

For those that simply repeat or skip a whole calendar day, life is
fairly simple. However, Alaska's 24-hour transition at 15:30 LMT Sitka
(incidentally combined with a change of calendar) is a bit trickier.
Also fix a typo I noticed in passing.

Write tests to determine what the actual behavior is and document
enough to make the actual behavior seem unsurprising once encountered,
without trying to go into all the excruciating details. Naturally, MS
time-zone data lacks the data on the historic transitions involved in
these tests, so MS (when not using ICU's time-zone data) is excluded.
It seems Cupertino believes Alaska was always in the USA, too.

Change-Id: Ia638c04d2ffc3a956a70a2a85badb7bbfdbb791c
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Edward Welbourne 2023-08-10 17:54:12 +02:00
parent a49ccc08c3
commit e72a898c50
2 changed files with 110 additions and 3 deletions

View File

@ -3533,7 +3533,7 @@ QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTime
possible. On Windows, where the system doesn't support historical timezone
data, historical accuracy is not maintained with respect to timezone
transitions, notably including DST. However, building Qt with the ICU
library will equipe QTimeZone with the same timezone database as is used on
library will equip QTimeZone with the same timezone database as is used on
Unix.
\section2 Timezone transitions
@ -3598,6 +3598,25 @@ QDateTime::Data QDateTimePrivate::create(QDate toDate, QTime toTime, const QTime
inaccurate. Furthermore, for future dates, the local time zone's offsets and
DST rules may change before that date comes around.
\section3 Whole day transitions
A small number of zones have skipped or repeated entire days as part of
moving The International Date Line across themselves. For these, daysTo()
will be unaware of the duplication or gap, simply using the difference in
calendar date; in contrast, msecsTo() and secsTo() know the true time
interval. Likewise, addMSecs() and addSecs() correspond directly to elapsed
time, where addDays(), addMonths() and addYears() follow the nominal
calendar, aside from where landing in a gap or duplication requires
resolving an ambiguity or invalidity due to a duplication or omission.
\note Days "lost" during a change of calendar, such as from Julian to
Gregorian, do not affect QDateTime. Although the two calendars describe
dates differently, the successive days across the change are described by
consecutive QDate instances, each one day later than the previous, as
described by either calendar or by their toJulianDay() values. In contrast,
a zone skipping or duplicating a day is changing its description of \e time,
not date, for all that it does so by a whole 24 hours.
\section2 Offsets From UTC
Offsets from UTC are measured in seconds east of Greenwich. The moment

View File

@ -11,6 +11,13 @@
#ifdef Q_OS_WIN
# include <qt_windows.h>
# if !QT_CONFIG(icu)
// The native MS back-end for time-zones lacks info about historic transitions:
# define INADEQUATE_TZ_DATA
# endif
#endif
#ifdef Q_OS_ANDROID // Also seems to lack full-day zone transitions:
# define INADEQUATE_TZ_DATA
#endif
using namespace Qt::StringLiterals;
@ -1289,7 +1296,50 @@ void tst_QDateTime::addDays()
QCOMPARE(dt2.timeSpec(), Qt::TimeZone);
QCOMPARE(dt2.timeZone(), cet);
}
#endif
# ifndef INADEQUATE_TZ_DATA
if (const QTimeZone lint("Pacific/Kiritimati"); lint.isValid()) {
// Line Islands Time skipped Sat 1994-12-31:
dt1 = QDateTime(QDate(1994, 12, 30), QTime(12, 0), lint);
dt2 = QDateTime(QDate(1995, 1, 1), QTime(12, 0), lint);
// Trying to step into the hole gets the other side:
QCOMPARE(dt1.addDays(1), dt2);
QCOMPARE(dt2.addDays(-1), dt1);
// But the other side is in fact two days away:
QCOMPARE(dt1.addDays(2), dt2);
QCOMPARE(dt2.addDays(-2), dt1);
QCOMPARE(dt1.daysTo(dt2), 2);
}
# ifndef Q_OS_DARWIN
if (const QTimeZone alaska("America/Anchorage"); alaska.isValid()) {
// On Julian date 1867, Sat Oct 7 (at 14:31 local solar mean time for
// Anchorage, 15:30 LMT in Sitka, which hosted the transfer ceremony)
// Russia sold Alaska to the USA, which changed the calendar to
// Gregorian, hence the date to Fri Oct 18. Compare addSecs:Alaska-Day.
// Friday evening and Saturday morning were repeated, with different dates.
// Friday noon, as described by the Russians:
dt1 = QDateTime(QDate(1867, 10, 6, QCalendar(QCalendar::System::Julian)),
QTime(12, 0), alaska);
// Sunday noon, as described by the Americans:
dt2 = QDateTime(QDate(1867, 10, 20), QTime(12, 0), alaska);
// Three elapsed days, but daysTo() and addDays only see two:
QCOMPARE(dt1.addDays(2), dt2);
QCOMPARE(dt2.addDays(-2), dt1);
QCOMPARE(dt1.daysTo(dt2), 2);
// Stepping into the duplicated day (Julian 7th, Gregorian 19th) gets
// the nearer side, with the same nominal date (and time):
QCOMPARE(dt1.addDays(1).date(), dt2.addDays(-1).date());
QCOMPARE(dt1.addDays(1).time(), dt2.addDays(-1).time());
QCOMPARE(dt1.addDays(1).daysTo(dt2.addDays(-1)), 0);
// Yet they differ by a day:
QCOMPARE_NE(dt1.addDays(1), dt2.addDays(-1));
QCOMPARE(dt1.addDays(1).secsTo(dt2.addDays(-1)), 24 * 60 * 60);
// Stepping from one duplicate one day towards the other jumps it:
QCOMPARE(dt1, dt2.addDays(-1).addDays(-1));
QCOMPARE(dt1.addDays(1).addDays(1), dt2);
}
# endif // Darwin
# endif // inadequate zone data
#endif // timezone
// Baja Mexico has a transition at the epoch, see fromStringDateFormat_data().
if (QDateTime(QDate(1969, 12, 30), QTime(0, 0)).secsTo(
@ -1578,6 +1628,43 @@ void tst_QDateTime::addMSecs_data()
QTest::newRow("to-first")
<< QDateTime::fromSecsSinceEpoch(1 - maxSeconds, UTC) << qint64(-1)
<< QDateTime::fromSecsSinceEpoch(-maxSeconds, UTC);
#if QT_CONFIG(timezone)
if (const QTimeZone cet("Europe/Oslo"); cet.isValid()) {
QTest::newRow("CET-spring-forward")
<< QDateTime(QDate(2023, 3, 26), QTime(1, 30), cet) << qint64(60 * 60)
<< QDateTime(QDate(2023, 3, 26), QTime(3, 30), cet);
QTest::newRow("CET-fall-back")
<< QDateTime(QDate(2023, 10, 29), QTime(1, 30), cet) << qint64(3 * 60 * 60)
<< QDateTime(QDate(2023, 10, 29), QTime(3, 30), cet);
}
# ifndef INADEQUATE_TZ_DATA
const QTimeZone lint("Pacific/Kiritimati");
if (lint.isValid()) {
// Line Islands Time skipped Sat 1994-12-31:
QTest::newRow("Kiritimati-day-off")
<< QDateTime(QDate(1994, 12, 30), QTime(23, 30), lint) << qint64(60 * 60)
<< QDateTime(QDate(1995, 1, 1), QTime(0, 30), lint);
}
# ifndef Q_OS_DARWIN
if (const QTimeZone alaska("America/Anchorage"); alaska.isValid()) {
// On Julian date 1867, Sat Oct 7 (at 14:31 local solar mean time for
// Anchorage, 15:30 LMT in Sitka, which hosted the transfer ceremony)
// Russia sold Alaska to the USA, which changed the calendar to
// Gregorian, hence the date to Fri Oct 18. Contrast addDays().
const QDate sat(1867, 10, 19);
Q_ASSERT(sat == QDate(1867, 10, 7, QCalendar(QCalendar::System::Julian)));
// At the start of the day, it was Sat 7th; by evening it was Fri 18th;
// then the next day was Sat 19th.
QTest::newRow("Alaska-Day")
// The actual morning of the hand-over:
<< QDateTime(sat, QTime(6, 0), alaska) << qint64(12 * 60 * 60)
// The evening of the same day.
<< QDateTime(sat, QTime(18, 0), alaska).addDays(-1);
}
# endif // Darwin
# endif // inadequate zone data
#endif // timezone
}
void tst_QDateTime::addSecs_data()
@ -1621,6 +1708,7 @@ void tst_QDateTime::addSecs()
QCOMPARE(result - std::chrono::seconds(nsecs), dt);
test3 -= std::chrono::seconds(nsecs);
QCOMPARE(test3, dt);
QCOMPARE(dt.secsTo(result), nsecs);
}
}