ICU-21190 Adding PluralRules select for number ranges

See #1309
diff --git a/icu4c/source/i18n/i18n.vcxproj b/icu4c/source/i18n/i18n.vcxproj
index de36301..218772f 100644
--- a/icu4c/source/i18n/i18n.vcxproj
+++ b/icu4c/source/i18n/i18n.vcxproj
@@ -221,6 +221,7 @@
     <ClCompile Include="numsys.cpp" />
     <ClCompile Include="olsontz.cpp" />
     <ClCompile Include="persncal.cpp" />
+    <ClCompile Include="pluralranges.cpp" />
     <ClCompile Include="plurfmt.cpp" />
     <ClCompile Include="plurrule.cpp" />
     <ClCompile Include="quantityformatter.cpp" />
@@ -391,6 +392,7 @@
     <ClInclude Include="nfsubs.h" />
     <ClInclude Include="olsontz.h" />
     <ClInclude Include="persncal.h" />
+    <ClInclude Include="pluralranges.h" />
     <ClInclude Include="plurrule_impl.h" />
     <ClInclude Include="quantityformatter.h" />
     <ClInclude Include="sharedbreakiterator.h" />
diff --git a/icu4c/source/i18n/i18n.vcxproj.filters b/icu4c/source/i18n/i18n.vcxproj.filters
index a012465..2fc88e0 100644
--- a/icu4c/source/i18n/i18n.vcxproj.filters
+++ b/icu4c/source/i18n/i18n.vcxproj.filters
@@ -234,6 +234,9 @@
     <ClCompile Include="persncal.cpp">
       <Filter>formatting</Filter>
     </ClCompile>
+    <ClCompile Include="pluralranges.cpp">
+      <Filter>formatting</Filter>
+    </ClCompile>
     <ClCompile Include="plurfmt.cpp">
       <Filter>formatting</Filter>
     </ClCompile>
@@ -983,6 +986,9 @@
     <ClInclude Include="persncal.h">
       <Filter>formatting</Filter>
     </ClInclude>
+    <ClInclude Include="pluralranges.h">
+      <Filter>formatting</Filter>
+    </ClInclude>
     <ClInclude Include="plurrule_impl.h">
       <Filter>formatting</Filter>
     </ClInclude>
diff --git a/icu4c/source/i18n/i18n_uwp.vcxproj b/icu4c/source/i18n/i18n_uwp.vcxproj
index be7f4da..f836aeb 100644
--- a/icu4c/source/i18n/i18n_uwp.vcxproj
+++ b/icu4c/source/i18n/i18n_uwp.vcxproj
@@ -454,6 +454,7 @@
     <ClCompile Include="numsys.cpp" />
     <ClCompile Include="olsontz.cpp" />
     <ClCompile Include="persncal.cpp" />
+    <ClCompile Include="pluralranges.cpp" />
     <ClCompile Include="plurfmt.cpp" />
     <ClCompile Include="plurrule.cpp" />
     <ClCompile Include="quantityformatter.cpp" />
@@ -622,6 +623,7 @@
     <ClInclude Include="nfsubs.h" />
     <ClInclude Include="olsontz.h" />
     <ClInclude Include="persncal.h" />
+    <ClInclude Include="pluralranges.h" />
     <ClInclude Include="plurrule_impl.h" />
     <ClInclude Include="quantityformatter.h" />
     <ClInclude Include="sharedbreakiterator.h" />
diff --git a/icu4c/source/i18n/number_output.cpp b/icu4c/source/i18n/number_output.cpp
index 9b1a11d..7129b94 100644
--- a/icu4c/source/i18n/number_output.cpp
+++ b/icu4c/source/i18n/number_output.cpp
@@ -11,6 +11,7 @@
 #include "util.h"
 #include "number_decimalquantity.h"
 #include "number_decnum.h"
+#include "numrange_impl.h"
 
 U_NAMESPACE_BEGIN
 namespace number {
@@ -47,6 +48,42 @@
 impl::UFormattedNumberData::~UFormattedNumberData() = default;
 
 
+UPRV_FORMATTED_VALUE_SUBCLASS_AUTO_IMPL(FormattedNumberRange)
+
+#define UPRV_NOARG
+
+UnicodeString FormattedNumberRange::getFirstDecimal(UErrorCode& status) const {
+    UPRV_FORMATTED_VALUE_METHOD_GUARD(ICU_Utility::makeBogusString())
+    return fData->quantity1.toScientificString();
+}
+
+UnicodeString FormattedNumberRange::getSecondDecimal(UErrorCode& status) const {
+    UPRV_FORMATTED_VALUE_METHOD_GUARD(ICU_Utility::makeBogusString())
+    return fData->quantity2.toScientificString();
+}
+
+void FormattedNumberRange::getDecimalNumbers(ByteSink& sink1, ByteSink& sink2, UErrorCode& status) const {
+    UPRV_FORMATTED_VALUE_METHOD_GUARD(UPRV_NOARG)
+    impl::DecNum decnum1;
+    impl::DecNum decnum2;
+    fData->quantity1.toDecNum(decnum1, status).toString(sink1, status);
+    fData->quantity2.toDecNum(decnum2, status).toString(sink2, status);
+}
+
+UNumberRangeIdentityResult FormattedNumberRange::getIdentityResult(UErrorCode& status) const {
+    UPRV_FORMATTED_VALUE_METHOD_GUARD(UNUM_IDENTITY_RESULT_NOT_EQUAL)
+    return fData->identityResult;
+}
+
+const impl::UFormattedNumberRangeData* FormattedNumberRange::getData(UErrorCode& status) const {
+    UPRV_FORMATTED_VALUE_METHOD_GUARD(nullptr)
+    return fData;
+}
+
+
+impl::UFormattedNumberRangeData::~UFormattedNumberRangeData() = default;
+
+
 } // namespace number
 U_NAMESPACE_END
 
diff --git a/icu4c/source/i18n/numrange_capi.cpp b/icu4c/source/i18n/numrange_capi.cpp
index 184f0ab..a440a53 100644
--- a/icu4c/source/i18n/numrange_capi.cpp
+++ b/icu4c/source/i18n/numrange_capi.cpp
@@ -59,8 +59,8 @@
     fImpl.fData = nullptr;
 }
 
-}
-}
+} // namespace impl
+} // namespace number
 U_NAMESPACE_END
 
 
@@ -71,6 +71,16 @@
     unumrf)
 
 
+const UFormattedNumberRangeData* number::impl::validateUFormattedNumberRange(
+        const UFormattedNumberRange* uresult, UErrorCode& status) {
+    auto* result = UFormattedNumberRangeApiHelper::validate(uresult, status);
+    if (U_FAILURE(status)) {
+        return nullptr;
+    }
+    return &result->fData;
+}
+
+
 U_CAPI UNumberRangeFormatter* U_EXPORT2
 unumrf_openForSkeletonWithCollapseAndIdentityFallback(
         const UChar* skeleton,
diff --git a/icu4c/source/i18n/numrange_fluent.cpp b/icu4c/source/i18n/numrange_fluent.cpp
index db1cdea..d9286d1 100644
--- a/icu4c/source/i18n/numrange_fluent.cpp
+++ b/icu4c/source/i18n/numrange_fluent.cpp
@@ -376,36 +376,4 @@
 }
 
 
-UPRV_FORMATTED_VALUE_SUBCLASS_AUTO_IMPL(FormattedNumberRange)
-
-#define UPRV_NOARG
-
-UnicodeString FormattedNumberRange::getFirstDecimal(UErrorCode& status) const {
-    UPRV_FORMATTED_VALUE_METHOD_GUARD(ICU_Utility::makeBogusString())
-    return fData->quantity1.toScientificString();
-}
-
-UnicodeString FormattedNumberRange::getSecondDecimal(UErrorCode& status) const {
-    UPRV_FORMATTED_VALUE_METHOD_GUARD(ICU_Utility::makeBogusString())
-    return fData->quantity2.toScientificString();
-}
-
-void FormattedNumberRange::getDecimalNumbers(ByteSink& sink1, ByteSink& sink2, UErrorCode& status) const {
-    UPRV_FORMATTED_VALUE_METHOD_GUARD(UPRV_NOARG)
-    impl::DecNum decnum1;
-    impl::DecNum decnum2;
-    fData->quantity1.toDecNum(decnum1, status).toString(sink1, status);
-    fData->quantity2.toDecNum(decnum2, status).toString(sink2, status);
-}
-
-UNumberRangeIdentityResult FormattedNumberRange::getIdentityResult(UErrorCode& status) const {
-    UPRV_FORMATTED_VALUE_METHOD_GUARD(UNUM_IDENTITY_RESULT_NOT_EQUAL)
-    return fData->identityResult;
-}
-
-
-UFormattedNumberRangeData::~UFormattedNumberRangeData() = default;
-
-
-
 #endif /* #if !UCONFIG_NO_FORMATTING */
diff --git a/icu4c/source/i18n/numrange_impl.cpp b/icu4c/source/i18n/numrange_impl.cpp
index 9fb3dee..3aae5c2 100644
--- a/icu4c/source/i18n/numrange_impl.cpp
+++ b/icu4c/source/i18n/numrange_impl.cpp
@@ -12,6 +12,7 @@
 #include "unicode/numberrangeformatter.h"
 #include "numrange_impl.h"
 #include "patternprops.h"
+#include "pluralranges.h"
 #include "uresimp.h"
 #include "util.h"
 
@@ -106,92 +107,9 @@
     sink.fillInDefaults(status);
 }
 
-class PluralRangesDataSink : public ResourceSink {
-  public:
-    PluralRangesDataSink(StandardPluralRanges& output) : fOutput(output) {}
-
-    void put(const char* /*key*/, ResourceValue& value, UBool /*noFallback*/, UErrorCode& status) U_OVERRIDE {
-        ResourceArray entriesArray = value.getArray(status);
-        if (U_FAILURE(status)) { return; }
-        fOutput.setCapacity(entriesArray.getSize());
-        for (int i = 0; entriesArray.getValue(i, value); i++) {
-            ResourceArray pluralFormsArray = value.getArray(status);
-            if (U_FAILURE(status)) { return; }
-            pluralFormsArray.getValue(0, value);
-            StandardPlural::Form first = StandardPlural::fromString(value.getUnicodeString(status), status);
-            if (U_FAILURE(status)) { return; }
-            pluralFormsArray.getValue(1, value);
-            StandardPlural::Form second = StandardPlural::fromString(value.getUnicodeString(status), status);
-            if (U_FAILURE(status)) { return; }
-            pluralFormsArray.getValue(2, value);
-            StandardPlural::Form result = StandardPlural::fromString(value.getUnicodeString(status), status);
-            if (U_FAILURE(status)) { return; }
-            fOutput.addPluralRange(first, second, result);
-        }
-    }
-
-  private:
-    StandardPluralRanges& fOutput;
-};
-
-void getPluralRangesData(const Locale& locale, StandardPluralRanges& output, UErrorCode& status) {
-    if (U_FAILURE(status)) { return; }
-    LocalUResourceBundlePointer rb(ures_openDirect(nullptr, "pluralRanges", &status));
-    if (U_FAILURE(status)) { return; }
-
-    CharString dataPath;
-    dataPath.append("locales/", -1, status);
-    dataPath.append(locale.getLanguage(), -1, status);
-    if (U_FAILURE(status)) { return; }
-    int32_t setLen;
-    // Not all languages are covered: fail gracefully
-    UErrorCode internalStatus = U_ZERO_ERROR;
-    const UChar* set = ures_getStringByKeyWithFallback(rb.getAlias(), dataPath.data(), &setLen, &internalStatus);
-    if (U_FAILURE(internalStatus)) { return; }
-
-    dataPath.clear();
-    dataPath.append("rules/", -1, status);
-    dataPath.appendInvariantChars(set, setLen, status);
-    if (U_FAILURE(status)) { return; }
-    PluralRangesDataSink sink(output);
-    ures_getAllItemsWithFallback(rb.getAlias(), dataPath.data(), sink, status);
-    if (U_FAILURE(status)) { return; }
-}
-
 } // namespace
 
 
-void StandardPluralRanges::initialize(const Locale& locale, UErrorCode& status) {
-    getPluralRangesData(locale, *this, status);
-}
-
-void StandardPluralRanges::addPluralRange(
-        StandardPlural::Form first,
-        StandardPlural::Form second,
-        StandardPlural::Form result) {
-    U_ASSERT(fTriplesLen < fTriples.getCapacity());
-    fTriples[fTriplesLen] = {first, second, result};
-    fTriplesLen++;
-}
-
-void StandardPluralRanges::setCapacity(int32_t length) {
-    if (length > fTriples.getCapacity()) {
-        fTriples.resize(length, 0);
-    }
-}
-
-StandardPlural::Form
-StandardPluralRanges::resolve(StandardPlural::Form first, StandardPlural::Form second) const {
-    for (int32_t i=0; i<fTriplesLen; i++) {
-        const auto& triple = fTriples[i];
-        if (triple.first == first && triple.second == second) {
-            return triple.result;
-        }
-    }
-    // Default fallback
-    return StandardPlural::OTHER;
-}
-
 
 NumberRangeFormatterImpl::NumberRangeFormatterImpl(const RangeMacroProps& macros, UErrorCode& status)
     : formatterImpl1(macros.formatter1.fMacros, status),
@@ -213,7 +131,7 @@
     fApproximatelyModifier = {data.approximatelyPattern, kUndefinedField, false};
 
     // TODO: Get locale from PluralRules instead?
-    fPluralRanges.initialize(macros.locale, status);
+    fPluralRanges = StandardPluralRanges::forLocale(macros.locale, status);
     if (U_FAILURE(status)) { return; }
 }
 
diff --git a/icu4c/source/i18n/numrange_impl.h b/icu4c/source/i18n/numrange_impl.h
index 8f4c8a4..b81a311 100644
--- a/icu4c/source/i18n/numrange_impl.h
+++ b/icu4c/source/i18n/numrange_impl.h
@@ -15,6 +15,7 @@
 #include "number_formatimpl.h"
 #include "formatted_string_builder.h"
 #include "formattedval_impl.h"
+#include "pluralranges.h"
 
 U_NAMESPACE_BEGIN namespace number {
 namespace impl {
@@ -40,36 +41,6 @@
 };
 
 
-class StandardPluralRanges : public UMemory {
-  public:
-    void initialize(const Locale& locale, UErrorCode& status);
-    StandardPlural::Form resolve(StandardPlural::Form first, StandardPlural::Form second) const;
-
-    /** Used for data loading. */
-    void addPluralRange(
-        StandardPlural::Form first,
-        StandardPlural::Form second,
-        StandardPlural::Form result);
-
-    /** Used for data loading. */
-    void setCapacity(int32_t length);
-
-  private:
-    struct StandardPluralRangeTriple {
-        StandardPlural::Form first;
-        StandardPlural::Form second;
-        StandardPlural::Form result;
-    };
-
-    // TODO: An array is simple here, but it results in linear lookup time.
-    // Certain locales have 20-30 entries in this list.
-    // Consider changing to a smarter data structure.
-    typedef MaybeStackArray<StandardPluralRangeTriple, 3> PluralRangeTriples;
-    PluralRangeTriples fTriples;
-    int32_t fTriplesLen = 0;
-};
-
-
 class NumberRangeFormatterImpl : public UMemory {
   public:
     NumberRangeFormatterImpl(const RangeMacroProps& macros, UErrorCode& status);
@@ -105,6 +76,11 @@
 };
 
 
+/** Helper function used in upluralrules.cpp */
+const UFormattedNumberRangeData* validateUFormattedNumberRange(
+    const UFormattedNumberRange* uresult, UErrorCode& status);
+
+
 } // namespace impl
 } // namespace number
 U_NAMESPACE_END
diff --git a/icu4c/source/i18n/pluralranges.cpp b/icu4c/source/i18n/pluralranges.cpp
new file mode 100644
index 0000000..da10e21
--- /dev/null
+++ b/icu4c/source/i18n/pluralranges.cpp
@@ -0,0 +1,144 @@
+// © 2018 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+#include "unicode/utypes.h"
+
+#if !UCONFIG_NO_FORMATTING
+
+// Allow implicit conversion from char16_t* to UnicodeString for this file:
+// Helpful in toString methods and elsewhere.
+#define UNISTR_FROM_STRING_EXPLICIT
+
+#include "unicode/numberrangeformatter.h"
+#include "pluralranges.h"
+#include "uresimp.h"
+#include "charstr.h"
+#include "uassert.h"
+#include "util.h"
+#include "numrange_impl.h"
+
+U_NAMESPACE_BEGIN
+
+
+namespace {
+
+class PluralRangesDataSink : public ResourceSink {
+  public:
+    PluralRangesDataSink(StandardPluralRanges& output) : fOutput(output) {}
+
+    void put(const char* /*key*/, ResourceValue& value, UBool /*noFallback*/, UErrorCode& status) U_OVERRIDE {
+        ResourceArray entriesArray = value.getArray(status);
+        if (U_FAILURE(status)) { return; }
+        fOutput.setCapacity(entriesArray.getSize(), status);
+        if (U_FAILURE(status)) { return; }
+        for (int i = 0; entriesArray.getValue(i, value); i++) {
+            ResourceArray pluralFormsArray = value.getArray(status);
+            if (U_FAILURE(status)) { return; }
+            if (pluralFormsArray.getSize() != 3) {
+                status = U_RESOURCE_TYPE_MISMATCH;
+                return;
+            }
+            pluralFormsArray.getValue(0, value);
+            StandardPlural::Form first = StandardPlural::fromString(value.getUnicodeString(status), status);
+            if (U_FAILURE(status)) { return; }
+            pluralFormsArray.getValue(1, value);
+            StandardPlural::Form second = StandardPlural::fromString(value.getUnicodeString(status), status);
+            if (U_FAILURE(status)) { return; }
+            pluralFormsArray.getValue(2, value);
+            StandardPlural::Form result = StandardPlural::fromString(value.getUnicodeString(status), status);
+            if (U_FAILURE(status)) { return; }
+            fOutput.addPluralRange(first, second, result);
+        }
+    }
+
+  private:
+    StandardPluralRanges& fOutput;
+};
+
+void getPluralRangesData(const Locale& locale, StandardPluralRanges& output, UErrorCode& status) {
+    LocalUResourceBundlePointer rb(ures_openDirect(nullptr, "pluralRanges", &status));
+    if (U_FAILURE(status)) { return; }
+
+    CharString dataPath;
+    dataPath.append("locales/", -1, status);
+    dataPath.append(locale.getLanguage(), -1, status);
+    if (U_FAILURE(status)) { return; }
+    int32_t setLen;
+    // Not all languages are covered: fail gracefully
+    UErrorCode internalStatus = U_ZERO_ERROR;
+    const UChar* set = ures_getStringByKeyWithFallback(rb.getAlias(), dataPath.data(), &setLen, &internalStatus);
+    if (U_FAILURE(internalStatus)) { return; }
+
+    dataPath.clear();
+    dataPath.append("rules/", -1, status);
+    dataPath.appendInvariantChars(set, setLen, status);
+    if (U_FAILURE(status)) { return; }
+    PluralRangesDataSink sink(output);
+    ures_getAllItemsWithFallback(rb.getAlias(), dataPath.data(), sink, status);
+}
+
+} // namespace
+
+
+StandardPluralRanges
+StandardPluralRanges::forLocale(const Locale& locale, UErrorCode& status) {
+    StandardPluralRanges result;
+    getPluralRangesData(locale, result, status);
+    return result;
+}
+
+StandardPluralRanges
+StandardPluralRanges::copy(UErrorCode& status) const {
+    StandardPluralRanges result;
+    if (fTriplesLen > result.fTriples.getCapacity()) {
+        if (result.fTriples.resize(fTriplesLen) == nullptr) {
+            status = U_MEMORY_ALLOCATION_ERROR;
+            return result;
+        }
+    }
+    uprv_memcpy(result.fTriples.getAlias(),
+        fTriples.getAlias(),
+        fTriplesLen * sizeof(fTriples[0]));
+    result.fTriplesLen = fTriplesLen;
+    return result;
+}
+
+LocalPointer<StandardPluralRanges>
+StandardPluralRanges::toPointer(UErrorCode& status) && noexcept {
+    return LocalPointer<StandardPluralRanges>(new StandardPluralRanges(std::move(*this)), status);
+}
+
+void StandardPluralRanges::addPluralRange(
+        StandardPlural::Form first,
+        StandardPlural::Form second,
+        StandardPlural::Form result) {
+    U_ASSERT(fTriplesLen < fTriples.getCapacity());
+    fTriples[fTriplesLen] = {first, second, result};
+    fTriplesLen++;
+}
+
+void StandardPluralRanges::setCapacity(int32_t length, UErrorCode& status) {
+    if (U_FAILURE(status)) { return; }
+    if (length > fTriples.getCapacity()) {
+        if (fTriples.resize(length, 0) == nullptr) {
+            status = U_MEMORY_ALLOCATION_ERROR;
+        }
+    }
+}
+
+StandardPlural::Form
+StandardPluralRanges::resolve(StandardPlural::Form first, StandardPlural::Form second) const {
+    for (int32_t i=0; i<fTriplesLen; i++) {
+        const auto& triple = fTriples[i];
+        if (triple.first == first && triple.second == second) {
+            return triple.result;
+        }
+    }
+    // Default fallback
+    return StandardPlural::OTHER;
+}
+
+
+U_NAMESPACE_END
+
+#endif /* #if !UCONFIG_NO_FORMATTING */
diff --git a/icu4c/source/i18n/pluralranges.h b/icu4c/source/i18n/pluralranges.h
new file mode 100644
index 0000000..eba59c7
--- /dev/null
+++ b/icu4c/source/i18n/pluralranges.h
@@ -0,0 +1,67 @@
+// © 2018 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+#ifndef __PLURALRANGES_H__
+#define __PLURALRANGES_H__
+
+#include "unicode/utypes.h"
+
+#if !UCONFIG_NO_FORMATTING
+
+#include "unicode/uobject.h"
+#include "unicode/locid.h"
+#include "unicode/plurrule.h"
+#include "standardplural.h"
+#include "cmemory.h"
+
+U_NAMESPACE_BEGIN
+
+// Forward declarations
+namespace number {
+namespace impl {
+class UFormattedNumberRangeData;
+}
+}
+
+class StandardPluralRanges : public UMemory {
+  public:
+    /** Create a new StandardPluralRanges for the given locale */
+    static StandardPluralRanges forLocale(const Locale& locale, UErrorCode& status);
+
+    /** Explicit copy constructor */
+    StandardPluralRanges copy(UErrorCode& status) const;
+
+    /** Create an object (called on an rvalue) */
+    LocalPointer<StandardPluralRanges> toPointer(UErrorCode& status) && noexcept;
+
+    /** Select rule based on the first and second forms */
+    StandardPlural::Form resolve(StandardPlural::Form first, StandardPlural::Form second) const;
+
+    /** Used for data loading. */
+    void addPluralRange(
+        StandardPlural::Form first,
+        StandardPlural::Form second,
+        StandardPlural::Form result);
+
+    /** Used for data loading. */
+    void setCapacity(int32_t length, UErrorCode& status);
+
+  private:
+    struct StandardPluralRangeTriple {
+        StandardPlural::Form first;
+        StandardPlural::Form second;
+        StandardPlural::Form result;
+    };
+
+    // TODO: An array is simple here, but it results in linear lookup time.
+    // Certain locales have 20-30 entries in this list.
+    // Consider changing to a smarter data structure.
+    typedef MaybeStackArray<StandardPluralRangeTriple, 3> PluralRangeTriples;
+    PluralRangeTriples fTriples;
+    int32_t fTriplesLen = 0;
+};
+
+U_NAMESPACE_END
+
+#endif /* #if !UCONFIG_NO_FORMATTING */
+#endif //__PLURALRANGES_H__
diff --git a/icu4c/source/i18n/plurrule.cpp b/icu4c/source/i18n/plurrule.cpp
index e7d5b5b..7ecf1bc 100644
--- a/icu4c/source/i18n/plurrule.cpp
+++ b/icu4c/source/i18n/plurrule.cpp
@@ -19,6 +19,7 @@
 #include "unicode/ures.h"
 #include "unicode/numfmt.h"
 #include "unicode/decimfmt.h"
+#include "unicode/numberrangeformatter.h"
 #include "charstr.h"
 #include "cmemory.h"
 #include "cstring.h"
@@ -36,6 +37,8 @@
 #include "unifiedcache.h"
 #include "number_decimalquantity.h"
 #include "util.h"
+#include "pluralranges.h"
+#include "numrange_impl.h"
 
 #if !UCONFIG_NO_FORMATTING
 
@@ -68,6 +71,7 @@
 PluralRules::PluralRules(UErrorCode& /*status*/)
 :   UObject(),
     mRules(nullptr),
+    mStandardPluralRanges(nullptr),
     mInternalStatus(U_ZERO_ERROR)
 {
 }
@@ -75,6 +79,7 @@
 PluralRules::PluralRules(const PluralRules& other)
 : UObject(other),
     mRules(nullptr),
+    mStandardPluralRanges(nullptr),
     mInternalStatus(U_ZERO_ERROR)
 {
     *this=other;
@@ -82,6 +87,7 @@
 
 PluralRules::~PluralRules() {
     delete mRules;
+    delete mStandardPluralRanges;
 }
 
 SharedPluralRules::~SharedPluralRules() {
@@ -90,14 +96,20 @@
 
 PluralRules*
 PluralRules::clone() const {
-    PluralRules* newObj = new PluralRules(*this);
     // Since clone doesn't have a 'status' parameter, the best we can do is return nullptr if
     // the newly created object was not fully constructed properly (an error occurred).
-    if (newObj != nullptr && U_FAILURE(newObj->mInternalStatus)) {
-        delete newObj;
-        newObj = nullptr;
+    UErrorCode localStatus = U_ZERO_ERROR;
+    return clone(localStatus);
+}
+
+PluralRules*
+PluralRules::clone(UErrorCode& status) const {
+    LocalPointer<PluralRules> newObj(new PluralRules(*this), status);
+    if (U_SUCCESS(status) && U_FAILURE(newObj->mInternalStatus)) {
+        status = newObj->mInternalStatus;
+        newObj.adoptInstead(nullptr);
     }
-    return newObj;
+    return newObj.orphan();
 }
 
 PluralRules&
@@ -105,6 +117,8 @@
     if (this != &other) {
         delete mRules;
         mRules = nullptr;
+        delete mStandardPluralRanges;
+        mStandardPluralRanges = nullptr;
         mInternalStatus = other.mInternalStatus;
         if (U_FAILURE(mInternalStatus)) {
             // bail out early if the object we were copying from was already 'invalid'.
@@ -120,6 +134,11 @@
                 mInternalStatus = mRules->fInternalStatus;
             }
         }
+        if (other.mStandardPluralRanges != nullptr) {
+            mStandardPluralRanges = other.mStandardPluralRanges->copy(mInternalStatus)
+                .toPointer(mInternalStatus)
+                .orphan();
+        }
     }
     return *this;
 }
@@ -212,11 +231,8 @@
     if (U_FAILURE(status)) {
         return nullptr;
     }
-    PluralRules *result = (*shared)->clone();
+    PluralRules *result = (*shared)->clone(status);
     shared->removeRef();
-    if (result == nullptr) {
-        status = U_MEMORY_ALLOCATION_ERROR;
-    }
     return result;
 }
 
@@ -253,6 +269,10 @@
         //        Original impl used default rules.
         //        Ask the question to ICU Core.
 
+    newObj->mStandardPluralRanges = StandardPluralRanges::forLocale(locale, status)
+        .toPointer(status)
+        .orphan();
+
     return newObj.orphan();
 }
 
@@ -273,6 +293,10 @@
     if (U_FAILURE(status)) {
         return ICU_Utility::makeBogusString();
     }
+    if (U_FAILURE(mInternalStatus)) {
+        status = mInternalStatus;
+        return ICU_Utility::makeBogusString();
+    }
     return select(dq);
 }
 
@@ -286,6 +310,33 @@
     }
 }
 
+UnicodeString
+PluralRules::select(const number::FormattedNumberRange& range, UErrorCode& status) const {
+    return select(range.getData(status), status);
+}
+
+UnicodeString
+PluralRules::select(const number::impl::UFormattedNumberRangeData* impl, UErrorCode& status) const {
+    if (U_FAILURE(status)) {
+        return ICU_Utility::makeBogusString();
+    }
+    if (U_FAILURE(mInternalStatus)) {
+        status = mInternalStatus;
+        return ICU_Utility::makeBogusString();
+    }
+    if (mStandardPluralRanges == nullptr) {
+        // Happens if PluralRules was constructed via createRules()
+        status = U_UNSUPPORTED_ERROR;
+        return ICU_Utility::makeBogusString();
+    }
+    auto form1 = StandardPlural::fromString(select(impl->quantity1), status);
+    auto form2 = StandardPlural::fromString(select(impl->quantity2), status);
+    if (U_FAILURE(status)) {
+        return ICU_Utility::makeBogusString();
+    }
+    auto result = mStandardPluralRanges->resolve(form1, form2);
+    return UnicodeString(StandardPlural::getKeyword(result), -1, US_INV);
+}
 
 
 StringEnumeration*
diff --git a/icu4c/source/i18n/sources.txt b/icu4c/source/i18n/sources.txt
index b26485d..b28f34c 100644
--- a/icu4c/source/i18n/sources.txt
+++ b/icu4c/source/i18n/sources.txt
@@ -140,6 +140,7 @@
 numsys.cpp
 olsontz.cpp
 persncal.cpp
+pluralranges.cpp
 plurfmt.cpp
 plurrule.cpp
 quant.cpp
diff --git a/icu4c/source/i18n/unicode/numberrangeformatter.h b/icu4c/source/i18n/unicode/numberrangeformatter.h
index e92db1d..4e0a15b 100644
--- a/icu4c/source/i18n/unicode/numberrangeformatter.h
+++ b/icu4c/source/i18n/unicode/numberrangeformatter.h
@@ -47,6 +47,9 @@
 
 U_NAMESPACE_BEGIN
 
+// Forward declarations:
+class PluralRules;
+
 namespace number {  // icu::number
 
 // Forward declarations:
@@ -733,6 +736,11 @@
 
     void getDecimalNumbers(ByteSink& sink1, ByteSink& sink2, UErrorCode& status) const;
 
+    const impl::UFormattedNumberRangeData* getData(UErrorCode& status) const;
+
+    // To allow PluralRules to access the underlying data
+    friend class ::icu::PluralRules;
+
     // To give LocalizedNumberRangeFormatter format methods access to this class's constructor:
     friend class LocalizedNumberRangeFormatter;
 
diff --git a/icu4c/source/i18n/unicode/plurrule.h b/icu4c/source/i18n/unicode/plurrule.h
index a92d4cb..dd53223 100644
--- a/icu4c/source/i18n/unicode/plurrule.h
+++ b/icu4c/source/i18n/unicode/plurrule.h
@@ -51,9 +51,14 @@
 class PluralKeywordEnumeration;
 class AndConstraint;
 class SharedPluralRules;
+class StandardPluralRanges;
 
 namespace number {
 class FormattedNumber;
+class FormattedNumberRange;
+namespace impl {
+class UFormattedNumberRangeData;
+}
 }
 
 /**
@@ -367,11 +372,35 @@
      */
     UnicodeString select(const number::FormattedNumber& number, UErrorCode& status) const;
 
+#ifndef U_HIDE_DRAFT_API
+    /**
+     * Given a formatted number range, returns the overall plural form of the
+     * range. For example, "3-5" returns "other" in English.
+     *
+     * To get a FormattedNumberRange, see NumberRangeFormatter.
+     * 
+     * This method only works if PluralRules was created with a locale. If it was created
+     * from PluralRules::createRules(), this method sets status code U_UNSUPPORTED_ERROR.
+     * 
+     * @param range  The number range onto which the rules will be applied.
+     * @param status Set if an error occurs while selecting plural keyword.
+     *               This could happen if the FormattedNumberRange is invalid,
+     *               or if plural ranges data is unavailable.
+     * @return       The keyword of the selected rule.
+     * @draft ICU 68
+     */
+    UnicodeString select(const number::FormattedNumberRange& range, UErrorCode& status) const;
+#endif // U_HIDE_DRAFT_API
+
 #ifndef U_HIDE_INTERNAL_API
     /**
-      * @internal
-      */
+     * @internal
+     */
     UnicodeString select(const IFixedDecimal &number) const;
+    /**
+     * @internal
+     */
+    UnicodeString select(const number::impl::UFormattedNumberRangeData* urange, UErrorCode& status) const;
 #endif  /* U_HIDE_INTERNAL_API */
 
     /**
@@ -513,12 +542,14 @@
 
 private:
     RuleChain  *mRules;
+    StandardPluralRanges *mStandardPluralRanges;
 
     PluralRules();   // default constructor not implemented
     void            parseDescription(const UnicodeString& ruleData, UErrorCode &status);
     int32_t         getNumberValue(const UnicodeString& token) const;
     UnicodeString   getRuleFromResource(const Locale& locale, UPluralType type, UErrorCode& status);
     RuleChain      *rulesForKeyword(const UnicodeString &keyword) const;
+    PluralRules    *clone(UErrorCode& status) const;
 
     /**
     * An internal status variable used to indicate that the object is in an 'invalid' state.
diff --git a/icu4c/source/i18n/unicode/upluralrules.h b/icu4c/source/i18n/unicode/upluralrules.h
index a94c9d9..71a45dd 100644
--- a/icu4c/source/i18n/unicode/upluralrules.h
+++ b/icu4c/source/i18n/unicode/upluralrules.h
@@ -26,6 +26,7 @@
 
 // Forward-declaration
 struct UFormattedNumber;
+struct UFormattedNumberRange;
 
 /**
  * \file
@@ -167,7 +168,7 @@
  * @param uplrules The UPluralRules object specifying the rules.
  * @param number The formatted number for which the rule has to be determined.
  * @param keyword The destination buffer for the keyword of the rule that
- *         applies to number.
+ *         applies to the number.
  * @param capacity The capacity of the keyword buffer.
  * @param status A pointer to a UErrorCode to receive any errors.
  * @return The length of the keyword.
@@ -179,6 +180,29 @@
                UChar *keyword, int32_t capacity,
                UErrorCode *status);
 
+#ifndef U_HIDE_DRAFT_API
+/**
+ * Given a formatted number range, returns the overall plural form of the
+ * range. For example, "3-5" returns "other" in English.
+ *
+ * To get a UFormattedNumberRange, see UNumberRangeFormatter.
+ *
+ * @param uplrules The UPluralRules object specifying the rules.
+ * @param urange The number range onto which the rules will be applied.
+ * @param keyword The destination buffer for the keyword of the rule that
+ *         applies to the number range.
+ * @param capacity The capacity of the keyword buffer.
+ * @param status A pointer to a UErrorCode to receive any errors.
+ * @return The length of the keyword.
+ * @draft ICU 68
+ */
+U_CAPI int32_t U_EXPORT2
+uplrules_selectForRange(const UPluralRules *uplrules,
+               const struct UFormattedNumberRange* urange,
+               UChar *keyword, int32_t capacity,
+               UErrorCode *status);
+#endif // U_HIDE_DRAFT_API
+
 #ifndef U_HIDE_INTERNAL_API
 /**
  * Given a number, returns the keyword of the first rule that applies to the
diff --git a/icu4c/source/i18n/upluralrules.cpp b/icu4c/source/i18n/upluralrules.cpp
index 5119257..73e59a7 100644
--- a/icu4c/source/i18n/upluralrules.cpp
+++ b/icu4c/source/i18n/upluralrules.cpp
@@ -20,6 +20,7 @@
 #include "unicode/unumberformatter.h"
 #include "number_decimalquantity.h"
 #include "number_utypes.h"
+#include "numrange_impl.h"
 
 U_NAMESPACE_USE
 
@@ -116,6 +117,25 @@
 }
 
 U_CAPI int32_t U_EXPORT2
+uplrules_selectForRange(const UPluralRules *uplrules,
+                const UFormattedNumberRange* urange,
+                UChar *keyword, int32_t capacity,
+                UErrorCode *status)
+{
+    if (U_FAILURE(*status)) {
+        return 0;
+    }
+    if (keyword == NULL ? capacity != 0 : capacity < 0) {
+        *status = U_ILLEGAL_ARGUMENT_ERROR;
+        return 0;
+    }
+    const number::impl::UFormattedNumberRangeData* impl =
+        number::impl::validateUFormattedNumberRange(urange, *status);
+    UnicodeString result = ((PluralRules*)uplrules)->select(impl, *status);
+    return result.extract(keyword, capacity, *status);
+}
+
+U_CAPI int32_t U_EXPORT2
 uplrules_selectWithFormat(const UPluralRules *uplrules,
                           double number,
                           const UNumberFormat *fmt,
diff --git a/icu4c/source/test/cintltst/cpluralrulestest.c b/icu4c/source/test/cintltst/cpluralrulestest.c
index b0ac873..e695154 100644
--- a/icu4c/source/test/cintltst/cpluralrulestest.c
+++ b/icu4c/source/test/cintltst/cpluralrulestest.c
@@ -14,6 +14,7 @@
 #include "unicode/ustring.h"
 #include "unicode/uenum.h"
 #include "unicode/unumberformatter.h"
+#include "unicode/unumberrangeformatter.h"
 #include "cintltst.h"
 #include "cmemory.h"
 #include "cstring.h"
@@ -22,6 +23,7 @@
 static void TestOrdinalRules(void);
 static void TestGetKeywords(void);
 static void TestFormatted(void);
+static void TestSelectRange(void);
 
 void addPluralRulesTest(TestNode** root);
 
@@ -33,6 +35,7 @@
     TESTCASE(TestOrdinalRules);
     TESTCASE(TestGetKeywords);
     TESTCASE(TestFormatted);
+    TESTCASE(TestSelectRange);
 }
 
 typedef struct {
@@ -295,4 +298,56 @@
     unumf_closeResult(uresult);
 }
 
+static void TestSelectRange() {
+    UErrorCode ec = U_ZERO_ERROR;
+    UNumberRangeFormatter* unumrf = NULL;
+    UFormattedNumberRange* uresult = NULL;
+    UPluralRules* uplrules = NULL;
+
+    int32_t d1 = 102;
+    int32_t d2 = 201;
+
+    // Locale sl has interesting data: one + two => few
+    uplrules = uplrules_open("sl", &ec);
+    if (!assertSuccess("open plural rules", &ec)) {
+        goto cleanup;
+    }
+
+    unumrf = unumrf_openForSkeletonWithCollapseAndIdentityFallback(
+        u"",
+        0,
+        UNUM_RANGE_COLLAPSE_AUTO,
+        UNUM_IDENTITY_FALLBACK_APPROXIMATELY,
+        "sl",
+        NULL,
+        &ec);
+    if (!assertSuccess("open unumrf", &ec)) {
+        goto cleanup;
+    }
+
+    uresult = unumrf_openResult(&ec);
+    if (!assertSuccess("open result", &ec)) {
+        goto cleanup;
+    }
+
+    unumrf_formatDoubleRange(unumrf, d1, d2, uresult, &ec);
+    if (!assertSuccess("format", &ec)) {
+        goto cleanup;
+    }
+
+    UChar buffer[40];
+    int32_t len = uplrules_selectForRange(uplrules, uresult, buffer, 40, &ec);
+    if (!assertSuccess("select", &ec)) {
+        goto cleanup;
+    }
+
+    assertUEquals("102-201 is plural category 'few' in sl", u"few", buffer);
+    assertIntEquals("Length should be as expected", u_strlen(buffer), len);
+
+cleanup:
+    uplrules_close(uplrules);
+    unumrf_close(unumrf);
+    unumrf_closeResult(uresult);
+}
+
 #endif /* #if !UCONFIG_NO_FORMATTING */
diff --git a/icu4c/source/test/depstest/dependencies.txt b/icu4c/source/test/depstest/dependencies.txt
index bee3766..fda37de 100644
--- a/icu4c/source/test/depstest/dependencies.txt
+++ b/icu4c/source/test/depstest/dependencies.txt
@@ -974,7 +974,7 @@
 group: number_output
     # PluralRules and FormattedNumber
     number_output.o
-    standardplural.o plurrule.o
+    standardplural.o plurrule.o pluralranges.o
   deps
     # FormattedNumber internals:
     number_representation format formatted_value_sbimpl units
diff --git a/icu4c/source/test/intltest/plurults.cpp b/icu4c/source/test/intltest/plurults.cpp
index b8a6c77..10f5251 100644
--- a/icu4c/source/test/intltest/plurults.cpp
+++ b/icu4c/source/test/intltest/plurults.cpp
@@ -23,6 +23,7 @@
 #include "unicode/plurrule.h"
 #include "unicode/stringpiece.h"
 #include "unicode/numberformatter.h"
+#include "unicode/numberrangeformatter.h"
 
 #include "cmemory.h"
 #include "plurrule_impl.h"
@@ -53,6 +54,7 @@
     TESTCASE_AUTO(testCompactDecimalPluralKeyword);
     TESTCASE_AUTO(testOrdinal);
     TESTCASE_AUTO(testSelect);
+    TESTCASE_AUTO(testSelectRange);
     TESTCASE_AUTO(testAvailbleLocales);
     TESTCASE_AUTO(testParseErrors);
     TESTCASE_AUTO(testFixedDecimal);
@@ -925,6 +927,50 @@
 }
 
 
+void PluralRulesTest::testSelectRange() {
+    IcuTestErrorCode status(*this, "testSelectRange");
+
+    int32_t d1 = 102;
+    int32_t d2 = 201;
+    Locale locale("sl");
+
+    // Locale sl has interesting data: one + two => few
+    auto range = NumberRangeFormatter::withLocale(locale).formatFormattableRange(d1, d2, status);
+    auto rules = LocalPointer<PluralRules>(PluralRules::forLocale(locale, status), status);
+    if (status.errIfFailureAndReset()) {
+        return;
+    }
+
+    // For testing: get plural form of first and second numbers
+    auto a = NumberFormatter::withLocale(locale).formatDouble(d1, status);
+    auto b = NumberFormatter::withLocale(locale).formatDouble(d2, status);
+    assertEquals("First plural", u"two", rules->select(a, status));
+    assertEquals("Second plural", u"one", rules->select(b, status));
+
+    // Check the range plural now:
+    auto form = rules->select(range, status);
+    assertEquals("Range plural", u"few", form);
+
+    // Test after copying:
+    PluralRules copy(*rules);
+    form = copy.select(range, status);
+    assertEquals("Range plural after copying", u"few", form);
+
+    // Test when plural ranges data is unavailable:
+    auto bare = LocalPointer<PluralRules>(
+        PluralRules::createRules(u"a: i = 0,1", status), status);
+    if (status.errIfFailureAndReset()) { return; }
+    form = bare->select(range, status);
+    status.expectErrorAndReset(U_UNSUPPORTED_ERROR);
+
+    // However, they should not set an error when no data is available for a language.
+    auto xyz = LocalPointer<PluralRules>(
+        PluralRules::forLocale("xyz", status));
+    form = xyz->select(range, status);
+    assertEquals("Fallback form", u"other", form);
+}
+
+
 void PluralRulesTest::testAvailbleLocales() {
     
     // Hash set of (char *) strings.
diff --git a/icu4c/source/test/intltest/plurults.h b/icu4c/source/test/intltest/plurults.h
index 301f852..2d7920d 100644
--- a/icu4c/source/test/intltest/plurults.h
+++ b/icu4c/source/test/intltest/plurults.h
@@ -35,6 +35,7 @@
     void testCompactDecimalPluralKeyword();
     void testOrdinal();
     void testSelect();
+    void testSelectRange();
     void testAvailbleLocales();
     void testParseErrors();
     void testFixedDecimal();
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/PluralRulesLoader.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/PluralRulesLoader.java
index c01d5be..e4d073a 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/PluralRulesLoader.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/PluralRulesLoader.java
@@ -18,7 +18,7 @@
 import java.util.Set;
 import java.util.TreeMap;
 
-import com.ibm.icu.text.PluralRanges;
+import com.ibm.icu.impl.number.range.StandardPluralRanges;
 import com.ibm.icu.text.PluralRules;
 import com.ibm.icu.text.PluralRules.PluralType;
 import com.ibm.icu.util.ULocale;
@@ -28,19 +28,19 @@
  * Loader for plural rules data.
  */
 public class PluralRulesLoader extends PluralRules.Factory {
-    private final Map<String, PluralRules> rulesIdToRules;
+    // Key is rules set + ranges set
+    private final Map<String, PluralRules> pluralRulesCache;
     // lazy init, use getLocaleIdToRulesIdMap to access
     private Map<String, String> localeIdToCardinalRulesId;
     private Map<String, String> localeIdToOrdinalRulesId;
     private Map<String, ULocale> rulesIdToEquivalentULocale;
-    private static Map<String, PluralRanges> localeIdToPluralRanges;
 
 
     /**
      * Access through singleton.
      */
     private PluralRulesLoader() {
-        rulesIdToRules = new HashMap<String, PluralRules>();
+        pluralRulesCache = new HashMap<String, PluralRules>();
     }
 
     /**
@@ -179,14 +179,20 @@
      * Gets the rule from the rulesId. If there is no rule for this rulesId,
      * return null.
      */
-    public PluralRules getRulesForRulesId(String rulesId) {
+    public PluralRules getOrCreateRulesForLocale(ULocale locale, PluralRules.PluralType type) {
+        String rulesId = getRulesIdForLocale(locale, type);
+        if (rulesId == null || rulesId.trim().length() == 0) {
+            return null;
+        }
+        String rangesId = StandardPluralRanges.getSetForLocale(locale);
+        String cacheKey = rulesId + "/" + rangesId; // could end with "/null" (this is OK)
         // synchronize on the map.  release the lock temporarily while we build the rules.
         PluralRules rules = null;
         boolean hasRules;  // Separate boolean because stored rules can be null.
-        synchronized (rulesIdToRules) {
-            hasRules = rulesIdToRules.containsKey(rulesId);
+        synchronized (pluralRulesCache) {
+            hasRules = pluralRulesCache.containsKey(cacheKey);
             if (hasRules) {
-                rules = rulesIdToRules.get(rulesId);  // can be null
+                rules = pluralRulesCache.get(cacheKey);  // can be null
             }
         }
         if (!hasRules) {
@@ -205,15 +211,16 @@
                     sb.append(": ");
                     sb.append(b.getString());
                 }
-                rules = PluralRules.parseDescription(sb.toString());
+                StandardPluralRanges ranges = StandardPluralRanges.forSet(rangesId);
+                rules = PluralRules.newInternal(sb.toString(), ranges);
             } catch (ParseException e) {
             } catch (MissingResourceException e) {
             }
-            synchronized (rulesIdToRules) {
-                if (rulesIdToRules.containsKey(rulesId)) {
-                    rules = rulesIdToRules.get(rulesId);
+            synchronized (pluralRulesCache) {
+                if (pluralRulesCache.containsKey(cacheKey)) {
+                    rules = pluralRulesCache.get(cacheKey);
                 } else {
-                    rulesIdToRules.put(rulesId, rules);  // can be null
+                    pluralRulesCache.put(cacheKey, rules);  // can be null
                 }
             }
         }
@@ -235,11 +242,7 @@
      * com.ibm.icu.text.PluralRules.DEFAULT is returned.
      */
     public PluralRules forLocale(ULocale locale, PluralRules.PluralType type) {
-        String rulesId = getRulesIdForLocale(locale, type);
-        if (rulesId == null || rulesId.trim().length() == 0) {
-            return PluralRules.DEFAULT;
-        }
-        PluralRules rules = getRulesForRulesId(rulesId);
+        PluralRules rules = getOrCreateRulesForLocale(locale, type);
         if (rules == null) {
             rules = PluralRules.DEFAULT;
         }
@@ -258,245 +261,4 @@
     public boolean hasOverride(ULocale locale) {
         return false;
     }
-    
-    private static final PluralRanges UNKNOWN_RANGE = new PluralRanges().freeze();
-
-    public PluralRanges getPluralRanges(ULocale locale) {
-        // TODO markdavis Fix the bad fallback, here and elsewhere in this file.
-        String localeId = ULocale.canonicalize(locale.getBaseName());
-        PluralRanges result;
-        while (null == (result = localeIdToPluralRanges.get(localeId))) {
-            int ix = localeId.lastIndexOf("_");
-            if (ix == -1) {
-                result = UNKNOWN_RANGE;
-                break;
-            }
-            localeId = localeId.substring(0, ix);
-        }
-        return result;
-    }
-
-    public boolean isPluralRangesAvailable(ULocale locale) {
-        return getPluralRanges(locale) == UNKNOWN_RANGE;
-    }
-
-    // TODO markdavis FIX HARD-CODED HACK once we have data from CLDR in the bundles
-    static {
-        String[][] pluralRangeData = {
-                {"locales", "id ja km ko lo ms my th vi zh"},
-                {"other", "other", "other"},
-
-                {"locales", "am bn fr gu hi hy kn mr pa zu"},
-                {"one", "one", "one"},
-                {"one", "other", "other"},
-                {"other", "other", "other"},
-
-                {"locales", "fa"},
-                {"one", "one", "other"},
-                {"one", "other", "other"},
-                {"other", "other", "other"},
-
-                {"locales", "ka"},
-                {"one", "other", "one"},
-                {"other", "one", "other"},
-                {"other", "other", "other"},
-
-                {"locales", "az de el gl hu it kk ky ml mn ne nl pt sq sw ta te tr ug uz"},
-                {"one", "other", "other"},
-                {"other", "one", "one"},
-                {"other", "other", "other"},
-
-                {"locales", "af bg ca en es et eu fi nb sv ur"},
-                {"one", "other", "other"},
-                {"other", "one", "other"},
-                {"other", "other", "other"},
-
-                {"locales", "da fil is"},
-                {"one", "one", "one"},
-                {"one", "other", "other"},
-                {"other", "one", "one"},
-                {"other", "other", "other"},
-
-                {"locales", "si"},
-                {"one", "one", "one"},
-                {"one", "other", "other"},
-                {"other", "one", "other"},
-                {"other", "other", "other"},
-
-                {"locales", "mk"},
-                {"one", "one", "other"},
-                {"one", "other", "other"},
-                {"other", "one", "other"},
-                {"other", "other", "other"},
-
-                {"locales", "lv"},
-                {"zero", "zero", "other"},
-                {"zero", "one", "one"},
-                {"zero", "other", "other"},
-                {"one", "zero", "other"},
-                {"one", "one", "one"},
-                {"one", "other", "other"},
-                {"other", "zero", "other"},
-                {"other", "one", "one"},
-                {"other", "other", "other"},
-
-                {"locales", "ro"},
-                {"one", "few", "few"},
-                {"one", "other", "other"},
-                {"few", "one", "few"},
-                {"few", "few", "few"},
-                {"few", "other", "other"},
-                {"other", "few", "few"},
-                {"other", "other", "other"},
-
-                {"locales", "hr sr bs"},
-                {"one", "one", "one"},
-                {"one", "few", "few"},
-                {"one", "other", "other"},
-                {"few", "one", "one"},
-                {"few", "few", "few"},
-                {"few", "other", "other"},
-                {"other", "one", "one"},
-                {"other", "few", "few"},
-                {"other", "other", "other"},
-
-                {"locales", "sl"},
-                {"one", "one", "few"},
-                {"one", "two", "two"},
-                {"one", "few", "few"},
-                {"one", "other", "other"},
-                {"two", "one", "few"},
-                {"two", "two", "two"},
-                {"two", "few", "few"},
-                {"two", "other", "other"},
-                {"few", "one", "few"},
-                {"few", "two", "two"},
-                {"few", "few", "few"},
-                {"few", "other", "other"},
-                {"other", "one", "few"},
-                {"other", "two", "two"},
-                {"other", "few", "few"},
-                {"other", "other", "other"},
-
-                {"locales", "he"},
-                {"one", "two", "other"},
-                {"one", "many", "many"},
-                {"one", "other", "other"},
-                {"two", "many", "other"},
-                {"two", "other", "other"},
-                {"many", "many", "many"},
-                {"many", "other", "many"},
-                {"other", "one", "other"},
-                {"other", "two", "other"},
-                {"other", "many", "many"},
-                {"other", "other", "other"},
-
-                {"locales", "cs pl sk"},
-                {"one", "few", "few"},
-                {"one", "many", "many"},
-                {"one", "other", "other"},
-                {"few", "few", "few"},
-                {"few", "many", "many"},
-                {"few", "other", "other"},
-                {"many", "one", "one"},
-                {"many", "few", "few"},
-                {"many", "many", "many"},
-                {"many", "other", "other"},
-                {"other", "one", "one"},
-                {"other", "few", "few"},
-                {"other", "many", "many"},
-                {"other", "other", "other"},
-
-                {"locales", "lt ru uk"},
-                {"one", "one", "one"},
-                {"one", "few", "few"},
-                {"one", "many", "many"},
-                {"one", "other", "other"},
-                {"few", "one", "one"},
-                {"few", "few", "few"},
-                {"few", "many", "many"},
-                {"few", "other", "other"},
-                {"many", "one", "one"},
-                {"many", "few", "few"},
-                {"many", "many", "many"},
-                {"many", "other", "other"},
-                {"other", "one", "one"},
-                {"other", "few", "few"},
-                {"other", "many", "many"},
-                {"other", "other", "other"},
-
-                {"locales", "cy"},
-                {"zero", "one", "one"},
-                {"zero", "two", "two"},
-                {"zero", "few", "few"},
-                {"zero", "many", "many"},
-                {"zero", "other", "other"},
-                {"one", "two", "two"},
-                {"one", "few", "few"},
-                {"one", "many", "many"},
-                {"one", "other", "other"},
-                {"two", "few", "few"},
-                {"two", "many", "many"},
-                {"two", "other", "other"},
-                {"few", "many", "many"},
-                {"few", "other", "other"},
-                {"many", "other", "other"},
-                {"other", "one", "one"},
-                {"other", "two", "two"},
-                {"other", "few", "few"},
-                {"other", "many", "many"},
-                {"other", "other", "other"},
-
-                {"locales", "ar"},
-                {"zero", "one", "zero"},
-                {"zero", "two", "zero"},
-                {"zero", "few", "few"},
-                {"zero", "many", "many"},
-                {"zero", "other", "other"},
-                {"one", "two", "other"},
-                {"one", "few", "few"},
-                {"one", "many", "many"},
-                {"one", "other", "other"},
-                {"two", "few", "few"},
-                {"two", "many", "many"},
-                {"two", "other", "other"},
-                {"few", "few", "few"},
-                {"few", "many", "many"},
-                {"few", "other", "other"},
-                {"many", "few", "few"},
-                {"many", "many", "many"},
-                {"many", "other", "other"},
-                {"other", "one", "other"},
-                {"other", "two", "other"},
-                {"other", "few", "few"},
-                {"other", "many", "many"},
-                {"other", "other", "other"},     
-        };
-        PluralRanges pr = null;
-        String[] locales = null;
-        HashMap<String, PluralRanges> tempLocaleIdToPluralRanges = new HashMap<String, PluralRanges>();
-        for (String[] row : pluralRangeData) {
-            if (row[0].equals("locales")) {
-                if (pr != null) {
-                    pr.freeze();
-                    for (String locale : locales) {
-                        tempLocaleIdToPluralRanges.put(locale, pr);
-                    }
-                }
-                locales = row[1].split(" ");
-                pr = new PluralRanges();
-            } else {
-                pr.add(
-                        StandardPlural.fromString(row[0]),
-                        StandardPlural.fromString(row[1]),
-                        StandardPlural.fromString(row[2]));
-            }
-        }
-        // do last one
-        for (String locale : locales) {
-            tempLocaleIdToPluralRanges.put(locale, pr);
-        }
-        // now make whole thing immutable
-        localeIdToPluralRanges = Collections.unmodifiableMap(tempLocaleIdToPluralRanges);
-    }
 }
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/StandardPluralRanges.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/StandardPluralRanges.java
index 3fb8119..78c5e7a 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/StandardPluralRanges.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/StandardPluralRanges.java
@@ -2,7 +2,9 @@
 // License & terms of use: http://www.unicode.org/copyright.html
 package com.ibm.icu.impl.number.range;
 
-import java.util.MissingResourceException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 import com.ibm.icu.impl.ICUData;
 import com.ibm.icu.impl.ICUResourceBundle;
@@ -10,6 +12,7 @@
 import com.ibm.icu.impl.UResource;
 import com.ibm.icu.util.ULocale;
 import com.ibm.icu.util.UResourceBundle;
+import com.ibm.icu.util.UResourceTypeMismatchException;
 
 /**
  * @author sffc
@@ -20,8 +23,54 @@
     StandardPlural[] flatTriples;
     int numTriples = 0;
 
+    /**
+     * An immutable map from language codes to set IDs.
+     * Pre-computed and cached in Java since it is used as a cache key for PluralRules.
+     */
+    private static volatile Map<String, String> languageToSet;
+
+    /** An empty StandardPluralRanges instance. */
+    public static final StandardPluralRanges DEFAULT = new StandardPluralRanges();
+
     ////////////////////
 
+    private static final class PluralRangeSetsDataSink extends UResource.Sink {
+
+        Map<String, String> output;
+
+        PluralRangeSetsDataSink(Map<String, String> output) {
+            this.output = output;
+        }
+
+        @Override
+        public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+            UResource.Table table = value.getTable();
+            for (int i = 0; table.getKeyAndValue(i, key, value); ++i) {
+                // The data has only languages; no regions/scripts. If this changes, this
+                // code and languageToSet will need to change.
+                assert key.toString().equals(new ULocale(key.toString()).getLanguage());
+                output.put(key.toString(), value.toString());
+            }
+        }
+    }
+
+    private static Map<String, String> getLanguageToSet() {
+        Map<String, String> candidate = languageToSet;
+        if (candidate == null) {
+            Map<String, String> map = new HashMap<String, String>();
+            PluralRangeSetsDataSink sink = new PluralRangeSetsDataSink(map);
+            ICUResourceBundle resource = (ICUResourceBundle)
+                UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "pluralRanges");
+            resource.getAllItemsWithFallback("locales", sink);
+            candidate = Collections.unmodifiableMap(map);
+        }
+        // Check if another thread set languageToSet in the mean time
+        if (languageToSet == null) {
+            languageToSet = candidate;
+        }
+        return languageToSet;
+    }
+
     private static final class PluralRangesDataSink extends UResource.Sink {
 
         StandardPluralRanges output;
@@ -36,6 +85,10 @@
             output.setCapacity(entriesArray.getSize());
             for (int i = 0; entriesArray.getValue(i, value); ++i) {
                 UResource.Array pluralFormsArray = value.getArray();
+                if (pluralFormsArray.getSize() != 3) {
+                    throw new UResourceTypeMismatchException(
+                        "Expected 3 elements in pluralRanges.txt array");
+                }
                 pluralFormsArray.getValue(0, value);
                 StandardPlural first = StandardPlural.fromString(value.getString());
                 pluralFormsArray.getValue(1, value);
@@ -48,34 +101,43 @@
     }
 
     private static void getPluralRangesData(
-            ULocale locale,
+            String set,
             StandardPluralRanges out) {
         StringBuilder sb = new StringBuilder();
         ICUResourceBundle resource;
         resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "pluralRanges");
-        sb.append("locales/");
-        sb.append(locale.getLanguage());
-        String key = sb.toString();
-        String set;
-        try {
-            set = resource.getStringWithFallback(key);
-        } catch (MissingResourceException e) {
-            // Not all languages are covered: fail gracefully
-            return;
-        }
-
         sb.setLength(0);
         sb.append("rules/");
         sb.append(set);
-        key = sb.toString();
+        String key = sb.toString();
         PluralRangesDataSink sink = new PluralRangesDataSink(out);
         resource.getAllItemsWithFallback(key, sink);
     }
 
     ////////////////////
 
-    public StandardPluralRanges(ULocale locale) {
-        getPluralRangesData(locale, this);
+    /** Create a StandardPluralRanges based on locale. */
+    public static StandardPluralRanges forLocale(ULocale locale) {
+        return forSet(getSetForLocale(locale));
+    }
+
+    /** Create a StandardPluralRanges based on set name. */
+    public static StandardPluralRanges forSet(String set) {
+        StandardPluralRanges result = new StandardPluralRanges();
+        if (set == null) {
+            // Not all languages are covered: fail gracefully
+            return DEFAULT;
+        }
+        getPluralRangesData(set, result);
+        return result;
+    }
+
+    /** Get the set name from the locale. */
+    public static String getSetForLocale(ULocale locale) {
+        return getLanguageToSet().get(locale.getLanguage());
+    }
+
+    private StandardPluralRanges() {
     }
 
     /** Used for data loading. */
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java
index 9180589..90d23c3 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java
@@ -13,6 +13,7 @@
 import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult;
 import com.ibm.icu.text.ConstrainedFieldPosition;
 import com.ibm.icu.text.FormattedValue;
+import com.ibm.icu.text.PluralRules.IFixedDecimal;
 import com.ibm.icu.util.ICUUncheckedIOException;
 
 /**
@@ -189,4 +190,22 @@
                 && quantity1.toBigDecimal().equals(_other.quantity1.toBigDecimal())
                 && quantity2.toBigDecimal().equals(_other.quantity2.toBigDecimal());
     }
+
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public IFixedDecimal getFirstFixedDecimal() {
+        return quantity1;
+    }
+
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public IFixedDecimal getSecondFixedDecimal() {
+        return quantity2;
+    }
 }
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java
index 38bd806..503344a 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java
@@ -148,7 +148,7 @@
         getNumberRangeData(macros.loc, nsName, this);
 
         // TODO: Get locale from PluralRules instead?
-        fPluralRanges = new StandardPluralRanges(macros.loc);
+        fPluralRanges = StandardPluralRanges.forLocale(macros.loc);
     }
 
     public FormattedNumberRange format(DecimalQuantity quantity1, DecimalQuantity quantity2, boolean equalBeforeRounding) {
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRanges.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRanges.java
deleted file mode 100644
index fbb8afe..0000000
--- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRanges.java
+++ /dev/null
@@ -1,366 +0,0 @@
-// © 2016 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
-/*
- *******************************************************************************
- * Copyright (C) 2008-2015, Google, International Business Machines Corporation and
- * others. All Rights Reserved.
- *******************************************************************************
- */
-package com.ibm.icu.text;
-
-import java.util.Arrays;
-import java.util.EnumSet;
-
-import com.ibm.icu.impl.StandardPlural;
-import com.ibm.icu.util.Freezable;
-import com.ibm.icu.util.Output;
-
-/**
- * Utility class for returning the plural category for a range of numbers, such as 1–5, so that appropriate messages can
- * be chosen. The rules for determining this value vary widely across locales.
- *
- * @author markdavis
- * @internal
- * @deprecated This API is ICU internal only.
- */
-@Deprecated
-public final class PluralRanges implements Freezable<PluralRanges>, Comparable<PluralRanges> {
-
-    private volatile boolean isFrozen;
-    private Matrix matrix = new Matrix();
-    private boolean[] explicit = new boolean[StandardPlural.COUNT];
-
-    /**
-     * Constructor
-     *
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public PluralRanges() {
-    }
-
-    /**
-     * Internal class for mapping from two StandardPluralCategories values to another.
-     */
-    private static final class Matrix implements Comparable<Matrix>, Cloneable {
-        private byte[] data = new byte[StandardPlural.COUNT * StandardPlural.COUNT];
-        {
-            for (int i = 0; i < data.length; ++i) {
-                data[i] = -1;
-            }
-        }
-
-        Matrix() {
-        }
-
-        /**
-         * Internal method for setting.
-         */
-        @SuppressWarnings("unused")
-        void set(StandardPlural start, StandardPlural end, StandardPlural result) {
-            data[start.ordinal() * StandardPlural.COUNT + end.ordinal()] = result == null ? (byte) -1
-                    : (byte) result.ordinal();
-        }
-
-        /**
-         * Internal method for setting; throws exception if already set.
-         */
-        void setIfNew(StandardPlural start, StandardPlural end,
-                StandardPlural result) {
-            byte old = data[start.ordinal() * StandardPlural.COUNT + end.ordinal()];
-            if (old >= 0) {
-                throw new IllegalArgumentException("Previously set value for <" + start + ", " + end + ", "
-                        + StandardPlural.VALUES.get(old) + ">");
-            }
-            data[start.ordinal() * StandardPlural.COUNT + end.ordinal()] = result == null ? (byte) -1
-                    : (byte) result.ordinal();
-        }
-
-        /**
-         * Internal method for getting.
-         */
-        StandardPlural get(StandardPlural start, StandardPlural end) {
-            byte result = data[start.ordinal() * StandardPlural.COUNT + end.ordinal()];
-            return result < 0 ? null : StandardPlural.VALUES.get(result);
-        }
-
-        /**
-         * Internal method to see if <*,end> values are all the same.
-         */
-        @SuppressWarnings("unused")
-        StandardPlural endSame(StandardPlural end) {
-            StandardPlural first = null;
-            for (StandardPlural start : StandardPlural.VALUES) {
-                StandardPlural item = get(start, end);
-                if (item == null) {
-                    continue;
-                }
-                if (first == null) {
-                    first = item;
-                    continue;
-                }
-                if (first != item) {
-                    return null;
-                }
-            }
-            return first;
-        }
-
-        /**
-         * Internal method to see if <start,*> values are all the same.
-         */
-        @SuppressWarnings("unused")
-        StandardPlural startSame(StandardPlural start,
-                EnumSet<StandardPlural> endDone, Output<Boolean> emit) {
-            emit.value = false;
-            StandardPlural first = null;
-            for (StandardPlural end : StandardPlural.VALUES) {
-                StandardPlural item = get(start, end);
-                if (item == null) {
-                    continue;
-                }
-                if (first == null) {
-                    first = item;
-                    continue;
-                }
-                if (first != item) {
-                    return null;
-                }
-                // only emit if we didn't cover with the 'end' values
-                if (!endDone.contains(end)) {
-                    emit.value = true;
-                }
-            }
-            return first;
-        }
-
-        @Override
-        public int hashCode() {
-            int result = 0;
-            for (int i = 0; i < data.length; ++i) {
-                result = result * 37 + data[i];
-            }
-            return result;
-        }
-
-        @Override
-        public boolean equals(Object other) {
-            if (!(other instanceof Matrix)) {
-                return false;
-            }
-            return 0 == compareTo((Matrix) other);
-        }
-
-        @Override
-        public int compareTo(Matrix o) {
-            for (int i = 0; i < data.length; ++i) {
-                int diff = data[i] - o.data[i];
-                if (diff != 0) {
-                    return diff;
-                }
-            }
-            return 0;
-        }
-
-        @Override
-        public Matrix clone() {
-            Matrix result = new Matrix();
-            result.data = data.clone();
-            return result;
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder result = new StringBuilder();
-            for (StandardPlural i : StandardPlural.values()) {
-                for (StandardPlural j : StandardPlural.values()) {
-                    StandardPlural x = get(i, j);
-                    if (x != null) {
-                        result.append(i + " & " + j + " → " + x + ";\n");
-                    }
-                }
-            }
-            return result.toString();
-        }
-    }
-
-    /**
-     * Internal method for building. If the start or end are null, it means everything of that type.
-     *
-     * @param rangeStart
-     *            plural category for the start of the range
-     * @param rangeEnd
-     *            plural category for the end of the range
-     * @param result
-     *            the resulting plural category
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public void add(StandardPlural rangeStart, StandardPlural rangeEnd,
-            StandardPlural result) {
-        if (isFrozen) {
-            throw new UnsupportedOperationException();
-        }
-        explicit[result.ordinal()] = true;
-        if (rangeStart == null) {
-            for (StandardPlural rs : StandardPlural.values()) {
-                if (rangeEnd == null) {
-                    for (StandardPlural re : StandardPlural.values()) {
-                        matrix.setIfNew(rs, re, result);
-                    }
-                } else {
-                    explicit[rangeEnd.ordinal()] = true;
-                    matrix.setIfNew(rs, rangeEnd, result);
-                }
-            }
-        } else if (rangeEnd == null) {
-            explicit[rangeStart.ordinal()] = true;
-            for (StandardPlural re : StandardPlural.values()) {
-                matrix.setIfNew(rangeStart, re, result);
-            }
-        } else {
-            explicit[rangeStart.ordinal()] = true;
-            explicit[rangeEnd.ordinal()] = true;
-            matrix.setIfNew(rangeStart, rangeEnd, result);
-        }
-    }
-
-    /**
-     * Returns the appropriate plural category for a range from start to end. If there is no available data, then
-     * 'end' is returned as an implicit value. (Such an implicit value can be tested for with {@link #isExplicit}.)
-     *
-     * @param start
-     *            plural category for the start of the range
-     * @param end
-     *            plural category for the end of the range
-     * @return the resulting plural category, or 'end' if there is no data.
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public StandardPlural get(StandardPlural start, StandardPlural end) {
-        StandardPlural result = matrix.get(start, end);
-        return result == null ? end : result;
-    }
-
-    /**
-     * Returns whether the appropriate plural category for a range from start to end
-     * is explicitly in the data (vs given an implicit value). See also {@link #get}.
-     *
-     * @param start
-     *            plural category for the start of the range
-     * @param end
-     *            plural category for the end of the range
-     * @return whether the value for (start,end) is explicit or not.
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public boolean isExplicit(StandardPlural start, StandardPlural end) {
-        return matrix.get(start, end) != null;
-    }
-
-    /**
-     * Internal method to determines whether the StandardPluralCategories was explicitly used in any add statement.
-     *
-     * @param count
-     *            plural category to test
-     * @return true if set
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public boolean isExplicitlySet(StandardPlural count) {
-        return explicit[count.ordinal()];
-    }
-
-    /**
-     * {@inheritDoc}
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    @Override
-    public boolean equals(Object other) {
-        if (this == other) {
-            return true;
-        }
-        if (!(other instanceof PluralRanges)) {
-            return false;
-        }
-        PluralRanges otherPR = (PluralRanges)other;
-        return matrix.equals(otherPR.matrix) && Arrays.equals(explicit, otherPR.explicit);
-    }
-
-    /**
-     * {@inheritDoc}
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Override
-    @Deprecated
-    public int hashCode() {
-        return matrix.hashCode();
-    }
-
-    /**
-     * {@inheritDoc}
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Override
-    @Deprecated
-    public int compareTo(PluralRanges that) {
-        return matrix.compareTo(that.matrix);
-    }
-
-    /**
-     * {@inheritDoc}
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Override
-    @Deprecated
-    public boolean isFrozen() {
-        return isFrozen;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Override
-    @Deprecated
-    public PluralRanges freeze() {
-        isFrozen = true;
-        return this;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Override
-    @Deprecated
-    public PluralRanges cloneAsThawed() {
-        PluralRanges result = new PluralRanges();
-        result.explicit = explicit.clone();
-        result.matrix = matrix.clone();
-        return result;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Override
-    @Deprecated
-    public String toString() {
-        return matrix.toString();
-    }
-}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java
index dfce204..0c1a405 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java
@@ -29,7 +29,10 @@
 import java.util.regex.Pattern;
 
 import com.ibm.icu.impl.PluralRulesLoader;
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.range.StandardPluralRanges;
 import com.ibm.icu.number.FormattedNumber;
+import com.ibm.icu.number.FormattedNumberRange;
 import com.ibm.icu.number.NumberFormatter;
 import com.ibm.icu.util.Output;
 import com.ibm.icu.util.ULocale;
@@ -181,6 +184,7 @@
 
     private final RuleList rules;
     private final transient Set<String> keywords;
+    private final transient StandardPluralRanges standardPluralRanges;
 
     /**
      * Provides a factory for returning plural rules
@@ -377,9 +381,7 @@
      */
     public static PluralRules parseDescription(String description)
             throws ParseException {
-
-        description = description.trim();
-        return description.length() == 0 ? DEFAULT : new PluralRules(parseRuleChain(description));
+        return newInternal(description, null);
     }
 
     /**
@@ -398,11 +400,24 @@
     }
 
     /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public static PluralRules newInternal(String description, StandardPluralRanges ranges)
+            throws ParseException {
+        description = description.trim();
+        return description.length() == 0
+            ? DEFAULT
+            : new PluralRules(parseRuleChain(description), ranges);
+    }
+
+    /**
      * The default rules that accept any number and return
      * {@link #KEYWORD_OTHER}.
      * @stable ICU 3.8
      */
-    public static final PluralRules DEFAULT = new PluralRules(new RuleList().addRule(DEFAULT_RULE));
+    public static final PluralRules DEFAULT = new PluralRules(
+        new RuleList().addRule(DEFAULT_RULE), StandardPluralRanges.DEFAULT);
 
     /**
      * @internal CLDR
@@ -2016,9 +2031,10 @@
     /*
      * Creates a new <code>PluralRules</code> object.  Immutable.
      */
-    private PluralRules(RuleList rules) {
+    private PluralRules(RuleList rules, StandardPluralRanges standardPluralRanges) {
         this.rules = rules;
         this.keywords = Collections.unmodifiableSet(rules.getKeywords());
+        this.standardPluralRanges = standardPluralRanges;
     }
 
     /**
@@ -2059,6 +2075,34 @@
     }
 
     /**
+     * Given a formatted number range, returns the overall plural form of the
+     * range. For example, "3-5" returns "other" in English.
+     *
+     * To get a FormattedNumberRange, see {@link com.ibm.icu.number.NumberRangeFormatter}.
+     *
+     * This method only works if PluralRules was created with a locale. If it was created
+     * from PluralRules.createRules(), or if it was deserialized, this method throws
+     * UnsupportedOperationException.
+     *
+     * @param range  The number range onto which the rules will be applied.
+     * @return       The keyword of the selected rule.
+     * @throws UnsupportedOperationException If called on an instance without plural ranges data.
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public String select(FormattedNumberRange range) {
+        if (standardPluralRanges == null) {
+            throw new UnsupportedOperationException("Plural ranges are unavailable on this instance");
+        }
+        StandardPlural form1 = StandardPlural.fromString(
+            select(range.getFirstFixedDecimal()));
+        StandardPlural form2 = StandardPlural.fromString(
+            select(range.getSecondFixedDecimal()));
+        StandardPlural result = standardPluralRanges.resolve(form1, form2);
+        return result.getKeyword();
+    }
+
+    /**
      * Given a number, returns the keyword of the first rule that applies to
      * the number.
      *
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java
index ffdaf98..2d6d667 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java
@@ -10,7 +10,6 @@
 
 import java.util.Arrays;
 
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -18,12 +17,14 @@
 import com.ibm.icu.dev.test.TestFmwk;
 import com.ibm.icu.impl.SimpleFormatterImpl;
 import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.range.StandardPluralRanges;
+import com.ibm.icu.number.FormattedNumberRange;
+import com.ibm.icu.number.NumberFormatter;
+import com.ibm.icu.number.NumberFormatter.UnitWidth;
+import com.ibm.icu.number.NumberRangeFormatter;
 import com.ibm.icu.text.MeasureFormat;
 import com.ibm.icu.text.MeasureFormat.FormatWidth;
-import com.ibm.icu.text.PluralRanges;
-import com.ibm.icu.text.PluralRules.Factory;
 import com.ibm.icu.util.Currency;
-import com.ibm.icu.util.Measure;
 import com.ibm.icu.util.MeasureUnit;
 import com.ibm.icu.util.ULocale;
 
@@ -37,7 +38,7 @@
     public void TestLocaleData() {
         String[][] tests = {
                 {"de", "other", "one", "one"},
-                {"xxx", "few", "few", "few" },
+                {"xxx", "other", "other", "other" },
                 {"de", "one", "other", "other"},
                 {"de", "other", "one", "one"},
                 {"de", "other", "other", "other"},
@@ -50,9 +51,9 @@
             final StandardPlural start = StandardPlural.fromString(test[1]);
             final StandardPlural end = StandardPlural.fromString(test[2]);
             final StandardPlural expected = StandardPlural.fromString(test[3]);
-            final PluralRanges pluralRanges = Factory.getDefaultFactory().getPluralRanges(locale);
+            final StandardPluralRanges pluralRanges = StandardPluralRanges.forLocale(locale);
 
-            StandardPlural actual = pluralRanges.get(start, end);
+            StandardPlural actual = pluralRanges.resolve(start, end);
             assertEquals("Deriving range category", expected, actual);
         }
     }
@@ -73,30 +74,28 @@
         }
     }
 
-    // TODO: Re-enable this test when #12454 is fixed.
-    @Ignore("http://bugs.icu-project.org/trac/ticket/12454")
     @Test
     public void TestFormatting() {
         Object[][] tests = {
-                {0.0, 1.0, ULocale.FRANCE, FormatWidth.WIDE, MeasureUnit.FAHRENHEIT, "0–1 degré Fahrenheit"},
-                {1.0, 2.0, ULocale.FRANCE, FormatWidth.WIDE, MeasureUnit.FAHRENHEIT, "1–2 degrés Fahrenheit"},
-                {3.1, 4.25, ULocale.FRANCE, FormatWidth.SHORT, MeasureUnit.FAHRENHEIT, "3,1–4,25 °F"},
-                {3.1, 4.25, ULocale.ENGLISH, FormatWidth.SHORT, MeasureUnit.FAHRENHEIT, "3.1–4.25°F"},
-                {3.1, 4.25, ULocale.CHINESE, FormatWidth.WIDE, MeasureUnit.INCH, "3.1-4.25英寸"},
-                {0.0, 1.0, ULocale.ENGLISH, FormatWidth.WIDE, MeasureUnit.INCH, "0–1 inches"},
+                {0.0, 1.0, ULocale.FRANCE, UnitWidth.FULL_NAME, MeasureUnit.FAHRENHEIT, "0–1\u00A0degré Fahrenheit"},
+                {1.0, 2.0, ULocale.FRANCE, UnitWidth.FULL_NAME, MeasureUnit.FAHRENHEIT, "1–2\u00A0degrés Fahrenheit"},
+                {3.1, 4.25, ULocale.FRANCE, UnitWidth.SHORT, MeasureUnit.FAHRENHEIT, "3,1–4,25\u202F°F"},
+                {3.1, 4.25, ULocale.ENGLISH, UnitWidth.SHORT, MeasureUnit.FAHRENHEIT, "3.1–4.25°F"},
+                {3.1, 4.25, ULocale.CHINESE, UnitWidth.FULL_NAME, MeasureUnit.INCH, "3.1-4.25英寸"},
+                {0.0, 1.0, ULocale.ENGLISH, UnitWidth.FULL_NAME, MeasureUnit.INCH, "0–1 inches"},
 
-                {0.0, 1.0, ULocale.ENGLISH, FormatWidth.NARROW, Currency.getInstance("EUR"), "€0.00–1.00"},
-                {0.0, 1.0, ULocale.FRENCH, FormatWidth.NARROW, Currency.getInstance("EUR"), "0,00–1,00 €"},
-                {0.0, 100.0, ULocale.FRENCH, FormatWidth.NARROW, Currency.getInstance("JPY"), "0–100\u00a0JPY"},
+                {0.0, 1.0, ULocale.ENGLISH, UnitWidth.NARROW, Currency.getInstance("EUR"), "€0.00 – €1.00"},
+                {0.0, 1.0, ULocale.FRENCH, UnitWidth.NARROW, Currency.getInstance("EUR"), "0,00–1,00 €"},
+                {0.0, 100.0, ULocale.FRENCH, UnitWidth.NARROW, Currency.getInstance("JPY"), "0–100\u00a0¥"},
 
-                {0.0, 1.0, ULocale.ENGLISH, FormatWidth.SHORT, Currency.getInstance("EUR"), "EUR0.00–1.00"},
-                {0.0, 1.0, ULocale.FRENCH, FormatWidth.SHORT, Currency.getInstance("EUR"), "0,00–1,00\u00a0EUR"},
-                {0.0, 100.0, ULocale.FRENCH, FormatWidth.SHORT, Currency.getInstance("JPY"), "0–100\u00a0JPY"},
+                {0.0, 1.0, ULocale.ENGLISH, UnitWidth.SHORT, Currency.getInstance("EUR"), "€0.00 – €1.00"},
+                {0.0, 1.0, ULocale.FRENCH, UnitWidth.SHORT, Currency.getInstance("EUR"), "0,00–1,00\u00a0€"},
+                {0.0, 100.0, ULocale.FRENCH, UnitWidth.SHORT, Currency.getInstance("JPY"), "0–100\u00a0JPY"},
 
-                {0.0, 1.0, ULocale.ENGLISH, FormatWidth.WIDE, Currency.getInstance("EUR"), "0.00–1.00 euros"},
-                {0.0, 1.0, ULocale.FRENCH, FormatWidth.WIDE, Currency.getInstance("EUR"), "0,00–1,00 euro"},
-                {0.0, 2.0, ULocale.FRENCH, FormatWidth.WIDE, Currency.getInstance("EUR"), "0,00–2,00 euros"},
-                {0.0, 100.0, ULocale.FRENCH, FormatWidth.WIDE, Currency.getInstance("JPY"), "0–100 yens japonais"},
+                {0.0, 1.0, ULocale.ENGLISH, UnitWidth.FULL_NAME, Currency.getInstance("EUR"), "0.00–1.00 euros"},
+                {0.0, 1.0, ULocale.FRENCH, UnitWidth.FULL_NAME, Currency.getInstance("EUR"), "0,00–1,00 euro"},
+                {0.0, 2.0, ULocale.FRENCH, UnitWidth.FULL_NAME, Currency.getInstance("EUR"), "0,00–2,00 euros"},
+                {0.0, 100.0, ULocale.FRENCH, UnitWidth.FULL_NAME, Currency.getInstance("JPY"), "0–100 yens japonais"},
         };
         int i = 0;
         for (Object[] test : tests) {
@@ -104,34 +103,15 @@
             double low = (Double) test[0];
             double high = (Double) test[1];
             final ULocale locale = (ULocale) test[2];
-            final FormatWidth width = (FormatWidth) test[3];
+            final UnitWidth unitWidth = (UnitWidth) test[3];
             final MeasureUnit unit = (MeasureUnit) test[4];
-            final Object expected = test[5];
+            final String expected = (String) test[5];
 
-            MeasureFormat mf = MeasureFormat.getInstance(locale, width);
-            Object actual;
-            try {
-                // TODO: Fix this when range formatting is added again.
-                // To let the code compile, the following line does list formatting.
-                actual = mf.formatMeasures(new Measure(low, unit), new Measure(high, unit));
-            } catch (Exception e) {
-                actual = e.getClass();
-            }
-            assertEquals(i + " Formatting unit", expected, actual);
-        }
-    }
-
-    @Test
-    public void TestBasic() {
-        PluralRanges a = new PluralRanges();
-        a.add(StandardPlural.ONE, StandardPlural.OTHER, StandardPlural.ONE);
-        StandardPlural actual = a.get(StandardPlural.ONE, StandardPlural.OTHER);
-        assertEquals("range", StandardPlural.ONE, actual);
-        a.freeze();
-        try {
-            a.add(StandardPlural.ONE, StandardPlural.ONE, StandardPlural.ONE);
-            errln("Failed to cause exception on frozen instance");
-        } catch (UnsupportedOperationException e) {
+            FormattedNumberRange actual = NumberRangeFormatter.with()
+                .numberFormatterBoth(NumberFormatter.with().unit(unit).unitWidth(unitWidth))
+                .locale(locale)
+                .formatRange(low, high);
+            assertEquals(i + " Formatting unit", expected, actual.toString());
         }
     }
 }
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java
index 6461d4a..5c2c303 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java
@@ -42,8 +42,10 @@
 import com.ibm.icu.impl.Relation;
 import com.ibm.icu.impl.Utility;
 import com.ibm.icu.number.FormattedNumber;
+import com.ibm.icu.number.FormattedNumberRange;
 import com.ibm.icu.number.LocalizedNumberFormatter;
 import com.ibm.icu.number.NumberFormatter;
+import com.ibm.icu.number.NumberRangeFormatter;
 import com.ibm.icu.number.Precision;
 import com.ibm.icu.number.UnlocalizedNumberFormatter;
 import com.ibm.icu.text.NumberFormat;
@@ -1279,4 +1281,37 @@
         Locale.setDefault(Locale.GERMAN);
         assertEquals("FixedDecimal toString", expected, fd.toString());
     }
+
+    @Test
+    public void testSelectRange() {
+        int d1 = 102;
+        int d2 = 201;
+        ULocale locale = new ULocale("sl");
+
+        // Locale sl has interesting data: one + two => few
+        FormattedNumberRange range = NumberRangeFormatter.withLocale(locale).formatRange(d1, d2);
+        PluralRules rules = PluralRules.forLocale(locale);
+
+        // For testing: get plural form of first and second numbers
+        FormattedNumber a = NumberFormatter.withLocale(locale).format(d1);
+        FormattedNumber b = NumberFormatter.withLocale(locale).format(d2);
+        assertEquals("First plural", "two", rules.select(a));
+        assertEquals("Second plural", "one", rules.select(b));
+
+        // Check the range plural now:
+        String form = rules.select(range);
+        assertEquals("Range plural", "few", form);
+
+        // Test when plural ranges data is unavailable:
+        PluralRules bare = PluralRules.createRules("a: i = 0,1");
+        try {
+            form = bare.select(range);
+            fail("Expected exception");
+        } catch (UnsupportedOperationException e) {}
+
+        // However, they should not throw when no data is available for a language.
+        PluralRules xyz = PluralRules.forLocale(new ULocale("xyz"));
+        form = xyz.select(range);
+        assertEquals("Fallback form", "other", form);
+    }
 }