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
diff --git a/icu4c/source/i18n/unitconverter.cpp b/icu4c/source/i18n/unitconverter.cpp
index cacf2db..516f223 100644
--- a/icu4c/source/i18n/unitconverter.cpp
+++ b/icu4c/source/i18n/unitconverter.cpp
@@ -5,7 +5,10 @@
#if !UCONFIG_NO_FORMATTING
+#include <cmath>
+
#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 @@
// 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 @@
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<int32_t>(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 @@
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 a7c70c4..39a8e81 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 @@
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 c92e151..4575332 100644
--- a/icu4c/source/i18n/unitsdata.cpp
+++ b/icu4c/source/i18n/unitsdata.cpp
@@ -15,6 +15,17 @@
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 @@
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 04e6485..dcb68bf 100644
--- a/icu4c/source/test/depstest/dependencies.txt
+++ b/icu4c/source/test/depstest/dependencies.txt
@@ -1076,7 +1076,7 @@
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 c264e09..38409d3 100644
--- a/icu4c/source/test/intltest/intltest.cpp
+++ b/icu4c/source/test/intltest/intltest.cpp
@@ -19,6 +19,7 @@
#include <stdlib.h>
#include <string.h>
#include <cmath>
+#include <math.h>
#include "unicode/ctest.h" // for str_timeDelta
#include "unicode/curramt.h"
@@ -2172,6 +2173,23 @@
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 59a7679..8f5bd4a 100644
--- a/icu4c/source/test/intltest/intltest.h
+++ b/icu4c/source/test/intltest/intltest.h
@@ -300,6 +300,7 @@
UBool assertEquals(const char* message, const UnicodeSet& expected, const UnicodeSet& actual);
UBool assertEquals(const char* message,
const std::vector<std::string>& expected, const std::vector<std::string>& 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 d3ecf60..4931bcd 100644
--- a/icu4c/source/test/intltest/unitstest.cpp
+++ b/icu4c/source/test/intltest/unitstest.cpp
@@ -29,11 +29,11 @@
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 @@
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::testBasic() {
-// IcuTestErrorCode status(*this, "Units testBasic");
+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}};
+ // 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) {
-// assertEquals("test convert", testConvert(testCase.source, testCase.target,
-// testCase.inputValue),
-// testCase.expectedValue);
-// }
-// }
+ for (const auto &testCase : testCases) {
+ UErrorCode status = U_ZERO_ERROR;
-// 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} //
-// };
+ MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
+ MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
-// for (const auto &testCase : testCases) {
-// assertEquals("test convert", testConvert(testCase.source, testCase.target,
-// testCase.inputValue),
-// testCase.expectedValue);
-// }
-// }
+ MaybeStackVector<MeasureUnit> units;
+ units.emplaceBack(source);
+ units.emplaceBack(target);
-// void UnitsTest::testMass() {
-// IcuTestErrorCode status(*this, "Units testMass");
+ ConversionRates conversionRates(status);
+ UnitConverter converter(source, target, conversionRates, status);
-// // 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} //
-// };
+ assertEqualsNear("test conversion", testCase.expectedValue,
+ converter.convert(testCase.inputValue), 0.001);
+ }
+}
-// 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 {
+ 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}, //
+ };
-// 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) {
+ UErrorCode status = U_ZERO_ERROR;
-// for (const auto &testCase : testCases) {
-// assertEquals("test convert", testConvert(testCase.source, testCase.target,
-// testCase.inputValue),
-// testCase.expectedValue);
-// }
-// }
+ MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
+ MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
-// void UnitsTest::testArea() {
-// IcuTestErrorCode status(*this, "Units Area");
+ MaybeStackVector<MeasureUnit> units;
+ units.emplaceBack(source);
+ units.emplaceBack(target);
-// // 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} //
-// };
+ ConversionRates conversionRates(status);
+ UnitConverter converter(source, target, conversionRates, status);
-// for (const auto &testCase : testCases) {
-// assertEquals("test convert", testConvert(testCase.source, testCase.target,
-// testCase.inputValue),
-// testCase.expectedValue);
-// }
-// }
+ 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<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::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<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::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<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);
+ }
+}
/**
* Trims whitespace (spaces only) off of the specified string.
@@ -233,10 +285,19 @@
};
/**
- * 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.
*
- * context must point at a UnitsTestContext struct.
+ * This is a UParseLineFn as required by u_parseDelimitedFile, intended for
+ * parsing unitsTest.txt.
+ *
+ * @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 @@
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);
}
/**