Fix formatting of dates before 1601 in MS system locale

The relevant MS system calls (say they) don't support date formatting
for years < 1601 (but apparently do in fact) and the year field of the
data structure is unsigned, so can't support years < 0. As a result,
the windows back-end for QSystemLocale failed for negative years.
So replace year < 1601 with a place-holder and substitute after
formatting.

Added new tests (based loosely on one in qtdeclarative that failed) to
verify that this actually works. These reveal that macOS also fails to
handle negative years; marked as expected failure there pending a fix.

Change-Id: I9b63cefd5f0b77a39cf1238549412de3e26ca1bd
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
bb10
Edward Welbourne 2021-05-26 18:12:40 +02:00
parent 2bf5a60bfa
commit 8926eb86c6
2 changed files with 89 additions and 1 deletions

View File

@ -46,6 +46,8 @@
#include "qdatetime.h"
#include "qdebug.h"
#include "QtCore/private/qgregoriancalendar_p.h" // for yearSharingWeekDays()
#ifdef Q_OS_WIN
# include <qt_windows.h>
# include <time.h>
@ -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;

View File

@ -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()