ICU-21947 Replace FixedDecimal with DecimalQuantity in PluralRule sample parsing

See #2007
diff --git a/icu4c/source/i18n/plurrule.cpp b/icu4c/source/i18n/plurrule.cpp
index 7d1037f..edc3d88 100644
--- a/icu4c/source/i18n/plurrule.cpp
+++ b/icu4c/source/i18n/plurrule.cpp
@@ -26,6 +26,7 @@
 #include "hash.h"
 #include "locutil.h"
 #include "mutex.h"
+#include "number_decnum.h"
 #include "patternprops.h"
 #include "plurrule_impl.h"
 #include "putilimp.h"
@@ -45,7 +46,9 @@
 U_NAMESPACE_BEGIN
 
 using namespace icu::pluralimpl;
+using icu::number::impl::DecNum;
 using icu::number::impl::DecimalQuantity;
+using icu::number::impl::RoundingMode;
 
 static const UChar PLURAL_KEYWORD_OTHER[]={LOW_O,LOW_T,LOW_H,LOW_E,LOW_R,0};
 static const UChar PLURAL_DEFAULT_RULE[]={LOW_O,LOW_T,LOW_H,LOW_E,LOW_R,COLON,SPACE,LOW_N,0};
@@ -369,36 +372,18 @@
     return 0;
 }
 
-
-static double scaleForInt(double d) {
-    double scale = 1.0;
-    while (d != floor(d)) {
-        d = d * 10.0;
-        scale = scale * 10.0;
-    }
-    return scale;
-}
-
-static const double powers10[7] = {1.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0}; // powers of 10 for 0..6
-static double applyExponent(double source, int32_t exponent) {
-    if (exponent >= 0 && exponent <= 6) {
-        return source * powers10[exponent];
-    }
-    return source * pow(10.0, exponent);
-}
-
 /**
- * Helper method for the overrides of getSamples() for double and FixedDecimal
- * return value types.  Provide only one of an allocated array of doubles or
- * FixedDecimals, and a nullptr for the other.
+ * Helper method for the overrides of getSamples() for double and DecimalQuantity
+ * return value types.  Provide only one of an allocated array of double or
+ * DecimalQuantity, and a nullptr for the other.
  */
 static int32_t
 getSamplesFromString(const UnicodeString &samples, double *destDbl,
-                        FixedDecimal* destFd, int32_t destCapacity,
+                        DecimalQuantity* destDq, int32_t destCapacity,
                         UErrorCode& status) {
 
-    if ((destDbl == nullptr && destFd == nullptr)
-            || (destDbl != nullptr && destFd != nullptr)) {
+    if ((destDbl == nullptr && destDq == nullptr)
+            || (destDbl != nullptr && destDq != nullptr)) {
         status = U_INTERNAL_PROGRAM_ERROR;
         return 0;
     }
@@ -420,58 +405,75 @@
         // std::cout << "PluralRules::getSamples(), samplesRange = \"" << sampleRange.toUTF8String(ss) << "\"\n";
         int32_t tildeIndex = sampleRange.indexOf(TILDE);
         if (tildeIndex < 0) {
-            FixedDecimal fixed(sampleRange, status);
+            DecimalQuantity dq = DecimalQuantity::fromExponentString(sampleRange, status);
             if (isDouble) {
-                double sampleValue = fixed.source;
-                if (fixed.visibleDecimalDigitCount == 0 || sampleValue != floor(sampleValue)) {
-                    destDbl[sampleCount++] = applyExponent(sampleValue, fixed.exponent);
+                // See warning note below about lack of precision for floating point samples for numbers with
+                // trailing zeroes in the decimal fraction representation.
+                double dblValue = dq.toDouble();
+                if (!(dblValue == floor(dblValue) && dq.fractionCount() > 0)) {
+                    destDbl[sampleCount++] = dblValue;
                 }
             } else {
-                destFd[sampleCount++] = fixed;
+                destDq[sampleCount++] = dq;
             }
         } else {
-            FixedDecimal fixedLo(sampleRange.tempSubStringBetween(0, tildeIndex), status);
-            FixedDecimal fixedHi(sampleRange.tempSubStringBetween(tildeIndex+1), status);
-            double rangeLo = fixedLo.source;
-            double rangeHi = fixedHi.source;
+            DecimalQuantity rangeLo =
+                DecimalQuantity::fromExponentString(sampleRange.tempSubStringBetween(0, tildeIndex), status);
+            DecimalQuantity rangeHi = DecimalQuantity::fromExponentString(sampleRange.tempSubStringBetween(tildeIndex+1), status);
             if (U_FAILURE(status)) {
                 break;
             }
-            if (rangeHi < rangeLo) {
+            if (rangeHi.toDouble() < rangeLo.toDouble()) {
                 status = U_INVALID_FORMAT_ERROR;
                 break;
             }
 
-            // For ranges of samples with fraction decimal digits, scale the number up so that we
-            //   are adding one in the units place. Avoids roundoffs from repetitive adds of tenths.
+            DecimalQuantity incrementDq;
+            incrementDq.setToInt(1);
+            int32_t lowerDispMag = rangeLo.getLowerDisplayMagnitude();
+            int32_t exponent = rangeLo.getExponent();
+            int32_t incrementScale = lowerDispMag + exponent;
+            incrementDq.adjustMagnitude(incrementScale);
+            double incrementVal = incrementDq.toDouble();  // 10 ^ incrementScale
+            
 
-            double scale = scaleForInt(rangeLo);
-            double t = scaleForInt(rangeHi);
-            if (t > scale) {
-                scale = t;
-            }
-            rangeLo *= scale;
-            rangeHi *= scale;
-            for (double n=rangeLo; n<=rangeHi; n+=1) {
-                double sampleValue = n/scale;
+            DecimalQuantity dq(rangeLo);
+            double dblValue = dq.toDouble();
+            double end = rangeHi.toDouble();
+
+            while (dblValue <= end) {
                 if (isDouble) {
                     // Hack Alert: don't return any decimal samples with integer values that
                     //    originated from a format with trailing decimals.
                     //    This API is returning doubles, which can't distinguish having displayed
                     //    zeros to the right of the decimal.
                     //    This results in test failures with values mapping back to a different keyword.
-                    if (!(sampleValue == floor(sampleValue) && fixedLo.visibleDecimalDigitCount > 0)) {
-                        destDbl[sampleCount++] = sampleValue;
+                    if (!(dblValue == floor(dblValue) && dq.fractionCount() > 0)) {
+                        destDbl[sampleCount++] = dblValue;
                     }
                 } else {
-                    int32_t v = (int32_t) fixedLo.getPluralOperand(PluralOperand::PLURAL_OPERAND_V);
-                    int32_t e = (int32_t) fixedLo.getPluralOperand(PluralOperand::PLURAL_OPERAND_E);
-                    FixedDecimal newSample = FixedDecimal::createWithExponent(sampleValue, v, e);
-                    destFd[sampleCount++] = newSample;
+                    destDq[sampleCount++] = dq;
                 }
                 if (sampleCount >= destCapacity) {
                     break;
                 }
+
+                // Increment dq for next iteration
+
+                // Because DecNum and DecimalQuantity do not support
+                // add operations, we need to convert to/from double,
+                // despite precision lossiness for decimal fractions like 0.1.
+                dblValue += incrementVal;
+                DecNum newDqDecNum;
+                newDqDecNum.setTo(dblValue, status);
+                DecimalQuantity newDq;             
+                newDq.setToDecNum(newDqDecNum, status);
+                newDq.setMinFraction(-lowerDispMag);
+                newDq.roundToMagnitude(lowerDispMag, RoundingMode::UNUM_ROUND_HALFEVEN, status);
+                newDq.adjustMagnitude(-exponent);
+                newDq.adjustExponent(exponent);
+                dblValue = newDq.toDouble();
+                dq = newDq;
             }
         }
         sampleStartIdx = sampleEndIdx + 1;
@@ -505,7 +507,7 @@
 }
 
 int32_t
-PluralRules::getSamples(const UnicodeString &keyword, FixedDecimal *dest,
+PluralRules::getSamples(const UnicodeString &keyword, DecimalQuantity *dest,
                         int32_t destCapacity, UErrorCode& status) {
     if (U_FAILURE(status)) {
         return 0;
diff --git a/icu4c/source/i18n/plurrule_impl.h b/icu4c/source/i18n/plurrule_impl.h
index 7274da5..c27b655 100644
--- a/icu4c/source/i18n/plurrule_impl.h
+++ b/icu4c/source/i18n/plurrule_impl.h
@@ -34,7 +34,7 @@
  * A FixedDecimal version of UPLRULES_NO_UNIQUE_VALUE used in PluralRulesTest
  * for parsing of samples.
  */
-#define UPLRULES_NO_UNIQUE_VALUE_DECIMAL (FixedDecimal((double)-0.00123456777))
+#define UPLRULES_NO_UNIQUE_VALUE_DECIMAL(ERROR_CODE) (DecimalQuantity::fromExponentString(u"-0.00123456777", ERROR_CODE))
 
 class PluralRulesTest;
 
diff --git a/icu4c/source/i18n/unicode/plurrule.h b/icu4c/source/i18n/unicode/plurrule.h
index e3822dd..b4298de 100644
--- a/icu4c/source/i18n/unicode/plurrule.h
+++ b/icu4c/source/i18n/unicode/plurrule.h
@@ -59,9 +59,15 @@
 class FormattedNumberRange;
 namespace impl {
 class UFormattedNumberRangeData;
+class DecimalQuantity;
+class DecNum;
 }
 }
 
+#ifndef U_HIDE_INTERNAL_API
+using icu::number::impl::DecimalQuantity;
+#endif  /* U_HIDE_INTERNAL_API */
+
 /**
  * Defines rules for mapping non-negative numeric values onto a small set of
  * keywords. Rules are constructed from a text description, consisting
@@ -468,7 +474,7 @@
 
 #ifndef U_HIDE_INTERNAL_API
     /**
-     * Internal-only function that returns FixedDecimals instead of doubles.
+     * Internal-only function that returns DecimalQuantitys instead of doubles.
      *
      * Returns sample values for which select() would return the keyword.  If
      * the keyword is unknown, returns no values, but this is not an error.
@@ -488,7 +494,7 @@
      * @internal
      */
     int32_t getSamples(const UnicodeString &keyword,
-                       FixedDecimal *dest, int32_t destCapacity,
+                       DecimalQuantity *dest, int32_t destCapacity,
                        UErrorCode& status);
 #endif  /* U_HIDE_INTERNAL_API */
 
diff --git a/icu4c/source/test/intltest/plurults.cpp b/icu4c/source/test/intltest/plurults.cpp
index 54cc77a..8de9745 100644
--- a/icu4c/source/test/intltest/plurults.cpp
+++ b/icu4c/source/test/intltest/plurults.cpp
@@ -50,7 +50,9 @@
     TESTCASE_AUTO(testAPI);
     // TESTCASE_AUTO(testGetUniqueKeywordValue);
     TESTCASE_AUTO(testGetSamples);
-    TESTCASE_AUTO(testGetFixedDecimalSamples);
+    TESTCASE_AUTO(testGetDecimalQuantitySamples);
+    TESTCASE_AUTO(testGetOrAddSamplesFromString);
+    TESTCASE_AUTO(testGetOrAddSamplesFromStringCompactNotation);
     TESTCASE_AUTO(testSamplesWithExponent);
     TESTCASE_AUTO(testSamplesWithCompactNotation);
     TESTCASE_AUTO(testWithin);
@@ -396,9 +398,16 @@
     assertRuleKeyValue("a: n is 1", "other", UPLRULES_NO_UNIQUE_VALUE); // key matches default rule
 }
 
+/**
+ * Using the double API for getting plural samples, assert all samples match the keyword
+ * they are listed under, for all locales.
+ * 
+ * Specifically, iterate over all locales, get plural rules for the locale, iterate over every rule,
+ * then iterate over every sample in the rule, parse sample to a number (double), use that number
+ * as an input to .select() for the rules object, and assert the actual return plural keyword matches
+ * what we expect based on the plural rule string.
+ */
 void PluralRulesTest::testGetSamples() {
-    // TODO: fix samples, re-enable this test.
-
     // no get functional equivalent API in ICU4C, so just
     // test every locale...
     UErrorCode status = U_ZERO_ERROR;
@@ -457,21 +466,24 @@
     }
 }
 
-void PluralRulesTest::testGetFixedDecimalSamples() {
-    // TODO: fix samples, re-enable this test.
-
+/**
+ * Using the DecimalQuantity API for getting plural samples, assert all samples match the keyword
+ * they are listed under, for all locales.
+ * 
+ * Specifically, iterate over all locales, get plural rules for the locale, iterate over every rule,
+ * then iterate over every sample in the rule, parse sample to a number (DecimalQuantity), use that number
+ * as an input to .select() for the rules object, and assert the actual return plural keyword matches
+ * what we expect based on the plural rule string.
+ */
+void PluralRulesTest::testGetDecimalQuantitySamples() {
     // no get functional equivalent API in ICU4C, so just
     // test every locale...
     UErrorCode status = U_ZERO_ERROR;
     int32_t numLocales;
     const Locale* locales = Locale::getAvailableLocales(numLocales);
 
-    FixedDecimal values[1000];
+    DecimalQuantity values[1000];
     for (int32_t i = 0; U_SUCCESS(status) && i < numLocales; ++i) {
-        //if (uprv_strcmp(locales[i].getLanguage(), "fr") == 0 &&
-        //        logKnownIssue("21322", "PluralRules::getSamples cannot distinguish 1e5 from 100000")) {
-        //    continue;
-        //}
         LocalPointer<PluralRules> rules(PluralRules::forLocale(locales[i], status));
         if (U_FAILURE(status)) {
             break;
@@ -501,21 +513,24 @@
                 count = UPRV_LENGTHOF(values);
             }
             for (int32_t j = 0; j < count; ++j) {
-                if (values[j] == UPLRULES_NO_UNIQUE_VALUE_DECIMAL) {
+                if (values[j] == UPLRULES_NO_UNIQUE_VALUE_DECIMAL(status)) {
                     errln("got 'no unique value' among values");
                 } else {
+                    if (U_FAILURE(status)){
+                        errln(UnicodeString(u"getSamples() failed for sample ") +
+                            values[j].toExponentString() +
+                            UnicodeString(u", keyword ") + *keyword);
+                        continue;
+                    }
                     UnicodeString resultKeyword = rules->select(values[j]);
                     // if (strcmp(locales[i].getName(), "uk") == 0) {    // Debug only.
                     //     std::cout << "  uk " << US(resultKeyword).cstr() << " " << values[j] << std::endl;
                     // }
                     if (*keyword != resultKeyword) {
-                        if (values[j].exponent == 0 || !logKnownIssue("21714", "PluralRules::select treats 1c6 as 1")) {
-                            UnicodeString valueString(values[j].toString());
-                            char valueBuf[16];
-                            valueString.extract(0, valueString.length(), valueBuf, sizeof(valueBuf));
-                            errln("file %s, line %d, Locale %s, sample for keyword \"%s\":  %s, select(%s) returns keyword \"%s\"",
-                                      __FILE__, __LINE__, locales[i].getName(), US(*keyword).cstr(), valueBuf, valueBuf, US(resultKeyword).cstr());
-                        }
+                        errln("file %s, line %d, Locale %s, sample for keyword \"%s\":  %s, select(%s) returns keyword \"%s\"",
+                            __FILE__, __LINE__, locales[i].getName(), US(*keyword).cstr(),
+                            US(values[j].toExponentString()).cstr(), US(values[j].toExponentString()).cstr(),
+                            US(resultKeyword).cstr());
                     }
                 }
             }
@@ -523,6 +538,102 @@
     }
 }
 
+/**
+ * Test addSamples (Java) / getSamplesFromString (C++) to ensure the expansion of plural rule sample range
+ * expands to a sequence of sample numbers that is incremented as the right scale.
+ *
+ *  Do this for numbers with fractional digits but no exponent.
+ */
+void PluralRulesTest::testGetOrAddSamplesFromString() {
+    UErrorCode status = U_ZERO_ERROR;
+    UnicodeString description(u"testkeyword: e != 0 @decimal 2.0c6~4.0c6, …");
+    LocalPointer<PluralRules> rules(PluralRules::createRules(description, status));
+    if (U_FAILURE(status)) {
+        errln("Couldn't create plural rules from a string, with error = %s", u_errorName(status));
+        return;
+    }
+
+    LocalPointer<StringEnumeration> keywords(rules->getKeywords(status));
+    if (U_FAILURE(status)) {
+        errln("Couldn't get keywords from a parsed rules object, with error = %s", u_errorName(status));
+        return;
+    }
+
+    DecimalQuantity values[1000];
+    const UnicodeString keyword(u"testkeyword");
+    int32_t count = rules->getSamples(keyword, values, UPRV_LENGTHOF(values), status);
+    if (U_FAILURE(status)) {
+        errln(UnicodeString(u"getSamples() failed for plural rule keyword ") + keyword);
+        return;
+    }
+
+    UnicodeString expDqStrs[] = {
+        u"2.0c6", u"2.1c6", u"2.2c6", u"2.3c6", u"2.4c6", u"2.5c6", u"2.6c6", u"2.7c6", u"2.8c6", u"2.9c6",
+        u"3.0c6", u"3.1c6", u"3.2c6", u"3.3c6", u"3.4c6", u"3.5c6", u"3.6c6", u"3.7c6", u"3.8c6", u"3.9c6",
+        u"4.0c6"
+    };
+    assertEquals(u"Number of parsed samples from test string incorrect", 21, count);
+    for (int i = 0; i < count; i++) {
+        UnicodeString expDqStr = expDqStrs[i];
+        DecimalQuantity sample = values[i];
+        UnicodeString sampleStr = sample.toExponentString();
+
+        assertEquals(u"Expansion of sample range to sequence of sample values should increment at the right scale",
+            expDqStr, sampleStr);
+    }
+}
+
+/**
+ * Test addSamples (Java) / getSamplesFromString (C++) to ensure the expansion of plural rule sample range
+ * expands to a sequence of sample numbers that is incremented as the right scale.
+ *
+ *  Do this for numbers written in a notation that has an exponent, for which the number is an
+ *  integer (also as defined in the UTS 35 spec for the plural operands) but whose representation
+ *  has fractional digits in the significand written before the exponent.
+ */
+void PluralRulesTest::testGetOrAddSamplesFromStringCompactNotation() {
+    UErrorCode status = U_ZERO_ERROR;
+    UnicodeString description(u"testkeyword: e != 0 @decimal 2.0~4.0, …");
+    LocalPointer<PluralRules> rules(PluralRules::createRules(description, status));
+    if (U_FAILURE(status)) {
+        errln("Couldn't create plural rules from a string, with error = %s", u_errorName(status));
+        return;
+    }
+
+    LocalPointer<StringEnumeration> keywords(rules->getKeywords(status));
+    if (U_FAILURE(status)) {
+        errln("Couldn't get keywords from a parsed rules object, with error = %s", u_errorName(status));
+        return;
+    }
+
+    DecimalQuantity values[1000];
+    const UnicodeString keyword(u"testkeyword");
+    int32_t count = rules->getSamples(keyword, values, UPRV_LENGTHOF(values), status);
+    if (U_FAILURE(status)) {
+        errln(UnicodeString(u"getSamples() failed for plural rule keyword ") + keyword);
+        return;
+    }
+
+    UnicodeString expDqStrs[] = {
+        u"2.0", u"2.1", u"2.2", u"2.3", u"2.4", u"2.5", u"2.6", u"2.7", u"2.8", u"2.9",
+        u"3.0", u"3.1", u"3.2", u"3.3", u"3.4", u"3.5", u"3.6", u"3.7", u"3.8", u"3.9",
+        u"4.0"
+    };
+    assertEquals(u"Number of parsed samples from test string incorrect", 21, count);
+    for (int i = 0; i < count; i++) {
+        UnicodeString expDqStr = expDqStrs[i];
+        DecimalQuantity sample = values[i];
+        UnicodeString sampleStr = sample.toExponentString();
+
+        assertEquals(u"Expansion of sample range to sequence of sample values should increment at the right scale",
+            expDqStr, sampleStr);
+    }
+}
+
+/**
+ * This test is for the support of X.YeZ scientific notation of numbers in
+ * the plural sample string.
+ */
 void PluralRulesTest::testSamplesWithExponent() {
     // integer samples
     UErrorCode status = U_ZERO_ERROR;
@@ -538,9 +649,9 @@
         errln("Couldn't create plural rules from a string using exponent notation, with error = %s", u_errorName(status));
         return;
     }
-    checkNewSamples(description, test, u"one", u"@integer 0, 1, 1e5", FixedDecimal(0));
-    checkNewSamples(description, test, u"many", u"@integer 1000000, 2e6, 3e6, 4e6, 5e6, 6e6, 7e6, …", FixedDecimal(1000000));
-    checkNewSamples(description, test, u"other", u"@integer 2~17, 100, 1000, 10000, 100000, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, …", FixedDecimal(2));
+    checkNewSamples(description, test, u"one", u"@integer 0, 1, 1e5", DecimalQuantity::fromExponentString(u"0", status));
+    checkNewSamples(description, test, u"many", u"@integer 1000000, 2e6, 3e6, 4e6, 5e6, 6e6, 7e6, …", DecimalQuantity::fromExponentString(u"1000000", status));
+    checkNewSamples(description, test, u"other", u"@integer 2~17, 100, 1000, 10000, 100000, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, …", DecimalQuantity::fromExponentString(u"2", status));
 
     // decimal samples
     status = U_ZERO_ERROR;
@@ -555,12 +666,15 @@
         errln("Couldn't create plural rules from a string using exponent notation, with error = %s", u_errorName(status));
         return;
     }
-    checkNewSamples(description2, test2, u"one", u"@decimal 0.0~1.5, 1.1e5", FixedDecimal(0, 1));
-    checkNewSamples(description2, test2, u"many", u"@decimal 2.1e6, 3.1e6, 4.1e6, 5.1e6, 6.1e6, 7.1e6, …", FixedDecimal::createWithExponent(2.1, 1, 6));
-    checkNewSamples(description2, test2, u"other", u"@decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1e5, 3.1e5, 4.1e5, 5.1e5, 6.1e5, 7.1e5, …", FixedDecimal(2.0, 1));
+    checkNewSamples(description2, test2, u"one", u"@decimal 0.0~1.5, 1.1e5", DecimalQuantity::fromExponentString(u"0.0", status));
+    checkNewSamples(description2, test2, u"many", u"@decimal 2.1e6, 3.1e6, 4.1e6, 5.1e6, 6.1e6, 7.1e6, …", DecimalQuantity::fromExponentString(u"2.1c6", status));
+    checkNewSamples(description2, test2, u"other", u"@decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1e5, 3.1e5, 4.1e5, 5.1e5, 6.1e5, 7.1e5, …", DecimalQuantity::fromExponentString(u"2.0", status));
 }
 
-
+/**
+ * This test is for the support of X.YcZ compact notation of numbers in
+ * the plural sample string.
+ */
 void PluralRulesTest::testSamplesWithCompactNotation() {
     // integer samples
     UErrorCode status = U_ZERO_ERROR;
@@ -576,9 +690,9 @@
         errln("Couldn't create plural rules from a string using exponent notation, with error = %s", u_errorName(status));
         return;
     }
-    checkNewSamples(description, test, u"one", u"@integer 0, 1, 1c5", FixedDecimal(0));
-    checkNewSamples(description, test, u"many", u"@integer 1000000, 2c6, 3c6, 4c6, 5c6, 6c6, 7c6, …", FixedDecimal(1000000));
-    checkNewSamples(description, test, u"other", u"@integer 2~17, 100, 1000, 10000, 100000, 2c5, 3c5, 4c5, 5c5, 6c5, 7c5, …", FixedDecimal(2));
+    checkNewSamples(description, test, u"one", u"@integer 0, 1, 1c5", DecimalQuantity::fromExponentString(u"0", status));
+    checkNewSamples(description, test, u"many", u"@integer 1000000, 2c6, 3c6, 4c6, 5c6, 6c6, 7c6, …", DecimalQuantity::fromExponentString(u"1000000", status));
+    checkNewSamples(description, test, u"other", u"@integer 2~17, 100, 1000, 10000, 100000, 2c5, 3c5, 4c5, 5c5, 6c5, 7c5, …", DecimalQuantity::fromExponentString(u"2", status));
 
     // decimal samples
     status = U_ZERO_ERROR;
@@ -593,9 +707,9 @@
         errln("Couldn't create plural rules from a string using exponent notation, with error = %s", u_errorName(status));
         return;
     }
-    checkNewSamples(description2, test2, u"one", u"@decimal 0.0~1.5, 1.1c5", FixedDecimal(0, 1));
-    checkNewSamples(description2, test2, u"many", u"@decimal 2.1c6, 3.1c6, 4.1c6, 5.1c6, 6.1c6, 7.1c6, …", FixedDecimal::createWithExponent(2.1, 1, 6));
-    checkNewSamples(description2, test2, u"other", u"@decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1c5, 3.1c5, 4.1c5, 5.1c5, 6.1c5, 7.1c5, …", FixedDecimal(2.0, 1));
+    checkNewSamples(description2, test2, u"one", u"@decimal 0.0~1.5, 1.1c5", DecimalQuantity::fromExponentString(u"0.0", status));
+    checkNewSamples(description2, test2, u"many", u"@decimal 2.1c6, 3.1c6, 4.1c6, 5.1c6, 6.1c6, 7.1c6, …", DecimalQuantity::fromExponentString(u"2.1c6", status));
+    checkNewSamples(description2, test2, u"other", u"@decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1c5, 3.1c5, 4.1c5, 5.1c5, 6.1c5, 7.1c5, …", DecimalQuantity::fromExponentString(u"2.0", status));
 }
 
 void PluralRulesTest::checkNewSamples(
@@ -603,17 +717,17 @@
         const LocalPointer<PluralRules> &test,
         UnicodeString keyword,
         UnicodeString samplesString,
-        FixedDecimal firstInRange) {
+        DecimalQuantity firstInRange) {
 
     UErrorCode status = U_ZERO_ERROR;
-    FixedDecimal samples[1000];
+    DecimalQuantity samples[1000];
     
     test->getSamples(keyword, samples, UPRV_LENGTHOF(samples), status);
     if (U_FAILURE(status)) {
         errln("Couldn't retrieve plural samples, with error = %s", u_errorName(status));
         return;
     }
-    FixedDecimal actualFirstSample = samples[0];
+    DecimalQuantity actualFirstSample = samples[0];
 
     if (!(firstInRange == actualFirstSample)) {
         CStr descCstr(description);
@@ -776,6 +890,11 @@
 
 // For the time being, the  compact notation exponent operand `c` is an alias
 // for the scientific exponent operand `e` and compact notation.
+/**
+ * Test the proper plural rule keyword selection given an input number that is
+ * already formatted into scientific notation. This exercises the `e` plural operand
+ * for the formatted number.
+ */
 void
 PluralRulesTest::testScientificPluralKeyword() {
     IcuTestErrorCode errorCode(*this, "testScientificPluralKeyword");
@@ -838,6 +957,11 @@
     }
 }
 
+/**
+ * Test the proper plural rule keyword selection given an input number that is
+ * already formatted into compact notation. This exercises the `c` plural operand
+ * for the formatted number.
+ */
 void
 PluralRulesTest::testCompactDecimalPluralKeyword() {
     IcuTestErrorCode errorCode(*this, "testCompactDecimalPluralKeyword");
diff --git a/icu4c/source/test/intltest/plurults.h b/icu4c/source/test/intltest/plurults.h
index 76ecb6b..f02a484 100644
--- a/icu4c/source/test/intltest/plurults.h
+++ b/icu4c/source/test/intltest/plurults.h
@@ -14,6 +14,7 @@
 #if !UCONFIG_NO_FORMATTING
 
 #include "intltest.h"
+#include "number_decimalquantity.h"
 #include "unicode/localpointer.h"
 #include "unicode/plurrule.h"
 
@@ -30,7 +31,9 @@
     void testAPI();
     void testGetUniqueKeywordValue();
     void testGetSamples();
-    void testGetFixedDecimalSamples();
+    void testGetDecimalQuantitySamples();
+    void testGetOrAddSamplesFromString();
+    void testGetOrAddSamplesFromStringCompactNotation();
     void testSamplesWithExponent();
     void testSamplesWithCompactNotation();
     void testWithin();
@@ -55,7 +58,7 @@
                          const LocalPointer<PluralRules> &test,
                          UnicodeString keyword,
                          UnicodeString samplesString,
-                         FixedDecimal firstInRange);
+                         ::icu::number::impl::DecimalQuantity firstInRange);
     UnicodeString getPluralKeyword(const LocalPointer<PluralRules> &rules,
                                    Locale locale, double number, const char16_t* skeleton);
     void checkSelect(const LocalPointer<PluralRules> &rules, UErrorCode &status, 
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 bf0e411..9cc7923 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
@@ -15,21 +15,22 @@
 import java.io.ObjectOutputStream;
 import java.io.ObjectStreamException;
 import java.io.Serializable;
+import java.math.BigDecimal;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
-import java.util.TreeSet;
 import java.util.regex.Pattern;
 
 import com.ibm.icu.impl.PluralRulesLoader;
 import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.DecimalQuantity;
+import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
 import com.ibm.icu.impl.number.range.StandardPluralRanges;
 import com.ibm.icu.number.FormattedNumber;
 import com.ibm.icu.number.FormattedNumberRange;
@@ -330,6 +331,16 @@
     public static final double NO_UNIQUE_VALUE = -0.00123456777;
 
     /**
+     * Value returned by {@link #getUniqueKeywordDecimalQuantityValue} when there is no
+     * unique value to return.
+     * @internal CLDR
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public static final DecimalQuantity NO_UNIQUE_VALUE_DECIMAL_QUANTITY =
+        new DecimalQuantity_DualStorageBCD(-0.00123456777);
+
+    /**
      * Type of plurals and PluralRules.
      * @stable ICU 50
      */
@@ -868,50 +879,6 @@
         }
 
         /**
-         * @internal CLDR
-         * @deprecated This API is ICU internal only.
-         */
-        @Deprecated
-        public FixedDecimal (String n) {
-            // Ugly, but for samples we don't care.
-            this(parseDecimalSampleRangeNumString(n));
-        }
-
-        /**
-         * @internal CLDR
-         * @deprecated This API is ICU internal only
-         */
-        @Deprecated
-        private static FixedDecimal parseDecimalSampleRangeNumString(String num) {
-            if (num.contains("e") || num.contains("c")) {
-                int ePos = num.lastIndexOf('e');
-                if (ePos < 0) {
-                    ePos = num.lastIndexOf('c');
-                }
-                int expNumPos = ePos + 1;
-                String exponentStr = num.substring(expNumPos);
-                int exponent = Integer.parseInt(exponentStr);
-                String fractionStr = num.substring(0, ePos);
-                return FixedDecimal.createWithExponent(
-                        Double.parseDouble(fractionStr),
-                        getVisibleFractionCount(fractionStr),
-                        exponent);
-            } else {
-                return new FixedDecimal(Double.parseDouble(num), getVisibleFractionCount(num));
-            }
-        }
-
-        private static int getVisibleFractionCount(String value) {
-            value = value.trim();
-            int decimalPos = value.indexOf('.') + 1;
-            if (decimalPos == 0) {
-                return 0;
-            } else {
-                return value.length() - decimalPos;
-            }
-        }
-
-        /**
          * {@inheritDoc}
          *
          * @internal CLDR
@@ -1070,19 +1037,6 @@
             return (isNegative ? -source : source) * Math.pow(10, exponent);
         }
 
-        /**
-         * @internal CLDR
-         * @deprecated This API is ICU internal only.
-         */
-        @Deprecated
-        public long getShiftedValue() {
-            if (exponent != 0 && visibleDecimalDigitCount == 0 && decimalDigits == 0) {
-                // Need to take exponent into account if we have it
-                return (long)(source * Math.pow(10, exponent));
-            }
-            return integerValue * baseFactor + decimalDigits;
-        }
-
         private void writeObject(
                 ObjectOutputStream out)
                         throws IOException {
@@ -1141,31 +1095,32 @@
     }
 
     /**
-     * A range of NumberInfo that includes all values with the same visibleFractionDigitCount.
+     * A range of DecimalQuantity representing PluralRules samples that includes
+     * all values with the same visibleFractionDigitCount.
      * @internal CLDR
      * @deprecated This API is ICU internal only.
      */
     @Deprecated
-    public static class FixedDecimalRange {
+    public static class DecimalQuantitySamplesRange {
         /**
          * @internal CLDR
          * @deprecated This API is ICU internal only.
          */
         @Deprecated
-        public final FixedDecimal start;
+        public final DecimalQuantity start;
         /**
          * @internal CLDR
          * @deprecated This API is ICU internal only.
          */
         @Deprecated
-        public final FixedDecimal end;
+        public final DecimalQuantity end;
         /**
          * @internal CLDR
          * @deprecated This API is ICU internal only.
          */
         @Deprecated
-        public FixedDecimalRange(FixedDecimal start, FixedDecimal end) {
-            if (start.visibleDecimalDigitCount != end.visibleDecimalDigitCount) {
+        public DecimalQuantitySamplesRange(DecimalQuantity start, DecimalQuantity end) {
+            if (start.getPluralOperand(Operand.v)!= end.getPluralOperand(Operand.v)) {
                 throw new IllegalArgumentException("Ranges must have the same number of visible decimals: " + start + "~" + end);
             }
             this.start = start;
@@ -1178,17 +1133,18 @@
         @Deprecated
         @Override
         public String toString() {
-            return start + (end == start ? "" : "~" + end);
+            return start.toExponentString() + (end == start ? "" : "~" + end.toExponentString());
         }
     }
 
     /**
-     * A list of NumberInfo that includes all values with the same visibleFractionDigitCount.
+     * A list of DecimalQuantity representing PluralRules that includes all
+     * values with the same visibleFractionDigitCount.
      * @internal CLDR
      * @deprecated This API is ICU internal only.
      */
     @Deprecated
-    public static class FixedDecimalSamples {
+    public static class DecimalQuantitySamples {
         /**
          * @internal CLDR
          * @deprecated This API is ICU internal only.
@@ -1200,7 +1156,7 @@
          * @deprecated This API is ICU internal only.
          */
         @Deprecated
-        public final Set<FixedDecimalRange> samples;
+        public final Set<DecimalQuantitySamplesRange> samples;
         /**
          * @internal CLDR
          * @deprecated This API is ICU internal only.
@@ -1212,7 +1168,7 @@
          * @param sampleType
          * @param samples
          */
-        private FixedDecimalSamples(SampleType sampleType, Set<FixedDecimalRange> samples, boolean bounded) {
+        private DecimalQuantitySamples(SampleType sampleType, Set<DecimalQuantitySamplesRange> samples, boolean bounded) {
             super();
             this.sampleType = sampleType;
             this.samples = samples;
@@ -1221,11 +1177,11 @@
         /*
          * Parse a list of the form described in CLDR. The source must be trimmed.
          */
-        static FixedDecimalSamples parse(String source) {
+        static DecimalQuantitySamples parse(String source) {
             SampleType sampleType2;
             boolean bounded2 = true;
             boolean haveBound = false;
-            Set<FixedDecimalRange> samples2 = new LinkedHashSet<>();
+            Set<DecimalQuantitySamplesRange> samples2 = new LinkedHashSet<>();
 
             if (source.startsWith("integer")) {
                 sampleType2 = SampleType.INTEGER;
@@ -1248,25 +1204,33 @@
                 String[] rangeParts = TILDE_SEPARATED.split(range, 0);
                 switch (rangeParts.length) {
                 case 1:
-                    FixedDecimal sample = new FixedDecimal(rangeParts[0]);
+                    DecimalQuantity sample =
+                        DecimalQuantity_DualStorageBCD.fromExponentString(rangeParts[0]);
                     checkDecimal(sampleType2, sample);
-                    samples2.add(new FixedDecimalRange(sample, sample));
+                    samples2.add(new DecimalQuantitySamplesRange(sample, sample));
                     break;
                 case 2:
-                    FixedDecimal start = new FixedDecimal(rangeParts[0]);
-                    FixedDecimal end = new FixedDecimal(rangeParts[1]);
+                    DecimalQuantity start =
+                            DecimalQuantity_DualStorageBCD.fromExponentString(rangeParts[0]);
+                    DecimalQuantity end =
+                            DecimalQuantity_DualStorageBCD.fromExponentString(rangeParts[1]);
                     checkDecimal(sampleType2, start);
                     checkDecimal(sampleType2, end);
-                    samples2.add(new FixedDecimalRange(start, end));
+                    samples2.add(new DecimalQuantitySamplesRange(start, end));
                     break;
                 default: throw new IllegalArgumentException("Ill-formed number range: " + range);
                 }
             }
-            return new FixedDecimalSamples(sampleType2, Collections.unmodifiableSet(samples2), bounded2);
+            return new DecimalQuantitySamples(sampleType2, Collections.unmodifiableSet(samples2), bounded2);
         }
 
-        private static void checkDecimal(SampleType sampleType2, FixedDecimal sample) {
-            if ((sampleType2 == SampleType.INTEGER) != (sample.getVisibleDecimalDigitCount() == 0)) {
+        private static void checkDecimal(SampleType sampleType2, DecimalQuantity sample) {
+            // TODO(CLDR-15452): Remove the need for the fallback check for exponent notation integers classified
+            // as "@decimal" type samples, if/when changes are made to
+            // resolve https://unicode-org.atlassian.net/browse/CLDR-15452
+            if ((sampleType2 == SampleType.INTEGER && sample.getPluralOperand(Operand.v) != 0)
+                    || (sampleType2 == SampleType.DECIMAL && sample.getPluralOperand(Operand.v) == 0
+                        && sample.getPluralOperand(Operand.e) == 0)) {
                 throw new IllegalArgumentException("Ill-formed number range: " + sample);
             }
         }
@@ -1276,17 +1240,64 @@
          * @deprecated This API is ICU internal only.
          */
         @Deprecated
-        public Set<Double> addSamples(Set<Double> result) {
-            for (FixedDecimalRange item : samples) {
-                // we have to convert to longs so we don't get strange double issues
-                long startDouble = item.start.getShiftedValue();
-                long endDouble = item.end.getShiftedValue();
+        public Collection<Double> addSamples(Collection<Double> result) {
+            addSamples(result, null);
+            return result;
+        }
 
-                for (long d = startDouble; d <= endDouble; d += 1) {
-                    result.add(d/(double)item.start.baseFactor);
+        /**
+         * @internal CLDR
+         * @deprecated This API is ICU internal only.
+         */
+        @Deprecated
+        public Collection<DecimalQuantity> addDecimalQuantitySamples(Collection<DecimalQuantity> result) {
+            addSamples(null, result);
+            return result;
+        }
+
+        /**
+         * @internal CLDR
+         * @deprecated This API is ICU internal only.
+         */
+        @Deprecated
+        public void addSamples(Collection<Double> doubleResult, Collection<DecimalQuantity> dqResult) {
+            if ((doubleResult == null && dqResult == null)
+                    || (doubleResult != null && dqResult != null)) {
+                return;
+            }
+            boolean isDouble = doubleResult != null;
+            for (DecimalQuantitySamplesRange range : samples) {
+                DecimalQuantity start = range.start;
+                DecimalQuantity end = range.end;
+                int lowerDispMag = start.getLowerDisplayMagnitude();
+                int exponent = start.getExponent();
+                int incrementScale = lowerDispMag + exponent;
+                BigDecimal incrementBd = BigDecimal.ONE.movePointRight(incrementScale);
+
+                for (DecimalQuantity dq = start.createCopy(); dq.toDouble() <= end.toDouble(); ) {
+                    if (isDouble) {
+                        double dblValue = dq.toDouble();
+                        // Hack Alert: don't return any decimal samples with integer values that
+                        //    originated from a format with trailing decimals.
+                        //    This API is returning doubles, which can't distinguish having displayed
+                        //    zeros to the right of the decimal.
+                        //    This results in test failures with values mapping back to a different keyword.
+                        if (!(dblValue == Math.floor(dblValue)) && dq.getPluralOperand(Operand.v) > 0) {
+                            doubleResult.add(dblValue);
+                        }
+                    } else {
+                        dqResult.add(dq);
+                    }
+
+                    // Increment dq for next iteration
+                    java.math.BigDecimal dqBd = dq.toBigDecimal();
+                    java.math.BigDecimal newDqBd = dqBd.add(incrementBd);
+                    dq = new DecimalQuantity_DualStorageBCD(newDqBd);
+                    dq.setMinFraction(-lowerDispMag);
+                    dq.adjustMagnitude(-exponent);
+                    dq.adjustExponent(exponent);
                 }
             }
-            return result;
         }
 
         /**
@@ -1298,7 +1309,7 @@
         public String toString() {
             StringBuilder b = new StringBuilder("@").append(sampleType.toString().toLowerCase(Locale.ENGLISH));
             boolean first = true;
-            for (FixedDecimalRange item : samples) {
+            for (DecimalQuantitySamplesRange item : samples) {
                 if (first) {
                     first = false;
                 } else {
@@ -1317,7 +1328,7 @@
          * @deprecated This API is ICU internal only.
          */
         @Deprecated
-        public Set<FixedDecimalRange> getSamples() {
+        public Set<DecimalQuantitySamplesRange> getSamples() {
             return samples;
         }
 
@@ -1326,10 +1337,10 @@
          * @deprecated This API is ICU internal only.
          */
         @Deprecated
-        public void getStartEndSamples(Set<FixedDecimal> target) {
-            for (FixedDecimalRange item : samples) {
-                target.add(item.start);
-                target.add(item.end);
+        public void getStartEndSamples(Set<DecimalQuantity> target) {
+            for (DecimalQuantitySamplesRange range : samples) {
+                target.add(range.start);
+                target.add(range.end);
             }
         }
     }
@@ -1610,19 +1621,19 @@
         description = description.substring(x+1).trim();
         String[] constraintOrSamples = AT_SEPARATED.split(description, 0);
         boolean sampleFailure = false;
-        FixedDecimalSamples integerSamples = null, decimalSamples = null;
+        DecimalQuantitySamples integerSamples = null, decimalSamples = null;
         switch (constraintOrSamples.length) {
         case 1: break;
         case 2:
-            integerSamples = FixedDecimalSamples.parse(constraintOrSamples[1]);
+            integerSamples = DecimalQuantitySamples.parse(constraintOrSamples[1]);
             if (integerSamples.sampleType == SampleType.DECIMAL) {
                 decimalSamples = integerSamples;
                 integerSamples = null;
             }
             break;
         case 3:
-            integerSamples = FixedDecimalSamples.parse(constraintOrSamples[1]);
-            decimalSamples = FixedDecimalSamples.parse(constraintOrSamples[2]);
+            integerSamples = DecimalQuantitySamples.parse(constraintOrSamples[1]);
+            decimalSamples = DecimalQuantitySamples.parse(constraintOrSamples[2]);
             if (integerSamples.sampleType != SampleType.INTEGER || decimalSamples.sampleType != SampleType.DECIMAL) {
                 throw new IllegalArgumentException("Must have @integer then @decimal in " + description);
             }
@@ -1857,10 +1868,10 @@
         private static final long serialVersionUID = 1;
         private final String keyword;
         private final Constraint constraint;
-        private final FixedDecimalSamples integerSamples;
-        private final FixedDecimalSamples decimalSamples;
+        private final DecimalQuantitySamples integerSamples;
+        private final DecimalQuantitySamples decimalSamples;
 
-        public Rule(String keyword, Constraint constraint, FixedDecimalSamples integerSamples, FixedDecimalSamples decimalSamples) {
+        public Rule(String keyword, Constraint constraint, DecimalQuantitySamples integerSamples, DecimalQuantitySamples decimalSamples) {
             this.keyword = keyword;
             this.constraint = constraint;
             this.integerSamples = integerSamples;
@@ -1972,7 +1983,7 @@
 
         public boolean isLimited(String keyword, SampleType sampleType) {
             if (hasExplicitBoundingInfo) {
-                FixedDecimalSamples mySamples = getDecimalSamples(keyword, sampleType);
+                DecimalQuantitySamples mySamples = getDecimalSamples(keyword, sampleType);
                 return mySamples == null ? true : mySamples.bounded;
             }
 
@@ -2024,7 +2035,7 @@
             return false;
         }
 
-        public FixedDecimalSamples getDecimalSamples(String keyword, SampleType sampleType) {
+        public DecimalQuantitySamples getDecimalSamples(String keyword, SampleType sampleType) {
             for (Rule rule : rules) {
                 if (rule.getKeyword().equals(keyword)) {
                     return sampleType == SampleType.INTEGER ? rule.integerSamples : rule.decimalSamples;
@@ -2285,11 +2296,29 @@
      * @stable ICU 4.8
      */
     public double getUniqueKeywordValue(String keyword) {
-        Collection<Double> values = getAllKeywordValues(keyword);
+        DecimalQuantity uniqValDq = getUniqueKeywordDecimalQuantityValue(keyword);
+        if (uniqValDq.equals(NO_UNIQUE_VALUE_DECIMAL_QUANTITY)) {
+            return NO_UNIQUE_VALUE;
+        } else {
+            return uniqValDq.toDouble();
+        }
+    }
+
+    /**
+     * Returns the unique value that this keyword matches, or {@link #NO_UNIQUE_VALUE}
+     * if the keyword matches multiple values or is not defined for this PluralRules.
+     *
+     * @param keyword the keyword to check for a unique value
+     * @internal Visible For Testing
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public DecimalQuantity getUniqueKeywordDecimalQuantityValue(String keyword) {
+        Collection<DecimalQuantity> values = getAllKeywordDecimalQuantityValues(keyword);
         if (values != null && values.size() == 1) {
             return values.iterator().next();
         }
-        return NO_UNIQUE_VALUE;
+        return NO_UNIQUE_VALUE_DECIMAL_QUANTITY;
     }
 
     /**
@@ -2302,6 +2331,31 @@
      * @stable ICU 4.8
      */
     public Collection<Double> getAllKeywordValues(String keyword) {
+        Collection<DecimalQuantity> samples = getAllKeywordDecimalQuantityValues(keyword);
+        if (samples == null) {
+            return null;
+        } else {
+            Collection<Double> result = new LinkedHashSet<>();
+            for (DecimalQuantity dq : samples) {
+                result.add(dq.toDouble());
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Returns all the values that trigger this keyword, or null if the number of such
+     * values is unlimited.
+     *
+     * @param keyword the keyword
+     * @return the values that trigger this keyword, or null.  The returned collection
+     * is immutable. It will be empty if the keyword is not defined.
+     *
+     * @internal Visible For Testing
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public Collection<DecimalQuantity> getAllKeywordDecimalQuantityValues(String keyword) {
         return getAllKeywordValues(keyword, SampleType.INTEGER);
     }
 
@@ -2318,12 +2372,11 @@
      * @deprecated This API is ICU internal only.
      */
     @Deprecated
-    public Collection<Double> getAllKeywordValues(String keyword, SampleType type) {
+    public Collection<DecimalQuantity> getAllKeywordValues(String keyword, SampleType type) {
         if (!isLimited(keyword, type)) {
             return null;
         }
-        Collection<Double> samples = getSamples(keyword, type);
-        return samples == null ? null : Collections.unmodifiableCollection(samples);
+        return getDecimalQuantitySamples(keyword, type);
     }
 
     /**
@@ -2341,6 +2394,22 @@
     }
 
     /**
+     * Returns a list of integer values for which select() would return that keyword,
+     * or null if the keyword is not defined. The returned collection is unmodifiable.
+     * The returned list is not complete, and there might be additional values that
+     * would return the keyword.
+     *
+     * @param keyword the keyword to test
+     * @return a list of values matching the keyword.
+     * @internal CLDR
+     * @deprecated ICU internal only
+     */
+    @Deprecated
+  public Collection<DecimalQuantity> getDecimalQuantitySamples(String keyword) {
+        return getDecimalQuantitySamples(keyword, SampleType.INTEGER);
+    }
+
+    /**
      * Returns a list of values for which select() would return that keyword,
      * or null if the keyword is not defined.
      * The returned collection is unmodifiable.
@@ -2356,15 +2425,43 @@
      */
     @Deprecated
     public Collection<Double> getSamples(String keyword, SampleType sampleType) {
+        Collection<DecimalQuantity> samples = getDecimalQuantitySamples(keyword, sampleType);
+        if (samples == null) {
+            return null;
+        } else {
+            Collection<Double> result = new LinkedHashSet<>();
+            for (DecimalQuantity dq: samples) {
+                result.add(dq.toDouble());
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Returns a list of values for which select() would return that keyword,
+     * or null if the keyword is not defined.
+     * The returned collection is unmodifiable.
+     * The returned list is not complete, and there might be additional values that
+     * would return the keyword. The keyword might be defined, and yet have an empty set of samples,
+     * IF there are samples for the other sampleType.
+     *
+     * @param keyword the keyword to test
+     * @param sampleType the type of samples requested, INTEGER or DECIMAL
+     * @return a list of values matching the keyword.
+     * @internal CLDR
+     * @deprecated ICU internal only
+     */
+    @Deprecated
+    public Collection<DecimalQuantity> getDecimalQuantitySamples(String keyword, SampleType sampleType) {
         if (!keywords.contains(keyword)) {
             return null;
         }
-        Set<Double> result = new TreeSet<>();
+        Set<DecimalQuantity> result = new LinkedHashSet<>();
 
         if (rules.hasExplicitBoundingInfo) {
-            FixedDecimalSamples samples = rules.getDecimalSamples(keyword, sampleType);
+            DecimalQuantitySamples samples = rules.getDecimalSamples(keyword, sampleType);
             return samples == null ? Collections.unmodifiableSet(result)
-                    : Collections.unmodifiableSet(samples.addSamples(result));
+                    : Collections.unmodifiableCollection(samples.addDecimalQuantitySamples(result));
         }
 
         // hack in case the rule is created without explicit samples
@@ -2373,28 +2470,31 @@
         switch (sampleType) {
         case INTEGER:
             for (int i = 0; i < 200; ++i) {
-                if (!addSample(keyword, i, maxCount, result)) {
+                if (!addSample(keyword, new DecimalQuantity_DualStorageBCD(i), maxCount, result)) {
                     break;
                 }
             }
-            addSample(keyword, 1000000, maxCount, result); // hack for Welsh
+            addSample(keyword, new DecimalQuantity_DualStorageBCD(1000000), maxCount, result); // hack for Welsh
             break;
         case DECIMAL:
             for (int i = 0; i < 2000; ++i) {
-                if (!addSample(keyword, new FixedDecimal(i/10d, 1), maxCount, result)) {
+                DecimalQuantity_DualStorageBCD nextSample = new DecimalQuantity_DualStorageBCD(i);
+                nextSample.adjustMagnitude(-1);
+                if (!addSample(keyword, nextSample, maxCount, result)) {
                     break;
                 }
             }
-            addSample(keyword, new FixedDecimal(1000000d, 1), maxCount, result); // hack for Welsh
+            addSample(keyword, DecimalQuantity_DualStorageBCD.fromExponentString("1000000.0"), maxCount, result); // hack for Welsh
             break;
         }
+
         return result.size() == 0 ? null : Collections.unmodifiableSet(result);
     }
 
-    private boolean addSample(String keyword, Number sample, int maxCount, Set<Double> result) {
-        String selectedKeyword = sample instanceof FixedDecimal ? select((FixedDecimal)sample) : select(sample.doubleValue());
+    private boolean addSample(String keyword, DecimalQuantity sample, int maxCount, Set<DecimalQuantity> result) {
+        String selectedKeyword = select(sample);
         if (selectedKeyword.equals(keyword)) {
-            result.add(sample.doubleValue());
+            result.add(sample);
             if (--maxCount < 0) {
                 return false;
             }
@@ -2416,7 +2516,7 @@
      * @deprecated This API is ICU internal only.
      */
     @Deprecated
-    public FixedDecimalSamples getDecimalSamples(String keyword, SampleType sampleType) {
+    public DecimalQuantitySamples getDecimalSamples(String keyword, SampleType sampleType) {
         return rules.getDecimalSamples(keyword, sampleType);
     }
 
@@ -2525,14 +2625,14 @@
      *            the offset used, or 0.0d if not. Internally, the offset is subtracted from each explicit value before
      *            checking against the keyword values.
      * @param explicits
-     *            a set of Doubles that are used explicitly (eg [=0], "[=1]"). May be empty or null.
+     *            a set of {@code DecimalQuantity}s that are used explicitly (eg [=0], "[=1]"). May be empty or null.
      * @param uniqueValue
      *            If non null, set to the unique value.
      * @return the KeywordStatus
      * @draft ICU 50
      */
-    public KeywordStatus getKeywordStatus(String keyword, int offset, Set<Double> explicits,
-            Output<Double> uniqueValue) {
+    public KeywordStatus getKeywordStatus(String keyword, int offset, Set<DecimalQuantity> explicits,
+            Output<DecimalQuantity> uniqueValue) {
         return getKeywordStatus(keyword, offset, explicits, uniqueValue, SampleType.INTEGER);
     }
     /**
@@ -2544,7 +2644,7 @@
      *            the offset used, or 0.0d if not. Internally, the offset is subtracted from each explicit value before
      *            checking against the keyword values.
      * @param explicits
-     *            a set of Doubles that are used explicitly (eg [=0], "[=1]"). May be empty or null.
+     *            a set of {@code DecimalQuantity}s that are used explicitly (eg [=0], "[=1]"). May be empty or null.
      * @param sampleType
      *            request KeywordStatus relative to INTEGER or DECIMAL values
      * @param uniqueValue
@@ -2554,8 +2654,8 @@
      * @deprecated This API is ICU internal only.
      */
     @Deprecated
-    public KeywordStatus getKeywordStatus(String keyword, int offset, Set<Double> explicits,
-            Output<Double> uniqueValue, SampleType sampleType) {
+    public KeywordStatus getKeywordStatus(String keyword, int offset,
+            Set<DecimalQuantity> explicits, Output<DecimalQuantity> uniqueValue, SampleType sampleType) {
         if (uniqueValue != null) {
             uniqueValue.value = null;
         }
@@ -2568,7 +2668,7 @@
             return KeywordStatus.UNBOUNDED;
         }
 
-        Collection<Double> values = getSamples(keyword, sampleType);
+        Collection<DecimalQuantity> values = getDecimalQuantitySamples(keyword, sampleType);
 
         int originalSize = values.size();
 
@@ -2590,9 +2690,12 @@
 
         // Compute if the quick test is insufficient.
 
-        HashSet<Double> subtractedSet = new HashSet<>(values);
-        for (Double explicit : explicits) {
-            subtractedSet.remove(explicit - offset);
+        ArrayList<DecimalQuantity> subtractedSet = new ArrayList<>(values);
+        for (DecimalQuantity explicit : explicits) {
+            BigDecimal explicitBd = explicit.toBigDecimal();
+            BigDecimal valToRemoveBd = explicitBd.subtract(new BigDecimal(offset));
+            DecimalQuantity_DualStorageBCD valToRemove = new DecimalQuantity_DualStorageBCD(valToRemoveBd);
+            subtractedSet.remove(valToRemove);
         }
         if (subtractedSet.size() == 0) {
             return KeywordStatus.SUPPRESSED;
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralSamples.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralSamples.java
deleted file mode 100644
index 6b260c6..0000000
--- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralSamples.java
+++ /dev/null
@@ -1,332 +0,0 @@
-// © 2016 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
-/*
- *******************************************************************************
- * Copyright (C) 2013-2015, International Business Machines Corporation and
- * others. All Rights Reserved.
- *******************************************************************************
- */
-package com.ibm.icu.text;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TreeSet;
-
-import com.ibm.icu.text.PluralRules.FixedDecimal;
-import com.ibm.icu.text.PluralRules.KeywordStatus;
-import com.ibm.icu.util.Output;
-
-/**
- * @author markdavis
- * Refactor samples as first step to moving into CLDR
- * 
- * @internal
- * @deprecated This API is ICU internal only.
- */
-@Deprecated
-public class PluralSamples {
-
-    private PluralRules pluralRules;
-    private final Map<String, List<Double>> _keySamplesMap;
-
-    /**
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public final Map<String, Boolean> _keyLimitedMap;
-    private final Map<String, Set<FixedDecimal>> _keyFractionSamplesMap;
-    private final Set<FixedDecimal> _fractionSamples;
-
-    /**
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public PluralSamples(PluralRules pluralRules) {
-        this.pluralRules = pluralRules;
-        Set<String> keywords = pluralRules.getKeywords();
-        // ensure both _keySamplesMap and _keyLimitedMap are initialized.
-        // If this were allowed to vary on a per-call basis, we'd have to recheck and
-        // possibly rebuild the samples cache.  Doesn't seem worth it.
-        // This 'max samples' value only applies to keywords that are unlimited, for
-        // other keywords all the matching values are returned.  This might be a lot.
-        final int MAX_SAMPLES = 3;
-
-        Map<String, Boolean> temp = new HashMap<String, Boolean>();
-        for (String k : keywords) {
-            temp.put(k, pluralRules.isLimited(k));
-        }
-        _keyLimitedMap = temp;
-
-        Map<String, List<Double>> sampleMap = new HashMap<String, List<Double>>();
-        int keywordsRemaining = keywords.size();
-
-        int limit = 128; // Math.max(5, getRepeatLimit() * MAX_SAMPLES) * 2;
-
-        for (int i = 0; keywordsRemaining > 0 && i < limit; ++i) {
-            keywordsRemaining = addSimpleSamples(pluralRules, MAX_SAMPLES, sampleMap, keywordsRemaining, i / 2.0);
-        }
-        // Hack for Celtic
-        keywordsRemaining = addSimpleSamples(pluralRules, MAX_SAMPLES, sampleMap, keywordsRemaining, 1000000);
-
-
-        // collect explicit samples
-        Map<String, Set<FixedDecimal>> sampleFractionMap = new HashMap<String, Set<FixedDecimal>>();
-        Set<FixedDecimal> mentioned = new TreeSet<FixedDecimal>();
-        // make sure that there is at least one 'other' value
-        Map<String, Set<FixedDecimal>> foundKeywords = new HashMap<String, Set<FixedDecimal>>();
-        for (FixedDecimal s : mentioned) {
-            String keyword = pluralRules.select(s);
-            addRelation(foundKeywords, keyword, s);
-        }
-        main:
-            if (foundKeywords.size() != keywords.size()) {
-                for (int i = 1; i < 1000; ++i) {
-                    boolean done = addIfNotPresent(i, mentioned, foundKeywords);
-                    if (done) break main;
-                }
-                // if we are not done, try tenths
-                for (int i = 10; i < 1000; ++i) {
-                    boolean done = addIfNotPresent(i/10d, mentioned, foundKeywords);
-                    if (done) break main;
-                }
-                System.out.println("Failed to find sample for each keyword: " + foundKeywords + "\n\t" + pluralRules + "\n\t" + mentioned);
-            }
-        mentioned.add(new FixedDecimal(0)); // always there
-        mentioned.add(new FixedDecimal(1)); // always there
-        mentioned.add(new FixedDecimal(2)); // always there
-        mentioned.add(new FixedDecimal(0.1,1)); // always there
-        mentioned.add(new FixedDecimal(1.99,2)); // always there
-        mentioned.addAll(fractions(mentioned));
-        for (FixedDecimal s : mentioned) {
-            String keyword = pluralRules.select(s);
-            Set<FixedDecimal> list = sampleFractionMap.get(keyword);
-            if (list == null) {
-                list = new LinkedHashSet<FixedDecimal>(); // will be sorted because the iteration is
-                sampleFractionMap.put(keyword, list);
-            }
-            list.add(s);
-        }
-
-        if (keywordsRemaining > 0) {
-            for (String k : keywords) {
-                if (!sampleMap.containsKey(k)) {
-                    sampleMap.put(k, Collections.<Double>emptyList());
-                }
-                if (!sampleFractionMap.containsKey(k)) {
-                    sampleFractionMap.put(k, Collections.<FixedDecimal>emptySet());
-                }
-            }
-        }
-
-        // Make lists immutable so we can return them directly
-        for (Entry<String, List<Double>> entry : sampleMap.entrySet()) {
-            sampleMap.put(entry.getKey(), Collections.unmodifiableList(entry.getValue()));
-        }
-        for (Entry<String, Set<FixedDecimal>> entry : sampleFractionMap.entrySet()) {
-            sampleFractionMap.put(entry.getKey(), Collections.unmodifiableSet(entry.getValue()));
-        }
-        _keySamplesMap = sampleMap;
-        _keyFractionSamplesMap = sampleFractionMap;
-        _fractionSamples = Collections.unmodifiableSet(mentioned);
-    }
-
-    private int addSimpleSamples(PluralRules pluralRules, final int MAX_SAMPLES, Map<String, List<Double>> sampleMap,
-            int keywordsRemaining, double val) {
-        String keyword = pluralRules.select(val);
-        boolean keyIsLimited = _keyLimitedMap.get(keyword);
-
-        List<Double> list = sampleMap.get(keyword);
-        if (list == null) {
-            list = new ArrayList<Double>(MAX_SAMPLES);
-            sampleMap.put(keyword, list);
-        } else if (!keyIsLimited && list.size() == MAX_SAMPLES) {
-            return keywordsRemaining;
-        }
-        list.add(Double.valueOf(val));
-
-        if (!keyIsLimited && list.size() == MAX_SAMPLES) {
-            --keywordsRemaining;
-        }
-        return keywordsRemaining;
-    }
-
-    private void addRelation(Map<String, Set<FixedDecimal>> foundKeywords, String keyword, FixedDecimal s) {
-        Set<FixedDecimal> set = foundKeywords.get(keyword);
-        if (set == null) {
-            foundKeywords.put(keyword, set = new HashSet<FixedDecimal>());
-        }
-        set.add(s);
-    }
-
-    private boolean addIfNotPresent(double d, Set<FixedDecimal> mentioned, Map<String, Set<FixedDecimal>> foundKeywords) {
-        FixedDecimal numberInfo = new FixedDecimal(d);
-        String keyword = pluralRules.select(numberInfo);
-        if (!foundKeywords.containsKey(keyword) || keyword.equals("other")) {
-            addRelation(foundKeywords, keyword, numberInfo);
-            mentioned.add(numberInfo);
-            if (keyword.equals("other")) {
-                if (foundKeywords.get("other").size() > 1) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    private static final int[] TENS = {1, 10, 100, 1000, 10000, 100000, 1000000};
-
-    private static final int LIMIT_FRACTION_SAMPLES = 3;
-
-
-    private Set<FixedDecimal> fractions(Set<FixedDecimal> original) {
-        Set<FixedDecimal> toAddTo = new HashSet<FixedDecimal>();
-
-        Set<Integer> result = new HashSet<Integer>();
-        for (FixedDecimal base1 : original) {
-            result.add((int)base1.integerValue);
-        }
-        List<Integer> ints = new ArrayList<Integer>(result);
-        Set<String> keywords = new HashSet<String>();
-
-        for (int j = 0; j < ints.size(); ++j) {
-            Integer base = ints.get(j);
-            String keyword = pluralRules.select(base);
-            if (keywords.contains(keyword)) {
-                continue;
-            }
-            keywords.add(keyword);
-            toAddTo.add(new FixedDecimal(base,1)); // add .0
-            toAddTo.add(new FixedDecimal(base,2)); // add .00
-            Integer fract = getDifferentCategory(ints, keyword);
-            if (fract >= TENS[LIMIT_FRACTION_SAMPLES-1]) { // make sure that we always get the value
-                toAddTo.add(new FixedDecimal(base + "." + fract));
-            } else {
-                for (int visibleFractions = 1; visibleFractions < LIMIT_FRACTION_SAMPLES; ++visibleFractions) {
-                    for (int i = 1; i <= visibleFractions; ++i) {
-                        // with visible fractions = 3, and fract = 1, then we should get x.10, 0.01
-                        // with visible fractions = 3, and fract = 15, then we should get x.15, x.15
-                        if (fract >= TENS[i]) {
-                            continue;
-                        }
-                        toAddTo.add(new FixedDecimal(base + fract/(double)TENS[i], visibleFractions));
-                    }
-                }
-            }
-        }
-        return toAddTo;
-    }
-
-    private Integer getDifferentCategory(List<Integer> ints, String keyword) {
-        for (int i = ints.size() - 1; i >= 0; --i) {
-            Integer other = ints.get(i);
-            String keywordOther = pluralRules.select(other);
-            if (!keywordOther.equals(keyword)) {
-                return other;
-            }
-        }
-        return 37;
-    }
-
-    /**
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public KeywordStatus getStatus(String keyword, int offset, Set<Double> explicits, Output<Double> uniqueValue) {
-        if (uniqueValue != null) {
-            uniqueValue.value = null;
-        }
-
-        if (!pluralRules.getKeywords().contains(keyword)) {
-            return KeywordStatus.INVALID;
-        }
-        Collection<Double> values = pluralRules.getAllKeywordValues(keyword);
-        if (values == null) {
-            return KeywordStatus.UNBOUNDED;
-        }
-        int originalSize = values.size();
-
-        if (explicits == null) {
-            explicits = Collections.emptySet();
-        }
-
-        // Quick check on whether there are multiple elements
-
-        if (originalSize > explicits.size()) {
-            if (originalSize == 1) {
-                if (uniqueValue != null) {
-                    uniqueValue.value = values.iterator().next();
-                }
-                return KeywordStatus.UNIQUE;
-            }
-            return KeywordStatus.BOUNDED;
-        }
-
-        // Compute if the quick test is insufficient.
-
-        HashSet<Double> subtractedSet = new HashSet<Double>(values);
-        for (Double explicit : explicits) {
-            subtractedSet.remove(explicit - offset);
-        }
-        if (subtractedSet.size() == 0) {
-            return KeywordStatus.SUPPRESSED;
-        }
-
-        if (uniqueValue != null && subtractedSet.size() == 1) {
-            uniqueValue.value = subtractedSet.iterator().next();
-        }
-
-        return originalSize == 1 ? KeywordStatus.UNIQUE : KeywordStatus.BOUNDED;
-    }
-
-    Map<String, List<Double>> getKeySamplesMap() {
-        return _keySamplesMap;
-    }
-
-    Map<String, Set<FixedDecimal>> getKeyFractionSamplesMap() {
-        return _keyFractionSamplesMap;
-    }
-
-    Set<FixedDecimal> getFractionSamples() {
-        return _fractionSamples;
-    }
-
-    /**
-     * Returns all the values that trigger this keyword, or null if the number of such
-     * values is unlimited.
-     *
-     * @param keyword the keyword
-     * @return the values that trigger this keyword, or null.  The returned collection
-     * is immutable. It will be empty if the keyword is not defined.
-     * @stable ICU 4.8
-     */
-
-    Collection<Double> getAllKeywordValues(String keyword) {
-        // HACK for now
-        if (!pluralRules.getKeywords().contains(keyword)) {
-            return Collections.<Double>emptyList();
-        }
-        Collection<Double> result = getKeySamplesMap().get(keyword);
-
-        // We depend on MAX_SAMPLES here.  It's possible for a conjunction
-        // of unlimited rules that 'looks' unlimited to return a limited
-        // number of values.  There's no bounds to this limited number, in
-        // general, because you can construct arbitrarily complex rules.  Since
-        // we always generate 3 samples if a rule is really unlimited, that's
-        // where we put the cutoff.
-        if (result.size() > 2 && !_keyLimitedMap.get(keyword)) {
-            return null;
-        }
-        return result;
-    }
-}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralFormatUnitTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralFormatUnitTest.java
index 0b22c8e..1e2c0d9 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralFormatUnitTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralFormatUnitTest.java
@@ -23,6 +23,7 @@
 import org.junit.runners.JUnit4;
 
 import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.impl.number.DecimalQuantity;
 import com.ibm.icu.text.DecimalFormat;
 import com.ibm.icu.text.DecimalFormatSymbols;
 import com.ibm.icu.text.MessageFormat;
@@ -228,10 +229,10 @@
             logln(localeName + "\ttoString\t" + rules.toString());
             Set<String> keywords = rules.getKeywords();
             for (String keyword : keywords) {
-                Collection<Double> list = rules.getSamples(keyword);
+                Collection<DecimalQuantity> list = rules.getDecimalQuantitySamples(keyword);
                 if (list.size() == 0) {
                     // if there aren't any integer samples, get the decimal ones.
-                    list = rules.getSamples(keyword, SampleType.DECIMAL);
+                    list = rules.getDecimalQuantitySamples(keyword, SampleType.DECIMAL);
                 }
 
                 if (list == null || list.size() == 0) {
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 d07f41e..f0bed53 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
@@ -23,6 +23,7 @@
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
@@ -31,6 +32,8 @@
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -41,6 +44,8 @@
 import com.ibm.icu.dev.util.CollectionUtilities;
 import com.ibm.icu.impl.Relation;
 import com.ibm.icu.impl.Utility;
+import com.ibm.icu.impl.number.DecimalQuantity;
+import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
 import com.ibm.icu.number.FormattedNumber;
 import com.ibm.icu.number.FormattedNumberRange;
 import com.ibm.icu.number.LocalizedNumberFormatter;
@@ -50,9 +55,9 @@
 import com.ibm.icu.number.UnlocalizedNumberFormatter;
 import com.ibm.icu.text.NumberFormat;
 import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.text.PluralRules.DecimalQuantitySamples;
+import com.ibm.icu.text.PluralRules.DecimalQuantitySamplesRange;
 import com.ibm.icu.text.PluralRules.FixedDecimal;
-import com.ibm.icu.text.PluralRules.FixedDecimalRange;
-import com.ibm.icu.text.PluralRules.FixedDecimalSamples;
 import com.ibm.icu.text.PluralRules.KeywordStatus;
 import com.ibm.icu.text.PluralRules.PluralType;
 import com.ibm.icu.text.PluralRules.SampleType;
@@ -177,17 +182,27 @@
         PluralRules test = PluralRules.createRules(description);
 
         checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 3, 19", true,
-                new FixedDecimal(3));
+                DecimalQuantity_DualStorageBCD.fromExponentString("3"));
         checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 3.50~3.53, …", false,
-                new FixedDecimal(3.5, 2));
-        checkOldSamples(description, test, "one", SampleType.INTEGER, 3d, 19d);
-        checkOldSamples(description, test, "one", SampleType.DECIMAL, 3.5d, 3.51d, 3.52d, 3.53d);
+                DecimalQuantity_DualStorageBCD.fromExponentString("3.50"));
+        checkOldSamples(description, test, "one", SampleType.INTEGER,
+                DecimalQuantity_DualStorageBCD.fromExponentString("3"),
+                DecimalQuantity_DualStorageBCD.fromExponentString("19"));
+        checkOldSamples(description, test, "one", SampleType.DECIMAL,
+                DecimalQuantity_DualStorageBCD.fromExponentString("3.50"),
+                DecimalQuantity_DualStorageBCD.fromExponentString("3.51"),
+                DecimalQuantity_DualStorageBCD.fromExponentString("3.52"),
+                DecimalQuantity_DualStorageBCD.fromExponentString("3.53"));
 
         checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "", true, null);
         checkNewSamples(description, test, "other", PluralRules.SampleType.DECIMAL, "@decimal 99.0~99.2, 999.0, …",
-                false, new FixedDecimal(99d, 1));
+                false, DecimalQuantity_DualStorageBCD.fromExponentString("99.0"));
         checkOldSamples(description, test, "other", SampleType.INTEGER);
-        checkOldSamples(description, test, "other", SampleType.DECIMAL, 99d, 99.1, 99.2d, 999d);
+        checkOldSamples(description, test, "other", SampleType.DECIMAL,
+                DecimalQuantity_DualStorageBCD.fromExponentString("99.0"),
+                DecimalQuantity_DualStorageBCD.fromExponentString("99.1"),
+                DecimalQuantity_DualStorageBCD.fromExponentString("99.2"),
+                DecimalQuantity_DualStorageBCD.fromExponentString("999.0"));
     }
 
     /**
@@ -205,22 +220,26 @@
         // Creating the PluralRules object means being able to parse numbers
         // like 1e5 and 1.1e5
         PluralRules test = PluralRules.createRules(description);
+
+        // Currently, 'c' is the canonical representation of numbers with suppressed exponent, and 'e'
+        // is an alias. The test helpers here skip 'e' for round-trip sample string parsing and formatting.
+
         checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 0, 1, 1e5", true,
-                new FixedDecimal(0));
+                DecimalQuantity_DualStorageBCD.fromExponentString("0"));
         checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 0.0~1.5, 1.1e5", true,
-                new FixedDecimal(0, 1));
+                DecimalQuantity_DualStorageBCD.fromExponentString("0.0"));
         checkNewSamples(description, test, "many", PluralRules.SampleType.INTEGER, "@integer 1000000, 2e6, 3e6, 4e6, 5e6, 6e6, 7e6, …", false,
-                new FixedDecimal(1000000));
+                DecimalQuantity_DualStorageBCD.fromExponentString("1000000"));
         checkNewSamples(description, test, "many", PluralRules.SampleType.DECIMAL, "@decimal 2.1e6, 3.1e6, 4.1e6, 5.1e6, 6.1e6, 7.1e6, …", false,
-                FixedDecimal.createWithExponent(2.1, 1, 6));
+                DecimalQuantity_DualStorageBCD.fromExponentString("2.1c6"));
         checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "@integer 2~17, 100, 1000, 10000, 100000, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, …", false,
-                new FixedDecimal(2));
+                DecimalQuantity_DualStorageBCD.fromExponentString("2"));
         checkNewSamples(description, test, "other", PluralRules.SampleType.DECIMAL, "@decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1e5, 3.1e5, 4.1e5, 5.1e5, 6.1e5, 7.1e5, …", false,
-                new FixedDecimal(2.0, 1));
+                DecimalQuantity_DualStorageBCD.fromExponentString("2.0"));
     }
 
     /**
-     * This test is for the support of X.YcZ compactnotation of numbers in
+     * This test is for the support of X.YcZ compact notation of numbers in
      * the plural sample string.
      */
     @Test
@@ -236,34 +255,53 @@
         // Note: Since `c` is currently an alias to `e`, the toString() of
         // FixedDecimal will return "1e5" even when input is "1c5".
         PluralRules test = PluralRules.createRules(description);
-        checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 0, 1, 1e5", true,
-                new FixedDecimal(0));
-        checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 0.0~1.5, 1.1e5", true,
-                new FixedDecimal(0, 1));
-        checkNewSamples(description, test, "many", PluralRules.SampleType.INTEGER, "@integer 1000000, 2e6, 3e6, 4e6, 5e6, 6e6, 7e6, …", false,
-                new FixedDecimal(1000000));
-        checkNewSamples(description, test, "many", PluralRules.SampleType.DECIMAL, "@decimal 2.1e6, 3.1e6, 4.1e6, 5.1e6, 6.1e6, 7.1e6, …", false,
-                FixedDecimal.createWithExponent(2.1, 1, 6));
-        checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "@integer 2~17, 100, 1000, 10000, 100000, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, …", false,
-                new FixedDecimal(2));
-        checkNewSamples(description, test, "other", PluralRules.SampleType.DECIMAL, "@decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1e5, 3.1e5, 4.1e5, 5.1e5, 6.1e5, 7.1e5, …", false,
-                new FixedDecimal(2.0, 1));
+
+        checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 0, 1, 1c5", true,
+                DecimalQuantity_DualStorageBCD.fromExponentString("0"));
+        checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 0.0~1.5, 1.1c5", true,
+                DecimalQuantity_DualStorageBCD.fromExponentString("0.0"));
+        checkNewSamples(description, test, "many", PluralRules.SampleType.INTEGER, "@integer 1000000, 2c6, 3c6, 4c6, 5c6, 6c6, 7c6, …", false,
+                DecimalQuantity_DualStorageBCD.fromExponentString("1000000"));
+        checkNewSamples(description, test, "many", PluralRules.SampleType.DECIMAL, "@decimal 2.1c6, 3.1c6, 4.1c6, 5.1c6, 6.1c6, 7.1c6, …", false,
+                DecimalQuantity_DualStorageBCD.fromExponentString("2.1c6"));
+        checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "@integer 2~17, 100, 1000, 10000, 100000, 2c5, 3c5, 4c5, 5c5, 6c5, 7c5, …", false,
+                DecimalQuantity_DualStorageBCD.fromExponentString("2"));
+        checkNewSamples(description, test, "other", PluralRules.SampleType.DECIMAL, "@decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1c5, 3.1c5, 4.1c5, 5.1c5, 6.1c5, 7.1c5, …", false,
+                DecimalQuantity_DualStorageBCD.fromExponentString("2.0"));
     }
 
     public void checkOldSamples(String description, PluralRules rules, String keyword, SampleType sampleType,
-            Double... expected) {
-        Collection<Double> oldSamples = rules.getSamples(keyword, sampleType);
-        if (!assertEquals("getOldSamples; " + keyword + "; " + description, new HashSet(Arrays.asList(expected)),
-                oldSamples)) {
+            DecimalQuantity... expected) {
+        Collection<DecimalQuantity> oldSamples = rules.getDecimalQuantitySamples(keyword, sampleType);
+
+        // Collect actual (oldSamples) and expected (expectedSamplesList) into the
+        // same concrete collection for comparison purposes.
+        ArrayList<DecimalQuantity> oldSamplesList = new ArrayList(oldSamples);
+        ArrayList<DecimalQuantity> expectedSamplesList = new ArrayList(Arrays.asList(expected));
+
+        if (!assertEquals("getOldSamples; " + keyword + "; " + description, expectedSamplesList,
+                oldSamplesList)) {
             rules.getSamples(keyword, sampleType);
         }
     }
 
     public void checkNewSamples(String description, PluralRules test, String keyword, SampleType sampleType,
-            String samplesString, boolean isBounded, FixedDecimal firstInRange) {
+            String samplesString, boolean isBounded, DecimalQuantity firstInRange) {
         String title = description + ", " + sampleType;
-        FixedDecimalSamples samples = test.getDecimalSamples(keyword, sampleType);
+        DecimalQuantitySamples samples = test.getDecimalSamples(keyword, sampleType);
         if (samples != null) {
+
+            // For now, skip round-trip formatting test when samples string uses
+            // 'e' instead of 'c' for compact notation.
+
+            // We are skipping tests for 'e' by replacing 'e' with 'c' in exponent
+            // notation.
+            Pattern p = Pattern.compile("(\\d+)(e)([-]?\\d+)");
+            Matcher m = p.matcher(samplesString);
+            if (m.find()) {
+                samplesString = m.replaceAll("$1c$3");
+            }
+
             assertEquals("samples; " + title, samplesString, samples.toString());
             assertEquals("bounded; " + title, isBounded, samples.bounded);
             assertEquals("first; " + title, firstInRange, samples.samples.iterator().next().start);
@@ -391,11 +429,11 @@
         main: for (ULocale locale : factory.getAvailableULocales()) {
             PluralRules rules = factory.forLocale(locale);
             Map<String, PluralRules> keywordToRule = new HashMap<>();
-            Collection<FixedDecimalSamples> samples = new LinkedHashSet<>();
+            Collection<DecimalQuantitySamples> samples = new LinkedHashSet<>();
 
             for (String keyword : rules.getKeywords()) {
                 for (SampleType sampleType : SampleType.values()) {
-                    FixedDecimalSamples samples2 = rules.getDecimalSamples(keyword, sampleType);
+                    DecimalQuantitySamples samples2 = rules.getDecimalSamples(keyword, sampleType);
                     if (samples2 != null) {
                         samples.add(samples2);
                     }
@@ -412,15 +450,16 @@
                 }
                 keywordToRule.put(keyword, singleRule);
             }
-            Map<FixedDecimal, String> collisionTest = new TreeMap();
-            for (FixedDecimalSamples sample3 : samples) {
-                Set<FixedDecimalRange> samples2 = sample3.getSamples();
+
+            Map<DecimalQuantity, String> collisionTest = new LinkedHashMap();
+            for (DecimalQuantitySamples sample3 : samples) {
+                Set<DecimalQuantitySamplesRange> samples2 = sample3.getSamples();
                 if (samples2 == null) {
                     continue;
                 }
-                for (FixedDecimalRange sample : samples2) {
+                for (DecimalQuantitySamplesRange sample : samples2) {
                     for (int i = 0; i < 1; ++i) {
-                        FixedDecimal item = i == 0 ? sample.start : sample.end;
+                        DecimalQuantity item = i == 0 ? sample.start : sample.end;
                         collisionTest.clear();
                         for (Entry<String, PluralRules> entry : keywordToRule.entrySet()) {
                             PluralRules rule = entry.getValue();
@@ -460,9 +499,9 @@
     }
 
     public void checkValue(String title1, PluralRules rules, String expected, String value) {
-        FixedDecimal fdNum = new FixedDecimal(value);
+        DecimalQuantity dqNum = DecimalQuantity_DualStorageBCD.fromExponentString(value);
 
-        String result = rules.select(fdNum);
+        String result = rules.select(dqNum);
         ULocale locale = null;
         assertEquals(getAssertMessage(title1, locale, rules, expected) + "; value: " + value, expected, result);
     }
@@ -733,13 +772,20 @@
         }
     }
 
-    private void assertRuleValue(String rule, double value) {
+    private void assertRuleValue(String rule, DecimalQuantity value) {
         assertRuleKeyValue("a:" + rule, "a", value);
     }
 
-    private void assertRuleKeyValue(String rule, String key, double value) {
+    private void assertRuleKeyValue(String rule, String key, DecimalQuantity value) {
         PluralRules pr = PluralRules.createRules(rule);
-        assertEquals(rule, value, pr.getUniqueKeywordValue(key));
+
+        // as a DecimalQuantity
+        assertEquals(rule, value, pr.getUniqueKeywordDecimalQuantityValue(key));
+
+        // as a double
+        double expDouble = value.equals(PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY) ?
+                PluralRules.NO_UNIQUE_VALUE : value.toDouble();
+        assertEquals(rule, expDouble, pr.getUniqueKeywordValue(key));
     }
 
     /*
@@ -747,26 +793,36 @@
      */
     @Test
     public void TestGetUniqueKeywordValue() {
-        assertRuleKeyValue("a: n is 1", "not_defined", PluralRules.NO_UNIQUE_VALUE); // key not defined
-        assertRuleValue("n within 2..2", 2);
-        assertRuleValue("n is 1", 1);
-        assertRuleValue("n in 2..2", 2);
-        assertRuleValue("n in 3..4", PluralRules.NO_UNIQUE_VALUE);
-        assertRuleValue("n within 3..4", PluralRules.NO_UNIQUE_VALUE);
-        assertRuleValue("n is 2 or n is 2", 2);
-        assertRuleValue("n is 2 and n is 2", 2);
-        assertRuleValue("n is 2 or n is 3", PluralRules.NO_UNIQUE_VALUE);
-        assertRuleValue("n is 2 and n is 3", PluralRules.NO_UNIQUE_VALUE);
-        assertRuleValue("n is 2 or n in 2..3", PluralRules.NO_UNIQUE_VALUE);
-        assertRuleValue("n is 2 and n in 2..3", 2);
-        assertRuleKeyValue("a: n is 1", "other", PluralRules.NO_UNIQUE_VALUE); // key matches default rule
-        assertRuleValue("n in 2,3", PluralRules.NO_UNIQUE_VALUE);
-        assertRuleValue("n in 2,3..6 and n not in 2..3,5..6", 4);
+        LocalizedNumberFormatter fmtr = NumberFormatter.withLocale(ULocale.ROOT);
+
+        assertRuleKeyValue("a: n is 1", "not_defined", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY); // key not defined
+        assertRuleValue("n within 2..2", new DecimalQuantity_DualStorageBCD(2));
+        assertRuleValue("n is 1", new DecimalQuantity_DualStorageBCD(1));
+        assertRuleValue("n in 2..2", new DecimalQuantity_DualStorageBCD(2));
+        assertRuleValue("n in 3..4", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
+        assertRuleValue("n within 3..4", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
+        assertRuleValue("n is 2 or n is 2", new DecimalQuantity_DualStorageBCD(2));
+        assertRuleValue("n is 2 and n is 2", new DecimalQuantity_DualStorageBCD(2));
+        assertRuleValue("n is 2 or n is 3", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
+        assertRuleValue("n is 2 and n is 3", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
+        assertRuleValue("n is 2 or n in 2..3", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
+        assertRuleValue("n is 2 and n in 2..3", new DecimalQuantity_DualStorageBCD(2));
+        assertRuleKeyValue("a: n is 1", "other", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY); // key matches default rule
+        assertRuleValue("n in 2,3", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
+        assertRuleValue("n in 2,3..6 and n not in 2..3,5..6", new DecimalQuantity_DualStorageBCD(4));
     }
 
     /**
      * The version in PluralFormatUnitTest is not really a test, and it's in the wrong place anyway, so I'm putting a
      * variant of it here.
+     *
+     * Using the double API for getting plural samples, assert all samples match the keyword
+     * they are listed under, for all locales.
+     *
+     * Specifically, iterate over all locales, get plural rules for the locale, iterate over every rule,
+     * then iterate over every sample in the rule, parse sample to a number (double), use that number
+     * as an input to .select() for the rules object, and assert the actual return plural keyword matches
+     * what we expect based on the plural rule string.
      */
     @Test
     public void TestGetSamples() {
@@ -775,10 +831,6 @@
             uniqueRuleSet.add(PluralRules.getFunctionalEquivalent(locale, null));
         }
         for (ULocale locale : uniqueRuleSet) {
-            //if (locale.getLanguage().equals("fr") &&
-            //        logKnownIssue("21322", "PluralRules::getSamples cannot distinguish 1e5 from 100000")) {
-            //    continue;
-            //}
             PluralRules rules = factory.forLocale(locale);
             logln("\nlocale: " + (locale == ULocale.ROOT ? "root" : locale.toString()) + ", rules: " + rules);
             Set<String> keywords = rules.getKeywords();
@@ -791,8 +843,8 @@
                 if (list.size() == 0) {
                     // when the samples (meaning integer samples) are null, then then integerSamples must be, and the
                     // decimalSamples must not be
-                    FixedDecimalSamples integerSamples = rules.getDecimalSamples(keyword, SampleType.INTEGER);
-                    FixedDecimalSamples decimalSamples = rules.getDecimalSamples(keyword, SampleType.DECIMAL);
+                    DecimalQuantitySamples integerSamples = rules.getDecimalSamples(keyword, SampleType.INTEGER);
+                    DecimalQuantitySamples decimalSamples = rules.getDecimalSamples(keyword, SampleType.DECIMAL);
                     assertTrue(getAssertMessage("List is not null", locale, rules, keyword), integerSamples == null
                             && decimalSamples != null && decimalSamples.samples.size() != 0);
                 } else {
@@ -816,6 +868,131 @@
         }
     }
 
+    /**
+     * This replicates the setup of TestGetSamples(), but parses samples as DecimalQuantity instead of double.
+     *
+     * Using the DecimalQuantity API for getting plural samples, assert all samples match the keyword
+     * they are listed under, for all locales.
+     *
+     * Specifically, iterate over all locales, get plural rules for the locale, iterate over every rule,
+     * then iterate over every sample in the rule, parse sample to a number (DecimalQuantity), use that number
+     * as an input to .select() for the rules object, and assert the actual return plural keyword matches
+     * what we expect based on the plural rule string.
+     */
+    @Test
+    public void TestGetDecimalQuantitySamples() {
+        Set<ULocale> uniqueRuleSet = new HashSet<>();
+        for (ULocale locale : factory.getAvailableULocales()) {
+            uniqueRuleSet.add(PluralRules.getFunctionalEquivalent(locale, null));
+        }
+        for (ULocale locale : uniqueRuleSet) {
+            PluralRules rules = factory.forLocale(locale);
+            logln("\nlocale: " + (locale == ULocale.ROOT ? "root" : locale.toString()) + ", rules: " + rules);
+            Set<String> keywords = rules.getKeywords();
+            for (String keyword : keywords) {
+                Collection<DecimalQuantity> list = rules.getDecimalQuantitySamples(keyword);
+                logln("keyword: " + keyword + ", samples: " + list);
+                // with fractions, the samples can be empty and thus the list null. In that case, however, there will be
+                // FixedDecimal values.
+                // So patch the test for that.
+                if (list.size() == 0) {
+                    // when the samples (meaning integer samples) are null, then then integerSamples must be, and the
+                    // decimalSamples must not be
+                    DecimalQuantitySamples integerSamples = rules.getDecimalSamples(keyword, SampleType.INTEGER);
+                    DecimalQuantitySamples decimalSamples = rules.getDecimalSamples(keyword, SampleType.DECIMAL);
+                    assertTrue(getAssertMessage("List is not null", locale, rules, keyword), integerSamples == null
+                            && decimalSamples != null && decimalSamples.samples.size() != 0);
+                } else {
+                    if (!assertTrue(getAssertMessage("Test getSamples.isEmpty", locale, rules, keyword),
+                            !list.isEmpty())) {
+                        rules.getDecimalQuantitySamples(keyword);
+                    }
+                    if (rules.toString().contains(": j")) {
+                        // hack until we remove j
+                    } else {
+                        for (DecimalQuantity value : list) {
+                            assertEquals(getAssertMessage("Match keyword", locale, rules, keyword) + "; value '"
+                                    + value + "'", keyword, rules.select(value));
+                        }
+                    }
+                }
+            }
+
+            assertNull(locale + ", list is null", rules.getDecimalQuantitySamples("@#$%^&*"));
+            assertNull(locale + ", list is null", rules.getDecimalQuantitySamples("@#$%^&*", SampleType.DECIMAL));
+        }
+    }
+
+    /**
+     * Test addSamples (Java) / getSamplesFromString (C++) to ensure the expansion of plural rule sample range
+     * expands to a sequence of sample numbers that is incremented as the right scale.
+     *
+     *  Do this for numbers with fractional digits but no exponent.
+     */
+    @Test
+    public void testGetOrAddSamplesFromString() {
+        PluralRules rules = PluralRules.createRules("testkeyword: e != 0 @decimal 2.0~4.0, …");
+
+        Set<String> keywords = rules.getKeywords();
+        assertTrue("At least parse the test keyword in the test rule string", 0 < keywords.size());
+
+        String expKeyword = "testkeyword";
+        Collection<DecimalQuantity> list = rules.getDecimalQuantitySamples(expKeyword, SampleType.DECIMAL);
+
+        String[] expDqStrs = {
+                "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9",
+                "3.0", "3.1", "3.2", "3.3", "3.4", "3.5", "3.6", "3.7", "3.8", "3.9",
+                "4.0"
+        };
+        assertEquals("Number of parsed samples from test string incorrect", expDqStrs.length, list.size());
+        ArrayList<DecimalQuantity> actSamples = new ArrayList<>(list);
+        for (int i = 0; i < list.size(); i++) {
+            String expDqStr = expDqStrs[i];
+            DecimalQuantity sample = actSamples.get(i);
+            String sampleStr = sample.toExponentString();
+
+            assertEquals("Expansion of sample range to sequence of sample values should increment at the right scale",
+                    expDqStr, sampleStr);
+
+        }
+    }
+
+    /**
+     * Test addSamples (Java) / getSamplesFromString (C++) to ensure the expansion of plural rule sample range
+     * expands to a sequence of sample numbers that is incremented as the right scale.
+     *
+     *  Do this for numbers written in a notation that has an exponent, for which the number is an
+     *  integer (also as defined in the UTS 35 spec for the plural operands) but whose representation
+     *  has fractional digits in the significand written before the exponent.
+     */
+    @Test
+    public void testGetOrAddSamplesFromStringCompactNotation() {
+        PluralRules rules = PluralRules.createRules("testkeyword: e != 0 @decimal 2.0c6~4.0c6, …");
+
+        Set<String> keywords = rules.getKeywords();
+        assertTrue("At least parse the test keyword in the test rule string", 0 < keywords.size());
+
+        String expKeyword = "testkeyword";
+        Collection<DecimalQuantity> list = rules.getDecimalQuantitySamples(expKeyword, SampleType.DECIMAL);
+
+        String[] expDqStrs = {
+                "2.0c6", "2.1c6", "2.2c6", "2.3c6", "2.4c6", "2.5c6", "2.6c6", "2.7c6", "2.8c6", "2.9c6",
+                "3.0c6", "3.1c6", "3.2c6", "3.3c6", "3.4c6", "3.5c6", "3.6c6", "3.7c6", "3.8c6", "3.9c6",
+                "4.0c6"
+        };
+        assertEquals("Number of parsed samples from test string incorrect", expDqStrs.length, list.size());
+        ArrayList<DecimalQuantity> actSamples = new ArrayList<>(list);
+        for (int i = 0; i < list.size(); i++) {
+            String expDqStr = expDqStrs[i];
+            DecimalQuantity sample = actSamples.get(i);
+            String sampleStr = sample.toExponentString();
+
+            assertEquals("Expansion of sample range to sequence of sample values should increment at the right scale",
+                    expDqStr, sampleStr);
+
+        }
+    }
+
     public String getAssertMessage(String message, ULocale locale, PluralRules rules, String keyword) {
         String ruleString = "";
         if (keyword != null) {
@@ -882,24 +1059,42 @@
                 if (valueList != null) {
                     valueList = valueList.trim();
                 }
-                Collection<Double> values;
+                Collection<DecimalQuantity> values;
                 if (valueList == null || valueList.length() == 0) {
                     values = Collections.EMPTY_SET;
                 } else if ("null".equals(valueList)) {
                     values = null;
                 } else {
-                    values = new TreeSet<>();
+                    values = new LinkedHashSet<>();
                     for (String value : valueList.split(",")) {
-                        values.add(Double.parseDouble(value));
+                        values.add(DecimalQuantity_DualStorageBCD.fromExponentString(value));
                     }
                 }
 
-                Collection<Double> results = p.getAllKeywordValues(keyword);
-                assertEquals(keyword + " in " + ruleDescription, values, results == null ? null : new HashSet(results));
+                Collection<DecimalQuantity> results = p.getAllKeywordDecimalQuantityValues(keyword);
+
+                // Convert DecimalQuantity using a 1:1 conversion to String for comparison purposes
+                Set<String> valuesForComparison = new HashSet<>();
+                if (values != null) {
+                    for (DecimalQuantity dq : values) {
+                        valuesForComparison.add(dq.toExponentString());
+                    }
+                }
+                Set<String> resultsForComparison = new HashSet<>();
+                if (results != null) {
+                    for (DecimalQuantity dq : results) {
+                        resultsForComparison.add(dq.toExponentString());
+                    }
+                }
+
+                assertEquals(keyword + " in " + ruleDescription,
+                        values == null ? null : valuesForComparison,
+                        results == null ? null : resultsForComparison
+                );
 
                 if (results != null) {
                     try {
-                        results.add(PluralRules.NO_UNIQUE_VALUE);
+                        results.add(PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
                         fail("returned set is modifiable");
                     } catch (UnsupportedOperationException e) {
                         // pass
@@ -967,13 +1162,9 @@
                     for (String keyword : rules.getKeywords()) {
                         boolean isLimited = rules.isLimited(keyword, sampleType);
                         boolean computeLimited = rules.computeLimited(keyword, sampleType);
-                        if (!keyword.equals("other") && !(locale.getLanguage().equals("fr") && logKnownIssue("ICU-21322", "fr plurals many case computeLimited == isLimited"))) {
-                            assertEquals(getAssertMessage("computeLimited == isLimited", locale, rules, keyword),
-                                    computeLimited, isLimited);
-                        }
-                        Collection<Double> samples = rules.getSamples(keyword, sampleType);
+                        Collection<DecimalQuantity> samples = rules.getDecimalQuantitySamples(keyword, sampleType);
                         assertNotNull(getAssertMessage("Samples must not be null", locale, rules, keyword), samples);
-                        /* FixedDecimalSamples decimalSamples = */rules.getDecimalSamples(keyword, sampleType);
+                        rules.getDecimalSamples(keyword, sampleType);
                         // assertNotNull(getAssertMessage("Decimal samples must be null if unlimited", locale, rules,
                         // keyword), decimalSamples);
                     }
@@ -985,29 +1176,30 @@
     @Test
     public void TestKeywords() {
         Set<String> possibleKeywords = new LinkedHashSet(Arrays.asList("zero", "one", "two", "few", "many", "other"));
+        DecimalQuantity ONE_INTEGER = DecimalQuantity_DualStorageBCD.fromExponentString("1");
         Object[][][] tests = {
                 // format is locale, explicits, then triples of keyword, status, unique value.
-                { { "en", null }, { "one", KeywordStatus.UNIQUE, 1.0d }, { "other", KeywordStatus.UNBOUNDED, null } },
-                { { "pl", null }, { "one", KeywordStatus.UNIQUE, 1.0d }, { "few", KeywordStatus.UNBOUNDED, null },
+                { { "en", null }, { "one", KeywordStatus.UNIQUE, ONE_INTEGER }, { "other", KeywordStatus.UNBOUNDED, null } },
+                { { "pl", null }, { "one", KeywordStatus.UNIQUE, ONE_INTEGER }, { "few", KeywordStatus.UNBOUNDED, null },
                         { "many", KeywordStatus.UNBOUNDED, null },
                         { "other", KeywordStatus.SUPPRESSED, null, KeywordStatus.UNBOUNDED, null } // note that it is
                                                                                                    // suppressed in
                                                                                                    // INTEGER but not
                                                                                                    // DECIMAL
-                }, { { "en", new HashSet<>(Arrays.asList(1.0d)) }, // check that 1 is suppressed
+                }, { { "en", new HashSet<>(Arrays.asList(ONE_INTEGER)) }, // check that 1 is suppressed
                         { "one", KeywordStatus.SUPPRESSED, null }, { "other", KeywordStatus.UNBOUNDED, null } }, };
-        Output<Double> uniqueValue = new Output<>();
+        Output<DecimalQuantity> uniqueValue = new Output<>();
         for (Object[][] test : tests) {
             ULocale locale = new ULocale((String) test[0][0]);
             // NumberType numberType = (NumberType) test[1];
-            Set<Double> explicits = (Set<Double>) test[0][1];
+            Set<DecimalQuantity> explicits = (Set<DecimalQuantity>) test[0][1];
             PluralRules pluralRules = factory.forLocale(locale);
             LinkedHashSet<String> remaining = new LinkedHashSet(possibleKeywords);
             for (int i = 1; i < test.length; ++i) {
                 Object[] row = test[i];
                 String keyword = (String) row[0];
                 KeywordStatus statusExpected = (KeywordStatus) row[1];
-                Double uniqueExpected = (Double) row[2];
+                DecimalQuantity uniqueExpected = (DecimalQuantity) row[2];
                 remaining.remove(keyword);
                 KeywordStatus status = pluralRules.getKeywordStatus(keyword, 0, explicits, uniqueValue);
                 assertEquals(getAssertMessage("Unique Value", locale, pluralRules, keyword), uniqueExpected,
@@ -1015,7 +1207,7 @@
                 assertEquals(getAssertMessage("Keyword Status", locale, pluralRules, keyword), statusExpected, status);
                 if (row.length > 3) {
                     statusExpected = (KeywordStatus) row[3];
-                    uniqueExpected = (Double) row[4];
+                    uniqueExpected = (DecimalQuantity) row[4];
                     status = pluralRules.getKeywordStatus(keyword, 0, explicits, uniqueValue, SampleType.DECIMAL);
                     assertEquals(getAssertMessage("Unique Value - decimal", locale, pluralRules, keyword),
                             uniqueExpected, uniqueValue.value);
@@ -1033,6 +1225,11 @@
 
     // For the time being, the compact notation exponent operand `c` is an alias
     // for the scientific exponent operand `e` and compact notation.
+    /**
+     * Test the proper plural rule keyword selection given an input number that is
+     * already formatted into scientific notation. This exercises the `e` plural operand
+     * for the formatted number.
+     */
     @Test
     public void testScientificPluralKeyword() {
         PluralRules rules = PluralRules.createRules("one: i = 0,1 @integer 0, 1 @decimal 0.0~1.5;  many: e = 0 and i % 1000000 = 0 and v = 0 or " +
@@ -1082,6 +1279,11 @@
         }
     }
 
+    /**
+     * Test the proper plural rule keyword selection given an input number that is
+     * already formatted into compact notation. This exercises the `c` plural operand
+     * for the formatted number.
+     */
     @Test
     public void testCompactDecimalPluralKeyword() {
         PluralRules rules = PluralRules.createRules("one: i = 0,1 @integer 0, 1 @decimal 0.0~1.5;  many: c = 0 and i % 1000000 = 0 and v = 0 or " +
@@ -1261,8 +1463,13 @@
 
     @Test
     public void TestLocales() {
+        // This test will fail when the locale snapshot gets out of sync with the real CLDR data.
+        // In that case, temporarily use "if (true)",
+        // copy & paste the output into the initializer above,
+        // and revert to "if (false)" for normal testing.
         if (false) {
             generateLOCALE_SNAPSHOT();
+            return;
         }
         for (String test : LOCALE_SNAPSHOT) {
             test = test.trim();
@@ -1308,7 +1515,7 @@
                 System.out.print("        \"" + CollectionUtilities.join(locales, ","));
                 for (StandardPluralCategories spc : set) {
                     String keyword = spc.toString();
-                    FixedDecimalSamples samples = rule.getDecimalSamples(keyword, SampleType.INTEGER);
+                    DecimalQuantitySamples samples = rule.getDecimalSamples(keyword, SampleType.INTEGER);
                     System.out.print("; " + spc + ": " + samples);
                 }
                 System.out.println("\",");