Frank Tang | 3e05d9d | 2021-11-08 14:04:04 -0800 | [diff] [blame] | 1 | // © 2019 and later: Unicode, Inc. and others. |
| 2 | // License & terms of use: http://www.unicode.org/copyright.html |
| 3 | |
| 4 | // localematchertest.cpp |
| 5 | // created: 2019jul04 Markus W. Scherer |
| 6 | |
| 7 | #include <string> |
| 8 | #include <vector> |
| 9 | #include <utility> |
| 10 | |
| 11 | #include "unicode/utypes.h" |
| 12 | #include "unicode/localematcher.h" |
| 13 | #include "unicode/locid.h" |
| 14 | #include "charstr.h" |
| 15 | #include "cmemory.h" |
| 16 | #include "intltest.h" |
| 17 | #include "localeprioritylist.h" |
| 18 | #include "ucbuf.h" |
| 19 | |
| 20 | #define ARRAY_RANGE(array) (array), ((array) + UPRV_LENGTHOF(array)) |
| 21 | |
| 22 | namespace { |
| 23 | |
| 24 | const char *locString(const Locale *loc) { |
| 25 | return loc != nullptr ? loc->getName() : "(null)"; |
| 26 | } |
| 27 | |
| 28 | struct TestCase { |
| 29 | int32_t lineNr = 0; |
| 30 | |
| 31 | CharString supported; |
| 32 | CharString def; |
| 33 | UnicodeString favor; |
| 34 | UnicodeString threshold; |
| 35 | CharString desired; |
| 36 | CharString expMatch; |
| 37 | CharString expDesired; |
| 38 | CharString expCombined; |
| 39 | |
| 40 | void reset() { |
| 41 | supported.clear(); |
| 42 | def.clear(); |
| 43 | favor.remove(); |
| 44 | threshold.remove(); |
| 45 | } |
| 46 | }; |
| 47 | |
| 48 | } // namespace |
| 49 | |
| 50 | class LocaleMatcherTest : public IntlTest { |
| 51 | public: |
| 52 | LocaleMatcherTest() {} |
| 53 | |
| 54 | void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par=NULL) override; |
| 55 | |
| 56 | void testEmpty(); |
| 57 | void testCopyErrorTo(); |
| 58 | void testBasics(); |
| 59 | void testSupportedDefault(); |
| 60 | void testUnsupportedDefault(); |
| 61 | void testNoDefault(); |
| 62 | void testDemotion(); |
| 63 | void testDirection(); |
| 64 | void testMaxDistanceAndIsMatch(); |
| 65 | void testMatch(); |
| 66 | void testResolvedLocale(); |
| 67 | void testDataDriven(); |
| 68 | |
| 69 | private: |
| 70 | UBool dataDriven(const TestCase &test, IcuTestErrorCode &errorCode); |
| 71 | }; |
| 72 | |
| 73 | extern IntlTest *createLocaleMatcherTest() { |
| 74 | return new LocaleMatcherTest(); |
| 75 | } |
| 76 | |
| 77 | void LocaleMatcherTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) { |
| 78 | if(exec) { |
| 79 | logln("TestSuite LocaleMatcherTest: "); |
| 80 | } |
| 81 | TESTCASE_AUTO_BEGIN; |
| 82 | TESTCASE_AUTO(testEmpty); |
| 83 | TESTCASE_AUTO(testCopyErrorTo); |
| 84 | TESTCASE_AUTO(testBasics); |
| 85 | TESTCASE_AUTO(testSupportedDefault); |
| 86 | TESTCASE_AUTO(testUnsupportedDefault); |
| 87 | TESTCASE_AUTO(testNoDefault); |
| 88 | TESTCASE_AUTO(testDemotion); |
| 89 | TESTCASE_AUTO(testDirection); |
| 90 | TESTCASE_AUTO(testMaxDistanceAndIsMatch); |
| 91 | TESTCASE_AUTO(testMatch); |
| 92 | TESTCASE_AUTO(testResolvedLocale); |
| 93 | TESTCASE_AUTO(testDataDriven); |
| 94 | TESTCASE_AUTO_END; |
| 95 | } |
| 96 | |
| 97 | void LocaleMatcherTest::testEmpty() { |
| 98 | IcuTestErrorCode errorCode(*this, "testEmpty"); |
| 99 | LocaleMatcher matcher = LocaleMatcher::Builder().build(errorCode); |
| 100 | const Locale *best = matcher.getBestMatch(Locale::getFrench(), errorCode); |
| 101 | assertEquals("getBestMatch(fr)", "(null)", locString(best)); |
| 102 | LocaleMatcher::Result result = matcher.getBestMatchResult("fr", errorCode); |
| 103 | assertEquals("getBestMatchResult(fr).des", "(null)", locString(result.getDesiredLocale())); |
| 104 | assertEquals("getBestMatchResult(fr).desIndex", -1, result.getDesiredIndex()); |
| 105 | assertEquals("getBestMatchResult(fr).supp", |
| 106 | "(null)", locString(result.getSupportedLocale())); |
| 107 | assertEquals("getBestMatchResult(fr).suppIndex", |
| 108 | -1, result.getSupportedIndex()); |
| 109 | } |
| 110 | |
| 111 | void LocaleMatcherTest::testCopyErrorTo() { |
| 112 | IcuTestErrorCode errorCode(*this, "testCopyErrorTo"); |
| 113 | // The builder does not set any errors except out-of-memory. |
| 114 | // Test what we can. |
| 115 | LocaleMatcher::Builder builder; |
| 116 | UErrorCode success = U_ZERO_ERROR; |
| 117 | assertFalse("no error", builder.copyErrorTo(success)); |
| 118 | assertTrue("still success", U_SUCCESS(success)); |
| 119 | UErrorCode failure = U_INVALID_FORMAT_ERROR; |
| 120 | assertTrue("failure passed in", builder.copyErrorTo(failure)); |
| 121 | assertEquals("same failure", U_INVALID_FORMAT_ERROR, failure); |
| 122 | } |
| 123 | |
| 124 | void LocaleMatcherTest::testBasics() { |
| 125 | IcuTestErrorCode errorCode(*this, "testBasics"); |
| 126 | Locale locales[] = { "fr", "en_GB", "en" }; |
| 127 | { |
| 128 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 129 | setSupportedLocales(ARRAY_RANGE(locales)).build(errorCode); |
| 130 | const Locale *best = matcher.getBestMatch("en_GB", errorCode); |
| 131 | assertEquals("fromRange.getBestMatch(en_GB)", "en_GB", locString(best)); |
| 132 | best = matcher.getBestMatch("en_US", errorCode); |
| 133 | assertEquals("fromRange.getBestMatch(en_US)", "en", locString(best)); |
| 134 | best = matcher.getBestMatch("fr_FR", errorCode); |
| 135 | assertEquals("fromRange.getBestMatch(fr_FR)", "fr", locString(best)); |
| 136 | best = matcher.getBestMatch("ja_JP", errorCode); |
| 137 | assertEquals("fromRange.getBestMatch(ja_JP)", "fr", locString(best)); |
| 138 | } |
| 139 | // Code coverage: Variations of setting supported locales. |
| 140 | { |
| 141 | std::vector<Locale> locales{ "fr", "en_GB", "en" }; |
| 142 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 143 | setSupportedLocales(locales.begin(), locales.end()).build(errorCode); |
| 144 | const Locale *best = matcher.getBestMatch("en_GB", errorCode); |
| 145 | assertEquals("fromRange.getBestMatch(en_GB)", "en_GB", locString(best)); |
| 146 | best = matcher.getBestMatch("en_US", errorCode); |
| 147 | assertEquals("fromRange.getBestMatch(en_US)", "en", locString(best)); |
| 148 | best = matcher.getBestMatch("fr_FR", errorCode); |
| 149 | assertEquals("fromRange.getBestMatch(fr_FR)", "fr", locString(best)); |
| 150 | best = matcher.getBestMatch("ja_JP", errorCode); |
| 151 | assertEquals("fromRange.getBestMatch(ja_JP)", "fr", locString(best)); |
| 152 | } |
| 153 | { |
| 154 | Locale::RangeIterator<Locale *> iter(ARRAY_RANGE(locales)); |
| 155 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 156 | setSupportedLocales(iter).build(errorCode); |
| 157 | const Locale *best = matcher.getBestMatch("en_GB", errorCode); |
| 158 | assertEquals("fromIter.getBestMatch(en_GB)", "en_GB", locString(best)); |
| 159 | best = matcher.getBestMatch("en_US", errorCode); |
| 160 | assertEquals("fromIter.getBestMatch(en_US)", "en", locString(best)); |
| 161 | best = matcher.getBestMatch("fr_FR", errorCode); |
| 162 | assertEquals("fromIter.getBestMatch(fr_FR)", "fr", locString(best)); |
| 163 | best = matcher.getBestMatch("ja_JP", errorCode); |
| 164 | assertEquals("fromIter.getBestMatch(ja_JP)", "fr", locString(best)); |
| 165 | } |
| 166 | { |
| 167 | Locale *pointers[] = { locales, locales + 1, locales + 2 }; |
| 168 | // Lambda with explicit reference return type to prevent copy-constructing a temporary |
| 169 | // which would be destructed right away. |
| 170 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 171 | setSupportedLocalesViaConverter( |
| 172 | ARRAY_RANGE(pointers), [](const Locale *p) -> const Locale & { return *p; }). |
| 173 | build(errorCode); |
| 174 | const Locale *best = matcher.getBestMatch("en_GB", errorCode); |
| 175 | assertEquals("viaConverter.getBestMatch(en_GB)", "en_GB", locString(best)); |
| 176 | best = matcher.getBestMatch("en_US", errorCode); |
| 177 | assertEquals("viaConverter.getBestMatch(en_US)", "en", locString(best)); |
| 178 | best = matcher.getBestMatch("fr_FR", errorCode); |
| 179 | assertEquals("viaConverter.getBestMatch(fr_FR)", "fr", locString(best)); |
| 180 | best = matcher.getBestMatch("ja_JP", errorCode); |
| 181 | assertEquals("viaConverter.getBestMatch(ja_JP)", "fr", locString(best)); |
| 182 | } |
| 183 | { |
| 184 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 185 | addSupportedLocale(locales[0]). |
| 186 | addSupportedLocale(locales[1]). |
| 187 | addSupportedLocale(locales[2]). |
| 188 | build(errorCode); |
| 189 | const Locale *best = matcher.getBestMatch("en_GB", errorCode); |
| 190 | assertEquals("added.getBestMatch(en_GB)", "en_GB", locString(best)); |
| 191 | best = matcher.getBestMatch("en_US", errorCode); |
| 192 | assertEquals("added.getBestMatch(en_US)", "en", locString(best)); |
| 193 | best = matcher.getBestMatch("fr_FR", errorCode); |
| 194 | assertEquals("added.getBestMatch(fr_FR)", "fr", locString(best)); |
| 195 | best = matcher.getBestMatch("ja_JP", errorCode); |
| 196 | assertEquals("added.getBestMatch(ja_JP)", "fr", locString(best)); |
| 197 | } |
| 198 | { |
| 199 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 200 | setSupportedLocalesFromListString( |
| 201 | " el, fr;q=0.555555, en-GB ; q = 0.88 , el; q =0, en;q=0.88 , fr "). |
| 202 | build(errorCode); |
| 203 | const Locale *best = matcher.getBestMatchForListString("el, fr, fr;q=0, en-GB", errorCode); |
| 204 | assertEquals("fromList.getBestMatch(en_GB)", "en_GB", locString(best)); |
| 205 | best = matcher.getBestMatch("en_US", errorCode); |
| 206 | assertEquals("fromList.getBestMatch(en_US)", "en", locString(best)); |
| 207 | best = matcher.getBestMatch("fr_FR", errorCode); |
| 208 | assertEquals("fromList.getBestMatch(fr_FR)", "fr", locString(best)); |
| 209 | best = matcher.getBestMatch("ja_JP", errorCode); |
| 210 | assertEquals("fromList.getBestMatch(ja_JP)", "fr", locString(best)); |
| 211 | } |
| 212 | // more API coverage |
| 213 | { |
| 214 | LocalePriorityList list("fr, en-GB", errorCode); |
| 215 | LocalePriorityList::Iterator iter(list.iterator()); |
| 216 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 217 | setSupportedLocales(iter). |
| 218 | addSupportedLocale(Locale::getEnglish()). |
| 219 | setDefaultLocale(&Locale::getGerman()). |
| 220 | build(errorCode); |
| 221 | const Locale *best = matcher.getBestMatch("en_GB", errorCode); |
| 222 | assertEquals("withDefault.getBestMatch(en_GB)", "en_GB", locString(best)); |
| 223 | best = matcher.getBestMatch("en_US", errorCode); |
| 224 | assertEquals("withDefault.getBestMatch(en_US)", "en", locString(best)); |
| 225 | best = matcher.getBestMatch("fr_FR", errorCode); |
| 226 | assertEquals("withDefault.getBestMatch(fr_FR)", "fr", locString(best)); |
| 227 | best = matcher.getBestMatch("ja_JP", errorCode); |
| 228 | assertEquals("withDefault.getBestMatch(ja_JP)", "de", locString(best)); |
| 229 | |
| 230 | Locale desired("en_GB"); // distinct object from Locale.UK |
| 231 | LocaleMatcher::Result result = matcher.getBestMatchResult(desired, errorCode); |
| 232 | assertTrue("withDefault: exactly desired en-GB object", |
| 233 | &desired == result.getDesiredLocale()); |
| 234 | assertEquals("withDefault: en-GB desired index", 0, result.getDesiredIndex()); |
| 235 | assertEquals("withDefault: en-GB supported", |
| 236 | "en_GB", locString(result.getSupportedLocale())); |
| 237 | assertEquals("withDefault: en-GB supported index", 1, result.getSupportedIndex()); |
| 238 | |
| 239 | LocalePriorityList list2("ja-JP, en-US", errorCode); |
| 240 | LocalePriorityList::Iterator iter2(list2.iterator()); |
| 241 | result = matcher.getBestMatchResult(iter2, errorCode); |
| 242 | assertEquals("withDefault: ja-JP, en-US desired index", 1, result.getDesiredIndex()); |
| 243 | assertEquals("withDefault: ja-JP, en-US desired", |
| 244 | "en_US", locString(result.getDesiredLocale())); |
| 245 | |
| 246 | desired = Locale("en", "US"); // distinct object from Locale.US |
| 247 | result = matcher.getBestMatchResult(desired, errorCode); |
| 248 | assertTrue("withDefault: exactly desired en-US object", |
| 249 | &desired == result.getDesiredLocale()); |
| 250 | assertEquals("withDefault: en-US desired index", 0, result.getDesiredIndex()); |
| 251 | assertEquals("withDefault: en-US supported", "en", locString(result.getSupportedLocale())); |
| 252 | assertEquals("withDefault: en-US supported index", 2, result.getSupportedIndex()); |
| 253 | |
| 254 | result = matcher.getBestMatchResult("ja_JP", errorCode); |
| 255 | assertEquals("withDefault: ja-JP desired", "(null)", locString(result.getDesiredLocale())); |
| 256 | assertEquals("withDefault: ja-JP desired index", -1, result.getDesiredIndex()); |
| 257 | assertEquals("withDefault: ja-JP supported", "de", locString(result.getSupportedLocale())); |
| 258 | assertEquals("withDefault: ja-JP supported index", -1, result.getSupportedIndex()); |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | void LocaleMatcherTest::testSupportedDefault() { |
| 263 | // The default locale is one of the supported locales. |
| 264 | IcuTestErrorCode errorCode(*this, "testSupportedDefault"); |
| 265 | Locale locales[] = { "fr", "en_GB", "en" }; |
| 266 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 267 | setSupportedLocales(ARRAY_RANGE(locales)). |
| 268 | setDefaultLocale(&locales[1]). |
| 269 | build(errorCode); |
| 270 | const Locale *best = matcher.getBestMatch("en_GB", errorCode); |
| 271 | assertEquals("getBestMatch(en_GB)", "en_GB", locString(best)); |
| 272 | best = matcher.getBestMatch("en_US", errorCode); |
| 273 | assertEquals("getBestMatch(en_US)", "en", locString(best)); |
| 274 | best = matcher.getBestMatch("fr_FR", errorCode); |
| 275 | assertEquals("getBestMatch(fr_FR)", "fr", locString(best)); |
| 276 | best = matcher.getBestMatch("ja_JP", errorCode); |
| 277 | assertEquals("getBestMatch(ja_JP)", "en_GB", locString(best)); |
| 278 | LocaleMatcher::Result result = matcher.getBestMatchResult("ja_JP", errorCode); |
| 279 | assertEquals("getBestMatchResult(ja_JP).supp", |
| 280 | "en_GB", locString(result.getSupportedLocale())); |
| 281 | assertEquals("getBestMatchResult(ja_JP).suppIndex", |
| 282 | -1, result.getSupportedIndex()); |
| 283 | } |
| 284 | |
| 285 | void LocaleMatcherTest::testUnsupportedDefault() { |
| 286 | // The default locale does not match any of the supported locales. |
| 287 | IcuTestErrorCode errorCode(*this, "testUnsupportedDefault"); |
| 288 | Locale locales[] = { "fr", "en_GB", "en" }; |
| 289 | Locale def("de"); |
| 290 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 291 | setSupportedLocales(ARRAY_RANGE(locales)). |
| 292 | setDefaultLocale(&def). |
| 293 | build(errorCode); |
| 294 | const Locale *best = matcher.getBestMatch("en_GB", errorCode); |
| 295 | assertEquals("getBestMatch(en_GB)", "en_GB", locString(best)); |
| 296 | best = matcher.getBestMatch("en_US", errorCode); |
| 297 | assertEquals("getBestMatch(en_US)", "en", locString(best)); |
| 298 | best = matcher.getBestMatch("fr_FR", errorCode); |
| 299 | assertEquals("getBestMatch(fr_FR)", "fr", locString(best)); |
| 300 | best = matcher.getBestMatch("ja_JP", errorCode); |
| 301 | assertEquals("getBestMatch(ja_JP)", "de", locString(best)); |
| 302 | LocaleMatcher::Result result = matcher.getBestMatchResult("ja_JP", errorCode); |
| 303 | assertEquals("getBestMatchResult(ja_JP).supp", |
| 304 | "de", locString(result.getSupportedLocale())); |
| 305 | assertEquals("getBestMatchResult(ja_JP).suppIndex", |
| 306 | -1, result.getSupportedIndex()); |
| 307 | } |
| 308 | |
| 309 | void LocaleMatcherTest::testNoDefault() { |
| 310 | // We want nullptr instead of any default locale. |
| 311 | IcuTestErrorCode errorCode(*this, "testNoDefault"); |
| 312 | Locale locales[] = { "fr", "en_GB", "en" }; |
| 313 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 314 | setSupportedLocales(ARRAY_RANGE(locales)). |
| 315 | setNoDefaultLocale(). |
| 316 | build(errorCode); |
| 317 | const Locale *best = matcher.getBestMatch("en_GB", errorCode); |
| 318 | assertEquals("getBestMatch(en_GB)", "en_GB", locString(best)); |
| 319 | best = matcher.getBestMatch("en_US", errorCode); |
| 320 | assertEquals("getBestMatch(en_US)", "en", locString(best)); |
| 321 | best = matcher.getBestMatch("fr_FR", errorCode); |
| 322 | assertEquals("getBestMatch(fr_FR)", "fr", locString(best)); |
| 323 | best = matcher.getBestMatch("ja_JP", errorCode); |
| 324 | assertEquals("getBestMatch(ja_JP)", "(null)", locString(best)); |
| 325 | LocaleMatcher::Result result = matcher.getBestMatchResult("ja_JP", errorCode); |
| 326 | assertEquals("getBestMatchResult(ja_JP).supp", |
| 327 | "(null)", locString(result.getSupportedLocale())); |
| 328 | assertEquals("getBestMatchResult(ja_JP).suppIndex", |
| 329 | -1, result.getSupportedIndex()); |
| 330 | } |
| 331 | |
| 332 | void LocaleMatcherTest::testDemotion() { |
| 333 | IcuTestErrorCode errorCode(*this, "testDemotion"); |
| 334 | Locale supported[] = { "fr", "de-CH", "it" }; |
| 335 | Locale desired[] = { "fr-CH", "de-CH", "it" }; |
| 336 | { |
| 337 | LocaleMatcher noDemotion = LocaleMatcher::Builder(). |
| 338 | setSupportedLocales(ARRAY_RANGE(supported)). |
| 339 | setDemotionPerDesiredLocale(ULOCMATCH_DEMOTION_NONE).build(errorCode); |
| 340 | Locale::RangeIterator<Locale *> desiredIter(ARRAY_RANGE(desired)); |
| 341 | assertEquals("no demotion", |
| 342 | "de_CH", locString(noDemotion.getBestMatch(desiredIter, errorCode))); |
| 343 | } |
| 344 | |
| 345 | { |
| 346 | LocaleMatcher regionDemotion = LocaleMatcher::Builder(). |
| 347 | setSupportedLocales(ARRAY_RANGE(supported)). |
| 348 | setDemotionPerDesiredLocale(ULOCMATCH_DEMOTION_REGION).build(errorCode); |
| 349 | Locale::RangeIterator<Locale *> desiredIter(ARRAY_RANGE(desired)); |
| 350 | assertEquals("region demotion", |
| 351 | "fr", locString(regionDemotion.getBestMatch(desiredIter, errorCode))); |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | void LocaleMatcherTest::testDirection() { |
| 356 | IcuTestErrorCode errorCode(*this, "testDirection"); |
| 357 | Locale supported[] = { "ar", "nn" }; |
| 358 | Locale desired[] = { "arz-EG", "nb-DK" }; |
| 359 | LocaleMatcher::Builder builder; |
| 360 | builder.setSupportedLocales(ARRAY_RANGE(supported)); |
| 361 | { |
| 362 | // arz is a close one-way match to ar, and the region matches. |
| 363 | // (Egyptian Arabic vs. Arabic) |
| 364 | // Also explicitly exercise the move copy constructor. |
| 365 | LocaleMatcher built = builder.build(errorCode); |
| 366 | LocaleMatcher withOneWay(std::move(built)); |
| 367 | Locale::RangeIterator<Locale *> desiredIter(ARRAY_RANGE(desired)); |
| 368 | assertEquals("with one-way", "ar", |
| 369 | locString(withOneWay.getBestMatch(desiredIter, errorCode))); |
| 370 | } |
| 371 | { |
| 372 | // nb is a less close two-way match to nn, and the regions differ. |
| 373 | // (Norwegian Bokmal vs. Nynorsk) |
| 374 | // Also explicitly exercise the move assignment operator. |
| 375 | LocaleMatcher onlyTwoWay = builder.build(errorCode); |
| 376 | LocaleMatcher built = |
| 377 | builder.setDirection(ULOCMATCH_DIRECTION_ONLY_TWO_WAY).build(errorCode); |
| 378 | onlyTwoWay = std::move(built); |
| 379 | Locale::RangeIterator<Locale *> desiredIter(ARRAY_RANGE(desired)); |
| 380 | assertEquals("only two-way", "nn", |
| 381 | locString(onlyTwoWay.getBestMatch(desiredIter, errorCode))); |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | void LocaleMatcherTest::testMaxDistanceAndIsMatch() { |
| 386 | IcuTestErrorCode errorCode(*this, "testMaxDistanceAndIsMatch"); |
| 387 | LocaleMatcher::Builder builder; |
| 388 | LocaleMatcher standard = builder.build(errorCode); |
| 389 | Locale germanLux("de-LU"); |
| 390 | Locale germanPhoenician("de-Phnx-AT"); |
| 391 | Locale greek("el"); |
| 392 | assertTrue("standard de-LU / de", standard.isMatch(germanLux, Locale::getGerman(), errorCode)); |
| 393 | assertFalse("standard de-Phnx-AT / de", |
| 394 | standard.isMatch(germanPhoenician, Locale::getGerman(), errorCode)); |
| 395 | |
| 396 | // Allow a script difference to still match. |
| 397 | LocaleMatcher loose = |
| 398 | builder.setMaxDistance(germanPhoenician, Locale::getGerman()).build(errorCode); |
| 399 | assertTrue("loose de-LU / de", loose.isMatch(germanLux, Locale::getGerman(), errorCode)); |
| 400 | assertTrue("loose de-Phnx-AT / de", |
| 401 | loose.isMatch(germanPhoenician, Locale::getGerman(), errorCode)); |
| 402 | assertFalse("loose el / de", loose.isMatch(greek, Locale::getGerman(), errorCode)); |
| 403 | |
| 404 | // Allow at most a regional difference. |
| 405 | LocaleMatcher regional = |
| 406 | builder.setMaxDistance(Locale("de-AT"), Locale::getGerman()).build(errorCode); |
| 407 | assertTrue("regional de-LU / de", |
| 408 | regional.isMatch(Locale("de-LU"), Locale::getGerman(), errorCode)); |
| 409 | assertFalse("regional da / no", regional.isMatch(Locale("da"), Locale("no"), errorCode)); |
| 410 | assertFalse("regional zh-Hant / zh", |
| 411 | regional.isMatch(Locale::getChinese(), Locale::getTraditionalChinese(), errorCode)); |
| 412 | } |
| 413 | |
| 414 | |
| 415 | void LocaleMatcherTest::testMatch() { |
| 416 | IcuTestErrorCode errorCode(*this, "testMatch"); |
| 417 | LocaleMatcher matcher = LocaleMatcher::Builder().build(errorCode); |
| 418 | |
| 419 | // Java test function testMatch_exact() |
| 420 | Locale en_CA("en_CA"); |
| 421 | assertEquals("exact match", 1.0, matcher.internalMatch(en_CA, en_CA, errorCode)); |
| 422 | |
| 423 | // testMatch_none |
| 424 | Locale ar_MK("ar_MK"); |
| 425 | double match = matcher.internalMatch(ar_MK, en_CA, errorCode); |
| 426 | assertTrue("mismatch: 0<=match<0.2", 0 <= match && match < 0.2); |
| 427 | |
| 428 | // testMatch_matchOnMaximized |
| 429 | Locale und_TW("und_TW"); |
| 430 | Locale zh("zh"); |
| 431 | Locale zh_Hant("zh_Hant"); |
| 432 | double matchZh = matcher.internalMatch(und_TW, zh, errorCode); |
| 433 | double matchZhHant = matcher.internalMatch(und_TW, zh_Hant, errorCode); |
| 434 | assertTrue("und_TW should be closer to zh_Hant than to zh", |
| 435 | matchZh < matchZhHant); |
| 436 | Locale en_Hant_TW("en_Hant_TW"); |
| 437 | double matchEnHantTw = matcher.internalMatch(en_Hant_TW, zh_Hant, errorCode); |
| 438 | assertTrue("zh_Hant should be closer to und_TW than to en_Hant_TW", |
| 439 | matchEnHantTw < matchZhHant); |
| 440 | assertTrue("zh should not match und_TW or en_Hant_TW", |
| 441 | matchZh == 0.0 && matchEnHantTw == 0.0); // with changes in CLDR-1435 |
| 442 | } |
| 443 | |
| 444 | void LocaleMatcherTest::testResolvedLocale() { |
| 445 | IcuTestErrorCode errorCode(*this, "testResolvedLocale"); |
| 446 | LocaleMatcher matcher = LocaleMatcher::Builder(). |
| 447 | addSupportedLocale("ar-EG"). |
| 448 | build(errorCode); |
| 449 | Locale desired("ar-SA-u-nu-latn"); |
| 450 | LocaleMatcher::Result result = matcher.getBestMatchResult(desired, errorCode); |
| 451 | assertEquals("best", "ar_EG", locString(result.getSupportedLocale())); |
| 452 | Locale resolved = result.makeResolvedLocale(errorCode); |
| 453 | assertEquals("ar-EG + ar-SA-u-nu-latn = ar-SA-u-nu-latn", |
| 454 | "ar-SA-u-nu-latn", |
| 455 | resolved.toLanguageTag<std::string>(errorCode).data()); |
| 456 | } |
| 457 | |
| 458 | namespace { |
| 459 | |
| 460 | bool toInvariant(const UnicodeString &s, CharString &inv, ErrorCode &errorCode) { |
| 461 | if (errorCode.isSuccess()) { |
| 462 | inv.clear().appendInvariantChars(s, errorCode); |
| 463 | return errorCode.isSuccess(); |
| 464 | } |
| 465 | return false; |
| 466 | } |
| 467 | |
| 468 | bool getSuffixAfterPrefix(const UnicodeString &s, int32_t limit, |
| 469 | const UnicodeString &prefix, UnicodeString &suffix) { |
| 470 | if (prefix.length() <= limit && s.startsWith(prefix)) { |
| 471 | suffix.setTo(s, prefix.length(), limit - prefix.length()); |
| 472 | return true; |
| 473 | } else { |
| 474 | return false; |
| 475 | } |
| 476 | } |
| 477 | |
| 478 | bool getInvariantSuffixAfterPrefix(const UnicodeString &s, int32_t limit, |
| 479 | const UnicodeString &prefix, CharString &suffix, |
| 480 | ErrorCode &errorCode) { |
| 481 | UnicodeString u_suffix; |
| 482 | return getSuffixAfterPrefix(s, limit, prefix, u_suffix) && |
| 483 | toInvariant(u_suffix, suffix, errorCode); |
| 484 | } |
| 485 | |
| 486 | bool readTestCase(const UnicodeString &line, TestCase &test, IcuTestErrorCode &errorCode) { |
| 487 | if (errorCode.isFailure()) { return false; } |
| 488 | ++test.lineNr; |
| 489 | // Start of comment, or end of line, minus trailing spaces. |
| 490 | int32_t limit = line.indexOf(u'#'); |
| 491 | if (limit < 0) { |
| 492 | limit = line.length(); |
| 493 | // Remove trailing CR LF. |
| 494 | char16_t c; |
| 495 | while (limit > 0 && ((c = line.charAt(limit - 1)) == u'\n' || c == u'\r')) { |
| 496 | --limit; |
| 497 | } |
| 498 | } |
| 499 | // Remove spaces before comment or at the end of the line. |
| 500 | char16_t c; |
| 501 | while (limit > 0 && ((c = line.charAt(limit - 1)) == u' ' || c == u'\t')) { |
| 502 | --limit; |
| 503 | } |
| 504 | if (limit == 0) { // empty line |
| 505 | return false; |
| 506 | } |
| 507 | if (line.startsWith(u"** test: ")) { |
| 508 | test.reset(); |
| 509 | } else if (getInvariantSuffixAfterPrefix(line, limit, u"@supported=", |
| 510 | test.supported, errorCode)) { |
| 511 | } else if (getInvariantSuffixAfterPrefix(line, limit, u"@default=", |
| 512 | test.def, errorCode)) { |
| 513 | } else if (getSuffixAfterPrefix(line, limit, u"@favor=", test.favor)) { |
| 514 | } else if (getSuffixAfterPrefix(line, limit, u"@threshold=", test.threshold)) { |
| 515 | } else { |
| 516 | int32_t matchSep = line.indexOf(u">>"); |
| 517 | // >> before an inline comment, and followed by more than white space. |
| 518 | if (0 <= matchSep && (matchSep + 2) < limit) { |
| 519 | toInvariant(line.tempSubStringBetween(0, matchSep).trim(), test.desired, errorCode); |
| 520 | test.expDesired.clear(); |
| 521 | test.expCombined.clear(); |
| 522 | int32_t start = matchSep + 2; |
| 523 | int32_t expLimit = line.indexOf(u'|', start); |
| 524 | if (expLimit < 0) { |
| 525 | toInvariant(line.tempSubStringBetween(start, limit).trim(), |
| 526 | test.expMatch, errorCode); |
| 527 | } else { |
| 528 | toInvariant(line.tempSubStringBetween(start, expLimit).trim(), |
| 529 | test.expMatch, errorCode); |
| 530 | start = expLimit + 1; |
| 531 | expLimit = line.indexOf(u'|', start); |
| 532 | if (expLimit < 0) { |
| 533 | toInvariant(line.tempSubStringBetween(start, limit).trim(), |
| 534 | test.expDesired, errorCode); |
| 535 | } else { |
| 536 | toInvariant(line.tempSubStringBetween(start, expLimit).trim(), |
| 537 | test.expDesired, errorCode); |
| 538 | toInvariant(line.tempSubStringBetween(expLimit + 1, limit).trim(), |
| 539 | test.expCombined, errorCode); |
| 540 | } |
| 541 | } |
| 542 | return errorCode.isSuccess(); |
| 543 | } else { |
| 544 | errorCode.set(U_INVALID_FORMAT_ERROR); |
| 545 | } |
| 546 | } |
| 547 | return false; |
| 548 | } |
| 549 | |
| 550 | Locale *getLocaleOrNull(const CharString &s, Locale &locale) { |
| 551 | if (s == "null") { |
| 552 | return nullptr; |
| 553 | } else { |
| 554 | return &(locale = Locale(s.data())); |
| 555 | } |
| 556 | } |
| 557 | |
| 558 | } // namespace |
| 559 | |
| 560 | UBool LocaleMatcherTest::dataDriven(const TestCase &test, IcuTestErrorCode &errorCode) { |
| 561 | LocaleMatcher::Builder builder; |
| 562 | builder.setSupportedLocalesFromListString(test.supported.toStringPiece()); |
| 563 | if (!test.def.isEmpty()) { |
| 564 | Locale defaultLocale(test.def.data()); |
| 565 | builder.setDefaultLocale(&defaultLocale); |
| 566 | } |
| 567 | if (!test.favor.isEmpty()) { |
| 568 | ULocMatchFavorSubtag favor; |
| 569 | if (test.favor == u"normal") { |
| 570 | favor = ULOCMATCH_FAVOR_LANGUAGE; |
| 571 | } else if (test.favor == u"script") { |
| 572 | favor = ULOCMATCH_FAVOR_SCRIPT; |
| 573 | } else { |
| 574 | errln(UnicodeString(u"unsupported FavorSubtag value ") + test.favor); |
Frank Tang | 1f164ee | 2022-11-08 12:31:27 -0800 | [diff] [blame^] | 575 | return false; |
Frank Tang | 3e05d9d | 2021-11-08 14:04:04 -0800 | [diff] [blame] | 576 | } |
| 577 | builder.setFavorSubtag(favor); |
| 578 | } |
| 579 | if (!test.threshold.isEmpty()) { |
| 580 | infoln("skipping test case on line %d with non-default threshold: not exposed via API", |
| 581 | (int)test.lineNr); |
Frank Tang | 1f164ee | 2022-11-08 12:31:27 -0800 | [diff] [blame^] | 582 | return true; |
Frank Tang | 3e05d9d | 2021-11-08 14:04:04 -0800 | [diff] [blame] | 583 | // int32_t threshold = Integer.valueOf(test.threshold); |
| 584 | // builder.internalSetThresholdDistance(threshold); |
| 585 | } |
| 586 | LocaleMatcher matcher = builder.build(errorCode); |
| 587 | if (errorCode.errIfFailureAndReset("LocaleMatcher::Builder::build()")) { |
Frank Tang | 1f164ee | 2022-11-08 12:31:27 -0800 | [diff] [blame^] | 588 | return false; |
Frank Tang | 3e05d9d | 2021-11-08 14:04:04 -0800 | [diff] [blame] | 589 | } |
| 590 | |
| 591 | Locale expMatchLocale(""); |
| 592 | Locale *expMatch = getLocaleOrNull(test.expMatch, expMatchLocale); |
| 593 | if (test.expDesired.isEmpty() && test.expCombined.isEmpty()) { |
| 594 | StringPiece desiredSP = test.desired.toStringPiece(); |
| 595 | const Locale *bestSupported = matcher.getBestMatchForListString(desiredSP, errorCode); |
| 596 | if (!assertEquals("bestSupported from string", |
| 597 | locString(expMatch), locString(bestSupported))) { |
Frank Tang | 1f164ee | 2022-11-08 12:31:27 -0800 | [diff] [blame^] | 598 | return false; |
Frank Tang | 3e05d9d | 2021-11-08 14:04:04 -0800 | [diff] [blame] | 599 | } |
| 600 | LocalePriorityList desired(test.desired.toStringPiece(), errorCode); |
| 601 | LocalePriorityList::Iterator desiredIter = desired.iterator(); |
| 602 | if (desired.getLength() == 1) { |
| 603 | const Locale &desiredLocale = desiredIter.next(); |
| 604 | bestSupported = matcher.getBestMatch(desiredLocale, errorCode); |
| 605 | UBool ok = assertEquals("bestSupported from Locale", |
| 606 | locString(expMatch), locString(bestSupported)); |
| 607 | |
| 608 | LocaleMatcher::Result result = matcher.getBestMatchResult(desiredLocale, errorCode); |
| 609 | return ok & assertEquals("result.getSupportedLocale from Locale", |
| 610 | locString(expMatch), locString(result.getSupportedLocale())); |
| 611 | } else { |
| 612 | bestSupported = matcher.getBestMatch(desiredIter, errorCode); |
| 613 | return assertEquals("bestSupported from Locale iterator", |
| 614 | locString(expMatch), locString(bestSupported)); |
| 615 | } |
| 616 | } else { |
| 617 | LocalePriorityList desired(test.desired.toStringPiece(), errorCode); |
| 618 | LocalePriorityList::Iterator desiredIter = desired.iterator(); |
| 619 | LocaleMatcher::Result result = matcher.getBestMatchResult(desiredIter, errorCode); |
| 620 | UBool ok = assertEquals("result.getSupportedLocale from Locales", |
| 621 | locString(expMatch), locString(result.getSupportedLocale())); |
| 622 | if (!test.expDesired.isEmpty()) { |
| 623 | Locale expDesiredLocale(""); |
| 624 | Locale *expDesired = getLocaleOrNull(test.expDesired, expDesiredLocale); |
| 625 | ok &= assertEquals("result.getDesiredLocale from Locales", |
| 626 | locString(expDesired), locString(result.getDesiredLocale())); |
| 627 | } |
| 628 | if (!test.expCombined.isEmpty()) { |
| 629 | if (test.expMatch.contains("-u-")) { |
| 630 | logKnownIssue("20727", |
| 631 | UnicodeString(u"ignoring makeResolvedLocale() line ") + test.lineNr); |
| 632 | return ok; |
| 633 | } |
| 634 | Locale expCombinedLocale(""); |
| 635 | Locale *expCombined = getLocaleOrNull(test.expCombined, expCombinedLocale); |
| 636 | Locale combined = result.makeResolvedLocale(errorCode); |
| 637 | ok &= assertEquals("combined Locale from Locales", |
| 638 | locString(expCombined), locString(&combined)); |
| 639 | } |
| 640 | return ok; |
| 641 | } |
| 642 | } |
| 643 | |
| 644 | void LocaleMatcherTest::testDataDriven() { |
| 645 | IcuTestErrorCode errorCode(*this, "testDataDriven"); |
| 646 | CharString path(getSourceTestData(errorCode), errorCode); |
| 647 | path.appendPathPart("localeMatcherTest.txt", errorCode); |
| 648 | const char *codePage = "UTF-8"; |
Frank Tang | 1f164ee | 2022-11-08 12:31:27 -0800 | [diff] [blame^] | 649 | LocalUCHARBUFPointer f(ucbuf_open(path.data(), &codePage, true, false, errorCode)); |
Frank Tang | 3e05d9d | 2021-11-08 14:04:04 -0800 | [diff] [blame] | 650 | if(errorCode.errIfFailureAndReset("ucbuf_open(localeMatcherTest.txt)")) { |
| 651 | return; |
| 652 | } |
| 653 | int32_t lineLength; |
| 654 | const UChar *p; |
| 655 | UnicodeString line; |
| 656 | TestCase test; |
| 657 | int32_t numPassed = 0; |
| 658 | while ((p = ucbuf_readline(f.getAlias(), &lineLength, errorCode)) != nullptr && |
| 659 | errorCode.isSuccess()) { |
Frank Tang | 1f164ee | 2022-11-08 12:31:27 -0800 | [diff] [blame^] | 660 | line.setTo(false, p, lineLength); |
Frank Tang | 3e05d9d | 2021-11-08 14:04:04 -0800 | [diff] [blame] | 661 | if (!readTestCase(line, test, errorCode)) { |
| 662 | if (errorCode.errIfFailureAndReset( |
| 663 | "test data syntax error on line %d", (int)test.lineNr)) { |
| 664 | infoln(line); |
| 665 | } |
| 666 | continue; |
| 667 | } |
| 668 | UBool ok = dataDriven(test, errorCode); |
| 669 | if (errorCode.errIfFailureAndReset("test error on line %d", (int)test.lineNr)) { |
| 670 | infoln(line); |
| 671 | } else if (!ok) { |
| 672 | infoln("test failure on line %d", (int)test.lineNr); |
| 673 | infoln(line); |
| 674 | } else { |
| 675 | ++numPassed; |
| 676 | } |
| 677 | } |
| 678 | infoln("number of passing test cases: %d", (int)numPassed); |
| 679 | } |