| // © 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 <cmath> |
| #include <iostream> |
| |
| #include "charstr.h" |
| #include "cmemory.h" |
| #include "filestrm.h" |
| #include "intltest.h" |
| #include "number_decimalquantity.h" |
| #include "putilimp.h" |
| #include "unicode/ctest.h" |
| #include "unicode/measunit.h" |
| #include "unicode/measure.h" |
| #include "unicode/unistr.h" |
| #include "unicode/unum.h" |
| #include "unicode/ures.h" |
| #include "units_complexconverter.h" |
| #include "units_converter.h" |
| #include "units_data.h" |
| #include "units_router.h" |
| #include "uparse.h" |
| #include "uresimp.h" |
| |
| struct UnitConversionTestCase { |
| const StringPiece source; |
| const StringPiece target; |
| const double inputValue; |
| const double expectedValue; |
| }; |
| |
| using ::icu::number::impl::DecimalQuantity; |
| using namespace ::icu::units; |
| |
| class UnitsTest : public IntlTest { |
| public: |
| UnitsTest() {} |
| |
| void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = NULL); |
| |
| void testUnitConstantFreshness(); |
| void testExtractConvertibility(); |
| void testConversionInfo(); |
| void testConverterWithCLDRTests(); |
| void testComplexUnitsConverter(); |
| void testComplexUnitsConverterSorting(); |
| void testUnitPreferencesWithCLDRTests(); |
| void testConverter(); |
| }; |
| |
| extern IntlTest *createUnitsTest() { return new UnitsTest(); } |
| |
| void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) { |
| if (exec) { |
| logln("TestSuite UnitsTest: "); |
| } |
| TESTCASE_AUTO_BEGIN; |
| TESTCASE_AUTO(testUnitConstantFreshness); |
| TESTCASE_AUTO(testExtractConvertibility); |
| TESTCASE_AUTO(testConversionInfo); |
| TESTCASE_AUTO(testConverterWithCLDRTests); |
| TESTCASE_AUTO(testComplexUnitsConverter); |
| TESTCASE_AUTO(testComplexUnitsConverterSorting); |
| TESTCASE_AUTO(testUnitPreferencesWithCLDRTests); |
| TESTCASE_AUTO(testConverter); |
| TESTCASE_AUTO_END; |
| } |
| |
| // Tests the hard-coded constants in the code against constants that appear in |
| // units.txt. |
| void UnitsTest::testUnitConstantFreshness() { |
| IcuTestErrorCode status(*this, "testUnitConstantFreshness"); |
| LocalUResourceBundlePointer unitsBundle(ures_openDirect(NULL, "units", status)); |
| LocalUResourceBundlePointer unitConstants( |
| ures_getByKey(unitsBundle.getAlias(), "unitConstants", NULL, status)); |
| |
| while (ures_hasNext(unitConstants.getAlias())) { |
| int32_t len; |
| const char *constant = NULL; |
| ures_getNextString(unitConstants.getAlias(), &len, &constant, status); |
| |
| Factor factor; |
| addSingleFactorConstant(constant, 1, POSITIVE, factor, status); |
| if (status.errDataIfFailureAndReset( |
| "addSingleFactorConstant(<%s>, ...).\n\n" |
| "If U_INVALID_FORMAT_ERROR, please check that \"icu4c/source/i18n/units_converter.cpp\" " |
| "has all constants? Is \"%s\" a new constant?\n" |
| "See docs/processes/release/tasks/updating-measure-unit.md for more information.\n", |
| constant, constant)) { |
| continue; |
| } |
| |
| // Check the values of constants that have a simple numeric value |
| factor.substituteConstants(); |
| int32_t uLen; |
| UnicodeString uVal = ures_getStringByKey(unitConstants.getAlias(), constant, &uLen, status); |
| CharString val; |
| val.appendInvariantChars(uVal, status); |
| if (status.errDataIfFailureAndReset("Failed to get constant value for %s.", constant)) { |
| continue; |
| } |
| DecimalQuantity dqVal; |
| UErrorCode parseStatus = U_ZERO_ERROR; |
| // TODO(units): unify with strToDouble() in units_converter.cpp |
| dqVal.setToDecNumber(val.toStringPiece(), parseStatus); |
| if (!U_SUCCESS(parseStatus)) { |
| // Not simple to parse, skip validating this constant's value. (We |
| // leave catching mistakes to the data-driven integration tests.) |
| continue; |
| } |
| double expectedNumerator = dqVal.toDouble(); |
| assertEquals(UnicodeString("Constant ") + constant + u" numerator", expectedNumerator, |
| factor.factorNum); |
| assertEquals(UnicodeString("Constant ") + constant + u" denominator", 1.0, factor.factorDen); |
| } |
| } |
| |
| void UnitsTest::testExtractConvertibility() { |
| IcuTestErrorCode status(*this, "UnitsTest::testExtractConvertibility"); |
| |
| struct TestCase { |
| const char *const source; |
| const char *const target; |
| const Convertibility expectedState; |
| } testCases[]{ |
| {"meter", "foot", CONVERTIBLE}, // |
| {"kilometer", "foot", CONVERTIBLE}, // |
| {"hectare", "square-foot", CONVERTIBLE}, // |
| {"kilometer-per-second", "second-per-meter", RECIPROCAL}, // |
| {"square-meter", "square-foot", CONVERTIBLE}, // |
| {"kilometer-per-second", "foot-per-second", CONVERTIBLE}, // |
| {"square-hectare", "pow4-foot", CONVERTIBLE}, // |
| {"square-kilometer-per-second", "second-per-square-meter", RECIPROCAL}, // |
| {"cubic-kilometer-per-second-meter", "second-per-square-meter", RECIPROCAL}, // |
| {"square-meter-per-square-hour", "hectare-per-square-second", CONVERTIBLE}, // |
| {"hertz", "revolution-per-second", CONVERTIBLE}, // |
| {"millimeter", "meter", CONVERTIBLE}, // |
| {"yard", "meter", CONVERTIBLE}, // |
| {"ounce-troy", "kilogram", CONVERTIBLE}, // |
| {"percent", "portion", CONVERTIBLE}, // |
| {"ofhg", "kilogram-per-square-meter-square-second", CONVERTIBLE}, // |
| {"second-per-meter", "meter-per-second", RECIPROCAL}, // |
| }; |
| |
| for (const auto &testCase : testCases) { |
| MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status); |
| if (status.errIfFailureAndReset("source MeasureUnitImpl::forIdentifier(\"%s\", ...)", |
| testCase.source)) { |
| continue; |
| } |
| MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status); |
| if (status.errIfFailureAndReset("target MeasureUnitImpl::forIdentifier(\"%s\", ...)", |
| testCase.target)) { |
| continue; |
| } |
| |
| ConversionRates conversionRates(status); |
| if (status.errIfFailureAndReset("conversionRates(status)")) { |
| continue; |
| } |
| auto convertibility = extractConvertibility(source, target, conversionRates, status); |
| if (status.errIfFailureAndReset("extractConvertibility(<%s>, <%s>, ...)", testCase.source, |
| testCase.target)) { |
| continue; |
| } |
| |
| assertEquals(UnicodeString("Conversion Capability: ") + testCase.source + " to " + |
| testCase.target, |
| testCase.expectedState, convertibility); |
| } |
| } |
| |
| void UnitsTest::testConversionInfo() { |
| IcuTestErrorCode status(*this, "UnitsTest::testExtractConvertibility"); |
| // Test Cases |
| struct TestCase { |
| const char *source; |
| const char *target; |
| const ConversionInfo expectedConversionInfo; |
| } testCases[]{ |
| { |
| "meter", |
| "meter", |
| {1.0, 0, false}, |
| }, |
| { |
| "meter", |
| "foot", |
| {3.28084, 0, false}, |
| }, |
| { |
| "foot", |
| "meter", |
| {0.3048, 0, false}, |
| }, |
| { |
| "celsius", |
| "kelvin", |
| {1, 273.15, false}, |
| }, |
| { |
| "fahrenheit", |
| "kelvin", |
| {5.0 / 9.0, 255.372, false}, |
| }, |
| { |
| "fahrenheit", |
| "celsius", |
| {5.0 / 9.0, -17.7777777778, false}, |
| }, |
| { |
| "celsius", |
| "fahrenheit", |
| {9.0 / 5.0, 32, false}, |
| }, |
| { |
| "fahrenheit", |
| "fahrenheit", |
| {1.0, 0, false}, |
| }, |
| { |
| "mile-per-gallon", |
| "liter-per-100-kilometer", |
| {0.00425143707, 0, true}, |
| }, |
| }; |
| |
| ConversionRates rates(status); |
| for (const auto &testCase : testCases) { |
| auto sourceImpl = MeasureUnitImpl::forIdentifier(testCase.source, status); |
| auto targetImpl = MeasureUnitImpl::forIdentifier(testCase.target, status); |
| UnitsConverter unitsConverter(sourceImpl, targetImpl, rates, status); |
| |
| if (status.errIfFailureAndReset()) { |
| continue; |
| } |
| |
| ConversionInfo actualConversionInfo = unitsConverter.getConversionInfo(); |
| UnicodeString message = |
| UnicodeString("testConverter: ") + testCase.source + " to " + testCase.target; |
| |
| double maxDelta = 1e-6 * uprv_fabs(testCase.expectedConversionInfo.conversionRate); |
| if (testCase.expectedConversionInfo.conversionRate == 0) { |
| maxDelta = 1e-12; |
| } |
| assertEqualsNear(message + ", conversion rate: ", testCase.expectedConversionInfo.conversionRate, |
| actualConversionInfo.conversionRate, maxDelta); |
| |
| maxDelta = 1e-6 * uprv_fabs(testCase.expectedConversionInfo.offset); |
| if (testCase.expectedConversionInfo.offset == 0) { |
| maxDelta = 1e-12; |
| } |
| assertEqualsNear(message + ", offset: ", testCase.expectedConversionInfo.offset, actualConversionInfo.offset, |
| maxDelta); |
| |
| assertEquals(message + ", reciprocal: ", testCase.expectedConversionInfo.reciprocal, |
| actualConversionInfo.reciprocal); |
| } |
| } |
| |
| void UnitsTest::testConverter() { |
| IcuTestErrorCode status(*this, "UnitsTest::testConverter"); |
| |
| // Test Cases |
| struct TestCase { |
| const char *source; |
| const char *target; |
| const double inputValue; |
| const double expectedValue; |
| } testCases[]{ |
| // SI Prefixes |
| {"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}, |
| {"megawatt", "watt", 1.0, 1000000}, |
| {"megawatt", "kilowatt", 1.0, 1000}, |
| // Binary Prefixes |
| {"kilobyte", "byte", 1, 1000}, |
| {"kibibyte", "byte", 1, 1024}, |
| {"mebibyte", "byte", 1, 1048576}, |
| {"gibibyte", "kibibyte", 1, 1048576}, |
| {"pebibyte", "tebibyte", 4, 4096}, |
| {"zebibyte", "pebibyte", 1.0 / 16, 65536.0}, |
| {"yobibyte", "exbibyte", 1, 1048576}, |
| // Mass |
| {"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}, |
| // Temperature |
| {"celsius", "fahrenheit", 0.0, 32.0}, |
| {"celsius", "fahrenheit", 10.0, 50.0}, |
| {"celsius", "fahrenheit", 1000, 1832}, |
| {"fahrenheit", "celsius", 32.0, 0.0}, |
| {"fahrenheit", "celsius", 89.6, 32}, |
| {"fahrenheit", "fahrenheit", 1000, 1000}, |
| {"kelvin", "fahrenheit", 0.0, -459.67}, |
| {"kelvin", "fahrenheit", 300, 80.33}, |
| {"kelvin", "celsius", 0.0, -273.15}, |
| {"kelvin", "celsius", 300.0, 26.85}, |
| // Area |
| {"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}, |
| // Fuel Consumption |
| {"cubic-meter-per-meter", "mile-per-gallon", 2.1383143939394E-6, 1.1}, |
| {"cubic-meter-per-meter", "mile-per-gallon", 2.6134953703704E-6, 0.9}, |
| }; |
| |
| for (const auto &testCase : testCases) { |
| MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status); |
| if (status.errIfFailureAndReset("source MeasureUnitImpl::forIdentifier(\"%s\", ...)", |
| testCase.source)) { |
| continue; |
| } |
| MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status); |
| if (status.errIfFailureAndReset("target MeasureUnitImpl::forIdentifier(\"%s\", ...)", |
| testCase.target)) { |
| continue; |
| } |
| |
| ConversionRates conversionRates(status); |
| if (status.errIfFailureAndReset("conversionRates(status)")) { |
| continue; |
| } |
| UnitsConverter converter(source, target, conversionRates, status); |
| if (status.errIfFailureAndReset("UnitsConverter(<%s>, <%s>, ...)", testCase.source, |
| testCase.target)) { |
| continue; |
| } |
| |
| double maxDelta = 1e-6 * uprv_fabs(testCase.expectedValue); |
| if (testCase.expectedValue == 0) { |
| maxDelta = 1e-12; |
| } |
| assertEqualsNear(UnicodeString("testConverter: ") + testCase.source + " to " + testCase.target, |
| testCase.expectedValue, converter.convert(testCase.inputValue), maxDelta); |
| |
| maxDelta = 1e-6 * uprv_fabs(testCase.inputValue); |
| if (testCase.inputValue == 0) { |
| maxDelta = 1e-12; |
| } |
| assertEqualsNear( |
| UnicodeString("testConverter inverse: ") + testCase.target + " back to " + testCase.source, |
| testCase.inputValue, converter.convertInverse(testCase.expectedValue), maxDelta); |
| } |
| } |
| |
| /** |
| * Trims whitespace off of the specified string. |
| * @param field is two pointers pointing at the start and end of the string. |
| * @return A StringPiece with initial and final space characters trimmed off. |
| */ |
| StringPiece trimField(char *(&field)[2]) { |
| const char *start = field[0]; |
| start = u_skipWhitespace(start); |
| if (start >= field[1]) { |
| start = field[1]; |
| } |
| const char *end = field[1]; |
| while ((start < end) && U_IS_INV_WHITESPACE(*(end - 1))) { |
| end--; |
| } |
| int32_t length = (int32_t)(end - start); |
| return StringPiece(start, length); |
| } |
| |
| // Used for passing context to unitsTestDataLineFn via u_parseDelimitedFile. |
| struct UnitsTestContext { |
| // Provides access to UnitsTest methods like logln. |
| UnitsTest *unitsTest; |
| // Conversion rates: does not take ownership. |
| ConversionRates *conversionRates; |
| }; |
| |
| /** |
| * 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. |
| * |
| * @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; |
| } |
| UnitsTestContext *ctx = (UnitsTestContext *)context; |
| UnitsTest *unitsTest = ctx->unitsTest; |
| (void)fieldCount; // unused UParseLineFn variable |
| IcuTestErrorCode status(*unitsTest, "unitsTestDatalineFn"); |
| |
| StringPiece quantity = trimField(fields[0]); |
| StringPiece x = trimField(fields[1]); |
| StringPiece y = trimField(fields[2]); |
| StringPiece commentConversionFormula = trimField(fields[3]); |
| StringPiece utf8Expected = trimField(fields[4]); |
| |
| 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, status); |
| unum_close(nf); |
| if (status.errIfFailureAndReset("unum_parseDouble(\"%s\") failed", utf8Expected)) { |
| return; |
| } |
| |
| CharString sourceIdent(x, status); |
| MeasureUnitImpl sourceUnit = MeasureUnitImpl::forIdentifier(x, status); |
| if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", x.length(), x.data())) { |
| return; |
| } |
| |
| CharString targetIdent(y, status); |
| MeasureUnitImpl targetUnit = MeasureUnitImpl::forIdentifier(y, status); |
| if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", y.length(), y.data())) { |
| return; |
| } |
| |
| unitsTest->logln("Quantity (Category): \"%.*s\", " |
| "Expected value of \"1000 %.*s in %.*s\": %f, " |
| "commentConversionFormula: \"%.*s\", ", |
| quantity.length(), quantity.data(), x.length(), x.data(), y.length(), y.data(), |
| expected, commentConversionFormula.length(), commentConversionFormula.data()); |
| |
| // Convertibility: |
| auto convertibility = extractConvertibility(sourceUnit, targetUnit, *ctx->conversionRates, status); |
| if (status.errIfFailureAndReset("extractConvertibility(<%s>, <%s>, ...)", |
| sourceIdent.data(), targetIdent.data())) { |
| return; |
| } |
| CharString msg; |
| msg.append("convertible: ", status) |
| .append(sourceIdent.data(), status) |
| .append(" -> ", status) |
| .append(targetIdent.data(), status); |
| if (status.errIfFailureAndReset("msg construction")) { |
| return; |
| } |
| unitsTest->assertNotEquals(msg.data(), UNCONVERTIBLE, convertibility); |
| |
| // Conversion: |
| UnitsConverter converter(sourceUnit, targetUnit, *ctx->conversionRates, status); |
| if (status.errIfFailureAndReset("UnitsConverter(<%s>, <%s>, ...)", sourceIdent.data(), |
| targetIdent.data())) { |
| 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 * expected); |
| double inverted = converter.convertInverse(got); |
| msg.clear(); |
| msg.append("Converting back to ", status).append(x, status).append(" from ", status).append(y, status); |
| unitsTest->assertEqualsNear(msg.data(), 1000, inverted, 0.0001); |
| } |
| |
| /** |
| * Runs data-driven unit tests for unit conversion. It looks for the test cases |
| * in source/test/testdata/cldr/units/unitsTest.txt, which originates in CLDR. |
| */ |
| void UnitsTest::testConverterWithCLDRTests() { |
| const char *filename = "unitsTest.txt"; |
| const int32_t kNumFields = 5; |
| char *fields[kNumFields][2]; |
| |
| IcuTestErrorCode errorCode(*this, "UnitsTest::testConverterWithCLDRTests"); |
| const char *sourceTestDataPath = getSourceTestData(errorCode); |
| if (errorCode.errIfFailureAndReset("unable to find the source/test/testdata " |
| "folder (getSourceTestData())")) { |
| return; |
| } |
| |
| CharString path(sourceTestDataPath, errorCode); |
| path.appendPathPart("cldr/units", errorCode); |
| path.appendPathPart(filename, errorCode); |
| |
| ConversionRates rates(errorCode); |
| UnitsTestContext ctx = {this, &rates}; |
| u_parseDelimitedFile(path.data(), ';', fields, kNumFields, unitsTestDataLineFn, &ctx, errorCode); |
| if (errorCode.errIfFailureAndReset("error parsing %s: %s\n", path.data(), u_errorName(errorCode))) { |
| return; |
| } |
| } |
| |
| void UnitsTest::testComplexUnitsConverter() { |
| IcuTestErrorCode status(*this, "UnitsTest::testComplexUnitsConverter"); |
| |
| // DBL_EPSILON is aproximately 2.22E-16, and is the precision of double for |
| // values in the range [1.0, 2.0), but half the precision of double for |
| // [2.0, 4.0). |
| U_ASSERT(1.0 + DBL_EPSILON > 1.0); |
| U_ASSERT(2.0 - DBL_EPSILON < 2.0); |
| U_ASSERT(2.0 + DBL_EPSILON == 2.0); |
| |
| struct TestCase { |
| const char* msg; |
| const char* input; |
| const char* output; |
| double value; |
| Measure expected[2]; |
| int32_t expectedCount; |
| // For mixed units, accuracy of the smallest unit |
| double accuracy; |
| } testCases[]{ |
| // Significantly less than 2.0. |
| {"1.9999", |
| "foot", |
| "foot-and-inch", |
| 1.9999, |
| {Measure(1, MeasureUnit::createFoot(status), status), |
| Measure(11.9988, MeasureUnit::createInch(status), status)}, |
| 2, |
| 0}, |
| |
| // A minimal nudge under 2.0, rounding up to 2.0 ft, 0 in. |
| {"2-eps", |
| "foot", |
| "foot-and-inch", |
| 2.0 - DBL_EPSILON, |
| {Measure(2, MeasureUnit::createFoot(status), status), |
| Measure(0, MeasureUnit::createInch(status), status)}, |
| 2, |
| 0}, |
| |
| // A slightly bigger nudge under 2.0, *not* rounding up to 2.0 ft! |
| {"2-3eps", |
| "foot", |
| "foot-and-inch", |
| 2.0 - 3 * DBL_EPSILON, |
| {Measure(1, MeasureUnit::createFoot(status), status), |
| // We expect 12*3*DBL_EPSILON inches (7.92e-15) less than 12. |
| Measure(12 - 36 * DBL_EPSILON, MeasureUnit::createInch(status), status)}, |
| 2, |
| // Might accuracy be lacking with some compilers or on some systems? In |
| // case it is somehow lacking, we'll allow a delta of 12 * DBL_EPSILON. |
| 12 * DBL_EPSILON}, |
| |
| // Testing precision with meter and light-year. |
| // |
| // DBL_EPSILON light-years, ~2.22E-16 light-years, is ~2.1 meters |
| // (maximum precision when exponent is 0). |
| // |
| // 1e-16 light years is 0.946073 meters. |
| |
| // A 2.1 meter nudge under 2.0 light years, rounding up to 2.0 ly, 0 m. |
| {"2-eps", |
| "light-year", |
| "light-year-and-meter", |
| 2.0 - DBL_EPSILON, |
| {Measure(2, MeasureUnit::createLightYear(status), status), |
| Measure(0, MeasureUnit::createMeter(status), status)}, |
| 2, |
| 0}, |
| |
| // A 2.1 meter nudge under 1.0 light years, rounding up to 1.0 ly, 0 m. |
| {"1-eps", |
| "light-year", |
| "light-year-and-meter", |
| 1.0 - DBL_EPSILON, |
| {Measure(1, MeasureUnit::createLightYear(status), status), |
| Measure(0, MeasureUnit::createMeter(status), status)}, |
| 2, |
| 0}, |
| |
| // 1e-15 light years is 9.46073 meters (calculated using "bc" and the |
| // CLDR conversion factor). With double-precision maths in C++, we get |
| // 10.5. In this case, we're off by a bit more than 1 meter. With Java |
| // BigDecimal, we get accurate results. |
| {"1 + 1e-15", |
| "light-year", |
| "light-year-and-meter", |
| 1.0 + 1e-15, |
| {Measure(1, MeasureUnit::createLightYear(status), status), |
| Measure(9.46073, MeasureUnit::createMeter(status), status)}, |
| 2, |
| 1.5 /* meters, precision */}, |
| |
| // 2.1 meters more than 1 light year is not rounded away. |
| {"1 + eps", |
| "light-year", |
| "light-year-and-meter", |
| 1.0 + DBL_EPSILON, |
| {Measure(1, MeasureUnit::createLightYear(status), status), |
| Measure(2.1, MeasureUnit::createMeter(status), status)}, |
| 2, |
| 0.001}, |
| }; |
| status.assertSuccess(); |
| |
| ConversionRates rates(status); |
| MeasureUnit input, output; |
| MeasureUnitImpl tempInput, tempOutput; |
| MaybeStackVector<Measure> measures; |
| for (const TestCase &testCase : testCases) { |
| input = MeasureUnit::forIdentifier(testCase.input, status); |
| output = MeasureUnit::forIdentifier(testCase.output, status); |
| const MeasureUnitImpl& inputImpl = MeasureUnitImpl::forMeasureUnit(input, tempInput, status); |
| const MeasureUnitImpl& outputImpl = MeasureUnitImpl::forMeasureUnit(output, tempOutput, status); |
| auto converter = ComplexUnitsConverter(inputImpl, outputImpl, rates, status); |
| measures = converter.convert(testCase.value, nullptr, status); |
| |
| CharString msg; |
| msg.append(testCase.msg, status); |
| msg.append(" ", status); |
| msg.append(testCase.input, status); |
| msg.append(" -> ", status); |
| msg.append(testCase.output, status); |
| |
| CharString msgCount(msg, status); |
| msgCount.append(", measures.length()", status); |
| assertEquals(msgCount.data(), testCase.expectedCount, measures.length()); |
| for (int i = 0; i < measures.length() && i < testCase.expectedCount; i++) { |
| if (i == testCase.expectedCount-1) { |
| assertEqualsNear(msg.data(), testCase.expected[i].getNumber().getDouble(status), |
| measures[i]->getNumber().getDouble(status), testCase.accuracy); |
| } else { |
| assertEquals(msg.data(), testCase.expected[i].getNumber().getDouble(status), |
| measures[i]->getNumber().getDouble(status)); |
| } |
| assertEquals(msg.data(), testCase.expected[i].getUnit().getIdentifier(), |
| measures[i]->getUnit().getIdentifier()); |
| } |
| } |
| status.assertSuccess(); |
| |
| // TODO(icu-units#63): test negative numbers! |
| } |
| |
| void UnitsTest::testComplexUnitsConverterSorting() { |
| IcuTestErrorCode status(*this, "UnitsTest::testComplexUnitsConverterSorting"); |
| ConversionRates conversionRates(status); |
| |
| status.assertSuccess(); |
| |
| struct TestCase { |
| const char *msg; |
| const char *input; |
| const char *output; |
| double inputValue; |
| Measure expected[3]; |
| int32_t expectedCount; |
| // For mixed units, accuracy of the smallest unit |
| double accuracy; |
| } testCases[]{{"inch-and-foot", |
| "meter", |
| "inch-and-foot", |
| 10.0, |
| { |
| Measure(9.70079, MeasureUnit::createInch(status), status), |
| Measure(32, MeasureUnit::createFoot(status), status), |
| Measure(0, MeasureUnit::createBit(status), status), |
| }, |
| 2, |
| 0.00001}, |
| {"inch-and-yard-and-foot", |
| "meter", |
| "inch-and-yard-and-foot", |
| 100.0, |
| { |
| Measure(1.0079, MeasureUnit::createInch(status), status), |
| Measure(109, MeasureUnit::createYard(status), status), |
| Measure(1, MeasureUnit::createFoot(status), status), |
| }, |
| 3, |
| 0.0001}}; |
| |
| for (const auto &testCase : testCases) { |
| MeasureUnitImpl inputImpl = MeasureUnitImpl::forIdentifier(testCase.input, status); |
| MeasureUnitImpl outputImpl = MeasureUnitImpl::forIdentifier(testCase.output, status); |
| ComplexUnitsConverter converter(inputImpl, outputImpl, conversionRates, status); |
| |
| auto actual = converter.convert(testCase.inputValue, nullptr, status); |
| |
| for (int i = 0; i < testCase.expectedCount; i++) { |
| assertEquals(testCase.msg, testCase.expected[i].getUnit().getIdentifier(), |
| actual[i]->getUnit().getIdentifier()); |
| |
| if (testCase.expected[i].getNumber().getType() == Formattable::Type::kInt64) { |
| assertEquals(testCase.msg, testCase.expected[i].getNumber().getInt64(), |
| actual[i]->getNumber().getInt64()); |
| } else { |
| assertEqualsNear(testCase.msg, testCase.expected[i].getNumber().getDouble(), |
| actual[i]->getNumber().getDouble(), testCase.accuracy); |
| } |
| } |
| } |
| |
| MeasureUnitImpl source = MeasureUnitImpl::forIdentifier("meter", status); |
| MeasureUnitImpl target = MeasureUnitImpl::forIdentifier("inch-and-foot", status); |
| |
| ComplexUnitsConverter complexConverter(source, target, conversionRates, status); |
| auto measures = complexConverter.convert(10.0, nullptr, status); |
| |
| if (2 == measures.length()) { |
| assertEquals("inch-and-foot unit 0", "inch", measures[0]->getUnit().getIdentifier()); |
| assertEquals("inch-and-foot unit 1", "foot", measures[1]->getUnit().getIdentifier()); |
| |
| assertEqualsNear("inch-and-foot value 0", 9.7008, measures[0]->getNumber().getDouble(), 0.0001); |
| assertEqualsNear("inch-and-foot value 1", 32, measures[1]->getNumber().getInt64(), 0.00001); |
| } |
| } |
| |
| /** |
| * This class represents the output fields from unitPreferencesTest.txt. Please |
| * see the documentation at the top of that file for details. |
| * |
| * For "mixed units" output, there are more (repeated) output fields. The last |
| * output unit has the expected output specified as both a rational fraction and |
| * a decimal fraction. This class ignores rational fractions, and expects to |
| * find a decimal fraction for each output unit. |
| */ |
| class ExpectedOutput { |
| 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; |
| |
| // Counts how many fields were skipped: we expect to skip only one per |
| // output unit type (the rational fraction). |
| int _skippedFields = 0; |
| |
| // The expected output units: more than one for "mixed units". |
| MeasureUnit _measureUnits[3]; |
| |
| // The amounts of each of the output units. |
| double _amounts[3]; |
| |
| /** |
| * Parse an expected output field from the test data file. |
| * |
| * @param output may be a string representation of an integer, a rational |
| * fraction, a decimal fraction, or it may be a unit identifier. Whitespace |
| * should already be trimmed. This function ignores rational fractions, |
| * saving only decimal fractions and their unit identifiers. |
| * @return true if the field was successfully parsed, false if parsing |
| * failed. |
| */ |
| void parseOutputField(StringPiece output, UErrorCode &errorCode) { |
| if (U_FAILURE(errorCode)) return; |
| DecimalQuantity dqOutputD; |
| |
| dqOutputD.setToDecNumber(output, errorCode); |
| if (U_SUCCESS(errorCode)) { |
| _amounts[_compoundCount] = dqOutputD.toDouble(); |
| return; |
| } else if (errorCode == U_DECIMAL_NUMBER_SYNTAX_ERROR) { |
| // Not a decimal fraction, it might be a rational fraction or a unit |
| // identifier: continue. |
| errorCode = U_ZERO_ERROR; |
| } else { |
| // Unexpected error, so we propagate it. |
| return; |
| } |
| |
| _measureUnits[_compoundCount] = MeasureUnit::forIdentifier(output, errorCode); |
| if (U_SUCCESS(errorCode)) { |
| _compoundCount++; |
| _skippedFields = 0; |
| return; |
| } |
| _skippedFields++; |
| if (_skippedFields < 2) { |
| // We are happy skipping one field per output unit: we want to skip |
| // rational fraction fields like "11 / 10". |
| errorCode = U_ZERO_ERROR; |
| return; |
| } else { |
| // Propagate the error. |
| return; |
| } |
| } |
| |
| /** |
| * Produces an output string for debug purposes. |
| */ |
| std::string toDebugString() { |
| std::string result; |
| for (int i = 0; i < _compoundCount; i++) { |
| result += std::to_string(_amounts[i]); |
| result += " "; |
| result += _measureUnits[i].getIdentifier(); |
| result += " "; |
| } |
| return result; |
| } |
| }; |
| |
| // Checks a vector of Measure instances against ExpectedOutput. |
| void checkOutput(UnitsTest *unitsTest, const char *msg, ExpectedOutput expected, |
| const MaybeStackVector<Measure> &actual, double precision) { |
| IcuTestErrorCode status(*unitsTest, "checkOutput"); |
| |
| 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); |
| } |
| if (!unitsTest->assertEquals(testMessage.data(), expected._compoundCount, actual.length())) { |
| return; |
| }; |
| for (int i = 0; i < actual.length(); i++) { |
| double permittedDiff = precision * expected._amounts[i]; |
| if (permittedDiff == 0) { |
| // If 0 is expected, still permit a small delta. |
| // TODO: revisit this experimentally chosen value: |
| permittedDiff = 0.00000001; |
| } |
| unitsTest->assertEqualsNear(testMessage.data(), expected._amounts[i], |
| actual[i]->getNumber().getDouble(status), permittedDiff); |
| } |
| } |
| |
| /** |
| * 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) { |
| if (U_FAILURE(*pErrorCode)) return; |
| UnitsTest *unitsTest = (UnitsTest *)context; |
| IcuTestErrorCode status(*unitsTest, "unitPreferencesTestDatalineFn"); |
| |
| if (!unitsTest->assertTrue(u"unitPreferencesTestDataLineFn expects 9 fields for simple and 11 " |
| u"fields for compound. Other field counts not yet supported. ", |
| fieldCount == 9 || fieldCount == 11)) { |
| return; |
| } |
| |
| StringPiece quantity = trimField(fields[0]); |
| StringPiece usage = trimField(fields[1]); |
| StringPiece region = trimField(fields[2]); |
| // Unused // StringPiece inputR = trimField(fields[3]); |
| StringPiece inputD = trimField(fields[4]); |
| StringPiece inputUnit = trimField(fields[5]); |
| ExpectedOutput expected; |
| for (int i = 6; i < fieldCount; i++) { |
| expected.parseOutputField(trimField(fields[i]), status); |
| } |
| if (status.errIfFailureAndReset("parsing unitPreferencesTestData.txt test case: %s", fields[0][0])) { |
| return; |
| } |
| |
| DecimalQuantity dqInputD; |
| dqInputD.setToDecNumber(inputD, status); |
| if (status.errIfFailureAndReset("parsing decimal quantity: \"%.*s\"", inputD.length(), |
| inputD.data())) { |
| return; |
| } |
| double inputAmount = dqInputD.toDouble(); |
| |
| MeasureUnit inputMeasureUnit = MeasureUnit::forIdentifier(inputUnit, status); |
| if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", inputUnit.length(), inputUnit.data())) { |
| return; |
| } |
| |
| 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(), |
| 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; |
| } |
| RouteResult routeResult = router.route(inputAmount, nullptr, status); |
| if (status.errIfFailureAndReset("router.route(inputAmount, ...)")) { |
| return; |
| } |
| // TODO: revisit this experimentally chosen precision: |
| checkOutput(unitsTest, msg.data(), expected, routeResult.measures, 0.0000000001); |
| } |
| |
| /** |
| * Parses the format used by unitPreferencesTest.txt, calling lineFn for each |
| * line. |
| * |
| * This is a modified version of u_parseDelimitedFile, customized for |
| * unitPreferencesTest.txt, due to it having a variable number of fields per |
| * line. |
| */ |
| void parsePreferencesTests(const char *filename, char delimiter, char *fields[][2], |
| int32_t maxFieldCount, UParseLineFn *lineFn, void *context, |
| UErrorCode *pErrorCode) { |
| FileStream *file; |
| char line[10000]; |
| char *start, *limit; |
| int32_t i; |
| |
| if (U_FAILURE(*pErrorCode)) { |
| return; |
| } |
| |
| if (fields == NULL || lineFn == NULL || maxFieldCount <= 0) { |
| *pErrorCode = U_ILLEGAL_ARGUMENT_ERROR; |
| return; |
| } |
| |
| if (filename == NULL || *filename == 0 || (*filename == '-' && filename[1] == 0)) { |
| filename = NULL; |
| file = T_FileStream_stdin(); |
| } else { |
| file = T_FileStream_open(filename, "r"); |
| } |
| if (file == NULL) { |
| *pErrorCode = U_FILE_ACCESS_ERROR; |
| return; |
| } |
| |
| while (T_FileStream_readLine(file, line, sizeof(line)) != NULL) { |
| /* remove trailing newline characters */ |
| u_rtrim(line); |
| |
| start = line; |
| *pErrorCode = U_ZERO_ERROR; |
| |
| /* skip this line if it is empty or a comment */ |
| if (*start == 0 || *start == '#') { |
| continue; |
| } |
| |
| /* remove in-line comments */ |
| limit = uprv_strchr(start, '#'); |
| if (limit != NULL) { |
| /* get white space before the pound sign */ |
| while (limit > start && U_IS_INV_WHITESPACE(*(limit - 1))) { |
| --limit; |
| } |
| |
| /* truncate the line */ |
| *limit = 0; |
| } |
| |
| /* skip lines with only whitespace */ |
| if (u_skipWhitespace(start)[0] == 0) { |
| continue; |
| } |
| |
| /* for each field, call the corresponding field function */ |
| for (i = 0; i < maxFieldCount; ++i) { |
| /* set the limit pointer of this field */ |
| limit = start; |
| while (*limit != delimiter && *limit != 0) { |
| ++limit; |
| } |
| |
| /* set the field start and limit in the fields array */ |
| fields[i][0] = start; |
| fields[i][1] = limit; |
| |
| /* set start to the beginning of the next field, if any */ |
| start = limit; |
| if (*start != 0) { |
| ++start; |
| } else { |
| break; |
| } |
| } |
| 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 (filename != NULL) { |
| T_FileStream_close(file); |
| } |
| } |
| |
| /** |
| * Runs data-driven unit tests for unit preferences. It looks for the test cases |
| * in source/test/testdata/cldr/units/unitPreferencesTest.txt, which originates |
| * in CLDR. |
| */ |
| void UnitsTest::testUnitPreferencesWithCLDRTests() { |
| const char *filename = "unitPreferencesTest.txt"; |
| const int32_t maxFields = 11; |
| char *fields[maxFields][2]; |
| |
| IcuTestErrorCode errorCode(*this, "UnitsTest::testUnitPreferencesWithCLDRTests"); |
| const char *sourceTestDataPath = getSourceTestData(errorCode); |
| if (errorCode.errIfFailureAndReset("unable to find the source/test/testdata " |
| "folder (getSourceTestData())")) { |
| return; |
| } |
| |
| CharString path(sourceTestDataPath, errorCode); |
| path.appendPathPart("cldr/units", errorCode); |
| path.appendPathPart(filename, errorCode); |
| |
| parsePreferencesTests(path.data(), ';', fields, maxFields, unitPreferencesTestDataLineFn, this, |
| errorCode); |
| if (errorCode.errIfFailureAndReset("error parsing %s: %s\n", path.data(), u_errorName(errorCode))) { |
| return; |
| } |
| } |
| |
| #endif /* #if !UCONFIG_NO_FORMATTING */ |