ICU-21349 Enhance Supporting Mixed Unit (such as "inch-and-foot")

See #1363
This commit is contained in:
younies 2021-01-20 11:10:43 +00:00 committed by Younies Mahmoud
parent c7ea02fcca
commit b594f4b2a9
22 changed files with 743 additions and 404 deletions

View file

@ -774,16 +774,20 @@ bool MeasureUnitImpl::appendSingleUnit(const SingleUnitImpl &singleUnit, UErrorC
return true;
}
MaybeStackVector<MeasureUnitImpl> MeasureUnitImpl::extractIndividualUnits(UErrorCode &status) const {
MaybeStackVector<MeasureUnitImpl> result;
MaybeStackVector<MeasureUnitImplWithIndex>
MeasureUnitImpl::extractIndividualUnitsWithIndices(UErrorCode &status) const {
MaybeStackVector<MeasureUnitImplWithIndex> result;
if (this->complexity != UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) {
result.emplaceBackAndCheckErrorCode(status, *this, status);
result.emplaceBackAndCheckErrorCode(status, 0, new MeasureUnitImpl(*this, status));
return result;
}
for (int32_t i = 0; i < singleUnits.length(); i++) {
result.emplaceBackAndCheckErrorCode(status, *singleUnits[i], status);
for (int32_t i = 0; i < singleUnits.length(); ++i) {
result.emplaceBackAndCheckErrorCode(status, i, new MeasureUnitImpl(*singleUnits[i], status));
if (U_FAILURE(status)) {
return result;
}
}
return result;

View file

@ -14,10 +14,32 @@
U_NAMESPACE_BEGIN
// Export an explicit template instantiation of the LocalPointer that is used as a
// data member of MeasureUnitImpl.
// (When building DLLs for Windows this is required.)
#if U_PF_WINDOWS <= U_PLATFORM && U_PLATFORM <= U_PF_CYGWIN
#if defined(_MSC_VER)
// Ignore warning 4661 as LocalPointerBase does not use operator== or operator!=
#pragma warning(push)
#pragma warning(disable : 4661)
#endif
template class U_I18N_API LocalPointerBase<MeasureUnitImpl>;
template class U_I18N_API LocalPointer<MeasureUnitImpl>;
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
#endif
static const char16_t kDefaultCurrency[] = u"XXX";
static const char kDefaultCurrency8[] = "XXX";
struct U_I18N_API MeasureUnitImplWithIndex : public UMemory {
const int32_t index;
LocalPointer<MeasureUnitImpl> unitImpl;
// Takes ownership of unitImpl.
MeasureUnitImplWithIndex(int32_t index, MeasureUnitImpl *unitImpl)
: index(index), unitImpl(unitImpl) {}
};
/**
* A struct representing a single unit (optional SI prefix and dimensionality).
@ -130,9 +152,12 @@ struct U_I18N_API SingleUnitImpl : public UMemory {
// MaybeStackVector. This is required when building DLLs for Windows. (See
// datefmt.h, collationiterator.h, erarules.h and others for similar examples.)
#if U_PF_WINDOWS <= U_PLATFORM && U_PLATFORM <= U_PF_CYGWIN
template class U_I18N_API MaybeStackArray<SingleUnitImpl*, 8>;
template class U_I18N_API MaybeStackArray<SingleUnitImpl *, 8>;
template class U_I18N_API MemoryPool<SingleUnitImpl, 8>;
template class U_I18N_API MaybeStackVector<SingleUnitImpl, 8>;
template class U_I18N_API MaybeStackArray<MeasureUnitImplWithIndex *, 8>;
template class U_I18N_API MemoryPool<MeasureUnitImplWithIndex, 8>;
template class U_I18N_API MaybeStackVector<MeasureUnitImplWithIndex, 8>;
#endif
/**
@ -149,7 +174,7 @@ class U_I18N_API MeasureUnitImpl : public UMemory {
MeasureUnitImpl &operator=(MeasureUnitImpl &&other) noexcept = default;
/** Extract the MeasureUnitImpl from a MeasureUnit. */
static inline const MeasureUnitImpl* get(const MeasureUnit& measureUnit) {
static inline const MeasureUnitImpl *get(const MeasureUnit &measureUnit) {
return measureUnit.fImpl;
}
@ -204,14 +229,15 @@ class U_I18N_API MeasureUnitImpl : public UMemory {
MeasureUnitImpl copy(UErrorCode& status) const;
/**
* Extracts the list of all the individual units inside the `MeasureUnitImpl`.
* Extracts the list of all the individual units inside the `MeasureUnitImpl` with their indices.
* For example:
* - if the `MeasureUnitImpl` is `foot-per-hour`
* it will return a list of 1 {`foot-per-hour`}
* it will return a list of 1 {(0, `foot-per-hour`)}
* - if the `MeasureUnitImpl` is `foot-and-inch`
* it will return a list of 2 { `foot`, `inch`}
* it will return a list of 2 {(0, `foot`), (1, `inch`)}
*/
MaybeStackVector<MeasureUnitImpl> extractIndividualUnits(UErrorCode &status) const;
MaybeStackVector<MeasureUnitImplWithIndex>
extractIndividualUnitsWithIndices(UErrorCode &status) const;
/** Mutates this MeasureUnitImpl to take the reciprocal. */
void takeReciprocal(UErrorCode& status);

View file

@ -250,10 +250,7 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps& macros, bool safe,
fUsagePrefsHandler.adoptInsteadAndCheckErrorCode(usagePrefsHandler, status);
chain = fUsagePrefsHandler.getAlias();
} else if (isMixedUnit) {
MeasureUnitImpl temp;
const MeasureUnitImpl &outputUnit = MeasureUnitImpl::forMeasureUnit(macros.unit, temp, status);
auto unitConversionHandler = new UnitConversionHandler(outputUnit.singleUnits[0]->build(status),
macros.unit, chain, status);
auto unitConversionHandler = new UnitConversionHandler(macros.unit, chain, status);
fUnitConversionHandler.adoptInsteadAndCheckErrorCode(unitConversionHandler, status);
chain = fUnitConversionHandler.getAlias();
}

View file

@ -440,9 +440,9 @@ void MixedUnitLongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUn
fillIn->rules = rules;
fillIn->parent = parent;
// We need a localised NumberFormatter for the integers of the bigger units
// We need a localised NumberFormatter for the numbers of the bigger units
// (providing Arabic numerals, for example).
fillIn->fIntegerFormatter = NumberFormatter::withLocale(loc);
fillIn->fNumberFormatter = NumberFormatter::withLocale(loc);
}
void MixedUnitLongNameHandler::processQuantity(DecimalQuantity &quantity, MicroProps &micros,
@ -462,12 +462,6 @@ const Modifier *MixedUnitLongNameHandler::getMixedUnitModifier(DecimalQuantity &
status = U_UNSUPPORTED_ERROR;
return &micros.helpers.emptyWeakModifier;
}
// If we don't have at least one mixedMeasure, the LongNameHandler would be
// sufficient and we shouldn't be running MixedUnitLongNameHandler code:
U_ASSERT(micros.mixedMeasuresCount > 0);
// mixedMeasures does not contain the last value:
U_ASSERT(fMixedUnitCount == micros.mixedMeasuresCount + 1);
U_ASSERT(fListFormatter.isValid());
// Algorithm:
//
@ -492,39 +486,41 @@ const Modifier *MixedUnitLongNameHandler::getMixedUnitModifier(DecimalQuantity &
return &micros.helpers.emptyWeakModifier;
}
StandardPlural::Form quantityPlural = StandardPlural::Form::OTHER;
for (int32_t i = 0; i < micros.mixedMeasuresCount; i++) {
DecimalQuantity fdec;
fdec.setToLong(micros.mixedMeasures[i]);
if (i > 0 && fdec.isNegative()) {
// If numbers are negative, only the first number needs to have its
// negative sign formatted.
fdec.negate();
// If numbers are negative, only the first number needs to have its
// negative sign formatted.
int64_t number = i > 0 ? std::abs(micros.mixedMeasures[i]) : micros.mixedMeasures[i];
if (micros.indexOfQuantity == i) { // Insert placeholder for `quantity`
// If quantity is not the first value and quantity is negative
if (micros.indexOfQuantity > 0 && quantity.isNegative()) {
quantity.negate();
}
StandardPlural::Form quantityPlural =
utils::getPluralSafe(micros.rounder, rules, quantity, status);
UnicodeString quantityFormatWithPlural =
getWithPlural(&fMixedUnitData[i * ARRAY_LENGTH], quantityPlural, status);
SimpleFormatter quantityFormatter(quantityFormatWithPlural, 0, 1, status);
quantityFormatter.format(UnicodeString(u"{0}"), outputMeasuresList[i], status);
} else {
fdec.setToLong(number);
StandardPlural::Form pluralForm = utils::getStandardPlural(rules, fdec);
UnicodeString simpleFormat =
getWithPlural(&fMixedUnitData[i * ARRAY_LENGTH], pluralForm, status);
SimpleFormatter compiledFormatter(simpleFormat, 0, 1, status);
UnicodeString num;
auto appendable = UnicodeStringAppendable(num);
fNumberFormatter.formatDecimalQuantity(fdec, status).appendTo(appendable, status);
compiledFormatter.format(num, outputMeasuresList[i], status);
}
StandardPlural::Form pluralForm = utils::getStandardPlural(rules, fdec);
UnicodeString simpleFormat =
getWithPlural(&fMixedUnitData[i * ARRAY_LENGTH], pluralForm, status);
SimpleFormatter compiledFormatter(simpleFormat, 0, 1, status);
UnicodeString num;
auto appendable = UnicodeStringAppendable(num);
fIntegerFormatter.formatDecimalQuantity(fdec, status).appendTo(appendable, status);
compiledFormatter.format(num, outputMeasuresList[i], status);
// TODO(icu-units#67): fix field positions
}
// Reiterated: we have at least one mixedMeasure:
U_ASSERT(micros.mixedMeasuresCount > 0);
// Thus if negative, a negative has already been formatted:
if (quantity.isNegative()) {
quantity.negate();
}
UnicodeString *finalSimpleFormats = &fMixedUnitData[(fMixedUnitCount - 1) * ARRAY_LENGTH];
StandardPlural::Form finalPlural = utils::getPluralSafe(micros.rounder, rules, quantity, status);
UnicodeString finalSimpleFormat = getWithPlural(finalSimpleFormats, finalPlural, status);
SimpleFormatter finalFormatter(finalSimpleFormat, 0, 1, status);
finalFormatter.format(UnicodeString(u"{0}"), outputMeasuresList[fMixedUnitCount - 1], status);
// Combine list into a "premixed" pattern
UnicodeString premixedFormatPattern;
@ -535,10 +531,8 @@ const Modifier *MixedUnitLongNameHandler::getMixedUnitModifier(DecimalQuantity &
return &micros.helpers.emptyWeakModifier;
}
// TODO(icu-units#67): fix field positions
// Return a SimpleModifier for the "premixed" pattern
micros.helpers.mixedUnitModifier =
SimpleModifier(premixedCompiled, kUndefinedField, false, {this, SIGNUM_POS_ZERO, finalPlural});
SimpleModifier(premixedCompiled, kUndefinedField, false, {this, SIGNUM_POS_ZERO, quantityPlural});
return &micros.helpers.mixedUnitModifier;
}

View file

@ -151,21 +151,24 @@ class MixedUnitLongNameHandler : public MicroPropsGenerator, public ModifierStor
private:
// Not owned
const PluralRules *rules;
// Not owned
const MicroPropsGenerator *parent;
// Total number of units in the MeasureUnit this handler was configured for:
// for "foot-and-inch", this will be 2.
int32_t fMixedUnitCount = 1;
// Stores unit data for each of the individual units. For each unit, it
// stores ARRAY_LENGTH strings, as returned by getMeasureData. (Each unit
// with index `i` has ARRAY_LENGTH strings starting at index
// `i*ARRAY_LENGTH` in this array.)
LocalArray<UnicodeString> fMixedUnitData;
// A localized NumberFormatter used to format the integer-valued bigger
// units of Mixed Unit measurements.
LocalizedNumberFormatter fIntegerFormatter;
// A localised list formatter for joining mixed units together.
// Formats the larger units of Mixed Unit measurements.
LocalizedNumberFormatter fNumberFormatter;
// Joins mixed units together.
LocalPointer<ListFormatter> fListFormatter;
MixedUnitLongNameHandler(const PluralRules *rules, const MicroPropsGenerator *parent)

View file

@ -36,8 +36,7 @@ class IntMeasures : public MaybeStackArray<int64_t, 2> {
* Stack Capacity: most mixed units are expected to consist of two or three
* subunits, so one or two integer measures should be enough.
*/
IntMeasures() : MaybeStackArray<int64_t, 2>() {
}
IntMeasures() : MaybeStackArray<int64_t, 2>() {}
/**
* Copy constructor.
@ -122,9 +121,14 @@ struct MicroProps : public MicroPropsGenerator {
// play.
MeasureUnit outputUnit;
// In the case of mixed units, this is the set of integer-only units
// *preceding* the final unit.
// Contains all the values of each unit in mixed units. For quantity (which is the floating value of
// the smallest unit in the mixed unit), the value stores in `quantity`.
// NOTE: the value of quantity in `mixedMeasures` will be left unset.
IntMeasures mixedMeasures;
// Points to quantity position, -1 if the position is not set yet.
int32_t indexOfQuantity = -1;
// Number of mixedMeasures that have been populated
int32_t mixedMeasuresCount = 0;

View file

@ -107,37 +107,42 @@ void Usage::set(StringPiece value) {
// measures.
void mixedMeasuresToMicros(const MaybeStackVector<Measure> &measures, DecimalQuantity *quantity,
MicroProps *micros, UErrorCode status) {
micros->mixedMeasuresCount = measures.length() - 1;
if (micros->mixedMeasuresCount > 0) {
#ifdef U_DEBUG
U_ASSERT(micros->outputUnit.getComplexity(status) == UMEASURE_UNIT_MIXED);
U_ASSERT(U_SUCCESS(status));
// Check that we received measurements with the expected MeasureUnits:
MeasureUnitImpl temp;
const MeasureUnitImpl& impl = MeasureUnitImpl::forMeasureUnit(micros->outputUnit, temp, status);
U_ASSERT(U_SUCCESS(status));
U_ASSERT(measures.length() == impl.singleUnits.length());
for (int32_t i = 0; i < measures.length(); i++) {
U_ASSERT(measures[i]->getUnit() == impl.singleUnits[i]->build(status));
micros->mixedMeasuresCount = measures.length();
if (micros->mixedMeasures.getCapacity() < micros->mixedMeasuresCount) {
if (micros->mixedMeasures.resize(micros->mixedMeasuresCount) == nullptr) {
status = U_MEMORY_ALLOCATION_ERROR;
return;
}
(void)impl;
#endif
// Mixed units: except for the last value, we pass all values to the
// LongNameHandler via micros->mixedMeasures.
if (micros->mixedMeasures.getCapacity() < micros->mixedMeasuresCount) {
if (micros->mixedMeasures.resize(micros->mixedMeasuresCount) == nullptr) {
status = U_MEMORY_ALLOCATION_ERROR;
return;
}
}
for (int32_t i = 0; i < micros->mixedMeasuresCount; i++) {
micros->mixedMeasures[i] = measures[i]->getNumber().getInt64();
}
} else {
micros->mixedMeasuresCount = 0;
}
// The last value (potentially the only value) gets passed on via quantity.
quantity->setToDouble(measures[measures.length() - 1]->getNumber().getDouble());
for (int32_t i = 0; i < micros->mixedMeasuresCount; i++) {
switch (measures[i]->getNumber().getType()) {
case Formattable::kInt64:
micros->mixedMeasures[i] = measures[i]->getNumber().getInt64();
break;
case Formattable::kDouble:
U_ASSERT(micros->indexOfQuantity < 0);
quantity->setToDouble(measures[i]->getNumber().getDouble());
micros->indexOfQuantity = i;
break;
default:
U_ASSERT(0 == "Found a Measure Number which is neither a double nor an int");
UPRV_UNREACHABLE;
break;
}
if (U_FAILURE(status)) {
return;
}
}
if (micros->indexOfQuantity < 0) {
// There is no quantity.
status = U_INTERNAL_PROGRAM_ERROR;
}
}
UsagePrefsHandler::UsagePrefsHandler(const Locale &locale,
@ -170,22 +175,20 @@ void UsagePrefsHandler::processQuantity(DecimalQuantity &quantity, MicroProps &m
mixedMeasuresToMicros(routedMeasures, &quantity, &micros, status);
}
UnitConversionHandler::UnitConversionHandler(const MeasureUnit &inputUnit, const MeasureUnit &outputUnit,
UnitConversionHandler::UnitConversionHandler(const MeasureUnit &targetUnit,
const MicroPropsGenerator *parent, UErrorCode &status)
: fOutputUnit(outputUnit), fParent(parent) {
: fOutputUnit(targetUnit), fParent(parent) {
MeasureUnitImpl tempInput, tempOutput;
const MeasureUnitImpl &inputUnitImpl = MeasureUnitImpl::forMeasureUnit(inputUnit, tempInput, status);
const MeasureUnitImpl &outputUnitImpl =
MeasureUnitImpl::forMeasureUnit(outputUnit, tempOutput, status);
// TODO: this should become an initOnce thing? Review with other
// ConversionRates usages.
ConversionRates conversionRates(status);
if (U_FAILURE(status)) {
return;
}
const MeasureUnitImpl &targetUnitImpl =
MeasureUnitImpl::forMeasureUnit(targetUnit, tempOutput, status);
fUnitConverter.adoptInsteadAndCheckErrorCode(
new ComplexUnitsConverter(inputUnitImpl, outputUnitImpl, conversionRates, status), status);
new ComplexUnitsConverter(targetUnitImpl, conversionRates, status), status);
}
void UnitConversionHandler::processQuantity(DecimalQuantity &quantity, MicroProps &micros,

View file

@ -97,14 +97,15 @@ class U_I18N_API UnitConversionHandler : public MicroPropsGenerator, public UMem
/**
* Constructor.
*
* @param inputUnit Specifies the input MeasureUnit. Mixed units are not
* supported as input (because input is just a single decimal quantity).
* @param outputUnit Specifies the output MeasureUnit.
* @param targetUnit Specifies the output MeasureUnit. The input MeasureUnit
* is derived from it: in case of a mixed unit, the biggest unit is
* taken as the input unit. If not a mixed unit, the input unit will be
* the same as the output unit and no unit conversion takes place.
* @param parent The parent MicroPropsGenerator.
* @param status Receives status.
*/
UnitConversionHandler(const MeasureUnit &inputUnit, const MeasureUnit &outputUnit,
const MicroPropsGenerator *parent, UErrorCode &status);
UnitConversionHandler(const MeasureUnit &targetUnit, const MicroPropsGenerator *parent,
UErrorCode &status);
/**
* Obtains the appropriate output values from the Unit Converter.

View file

@ -21,34 +21,58 @@
U_NAMESPACE_BEGIN
namespace units {
ComplexUnitsConverter::ComplexUnitsConverter(const MeasureUnitImpl &targetUnit,
const ConversionRates &ratesInfo, UErrorCode &status)
: units_(targetUnit.extractIndividualUnitsWithIndices(status)) {
if (U_FAILURE(status)) {
return;
}
U_ASSERT(units_.length() != 0);
// Just borrowing a pointer to the instance
MeasureUnitImpl *biggestUnit = units_[0]->unitImpl.getAlias();
for (int32_t i = 1; i < units_.length(); i++) {
if (UnitConverter::compareTwoUnits(*units_[i]->unitImpl, *biggestUnit, ratesInfo, status) > 0 &&
U_SUCCESS(status)) {
biggestUnit = units_[i]->unitImpl.getAlias();
}
if (U_FAILURE(status)) {
return;
}
}
this->init(*biggestUnit, ratesInfo, status);
}
ComplexUnitsConverter::ComplexUnitsConverter(const MeasureUnitImpl &inputUnit,
const MeasureUnitImpl &outputUnits,
const ConversionRates &ratesInfo, UErrorCode &status)
: units_(outputUnits.extractIndividualUnits(status)) {
: units_(outputUnits.extractIndividualUnitsWithIndices(status)) {
if (U_FAILURE(status)) {
return;
}
U_ASSERT(units_.length() != 0);
// Save the desired order of output units before we sort units_
for (int32_t i = 0; i < units_.length(); i++) {
outputUnits_.emplaceBackAndCheckErrorCode(status, units_[i]->copy(status).build(status));
}
this->init(inputUnit, ratesInfo, status);
}
void ComplexUnitsConverter::init(const MeasureUnitImpl &inputUnit,
const ConversionRates &ratesInfo,
UErrorCode &status) {
// Sorts units in 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);
const auto *leftPointer = static_cast<const MeasureUnitImplWithIndex *const *>(left);
const auto *rightPointer = static_cast<const MeasureUnitImplWithIndex *const *>(right);
return -1 * UnitConverter::compareTwoUnits(**leftPointer, //
**rightPointer, //
*static_cast<const ConversionRates *>(context), //
status);
// Multiply by -1 to sort in descending order
return (-1) * UnitConverter::compareTwoUnits(*((**leftPointer).unitImpl) /* left unit*/, //
*((**rightPointer).unitImpl) /* right unit */, //
*static_cast<const ConversionRates *>(context), //
status);
};
uprv_sortArray(units_.getAlias(), //
@ -76,11 +100,11 @@ ComplexUnitsConverter::ComplexUnitsConverter(const MeasureUnitImpl &inputUnit,
// 3. then, the final result will be (6 feet and 6.74016 inches)
for (int i = 0, n = units_.length(); i < n; i++) {
if (i == 0) { // first element
unitConverters_.emplaceBackAndCheckErrorCode(status, inputUnit, *units_[i], ratesInfo,
status);
unitConverters_.emplaceBackAndCheckErrorCode(status, inputUnit, *(units_[i]->unitImpl),
ratesInfo, status);
} else {
unitConverters_.emplaceBackAndCheckErrorCode(status, *units_[i - 1], *units_[i], ratesInfo,
status);
unitConverters_.emplaceBackAndCheckErrorCode(status, *(units_[i - 1]->unitImpl),
*(units_[i]->unitImpl), ratesInfo, status);
}
if (U_FAILURE(status)) {
@ -100,7 +124,7 @@ UBool ComplexUnitsConverter::greaterThanOrEqual(double quantity, double limit) c
MaybeStackVector<Measure> ComplexUnitsConverter::convert(double quantity,
icu::number::impl::RoundingImpl *rounder,
UErrorCode &status) const {
// TODO(hugovdm): return an error for "foot-and-foot"?
// TODO: return an error for "foot-and-foot"?
MaybeStackVector<Measure> result;
int sign = 1;
if (quantity < 0) {
@ -110,7 +134,7 @@ MaybeStackVector<Measure> ComplexUnitsConverter::convert(double quantity,
// For N converters:
// - the first converter converts from the input unit to the largest unit,
// - N-1 converters convert to bigger units for which we want integers,
// - the following N-2 converters convert to bigger units for which we want integers,
// - the Nth converter (index N-1) converts to the smallest unit, for which
// we keep a double.
MaybeStackArray<int64_t, 5> intValues(unitConverters_.length() - 1, status);
@ -137,102 +161,85 @@ MaybeStackVector<Measure> ComplexUnitsConverter::convert(double quantity,
} else {
quantity = remainder;
}
} else { // LAST ELEMENT
if (rounder == nullptr) {
// Nothing to do for the last element.
break;
}
// Round the last value
// TODO(ICU-21288): get smarter about precision for mixed units.
number::impl::DecimalQuantity quant;
quant.setToDouble(quantity);
rounder->apply(quant, status);
if (U_FAILURE(status)) {
return result;
}
quantity = quant.toDouble();
if (i == 0) {
// Last element is also the first element, so we're done
break;
}
// Check if there's a carry, and bubble it back up the resulting intValues.
int64_t carry = floor(unitConverters_[i]->convertInverse(quantity) * (1 + DBL_EPSILON));
if (carry <= 0) {
break;
}
quantity -= unitConverters_[i]->convert(carry);
intValues[i - 1] += carry;
// We don't use the first converter: that one is for the input unit
for (int32_t j = i - 1; j > 0; j--) {
carry = floor(unitConverters_[j]->convertInverse(intValues[j]) * (1 + DBL_EPSILON));
if (carry <= 0) {
break;
}
intValues[j] -= round(unitConverters_[j]->convert(carry));
intValues[j - 1] += carry;
}
}
}
}
// Package values into Measure instances in result:
applyRounder(intValues, quantity, rounder, status);
// Initialize empty result. We use a MaybeStackArray directly so we can
// assign pointers - for this privilege we have to take care of cleanup.
MaybeStackArray<Measure *, 4> tmpResult(unitConverters_.length(), status);
if (U_FAILURE(status)) {
return result;
}
// Package values into temporary Measure instances in tmpResult:
for (int i = 0, n = unitConverters_.length(); i < n; ++i) {
if (i < n - 1) {
Formattable formattableQuantity(intValues[i] * sign);
// Measure takes ownership of the MeasureUnit*
MeasureUnit *type = new MeasureUnit(units_[i]->copy(status).build(status));
if (result.emplaceBackAndCheckErrorCode(status, formattableQuantity, type, status) ==
nullptr) {
// Ownership wasn't taken
U_ASSERT(U_FAILURE(status));
delete type;
}
if (U_FAILURE(status)) {
return result;
}
MeasureUnit *type = new MeasureUnit(units_[i]->unitImpl->copy(status).build(status));
tmpResult[units_[i]->index] = new Measure(formattableQuantity, type, status);
} else { // LAST ELEMENT
// Add the last element, not an integer:
Formattable formattableQuantity(quantity * sign);
// Measure takes ownership of the MeasureUnit*
MeasureUnit *type = new MeasureUnit(units_[i]->copy(status).build(status));
if (result.emplaceBackAndCheckErrorCode(status, formattableQuantity, type, status) ==
nullptr) {
// Ownership wasn't taken
U_ASSERT(U_FAILURE(status));
delete type;
}
if (U_FAILURE(status)) {
return result;
}
U_ASSERT(result.length() == i + 1);
U_ASSERT(result[i] != nullptr);
MeasureUnit *type = new MeasureUnit(units_[i]->unitImpl->copy(status).build(status));
tmpResult[units_[i]->index] = new Measure(formattableQuantity, type, status);
}
}
MaybeStackVector<Measure> orderedResult;
int32_t unitsCount = outputUnits_.length();
U_ASSERT(unitsCount == units_.length());
Measure **arr = result.getAlias();
// O(N^2) is fine: mixed units' unitsCount is usually 2 or 3.
for (int32_t i = 0; i < unitsCount; i++) {
for (int32_t j = i; j < unitsCount; j++) {
// Find the next expected unit, and swap it into place.
U_ASSERT(result[j] != nullptr);
if (result[j]->getUnit() == *outputUnits_[i]) {
if (j != i) {
Measure *tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
}
}
// Transfer values into result and return:
for(int32_t i = 0, n = unitConverters_.length(); i < n; ++i) {
U_ASSERT(tmpResult[i] != nullptr);
result.emplaceBackAndCheckErrorCode(status, *tmpResult[i]);
delete tmpResult[i];
}
return result;
}
void ComplexUnitsConverter::applyRounder(MaybeStackArray<int64_t, 5> &intValues, double &quantity,
icu::number::impl::RoundingImpl *rounder,
UErrorCode &status) const {
if (rounder == nullptr) {
// Nothing to do for the quantity.
return;
}
number::impl::DecimalQuantity decimalQuantity;
decimalQuantity.setToDouble(quantity);
rounder->apply(decimalQuantity, status);
if (U_FAILURE(status)) {
return;
}
quantity = decimalQuantity.toDouble();
int32_t lastIndex = unitConverters_.length() - 1;
if (lastIndex == 0) {
// Only one element, no need to bubble up the carry
return;
}
// Check if there's a carry, and bubble it back up the resulting intValues.
int64_t carry = floor(unitConverters_[lastIndex]->convertInverse(quantity) * (1 + DBL_EPSILON));
if (carry <= 0) {
return;
}
quantity -= unitConverters_[lastIndex]->convert(carry);
intValues[lastIndex - 1] += carry;
// We don't use the first converter: that one is for the input unit
for (int32_t j = lastIndex - 1; j > 0; j--) {
carry = floor(unitConverters_[j]->convertInverse(intValues[j]) * (1 + DBL_EPSILON));
if (carry <= 0) {
return;
}
intValues[j] -= round(unitConverters_[j]->convert(carry));
intValues[j - 1] += carry;
}
}
} // namespace units
U_NAMESPACE_END

View file

@ -48,6 +48,22 @@ namespace units {
*/
class U_I18N_API ComplexUnitsConverter : public UMemory {
public:
/**
* Constructs `ComplexUnitsConverter` for an `targetUnit` that could be Single, Compound or Mixed.
* In case of:
* 1- Single and Compound units,
* the conversion will not perform anything, the input will be equal to the output.
* 2- Mixed Unit
* the conversion will consider the input is the biggest unit. And will convert it to be spread
* through the target units. For example: if target unit is "inch-and-foot", and the input is 2.5. The
* converter will consider the input value in "foot", because foot is the biggest unit. Then, it
* will convert 2.5 feet to "inch-and-foot".
*
* @param targetUnit could be any type. (single, compound or mixed).
* @param status
*/
ComplexUnitsConverter(const MeasureUnitImpl &targetUnit, const ConversionRates &ratesInfo,
UErrorCode &status);
/**
* Constructor of `ComplexUnitsConverter`.
* NOTE:
@ -79,10 +95,20 @@ class U_I18N_API ComplexUnitsConverter : public UMemory {
private:
MaybeStackVector<UnitConverter> unitConverters_;
// Individual units of mixed units, sorted big to small
MaybeStackVector<MeasureUnitImpl> units_;
// Individual units of mixed units, sorted in desired output order
MaybeStackVector<MeasureUnit> outputUnits_;
// Individual units of mixed units, sorted big to small, with indices
// indicating the requested output mixed unit order.
MaybeStackVector<MeasureUnitImplWithIndex> units_;
// Sorts units_, which must be populated before calling this, and populates
// unitConverters_.
void init(const MeasureUnitImpl &inputUnit, const ConversionRates &ratesInfo, UErrorCode &status);
// Applies the rounder to the quantity (last element) and bubble up any carried value to all the
// intValues.
// TODO(ICU-21288): get smarter about precision for mixed units.
void applyRounder(MaybeStackArray<int64_t, 5> &intValues, double &quantity,
icu::number::impl::RoundingImpl *rounder, UErrorCode &status) const;
};
} // namespace units

View file

@ -769,6 +769,67 @@ void NumberFormatterApiTest::unitMeasure() {
4.28571,
u"4 metric tons, 285 kilograms, 710 grams");
assertFormatSingle(u"Mixed Unit (Not Sorted) [metric]", //
u"unit/gram-and-kilogram unit-width-full-name", //
u"unit/gram-and-kilogram unit-width-full-name", //
NumberFormatter::with() //
.unit(MeasureUnit::forIdentifier("gram-and-kilogram", status)) //
.unitWidth(UNUM_UNIT_WIDTH_FULL_NAME), //
Locale("en-US"), //
4.28571, //
u"285.71 grams, 4 kilograms"); //
assertFormatSingle(u"Mixed Unit (Not Sorted) [imperial]", //
u"unit/inch-and-yard-and-foot unit-width-full-name", //
u"unit/inch-and-yard-and-foot unit-width-full-name", //
NumberFormatter::with() //
.unit(MeasureUnit::forIdentifier("inch-and-yard-and-foot", status)) //
.unitWidth(UNUM_UNIT_WIDTH_FULL_NAME), //
Locale("en-US"), //
4.28571, //
u"10.28556 inches, 4 yards, 0 feet"); //
assertFormatSingle(u"Mixed Unit (Not Sorted) [imperial full]", //
u"unit/inch-and-yard-and-foot unit-width-full-name", //
u"unit/inch-and-yard-and-foot unit-width-full-name", //
NumberFormatter::with() //
.unit(MeasureUnit::forIdentifier("inch-and-yard-and-foot", status)) //
.unitWidth(UNUM_UNIT_WIDTH_FULL_NAME), //
Locale("en-US"), //
4.38571, //
u"1.88556 inches, 4 yards, 1 foot"); //
assertFormatSingle(u"Mixed Unit (Not Sorted) [imperial full integers]", //
u"unit/inch-and-yard-and-foot @# unit-width-full-name", //
u"unit/inch-and-yard-and-foot @# unit-width-full-name", //
NumberFormatter::with() //
.unit(MeasureUnit::forIdentifier("inch-and-yard-and-foot", status)) //
.unitWidth(UNUM_UNIT_WIDTH_FULL_NAME) //
.precision(Precision::maxSignificantDigits(2)), //
Locale("en-US"), //
4.36112, //
u"1 inch, 4 yards, 1 foot"); //
assertFormatSingle(u"Mixed Unit (Not Sorted) [imperial full] with `And` in the end", //
u"unit/inch-and-yard-and-foot unit-width-full-name", //
u"unit/inch-and-yard-and-foot unit-width-full-name", //
NumberFormatter::with() //
.unit(MeasureUnit::forIdentifier("inch-and-yard-and-foot", status)) //
.unitWidth(UNUM_UNIT_WIDTH_FULL_NAME), //
Locale("fr-FR"), //
4.38571, //
u"1,88556\u00A0pouce, 4\u00A0yards et 1\u00A0pied"); //
assertFormatSingle(u"Mixed unit, Scientific [Not in Order]", //
u"unit/foot-and-inch-and-yard E0", //
u"unit/foot-and-inch-and-yard E0", //
NumberFormatter::with() //
.unit(MeasureUnit::forIdentifier("foot-and-inch-and-yard", status)) //
.notation(Notation::scientific()), //
Locale("en-US"), //
3.65, //
"1 ft, 1.14E1 in, 3 yd"); //
assertFormatSingle(
u"Testing \"1 foot 12 inches\"",
u"unit/foot-and-inch @### unit-width-full-name",

View file

@ -549,10 +549,65 @@ void UnitsTest::testComplexUnitsConverter() {
void UnitsTest::testComplexUnitConverterSorting() {
IcuTestErrorCode status(*this, "UnitsTest::testComplexUnitConverterSorting");
ConversionRates conversionRates(status);
status.assertSuccess();
struct TestCase {
const char *msg;
const char *input;
const char *output;
double inputValue;
Measure expected[3];
int32_t expectedCount;
// For mixed units, accuracy of the smallest unit
double accuracy;
} testCases[]{{"inch-and-foot",
"meter",
"inch-and-foot",
10.0,
{
Measure(9.70079, MeasureUnit::createInch(status), status),
Measure(32, MeasureUnit::createFoot(status), status),
Measure(0, MeasureUnit::createBit(status), status),
},
2,
0.00001},
{"inch-and-yard-and-foot",
"meter",
"inch-and-yard-and-foot",
100.0,
{
Measure(1.0079, MeasureUnit::createInch(status), status),
Measure(109, MeasureUnit::createYard(status), status),
Measure(1, MeasureUnit::createFoot(status), status),
},
3,
0.0001}};
for (const auto &testCase : testCases) {
MeasureUnitImpl inputImpl = MeasureUnitImpl::forIdentifier(testCase.input, status);
MeasureUnitImpl outputImpl = MeasureUnitImpl::forIdentifier(testCase.output, status);
ComplexUnitsConverter converter(inputImpl, outputImpl, conversionRates, status);
auto actual = converter.convert(testCase.inputValue, nullptr, status);
for (int i = 0; i < testCase.expectedCount; i++) {
assertEquals(testCase.msg, testCase.expected[i].getUnit().getIdentifier(),
actual[i]->getUnit().getIdentifier());
if (testCase.expected[i].getNumber().getType() == Formattable::Type::kInt64) {
assertEquals(testCase.msg, testCase.expected[i].getNumber().getInt64(),
actual[i]->getNumber().getInt64());
} else {
assertEqualsNear(testCase.msg, testCase.expected[i].getNumber().getDouble(),
actual[i]->getNumber().getDouble(), testCase.accuracy);
}
}
}
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, nullptr, status);

View file

@ -53,10 +53,16 @@ public class MicroProps implements Cloneable, MicroPropsGenerator {
// play.
public MeasureUnit outputUnit;
// In the case of mixed units, this is the set of integer-only units
// *preceding* the final unit.
/**
* Contains all the measures.
*/
public List<Measure> mixedMeasures;
/**
* Points to quantity position, -1 if the position is not set yet.
*/
public int indexOfQuantity = -1;
private volatile boolean exhausted;
/**

View file

@ -168,14 +168,33 @@ public class MixedUnitLongNameHandler
List<String> outputMeasuresList = new ArrayList<>();
StandardPlural quantityPlural = StandardPlural.OTHER;
for (int i = 0; i < micros.mixedMeasures.size(); i++) {
if ( i == micros.indexOfQuantity) {
if (i > 0 && quantity.isNegative()) {
// If numbers are negative, only the first number needs to have its
// negative sign formatted.
quantity.negate();
}
quantityPlural = RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
String quantitySimpleFormat = LongNameHandler.getWithPlural(this.fMixedUnitData.get(i), quantityPlural);
SimpleFormatter finalFormatter = SimpleFormatter.compileMinMaxArguments(quantitySimpleFormat, 0, 1);
outputMeasuresList.add(finalFormatter.format("{0}"));
continue;
}
DecimalQuantity fdec = new DecimalQuantity_DualStorageBCD(micros.mixedMeasures.get(i).getNumber());
if (i > 0 && fdec.isNegative()) {
// If numbers are negative, only the first number needs to have its
// negative sign formatted.
fdec.negate();
}
StandardPlural pluralForm = fdec.getStandardPlural(rules);
StandardPlural pluralForm = RoundingUtils.getPluralSafe(micros.rounder, rules, fdec);
String simpleFormat = LongNameHandler.getWithPlural(this.fMixedUnitData.get(i), pluralForm);
SimpleFormatter compiledFormatter = SimpleFormatter.compileMinMaxArguments(simpleFormat, 0, 1);
@ -186,18 +205,6 @@ public class MixedUnitLongNameHandler
// TODO(icu-units#67): fix field positions
}
// Reiterated: we have at least one mixedMeasure:
assert micros.mixedMeasures.size() > 0;
// Thus if negative, a negative has already been formatted:
if (quantity.isNegative()) {
quantity.negate();
}
String[] finalSimpleFormats = this.fMixedUnitData.get(this.fMixedUnitData.size() - 1);
StandardPlural finalPlural = RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
String finalSimpleFormat = LongNameHandler.getWithPlural(finalSimpleFormats, finalPlural);
SimpleFormatter finalFormatter = SimpleFormatter.compileMinMaxArguments(finalSimpleFormat, 0, 1);
outputMeasuresList.add(finalFormatter.format("{0}"));
// Combine list into a "premixed" pattern
String premixedFormatPattern = this.fListFormatter.format(outputMeasuresList);
@ -209,7 +216,7 @@ public class MixedUnitLongNameHandler
Modifier.Parameters params = new Modifier.Parameters();
params.obj = this;
params.signum = Modifier.Signum.POS_ZERO;
params.plural = finalPlural;
params.plural = quantityPlural;
// Return a SimpleModifier for the "premixed" pattern
return new SimpleModifier(premixedCompiled, null, false, params);
}

View file

@ -5,8 +5,8 @@ package com.ibm.icu.impl.number;
import java.util.List;
import com.ibm.icu.impl.units.ComplexUnitsConverter;
import com.ibm.icu.impl.units.ConversionRates;
import com.ibm.icu.impl.units.MeasureUnitImpl;
import com.ibm.icu.impl.units.UnitsData;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
@ -22,22 +22,17 @@ public class UnitConversionHandler implements MicroPropsGenerator {
private ComplexUnitsConverter fComplexUnitConverter;
/**
* Constructor.
*
* @param inputUnit Specifies the input MeasureUnit. Mixed units are not
* supported as input (because input is just a single decimal quantity).
* @param outputUnit Specifies the output MeasureUnit.
* @param parent The parent MicroPropsGenerator.
* @param targetUnit Specifies the output MeasureUnit. The input MeasureUnit
* is derived from it: in case of a mixed unit, the biggest unit is
* taken as the input unit. If not a mixed unit, the input unit will be
* the same as the output unit and no unit conversion takes place.
* @param parent The parent MicroPropsGenerator.
*/
public UnitConversionHandler(MeasureUnit inputUnit,
MeasureUnit outputUnit,
MicroPropsGenerator parent) {
this.fOutputUnit = outputUnit;
public UnitConversionHandler(MeasureUnit targetUnit, MicroPropsGenerator parent) {
this.fOutputUnit = targetUnit;
this.fParent = parent;
MeasureUnitImpl inputUnitImpl = MeasureUnitImpl.forIdentifier(inputUnit.getIdentifier());
MeasureUnitImpl outputUnitImpl = MeasureUnitImpl.forIdentifier(outputUnit.getIdentifier());
this.fComplexUnitConverter = new ComplexUnitsConverter(inputUnitImpl, outputUnitImpl,
new UnitsData().getConversionRates());
MeasureUnitImpl targetUnitImpl = MeasureUnitImpl.forIdentifier(targetUnit.getIdentifier());
this.fComplexUnitConverter = new ComplexUnitsConverter(targetUnitImpl, new ConversionRates());
}
/**
@ -48,10 +43,11 @@ public class UnitConversionHandler implements MicroPropsGenerator {
MicroProps result = this.fParent.processQuantity(quantity);
quantity.roundToInfinity(); // Enables toDouble
List<Measure> measures = this.fComplexUnitConverter.convert(quantity.toBigDecimal(), result.rounder);
ComplexUnitsConverter.ComplexConverterResult complexConverterResult
= this.fComplexUnitConverter.convert(quantity.toBigDecimal(), result.rounder);
result.outputUnit = this.fOutputUnit;
UsagePrefsHandler.mixedMeasuresToMicros(measures, quantity, result);
UsagePrefsHandler.mixedMeasuresToMicros(complexConverterResult, quantity, result);
return result;
}

View file

@ -6,6 +6,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import com.ibm.icu.impl.units.ComplexUnitsConverter;
import com.ibm.icu.impl.units.MeasureUnitImpl;
import com.ibm.icu.impl.units.UnitsRouter;
import com.ibm.icu.util.Measure;
@ -30,24 +31,10 @@ public class UsagePrefsHandler implements MicroPropsGenerator {
* in measures.
*/
protected static void
mixedMeasuresToMicros(List<Measure> measures, DecimalQuantity outQuantity, MicroProps outMicros) {
outMicros.mixedMeasures = new ArrayList<>();
if (measures.size() > 1) {
// For debugging
assert (outMicros.outputUnit.getComplexity() == MeasureUnit.Complexity.MIXED);
// Check that we received the expected number of measurements:
assert measures.size() == outMicros.outputUnit.splitToSingleUnits().size();
// Mixed units: except for the last value, we pass all values to the
// LongNameHandler via micros->mixedMeasures.
for (int i = 0, n = measures.size() - 1; i < n; i++) {
outMicros.mixedMeasures.add(measures.get(i));
}
}
// The last value (potentially the only value) gets passed on via quantity.
outQuantity.setToBigDecimal((BigDecimal) measures.get(measures.size()- 1).getNumber());
mixedMeasuresToMicros(ComplexUnitsConverter.ComplexConverterResult complexConverterResult, DecimalQuantity quantity, MicroProps outMicros) {
outMicros.mixedMeasures = complexConverterResult.measures;
outMicros.indexOfQuantity = complexConverterResult.indexOfQuantity;
quantity.setToBigDecimal((BigDecimal) outMicros.mixedMeasures.get(outMicros.indexOfQuantity).getNumber());
}
/**
@ -74,11 +61,8 @@ public class UsagePrefsHandler implements MicroPropsGenerator {
quantity.roundToInfinity(); // Enables toDouble
final UnitsRouter.RouteResult routed = fUnitsRouter.route(quantity.toBigDecimal(), micros);
final List<Measure> routedMeasures = routed.measures;
micros.outputUnit = routed.outputUnit.build();
UsagePrefsHandler.mixedMeasuresToMicros(routedMeasures, quantity, micros);
UsagePrefsHandler.mixedMeasuresToMicros(routed.complexConverterResult, quantity, micros);
return micros;
}
}

View file

@ -3,6 +3,7 @@
package com.ibm.icu.impl.units;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
@ -12,49 +13,79 @@ import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
import com.ibm.icu.number.Precision;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
/**
* Converts from single or compound unit to single, compound or mixed units.
* For example, from `meter` to `foot+inch`.
* Converts from single or compound unit to single, compound or mixed units. For example, from `meter` to `foot+inch`.
* <p>
* DESIGN:
* This class uses `UnitConverter` in order to perform the single converter (i.e. converters from a
* single unit to another single unit). Therefore, `ComplexUnitsConverter` class contains multiple
* instances of the `UnitConverter` to perform the conversion.
* DESIGN: This class uses `UnitConverter` in order to perform the single converter (i.e. converters from a single unit
* to another single unit). Therefore, `ComplexUnitsConverter` class contains multiple instances of the `UnitConverter`
* to perform the conversion.
*/
public class ComplexUnitsConverter {
public static final BigDecimal EPSILON = BigDecimal.valueOf(Math.ulp(1.0));
public static final BigDecimal EPSILON_MULTIPLIER = BigDecimal.valueOf(1).add(EPSILON);
private ArrayList<UnitConverter> unitConverters_;
// Individual units of mixed units, sorted big to small
private ArrayList<MeasureUnitImpl> units_;
// Individual units of mixed units, sorted in desired output order
private ArrayList<MeasureUnit> outputUnits_;
/**
* Individual units of mixed units, sorted big to small, with indices
* indicating the requested output mixed unit order.
*/
private List<MeasureUnitImpl.MeasureUnitImplWithIndex> units_;
private MeasureUnitImpl inputUnit_;
/**
* Constructor of `ComplexUnitsConverter`.
* NOTE:
* - inputUnit and outputUnits must be under the same category
* - e.g. meter to feet and inches --> all of them are length units.
* Constructs <code>ComplexUnitsConverter</code> for an <code>inputUnit</code> that could be Single, Compound or
* Mixed. In case of: 1- Single and Compound units, the conversion will not perform anything, the input will be
* equal to the output. 2- Mixed Unit the conversion will consider the input in the biggest unit. and will convert
* it to be spread throw the input units. For example: if input unit is "inch-and-foot", and the input is 2.5. The
* converter will consider the input value in "foot", because foot is the biggest unit. Then, it will convert 2.5
* feet to "inch-and-foot".
*
* @param inputUnit represents the source unit. (should be single or compound unit).
* @param outputUnits represents the output unit. could be any type. (single, compound or mixed).
* @param targetUnit
* represents the input unit. could be any type. (single, compound or mixed).
*/
public ComplexUnitsConverter(MeasureUnitImpl inputUnit, MeasureUnitImpl outputUnits,
ConversionRates conversionRates) {
units_ = outputUnits.extractIndividualUnits();
outputUnits_ = new ArrayList<>(units_.size());
for (MeasureUnitImpl itr : units_) {
outputUnits_.add(itr.build());
public ComplexUnitsConverter(MeasureUnitImpl targetUnit, ConversionRates conversionRates) {
this.units_ = targetUnit.extractIndividualUnitsWithIndices();
assert (!this.units_.isEmpty());
// Assign the biggest unit to inputUnit_.
this.inputUnit_ = this.units_.get(0).unitImpl;
MeasureUnitImpl.MeasureUnitImplComparator comparator = new MeasureUnitImpl.MeasureUnitImplComparator(
conversionRates);
for (MeasureUnitImpl.MeasureUnitImplWithIndex unitWithIndex : this.units_) {
if (comparator.compare(unitWithIndex.unitImpl, this.inputUnit_) > 0) {
this.inputUnit_ = unitWithIndex.unitImpl;
}
}
assert (!units_.isEmpty());
this.init(conversionRates);
}
/**
* Constructs <code>ComplexUnitsConverter</code> NOTE: - inputUnit and outputUnits must be under the same category -
* e.g. meter to feet and inches --> all of them are length units.
*
* @param targetUnit
* represents the source unit. (should be single or compound unit).
* @param outputUnits
* represents the output unit. could be any type. (single, compound or mixed).
*/
public ComplexUnitsConverter(MeasureUnitImpl targetUnit, MeasureUnitImpl outputUnits,
ConversionRates conversionRates) {
this.inputUnit_ = targetUnit;
this.units_ = outputUnits.extractIndividualUnitsWithIndices();
assert (!this.units_.isEmpty());
this.init(conversionRates);
}
/**
* Sorts units_, which must be populated before calling this, and populates
* unitConverters_.
*/
private void init(ConversionRates conversionRates) {
// Sort the units in a descending order.
Collections.sort(
this.units_,
Collections.reverseOrder(new MeasureUnitImpl.MeasureUnitImplComparator(conversionRates)));
Collections.sort(this.units_,
Collections.reverseOrder(new MeasureUnitImpl.MeasureUnitImplWithIndexComparator(conversionRates)));
// If the `outputUnits` is `UMEASURE_UNIT_MIXED` such as `foot+inch`. Thus means there is more than one unit
// and In this case we need more converters to convert from the `inputUnit` to the first unit in the
@ -73,20 +104,20 @@ public class ComplexUnitsConverter {
unitConverters_ = new ArrayList<>();
for (int i = 0, n = units_.size(); i < n; i++) {
if (i == 0) { // first element
unitConverters_.add(new UnitConverter(inputUnit, units_.get(i), conversionRates));
unitConverters_.add(new UnitConverter(this.inputUnit_, units_.get(i).unitImpl, conversionRates));
} else {
unitConverters_.add(new UnitConverter(units_.get(i - 1), units_.get(i), conversionRates));
unitConverters_
.add(new UnitConverter(units_.get(i - 1).unitImpl, units_.get(i).unitImpl, conversionRates));
}
}
}
/**
* Returns true if the specified `quantity` of the `inputUnit`, expressed in terms of the biggest
* unit in the MeasureUnit `outputUnit`, is greater than or equal to `limit`.
* Returns true if the specified `quantity` of the `inputUnit`, expressed in terms of the biggest unit in the
* MeasureUnit `outputUnit`, is greater than or equal to `limit`.
* <p>
* For example, if the input unit is `meter` and the target unit is `foot+inch`. Therefore, this
* function will convert the `quantity` from `meter` to `foot`, then, it will compare the value in
* `foot` with the `limit`.
* For example, if the input unit is `meter` and the target unit is `foot+inch`. Therefore, this function will
* convert the `quantity` from `meter` to `foot`, then, it will compare the value in `foot` with the `limit`.
*/
public boolean greaterThanOrEqual(BigDecimal quantity, BigDecimal limit) {
assert !units_.isEmpty();
@ -95,6 +126,16 @@ public class ComplexUnitsConverter {
return unitConverters_.get(0).convert(quantity).multiply(EPSILON_MULTIPLIER).compareTo(limit) >= 0;
}
public static class ComplexConverterResult {
public final int indexOfQuantity;
public final List<Measure> measures;
ComplexConverterResult(int indexOfQuantity, List<Measure> measures) {
this.indexOfQuantity = indexOfQuantity;
this.measures = measures;
}
}
/**
* Returns outputMeasures which is an array with the corresponding values.
* - E.g. converting meters to feet and inches.
@ -103,9 +144,8 @@ public class ComplexUnitsConverter {
* the smallest element is the only element that could have fractional values. And all
* other elements are floored to the nearest integer
*/
public List<Measure> convert(BigDecimal quantity, Precision rounder) {
List<Measure> result = new ArrayList<>(unitConverters_.size());
BigDecimal sign = BigDecimal.ONE;
public ComplexConverterResult convert(BigDecimal quantity, Precision rounder) {
BigInteger sign = BigInteger.ONE;
if (quantity.compareTo(BigDecimal.ZERO) < 0) {
quantity = quantity.abs();
sign = sign.negate();
@ -117,8 +157,7 @@ public class ComplexUnitsConverter {
// - N-1 converters convert to bigger units for which we want integers,
// - the Nth converter (index N-1) converts to the smallest unit, which
// isn't (necessarily) an integer.
List<BigDecimal> intValues = new ArrayList<>(unitConverters_.size() - 1);
List<BigInteger> intValues = new ArrayList<>(unitConverters_.size() - 1);
for (int i = 0, n = unitConverters_.size(); i < n; ++i) {
quantity = (unitConverters_.get(i)).convert(quantity);
@ -129,83 +168,89 @@ public class ComplexUnitsConverter {
// decision is made. However after the thresholding, we use the
// original values to ensure unbiased accuracy (to the extent of
// double's capabilities).
BigDecimal flooredQuantity =
quantity.multiply(EPSILON_MULTIPLIER).setScale(0, RoundingMode.FLOOR);
BigInteger flooredQuantity = quantity.multiply(EPSILON_MULTIPLIER).setScale(0, RoundingMode.FLOOR).toBigInteger();
intValues.add(flooredQuantity);
// Keep the residual of the quantity.
// For example: `3.6 feet`, keep only `0.6 feet`
BigDecimal remainder = quantity.subtract(flooredQuantity);
// For example: `3.6 feet`, keep only `0.6 feet`
BigDecimal remainder = quantity.subtract(BigDecimal.valueOf(flooredQuantity.longValue()));
if (remainder.compareTo(BigDecimal.ZERO) == -1) {
quantity = BigDecimal.ZERO;
} else {
quantity = remainder;
}
} else { // LAST ELEMENT
if (rounder == null) {
// Nothing to do for the last element.
break;
}
// Round the last value
// TODO(ICU-21288): get smarter about precision for mixed units.
DecimalQuantity quant = new DecimalQuantity_DualStorageBCD(quantity);
rounder.apply(quant);
quantity = quant.toBigDecimal();
if (i == 0) {
// Last element is also the first element, so we're done
break;
}
// Check if there's a carry, and bubble it back up the resulting intValues.
BigDecimal carry = unitConverters_.get(i)
.convertInverse(quantity)
.multiply(EPSILON_MULTIPLIER)
.setScale(0, RoundingMode.FLOOR);
if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
break;
}
quantity = quantity.subtract(unitConverters_.get(i).convert(carry));
intValues.set(i - 1, intValues.get(i - 1).add(carry));
// We don't use the first converter: that one is for the input unit
for (int j = i - 1; j > 0; j--) {
carry = unitConverters_.get(j)
.convertInverse(intValues.get(j))
.multiply(EPSILON_MULTIPLIER)
.setScale(0, RoundingMode.FLOOR);
if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
break;
}
intValues.set(j, intValues.get(j).subtract(unitConverters_.get(j).convert(carry)));
intValues.set(j - 1, intValues.get(j - 1).add(carry));
}
}
}
// Package values into Measure instances in result:
quantity = applyRounder(intValues, quantity, rounder);
// Initialize empty measures.
List<Measure> measures = new ArrayList<>(unitConverters_.size());
for (int i = 0; i < unitConverters_.size(); i++) {
measures.add(null);
}
// Package values into Measure instances in measures:
int indexOfQuantity = -1;
for (int i = 0, n = unitConverters_.size(); i < n; ++i) {
if (i < n - 1) {
result.add(new Measure(intValues.get(i).multiply(sign), units_.get(i).build()));
Measure measure = new Measure(intValues.get(i).multiply(sign), units_.get(i).unitImpl.build());
measures.set(units_.get(i).index, measure);
} else {
result.add(new Measure(quantity.multiply(sign), units_.get(i).build()));
indexOfQuantity = units_.get(i).index;
Measure measure =
new Measure(quantity.multiply(BigDecimal.valueOf(sign.longValue())),
units_.get(i).unitImpl.build());
measures.set(indexOfQuantity, measure);
}
}
for (int i = 0; i < result.size(); i++) {
for (int j = i; j < result.size(); j++) {
// Find the next expected unit, and swap it into place.
if (result.get(j).getUnit().equals(outputUnits_.get(i))) {
if (j != i) {
Measure tmp = result.get(j);
result.set(j, result.get(i));
result.set(i, tmp);
}
}
}
return new ComplexConverterResult(indexOfQuantity , measures);
}
/**
* Applies the rounder to the quantity (last element) and bubble up any carried value to all the intValues.
*
* @return the rounded quantity
*/
private BigDecimal applyRounder(List<BigInteger> intValues, BigDecimal quantity, Precision rounder) {
if (rounder == null) {
return quantity;
}
return result;
DecimalQuantity quantityBCD = new DecimalQuantity_DualStorageBCD(quantity);
rounder.apply(quantityBCD);
quantity = quantityBCD.toBigDecimal();
if (intValues.size() == 0) {
// There is only one element, Therefore, nothing to be done
return quantity;
}
// Check if there's a carry, and bubble it back up the resulting intValues.
int lastIndex = unitConverters_.size() - 1;
BigDecimal carry = unitConverters_.get(lastIndex).convertInverse(quantity).multiply(EPSILON_MULTIPLIER)
.setScale(0, RoundingMode.FLOOR);
if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
return quantity;
}
quantity = quantity.subtract(unitConverters_.get(lastIndex).convert(carry));
intValues.set(lastIndex - 1, intValues.get(lastIndex - 1).add(carry.toBigInteger()));
// We don't use the first converter: that one is for the input unit
for (int j = lastIndex - 1; j > 0; j--) {
carry = unitConverters_.get(j)
.convertInverse(BigDecimal.valueOf(intValues.get(j).longValue()))
.multiply(EPSILON_MULTIPLIER)
.setScale(0, RoundingMode.FLOOR);
if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
break;
}
intValues.set(j, intValues.get(j).subtract(unitConverters_.get(j).convert(carry).toBigInteger()));
intValues.set(j - 1, intValues.get(j - 1).add(carry.toBigInteger()));
}
return quantity;
}
@Override

View file

@ -17,21 +17,18 @@ import com.ibm.icu.util.StringTrieBuilder;
public class MeasureUnitImpl {
/**
* The full unit identifier. Null if not computed.
* The full unit identifier. Null if not computed.
*/
private String identifier = null;
/**
* The complexity, either SINGLE, COMPOUND, or MIXED.
*/
private MeasureUnit.Complexity complexity = MeasureUnit.Complexity.SINGLE;
/**
* The list of single units. These may be summed or multiplied, based on the
* value of the complexity field.
* <p>
* The "dimensionless" unit (SingleUnitImpl default constructor) must not be
* added to this list.
* The "dimensionless" unit (SingleUnitImpl default constructor) must not be added to this list.
* <p>
* The "dimensionless" <code>MeasureUnitImpl</code> has an empty <code>singleUnits</code>.
*/
@ -94,29 +91,20 @@ public class MeasureUnitImpl {
}
}
/**
* Extracts the list of all the individual units inside the `MeasureUnitImpl`.
* For example:
* - if the <code>MeasureUnitImpl</code> is <code>foot-per-hour</code>
* it will return a list of 1 <code>{foot-per-hour}</code>
* - if the <code>MeasureUnitImpl</code> is <code>foot-and-inch</code>
* it will return a list of 2 <code>{ foot, inch}</code>
*
* @return a list of <code>MeasureUnitImpl</code>
*/
public ArrayList<MeasureUnitImpl> extractIndividualUnits() {
ArrayList<MeasureUnitImpl> result = new ArrayList<>();
public ArrayList<MeasureUnitImplWithIndex> extractIndividualUnitsWithIndices() {
ArrayList<MeasureUnitImplWithIndex> result = new ArrayList<>();
if (this.getComplexity() == MeasureUnit.Complexity.MIXED) {
// In case of mixed units, each single unit can be considered as a stand alone MeasureUnitImpl.
int i = 0;
for (SingleUnitImpl singleUnit :
this.getSingleUnits()) {
result.add(new MeasureUnitImpl(singleUnit));
result.add(new MeasureUnitImplWithIndex(i++, new MeasureUnitImpl(singleUnit)));
}
return result;
}
result.add(this.copy());
result.add(new MeasureUnitImplWithIndex(0, this.copy()));
return result;
}
@ -198,7 +186,6 @@ public class MeasureUnitImpl {
throw new UnsupportedOperationException();
}
/**
* Returns the CLDR unit identifier and null if not computed.
*/
@ -266,6 +253,11 @@ public class MeasureUnitImpl {
this.identifier = result.toString();
}
@Override
public String toString() {
return "MeasureUnitImpl [" + build().getIdentifier() + "]";
}
public enum CompoundPart {
// Represents "-per-"
PER(0),
@ -369,6 +361,16 @@ public class MeasureUnitImpl {
}
public static class MeasureUnitImplWithIndex {
int index;
MeasureUnitImpl unitImpl;
MeasureUnitImplWithIndex(int index, MeasureUnitImpl unitImpl) {
this.index = index;
this.unitImpl = unitImpl;
}
}
public static class UnitsParser {
// This used only to not build the trie each time we use the parser
private volatile static CharsTrie savedTrie = null;
@ -748,19 +750,28 @@ public class MeasureUnitImpl {
public int compare(MeasureUnitImpl o1, MeasureUnitImpl o2) {
BigDecimal factor1 = this.conversionRates.getFactorToBase(o1).getConversionRate();
BigDecimal factor2 = this.conversionRates.getFactorToBase(o2).getConversionRate();
return factor1.compareTo(factor2);
}
}
static class MeasureUnitImplWithIndexComparator implements Comparator<MeasureUnitImplWithIndex> {
private MeasureUnitImplComparator measureUnitImplComparator;
public MeasureUnitImplWithIndexComparator(ConversionRates conversionRates) {
this.measureUnitImplComparator = new MeasureUnitImplComparator(conversionRates);
}
@Override
public int compare(MeasureUnitImplWithIndex o1, MeasureUnitImplWithIndex o2) {
return this.measureUnitImplComparator.compare(o1.unitImpl, o2.unitImpl);
}
}
static class SingleUnitComparator implements Comparator<SingleUnitImpl> {
@Override
public int compare(SingleUnitImpl o1, SingleUnitImpl o2) {
return o1.compareTo(o2);
}
}
@Override
public String toString() {
return "MeasureUnitImpl [" + build().getIdentifier() + "]";
}
}

View file

@ -176,19 +176,15 @@ public class UnitsRouter {
}
public class RouteResult {
// A list of measures: a single measure for single units, multiple measures
// for mixed units.
//
// TODO(icu-units/icu#21): figure out the right mixed unit API.
public final List<Measure> measures;
public final ComplexUnitsConverter.ComplexConverterResult complexConverterResult;
// The output unit for this RouteResult. This may be a MIXED unit - for
// example: "yard-and-foot-and-inch", for which `measures` will have three
// elements.
public final MeasureUnitImpl outputUnit;
RouteResult(List<Measure> measures, MeasureUnitImpl outputUnit) {
this.measures = measures;
RouteResult(ComplexUnitsConverter.ComplexConverterResult complexConverterResult, MeasureUnitImpl outputUnit) {
this.complexConverterResult = complexConverterResult;
this.outputUnit = outputUnit;
}
}

View file

@ -36,6 +36,7 @@ import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.MeasureUnit;
/**
* This is the "brain" of the number formatting pipeline. It ties all the pieces together, taking in a
* MacroProps and a DecimalQuantity and outputting a properly formatted number string.
@ -274,9 +275,7 @@ class NumberFormatterImpl {
}
chain = usagePrefsHandler = new UsagePrefsHandler(macros.loc, macros.unit, macros.usage, chain);
} else if (isMixedUnit) {
// TODO(icu-units#97): The input unit should be the largest unit, not the first unit, in the identifier.
MeasureUnit inputUnit = macros.unit.splitToSingleUnits().get(0);
chain = new UnitConversionHandler(inputUnit, macros.unit, chain);
chain = new UnitConversionHandler(macros.unit, chain);
}
// Multiplier

View file

@ -138,7 +138,7 @@ public class UnitsTest {
final MeasureUnitImpl inputImpl = MeasureUnitImpl.forIdentifier(input.getIdentifier());
final MeasureUnitImpl outputImpl = MeasureUnitImpl.forIdentifier(output.getIdentifier());
ComplexUnitsConverter converter = new ComplexUnitsConverter(inputImpl, outputImpl, rates);
measures = converter.convert(testCase.value, null);
measures = converter.convert(testCase.value, null).measures;
assertEquals("measures length", testCase.expected.length, measures.size());
int i = 0;
@ -166,20 +166,67 @@ public class UnitsTest {
@Test
public void testComplexUnitConverterSorting() {
class TestCase {
String message;
String inputUnit;
String outputUnit;
double inputValue;
Measure[] expectedMeasures;
double accuracy;
public TestCase(String message, String inputUnit, String outputUnit, double inputValue, Measure[] expectedMeasures, double accuracy) {
this.message = message;
this.inputUnit = inputUnit;
this.outputUnit = outputUnit;
this.inputValue = inputValue;
this.expectedMeasures = expectedMeasures;
this.accuracy = accuracy;
}
}
TestCase[] testCases = new TestCase[]{
new TestCase(
"inch-and-foot",
"meter",
"inch-and-foot",
10.0,
new Measure[]{
new Measure(9.70079, MeasureUnit.INCH),
new Measure(32, MeasureUnit.FOOT),
},
0.0001
),
new TestCase(
"inch-and-yard-and-foot",
"meter",
"inch-and-yard-and-foot",
100.0,
new Measure[]{
new Measure(1.0079, MeasureUnit.INCH),
new Measure(109, MeasureUnit.YARD),
new Measure(1, MeasureUnit.FOOT),
},
0.0001
),
};
MeasureUnitImpl source = MeasureUnitImpl.forIdentifier("meter");
MeasureUnitImpl target = MeasureUnitImpl.forIdentifier("inch-and-foot");
ConversionRates conversionRates = new ConversionRates();
for (TestCase testCase : testCases) {
MeasureUnitImpl input = MeasureUnitImpl.forIdentifier(testCase.inputUnit);
MeasureUnitImpl output = MeasureUnitImpl.forIdentifier(testCase.outputUnit);
ComplexUnitsConverter complexConverter = new ComplexUnitsConverter(source, target, conversionRates);
List<Measure> measures = complexConverter.convert(BigDecimal.valueOf(10.0), null);
ComplexUnitsConverter converter = new ComplexUnitsConverter(input, output, conversionRates);
List<Measure> actualMeasures = converter.convert(BigDecimal.valueOf(testCase.inputValue), null).measures;
assertEquals(measures.size(), 2);
assertEquals("inch-and-foot unit 0", "inch", measures.get(0).getUnit().getIdentifier());
assertEquals("inch-and-foot unit 1", "foot", measures.get(1).getUnit().getIdentifier());
assertEquals("inch-and-foot value 0", 9.7008, measures.get(0).getNumber().doubleValue(), 0.0001);
assertEquals("inch-and-foot value 1", 32, measures.get(1).getNumber().doubleValue(), 0.0001);
assertEquals(testCase.message, testCase.expectedMeasures.length, actualMeasures.size());
for (int i = 0; i < testCase.expectedMeasures.length; i++) {
assertEquals(testCase.message, testCase.expectedMeasures[i].getUnit(), actualMeasures.get(i).getUnit());
assertEquals(testCase.message,
testCase.expectedMeasures[i].getNumber().doubleValue(),
actualMeasures.get(i).getNumber().doubleValue(),
testCase.accuracy);
}
}
}
@ -443,7 +490,7 @@ public class UnitsTest {
for (TestCase testCase :
tests) {
UnitsRouter router = new UnitsRouter(testCase.inputUnit.second, testCase.region, testCase.usage);
List<Measure> measures = router.route(testCase.input, null).measures;
List<Measure> measures = router.route(testCase.input, null).complexConverterResult.measures;
assertEquals("Measures size must be the same as expected units",
measures.size(), testCase.expectedInOrder.size());

View file

@ -727,6 +727,73 @@ public class NumberFormatterApiTest extends TestFmwk {
4.28571,
"4 metric tons, 285 kilograms, 710 grams");
assertFormatSingle(
"Mixed Unit (Not Sorted) [metric]",
"unit/gram-and-kilogram unit-width-full-name",
"unit/gram-and-kilogram unit-width-full-name",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("gram-and-kilogram"))
.unitWidth(UnitWidth.FULL_NAME),
new ULocale("en-US"),
4.28571,
"285.71 grams, 4 kilograms");
assertFormatSingle(
"Mixed Unit (Not Sorted) [imperial]",
"unit/inch-and-yard-and-foot unit-width-full-name",
"unit/inch-and-yard-and-foot unit-width-full-name",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("inch-and-yard-and-foot"))
.unitWidth(UnitWidth.FULL_NAME),
new ULocale("en-US"),
4.28571,
"10.28556 inches, 4 yards, 0 feet");
assertFormatSingle(
"Mixed Unit (Not Sorted) [imperial full]",
"unit/inch-and-yard-and-foot unit-width-full-name",
"unit/inch-and-yard-and-foot unit-width-full-name",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("inch-and-yard-and-foot"))
.unitWidth(UnitWidth.FULL_NAME),
new ULocale("en-US"),
4.38571,
"1.88556 inches, 4 yards, 1 foot");
assertFormatSingle(
"Mixed Unit (Not Sorted) [imperial full integers]",
"unit/inch-and-yard-and-foot @# unit-width-full-name",
"unit/inch-and-yard-and-foot @# unit-width-full-name",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("inch-and-yard-and-foot"))
.unitWidth(UnitWidth.FULL_NAME)
.precision(Precision.maxSignificantDigits(2)),
new ULocale("en-US"),
4.36112,
"1 inch, 4 yards, 1 foot");
assertFormatSingle(
"Mixed Unit (Not Sorted) [imperial full] with `And` in the end",
"unit/inch-and-yard-and-foot unit-width-full-name",
"unit/inch-and-yard-and-foot unit-width-full-name",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("inch-and-yard-and-foot"))
.unitWidth(UnitWidth.FULL_NAME),
new ULocale("fr-FR"),
4.38571,
"1,88556\u00A0pouce, 4\u00A0yards et 1\u00A0pied");
assertFormatSingle(
"Mixed unit, Scientific [Not in Order]",
"unit/foot-and-inch-and-yard E0",
"unit/foot-and-inch-and-yard E0",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("foot-and-inch-and-yard"))
.notation(Notation.scientific()),
new ULocale("en-US"),
3.65,
"1 ft, 1.14E1 in, 3 yd");
assertFormatSingle(
"Testing \"1 foot 12 inches\"",
"unit/foot-and-inch @### unit-width-full-name",