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

View File

@ -432,6 +432,28 @@ static QVariant macToQtFormat(QStringView sys_fmt)
return !result.isEmpty() ? QVariant::fromValue(result) : QVariant(); 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) static QVariant getMacDateFormat(CFDateFormatterStyle style)
{ {
QCFType<CFLocaleRef> l = CFLocaleCopyCurrent(); QCFType<CFLocaleRef> l = CFLocaleCopyCurrent();
@ -594,6 +616,8 @@ QVariant QSystemLocale::query(QueryType type, QVariant &&in) const
return getLocaleValue<QLocalePrivate::codeToScript>(kCFLocaleScriptCode); return getLocaleValue<QLocalePrivate::codeToScript>(kCFLocaleScriptCode);
case DecimalPoint: case DecimalPoint:
return getCFLocaleValue(kCFLocaleDecimalSeparator); return getCFLocaleValue(kCFLocaleDecimalSeparator);
case Grouping:
return getGroupingSizes();
case GroupSeparator: case GroupSeparator:
return getCFLocaleValue(kCFLocaleGroupingSeparator); return getCFLocaleValue(kCFLocaleGroupingSeparator);
case DateFormatLong: case DateFormatLong:

View File

@ -125,6 +125,7 @@ public:
LanguageId, // uint LanguageId, // uint
TerritoryId, // uint TerritoryId, // uint
DecimalPoint, // QString DecimalPoint, // QString
Grouping, // QLocaleData::GroupSizes
GroupSeparator, // QString (empty QString means: don't group digits) GroupSeparator, // QString (empty QString means: don't group digits)
ZeroDigit, // QString ZeroDigit, // QString
NegativeSign, // QString NegativeSign, // QString
@ -174,6 +175,12 @@ public:
virtual QLocale fallbackLocale() const; virtual QLocale fallbackLocale() const;
inline qsizetype fallbackLocaleIndex() 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::QueryType, Q_PRIMITIVE_TYPE);
Q_DECLARE_TYPEINFO(QSystemLocale::CurrencyToStringArgument, Q_RELOCATABLE_TYPE); Q_DECLARE_TYPEINFO(QSystemLocale::CurrencyToStringArgument, Q_RELOCATABLE_TYPE);
@ -274,6 +281,14 @@ public:
enum NumberMode { IntegerMode, DoubleStandardMode, DoubleScientificMode }; 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: private:
enum PrecisionMode { enum PrecisionMode {
PMDecimalDigits = 0x01, PMDecimalDigits = 0x01,
@ -397,6 +412,7 @@ public:
[[nodiscard]] QString positiveSign() const; [[nodiscard]] QString positiveSign() const;
[[nodiscard]] QString negativeSign() const; [[nodiscard]] QString negativeSign() const;
[[nodiscard]] QString exponentSeparator() const; [[nodiscard]] QString exponentSeparator() const;
[[nodiscard]] Q_CORE_EXPORT GroupSizes groupSizes() const;
struct DataRange struct DataRange
{ {
@ -498,11 +514,13 @@ public:
quint8 m_first_day_of_week : 3; quint8 m_first_day_of_week : 3;
quint8 m_weekend_start : 3; quint8 m_weekend_start : 3;
quint8 m_weekend_end : 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_higher : 3; // Number of digits between grouping separators
quint8 m_grouping_least : 3; // Number of digits after last grouping separator (before decimal). quint8 m_grouping_least : 3; // Number of digits after last grouping separator (before decimal).
}; };
Q_DECLARE_TYPEINFO(QLocaleData::GroupSizes, Q_PRIMITIVE_TYPE);
class QLocalePrivate class QLocalePrivate
{ {
public: public:

View File

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

View File

@ -108,6 +108,7 @@ struct QSystemLocalePrivate
QVariant zeroDigit(); QVariant zeroDigit();
QVariant decimalPoint(); QVariant decimalPoint();
QVariant groupingSizes();
QVariant groupSeparator(); QVariant groupSeparator();
QVariant negativeSign(); QVariant negativeSign();
QVariant positiveSign(); QVariant positiveSign();
@ -145,6 +146,7 @@ private:
LCID lcid; LCID lcid;
SubstitutionType substitutionType = SUnknown; SubstitutionType substitutionType = SUnknown;
QString zero; // cached value for zeroDigit() QString zero; // cached value for zeroDigit()
QLocaleData::GroupSizes sizes; // cached value for groupingSizes()
int getLocaleInfo(LCTYPE type, LPWSTR data, int size); int getLocaleInfo(LCTYPE type, LPWSTR data, int size);
QVariant getLocaleInfo(LCTYPE type); QVariant getLocaleInfo(LCTYPE type);
@ -319,6 +321,46 @@ QVariant QSystemLocalePrivate::decimalPoint()
return nullIfEmpty(getLocaleInfo(LOCALE_SDECIMAL).toString()); 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() QVariant QSystemLocalePrivate::groupSeparator()
{ {
return getLocaleInfo(LOCALE_STHOUSAND); // Empty means don't group digits. return getLocaleInfo(LOCALE_STHOUSAND); // Empty means don't group digits.
@ -831,6 +873,8 @@ QVariant QSystemLocale::query(QueryType type, QVariant &&in) const
switch(type) { switch(type) {
case DecimalPoint: case DecimalPoint:
return d->decimalPoint(); return d->decimalPoint();
case Grouping:
return d->groupingSizes();
case GroupSeparator: case GroupSeparator:
return d->groupSeparator(); return d->groupSeparator();
case NegativeSign: case NegativeSign:

View File

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

View File

@ -145,6 +145,8 @@ private slots:
# ifdef QT_BUILD_INTERNAL # ifdef QT_BUILD_INTERNAL
void mySystemLocale_data(); void mySystemLocale_data();
void mySystemLocale(); void mySystemLocale();
void systemGrouping_data();
void systemGrouping();
# endif # endif
void systemLocaleDayAndMonthNames_data(); void systemLocaleDayAndMonthNames_data();
@ -4103,7 +4105,18 @@ public:
return m_id.territory_id; return m_id.territory_id;
case ScriptId: case ScriptId:
return m_id.script_id; 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: default:
break; break;
} }
@ -4255,6 +4268,75 @@ void tst_QLocale::mySystemLocale()
QT_TEST_EQUALITY_OPS(QLocale(), originalLocale, true); QT_TEST_EQUALITY_OPS(QLocale(), originalLocale, true);
QT_TEST_EQUALITY_OPS(QLocale::system(), originalSystemLocale, 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 # endif // QT_BUILD_INTERNAL
void tst_QLocale::systemLocaleDayAndMonthNames_data() void tst_QLocale::systemLocaleDayAndMonthNames_data()

View File

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