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
_______________________________________________ Development mailing list Development@qt-project.org http://lists.qt-project.org/mailman/listinfo/development