From eaee0b175ec221928b398261a530c44ae36c8117 Mon Sep 17 00:00:00 2001 From: Markus Scherer Date: Tue, 19 May 2020 19:17:44 -0700 Subject: [PATCH] ICU-21029 LocaleMatcher: add option to turn off default locale --- icu4c/source/common/localematcher.cpp | 16 +++++++-- icu4c/source/common/unicode/localematcher.h | 19 ++++++++-- .../test/intltest/localematchertest.cpp | 25 +++++++++++++ .../src/com/ibm/icu/util/LocaleMatcher.java | 35 +++++++++++++++---- .../icu/dev/test/util/LocaleMatcherTest.java | 24 +++++++++++++ 5 files changed, 109 insertions(+), 10 deletions(-) diff --git a/icu4c/source/common/localematcher.cpp b/icu4c/source/common/localematcher.cpp index 85db8c8bf32..a7a11370858 100644 --- a/icu4c/source/common/localematcher.cpp +++ b/icu4c/source/common/localematcher.cpp @@ -131,6 +131,7 @@ LocaleMatcher::Builder::Builder(LocaleMatcher::Builder &&src) U_NOEXCEPT : thresholdDistance_(src.thresholdDistance_), demotion_(src.demotion_), defaultLocale_(src.defaultLocale_), + withDefault_(src.withDefault_), favor_(src.favor_), direction_(src.direction_) { src.supportedLocales_ = nullptr; @@ -150,6 +151,7 @@ LocaleMatcher::Builder &LocaleMatcher::Builder::operator=(LocaleMatcher::Builder thresholdDistance_ = src.thresholdDistance_; demotion_ = src.demotion_; defaultLocale_ = src.defaultLocale_; + withDefault_ = src.withDefault_, favor_ = src.favor_; direction_ = src.direction_; @@ -229,6 +231,14 @@ LocaleMatcher::Builder &LocaleMatcher::Builder::addSupportedLocale(const Locale return *this; } +LocaleMatcher::Builder &LocaleMatcher::Builder::setNoDefaultLocale() { + if (U_FAILURE(errorCode_)) { return *this; } + delete defaultLocale_; + defaultLocale_ = nullptr; + withDefault_ = false; + return *this; +} + LocaleMatcher::Builder &LocaleMatcher::Builder::setDefaultLocale(const Locale *defaultLocale) { if (U_FAILURE(errorCode_)) { return *this; } Locale *clone = nullptr; @@ -241,6 +251,7 @@ LocaleMatcher::Builder &LocaleMatcher::Builder::setDefaultLocale(const Locale *d } delete defaultLocale_; defaultLocale_ = clone; + withDefault_ = true; return *this; } @@ -417,13 +428,14 @@ LocaleMatcher::LocaleMatcher(const Builder &builder, UErrorCode &errorCode) : for (int32_t i = 0; i < supportedLocalesLength; ++i) { const Locale &locale = *supportedLocales[i]; const LSR &lsr = lsrs[i]; - if (defLSR == nullptr) { + if (defLSR == nullptr && builder.withDefault_) { + // Implicit default locale = first supported locale, if not turned off. U_ASSERT(i == 0); def = &locale; defLSR = &lsr; order[i] = 1; suppLength = putIfAbsent(lsr, 0, suppLength, errorCode); - } else if (lsr.isEquivalentTo(*defLSR)) { + } else if (defLSR != nullptr && lsr.isEquivalentTo(*defLSR)) { order[i] = 1; suppLength = putIfAbsent(lsr, i, suppLength, errorCode); } else if (localeDistance.isParadigmLSR(lsr)) { diff --git a/icu4c/source/common/unicode/localematcher.h b/icu4c/source/common/unicode/localematcher.h index 2e1a7a349f3..3ec71df82c4 100644 --- a/icu4c/source/common/unicode/localematcher.h +++ b/icu4c/source/common/unicode/localematcher.h @@ -231,8 +231,8 @@ public: /** * Returns the best-matching supported locale. * If none matched well enough, this is the default locale. - * The default locale is nullptr if the list of supported locales is empty and - * no explicit default locale is set. + * The default locale is nullptr if Builder::setNoDefaultLocale() was called, + * or if the list of supported locales is empty and no explicit default locale is set. * * @return the best-matching supported locale, or nullptr. * @draft ICU 65 @@ -419,9 +419,23 @@ public: */ Builder &addSupportedLocale(const Locale &locale); +#ifndef U_HIDE_DRAFT_API + /** + * Sets no default locale. + * There will be no explicit or implicit default locale. + * If there is no good match, then the matcher will return nullptr for the + * best supported locale. + * + * @draft ICU 68 + */ + Builder &setNoDefaultLocale(); +#endif // U_HIDE_DRAFT_API + /** * Sets the default locale; if nullptr, or if it is not set explicitly, * then the first supported locale is used as the default locale. + * There is no default locale at all (nullptr will be returned instead) + * if setNoDefaultLocale() is called. * * @param defaultLocale the default locale (will be copied) * @return this Builder object @@ -505,6 +519,7 @@ public: int32_t thresholdDistance_ = -1; ULocMatchDemotion demotion_ = ULOCMATCH_DEMOTION_REGION; Locale *defaultLocale_ = nullptr; + bool withDefault_ = true; ULocMatchFavorSubtag favor_ = ULOCMATCH_FAVOR_LANGUAGE; ULocMatchDirection direction_ = ULOCMATCH_DIRECTION_WITH_ONE_WAY; }; diff --git a/icu4c/source/test/intltest/localematchertest.cpp b/icu4c/source/test/intltest/localematchertest.cpp index 683466b3c56..62364ae0f57 100644 --- a/icu4c/source/test/intltest/localematchertest.cpp +++ b/icu4c/source/test/intltest/localematchertest.cpp @@ -58,6 +58,7 @@ public: void testBasics(); void testSupportedDefault(); void testUnsupportedDefault(); + void testNoDefault(); void testDemotion(); void testDirection(); void testMatch(); @@ -82,6 +83,7 @@ void LocaleMatcherTest::runIndexedTest(int32_t index, UBool exec, const char *&n TESTCASE_AUTO(testBasics); TESTCASE_AUTO(testSupportedDefault); TESTCASE_AUTO(testUnsupportedDefault); + TESTCASE_AUTO(testNoDefault); TESTCASE_AUTO(testDemotion); TESTCASE_AUTO(testDirection); TESTCASE_AUTO(testMatch); @@ -302,6 +304,29 @@ void LocaleMatcherTest::testUnsupportedDefault() { -1, result.getSupportedIndex()); } +void LocaleMatcherTest::testNoDefault() { + // We want nullptr instead of any default locale. + IcuTestErrorCode errorCode(*this, "testNoDefault"); + Locale locales[] = { "fr", "en_GB", "en" }; + LocaleMatcher matcher = LocaleMatcher::Builder(). + setSupportedLocales(ARRAY_RANGE(locales)). + setNoDefaultLocale(). + build(errorCode); + const Locale *best = matcher.getBestMatch("en_GB", errorCode); + assertEquals("getBestMatch(en_GB)", "en_GB", locString(best)); + best = matcher.getBestMatch("en_US", errorCode); + assertEquals("getBestMatch(en_US)", "en", locString(best)); + best = matcher.getBestMatch("fr_FR", errorCode); + assertEquals("getBestMatch(fr_FR)", "fr", locString(best)); + best = matcher.getBestMatch("ja_JP", errorCode); + assertEquals("getBestMatch(ja_JP)", "(null)", locString(best)); + LocaleMatcher::Result result = matcher.getBestMatchResult("ja_JP", errorCode); + assertEquals("getBestMatchResult(ja_JP).supp", + "(null)", locString(result.getSupportedLocale())); + assertEquals("getBestMatchResult(ja_JP).suppIndex", + -1, result.getSupportedIndex()); +} + void LocaleMatcherTest::testDemotion() { IcuTestErrorCode errorCode(*this, "testDemotion"); Locale supported[] = { "fr", "de-CH", "it" }; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/LocaleMatcher.java b/icu4j/main/classes/core/src/com/ibm/icu/util/LocaleMatcher.java index 73a9f4dd02d..400090ae15e 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/LocaleMatcher.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/LocaleMatcher.java @@ -244,8 +244,8 @@ public final class LocaleMatcher { /** * Returns the best-matching supported locale. * If none matched well enough, this is the default locale. - * The default locale is null if the list of supported locales is empty and - * no explicit default locale is set. + * The default locale is null if {@link Builder#setNoDefaultLocale()} was called, + * or if the list of supported locales is empty and no explicit default locale is set. * * @return the best-matching supported locale, or null. * @draft ICU 65 @@ -255,8 +255,8 @@ public final class LocaleMatcher { /** * Returns the best-matching supported locale. * If none matched well enough, this is the default locale. - * The default locale is null if the list of supported locales is empty and - * no explicit default locale is set. + * The default locale is null if {@link Builder#setNoDefaultLocale()} was called, + * or if the list of supported locales is empty and no explicit default locale is set. * * @return the best-matching supported locale, or null. * @draft ICU 65 @@ -382,6 +382,7 @@ public final class LocaleMatcher { private int thresholdDistance = -1; private Demotion demotion; private ULocale defaultLocale; + private boolean withDefault = true; private FavorSubtag favor; private Direction direction; @@ -464,9 +465,26 @@ public final class LocaleMatcher { return addSupportedULocale(ULocale.forLocale(locale)); } + /** + * Sets no default locale. + * There will be no explicit or implicit default locale. + * If there is no good match, then the matcher will return null for the + * best supported locale. + * + * @draft ICU 68 + * @provisional This API might change or be removed in a future release. + */ + public Builder setNoDefaultLocale() { + this.defaultLocale = null; + withDefault = false; + return this; + } + /** * Sets the default locale; if null, or if it is not set explicitly, * then the first supported locale is used as the default locale. + * There is no default locale at all (null will be returned instead) + * if {@link #setNoDefaultLocale()} is called. * * @param defaultLocale the default locale * @return this Builder object @@ -475,12 +493,15 @@ public final class LocaleMatcher { */ public Builder setDefaultULocale(ULocale defaultLocale) { this.defaultLocale = defaultLocale; + withDefault = true; return this; } /** * Sets the default locale; if null, or if it is not set explicitly, * then the first supported locale is used as the default locale. + * There is no default locale at all (null will be returned instead) + * if {@link #setNoDefaultLocale()} is called. * * @param defaultLocale the default locale * @return this Builder object @@ -489,6 +510,7 @@ public final class LocaleMatcher { */ public Builder setDefaultLocale(Locale defaultLocale) { this.defaultLocale = ULocale.forLocale(defaultLocale); + withDefault = true; return this; } @@ -673,13 +695,14 @@ public final class LocaleMatcher { i = 0; for (ULocale locale : supportedULocales) { LSR lsr = lsrs[i]; - if (defLSR == null) { + if (defLSR == null && builder.withDefault) { + // Implicit default locale = first supported locale, if not turned off. assert i == 0; udef = locale; def = supportedLocales[0]; defLSR = lsr; suppLength = putIfAbsent(lsr, 0, suppLength); - } else if (lsr.isEquivalentTo(defLSR)) { + } else if (defLSR != null && lsr.isEquivalentTo(defLSR)) { suppLength = putIfAbsent(lsr, i, suppLength); } else if (LocaleDistance.INSTANCE.isParadigmLSR(lsr)) { order[i] = 2; diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/LocaleMatcherTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/LocaleMatcherTest.java index ff6e7000397..d4df833e4e4 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/LocaleMatcherTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/LocaleMatcherTest.java @@ -221,6 +221,30 @@ public class LocaleMatcherTest extends TestFmwk { -1, result.getSupportedIndex()); } + @Test + public void testNoDefault() { + // We want null instead of any default locale. + List locales = Arrays.asList( + new ULocale("fr"), new ULocale("en_GB"), new ULocale("en")); + LocaleMatcher matcher = LocaleMatcher.builder(). + setSupportedULocales(locales). + setNoDefaultLocale(). + build(); + ULocale best = matcher.getBestMatch("en_GB"); + assertEquals("getBestMatch(en_GB)", "en_GB", locString(best)); + best = matcher.getBestMatch("en_US"); + assertEquals("getBestMatch(en_US)", "en", locString(best)); + best = matcher.getBestMatch("fr_FR"); + assertEquals("getBestMatch(fr_FR)", "fr", locString(best)); + best = matcher.getBestMatch("ja_JP"); + assertEquals("getBestMatch(ja_JP)", "(null)", locString(best)); + LocaleMatcher.Result result = matcher.getBestMatchResult(new ULocale("ja_JP")); + assertEquals("getBestMatchResult(ja_JP).supp", + "(null)", locString(result.getSupportedULocale())); + assertEquals("getBestMatchResult(ja_JP).suppIndex", + -1, result.getSupportedIndex()); + } + @Test public void testFallback() { // check that script fallbacks are handled right