From: younies Date: Wed, 11 Mar 2020 22:11:23 +0000 (+0100) Subject: ICU-20568 UnitsRouter, ComplexUnitConverter, numberformatter.h X-Git-Tag: cldr/2020-09-22~44 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=72056d4df2a55c21af0dcda24779eaf0335f6c29;p=icu ICU-20568 UnitsRouter, ComplexUnitConverter, numberformatter.h add usage to number formatter settings header PR: https://github.com/sffc/icu/pull/23 Commit: 6d78a95d6dbe0ef946624e7253f57d414168c77a Implementation of UnitsRouter and ComplexUnitConverter. PR: https://github.com/sffc/icu/pull/30 Commit: 1ae7190d1950377d5fdab822bb65eb67a8891104 --- diff --git a/icu4c/source/i18n/complexunitsconverter.cpp b/icu4c/source/i18n/complexunitsconverter.cpp new file mode 100644 index 00000000000..4a77b79a0e8 --- /dev/null +++ b/icu4c/source/i18n/complexunitsconverter.cpp @@ -0,0 +1,128 @@ +// © 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 +#include + +#include "cmemory.h" +#include "complexunitsconverter.h" +#include "uassert.h" +#include "unicode/fmtable.h" +#include "unitconverter.h" + +U_NAMESPACE_BEGIN + +ComplexUnitsConverter::ComplexUnitsConverter(const MeasureUnit &inputUnit, + const MeasureUnit &outputUnits, + const ConversionRates &ratesInfo, UErrorCode &status) { + + if (outputUnits.getComplexity(status) != UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) { + unitConverters_.emplaceBackAndCheckErrorCode(status, inputUnit, outputUnits, ratesInfo, status); + if (U_FAILURE(status)) { + return; + } + + units_.emplaceBackAndCheckErrorCode(status, outputUnits); + return; + } + + // In case the `outputUnits` are `UMEASURE_UNIT_MIXED` such as `foot+inch`. In this case we need more + // converters to convert from the `inputUnit` to the first unit in the `outputUnits`. Then, a + // converter from the first unit in the `outputUnits` to the second unit and so on. + // For Example: + // - inputUnit is `meter` + // - outputUnits is `foot+inch` + // - Therefore, we need to have two converters: + // 1. a converter from `meter` to `foot` + // 2. a converter from `foot` to `inch` + // - Therefore, if the input is `2 meter`: + // 1. convert `meter` to `foot` --> 2 meter to 6.56168 feet + // 2. convert the residual of 6.56168 feet (0.56168) to inches, which will be (6.74016 + // inches) + // 3. then, the final result will be (6 feet and 6.74016 inches) + int32_t length; + auto singleUnits = outputUnits.splitToSingleUnits(length, status); + MaybeStackVector singleUnitsInOrder; + for (int i = 0; i < length; ++i) { + /** + * TODO(younies): ensure units being in order by the biggest unit at first. + * + * HINT: + * MaybeStackVector singleUnitsInOrder = MeasureUnitImpl::forMeasureUnitMaybeCopy(outputUnits, status).units; + * uprv_sortArray( + * singleUnitsInOrder.getAlias(), + * singleUnitsInOrder.length(), + * sizeof(singleUnitsInOrder[0]), + * compareSingleUnits, + * nullptr, + * false, + * &status); + */ + singleUnitsInOrder.emplaceBackAndCheckErrorCode(status, singleUnits[i]); + } + + if (singleUnitsInOrder.length() == 0) { + status = U_ILLEGAL_ARGUMENT_ERROR; + return; + } + + for (int i = 0, n = singleUnitsInOrder.length(); i < n; i++) { + if (i == 0) { // first element + unitConverters_.emplaceBackAndCheckErrorCode(status, inputUnit, *singleUnitsInOrder[i], + ratesInfo, status); + } else { + unitConverters_.emplaceBackAndCheckErrorCode(status, *singleUnitsInOrder[i - 1], + *singleUnitsInOrder[i], ratesInfo, status); + } + + if (U_FAILURE(status)) { + return; + } + } + + units_.appendAll(singleUnitsInOrder, status); +} + +UBool ComplexUnitsConverter::greaterThanOrEqual(double quantity, double limit) const { + U_ASSERT(unitConverters_.length() > 0); + + // First converter converts to the biggest quantity. + double newQuantity = unitConverters_[0]->convert(quantity); + return newQuantity >= limit; +} + +MaybeStackVector ComplexUnitsConverter::convert(double quantity, UErrorCode &status) const { + MaybeStackVector result; + + for (int i = 0, n = unitConverters_.length(); i < n; ++i) { + quantity = (*unitConverters_[i]).convert(quantity); + if (i < n - 1) { + int64_t newQuantity = floor(quantity); + Formattable formattableNewQuantity(newQuantity); + + // NOTE: Measure would own its MeasureUnit. + result.emplaceBackAndCheckErrorCode(status, formattableNewQuantity, + new MeasureUnit(*units_[i]), status); + + // Keep the residual of the quantity. + // For example: `3.6 feet`, keep only `0.6 feet` + quantity -= newQuantity; + } else { // LAST ELEMENT + Formattable formattableQuantity(quantity); + + // NOTE: Measure would own its MeasureUnit. + result.emplaceBackAndCheckErrorCode(status, formattableQuantity, new MeasureUnit(*units_[i]), + status); + } + } + + return result; +} + +U_NAMESPACE_END + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/complexunitsconverter.h b/icu4c/source/i18n/complexunitsconverter.h new file mode 100644 index 00000000000..0fe76ba52b2 --- /dev/null +++ b/icu4c/source/i18n/complexunitsconverter.h @@ -0,0 +1,67 @@ +// © 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 __COMPLEXUNITSCONVERTER_H__ +#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 + +/** + * Converts from single or compound unit to single, compound or mixed units. + * For example, from `meter` to `foot+inch`. + * + * DESIGN: + * This class uses `UnitConverter` in order to perform the single converter (i.e. converters from a + * single unit to another single unit). Therefore, `ComplexUnitsConverter` class contains multiple + * instances of the `UnitConverter` to perform the conversion. + */ +class U_I18N_API ComplexUnitsConverter { + public: + /** + * Constructor of `ComplexUnitsConverter`. + * NOTE: + * - inputUnit and outputUnits must be under the same category + * - e.g. meter to feet and inches --> all of them are length units. + * + * @param inputUnit represents the source unit. (should be single or compound unit). + * @param outputUnits represents the output unit. could be any type. (single, compound or mixed). + * @param status + */ + ComplexUnitsConverter(const MeasureUnit &inputUnit, const MeasureUnit &outputUnits, + const ConversionRates &ratesInfo, UErrorCode &status); + + // Returns true if the specified `quantity` of the `inputUnit`, expressed in terms of the biggest + // unit in the MeasureUnit `outputUnit`, is greater than or equal to `limit`. + // For example, if the input unit is `meter` and the target unit is `foot+inch`. Therefore, this + // function will convert the `quantity` from `meter` to `foot`, then, it will compare the value in + // `foot` with the `limit`. + UBool greaterThanOrEqual(double quantity, double limit) const; + + // Returns outputMeasures which is an array with the corresponding values. + // - E.g. converting meters to feet and inches. + // 1 meter --> 3 feet, 3.3701 inches + // NOTE: + // the smallest element is the only element that could have fractional values. And all + // other elements are floored to the nearest integer + MaybeStackVector convert(double quantity, UErrorCode &status) const; + + private: + MaybeStackVector unitConverters_; + MaybeStackVector units_; +}; + +U_NAMESPACE_END + +#endif //__COMPLEXUNITSCONVERTER_H__ + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/i18n.vcxproj b/icu4c/source/i18n/i18n.vcxproj index f39f5033225..ea139c08f4a 100644 --- a/icu4c/source/i18n/i18n.vcxproj +++ b/icu4c/source/i18n/i18n.vcxproj @@ -251,6 +251,8 @@ + + @@ -487,6 +489,8 @@ + + diff --git a/icu4c/source/i18n/i18n.vcxproj.filters b/icu4c/source/i18n/i18n.vcxproj.filters index 1df9dd6f7dc..1dc9b5c1d3c 100644 --- a/icu4c/source/i18n/i18n.vcxproj.filters +++ b/icu4c/source/i18n/i18n.vcxproj.filters @@ -654,6 +654,12 @@ formatting + + formatting + + + formatting + formatting @@ -1004,6 +1010,12 @@ formatting + + formatting + + + formatting + formatting @@ -1247,4 +1259,4 @@ misc - \ No newline at end of file + diff --git a/icu4c/source/i18n/i18n_uwp.vcxproj b/icu4c/source/i18n/i18n_uwp.vcxproj index aabf1003714..30c98a05c82 100644 --- a/icu4c/source/i18n/i18n_uwp.vcxproj +++ b/icu4c/source/i18n/i18n_uwp.vcxproj @@ -482,6 +482,8 @@ + + @@ -718,6 +720,8 @@ + + @@ -729,4 +733,4 @@ - \ No newline at end of file + diff --git a/icu4c/source/i18n/sources.txt b/icu4c/source/i18n/sources.txt index 6c9e1348834..ce2486bf624 100644 --- a/icu4c/source/i18n/sources.txt +++ b/icu4c/source/i18n/sources.txt @@ -32,6 +32,7 @@ collationsettings.cpp collationtailoring.cpp collationweights.cpp compactdecimalformat.cpp +complexunitsconverter.cpp coptccal.cpp cpdtrans.cpp csdetect.cpp @@ -210,6 +211,7 @@ unesctrn.cpp uni2name.cpp unitconverter.cpp unitsdata.cpp +unitsrouter.cpp unum.cpp unumsys.cpp upluralrules.cpp diff --git a/icu4c/source/i18n/unicode/measunit.h b/icu4c/source/i18n/unicode/measunit.h index 7c597356c3e..faa7aef81d4 100644 --- a/icu4c/source/i18n/unicode/measunit.h +++ b/icu4c/source/i18n/unicode/measunit.h @@ -434,7 +434,7 @@ class U_I18N_API MeasureUnit: public UObject { * For example, if the receiver is "kilowatt" and the argument is "hour-per-day", then the * unit "kilowatt-hour-per-day" is returned. * - * NOTE: Only works on SINGLE and COMPOUND units. If either unit (receivee and argument) is a + * NOTE: Only works on SINGLE and COMPOUND units. If either unit (receiver and argument) is a * MIXED unit, an error will occur. For more information, see UMeasureUnitComplexity. * * @param other The MeasureUnit to multiply with the target. diff --git a/icu4c/source/i18n/unicode/numberformatter.h b/icu4c/source/i18n/unicode/numberformatter.h index 50da816f355..0fb16c26901 100644 --- a/icu4c/source/i18n/unicode/numberformatter.h +++ b/icu4c/source/i18n/unicode/numberformatter.h @@ -1508,10 +1508,15 @@ class U_I18N_API NumberFormatterSettings { * All units will be properly localized with locale data, and all units are compatible with notation styles, * rounding precisions, and other number formatter settings. * + * \note If the usage() is set, the output unit **will be changed** to + * produce localised units, according to usage, locale and unit. See + * FormattedNumber::getOutputUnit(). + * * Pass this method any instance of {@link MeasureUnit}. For units of measure: * *
      * NumberFormatter::with().unit(MeasureUnit::getMeter())
+     * NumberFormatter::with().unit(MeasureUnit::forIdentifier("foot-per-second", status))
      * 
* * Currency: @@ -2039,6 +2044,54 @@ class U_I18N_API NumberFormatterSettings { */ Derived scale(const Scale &scale) &&; +#ifndef U_HIDE_DRAFT_API + /** + * Specifies the usage for which numbers will be formatted ("person", + * "road", "person", etc.) + * + * When a `usage` is specified, the output unit will change depending on the + * `Locale` and the unit quantity. For example, formatting length + * measurements specified in meters: + * + * `NumberFormatter::with().usage("person").unit(MeasureUnit::getMeter()).locale("en-US")` + * * When formatting 0.25, the output will be "10 inches". + * * When formatting 1.50, the output will be "4 feet and 11 inches". + * + * The input unit specified via unit() determines the type of measurement + * being formatted (e.g. "length" when the unit is "foot"). The usage + * requested will be looked for only within this category of measurement + * units. + * + * The output unit can be found via FormattedNumber::getOutputUnit(). + * + * If the usage has multiple parts (e.g. "land-agriculture-grain") and does + * not match a known usage preference, the last part will be dropped + * repeatedly until a match is found (e.g. trying "land-agriculture", then + * "land"). If a match is still not found, usage will fall back to + * "default". + * + * Setting usage to an empty string clears the usage (disables usage-based + * localized 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 + * supplemental/units.xml](https://github.com/unicode-org/cldr/blob/master/common/supplemental/units.xml). + * @return The fluent chain. + * @draft ICU 68 + */ + Derived usage(StringPiece usage) const &; + + /** + * Overload of usage() for use on an rvalue reference. + * + * @param usage The unit `usage`. + * @return The fluent chain. + * @draft ICU 68 + */ + Derived usage(StringPiece usage) &&; +#endif // U_HIDE_DRAFT_API + #ifndef U_HIDE_INTERNAL_API /** @@ -2515,6 +2568,21 @@ class U_I18N_API FormattedNumber : public UMemory, public FormattedValue { inline StringClass toDecimalNumber(UErrorCode& status) const; #endif // U_HIDE_DRAFT_API +#ifndef U_HIDE_DRAFT_API + /** + * Gets the resolved output unit. + * + * 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". + * + * @return `MeasureUnit`. + * @draft ICU 68 + */ + MeasureUnit getOutputUnit(UErrorCode& status) const; +#endif // U_HIDE_DRAFT_API + #ifndef U_HIDE_INTERNAL_API /** diff --git a/icu4c/source/i18n/unitconverter.cpp b/icu4c/source/i18n/unitconverter.cpp index 516f223a707..70adbd7e88f 100644 --- a/icu4c/source/i18n/unitconverter.cpp +++ b/icu4c/source/i18n/unitconverter.cpp @@ -126,14 +126,19 @@ struct Factor { constantsValues[CONSTANT_GAL_IMP2M3] = 0.00454609; for (int i = 0; i < CONSTANTS_COUNT; i++) { - if (this->constants[i] == 0) { continue;} + 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;} + if (powerSig == SigNum::NEGATIVE) { + this->factorDen *= absConstantValue; + } else { + this->factorNum *= absConstantValue; + } this->constants[i] = 0; } @@ -152,7 +157,9 @@ double strToDouble(StringPiece strNum, UErrorCode &status) { StringToDoubleConverter converter(0, 0, 0, "", ""); int32_t count; double result = converter.StringToDouble(strNum.data(), strNum.length(), &count); - if (count != strNum.length()) { status = U_INVALID_FORMAT_ERROR; } + if (count != strNum.length()) { + status = U_INVALID_FORMAT_ERROR; + } return result; } @@ -176,48 +183,6 @@ double strHasDivideSignToDouble(StringPiece strWithDivide, UErrorCode &status) { return strToDouble(strWithDivide, status); } -/** - * 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` - */ -MeasureUnit extractCompoundBaseUnit(const MeasureUnit &source, const ConversionRates &conversionRates, - UErrorCode &status) { - MeasureUnit result; - int32_t count; - const auto singleUnits = source.splitToSingleUnits(count, status); - if (U_FAILURE(status)) return result; - - for (int i = 0; i < count; ++i) { - const auto &singleUnit = singleUnits[i]; - // Extract `ConversionRateInfo` using the absolute unit. For example: in case of `square-meter`, - // we will use `meter` - const auto singleUnitImpl = SingleUnitImpl::forMeasureUnit(singleUnit, status); - const auto rateInfo = conversionRates.extractConversionInfo(singleUnitImpl.getSimpleUnitID(), status); - if (U_FAILURE(status)) { return result; } - if (rateInfo == nullptr) { - status = U_INTERNAL_PROGRAM_ERROR; - return result; - } - - // Multiply the power of the singleUnit by the power of the baseUnit. For example, square-hectare - // must be p4-meter. (NOTE: hectare --> square-meter) - auto compoundBaseUnit = MeasureUnit::forIdentifier(rateInfo->baseUnit.toStringPiece(), status); - - int32_t baseUnitsCount; - auto baseUnits = compoundBaseUnit.splitToSingleUnits(baseUnitsCount, status); - for (int j = 0; j < baseUnitsCount; j++) { - int8_t newDimensionality = - baseUnits[j].getDimensionality(status) * singleUnit.getDimensionality(status); - result = result.product(baseUnits[j].withDimensionality(newDimensionality, status), status); - - if (U_FAILURE(status)) { return result; } - } - } - - return result; -} - -// TODO: Load those constant from units data. /* * Adds a single factor element to the `Factor`. e.g "ft3m", "2.333" or "cup2m3". But not "cup2m3^3". */ @@ -365,7 +330,9 @@ UBool checkSimpleUnit(const MeasureUnit &unit, UErrorCode &status) { const auto &compoundSourceUnit = MeasureUnitImpl::forMeasureUnit(unit, memory, status); if (U_FAILURE(status)) return false; - if (compoundSourceUnit.complexity != UMEASURE_UNIT_SINGLE) { return false; } + if (compoundSourceUnit.complexity != UMEASURE_UNIT_SINGLE) { + return false; + } U_ASSERT(compoundSourceUnit.units.length() == 1); auto singleUnit = *(compoundSourceUnit.units[0]); @@ -419,10 +386,63 @@ void loadConversionRate(ConversionRate &conversionRate, const MeasureUnit &sourc } // namespace +/** + * 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` + */ +MeasureUnit U_I18N_API extractCompoundBaseUnit(const MeasureUnit &source, + const ConversionRates &conversionRates, + UErrorCode &status) { + MeasureUnit result; + int32_t count; + const auto singleUnits = source.splitToSingleUnits(count, status); + if (U_FAILURE(status)) return result; + + for (int i = 0; i < count; ++i) { + const auto &singleUnit = singleUnits[i]; + // Extract `ConversionRateInfo` using the absolute unit. For example: in case of `square-meter`, + // we will use `meter` + const auto singleUnitImpl = SingleUnitImpl::forMeasureUnit(singleUnit, status); + const auto rateInfo = + conversionRates.extractConversionInfo(singleUnitImpl.getSimpleUnitID(), status); + if (U_FAILURE(status)) { + return result; + } + if (rateInfo == nullptr) { + status = U_INTERNAL_PROGRAM_ERROR; + return result; + } + + // Multiply the power of the singleUnit by the power of the baseUnit. For example, square-hectare + // must be p4-meter. (NOTE: hectare --> square-meter) + auto compoundBaseUnit = MeasureUnit::forIdentifier(rateInfo->baseUnit.toStringPiece(), status); + + int32_t baseUnitsCount; + auto baseUnits = compoundBaseUnit.splitToSingleUnits(baseUnitsCount, status); + for (int j = 0; j < baseUnitsCount; j++) { + int8_t newDimensionality = + baseUnits[j].getDimensionality(status) * singleUnit.getDimensionality(status); + result = result.product(baseUnits[j].withDimensionality(newDimensionality, status), status); + + if (U_FAILURE(status)) { + return result; + } + } + } + + return result; +} + UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &source, const MeasureUnit &target, const ConversionRates &conversionRates, UErrorCode &status) { + if (source.getComplexity(status) == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED || + target.getComplexity(status) == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) { + status = U_INTERNAL_PROGRAM_ERROR; + return UNCONVERTIBLE; + } + auto sourceBaseUnit = extractCompoundBaseUnit(source, conversionRates, status); auto targetBaseUnit = extractCompoundBaseUnit(target, conversionRates, status); @@ -431,11 +451,24 @@ UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &sourc if (sourceBaseUnit == targetBaseUnit) return CONVERTIBLE; if (sourceBaseUnit == targetBaseUnit.reciprocal(status)) return RECIPROCAL; + auto sourceSimplified = sourceBaseUnit.simplify(status); + auto targetSimplified = targetBaseUnit.simplify(status); + + if (sourceSimplified == targetSimplified) return CONVERTIBLE; + if (sourceSimplified == targetSimplified.reciprocal(status)) return RECIPROCAL; + return UNCONVERTIBLE; } UnitConverter::UnitConverter(MeasureUnit source, MeasureUnit target, const ConversionRates &ratesInfo, UErrorCode &status) { + + if (source.getComplexity(status) == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED || + target.getComplexity(status) == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) { + status = U_INTERNAL_PROGRAM_ERROR; + return; + } + UnitsConvertibilityState unitsState = checkConvertibility(source, target, ratesInfo, status); if (U_FAILURE(status)) return; if (unitsState == UnitsConvertibilityState::UNCONVERTIBLE) { @@ -459,8 +492,15 @@ double UnitConverter::convert(double inputValue) const { if (result == 0) return 0.0; // If the result is zero, it does not matter if the conversion are reciprocal or not. - if (conversionRate_.reciprocal) { result = 1.0 / result; } - return result; + if (conversionRate_.reciprocal) { + result = 1.0 / result; + } + + // TODO: remove the multiplication by 1.000,000,000,001 after using `decNumber` + + // Multiply the result by 1.000,000,000,001 to fix the deterioration from using `double` (the + // deterioration is around 15 to 17 decimal digit). + return result * 1.000000000001; } U_NAMESPACE_END diff --git a/icu4c/source/i18n/unitconverter.h b/icu4c/source/i18n/unitconverter.h index 39a8e81654e..3b29a663796 100644 --- a/icu4c/source/i18n/unitconverter.h +++ b/icu4c/source/i18n/unitconverter.h @@ -34,6 +34,21 @@ enum U_I18N_API UnitsConvertibilityState { UNCONVERTIBLE, }; +MeasureUnit U_I18N_API extractCompoundBaseUnit(const MeasureUnit &source, + const ConversionRates &conversionRates, + UErrorCode &status); + +/** + * Check if the convertibility between `source` and `target`. + * For example: + * `meter` and `foot` are `CONVERTIBLE`. + * `meter-per-second` and `second-per-meter` are `RECIPROCAL`. + * `meter` and `pound` are `UNCONVERTIBLE`. + * + * NOTE: + * Only works with SINGLE and COMPOUND units. If one of the units is a + * MIXED unit, an error will occur. For more information, see UMeasureUnitComplexity. + */ UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &source, const MeasureUnit &target, const ConversionRates &conversionRates, @@ -41,8 +56,12 @@ UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &sourc /** * Converts from a source `MeasureUnit` to a target `MeasureUnit`. + * + * NOTE: + * Only works with SINGLE and COMPOUND units. If one of the units is a + * MIXED unit, an error will occur. For more information, see UMeasureUnitComplexity. */ -class U_I18N_API UnitConverter { +class U_I18N_API UnitConverter : public UMemory { public: /** * Constructor of `UnitConverter`. @@ -55,8 +74,8 @@ class U_I18N_API UnitConverter { * @param ratesInfo Contains all the needed conversion rates. * @param status */ - UnitConverter(MeasureUnit source, MeasureUnit target, - const ConversionRates &ratesInfo, UErrorCode &status); + UnitConverter(MeasureUnit source, MeasureUnit target, const ConversionRates &ratesInfo, + UErrorCode &status); /** * Convert a value in the source unit to another value in the target unit. diff --git a/icu4c/source/i18n/unitsdata.cpp b/icu4c/source/i18n/unitsdata.cpp index 05d759ba9ae..1ac285fe516 100644 --- a/icu4c/source/i18n/unitsdata.cpp +++ b/icu4c/source/i18n/unitsdata.cpp @@ -5,6 +5,7 @@ #if !UCONFIG_NO_FORMATTING +#include "number_decimalquantity.h" #include "cstring.h" #include "number_decimalquantity.h" #include "resource.h" @@ -16,7 +17,7 @@ U_NAMESPACE_BEGIN namespace { -using number::impl::DecimalQuantity; +using icu::number::impl::DecimalQuantity; void trimSpaces(CharString& factor, UErrorCode& status){ CharString trimmed; @@ -399,7 +400,7 @@ U_I18N_API UnitPreferences::UnitPreferences(UErrorCode &status) { // TODO: make outPreferences const? // -// TODO: consider replacing `UnitPreference **&outPrefrences` with slice class +// TODO: consider replacing `UnitPreference **&outPreferences` with slice class // of some kind. void U_I18N_API UnitPreferences::getPreferencesFor(StringPiece category, StringPiece usage, StringPiece region, diff --git a/icu4c/source/i18n/unitsdata.h b/icu4c/source/i18n/unitsdata.h index 0acea7ad045..1ea8deea08f 100644 --- a/icu4c/source/i18n/unitsdata.h +++ b/icu4c/source/i18n/unitsdata.h @@ -171,7 +171,7 @@ class U_I18N_API UnitPreferences { * result set. * @param status Receives status. * - * TODO(hugovdm): maybe replace `UnitPreference **&outPrefrences` with a slice class? + * TODO(hugovdm): maybe replace `UnitPreference **&outPreferences` with a slice class? */ void getPreferencesFor(StringPiece category, StringPiece usage, StringPiece region, const UnitPreference *const *&outPreferences, int32_t &preferenceCount, diff --git a/icu4c/source/i18n/unitsrouter.cpp b/icu4c/source/i18n/unitsrouter.cpp new file mode 100644 index 00000000000..9f787ce82e6 --- /dev/null +++ b/icu4c/source/i18n/unitsrouter.cpp @@ -0,0 +1,68 @@ +// © 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 +#include + +#include "cmemory.h" +#include "cstring.h" +#include "number_decimalquantity.h" +#include "resource.h" +#include "unitconverter.h" // for extractCompoundBaseUnit +#include "unitsdata.h" // for getUnitCategory +#include "unitsrouter.h" +#include "uresimp.h" + +U_NAMESPACE_BEGIN + +UnitsRouter::UnitsRouter(MeasureUnit inputUnit, StringPiece region, StringPiece usage, + UErrorCode &status) { + // TODO: do we want to pass in ConversionRates and UnitPreferences instead + // of loading in each UnitsRouter instance? (Or make global?) + ConversionRates conversionRates(status); + UnitPreferences prefs(status); + + MeasureUnit baseUnit = extractCompoundBaseUnit(inputUnit, conversionRates, status); + CharString category = getUnitCategory(baseUnit.getIdentifier(), status); + + const UnitPreference *const *unitPreferences; + int32_t preferencesCount; + prefs.getPreferencesFor(category.data(), usage, region, unitPreferences, preferencesCount, status); + + for (int i = 0; i < preferencesCount; ++i) { + const auto &preference = *unitPreferences[i]; + + MeasureUnit complexTargetUnit = MeasureUnit::forIdentifier(preference.unit.data(), status); + if (U_FAILURE(status)) { + return; + } + + converterPreferences_.emplaceBackAndCheckErrorCode(status, inputUnit, complexTargetUnit, + preference.geq, conversionRates, status); + if (U_FAILURE(status)) { + return; + } + } +} + +MaybeStackVector UnitsRouter::route(double quantity, UErrorCode &status) { + for (int i = 0, n = converterPreferences_.length(); i < n; i++) { + const auto &converterPreference = *converterPreferences_[i]; + + if (converterPreference.converter.greaterThanOrEqual(quantity, converterPreference.limit)) { + return converterPreference.converter.convert(quantity, status); + } + } + + // In case of the `quantity` does not fit in any converter limit, use the last converter. + const auto &lastConverter = (*converterPreferences_[converterPreferences_.length() - 1]).converter; + return lastConverter.convert(quantity, status); +} + +U_NAMESPACE_END + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/unitsrouter.h b/icu4c/source/i18n/unitsrouter.h new file mode 100644 index 00000000000..1b29d9d8f3a --- /dev/null +++ b/icu4c/source/i18n/unitsrouter.h @@ -0,0 +1,88 @@ +// © 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 __UNITSROUTER_H__ +#define __UNITSROUTER_H__ + +#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 "unitsdata.h" + +U_NAMESPACE_BEGIN + +/** + * 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 + * equals 3.0, thus means the converter should not convert to a value less than `3.0 feet`. + * + * NOTE: + * if the limit doest not has a value `i.e. (std::numeric_limits::lowest())`, this mean there + * is no limit for the converter. + */ +struct ConverterPreference : UMemory { + ComplexUnitsConverter converter; + double limit; + + ConverterPreference(MeasureUnit source, MeasureUnit complexTarget, double limit, + const ConversionRates &ratesInfo, UErrorCode &status) + : converter(source, complexTarget, ratesInfo, status), limit(limit) {} + + ConverterPreference(MeasureUnit source, MeasureUnit complexTarget, const ConversionRates &ratesInfo, + UErrorCode &status) + : ConverterPreference(source, complexTarget, std::numeric_limits::lowest(), ratesInfo, + status) {} +}; + +/** + * `UnitsRouter` responsible for converting from a single unit (such as `meter` or `meter-per-second`) to + * one of the complex units based on the limits. + * For example: + * if the input is `meter` and the output as following + * {`foot+inch`, limit: 3.0} + * {`inch` , limit: no value (-inf)} + * Thus means if the input in `meter` is greater than or equal to `3.0 feet`, the output will be in + * `foot+inch`, otherwise, the output will be in `inch`. + * + * NOTE: + * the output units and the their limits MUST BE in order, for example, if the output units, from the + * previous example, are the following: + * {`inch` , limit: no value (-inf)} + * {`foot+inch`, limit: 3.0} + * IN THIS CASE THE OUTPUT WILL BE ALWAYS IN `inch`. + * + * NOTE: + * the output units and their limits will be extracted from the units preferences database by knowing + * the followings: + * - input unit + * - locale + * - usage + * + * DESIGN: + * `UnitRouter` uses internally `ComplexUnitConverter` in order to convert the input units to the + * desired complex units and to check the limit too. + */ +class U_I18N_API UnitsRouter { + public: + UnitsRouter(MeasureUnit inputUnit, StringPiece locale, StringPiece usage, UErrorCode &status); + + MaybeStackVector route(double quantity, UErrorCode &status); + + private: + MaybeStackVector converterPreferences_; +}; + +U_NAMESPACE_END + +#endif //__UNITSROUTER_H__ + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/depstest/dependencies.txt b/icu4c/source/test/depstest/dependencies.txt index e7db664840f..480f07f5ff1 100644 --- a/icu4c/source/test/depstest/dependencies.txt +++ b/icu4c/source/test/depstest/dependencies.txt @@ -1074,9 +1074,9 @@ group: units stringenumeration errorcode group: unitsformatter - unitsdata.o unitconverter.o + unitsdata.o unitconverter.o complexunitsconverter.o unitsrouter.o deps - resourcebundle units_extra double_conversion number_representation + resourcebundle units_extra double_conversion number_representation formattable group: decnumber decContext.o decNumber.o diff --git a/icu4c/source/test/intltest/Makefile.in b/icu4c/source/test/intltest/Makefile.in index 74a6f907564..cb356c44db9 100644 --- a/icu4c/source/test/intltest/Makefile.in +++ b/icu4c/source/test/intltest/Makefile.in @@ -69,7 +69,7 @@ string_segment_test.o \ numbertest_parse.o numbertest_doubleconversion.o numbertest_skeletons.o \ static_unisets_test.o numfmtdatadriventest.o numbertest_range.o erarulestest.o \ formattedvaluetest.o formatted_string_builder_test.o numbertest_permutation.o \ -unitsdatatest.o unitstest.o +unitsdatatest.o unitstest.o unitsroutertest.o DEPS = $(OBJECTS:.o=.d) diff --git a/icu4c/source/test/intltest/intltest.vcxproj b/icu4c/source/test/intltest/intltest.vcxproj index 4fdaa2359a3..e6cee37ad3d 100644 --- a/icu4c/source/test/intltest/intltest.vcxproj +++ b/icu4c/source/test/intltest/intltest.vcxproj @@ -285,7 +285,9 @@ + +
diff --git a/icu4c/source/test/intltest/intltest.vcxproj.filters b/icu4c/source/test/intltest/intltest.vcxproj.filters index f068ff80e7f..bba4e921982 100644 --- a/icu4c/source/test/intltest/intltest.vcxproj.filters +++ b/icu4c/source/test/intltest/intltest.vcxproj.filters @@ -550,9 +550,15 @@ locales & resources + + formatting + formatting + + formatting + diff --git a/icu4c/source/test/intltest/itformat.cpp b/icu4c/source/test/intltest/itformat.cpp index bd804888fbf..a2b5ffec67a 100644 --- a/icu4c/source/test/intltest/itformat.cpp +++ b/icu4c/source/test/intltest/itformat.cpp @@ -76,6 +76,7 @@ extern IntlTest *createFormattedStringBuilderTest(); extern IntlTest *createStringSegmentTest(); extern IntlTest *createUnitsDataTest(); extern IntlTest *createUnitsTest(); +extern IntlTest *createUnitsRouterTest(); #define TESTCLASS(id, TestClass) \ @@ -267,6 +268,15 @@ void IntlTestFormat::runIndexedTest( int32_t index, UBool exec, const char* &nam callTest(*test, par); } break; + case 58: + name = "UnitsRouterTest"; + if (exec) { + logln("UnitsRouterTest test---"); + logln((UnicodeString)""); + LocalPointer test(createUnitsRouterTest()); + callTest(*test, par); + } + break; default: name = ""; break; //needed to end loop } if (exec) { diff --git a/icu4c/source/test/intltest/unitsdatatest.cpp b/icu4c/source/test/intltest/unitsdatatest.cpp index 012784ca4c6..d79ca782bb7 100644 --- a/icu4c/source/test/intltest/unitsdatatest.cpp +++ b/icu4c/source/test/intltest/unitsdatatest.cpp @@ -114,7 +114,7 @@ void UnitsDataTest::testGetPreferencesFor() { {"GB area", "area", "default", "GB", "square-mile", "square-inch"}, {"001 area geograph", "area", "geograph", "001", "square-kilometer", "square-kilometer"}, {"GB area geograph", "area", "geograph", "GB", "square-mile", "square-mile"}, - {"CA person-height", "length", "person-height", "CA", "foot-and-inch", "foot-and-inch"}, + {"CA person-height", "length", "person-height", "CA", "foot-and-inch", "inch"}, {"AT person-height", "length", "person-height", "AT", "meter-and-centimeter", "meter-and-centimeter"}, }; diff --git a/icu4c/source/test/intltest/unitsroutertest.cpp b/icu4c/source/test/intltest/unitsroutertest.cpp new file mode 100644 index 00000000000..8d2489e577e --- /dev/null +++ b/icu4c/source/test/intltest/unitsroutertest.cpp @@ -0,0 +1,33 @@ +// © 2020 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License + +#include "unicode/utypes.h" + +#if !UCONFIG_NO_FORMATTING + +#include "intltest.h" +#include "unicode/unistr.h" +#include "unitsrouter.h" + + +class UnitsRouterTest : public IntlTest { + public: + UnitsRouterTest() {} + + void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = NULL); + + void testBasic(); +}; + +extern IntlTest *createUnitsRouterTest() { return new UnitsRouterTest(); } + +void UnitsRouterTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) { + if (exec) { logln("TestSuite UnitsRouterTest: "); } + TESTCASE_AUTO_BEGIN; + TESTCASE_AUTO(testBasic); + TESTCASE_AUTO_END; +} + +void UnitsRouterTest::testBasic() { IcuTestErrorCode status(*this, "UnitsRouter testBasic"); } + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/intltest/unitstest.cpp b/icu4c/source/test/intltest/unitstest.cpp index 4931bcde87f..cc31410038e 100644 --- a/icu4c/source/test/intltest/unitstest.cpp +++ b/icu4c/source/test/intltest/unitstest.cpp @@ -5,6 +5,9 @@ #if !UCONFIG_NO_FORMATTING +#include +#include + #include "charstr.h" #include "cmemory.h" #include "filestrm.h" @@ -16,8 +19,16 @@ #include "unicode/unum.h" #include "unitconverter.h" #include "unitsdata.h" +#include "unitsrouter.h" #include "uparse.h" +struct UnitConversionTestCase { + const StringPiece source; + const StringPiece target; + const double inputValue; + const double expectedValue; +}; + using icu::number::impl::DecimalQuantity; class UnitsTest : public IntlTest { @@ -29,7 +40,6 @@ class UnitsTest : public IntlTest { void testConversionCapability(); void testConversions(); void testPreferences(); - void testBasic(); void testSiPrefixes(); void testMass(); void testTemperature(); @@ -39,12 +49,13 @@ class UnitsTest : public IntlTest { extern IntlTest *createUnitsTest() { return new UnitsTest(); } void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) { - if (exec) { logln("TestSuite UnitsTest: "); } + if (exec) { + logln("TestSuite UnitsTest: "); + } TESTCASE_AUTO_BEGIN; TESTCASE_AUTO(testConversionCapability); TESTCASE_AUTO(testConversions); TESTCASE_AUTO(testPreferences); - TESTCASE_AUTO(testBasic); TESTCASE_AUTO(testSiPrefixes); TESTCASE_AUTO(testMass); TESTCASE_AUTO(testTemperature); @@ -81,38 +92,6 @@ void UnitsTest::testConversionCapability() { } } -void UnitsTest::testBasic() { - IcuTestErrorCode status(*this, "Units testBasic"); - - // Test Cases - struct TestCase { - StringPiece source; - StringPiece target; - const double inputValue; - const double expectedValue; - } testCases[]{ - {"meter", "foot", 1.0, 3.28084}, // - {"kilometer", "foot", 1.0, 3280.84}, // - }; - - for (const auto &testCase : testCases) { - UErrorCode status = U_ZERO_ERROR; - - 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); - } -} - void UnitsTest::testSiPrefixes() { IcuTestErrorCode status(*this, "Units testSiPrefixes"); // Test Cases @@ -236,9 +215,17 @@ void UnitsTest::testArea() { const double inputValue; const double expectedValue; } testCases[]{ - {"square-meter", "square-yard", 10.0, 11.9599}, // - {"hectare", "square-yard", 1.0, 11959.9}, // - {"square-mile", "square-foot", 0.0001, 2787.84} // + {"square-meter", "square-yard", 10.0, 11.9599}, // + {"hectare", "square-yard", 1.0, 11959.9}, // + {"square-mile", "square-foot", 0.0001, 2787.84}, // + {"hectare", "square-yard", 1.0, 11959.9}, // + {"hectare", "square-meter", 1.0, 10000}, // + {"hectare", "square-meter", 0.0, 0.0}, // + {"square-mile", "square-foot", 0.0001, 2787.84}, // + {"square-yard", "square-foot", 10, 90}, // + {"square-yard", "square-foot", 0, 0}, // + {"square-yard", "square-foot", 0.000001, 0.000009}, // + {"square-mile", "square-foot", 0.0, 0.0}, // }; for (const auto &testCase : testCases) { @@ -300,9 +287,11 @@ struct UnitsTestContext { * @param pErrorCode Receives status. */ void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, UErrorCode *pErrorCode) { - if (U_FAILURE(*pErrorCode)) { return; } + if (U_FAILURE(*pErrorCode)) { + return; + } UnitsTestContext *ctx = (UnitsTestContext *)context; - UnitsTest* unitsTest = ctx->unitsTest; + UnitsTest *unitsTest = ctx->unitsTest; (void)fieldCount; // unused UParseLineFn variable IcuTestErrorCode status(*unitsTest, "unitsTestDatalineFn"); @@ -312,16 +301,26 @@ void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, U StringPiece commentConversionFormula = trimField(fields[3]); StringPiece utf8Expected = trimField(fields[4]); - UNumberFormat *nf = unum_open(UNUM_DEFAULT, NULL, -1, "en_US", NULL, pErrorCode); + UNumberFormat *nf = unum_open(UNUM_DEFAULT, NULL, -1, "en_US", NULL, status); + if (status.errIfFailureAndReset("unum_open failed")) { + return; + } UnicodeString uExpected = UnicodeString::fromUTF8(utf8Expected); - double expected = unum_parseDouble(nf, uExpected.getBuffer(), uExpected.length(), 0, pErrorCode); + double expected = unum_parseDouble(nf, uExpected.getBuffer(), uExpected.length(), 0, status); unum_close(nf); + if (status.errIfFailureAndReset("unum_parseDouble(\"%s\") failed", utf8Expected)) { + return; + } MeasureUnit sourceUnit = MeasureUnit::forIdentifier(x, status); - if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", x.length(), x.data())) { return; } + if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", x.length(), x.data())) { + return; + } MeasureUnit targetUnit = MeasureUnit::forIdentifier(y, status); - if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", y.length(), y.data())) { return; } + if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", y.length(), y.data())) { + return; + } unitsTest->logln("Quantity (Category): \"%.*s\", " "Expected value of \"1000 %.*s in %.*s\": %f, " @@ -340,7 +339,9 @@ void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, U .append(sourceUnit.getIdentifier(), status) .append(" -> ", status) .append(targetUnit.getIdentifier(), status); - if (status.errIfFailureAndReset("msg construction")) { return; } + if (status.errIfFailureAndReset("msg construction")) { + return; + } unitsTest->assertNotEquals(msg.data(), UNCONVERTIBLE, convertibility); // Conversion: @@ -393,7 +394,7 @@ void UnitsTest::testConversions() { * find a decimal fraction for each output unit. */ class ExpectedOutput { - private: + public: // Counts number of units in the output. When this is more than one, we have // "mixed units" in the expected output. int _compoundCount = 0; @@ -408,7 +409,6 @@ class ExpectedOutput { // The amounts of each of the output units. double _amounts[3]; - public: /** * Parse an expected output field from the test data file. * @@ -445,7 +445,7 @@ class ExpectedOutput { _skippedFields++; if (_skippedFields < 2) { // We are happy skipping one field per output unit: we want to skip - // rational fraction fiels like "11 / 10". + // rational fraction fields like "11 / 10". errorCode = U_ZERO_ERROR; return; } else { @@ -469,9 +469,55 @@ class ExpectedOutput { } }; +// TODO(Hugo): Add a comment and Use AssertEqualsNear. +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); + testMessage.append(msg, 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++) { + testMessage.append(" ", status); + testMessage.append(std::to_string(actual[i]->getNumber().getDouble(status)), status); + testMessage.append(" ", status); + testMessage.appendInvariantChars(actual[i]->getUnit().getIdentifier(), status); + } + + unitsTest->assertTrue(testMessage.data(), success); +} + /** - * WIP(hugovdm): deals with a single data-driven unit test for unit preferences. - * This is a UParseLineFn as required by u_parseDelimitedFile. + * Runs a single data-driven unit test for unit preferences. + * + * This is a UParseLineFn as required by u_parseDelimitedFile, intended for + * parsing unitPreferencesTest.txt. */ void unitPreferencesTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, UErrorCode *pErrorCode) { @@ -491,9 +537,9 @@ void unitPreferencesTestDataLineFn(void *context, char *fields[][2], int32_t fie // Unused // StringPiece inputR = trimField(fields[3]); StringPiece inputD = trimField(fields[4]); StringPiece inputUnit = trimField(fields[5]); - ExpectedOutput output; + ExpectedOutput expected; for (int i = 6; i < fieldCount; i++) { - output.parseOutputField(trimField(fields[i]), status); + expected.parseOutputField(trimField(fields[i]), status); } if (status.errIfFailureAndReset("parsing unitPreferencesTestData.txt test case: %s", fields[0][0])) { return; @@ -512,25 +558,46 @@ void unitPreferencesTestDataLineFn(void *context, char *fields[][2], int32_t fie return; } - // WIP(hugovdm): hook this up to actual tests. - // - // Possible after merging in younies/tryingdouble: - // UnitConverter converter(sourceUnit, targetUnit, *pErrorCode); - // double got = converter.convert(1000, *pErrorCode); - // ((UnitsTest*)context)->assertEqualsNear(quantity.data(), expected, got, 0.0001); - unitsTest->logln("Quantity (Category): \"%.*s\", Usage: \"%.*s\", Region: \"%.*s\", " "Input: \"%f %s\", Expected Output: %s", quantity.length(), quantity.data(), usage.length(), usage.data(), region.length(), region.data(), inputAmount, inputMeasureUnit.getIdentifier(), - output.toDebugString().c_str()); + expected.toDebugString().c_str()); + + if (U_FAILURE(status)) { + return; + } + UnitsRouter router(inputMeasureUnit, region, usage, status); + if (status.errIfFailureAndReset("UnitsRouter(<%s>, \"%.*s\", \"%.*s\", status)", + inputMeasureUnit.getIdentifier(), region.length(), region.data(), + usage.length(), usage.data())) { + return; + } + + CharString msg(quantity, status); + msg.append(" ", status); + msg.append(usage, status); + msg.append(" ", status); + msg.append(region, status); + msg.append(" ", status); + msg.append(inputD, status); + msg.append(" ", status); + msg.append(inputMeasureUnit.getIdentifier(), status); + if (status.errIfFailureAndReset("Failure before router.route")) { + return; + } + MaybeStackVector result = router.route(inputAmount, status); + if (status.errIfFailureAndReset("router.route(inputAmount, ...)")) { + return; + } + checkOutput(unitsTest, msg.data(), expected, result, 0.0001); } /** * Parses the format used by unitPreferencesTest.txt, calling lineFn for each * line. * - * This is a modified version of u_parseDelimitedFile, customised for + * This is a modified version of u_parseDelimitedFile, customized for * unitPreferencesTest.txt, due to it having a variable number of fields per * line. */ @@ -542,7 +609,9 @@ void parsePreferencesTests(const char *filename, char delimiter, char *fields[][ char *start, *limit; int32_t i; - if (U_FAILURE(*pErrorCode)) { return; } + if (U_FAILURE(*pErrorCode)) { + return; + } if (fields == NULL || lineFn == NULL || maxFieldCount <= 0) { *pErrorCode = U_ILLEGAL_ARGUMENT_ERROR; @@ -568,7 +637,9 @@ void parsePreferencesTests(const char *filename, char delimiter, char *fields[][ *pErrorCode = U_ZERO_ERROR; /* skip this line if it is empty or a comment */ - if (*start == 0 || *start == '#') { continue; } + if (*start == 0 || *start == '#') { + continue; + } /* remove in-line comments */ limit = uprv_strchr(start, '#'); @@ -583,7 +654,9 @@ void parsePreferencesTests(const char *filename, char delimiter, char *fields[][ } /* skip lines with only whitespace */ - if (u_skipWhitespace(start)[0] == 0) { continue; } + if (u_skipWhitespace(start)[0] == 0) { + continue; + } /* for each field, call the corresponding field function */ for (i = 0; i < maxFieldCount; ++i) { @@ -605,15 +678,21 @@ void parsePreferencesTests(const char *filename, char delimiter, char *fields[][ break; } } - if (i == maxFieldCount) { *pErrorCode = U_PARSE_ERROR; } + if (i == maxFieldCount) { + *pErrorCode = U_PARSE_ERROR; + } int fieldCount = i + 1; /* call the field function */ lineFn(context, fields, fieldCount, pErrorCode); - if (U_FAILURE(*pErrorCode)) { break; } + if (U_FAILURE(*pErrorCode)) { + break; + } } - if (filename != NULL) { T_FileStream_close(file); } + if (filename != NULL) { + T_FileStream_close(file); + } } /**