--- /dev/null
+// © 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 <math.h>
+#include <utility>
+
+#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<MeasureUnit> singleUnitsInOrder;
+ for (int i = 0; i < length; ++i) {
+ /**
+ * TODO(younies): ensure units being in order by the biggest unit at first.
+ *
+ * HINT:
+ * MaybeStackVector<SingleUnitImpl> 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<Measure> ComplexUnitsConverter::convert(double quantity, UErrorCode &status) const {
+ MaybeStackVector<Measure> 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 */
--- /dev/null
+// © 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<Measure> convert(double quantity, UErrorCode &status) const;
+
+ private:
+ MaybeStackVector<UnitConverter> unitConverters_;
+ MaybeStackVector<MeasureUnit> units_;
+};
+
+U_NAMESPACE_END
+
+#endif //__COMPLEXUNITSCONVERTER_H__
+
+#endif /* #if !UCONFIG_NO_FORMATTING */
<ClCompile Include="ulocdata.cpp" />
<ClCompile Include="umsg.cpp" />
<ClCompile Include="unitconverter.cpp" />
+ <ClCompile Include="complexunitsconverter.cpp" />
+ <ClCompile Include="unitsrouter.cpp" />
<ClCompile Include="unitsdata.cpp" />
<ClCompile Include="unum.cpp" />
<ClCompile Include="unumsys.cpp" />
<ClInclude Include="numrange_impl.h" />
<ClInclude Include="formattedval_impl.h" />
<ClInclude Include="unitconverter.h" />
+ <ClInclude Include="complexunitsconverter.h" />
+ <ClInclude Include="unitsrouter.h" />
<ClInclude Include="unitsdata.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="unitconverter.cpp">
<Filter>formatting</Filter>
</ClCompile>
+ <ClCompile Include="complexunitsconverter.cpp">
+ <Filter>formatting</Filter>
+ </ClCompile>
+ <ClCompile Include="unitsrouter.cpp">
+ <Filter>formatting</Filter>
+ </ClCompile>
<ClCompile Include="unitsdata.cpp">
<Filter>formatting</Filter>
</ClCompile>
<ClInclude Include="unitconveter.h">
<Filter>formatting</Filter>
</ClInclude>
+ <ClInclude Include="complexunitsconverter.h">
+ <Filter>formatting</Filter>
+ </ClInclude>
+ <ClInclude Include="unitsrouter.h">
+ <Filter>formatting</Filter>
+ </ClInclude>
<ClInclude Include="unitsdata.h">
<Filter>formatting</Filter>
</ClInclude>
<Filter>misc</Filter>
</ResourceCompile>
</ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
<ClCompile Include="ulocdata.cpp" />
<ClCompile Include="umsg.cpp" />
<ClCompile Include="unitconverter.cpp" />
+ <ClCompile Include="complexunitsconverter.cpp" />
+ <ClCompile Include="unitsrouter.cpp" />
<ClCompile Include="unitsdata.cpp" />
<ClCompile Include="unum.cpp" />
<ClCompile Include="unumsys.cpp" />
<ClInclude Include="numrange_impl.h" />
<ClInclude Include="formattedval_impl.h" />
<ClInclude Include="unitconverter.h" />
+ <ClInclude Include="complexunitsconverter.h" />
+ <ClInclude Include="unitsrouter.h" />
<ClInclude Include="unitsdata.h" />
</ItemGroup>
<ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" Condition="'$(SkipUWP)'!='true'" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
collationtailoring.cpp
collationweights.cpp
compactdecimalformat.cpp
+complexunitsconverter.cpp
coptccal.cpp
cpdtrans.cpp
csdetect.cpp
uni2name.cpp
unitconverter.cpp
unitsdata.cpp
+unitsrouter.cpp
unum.cpp
unumsys.cpp
upluralrules.cpp
* 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.
* 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:
*
* <pre>
* NumberFormatter::with().unit(MeasureUnit::getMeter())
+ * NumberFormatter::with().unit(MeasureUnit::forIdentifier("foot-per-second", status))
* </pre>
*
* Currency:
*/
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
/**
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
/**
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;
}
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;
}
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".
*/
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]);
} // 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);
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) {
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
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,
/**
* 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`.
* @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.
#if !UCONFIG_NO_FORMATTING
+#include "number_decimalquantity.h"
#include "cstring.h"
#include "number_decimalquantity.h"
#include "resource.h"
namespace {
-using number::impl::DecimalQuantity;
+using icu::number::impl::DecimalQuantity;
void trimSpaces(CharString& factor, UErrorCode& status){
CharString trimmed;
// 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,
* 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,
--- /dev/null
+// © 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 <stdio.h>
+#include <utility>
+
+#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<Measure> 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 */
--- /dev/null
+// © 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 <limits>
+
+#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<double>::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<double>::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<Measure> route(double quantity, UErrorCode &status);
+
+ private:
+ MaybeStackVector<ConverterPreference> converterPreferences_;
+};
+
+U_NAMESPACE_END
+
+#endif //__UNITSROUTER_H__
+
+#endif /* #if !UCONFIG_NO_FORMATTING */
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
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)
<ClCompile Include="formattedvaluetest.cpp" />
<ClCompile Include="localebuildertest.cpp" />
<ClCompile Include="localematchertest.cpp" />
+ <ClCompile Include="unitsdatatest.cpp" />
<ClCompile Include="unitstest.cpp" />
+ <ClCompile Include="unitsroutertest.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="colldata.h" />
<ClCompile Include="localematchertest.cpp">
<Filter>locales & resources</Filter>
</ClCompile>
+ <ClCompile Include="unitsdatatest.cpp">
+ <Filter>formatting</Filter>
+ </ClCompile>
<ClCompile Include="unitstest.cpp">
<Filter>formatting</Filter>
</ClCompile>
+ <ClCompile Include="unitsroutertest.cpp">
+ <Filter>formatting</Filter>
+ </ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="itrbbi.h">
extern IntlTest *createStringSegmentTest();
extern IntlTest *createUnitsDataTest();
extern IntlTest *createUnitsTest();
+extern IntlTest *createUnitsRouterTest();
#define TESTCLASS(id, TestClass) \
callTest(*test, par);
}
break;
+ case 58:
+ name = "UnitsRouterTest";
+ if (exec) {
+ logln("UnitsRouterTest test---");
+ logln((UnicodeString)"");
+ LocalPointer<IntlTest> test(createUnitsRouterTest());
+ callTest(*test, par);
+ }
+ break;
default: name = ""; break; //needed to end loop
}
if (exec) {
{"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"},
};
--- /dev/null
+// © 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 */
#if !UCONFIG_NO_FORMATTING
+#include <cmath>
+#include <iostream>
+
#include "charstr.h"
#include "cmemory.h"
#include "filestrm.h"
#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 {
void testConversionCapability();
void testConversions();
void testPreferences();
- void testBasic();
void testSiPrefixes();
void testMass();
void testTemperature();
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);
}
}
-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<MeasureUnit> 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
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) {
* @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");
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, "
.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:
* 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;
// The amounts of each of the output units.
double _amounts[3];
- public:
/**
* Parse an expected output field from the test data file.
*
_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 {
}
};
+// TODO(Hugo): Add a comment and Use AssertEqualsNear.
+void checkOutput(UnitsTest *unitsTest, const char *msg, ExpectedOutput expected,
+ const MaybeStackVector<Measure> &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) {
// 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;
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<Measure> 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.
*/
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;
*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, '#');
}
/* 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) {
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);
+ }
}
/**