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:
parent
cdb028edf5
commit
65bbf92f78
@ -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 */
|
||||
|
@ -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__
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 */
|
||||
|
Loading…
Reference in New Issue
Block a user