pythoncalendarpyside6qdateqdateedit

QDateEdit restrictions for impossible georgian dates (2024/06/31)


I'm trying to use a QDateEdit for my program but I want it to work with Jalali (Persian/Solar Hijri) calender in this type of calenders for the first 6 months we have 31 days and for the next 5 month we have 30 days and for the 12th month we have 29 days (30 in leap years)

Also I change the minimum Date to 1300 becuase we are now at 1403 in Jalali calender
for example we have 1403/06/31 but I can't use it with QDateEdit and it forces me to change to 1403/06/03

Here's my code if it helps

from persiantools.jdatetime import JalaliDate
import sys
from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QDateEdit)
from PySide6.QtCore import QDate, Qt

class Program(QWidget):
    def __init__(self):
        super().__init__()

        # Main layout
        self.layout = QVBoxLayout()

        self.date_input = QDateEdit(self)
        self.date_input.setDisplayFormat("yyyy/MM/dd")
        self.date_input.setMinimumDate(QDate(1300, 1, 1))
        jalali_date=JalaliDate(1403,6,31)
        self.date_input.setDate(QDate(jalali_date.year, jalali_date.month, jalali_date.day))
        # it sets to 2000/01/01 because the day is 31 and if it is 30 it works fine
        self.date_input.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.date_input.setFixedWidth(150)

        input_layout = QHBoxLayout()
        input_layout.addWidget(self.date_input)
        self.layout.addLayout(input_layout)
        self.setLayout(self.layout)
        self.setWindowTitle("Program")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Program()
    window.show()
    sys.exit(app.exec())

Solution

  • QDate (as QDateTime) is based on the Julian/Gregorian calendar[1], and, for obvious reasons, you cannot use values based on a Jalali calendar date with its default constructor (QDate(year, month, day)) to create a QDate object, as they would make no sense; for instance, the second month can only have 28 or 29 days at most: for some years, some dates do not even exist[2].

    Since Qt 5.14, though, the QCalendar class has been introduced in order to work around these requirements, and can be used along with the static functions of QDate, such as QDate.fromString(string, format, calendar).

    Based on the persiantools documentation, you can get a simple string representation of the date using isoformat(), which would be shown as yyyy-MM-dd in Qt date format; getting a valid QDate is then just a matter of:

    jalali_cal = QCalendar(QCalendar.System.Jalali)
    date = QDate.fromString(jalali_date.isoformat(), 'yyyy-MM-dd', jalali_cal)
    

    The QDate object will still use the Julian/Gregorian standard internally and for its basic functions, but its "Jalali values" can be retrieved back with the related getter functions, such as date.year(jalali_cal).


    Now we have a proper date, but what about the QDateEdit?

    QDateEdit, similarly to QTimeEdit, is a convenience class. From the documentation (emphasis mine):

    The QDateEdit class provides a widget for editing dates based on the QDateTimeEdit widget.
    [...]
    Many of the properties and functions provided by QDateEdit are implemented in QDateTimeEdit.

    This means that you must always check the QDateTimeEdit documentation to get the full API.

    Specifically, since Qt6, QDateTimeEdit implements setCalendar(), which was introduced exactly for these needs and uses the QCalendar class mentioned above, in order to properly show dates consistent with the given calendar system.

    self.date_input.setCalendar(jalali_cal)
    

    Note that checking the documentation of inherited classes is a required studying step for all classes: always check the "Inherits from" section in the docs header, as the documentation page of a class almost never includes sections related to inherited members[3].

    You probably checked the "Qt for Python" docs only, and we normally suggest to ignore them to begin with:

    Always check the C++ docs first, then eventually verify the Python docs in case of doubt, or use help(<class>.<member>) in the Python interactive shell.

    [1] QDate always follows the Julian/Gregorian calendar system, with 12 months alternating 31 or 30 days (except for the seventh and eighth, which repeat 31), with 28/29 for the second depending on the year.
    [2] QDate internally stores dates based on the "Julian Day", but Qt4 followed the 10 day gap introduced in October 1582 when the Gregorian calendar was introduced; before Qt5, QDate(1582, 10, 4) was followed by QDate(1582, 10, 15); this change did result in somehow inconsistent dates, which the QCalendar introduction fixed.
    [3] That's typical in Qt as it is for most properly written documentation, and for good reasons; the docs about a specific class should only contain its direct members, as including all inherited ones would be pointless, quite distracting and annoying, if not just stupid; for instance, the inheritance path of QDateEdit is: QDateEdit > QDateTimeEdit > QAbstractSpinBox > QWidget > QObject; compare the QDateEdit page, and then imagine if it would contain everything in its list of all members, including inherited ones (containing all members of the inheritance path shown above).