// © 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 #include #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); // TODO: Test UnitsConverter created using CLDR separately. // Test UnitsConverter created by CLDR unit identifiers UnitsConverter converter2(testCase.source, testCase.target, status); if (status.errIfFailureAndReset("UnitsConverter(<%s>, <%s>, ...)", testCase.source, testCase.target)) { continue; } maxDelta = 1e-6 * uprv_fabs(testCase.expectedValue); if (testCase.expectedValue == 0) { maxDelta = 1e-12; } assertEqualsNear(UnicodeString("testConverter2: ") + testCase.source + " to " + testCase.target, testCase.expectedValue, converter2.convert(testCase.inputValue), maxDelta); maxDelta = 1e-6 * uprv_fabs(testCase.inputValue); if (testCase.inputValue == 0) { maxDelta = 1e-12; } assertEqualsNear( UnicodeString("testConverter2 inverse: ") + testCase.target + " back to " + testCase.source, testCase.inputValue, converter2.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 approximately 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 measures; auto testATestCase = [&](const ComplexUnitsConverter& converter ,StringPiece initMsg , const TestCase &testCase) { measures = converter.convert(testCase.value, nullptr, status); CharString msg(initMsg, status); 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()); } }; for (const auto &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); ComplexUnitsConverter converter1(inputImpl, outputImpl, rates, status); testATestCase(converter1, "ComplexUnitsConverter #1 " , testCase); // Test ComplexUnitsConverter created with CLDR units identifiers. ComplexUnitsConverter converter2( testCase.input, testCase.output, status); testATestCase(converter2, "ComplexUnitsConverter #1 " , testCase); } 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); if (status.errIfFailureAndReset()) { continue; } MeasureUnitImpl outputImpl = MeasureUnitImpl::forIdentifier(testCase.output, status); if (status.errIfFailureAndReset()) { continue; } ComplexUnitsConverter converter(inputImpl, outputImpl, conversionRates, status); if (status.errIfFailureAndReset()) { continue; } auto actual = converter.convert(testCase.inputValue, nullptr, status); if (status.errIfFailureAndReset()) { continue; } if (actual.length() < testCase.expectedCount) { errln("converter.convert(...) returned too few Measures"); continue; } 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); } } } } /** * 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 &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); // Test UnitsRouter created with CLDR units identifiers. CharString inputUnitIdentifier(inputUnit, status); UnitsRouter router2(inputUnitIdentifier.data(), region, usage, status); if (status.errIfFailureAndReset("UnitsRouter2(<%s>, \"%.*s\", \"%.*s\", status)", inputUnitIdentifier.data(), region.length(), region.data(), usage.length(), usage.data())) { return; } CharString msg2(quantity, status); msg2.append(" ", status); msg2.append(usage, status); msg2.append(" ", status); msg2.append(region, status); msg2.append(" ", status); msg2.append(inputD, status); msg2.append(" ", status); msg2.append(inputUnitIdentifier.data(), status); if (status.errIfFailureAndReset("Failure before router2.route")) { return; } RouteResult routeResult2 = router2.route(inputAmount, nullptr, status); if (status.errIfFailureAndReset("router2.route(inputAmount, ...)")) { return; } // TODO: revisit this experimentally chosen precision: checkOutput(unitsTest, msg2.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 */