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
This commit is contained in:
Hugo van der Merwe 2020-07-23 17:19:51 +02:00
parent cdb028edf5
commit 65bbf92f78
6 changed files with 575 additions and 23 deletions

View File

@ -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 @@ U_NAMESPACE_BEGIN
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 @@ class ConversionRateDataSink : public ResourceSink {
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 @@ class ConversionRateDataSink : public ResourceSink {
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 @@ class ConversionRateDataSink : public ResourceSink {
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 @@ const ConversionRateInfo *ConversionRates::extractConversionInfo(StringPiece sou
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 */

View File

@ -14,6 +14,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 @@ U_NAMESPACE_BEGIN
*/
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 @@ class U_I18N_API ConversionRateInfo : public UMemory {
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 @@ class U_I18N_API ConversionRates {
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__

View File

@ -1076,7 +1076,7 @@ group: units
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

View File

@ -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 @@ UBool IntlTest::assertEquals(const char* message,
return TRUE;
}
UBool IntlTest::assertEquals(const char* message,
UBool expected,
UBool actual) {
@ -2173,18 +2173,25 @@ UBool IntlTest::assertNotEquals(const char* message,
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 @@ UBool IntlTest::assertNotEquals(const UnicodeString &message,
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,

View File

@ -296,11 +296,25 @@ public:
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 @@ public:
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,

View File

@ -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 @@ class UnitsDataTest : public IntlTest {
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 @@ extern IntlTest *createUnitsDataTest() { return new UnitsDataTest(); }
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 @@ void UnitsDataTest::testGetAllConversionRates() {
}
}
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 */