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