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 */