blob: ca41e3b0d030c242def6ef0a6f13b8b3a7d33490 [file] [log] [blame]
Frank Tang3e05d9d2021-11-08 14:04:04 -08001// © 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
30struct UnitConversionTestCase {
31 const StringPiece source;
32 const StringPiece target;
33 const double inputValue;
34 const double expectedValue;
35};
36
37using ::icu::number::impl::DecimalQuantity;
38using namespace ::icu::units;
39
40class 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
56extern IntlTest *createUnitsTest() { return new UnitsTest(); }
57
58void 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.
76void 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
123void 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
178void 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
266void 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},
Frank Tangd2858cb2022-04-08 20:34:12 -0700328 {"liter-per-100-kilometer", "mile-per-gallon", 6.6, 35.6386},
329 {"liter-per-100-kilometer", "mile-per-gallon", 0, uprv_getInfinity()},
330 {"mile-per-gallon", "liter-per-100-kilometer", 0, uprv_getInfinity()},
331 {"mile-per-gallon", "liter-per-100-kilometer", uprv_getInfinity(), 0},
332 // We skip testing -Inf, because the inverse conversion loses the sign:
333 // {"mile-per-gallon", "liter-per-100-kilometer", -uprv_getInfinity(), 0},
Frank Tang3e05d9d2021-11-08 14:04:04 -0800334
335 // Test Aliases
336 // Alias is just another name to the same unit. Therefore, converting
337 // between them should be the same.
338 {"foodcalorie", "kilocalorie", 1.0, 1.0},
339 {"dot-per-centimeter", "pixel-per-centimeter", 1.0, 1.0},
340 {"dot-per-inch", "pixel-per-inch", 1.0, 1.0},
341 {"dot", "pixel", 1.0, 1.0},
342
343 };
344
345 for (const auto &testCase : testCases) {
346 MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
347 if (status.errIfFailureAndReset("source MeasureUnitImpl::forIdentifier(\"%s\", ...)",
348 testCase.source)) {
349 continue;
350 }
351 MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
352 if (status.errIfFailureAndReset("target MeasureUnitImpl::forIdentifier(\"%s\", ...)",
353 testCase.target)) {
354 continue;
355 }
356
Frank Tangd2858cb2022-04-08 20:34:12 -0700357 double maxDelta = 1e-6 * uprv_fabs(testCase.expectedValue);
358 if (testCase.expectedValue == 0) {
359 maxDelta = 1e-12;
360 }
361 double inverseMaxDelta = 1e-6 * uprv_fabs(testCase.inputValue);
362 if (testCase.inputValue == 0) {
363 inverseMaxDelta = 1e-12;
364 }
365
Frank Tang3e05d9d2021-11-08 14:04:04 -0800366 ConversionRates conversionRates(status);
367 if (status.errIfFailureAndReset("conversionRates(status)")) {
368 continue;
369 }
Frank Tangd2858cb2022-04-08 20:34:12 -0700370
Frank Tang3e05d9d2021-11-08 14:04:04 -0800371 UnitsConverter converter(source, target, conversionRates, status);
372 if (status.errIfFailureAndReset("UnitsConverter(<%s>, <%s>, ...)", testCase.source,
373 testCase.target)) {
374 continue;
375 }
Frank Tang3e05d9d2021-11-08 14:04:04 -0800376 assertEqualsNear(UnicodeString("testConverter: ") + testCase.source + " to " + testCase.target,
377 testCase.expectedValue, converter.convert(testCase.inputValue), maxDelta);
Frank Tang3e05d9d2021-11-08 14:04:04 -0800378 assertEqualsNear(
379 UnicodeString("testConverter inverse: ") + testCase.target + " back to " + testCase.source,
Frank Tangd2858cb2022-04-08 20:34:12 -0700380 testCase.inputValue, converter.convertInverse(testCase.expectedValue), inverseMaxDelta);
Frank Tang3e05d9d2021-11-08 14:04:04 -0800381
Frank Tang3e05d9d2021-11-08 14:04:04 -0800382 // Test UnitsConverter created by CLDR unit identifiers
383 UnitsConverter converter2(testCase.source, testCase.target, status);
384 if (status.errIfFailureAndReset("UnitsConverter(<%s>, <%s>, ...)", testCase.source,
385 testCase.target)) {
386 continue;
387 }
Frank Tang3e05d9d2021-11-08 14:04:04 -0800388 assertEqualsNear(UnicodeString("testConverter2: ") + testCase.source + " to " + testCase.target,
389 testCase.expectedValue, converter2.convert(testCase.inputValue), maxDelta);
Frank Tang3e05d9d2021-11-08 14:04:04 -0800390 assertEqualsNear(
391 UnicodeString("testConverter2 inverse: ") + testCase.target + " back to " + testCase.source,
Frank Tangd2858cb2022-04-08 20:34:12 -0700392 testCase.inputValue, converter2.convertInverse(testCase.expectedValue), inverseMaxDelta);
Frank Tang3e05d9d2021-11-08 14:04:04 -0800393 }
394}
395
396/**
397 * Trims whitespace off of the specified string.
398 * @param field is two pointers pointing at the start and end of the string.
399 * @return A StringPiece with initial and final space characters trimmed off.
400 */
401StringPiece trimField(char *(&field)[2]) {
402 const char *start = field[0];
403 start = u_skipWhitespace(start);
404 if (start >= field[1]) {
405 start = field[1];
406 }
407 const char *end = field[1];
408 while ((start < end) && U_IS_INV_WHITESPACE(*(end - 1))) {
409 end--;
410 }
411 int32_t length = (int32_t)(end - start);
412 return StringPiece(start, length);
413}
414
415// Used for passing context to unitsTestDataLineFn via u_parseDelimitedFile.
416struct UnitsTestContext {
417 // Provides access to UnitsTest methods like logln.
418 UnitsTest *unitsTest;
419 // Conversion rates: does not take ownership.
420 ConversionRates *conversionRates;
421};
422
423/**
424 * Deals with a single data-driven unit test for unit conversions.
425 *
426 * This is a UParseLineFn as required by u_parseDelimitedFile, intended for
427 * parsing unitsTest.txt.
428 *
429 * @param context Must point at a UnitsTestContext struct.
430 * @param fields A list of pointer-pairs, each pair pointing at the start and
431 * end of each field. End pointers are important because these are *not*
432 * null-terminated strings. (Interpreted as a null-terminated string,
433 * fields[0][0] points at the whole line.)
434 * @param fieldCount The number of fields (pointer pairs) passed to the fields
435 * parameter.
436 * @param pErrorCode Receives status.
437 */
438void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, UErrorCode *pErrorCode) {
439 if (U_FAILURE(*pErrorCode)) {
440 return;
441 }
442 UnitsTestContext *ctx = (UnitsTestContext *)context;
443 UnitsTest *unitsTest = ctx->unitsTest;
444 (void)fieldCount; // unused UParseLineFn variable
445 IcuTestErrorCode status(*unitsTest, "unitsTestDatalineFn");
446
447 StringPiece quantity = trimField(fields[0]);
448 StringPiece x = trimField(fields[1]);
449 StringPiece y = trimField(fields[2]);
450 StringPiece commentConversionFormula = trimField(fields[3]);
451 StringPiece utf8Expected = trimField(fields[4]);
452
453 UNumberFormat *nf = unum_open(UNUM_DEFAULT, NULL, -1, "en_US", NULL, status);
454 if (status.errIfFailureAndReset("unum_open failed")) {
455 return;
456 }
457 UnicodeString uExpected = UnicodeString::fromUTF8(utf8Expected);
458 double expected = unum_parseDouble(nf, uExpected.getBuffer(), uExpected.length(), 0, status);
459 unum_close(nf);
460 if (status.errIfFailureAndReset("unum_parseDouble(\"%s\") failed", utf8Expected)) {
461 return;
462 }
463
464 CharString sourceIdent(x, status);
465 MeasureUnitImpl sourceUnit = MeasureUnitImpl::forIdentifier(x, status);
466 if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", x.length(), x.data())) {
467 return;
468 }
469
470 CharString targetIdent(y, status);
471 MeasureUnitImpl targetUnit = MeasureUnitImpl::forIdentifier(y, status);
472 if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", y.length(), y.data())) {
473 return;
474 }
475
476 unitsTest->logln("Quantity (Category): \"%.*s\", "
477 "Expected value of \"1000 %.*s in %.*s\": %f, "
478 "commentConversionFormula: \"%.*s\", ",
479 quantity.length(), quantity.data(), x.length(), x.data(), y.length(), y.data(),
480 expected, commentConversionFormula.length(), commentConversionFormula.data());
481
482 // Convertibility:
483 auto convertibility = extractConvertibility(sourceUnit, targetUnit, *ctx->conversionRates, status);
484 if (status.errIfFailureAndReset("extractConvertibility(<%s>, <%s>, ...)",
485 sourceIdent.data(), targetIdent.data())) {
486 return;
487 }
488 CharString msg;
489 msg.append("convertible: ", status)
490 .append(sourceIdent.data(), status)
491 .append(" -> ", status)
492 .append(targetIdent.data(), status);
493 if (status.errIfFailureAndReset("msg construction")) {
494 return;
495 }
496 unitsTest->assertNotEquals(msg.data(), UNCONVERTIBLE, convertibility);
497
498 // Conversion:
499 UnitsConverter converter(sourceUnit, targetUnit, *ctx->conversionRates, status);
500 if (status.errIfFailureAndReset("UnitsConverter(<%s>, <%s>, ...)", sourceIdent.data(),
501 targetIdent.data())) {
502 return;
503 }
504 double got = converter.convert(1000);
505 msg.clear();
506 msg.append("Converting 1000 ", status).append(x, status).append(" to ", status).append(y, status);
507 unitsTest->assertEqualsNear(msg.data(), expected, got, 0.0001 * expected);
508 double inverted = converter.convertInverse(got);
509 msg.clear();
510 msg.append("Converting back to ", status).append(x, status).append(" from ", status).append(y, status);
511 unitsTest->assertEqualsNear(msg.data(), 1000, inverted, 0.0001);
512}
513
514/**
515 * Runs data-driven unit tests for unit conversion. It looks for the test cases
516 * in source/test/testdata/cldr/units/unitsTest.txt, which originates in CLDR.
517 */
518void UnitsTest::testConverterWithCLDRTests() {
519 const char *filename = "unitsTest.txt";
520 const int32_t kNumFields = 5;
521 char *fields[kNumFields][2];
522
523 IcuTestErrorCode errorCode(*this, "UnitsTest::testConverterWithCLDRTests");
524 const char *sourceTestDataPath = getSourceTestData(errorCode);
525 if (errorCode.errIfFailureAndReset("unable to find the source/test/testdata "
526 "folder (getSourceTestData())")) {
527 return;
528 }
529
530 CharString path(sourceTestDataPath, errorCode);
531 path.appendPathPart("cldr/units", errorCode);
532 path.appendPathPart(filename, errorCode);
533
534 ConversionRates rates(errorCode);
535 UnitsTestContext ctx = {this, &rates};
536 u_parseDelimitedFile(path.data(), ';', fields, kNumFields, unitsTestDataLineFn, &ctx, errorCode);
537 if (errorCode.errIfFailureAndReset("error parsing %s: %s\n", path.data(), u_errorName(errorCode))) {
538 return;
539 }
540}
541
542void UnitsTest::testComplexUnitsConverter() {
543 IcuTestErrorCode status(*this, "UnitsTest::testComplexUnitsConverter");
544
545 // DBL_EPSILON is approximately 2.22E-16, and is the precision of double for
546 // values in the range [1.0, 2.0), but half the precision of double for
547 // [2.0, 4.0).
548 U_ASSERT(1.0 + DBL_EPSILON > 1.0);
549 U_ASSERT(2.0 - DBL_EPSILON < 2.0);
550 U_ASSERT(2.0 + DBL_EPSILON == 2.0);
551
552 struct TestCase {
553 const char* msg;
554 const char* input;
555 const char* output;
556 double value;
557 Measure expected[2];
558 int32_t expectedCount;
559 // For mixed units, accuracy of the smallest unit
560 double accuracy;
561 } testCases[]{
562 // Significantly less than 2.0.
563 {"1.9999",
564 "foot",
565 "foot-and-inch",
566 1.9999,
567 {Measure(1, MeasureUnit::createFoot(status), status),
568 Measure(11.9988, MeasureUnit::createInch(status), status)},
569 2,
570 0},
571
572 // A minimal nudge under 2.0, rounding up to 2.0 ft, 0 in.
573 {"2-eps",
574 "foot",
575 "foot-and-inch",
576 2.0 - DBL_EPSILON,
577 {Measure(2, MeasureUnit::createFoot(status), status),
578 Measure(0, MeasureUnit::createInch(status), status)},
579 2,
580 0},
581
582 // A slightly bigger nudge under 2.0, *not* rounding up to 2.0 ft!
583 {"2-3eps",
584 "foot",
585 "foot-and-inch",
586 2.0 - 3 * DBL_EPSILON,
587 {Measure(1, MeasureUnit::createFoot(status), status),
588 // We expect 12*3*DBL_EPSILON inches (7.92e-15) less than 12.
589 Measure(12 - 36 * DBL_EPSILON, MeasureUnit::createInch(status), status)},
590 2,
591 // Might accuracy be lacking with some compilers or on some systems? In
592 // case it is somehow lacking, we'll allow a delta of 12 * DBL_EPSILON.
593 12 * DBL_EPSILON},
594
595 // Testing precision with meter and light-year.
596 //
597 // DBL_EPSILON light-years, ~2.22E-16 light-years, is ~2.1 meters
598 // (maximum precision when exponent is 0).
599 //
600 // 1e-16 light years is 0.946073 meters.
601
602 // A 2.1 meter nudge under 2.0 light years, rounding up to 2.0 ly, 0 m.
603 {"2-eps",
604 "light-year",
605 "light-year-and-meter",
606 2.0 - DBL_EPSILON,
607 {Measure(2, MeasureUnit::createLightYear(status), status),
608 Measure(0, MeasureUnit::createMeter(status), status)},
609 2,
610 0},
611
612 // A 2.1 meter nudge under 1.0 light years, rounding up to 1.0 ly, 0 m.
613 {"1-eps",
614 "light-year",
615 "light-year-and-meter",
616 1.0 - DBL_EPSILON,
617 {Measure(1, MeasureUnit::createLightYear(status), status),
618 Measure(0, MeasureUnit::createMeter(status), status)},
619 2,
620 0},
621
622 // 1e-15 light years is 9.46073 meters (calculated using "bc" and the
623 // CLDR conversion factor). With double-precision maths in C++, we get
624 // 10.5. In this case, we're off by a bit more than 1 meter. With Java
625 // BigDecimal, we get accurate results.
626 {"1 + 1e-15",
627 "light-year",
628 "light-year-and-meter",
629 1.0 + 1e-15,
630 {Measure(1, MeasureUnit::createLightYear(status), status),
631 Measure(9.46073, MeasureUnit::createMeter(status), status)},
632 2,
633 1.5 /* meters, precision */},
634
635 // 2.1 meters more than 1 light year is not rounded away.
636 {"1 + eps",
637 "light-year",
638 "light-year-and-meter",
639 1.0 + DBL_EPSILON,
640 {Measure(1, MeasureUnit::createLightYear(status), status),
641 Measure(2.1, MeasureUnit::createMeter(status), status)},
642 2,
643 0.001},
Frank Tangd2858cb2022-04-08 20:34:12 -0700644
645 // Negative numbers
646 {"Negative number conversion",
647 "yard",
648 "mile-and-yard",
649 -1800,
650 {Measure(-1, MeasureUnit::createMile(status), status),
651 Measure(-40, MeasureUnit::createYard(status), status)},
652 2,
653 1e-10},
Frank Tang3e05d9d2021-11-08 14:04:04 -0800654 };
655 status.assertSuccess();
656
657 ConversionRates rates(status);
658 MeasureUnit input, output;
659 MeasureUnitImpl tempInput, tempOutput;
660 MaybeStackVector<Measure> measures;
661 auto testATestCase = [&](const ComplexUnitsConverter& converter ,StringPiece initMsg , const TestCase &testCase) {
662 measures = converter.convert(testCase.value, nullptr, status);
663
664 CharString msg(initMsg, status);
665 msg.append(testCase.msg, status);
666 msg.append(" ", status);
667 msg.append(testCase.input, status);
668 msg.append(" -> ", status);
669 msg.append(testCase.output, status);
670
671 CharString msgCount(msg, status);
672 msgCount.append(", measures.length()", status);
673 assertEquals(msgCount.data(), testCase.expectedCount, measures.length());
674 for (int i = 0; i < measures.length() && i < testCase.expectedCount; i++) {
675 if (i == testCase.expectedCount-1) {
676 assertEqualsNear(msg.data(), testCase.expected[i].getNumber().getDouble(status),
677 measures[i]->getNumber().getDouble(status), testCase.accuracy);
678 } else {
679 assertEquals(msg.data(), testCase.expected[i].getNumber().getDouble(status),
680 measures[i]->getNumber().getDouble(status));
681 }
682 assertEquals(msg.data(), testCase.expected[i].getUnit().getIdentifier(),
683 measures[i]->getUnit().getIdentifier());
684 }
685 };
686
687 for (const auto &testCase : testCases)
688 {
689 input = MeasureUnit::forIdentifier(testCase.input, status);
690 output = MeasureUnit::forIdentifier(testCase.output, status);
691 const MeasureUnitImpl& inputImpl = MeasureUnitImpl::forMeasureUnit(input, tempInput, status);
692 const MeasureUnitImpl& outputImpl = MeasureUnitImpl::forMeasureUnit(output, tempOutput, status);
693
694 ComplexUnitsConverter converter1(inputImpl, outputImpl, rates, status);
695 testATestCase(converter1, "ComplexUnitsConverter #1 " , testCase);
696
697 // Test ComplexUnitsConverter created with CLDR units identifiers.
698 ComplexUnitsConverter converter2( testCase.input, testCase.output, status);
699 testATestCase(converter2, "ComplexUnitsConverter #1 " , testCase);
700 }
Frank Tang3e05d9d2021-11-08 14:04:04 -0800701 status.assertSuccess();
Frank Tang3e05d9d2021-11-08 14:04:04 -0800702}
703
704void 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 */
789class 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.
866void 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 */
902void 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 */
1015void 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 */
1114void 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 */