diff --git a/src/corelib/text/qlocale_win.cpp b/src/corelib/text/qlocale_win.cpp index 191533c886..dafe441ee0 100644 --- a/src/corelib/text/qlocale_win.cpp +++ b/src/corelib/text/qlocale_win.cpp @@ -46,6 +46,8 @@ #include "qdatetime.h" #include "qdebug.h" +#include "QtCore/private/qgregoriancalendar_p.h" // for yearSharingWeekDays() + #ifdef Q_OS_WIN # include # include @@ -149,6 +151,7 @@ private: SubstitutionType substitution(); QString substituteDigits(QString &&string); + QString yearFix(int year, int fakeYear, QString &&formatted); static QString winToQtFormat(QStringView sys_fmt); @@ -427,17 +430,78 @@ QVariant QSystemLocalePrivate::monthName(int month, QLocale::FormatType type) return {}; } +static QString fourDigitYear(int year) +{ + // Return year formatted as an (at least) four digit number: + return QStringLiteral("%1").arg(year, 4, 10, QChar(u'0')); +} + +QString QSystemLocalePrivate::yearFix(int year, int fakeYear, QString &&formatted) +{ + // Replace our ersatz fakeYear (that MS formats faithfully) with the correct + // form of year. We know the two-digit short form of fakeYear can not be + // mistaken for the month or day-of-month in the formatted date. + Q_ASSERT(fakeYear >= 1970 && fakeYear <= 2400); + const bool matchTwo = year >= 0 && year % 100 == fakeYear % 100; + auto yearUsed = fourDigitYear(fakeYear); + QString sign(year < 0 ? 1 : 0, QLatin1Char('-')); + auto trueYear = fourDigitYear(year < 0 ? -year : year); + if (formatted.contains(yearUsed)) + return std::move(formatted).replace(yearUsed, sign + trueYear); + + auto tail = QStringView{yearUsed}.last(2); + Q_ASSERT(!matchTwo || tail == QString(sign + trueYear.last(2))); + if (formatted.contains(tail)) { + if (matchTwo) + return std::move(formatted); + return std::move(formatted).replace(tail.toString(), sign + trueYear.last(2)); + } + + // Localized digits, perhaps ? + // First call to substituteDigits() ensures zero is initialized: + trueYear = substituteDigits(std::move(trueYear)); + if (zero != u'0') { + yearUsed = substituteDigits(std::move(yearUsed)); + if (year < 0) + sign = negativeSign().toString(); + + if (formatted.contains(yearUsed)) + return std::move(formatted).replace(yearUsed, sign + trueYear); + + const int twoDigits = 2 * zero.size(); + tail = QStringView{yearUsed}.last(twoDigits); + if (formatted.contains(tail)) { + if (matchTwo) + return std::move(formatted); + return std::move(formatted).replace(tail.toString(), sign + trueYear.last(twoDigits)); + } + } + qWarning("Failed to fix up year in formatted date-string using %d for %d", fakeYear, year); + return std::move(formatted); +} + QVariant QSystemLocalePrivate::toString(QDate date, QLocale::FormatType type) { SYSTEMTIME st = {}; - st.wYear = date.year(); + const int year = date.year(); + // st.wYear is unsigned; and GetDateFormat() is documented to not handle + // dates before 1601. + const bool fixup = year < 1601; + st.wYear = fixup ? QGregorianCalendar::yearSharingWeekDays(date) : year; st.wMonth = date.month(); st.wDay = date.day(); + Q_ASSERT(!fixup || st.wYear % 100 != st.wMonth); + Q_ASSERT(!fixup || st.wYear % 100 != st.wDay); + // i.e. yearFix() can trust a match of its fakeYear's last two digits to not + // be the month or day part of the formatted date. + DWORD flags = (type == QLocale::LongFormat ? DATE_LONGDATE : DATE_SHORTDATE); wchar_t buf[255]; if (getDateFormat(flags, &st, NULL, buf, 255)) { QString text = QString::fromWCharArray(buf); + if (fixup) + text = yearFix(year, st.wYear, std::move(text)); if (substitution() == SAlways) text = substituteDigits(std::move(text)); return text; diff --git a/tests/auto/corelib/text/qlocale/tst_qlocale.cpp b/tests/auto/corelib/text/qlocale/tst_qlocale.cpp index c5ba003363..cd3682a207 100644 --- a/tests/auto/corelib/text/qlocale/tst_qlocale.cpp +++ b/tests/auto/corelib/text/qlocale/tst_qlocale.cpp @@ -2630,6 +2630,30 @@ void tst_QLocale::dateFormat() const auto sys = QLocale::system(); // QTBUG-92018, ru_RU on MS const QDate date(2021, 3, 17); QCOMPARE(sys.toString(date, sys.dateFormat(QLocale::LongFormat)), sys.toString(date)); + + // Check that system locale can format a date with year < 1601 (MS cut-off): + QString old = sys.toString(QDate(1564, 2, 15), QLocale::LongFormat); + QVERIFY(!old.isEmpty()); + QVERIFY2(old.contains(u"1564"), qPrintable(old + QLatin1String(" for locale ") + sys.name())); + old = sys.toString(QDate(1564, 2, 15), QLocale::ShortFormat); + QVERIFY(!old.isEmpty()); + QVERIFY2(old.contains(u"64"), qPrintable(old + QLatin1String(" for locale ") + sys.name())); + + // Including one with year % 100 < 12 (lest we substitute year for month or day) + old = sys.toString(QDate(1511, 11, 11), QLocale::LongFormat); + QVERIFY(!old.isEmpty()); + QVERIFY2(old.contains(u"1511"), qPrintable(old + QLatin1String(" for locale ") + sys.name())); + old = sys.toString(QDate(1511, 11, 11), QLocale::ShortFormat); + QVERIFY(!old.isEmpty()); + QVERIFY2(old.contains(u"11"), qPrintable(old + QLatin1String(" for locale ") + sys.name())); + + // And, indeed, one for a negative year: + old = sys.toString(QDate(-1173, 5, 1), QLocale::LongFormat); + QVERIFY(!old.isEmpty()); +#ifdef Q_OS_DARWIN // bug in qlocale_mac.mm, fix coming shortly + QEXPECT_FAIL("", "Darwin converts year to positive", Continue); +#endif + QVERIFY2(old.contains(u"-1173"), qPrintable(old + QLatin1String(" for locale ") + sys.name())); } void tst_QLocale::timeFormat()