Unifying Qt::TimeSpec within QTimeZone

Qt 6.5 sees a quiet revolution in QDateTime's API, built on one in QTimeZone's.

Previously

QDateTime had an overload pair of constructors that looked like:

    QDateTime(QDate date, QTime time, Qt::TimeSpec spec = Qt::LocalTime, int offsetSeconds = 0);
#if QT_CONFIG(timezone)
    QDateTime(QDate date, QTime time, const QTimeZone &timeZone);
#endif // timezone

which, in turn, implied a similar duplication in various methods of QDateTime and QDate that returned a new QDateTime object.

class Q_CORE_EXPORT QDate
{
    // …
    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
    // …
};

// …

class Q_CORE_EXPORT QDateTime
{
    // …
    void setTimeSpec(Qt::TimeSpec spec);
    void setOffsetFromUtc(int offsetSeconds);
#if QT_CONFIG(timezone)
    void setTimeZone(const QTimeZone &toZone);
#endif // timezone
    // … similar for s/set/to/ …

    static QDateTime fromMSecsSinceEpoch(qint64 msecs, Qt::TimeSpec spec = Qt::LocalTime,
                                         int offsetFromUtc = 0);
    static QDateTime fromSecsSinceEpoch(qint64 secs, Qt::TimeSpec spec = Qt::LocalTime,
                                        int offsetFromUtc = 0);

#if QT_CONFIG(timezone)
    static QDateTime fromMSecsSinceEpoch(qint64 msecs, const QTimeZone &timeZone);
    static QDateTime fromSecsSinceEpoch(qint64 secs, const QTimeZone &timeZone);
#endif

    // …
};

Any future new methods or constructors were doomed to repeat this duplication, complete with its #if-ery (on a feature that's only defined on most platforms).  So it wasn't a nice API to try to grow or evolve.  But that's my problem, as a developer working on it; how about client code ?

Client code's problem

Code that needed to manipulate QDateTime instances had, in some cases, to switch on the Qt::TimeSpec of one of them.  For example, if your code has a QDateTime active that it wants all the others it manipulates to use the same time spec as, it ended up needing a time-spec converter, for any received QDateTime datetime that looked like

    switch (active.timeSpec()) {
    case Qt::UTC:
        return datetime.toUTC();
    case Qt::LocalTime:
        return datetime.toLocalTime();
    case Qt::OffsetFromUTC:
        return datetime.toOffsetFromUtc(active.offsetFromUtc());
    case Qt::TimeZone:
#if QT_CONFIG(timezone)
        return datetime.toTimeZone(active.timeZone());
#else
        qWarning("Enable timezone feature to support Qt::TimeZone");
        return datetime; // Failed to convert.
#endif
    }
    Q_UNREACHABLE();

The extra complexity of such changes is one factor in why QDateTimeEdit still only supports UTC and local time; see QTBUG-80417.

The solution

The initial version of this was called QTimeSystem but Thiago persuaded me to roll it into QTimeZone, which did make a whole lot of sense, although the implementation had some trickiness to it.  (Thanks to Ville and Marc for help with working out how to do that.)  So now QTimeZone can be either


  • an actual backend-backed time-zone object, as before, or

  •  
  • a lightweight time representation.

Only the former is pimpled; the latter inhabits the space its d-ptr would occupy, with the least significant bits of that being an enum to tell it what it is; 0 means pimpled, of course.

A lightweight time representation, then, is just a Qt::TimeSpec packaged, when necessary, with the offset it needs to carry around with it. When the Qt::TimeSpec is Qt::TimeZone, we're in the pimpled side of the union and using the same old back-end as ever, subject to the same old #if-ery on feature timezone.  The rest of the class is now freed of that #if-ery and the header no longer does a QT_REQUIRE_CONFIG(timezone); so it can always be included.

6.5 APIs

The changed QTimeZone API now only conditions the backend-based aspects on feature timezone; all the rest is always present.

The changed QDateTime API now prefers to go via QTimeZone, obtained from QDateTime::timeRepresentation() – the name timeZone() being already taken; it could now be implemented as timeRepresentation().asBackendZone().  That client code converter for time-spec becomes a one-liner, converting to the time-zone of the active value:

    return datetime.toTimeZone(active.timeRepresentation());

and passing a QTimeZone to functions returning a QDateTime is now the uniform way to call relevant functions.  Qt::TimeSpec should now almost never need to be seen in public any more.

The one frustrating detail is that, to avoid mutual inclusion issues, qdatetime.h can't #include <QTimeZone> so the methods there taking a QTimeZone parameter can't pass QTimeZone::LocalTime as default, obliging them to still have an overload without the QTimeZone parameter, for backwards compatibility – but you should never need to use these.

The usual extent of change needed in existing code, to minimally adapt to the coming deprecations, is to replace the Qt prefix with QTimeZone in all uses of Qt::UTC and Qt::LocalTime (which are easy to search for).  Further changes to make the most of the new API can follow at leisure.

As a result, QTBUG-80417 is now more or less at the point where we just need to change the API of QDateTimeEdit to access the internals which just got simpler while becoming able to support any time-zone, as soon as the public API exposes that.


Comments