ICU-21029 LocaleMatcher: add option to turn off default locale
diff --git a/icu4c/source/common/localematcher.cpp b/icu4c/source/common/localematcher.cpp
index 85db8c8..a7a1137 100644
--- a/icu4c/source/common/localematcher.cpp
+++ b/icu4c/source/common/localematcher.cpp
@@ -131,6 +131,7 @@
         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 @@
     thresholdDistance_ = src.thresholdDistance_;
     demotion_ = src.demotion_;
     defaultLocale_ = src.defaultLocale_;
+    withDefault_ = src.withDefault_,
     favor_ = src.favor_;
     direction_ = src.direction_;
 
@@ -229,6 +231,14 @@
     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 @@
     }
     delete defaultLocale_;
     defaultLocale_ = clone;
+    withDefault_ = true;
     return *this;
 }
 
@@ -417,13 +428,14 @@
         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 2e1a7a3..3ec71df 100644
--- a/icu4c/source/common/unicode/localematcher.h
+++ b/icu4c/source/common/unicode/localematcher.h
@@ -231,8 +231,8 @@
         /**
          * 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 @@
          */
         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 @@
         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 683466b..62364ae 100644
--- a/icu4c/source/test/intltest/localematchertest.cpp
+++ b/icu4c/source/test/intltest/localematchertest.cpp
@@ -58,6 +58,7 @@
     void testBasics();
     void testSupportedDefault();
     void testUnsupportedDefault();
+    void testNoDefault();
     void testDemotion();
     void testDirection();
     void testMatch();
@@ -82,6 +83,7 @@
     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 @@
                  -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 73a9f4d..400090a 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 @@
         /**
          * 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 @@
         /**
          * 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 @@
         private int thresholdDistance = -1;
         private Demotion demotion;
         private ULocale defaultLocale;
+        private boolean withDefault = true;
         private FavorSubtag favor;
         private Direction direction;
 
@@ -465,8 +466,25 @@
         }
 
         /**
+         * 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 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 Builder setDefaultLocale(Locale defaultLocale) {
             this.defaultLocale = ULocale.forLocale(defaultLocale);
+            withDefault = true;
             return this;
         }
 
@@ -673,13 +695,14 @@
         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 ff6e700..d4df833 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
@@ -222,6 +222,30 @@
     }
 
     @Test
+    public void testNoDefault() {
+        // We want null instead of any default locale.
+        List<ULocale> 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
         final LocaleMatcher matcher = newLocaleMatcher("zh_CN, zh_TW, iw");