Merge pull request #52 from icu-units/mixedunits

ICU-20941 Support Mixed Units in NumberFormatter when using usage()
This commit is contained in:
Hugo van der Merwe 2020-08-28 09:56:18 +02:00 committed by GitHub
commit cc5a122021
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 657 additions and 68 deletions

View file

@ -383,9 +383,12 @@ public:
* caller becomes responsible for deleting the array
*/
inline T *orphanOrClone(int32_t length, int32_t &resultCapacity);
private:
protected: // TODO(icu-units#64): make these private again if possible?
T *ptr;
int32_t capacity;
private:
UBool needToRelease;
T stackArray[stackCapacity];
void releaseArray() {

View file

@ -604,9 +604,9 @@ static const char * const gSubTypes[] = {
static int32_t unitPerUnitToSingleUnit[][4] = {
{378, 382, 12, 5},
{378, 387, 12, 6},
{388, 343, 19, 0},
{390, 350, 19, 2},
{392, 343, 19, 3},
{388, 343, 19, 0}, // kilometer per hour
{390, 350, 19, 2}, // meter per second
{392, 343, 19, 3}, // mile per hour
{392, 463, 4, 2},
{392, 464, 4, 3},
{411, 460, 3, 1},

View file

@ -34,6 +34,7 @@ constexpr int32_t DNAM_INDEX = StandardPlural::Form::COUNT;
* @copydoc DNAM_INDEX
*/
constexpr int32_t PER_INDEX = StandardPlural::Form::COUNT + 1;
// Number of keys in the array populated by PluralTableSink.
constexpr int32_t ARRAY_LENGTH = StandardPlural::Form::COUNT + 2;
static int32_t getIndex(const char* pluralKeyword, UErrorCode& status) {
@ -48,6 +49,11 @@ static int32_t getIndex(const char* pluralKeyword, UErrorCode& status) {
}
}
// Selects a string out of the `strings` array which corresponds to the
// specified plural form, with fallback to the OTHER form.
//
// The `strings` array must have ARRAY_LENGTH items: one corresponding to each
// of the plural forms, plus a display name ("dnam") and a "per" form.
static UnicodeString getWithPlural(
const UnicodeString* strings,
StandardPlural::Form plural,
@ -97,12 +103,18 @@ class PluralTableSink : public ResourceSink {
// NOTE: outArray MUST have room for all StandardPlural values. No bounds checking is performed.
// Populates outArray with `locale`-specific values for `unit` through use of
// PluralTableSink, reading from resources *unitsNarrow* and *unitsShort* (for
// width UNUM_UNIT_WIDTH_NARROW), or just *unitsShort* (for width
// UNUM_UNIT_WIDTH_SHORT). For other widths, it would read just "units".
//
// outArray must be of fixed length ARRAY_LENGTH.
/**
* Populates outArray with `locale`-specific values for `unit` through use of
* PluralTableSink. Only the set of basic units are supported!
*
* Reading from resources *unitsNarrow* and *unitsShort* (for width
* UNUM_UNIT_WIDTH_NARROW), or just *unitsShort* (for width
* UNUM_UNIT_WIDTH_SHORT). For other widths, it reads just "units".
*
* @param unit must have a type and subtype (i.e. it must be a unit listed in
* gTypes and gSubTypes in measunit.cpp).
* @param outArray must be of fixed length ARRAY_LENGTH.
*/
void getMeasureData(const Locale &locale, const MeasureUnit &unit, const UNumberUnitWidth &width,
UnicodeString *outArray, UErrorCode &status) {
PluralTableSink sink(outArray);
@ -200,24 +212,26 @@ UnicodeString getPerUnitFormat(const Locale& locale, const UNumberUnitWidth &wid
} // namespace
// TODO(units,hugovdm): deal properly with "perUnit" parameter here:
void LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitRef,
const MeasureUnit &perUnit, const UNumberUnitWidth &width,
const PluralRules *rules, const MicroPropsGenerator *parent,
LongNameHandler *fillIn, UErrorCode &status) {
if (fillIn == nullptr) {
status = U_INTERNAL_PROGRAM_ERROR;
return;
}
// Not valid for mixed units that aren't built-in units, and there should
// not be any built-in mixed units!
U_ASSERT(uprv_strlen(unitRef.getType()) > 0 || unitRef.getComplexity(status) != UMEASURE_UNIT_MIXED);
U_ASSERT(fillIn != nullptr);
if (uprv_strlen(unitRef.getType()) == 0 || uprv_strlen(perUnit.getType()) == 0) {
// TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an error code.
// TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
// error code. Once we support not-built-in units here, unitRef may be
// anything, but if not built-in, perUnit has to be "none".
status = U_UNSUPPORTED_ERROR;
return;
}
MeasureUnit unit = unitRef;
if (uprv_strcmp(perUnit.getType(), "none") != 0) {
// Compound unit: first try to simplify (e.g., meters per second is its own unit).
// Compound unit: first try to simplify (e.g. "meter per second" is a
// built-in unit).
bool isResolved = false;
MeasureUnit resolved = MeasureUnit::resolveUnitPerUnit(unit, perUnit, &isResolved);
if (isResolved) {
@ -240,7 +254,6 @@ void LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitR
status);
}
// TODO(units,hugovdm): deal properly with "perUnit" parameter here:
void LongNameHandler::forCompoundUnit(const Locale &loc, const MeasureUnit &unit,
const MeasureUnit &perUnit, const UNumberUnitWidth &width,
const PluralRules *rules, const MicroPropsGenerator *parent,
@ -386,6 +399,125 @@ const Modifier* LongNameHandler::getModifier(Signum /*signum*/, StandardPlural::
return &fModifiers[plural];
}
void MixedUnitLongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &mixedUnit,
const UNumberUnitWidth &width, const PluralRules *rules,
const MicroPropsGenerator *parent,
MixedUnitLongNameHandler *fillIn, UErrorCode &status) {
U_ASSERT(mixedUnit.getComplexity(status) == UMEASURE_UNIT_MIXED);
U_ASSERT(fillIn != nullptr);
LocalArray<MeasureUnit> individualUnits =
mixedUnit.splitToSingleUnits(fillIn->fMixedUnitCount, status);
fillIn->fMixedUnitData.adoptInstead(new UnicodeString[fillIn->fMixedUnitCount * ARRAY_LENGTH]);
for (int32_t i = 0; i < fillIn->fMixedUnitCount; i++) {
// Grab data for each of the components.
UnicodeString *unitData = &fillIn->fMixedUnitData[i * ARRAY_LENGTH];
getMeasureData(loc, individualUnits[i], width, unitData, status);
}
fillIn->fListFormatter.adoptInsteadAndCheckErrorCode(ListFormatter::createInstance(loc, status),
status);
fillIn->rules = rules;
fillIn->parent = parent;
// We need a localised NumberFormatter for the integers of the bigger units
// (providing Arabic numerals, for example).
fillIn->fIntegerFormatter = NumberFormatter::withLocale(loc);
}
void MixedUnitLongNameHandler::processQuantity(DecimalQuantity &quantity, MicroProps &micros,
UErrorCode &status) const {
U_ASSERT(fMixedUnitCount > 1);
if (parent != nullptr) {
parent->processQuantity(quantity, micros, status);
}
micros.modOuter = getMixedUnitModifier(quantity, micros, status);
}
const Modifier *MixedUnitLongNameHandler::getMixedUnitModifier(DecimalQuantity &quantity,
MicroProps &micros,
UErrorCode &status) const {
// TODO(icu-units#21): mixed units without usage() is not yet supported.
// That should be the only reason why this happens, so delete this whole if
// once fixed:
if (micros.mixedMeasuresCount == 0) {
status = U_UNSUPPORTED_ERROR;
return &micros.helpers.emptyWeakModifier;
}
U_ASSERT(micros.mixedMeasuresCount > 0);
// mixedMeasures does not contain the last value:
U_ASSERT(fMixedUnitCount == micros.mixedMeasuresCount + 1);
U_ASSERT(fListFormatter.isValid());
// Algorithm:
//
// For the mixed-units measurement of: "3 yard, 1 foot, 2.6 inch", we should
// find "3 yard" and "1 foot" in micros.mixedMeasures.
//
// Obtain long-names with plural forms corresponding to measure values:
// * {0} yards, {0} foot, {0} inches
//
// Format the integer values appropriately and modify with the format
// strings:
// - 3 yards, 1 foot
//
// Use ListFormatter to combine, with one placeholder:
// - 3 yards, 1 foot and {0} inches
//
// Return a SimpleModifier for this pattern, letting the rest of the
// pipeline take care of the remaining inches.
LocalArray<UnicodeString> outputMeasuresList(new UnicodeString[fMixedUnitCount], status);
if (U_FAILURE(status)) {
return &micros.helpers.emptyWeakModifier;
}
for (int32_t i = 0; i < micros.mixedMeasuresCount; i++) {
DecimalQuantity fdec;
fdec.setToLong(micros.mixedMeasures[i]);
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);
}
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;
fListFormatter->format(outputMeasuresList.getAlias(), fMixedUnitCount, premixedFormatPattern,
status);
SimpleFormatter premixedCompiled(premixedFormatPattern, 0, 1, status);
if (U_FAILURE(status)) {
return &micros.helpers.emptyWeakModifier;
}
// Return a SimpleModifier for the "premixed" pattern
micros.helpers.mixedUnitModifier =
SimpleModifier(premixedCompiled, {UFIELD_CATEGORY_NUMBER, UNUM_MEASURE_UNIT_FIELD}, false,
{this, SIGNUM_POS_ZERO, finalPlural});
return &micros.helpers.mixedUnitModifier;
}
const Modifier *MixedUnitLongNameHandler::getModifier(Signum /*signum*/,
StandardPlural::Form /*plural*/) const {
// TODO(units): investigate this method when investigating where
// LongNameHandler::getModifier() gets used. To be sure it remains
// unreachable:
UPRV_UNREACHABLE;
return nullptr;
}
LongNameMultiplexer *
LongNameMultiplexer::forMeasureUnits(const Locale &loc, const MaybeStackVector<MeasureUnit> &units,
const UNumberUnitWidth &width, const PluralRules *rules,
@ -395,16 +527,23 @@ LongNameMultiplexer::forMeasureUnits(const Locale &loc, const MaybeStackVector<M
return nullptr;
}
U_ASSERT(units.length() > 0);
if (result->fHandlers.resize(units.length()) == nullptr) {
status = U_MEMORY_ALLOCATION_ERROR;
return nullptr;
}
result->fMeasureUnits.adoptInstead(new MeasureUnit[units.length()]);
for (int32_t i = 0, length = units.length(); i < length; i++) {
// Create empty new LongNameHandler:
LongNameHandler *lnh =
result->fLongNameHandlers.emplaceBackAndCheckErrorCode(status);
result->fMeasureUnits[i] = *units[i];
// Fill in LongNameHandler:
LongNameHandler::forMeasureUnit(loc, *units[i],
MeasureUnit(), // TODO(units): deal with COMPOUND and MIXED units
width, rules, NULL, lnh, status);
const MeasureUnit& unit = *units[i];
result->fMeasureUnits[i] = unit;
if (unit.getComplexity(status) == UMEASURE_UNIT_MIXED) {
MixedUnitLongNameHandler *mlnh = result->fMixedUnitHandlers.createAndCheckErrorCode(status);
MixedUnitLongNameHandler::forMeasureUnit(loc, unit, width, rules, NULL, mlnh, status);
result->fHandlers[i] = mlnh;
} else {
LongNameHandler *lnh = result->fLongNameHandlers.createAndCheckErrorCode(status);
LongNameHandler::forMeasureUnit(loc, unit, MeasureUnit(), width, rules, NULL, lnh, status);
result->fHandlers[i] = lnh;
}
if (U_FAILURE(status)) {
return nullptr;
}
@ -420,9 +559,9 @@ void LongNameMultiplexer::processQuantity(DecimalQuantity &quantity, MicroProps
fParent->processQuantity(quantity, micros, status);
// Call the correct LongNameHandler based on outputUnit
for (int i = 0; i < fLongNameHandlers.length(); i++) {
for (int i = 0; i < fHandlers.getCapacity(); i++) {
if (fMeasureUnits[i] == micros.outputUnit) {
fLongNameHandlers[i]->processQuantity(quantity, micros, status);
fHandlers[i]->processQuantity(quantity, micros, status);
return;
}
}

View file

@ -8,6 +8,7 @@
#define __NUMBER_LONGNAMES_H__
#include "cmemory.h"
#include "unicode/listformatter.h"
#include "unicode/uversion.h"
#include "number_utils.h"
#include "number_modifiers.h"
@ -34,17 +35,47 @@ class LongNameHandler : public MicroPropsGenerator, public ModifierStore, public
forCurrencyLongNames(const Locale &loc, const CurrencyUnit &currency, const PluralRules *rules,
const MicroPropsGenerator *parent, UErrorCode &status);
/**
* Construct a localized LongNameHandler for the specified MeasureUnit.
*
* Compound units can be constructed via `unit` and `perUnit`. Both of these
* must then be built-in units.
*
* Mixed units are not supported, use MixedUnitLongNameHandler::forMeasureUnit.
*
* This function uses a fillIn intead of returning a pointer, because we
* want to fill in instances in a MemoryPool (which cannot adopt pointers it
* didn't create itself).
*
* @param loc The desired locale.
* @param unit The measure unit to construct a LongNameHandler for. If
* `perUnit` is also defined, `unit` must not be a mixed unit.
* @param perUnit If `unit` is a mixed unit, `perUnit` must be "none".
* @param width Specifies the desired unit rendering.
* @param rules Does not take ownership.
* @param parent Does not take ownership.
* @param fillIn Required.
*/
static void forMeasureUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit,
const UNumberUnitWidth &width, const PluralRules *rules,
const MicroPropsGenerator *parent, LongNameHandler *fillIn,
UErrorCode &status);
/**
* Selects the plural-appropriate Modifier from the set of fModifiers based
* on the plural form.
*/
void
processQuantity(DecimalQuantity &quantity, MicroProps &micros, UErrorCode &status) const U_OVERRIDE;
// TODO(units): investigate whether we might run into Mixed Unit trouble
// with this. This override for ModifierStore::getModifier does not support
// mixed units: investigate under which circumstances it gets called (check
// both ImmutablePatternModifier and in NumberRangeFormatterImpl).
const Modifier* getModifier(Signum signum, StandardPlural::Form plural) const U_OVERRIDE;
private:
// A set of pre-computed modifiers, one for each plural form.
SimpleModifier fModifiers[StandardPlural::Form::COUNT];
// Not owned
const PluralRules *rules;
@ -58,34 +89,132 @@ class LongNameHandler : public MicroPropsGenerator, public ModifierStore, public
LongNameHandler() : rules(nullptr), parent(nullptr) {
}
friend class MemoryPool<LongNameHandler>; // To enable emplaceBack();
// Enables MemoryPool<LongNameHandler>::emplaceBack(): requires access to
// the private constructors.
friend class MemoryPool<LongNameHandler>;
friend class NumberFormatterImpl;
// Fills in LongNameHandler fields for formatting compound units identified
// via `unit` and `perUnit`. Both `unit` and `perUnit` need to be built-in
// units (for which data exists).
static void forCompoundUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit,
const UNumberUnitWidth &width, const PluralRules *rules,
const MicroPropsGenerator *parent, LongNameHandler *fillIn,
UErrorCode &status);
// Sets fModifiers to use the patterns from `simpleFormats`.
void simpleFormatsToModifiers(const UnicodeString *simpleFormats, Field field, UErrorCode &status);
// Sets fModifiers to a combination of `leadFormats` (one per plural form)
// and `trailFormat` appended to each.
//
// With a leadFormat of "{0}m" and a trailFormat of "{0}/s", it produces a
// pattern of "{0}m/s" by inserting the leadFormat pattern into trailFormat.
void multiSimpleFormatsToModifiers(const UnicodeString *leadFormats, UnicodeString trailFormat,
Field field, UErrorCode &status);
};
const int MAX_PREFS_COUNT = 10;
// Similar to LongNameHandler, but only for MIXED units.
class MixedUnitLongNameHandler : public MicroPropsGenerator, public ModifierStore, public UMemory {
public:
/**
* Construct a localized MixedUnitLongNameHandler for the specified
* MeasureUnit. It must be a MIXED unit.
*
* This function uses a fillIn intead of returning a pointer, because we
* want to fill in instances in a MemoryPool (which cannot adopt pointers it
* didn't create itself).
*
* @param loc The desired locale.
* @param mixedUnit The mixed measure unit to construct a
* MixedUnitLongNameHandler for.
* @param width Specifies the desired unit rendering.
* @param rules Does not take ownership.
* @param parent Does not take ownership.
* @param fillIn Required.
*/
static void forMeasureUnit(const Locale &loc, const MeasureUnit &mixedUnit,
const UNumberUnitWidth &width, const PluralRules *rules,
const MicroPropsGenerator *parent, MixedUnitLongNameHandler *fillIn,
UErrorCode &status);
/**
* Produces a plural-appropriate Modifier for a mixed unit: `quantity` is
* taken as the final smallest unit, while the larger unit values must be
* provided via `micros.mixedMeasures`.
*/
void processQuantity(DecimalQuantity &quantity, MicroProps &micros,
UErrorCode &status) const U_OVERRIDE;
// Required for ModifierStore. And ModifierStore is required by
// SimpleModifier constructor's last parameter. We assert his will never get
// called though.
const Modifier *getModifier(Signum signum, StandardPlural::Form plural) const U_OVERRIDE;
private:
// Not owned
const PluralRules *rules;
// Not owned
const MicroPropsGenerator *parent;
// Total number of units in the MeasureUnit this LongNameHandler was
// configured for: for "foot-and-inch", this will be 2. (If not a mixed unit,
// this will be 1.)
int32_t fMixedUnitCount = 1;
// If this LongNameHandler is for a mixed unit, this 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.
LocalPointer<ListFormatter> fListFormatter;
MixedUnitLongNameHandler(const PluralRules *rules, const MicroPropsGenerator *parent)
: rules(rules), parent(parent) {
}
MixedUnitLongNameHandler() : rules(nullptr), parent(nullptr) {
}
// Enables MemoryPool<LongNameHandler>::emplaceBack(): requires access to
// the private constructors.
friend class MemoryPool<MixedUnitLongNameHandler>;
// Fills in LongNameHandler fields for formatting mixed units. Each unit in
// a mixed unit must be a built-in unit.
static void forMixedUnit(const Locale &loc, const MeasureUnit &unit, const UNumberUnitWidth &width,
const PluralRules *rules, const MicroPropsGenerator *parent,
MixedUnitLongNameHandler *fillIn, UErrorCode &status);
// For a mixed unit, returns a Modifier that takes only one parameter: the
// smallest and final unit of the set. The bigger units' values and labels
// get baked into this Modifier, together with the unit label of the final
// unit.
const Modifier *getMixedUnitModifier(DecimalQuantity &quantity, MicroProps &micros,
UErrorCode &status) const;
};
/**
* A MicroPropsGenerator that multiplexes between different LongNameHandlers,
* depending on the outputUnit (micros.helpers.outputUnit should be set earlier
* in the chain).
* depending on the outputUnit.
*
* See processQuantity() for the input requirements.
*/
class LongNameMultiplexer : public MicroPropsGenerator, public UMemory {
public:
// FIXME: docstring?
// Produces a multiplexer for LongNameHandlers, one for each unit in
// `units`. An individual unit might be a mixed unit.
static LongNameMultiplexer *forMeasureUnits(const Locale &loc,
const MaybeStackVector<MeasureUnit> &units,
const UNumberUnitWidth &width, const PluralRules *rules,
const MicroPropsGenerator *parent, UErrorCode &status);
// The output unit must be provided via `micros.outputUnit`, it must match
// one of the units provided to the factory function.
void processQuantity(DecimalQuantity &quantity, MicroProps &micros,
UErrorCode &status) const U_OVERRIDE;
@ -95,8 +224,14 @@ class LongNameMultiplexer : public MicroPropsGenerator, public UMemory {
* earlier MicroPropsGenerators in the chain, LongNameMultiplexer keeps the
* parent link, while the LongNameHandlers are given no parents.
*/
MaybeStackVector<LongNameHandler> fLongNameHandlers;
MemoryPool<LongNameHandler> fLongNameHandlers;
MemoryPool<MixedUnitLongNameHandler> fMixedUnitHandlers;
// Unowned pointers to instances owned by MaybeStackVectors.
MaybeStackArray<MicroPropsGenerator *, 8> fHandlers;
// Each MeasureUnit corresponds to the same-index MicroPropsGenerator
// pointed to in fHandlers.
LocalArray<MeasureUnit> fMeasureUnits;
const MicroPropsGenerator *fParent;
LongNameMultiplexer(const MicroPropsGenerator *parent) : fParent(parent) {

View file

@ -22,6 +22,57 @@
U_NAMESPACE_BEGIN namespace number {
namespace impl {
/**
* A copyable container for the integer values of mixed unit measurements.
*
* If memory allocation fails during copying, no values are copied and status is
* set to U_MEMORY_ALLOCATION_ERROR.
*/
class IntMeasures : public MaybeStackArray<int64_t, 2> {
public:
/**
* Default constructor initializes with internal T[stackCapacity] buffer.
*
* 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>() {
}
/**
* Copy constructor.
*
* If memory allocation fails during copying, no values are copied and
* status is set to U_MEMORY_ALLOCATION_ERROR.
*/
IntMeasures(const IntMeasures &other) {
this->operator=(other);
};
// Assignment operator
IntMeasures &operator=(const IntMeasures &rhs) {
if (this == &rhs) {
return *this;
}
int32_t length = rhs.capacity;
if (this->resize(length, 0) != NULL) {
U_ASSERT(this->capacity == rhs.capacity);
uprv_memcpy(this->ptr, rhs.ptr, (size_t)length * sizeof(int64_t));
} else {
status = U_MEMORY_ALLOCATION_ERROR;
}
return *this;
};
/** Move constructor */
IntMeasures(IntMeasures &&src) = default;
/** Move assignment */
IntMeasures &operator=(IntMeasures &&src) = default;
UErrorCode status = U_ZERO_ERROR;
};
// TODO(units): generated by MicroPropsGenerator, but inherits from it too. Do
// we want to better document why? There's an explanation for processQuantity:
// * As MicroProps is the "base instance", this implementation of
@ -41,22 +92,48 @@ struct MicroProps : public MicroPropsGenerator {
// Note: This struct has no direct ownership of the following pointers.
const DecimalFormatSymbols* symbols;
// Pointers to Modifiers provided by the number formatting pipeline (when
// the value is known):
// A Modifier provided by LongNameHandler, used for currency long names and
// units. If there is no LongNameHandler needed, this should be an
// EmptyModifier. (This is typically the third modifier applied.)
const Modifier* modOuter;
// A Modifier for short currencies and compact notation. (This is typically
// the second modifier applied.)
const Modifier* modMiddle = nullptr;
// A Modifier provided by ScientificHandler, used for scientific notation.
// This is typically the first modifier applied.
const Modifier* modInner;
// The following "helper" fields may optionally be used during the MicroPropsGenerator.
// They live here to retain memory.
struct {
// The ScientificModifier for which ScientificHandler is responsible.
// ScientificHandler::processQuantity() modifies this Modifier.
ScientificModifier scientificModifier;
// EmptyModifier used for modOuter
EmptyModifier emptyWeakModifier{false};
// EmptyModifier used for modInner
EmptyModifier emptyStrongModifier{true};
MultiplierFormatHandler multiplier;
// A Modifier used for Mixed Units. When formatting mixed units,
// LongNameHandler assigns this Modifier.
SimpleModifier mixedUnitModifier;
} helpers;
// The MeasureUnit with which the output measurement is represented.
// The MeasureUnit with which the output is represented. May also have
// UMEASURE_UNIT_MIXED complexity, in which case mixedMeasures comes into
// play.
MeasureUnit outputUnit;
// In the case of mixed units, this is the set of integer-only units
// *preceding* the final unit.
IntMeasures mixedMeasures;
// Number of mixedMeasures that have been populated
int32_t mixedMeasuresCount = 0;
MicroProps() = default;
MicroProps(const MicroProps& other) = default;

View file

@ -1061,7 +1061,15 @@ void blueprint_helpers::parseIdentifierUnitOption(const StringSegment& segment,
return;
}
// TODO(ICU-20941): Clean this up.
// Mixed units can only be represented by a full MeasureUnit instances, so
// we ignore macros.perUnit.
if (fullUnit.complexity == UMEASURE_UNIT_MIXED) {
macros.unit = std::move(fullUnit).build(status);
return;
}
// TODO(ICU-20941): Clean this up (see also
// https://github.com/icu-units/icu/issues/35).
for (int32_t i = 0; i < fullUnit.units.length(); i++) {
SingleUnitImpl* subUnit = fullUnit.units[i];
if (subUnit->dimensionality > 0) {

View file

@ -246,6 +246,10 @@ void generateMeasureUnitOption(const MeasureUnit& measureUnit, UnicodeString& sb
void parseMeasurePerUnitOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
/**
* Parses unit identifiers like "meter-per-second" and "foot-and-inch", as
* specified via a "unit/" concise skeleton.
*/
void parseIdentifierUnitOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
void parseUnitUsageOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);

View file

@ -41,10 +41,7 @@ Usage &Usage::operator=(const Usage &other) {
return *this;
}
// Move constructor - can it be improved by taking over src's "this" instead of
// copying contents? Swapping pointers makes sense for heap objects but not for
// stack objects.
// *this = std::move(src);
// Move constructor
Usage::Usage(Usage &&src) U_NOEXCEPT : fUsage(src.fUsage), fLength(src.fLength), fError(src.fError) {
// Take ownership away from src if necessary
src.fUsage = nullptr;
@ -105,9 +102,43 @@ void UsagePrefsHandler::processQuantity(DecimalQuantity &quantity, MicroProps &m
if (U_FAILURE(status)) {
return;
}
const auto& routedUnits = routed.measures;
micros.outputUnit = routedUnits[0]->getUnit();
quantity.setToDouble(routedUnits[0]->getNumber().getDouble());
const MaybeStackVector<Measure>& routedUnits = routed.measures;
micros.outputUnit = routed.outputUnit.copy(status).build(status);
if (U_FAILURE(status)) {
return;
}
micros.mixedMeasuresCount = routedUnits.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:
int32_t singleUnitsCount;
LocalArray<MeasureUnit> singleUnits =
micros.outputUnit.splitToSingleUnits(singleUnitsCount, status);
U_ASSERT(U_SUCCESS(status));
U_ASSERT(routedUnits.length() == singleUnitsCount);
for (int32_t i = 0; i < routedUnits.length(); i++) {
U_ASSERT(routedUnits[i]->getUnit() == singleUnits[i]);
}
#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] = routedUnits[i]->getNumber().getInt64();
}
} else {
micros.mixedMeasuresCount = 0;
}
// The last value (potentially the only value) gets passed on via quantity.
quantity.setToDouble(routedUnits[routedUnits.length() - 1]->getNumber().getDouble());
UnicodeString precisionSkeleton = routed.precision;
if (micros.rounder.fPrecision.isBogus()) {

View file

@ -9,6 +9,8 @@
#include "cmemory.h"
#include "number_types.h"
#include "unicode/listformatter.h"
#include "unicode/localpointer.h"
#include "unicode/locid.h"
#include "unicode/measunit.h"
#include "unicode/stringpiece.h"
@ -34,6 +36,9 @@ class U_I18N_API UsagePrefsHandler : public MicroPropsGenerator, public UMemory
/**
* Obtains the appropriate output value, MeasurementUnit and
* rounding/precision behaviour from the UnitsRouter.
*
* The output unit is passed on to the LongNameHandler via
* micros.outputUnit.
*/
void processQuantity(DecimalQuantity &quantity, MicroProps &micros,
UErrorCode &status) const U_OVERRIDE;

View file

@ -72,17 +72,17 @@ RouteResult UnitsRouter::route(double quantity, UErrorCode &status) const {
const auto &converterPreference = *converterPreferences_[i];
if (converterPreference.converter.greaterThanOrEqual(quantity, converterPreference.limit)) {
return RouteResult(converterPreference.converter.convert(quantity, status), //
converterPreference.precision //
);
return RouteResult(converterPreference.converter.convert(quantity, status),
converterPreference.precision,
converterPreference.targetUnit.copy(status));
}
}
// In case of the `quantity` does not fit in any converter limit, use the last converter.
const auto &lastConverterPreference = (*converterPreferences_[converterPreferences_.length() - 1]);
return RouteResult(lastConverterPreference.converter.convert(quantity, status), //
lastConverterPreference.precision //
);
return RouteResult(lastConverterPreference.converter.convert(quantity, status),
lastConverterPreference.precision,
lastConverterPreference.targetUnit.copy(status));
}
const MaybeStackVector<MeasureUnit> *UnitsRouter::getOutputUnits() const {

View file

@ -37,8 +37,13 @@ struct RouteResult : UMemory {
// or document that other skeleton elements are ignored?
UnicodeString precision;
RouteResult(MaybeStackVector<Measure> measures, UnicodeString precision)
: measures(std::move(measures)), precision(std::move(precision)) {}
// 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.
MeasureUnitImpl outputUnit;
RouteResult(MaybeStackVector<Measure> measures, UnicodeString precision, MeasureUnitImpl outputUnit)
: measures(std::move(measures)), precision(std::move(precision)), outputUnit(std::move(outputUnit)) {}
};
/**
@ -55,6 +60,10 @@ struct ConverterPreference : UMemory {
double limit;
UnicodeString precision;
// The output unit for this ConverterPreference. This may be a MIXED unit -
// for example: "yard-and-foot-and-inch".
MeasureUnitImpl targetUnit;
// In case there is no limit, the limit will be -inf.
ConverterPreference(const MeasureUnitImpl &source, const MeasureUnitImpl &complexTarget,
UnicodeString precision, const ConversionRates &ratesInfo, UErrorCode &status)
@ -65,7 +74,7 @@ struct ConverterPreference : UMemory {
double limit, UnicodeString precision, const ConversionRates &ratesInfo,
UErrorCode &status)
: converter(source, complexTarget, ratesInfo, status), limit(limit),
precision(std::move(precision)) {}
precision(std::move(precision)), targetUnit(complexTarget.copy(status)) {}
};
/**

View file

@ -998,7 +998,7 @@ group: numberformatter
numrange_fluent.o numrange_impl.o
deps
decnumber double_conversion formattable units unitsformatter
number_representation number_output
listformatter number_representation number_output
numsys
number_usageprefs
uclean_i18n common

View file

@ -81,6 +81,7 @@ private:
void TestNumericTimeSomeSpecialFormats();
void TestIdentifiers();
void TestInvalidIdentifiers();
void TestParseToBuiltIn();
void TestCompoundUnitOperations();
void TestDimensionlessBehaviour();
void Test21060_AddressSanitizerProblem();
@ -205,6 +206,7 @@ void MeasureFormatTest::runIndexedTest(
TESTCASE_AUTO(TestNumericTimeSomeSpecialFormats);
TESTCASE_AUTO(TestIdentifiers);
TESTCASE_AUTO(TestInvalidIdentifiers);
TESTCASE_AUTO(TestParseToBuiltIn);
TESTCASE_AUTO(TestCompoundUnitOperations);
TESTCASE_AUTO(TestDimensionlessBehaviour);
TESTCASE_AUTO(Test21060_AddressSanitizerProblem);
@ -3130,11 +3132,23 @@ void MeasureFormatTest::TestIndividualPluralFallback() {
// and falls back to fr for the "other" form.
IcuTestErrorCode errorCode(*this, "TestIndividualPluralFallback");
MeasureFormat mf("fr_CA", UMEASFMT_WIDTH_SHORT, errorCode);
if (errorCode.errIfFailureAndReset("MeasureFormat mf(...) failed.")) {
return;
}
LocalPointer<Measure> twoDeg(
new Measure(2.0, MeasureUnit::createGenericTemperature(errorCode), errorCode), errorCode);
if (errorCode.errIfFailureAndReset("Creating twoDeg failed.")) {
return;
}
UnicodeString expected = UNICODE_STRING_SIMPLE("2\\u00B0").unescape();
UnicodeString actual;
assertEquals("2 deg temp in fr_CA", expected, mf.format(twoDeg.orphan(), actual, errorCode), TRUE);
// Formattable adopts the pointer
mf.format(Formattable(twoDeg.orphan()), actual, errorCode);
if (errorCode.errIfFailureAndReset("mf.format(...) failed.")) {
return;
}
assertEquals("2 deg temp in fr_CA", expected, actual, TRUE);
errorCode.errIfFailureAndReset("mf.format failed");
}
void MeasureFormatTest::Test20332_PersonUnits() {
@ -3321,6 +3335,33 @@ void MeasureFormatTest::TestInvalidIdentifiers() {
}
}
void MeasureFormatTest::TestParseToBuiltIn() {
IcuTestErrorCode status(*this, "TestParseToBuiltIn()");
const struct TestCase {
const char *identifier;
MeasureUnit expectedBuiltIn;
} cases[] = {
{"meter-per-second-per-second", MeasureUnit::getMeterPerSecondSquared()},
{"meter-per-second-second", MeasureUnit::getMeterPerSecondSquared()},
{"centimeter-centimeter", MeasureUnit::getSquareCentimeter()},
{"square-foot", MeasureUnit::getSquareFoot()},
{"pow2-inch", MeasureUnit::getSquareInch()},
{"milligram-per-deciliter", MeasureUnit::getMilligramPerDeciliter()},
{"pound-force-per-pow2-inch", MeasureUnit::getPoundPerSquareInch()},
{"yard-pow2-yard", MeasureUnit::getCubicYard()},
{"square-yard-yard", MeasureUnit::getCubicYard()},
};
for (auto &cas : cases) {
MeasureUnit fromIdent = MeasureUnit::forIdentifier(cas.identifier, status);
status.assertSuccess();
assertEquals("forIdentifier returns a normal built-in unit when it exists",
cas.expectedBuiltIn.getOffset(), fromIdent.getOffset());
assertEquals("type", cas.expectedBuiltIn.getType(), fromIdent.getType());
assertEquals("subType", cas.expectedBuiltIn.getSubtype(), fromIdent.getSubtype());
}
}
void MeasureFormatTest::TestCompoundUnitOperations() {
IcuTestErrorCode status(*this, "TestCompoundUnitOperations");

View file

@ -53,6 +53,7 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition {
void notationScientific();
void notationCompact();
void unitMeasure();
void unitPipeline();
void unitCompoundMeasure();
void unitUsage();
void unitUsageErrorCodes();
@ -87,6 +88,7 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition {
void localPointerCAPI();
void toObject();
void toDecimalNumber();
void microPropsInternals();
void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0);

View file

@ -11,12 +11,13 @@
#include <memory>
#include "unicode/unum.h"
#include "unicode/numberformatter.h"
#include "unicode/utypes.h"
#include "number_asformat.h"
#include "number_types.h"
#include "number_utils.h"
#include "numbertest.h"
#include "unicode/utypes.h"
#include "number_utypes.h"
#include "number_microprops.h"
#include "numbertest.h"
using number::impl::UFormattedNumberData;
@ -74,6 +75,7 @@ void NumberFormatterApiTest::runIndexedTest(int32_t index, UBool exec, const cha
TESTCASE_AUTO(notationScientific);
TESTCASE_AUTO(notationCompact);
TESTCASE_AUTO(unitMeasure);
TESTCASE_AUTO(unitPipeline);
TESTCASE_AUTO(unitCompoundMeasure);
TESTCASE_AUTO(unitUsage);
TESTCASE_AUTO(unitUsageErrorCodes);
@ -115,6 +117,7 @@ void NumberFormatterApiTest::runIndexedTest(int32_t index, UBool exec, const cha
TESTCASE_AUTO(localPointerCAPI);
TESTCASE_AUTO(toObject);
TESTCASE_AUTO(toDecimalNumber);
TESTCASE_AUTO(microPropsInternals);
TESTCASE_AUTO_END;
}
@ -679,24 +682,27 @@ void NumberFormatterApiTest::unitMeasure() {
u"5 a\u00F1os");
}
// TODO(hugovdm): once one of #52 and #61 has been merged into the other, move
// down for consistent method order.
void NumberFormatterApiTest::unitUsage() {
UnlocalizedNumberFormatter unloc_formatter =
NumberFormatter::with().usage("road").unit(MeasureUnit::getMeter());
IcuTestErrorCode status(*this, "unitUsage()");
UnlocalizedNumberFormatter unloc_formatter;
LocalizedNumberFormatter formatter;
FormattedNumber formattedNum;
UnicodeString uTestCase;
unloc_formatter = NumberFormatter::with().usage("road").unit(MeasureUnit::getMeter());
uTestCase = u"unitUsage() en-ZA road";
formatter = unloc_formatter.locale("en-ZA");
formattedNum = formatter.formatDouble(321, status);
status.errIfFailureAndReset("unitUsage() en-ZA road, formatDouble(...)");
assertTrue(UnicodeString("unitUsage() en-ZA road, got outputUnit: \"") +
formattedNum.getOutputUnit(status).getIdentifier() + "\"",
MeasureUnit::getMeter() == formattedNum.getOutputUnit(status));
assertEquals("unitUsage() en-ZA road", "300 m", formattedNum.toString(status));
status.errIfFailureAndReset("unitUsage() en-ZA road formatDouble");
assertTrue(
uTestCase + ", got outputUnit: \"" + formattedNum.getOutputUnit(status).getIdentifier() + "\"",
MeasureUnit::getMeter() == formattedNum.getOutputUnit(status));
assertEquals(uTestCase, "300 m", formattedNum.toString(status));
assertFormatDescendingBig(
u"unitUsage() en-ZA road",
uTestCase.getTerminatedBuffer(),
u"measure-unit/length-meter usage/road",
u"unit/meter usage/road",
unloc_formatter,
@ -711,6 +717,7 @@ void NumberFormatterApiTest::unitUsage() {
u"10 m",
u"0 m");
uTestCase = u"unitUsage() en-GB road";
formatter = unloc_formatter.locale("en-GB");
formattedNum = formatter.formatDouble(321, status);
status.errIfFailureAndReset("unitUsage() en-GB road, formatDouble(...)");
@ -724,7 +731,7 @@ void NumberFormatterApiTest::unitUsage() {
status.errIfFailureAndReset("unitUsage() en-GB road, toString(...)");
U_ASSERT(status == U_ZERO_ERROR);
assertFormatDescendingBig(
u"unitUsage() en-GB road",
uTestCase.getTerminatedBuffer(),
u"measure-unit/length-meter usage/road",
u"unit/meter usage/road",
unloc_formatter,
@ -739,6 +746,7 @@ void NumberFormatterApiTest::unitUsage() {
u"9.6 yd",
u"0 yd");
uTestCase = u"unitUsage() en-US road";
formatter = unloc_formatter.locale("en-US");
formattedNum = formatter.formatDouble(321, status);
status.errIfFailureAndReset("unitUsage() en-US road, formatDouble(...)");
@ -752,7 +760,7 @@ void NumberFormatterApiTest::unitUsage() {
status.errIfFailureAndReset("unitUsage() en-US road, toString(...)");
U_ASSERT(status == U_ZERO_ERROR);
assertFormatDescendingBig(
u"unitUsage() en-US road",
uTestCase.getTerminatedBuffer(),
u"measure-unit/length-meter usage/road",
u"unit/meter usage/road",
unloc_formatter,
@ -767,6 +775,39 @@ void NumberFormatterApiTest::unitUsage() {
u"30 ft",
u"0 ft");
unloc_formatter = NumberFormatter::with().usage("person").unit(MeasureUnit::getKilogram());
uTestCase = u"unitUsage() en-GB person";
formatter = unloc_formatter.locale("en-GB");
formattedNum = formatter.formatDouble(80, status);
status.errIfFailureAndReset("unitUsage() en-GB person formatDouble");
assertTrue(
uTestCase + ", got outputUnit: \"" + formattedNum.getOutputUnit(status).getIdentifier() + "\"",
MeasureUnit::forIdentifier("stone-and-pound", status) == formattedNum.getOutputUnit(status));
status.errIfFailureAndReset("unitUsage() en-GB person - formattedNum.getOutputUnit(status)");
assertEquals(uTestCase, "12 st and 8.4 lb", formattedNum.toString(status));
assertFormatDescending(
uTestCase.getTerminatedBuffer(),
u"measure-unit/mass-kilogram usage/person",
u"unit/kilogram usage/person",
unloc_formatter,
Locale("en-GB"),
u"13,802 st and 7.2 lb",
u"1,380 st and 3.5 lb",
u"138 st and 0.35 lb",
u"13 st and 11 lb",
u"1 st and 5.3 lb",
u"1 lb and 15 oz",
u"0 lb and 3.1 oz",
u"0 lb and 0.31 oz",
u"0 lb and 0 oz");
// TODO(icu-units#60): determine appropriate ListFormatter style output. Consider:
// * Unit Widths: narrow, short, full-name, iso-code, formal, variant,
// hidden.
// * List Format widths: wide, short, narrow? (From ULISTFMT_WIDTH_*.) Or is
// it "standard", "duration", or "duration-short"? (From an internal
// ListFormatter::createInstance method.)
assertFormatDescendingBig(
u"Scientific notation with Usage: possible when using a reasonable Precision",
u"scientific @### usage/default measure-unit/area-square-meter unit-width-full-name",
@ -926,6 +967,73 @@ void NumberFormatterApiTest::unitUsageSkeletons() {
status.assertSuccess();
}
void NumberFormatterApiTest::unitPipeline() {
IcuTestErrorCode status(*this, "unitPipeline()");
assertFormatSingle(
u"Built-in unit, meter-per-second",
u"measure-unit/speed-meter-per-second",
u"~unit/meter-per-second", // TODO(icu-units#35): does not normalize as expected
NumberFormatter::with().unit(MeasureUnit::getMeterPerSecond()),
Locale("en-GB"),
2.4,
u"2.4 m/s");
assertFormatSingle(
u"Built-in unit meter-per-second specified as .unit(built-in).perUnit(built-in)",
u"measure-unit/length-meter per-measure-unit/duration-second",
u"unit/meter-per-second", // TODO(icu-units#35): check whether desired behaviour?
NumberFormatter::with().unit(METER).perUnit(SECOND),
Locale("en-GB"),
2.4,
"2.4 m/s");
// TODO(icu-units#59): THIS UNIT TEST DEMONSTRATES UNDESIREABLE BEHAVIOUR!
// When specifying built-in types, one can give both a unit and a perUnit.
// Resolving to a built-in unit does not always work.
//
// (Unit-testing philosophy: leave enabled to demonstrate current behaviour
// and changing behaviour in the future? Comment out to not assert this is
// "correct"?)
assertFormatSingle(
u"DEMONSTRATING BAD BEHAVIOUR, TODO(icu-units#59)",
u"measure-unit/speed-meter-per-second per-measure-unit/duration-second",
u"measure-unit/speed-meter-per-second per-measure-unit/duration-second",
NumberFormatter::with().unit(MeasureUnit::getMeterPerSecond()).perUnit(MeasureUnit::getSecond()),
Locale("en-GB"),
2.4,
"2.4 m/s/s");
LocalizedNumberFormatter nf;
FormattedNumber num;
// If unit is not a built-in type, perUnit is not allowed
nf = NumberFormatter::with()
.unit(MeasureUnit::forIdentifier("furlong-pascal", status))
.perUnit(METER)
.locale("en-GB");
status.assertSuccess(); // Error is only returned once we try to format.
num = nf.formatDouble(2.4, status);
if (!status.expectErrorAndReset(U_UNSUPPORTED_ERROR)) {
errln(UnicodeString("Expected failure, got: \"") +
nf.formatDouble(2.4, status).toString(status) + "\".");
status.assertSuccess();
}
// perUnit is only allowed to be a built-in type
nf = NumberFormatter::with()
.unit(MeasureUnit::getMeter())
.perUnit(MeasureUnit::forIdentifier("square-second", status))
.locale("en-GB");
status.assertSuccess(); // Error is only returned once we try to format.
num = nf.formatDouble(2.4, status);
if (!status.expectErrorAndReset(U_UNSUPPORTED_ERROR)) {
errln(UnicodeString("Expected failure, got: \"") +
nf.formatDouble(2.4, status).toString(status) + "\".");
status.assertSuccess();
}
}
void NumberFormatterApiTest::unitCompoundMeasure() {
assertFormatDescending(
u"Meters Per Second Short (unit that simplifies) and perUnit method",
@ -3698,6 +3806,33 @@ void NumberFormatterApiTest::toDecimalNumber() {
"9.8765E+14", fn.toDecimalNumber<std::string>(status).c_str());
}
void NumberFormatterApiTest::microPropsInternals(void) {
// Verify copy construction and assignment operators.
int64_t testValues[2] = {4, 61};
MicroProps mp;
assertEquals("capacity", 2, mp.mixedMeasures.getCapacity());
mp.mixedMeasures[0] = testValues[0];
mp.mixedMeasures[1] = testValues[1];
MicroProps copyConstructed(mp);
MicroProps copyAssigned;
int64_t *resizeResult = mp.mixedMeasures.resize(4, 4);
assertTrue("Resize success", resizeResult != NULL);
copyAssigned = mp;
assertTrue("MicroProps success status", U_SUCCESS(mp.mixedMeasures.status));
assertTrue("Copy Constructed success status", U_SUCCESS(copyConstructed.mixedMeasures.status));
assertTrue("Copy Assigned success status", U_SUCCESS(copyAssigned.mixedMeasures.status));
assertEquals("Original values[0]", testValues[0], mp.mixedMeasures[0]);
assertEquals("Original values[1]", testValues[1], mp.mixedMeasures[1]);
assertEquals("Copy Constructed[0]", testValues[0], copyConstructed.mixedMeasures[0]);
assertEquals("Copy Constructed[1]", testValues[1], copyConstructed.mixedMeasures[1]);
assertEquals("Copy Assigned[0]", testValues[0], copyAssigned.mixedMeasures[0]);
assertEquals("Copy Assigned[1]", testValues[1], copyAssigned.mixedMeasures[1]);
assertEquals("Original capacity", 4, mp.mixedMeasures.getCapacity());
assertEquals("Copy Constructed capacity", 2, copyConstructed.mixedMeasures.getCapacity());
assertEquals("Copy Assigned capacity", 4, copyAssigned.mixedMeasures.getCapacity());
}
void NumberFormatterApiTest::assertFormatDescending(
const char16_t* umessage,