| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2020 The Qt Company Ltd. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the test suite of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 21 | ** included in the packaging of this file. Please review the following |
| 22 | ** information to ensure the GNU General Public License requirements will |
| 23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 24 | ** |
| 25 | ** $QT_END_LICENSE$ |
| 26 | ** |
| 27 | ****************************************************************************/ |
| 28 | |
| 29 | #include <QtTest/QtTest> |
| 30 | |
| 31 | #include <qlocale.h> |
| 32 | #include <qcollator.h> |
| 33 | #include <private/qglobal_p.h> |
| 34 | |
| 35 | #include <cstring> |
| 36 | |
| 37 | class tst_QCollator : public QObject |
| 38 | { |
| 39 | Q_OBJECT |
| 40 | |
| 41 | private Q_SLOTS: |
| 42 | void moveSemantics(); |
| 43 | |
| 44 | void compare_data(); |
| 45 | void compare(); |
| 46 | |
| 47 | void state(); |
| 48 | }; |
| 49 | |
| 50 | static bool dpointer_is_null(QCollator &c) |
| 51 | { |
| 52 | char mem[sizeof c]; |
| 53 | using namespace std; |
| 54 | memcpy(dest: mem, src: &c, n: sizeof c); |
| 55 | for (size_t i = 0; i < sizeof c; ++i) |
| 56 | if (mem[i]) |
| 57 | return false; |
| 58 | return true; |
| 59 | } |
| 60 | |
| 61 | void tst_QCollator::moveSemantics() |
| 62 | { |
| 63 | const QLocale de_AT(QLocale::German, QLocale::Austria); |
| 64 | |
| 65 | QCollator c1(de_AT); |
| 66 | QCOMPARE(c1.locale(), de_AT); |
| 67 | |
| 68 | QCollator c2(std::move(c1)); |
| 69 | QCOMPARE(c2.locale(), de_AT); |
| 70 | QVERIFY(dpointer_is_null(c1)); |
| 71 | |
| 72 | QCollator c3(c1); |
| 73 | QVERIFY(dpointer_is_null(c3)); |
| 74 | |
| 75 | c1 = std::move(c2); |
| 76 | QCOMPARE(c1.locale(), de_AT); |
| 77 | QVERIFY(dpointer_is_null(c2)); |
| 78 | } |
| 79 | |
| 80 | |
| 81 | void tst_QCollator::compare_data() |
| 82 | { |
| 83 | QTest::addColumn<QString>(name: "locale" ); |
| 84 | QTest::addColumn<QString>(name: "s1" ); |
| 85 | QTest::addColumn<QString>(name: "s2" ); |
| 86 | QTest::addColumn<int>(name: "result" ); |
| 87 | QTest::addColumn<int>(name: "caseInsensitiveResult" ); |
| 88 | QTest::addColumn<bool>(name: "numericMode" ); |
| 89 | QTest::addColumn<bool>(name: "ignorePunctuation" ); |
| 90 | QTest::addColumn<int>(name: "punctuationResult" ); // Test ignores punctuation *and case* |
| 91 | |
| 92 | /* |
| 93 | It's hard to test English, because it's treated differently |
| 94 | on different platforms. For example, on Linux, it uses the |
| 95 | iso14651_t1 template file, which happens to provide good |
| 96 | defaults for Swedish. OS X seems to do a pure bytewise |
| 97 | comparison of Latin-1 values, although I'm not sure. So I |
| 98 | just test digits to make sure that it's not totally broken. |
| 99 | */ |
| 100 | QTest::newRow(dataTag: "english1" ) << QString("en_US" ) << QString("5" ) << QString("4" ) << 1 << 1 << false << false << 1; |
| 101 | QTest::newRow(dataTag: "english2" ) << QString("en_US" ) << QString("4" ) << QString("6" ) << -1 << -1 << false << false << -1; |
| 102 | QTest::newRow(dataTag: "english3" ) << QString("en_US" ) << QString("5" ) << QString("6" ) << -1 << -1 << false << false << -1; |
| 103 | QTest::newRow(dataTag: "english4" ) << QString("en_US" ) << QString("a" ) << QString("b" ) << -1 << -1 << false << false << -1; |
| 104 | QTest::newRow(dataTag: "english5" ) << QString("en_US" ) << QString("test 9" ) << QString("test 19" ) << -1 << -1 << true << false << -1; |
| 105 | QTest::newRow(dataTag: "english6" ) << QString("en_US" ) << QString("test 9" ) << QString("test_19" ) << -1 << -1 << true << true << -1; |
| 106 | QTest::newRow(dataTag: "english7" ) << QString("en_US" ) << QString("test_19" ) << QString("test 19" ) << 1 << 1 << true << false << 1; |
| 107 | QTest::newRow(dataTag: "english8" ) << QString("en_US" ) << QString("test.19" ) << QString("test,19" ) << 1 << 1 << true << true << 0; |
| 108 | QTest::newRow(dataTag: "en-empty-word" ) << QString("en_US" ) << QString() << QString("non-empty" ) << -1 << -1 << false << true << -1; |
| 109 | QTest::newRow(dataTag: "en-empty-number" ) << QString("en_US" ) << QString() << QString("42" ) << -1 << -1 << true << true << -1; |
| 110 | |
| 111 | /* |
| 112 | In Swedish, a with ring above (E5) comes before a with |
| 113 | diaresis (E4), which comes before o diaresis (F6), which |
| 114 | all come after z. |
| 115 | */ |
| 116 | QTest::newRow(dataTag: "swedish1" ) << QString("sv_SE" ) << QString::fromLatin1(str: "\xe5" ) << QString::fromLatin1(str: "\xe4" ) << -1 << -1 << false << false << -1; |
| 117 | QTest::newRow(dataTag: "swedish2" ) << QString("sv_SE" ) << QString::fromLatin1(str: "\xe4" ) << QString::fromLatin1(str: "\xf6" ) << -1 << -1 << false << false << -1; |
| 118 | QTest::newRow(dataTag: "swedish3" ) << QString("sv_SE" ) << QString::fromLatin1(str: "\xe5" ) << QString::fromLatin1(str: "\xf6" ) << -1 << -1 << false << false << -1; |
| 119 | QTest::newRow(dataTag: "swedish4" ) << QString("sv_SE" ) << QString::fromLatin1(str: "z" ) << QString::fromLatin1(str: "\xe5" ) << -1 << -1 << false << false << -1; |
| 120 | QTest::newRow(dataTag: "swedish5" ) << QString("sv_SE" ) << QString("9" ) << QString("19" ) << -1 << -1 << true << false << -1; |
| 121 | QTest::newRow(dataTag: "swedish6" ) << QString("sv_SE" ) << QString("Test 9" ) << QString("Test_19" ) << -1 << -1 << true << true << -1; |
| 122 | QTest::newRow(dataTag: "swedish7" ) << QString("sv_SE" ) << QString("test_19" ) << QString("test 19" ) << 1 << 1 << true << false << 1; |
| 123 | QTest::newRow(dataTag: "swedish8" ) << QString("sv_SE" ) << QString("test.19" ) << QString("test,19" ) << 1 << 1 << true << true << 0; |
| 124 | QTest::newRow(dataTag: "sv-empty-word" ) << QString("sv_SE" ) << QString() << QString("mett" ) << -1 << -1 << false << true << -1; |
| 125 | QTest::newRow(dataTag: "sv-empty-number" ) << QString("sv_SE" ) << QString() << QString("42" ) << -1 << -1 << true << true << -1; |
| 126 | |
| 127 | |
| 128 | /* |
| 129 | In Norwegian, ae (E6) comes before o with stroke (D8), which |
| 130 | comes before a with ring above (E5). |
| 131 | */ |
| 132 | QTest::newRow(dataTag: "norwegian1" ) << QString("no_NO" ) << QString::fromLatin1(str: "\xe6" ) << QString::fromLatin1(str: "\xd8" ) << -1 << -1 << false << false << -1; |
| 133 | QTest::newRow(dataTag: "norwegian2" ) << QString("no_NO" ) << QString::fromLatin1(str: "\xd8" ) << QString::fromLatin1(str: "\xe5" ) << -1 << -1 << false << false << -1; |
| 134 | QTest::newRow(dataTag: "norwegian3" ) << QString("no_NO" ) << QString::fromLatin1(str: "\xe6" ) << QString::fromLatin1(str: "\xe5" ) << -1 << -1 << false << false << -1; |
| 135 | QTest::newRow(dataTag: "norwegian4" ) << QString("no_NO" ) << QString("9" ) << QString("19" ) << -1 << -1 << true << false << -1; |
| 136 | QTest::newRow(dataTag: "norwegian5" ) << QString("no_NO" ) << QString("Test 9" ) << QString("Test_19" ) << -1 << -1 << true << true << -1; |
| 137 | QTest::newRow(dataTag: "norwegian6" ) << QString("no_NO" ) << QString("Test 9" ) << QString("Test_19" ) << -1 << -1 << true << true << -1; |
| 138 | QTest::newRow(dataTag: "norwegian7" ) << QString("no_NO" ) << QString("test_19" ) << QString("test 19" ) << 1 << 1 << true << false << 1; |
| 139 | QTest::newRow(dataTag: "norwegian8" ) << QString("no_NO" ) << QString("test.19" ) << QString("test,19" ) << 1 << 1 << true << true << 0; |
| 140 | QTest::newRow(dataTag: "nb-empty-word" ) << QString("nb_NO" ) << QString() << QString("mett" ) << -1 << -1 << false << true << -1; |
| 141 | QTest::newRow(dataTag: "nb-empty-number" ) << QString("nb_NO" ) << QString() << QString("42" ) << -1 << -1 << true << true << -1; |
| 142 | |
| 143 | /* |
| 144 | In German, z comes *after* a with diaresis (E4), |
| 145 | which comes before o diaresis (F6). |
| 146 | */ |
| 147 | QTest::newRow(dataTag: "german1" ) << QString("de_DE" ) << QString::fromLatin1(str: "a" ) << QString::fromLatin1(str: "\xe4" ) << -1 << -1 << false << false << -1; |
| 148 | QTest::newRow(dataTag: "german2" ) << QString("de_DE" ) << QString::fromLatin1(str: "b" ) << QString::fromLatin1(str: "\xe4" ) << 1 << 1 << false << false << 1; |
| 149 | QTest::newRow(dataTag: "german3" ) << QString("de_DE" ) << QString::fromLatin1(str: "z" ) << QString::fromLatin1(str: "\xe4" ) << 1 << 1 << false << false << 1; |
| 150 | QTest::newRow(dataTag: "german4" ) << QString("de_DE" ) << QString::fromLatin1(str: "\xe4" ) << QString::fromLatin1(str: "\xf6" ) << -1 << -1 << false << false << -1; |
| 151 | QTest::newRow(dataTag: "german5" ) << QString("de_DE" ) << QString::fromLatin1(str: "z" ) << QString::fromLatin1(str: "\xf6" ) << 1 << 1 << false << false << 1; |
| 152 | QTest::newRow(dataTag: "german6" ) << QString("de_DE" ) << QString::fromLatin1(str: "\xc0" ) << QString::fromLatin1(str: "\xe0" ) << 1 << 0 << false << false << 0; |
| 153 | QTest::newRow(dataTag: "german7" ) << QString("de_DE" ) << QString::fromLatin1(str: "\xd6" ) << QString::fromLatin1(str: "\xf6" ) << 1 << 0 << false << false << 0; |
| 154 | QTest::newRow(dataTag: "german8" ) << QString("de_DE" ) << QString::fromLatin1(str: "oe" ) << QString::fromLatin1(str: "\xf6" ) << 1 << 1 << false << false << 1; |
| 155 | QTest::newRow(dataTag: "german9" ) << QString("de_DE" ) << QString("A" ) << QString("a" ) << 1 << 0 << false << false << 0; |
| 156 | QTest::newRow(dataTag: "german10" ) << QString("de_DE" ) << QString("9" ) << QString("19" ) << -1 << -1 << true << false << -1; |
| 157 | QTest::newRow(dataTag: "german11" ) << QString("de_DE" ) << QString("Test 9" ) << QString("Test_19" ) << -1 << -1 << true << true << -1; |
| 158 | QTest::newRow(dataTag: "german12" ) << QString("de_DE" ) << QString("test_19" ) << QString("test 19" ) << 1 << 1 << true << false << 1; |
| 159 | QTest::newRow(dataTag: "german13" ) << QString("de_DE" ) << QString("test.19" ) << QString("test,19" ) << 1 << 1 << true << true << 0; |
| 160 | QTest::newRow(dataTag: "de-empty-word" ) << QString("de_DE" ) << QString() << QString("satt" ) << -1 << -1 << false << true << -1; |
| 161 | QTest::newRow(dataTag: "de-empty-number" ) << QString("de_DE" ) << QString() << QString("42" ) << -1 << -1 << true << true << -1; |
| 162 | |
| 163 | /* |
| 164 | French sorting of e and e with acute accent |
| 165 | */ |
| 166 | QTest::newRow(dataTag: "french1" ) << QString("fr_FR" ) << QString::fromLatin1(str: "\xe9" ) << QString::fromLatin1(str: "e" ) << 1 << 1 << false << false << 1; |
| 167 | QTest::newRow(dataTag: "french2" ) << QString("fr_FR" ) << QString::fromLatin1(str: "\xe9t" ) << QString::fromLatin1(str: "et" ) << 1 << 1 << false << false << 1; |
| 168 | QTest::newRow(dataTag: "french3" ) << QString("fr_FR" ) << QString::fromLatin1(str: "\xe9" ) << QString::fromLatin1(str: "d" ) << 1 << 1 << false << false << 1; |
| 169 | QTest::newRow(dataTag: "french4" ) << QString("fr_FR" ) << QString::fromLatin1(str: "\xe9" ) << QString::fromLatin1(str: "f" ) << -1 << -1 << false << false << -1; |
| 170 | QTest::newRow(dataTag: "french5" ) << QString("fr_FR" ) << QString("9" ) << QString("19" ) << -1 << -1 << true << false << -1; |
| 171 | QTest::newRow(dataTag: "french6" ) << QString("fr_FR" ) << QString("Test 9" ) << QString("Test_19" ) << -1 << -1 << true << true << -1; |
| 172 | QTest::newRow(dataTag: "french7" ) << QString("fr_FR" ) << QString("test_19" ) << QString("test 19" ) << 1 << 1 << true << false << 1; |
| 173 | QTest::newRow(dataTag: "french8" ) << QString("fr_FR" ) << QString("test.19" ) << QString("test,19" ) << 1 << 1 << true << true << 0; |
| 174 | QTest::newRow(dataTag: "fr-empty-word" ) << QString("fr_FR" ) << QString() << QString("plein" ) << -1 << -1 << false << true << -1; |
| 175 | QTest::newRow(dataTag: "fr-empty-number" ) << QString("fr_FR" ) << QString() << QString("42" ) << -1 << -1 << true << true << -1; |
| 176 | |
| 177 | // C locale: case sensitive [A-Z] < [a-z] but case insensitive [Aa] < [Bb] <...< [Zz] |
| 178 | const QString C = QStringLiteral("C" ); |
| 179 | QTest::newRow(dataTag: "C:ABBA:AaaA" ) << C << QStringLiteral("ABBA" ) << QStringLiteral("AaaA" ) << -1 << 1 << false << false << 1; |
| 180 | QTest::newRow(dataTag: "C:AZa:aAZ" ) << C << QStringLiteral("AZa" ) << QStringLiteral("aAZ" ) << -1 << 1 << false << false << 1; |
| 181 | QTest::newRow(dataTag: "C-empty-word" ) << QString(C) << QString() << QString("non-empty" ) << -1 << -1 << false << true << -1; |
| 182 | QTest::newRow(dataTag: "C-empty-number" ) << QString(C) << QString() << QString("42" ) << -1 << -1 << true << true << -1; |
| 183 | } |
| 184 | |
| 185 | void tst_QCollator::compare() |
| 186 | { |
| 187 | QFETCH(QString, locale); |
| 188 | QFETCH(QString, s1); |
| 189 | QFETCH(QString, s2); |
| 190 | QFETCH(int, result); |
| 191 | QFETCH(int, caseInsensitiveResult); |
| 192 | QFETCH(bool, numericMode); |
| 193 | QFETCH(bool, ignorePunctuation); |
| 194 | QFETCH(int, punctuationResult); |
| 195 | |
| 196 | QCollator collator(locale); |
| 197 | // Need to canonicalize sign to -1, 0 or 1, as .compare() can produce any -ve for <, any +ve for >. |
| 198 | auto asSign = [](int compared) { |
| 199 | return compared < 0 ? -1 : compared > 0 ? 1 : 0; |
| 200 | }; |
| 201 | |
| 202 | #if defined(Q_OS_ANDROID) && !defined(Q_OS_ANDROID_EMBEDDED) |
| 203 | if (collator.locale() != QLocale()) |
| 204 | QSKIP("Posix implementation of collation only supports default locale" ); |
| 205 | #endif |
| 206 | |
| 207 | if (numericMode) |
| 208 | collator.setNumericMode(true); |
| 209 | |
| 210 | QCOMPARE(asSign(collator.compare(s1, s2)), result); |
| 211 | collator.setCaseSensitivity(Qt::CaseInsensitive); |
| 212 | QCOMPARE(asSign(collator.compare(s1, s2)), caseInsensitiveResult); |
| 213 | #if !QT_CONFIG(iconv) |
| 214 | collator.setIgnorePunctuation(ignorePunctuation); |
| 215 | QCOMPARE(asSign(collator.compare(s1, s2)), punctuationResult); |
| 216 | #endif |
| 217 | } |
| 218 | |
| 219 | |
| 220 | void tst_QCollator::state() |
| 221 | { |
| 222 | QCollator c; |
| 223 | c.setCaseSensitivity(Qt::CaseInsensitive); |
| 224 | c.setLocale(QLocale::German); |
| 225 | |
| 226 | c.compare(s1: QString("a" ), s2: QString("b" )); |
| 227 | |
| 228 | QCOMPARE(c.caseSensitivity(), Qt::CaseInsensitive); |
| 229 | QCOMPARE(c.locale(), QLocale(QLocale::German)); |
| 230 | |
| 231 | c.setLocale(QLocale::French); |
| 232 | c.setNumericMode(true); |
| 233 | c.setIgnorePunctuation(true); |
| 234 | c.setLocale(QLocale::NorwegianBokmal); |
| 235 | |
| 236 | QCOMPARE(c.caseSensitivity(), Qt::CaseInsensitive); |
| 237 | QCOMPARE(c.numericMode(), true); |
| 238 | QCOMPARE(c.ignorePunctuation(), true); |
| 239 | QCOMPARE(c.locale(), QLocale(QLocale::NorwegianBokmal)); |
| 240 | |
| 241 | } |
| 242 | |
| 243 | QTEST_APPLESS_MAIN(tst_QCollator) |
| 244 | |
| 245 | #include "tst_qcollator.moc" |
| 246 | |