Frank Tang | 3e05d9d | 2021-11-08 14:04:04 -0800 | [diff] [blame] | 1 | // © 2020 and later: Unicode, Inc. and others. |
| 2 | // License & terms of use: http://www.unicode.org/copyright.html#License |
| 3 | |
| 4 | #include "unicode/utypes.h" |
| 5 | |
| 6 | #if !UCONFIG_NO_FORMATTING |
| 7 | |
| 8 | #include <cmath> |
| 9 | #include <iostream> |
| 10 | |
| 11 | #include "charstr.h" |
| 12 | #include "cmemory.h" |
| 13 | #include "filestrm.h" |
| 14 | #include "intltest.h" |
| 15 | #include "number_decimalquantity.h" |
| 16 | #include "putilimp.h" |
| 17 | #include "unicode/ctest.h" |
| 18 | #include "unicode/measunit.h" |
| 19 | #include "unicode/measure.h" |
| 20 | #include "unicode/unistr.h" |
| 21 | #include "unicode/unum.h" |
| 22 | #include "unicode/ures.h" |
| 23 | #include "units_complexconverter.h" |
| 24 | #include "units_converter.h" |
| 25 | #include "units_data.h" |
| 26 | #include "units_router.h" |
| 27 | #include "uparse.h" |
| 28 | #include "uresimp.h" |
| 29 | |
| 30 | struct UnitConversionTestCase { |
| 31 | const StringPiece source; |
| 32 | const StringPiece target; |
| 33 | const double inputValue; |
| 34 | const double expectedValue; |
| 35 | }; |
| 36 | |
| 37 | using ::icu::number::impl::DecimalQuantity; |
| 38 | using namespace ::icu::units; |
| 39 | |
| 40 | class UnitsTest : public IntlTest { |
| 41 | public: |
| 42 | UnitsTest() {} |
| 43 | |
| 44 | void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = NULL) override; |
| 45 | |
| 46 | void testUnitConstantFreshness(); |
| 47 | void testExtractConvertibility(); |
| 48 | void testConversionInfo(); |
| 49 | void testConverterWithCLDRTests(); |
| 50 | void testComplexUnitsConverter(); |
| 51 | void testComplexUnitsConverterSorting(); |
| 52 | void testUnitPreferencesWithCLDRTests(); |
| 53 | void testConverter(); |
| 54 | }; |
| 55 | |
| 56 | extern IntlTest *createUnitsTest() { return new UnitsTest(); } |
| 57 | |
| 58 | void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) { |
| 59 | if (exec) { |
| 60 | logln("TestSuite UnitsTest: "); |
| 61 | } |
| 62 | TESTCASE_AUTO_BEGIN; |
| 63 | TESTCASE_AUTO(testUnitConstantFreshness); |
| 64 | TESTCASE_AUTO(testExtractConvertibility); |
| 65 | TESTCASE_AUTO(testConversionInfo); |
| 66 | TESTCASE_AUTO(testConverterWithCLDRTests); |
| 67 | TESTCASE_AUTO(testComplexUnitsConverter); |
| 68 | TESTCASE_AUTO(testComplexUnitsConverterSorting); |
| 69 | TESTCASE_AUTO(testUnitPreferencesWithCLDRTests); |
| 70 | TESTCASE_AUTO(testConverter); |
| 71 | TESTCASE_AUTO_END; |
| 72 | } |
| 73 | |
| 74 | // Tests the hard-coded constants in the code against constants that appear in |
| 75 | // units.txt. |
| 76 | void UnitsTest::testUnitConstantFreshness() { |
| 77 | IcuTestErrorCode status(*this, "testUnitConstantFreshness"); |
| 78 | LocalUResourceBundlePointer unitsBundle(ures_openDirect(NULL, "units", status)); |
| 79 | LocalUResourceBundlePointer unitConstants( |
| 80 | ures_getByKey(unitsBundle.getAlias(), "unitConstants", NULL, status)); |
| 81 | |
| 82 | while (ures_hasNext(unitConstants.getAlias())) { |
| 83 | int32_t len; |
| 84 | const char *constant = NULL; |
| 85 | ures_getNextString(unitConstants.getAlias(), &len, &constant, status); |
| 86 | |
| 87 | Factor factor; |
| 88 | addSingleFactorConstant(constant, 1, POSITIVE, factor, status); |
| 89 | if (status.errDataIfFailureAndReset( |
| 90 | "addSingleFactorConstant(<%s>, ...).\n\n" |
| 91 | "If U_INVALID_FORMAT_ERROR, please check that \"icu4c/source/i18n/units_converter.cpp\" " |
| 92 | "has all constants? Is \"%s\" a new constant?\n" |
| 93 | "See docs/processes/release/tasks/updating-measure-unit.md for more information.\n", |
| 94 | constant, constant)) { |
| 95 | continue; |
| 96 | } |
| 97 | |
| 98 | // Check the values of constants that have a simple numeric value |
| 99 | factor.substituteConstants(); |
| 100 | int32_t uLen; |
| 101 | UnicodeString uVal = ures_getStringByKey(unitConstants.getAlias(), constant, &uLen, status); |
| 102 | CharString val; |
| 103 | val.appendInvariantChars(uVal, status); |
| 104 | if (status.errDataIfFailureAndReset("Failed to get constant value for %s.", constant)) { |
| 105 | continue; |
| 106 | } |
| 107 | DecimalQuantity dqVal; |
| 108 | UErrorCode parseStatus = U_ZERO_ERROR; |
| 109 | // TODO(units): unify with strToDouble() in units_converter.cpp |
| 110 | dqVal.setToDecNumber(val.toStringPiece(), parseStatus); |
| 111 | if (!U_SUCCESS(parseStatus)) { |
| 112 | // Not simple to parse, skip validating this constant's value. (We |
| 113 | // leave catching mistakes to the data-driven integration tests.) |
| 114 | continue; |
| 115 | } |
| 116 | double expectedNumerator = dqVal.toDouble(); |
| 117 | assertEquals(UnicodeString("Constant ") + constant + u" numerator", expectedNumerator, |
| 118 | factor.factorNum); |
| 119 | assertEquals(UnicodeString("Constant ") + constant + u" denominator", 1.0, factor.factorDen); |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | void UnitsTest::testExtractConvertibility() { |
| 124 | IcuTestErrorCode status(*this, "UnitsTest::testExtractConvertibility"); |
| 125 | |
| 126 | struct TestCase { |
| 127 | const char *const source; |
| 128 | const char *const target; |
| 129 | const Convertibility expectedState; |
| 130 | } testCases[]{ |
| 131 | {"meter", "foot", CONVERTIBLE}, // |
| 132 | {"kilometer", "foot", CONVERTIBLE}, // |
| 133 | {"hectare", "square-foot", CONVERTIBLE}, // |
| 134 | {"kilometer-per-second", "second-per-meter", RECIPROCAL}, // |
| 135 | {"square-meter", "square-foot", CONVERTIBLE}, // |
| 136 | {"kilometer-per-second", "foot-per-second", CONVERTIBLE}, // |
| 137 | {"square-hectare", "pow4-foot", CONVERTIBLE}, // |
| 138 | {"square-kilometer-per-second", "second-per-square-meter", RECIPROCAL}, // |
| 139 | {"cubic-kilometer-per-second-meter", "second-per-square-meter", RECIPROCAL}, // |
| 140 | {"square-meter-per-square-hour", "hectare-per-square-second", CONVERTIBLE}, // |
| 141 | {"hertz", "revolution-per-second", CONVERTIBLE}, // |
| 142 | {"millimeter", "meter", CONVERTIBLE}, // |
| 143 | {"yard", "meter", CONVERTIBLE}, // |
| 144 | {"ounce-troy", "kilogram", CONVERTIBLE}, // |
| 145 | {"percent", "portion", CONVERTIBLE}, // |
| 146 | {"ofhg", "kilogram-per-square-meter-square-second", CONVERTIBLE}, // |
| 147 | {"second-per-meter", "meter-per-second", RECIPROCAL}, // |
| 148 | }; |
| 149 | |
| 150 | for (const auto &testCase : testCases) { |
| 151 | MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status); |
| 152 | if (status.errIfFailureAndReset("source MeasureUnitImpl::forIdentifier(\"%s\", ...)", |
| 153 | testCase.source)) { |
| 154 | continue; |
| 155 | } |
| 156 | MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status); |
| 157 | if (status.errIfFailureAndReset("target MeasureUnitImpl::forIdentifier(\"%s\", ...)", |
| 158 | testCase.target)) { |
| 159 | continue; |
| 160 | } |
| 161 | |
| 162 | ConversionRates conversionRates(status); |
| 163 | if (status.errIfFailureAndReset("conversionRates(status)")) { |
| 164 | continue; |
| 165 | } |
| 166 | auto convertibility = extractConvertibility(source, target, conversionRates, status); |
| 167 | if (status.errIfFailureAndReset("extractConvertibility(<%s>, <%s>, ...)", testCase.source, |
| 168 | testCase.target)) { |
| 169 | continue; |
| 170 | } |
| 171 | |
| 172 | assertEquals(UnicodeString("Conversion Capability: ") + testCase.source + " to " + |
| 173 | testCase.target, |
| 174 | testCase.expectedState, convertibility); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | void UnitsTest::testConversionInfo() { |
| 179 | IcuTestErrorCode status(*this, "UnitsTest::testExtractConvertibility"); |
| 180 | // Test Cases |
| 181 | struct TestCase { |
| 182 | const char *source; |
| 183 | const char *target; |
| 184 | const ConversionInfo expectedConversionInfo; |
| 185 | } testCases[]{ |
| 186 | { |
| 187 | "meter", |
| 188 | "meter", |
| 189 | {1.0, 0, false}, |
| 190 | }, |
| 191 | { |
| 192 | "meter", |
| 193 | "foot", |
| 194 | {3.28084, 0, false}, |
| 195 | }, |
| 196 | { |
| 197 | "foot", |
| 198 | "meter", |
| 199 | {0.3048, 0, false}, |
| 200 | }, |
| 201 | { |
| 202 | "celsius", |
| 203 | "kelvin", |
| 204 | {1, 273.15, false}, |
| 205 | }, |
| 206 | { |
| 207 | "fahrenheit", |
| 208 | "kelvin", |
| 209 | {5.0 / 9.0, 255.372, false}, |
| 210 | }, |
| 211 | { |
| 212 | "fahrenheit", |
| 213 | "celsius", |
| 214 | {5.0 / 9.0, -17.7777777778, false}, |
| 215 | }, |
| 216 | { |
| 217 | "celsius", |
| 218 | "fahrenheit", |
| 219 | {9.0 / 5.0, 32, false}, |
| 220 | }, |
| 221 | { |
| 222 | "fahrenheit", |
| 223 | "fahrenheit", |
| 224 | {1.0, 0, false}, |
| 225 | }, |
| 226 | { |
| 227 | "mile-per-gallon", |
| 228 | "liter-per-100-kilometer", |
| 229 | {0.00425143707, 0, true}, |
| 230 | }, |
| 231 | }; |
| 232 | |
| 233 | ConversionRates rates(status); |
| 234 | for (const auto &testCase : testCases) { |
| 235 | auto sourceImpl = MeasureUnitImpl::forIdentifier(testCase.source, status); |
| 236 | auto targetImpl = MeasureUnitImpl::forIdentifier(testCase.target, status); |
| 237 | UnitsConverter unitsConverter(sourceImpl, targetImpl, rates, status); |
| 238 | |
| 239 | if (status.errIfFailureAndReset()) { |
| 240 | continue; |
| 241 | } |
| 242 | |
| 243 | ConversionInfo actualConversionInfo = unitsConverter.getConversionInfo(); |
| 244 | UnicodeString message = |
| 245 | UnicodeString("testConverter: ") + testCase.source + " to " + testCase.target; |
| 246 | |
| 247 | double maxDelta = 1e-6 * uprv_fabs(testCase.expectedConversionInfo.conversionRate); |
| 248 | if (testCase.expectedConversionInfo.conversionRate == 0) { |
| 249 | maxDelta = 1e-12; |
| 250 | } |
| 251 | assertEqualsNear(message + ", conversion rate: ", testCase.expectedConversionInfo.conversionRate, |
| 252 | actualConversionInfo.conversionRate, maxDelta); |
| 253 | |
| 254 | maxDelta = 1e-6 * uprv_fabs(testCase.expectedConversionInfo.offset); |
| 255 | if (testCase.expectedConversionInfo.offset == 0) { |
| 256 | maxDelta = 1e-12; |
| 257 | } |
| 258 | assertEqualsNear(message + ", offset: ", testCase.expectedConversionInfo.offset, actualConversionInfo.offset, |
| 259 | maxDelta); |
| 260 | |
| 261 | assertEquals(message + ", reciprocal: ", testCase.expectedConversionInfo.reciprocal, |
| 262 | actualConversionInfo.reciprocal); |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | void UnitsTest::testConverter() { |
| 267 | IcuTestErrorCode status(*this, "UnitsTest::testConverter"); |
| 268 | |
| 269 | // Test Cases |
| 270 | struct TestCase { |
| 271 | const char *source; |
| 272 | const char *target; |
| 273 | const double inputValue; |
| 274 | const double expectedValue; |
| 275 | } testCases[]{ |
| 276 | // SI Prefixes |
| 277 | {"gram", "kilogram", 1.0, 0.001}, |
| 278 | {"milligram", "kilogram", 1.0, 0.000001}, |
| 279 | {"microgram", "kilogram", 1.0, 0.000000001}, |
| 280 | {"megagram", "gram", 1.0, 1000000}, |
| 281 | {"megagram", "kilogram", 1.0, 1000}, |
| 282 | {"gigabyte", "byte", 1.0, 1000000000}, |
| 283 | {"megawatt", "watt", 1.0, 1000000}, |
| 284 | {"megawatt", "kilowatt", 1.0, 1000}, |
| 285 | // Binary Prefixes |
| 286 | {"kilobyte", "byte", 1, 1000}, |
| 287 | {"kibibyte", "byte", 1, 1024}, |
| 288 | {"mebibyte", "byte", 1, 1048576}, |
| 289 | {"gibibyte", "kibibyte", 1, 1048576}, |
| 290 | {"pebibyte", "tebibyte", 4, 4096}, |
| 291 | {"zebibyte", "pebibyte", 1.0 / 16, 65536.0}, |
| 292 | {"yobibyte", "exbibyte", 1, 1048576}, |
| 293 | // Mass |
| 294 | {"gram", "kilogram", 1.0, 0.001}, |
| 295 | {"pound", "kilogram", 1.0, 0.453592}, |
| 296 | {"pound", "kilogram", 2.0, 0.907185}, |
| 297 | {"ounce", "pound", 16.0, 1.0}, |
| 298 | {"ounce", "kilogram", 16.0, 0.453592}, |
| 299 | {"ton", "pound", 1.0, 2000}, |
| 300 | {"stone", "pound", 1.0, 14}, |
| 301 | {"stone", "kilogram", 1.0, 6.35029}, |
| 302 | // Temperature |
| 303 | {"celsius", "fahrenheit", 0.0, 32.0}, |
| 304 | {"celsius", "fahrenheit", 10.0, 50.0}, |
| 305 | {"celsius", "fahrenheit", 1000, 1832}, |
| 306 | {"fahrenheit", "celsius", 32.0, 0.0}, |
| 307 | {"fahrenheit", "celsius", 89.6, 32}, |
| 308 | {"fahrenheit", "fahrenheit", 1000, 1000}, |
| 309 | {"kelvin", "fahrenheit", 0.0, -459.67}, |
| 310 | {"kelvin", "fahrenheit", 300, 80.33}, |
| 311 | {"kelvin", "celsius", 0.0, -273.15}, |
| 312 | {"kelvin", "celsius", 300.0, 26.85}, |
| 313 | // Area |
| 314 | {"square-meter", "square-yard", 10.0, 11.9599}, |
| 315 | {"hectare", "square-yard", 1.0, 11959.9}, |
| 316 | {"square-mile", "square-foot", 0.0001, 2787.84}, |
| 317 | {"hectare", "square-yard", 1.0, 11959.9}, |
| 318 | {"hectare", "square-meter", 1.0, 10000}, |
| 319 | {"hectare", "square-meter", 0.0, 0.0}, |
| 320 | {"square-mile", "square-foot", 0.0001, 2787.84}, |
| 321 | {"square-yard", "square-foot", 10, 90}, |
| 322 | {"square-yard", "square-foot", 0, 0}, |
| 323 | {"square-yard", "square-foot", 0.000001, 0.000009}, |
| 324 | {"square-mile", "square-foot", 0.0, 0.0}, |
| 325 | // Fuel Consumption |
| 326 | {"cubic-meter-per-meter", "mile-per-gallon", 2.1383143939394E-6, 1.1}, |
| 327 | {"cubic-meter-per-meter", "mile-per-gallon", 2.6134953703704E-6, 0.9}, |
| 328 | |
| 329 | // Test Aliases |
| 330 | // Alias is just another name to the same unit. Therefore, converting |
| 331 | // between them should be the same. |
| 332 | {"foodcalorie", "kilocalorie", 1.0, 1.0}, |
| 333 | {"dot-per-centimeter", "pixel-per-centimeter", 1.0, 1.0}, |
| 334 | {"dot-per-inch", "pixel-per-inch", 1.0, 1.0}, |
| 335 | {"dot", "pixel", 1.0, 1.0}, |
| 336 | |
| 337 | }; |
| 338 | |
| 339 | for (const auto &testCase : testCases) { |
| 340 | MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status); |
| 341 | if (status.errIfFailureAndReset("source MeasureUnitImpl::forIdentifier(\"%s\", ...)", |
| 342 | testCase.source)) { |
| 343 | continue; |
| 344 | } |
| 345 | MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status); |
| 346 | if (status.errIfFailureAndReset("target MeasureUnitImpl::forIdentifier(\"%s\", ...)", |
| 347 | testCase.target)) { |
| 348 | continue; |
| 349 | } |
| 350 | |
| 351 | ConversionRates conversionRates(status); |
| 352 | if (status.errIfFailureAndReset("conversionRates(status)")) { |
| 353 | continue; |
| 354 | } |
| 355 | UnitsConverter converter(source, target, conversionRates, status); |
| 356 | if (status.errIfFailureAndReset("UnitsConverter(<%s>, <%s>, ...)", testCase.source, |
| 357 | testCase.target)) { |
| 358 | continue; |
| 359 | } |
| 360 | |
| 361 | double maxDelta = 1e-6 * uprv_fabs(testCase.expectedValue); |
| 362 | if (testCase.expectedValue == 0) { |
| 363 | maxDelta = 1e-12; |
| 364 | } |
| 365 | assertEqualsNear(UnicodeString("testConverter: ") + testCase.source + " to " + testCase.target, |
| 366 | testCase.expectedValue, converter.convert(testCase.inputValue), maxDelta); |
| 367 | |
| 368 | maxDelta = 1e-6 * uprv_fabs(testCase.inputValue); |
| 369 | if (testCase.inputValue == 0) { |
| 370 | maxDelta = 1e-12; |
| 371 | } |
| 372 | assertEqualsNear( |
| 373 | UnicodeString("testConverter inverse: ") + testCase.target + " back to " + testCase.source, |
| 374 | testCase.inputValue, converter.convertInverse(testCase.expectedValue), maxDelta); |
| 375 | |
| 376 | |
| 377 | // TODO: Test UnitsConverter created using CLDR separately. |
| 378 | // Test UnitsConverter created by CLDR unit identifiers |
| 379 | UnitsConverter converter2(testCase.source, testCase.target, status); |
| 380 | if (status.errIfFailureAndReset("UnitsConverter(<%s>, <%s>, ...)", testCase.source, |
| 381 | testCase.target)) { |
| 382 | continue; |
| 383 | } |
| 384 | |
| 385 | maxDelta = 1e-6 * uprv_fabs(testCase.expectedValue); |
| 386 | if (testCase.expectedValue == 0) { |
| 387 | maxDelta = 1e-12; |
| 388 | } |
| 389 | assertEqualsNear(UnicodeString("testConverter2: ") + testCase.source + " to " + testCase.target, |
| 390 | testCase.expectedValue, converter2.convert(testCase.inputValue), maxDelta); |
| 391 | |
| 392 | maxDelta = 1e-6 * uprv_fabs(testCase.inputValue); |
| 393 | if (testCase.inputValue == 0) { |
| 394 | maxDelta = 1e-12; |
| 395 | } |
| 396 | assertEqualsNear( |
| 397 | UnicodeString("testConverter2 inverse: ") + testCase.target + " back to " + testCase.source, |
| 398 | testCase.inputValue, converter2.convertInverse(testCase.expectedValue), maxDelta); |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | /** |
| 403 | * Trims whitespace off of the specified string. |
| 404 | * @param field is two pointers pointing at the start and end of the string. |
| 405 | * @return A StringPiece with initial and final space characters trimmed off. |
| 406 | */ |
| 407 | StringPiece trimField(char *(&field)[2]) { |
| 408 | const char *start = field[0]; |
| 409 | start = u_skipWhitespace(start); |
| 410 | if (start >= field[1]) { |
| 411 | start = field[1]; |
| 412 | } |
| 413 | const char *end = field[1]; |
| 414 | while ((start < end) && U_IS_INV_WHITESPACE(*(end - 1))) { |
| 415 | end--; |
| 416 | } |
| 417 | int32_t length = (int32_t)(end - start); |
| 418 | return StringPiece(start, length); |
| 419 | } |
| 420 | |
| 421 | // Used for passing context to unitsTestDataLineFn via u_parseDelimitedFile. |
| 422 | struct UnitsTestContext { |
| 423 | // Provides access to UnitsTest methods like logln. |
| 424 | UnitsTest *unitsTest; |
| 425 | // Conversion rates: does not take ownership. |
| 426 | ConversionRates *conversionRates; |
| 427 | }; |
| 428 | |
| 429 | /** |
| 430 | * Deals with a single data-driven unit test for unit conversions. |
| 431 | * |
| 432 | * This is a UParseLineFn as required by u_parseDelimitedFile, intended for |
| 433 | * parsing unitsTest.txt. |
| 434 | * |
| 435 | * @param context Must point at a UnitsTestContext struct. |
| 436 | * @param fields A list of pointer-pairs, each pair pointing at the start and |
| 437 | * end of each field. End pointers are important because these are *not* |
| 438 | * null-terminated strings. (Interpreted as a null-terminated string, |
| 439 | * fields[0][0] points at the whole line.) |
| 440 | * @param fieldCount The number of fields (pointer pairs) passed to the fields |
| 441 | * parameter. |
| 442 | * @param pErrorCode Receives status. |
| 443 | */ |
| 444 | void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, UErrorCode *pErrorCode) { |
| 445 | if (U_FAILURE(*pErrorCode)) { |
| 446 | return; |
| 447 | } |
| 448 | UnitsTestContext *ctx = (UnitsTestContext *)context; |
| 449 | UnitsTest *unitsTest = ctx->unitsTest; |
| 450 | (void)fieldCount; // unused UParseLineFn variable |
| 451 | IcuTestErrorCode status(*unitsTest, "unitsTestDatalineFn"); |
| 452 | |
| 453 | StringPiece quantity = trimField(fields[0]); |
| 454 | StringPiece x = trimField(fields[1]); |
| 455 | StringPiece y = trimField(fields[2]); |
| 456 | StringPiece commentConversionFormula = trimField(fields[3]); |
| 457 | StringPiece utf8Expected = trimField(fields[4]); |
| 458 | |
| 459 | UNumberFormat *nf = unum_open(UNUM_DEFAULT, NULL, -1, "en_US", NULL, status); |
| 460 | if (status.errIfFailureAndReset("unum_open failed")) { |
| 461 | return; |
| 462 | } |
| 463 | UnicodeString uExpected = UnicodeString::fromUTF8(utf8Expected); |
| 464 | double expected = unum_parseDouble(nf, uExpected.getBuffer(), uExpected.length(), 0, status); |
| 465 | unum_close(nf); |
| 466 | if (status.errIfFailureAndReset("unum_parseDouble(\"%s\") failed", utf8Expected)) { |
| 467 | return; |
| 468 | } |
| 469 | |
| 470 | CharString sourceIdent(x, status); |
| 471 | MeasureUnitImpl sourceUnit = MeasureUnitImpl::forIdentifier(x, status); |
| 472 | if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", x.length(), x.data())) { |
| 473 | return; |
| 474 | } |
| 475 | |
| 476 | CharString targetIdent(y, status); |
| 477 | MeasureUnitImpl targetUnit = MeasureUnitImpl::forIdentifier(y, status); |
| 478 | if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", y.length(), y.data())) { |
| 479 | return; |
| 480 | } |
| 481 | |
| 482 | unitsTest->logln("Quantity (Category): \"%.*s\", " |
| 483 | "Expected value of \"1000 %.*s in %.*s\": %f, " |
| 484 | "commentConversionFormula: \"%.*s\", ", |
| 485 | quantity.length(), quantity.data(), x.length(), x.data(), y.length(), y.data(), |
| 486 | expected, commentConversionFormula.length(), commentConversionFormula.data()); |
| 487 | |
| 488 | // Convertibility: |
| 489 | auto convertibility = extractConvertibility(sourceUnit, targetUnit, *ctx->conversionRates, status); |
| 490 | if (status.errIfFailureAndReset("extractConvertibility(<%s>, <%s>, ...)", |
| 491 | sourceIdent.data(), targetIdent.data())) { |
| 492 | return; |
| 493 | } |
| 494 | CharString msg; |
| 495 | msg.append("convertible: ", status) |
| 496 | .append(sourceIdent.data(), status) |
| 497 | .append(" -> ", status) |
| 498 | .append(targetIdent.data(), status); |
| 499 | if (status.errIfFailureAndReset("msg construction")) { |
| 500 | return; |
| 501 | } |
| 502 | unitsTest->assertNotEquals(msg.data(), UNCONVERTIBLE, convertibility); |
| 503 | |
| 504 | // Conversion: |
| 505 | UnitsConverter converter(sourceUnit, targetUnit, *ctx->conversionRates, status); |
| 506 | if (status.errIfFailureAndReset("UnitsConverter(<%s>, <%s>, ...)", sourceIdent.data(), |
| 507 | targetIdent.data())) { |
| 508 | return; |
| 509 | } |
| 510 | double got = converter.convert(1000); |
| 511 | msg.clear(); |
| 512 | msg.append("Converting 1000 ", status).append(x, status).append(" to ", status).append(y, status); |
| 513 | unitsTest->assertEqualsNear(msg.data(), expected, got, 0.0001 * expected); |
| 514 | double inverted = converter.convertInverse(got); |
| 515 | msg.clear(); |
| 516 | msg.append("Converting back to ", status).append(x, status).append(" from ", status).append(y, status); |
| 517 | unitsTest->assertEqualsNear(msg.data(), 1000, inverted, 0.0001); |
| 518 | } |
| 519 | |
| 520 | /** |
| 521 | * Runs data-driven unit tests for unit conversion. It looks for the test cases |
| 522 | * in source/test/testdata/cldr/units/unitsTest.txt, which originates in CLDR. |
| 523 | */ |
| 524 | void UnitsTest::testConverterWithCLDRTests() { |
| 525 | const char *filename = "unitsTest.txt"; |
| 526 | const int32_t kNumFields = 5; |
| 527 | char *fields[kNumFields][2]; |
| 528 | |
| 529 | IcuTestErrorCode errorCode(*this, "UnitsTest::testConverterWithCLDRTests"); |
| 530 | const char *sourceTestDataPath = getSourceTestData(errorCode); |
| 531 | if (errorCode.errIfFailureAndReset("unable to find the source/test/testdata " |
| 532 | "folder (getSourceTestData())")) { |
| 533 | return; |
| 534 | } |
| 535 | |
| 536 | CharString path(sourceTestDataPath, errorCode); |
| 537 | path.appendPathPart("cldr/units", errorCode); |
| 538 | path.appendPathPart(filename, errorCode); |
| 539 | |
| 540 | ConversionRates rates(errorCode); |
| 541 | UnitsTestContext ctx = {this, &rates}; |
| 542 | u_parseDelimitedFile(path.data(), ';', fields, kNumFields, unitsTestDataLineFn, &ctx, errorCode); |
| 543 | if (errorCode.errIfFailureAndReset("error parsing %s: %s\n", path.data(), u_errorName(errorCode))) { |
| 544 | return; |
| 545 | } |
| 546 | } |
| 547 | |
| 548 | void UnitsTest::testComplexUnitsConverter() { |
| 549 | IcuTestErrorCode status(*this, "UnitsTest::testComplexUnitsConverter"); |
| 550 | |
| 551 | // DBL_EPSILON is approximately 2.22E-16, and is the precision of double for |
| 552 | // values in the range [1.0, 2.0), but half the precision of double for |
| 553 | // [2.0, 4.0). |
| 554 | U_ASSERT(1.0 + DBL_EPSILON > 1.0); |
| 555 | U_ASSERT(2.0 - DBL_EPSILON < 2.0); |
| 556 | U_ASSERT(2.0 + DBL_EPSILON == 2.0); |
| 557 | |
| 558 | struct TestCase { |
| 559 | const char* msg; |
| 560 | const char* input; |
| 561 | const char* output; |
| 562 | double value; |
| 563 | Measure expected[2]; |
| 564 | int32_t expectedCount; |
| 565 | // For mixed units, accuracy of the smallest unit |
| 566 | double accuracy; |
| 567 | } testCases[]{ |
| 568 | // Significantly less than 2.0. |
| 569 | {"1.9999", |
| 570 | "foot", |
| 571 | "foot-and-inch", |
| 572 | 1.9999, |
| 573 | {Measure(1, MeasureUnit::createFoot(status), status), |
| 574 | Measure(11.9988, MeasureUnit::createInch(status), status)}, |
| 575 | 2, |
| 576 | 0}, |
| 577 | |
| 578 | // A minimal nudge under 2.0, rounding up to 2.0 ft, 0 in. |
| 579 | {"2-eps", |
| 580 | "foot", |
| 581 | "foot-and-inch", |
| 582 | 2.0 - DBL_EPSILON, |
| 583 | {Measure(2, MeasureUnit::createFoot(status), status), |
| 584 | Measure(0, MeasureUnit::createInch(status), status)}, |
| 585 | 2, |
| 586 | 0}, |
| 587 | |
| 588 | // A slightly bigger nudge under 2.0, *not* rounding up to 2.0 ft! |
| 589 | {"2-3eps", |
| 590 | "foot", |
| 591 | "foot-and-inch", |
| 592 | 2.0 - 3 * DBL_EPSILON, |
| 593 | {Measure(1, MeasureUnit::createFoot(status), status), |
| 594 | // We expect 12*3*DBL_EPSILON inches (7.92e-15) less than 12. |
| 595 | Measure(12 - 36 * DBL_EPSILON, MeasureUnit::createInch(status), status)}, |
| 596 | 2, |
| 597 | // Might accuracy be lacking with some compilers or on some systems? In |
| 598 | // case it is somehow lacking, we'll allow a delta of 12 * DBL_EPSILON. |
| 599 | 12 * DBL_EPSILON}, |
| 600 | |
| 601 | // Testing precision with meter and light-year. |
| 602 | // |
| 603 | // DBL_EPSILON light-years, ~2.22E-16 light-years, is ~2.1 meters |
| 604 | // (maximum precision when exponent is 0). |
| 605 | // |
| 606 | // 1e-16 light years is 0.946073 meters. |
| 607 | |
| 608 | // A 2.1 meter nudge under 2.0 light years, rounding up to 2.0 ly, 0 m. |
| 609 | {"2-eps", |
| 610 | "light-year", |
| 611 | "light-year-and-meter", |
| 612 | 2.0 - DBL_EPSILON, |
| 613 | {Measure(2, MeasureUnit::createLightYear(status), status), |
| 614 | Measure(0, MeasureUnit::createMeter(status), status)}, |
| 615 | 2, |
| 616 | 0}, |
| 617 | |
| 618 | // A 2.1 meter nudge under 1.0 light years, rounding up to 1.0 ly, 0 m. |
| 619 | {"1-eps", |
| 620 | "light-year", |
| 621 | "light-year-and-meter", |
| 622 | 1.0 - DBL_EPSILON, |
| 623 | {Measure(1, MeasureUnit::createLightYear(status), status), |
| 624 | Measure(0, MeasureUnit::createMeter(status), status)}, |
| 625 | 2, |
| 626 | 0}, |
| 627 | |
| 628 | // 1e-15 light years is 9.46073 meters (calculated using "bc" and the |
| 629 | // CLDR conversion factor). With double-precision maths in C++, we get |
| 630 | // 10.5. In this case, we're off by a bit more than 1 meter. With Java |
| 631 | // BigDecimal, we get accurate results. |
| 632 | {"1 + 1e-15", |
| 633 | "light-year", |
| 634 | "light-year-and-meter", |
| 635 | 1.0 + 1e-15, |
| 636 | {Measure(1, MeasureUnit::createLightYear(status), status), |
| 637 | Measure(9.46073, MeasureUnit::createMeter(status), status)}, |
| 638 | 2, |
| 639 | 1.5 /* meters, precision */}, |
| 640 | |
| 641 | // 2.1 meters more than 1 light year is not rounded away. |
| 642 | {"1 + eps", |
| 643 | "light-year", |
| 644 | "light-year-and-meter", |
| 645 | 1.0 + DBL_EPSILON, |
| 646 | {Measure(1, MeasureUnit::createLightYear(status), status), |
| 647 | Measure(2.1, MeasureUnit::createMeter(status), status)}, |
| 648 | 2, |
| 649 | 0.001}, |
| 650 | }; |
| 651 | status.assertSuccess(); |
| 652 | |
| 653 | ConversionRates rates(status); |
| 654 | MeasureUnit input, output; |
| 655 | MeasureUnitImpl tempInput, tempOutput; |
| 656 | MaybeStackVector<Measure> measures; |
| 657 | auto testATestCase = [&](const ComplexUnitsConverter& converter ,StringPiece initMsg , const TestCase &testCase) { |
| 658 | measures = converter.convert(testCase.value, nullptr, status); |
| 659 | |
| 660 | CharString msg(initMsg, status); |
| 661 | msg.append(testCase.msg, status); |
| 662 | msg.append(" ", status); |
| 663 | msg.append(testCase.input, status); |
| 664 | msg.append(" -> ", status); |
| 665 | msg.append(testCase.output, status); |
| 666 | |
| 667 | CharString msgCount(msg, status); |
| 668 | msgCount.append(", measures.length()", status); |
| 669 | assertEquals(msgCount.data(), testCase.expectedCount, measures.length()); |
| 670 | for (int i = 0; i < measures.length() && i < testCase.expectedCount; i++) { |
| 671 | if (i == testCase.expectedCount-1) { |
| 672 | assertEqualsNear(msg.data(), testCase.expected[i].getNumber().getDouble(status), |
| 673 | measures[i]->getNumber().getDouble(status), testCase.accuracy); |
| 674 | } else { |
| 675 | assertEquals(msg.data(), testCase.expected[i].getNumber().getDouble(status), |
| 676 | measures[i]->getNumber().getDouble(status)); |
| 677 | } |
| 678 | assertEquals(msg.data(), testCase.expected[i].getUnit().getIdentifier(), |
| 679 | measures[i]->getUnit().getIdentifier()); |
| 680 | } |
| 681 | }; |
| 682 | |
| 683 | for (const auto &testCase : testCases) |
| 684 | { |
| 685 | input = MeasureUnit::forIdentifier(testCase.input, status); |
| 686 | output = MeasureUnit::forIdentifier(testCase.output, status); |
| 687 | const MeasureUnitImpl& inputImpl = MeasureUnitImpl::forMeasureUnit(input, tempInput, status); |
| 688 | const MeasureUnitImpl& outputImpl = MeasureUnitImpl::forMeasureUnit(output, tempOutput, status); |
| 689 | |
| 690 | ComplexUnitsConverter converter1(inputImpl, outputImpl, rates, status); |
| 691 | testATestCase(converter1, "ComplexUnitsConverter #1 " , testCase); |
| 692 | |
| 693 | // Test ComplexUnitsConverter created with CLDR units identifiers. |
| 694 | ComplexUnitsConverter converter2( testCase.input, testCase.output, status); |
| 695 | testATestCase(converter2, "ComplexUnitsConverter #1 " , testCase); |
| 696 | } |
| 697 | |
| 698 | |
| 699 | status.assertSuccess(); |
| 700 | |
| 701 | // TODO(icu-units#63): test negative numbers! |
| 702 | } |
| 703 | |
| 704 | void UnitsTest::testComplexUnitsConverterSorting() { |
| 705 | IcuTestErrorCode status(*this, "UnitsTest::testComplexUnitsConverterSorting"); |
| 706 | ConversionRates conversionRates(status); |
| 707 | |
| 708 | status.assertSuccess(); |
| 709 | |
| 710 | struct TestCase { |
| 711 | const char *msg; |
| 712 | const char *input; |
| 713 | const char *output; |
| 714 | double inputValue; |
| 715 | Measure expected[3]; |
| 716 | int32_t expectedCount; |
| 717 | // For mixed units, accuracy of the smallest unit |
| 718 | double accuracy; |
| 719 | } testCases[]{{"inch-and-foot", |
| 720 | "meter", |
| 721 | "inch-and-foot", |
| 722 | 10.0, |
| 723 | { |
| 724 | Measure(9.70079, MeasureUnit::createInch(status), status), |
| 725 | Measure(32, MeasureUnit::createFoot(status), status), |
| 726 | Measure(0, MeasureUnit::createBit(status), status), |
| 727 | }, |
| 728 | 2, |
| 729 | 0.00001}, |
| 730 | {"inch-and-yard-and-foot", |
| 731 | "meter", |
| 732 | "inch-and-yard-and-foot", |
| 733 | 100.0, |
| 734 | { |
| 735 | Measure(1.0079, MeasureUnit::createInch(status), status), |
| 736 | Measure(109, MeasureUnit::createYard(status), status), |
| 737 | Measure(1, MeasureUnit::createFoot(status), status), |
| 738 | }, |
| 739 | 3, |
| 740 | 0.0001}}; |
| 741 | |
| 742 | for (const auto &testCase : testCases) { |
| 743 | MeasureUnitImpl inputImpl = MeasureUnitImpl::forIdentifier(testCase.input, status); |
| 744 | if (status.errIfFailureAndReset()) { |
| 745 | continue; |
| 746 | } |
| 747 | MeasureUnitImpl outputImpl = MeasureUnitImpl::forIdentifier(testCase.output, status); |
| 748 | if (status.errIfFailureAndReset()) { |
| 749 | continue; |
| 750 | } |
| 751 | ComplexUnitsConverter converter(inputImpl, outputImpl, conversionRates, status); |
| 752 | if (status.errIfFailureAndReset()) { |
| 753 | continue; |
| 754 | } |
| 755 | |
| 756 | auto actual = converter.convert(testCase.inputValue, nullptr, status); |
| 757 | if (status.errIfFailureAndReset()) { |
| 758 | continue; |
| 759 | } |
| 760 | if (actual.length() < testCase.expectedCount) { |
| 761 | errln("converter.convert(...) returned too few Measures"); |
| 762 | continue; |
| 763 | } |
| 764 | |
| 765 | for (int i = 0; i < testCase.expectedCount; i++) { |
| 766 | assertEquals(testCase.msg, testCase.expected[i].getUnit().getIdentifier(), |
| 767 | actual[i]->getUnit().getIdentifier()); |
| 768 | |
| 769 | if (testCase.expected[i].getNumber().getType() == Formattable::Type::kInt64) { |
| 770 | assertEquals(testCase.msg, testCase.expected[i].getNumber().getInt64(), |
| 771 | actual[i]->getNumber().getInt64()); |
| 772 | } else { |
| 773 | assertEqualsNear(testCase.msg, testCase.expected[i].getNumber().getDouble(), |
| 774 | actual[i]->getNumber().getDouble(), testCase.accuracy); |
| 775 | } |
| 776 | } |
| 777 | } |
| 778 | } |
| 779 | |
| 780 | /** |
| 781 | * This class represents the output fields from unitPreferencesTest.txt. Please |
| 782 | * see the documentation at the top of that file for details. |
| 783 | * |
| 784 | * For "mixed units" output, there are more (repeated) output fields. The last |
| 785 | * output unit has the expected output specified as both a rational fraction and |
| 786 | * a decimal fraction. This class ignores rational fractions, and expects to |
| 787 | * find a decimal fraction for each output unit. |
| 788 | */ |
| 789 | class ExpectedOutput { |
| 790 | public: |
| 791 | // Counts number of units in the output. When this is more than one, we have |
| 792 | // "mixed units" in the expected output. |
| 793 | int _compoundCount = 0; |
| 794 | |
| 795 | // Counts how many fields were skipped: we expect to skip only one per |
| 796 | // output unit type (the rational fraction). |
| 797 | int _skippedFields = 0; |
| 798 | |
| 799 | // The expected output units: more than one for "mixed units". |
| 800 | MeasureUnit _measureUnits[3]; |
| 801 | |
| 802 | // The amounts of each of the output units. |
| 803 | double _amounts[3]; |
| 804 | |
| 805 | /** |
| 806 | * Parse an expected output field from the test data file. |
| 807 | * |
| 808 | * @param output may be a string representation of an integer, a rational |
| 809 | * fraction, a decimal fraction, or it may be a unit identifier. Whitespace |
| 810 | * should already be trimmed. This function ignores rational fractions, |
| 811 | * saving only decimal fractions and their unit identifiers. |
| 812 | * @return true if the field was successfully parsed, false if parsing |
| 813 | * failed. |
| 814 | */ |
| 815 | void parseOutputField(StringPiece output, UErrorCode &errorCode) { |
| 816 | if (U_FAILURE(errorCode)) return; |
| 817 | DecimalQuantity dqOutputD; |
| 818 | |
| 819 | dqOutputD.setToDecNumber(output, errorCode); |
| 820 | if (U_SUCCESS(errorCode)) { |
| 821 | _amounts[_compoundCount] = dqOutputD.toDouble(); |
| 822 | return; |
| 823 | } else if (errorCode == U_DECIMAL_NUMBER_SYNTAX_ERROR) { |
| 824 | // Not a decimal fraction, it might be a rational fraction or a unit |
| 825 | // identifier: continue. |
| 826 | errorCode = U_ZERO_ERROR; |
| 827 | } else { |
| 828 | // Unexpected error, so we propagate it. |
| 829 | return; |
| 830 | } |
| 831 | |
| 832 | _measureUnits[_compoundCount] = MeasureUnit::forIdentifier(output, errorCode); |
| 833 | if (U_SUCCESS(errorCode)) { |
| 834 | _compoundCount++; |
| 835 | _skippedFields = 0; |
| 836 | return; |
| 837 | } |
| 838 | _skippedFields++; |
| 839 | if (_skippedFields < 2) { |
| 840 | // We are happy skipping one field per output unit: we want to skip |
| 841 | // rational fraction fields like "11 / 10". |
| 842 | errorCode = U_ZERO_ERROR; |
| 843 | return; |
| 844 | } else { |
| 845 | // Propagate the error. |
| 846 | return; |
| 847 | } |
| 848 | } |
| 849 | |
| 850 | /** |
| 851 | * Produces an output string for debug purposes. |
| 852 | */ |
| 853 | std::string toDebugString() { |
| 854 | std::string result; |
| 855 | for (int i = 0; i < _compoundCount; i++) { |
| 856 | result += std::to_string(_amounts[i]); |
| 857 | result += " "; |
| 858 | result += _measureUnits[i].getIdentifier(); |
| 859 | result += " "; |
| 860 | } |
| 861 | return result; |
| 862 | } |
| 863 | }; |
| 864 | |
| 865 | // Checks a vector of Measure instances against ExpectedOutput. |
| 866 | void checkOutput(UnitsTest *unitsTest, const char *msg, ExpectedOutput expected, |
| 867 | const MaybeStackVector<Measure> &actual, double precision) { |
| 868 | IcuTestErrorCode status(*unitsTest, "checkOutput"); |
| 869 | |
| 870 | CharString testMessage("Test case \"", status); |
| 871 | testMessage.append(msg, status); |
| 872 | testMessage.append("\": expected output: ", status); |
| 873 | testMessage.append(expected.toDebugString().c_str(), status); |
| 874 | testMessage.append(", obtained output:", status); |
| 875 | for (int i = 0; i < actual.length(); i++) { |
| 876 | testMessage.append(" ", status); |
| 877 | testMessage.append(std::to_string(actual[i]->getNumber().getDouble(status)), status); |
| 878 | testMessage.append(" ", status); |
| 879 | testMessage.appendInvariantChars(actual[i]->getUnit().getIdentifier(), status); |
| 880 | } |
| 881 | if (!unitsTest->assertEquals(testMessage.data(), expected._compoundCount, actual.length())) { |
| 882 | return; |
| 883 | }; |
| 884 | for (int i = 0; i < actual.length(); i++) { |
| 885 | double permittedDiff = precision * expected._amounts[i]; |
| 886 | if (permittedDiff == 0) { |
| 887 | // If 0 is expected, still permit a small delta. |
| 888 | // TODO: revisit this experimentally chosen value: |
| 889 | permittedDiff = 0.00000001; |
| 890 | } |
| 891 | unitsTest->assertEqualsNear(testMessage.data(), expected._amounts[i], |
| 892 | actual[i]->getNumber().getDouble(status), permittedDiff); |
| 893 | } |
| 894 | } |
| 895 | |
| 896 | /** |
| 897 | * Runs a single data-driven unit test for unit preferences. |
| 898 | * |
| 899 | * This is a UParseLineFn as required by u_parseDelimitedFile, intended for |
| 900 | * parsing unitPreferencesTest.txt. |
| 901 | */ |
| 902 | void unitPreferencesTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, |
| 903 | UErrorCode *pErrorCode) { |
| 904 | if (U_FAILURE(*pErrorCode)) return; |
| 905 | UnitsTest *unitsTest = (UnitsTest *)context; |
| 906 | IcuTestErrorCode status(*unitsTest, "unitPreferencesTestDatalineFn"); |
| 907 | |
| 908 | if (!unitsTest->assertTrue(u"unitPreferencesTestDataLineFn expects 9 fields for simple and 11 " |
| 909 | u"fields for compound. Other field counts not yet supported. ", |
| 910 | fieldCount == 9 || fieldCount == 11)) { |
| 911 | return; |
| 912 | } |
| 913 | |
| 914 | StringPiece quantity = trimField(fields[0]); |
| 915 | StringPiece usage = trimField(fields[1]); |
| 916 | StringPiece region = trimField(fields[2]); |
| 917 | // Unused // StringPiece inputR = trimField(fields[3]); |
| 918 | StringPiece inputD = trimField(fields[4]); |
| 919 | StringPiece inputUnit = trimField(fields[5]); |
| 920 | ExpectedOutput expected; |
| 921 | for (int i = 6; i < fieldCount; i++) { |
| 922 | expected.parseOutputField(trimField(fields[i]), status); |
| 923 | } |
| 924 | if (status.errIfFailureAndReset("parsing unitPreferencesTestData.txt test case: %s", fields[0][0])) { |
| 925 | return; |
| 926 | } |
| 927 | |
| 928 | DecimalQuantity dqInputD; |
| 929 | dqInputD.setToDecNumber(inputD, status); |
| 930 | if (status.errIfFailureAndReset("parsing decimal quantity: \"%.*s\"", inputD.length(), |
| 931 | inputD.data())) { |
| 932 | return; |
| 933 | } |
| 934 | double inputAmount = dqInputD.toDouble(); |
| 935 | |
| 936 | MeasureUnit inputMeasureUnit = MeasureUnit::forIdentifier(inputUnit, status); |
| 937 | if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", inputUnit.length(), inputUnit.data())) { |
| 938 | return; |
| 939 | } |
| 940 | |
| 941 | unitsTest->logln("Quantity (Category): \"%.*s\", Usage: \"%.*s\", Region: \"%.*s\", " |
| 942 | "Input: \"%f %s\", Expected Output: %s", |
| 943 | quantity.length(), quantity.data(), usage.length(), usage.data(), region.length(), |
| 944 | region.data(), inputAmount, inputMeasureUnit.getIdentifier(), |
| 945 | expected.toDebugString().c_str()); |
| 946 | |
| 947 | if (U_FAILURE(status)) { |
| 948 | return; |
| 949 | } |
| 950 | |
| 951 | UnitsRouter router(inputMeasureUnit, region, usage, status); |
| 952 | if (status.errIfFailureAndReset("UnitsRouter(<%s>, \"%.*s\", \"%.*s\", status)", |
| 953 | inputMeasureUnit.getIdentifier(), region.length(), region.data(), |
| 954 | usage.length(), usage.data())) { |
| 955 | return; |
| 956 | } |
| 957 | |
| 958 | CharString msg(quantity, status); |
| 959 | msg.append(" ", status); |
| 960 | msg.append(usage, status); |
| 961 | msg.append(" ", status); |
| 962 | msg.append(region, status); |
| 963 | msg.append(" ", status); |
| 964 | msg.append(inputD, status); |
| 965 | msg.append(" ", status); |
| 966 | msg.append(inputMeasureUnit.getIdentifier(), status); |
| 967 | if (status.errIfFailureAndReset("Failure before router.route")) { |
| 968 | return; |
| 969 | } |
| 970 | RouteResult routeResult = router.route(inputAmount, nullptr, status); |
| 971 | if (status.errIfFailureAndReset("router.route(inputAmount, ...)")) { |
| 972 | return; |
| 973 | } |
| 974 | // TODO: revisit this experimentally chosen precision: |
| 975 | checkOutput(unitsTest, msg.data(), expected, routeResult.measures, 0.0000000001); |
| 976 | |
| 977 | // Test UnitsRouter created with CLDR units identifiers. |
| 978 | CharString inputUnitIdentifier(inputUnit, status); |
| 979 | UnitsRouter router2(inputUnitIdentifier.data(), region, usage, status); |
| 980 | if (status.errIfFailureAndReset("UnitsRouter2(<%s>, \"%.*s\", \"%.*s\", status)", |
| 981 | inputUnitIdentifier.data(), region.length(), region.data(), |
| 982 | usage.length(), usage.data())) { |
| 983 | return; |
| 984 | } |
| 985 | |
| 986 | CharString msg2(quantity, status); |
| 987 | msg2.append(" ", status); |
| 988 | msg2.append(usage, status); |
| 989 | msg2.append(" ", status); |
| 990 | msg2.append(region, status); |
| 991 | msg2.append(" ", status); |
| 992 | msg2.append(inputD, status); |
| 993 | msg2.append(" ", status); |
| 994 | msg2.append(inputUnitIdentifier.data(), status); |
| 995 | if (status.errIfFailureAndReset("Failure before router2.route")) { |
| 996 | return; |
| 997 | } |
| 998 | |
| 999 | RouteResult routeResult2 = router2.route(inputAmount, nullptr, status); |
| 1000 | if (status.errIfFailureAndReset("router2.route(inputAmount, ...)")) { |
| 1001 | return; |
| 1002 | } |
| 1003 | // TODO: revisit this experimentally chosen precision: |
| 1004 | checkOutput(unitsTest, msg2.data(), expected, routeResult.measures, 0.0000000001); |
| 1005 | } |
| 1006 | |
| 1007 | /** |
| 1008 | * Parses the format used by unitPreferencesTest.txt, calling lineFn for each |
| 1009 | * line. |
| 1010 | * |
| 1011 | * This is a modified version of u_parseDelimitedFile, customized for |
| 1012 | * unitPreferencesTest.txt, due to it having a variable number of fields per |
| 1013 | * line. |
| 1014 | */ |
| 1015 | void parsePreferencesTests(const char *filename, char delimiter, char *fields[][2], |
| 1016 | int32_t maxFieldCount, UParseLineFn *lineFn, void *context, |
| 1017 | UErrorCode *pErrorCode) { |
| 1018 | FileStream *file; |
| 1019 | char line[10000]; |
| 1020 | char *start, *limit; |
| 1021 | int32_t i; |
| 1022 | |
| 1023 | if (U_FAILURE(*pErrorCode)) { |
| 1024 | return; |
| 1025 | } |
| 1026 | |
| 1027 | if (fields == NULL || lineFn == NULL || maxFieldCount <= 0) { |
| 1028 | *pErrorCode = U_ILLEGAL_ARGUMENT_ERROR; |
| 1029 | return; |
| 1030 | } |
| 1031 | |
| 1032 | if (filename == NULL || *filename == 0 || (*filename == '-' && filename[1] == 0)) { |
| 1033 | filename = NULL; |
| 1034 | file = T_FileStream_stdin(); |
| 1035 | } else { |
| 1036 | file = T_FileStream_open(filename, "r"); |
| 1037 | } |
| 1038 | if (file == NULL) { |
| 1039 | *pErrorCode = U_FILE_ACCESS_ERROR; |
| 1040 | return; |
| 1041 | } |
| 1042 | |
| 1043 | while (T_FileStream_readLine(file, line, sizeof(line)) != NULL) { |
| 1044 | /* remove trailing newline characters */ |
| 1045 | u_rtrim(line); |
| 1046 | |
| 1047 | start = line; |
| 1048 | *pErrorCode = U_ZERO_ERROR; |
| 1049 | |
| 1050 | /* skip this line if it is empty or a comment */ |
| 1051 | if (*start == 0 || *start == '#') { |
| 1052 | continue; |
| 1053 | } |
| 1054 | |
| 1055 | /* remove in-line comments */ |
| 1056 | limit = uprv_strchr(start, '#'); |
| 1057 | if (limit != NULL) { |
| 1058 | /* get white space before the pound sign */ |
| 1059 | while (limit > start && U_IS_INV_WHITESPACE(*(limit - 1))) { |
| 1060 | --limit; |
| 1061 | } |
| 1062 | |
| 1063 | /* truncate the line */ |
| 1064 | *limit = 0; |
| 1065 | } |
| 1066 | |
| 1067 | /* skip lines with only whitespace */ |
| 1068 | if (u_skipWhitespace(start)[0] == 0) { |
| 1069 | continue; |
| 1070 | } |
| 1071 | |
| 1072 | /* for each field, call the corresponding field function */ |
| 1073 | for (i = 0; i < maxFieldCount; ++i) { |
| 1074 | /* set the limit pointer of this field */ |
| 1075 | limit = start; |
| 1076 | while (*limit != delimiter && *limit != 0) { |
| 1077 | ++limit; |
| 1078 | } |
| 1079 | |
| 1080 | /* set the field start and limit in the fields array */ |
| 1081 | fields[i][0] = start; |
| 1082 | fields[i][1] = limit; |
| 1083 | |
| 1084 | /* set start to the beginning of the next field, if any */ |
| 1085 | start = limit; |
| 1086 | if (*start != 0) { |
| 1087 | ++start; |
| 1088 | } else { |
| 1089 | break; |
| 1090 | } |
| 1091 | } |
| 1092 | if (i == maxFieldCount) { |
| 1093 | *pErrorCode = U_PARSE_ERROR; |
| 1094 | } |
| 1095 | int fieldCount = i + 1; |
| 1096 | |
| 1097 | /* call the field function */ |
| 1098 | lineFn(context, fields, fieldCount, pErrorCode); |
| 1099 | if (U_FAILURE(*pErrorCode)) { |
| 1100 | break; |
| 1101 | } |
| 1102 | } |
| 1103 | |
| 1104 | if (filename != NULL) { |
| 1105 | T_FileStream_close(file); |
| 1106 | } |
| 1107 | } |
| 1108 | |
| 1109 | /** |
| 1110 | * Runs data-driven unit tests for unit preferences. It looks for the test cases |
| 1111 | * in source/test/testdata/cldr/units/unitPreferencesTest.txt, which originates |
| 1112 | * in CLDR. |
| 1113 | */ |
| 1114 | void UnitsTest::testUnitPreferencesWithCLDRTests() { |
| 1115 | const char *filename = "unitPreferencesTest.txt"; |
| 1116 | const int32_t maxFields = 11; |
| 1117 | char *fields[maxFields][2]; |
| 1118 | |
| 1119 | IcuTestErrorCode errorCode(*this, "UnitsTest::testUnitPreferencesWithCLDRTests"); |
| 1120 | const char *sourceTestDataPath = getSourceTestData(errorCode); |
| 1121 | if (errorCode.errIfFailureAndReset("unable to find the source/test/testdata " |
| 1122 | "folder (getSourceTestData())")) { |
| 1123 | return; |
| 1124 | } |
| 1125 | |
| 1126 | CharString path(sourceTestDataPath, errorCode); |
| 1127 | path.appendPathPart("cldr/units", errorCode); |
| 1128 | path.appendPathPart(filename, errorCode); |
| 1129 | |
| 1130 | parsePreferencesTests(path.data(), ';', fields, maxFields, unitPreferencesTestDataLineFn, this, |
| 1131 | errorCode); |
| 1132 | if (errorCode.errIfFailureAndReset("error parsing %s: %s\n", path.data(), u_errorName(errorCode))) { |
| 1133 | return; |
| 1134 | } |
| 1135 | } |
| 1136 | |
| 1137 | #endif /* #if !UCONFIG_NO_FORMATTING */ |