[Development] Calendar Systems proposal

Soroush Rabiei soroush.rabiei at gmail.com
Thu Dec 15 11:30:33 CET 2016


1. Purpose

Nowadays, almost all major programming frameworks support calendar
globalization. We are a small group of developers working with
non-Gregorian
calendars and we believe Qt must support this too. This proposal discusses
details of our plan (and early implementation) to add support for multiple
calendar systems in Qt library.

2.History

Originally there was four plans described in [1] based on ideas discussed
in
[2]. These talks (dated back to 2011) were never implemented and eventually
abandoned.

3. Current Status

Currently there is no support of any calendar system other than Gregorian.
Gregorian calendar is widely used in western countries. However most
countries
in Middle-east, Asia and Africa use other calendar systems. Unfortunately
there
is no support for them.

4. Requirements

Users of calendar systems expect same functionality provided by default
calendar for their local calendar (at least we do so). Following list
describes
requirements of date/time API.

4.1. Such a feature must provide an API same as QDate, and all related
classes.
Adding convenient classed for each calendar type (QJalaliDate, QHebrewDate,
QIslamicDate, etc.) will be discussed, Tough we believe any API without
QDate
integration will not satisfy our needs.

4.2. Backward compatibility must be kept in both public API and semantics
of
QDate, QDateTime and related classes. Such a feature must not affect any
program that uses date, but has nothing to do with the calendar systems. A
program compiled with Qt 5.9 must behave the way it do with 5.8, whether it
utilizes calendar systems or not. There will be no assumption on user
locale,
nor the host system’s default calendar (as there is not any now).

4.3. The Public API and semantics of other parts of Qt that work with
QDate,
should not be changed. For instance, regardless of the calendar system that
a
QDate object uses, a QSqlQuery must be able to use it. And QVarient must be
able to store/read it the way it did before. We prefer not to change
private
API and implementation as well, but if we had have to, (and it’s an
affordable
change) we will do it. For example we may need to change QtSql and QtCore
classes to overwrite calendar system of QDate object to work properly. We
hope
we will find a way to avoid this. These examples are based on the
assumption
that QDate API will change, and calendar system support will be added
directly
to QDate (See S5 below)

4.4. Calendar systems and Locales, are irrelevant except month naming.
There is
nothing to do with the locales while implementing calendar system: Adding a
new
calendar is not about naming months for some locale. Some calendars are
different in the math behind. For example, the year in Jalali calendar
starts
in 21st March. First 6 months are 31 days and next 6 months are 30 except
12th.

Changing locale should not change calendar system. And there will be no
information on calendar system in supported locales. (There is no default
calendar in locale definition).

It’s necessary to have multiple calendars at the same time in an
application,
and it’s necessary to have calendar system in all locales. Also calendar
System
and Time Zones are irrelevant. There were assumptions on previous plans
(described in [1]) that are totally misunderstanding of the calendar
systems.
A particular plan was about adding calendar integration to QLocale, which
we do
believe is wrong in all aspects. Because calendar system has nothing to do
with
culture. A calendar is an abstract astronomical concept.

5. API Candidates

The plan is to implement support for at least five most-used calendar
systems.
Each calendar’s implementation will be added into a new class named
`QCalendarSystem`. This class will provide information on calendar system
details, like how many days are in each month and if a year is leap year or
normal.

This class will also contain the calculation code needed for QDate. Most
importantly, how to convert between julian day and calendar date. Currently
these calculations are implemented in QDate class
(qtbase/src/corelib/tools/qdatetime.cpp).

There are several candidates for date object APIs. Following list discusses
these APIs.

5.1. Integrate calendar system semantics into QDate

This is what we plan to do. There are several ways to do this. All of
course
will preserve backward compatibility. First three options are based on John
Layt’s ideas described at [3]

5.1.1. Add a calculator class, add convenience methods to QDate

    QDate myDate = QDate::gregorianDate(2000, 1, 1);
    QDateCalculator myCalc;
    int localYear = myCalc.year(myDate);
    QString localString = QLocale::system().toString(myDate);
    QDateTimeEdit myEdit(myDate);
    myCalc.setCalendar(QLocale::HebrewCalendar);
    int hebrewYear = myCalc.year();

There are several issues with this API. Most importantly it does not
preserve
backward-compatibility of QDate API. This also does not meet the
requirement of
having QDate-like API.

5.1.2. Add new methods to QDate

    QDate myDate = QDate::gregorianDate(2000,1,1);
    int localYear = myDate.localYear();
    QString localString = QLocale::system().toString(myDate);
    QDateTimeEdit myEdit(myDate);
    int hebrewYear = myDate.localYear(QLocale::HebrewCalendar);

This option has previous one’s problems. And there is something not clear
with
this option: How QLocale knows about calendar system of a given date
object?
Is there a member added to QDate?

5.1.3. Add `calendar()` method to QDate, and return a QLocalDate

    QDate myDate = QDate::gregorianDate(2000,1,1);
    int localYear = myDate.calendar().year();
    QString localString = myDate.calendar().toString();
    QDateTimeEdit myEdit(myDate);
    int hebrewYear = myDate.calendar(QLocale::HebrewCalendar).year();

This is the best of above three, yet does not meet the requirements:
myDate.calendar(someCalendar).year() is not a good way to obtain year
number of
a date object. We prefer QDate::year().

5.1.4. Using an enumeration to perform the math

This option is complicated, and backward-compatible. We plan to implement
this
one if there is no problem.

Needed API changes are:

    * Adding an enum to the QLocale class:

        enum Calendar{Default, Gregorian=Default, Hebrew, Jalali,
Islamic,...}

    * Adding arguments of this enum with default values to `Gregorian` to
these
      member functions:

        QString monthName(int, FormatType format = LongFormat,
                          Calendar calendar = Default) const;
        QString standaloneMonthName(int, FormatType format = LongFormat,
                                    Calendar calendar = Default) const;
    * Adding `QCalendarSystem` class
    * Adding three member functions to the QDate:
        setCalendarSystem, calendarSyste and a constructor

The QCalendarSystem will contain information on calendar systems and the
math.
This will help us to move calculations out of QDate, by providing a handle
(a
private member) to a calendar system object. This object will have the
dirty
code of calendar system, and will keep QDate implementation cleaner.

So we will have something like:

    // qdatetime.cpp:
    void QDate::setCalendarSystem(QCalendarSystem::Type t){
    // This is the new API
        d_calendar.setType(t);
    }

    int QDate::year() const {
        #ifndef QT_NOCALENDARSYSTEM
        // Previous implementation
        #else
        return d_calendar.year();
        #endif
    }

    // qcalendar_system.cpp:
    QCalendarSystem::year(quint64 jd){
        switch(m_type){
            case CalendarSyste::Gregorian:
                return q_gregorianYearFromJulianDay(jd);
            case CalendarSyste::Jalali:
                return q_jalaliYearFromJulianDay(jd);
        }
    }

    // the calendar math (not optimized):
    QCalendarSystemPrivate::ParsedDate
    jalaliDateFromJulianDay(quint64 julianDay) {
        quint64 depoch = julianDay - PERSIAN_EPOCH;
        quint64 cycle = depoch / 1029983;
        quint64 cyear = depoch % 1029983;
        quint64 ycycle ;
        quint64 aux1,aux2;
        if(cyear==1029982){
            ycycle=2820;
        } else{
            aux1 = cyear / 366;
            aux2 = cyear % 366;
            ycycle = (((2134 * aux1) + (2816 * aux2) + 2815) / 1028522)
                   + aux1 + 1;
        }
        int year = ycycle + (2820 * cycle) + 474;
        int month;
        if(year <= 0){
            --year;
        }
        quint64 yday = (julianDay - persian_jdn(year, 1, 1)) + 1;
        if(yday <= 186){
            month = ::ceil(static_cast<float>(yday)/31.0);
        } else {
            month = ::ceil(static_cast<float>(yday-6.0)/30.0);
        }
        int day = julianDay-persian_jdn(year, 1, 1)+1;
        const QCalendarSystemPrivate::ParsedDate result = {year,month,day};
        return result;
    }

5.2. Provide separated date classes for each calendar system

So there will be QGregorianDate, QJalaliDate, QIslamicDate, QHebrewDate...
Possibly all inherited a common base. This solution is not reasonable. This
will not meet any of our requirements. And will not satisfy the need to
have
calendar system integrated into QDate (So that we can change the calnedar
easily). We prefer transparent API which is integrated to Qt itself, not
many
irrelevant classes. We already have them in several libraries, and our own
hand-made APIs that we currently use. However this API is open for
discussion.
If you think we must go with this option, feel free to discuss about your
reasons.

6. Known Problems

There are several issues with option 5.1.4 that are discussed here. I will
not
discuss other options (5.1.1 to 5.1.3) as I don't want to implement them.
If
you think they may fit the requirements, please let me know about the
reason
and issues. And also if you see any other issues not mentioned here, please
let
me know about.

6.1. Missing calendar localization in CLDR

Currently, month names are provided by QLocale and being read from the CLDR
embedded in Qt source code. Unfortunately there is no data in CLDR for
non-Gregorian calendars. This problem is submited as a proposal in 2012 [4]
which is not added to CLDR yet. So how do we provide data for month names?
I
have implemented a workaround for this:

    QString
    QLocale::monthName(int month, FormatType type, Calendar calendar) const
    {
        if (month < 1 || month > 12)
            return QString();

        switch (calendar) {
        case Gregorian:
        {
    #ifndef QT_NO_SYSTEMLOCALE
            if (d->m_data == systemData()) {
                QVariant res =
                systemLocale()->query(type == LongFormat
                                            ? QSystemLocale::MonthNameLong:
                                            QSystemLocale::MonthNameShort,
                                            month);
                if (!res.isNull())
                    return res.toString();
            }
    #endif

            quint32 idx, size;
            switch (type) {
            case QLocale::LongFormat:
                idx = d->m_data->m_long_month_names_idx;
                size = d->m_data->m_long_month_names_size;
                break;
            case QLocale::ShortFormat:
                idx = d->m_data->m_short_month_names_idx;
                size = d->m_data->m_short_month_names_size;
                break;
            case QLocale::NarrowFormat:
                idx = d->m_data->m_narrow_month_names_idx;
                size = d->m_data->m_narrow_month_names_size;
                break;
            default:
                return QString();
            }
            return getLocaleListData(months_data + idx, size, month - 1);
        }
        case Jalali:
            switch (type) {
            case QLocale::LongFormat:
                // TODO: Add local month names at least for
                // Persian, Afghani, Pashtoo, English and Arabic
                return jalaliLongMonthNames[month];
                break;
            case QLocale::ShortFormat:
            case QLocale::NarrowFormat:
                // TODO: Add local month names at least for
                // Persian, Afghani, Pashtoo, English and Arabic
                return jalaliShortMonthNames[month];
                break;
            default:
                return QString();
            }
        default:
            return QString();
        }

    }

It works, but it's not good enough. Can we add month names to the CLDR and
pass
the calnedar type to query?

6.2. Calendar system support must be optional. Adding calendars following
option 5.1.4, will add a dependency to QDate. And qmake will also need to
compile that. I'm thinking of adding options to configure script to enable
calendar support. And let qmake be compiled without calendar object in it.
This
requires a lot of #ifdef preprocessors in QDate class, to keep previous
implementation. That will cause a dirty code in QDate...

6.3. ICU support must be in place when ICU is available. We can do calendar
math, conversions using ICU. Though not having ICU, we must have calendars
available. That also requires a lot of math in QCalendarSystem messed up
with
loads of #ifdefs.

7. References

[1] https://wiki.qt.io/Locale_Support_in_Qt_5
[2] https://wiki.qt.io/Qt_5_ICU#QCalendarSystem_Design
[3]
http://lists.qt-project.org/pipermail/qt5-feedback/2011-September/001126.html
[4]
http://cldr.unicode.org/development/development-process/design-proposals/generic
-calendar-data

Cheers,
soroush
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.qt-project.org/pipermail/development/attachments/20161215/7e25a1f5/attachment.html>


More information about the Development mailing list