From f75d36fd18f58b91689aca2a389c62627c38e6b4 Mon Sep 17 00:00:00 2001 From: Edward Welbourne Date: Fri, 5 Nov 2021 19:28:40 +0100 Subject: Fix macOS system locale's formatting of negative years It leaves off the minus sign. Change-Id: Iad72349368d8849330524144033453cbd79e9e7c Reviewed-by: Timur Pocheptsov --- src/corelib/text/qlocale_mac.mm | 129 ++++++++++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 17 deletions(-) (limited to 'src/corelib/text') diff --git a/src/corelib/text/qlocale_mac.mm b/src/corelib/text/qlocale_mac.mm index ce693fb224..ef9d1d24a3 100644 --- a/src/corelib/text/qlocale_mac.mm +++ b/src/corelib/text/qlocale_mac.mm @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2020 The Qt Company Ltd. +** Copyright (C) 2021 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtCore module of the Qt Toolkit. @@ -43,6 +43,8 @@ #include "qvariant.h" #include "qdatetime.h" +#include "private/qstringiterator_p.h" +#include "private/qgregoriancalendar_p.h" #ifdef Q_OS_DARWIN #include "private/qcore_mac_p.h" #include @@ -148,19 +150,123 @@ static QVariant macDayName(int day, QSystemLocale::QueryType type) return {}; } -static QVariant macDateToString(QDate date, bool short_format) +static QString macZeroDigit() +{ + QCFType locale = CFLocaleCopyCurrent(); + QCFType numberFormatter = + CFNumberFormatterCreate(nullptr, locale, kCFNumberFormatterNoStyle); + const int zeroDigit = 0; + QCFType value + = CFNumberFormatterCreateStringWithValue(nullptr, numberFormatter, + kCFNumberIntType, &zeroDigit); + return QString::fromCFString(value); +} + +static QString zeroPad(QString &&number, int minDigits, const QString &zero) +{ + // Need to pad with zeros, possibly after a sign. + int insert = -1, digits = 0; + auto it = QStringIterator(number); + while (it.hasNext()) { + int here = it.index(); + if (QChar::isDigit(it.next())) { + if (insert < 0) + insert = here; + ++digits; + } // else: assume we're stepping over a sign (or maybe grouping separator) + } + Q_ASSERT(digits > 0); + Q_ASSERT(insert >= 0); + while (digits++ < minDigits) + number.insert(insert, zero); + + return std::move(number); +} + +static QString trimTwoDigits(QString &&number) +{ + // Retain any sign, but remove all but the last two digits. + // We know number has at least four digits - it came from fourDigitYear(). + // Note that each digit might be a surrogate pair. + int first = -1, prev = -1, last = -1; + auto it = QStringIterator(number); + while (it.hasNext()) { + int here = it.index(); + if (QChar::isDigit(it.next())) { + if (first == -1) + last = first = here; + else if (last != -1) + prev = std::exchange(last, here); + } + } + Q_ASSERT(first >= 0); + Q_ASSERT(prev > first); + Q_ASSERT(last > prev); + number.remove(first, prev - first); + return std::move(number); +} + +static QString fourDigitYear(int year, const QString &zero) { - QCFType myDate = QDateTime(date, QTime()).toCFDate(); + // Return year formatted as an (at least) four digit number: + QCFType locale = CFLocaleCopyCurrent(); + QCFType numberFormatter = + CFNumberFormatterCreate(nullptr, locale, kCFNumberFormatterNoStyle); + QCFType value = CFNumberFormatterCreateStringWithValue(nullptr, numberFormatter, + kCFNumberIntType, &year); + auto text = QString::fromCFString(value); + if (year > -1000 && year < 1000) + text = zeroPad(std::move(text), 4, zero); + return text; +} + +static QString macDateToStringImpl(QDate date, CFDateFormatterStyle style) +{ + QCFType myDate = date.startOfDay().toCFDate(); QCFType mylocale = CFLocaleCopyCurrent(); - CFDateFormatterStyle style = short_format ? kCFDateFormatterShortStyle : kCFDateFormatterLongStyle; QCFType myFormatter - = CFDateFormatterCreate(kCFAllocatorDefault, - mylocale, style, + = CFDateFormatterCreate(kCFAllocatorDefault, mylocale, style, kCFDateFormatterNoStyle); QCFType text = CFDateFormatterCreateStringWithDate(0, myFormatter, myDate); return QString::fromCFString(text); } +static QVariant macDateToString(QDate date, bool short_format) +{ + const int year = date.year(); + QString fakeYear, trueYear; + if (year < 0) { + // System API (in macOS 11.0, at least) discards sign :-( + // Simply negating the year won't do as the resulting year typically has + // a different pattern of week-days. + int matcher = QGregorianCalendar::yearSharingWeekDays(date); + Q_ASSERT(matcher > 0); + Q_ASSERT(matcher % 100 != date.month()); + Q_ASSERT(matcher % 100 != date.day()); + // i.e. there can't be any confusion between the two-digit year and + // month or day-of-month in the formatted date. + QString zero = macZeroDigit(); + fakeYear = fourDigitYear(matcher, zero); + trueYear = fourDigitYear(year, zero); + date = QDate(matcher, date.month(), date.day()); + } + QString text = macDateToStringImpl(date, short_format + ? kCFDateFormatterShortStyle + : kCFDateFormatterLongStyle); + if (year < 0) { + if (text.contains(fakeYear)) + return std::move(text).replace(fakeYear, trueYear); + // Cope with two-digit year: + fakeYear = trimTwoDigits(std::move(fakeYear)); + trueYear = trimTwoDigits(std::move(trueYear)); + if (text.contains(fakeYear)) + return std::move(text).replace(fakeYear, trueYear); + // That should have worked. + qWarning("Failed to fix up year when formatting a date in year %d", year); + } + return text; +} + static QVariant macTimeToString(QTime time, bool short_format) { QCFType myDate = QDateTime(QDate::currentDate(), time).toCFDate(); @@ -363,17 +469,6 @@ static QVariant macCurrencySymbol(QLocale::CurrencySymbolFormat format) return {}; } -static QVariant macZeroDigit() -{ - QCFType locale = CFLocaleCopyCurrent(); - QCFType numberFormatter = - CFNumberFormatterCreate(nullptr, locale, kCFNumberFormatterNoStyle); - static const int zeroDigit = 0; - QCFType value = CFNumberFormatterCreateStringWithValue(nullptr, numberFormatter, - kCFNumberIntType, &zeroDigit); - return QString::fromCFString(value); -} - #ifndef QT_NO_SYSTEMLOCALE static QVariant macFormatCurrency(const QSystemLocale::CurrencyToStringArgument &arg) { -- cgit v1.2.3