QSystemLocale: add group size query

Add a GroupSizes struct and a corresponding query type to
QSystemLocale that would return a struct of form
struct { int first, higher, least; } by consulting suitable
platform-specific APIs.

Fixes: QTBUG-109955
Change-Id: I2deee814f161ac914f810080866eea1cc432acbe
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
This commit is contained in:
Eimen Oueslati 2024-08-20 11:07:05 +02:00
parent a317b28d87
commit 5465e4723d
8 changed files with 220 additions and 21 deletions

View File

@ -1108,6 +1108,29 @@ QString QLocaleData::exponentSeparator() const
return exponential().getData(single_character_data);
}
QLocaleData::GroupSizes QLocaleData::groupSizes() const
{
#ifndef QT_NO_SYSTEMLOCALE
if (this == &systemLocaleData) {
QVariant queryResult = systemLocale()->query(QSystemLocale::Grouping);
if (!queryResult.isNull()) {
QLocaleData::GroupSizes sysGroupSizes =
queryResult.value<QLocaleData::GroupSizes>();
if (sysGroupSizes.first <= 0)
sysGroupSizes.first = m_grouping_first;
if (sysGroupSizes.higher <= 0)
sysGroupSizes.higher = m_grouping_higher;
if (sysGroupSizes.least <= 0)
sysGroupSizes.least = m_grouping_least;
return sysGroupSizes;
}
}
#endif
return { m_grouping_first,
m_grouping_higher,
m_grouping_least };
}
/*!
\internal
*/
@ -3992,11 +4015,12 @@ QString QLocaleData::doubleToString(double d, int precision, DoubleForm form,
// Set bias to everything added to exponent form but not
// decimal, minus the converse.
const QLocaleData::GroupSizes grouping = groupSizes();
// Exponent adds separator, sign and digits:
int bias = 2 + minExponentDigits;
// Decimal form may get grouping separators inserted:
if (groupDigits && decpt >= m_grouping_top + m_grouping_least)
bias -= (decpt - m_grouping_least) / m_grouping_higher + 1;
if (groupDigits && decpt >= grouping.first + grouping.least)
bias -= (decpt - grouping.least) / grouping.higher + 1;
// X = decpt - 1 needs two digits if decpt > 10:
if (decpt > 10 && minExponentDigits == 1)
++bias;
@ -4081,11 +4105,12 @@ QString QLocaleData::decimalForm(QString &&digits, int decpt, int precision,
digits.insert(decpt * digitWidth, decimalPoint());
if (groupDigits) {
const QLocaleData::GroupSizes grouping = groupSizes();
const QString group = groupSeparator();
qsizetype i = decpt - m_grouping_least;
if (i >= m_grouping_top) {
qsizetype i = decpt - grouping.least;
if (i >= grouping.first) {
digits.insert(i * digitWidth, group);
while ((i -= m_grouping_higher) > 0)
while ((i -= grouping.higher) > 0)
digits.insert(i * digitWidth, group);
}
}
@ -4188,12 +4213,13 @@ QString QLocaleData::applyIntegerFormatting(QString &&numStr, bool negative, int
qsizetype usedWidth = digitCount + prefix.size();
if (base == 10 && flags & GroupDigits) {
const QLocaleData::GroupSizes grouping = groupSizes();
const QString group = groupSeparator();
qsizetype i = digitCount - m_grouping_least;
if (i >= m_grouping_top) {
qsizetype i = digitCount - grouping.least;
if (i >= grouping.first) {
numStr.insert(i * digitWidth, group);
++usedWidth;
while ((i -= m_grouping_higher) > 0) {
while ((i -= grouping.higher) > 0) {
numStr.insert(i * digitWidth, group);
++usedWidth;
}
@ -4462,6 +4488,7 @@ bool QLocaleData::numberToCLocale(QStringView s, QLocale::NumberOptions number_o
qsizetype digitsInGroup = 0;
qsizetype last_separator_idx = -1;
qsizetype start_of_digits_idx = -1;
const QLocaleData::GroupSizes grouping = groupSizes();
// Floating-point details (non-integer modes):
qsizetype decpt_idx = -1;
@ -4511,13 +4538,13 @@ bool QLocaleData::numberToCLocale(QStringView s, QLocale::NumberOptions number_o
if (last_separator_idx == -1) {
// Check distance from the beginning of the digits:
if (start_of_digits_idx == -1 || m_grouping_top > digitsInGroup
|| digitsInGroup >= m_grouping_least + m_grouping_top) {
if (start_of_digits_idx == -1 || grouping.first > digitsInGroup
|| digitsInGroup >= grouping.least + grouping.first) {
return false;
}
} else {
// Check distance from the last separator:
if (digitsInGroup != m_grouping_higher)
if (digitsInGroup != grouping.higher)
return false;
}
@ -4526,7 +4553,7 @@ bool QLocaleData::numberToCLocale(QStringView s, QLocale::NumberOptions number_o
} else if (mode != IntegerMode && (out == '.' || idx == exponent_idx)
&& last_separator_idx != -1) {
// Were there enough digits since the last group separator?
if (digitsInGroup != m_grouping_least)
if (digitsInGroup != grouping.least)
return false;
// stop processing separators
@ -4543,7 +4570,7 @@ bool QLocaleData::numberToCLocale(QStringView s, QLocale::NumberOptions number_o
if (!number_options.testFlag(QLocale::RejectGroupSeparator) && last_separator_idx != -1) {
// Were there enough digits since the last group separator?
if (digitsInGroup != m_grouping_least)
if (digitsInGroup != grouping.least)
return false;
}

View File

@ -432,6 +432,28 @@ static QVariant macToQtFormat(QStringView sys_fmt)
return !result.isEmpty() ? QVariant::fromValue(result) : QVariant();
}
static QVariant getGroupingSizes()
{
// It does not seem like you can directly query the group sizes from CFLocale as there
// is no key that corresponds to it, see:
// https://developer.apple.com/documentation/corefoundation/cflocalekey
// We have to create a number formatter for the locale and query the data from there.
// see: https://developer.apple.com/documentation/corefoundation/1390801-cfnumberformattercopyproperty
QLocaleData::GroupSizes sizes;
QCFType<CFLocaleRef> locale = CFLocaleCopyCurrent();
QCFType<CFNumberFormatterRef> numberFormatter =
CFNumberFormatterCreate(NULL, locale, kCFNumberFormatterDecimalStyle);
CFTypeRef numTref =
CFNumberFormatterCopyProperty(numberFormatter, kCFNumberFormatterGroupingSize);
CFNumberRef num = static_cast<CFNumberRef>(numTref);
int value;
if (CFNumberGetValue(num, kCFNumberIntType, &value) && value > 0) {
sizes.least = value;
sizes.higher = value;
}
return QVariant::fromValue(sizes);
}
static QVariant getMacDateFormat(CFDateFormatterStyle style)
{
QCFType<CFLocaleRef> l = CFLocaleCopyCurrent();
@ -594,6 +616,8 @@ QVariant QSystemLocale::query(QueryType type, QVariant &&in) const
return getLocaleValue<QLocalePrivate::codeToScript>(kCFLocaleScriptCode);
case DecimalPoint:
return getCFLocaleValue(kCFLocaleDecimalSeparator);
case Grouping:
return getGroupingSizes();
case GroupSeparator:
return getCFLocaleValue(kCFLocaleGroupingSeparator);
case DateFormatLong:

View File

@ -125,6 +125,7 @@ public:
LanguageId, // uint
TerritoryId, // uint
DecimalPoint, // QString
Grouping, // QLocaleData::GroupSizes
GroupSeparator, // QString (empty QString means: don't group digits)
ZeroDigit, // QString
NegativeSign, // QString
@ -174,6 +175,12 @@ public:
virtual QLocale fallbackLocale() const;
inline qsizetype fallbackLocaleIndex() const;
protected:
inline const QSharedDataPointer<QLocalePrivate> localeData(const QLocale &locale) const
{
return locale.d;
}
};
Q_DECLARE_TYPEINFO(QSystemLocale::QueryType, Q_PRIMITIVE_TYPE);
Q_DECLARE_TYPEINFO(QSystemLocale::CurrencyToStringArgument, Q_RELOCATABLE_TYPE);
@ -274,6 +281,14 @@ public:
enum NumberMode { IntegerMode, DoubleStandardMode, DoubleScientificMode };
struct GroupSizes
{
int first = 0;
int higher = 0;
int least = 0;
bool isValid() const { return least > 0 && higher > first && first > 0; }
};
private:
enum PrecisionMode {
PMDecimalDigits = 0x01,
@ -397,6 +412,7 @@ public:
[[nodiscard]] QString positiveSign() const;
[[nodiscard]] QString negativeSign() const;
[[nodiscard]] QString exponentSeparator() const;
[[nodiscard]] Q_CORE_EXPORT GroupSizes groupSizes() const;
struct DataRange
{
@ -498,11 +514,13 @@ public:
quint8 m_first_day_of_week : 3;
quint8 m_weekend_start : 3;
quint8 m_weekend_end : 3;
quint8 m_grouping_top : 2; // Don't group until more significant group has this many digits.
quint8 m_grouping_first : 2; // Don't group until more significant group has this many digits.
quint8 m_grouping_higher : 3; // Number of digits between grouping separators
quint8 m_grouping_least : 3; // Number of digits after last grouping separator (before decimal).
};
Q_DECLARE_TYPEINFO(QLocaleData::GroupSizes, Q_PRIMITIVE_TYPE);
class QLocalePrivate
{
public:

View File

@ -143,6 +143,8 @@ QVariant QSystemLocale::query(QueryType type, QVariant &&in) const
switch (type) {
case DecimalPoint:
return lc_numeric.decimalPoint();
case Grouping:
return QVariant::fromValue(lc_numeric.d->m_data->groupSizes());
case GroupSeparator:
return lc_numeric.groupSeparator();
case ZeroDigit:

View File

@ -108,6 +108,7 @@ struct QSystemLocalePrivate
QVariant zeroDigit();
QVariant decimalPoint();
QVariant groupingSizes();
QVariant groupSeparator();
QVariant negativeSign();
QVariant positiveSign();
@ -145,6 +146,7 @@ private:
LCID lcid;
SubstitutionType substitutionType = SUnknown;
QString zero; // cached value for zeroDigit()
QLocaleData::GroupSizes sizes; // cached value for groupingSizes()
int getLocaleInfo(LCTYPE type, LPWSTR data, int size);
QVariant getLocaleInfo(LCTYPE type);
@ -319,6 +321,46 @@ QVariant QSystemLocalePrivate::decimalPoint()
return nullIfEmpty(getLocaleInfo(LOCALE_SDECIMAL).toString());
}
QVariant QSystemLocalePrivate::groupingSizes()
{
if (sizes.higher == 0) {
wchar_t grouping[10];
/*
* Nine digits/semicolons plus a terminator.
* https://learn.microsoft.com/en-us/windows/win32/intl/locale-sgrouping
* "Sizes for each group of digits to the left of the decimal. The maximum
* number of characters allowed for this string is ten, including a
* terminating null character."
*/
int dataSize = getLocaleInfo(LOCALE_SGROUPING, grouping, int(std::size(grouping)));
if (dataSize) {
// MS does not seem to include {first} so it will always be NAN.
QString sysGroupingStr = QString::fromWCharArray(grouping, dataSize);
auto tokenized = sysGroupingStr.tokenize(u";");
int width[2] = {0, 0};
int index = 0;
for (const auto tok : tokenized) {
bool ok = false;
int value = tok.toInt(&ok);
if (!ok || !value || index >= 2)
break;
width[index++] = value;
}
// The MS docs allow patterns Qt doesn't support, so we treat "X;Y" as "X;Y;0"
// and "X" as "X;0" and ignore all but the first two widths. The MS API does
// not support an equivalent of sizes.first.
if (index > 1) {
sizes.least = width[0];
sizes.higher = width[1];
} else if (index) {
sizes.least = sizes.higher = width[0];
}
}
}
return QVariant::fromValue(sizes);
}
QVariant QSystemLocalePrivate::groupSeparator()
{
return getLocaleInfo(LOCALE_STHOUSAND); // Empty means don't group digits.
@ -831,6 +873,8 @@ QVariant QSystemLocale::query(QueryType type, QVariant &&in) const
switch(type) {
case DecimalPoint:
return d->decimalPoint();
case Grouping:
return d->groupingSizes();
case GroupSeparator:
return d->groupSeparator();
case NegativeSign:

View File

@ -110,6 +110,8 @@ QVariant QAndroidSystemLocale::query(QueryType type, QVariant &&in) const
switch (type) {
case DecimalPoint:
return m_locale.decimalPoint();
case Grouping:
return QVariant::fromValue(localeData(m_locale)->m_data->groupSizes());
case GroupSeparator:
return m_locale.groupSeparator();
case ZeroDigit:

View File

@ -145,6 +145,8 @@ private slots:
# ifdef QT_BUILD_INTERNAL
void mySystemLocale_data();
void mySystemLocale();
void systemGrouping_data();
void systemGrouping();
# endif
void systemLocaleDayAndMonthNames_data();
@ -4103,7 +4105,18 @@ public:
return m_id.territory_id;
case ScriptId:
return m_id.script_id;
case Grouping:
if (m_name == u"en-ES") // CLDR: 1,3,3
return QVariant::fromValue(QLocaleData::GroupSizes{2,3,3});
if (m_name == u"en-BD") // CLDR: 1,3,3
return QVariant::fromValue(QLocaleData::GroupSizes{1,2,3});
if (m_name == u"ccp") // CLDR: 1,3,3
return QVariant::fromValue(QLocaleData::GroupSizes{2,2,3});
if (m_name == u"en-BT") // CLDR: 1,3,3
return QVariant::fromValue(QLocaleData::GroupSizes{0,2,3});
if (m_name == u"en-NP") // CLDR: 1,3,3
return QVariant::fromValue(QLocaleData::GroupSizes{0,2,0});
break;
default:
break;
}
@ -4255,6 +4268,75 @@ void tst_QLocale::mySystemLocale()
QT_TEST_EQUALITY_OPS(QLocale(), originalLocale, true);
QT_TEST_EQUALITY_OPS(QLocale::system(), originalSystemLocale, true);
}
void tst_QLocale::systemGrouping_data()
{
QTest::addColumn<QString>("name");
QTest::addColumn<QString>("separator");
QTest::addColumn<QString>("zeroDigit");
QTest::addColumn<int>("number");
QTest::addColumn<QString>("formattedString");
// Testing locales with non {1, 3, 3} groupe sizes, plus some locales
// that return invalid group sizes to test that we fallbakc to CLDR data.
QTest::newRow("en-ES") // {2,3,3}
<< u"en-ES"_s << u","_s << u"0"_s << 1234 << u"1234"_s;
QTest::newRow("en-ES-grouped") // {2,3,3}
<< u"en-ES"_s << u","_s << u"0"_s << 12345 << u"12,345"_s;
QTest::newRow("en-BD") // {1,2,3}
<< u"en-BD"_s << u","_s << u"0"_s << 123456789 << u"12,34,56,789"_s;
QTest::newRow("en-BT") // {1,2,3}
<< u"en-BT"_s << u","_s << u"0"_s << 123456789 << u"12,34,56,789"_s;
QTest::newRow("en-NP") // {1,2,3}
<< u"en-NP"_s << u","_s << u"0"_s << 123456789 << u"12,34,56,789"_s;
// Testing with Chakma locale
const char32_t zeroVal = 0x11136; // Chakma zero
const QChar data[] = {
QChar::highSurrogate(zeroVal), QChar::lowSurrogate(zeroVal),
QChar::highSurrogate(zeroVal + 1), QChar::lowSurrogate(zeroVal + 1),
QChar::highSurrogate(zeroVal + 2), QChar::lowSurrogate(zeroVal + 2),
QChar::highSurrogate(zeroVal + 3), QChar::lowSurrogate(zeroVal + 3),
QChar::highSurrogate(zeroVal + 4), QChar::lowSurrogate(zeroVal + 4),
QChar::highSurrogate(zeroVal + 5), QChar::lowSurrogate(zeroVal + 5),
};
const QChar separator(QLatin1Char(',')); // Separator for the Chakma locale
const QString
// Copy zero so it persists through QFETCH(), after data falls off the stack.
zero = QString(data, 2),
one = QString::fromRawData(data + 2, 2),
two = QString::fromRawData(data + 4, 2),
three = QString::fromRawData(data + 6, 2),
four = QString::fromRawData(data + 8, 2),
five = QString::fromRawData(data + 10, 2);
QString fourDigit = one + two + three + four;
QString fiveDigit = one + two + separator + three + four + five;
QTest::newRow("Chakma-short") // {2,2,3}
<< u"ccp"_s << QString(separator)
<< zero << 1234 << fourDigit;
QTest::newRow("Chakma") // {2,2,3}
<< u"ccp"_s << QString(separator)
<< zero << 12345 << fiveDigit;
}
void tst_QLocale::systemGrouping()
{
QFETCH(QString, name);
QFETCH(QString, separator);
QFETCH(QString, zeroDigit);
QFETCH(int, number);
QFETCH(QString, formattedString);
{
MySystemLocale sLocale(name);
QLocale sys = QLocale::system();
QCOMPARE(sys.groupSeparator(), separator);
QCOMPARE(sys.zeroDigit(), zeroDigit);
QCOMPARE(sys.toString(number), formattedString);
}
}
# endif // QT_BUILD_INTERNAL
void tst_QLocale::systemLocaleDayAndMonthNames_data()

View File

@ -684,12 +684,12 @@ class LocaleScanner (object):
def __numberGrouping(self, system: str) -> tuple[int, int, int]:
"""Sizes of groups of digits within a number.
Returns a triple (least, higher, top) for which:
Returns a triple (least, higher, fist) for which:
* least is the number of digits after the last grouping
separator;
* higher is the number of digits between grouping
separators;
* top is the fewest digits that can appear before the first
* first is the fewest digits that can appear before the first
grouping separator.
Thus (4, 3, 2) would want 1e7 as 1000,0000 but 1e8 as 10,000,0000.
@ -699,17 +699,17 @@ class LocaleScanner (object):
is placement of the sign character anywhere but at the start
of the number (some formats may place it at the end, possibly
elsewhere)."""
top = int(self.find('numbers/minimumGroupingDigits'))
assert top < 4, top # We store it in a 2-bit field
first = int(self.find('numbers/minimumGroupingDigits'))
assert first < 4, first # We store it in a 2-bit field
grouping: str | None = self.find(f'numbers/decimalFormats[numberSystem={system}]/'
'decimalFormatLength/decimalFormat/pattern')
groups: list[str] = grouping.split('.')[0].split(',')[-3:]
assert all(len(x) < 8 for x in groups[-2:]), grouping # we store them in 3-bit fields
if len(groups) > 2:
return len(groups[-1]), len(groups[-2]), top
return len(groups[-1]), len(groups[-2]), first
size = len(groups[-1]) if len(groups) == 2 else 3
return size, size, top
return size, size, first
@staticmethod
def __currencyFormats(patterns: str, plus: str, minus: str) -> Iterator[str]: