ICU-20568 Use Impl libraries, add precision UnitsRouter#route output

Add precision to the output of UnitsRouter#route
PR: https://github.com/icu-units/icu/pull/10
Commit: 030bda3ec8

Use `Impl` libraries for all internal libraries
PR: https://github.com/icu-units/icu/pull/15
Commit: cc786cfb3b

Sort the units in ComplexUnitConverter
PR: https://github.com/icu-units/icu/pull/6
Commit: f65b181c4447bb4eb9eef5dc20ea1b296d053ffa
This commit is contained in:
younies 2020-06-23 14:46:50 +02:00 committed by Hugo van der Merwe
parent 7ed2a2d233
commit 1b853904cd
13 changed files with 374 additions and 205 deletions

View file

@ -5,33 +5,64 @@
#if !UCONFIG_NO_FORMATTING
#include <math.h>
#include <cmath>
#include "cmemory.h"
#include "complexunitsconverter.h"
#include "uarrsort.h"
#include "uassert.h"
#include "unicode/fmtable.h"
#include "unicode/localpointer.h"
#include "unicode/measunit.h"
#include "unicode/measure.h"
#include "unitconverter.h"
U_NAMESPACE_BEGIN
namespace units {
ComplexUnitsConverter::ComplexUnitsConverter(const MeasureUnit &inputUnit,
const MeasureUnit &outputUnits,
const ConversionRates &ratesInfo, UErrorCode &status) {
if (outputUnits.getComplexity(status) != UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) {
unitConverters_.emplaceBackAndCheckErrorCode(status, inputUnit, outputUnits, ratesInfo, status);
if (U_FAILURE(status)) {
return;
}
units_.emplaceBackAndCheckErrorCode(status, outputUnits);
ComplexUnitsConverter::ComplexUnitsConverter(const MeasureUnitImpl &inputUnit,
const MeasureUnitImpl &outputUnits,
const ConversionRates &ratesInfo, UErrorCode &status)
: units_(outputUnits.extractIndividualUnits(status)) {
if (U_FAILURE(status)) {
return;
}
U_ASSERT(units_.length() != 0);
// NOTE:
// This comparator is used to sort the units in a descending order. Therefore, we return -1 if
// the left is bigger than right and so on.
auto descendingCompareUnits = [](const void *context, const void *left, const void *right) {
UErrorCode status = U_ZERO_ERROR;
const auto *leftPointer = static_cast<const MeasureUnitImpl *const *>(left);
const auto *rightPointer = static_cast<const MeasureUnitImpl *const *>(right);
UnitConverter fromLeftToRight(**leftPointer, //
**rightPointer, //
*static_cast<const ConversionRates *>(context), //
status);
double rightFromOneLeft = fromLeftToRight.convert(1.0);
if (std::abs(rightFromOneLeft - 1.0) < 0.0000000001) { // Equals To
return 0;
} else if (rightFromOneLeft > 1.0) { // Greater Than
return -1;
}
return 1; // Less Than
};
uprv_sortArray(units_.getAlias(), //
units_.length(), //
sizeof units_[0], /* NOTE: we have already asserted that the units_ is not empty.*/ //
descendingCompareUnits, //
&ratesInfo, //
false, //
&status //
);
// In case the `outputUnits` are `UMEASURE_UNIT_MIXED` such as `foot+inch`. In this case we need more
// converters to convert from the `inputUnit` to the first unit in the `outputUnits`. Then, a
// converter from the first unit in the `outputUnits` to the second unit and so on.
@ -46,47 +77,19 @@ ComplexUnitsConverter::ComplexUnitsConverter(const MeasureUnit &inputUnit,
// 2. convert the residual of 6.56168 feet (0.56168) to inches, which will be (6.74016
// inches)
// 3. then, the final result will be (6 feet and 6.74016 inches)
int32_t length;
auto singleUnits = outputUnits.splitToSingleUnits(length, status);
MaybeStackVector<MeasureUnit> singleUnitsInOrder;
for (int i = 0; i < length; ++i) {
/**
* TODO(younies): ensure units being in order by the biggest unit at first.
*
* HINT:
* MaybeStackVector<SingleUnitImpl> singleUnitsInOrder = MeasureUnitImpl::forMeasureUnitMaybeCopy(outputUnits, status).units;
* uprv_sortArray(
* singleUnitsInOrder.getAlias(),
* singleUnitsInOrder.length(),
* sizeof(singleUnitsInOrder[0]),
* compareSingleUnits,
* nullptr,
* false,
* &status);
*/
singleUnitsInOrder.emplaceBackAndCheckErrorCode(status, singleUnits[i]);
}
if (singleUnitsInOrder.length() == 0) {
status = U_ILLEGAL_ARGUMENT_ERROR;
return;
}
for (int i = 0, n = singleUnitsInOrder.length(); i < n; i++) {
for (int i = 0, n = units_.length(); i < n; i++) {
if (i == 0) { // first element
unitConverters_.emplaceBackAndCheckErrorCode(status, inputUnit, *singleUnitsInOrder[i],
ratesInfo, status);
unitConverters_.emplaceBackAndCheckErrorCode(status, inputUnit, *units_[i], ratesInfo,
status);
} else {
unitConverters_.emplaceBackAndCheckErrorCode(status, *singleUnitsInOrder[i - 1],
*singleUnitsInOrder[i], ratesInfo, status);
unitConverters_.emplaceBackAndCheckErrorCode(status, *units_[i - 1], *units_[i], ratesInfo,
status);
}
if (U_FAILURE(status)) {
return;
}
}
units_.appendAll(singleUnitsInOrder, status);
}
UBool ComplexUnitsConverter::greaterThanOrEqual(double quantity, double limit) const {
@ -107,8 +110,8 @@ MaybeStackVector<Measure> ComplexUnitsConverter::convert(double quantity, UError
Formattable formattableNewQuantity(newQuantity);
// NOTE: Measure would own its MeasureUnit.
result.emplaceBackAndCheckErrorCode(status, formattableNewQuantity,
new MeasureUnit(*units_[i]), status);
MeasureUnit *type = new MeasureUnit(units_[i]->copy(status).build(status));
result.emplaceBackAndCheckErrorCode(status, formattableNewQuantity, type, status);
// Keep the residual of the quantity.
// For example: `3.6 feet`, keep only `0.6 feet`
@ -117,8 +120,8 @@ MaybeStackVector<Measure> ComplexUnitsConverter::convert(double quantity, UError
Formattable formattableQuantity(quantity);
// NOTE: Measure would own its MeasureUnit.
result.emplaceBackAndCheckErrorCode(status, formattableQuantity, new MeasureUnit(*units_[i]),
status);
MeasureUnit *type = new MeasureUnit(units_[i]->copy(status).build(status));
result.emplaceBackAndCheckErrorCode(status, formattableQuantity, type, status);
}
}

View file

@ -8,15 +8,14 @@
#define __COMPLEXUNITSCONVERTER_H__
#include "cmemory.h"
#include "unicode/measunit.h"
#include "measunit_impl.h"
#include "unicode/errorcode.h"
#include "unicode/measure.h"
#include "unitconverter.h"
#include "unitsdata.h"
U_NAMESPACE_BEGIN
// Forward declarations
class Measure;
namespace units {
/**
@ -28,7 +27,7 @@ namespace units {
* single unit to another single unit). Therefore, `ComplexUnitsConverter` class contains multiple
* instances of the `UnitConverter` to perform the conversion.
*/
class U_I18N_API ComplexUnitsConverter {
class U_I18N_API ComplexUnitsConverter : UMemory {
public:
/**
* Constructor of `ComplexUnitsConverter`.
@ -40,7 +39,7 @@ class U_I18N_API ComplexUnitsConverter {
* @param outputUnits represents the output unit. could be any type. (single, compound or mixed).
* @param status
*/
ComplexUnitsConverter(const MeasureUnit &inputUnit, const MeasureUnit &outputUnits,
ComplexUnitsConverter(const MeasureUnitImpl &inputUnit, const MeasureUnitImpl &outputUnits,
const ConversionRates &ratesInfo, UErrorCode &status);
// Returns true if the specified `quantity` of the `inputUnit`, expressed in terms of the biggest
@ -60,7 +59,7 @@ class U_I18N_API ComplexUnitsConverter {
private:
MaybeStackVector<UnitConverter> unitConverters_;
MaybeStackVector<MeasureUnit> units_;
MaybeStackVector<MeasureUnitImpl> units_;
};
} // namespace units

View file

@ -788,6 +788,14 @@ const char *SingleUnitImpl::getSimpleUnitID() const {
return gSimpleUnits[index];
}
MeasureUnitImpl::MeasureUnitImpl(const MeasureUnitImpl &other, UErrorCode &status) {
*this = other.copy(status);
}
MeasureUnitImpl::MeasureUnitImpl(const SingleUnitImpl &singleUnit, UErrorCode &status) {
this->append(singleUnit, status);
}
MeasureUnitImpl MeasureUnitImpl::forIdentifier(StringPiece identifier, UErrorCode& status) {
return Parser::from(identifier, status).parse(status);
}
@ -823,12 +831,26 @@ bool MeasureUnitImpl::append(const SingleUnitImpl& singleUnit, UErrorCode& statu
return appendImpl(*this, singleUnit, status);
}
MaybeStackVector<MeasureUnitImpl> MeasureUnitImpl::extractIndividualUnits(UErrorCode &status) const {
MaybeStackVector<MeasureUnitImpl> result;
if (this->complexity != UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) {
result.emplaceBackAndCheckErrorCode(status, *this, status);
return result;
}
for (int32_t i = 0; i < units.length(); i++) {
result.emplaceBackAndCheckErrorCode(status, *units[i], status);
}
return result;
}
MeasureUnit MeasureUnitImpl::build(UErrorCode& status) && {
serialize(*this, status);
return MeasureUnit(std::move(*this));
}
MeasureUnit MeasureUnit::forIdentifier(StringPiece identifier, UErrorCode& status) {
return Parser::from(identifier, status).parse(status).build(status);
}

View file

@ -134,6 +134,13 @@ template class U_I18N_API MaybeStackVector<SingleUnitImpl, 8>;
* including mixed and compound units.
*/
struct U_I18N_API MeasureUnitImpl : public UMemory {
MeasureUnitImpl() = default;
MeasureUnitImpl(MeasureUnitImpl &&other) = default;
MeasureUnitImpl(const MeasureUnitImpl &other, UErrorCode &status);
MeasureUnitImpl(const SingleUnitImpl &singleUnit, UErrorCode &status);
MeasureUnitImpl &operator=(MeasureUnitImpl &&other) noexcept = default;
/** Extract the MeasureUnitImpl from a MeasureUnit. */
static inline const MeasureUnitImpl* get(const MeasureUnit& measureUnit) {
return measureUnit.fImpl;
@ -189,6 +196,16 @@ struct U_I18N_API MeasureUnitImpl : public UMemory {
*/
MeasureUnitImpl copy(UErrorCode& status) const;
/**
* Extracts the list of all the individual units inside the `MeasureUnitImpl`.
* For example:
* - if the `MeasureUnitImpl` is `foot-per-hour`
* it will return a list of 1 {`foot-per-hour`}
* - if the `MeasureUnitImpl` is `foot-and-inch`
* it will return a list of 2 { `foot`, `inch`}
*/
MaybeStackVector<MeasureUnitImpl> extractIndividualUnits(UErrorCode &status) const;
/** Mutates this MeasureUnitImpl to take the reciprocal. */
void takeReciprocal(UErrorCode& status);

View file

@ -101,15 +101,17 @@ void UsagePrefsHandler::processQuantity(DecimalQuantity &quantity, MicroProps &m
}
quantity.roundToInfinity(); // Enables toDouble
auto routed = fUnitsRouter.route(quantity.toDouble(), status);
micros.outputUnit = routed[0]->getUnit();
quantity.setToDouble(routed[0]->getNumber().getDouble());
const auto routed = fUnitsRouter.route(quantity.toDouble(), status);
const auto& routedUnits = routed.measures;
micros.outputUnit = routedUnits[0]->getUnit();
quantity.setToDouble(routedUnits[0]->getNumber().getDouble());
// TODO(units): here we are always overriding Precision. (1) get precision
// from fUnitsRouter, (2) ensure we use the UnitPreference skeleton's
// precision only when there isn't an explicit override we prefer to use.
// This needs to be handled within
// NumberFormatterImpl::macrosToMicroGenerator in number_formatimpl.cpp
// TODO: Use precision from `routed` result.
Precision precision = Precision::integer().withMinDigits(2);
UNumberFormatRoundingMode roundingMode;
// Temporary until ICU 64?

View file

@ -10,8 +10,8 @@
#include "double-conversion-string-to-double.h"
#include "measunit_impl.h"
#include "uassert.h"
#include "unicode/errorcode.h"
#include "unicode/localpointer.h"
#include "unicode/measunit.h"
#include "unicode/stringpiece.h"
#include "unitconverter.h"
#include <algorithm>
@ -88,20 +88,6 @@ void U_I18N_API Factor::applySiPrefix(UMeasureSIPrefix siPrefix) {
}
void U_I18N_API Factor::substituteConstants() {
// These values are a hard-coded subset of unitConstants in the units
// resources file. A unit test checks that all constants in the resource
// file are at least recognised by the code. Derived constants' values or
// hard-coded derivations are not checked.
// double constantsValues[CONSTANTS_COUNT];
static const double constantsValues[CONSTANTS_COUNT] = {
[CONSTANT_FT2M] = 0.3048, //
[CONSTANT_PI] = 411557987.0 / 131002976.0, //
[CONSTANT_GRAVITY] = 9.80665, //
[CONSTANT_G] = 6.67408E-11, //
[CONSTANT_GAL_IMP2M3] = 0.00454609, //
[CONSTANT_LB2KG] = 0.45359237, //
};
for (int i = 0; i < CONSTANTS_COUNT; i++) {
if (this->constants[i] == 0) {
continue;
@ -235,16 +221,12 @@ Factor loadSingleFactor(StringPiece source, const ConversionRates &ratesInfo, UE
}
// Load Factor of a compound source unit.
Factor loadCompoundFactor(const MeasureUnit &source, const ConversionRates &ratesInfo,
Factor loadCompoundFactor(const MeasureUnitImpl &source, const ConversionRates &ratesInfo,
UErrorCode &status) {
Factor result;
MeasureUnitImpl memory;
const auto &compoundSourceUnit = MeasureUnitImpl::forMeasureUnit(source, memory, status);
if (U_FAILURE(status)) return result;
for (int32_t i = 0, n = compoundSourceUnit.units.length(); i < n; i++) {
auto singleUnit = *compoundSourceUnit.units[i]; // a SingleUnitImpl
for (int32_t i = 0, n = source.units.length(); i < n; i++) {
SingleUnitImpl singleUnit = *source.units[i];
Factor singleFactor = loadSingleFactor(singleUnit.getSimpleUnitID(), ratesInfo, status);
if (U_FAILURE(status)) return result;
@ -264,30 +246,35 @@ Factor loadCompoundFactor(const MeasureUnit &source, const ConversionRates &rate
/**
* Checks if the source unit and the target unit are simple. For example celsius or fahrenheit. But not
* square-celsius or square-fahrenheit.
*
* NOTE:
* Empty unit means simple unit.
*/
UBool checkSimpleUnit(const MeasureUnit &unit, UErrorCode &status) {
MeasureUnitImpl memory;
const auto &compoundSourceUnit = MeasureUnitImpl::forMeasureUnit(unit, memory, status);
UBool checkSimpleUnit(const MeasureUnitImpl &unit, UErrorCode &status) {
if (U_FAILURE(status)) return false;
if (compoundSourceUnit.complexity != UMEASURE_UNIT_SINGLE) {
if (unit.complexity != UMEASURE_UNIT_SINGLE) {
return false;
}
if (unit.units.length() == 0) {
// Empty units means simple unit.
return true;
}
U_ASSERT(compoundSourceUnit.units.length() == 1);
auto singleUnit = *(compoundSourceUnit.units[0]);
auto singleUnit = *(unit.units[0]);
if (singleUnit.dimensionality != 1 || singleUnit.siPrefix != UMEASURE_SI_PREFIX_ONE) {
return false;
}
return true;
}
/**
* Extract conversion rate from `source` to `target`
*/
void loadConversionRate(ConversionRate &conversionRate, const MeasureUnit &source,
const MeasureUnit &target, UnitsConvertibilityState unitsState,
void loadConversionRate(ConversionRate &conversionRate, const MeasureUnitImpl &source,
const MeasureUnitImpl &target, Convertibility unitsState,
const ConversionRates &ratesInfo, UErrorCode &status) {
// Represents the conversion factor from the source to the target.
Factor finalFactor;
@ -299,9 +286,9 @@ void loadConversionRate(ConversionRate &conversionRate, const MeasureUnit &sourc
// Merger Factors
finalFactor.multiplyBy(sourceToBase);
if (unitsState == UnitsConvertibilityState::CONVERTIBLE) {
if (unitsState == Convertibility::CONVERTIBLE) {
finalFactor.divideBy(targetToBase);
} else if (unitsState == UnitsConvertibilityState::RECIPROCAL) {
} else if (unitsState == Convertibility::RECIPROCAL) {
finalFactor.multiplyBy(targetToBase);
} else {
status = UErrorCode::U_ARGUMENT_TYPE_MISMATCH;
@ -321,7 +308,48 @@ void loadConversionRate(ConversionRate &conversionRate, const MeasureUnit &sourc
targetToBase.offset * targetToBase.factorDen / targetToBase.factorNum;
}
conversionRate.reciprocal = unitsState == UnitsConvertibilityState::RECIPROCAL;
conversionRate.reciprocal = unitsState == Convertibility::RECIPROCAL;
}
struct UnitIndexAndDimension : UMemory {
int32_t index = 0;
int32_t dimensionality = 0;
UnitIndexAndDimension(const SingleUnitImpl &singleUnit, int32_t multiplier) {
index = singleUnit.index;
dimensionality = singleUnit.dimensionality * multiplier;
}
};
void mergeSingleUnitWithDimension(MaybeStackVector<UnitIndexAndDimension> &unitIndicesWithDimension,
const SingleUnitImpl &shouldBeMerged, int32_t multiplier) {
for (int32_t i = 0; i < unitIndicesWithDimension.length(); i++) {
auto &unitWithIndex = *unitIndicesWithDimension[i];
if (unitWithIndex.index == shouldBeMerged.index) {
unitWithIndex.dimensionality += shouldBeMerged.dimensionality * multiplier;
return;
}
}
unitIndicesWithDimension.emplaceBack(shouldBeMerged, multiplier);
}
void mergeUnitsAndDimensions(MaybeStackVector<UnitIndexAndDimension> &unitIndicesWithDimension,
const MeasureUnitImpl &shouldBeMerged, int32_t multiplier) {
for (int32_t unit_i = 0; unit_i < shouldBeMerged.units.length(); unit_i++) {
auto singleUnit = *shouldBeMerged.units[unit_i];
mergeSingleUnitWithDimension(unitIndicesWithDimension, singleUnit, multiplier);
}
}
UBool checkAllDimensionsAreZeros(const MaybeStackVector<UnitIndexAndDimension> &dimensionVector) {
for (int32_t i = 0; i < dimensionVector.length(); i++) {
if (dimensionVector[i]->dimensionality != 0) {
return false;
}
}
return true;
}
} // namespace
@ -368,21 +396,20 @@ void U_I18N_API addSingleFactorConstant(StringPiece baseStr, int32_t power, Sign
* Extracts the compound base unit of a compound unit (`source`). For example, if the source unit is
* `square-mile-per-hour`, the compound base unit will be `square-meter-per-second`
*/
MeasureUnit U_I18N_API extractCompoundBaseUnit(const MeasureUnit &source,
const ConversionRates &conversionRates,
UErrorCode &status) {
MeasureUnit result;
int32_t count;
const auto singleUnits = source.splitToSingleUnits(count, status);
MeasureUnitImpl U_I18N_API extractCompoundBaseUnit(const MeasureUnitImpl &source,
const ConversionRates &conversionRates,
UErrorCode &status) {
MeasureUnitImpl result;
if (U_FAILURE(status)) return result;
for (int i = 0; i < count; ++i) {
const auto &singleUnit = singleUnits[i];
const auto &singleUnits = source.units;
for (int i = 0, count = singleUnits.length(); i < count; ++i) {
const auto &singleUnit = *singleUnits[i];
// Extract `ConversionRateInfo` using the absolute unit. For example: in case of `square-meter`,
// we will use `meter`
const auto singleUnitImpl = SingleUnitImpl::forMeasureUnit(singleUnit, status);
const auto rateInfo =
conversionRates.extractConversionInfo(singleUnitImpl.getSimpleUnitID(), status);
conversionRates.extractConversionInfo(singleUnit.getSimpleUnitID(), status);
if (U_FAILURE(status)) {
return result;
}
@ -393,14 +420,12 @@ MeasureUnit U_I18N_API extractCompoundBaseUnit(const MeasureUnit &source,
// Multiply the power of the singleUnit by the power of the baseUnit. For example, square-hectare
// must be pow4-meter. (NOTE: hectare --> square-meter)
auto compoundBaseUnit = MeasureUnit::forIdentifier(rateInfo->baseUnit.toStringPiece(), status);
int32_t baseUnitsCount;
auto baseUnits = compoundBaseUnit.splitToSingleUnits(baseUnitsCount, status);
for (int j = 0; j < baseUnitsCount; j++) {
int8_t newDimensionality =
baseUnits[j].getDimensionality(status) * singleUnit.getDimensionality(status);
result = result.product(baseUnits[j].withDimensionality(newDimensionality, status), status);
auto baseUnits =
MeasureUnitImpl::forIdentifier(rateInfo->baseUnit.toStringPiece(), status).units;
for (int32_t i = 0, baseUnitsCount = baseUnits.length(); i < baseUnitsCount; i++) {
baseUnits[i]->dimensionality *= singleUnit.dimensionality;
// TODO: Deal with SI-prefix
result.append(*baseUnits[i], status);
if (U_FAILURE(status)) {
return result;
@ -411,53 +436,70 @@ MeasureUnit U_I18N_API extractCompoundBaseUnit(const MeasureUnit &source,
return result;
}
UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &source,
const MeasureUnit &target,
const ConversionRates &conversionRates,
UErrorCode &status) {
if (source.getComplexity(status) == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED ||
target.getComplexity(status) == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) {
/**
* Determine the convertibility between `source` and `target`.
* For example:
* `meter` and `foot` are `CONVERTIBLE`.
* `meter-per-second` and `second-per-meter` are `RECIPROCAL`.
* `meter` and `pound` are `UNCONVERTIBLE`.
*
* NOTE:
* Only works with SINGLE and COMPOUND units. If one of the units is a
* MIXED unit, an error will occur. For more information, see UMeasureUnitComplexity.
*/
Convertibility U_I18N_API extractConvertibility(const MeasureUnitImpl &source,
const MeasureUnitImpl &target,
const ConversionRates &conversionRates,
UErrorCode &status) {
if (source.complexity == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED ||
target.complexity == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) {
status = U_INTERNAL_PROGRAM_ERROR;
return UNCONVERTIBLE;
}
auto sourceBaseUnit = extractCompoundBaseUnit(source, conversionRates, status);
auto targetBaseUnit = extractCompoundBaseUnit(target, conversionRates, status);
MeasureUnitImpl sourceBaseUnit = extractCompoundBaseUnit(source, conversionRates, status);
MeasureUnitImpl targetBaseUnit = extractCompoundBaseUnit(target, conversionRates, status);
if (U_FAILURE(status)) return UNCONVERTIBLE;
if (sourceBaseUnit == targetBaseUnit) return CONVERTIBLE;
if (sourceBaseUnit == targetBaseUnit.reciprocal(status)) return RECIPROCAL;
MaybeStackVector<UnitIndexAndDimension> convertible;
MaybeStackVector<UnitIndexAndDimension> reciprocal;
auto sourceSimplified = sourceBaseUnit.simplify(status);
auto targetSimplified = targetBaseUnit.simplify(status);
mergeUnitsAndDimensions(convertible, sourceBaseUnit, 1);
mergeUnitsAndDimensions(reciprocal, sourceBaseUnit, 1);
if (sourceSimplified == targetSimplified) return CONVERTIBLE;
if (sourceSimplified == targetSimplified.reciprocal(status)) return RECIPROCAL;
mergeUnitsAndDimensions(convertible, targetBaseUnit, -1);
mergeUnitsAndDimensions(reciprocal, targetBaseUnit, 1);
if (checkAllDimensionsAreZeros(convertible)) {
return CONVERTIBLE;
}
if (checkAllDimensionsAreZeros(reciprocal)) {
return RECIPROCAL;
}
return UNCONVERTIBLE;
}
UnitConverter::UnitConverter(MeasureUnit source, MeasureUnit target, const ConversionRates &ratesInfo,
UErrorCode &status) {
if (source.getComplexity(status) == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED ||
target.getComplexity(status) == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) {
UnitConverter::UnitConverter(const MeasureUnitImpl &source, const MeasureUnitImpl &target,
const ConversionRates &ratesInfo, UErrorCode &status)
: conversionRate_(source.copy(status), target.copy(status)) {
if (source.complexity == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED ||
target.complexity == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) {
status = U_INTERNAL_PROGRAM_ERROR;
return;
}
UnitsConvertibilityState unitsState = checkConvertibility(source, target, ratesInfo, status);
Convertibility unitsState = extractConvertibility(source, target, ratesInfo, status);
if (U_FAILURE(status)) return;
if (unitsState == UnitsConvertibilityState::UNCONVERTIBLE) {
if (unitsState == Convertibility::UNCONVERTIBLE) {
status = U_INTERNAL_PROGRAM_ERROR;
return;
}
conversionRate_.source = source;
conversionRate_.target = target;
loadConversionRate(conversionRate_, source, target, unitsState, ratesInfo, status);
loadConversionRate(conversionRate_, conversionRate_.source, conversionRate_.target, unitsState,
ratesInfo, status);
}
double UnitConverter::convert(double inputValue) const {

View file

@ -7,9 +7,12 @@
#ifndef __UNITCONVERTER_H__
#define __UNITCONVERTER_H__
#include "unicode/measunit.h"
#include "cmemory.h"
#include "measunit_impl.h"
#include "unicode/errorcode.h"
#include "unicode/stringpiece.h"
#include "unicode/uobject.h"
#include "unitconverter.h"
#include "unitsdata.h"
U_NAMESPACE_BEGIN
@ -29,6 +32,19 @@ enum Constants {
CONSTANTS_COUNT
};
// These values are a hard-coded subset of unitConstants in the units
// resources file. A unit test checks that all constants in the resource
// file are at least recognised by the code. Derived constants' values or
// hard-coded derivations are not checked.
static const double constantsValues[CONSTANTS_COUNT] = {
0.3048, // CONSTANT_FT2M
411557987.0 / 131002976.0, // CONSTANT_PI
9.80665, // CONSTANT_GRAVITY
6.67408E-11, // CONSTANT_G
0.00454609, // CONSTANT_GAL_IMP2M3
0.45359237, // CONSTANT_LB2KG
};
typedef enum Signum {
NEGATIVE = -1,
POSITIVE = 1,
@ -65,25 +81,28 @@ void U_I18N_API addSingleFactorConstant(StringPiece baseStr, int32_t power, Sign
/**
* Represents the conversion rate between `source` and `target`.
*/
struct ConversionRate {
MeasureUnit source;
MeasureUnit target;
struct ConversionRate : public UMemory {
const MeasureUnitImpl source;
const MeasureUnitImpl target;
double factorNum = 1;
double factorDen = 1;
double sourceOffset = 0;
double targetOffset = 0;
bool reciprocal = false;
ConversionRate(MeasureUnitImpl &&source, MeasureUnitImpl &&target)
: source(std::move(source)), target(std::move(target)) {}
};
enum U_I18N_API UnitsConvertibilityState {
enum U_I18N_API Convertibility {
RECIPROCAL,
CONVERTIBLE,
UNCONVERTIBLE,
};
MeasureUnit U_I18N_API extractCompoundBaseUnit(const MeasureUnit &source,
const ConversionRates &conversionRates,
UErrorCode &status);
MeasureUnitImpl U_I18N_API extractCompoundBaseUnit(const MeasureUnitImpl &source,
const ConversionRates &conversionRates,
UErrorCode &status);
/**
* Check if the convertibility between `source` and `target`.
@ -96,10 +115,10 @@ MeasureUnit U_I18N_API extractCompoundBaseUnit(const MeasureUnit &source,
* Only works with SINGLE and COMPOUND units. If one of the units is a
* MIXED unit, an error will occur. For more information, see UMeasureUnitComplexity.
*/
UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &source,
const MeasureUnit &target,
const ConversionRates &conversionRates,
UErrorCode &status);
Convertibility U_I18N_API extractConvertibility(const MeasureUnitImpl &source,
const MeasureUnitImpl &target,
const ConversionRates &conversionRates,
UErrorCode &status);
/**
* Converts from a source `MeasureUnit` to a target `MeasureUnit`.
@ -121,8 +140,8 @@ class U_I18N_API UnitConverter : public UMemory {
* @param ratesInfo Contains all the needed conversion rates.
* @param status
*/
UnitConverter(MeasureUnit source, MeasureUnit target, const ConversionRates &ratesInfo,
UErrorCode &status);
UnitConverter(const MeasureUnitImpl &source, const MeasureUnitImpl &target,
const ConversionRates &ratesInfo, UErrorCode &status);
/**
* Convert a value in the source unit to another value in the target unit.

View file

@ -214,9 +214,7 @@ class UnitPreferencesSink : public ResourceSink {
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);
up->skeleton = value.getUnicodeString(status);
}
}
}

View file

@ -7,6 +7,8 @@
#ifndef __GETUNITSDATA_H__
#define __GETUNITSDATA_H__
#include <limits>
#include "charstr.h"
#include "cmemory.h"
#include "unicode/stringpiece.h"
@ -92,10 +94,11 @@ class U_I18N_API ConversionRates {
// Encapsulates unitPreferenceData information from units resources, specifying
// a sequence of output unit preferences.
struct U_I18N_API UnitPreference : public UMemory {
UnitPreference() : geq(1) {}
// Set geq to 1.0 by default
UnitPreference() : geq(1.0) {}
CharString unit;
double geq;
CharString skeleton;
UnicodeString skeleton;
};
/**

View file

@ -7,8 +7,11 @@
#include "charstr.h"
#include "cmemory.h"
#include "cstring.h"
#include "measunit_impl.h"
#include "number_decimalquantity.h"
#include "resource.h"
#include "unicode/measure.h"
#include "unitconverter.h"
#include "unitsdata.h"
#include "unitsrouter.h"
@ -22,7 +25,9 @@ UnitsRouter::UnitsRouter(MeasureUnit inputUnit, StringPiece region, StringPiece
ConversionRates conversionRates(status);
UnitPreferences prefs(status);
MeasureUnit baseUnit = extractCompoundBaseUnit(inputUnit, conversionRates, status);
MeasureUnitImpl inputUnitImpl = MeasureUnitImpl::forMeasureUnitMaybeCopy(inputUnit, status);
MeasureUnit baseUnit =
(extractCompoundBaseUnit(inputUnitImpl, conversionRates, status)).build(status);
CharString category = getUnitCategory(baseUnit.getIdentifier(), status);
const UnitPreference *const *unitPreferences;
@ -32,35 +37,57 @@ UnitsRouter::UnitsRouter(MeasureUnit inputUnit, StringPiece region, StringPiece
for (int i = 0; i < preferencesCount; ++i) {
const auto &preference = *unitPreferences[i];
MeasureUnit complexTargetUnit = MeasureUnit::forIdentifier(preference.unit.data(), status);
MeasureUnitImpl complexTargetUnitImpl =
MeasureUnitImpl::forIdentifier(preference.unit.data(), status);
if (U_FAILURE(status)) {
return;
}
outputUnits_.emplaceBackAndCheckErrorCode(status, complexTargetUnit);
converterPreferences_.emplaceBackAndCheckErrorCode(status, inputUnit, complexTargetUnit,
preference.geq, conversionRates, status);
UnicodeString precision = preference.skeleton;
// For now, we only have "precision-increment" in Units Preferences skeleton.
// Therefore, we check if the skeleton starts with "precision-increment" and force the program to
// fail otherwise.
// NOTE:
// It is allowed to have an empty precision.
if (!precision.isEmpty() && !precision.startsWith(u"precision-increment", 19)) {
status = U_INTERNAL_PROGRAM_ERROR;
return;
}
outputUnits_.emplaceBackAndCheckErrorCode(status,
complexTargetUnitImpl.copy(status).build(status));
converterPreferences_.emplaceBackAndCheckErrorCode(status, inputUnitImpl, complexTargetUnitImpl,
preference.geq, std::move(precision),
conversionRates, status);
if (U_FAILURE(status)) {
return;
}
}
}
MaybeStackVector<Measure> UnitsRouter::route(double quantity, UErrorCode &status) const {
RouteResult UnitsRouter::route(double quantity, UErrorCode &status) const {
for (int i = 0, n = converterPreferences_.length(); i < n; i++) {
const auto &converterPreference = *converterPreferences_[i];
if (converterPreference.converter.greaterThanOrEqual(quantity, converterPreference.limit)) {
return converterPreference.converter.convert(quantity, status);
return RouteResult(converterPreference.converter.convert(quantity, status), //
converterPreference.precision //
);
}
}
// In case of the `quantity` does not fit in any converter limit, use the last converter.
const auto &lastConverter = (*converterPreferences_[converterPreferences_.length() - 1]).converter;
return lastConverter.convert(quantity, status);
const auto &lastConverterPreference = (*converterPreferences_[converterPreferences_.length() - 1]);
return RouteResult(lastConverterPreference.converter.convert(quantity, status), //
lastConverterPreference.precision //
);
}
const MaybeStackVector<MeasureUnit> *UnitsRouter::getOutputUnits() const {
// TODO: consider pulling this from converterPreferences_ and dropping
// outputUnits_?
return &outputUnits_;
}

View file

@ -11,6 +11,7 @@
#include "cmemory.h"
#include "complexunitsconverter.h"
#include "measunit_impl.h"
#include "unicode/measunit.h"
#include "unicode/stringpiece.h"
#include "unicode/uobject.h"
@ -23,6 +24,14 @@ class Measure;
namespace units {
struct RouteResult : UMemory {
MaybeStackVector<Measure> measures;
UnicodeString precision;
RouteResult(MaybeStackVector<Measure> measures, UnicodeString precision)
: measures(std::move(measures)), precision(std::move(precision)) {}
};
/**
* Contains the complex unit converter and the limit which representing the smallest value that the
* converter should accept. For example, if the converter is converting to `foot+inch` and the limit
@ -35,15 +44,19 @@ namespace units {
struct ConverterPreference : UMemory {
ComplexUnitsConverter converter;
double limit;
UnicodeString precision;
ConverterPreference(MeasureUnit source, MeasureUnit complexTarget, double limit,
const ConversionRates &ratesInfo, UErrorCode &status)
: converter(source, complexTarget, ratesInfo, status), limit(limit) {}
// In case there is no limit, the limit will be -inf.
ConverterPreference(const MeasureUnitImpl &source, const MeasureUnitImpl &complexTarget,
UnicodeString precision, const ConversionRates &ratesInfo, UErrorCode &status)
: ConverterPreference(source, complexTarget, std::numeric_limits<double>::lowest(), precision,
ratesInfo, status) {}
ConverterPreference(MeasureUnit source, MeasureUnit complexTarget, const ConversionRates &ratesInfo,
ConverterPreference(const MeasureUnitImpl &source, const MeasureUnitImpl &complexTarget,
double limit, UnicodeString precision, const ConversionRates &ratesInfo,
UErrorCode &status)
: ConverterPreference(source, complexTarget, std::numeric_limits<double>::lowest(), ratesInfo,
status) {}
: converter(source, complexTarget, ratesInfo, status), limit(limit),
precision(std::move(precision)) {}
};
/**
@ -78,7 +91,7 @@ class U_I18N_API UnitsRouter {
public:
UnitsRouter(MeasureUnit inputUnit, StringPiece locale, StringPiece usage, UErrorCode &status);
MaybeStackVector<Measure> route(double quantity, UErrorCode &status) const;
RouteResult route(double quantity, UErrorCode &status) const;
/**
* Returns the list of possible output units, i.e. the full set of
@ -90,7 +103,9 @@ class U_I18N_API UnitsRouter {
const MaybeStackVector<MeasureUnit> *getOutputUnits() const;
private:
// List of possible output units
// List of possible output units. TODO: converterPreferences_ now also has
// this data available. Maybe drop outputUnits_ and have getOutputUnits
// construct a the list from data in converterPreferences_ instead?
MaybeStackVector<MeasureUnit> outputUnits_;
MaybeStackVector<ConverterPreference> converterPreferences_;

View file

@ -1078,7 +1078,7 @@ group: units
group: unitsformatter
unitsdata.o unitconverter.o complexunitsconverter.o unitsrouter.o
deps
resourcebundle units_extra double_conversion number_representation formattable
resourcebundle units_extra double_conversion number_representation formattable sort
group: decnumber
decContext.o decNumber.o

View file

@ -44,6 +44,7 @@ class UnitsTest : public IntlTest {
void testUnitConstantFreshness();
void testConversionCapability();
void testConversions();
void testComplexUnitConverterSorting();
void testPreferences();
void testSiPrefixes();
void testMass();
@ -61,6 +62,7 @@ void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, cha
TESTCASE_AUTO(testUnitConstantFreshness);
TESTCASE_AUTO(testConversionCapability);
TESTCASE_AUTO(testConversions);
TESTCASE_AUTO(testComplexUnitConverterSorting);
TESTCASE_AUTO(testPreferences);
TESTCASE_AUTO(testSiPrefixes);
TESTCASE_AUTO(testMass);
@ -121,26 +123,27 @@ void UnitsTest::testConversionCapability() {
struct TestCase {
const char *const source;
const char *const target;
const UnitsConvertibilityState expectedState;
const Convertibility expectedState;
} testCases[]{
{"meter", "foot", CONVERTIBLE}, //
{"kilometer", "foot", CONVERTIBLE}, //
{"hectare", "square-foot", CONVERTIBLE}, //
{"kilometer-per-second", "second-per-meter", RECIPROCAL}, //
{"square-meter", "square-foot", CONVERTIBLE}, //
{"kilometer-per-second", "foot-per-second", CONVERTIBLE}, //
{"square-hectare", "pow4-foot", CONVERTIBLE}, //
{"square-kilometer-per-second", "second-per-square-meter", RECIPROCAL}, //
{"meter", "foot", CONVERTIBLE}, //
{"kilometer", "foot", CONVERTIBLE}, //
{"hectare", "square-foot", CONVERTIBLE}, //
{"kilometer-per-second", "second-per-meter", RECIPROCAL}, //
{"square-meter", "square-foot", CONVERTIBLE}, //
{"kilometer-per-second", "foot-per-second", CONVERTIBLE}, //
{"square-hectare", "pow4-foot", CONVERTIBLE}, //
{"square-kilometer-per-second", "second-per-square-meter", RECIPROCAL}, //
{"cubic-kilometer-per-second-meter", "second-per-square-meter", RECIPROCAL}, //
};
for (const auto &testCase : testCases) {
UErrorCode status = U_ZERO_ERROR;
MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
ConversionRates conversionRates(status);
auto convertibility = checkConvertibility(source, target, conversionRates, status);
auto convertibility = extractConvertibility(source, target, conversionRates, status);
assertEquals(UnicodeString("Conversion Capability: ") + testCase.source + " to " +
testCase.target,
@ -171,8 +174,8 @@ void UnitsTest::testSiPrefixes() {
for (const auto &testCase : testCases) {
UErrorCode status = U_ZERO_ERROR;
MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
ConversionRates conversionRates(status);
UnitConverter converter(source, target, conversionRates, status);
@ -206,8 +209,8 @@ void UnitsTest::testMass() {
for (const auto &testCase : testCases) {
UErrorCode status = U_ZERO_ERROR;
MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
ConversionRates conversionRates(status);
UnitConverter converter(source, target, conversionRates, status);
@ -240,8 +243,8 @@ void UnitsTest::testTemperature() {
for (const auto &testCase : testCases) {
UErrorCode status = U_ZERO_ERROR;
MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
ConversionRates conversionRates(status);
UnitConverter converter(source, target, conversionRates, status);
@ -278,8 +281,8 @@ void UnitsTest::testArea() {
for (const auto &testCase : testCases) {
UErrorCode status = U_ZERO_ERROR;
MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
ConversionRates conversionRates(status);
UnitConverter converter(source, target, conversionRates, status);
@ -356,12 +359,12 @@ void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, U
return;
}
MeasureUnit sourceUnit = MeasureUnit::forIdentifier(x, status);
MeasureUnitImpl sourceUnit = MeasureUnitImpl::forIdentifier(x, status);
if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", x.length(), x.data())) {
return;
}
MeasureUnit targetUnit = MeasureUnit::forIdentifier(y, status);
MeasureUnitImpl targetUnit = MeasureUnitImpl::forIdentifier(y, status);
if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", y.length(), y.data())) {
return;
}
@ -373,16 +376,16 @@ void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, U
expected, commentConversionFormula.length(), commentConversionFormula.data());
// Convertibility:
auto convertibility = checkConvertibility(sourceUnit, targetUnit, *ctx->conversionRates, status);
if (status.errIfFailureAndReset("checkConvertibility(<%s>, <%s>, ...)", sourceUnit.getIdentifier(),
targetUnit.getIdentifier())) {
auto convertibility = extractConvertibility(sourceUnit, targetUnit, *ctx->conversionRates, status);
if (status.errIfFailureAndReset("extractConvertibility(<%s>, <%s>, ...)",
sourceUnit.identifier.data(), targetUnit.identifier.data())) {
return;
}
CharString msg;
msg.append("convertible: ", status)
.append(sourceUnit.getIdentifier(), status)
.append(sourceUnit.identifier.data(), status)
.append(" -> ", status)
.append(targetUnit.getIdentifier(), status);
.append(targetUnit.identifier.data(), status);
if (status.errIfFailureAndReset("msg construction")) {
return;
}
@ -391,7 +394,7 @@ void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, U
// Conversion:
UnitConverter converter(sourceUnit, targetUnit, *ctx->conversionRates, status);
if (status.errIfFailureAndReset("constructor: UnitConverter(<%s>, <%s>, status)",
sourceUnit.getIdentifier(), targetUnit.getIdentifier())) {
sourceUnit.identifier.data(), targetUnit.identifier.data())) {
return;
}
double got = converter.convert(1000);
@ -428,6 +431,24 @@ void UnitsTest::testConversions() {
}
}
void UnitsTest::testComplexUnitConverterSorting() {
IcuTestErrorCode status(*this, "UnitsTest::testComplexUnitConverterSorting");
MeasureUnitImpl source = MeasureUnitImpl::forIdentifier("meter", status);
MeasureUnitImpl target = MeasureUnitImpl::forIdentifier("inch-and-foot", status);
ConversionRates conversionRates(status);
ComplexUnitsConverter complexConverter(source, target, conversionRates, status);
auto measures = complexConverter.convert(10.0, status);
U_ASSERT(measures.length() == 2);
assertEquals("Sorted Data", "foot", measures[0]->getUnit().getIdentifier());
assertEquals("Sorted Data", "inch", measures[1]->getUnit().getIdentifier());
assertEqualsNear("Sorted Data", 32, measures[0]->getNumber().getInt64(), 0.00001);
assertEqualsNear("Sorted Data", 9.7008, measures[1]->getNumber().getDouble(), 0.0001);
}
/**
* This class represents the output fields from unitPreferencesTest.txt. Please
* see the documentation at the top of that file for details.
@ -598,6 +619,7 @@ void unitPreferencesTestDataLineFn(void *context, char *fields[][2], int32_t fie
if (U_FAILURE(status)) {
return;
}
UnitsRouter router(inputMeasureUnit, region, usage, status);
if (status.errIfFailureAndReset("UnitsRouter(<%s>, \"%.*s\", \"%.*s\", status)",
inputMeasureUnit.getIdentifier(), region.length(), region.data(),
@ -617,12 +639,12 @@ void unitPreferencesTestDataLineFn(void *context, char *fields[][2], int32_t fie
if (status.errIfFailureAndReset("Failure before router.route")) {
return;
}
MaybeStackVector<Measure> result = router.route(inputAmount, status);
auto routeResult = router.route(inputAmount, status);
if (status.errIfFailureAndReset("router.route(inputAmount, ...)")) {
return;
}
// TODO: revisit this experimentally chosen precision:
checkOutput(unitsTest, msg.data(), expected, result, 0.0000000001);
checkOutput(unitsTest, msg.data(), expected, routeResult.measures, 0.0000000001);
}
/**