diff --git a/src/corelib/text/qlocale.cpp b/src/corelib/text/qlocale.cpp index e0af809c0b6..6df13603d01 100644 --- a/src/corelib/text/qlocale.cpp +++ b/src/corelib/text/qlocale.cpp @@ -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(); + 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; } diff --git a/src/corelib/text/qlocale_mac.mm b/src/corelib/text/qlocale_mac.mm index 2f560936472..ff1abcd0447 100644 --- a/src/corelib/text/qlocale_mac.mm +++ b/src/corelib/text/qlocale_mac.mm @@ -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 locale = CFLocaleCopyCurrent(); + QCFType numberFormatter = + CFNumberFormatterCreate(NULL, locale, kCFNumberFormatterDecimalStyle); + CFTypeRef numTref = + CFNumberFormatterCopyProperty(numberFormatter, kCFNumberFormatterGroupingSize); + CFNumberRef num = static_cast(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 l = CFLocaleCopyCurrent(); @@ -594,6 +616,8 @@ QVariant QSystemLocale::query(QueryType type, QVariant &&in) const return getLocaleValue(kCFLocaleScriptCode); case DecimalPoint: return getCFLocaleValue(kCFLocaleDecimalSeparator); + case Grouping: + return getGroupingSizes(); case GroupSeparator: return getCFLocaleValue(kCFLocaleGroupingSeparator); case DateFormatLong: diff --git a/src/corelib/text/qlocale_p.h b/src/corelib/text/qlocale_p.h index 33bb02e9e94..766911ab0ba 100644 --- a/src/corelib/text/qlocale_p.h +++ b/src/corelib/text/qlocale_p.h @@ -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 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: diff --git a/src/corelib/text/qlocale_unix.cpp b/src/corelib/text/qlocale_unix.cpp index a934f24c016..3f563db107e 100644 --- a/src/corelib/text/qlocale_unix.cpp +++ b/src/corelib/text/qlocale_unix.cpp @@ -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: diff --git a/src/corelib/text/qlocale_win.cpp b/src/corelib/text/qlocale_win.cpp index a66a08cadeb..b7357ce56d8 100644 --- a/src/corelib/text/qlocale_win.cpp +++ b/src/corelib/text/qlocale_win.cpp @@ -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: diff --git a/src/plugins/platforms/android/qandroidsystemlocale.cpp b/src/plugins/platforms/android/qandroidsystemlocale.cpp index 599c8d4f93a..3322af14c35 100644 --- a/src/plugins/platforms/android/qandroidsystemlocale.cpp +++ b/src/plugins/platforms/android/qandroidsystemlocale.cpp @@ -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: diff --git a/tests/auto/corelib/text/qlocale/tst_qlocale.cpp b/tests/auto/corelib/text/qlocale/tst_qlocale.cpp index d421aecb6b8..24bd3e03f36 100644 --- a/tests/auto/corelib/text/qlocale/tst_qlocale.cpp +++ b/tests/auto/corelib/text/qlocale/tst_qlocale.cpp @@ -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("name"); + QTest::addColumn("separator"); + QTest::addColumn("zeroDigit"); + QTest::addColumn("number"); + QTest::addColumn("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() diff --git a/util/locale_database/ldml.py b/util/locale_database/ldml.py index 0f8bfaf0c5a..92f9ee0ed3e 100644 --- a/util/locale_database/ldml.py +++ b/util/locale_database/ldml.py @@ -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]: