From cdb028edf50a0048c505f3395bdf6b787f8d895b Mon Sep 17 00:00:00 2001 From: younies Date: Tue, 31 Mar 2020 17:16:32 +0200 Subject: [PATCH] ICU-20568 Add unit converter. Add unit converter. PR: https://github.com/sffc/icu/pull/21 Commit: 9bcc4b698ff4b2afbf321188bceff809a27342f2 add comment about ratesInfo param in UnitConverter PR: https://github.com/icu-units/icu/pull/55 Commit: cbed63622771dfc3b3e3c44346f1e530f1b86b65 --- icu4c/source/i18n/unitconverter.cpp | 392 +++++++++++++++++++- icu4c/source/i18n/unitconverter.h | 45 +++ icu4c/source/i18n/unitsdata.cpp | 12 + icu4c/source/test/depstest/dependencies.txt | 2 +- icu4c/source/test/intltest/intltest.cpp | 18 + icu4c/source/test/intltest/intltest.h | 1 + icu4c/source/test/intltest/unitstest.cpp | 356 ++++++++++-------- 7 files changed, 677 insertions(+), 149 deletions(-) diff --git a/icu4c/source/i18n/unitconverter.cpp b/icu4c/source/i18n/unitconverter.cpp index cacf2dba813..516f223a707 100644 --- a/icu4c/source/i18n/unitconverter.cpp +++ b/icu4c/source/i18n/unitconverter.cpp @@ -5,7 +5,10 @@ #if !UCONFIG_NO_FORMATTING +#include + #include "charstr.h" +#include "double-conversion.h" #include "measunit_impl.h" #include "unicode/errorcode.h" #include "unicode/measunit.h" @@ -15,6 +18,164 @@ U_NAMESPACE_BEGIN 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; + +// TODO: Make this a shared-utility function. +// Returns `double` from a scientific number(i.e. "1", "2.01" or "3.09E+4") +double strToDouble(StringPiece strNum, UErrorCode &status) { + // We are processing well-formed input, so we don't need any special options to + // StringToDoubleConverter. + 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; } + + return result; +} + +// Returns `double` from a scientific number that could has a division sign (i.e. "1", "2.01", "3.09E+4" +// or "2E+2/3") +double strHasDivideSignToDouble(StringPiece strWithDivide, UErrorCode &status) { + int divisionSignInd = -1; + for (int i = 0, n = strWithDivide.length(); i < n; ++i) { + if (strWithDivide.data()[i] == '/') { + divisionSignInd = i; + break; + } + } + + if (divisionSignInd >= 0) { + return strToDouble(strWithDivide.substr(0, divisionSignInd), status) / + strToDouble(strWithDivide.substr(divisionSignInd + 1), 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` @@ -32,7 +193,7 @@ MeasureUnit extractCompoundBaseUnit(const MeasureUnit &source, const ConversionR // 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 (U_FAILURE(status)) { return result; } if (rateInfo == nullptr) { status = U_INTERNAL_PROGRAM_ERROR; return result; @@ -56,6 +217,206 @@ MeasureUnit extractCompoundBaseUnit(const MeasureUnit &source, const ConversionR 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". + */ +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) { + StringPiece baseStr; + StringPiece powerStr; + int32_t power = + 1; // In case the power is not written, then, the power is equal 1 ==> `ft2m^1` == `ft2m` + + // Search for the power part + int32_t powerInd = -1; + for (int32_t i = 0, n = elementStr.length(); i < n; ++i) { + if (elementStr.data()[i] == '^') { + powerInd = i; + break; + } + } + + if (powerInd > -1) { + // There is power + baseStr = elementStr.substr(0, powerInd); + powerStr = elementStr.substr(powerInd + 1); + + power = static_cast(strToDouble(powerStr, status)); + } else { + baseStr = elementStr; + } + + addSingleFactorConstant(baseStr, power, sigNum, factor, status); +} + +/* + * Extracts `Factor` from a complete string factor. e.g. "ft2m^3*1007/cup2m3*3" + */ +Factor extractFactorConversions(StringPiece stringFactor, UErrorCode &status) { + Factor result; + 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); + + 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); + } + + if (factorData[i] == '/') { + sigNum = SigNum::NEGATIVE; // Change the sigNum because we reached the Denominator. + } + } + + return result; +} + +// Load factor for a single source +Factor loadSingleFactor(StringPiece source, const ConversionRates &ratesInfo, UErrorCode &status) { + const auto conversionUnit = ratesInfo.extractConversionInfo(source, status); + if (U_FAILURE(status)) return Factor(); + if (conversionUnit == nullptr) { + status = U_INTERNAL_PROGRAM_ERROR; + return Factor(); + } + + Factor result = extractFactorConversions(conversionUnit->factor.toStringPiece(), status); + result.offset = strHasDivideSignToDouble(conversionUnit->offset.toStringPiece(), status); + + return result; +} + +// Load Factor of a compound source unit. +Factor loadCompoundFactor(const MeasureUnit &source, const ConversionRates &ratesInfo, + UErrorCode &status) { + + Factor result; + MeasureUnitImpl memory; + const auto &compoundSourceUnit = MeasureUnitImpl::forMeasureUnit(source, memory, status); + if (U_FAILURE(status)) return result; + + for (int32_t i = 0, n = compoundSourceUnit.units.length(); i < n; i++) { + auto singleUnit = *compoundSourceUnit.units[i]; // a SingleUnitImpl + + Factor singleFactor = loadSingleFactor(singleUnit.getSimpleUnitID(), ratesInfo, status); + if (U_FAILURE(status)) return result; + + // Apply SiPrefix before the power, because the power may be will flip the factor. + singleFactor.applySiPrefix(singleUnit.siPrefix); + + // Apply the power of the `dimensionality` + singleFactor.power(singleUnit.dimensionality); + + result.multiplyBy(singleFactor); + } + + return result; +} + +/** + * Checks if the source unit and the target unit are simple. For example celsius or fahrenheit. But not + * square-celsius or square-fahrenheit. + */ +UBool checkSimpleUnit(const MeasureUnit &unit, UErrorCode &status) { + MeasureUnitImpl memory; + const auto &compoundSourceUnit = MeasureUnitImpl::forMeasureUnit(unit, memory, status); + if (U_FAILURE(status)) return false; + + if (compoundSourceUnit.complexity != UMEASURE_UNIT_SINGLE) { return false; } + + U_ASSERT(compoundSourceUnit.units.length() == 1); + auto singleUnit = *(compoundSourceUnit.units[0]); + + if (singleUnit.dimensionality != 1 || singleUnit.siPrefix != UMEASURE_SI_PREFIX_ONE) { + return false; + } + return true; +} + +/** + * Extract conversion rate from `source` to `target` + */ +void loadConversionRate(ConversionRate &conversionRate, const MeasureUnit &source, + const MeasureUnit &target, UnitsConvertibilityState unitsState, + const ConversionRates &ratesInfo, UErrorCode &status) { + // Represents the conversion factor from the source to the target. + Factor finalFactor; + + // Represents the conversion factor from the source to the base unit that specified in the conversion + // data which is considered as the root of the source and the target. + Factor sourceToBase = loadCompoundFactor(source, ratesInfo, status); + Factor targetToBase = loadCompoundFactor(target, ratesInfo, status); + + // Merger Factors + finalFactor.multiplyBy(sourceToBase); + if (unitsState == UnitsConvertibilityState::CONVERTIBLE) { + finalFactor.divideBy(targetToBase); + } else if (unitsState == UnitsConvertibilityState::RECIPROCAL) { + finalFactor.multiplyBy(targetToBase); + } else { + status = UErrorCode::U_ARGUMENT_TYPE_MISMATCH; + return; + } + + finalFactor.substituteConstants(); + + conversionRate.factorNum = finalFactor.factorNum; + conversionRate.factorDen = finalFactor.factorDen; + + // In case of simple units (such as: celsius or fahrenheit), offsets are considered. + if (checkSimpleUnit(source, status) && checkSimpleUnit(target, status)) { + conversionRate.sourceOffset = + sourceToBase.offset * sourceToBase.factorDen / sourceToBase.factorNum; + conversionRate.targetOffset = + targetToBase.offset * targetToBase.factorDen / targetToBase.factorNum; + } + + conversionRate.reciprocal = unitsState == UnitsConvertibilityState::RECIPROCAL; +} + } // namespace UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &source, @@ -73,6 +434,35 @@ UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &sourc return UNCONVERTIBLE; } +UnitConverter::UnitConverter(MeasureUnit source, MeasureUnit target, const ConversionRates &ratesInfo, + UErrorCode &status) { + UnitsConvertibilityState unitsState = checkConvertibility(source, target, ratesInfo, status); + if (U_FAILURE(status)) return; + if (unitsState == UnitsConvertibilityState::UNCONVERTIBLE) { + status = U_INTERNAL_PROGRAM_ERROR; + return; + } + + conversionRate_.source = source; + conversionRate_.target = target; + + loadConversionRate(conversionRate_, source, target, unitsState, ratesInfo, status); +} + +double UnitConverter::convert(double inputValue) const { + double result = + inputValue + conversionRate_.sourceOffset; // Reset the input to the target zero index. + // Convert the quantity to from the source scale to the target scale. + result *= conversionRate_.factorNum / conversionRate_.factorDen; + + result -= conversionRate_.targetOffset; // Set the result to its index. + + 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; +} + U_NAMESPACE_END #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/unitconverter.h b/icu4c/source/i18n/unitconverter.h index a7c70c43e54..39a8e81654e 100644 --- a/icu4c/source/i18n/unitconverter.h +++ b/icu4c/source/i18n/unitconverter.h @@ -15,6 +15,19 @@ U_NAMESPACE_BEGIN +/** + * Represents the conversion rate between `source` and `target`. + */ +struct ConversionRate { + MeasureUnit source; + MeasureUnit target; + double factorNum = 1; + double factorDen = 1; + double sourceOffset = 0; + double targetOffset = 0; + bool reciprocal = false; +}; + enum U_I18N_API UnitsConvertibilityState { RECIPROCAL, CONVERTIBLE, @@ -26,6 +39,38 @@ UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &sourc const ConversionRates &conversionRates, UErrorCode &status); +/** + * Converts from a source `MeasureUnit` to a target `MeasureUnit`. + */ +class U_I18N_API UnitConverter { + public: + /** + * Constructor of `UnitConverter`. + * NOTE: + * - source and target must be under the same category + * - e.g. meter to mile --> both of them are length units. + * + * @param source represents the source unit. + * @param target represents the target unit. + * @param ratesInfo Contains all the needed conversion rates. + * @param 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. + * + * @param input_value the value that needs to be converted. + * @param output_value the value that holds the result of the conversion. + * @param status + */ + double convert(double inputValue) const; + + private: + ConversionRate conversionRate_; +}; + U_NAMESPACE_END #endif //__UNITCONVERTER_H__ diff --git a/icu4c/source/i18n/unitsdata.cpp b/icu4c/source/i18n/unitsdata.cpp index c92e151604a..457533277cd 100644 --- a/icu4c/source/i18n/unitsdata.cpp +++ b/icu4c/source/i18n/unitsdata.cpp @@ -15,6 +15,17 @@ U_NAMESPACE_BEGIN namespace { +void trimSpaces(CharString& factor, UErrorCode& status){ + CharString trimmed; + for (int i = 0 ; i < factor.length(); i++) { + if (factor[i] == ' ') continue; + + trimmed.append(factor[i], status); + } + + factor = std::move(trimmed); +} + /** * A ResourceSink that collects conversion rate information. * @@ -84,6 +95,7 @@ class ConversionRateDataSink : public ResourceSink { cr->sourceUnit.append(srcUnit, status); cr->baseUnit.appendInvariantChars(baseUnit, status); cr->factor.appendInvariantChars(factor, status); + trimSpaces(cr->factor, status); if (!offset.isBogus()) cr->offset.appendInvariantChars(offset, status); } } diff --git a/icu4c/source/test/depstest/dependencies.txt b/icu4c/source/test/depstest/dependencies.txt index 04e6485bf77..dcb68bff0e2 100644 --- a/icu4c/source/test/depstest/dependencies.txt +++ b/icu4c/source/test/depstest/dependencies.txt @@ -1076,7 +1076,7 @@ group: units group: unitsformatter unitsdata.o unitconverter.o deps - resourcebundle units_extra + resourcebundle units_extra double_conversion group: decnumber decContext.o decNumber.o diff --git a/icu4c/source/test/intltest/intltest.cpp b/icu4c/source/test/intltest/intltest.cpp index c264e095dcd..38409d3d43a 100644 --- a/icu4c/source/test/intltest/intltest.cpp +++ b/icu4c/source/test/intltest/intltest.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include "unicode/ctest.h" // for str_timeDelta #include "unicode/curramt.h" @@ -2172,6 +2173,23 @@ UBool IntlTest::assertNotEquals(const char* message, return TRUE; } +// http://junit.sourceforge.net/javadoc/org/junit/Assert.html#assertEquals(java.lang.String,%20double,%20double,%20double) +UBool IntlTest::assertEqualsNear(const char *message, double expected, double actual, double precision) { + double diff = std::abs(expected - actual); + double diffPercent = expected != 0? diff / expected : diff; // If the expected is equals zero, we + + if (diffPercent > precision) { + errln((UnicodeString) "FAIL: " + message + "; got " + actual + "; expected " + expected); + return FALSE; + } +#ifdef VERBOSE_ASSERTIONS + else { + logln((UnicodeString) "Ok: " + message + "; got " + expected); + } +#endif + return TRUE; +} + static char ASSERT_BUF[256]; static const char* extractToAssertBuf(const UnicodeString& message) { diff --git a/icu4c/source/test/intltest/intltest.h b/icu4c/source/test/intltest/intltest.h index 59a7679b1e3..8f5bd4a28f0 100644 --- a/icu4c/source/test/intltest/intltest.h +++ b/icu4c/source/test/intltest/intltest.h @@ -300,6 +300,7 @@ public: UBool assertEquals(const char* message, const UnicodeSet& expected, const UnicodeSet& actual); UBool assertEquals(const char* message, const std::vector& expected, const std::vector& actual); + UBool assertEqualsNear(const char* message, double expected, double actual, double precision); #if !UCONFIG_NO_FORMATTING UBool assertEquals(const char* message, const Formattable& expected, const Formattable& actual, UBool possibleDataError=FALSE); diff --git a/icu4c/source/test/intltest/unitstest.cpp b/icu4c/source/test/intltest/unitstest.cpp index d3ecf60a177..4931bcde87f 100644 --- a/icu4c/source/test/intltest/unitstest.cpp +++ b/icu4c/source/test/intltest/unitstest.cpp @@ -29,11 +29,11 @@ class UnitsTest : public IntlTest { void testConversionCapability(); void testConversions(); void testPreferences(); - // void testBasic(); - // void testSiPrefixes(); - // void testMass(); - // void testTemperature(); - // void testArea(); + void testBasic(); + void testSiPrefixes(); + void testMass(); + void testTemperature(); + void testArea(); }; extern IntlTest *createUnitsTest() { return new UnitsTest(); } @@ -44,23 +44,14 @@ void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, cha TESTCASE_AUTO(testConversionCapability); TESTCASE_AUTO(testConversions); TESTCASE_AUTO(testPreferences); - // TESTCASE_AUTO(testBasic); - // TESTCASE_AUTO(testSiPrefixes); - // TESTCASE_AUTO(testMass); - // TESTCASE_AUTO(testTemperature); - // TESTCASE_AUTO(testArea); + TESTCASE_AUTO(testBasic); + TESTCASE_AUTO(testSiPrefixes); + TESTCASE_AUTO(testMass); + TESTCASE_AUTO(testTemperature); + TESTCASE_AUTO(testArea); TESTCASE_AUTO_END; } -// Just for testing quick conversion ability. -double testConvert(UnicodeString source, UnicodeString target, double input) { - if (source == u"meter" && target == u"foot" && input == 1.0) return 3.28084; - - if (source == u"kilometer" && target == u"foot" && input == 1.0) return 328.084; - - return -1; -} - void UnitsTest::testConversionCapability() { struct TestCase { const StringPiece source; @@ -90,122 +81,183 @@ void UnitsTest::testConversionCapability() { } } -// void UnitsTest::testBasic() { -// IcuTestErrorCode status(*this, "Units testBasic"); - -// // Test Cases -// struct TestCase { -// const char16_t *source; -// const char16_t *target; -// const double inputValue; -// const double expectedValue; -// } testCases[]{{u"meter", u"foot", 1.0, 3.28084}, {u"kilometer", u"foot", 1.0, 328.084}}; - -// for (const auto &testCase : testCases) { -// assertEquals("test convert", testConvert(testCase.source, testCase.target, -// testCase.inputValue), -// testCase.expectedValue); -// } -// } - -// void UnitsTest::testSiPrefixes() { -// IcuTestErrorCode status(*this, "Units testSiPrefixes"); -// // Test Cases -// struct TestCase { -// const char16_t *source; -// const char16_t *target; -// const double inputValue; -// const double expectedValue; -// } testCases[]{ -// {u"gram", u"kilogram", 1.0, 0.001}, // -// {u"milligram", u"kilogram", 1.0, 0.000001}, // -// {u"microgram", u"kilogram", 1.0, 0.000000001}, // -// {u"megawatt", u"watt", 1, 1000000}, // -// {u"megawatt", u"kilowatt", 1.0, 1000}, // -// {u"gigabyte", u"byte", 1, 1000000000} // -// }; - -// for (const auto &testCase : testCases) { -// assertEquals("test convert", testConvert(testCase.source, testCase.target, -// testCase.inputValue), -// testCase.expectedValue); -// } -// } - -// void UnitsTest::testMass() { -// IcuTestErrorCode status(*this, "Units testMass"); - -// // Test Cases -// struct TestCase { -// const char16_t *source; -// const char16_t *target; -// const double inputValue; -// const double expectedValue; -// } testCases[]{ -// {u"gram", u"kilogram", 1.0, 0.001}, // -// {u"pound", u"kilogram", 1.0, 0.453592}, // -// {u"pound", u"kilogram", 2.0, 0.907185}, // -// {u"ounce", u"pound", 16.0, 1.0}, // -// {u"ounce", u"kilogram", 16.0, 0.453592}, // -// {u"ton", u"pound", 1.0, 2000}, // -// {u"stone", u"pound", 1.0, 14}, // -// {u"stone", u"kilogram", 1.0, 6.35029} // -// }; - -// for (const auto &testCase : testCases) { -// assertEquals("test convert", testConvert(testCase.source, testCase.target, -// testCase.inputValue), -// testCase.expectedValue); -// } -// } - -// void UnitsTest::testTemperature() { -// IcuTestErrorCode status(*this, "Units testTemperature"); -// // Test Cases -// struct TestCase { -// const char16_t *source; -// const char16_t *target; -// const double inputValue; -// const double expectedValue; -// } testCases[]{ -// {u"celsius", u"fahrenheit", 0.0, 32.0}, // -// {u"celsius", u"fahrenheit", 10.0, 50.0}, // -// {u"fahrenheit", u"celsius", 32.0, 0.0}, // -// {u"fahrenheit", u"celsius", 89.6, 32}, // -// {u"kelvin", u"fahrenheit", 0.0, -459.67}, // -// {u"kelvin", u"fahrenheit", 300, 80.33}, // -// {u"kelvin", u"celsius", 0.0, -273.15}, // -// {u"kelvin", u"celsius", 300.0, 26.85} // -// }; - -// for (const auto &testCase : testCases) { -// assertEquals("test convert", testConvert(testCase.source, testCase.target, -// testCase.inputValue), -// testCase.expectedValue); -// } -// } - -// void UnitsTest::testArea() { -// IcuTestErrorCode status(*this, "Units Area"); - -// // Test Cases -// struct TestCase { -// const char16_t *source; -// const char16_t *target; -// const double inputValue; -// const double expectedValue; -// } testCases[]{ -// {u"square-meter", u"square-yard", 10.0, 11.9599}, // -// {u"hectare", u"square-yard", 1.0, 11959.9}, // -// {u"square-mile", u"square-foot", 0.0001, 2787.84} // -// }; - -// for (const auto &testCase : testCases) { -// assertEquals("test convert", testConvert(testCase.source, testCase.target, -// testCase.inputValue), -// testCase.expectedValue); -// } -// } +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 + struct TestCase { + StringPiece source; + StringPiece target; + const double inputValue; + const double expectedValue; + } testCases[]{ + {"gram", "kilogram", 1.0, 0.001}, // + {"milligram", "kilogram", 1.0, 0.000001}, // + {"microgram", "kilogram", 1.0, 0.000000001}, // + {"megagram", "gram", 1.0, 1000000}, // + {"megagram", "kilogram", 1.0, 1000}, // + {"gigabyte", "byte", 1.0, 1000000000}, // + // TODO: Fix `watt` probelms. + // {"megawatt", "watt", 1.0, 1000000}, // + // {"megawatt", "kilowatt", 1.0, 1000}, // + }; + + 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::testMass() { + IcuTestErrorCode status(*this, "Units testMass"); + + // Test Cases + struct TestCase { + StringPiece source; + StringPiece target; + const double inputValue; + const double expectedValue; + } testCases[]{ + {"gram", "kilogram", 1.0, 0.001}, // + {"pound", "kilogram", 1.0, 0.453592}, // + {"pound", "kilogram", 2.0, 0.907185}, // + {"ounce", "pound", 16.0, 1.0}, // + {"ounce", "kilogram", 16.0, 0.453592}, // + {"ton", "pound", 1.0, 2000}, // + {"stone", "pound", 1.0, 14}, // + {"stone", "kilogram", 1.0, 6.35029} // + }; + + 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::testTemperature() { + IcuTestErrorCode status(*this, "Units testTemperature"); + // Test Cases + struct TestCase { + StringPiece source; + StringPiece target; + const double inputValue; + const double expectedValue; + } testCases[]{ + {"celsius", "fahrenheit", 0.0, 32.0}, // + {"celsius", "fahrenheit", 10.0, 50.0}, // + {"fahrenheit", "celsius", 32.0, 0.0}, // + {"fahrenheit", "celsius", 89.6, 32}, // + {"kelvin", "fahrenheit", 0.0, -459.67}, // + {"kelvin", "fahrenheit", 300, 80.33}, // + {"kelvin", "celsius", 0.0, -273.15}, // + {"kelvin", "celsius", 300.0, 26.85} // + }; + + 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::testArea() { + IcuTestErrorCode status(*this, "Units Area"); + + // Test Cases + struct TestCase { + StringPiece source; + StringPiece target; + 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} // + }; + + 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); + } +} /** * Trims whitespace (spaces only) off of the specified string. @@ -233,10 +285,19 @@ struct UnitsTestContext { }; /** - * WIP(hugovdm): deals with a single data-driven unit test for unit conversions. - * This is a UParseLineFn as required by u_parseDelimitedFile. + * Deals with a single data-driven unit test for unit conversions. + * + * This is a UParseLineFn as required by u_parseDelimitedFile, intended for + * parsing unitsTest.txt. * - * context must point at a UnitsTestContext struct. + * @param context Must point at a UnitsTestContext struct. + * @param fields A list of pointer-pairs, each pair pointing at the start and + * end of each field. End pointers are important because these are *not* + * null-terminated strings. (Interpreted as a null-terminated string, + * fields[0][0] points at the whole line.) + * @param fieldCount The number of fields (pointer pairs) passed to the fields + * parameter. + * @param pErrorCode Receives status. */ void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, UErrorCode *pErrorCode) { if (U_FAILURE(*pErrorCode)) { return; } @@ -282,15 +343,16 @@ void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, U if (status.errIfFailureAndReset("msg construction")) { return; } unitsTest->assertNotEquals(msg.data(), UNCONVERTIBLE, convertibility); - // TODO(hugovdm,younies): the following code can be uncommented (and - // fixed) once merged with a UnitConverter branch: - // UnitConverter converter(sourceUnit, targetUnit, unitsTest->conversionRates_, status); - // if (status.errIfFailureAndReset("constructor: UnitConverter(<%s>, <%s>, status)", - // sourceUnit.getIdentifier(), targetUnit.getIdentifier())) { - // return; - // } - // double got = converter.convert(1000); - // unitsTest->assertEqualsNear(fields[0][0], expected, got, 0.0001); + // Conversion: + UnitConverter converter(sourceUnit, targetUnit, *ctx->conversionRates, status); + if (status.errIfFailureAndReset("constructor: UnitConverter(<%s>, <%s>, status)", + sourceUnit.getIdentifier(), targetUnit.getIdentifier())) { + return; + } + 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); } /** -- 2.40.0