[Development] Calendar Systems proposal

Edward Welbourne edward.welbourne at qt.io
Thu Jan 5 17:12:16 CET 2017


Well, I've missed a long and lively discussion while I was away.  I've
now caught up with e-mail, digested the thread and done some (but by no
means enough) background reading; that's left me with some jumbled
notes.  This shall be long - let's start with the thread (and fit some
of those notes into answering it):

Soroush Rabiei:
>>>>>>>>>>> Please tell me more about your idea when you're back.

Take KCalendarSystem [0], replace its uses of QDate with a Julian day
number (as int [1]), tidy up a bit and have relevant Q(Date|Time)+
methods take one of these as an optional parameter (default: Gregorian)
that configures what they mean by things like year, month, week and day
within any of these.

[0] https://api.kde.org/4.x-api/kdelibs-apidocs/kdecore/html/classKCalendarSystem.html

[1] I notice that QDate uses a qint64 for Julian day, which is overkill;
a 32-bit integral type lets us have 5,879,610 years each side of our
epoch, which is ample, IMO.  KCalendarSystem seems to agree.  QDate
can't change that for its data member, nor do I propose to change its
method signatures, but I see no reason for the calendar system to use
anything bigger than int for Julian dates.

Of course, BC/SC reasons mean "optional parameter" won't (until maybe Qt
6) be an added parameter (with a default) at the ends of existing
methods but a (probably first and non-optional) parameter of overloads
of the existing methods.  Or something like that.

>>>>>>>>>>> I suppose adding new calendars by users, requires
>>>>>>>>>>> subclassing QDate?

A user calendar would be implemented by sub-classing the calendar system
virtual base class.  Instances of that would be passed to methods of
(stock) QDate to over-ride its default use of Gregorian.

>>>>>>>>>>> Or maybe somehow extend the enum that contains calendar
>>>>>>>>>>> systems[?].

An enum can only support the calendar systems for which Qt contains
code.  There shall always be calendars we don't support: it would be
best to ensure users can get support for that, one way or another.  An
app-developer with enough users who want a particular calendar system
should be able to add support for it without waiting for us to care
about that calendar system.

>>>>>>>>>>> I think adding the information on which calendar system is
>>>>>>>>>>> current date is on, can be added as a member (handlre/enum)
>>>>>>>>>>> to QDate.

{Backwards,Source} Compatibility (BC/SC, above) issues preclude that.

Sune Vuorela:
>>>>>>>>>> I think you need to touch quite some of the 'inner bits' of
>>>>>>>>>> date / time

That's fine - messing with internals is OK, it's public A[BP]Is that
can't be changed - except by adding methods.  Most of what I envisage us
changing for this would be turning some existing static functions in the
code into methods of a QGregorianCalendarSystem class, that implements
a generic QCalendarSystem API.

>>>>>>>>>> my two missing pet features:
>>>>>>>>>>  - Partial dates

I'm quite sure these don't belong in Q(Date|Time)+, for all that I can
see their value in calendar (and similar) apps.  There might be a case
for a recurrent event class, that supports all the funky things for
repeating calendar entries - e.g. the first Sunday following a full Moon
which falls on or after the Spring equinox, optionally with "full moon"
and/or "equinox" defined by some arbitrary set of rules rather than by
actual astronomical reality [2] - which would surely include partial
dates.  I encourage you to think about designing that and would be happy
to review any contribution that results ;^>

[2] https://en.wikipedia.org/wiki/Computus
I did not just make that up.

>>>>>>>>>>  - Date/time intervals/delta's.

Those I can see a better case for: they could be handled by operator-()
methods (with returns measured in the given units) of QDate (days),
QTime (milliseconds), QDateTime (milliseconds, 64-bit).  No operator+;
use addDays(), addSeconds(), &c.

>>>>>>>>>> (by date/time deltas, I e.g. I started working somewhere on
>>>>>>>>>> november 1st 2014. and stopped january 3rd 2015. How long did
>>>>>>>>>> I work there)

Quite.  Note, however, that KCalendarSystem::dateDifference() raises a
valid point for broken-down difference in days, months, years - which
need not equate to a simple number of days. [2016-03-01] - [2016-02-01]
is 29 days but the dateDifference is 1 month; while the same in 2017 is
28 days but still 1 month; and [2017-02-01] - [2016-02-01] is one year,
the same as [2017-05-01] - [2016-05-01], but the former is 366 days and
the latter is 365 days.  Other calendar systems shall doubtless give
more such subtleties.

André Somers
>>>>>>>>> If memory serves me right: When, years ago, I tried to get the
>>>>>>>>> latter in, the work was a bit blocked because somebody else
>>>>>>>>> what working on... calendar support. :-)

If you can find either your patches for diff or their patches for
calendar support, I'm sure they'd be useful input to the present effort.

Ch'Gans (a.k.a. Chris):
>>>>>>>> Boost have them all: date/time, calendar, time zone, time
>>>>>>>> period and time duration
>>>>>>>> Date/Time is a very tricky subject, why not rely on boost
>>>>>>>> implementation?
>>>>>>>>
>>>>>>>> http://www.boost.org/doc/libs/1_51_0/libs/locale/doc/html/group__date__time.html

The documentation is a bit thin - leaving me guessing at rather more
than I should need to - but my impression is that this isn't actually as
good as what we have, although it does have some (inadequately
described) handling of alternative calendars.  I'm fairly sure we can do
better.

Thiago
>>>>>>> I don't expect the calendaring system to require any changes to
>>>>>>> QDate internals. It stores a Julian day, that's all.

The QDate data object's internals won't need to change, no; but the code
it presently uses to map between its internal Julian day and years,
months, etc. is all Gregorian and would need rearranged.

Soroush:
>>>>>> QDate knows how many days are in a month, what are month names
>>>>>> (in case locale is not present), how to find leap years, and how
>>>>>> to convert a JulianDay to year, month and day in Gregorian
>>>>>> calendar.

... all of which should clearly move to the Gregorian calendar class
that'll still be used by default to do these jobs; but we can have
methods accept another calendar object instead.

Thiago:
>>>>> QDate will continue to support Gregorian day, month, year, as well
>>>>> as ISO weeks.

... *by default* - but we should be able to have it support some other
calendar's day, month, year and weeks by supplying other calendar
objects to suitable overloads of methods.

Frédéric Marchal:
>>>> As a developer, I don't want to know about the gory details of date
>>>> computation with calendars I have never heard of. Adding "one
>>>> month" to a date should use the same code whatever the underlying
>>>> calendar is.

The calendar wouldn't be "underlying" in what I'm suggesting: client
code (e.g. a calendar app) would need a calendar-system object to pass
in to QDate's methods.  We can't have QDate remember which calendar to
use (ABI change); in any case, I'm fairly sure I don't want it to - as I
do also want to be able to ask about the Gregorian date of the start of
Ramadan (for example), which will mean creating a QDate with one
calendar system and using another to query it.  The QDate shall remain
just a Julian day packaged with some suitable methods, some of which
shall know how to interact with calendar systems.

>>>> The correct calendar to use should be a simple configuration
>>>> parameter

How the app built on Qt picks its calendar system is for the app to
decide.  If the app has a config enum that tells it which
Q*CalendarSystem to instantiate, that's fine by me: but I do also want
apps to be able to support calendar systems that aren't mass-market
enough that it makes sense for *us* to support them.

The nice thing about an instance-based approach is that an app can even
support loading a user-selected plugin that supports an API that lets
the app extract a calendar-system object that describes the calendar the
user actually wants to use - all without the app developer (or us)
having to know the details of that calendar system, merely how to load
the object for it.  Then the long tail of users with arcane calendars
can be catered to without us (or the app developer) having to ever see
(much less write, maintain or debug) the code that implements it.  One
member of the community that uses that calendar system shall need to
write the plugin (or whatever) and then all apps that know how to load
that plugin can support those users to their satisfaction.  We could
just support the more widely-used calendar systems.

Soroush:
>>>> Can we add arguments to all methods with a default value? Something
>>>> like:
>>>>
>>>>     QDate d;
>>>>     qDebug() << d.year();               // prints 2016
>>>>     qDebug() << d.year(QCalendar::Jalali); // prints 1395

That's what I have in mind, only with a QJalaliCalendar() instance in
place of your enum.

>>>> And then force relevant widgets/views to show/edit date and time on
>>>> a specific calendar system:
>>>>
>>>>     QDateEdit de;
>>>>     de.setDate(d);
>>>>     de.setCalendarSystem(QCalendar::Hebrew); // Is this possible?

That's trickier, as it would change QDateEdit's ABI.  Changing QDateEdit
to support other calendar systems would likely involve a major re-write.
However, a calendar-system-aware QDate might help you write your own
CalendarAwareDateEdit widget, based on QDateEdit.  It might then replace
QDateEdit in Qt6.

Lars Knoll:
>>>> I wonder whether we can't keep handling of different calendars
>>>> completely outside of QDate. Something similar to what we've done
>>>> with QString/QLocale. So QDate would continue unchanged and only
>>>> support the standard Gregorian calendar. In addition, we have a
>>>> QCalendar class, that can be constructed with a different calendar
>>>> system, and can then return 'localized' date strings, days, months
>>>> and years for this calendar system.

That would be another option (and it would approximately just upstream
KCalendarSystem); but I actually think the plan I sketch above can work
and will be more natural.  When it comes down to it, QDate really is
just a Julian day number and everything it does for the Gregorian
calendar is well-enough separated that I believe we can split it out.

>>>> Something like:
>>>>
>>>> QDate date;
>>>> QCalendar c(QCalendar::Hebrew);
>>>> QString hebrewDateString = c.toString(date);
>>>> int hebrewYear = c.year(date);

What I have in mind would look like: <code>

Type func(const QDate &date, const QCalendarSystem &calendar)
{
    QString text = date.toString(calendar);
    int year = date.year(calendar);
    ...
}

Type val = func(QDate(QJalaliCalendar(), 1395, 7, 15), QHebrewCalendar());

</code> (and I have no idea what I just did), except that the naked
instantiations would, in real application code, almost always be
replaced by forwarding const QCalendarSystem & values obtained from
elsewhere.

A calendar app can then have several panels or views, each with an
independent choice of calendar system; it the user drags a date from one
to another, it's just a Julian day so transfers safely but gets
displayed utterly differently after the transfer; all the same, the
events on the day get carried over just fine.  Or I can drag an event
from one to another; the event remembers its day, so lands in the right
place, for all that this is displayed differently.  The user can change
the calendar system in use by a view; all the entries change their
appearance but the underlying QDate objects stay the same.

>>>> Maybe one could even integrate this into QLocale, that already
>>>> provides support for localized month and day names?

QCalendarSystem or the QDate code using it is clearly going to need to
interact with QLocale, if only to get localised names of months and
week-days.  That probably won't happen by translating "February" or
"Wednesday" though; there may be no clean match between the months and
days of the other calendar and those of the European calendars.

However, calendar system is separate from locale; see the last paragraph
of my earlier mail and Soroush's response to it.

Frédéric Marchal
>>> There is more to it than converting a date to a string:
>>>
>>> * Add N days to a date.

That one's easy, because QDate is just a Julian day; increment it by N.
The part needing work is addMonths(const QCalendarSystem&, int) and
addYears(const QCalendarSystem &, int).  Eminently feasible.

>>> * Find the number of days in a month.

Not something we presently do for Gregorian; but not an unreasonable
thing to consider adding to the QDate (and QDateTime) API:

  int daysInMonth(const QCalendarSystem &cs = QGregorianCalendarSystem());

>>> * Compare two dates.

No need to change, just compare Julian day numbers.

>>> * Count the number of days between two dates.

Difference between Julian day numbers, easy.  More fun, but eminently
do-able, is the equivalent of KCalendarSystem::dateDifference().

>>> For instance a program wishing a happy new year to its users should
>>> do it with as little modifications as possible.

Yes; and I think that's feasible.

>>> Using a plain QDate would have been the easiest way to reach more
>>> users because it doesn't require to replace lots of QDate with a
>>> new, very similar, class. As it is not possible to change QDate for
>>> now, Soroush is looking for a temporary solution that would bridge
>>> the gap until Qt6 is out.

I think what I have in mind will manage this without needing to wait for
Qt6 (although we might want to clean up at Qt6).

Lars Knoll:
>> it might be a good idea to have this functionality in a class
>> separate from QDate. We’ve done the same design decision for QString
>> (having no locale specific functionality in QString), and this worked
>> out rather nicely. So I would encourage you to have a look whether
>> and how a similar design could be done for calendar system support.

QDate already has (Gregorian-based) methods entangling it with notions
of year, month, week and day within each; this is intrinsically
calendar-system-specific.  Aside from that it's just a Julian day,
stored in an oversized data-type (qint64, where int would suffice); so
there's nothing there except the bits that we'd need to remove to do the
equivalent of taking QLocale out of QString.

Sérgio Martins very helpfully linked to KCalendarSystem - thank you.
One of the things we should clearly aim for is to make it easy for
KCalendarSystem to (when its developers care to and can find the time,
with no pressure to do so) adapt to use QCalendarSystem and the adapted
QDate.  TODO: I also need to find and talk to its maintainers.

Thiago:
> Just like QString being able to format numbers in one particular
> locale (the "C" locale), we ought to support the C locale's
> calendaring system in QDate too.

I entirely agree that we should continue to support Gregorian by
default.  I think it makes sense, though, to have QDate work with other
calendaring systems.  Even if QCalendarSystem sits outside QDate and
does all the work for itself, then - with a putative

  QGregorianCalendarSystem greg;
  QDate date;

in scope - date.method(args) shall in many cases be equivalent to
greg.method(date.toJulianDay(), args) and QDate(args) to
QDate::fromJulianDay(greg.julianDay(args)).  It seems needlessly
limiting, to support for other calendar systems, not to also let these
be expressed in QDate as date.method(greg, args) and QDate(greg, args),
respectively, so that we can replace greg with another calendar system
object and save clients some tedious boilerplate.

I think my main reason for wanting this is that, without it, code that
supports non-Gregorian calendars shall have essentially no use for QDate
- its only use would be to extract its .toJulianDate(), do real work
with that, then QDate::fromJulianDate() back in order to interact with
the rest of Qt.  It would then make more sense for such code so simply
discard QDate and just use a Julian day instead - the need to convert
to/from QDate in order to interact with Qt would just be a hindrance,
rather than a help.

QDate should make it easy for app developers to add support for other
calendar systems, with minimal re-writing, rather than seducing them
into writing Gregorian-specific code that they would have to throw away
entirely in order to add that support.

... and that's me caught up with the thread.

Issue: Q(Date|Time)+ think day-changes happen at midnight.  Some
calendar systems think they happen at sunset or sunrise; these are both
rather tricky, as their time depends on date and latitude [3] - and I'm
not sure what they do about days when the sun doesn't set or rise at
all.  I don't see any hint of coping with this in KCalendarSystem and
I'm fairly sure I don't want to solve that problem, so I think we stick
with midnight and leave apps using the result to fix up after the fact
if they really care.  Indeed, what *is* common practice in The Middle
East, as regards how evenings get handled - as the tail of one day or
the start of the next ?  Has secular society shifted the day boundary to
midnight in practice ?

[3] also geography, altitude and even local climactic complications:
    https://en.wikipedia.org/wiki/Novaya_Zemlya_effect

Kindred issue with the ancient Julian calendar: before some time
mediaeval, when year transitions got moved to a month boundary, the day
after (somewhen like) March 14th of one year would be March 15th of the
*next* year.  (March is month 1: that's why Sept = 7, Oct = 8, Nov = 9
and Dec = 10 are the embers of the year, whose month numbers are now two
more than what their names claim.)

That one probably can be handled, but it's the sort of mess I'd rather
not implement myself, so I want an API that lets the app, or a plugin
it's loaded, supply the calendar system object to handle it.  Some
calendar systems matter a lot to those that care about them but are
nowhere near mass-market enough that it makes any sense for us to plan
on implementing them all.  There are a few widely-used calendars and a
long tail of more obscure ones: we need an architecture which lets
client code cater to the ones that we aren't about to handle natively.

KCalendarSystem's daysIn{Year,Month,Week} are sensible methods for the
calendar system class, albeit taking a Julian day rather than a QDate
(which is equivalent).  Various other methods that take or return QDate
would replace it with a Julian day; QDate would then use these in place
of its present hard-coded Gregorian code.

	Eddy.



More information about the Development mailing list