ICU-20568 getPreferencesFor() and getUnitCategory()

UnitPreferences class in unitsdata.cpp
PR: https://github.com/sffc/icu/pull/42
Commit: 24494d985e1eeb60e5daa450e26f7f0c3437a246

Add getUnitCategory()
PR: https://github.com/sffc/icu/pull/43
Commit: d406b915c4985e541b0d4cd8c324bcfdb0b7f194

Support usage component dropping, and more
PR: https://github.com/sffc/icu/pull/45
Commit: 6b14d7f1a0fa16fc6f80ca4fc87f17a8c687cb28

Add six more unit tests for getPreferencesFor.
PR: https://github.com/sffc/icu/pull/46
Commit: 5e4f8d4fe490ab82682ba233e0e6d38e8bf570a0

Change getPreferencesFor parameters from char* to StringPiece.
PR: https://github.com/sffc/icu/pull/47
Commit: a7ca496f9e60ad22dc9526259873b6f2bf52dd86
diff --git a/icu4c/source/i18n/unitsdata.cpp b/icu4c/source/i18n/unitsdata.cpp
index 4575332..05d759b 100644
--- a/icu4c/source/i18n/unitsdata.cpp
+++ b/icu4c/source/i18n/unitsdata.cpp
@@ -6,6 +6,7 @@
 #if !UCONFIG_NO_FORMATTING
 
 #include "cstring.h"
+#include "number_decimalquantity.h"
 #include "resource.h"
 #include "unitsdata.h"
 #include "uresimp.h"
@@ -15,6 +16,8 @@
 
 namespace {
 
+using number::impl::DecimalQuantity;
+
 void trimSpaces(CharString& factor, UErrorCode& status){
    CharString trimmed;
    for (int i = 0 ; i < factor.length(); i++) {
@@ -41,20 +44,18 @@
     explicit ConversionRateDataSink(MaybeStackVector<ConversionRateInfo> *out) : outVector(out) {}
 
     /**
-     * Adds the conversion rate information found in value to the output vector.
+     * Method for use by `ures_getAllItemsWithFallback`. Adds the unit
+     * conversion rates that are found in `value` to the output vector.
      *
-     * Each call to put() collects a ConversionRateInfo instance for the
-     * specified source unit identifier into the vector passed to the
-     * constructor, but only if an identical instance isn't already present.
-     *
-     * @param source The source unit identifier.
-     * @param value A resource containing conversion rate info (the base unit
-     * and factor, and possibly an offset).
+     * @param source This string must be "convertUnits": the resource that this
+     * class supports reading.
+     * @param value The "convertUnits" resource, containing unit conversion rate
+     * information.
      * @param noFallback Ignored.
      * @param status The standard ICU error code output parameter.
      */
     void put(const char *source, ResourceValue &value, UBool /*noFallback*/, UErrorCode &status) {
-        if (U_FAILURE(status)) return;
+        if (U_FAILURE(status)) { return; }
         if (uprv_strcmp(source, "convertUnits") != 0) {
             // This is very strict, however it is the cheapest way to be sure
             // that with `value`, we're looking at the convertUnits table.
@@ -79,7 +80,7 @@
                     offset = value.getUnicodeString(status);
                 }
             }
-            if (U_FAILURE(status)) return;
+            if (U_FAILURE(status)) { return; }
             if (baseUnit.isBogus() || factor.isBogus()) {
                 // We could not find a usable conversion rate: bad resource.
                 status = U_MISSING_RESOURCE_ERROR;
@@ -106,8 +107,274 @@
     MaybeStackVector<ConversionRateInfo> *outVector;
 };
 
+UnitPreferenceMetadata::UnitPreferenceMetadata(StringPiece category, StringPiece usage,
+                                               StringPiece region, int32_t prefsOffset,
+                                               int32_t prefsCount, UErrorCode &status) {
+    this->category.append(category, status);
+    this->usage.append(usage, status);
+    this->region.append(region, status);
+    this->prefsOffset = prefsOffset;
+    this->prefsCount = prefsCount;
+}
+
+int32_t UnitPreferenceMetadata::compareTo(const UnitPreferenceMetadata &other) const {
+    int32_t cmp = uprv_strcmp(category.data(), other.category.data());
+    if (cmp == 0) { cmp = uprv_strcmp(usage.data(), other.usage.data()); }
+    if (cmp == 0) { cmp = uprv_strcmp(region.data(), other.region.data()); }
+    return cmp;
+}
+
+int32_t UnitPreferenceMetadata::compareTo(const UnitPreferenceMetadata &other, bool *foundCategory,
+                                          bool *foundUsage, bool *foundRegion) const {
+    int32_t cmp = uprv_strcmp(category.data(), other.category.data());
+    if (cmp == 0) {
+        *foundCategory = true;
+        cmp = uprv_strcmp(usage.data(), other.usage.data());
+    }
+    if (cmp == 0) {
+        *foundUsage = true;
+        cmp = uprv_strcmp(region.data(), other.region.data());
+    }
+    if (cmp == 0) {
+        *foundRegion = true;
+    }
+    return cmp;
+}
+
+bool operator<(const UnitPreferenceMetadata &a, const UnitPreferenceMetadata &b) {
+    return a.compareTo(b) < 0;
+}
+
+/**
+ * A ResourceSink that collects unit preferences information.
+ *
+ * This class is for use by ures_getAllItemsWithFallback.
+ */
+class UnitPreferencesSink : public ResourceSink {
+  public:
+    /**
+     * Constructor.
+     * @param outPrefs The vector to which UnitPreference instances are to be
+     * added. This vector must outlive the use of the ResourceSink.
+     * @param outMetadata  The vector to which UnitPreferenceMetadata instances
+     * are to be added. This vector must outlive the use of the ResourceSink.
+     */
+    explicit UnitPreferencesSink(MaybeStackVector<UnitPreference> *outPrefs,
+                                 MaybeStackVector<UnitPreferenceMetadata> *outMetadata)
+        : preferences(outPrefs), metadata(outMetadata) {}
+
+    /**
+     * Method for use by `ures_getAllItemsWithFallback`. Adds the unit
+     * preferences info that are found in `value` to the output vector.
+     *
+     * @param source This string must be "unitPreferenceData": the resource that
+     * this class supports reading.
+     * @param value The "unitPreferenceData" resource, containing unit
+     * preferences data.
+     * @param noFallback Ignored.
+     * @param status The standard ICU error code output parameter. Note: if an
+     * error is returned, outPrefs and outMetadata may be inconsistent.
+     */
+    void put(const char *key, ResourceValue &value, UBool /*noFallback*/, UErrorCode &status) {
+        if (U_FAILURE(status)) { return; }
+        if (uprv_strcmp(key, "unitPreferenceData") != 0) {
+            // This is very strict, however it is the cheapest way to be sure
+            // that with `value`, we're looking at the convertUnits table.
+            status = U_ILLEGAL_ARGUMENT_ERROR;
+            return;
+        }
+        // The unitPreferenceData structure (see data/misc/units.txt) contains a
+        // hierarchy of category/usage/region, within which are a set of
+        // preferences. Hence three for-loops and another loop for the
+        // preferences themselves:
+        ResourceTable unitPreferenceDataTable = value.getTable(status);
+        const char *category;
+        for (int32_t i = 0; unitPreferenceDataTable.getKeyAndValue(i, category, value); i++) {
+            ResourceTable categoryTable = value.getTable(status);
+            const char *usage;
+            for (int32_t j = 0; categoryTable.getKeyAndValue(j, usage, value); j++) {
+                ResourceTable regionTable = value.getTable(status);
+                const char *region;
+                for (int32_t k = 0; regionTable.getKeyAndValue(k, region, value); k++) {
+                    // `value` now contains the set of preferences for
+                    // category/usage/region.
+                    ResourceArray unitPrefs = value.getArray(status);
+                    if (U_FAILURE(status)) { return; }
+                    int32_t prefLen = unitPrefs.getSize();
+
+                    // Update metadata for this set of preferences.
+                    UnitPreferenceMetadata *meta = metadata->emplaceBack(
+                        category, usage, region, preferences->length(), prefLen, status);
+                    if (!meta) {
+                        status = U_MEMORY_ALLOCATION_ERROR;
+                        return;
+                    }
+                    if (U_FAILURE(status)) { return; }
+                    if (metadata->length() > 1) {
+                        // Verify that unit preferences are sorted and
+                        // without duplicates.
+                        if (!(*(*metadata)[metadata->length() - 2] <
+                              *(*metadata)[metadata->length() - 1])) {
+                            status = U_INVALID_FORMAT_ERROR;
+                            return;
+                        }
+                    }
+
+                    // Collect the individual preferences.
+                    for (int32_t i = 0; unitPrefs.getValue(i, value); i++) {
+                        UnitPreference *up = preferences->emplaceBack();
+                        if (!up) {
+                            status = U_MEMORY_ALLOCATION_ERROR;
+                            return;
+                        }
+                        ResourceTable unitPref = value.getTable(status);
+                        if (U_FAILURE(status)) { return; }
+                        for (int32_t i = 0; unitPref.getKeyAndValue(i, key, value); ++i) {
+                            if (uprv_strcmp(key, "unit") == 0) {
+                                int32_t length;
+                                const UChar *u = value.getString(length, status);
+                                up->unit.appendInvariantChars(u, length, status);
+                            } else if (uprv_strcmp(key, "geq") == 0) {
+                                int32_t length;
+                                const UChar *g = value.getString(length, status);
+                                CharString geq;
+                                geq.appendInvariantChars(g, length, status);
+                                DecimalQuantity dq;
+                                dq.setToDecNumber(geq.data(), status);
+                                up->geq = dq.toDouble();
+                            } else if (uprv_strcmp(key, "skeleton") == 0) {
+                                int32_t length;
+                                const UChar *s = value.getString(length, status);
+                                up->skeleton.appendInvariantChars(s, length, status);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+  private:
+    MaybeStackVector<UnitPreference> *preferences;
+    MaybeStackVector<UnitPreferenceMetadata> *metadata;
+};
+
+int32_t binarySearch(const MaybeStackVector<UnitPreferenceMetadata> *metadata,
+                     const UnitPreferenceMetadata &desired, bool *foundCategory, bool *foundUsage,
+                     bool *foundRegion, UErrorCode &status) {
+    if (U_FAILURE(status)) { return -1; }
+    int32_t start = 0;
+    int32_t end = metadata->length();
+    *foundCategory = false;
+    *foundUsage = false;
+    *foundRegion = false;
+    while (start < end) {
+        int32_t mid = (start + end) / 2;
+        int32_t cmp = (*metadata)[mid]->compareTo(desired, foundCategory, foundUsage, foundRegion);
+        if (cmp < 0) {
+            start = mid + 1;
+        } else if (cmp > 0) {
+            end = mid;
+        } else {
+            return mid;
+        }
+    }
+    return -1;
+}
+
+/**
+ * Finds the UnitPreferenceMetadata instance that matches the given category,
+ * usage and region: if missing, region falls back to "001", and usage
+ * repeatedly drops tailing components, eventually trying "default"
+ * ("land-agriculture-grain" -> "land-agriculture" -> "land" -> "default").
+ *
+ * @param metadata The full list of UnitPreferenceMetadata instances.
+ * @param category The category to search for. See getUnitCategory().
+ * @param usage The usage for which formatting preferences is needed. If the
+ * given usage is not known, automatic fallback occurs, see function description
+ * above.
+ * @param region The region for which preferences are needed. If there are no
+ * region-specific preferences, this function automatically falls back to the
+ * "001" region (global).
+ * @param status The standard ICU error code output parameter.
+ *   * If an invalid category is given, status will be U_ILLEGAL_ARGUMENT_ERROR.
+ *   * If fallback to "default" or "001" didn't resolve, status will be
+ *     U_MISSING_RESOURCE.
+ * @return The index into the metadata vector which represents the appropriate
+ * preferences. If appropriate preferences are not found, -1 is returned.
+ */
+int32_t getPreferenceMetadataIndex(const MaybeStackVector<UnitPreferenceMetadata> *metadata,
+                                   StringPiece category, StringPiece usage, StringPiece region,
+                                   UErrorCode &status) {
+    if (U_FAILURE(status)) { return -1; }
+    bool foundCategory, foundUsage, foundRegion;
+    UnitPreferenceMetadata desired(category, usage, region, -1, -1, status);
+    int32_t idx = binarySearch(metadata, desired, &foundCategory, &foundUsage, &foundRegion, status);
+    if (U_FAILURE(status)) { return -1; }
+    if (idx >= 0) { return idx; }
+    if (!foundCategory) {
+        status = U_ILLEGAL_ARGUMENT_ERROR;
+        return -1;
+    }
+    U_ASSERT(foundCategory);
+    while (!foundUsage) {
+        int32_t lastDashIdx = desired.usage.lastIndexOf('-');
+        if (lastDashIdx > 0) {
+            desired.usage.truncate(lastDashIdx);
+        } else if (uprv_strcmp(desired.usage.data(), "default") != 0) {
+            desired.usage.truncate(0).append("default", status);
+        } else {
+            status = U_MISSING_RESOURCE_ERROR;
+            return -1;
+        }
+        idx = binarySearch(metadata, desired, &foundCategory, &foundUsage, &foundRegion, status);
+        if (U_FAILURE(status)) { return -1; }
+    }
+    U_ASSERT(foundCategory);
+    U_ASSERT(foundUsage);
+    if (!foundRegion) {
+        if (uprv_strcmp(desired.region.data(), "001") != 0) {
+            desired.region.truncate(0).append("001", status);
+            idx = binarySearch(metadata, desired, &foundCategory, &foundUsage, &foundRegion, status);
+        }
+        if (!foundRegion) {
+            status = U_MISSING_RESOURCE_ERROR;
+            return -1;
+        }
+    }
+    U_ASSERT(foundCategory);
+    U_ASSERT(foundUsage);
+    U_ASSERT(foundRegion);
+    U_ASSERT(idx >= 0);
+    return idx;
+}
+
 } // namespace
 
+CharString U_I18N_API getUnitCategory(const char *baseUnitIdentifier, UErrorCode &status) {
+    CharString result;
+    LocalUResourceBundlePointer unitsBundle(ures_openDirect(NULL, "units", &status));
+    LocalUResourceBundlePointer unitQuantities(
+        ures_getByKey(unitsBundle.getAlias(), "unitQuantities", NULL, &status));
+    int32_t categoryLength;
+    if (U_FAILURE(status)) { return result; }
+    const UChar *uCategory =
+        ures_getStringByKey(unitQuantities.getAlias(), baseUnitIdentifier, &categoryLength, &status);
+    if (U_FAILURE(status)) {
+        // TODO(CLDR-13787,hugovdm): special-casing the consumption-inverse
+        // case. Once CLDR-13787 is clarified, this should be generalised (or
+        // possibly removed):
+        if (uprv_strcmp(baseUnitIdentifier, "meter-per-cubic-meter") == 0) {
+            status = U_ZERO_ERROR;
+            result.append("consumption-inverse", status);
+            return result;
+        }
+    }
+    result.appendInvariantChars(uCategory, categoryLength, status);
+    return result;
+}
+
+// TODO: this may be unnecessary. Fold into ConversionRates class? Or move to anonymous namespace?
 void U_I18N_API getAllConversionRates(MaybeStackVector<ConversionRateInfo> &result, UErrorCode &status) {
     LocalUResourceBundlePointer unitsBundle(ures_openDirect(NULL, "units", &status));
     ConversionRateDataSink sink(&result);
@@ -124,6 +391,28 @@
     return nullptr;
 }
 
+U_I18N_API UnitPreferences::UnitPreferences(UErrorCode &status) {
+    LocalUResourceBundlePointer unitsBundle(ures_openDirect(NULL, "units", &status));
+    UnitPreferencesSink sink(&unitPrefs_, &metadata_);
+    ures_getAllItemsWithFallback(unitsBundle.getAlias(), "unitPreferenceData", sink, status);
+}
+
+// TODO: make outPreferences const?
+//
+// TODO: consider replacing `UnitPreference **&outPrefrences` with slice class
+// of some kind.
+void U_I18N_API UnitPreferences::getPreferencesFor(StringPiece category, StringPiece usage,
+                                                   StringPiece region,
+                                                   const UnitPreference *const *&outPreferences,
+                                                   int32_t &preferenceCount, UErrorCode &status) const {
+    int32_t idx = getPreferenceMetadataIndex(&metadata_, category, usage, region, status);
+    if (U_FAILURE(status)) { return; }
+    U_ASSERT(idx >= 0); // Failures should have been taken care of by `status`.
+    const UnitPreferenceMetadata *m = metadata_[idx];
+    outPreferences = unitPrefs_.getAlias() + m->prefsOffset;
+    preferenceCount = m->prefsCount;
+}
+
 U_NAMESPACE_END
 
 #endif /* #if !UCONFIG_NO_FORMATTING */
diff --git a/icu4c/source/i18n/unitsdata.h b/icu4c/source/i18n/unitsdata.h
index 9938357..0acea7a 100644
--- a/icu4c/source/i18n/unitsdata.h
+++ b/icu4c/source/i18n/unitsdata.h
@@ -15,6 +15,22 @@
 U_NAMESPACE_BEGIN
 
 /**
+ * Looks up the unit category of a base unit identifier.
+ *
+ * Only supports base units, other units must be resolved to base units before
+ * passing to this function.
+ *
+ * Categories are found in `unitQuantities` in the `units` resource (see
+ * `units.txt`).
+ *
+ * TODO(hugovdm): if we give unitsdata.cpp access to the functionality of
+ * `extractCompoundBaseUnit` which is currently in unitconverter.cpp, we could
+ * support all units for which there is a category. Does it make sense to move
+ * that function to unitsdata.cpp?
+ */
+CharString U_I18N_API getUnitCategory(const char *baseUnitIdentifier, UErrorCode &status);
+
+/**
  * Encapsulates "convertUnits" information from units resources, specifying how
  * to convert from one unit to another.
  *
@@ -25,7 +41,7 @@
  */
 class U_I18N_API ConversionRateInfo : public UMemory {
   public:
-    ConversionRateInfo(){};
+    ConversionRateInfo() {}
     ConversionRateInfo(StringPiece sourceUnit, StringPiece baseUnit, StringPiece factor,
                        StringPiece offset, UErrorCode &status)
         : sourceUnit(), baseUnit(), factor(), offset() {
@@ -33,7 +49,7 @@
         this->baseUnit.append(baseUnit, status);
         this->factor.append(factor, status);
         this->offset.append(offset, status);
-    };
+    }
     CharString sourceUnit;
     CharString baseUnit;
     CharString factor;
@@ -72,6 +88,104 @@
     MaybeStackVector<ConversionRateInfo> conversionInfo_;
 };
 
+// Encapsulates unitPreferenceData information from units resources, specifying
+// a sequence of output unit preferences.
+struct U_I18N_API UnitPreference : public UMemory {
+    UnitPreference() : geq(1) {}
+    CharString unit;
+    double geq;
+    CharString skeleton;
+};
+
+namespace {
+
+/**
+ * Metadata about the preferences in UnitPreferences::unitPrefs_.
+ *
+ * This class owns all of its data.
+ *
+ * UnitPreferenceMetadata lives in the anonymous namespace, because it should
+ * only be useful to internal code and unit testing code.
+ */
+class U_I18N_API UnitPreferenceMetadata : public UMemory {
+  public:
+    UnitPreferenceMetadata() {}
+    // Constructor, makes copies of the parameters passed to it.
+    UnitPreferenceMetadata(StringPiece category, StringPiece usage, StringPiece region,
+                           int32_t prefsOffset, int32_t prefsCount, UErrorCode &status);
+
+    // Unit category (e.g. "length", "mass", "electric-capacitance").
+    CharString category;
+    // Usage (e.g. "road", "vehicle-fuel", "blood-glucose"). Every category
+    // should have an entry for "default" usage. TODO(hugovdm): add a test for
+    // this.
+    CharString usage;
+    // Region code (e.g. "US", "CZ", "001"). Every usage should have an entry
+    // for the "001" region ("world"). TODO(hugovdm): add a test for this.
+    CharString region;
+    // Offset into the UnitPreferences::unitPrefs_ list where the relevant
+    // preferences are found.
+    int32_t prefsOffset;
+    // The number of preferences that form this set.
+    int32_t prefsCount;
+
+    int32_t compareTo(const UnitPreferenceMetadata &other) const;
+    int32_t compareTo(const UnitPreferenceMetadata &other, bool *foundCategory, bool *foundUsage,
+                      bool *foundRegion) const;
+};
+
+} // namespace
+
+/**
+ * Unit Preferences information for various locales and usages.
+ */
+class U_I18N_API UnitPreferences {
+  public:
+    /**
+     * Constructor, loads all the preference data.
+     *
+     * @param status Receives status.
+     */
+    UnitPreferences(UErrorCode &status);
+
+    /**
+     * Returns the set of unit preferences in the particular category that best
+     * matches the specified usage and region.
+     *
+     * If region can't be found, falls back to global (001). If usage can't be
+     * found, falls back to "default".
+     *
+     * @param category The category within which to look up usage and region.
+     * (TODO(hugovdm): improve docs on how to find the category, once the lookup
+     * function is added.)
+     * @param usage The usage parameter. (TODO(hugovdm): improve this
+     * documentation. Add reference to some list of usages we support.) If the
+     * given usage is not found, the method automatically falls back to
+     * "default".
+     * @param region The region whose preferences are desired. If there are no
+     * specific preferences for the requested region, the method automatically
+     * falls back to region "001" ("world").
+     * @param outPreferences A pointer into an array of preferences: essentially
+     * an array slice in combination with preferenceCount.
+     * @param preferenceCount The number of unit preferences that belong to the
+     * result set.
+     * @param status Receives status.
+     *
+     * TODO(hugovdm): maybe replace `UnitPreference **&outPrefrences` with a slice class?
+     */
+    void getPreferencesFor(StringPiece category, StringPiece usage, StringPiece region,
+                           const UnitPreference *const *&outPreferences, int32_t &preferenceCount,
+                           UErrorCode &status) const;
+
+  protected:
+    // Metadata about the sets of preferences, this is the index for looking up
+    // preferences in the unitPrefs_ list.
+    MaybeStackVector<UnitPreferenceMetadata> metadata_;
+    // All the preferences as a flat list: which usage and region preferences
+    // are associated with is stored in `metadata_`.
+    MaybeStackVector<UnitPreference> unitPrefs_;
+};
+
 U_NAMESPACE_END
 
 #endif //__GETUNITSDATA_H__
diff --git a/icu4c/source/test/depstest/dependencies.txt b/icu4c/source/test/depstest/dependencies.txt
index dcb68bf..e7db664 100644
--- a/icu4c/source/test/depstest/dependencies.txt
+++ b/icu4c/source/test/depstest/dependencies.txt
@@ -1076,7 +1076,7 @@
 group: unitsformatter
     unitsdata.o unitconverter.o
   deps
-    resourcebundle units_extra double_conversion
+    resourcebundle units_extra double_conversion number_representation
 
 group: decnumber
     decContext.o decNumber.o
diff --git a/icu4c/source/test/intltest/intltest.cpp b/icu4c/source/test/intltest/intltest.cpp
index 38409d3..59d2c46 100644
--- a/icu4c/source/test/intltest/intltest.cpp
+++ b/icu4c/source/test/intltest/intltest.cpp
@@ -45,6 +45,7 @@
 #include "udbgutil.h"
 #include "umutex.h"
 #include "uoptions.h"
+#include "number_decnum.h"
 
 #ifdef XP_MAC_CONSOLE
 #include <console.h>
@@ -2039,7 +2040,6 @@
     return TRUE;
 }
 
-
 UBool IntlTest::assertEquals(const char* message,
                              UBool expected,
                              UBool actual) {
@@ -2173,18 +2173,25 @@
     return TRUE;
 }
 
-// http://junit.sourceforge.net/javadoc/org/junit/Assert.html#assertEquals(java.lang.String,%20double,%20double,%20double)
-UBool IntlTest::assertEqualsNear(const char *message, double expected, double actual, double precision) {
-    double diff = std::abs(expected - actual);
-    double diffPercent = expected != 0? diff / expected : diff; // If the expected is equals zero, we 
-
-    if (diffPercent > precision) {
-        errln((UnicodeString) "FAIL: " + message + "; got " + actual + "; expected " + expected);
+UBool IntlTest::assertEqualsNear(const char* message,
+                                 double expected,
+                                 double actual,
+                                 double delta) {
+    if (std::isnan(delta) || std::isinf(delta)) {
+        errln((UnicodeString)("FAIL: ") + message + "; nonsensical delta " + delta +
+              " - delta may not be NaN or Inf");
+        return FALSE;
+    }
+    bool bothNaN = std::isnan(expected) && std::isnan(actual);
+    double difference = std::abs(expected - actual);
+    if (expected != actual && (difference > delta || std::isnan(difference)) && !bothNaN) {
+        errln((UnicodeString)("FAIL: ") + message + "; got " + actual + "; expected " + expected +
+              "; acceptable delta " + delta);
         return FALSE;
     }
 #ifdef VERBOSE_ASSERTIONS
     else {
-        logln((UnicodeString) "Ok: " + message + "; got " + expected);
+        logln((UnicodeString)("Ok: ") + message + "; got " + actual);
     }
 #endif
     return TRUE;
@@ -2264,6 +2271,12 @@
                                 int32_t actual) {
     return assertNotEquals(extractToAssertBuf(message), expectedNot, actual);
 }
+UBool IntlTest::assertEqualsNear(const UnicodeString& message,
+                                 double expected,
+                                 double actual,
+                                 double delta) {
+    return assertEqualsNear(extractToAssertBuf(message), expected, actual, delta);
+}
 
 #if !UCONFIG_NO_FORMATTING
 UBool IntlTest::assertEquals(const UnicodeString& message,
diff --git a/icu4c/source/test/intltest/intltest.h b/icu4c/source/test/intltest/intltest.h
index 8f5bd4a..1d8146b 100644
--- a/icu4c/source/test/intltest/intltest.h
+++ b/icu4c/source/test/intltest/intltest.h
@@ -296,11 +296,25 @@
     UBool assertEquals(const char* message, int32_t expected, int32_t actual);
     UBool assertEquals(const char* message, int64_t expected, int64_t actual);
     UBool assertEquals(const char* message, double expected, double actual);
+    /**
+     * Asserts that two doubles are equal to within a positive delta. Returns
+     * false if they are not.
+     *
+     * NaNs are considered equal: assertEquals(msg, NaN, NaN, *) passes.
+     * Infs are considered equal: assertEquals(msg, inf, inf, *) passes.
+     *
+     * @param message - the identifying message for the AssertionError.
+     * @param expected - expected value.
+     * @param actual - the value to check against expected.
+     * @param delta - the maximum delta for the absolute difference between
+     * expected and actual for which both numbers are still considered equal.
+     */
+    UBool assertEqualsNear(const char* message, double expected, double actual, double delta);
     UBool assertEquals(const char* message, UErrorCode expected, UErrorCode actual);
     UBool assertEquals(const char* message, const UnicodeSet& expected, const UnicodeSet& actual);
     UBool assertEquals(const char* message,
         const std::vector<std::string>& expected, const std::vector<std::string>& actual);
-    UBool assertEqualsNear(const char* message, double expected, double actual, double precision);     
+
 #if !UCONFIG_NO_FORMATTING
     UBool assertEquals(const char* message, const Formattable& expected,
                        const Formattable& actual, UBool possibleDataError=FALSE);
@@ -318,6 +332,20 @@
     UBool assertEquals(const UnicodeString& message, int32_t expected, int32_t actual);
     UBool assertEquals(const UnicodeString& message, int64_t expected, int64_t actual);
     UBool assertEquals(const UnicodeString& message, double expected, double actual);
+    /**
+     * Asserts that two doubles are equal to within a positive delta. Returns
+     * false if they are not.
+     *
+     * NaNs are considered equal: assertEquals(msg, NaN, NaN, *) passes.
+     * Infs are considered equal: assertEquals(msg, inf, inf, *) passes.
+     *
+     * @param message - the identifying message for the AssertionError.
+     * @param expected - expected value.
+     * @param actual - the value to check against expected.
+     * @param delta - the maximum delta between expected and actual for which
+     * both numbers are still considered equal.
+     */
+    UBool assertEqualsNear(const UnicodeString& message, double expected, double actual, double delta);
     UBool assertEquals(const UnicodeString& message, UErrorCode expected, UErrorCode actual);
     UBool assertEquals(const UnicodeString& message, const UnicodeSet& expected, const UnicodeSet& actual);
     UBool assertEquals(const UnicodeString& message,
diff --git a/icu4c/source/test/intltest/unitsdatatest.cpp b/icu4c/source/test/intltest/unitsdatatest.cpp
index 8170822..012784c 100644
--- a/icu4c/source/test/intltest/unitsdatatest.cpp
+++ b/icu4c/source/test/intltest/unitsdatatest.cpp
@@ -1,6 +1,8 @@
 // © 2020 and later: Unicode, Inc. and others.
 // License & terms of use: http://www.unicode.org/copyright.html#License
 
+#include "unicode/utypes.h"
+
 #if !UCONFIG_NO_FORMATTING
 
 #include "unitsdata.h"
@@ -12,7 +14,9 @@
 
     void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = NULL);
 
+    void testGetUnitCategory();
     void testGetAllConversionRates();
+    void testGetPreferencesFor();
 };
 
 extern IntlTest *createUnitsDataTest() { return new UnitsDataTest(); }
@@ -20,10 +24,34 @@
 void UnitsDataTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) {
     if (exec) { logln("TestSuite UnitsDataTest: "); }
     TESTCASE_AUTO_BEGIN;
+    TESTCASE_AUTO(testGetUnitCategory);
     TESTCASE_AUTO(testGetAllConversionRates);
+    TESTCASE_AUTO(testGetPreferencesFor);
     TESTCASE_AUTO_END;
 }
 
+void UnitsDataTest::testGetUnitCategory() {
+    struct TestCase {
+        const char *unit;
+        const char *expectedCategory;
+    } testCases[]{
+        {"kilogram-per-cubic-meter", "mass-density"},
+        {"cubic-meter-per-meter", "consumption"},
+        // TODO(CLDR-13787,hugovdm): currently we're treating
+        // consumption-inverse as a separate category. Once consumption
+        // preference handling has been clarified by CLDR-13787, this function
+        // should be fixed.
+        {"meter-per-cubic-meter", "consumption-inverse"},
+    };
+
+    IcuTestErrorCode status(*this, "testGetUnitCategory");
+    for (const auto &t : testCases) {
+        CharString category = getUnitCategory(t.unit, status);
+        status.errIfFailureAndReset("getUnitCategory(%s)", t.unit);
+        assertEquals("category", t.expectedCategory, category.data());
+    }
+}
+
 void UnitsDataTest::testGetAllConversionRates() {
     IcuTestErrorCode status(*this, "testGetAllConversionRates");
     MaybeStackVector<ConversionRateInfo> conversionInfo;
@@ -40,4 +68,84 @@
     }
 }
 
+class UnitPreferencesOpenedUp : public UnitPreferences {
+  public:
+    UnitPreferencesOpenedUp(UErrorCode &status) : UnitPreferences(status) {}
+    const MaybeStackVector<UnitPreferenceMetadata> *getInternalMetadata() const { return &metadata_; }
+    const MaybeStackVector<UnitPreference> *getInternalUnitPrefs() const { return &unitPrefs_; }
+};
+
+/**
+ * This test is dependent upon CLDR Data: when the preferences change, the test
+ * may fail: see the constants for expected Max/Min unit identifiers, for US and
+ * World, and for Roads and default lengths.
+ */
+void UnitsDataTest::testGetPreferencesFor() {
+    const char* USRoadMax = "mile";
+    const char* USRoadMin = "foot";
+    const char* USLenMax = "mile";
+    const char* USLenMin = "inch";
+    const char* WorldRoadMax = "kilometer";
+    const char* WorldRoadMin = "meter";
+    const char* WorldLenMax = "kilometer";
+    const char* WorldLenMin = "centimeter";
+    struct TestCase {
+        const char *name;
+        const char *category;
+        const char *usage;
+        const char *region;
+        const char *expectedBiggest;
+        const char *expectedSmallest;
+    } testCases[]{
+        {"US road", "length", "road", "US", USRoadMax, USRoadMin},
+        {"001 road", "length", "road", "001", WorldRoadMax, WorldRoadMin},
+        {"US lengths", "length", "default", "US", USLenMax, USLenMin},
+        {"001 lengths", "length", "default", "001", WorldLenMax, WorldLenMin},
+        {"XX road falls back to 001", "length", "road", "XX", WorldRoadMax, WorldRoadMin},
+        {"XX default falls back to 001", "length", "default", "XX", WorldLenMax, WorldLenMin},
+        {"Unknown usage US", "length", "foobar", "US", USLenMax, USLenMin},
+        {"Unknown usage 001", "length", "foobar", "XX", WorldLenMax, WorldLenMin},
+        {"Fallback", "length", "person-height-xyzzy", "DE", "meter-and-centimeter",
+         "meter-and-centimeter"},
+        {"Fallback twice", "length", "person-height-xyzzy-foo", "DE", "meter-and-centimeter",
+         "meter-and-centimeter"},
+        // Confirming results for some unitPreferencesTest.txt test cases
+        {"001 area", "area", "default", "001", "square-kilometer", "square-centimeter"},
+        {"GB area", "area", "default", "GB", "square-mile", "square-inch"},
+        {"001 area geograph", "area", "geograph", "001", "square-kilometer", "square-kilometer"},
+        {"GB area geograph", "area", "geograph", "GB", "square-mile", "square-mile"},
+        {"CA person-height", "length", "person-height", "CA", "foot-and-inch", "foot-and-inch"},
+        {"AT person-height", "length", "person-height", "AT", "meter-and-centimeter",
+         "meter-and-centimeter"},
+    };
+    IcuTestErrorCode status(*this, "testGetPreferencesFor");
+    UnitPreferencesOpenedUp preferences(status);
+    auto *metadata = preferences.getInternalMetadata();
+    auto *unitPrefs = preferences.getInternalUnitPrefs();
+    assertTrue(UnicodeString("Metadata count: ") + metadata->length() + " > 200",
+               metadata->length() > 200);
+    assertTrue(UnicodeString("Preferences count: ") + unitPrefs->length() + " > 250",
+               unitPrefs->length() > 250);
+
+    for (const auto &t : testCases) {
+        logln(t.name);
+        const UnitPreference *const *prefs;
+        int32_t prefsCount;
+        preferences.getPreferencesFor(t.category, t.usage, t.region, prefs, prefsCount, status);
+        if (status.errIfFailureAndReset("getPreferencesFor(\"%s\", \"%s\", \"%s\", ...", t.category,
+                                        t.usage, t.region)) {
+            continue;
+        }
+        if (prefsCount > 0) {
+            assertEquals(UnicodeString(t.name) + " - max unit", t.expectedBiggest,
+                         prefs[0]->unit.data());
+            assertEquals(UnicodeString(t.name) + " - min unit", t.expectedSmallest,
+                         prefs[prefsCount - 1]->unit.data());
+        } else {
+            errln(UnicodeString(t.name) + ": failed to find preferences");
+        }
+        status.errIfFailureAndReset("testCase '%s'", t.name);
+    }
+}
+
 #endif /* #if !UCONFIG_NO_FORMATTING */