diff --git a/icu4c/source/i18n/complexunitsconverter.cpp b/icu4c/source/i18n/complexunitsconverter.cpp index 4a77b79a0e8..4890f748c33 100644 --- a/icu4c/source/i18n/complexunitsconverter.cpp +++ b/icu4c/source/i18n/complexunitsconverter.cpp @@ -6,15 +6,17 @@ #if !UCONFIG_NO_FORMATTING #include -#include #include "cmemory.h" #include "complexunitsconverter.h" #include "uassert.h" #include "unicode/fmtable.h" +#include "unicode/localpointer.h" +#include "unicode/measure.h" #include "unitconverter.h" U_NAMESPACE_BEGIN +namespace units { ComplexUnitsConverter::ComplexUnitsConverter(const MeasureUnit &inputUnit, const MeasureUnit &outputUnits, @@ -123,6 +125,7 @@ MaybeStackVector ComplexUnitsConverter::convert(double quantity, UError return result; } +} // namespace units U_NAMESPACE_END #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/complexunitsconverter.h b/icu4c/source/i18n/complexunitsconverter.h index 0fe76ba52b2..fbae2b1b6e3 100644 --- a/icu4c/source/i18n/complexunitsconverter.h +++ b/icu4c/source/i18n/complexunitsconverter.h @@ -8,14 +8,17 @@ #define __COMPLEXUNITSCONVERTER_H__ #include "cmemory.h" -#include "unicode/errorcode.h" #include "unicode/measunit.h" -#include "unicode/measure.h" #include "unitconverter.h" #include "unitsdata.h" U_NAMESPACE_BEGIN +// Forward declarations +class Measure; + +namespace units { + /** * Converts from single or compound unit to single, compound or mixed units. * For example, from `meter` to `foot+inch`. @@ -60,6 +63,7 @@ class U_I18N_API ComplexUnitsConverter { MaybeStackVector units_; }; +} // namespace units U_NAMESPACE_END #endif //__COMPLEXUNITSCONVERTER_H__ diff --git a/icu4c/source/i18n/formatted_string_builder.h b/icu4c/source/i18n/formatted_string_builder.h index 4567dc1d66b..4a886b97821 100644 --- a/icu4c/source/i18n/formatted_string_builder.h +++ b/icu4c/source/i18n/formatted_string_builder.h @@ -25,7 +25,7 @@ class FormattedValueStringBuilderImpl; * *
    *
  1. Efficient prepend as well as append. - *
  2. Keeps tracks of Fields in an efficient manner. + *
  3. Keeps track of Fields in an efficient manner. *
* * See also FormattedValueStringBuilderImpl. diff --git a/icu4c/source/i18n/i18n.vcxproj b/icu4c/source/i18n/i18n.vcxproj index ea139c08f4a..cf5ea8c0b37 100644 --- a/icu4c/source/i18n/i18n.vcxproj +++ b/icu4c/source/i18n/i18n.vcxproj @@ -196,6 +196,7 @@ + @@ -469,6 +470,7 @@ + diff --git a/icu4c/source/i18n/i18n_uwp.vcxproj b/icu4c/source/i18n/i18n_uwp.vcxproj index 30c98a05c82..2742efe6cfd 100644 --- a/icu4c/source/i18n/i18n_uwp.vcxproj +++ b/icu4c/source/i18n/i18n_uwp.vcxproj @@ -429,11 +429,13 @@ + + @@ -700,12 +702,14 @@ + + diff --git a/icu4c/source/i18n/number_decimalquantity.h b/icu4c/source/i18n/number_decimalquantity.h index d9b35c03366..bfc908f0843 100644 --- a/icu4c/source/i18n/number_decimalquantity.h +++ b/icu4c/source/i18n/number_decimalquantity.h @@ -20,7 +20,7 @@ namespace impl { class DecNum; /** - * An class for representing a number to be processed by the decimal formatting pipeline. Includes + * A class for representing a number to be processed by the decimal formatting pipeline. Includes * methods for rounding, plural rules, and decimal digit extraction. * *

By design, this is NOT IMMUTABLE and NOT THREAD SAFE. It is intended to be an intermediate @@ -217,7 +217,13 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory { DecimalQuantity &setToDouble(double n); - /** decNumber is similar to BigDecimal in Java. */ + /** + * Produces a DecimalQuantity that was parsed from a string by the decNumber + * C Library. + * + * decNumber is similar to BigDecimal in Java, and supports parsing strings + * such as "123.456621E+40". + */ DecimalQuantity &setToDecNumber(StringPiece n, UErrorCode& status); /** Internal method if the caller already has a DecNum. */ diff --git a/icu4c/source/i18n/number_fluent.cpp b/icu4c/source/i18n/number_fluent.cpp index da8cc7ed178..579f3062142 100644 --- a/icu4c/source/i18n/number_fluent.cpp +++ b/icu4c/source/i18n/number_fluent.cpp @@ -274,6 +274,20 @@ Derived NumberFormatterSettings::scale(const Scale& scale)&& { return move; } +template +Derived NumberFormatterSettings::usage(const StringPiece usage) const& { + Derived copy(*this); + copy.fMacros.usage.set(usage); + return copy; +} + +template +Derived NumberFormatterSettings::usage(const StringPiece usage)&& { + Derived move(std::move(*this)); + move.fMacros.usage.set(usage); + return move; +} + template Derived NumberFormatterSettings::padding(const Padder& padder) const& { Derived copy(*this); @@ -702,9 +716,9 @@ LocalizedNumberFormatter::formatDecimalQuantity(const DecimalQuantity& dq, UErro void LocalizedNumberFormatter::formatImpl(impl::UFormattedNumberData* results, UErrorCode& status) const { if (computeCompiled(status)) { - fCompiled->format(results->quantity, results->getStringRef(), status); + fCompiled->format(results, status); } else { - NumberFormatterImpl::formatStatic(fMacros, results->quantity, results->getStringRef(), status); + NumberFormatterImpl::formatStatic(fMacros, results, status); } if (U_FAILURE(status)) { return; diff --git a/icu4c/source/i18n/number_formatimpl.cpp b/icu4c/source/i18n/number_formatimpl.cpp index 875f71bcc82..c047a1ff997 100644 --- a/icu4c/source/i18n/number_formatimpl.cpp +++ b/icu4c/source/i18n/number_formatimpl.cpp @@ -32,13 +32,16 @@ NumberFormatterImpl::NumberFormatterImpl(const MacroProps& macros, UErrorCode& s : NumberFormatterImpl(macros, true, status) { } -int32_t NumberFormatterImpl::formatStatic(const MacroProps& macros, DecimalQuantity& inValue, - FormattedStringBuilder& outString, UErrorCode& status) { +int32_t NumberFormatterImpl::formatStatic(const MacroProps ¯os, UFormattedNumberData *results, + UErrorCode &status) { + DecimalQuantity &inValue = results->quantity; + FormattedStringBuilder &outString = results->getStringRef(); NumberFormatterImpl impl(macros, false, status); MicroProps& micros = impl.preProcessUnsafe(inValue, status); if (U_FAILURE(status)) { return 0; } int32_t length = writeNumber(micros, inValue, outString, 0, status); length += writeAffixes(micros, outString, 0, length, status); + results->outputUnit = std::move(micros.outputUnit); return length; } @@ -54,13 +57,15 @@ int32_t NumberFormatterImpl::getPrefixSuffixStatic(const MacroProps& macros, Sig // The "unsafe" method simply re-uses fMicros, eliminating the extra copy operation. // See MicroProps::processQuantity() for details. -int32_t NumberFormatterImpl::format(DecimalQuantity& inValue, FormattedStringBuilder& outString, - UErrorCode& status) const { +int32_t NumberFormatterImpl::format(UFormattedNumberData *results, UErrorCode &status) const { + DecimalQuantity &inValue = results->quantity; + FormattedStringBuilder &outString = results->getStringRef(); MicroProps micros; preProcess(inValue, micros, status); if (U_FAILURE(status)) { return 0; } int32_t length = writeNumber(micros, inValue, outString, 0, status); length += writeAffixes(micros, outString, 0, length, status); + results->outputUnit = std::move(micros.outputUnit); return length; } @@ -233,6 +238,19 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps& macros, bool safe, /// START POPULATING THE DEFAULT MICROPROPS AND BUILDING THE MICROPROPS GENERATOR /// ///////////////////////////////////////////////////////////////////////////////////// + // Unit Preferences and Conversions as our first step + if (macros.usage.isSet()) { + if (!isCldrUnit) { + // We only support "usage" when the input unit is a CLDR Unit. + status = U_ILLEGAL_ARGUMENT_ERROR; + return nullptr; + } + auto usagePrefsHandler = + new UsagePrefsHandler(macros.locale, macros.unit, macros.usage.fUsage, chain, status); + fUsagePrefsHandler.adoptInsteadAndCheckErrorCode(usagePrefsHandler, status); + chain = fUsagePrefsHandler.getAlias(); + } + // Multiplier if (macros.scale.isValid()) { fMicros.helpers.multiplier.setAndChain(macros.scale, chain); @@ -341,7 +359,8 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps& macros, bool safe, patternModifier->setSymbols(fMicros.symbols, currency, unitWidth, nullptr, status); } if (safe) { - fImmutablePatternModifier.adoptInstead(patternModifier->createImmutable(status)); + fImmutablePatternModifier.adoptInsteadAndCheckErrorCode(patternModifier->createImmutable(status), + status); } if (U_FAILURE(status)) { return nullptr; @@ -349,24 +368,26 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps& macros, bool safe, // Outer modifier (CLDR units and currency long names) if (isCldrUnit) { - fLongNameHandler.adoptInstead( - LongNameHandler::forMeasureUnit( - macros.locale, - macros.unit, - macros.perUnit, - unitWidth, - resolvePluralRules(macros.rules, macros.locale, status), - chain, - status)); - chain = fLongNameHandler.getAlias(); + if (macros.usage.isSet()) { + fLongNameMultiplexer.adoptInsteadAndCheckErrorCode( + LongNameMultiplexer::forMeasureUnits( + macros.locale, *fUsagePrefsHandler->getOutputUnits(), unitWidth, + resolvePluralRules(macros.rules, macros.locale, status), chain, status), + status); + chain = fLongNameMultiplexer.getAlias(); + } else { + fLongNameHandler.adoptInsteadAndCheckErrorCode(new LongNameHandler(), status); + LongNameHandler::forMeasureUnit(macros.locale, macros.unit, macros.perUnit, unitWidth, + resolvePluralRules(macros.rules, macros.locale, status), + chain, fLongNameHandler.getAlias(), status); + chain = fLongNameHandler.getAlias(); + } } else if (isCurrency && unitWidth == UNUM_UNIT_WIDTH_FULL_NAME) { - fLongNameHandler.adoptInstead( - LongNameHandler::forCurrencyLongNames( - macros.locale, - currency, - resolvePluralRules(macros.rules, macros.locale, status), - chain, - status)); + fLongNameHandler.adoptInsteadAndCheckErrorCode( + LongNameHandler::forCurrencyLongNames( + macros.locale, currency, resolvePluralRules(macros.rules, macros.locale, status), chain, + status), + status); chain = fLongNameHandler.getAlias(); } else { // No outer modifier required @@ -390,6 +411,9 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps& macros, bool safe, safe, chain, status); + if (U_FAILURE(status)) { + return nullptr; + } if (newCompactHandler == nullptr) { status = U_MEMORY_ALLOCATION_ERROR; return nullptr; diff --git a/icu4c/source/i18n/number_formatimpl.h b/icu4c/source/i18n/number_formatimpl.h index 084bc4a9d0b..062191a0327 100644 --- a/icu4c/source/i18n/number_formatimpl.h +++ b/icu4c/source/i18n/number_formatimpl.h @@ -10,11 +10,13 @@ #include "number_types.h" #include "formatted_string_builder.h" #include "number_patternstring.h" +#include "number_usageprefs.h" #include "number_utils.h" #include "number_patternmodifier.h" #include "number_longnames.h" #include "number_compact.h" #include "number_microprops.h" +#include "number_utypes.h" U_NAMESPACE_BEGIN namespace number { namespace impl { @@ -34,9 +36,8 @@ class NumberFormatterImpl : public UMemory { /** * Builds and evaluates an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once. */ - static int32_t - formatStatic(const MacroProps ¯os, DecimalQuantity &inValue, FormattedStringBuilder &outString, - UErrorCode &status); + static int32_t formatStatic(const MacroProps ¯os, UFormattedNumberData *results, + UErrorCode &status); /** * Prints only the prefix and suffix; used for DecimalFormat getters. @@ -51,7 +52,7 @@ class NumberFormatterImpl : public UMemory { /** * Evaluates the "safe" MicroPropsGenerator created by "fromMacros". */ - int32_t format(DecimalQuantity& inValue, FormattedStringBuilder& outString, UErrorCode& status) const; + int32_t format(UFormattedNumberData *results, UErrorCode &status) const; /** * Like format(), but saves the result into an output MicroProps without additional processing. @@ -82,7 +83,9 @@ class NumberFormatterImpl : public UMemory { int32_t end, UErrorCode& status); private: - // Head of the MicroPropsGenerator linked list: + // Head of the MicroPropsGenerator linked list. Subclasses' processQuantity + // methods process this list in a parent-first order, such that the last + // item added, which this points to, typically has its logic executed last. const MicroPropsGenerator *fMicroPropsGenerator = nullptr; // Tail of the list: @@ -90,13 +93,15 @@ class NumberFormatterImpl : public UMemory { // Other fields possibly used by the number formatting pipeline: // TODO: Convert more of these LocalPointers to value objects to reduce the number of news? + LocalPointer fUsagePrefsHandler; LocalPointer fSymbols; LocalPointer fRules; LocalPointer fPatternInfo; LocalPointer fScientificHandler; LocalPointer fPatternModifier; LocalPointer fImmutablePatternModifier; - LocalPointer fLongNameHandler; + LocalPointer fLongNameHandler; + LocalPointer fLongNameMultiplexer; LocalPointer fCompactHandler; // Value objects possibly used by the number formatting pipeline: diff --git a/icu4c/source/i18n/number_longnames.cpp b/icu4c/source/i18n/number_longnames.cpp index bb32d0381a5..7ace7fdc5ed 100644 --- a/icu4c/source/i18n/number_longnames.cpp +++ b/icu4c/source/i18n/number_longnames.cpp @@ -22,7 +22,21 @@ using namespace icu::number::impl; namespace { +/** + * Display Name (this format has no placeholder). + * + * Used as an index into the LongNameHandler::simpleFormats array. Units + * resources cover the normal set of PluralRules keys, as well as `dnam` and + * `per` forms. + */ constexpr int32_t DNAM_INDEX = StandardPlural::Form::COUNT; +/** + * "per" form (e.g. "{0} per day" is day's "per" form). + * + * Used as an index into the LongNameHandler::simpleFormats array. Units + * resources cover the normal set of PluralRules keys, as well as `dnam` and + * `per` forms. + */ constexpr int32_t PER_INDEX = StandardPlural::Form::COUNT + 1; constexpr int32_t ARRAY_LENGTH = StandardPlural::Form::COUNT + 2; @@ -87,6 +101,12 @@ 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. void getMeasureData(const Locale &locale, const MeasureUnit &unit, const UNumberUnitWidth &width, UnicodeString *outArray, UErrorCode &status) { PluralTableSink sink(outArray); @@ -184,14 +204,19 @@ UnicodeString getPerUnitFormat(const Locale& locale, const UNumberUnitWidth &wid } // namespace -LongNameHandler* -LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitRef, const MeasureUnit &perUnit, - const UNumberUnitWidth &width, const PluralRules *rules, - const MicroPropsGenerator *parent, UErrorCode &status) { +// 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; + } if (uprv_strlen(unitRef.getType()) == 0 || uprv_strlen(perUnit.getType()) == 0) { // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an error code. status = U_UNSUPPORTED_ERROR; - return nullptr; + return; } MeasureUnit unit = unitRef; @@ -203,59 +228,75 @@ LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitRef, c unit = resolved; } else { // No simplified form is available. - return forCompoundUnit(loc, unit, perUnit, width, rules, parent, status); + forCompoundUnit(loc, unit, perUnit, width, rules, parent, fillIn, status); + return; } } - auto* result = new LongNameHandler(rules, parent); - if (result == nullptr) { - status = U_MEMORY_ALLOCATION_ERROR; - return nullptr; - } UnicodeString simpleFormats[ARRAY_LENGTH]; getMeasureData(loc, unit, width, simpleFormats, status); - if (U_FAILURE(status)) { return result; } - result->simpleFormatsToModifiers(simpleFormats, {UFIELD_CATEGORY_NUMBER, UNUM_MEASURE_UNIT_FIELD}, status); - return result; + if (U_FAILURE(status)) { + return; + } + fillIn->rules = rules; + fillIn->parent = parent; + fillIn->simpleFormatsToModifiers(simpleFormats, {UFIELD_CATEGORY_NUMBER, UNUM_MEASURE_UNIT_FIELD}, + status); } -LongNameHandler* -LongNameHandler::forCompoundUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit, - const UNumberUnitWidth &width, const PluralRules *rules, - const MicroPropsGenerator *parent, UErrorCode &status) { - auto* result = new LongNameHandler(rules, parent); - if (result == nullptr) { - status = U_MEMORY_ALLOCATION_ERROR; - return nullptr; +// 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, + LongNameHandler *fillIn, UErrorCode &status) { + if (fillIn == nullptr) { + status = U_INTERNAL_PROGRAM_ERROR; + return; } UnicodeString primaryData[ARRAY_LENGTH]; getMeasureData(loc, unit, width, primaryData, status); - if (U_FAILURE(status)) { return result; } + if (U_FAILURE(status)) { + return; + } UnicodeString secondaryData[ARRAY_LENGTH]; getMeasureData(loc, perUnit, width, secondaryData, status); - if (U_FAILURE(status)) { return result; } + if (U_FAILURE(status)) { + return; + } UnicodeString perUnitFormat; if (!secondaryData[PER_INDEX].isBogus()) { perUnitFormat = secondaryData[PER_INDEX]; } else { UnicodeString rawPerUnitFormat = getPerUnitFormat(loc, width, status); - if (U_FAILURE(status)) { return result; } + if (U_FAILURE(status)) { + return; + } // rawPerUnitFormat is something like "{0}/{1}"; we need to substitute in the secondary unit. SimpleFormatter compiled(rawPerUnitFormat, 2, 2, status); - if (U_FAILURE(status)) { return result; } + if (U_FAILURE(status)) { + return; + } UnicodeString secondaryFormat = getWithPlural(secondaryData, StandardPlural::Form::ONE, status); - if (U_FAILURE(status)) { return result; } + if (U_FAILURE(status)) { + return; + } // Some "one" pattern may not contain "{0}". For example in "ar" or "ne" locale. SimpleFormatter secondaryCompiled(secondaryFormat, 0, 1, status); - if (U_FAILURE(status)) { return result; } + if (U_FAILURE(status)) { + return; + } UnicodeString secondaryString = secondaryCompiled.getTextWithNoArguments().trim(); // TODO: Why does UnicodeString need to be explicit in the following line? compiled.format(UnicodeString(u"{0}"), secondaryString, perUnitFormat, status); - if (U_FAILURE(status)) { return result; } + if (U_FAILURE(status)) { + return; + } } - result->multiSimpleFormatsToModifiers(primaryData, perUnitFormat, {UFIELD_CATEGORY_NUMBER, UNUM_MEASURE_UNIT_FIELD}, status); - return result; + fillIn->rules = rules; + fillIn->parent = parent; + fillIn->multiSimpleFormatsToModifiers(primaryData, perUnitFormat, + {UFIELD_CATEGORY_NUMBER, UNUM_MEASURE_UNIT_FIELD}, status); } UnicodeString LongNameHandler::getUnitDisplayName( @@ -338,7 +379,9 @@ void LongNameHandler::multiSimpleFormatsToModifiers(const UnicodeString *leadFor void LongNameHandler::processQuantity(DecimalQuantity &quantity, MicroProps µs, UErrorCode &status) const { - parent->processQuantity(quantity, micros, status); + if (parent != NULL) { + parent->processQuantity(quantity, micros, status); + } StandardPlural::Form pluralForm = utils::getPluralSafe(micros.rounder, rules, quantity, status); micros.modOuter = &fModifiers[pluralForm]; } @@ -347,4 +390,49 @@ const Modifier* LongNameHandler::getModifier(Signum /*signum*/, StandardPlural:: return &fModifiers[plural]; } +LongNameMultiplexer * +LongNameMultiplexer::forMeasureUnits(const Locale &loc, const MaybeStackVector &units, + const UNumberUnitWidth &width, const PluralRules *rules, + const MicroPropsGenerator *parent, UErrorCode &status) { + LocalPointer result(new LongNameMultiplexer(parent), status); + if (U_FAILURE(status)) { + return nullptr; + } + U_ASSERT(units.length() > 0); + 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); + if (U_FAILURE(status)) { + return nullptr; + } + } + return result.orphan(); +} + +void LongNameMultiplexer::processQuantity(DecimalQuantity &quantity, MicroProps µs, + UErrorCode &status) const { + // We call parent->processQuantity() from the Multiplexer, instead of + // letting LongNameHandler handle it: we don't know which LongNameHandler to + // call until we've called the parent! + fParent->processQuantity(quantity, micros, status); + + // Call the correct LongNameHandler based on outputUnit + for (int i = 0; i < fLongNameHandlers.length(); i++) { + if (fMeasureUnits[i] == micros.outputUnit) { + fLongNameHandlers[i]->processQuantity(quantity, micros, status); + return; + } + } + // We shouldn't receive any outputUnit for which we haven't already got a + // LongNameHandler: + status = U_INTERNAL_PROGRAM_ERROR; +} + #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/number_longnames.h b/icu4c/source/i18n/number_longnames.h index a19425aa268..db08c119cd0 100644 --- a/icu4c/source/i18n/number_longnames.h +++ b/icu4c/source/i18n/number_longnames.h @@ -7,6 +7,7 @@ #ifndef __NUMBER_LONGNAMES_H__ #define __NUMBER_LONGNAMES_H__ +#include "cmemory.h" #include "unicode/uversion.h" #include "number_utils.h" #include "number_modifiers.h" @@ -33,10 +34,10 @@ class LongNameHandler : public MicroPropsGenerator, public ModifierStore, public forCurrencyLongNames(const Locale &loc, const CurrencyUnit ¤cy, const PluralRules *rules, const MicroPropsGenerator *parent, UErrorCode &status); - static LongNameHandler* - forMeasureUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit, - const UNumberUnitWidth &width, const PluralRules *rules, - const MicroPropsGenerator *parent, UErrorCode &status); + 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); void processQuantity(DecimalQuantity &quantity, MicroProps µs, UErrorCode &status) const U_OVERRIDE; @@ -45,22 +46,63 @@ class LongNameHandler : public MicroPropsGenerator, public ModifierStore, public private: SimpleModifier fModifiers[StandardPlural::Form::COUNT]; + // Not owned const PluralRules *rules; + // Not owned const MicroPropsGenerator *parent; LongNameHandler(const PluralRules *rules, const MicroPropsGenerator *parent) - : rules(rules), parent(parent) {} + : rules(rules), parent(parent) { + } - static LongNameHandler* - forCompoundUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit, - const UNumberUnitWidth &width, const PluralRules *rules, - const MicroPropsGenerator *parent, UErrorCode &status); + LongNameHandler() : rules(nullptr), parent(nullptr) { + } + + friend class MemoryPool; // To enable emplaceBack(); + friend class NumberFormatterImpl; + + 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); void simpleFormatsToModifiers(const UnicodeString *simpleFormats, Field field, UErrorCode &status); void multiSimpleFormatsToModifiers(const UnicodeString *leadFormats, UnicodeString trailFormat, Field field, UErrorCode &status); }; +const int MAX_PREFS_COUNT = 10; + +/** + * A MicroPropsGenerator that multiplexes between different LongNameHandlers, + * depending on the outputUnit (micros.helpers.outputUnit should be set earlier + * in the chain). + */ +class LongNameMultiplexer : public MicroPropsGenerator, public UMemory { + public: + // FIXME: docstring? + static LongNameMultiplexer *forMeasureUnits(const Locale &loc, + const MaybeStackVector &units, + const UNumberUnitWidth &width, const PluralRules *rules, + const MicroPropsGenerator *parent, UErrorCode &status); + + void processQuantity(DecimalQuantity &quantity, MicroProps µs, + UErrorCode &status) const U_OVERRIDE; + + private: + /** + * Because we only know which LongNameHandler we wish to call after calling + * earlier MicroPropsGenerators in the chain, LongNameMultiplexer keeps the + * parent link, while the LongNameHandlers are given no parents. + */ + MaybeStackVector fLongNameHandlers; + LocalArray fMeasureUnits; + const MicroPropsGenerator *fParent; + + LongNameMultiplexer(const MicroPropsGenerator *parent) : fParent(parent) { + } +}; + } // namespace impl } // namespace number U_NAMESPACE_END diff --git a/icu4c/source/i18n/number_microprops.h b/icu4c/source/i18n/number_microprops.h index 56512f5e6f9..cb68c41a744 100644 --- a/icu4c/source/i18n/number_microprops.h +++ b/icu4c/source/i18n/number_microprops.h @@ -22,6 +22,11 @@ U_NAMESPACE_BEGIN namespace number { namespace impl { +// 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 +// * MicroPropsGenerator::processQuantity() just ensures that the output +// * `micros` is correctly initialized. struct MicroProps : public MicroPropsGenerator { // NOTE: All of these fields are properly initialized in NumberFormatterImpl. @@ -49,6 +54,8 @@ struct MicroProps : public MicroPropsGenerator { MultiplierFormatHandler multiplier; } helpers; + // The MeasureUnit with which the output measurement is represented. + MeasureUnit outputUnit; MicroProps() = default; @@ -56,7 +63,23 @@ struct MicroProps : public MicroPropsGenerator { MicroProps& operator=(const MicroProps& other) = default; - void processQuantity(DecimalQuantity&, MicroProps& micros, UErrorCode& status) const U_OVERRIDE { + /** + * As MicroProps is the "base instance", this implementation of + * MicroPropsGenerator::processQuantity() just ensures that the output + * `micros` is correctly initialized. + * + * For the "safe" invocation of this function, micros must not be *this, + * such that a copy of the base instance is made. For the "unsafe" path, + * this function can be used only once, because the base MicroProps instance + * will be modified and thus not be available for re-use. + * + * @param quantity The quantity for consideration and optional mutation. + * @param micros The MicroProps instance to populate. If this parameter is + * not already `*this`, it will be overwritten with a copy of `*this`. + */ + void processQuantity(DecimalQuantity &quantity, MicroProps µs, + UErrorCode &status) const U_OVERRIDE { + (void) quantity; (void) status; if (this == µs) { // Unsafe path: no need to perform a copy. @@ -65,6 +88,7 @@ struct MicroProps : public MicroPropsGenerator { U_ASSERT(exhausted); } else { // Safe path: copy self into the output micros. + U_ASSERT(!exhausted); micros = *this; } } diff --git a/icu4c/source/i18n/number_output.cpp b/icu4c/source/i18n/number_output.cpp index 40192a9225b..9b1a11dceb0 100644 --- a/icu4c/source/i18n/number_output.cpp +++ b/icu4c/source/i18n/number_output.cpp @@ -5,6 +5,7 @@ #if !UCONFIG_NO_FORMATTING +#include "unicode/measunit.h" #include "unicode/numberformatter.h" #include "number_utypes.h" #include "util.h" @@ -32,6 +33,11 @@ void FormattedNumber::getAllFieldPositionsImpl(FieldPositionIteratorHandler& fpi fData->getAllFieldPositions(fpih, status); } +MeasureUnit FormattedNumber::getOutputUnit(UErrorCode& status) const { + UPRV_FORMATTED_VALUE_METHOD_GUARD(MeasureUnit()) + return fData->outputUnit; +} + void FormattedNumber::getDecimalQuantity(impl::DecimalQuantity& output, UErrorCode& status) const { UPRV_FORMATTED_VALUE_METHOD_GUARD(UPRV_NOARG) output = fData->quantity; diff --git a/icu4c/source/i18n/number_skeletons.cpp b/icu4c/source/i18n/number_skeletons.cpp index dd163f85634..7c2a75902e6 100644 --- a/icu4c/source/i18n/number_skeletons.cpp +++ b/icu4c/source/i18n/number_skeletons.cpp @@ -99,6 +99,7 @@ void U_CALLCONV initNumberSkeletons(UErrorCode& status) { b.add(u"measure-unit", STEM_MEASURE_UNIT, status); b.add(u"per-measure-unit", STEM_PER_MEASURE_UNIT, status); b.add(u"unit", STEM_UNIT, status); + b.add(u"usage", STEM_UNIT_USAGE, status); b.add(u"currency", STEM_CURRENCY, status); b.add(u"integer-width", STEM_INTEGER_WIDTH, status); b.add(u"numbering-system", STEM_NUMBERING_SYSTEM, status); @@ -559,6 +560,7 @@ MacroProps skeleton::parseSkeleton( case STATE_MEASURE_UNIT: case STATE_PER_MEASURE_UNIT: case STATE_IDENTIFIER_UNIT: + case STATE_UNIT_USAGE: case STATE_CURRENCY_UNIT: case STATE_INTEGER_WIDTH: case STATE_NUMBERING_SYSTEM: @@ -716,7 +718,7 @@ skeleton::parseStem(const StringSegment& segment, const UCharsTrie& stemTrie, Se macros.decimal = stem_to_object::decimalSeparatorDisplay(stem); return STATE_NULL; - // Stems requiring an option: + // Stems requiring an option: case STEM_PRECISION_INCREMENT: CHECK_NULL(seen, precision, status); @@ -735,6 +737,10 @@ skeleton::parseStem(const StringSegment& segment, const UCharsTrie& stemTrie, Se CHECK_NULL(seen, perUnit, status); return STATE_IDENTIFIER_UNIT; + case STEM_UNIT_USAGE: + CHECK_NULL(seen, usage, status); + return STATE_UNIT_USAGE; + case STEM_CURRENCY: CHECK_NULL(seen, unit, status); return STATE_CURRENCY_UNIT; @@ -774,6 +780,9 @@ ParseState skeleton::parseOption(ParseState stem, const StringSegment& segment, case STATE_IDENTIFIER_UNIT: blueprint_helpers::parseIdentifierUnitOption(segment, macros, status); return STATE_NULL; + case STATE_UNIT_USAGE: + blueprint_helpers::parseUnitUsageOption(segment, macros, status); + return STATE_NULL; case STATE_INCREMENT_PRECISION: blueprint_helpers::parseIncrementOption(segment, macros, status); return STATE_NULL; @@ -848,6 +857,10 @@ void GeneratorHelpers::generateSkeleton(const MacroProps& macros, UnicodeString& sb.append(u' '); } if (U_FAILURE(status)) { return; } + if (GeneratorHelpers::usage(macros, sb, status)) { + sb.append(u' '); + } + if (U_FAILURE(status)) { return; } if (GeneratorHelpers::precision(macros, sb, status)) { sb.append(u' '); } @@ -1068,6 +1081,17 @@ void blueprint_helpers::parseIdentifierUnitOption(const StringSegment& segment, } } +void blueprint_helpers::parseUnitUsageOption(const StringSegment &segment, MacroProps ¯os, + UErrorCode &status) { + // Need to do char <-> UChar conversion... + U_ASSERT(U_SUCCESS(status)); + CharString buffer; + SKELETON_UCHAR_TO_CHAR(buffer, segment.toTempUnicodeString(), 0, segment.length(), status); + macros.usage.set(buffer.toStringPiece()); + // We do not do any validation of the usage string: it depends on the + // unitPreferenceData in the units resources. +} + void blueprint_helpers::parseFractionStem(const StringSegment& segment, MacroProps& macros, UErrorCode& status) { U_ASSERT(segment.charAt(0) == u'.'); @@ -1549,6 +1573,15 @@ bool GeneratorHelpers::perUnit(const MacroProps& macros, UnicodeString& sb, UErr } } +bool GeneratorHelpers::usage(const MacroProps& macros, UnicodeString& sb, UErrorCode&) { + if (macros.usage.fLength > 0) { + sb.append(u"usage/", -1); + sb.append(UnicodeString(macros.usage.fUsage, -1, US_INV)); + return true; + } + return false; +} + bool GeneratorHelpers::precision(const MacroProps& macros, UnicodeString& sb, UErrorCode& status) { if (macros.precision.fType == Precision::RND_NONE) { sb.append(u"precision-unlimited", -1); diff --git a/icu4c/source/i18n/number_skeletons.h b/icu4c/source/i18n/number_skeletons.h index dd3813e7a65..313c7ac54c2 100644 --- a/icu4c/source/i18n/number_skeletons.h +++ b/icu4c/source/i18n/number_skeletons.h @@ -22,10 +22,12 @@ struct SeenMacroProps; // namespace for enums and entrypoint functions namespace skeleton { -/////////////////////////////////////////////////////////////////////////////////////// -// NOTE: For an example of how to add a new stem to the number skeleton parser, see: // -// http://bugs.icu-project.org/trac/changeset/41193 // -/////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////// +// NOTE: For examples of how to add a new stem to the number skeleton parser, see: // +// https://github.com/unicode-org/icu/commit/a2a7982216b2348070dc71093775ac7195793d73 // +// and // +// https://github.com/unicode-org/icu/commit/6fe86f3934a8a5701034f648a8f7c5087e84aa28 // +//////////////////////////////////////////////////////////////////////////////////////// /** * While parsing a skeleton, this enum records what type of option we expect to find next. @@ -47,6 +49,7 @@ enum ParseState { STATE_MEASURE_UNIT, STATE_PER_MEASURE_UNIT, STATE_IDENTIFIER_UNIT, + STATE_UNIT_USAGE, STATE_CURRENCY_UNIT, STATE_INTEGER_WIDTH, STATE_NUMBERING_SYSTEM, @@ -114,6 +117,7 @@ enum StemEnum { STEM_MEASURE_UNIT, STEM_PER_MEASURE_UNIT, STEM_UNIT, + STEM_UNIT_USAGE, STEM_CURRENCY, STEM_INTEGER_WIDTH, STEM_NUMBERING_SYSTEM, @@ -244,6 +248,8 @@ void parseMeasurePerUnitOption(const StringSegment& segment, MacroProps& macros, void parseIdentifierUnitOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status); +void parseUnitUsageOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status); + void parseFractionStem(const StringSegment& segment, MacroProps& macros, UErrorCode& status); void generateFractionStem(int32_t minFrac, int32_t maxFrac, UnicodeString& sb, UErrorCode& status); @@ -306,6 +312,8 @@ class GeneratorHelpers { static bool perUnit(const MacroProps& macros, UnicodeString& sb, UErrorCode& status); + static bool usage(const MacroProps& macros, UnicodeString& sb, UErrorCode& status); + static bool precision(const MacroProps& macros, UnicodeString& sb, UErrorCode& status); static bool roundingMode(const MacroProps& macros, UnicodeString& sb, UErrorCode& status); @@ -334,6 +342,7 @@ struct SeenMacroProps { bool notation = false; bool unit = false; bool perUnit = false; + bool usage = false; bool precision = false; bool roundingMode = false; bool grouper = false; diff --git a/icu4c/source/i18n/number_types.h b/icu4c/source/i18n/number_types.h index 5c2b8cf8b5d..8180fe55317 100644 --- a/icu4c/source/i18n/number_types.h +++ b/icu4c/source/i18n/number_types.h @@ -246,16 +246,17 @@ class U_I18N_API ModifierStore { * itself. The {@link #processQuantity} method performs the final step in the number processing pipeline: it uses the * quantity to generate a finalized {@link MicroProps}, which can be used to render the number to output. * - *

* In other words, this interface is used for the parts of number processing that are quantity-dependent. * - *

* In order to allow for multiple different objects to all mutate the same MicroProps, a "chain" of MicroPropsGenerators * are linked together, and each one is responsible for manipulating a certain quantity-dependent part of the * MicroProps. At the tail of the linked list is a base instance of {@link MicroProps} with properties that are not * quantity-dependent. Each element in the linked list calls {@link #processQuantity} on its "parent", then does its * work, and then returns the result. * + * This chain of MicroPropsGenerators is typically constructed by NumberFormatterImpl::macrosToMicroGenerator() when + * constructing a NumberFormatter. + * * Exported as U_I18N_API because it is a base class for other exported types * */ @@ -264,13 +265,12 @@ class U_I18N_API MicroPropsGenerator { virtual ~MicroPropsGenerator(); /** - * Considers the given {@link DecimalQuantity}, optionally mutates it, and returns a {@link MicroProps}. + * Considers the given {@link DecimalQuantity}, optionally mutates it, and + * populates a {@link MicroProps} instance. * - * @param quantity - * The quantity for consideration and optional mutation. - * @param micros - * The MicroProps instance to populate. - * @return A MicroProps instance resolved for the quantity. + * @param quantity The quantity for consideration and optional mutation. + * @param micros The MicroProps instance to populate. It will be modified as + * needed for the given quantity. */ virtual void processQuantity(DecimalQuantity& quantity, MicroProps& micros, UErrorCode& status) const = 0; diff --git a/icu4c/source/i18n/number_usageprefs.cpp b/icu4c/source/i18n/number_usageprefs.cpp new file mode 100644 index 00000000000..3dcc8ef7f51 --- /dev/null +++ b/icu4c/source/i18n/number_usageprefs.cpp @@ -0,0 +1,124 @@ +// © 2020 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#include "unicode/utypes.h" + +#if !UCONFIG_NO_FORMATTING + +#include "number_usageprefs.h" +#include "cstring.h" +#include "number_decimalquantity.h" +#include "number_microprops.h" +#include "number_roundingutils.h" +#include "unicode/char16ptr.h" +#include "unicode/currunit.h" +#include "unicode/fmtable.h" +#include "unicode/measure.h" +#include "unicode/numberformatter.h" +#include "unicode/platform.h" +#include "unicode/unum.h" +#include "unicode/urename.h" + +using namespace icu::number; +using namespace icu::number::impl; + +// Copy constructor +Usage::Usage(const Usage &other) : fUsage(nullptr), fLength(other.fLength), fError(other.fError) { + if (other.fUsage != nullptr) { + fUsage = (char *)uprv_malloc(fLength + 1); + uprv_strncpy(fUsage, other.fUsage, fLength + 1); + } +} + +// Copy assignment operator +Usage &Usage::operator=(const Usage &other) { + fLength = other.fLength; + if (other.fUsage != nullptr) { + fUsage = (char *)uprv_malloc(fLength + 1); + uprv_strncpy(fUsage, other.fUsage, fLength + 1); + } + fError = other.fError; + 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); +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; +} + +// Move assignment operator +Usage &Usage::operator=(Usage &&src) U_NOEXCEPT { + if (this == &src) { + return *this; + } + if (fUsage != nullptr) { + uprv_free(fUsage); + } + fUsage = src.fUsage; + fLength = src.fLength; + fError = src.fError; + // Take ownership away from src if necessary + src.fUsage = nullptr; + return *this; +} + +Usage::~Usage() { + if (fUsage != nullptr) { + uprv_free(fUsage); + fUsage = nullptr; + } +} + +void Usage::set(StringPiece value) { + if (fUsage != nullptr) { + uprv_free(fUsage); + fUsage = nullptr; + } + fLength = value.length(); + fUsage = (char *)uprv_malloc(fLength + 1); + uprv_strncpy(fUsage, value.data(), fLength); + fUsage[fLength] = 0; +} + +UsagePrefsHandler::UsagePrefsHandler(const Locale &locale, + const MeasureUnit inputUnit, + const StringPiece usage, + const MicroPropsGenerator *parent, + UErrorCode &status) + : fUnitsRouter(inputUnit, StringPiece(locale.getCountry()), usage, status), + fParent(parent) { +} + +void UsagePrefsHandler::processQuantity(DecimalQuantity &quantity, MicroProps µs, + UErrorCode &status) const { + fParent->processQuantity(quantity, micros, status); + if (U_FAILURE(status)) { + return; + } + + quantity.roundToInfinity(); // Enables toDouble + auto routed = fUnitsRouter.route(quantity.toDouble(), status); + micros.outputUnit = routed[0]->getUnit(); + quantity.setToDouble(routed[0]->getNumber().getDouble()); + + // TODO(units): here we are always overriding Precision. (1) get precision + // from fUnitsRouter, (2) ensure we use the UnitPreference skeleton's + // precision only when there isn't an explicit override we prefer to use. + // This needs to be handled within + // NumberFormatterImpl::macrosToMicroGenerator in number_formatimpl.cpp + Precision precision = Precision::integer().withMinDigits(2); + UNumberFormatRoundingMode roundingMode; + // Temporary until ICU 64? + roundingMode = precision.fRoundingMode; + CurrencyUnit currency(u"", status); + micros.rounder = {precision, roundingMode, currency, status}; + if (U_FAILURE(status)) { + return; + } +} + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/number_usageprefs.h b/icu4c/source/i18n/number_usageprefs.h new file mode 100644 index 00000000000..fa56c1ea5c7 --- /dev/null +++ b/icu4c/source/i18n/number_usageprefs.h @@ -0,0 +1,62 @@ +// © 2020 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#include "unicode/utypes.h" + +#if !UCONFIG_NO_FORMATTING +#ifndef __NUMBER_USAGEPREFS_H__ +#define __NUMBER_USAGEPREFS_H__ + +#include "cmemory.h" +#include "number_types.h" +#include "unicode/locid.h" +#include "unicode/measunit.h" +#include "unicode/stringpiece.h" +#include "unicode/uobject.h" +#include "unitsrouter.h" + +U_NAMESPACE_BEGIN +namespace number { +namespace impl { + +using ::icu::units::UnitsRouter; + +/** + * A MicroPropsGenerator which uses UnitsRouter to produce output converted to a + * MeasureUnit appropriate for a particular localized usage: see + * NumberFormatterSettings::usage(). + */ +class U_I18N_API UsagePrefsHandler : public MicroPropsGenerator, public UMemory { + public: + UsagePrefsHandler(const Locale &locale, const MeasureUnit inputUnit, const StringPiece usage, + const MicroPropsGenerator *parent, UErrorCode &status); + + /** + * Obtains the appropriate output value, MeasurementUnit and + * rounding/precision behaviour from the UnitsRouter. + */ + void processQuantity(DecimalQuantity &quantity, MicroProps µs, + UErrorCode &status) const U_OVERRIDE; + + /** + * Returns the list of possible output units, i.e. the full set of + * preferences, for the localized, usage-specific unit preferences. + * + * The returned pointer should be valid for the lifetime of the + * UsagePrefsHandler instance. + */ + const MaybeStackVector *getOutputUnits() const { + return fUnitsRouter.getOutputUnits(); + } + + private: + UnitsRouter fUnitsRouter; + const MicroPropsGenerator *fParent; +}; + +} // namespace impl +} // namespace number +U_NAMESPACE_END + +#endif // __NUMBER_USAGEPREFS_H__ +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/number_utypes.h b/icu4c/source/i18n/number_utypes.h index 7a1b7a4e80a..d97eadc5cdb 100644 --- a/icu4c/source/i18n/number_utypes.h +++ b/icu4c/source/i18n/number_utypes.h @@ -28,9 +28,6 @@ const DecimalQuantity* validateUFormattedNumberToDecimalQuantity( * This struct is held internally by the C++ version FormattedNumber since the member types are not * declared in the public header file. * - * The DecimalQuantity is not currently being used by FormattedNumber, but at some point it could be used - * to add a toDecNumber() or similar method. - * * Exported as U_I18N_API for tests */ class U_I18N_API UFormattedNumberData : public FormattedValueStringBuilderImpl { @@ -38,7 +35,13 @@ public: UFormattedNumberData() : FormattedValueStringBuilderImpl(kUndefinedField) {} virtual ~UFormattedNumberData(); + // The formatted quantity. DecimalQuantity quantity; + + // The output unit for the formatted quantity. + // TODO(units,hugovdm): populate this correctly for the general case - it's + // currently only implemented for the .usage() use case. + MeasureUnit outputUnit; }; diff --git a/icu4c/source/i18n/sources.txt b/icu4c/source/i18n/sources.txt index ce2486bf624..8c989edccc8 100644 --- a/icu4c/source/i18n/sources.txt +++ b/icu4c/source/i18n/sources.txt @@ -122,6 +122,7 @@ number_patternstring.cpp number_rounding.cpp number_scientific.cpp number_skeletons.cpp +number_usageprefs.cpp number_utils.cpp numfmt.cpp numparse_affixes.cpp diff --git a/icu4c/source/i18n/unicode/numberformatter.h b/icu4c/source/i18n/unicode/numberformatter.h index 0fb16c26901..bc4326fe928 100644 --- a/icu4c/source/i18n/unicode/numberformatter.h +++ b/icu4c/source/i18n/unicode/numberformatter.h @@ -158,6 +158,7 @@ struct UFormattedNumberImpl; class MutablePatternModifier; class ImmutablePatternModifier; struct DecimalFormatWarehouse; +class UsagePrefsHandler; /** * Used for NumberRangeFormatter and implemented in numrange_fluent.cpp. @@ -767,6 +768,11 @@ class U_I18N_API Precision : public UMemory { // To allow access to the skeleton generation code: friend class impl::GeneratorHelpers; + + // TODO(units): revisit when UnitsRouter is changed: do we still need this + // once Precision is returned by UnitsRouter? For now, we allow access to + // Precision constructor from UsagePrefsHandler: + friend class impl::UsagePrefsHandler; }; /** @@ -1127,6 +1133,61 @@ class U_I18N_API Scale : public UMemory { namespace impl { +// Do not enclose entire Usage with #ifndef U_HIDE_INTERNAL_API, needed for a protected field +/** + * Manages NumberFormatterSettings::usage()'s char* instance on the heap. + * @internal + */ +class U_I18N_API Usage : public UMemory { + +#ifndef U_HIDE_INTERNAL_API + + public: + /** @internal */ + Usage(const Usage& other); + + /** @internal */ + Usage& operator=(const Usage& other); + + /** @internal */ + Usage(Usage &&src) U_NOEXCEPT; + + /** @internal */ + Usage& operator=(Usage&& src) U_NOEXCEPT; + + /** @internal */ + ~Usage(); + + /** @internal */ + int16_t length() const { return fLength; } + + /** @internal + * Makes a copy of value. + */ + void set(StringPiece value); + + /** @internal */ + bool isSet() const { return fLength > 0; } + + private: + char *fUsage; + int16_t fLength; + UErrorCode fError; + + Usage() : fUsage(nullptr), fLength(0), fError(U_ZERO_ERROR) {} + + // Allow NumberFormatterImpl to access fUsage. + friend class impl::NumberFormatterImpl; + + // Allow skeleton generation code to access private members. + friend class impl::GeneratorHelpers; + + // Allow MacroProps/MicroProps to initialize empty instances. + friend struct impl::MacroProps; + +#endif // U_HIDE_INTERNAL_API +}; + // Do not enclose entire SymbolsWrapper with #ifndef U_HIDE_INTERNAL_API, needed for a protected field /** @internal */ class U_I18N_API SymbolsWrapper : public UMemory { @@ -1410,6 +1471,9 @@ struct U_I18N_API MacroProps : public UMemory { /** @internal */ Scale scale; // = Scale(); (benign value) + /** @internal */ + Usage usage; // = Usage(); (no usage) + /** @internal */ const AffixPatternProvider* affixProvider = nullptr; // no ownership @@ -2073,6 +2137,13 @@ class U_I18N_API NumberFormatterSettings { * Setting usage to an empty string clears the usage (disables usage-based * localized formatting). * + * Setting a usage string but not a correct input unit will result in an + * U_ILLEGAL_ARGUMENT_ERROR. + * + * When using usage, specifying rounding or precision is unnecessary. + * Specifying a precision in some manner will override the default + * formatting. + * * @param usage A `usage` parameter from the units resource. See the * unitPreferenceData in *source/data/misc/units.txt*, generated from * `unitPreferenceData` in [CLDR's @@ -2575,7 +2646,7 @@ class U_I18N_API FormattedNumber : public UMemory, public FormattedValue { * The output unit is dependent upon the localized preferences for the usage * specified via NumberFormatterSettings::usage(), and may be a unit with * UMEASURE_UNIT_MIXED unit complexity (MeasureUnit::getComplexity()), such - * as "foot+inch" or "hour+minute+second". + * as "foot-and-inch" or "hour-and-minute-and-second". * * @return `MeasureUnit`. * @draft ICU 68 diff --git a/icu4c/source/i18n/unitconverter.cpp b/icu4c/source/i18n/unitconverter.cpp index 70adbd7e88f..97a5e3d5a73 100644 --- a/icu4c/source/i18n/unitconverter.cpp +++ b/icu4c/source/i18n/unitconverter.cpp @@ -5,146 +5,124 @@ #if !UCONFIG_NO_FORMATTING -#include - #include "charstr.h" -#include "double-conversion.h" +#include "cmemory.h" +#include "double-conversion-string-to-double.h" #include "measunit_impl.h" -#include "unicode/errorcode.h" +#include "uassert.h" +#include "unicode/localpointer.h" #include "unicode/measunit.h" #include "unicode/stringpiece.h" #include "unitconverter.h" +#include +#include +#include +#include U_NAMESPACE_BEGIN +namespace units { + +void U_I18N_API Factor::multiplyBy(const Factor &rhs) { + factorNum *= rhs.factorNum; + factorDen *= rhs.factorDen; + for (int i = 0; i < CONSTANTS_COUNT; i++) { + constants[i] += rhs.constants[i]; + } + + // NOTE + // We need the offset when the source and the target are simple units. e.g. the source is + // celsius and the target is Fahrenheit. Therefore, we just keep the value using `std::max`. + offset = std::max(rhs.offset, offset); +} + +void U_I18N_API Factor::divideBy(const Factor &rhs) { + factorNum *= rhs.factorDen; + factorDen *= rhs.factorNum; + for (int i = 0; i < CONSTANTS_COUNT; i++) { + constants[i] -= rhs.constants[i]; + } + + // NOTE + // We need the offset when the source and the target are simple units. e.g. the source is + // celsius and the target is Fahrenheit. Therefore, we just keep the value using `std::max`. + offset = std::max(rhs.offset, offset); +} + +void U_I18N_API Factor::power(int32_t power) { + // multiply all the constant by the power. + for (int i = 0; i < CONSTANTS_COUNT; i++) { + constants[i] *= power; + } + + bool shouldFlip = power < 0; // This means that after applying the absolute power, we should flip + // the Numerator and Denominator. + + factorNum = std::pow(factorNum, std::abs(power)); + factorDen = std::pow(factorDen, std::abs(power)); + + if (shouldFlip) { + // Flip Numerator and Denominator. + std::swap(factorNum, factorDen); + } +} + +void U_I18N_API Factor::flip() { + std::swap(factorNum, factorDen); + + for (int i = 0; i < CONSTANTS_COUNT; i++) { + constants[i] *= -1; + } +} + +void U_I18N_API Factor::applySiPrefix(UMeasureSIPrefix siPrefix) { + if (siPrefix == UMeasureSIPrefix::UMEASURE_SI_PREFIX_ONE) return; // No need to do anything + + double siApplied = std::pow(10.0, std::abs(siPrefix)); + + if (siPrefix < 0) { + factorDen *= siApplied; + return; + } + + factorNum *= siApplied; +} + +void U_I18N_API Factor::substituteConstants() { + // These values are a hard-coded subset of unitConstants in the units + // resources file. A unit test checks that all constants in the resource + // file are at least recognised by the code. Derived constants' values or + // hard-coded derivations are not checked. + // double constantsValues[CONSTANTS_COUNT]; + static const double constantsValues[CONSTANTS_COUNT] = { + [CONSTANT_FT2M] = 0.3048, // + [CONSTANT_PI] = 411557987.0 / 131002976.0, // + [CONSTANT_GRAVITY] = 9.80665, // + [CONSTANT_G] = 6.67408E-11, // + [CONSTANT_GAL_IMP2M3] = 0.00454609, // + [CONSTANT_LB2KG] = 0.45359237, // + }; + + for (int i = 0; i < CONSTANTS_COUNT; i++) { + if (this->constants[i] == 0) { + continue; + } + + auto absPower = std::abs(this->constants[i]); + Signum powerSig = this->constants[i] < 0 ? Signum::NEGATIVE : Signum::POSITIVE; + double absConstantValue = std::pow(constantsValues[i], absPower); + + if (powerSig == Signum::NEGATIVE) { + this->factorDen *= absConstantValue; + } else { + this->factorNum *= absConstantValue; + } + + this->constants[i] = 0; + } +} namespace { -/* Internal Structure */ - -enum Constants { - CONSTANT_FT2M, // ft2m stands for foot to meter. - CONSTANT_PI, // PI - CONSTANT_GRAVITY, // Gravity - CONSTANT_G, - CONSTANT_GAL_IMP2M3, // Gallon imp to m3 - CONSTANT_LB2KG, // Pound to Kilogram - - // Must be the last element. - CONSTANTS_COUNT -}; - -typedef enum SigNum { - NEGATIVE = -1, - POSITIVE = 1, -} SigNum; - -/* Represents a conversion factor */ -struct Factor { - double factorNum = 1; - double factorDen = 1; - double offset = 0; - bool reciprocal = false; - int32_t constants[CONSTANTS_COUNT] = {}; - - void multiplyBy(const Factor &rhs) { - factorNum *= rhs.factorNum; - factorDen *= rhs.factorDen; - for (int i = 0; i < CONSTANTS_COUNT; i++) { - constants[i] += rhs.constants[i]; - } - - // NOTE - // We need the offset when the source and the target are simple units. e.g. the source is - // celsius and the target is Fahrenheit. Therefore, we just keep the value using `std::max`. - offset = std::max(rhs.offset, offset); - } - - void divideBy(const Factor &rhs) { - factorNum *= rhs.factorDen; - factorDen *= rhs.factorNum; - for (int i = 0; i < CONSTANTS_COUNT; i++) { - constants[i] -= rhs.constants[i]; - } - - // NOTE - // We need the offset when the source and the target are simple units. e.g. the source is - // celsius and the target is Fahrenheit. Therefore, we just keep the value using `std::max`. - offset = std::max(rhs.offset, offset); - } - - // Apply the power to the factor. - void power(int32_t power) { - // multiply all the constant by the power. - for (int i = 0; i < CONSTANTS_COUNT; i++) { - constants[i] *= power; - } - - bool shouldFlip = power < 0; // This means that after applying the absolute power, we should flip - // the Numerator and Denominator. - - factorNum = std::pow(factorNum, std::abs(power)); - factorDen = std::pow(factorDen, std::abs(power)); - - if (shouldFlip) { - // Flip Numerator and Denominator. - std::swap(factorNum, factorDen); - } - } - - // Flip the `Factor`, for example, factor= 2/3, flippedFactor = 3/2 - void flip() { - std::swap(factorNum, factorDen); - - for (int i = 0; i < CONSTANTS_COUNT; i++) { - constants[i] *= -1; - } - } - - // Apply SI prefix to the `Factor` - void applySiPrefix(UMeasureSIPrefix siPrefix) { - if (siPrefix == UMeasureSIPrefix::UMEASURE_SI_PREFIX_ONE) return; // No need to do anything - - double siApplied = std::pow(10.0, std::abs(siPrefix)); - - if (siPrefix < 0) { - factorDen *= siApplied; - return; - } - - factorNum *= siApplied; - } - - void substituteConstants() { - double constantsValues[CONSTANTS_COUNT]; - - // TODO: Load those constant values from units data. - constantsValues[CONSTANT_FT2M] = 0.3048; - constantsValues[CONSTANT_PI] = 411557987.0 / 131002976.0; - constantsValues[CONSTANT_GRAVITY] = 9.80665; - constantsValues[CONSTANT_G] = 6.67408E-11; - constantsValues[CONSTANT_LB2KG] = 0.45359237; - constantsValues[CONSTANT_GAL_IMP2M3] = 0.00454609; - - for (int i = 0; i < CONSTANTS_COUNT; i++) { - if (this->constants[i] == 0) { - continue; - } - - auto absPower = std::abs(this->constants[i]); - SigNum powerSig = this->constants[i] < 0 ? SigNum::NEGATIVE : SigNum::POSITIVE; - double absConstantValue = std::pow(constantsValues[i], absPower); - - if (powerSig == SigNum::NEGATIVE) { - this->factorDen *= absConstantValue; - } else { - this->factorNum *= absConstantValue; - } - - this->constants[i] = 0; - } - } -}; - /* Helpers */ using icu::double_conversion::StringToDoubleConverter; @@ -183,49 +161,11 @@ double strHasDivideSignToDouble(StringPiece strWithDivide, UErrorCode &status) { return strToDouble(strWithDivide, status); } -/* - * Adds a single factor element to the `Factor`. e.g "ft3m", "2.333" or "cup2m3". But not "cup2m3^3". - */ -void addSingleFactorConstant(StringPiece baseStr, int32_t power, SigNum sigNum, Factor &factor, - UErrorCode &status) { - - if (baseStr == "ft_to_m") { - factor.constants[CONSTANT_FT2M] += power * sigNum; - } else if (baseStr == "ft2_to_m2") { - factor.constants[CONSTANT_FT2M] += 2 * power * sigNum; - } else if (baseStr == "ft3_to_m3") { - factor.constants[CONSTANT_FT2M] += 3 * power * sigNum; - } else if (baseStr == "in3_to_m3") { - factor.constants[CONSTANT_FT2M] += 3 * power * sigNum; - factor.factorDen *= 12 * 12 * 12; - } else if (baseStr == "gal_to_m3") { - factor.factorNum *= 231; - factor.constants[CONSTANT_FT2M] += 3 * power * sigNum; - factor.factorDen *= 12 * 12 * 12; - } else if (baseStr == "gal_imp_to_m3") { - factor.constants[CONSTANT_GAL_IMP2M3] += power * sigNum; - } else if (baseStr == "G") { - factor.constants[CONSTANT_G] += power * sigNum; - } else if (baseStr == "gravity") { - factor.constants[CONSTANT_GRAVITY] += power * sigNum; - } else if (baseStr == "lb_to_kg") { - factor.constants[CONSTANT_LB2KG] += power * sigNum; - } else if (baseStr == "PI") { - factor.constants[CONSTANT_PI] += power * sigNum; - } else { - if (sigNum == SigNum::NEGATIVE) { - factor.factorDen *= std::pow(strToDouble(baseStr, status), power); - } else { - factor.factorNum *= std::pow(strToDouble(baseStr, status), power); - } - } -} - /* Adds single factor to a `Factor` object. Single factor means "23^2", "23.3333", "ft2m^3" ...etc. However, complex factor are not included, such as "ft2m^3*200/3" */ -void addFactorElement(Factor &factor, StringPiece elementStr, SigNum sigNum, UErrorCode &status) { +void addFactorElement(Factor &factor, StringPiece elementStr, Signum signum, UErrorCode &status) { StringPiece baseStr; StringPiece powerStr; int32_t power = @@ -250,7 +190,7 @@ void addFactorElement(Factor &factor, StringPiece elementStr, SigNum sigNum, UEr baseStr = elementStr; } - addSingleFactorConstant(baseStr, power, sigNum, factor, status); + addSingleFactorConstant(baseStr, power, signum, factor, status); } /* @@ -258,21 +198,21 @@ void addFactorElement(Factor &factor, StringPiece elementStr, SigNum sigNum, UEr */ Factor extractFactorConversions(StringPiece stringFactor, UErrorCode &status) { Factor result; - SigNum sigNum = SigNum::POSITIVE; + Signum signum = Signum::POSITIVE; auto factorData = stringFactor.data(); for (int32_t i = 0, start = 0, n = stringFactor.length(); i < n; i++) { if (factorData[i] == '*' || factorData[i] == '/') { StringPiece factorElement = stringFactor.substr(start, i - start); - addFactorElement(result, factorElement, sigNum, status); + addFactorElement(result, factorElement, signum, status); start = i + 1; // Set `start` to point to the start of the new element. } else if (i == n - 1) { // Last element - addFactorElement(result, stringFactor.substr(start, i + 1), sigNum, status); + addFactorElement(result, stringFactor.substr(start, i + 1), signum, status); } if (factorData[i] == '/') { - sigNum = SigNum::NEGATIVE; // Change the sigNum because we reached the Denominator. + signum = Signum::NEGATIVE; // Change the signum because we reached the Denominator. } } @@ -386,6 +326,44 @@ void loadConversionRate(ConversionRate &conversionRate, const MeasureUnit &sourc } // namespace +// Conceptually, this modifies factor: factor *= baseStr^(signum*power). +// +// baseStr must be a known constant or a value that strToDouble() is able to +// parse. +void U_I18N_API addSingleFactorConstant(StringPiece baseStr, int32_t power, Signum signum, + Factor &factor, UErrorCode &status) { + if (baseStr == "ft_to_m") { + factor.constants[CONSTANT_FT2M] += power * signum; + } else if (baseStr == "ft2_to_m2") { + factor.constants[CONSTANT_FT2M] += 2 * power * signum; + } else if (baseStr == "ft3_to_m3") { + factor.constants[CONSTANT_FT2M] += 3 * power * signum; + } else if (baseStr == "in3_to_m3") { + factor.constants[CONSTANT_FT2M] += 3 * power * signum; + factor.factorDen *= 12 * 12 * 12; + } else if (baseStr == "gal_to_m3") { + factor.factorNum *= 231; + factor.constants[CONSTANT_FT2M] += 3 * power * signum; + factor.factorDen *= 12 * 12 * 12; + } else if (baseStr == "gal_imp_to_m3") { + factor.constants[CONSTANT_GAL_IMP2M3] += power * signum; + } else if (baseStr == "G") { + factor.constants[CONSTANT_G] += power * signum; + } else if (baseStr == "gravity") { + factor.constants[CONSTANT_GRAVITY] += power * signum; + } else if (baseStr == "lb_to_kg") { + factor.constants[CONSTANT_LB2KG] += power * signum; + } else if (baseStr == "PI") { + factor.constants[CONSTANT_PI] += power * signum; + } else { + if (signum == Signum::NEGATIVE) { + factor.factorDen *= std::pow(strToDouble(baseStr, status), power); + } else { + factor.factorNum *= std::pow(strToDouble(baseStr, status), power); + } + } +} + /** * Extracts the compound base unit of a compound unit (`source`). For example, if the source unit is * `square-mile-per-hour`, the compound base unit will be `square-meter-per-second` @@ -414,7 +392,7 @@ MeasureUnit U_I18N_API extractCompoundBaseUnit(const MeasureUnit &source, } // Multiply the power of the singleUnit by the power of the baseUnit. For example, square-hectare - // must be p4-meter. (NOTE: hectare --> square-meter) + // must be pow4-meter. (NOTE: hectare --> square-meter) auto compoundBaseUnit = MeasureUnit::forIdentifier(rateInfo->baseUnit.toStringPiece(), status); int32_t baseUnitsCount; @@ -503,6 +481,7 @@ double UnitConverter::convert(double inputValue) const { return result * 1.000000000001; } +} // namespace units U_NAMESPACE_END #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/unitconverter.h b/icu4c/source/i18n/unitconverter.h index 3b29a663796..d19994b9d67 100644 --- a/icu4c/source/i18n/unitconverter.h +++ b/icu4c/source/i18n/unitconverter.h @@ -7,13 +7,60 @@ #ifndef __UNITCONVERTER_H__ #define __UNITCONVERTER_H__ -#include "cmemory.h" -#include "unicode/errorcode.h" #include "unicode/measunit.h" -#include "unitconverter.h" +#include "unicode/stringpiece.h" +#include "unicode/uobject.h" #include "unitsdata.h" U_NAMESPACE_BEGIN +namespace units { + +/* Internal Structure */ + +enum Constants { + CONSTANT_FT2M, // ft2m stands for foot to meter. + CONSTANT_PI, // PI + CONSTANT_GRAVITY, // Gravity + CONSTANT_G, + CONSTANT_GAL_IMP2M3, // Gallon imp to m3 + CONSTANT_LB2KG, // Pound to Kilogram + + // Must be the last element. + CONSTANTS_COUNT +}; + +typedef enum Signum { + NEGATIVE = -1, + POSITIVE = 1, +} Signum; + +/* Represents a conversion factor */ +struct U_I18N_API Factor { + double factorNum = 1; + double factorDen = 1; + double offset = 0; + bool reciprocal = false; + int32_t constants[CONSTANTS_COUNT] = {}; + + void multiplyBy(const Factor &rhs); + void divideBy(const Factor &rhs); + + // Apply the power to the factor. + void power(int32_t power); + + // Flip the `Factor`, for example, factor= 2/3, flippedFactor = 3/2 + void flip(); + + // Apply SI prefix to the `Factor` + void applySiPrefix(UMeasureSIPrefix siPrefix); + void substituteConstants(); +}; + +/* + * Adds a single factor element to the `Factor`. e.g "ft3m", "2.333" or "cup2m3". But not "cup2m3^3". + */ +void U_I18N_API addSingleFactorConstant(StringPiece baseStr, int32_t power, Signum sigNum, + Factor &factor, UErrorCode &status); /** * Represents the conversion rate between `source` and `target`. @@ -90,6 +137,7 @@ class U_I18N_API UnitConverter : public UMemory { ConversionRate conversionRate_; }; +} // namespace units U_NAMESPACE_END #endif //__UNITCONVERTER_H__ diff --git a/icu4c/source/i18n/unitsdata.cpp b/icu4c/source/i18n/unitsdata.cpp index 1ac285fe516..7b7f7dff648 100644 --- a/icu4c/source/i18n/unitsdata.cpp +++ b/icu4c/source/i18n/unitsdata.cpp @@ -5,15 +5,19 @@ #if !UCONFIG_NO_FORMATTING -#include "number_decimalquantity.h" #include "cstring.h" #include "number_decimalquantity.h" #include "resource.h" +#include "uassert.h" +#include "unicode/unistr.h" +#include "unicode/ures.h" #include "unitsdata.h" #include "uresimp.h" #include "util.h" +#include U_NAMESPACE_BEGIN +namespace units { namespace { @@ -108,40 +112,6 @@ class ConversionRateDataSink : public ResourceSink { MaybeStackVector *outVector; }; -UnitPreferenceMetadata::UnitPreferenceMetadata(StringPiece category, StringPiece usage, - StringPiece region, int32_t prefsOffset, - int32_t prefsCount, UErrorCode &status) { - this->category.append(category, status); - this->usage.append(usage, status); - this->region.append(region, status); - this->prefsOffset = prefsOffset; - this->prefsCount = prefsCount; -} - -int32_t UnitPreferenceMetadata::compareTo(const UnitPreferenceMetadata &other) const { - int32_t cmp = uprv_strcmp(category.data(), other.category.data()); - if (cmp == 0) { cmp = uprv_strcmp(usage.data(), other.usage.data()); } - if (cmp == 0) { cmp = uprv_strcmp(region.data(), other.region.data()); } - return cmp; -} - -int32_t UnitPreferenceMetadata::compareTo(const UnitPreferenceMetadata &other, bool *foundCategory, - bool *foundUsage, bool *foundRegion) const { - int32_t cmp = uprv_strcmp(category.data(), other.category.data()); - if (cmp == 0) { - *foundCategory = true; - cmp = uprv_strcmp(usage.data(), other.usage.data()); - } - if (cmp == 0) { - *foundUsage = true; - cmp = uprv_strcmp(region.data(), other.region.data()); - } - if (cmp == 0) { - *foundRegion = true; - } - return cmp; -} - bool operator<(const UnitPreferenceMetadata &a, const UnitPreferenceMetadata &b) { return a.compareTo(b) < 0; } @@ -325,6 +295,9 @@ int32_t getPreferenceMetadataIndex(const MaybeStackVectorcategory.append(category, status); + this->usage.append(usage, status); + this->region.append(region, status); + this->prefsOffset = prefsOffset; + this->prefsCount = prefsCount; +} + +int32_t UnitPreferenceMetadata::compareTo(const UnitPreferenceMetadata &other) const { + int32_t cmp = uprv_strcmp(category.data(), other.category.data()); + if (cmp == 0) { + cmp = uprv_strcmp(usage.data(), other.usage.data()); + } + if (cmp == 0) { + cmp = uprv_strcmp(region.data(), other.region.data()); + } + return cmp; +} + +int32_t UnitPreferenceMetadata::compareTo(const UnitPreferenceMetadata &other, bool *foundCategory, + bool *foundUsage, bool *foundRegion) const { + int32_t cmp = uprv_strcmp(category.data(), other.category.data()); + if (cmp == 0) { + *foundCategory = true; + cmp = uprv_strcmp(usage.data(), other.usage.data()); + } + if (cmp == 0) { + *foundUsage = true; + cmp = uprv_strcmp(region.data(), other.region.data()); + } + if (cmp == 0) { + *foundRegion = true; + } + return cmp; +} + CharString U_I18N_API getUnitCategory(const char *baseUnitIdentifier, UErrorCode &status) { CharString result; LocalUResourceBundlePointer unitsBundle(ures_openDirect(NULL, "units", &status)); @@ -414,6 +425,7 @@ void U_I18N_API UnitPreferences::getPreferencesFor(StringPiece category, StringP preferenceCount = m->prefsCount; } +} // namespace units U_NAMESPACE_END #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/unitsdata.h b/icu4c/source/i18n/unitsdata.h index 1ea8deea08f..6f1c1ecfd9c 100644 --- a/icu4c/source/i18n/unitsdata.h +++ b/icu4c/source/i18n/unitsdata.h @@ -9,10 +9,11 @@ #include "charstr.h" #include "cmemory.h" -#include "unicode/measunit.h" #include "unicode/stringpiece.h" +#include "unicode/uobject.h" U_NAMESPACE_BEGIN +namespace units { /** * Looks up the unit category of a base unit identifier. @@ -97,8 +98,6 @@ struct U_I18N_API UnitPreference : public UMemory { CharString skeleton; }; -namespace { - /** * Metadata about the preferences in UnitPreferences::unitPrefs_. * @@ -134,8 +133,6 @@ class U_I18N_API UnitPreferenceMetadata : public UMemory { bool *foundRegion) const; }; -} // namespace - /** * Unit Preferences information for various locales and usages. */ @@ -186,6 +183,7 @@ class U_I18N_API UnitPreferences { MaybeStackVector unitPrefs_; }; +} // namespace units U_NAMESPACE_END #endif //__GETUNITSDATA_H__ diff --git a/icu4c/source/i18n/unitsrouter.cpp b/icu4c/source/i18n/unitsrouter.cpp index 9f787ce82e6..5ca7b62e038 100644 --- a/icu4c/source/i18n/unitsrouter.cpp +++ b/icu4c/source/i18n/unitsrouter.cpp @@ -5,19 +5,15 @@ #if !UCONFIG_NO_FORMATTING -#include -#include - +#include "charstr.h" #include "cmemory.h" -#include "cstring.h" -#include "number_decimalquantity.h" -#include "resource.h" -#include "unitconverter.h" // for extractCompoundBaseUnit -#include "unitsdata.h" // for getUnitCategory +#include "unicode/measure.h" +#include "unitconverter.h" +#include "unitsdata.h" #include "unitsrouter.h" -#include "uresimp.h" U_NAMESPACE_BEGIN +namespace units { UnitsRouter::UnitsRouter(MeasureUnit inputUnit, StringPiece region, StringPiece usage, UErrorCode &status) { @@ -41,6 +37,7 @@ UnitsRouter::UnitsRouter(MeasureUnit inputUnit, StringPiece region, StringPiece return; } + outputUnits_.emplaceBackAndCheckErrorCode(status, complexTargetUnit); converterPreferences_.emplaceBackAndCheckErrorCode(status, inputUnit, complexTargetUnit, preference.geq, conversionRates, status); if (U_FAILURE(status)) { @@ -49,7 +46,7 @@ UnitsRouter::UnitsRouter(MeasureUnit inputUnit, StringPiece region, StringPiece } } -MaybeStackVector UnitsRouter::route(double quantity, UErrorCode &status) { +MaybeStackVector UnitsRouter::route(double quantity, UErrorCode &status) const { for (int i = 0, n = converterPreferences_.length(); i < n; i++) { const auto &converterPreference = *converterPreferences_[i]; @@ -63,6 +60,11 @@ MaybeStackVector UnitsRouter::route(double quantity, UErrorCode &status return lastConverter.convert(quantity, status); } +const MaybeStackVector *UnitsRouter::getOutputUnits() const { + return &outputUnits_; +} + +} // namespace units U_NAMESPACE_END #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/unitsrouter.h b/icu4c/source/i18n/unitsrouter.h index 1b29d9d8f3a..c8b8a646f61 100644 --- a/icu4c/source/i18n/unitsrouter.h +++ b/icu4c/source/i18n/unitsrouter.h @@ -9,17 +9,20 @@ #include -#include "charstr.h" // CharString #include "cmemory.h" #include "complexunitsconverter.h" -#include "unicode/errorcode.h" #include "unicode/measunit.h" -#include "unicode/measure.h" #include "unicode/stringpiece.h" +#include "unicode/uobject.h" #include "unitsdata.h" U_NAMESPACE_BEGIN +// Forward declarations +class Measure; + +namespace units { + /** * Contains the complex unit converter and the limit which representing the smallest value that the * converter should accept. For example, if the converter is converting to `foot+inch` and the limit @@ -75,12 +78,25 @@ class U_I18N_API UnitsRouter { public: UnitsRouter(MeasureUnit inputUnit, StringPiece locale, StringPiece usage, UErrorCode &status); - MaybeStackVector route(double quantity, UErrorCode &status); + MaybeStackVector route(double quantity, UErrorCode &status) const; + + /** + * Returns the list of possible output units, i.e. the full set of + * preferences, for the localized, usage-specific unit preferences. + * + * The returned pointer should be valid for the lifetime of the + * UnitsRouter instance. + */ + const MaybeStackVector *getOutputUnits() const; private: + // List of possible output units + MaybeStackVector outputUnits_; + MaybeStackVector converterPreferences_; }; +} // namespace units U_NAMESPACE_END #endif //__UNITSROUTER_H__ diff --git a/icu4c/source/test/depstest/dependencies.txt b/icu4c/source/test/depstest/dependencies.txt index 480f07f5ff1..b35fa45327e 100644 --- a/icu4c/source/test/depstest/dependencies.txt +++ b/icu4c/source/test/depstest/dependencies.txt @@ -979,6 +979,8 @@ group: number_output number_representation format formatted_value_sbimpl # PluralRules internals: unifiedcache + # Unit Formatting + units group: numberformatter # ICU 60+ NumberFormatter API @@ -990,11 +992,11 @@ group: numberformatter number_mapper.o number_modifiers.o number_multiplier.o number_notation.o number_padding.o number_patternmodifier.o number_patternstring.o number_rounding.o - number_scientific.o + number_scientific.o number_usageprefs.o currpinf.o dcfmtsym.o numsys.o numrange_fluent.o numrange_impl.o deps - decnumber double_conversion formattable units + decnumber double_conversion formattable units unitsformatter number_representation number_output uclean_i18n common diff --git a/icu4c/source/test/intltest/itformat.h b/icu4c/source/test/intltest/itformat.h index d8b17993c30..feb55e2b3c3 100644 --- a/icu4c/source/test/intltest/itformat.h +++ b/icu4c/source/test/intltest/itformat.h @@ -35,6 +35,10 @@ typedef struct UFieldPositionWithCategory { class IntlTestWithFieldPosition : public IntlTest { public: + // Tests FormattedValue's toString, toTempString, and nextPosition methods. + // + // expectedCategory gets combined with expectedFieldPositions to call + // checkMixedFormattedValue. void checkFormattedValue( const char16_t* message, const FormattedValue& fv, @@ -43,6 +47,7 @@ public: const UFieldPosition* expectedFieldPositions, int32_t length); + // Tests FormattedValue's toString, toTempString, and nextPosition methods. void checkMixedFormattedValue( const char16_t* message, const FormattedValue& fv, diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index 668885dabe8..c96c329ad20 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -54,6 +54,7 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition { void notationCompact(); void unitMeasure(); void unitCompoundMeasure(); + void unitUsage(); void unitCurrency(); void unitPercent(); void percentParity(); diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp index 88cb1da77e8..cb30123ca9b 100644 --- a/icu4c/source/test/intltest/numbertest_api.cpp +++ b/icu4c/source/test/intltest/numbertest_api.cpp @@ -75,6 +75,7 @@ void NumberFormatterApiTest::runIndexedTest(int32_t index, UBool exec, const cha TESTCASE_AUTO(notationCompact); TESTCASE_AUTO(unitMeasure); TESTCASE_AUTO(unitCompoundMeasure); + TESTCASE_AUTO(unitUsage); TESTCASE_AUTO(unitCurrency); TESTCASE_AUTO(unitPercent); if (!quick) { @@ -687,6 +688,79 @@ void NumberFormatterApiTest::unitMeasure() { u"5 a\u00F1os"); } +void NumberFormatterApiTest::unitUsage() { + UnlocalizedNumberFormatter unloc_formatter = + NumberFormatter::with().usage("road").unit(MeasureUnit::getMeter()); + + IcuTestErrorCode status(*this, "unitUsage()"); + + LocalizedNumberFormatter formatter = unloc_formatter.locale("en-ZA"); + FormattedNumber formattedNum = formatter.formatDouble(300, status); + 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)); + assertFormatDescendingBig( + u"unitUsage() en-ZA road", + u"measure-unit/length-meter usage/road", + u"unit/meter usage/road", + unloc_formatter, + Locale("en-ZA"), + u"87\u00A0650 km", + u"8\u00A0765 km", + u"877 km", + u"88 km", + u"8,8 km", + u"877 m", + u"88 m", + u"8,8 m", + u"0 m"); + + formatter = unloc_formatter.locale("en-GB"); + formattedNum = formatter.formatDouble(300, status); + assertTrue(UnicodeString("unitUsage() en-GB road, got outputUnit: \"") + + formattedNum.getOutputUnit(status).getIdentifier() + "\"", + MeasureUnit::getYard() == formattedNum.getOutputUnit(status)); + assertEquals("unitUsage() en-GB road", "328 yd", formattedNum.toString(status)); + assertFormatDescendingBig( + u"unitUsage() en-GB road", + u"measure-unit/length-meter usage/road", + u"unit/meter usage/road", + unloc_formatter, + Locale("en-GB"), + u"54,463 mi", + u"5,446 mi", + u"545 mi", + u"54 mi", + u"5.4 mi", + u"0.54 mi", + u"96 yd", + u"9.6 yd", + u"0 yd"); + + formatter = unloc_formatter.locale("en-US"); + formattedNum = formatter.formatDouble(300, status); + assertTrue(UnicodeString("unitUsage() en-US road, got outputUnit: \"") + + formattedNum.getOutputUnit(status).getIdentifier() + "\"", + MeasureUnit::getFoot() == formattedNum.getOutputUnit(status)); + assertEquals("unitUsage() en-US road", "984 ft", formattedNum.toString(status)); + assertFormatDescendingBig( + u"unitUsage() en-US road", + u"measure-unit/length-meter usage/road", + u"unit/meter usage/road", + unloc_formatter, + Locale("en-US"), + u"54,463 mi", + u"5,446 mi", + u"545 mi", + u"54 mi", + u"5.4 mi", + u"0.54 mi", + u"288 ft", + u"29 ft", + u"0 ft"); +} + void NumberFormatterApiTest::unitCompoundMeasure() { assertFormatDescending( u"Meters Per Second Short (unit that simplifies) and perUnit method", diff --git a/icu4c/source/test/intltest/unitsdatatest.cpp b/icu4c/source/test/intltest/unitsdatatest.cpp index d79ca782bb7..3ac36f7d0f8 100644 --- a/icu4c/source/test/intltest/unitsdatatest.cpp +++ b/icu4c/source/test/intltest/unitsdatatest.cpp @@ -8,6 +8,8 @@ #include "unitsdata.h" #include "intltest.h" +using namespace ::icu::units; + class UnitsDataTest : public IntlTest { public: UnitsDataTest() {} diff --git a/icu4c/source/test/intltest/unitstest.cpp b/icu4c/source/test/intltest/unitstest.cpp index cc31410038e..ba208ae1a63 100644 --- a/icu4c/source/test/intltest/unitstest.cpp +++ b/icu4c/source/test/intltest/unitstest.cpp @@ -13,14 +13,17 @@ #include "filestrm.h" #include "intltest.h" #include "number_decimalquantity.h" +#include "putilimp.h" #include "unicode/ctest.h" #include "unicode/measunit.h" #include "unicode/unistr.h" #include "unicode/unum.h" +#include "unicode/ures.h" #include "unitconverter.h" #include "unitsdata.h" #include "unitsrouter.h" #include "uparse.h" +#include "uresimp.h" struct UnitConversionTestCase { const StringPiece source; @@ -29,7 +32,8 @@ struct UnitConversionTestCase { const double expectedValue; }; -using icu::number::impl::DecimalQuantity; +using ::icu::number::impl::DecimalQuantity; +using namespace ::icu::units; class UnitsTest : public IntlTest { public: @@ -37,6 +41,7 @@ class UnitsTest : public IntlTest { void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = NULL); + void testUnitConstantFreshness(); void testConversionCapability(); void testConversions(); void testPreferences(); @@ -53,6 +58,7 @@ void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, cha logln("TestSuite UnitsTest: "); } TESTCASE_AUTO_BEGIN; + TESTCASE_AUTO(testUnitConstantFreshness); TESTCASE_AUTO(testConversionCapability); TESTCASE_AUTO(testConversions); TESTCASE_AUTO(testPreferences); @@ -63,10 +69,58 @@ void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, cha TESTCASE_AUTO_END; } +// Tests the hard-coded constants in the code against constants that appear in +// units.txt. +void UnitsTest::testUnitConstantFreshness() { + IcuTestErrorCode status(*this, "testUnitConstantFreshness"); + LocalUResourceBundlePointer unitsBundle(ures_openDirect(NULL, "units", status)); + LocalUResourceBundlePointer unitConstants( + ures_getByKey(unitsBundle.getAlias(), "unitConstants", NULL, status)); + + while (ures_hasNext(unitConstants.getAlias())) { + int32_t len; + const char *constant = NULL; + ures_getNextString(unitConstants.getAlias(), &len, &constant, status); + + Factor factor; + addSingleFactorConstant(constant, 1, POSITIVE, factor, status); + if (status.errDataIfFailureAndReset( + "addSingleFactorConstant(<%s>, ...).\n\n" + "If U_INVALID_FORMAT_ERROR, please check that \"icu4c/source/i18n/unitconverter.cpp\" " + "has all constants? Is \"%s\" a new constant?\n", + constant, constant)) { + continue; + } + + // Check the values of constants that have a simple numeric value + factor.substituteConstants(); + int32_t uLen; + UnicodeString uVal = ures_getStringByKey(unitConstants.getAlias(), constant, &uLen, status); + CharString val; + val.appendInvariantChars(uVal, status); + if (status.errDataIfFailureAndReset("Failed to get constant value for %s.", constant)) { + continue; + } + DecimalQuantity dqVal; + UErrorCode parseStatus = U_ZERO_ERROR; + // TODO(units): unify with strToDouble() in unitconverter.cpp + dqVal.setToDecNumber(val.toStringPiece(), parseStatus); + if (!U_SUCCESS(parseStatus)) { + // Not simple to parse, skip validating this constant's value. (We + // leave catching mistakes to the data-driven integration tests.) + continue; + } + double expectedNumerator = dqVal.toDouble(); + assertEquals(UnicodeString("Constant ") + constant + u" numerator", expectedNumerator, + factor.factorNum); + assertEquals(UnicodeString("Constant ") + constant + u" denominator", 1.0, factor.factorDen); + } +} + void UnitsTest::testConversionCapability() { struct TestCase { - const StringPiece source; - const StringPiece target; + const char *const source; + const char *const target; const UnitsConvertibilityState expectedState; } testCases[]{ {"meter", "foot", CONVERTIBLE}, // @@ -75,7 +129,7 @@ void UnitsTest::testConversionCapability() { {"kilometer-per-second", "second-per-meter", RECIPROCAL}, // {"square-meter", "square-foot", CONVERTIBLE}, // {"kilometer-per-second", "foot-per-second", CONVERTIBLE}, // - {"square-hectare", "p4-foot", CONVERTIBLE}, // + {"square-hectare", "pow4-foot", CONVERTIBLE}, // {"square-kilometer-per-second", "second-per-square-meter", RECIPROCAL}, // }; @@ -86,9 +140,11 @@ void UnitsTest::testConversionCapability() { MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status); ConversionRates conversionRates(status); - auto convertibility = icu::checkConvertibility(source, target, conversionRates, status); + auto convertibility = checkConvertibility(source, target, conversionRates, status); - assertEquals("Conversion Capability", testCase.expectedState, convertibility); + assertEquals(UnicodeString("Conversion Capability: ") + testCase.source + " to " + + testCase.target, + testCase.expectedState, convertibility); } } @@ -96,8 +152,8 @@ void UnitsTest::testSiPrefixes() { IcuTestErrorCode status(*this, "Units testSiPrefixes"); // Test Cases struct TestCase { - StringPiece source; - StringPiece target; + const char *source; + const char *target; const double inputValue; const double expectedValue; } testCases[]{ @@ -118,15 +174,12 @@ void UnitsTest::testSiPrefixes() { MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status); MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status); - MaybeStackVector units; - units.emplaceBack(source); - units.emplaceBack(target); - ConversionRates conversionRates(status); UnitConverter converter(source, target, conversionRates, status); - assertEqualsNear("test conversion", testCase.expectedValue, - converter.convert(testCase.inputValue), 0.001); + assertEqualsNear(UnicodeString("testSiPrefixes: ") + testCase.source + " to " + testCase.target, + testCase.expectedValue, converter.convert(testCase.inputValue), + 0.0001 * testCase.expectedValue); } } @@ -135,8 +188,8 @@ void UnitsTest::testMass() { // Test Cases struct TestCase { - StringPiece source; - StringPiece target; + const char *source; + const char *target; const double inputValue; const double expectedValue; } testCases[]{ @@ -156,15 +209,12 @@ void UnitsTest::testMass() { MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status); MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status); - MaybeStackVector units; - units.emplaceBack(source); - units.emplaceBack(target); - ConversionRates conversionRates(status); UnitConverter converter(source, target, conversionRates, status); - assertEqualsNear("test conversion", testCase.expectedValue, - converter.convert(testCase.inputValue), 0.001); + assertEqualsNear(UnicodeString("testMass: ") + testCase.source + " to " + testCase.target, + testCase.expectedValue, converter.convert(testCase.inputValue), + 0.0001 * testCase.expectedValue); } } @@ -172,8 +222,8 @@ void UnitsTest::testTemperature() { IcuTestErrorCode status(*this, "Units testTemperature"); // Test Cases struct TestCase { - StringPiece source; - StringPiece target; + const char *source; + const char *target; const double inputValue; const double expectedValue; } testCases[]{ @@ -193,15 +243,12 @@ void UnitsTest::testTemperature() { MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status); MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status); - MaybeStackVector units; - units.emplaceBack(source); - units.emplaceBack(target); - ConversionRates conversionRates(status); UnitConverter converter(source, target, conversionRates, status); - assertEqualsNear("test conversion", testCase.expectedValue, - converter.convert(testCase.inputValue), 0.001); + assertEqualsNear(UnicodeString("testTemperature: ") + testCase.source + " to " + testCase.target, + testCase.expectedValue, converter.convert(testCase.inputValue), + 0.0001 * uprv_fabs(testCase.expectedValue)); } } @@ -210,8 +257,8 @@ void UnitsTest::testArea() { // Test Cases struct TestCase { - StringPiece source; - StringPiece target; + const char *source; + const char *target; const double inputValue; const double expectedValue; } testCases[]{ @@ -234,15 +281,12 @@ void UnitsTest::testArea() { MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status); MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status); - MaybeStackVector units; - units.emplaceBack(source); - units.emplaceBack(target); - ConversionRates conversionRates(status); UnitConverter converter(source, target, conversionRates, status); - assertEqualsNear("test conversion", testCase.expectedValue, - converter.convert(testCase.inputValue), 0.001); + assertEqualsNear(UnicodeString("testArea: ") + testCase.source + " to " + testCase.target, + testCase.expectedValue, converter.convert(testCase.inputValue), + 0.0001 * testCase.expectedValue); } } @@ -353,7 +397,7 @@ void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, U double got = converter.convert(1000); msg.clear(); msg.append("Converting 1000 ", status).append(x, status).append(" to ", status).append(y, status); - unitsTest->assertEqualsNear(msg.data(), expected, got, 0.0001); + unitsTest->assertEqualsNear(msg.data(), expected, got, 0.0001 * expected); } /** @@ -469,38 +513,14 @@ class ExpectedOutput { } }; -// TODO(Hugo): Add a comment and Use AssertEqualsNear. +// Checks a vector of Measure instances against ExpectedOutput. void checkOutput(UnitsTest *unitsTest, const char *msg, ExpectedOutput expected, const MaybeStackVector &actual, double precision) { IcuTestErrorCode status(*unitsTest, "checkOutput"); - bool success = true; - if (expected._compoundCount != actual.length()) { - success = false; - } - for (int i = 0; i < actual.length(); i++) { - if (i >= expected._compoundCount) { - break; - } - // assertEqualsNear("test conversion", expected._amounts[i], - // actual[i]->getNumber().getDouble(status), 0.0001); - - double diff = std::abs(expected._amounts[i] - actual[i]->getNumber().getDouble(status)); - double diffPercent = expected._amounts[i] != 0 ? diff / expected._amounts[i] : diff; - if (diffPercent > precision) { - success = false; - break; - } - - if (expected._measureUnits[i] != actual[i]->getUnit()) { - success = false; - break; - } - } - - CharString testMessage("test case: ", status); + CharString testMessage("Test case \"", status); testMessage.append(msg, status); - testMessage.append(", expected output: ", status); + testMessage.append("\": expected output: ", status); testMessage.append(expected.toDebugString().c_str(), status); testMessage.append(", obtained output:", status); for (int i = 0; i < actual.length(); i++) { @@ -509,8 +529,19 @@ void checkOutput(UnitsTest *unitsTest, const char *msg, ExpectedOutput expected, testMessage.append(" ", status); testMessage.appendInvariantChars(actual[i]->getUnit().getIdentifier(), status); } - - unitsTest->assertTrue(testMessage.data(), success); + if (!unitsTest->assertEquals(testMessage.data(), expected._compoundCount, actual.length())) { + return; + }; + for (int i = 0; i < actual.length(); i++) { + double permittedDiff = precision * expected._amounts[i]; + if (permittedDiff == 0) { + // If 0 is expected, still permit a small delta. + // TODO: revisit this experimentally chosen value: + permittedDiff = 0.00000001; + } + unitsTest->assertEqualsNear(testMessage.data(), expected._amounts[i], + actual[i]->getNumber().getDouble(status), permittedDiff); + } } /** @@ -590,7 +621,8 @@ void unitPreferencesTestDataLineFn(void *context, char *fields[][2], int32_t fie if (status.errIfFailureAndReset("router.route(inputAmount, ...)")) { return; } - checkOutput(unitsTest, msg.data(), expected, result, 0.0001); + // TODO: revisit this experimentally chosen precision: + checkOutput(unitsTest, msg.data(), expected, result, 0.0000000001); } /**