mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-13 17:01:16 +00:00
ICU-21349 Enhance Supporting Mixed Unit (such as "inch-and-foot")
See #1363
This commit is contained in:
parent
c7ea02fcca
commit
b594f4b2a9
22 changed files with 743 additions and 404 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() + "]";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue