ICU-21349 Enhance Supporting Mixed Unit (such as "inch-and-foot")
See #1363
diff --git a/icu4c/source/i18n/measunit_extra.cpp b/icu4c/source/i18n/measunit_extra.cpp
index 03aefcd..9c6d4bd 100644
--- a/icu4c/source/i18n/measunit_extra.cpp
+++ b/icu4c/source/i18n/measunit_extra.cpp
@@ -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;
diff --git a/icu4c/source/i18n/measunit_impl.h b/icu4c/source/i18n/measunit_impl.h
index ef027ec..137d0ac 100644
--- a/icu4c/source/i18n/measunit_impl.h
+++ b/icu4c/source/i18n/measunit_impl.h
@@ -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);
diff --git a/icu4c/source/i18n/number_formatimpl.cpp b/icu4c/source/i18n/number_formatimpl.cpp
index 1409131..4661c27 100644
--- a/icu4c/source/i18n/number_formatimpl.cpp
+++ b/icu4c/source/i18n/number_formatimpl.cpp
@@ -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();
}
diff --git a/icu4c/source/i18n/number_longnames.cpp b/icu4c/source/i18n/number_longnames.cpp
index bf89a7a..fc2baa2 100644
--- a/icu4c/source/i18n/number_longnames.cpp
+++ b/icu4c/source/i18n/number_longnames.cpp
@@ -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 µs,
@@ -462,12 +462,6 @@ const Modifier *MixedUnitLongNameHandler::getMixedUnitModifier(DecimalQuantity &
status = U_UNSUPPORTED_ERROR;
return µs.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 µs.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 µs.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 µs.helpers.mixedUnitModifier;
}
diff --git a/icu4c/source/i18n/number_longnames.h b/icu4c/source/i18n/number_longnames.h
index 66eb9a9..9d34c3c 100644
--- a/icu4c/source/i18n/number_longnames.h
+++ b/icu4c/source/i18n/number_longnames.h
@@ -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)
diff --git a/icu4c/source/i18n/number_microprops.h b/icu4c/source/i18n/number_microprops.h
index 058c592..98bfa40 100644
--- a/icu4c/source/i18n/number_microprops.h
+++ b/icu4c/source/i18n/number_microprops.h
@@ -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;
diff --git a/icu4c/source/i18n/number_usageprefs.cpp b/icu4c/source/i18n/number_usageprefs.cpp
index ea684ba..a0f265d 100644
--- a/icu4c/source/i18n/number_usageprefs.cpp
+++ b/icu4c/source/i18n/number_usageprefs.cpp
@@ -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, µs, 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 µs,
diff --git a/icu4c/source/i18n/number_usageprefs.h b/icu4c/source/i18n/number_usageprefs.h
index 9e8bd93..7054722 100644
--- a/icu4c/source/i18n/number_usageprefs.h
+++ b/icu4c/source/i18n/number_usageprefs.h
@@ -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.
diff --git a/icu4c/source/i18n/units_complexconverter.cpp b/icu4c/source/i18n/units_complexconverter.cpp
index 7abe89f..7e9c713 100644
--- a/icu4c/source/i18n/units_complexconverter.cpp
+++ b/icu4c/source/i18n/units_complexconverter.cpp
@@ -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
diff --git a/icu4c/source/i18n/units_complexconverter.h b/icu4c/source/i18n/units_complexconverter.h
index 83c5b94..a727ea9 100644
--- a/icu4c/source/i18n/units_complexconverter.h
+++ b/icu4c/source/i18n/units_complexconverter.h
@@ -49,6 +49,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:
* - inputUnit and outputUnits must be under the same category
@@ -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
diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp
index a57cf3d..ccc3360 100644
--- a/icu4c/source/test/intltest/numbertest_api.cpp
+++ b/icu4c/source/test/intltest/numbertest_api.cpp
@@ -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",
diff --git a/icu4c/source/test/intltest/units_test.cpp b/icu4c/source/test/intltest/units_test.cpp
index fb67503..a72fff3 100644
--- a/icu4c/source/test/intltest/units_test.cpp
+++ b/icu4c/source/test/intltest/units_test.cpp
@@ -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);
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java
index a75ac6b..dd16a58 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java
@@ -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;
/**
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MixedUnitLongNameHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MixedUnitLongNameHandler.java
index d1b7965..8487524 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MixedUnitLongNameHandler.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MixedUnitLongNameHandler.java
@@ -168,14 +168,33 @@ private Modifier getMixedUnitModifier(DecimalQuantity quantity, MicroProps micro
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 @@ private Modifier getMixedUnitModifier(DecimalQuantity quantity, MicroProps micro
// 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 @@ private Modifier getMixedUnitModifier(DecimalQuantity quantity, MicroProps micro
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);
}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UnitConversionHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UnitConversionHandler.java
index 424a58a..84a77ad 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UnitConversionHandler.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UnitConversionHandler.java
@@ -5,8 +5,8 @@
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 MicroProps processQuantity(DecimalQuantity quantity) {
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;
}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java
index b490204..b2c7ec8 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java
@@ -6,6 +6,7 @@
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 UsagePrefsHandler(ULocale locale, MeasureUnit inputUnit, String usage, Mi
* 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 MicroProps processQuantity(DecimalQuantity quantity) {
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;
}
}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/ComplexUnitsConverter.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/ComplexUnitsConverter.java
index cf52263..3ea9030 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/ComplexUnitsConverter.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/ComplexUnitsConverter.java
@@ -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_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 ComplexUnitsConverter(MeasureUnitImpl inputUnit, MeasureUnitImpl outputUn
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 boolean greaterThanOrEqual(BigDecimal quantity, BigDecimal limit) {
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 boolean greaterThanOrEqual(BigDecimal quantity, BigDecimal limit) {
* 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 List<Measure> convert(BigDecimal quantity, Precision rounder) {
// - 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 List<Measure> convert(BigDecimal quantity, Precision rounder) {
// 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
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
index e0aa3c5..5658019 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
@@ -17,21 +17,18 @@
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 void takeReciprocal() {
}
}
- /**
- * 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 SingleUnitImpl getSingleUnitImpl() {
throw new UnsupportedOperationException();
}
-
/**
* Returns the CLDR unit identifier and null if not computed.
*/
@@ -266,6 +253,11 @@ public void serialize() {
this.identifier = result.toString();
}
+ @Override
+ public String toString() {
+ return "MeasureUnitImpl [" + build().getIdentifier() + "]";
+ }
+
public enum CompoundPart {
// Represents "-per-"
PER(0),
@@ -369,6 +361,16 @@ public int getValue() {
}
+ 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 MeasureUnitImplComparator(ConversionRates conversionRates) {
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() + "]";
- }
}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java
index d344f3e..b7c925c 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java
@@ -176,19 +176,15 @@ public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit,
}
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;
}
}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java
index abcb141..3954c4f 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java
@@ -36,6 +36,7 @@
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 @@ private static MicroPropsGenerator macrosToMicroGenerator(MacroProps macros, Mic
}
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
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java
index d7ec361..99e21cc 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java
@@ -138,7 +138,7 @@ class TestCase {
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 @@ class TestCase {
@Test
public void testComplexUnitConverterSorting() {
+ class TestCase {
+ String message;
+ String inputUnit;
+ String outputUnit;
+ double inputValue;
+ Measure[] expectedMeasures;
+ double accuracy;
- MeasureUnitImpl source = MeasureUnitImpl.forIdentifier("meter");
- MeasureUnitImpl target = MeasureUnitImpl.forIdentifier("inch-and-foot");
+ 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
+ ),
+ };
+
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 @@ private void insertData(String category,
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());
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
index 3e14dc0..dec74b9 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
@@ -728,6 +728,73 @@ public void unitMeasure() {
"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",
"unit/foot-and-inch @### unit-width-full-name",