diff --git a/icu4c/source/common/cmemory.h b/icu4c/source/common/cmemory.h index 313180b29fb..9f8953d073e 100644 --- a/icu4c/source/common/cmemory.h +++ b/icu4c/source/common/cmemory.h @@ -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() { diff --git a/icu4c/source/i18n/measunit.cpp b/icu4c/source/i18n/measunit.cpp index 32da8ebb427..fb01e86fdcc 100644 --- a/icu4c/source/i18n/measunit.cpp +++ b/icu4c/source/i18n/measunit.cpp @@ -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}, diff --git a/icu4c/source/i18n/number_longnames.cpp b/icu4c/source/i18n/number_longnames.cpp index 32a5be9d591..4b70897640c 100644 --- a/icu4c/source/i18n/number_longnames.cpp +++ b/icu4c/source/i18n/number_longnames.cpp @@ -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 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 µs, + 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 µs, + 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 µs.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 outputMeasuresList(new UnicodeString[fMixedUnitCount], status); + if (U_FAILURE(status)) { + return µs.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 µs.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 µs.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 &units, const UNumberUnitWidth &width, const PluralRules *rules, @@ -395,16 +527,23 @@ LongNameMultiplexer::forMeasureUnits(const Locale &loc, const MaybeStackVector 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; } } diff --git a/icu4c/source/i18n/number_longnames.h b/icu4c/source/i18n/number_longnames.h index db08c119cd0..59b41c97c43 100644 --- a/icu4c/source/i18n/number_longnames.h +++ b/icu4c/source/i18n/number_longnames.h @@ -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 ¤cy, 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 µs, 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; // To enable emplaceBack(); + // Enables MemoryPool::emplaceBack(): requires access to + // the private constructors. + friend class MemoryPool; + 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 µs, + 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 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 fListFormatter; + + MixedUnitLongNameHandler(const PluralRules *rules, const MicroPropsGenerator *parent) + : rules(rules), parent(parent) { + } + + MixedUnitLongNameHandler() : rules(nullptr), parent(nullptr) { + } + + // Enables MemoryPool::emplaceBack(): requires access to + // the private constructors. + friend class MemoryPool; + + // 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 µs, + 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 &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 µs, 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 fLongNameHandlers; + MemoryPool fLongNameHandlers; + MemoryPool fMixedUnitHandlers; + // Unowned pointers to instances owned by MaybeStackVectors. + MaybeStackArray fHandlers; + // Each MeasureUnit corresponds to the same-index MicroPropsGenerator + // pointed to in fHandlers. LocalArray fMeasureUnits; + const MicroPropsGenerator *fParent; LongNameMultiplexer(const MicroPropsGenerator *parent) : fParent(parent) { diff --git a/icu4c/source/i18n/number_microprops.h b/icu4c/source/i18n/number_microprops.h index 9087e2613a4..ada11ce7640 100644 --- a/icu4c/source/i18n/number_microprops.h +++ b/icu4c/source/i18n/number_microprops.h @@ -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 { + 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() { + } + + /** + * 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; diff --git a/icu4c/source/i18n/number_skeletons.cpp b/icu4c/source/i18n/number_skeletons.cpp index 207fc05daac..2173aac70f6 100644 --- a/icu4c/source/i18n/number_skeletons.cpp +++ b/icu4c/source/i18n/number_skeletons.cpp @@ -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) { diff --git a/icu4c/source/i18n/number_skeletons.h b/icu4c/source/i18n/number_skeletons.h index b29b7f9960b..64cb60f3089 100644 --- a/icu4c/source/i18n/number_skeletons.h +++ b/icu4c/source/i18n/number_skeletons.h @@ -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); diff --git a/icu4c/source/i18n/number_usageprefs.cpp b/icu4c/source/i18n/number_usageprefs.cpp index 4738bfa5b0b..5fcdcc3805f 100644 --- a/icu4c/source/i18n/number_usageprefs.cpp +++ b/icu4c/source/i18n/number_usageprefs.cpp @@ -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& 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 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()) { diff --git a/icu4c/source/i18n/number_usageprefs.h b/icu4c/source/i18n/number_usageprefs.h index bdb2a54f82c..4ed58d1643b 100644 --- a/icu4c/source/i18n/number_usageprefs.h +++ b/icu4c/source/i18n/number_usageprefs.h @@ -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 µs, UErrorCode &status) const U_OVERRIDE; diff --git a/icu4c/source/i18n/unitsrouter.cpp b/icu4c/source/i18n/unitsrouter.cpp index 7381d550c5b..cf77dcad39d 100644 --- a/icu4c/source/i18n/unitsrouter.cpp +++ b/icu4c/source/i18n/unitsrouter.cpp @@ -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 *UnitsRouter::getOutputUnits() const { diff --git a/icu4c/source/i18n/unitsrouter.h b/icu4c/source/i18n/unitsrouter.h index b98ee83a786..1cade00885b 100644 --- a/icu4c/source/i18n/unitsrouter.h +++ b/icu4c/source/i18n/unitsrouter.h @@ -37,8 +37,13 @@ struct RouteResult : UMemory { // or document that other skeleton elements are ignored? UnicodeString precision; - RouteResult(MaybeStackVector 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 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)) {} }; /** diff --git a/icu4c/source/test/depstest/dependencies.txt b/icu4c/source/test/depstest/dependencies.txt index b43ceadd280..44e41e28c57 100644 --- a/icu4c/source/test/depstest/dependencies.txt +++ b/icu4c/source/test/depstest/dependencies.txt @@ -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 diff --git a/icu4c/source/test/intltest/measfmttest.cpp b/icu4c/source/test/intltest/measfmttest.cpp index b2b2877cca8..5dd85cf2124 100644 --- a/icu4c/source/test/intltest/measfmttest.cpp +++ b/icu4c/source/test/intltest/measfmttest.cpp @@ -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 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"); diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index 9ed6877b0a4..6659465c2f9 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -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); diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp index 0e4c976d401..895bef24d41 100644 --- a/icu4c/source/test/intltest/numbertest_api.cpp +++ b/icu4c/source/test/intltest/numbertest_api.cpp @@ -11,12 +11,13 @@ #include #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(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,