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/unistr.h"
20 #include "unicode/unum.h"
21 #include "unicode/ures.h"
22 #include "units_complexconverter.h"
23 #include "units_converter.h"
24 #include "units_data.h"
25 #include "units_router.h"
26 #include "uparse.h"
27 #include "uresimp.h"
28
29 struct UnitConversionTestCase {
30 const StringPiece source;
31 const StringPiece target;
32 const double inputValue;
33 const double expectedValue;
34 };
35
36 using ::icu::number::impl::DecimalQuantity;
37 using namespace ::icu::units;
38
39 class UnitsTest : public IntlTest {
40 public:
UnitsTest()41 UnitsTest() {}
42
43 void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = NULL);
44
45 void testUnitConstantFreshness();
46 void testConversionCapability();
47 void testConversions();
48 void testComplexUnitsConverter();
49 void testComplexUnitConverterSorting();
50 void testPreferences();
51 void testSiPrefixes();
52 void testMass();
53 void testTemperature();
54 void testArea();
55 };
56
createUnitsTest()57 extern IntlTest *createUnitsTest() { return new UnitsTest(); }
58
runIndexedTest(int32_t index,UBool exec,const char * & name,char *)59 void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) {
60 if (exec) {
61 logln("TestSuite UnitsTest: ");
62 }
63 TESTCASE_AUTO_BEGIN;
64 TESTCASE_AUTO(testUnitConstantFreshness);
65 TESTCASE_AUTO(testConversionCapability);
66 TESTCASE_AUTO(testConversions);
67 TESTCASE_AUTO(testComplexUnitsConverter);
68 TESTCASE_AUTO(testComplexUnitConverterSorting);
69 TESTCASE_AUTO(testPreferences);
70 TESTCASE_AUTO(testSiPrefixes);
71 TESTCASE_AUTO(testMass);
72 TESTCASE_AUTO(testTemperature);
73 TESTCASE_AUTO(testArea);
74 TESTCASE_AUTO_END;
75 }
76
77 // Tests the hard-coded constants in the code against constants that appear in
78 // units.txt.
testUnitConstantFreshness()79 void UnitsTest::testUnitConstantFreshness() {
80 IcuTestErrorCode status(*this, "testUnitConstantFreshness");
81 LocalUResourceBundlePointer unitsBundle(ures_openDirect(NULL, "units", status));
82 LocalUResourceBundlePointer unitConstants(
83 ures_getByKey(unitsBundle.getAlias(), "unitConstants", NULL, status));
84
85 while (ures_hasNext(unitConstants.getAlias())) {
86 int32_t len;
87 const char *constant = NULL;
88 ures_getNextString(unitConstants.getAlias(), &len, &constant, status);
89
90 Factor factor;
91 addSingleFactorConstant(constant, 1, POSITIVE, factor, status);
92 if (status.errDataIfFailureAndReset(
93 "addSingleFactorConstant(<%s>, ...).\n\n"
94 "If U_INVALID_FORMAT_ERROR, please check that \"icu4c/source/i18n/units_converter.cpp\" "
95 "has all constants? Is \"%s\" a new constant?\n",
96 constant, constant)) {
97 continue;
98 }
99
100 // Check the values of constants that have a simple numeric value
101 factor.substituteConstants();
102 int32_t uLen;
103 UnicodeString uVal = ures_getStringByKey(unitConstants.getAlias(), constant, &uLen, status);
104 CharString val;
105 val.appendInvariantChars(uVal, status);
106 if (status.errDataIfFailureAndReset("Failed to get constant value for %s.", constant)) {
107 continue;
108 }
109 DecimalQuantity dqVal;
110 UErrorCode parseStatus = U_ZERO_ERROR;
111 // TODO(units): unify with strToDouble() in units_converter.cpp
112 dqVal.setToDecNumber(val.toStringPiece(), parseStatus);
113 if (!U_SUCCESS(parseStatus)) {
114 // Not simple to parse, skip validating this constant's value. (We
115 // leave catching mistakes to the data-driven integration tests.)
116 continue;
117 }
118 double expectedNumerator = dqVal.toDouble();
119 assertEquals(UnicodeString("Constant ") + constant + u" numerator", expectedNumerator,
120 factor.factorNum);
121 assertEquals(UnicodeString("Constant ") + constant + u" denominator", 1.0, factor.factorDen);
122 }
123 }
124
testConversionCapability()125 void UnitsTest::testConversionCapability() {
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 };
141
142 for (const auto &testCase : testCases) {
143 UErrorCode status = U_ZERO_ERROR;
144
145 MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
146 MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
147
148 ConversionRates conversionRates(status);
149 auto convertibility = extractConvertibility(source, target, conversionRates, status);
150
151 assertEquals(UnicodeString("Conversion Capability: ") + testCase.source + " to " +
152 testCase.target,
153 testCase.expectedState, convertibility);
154 }
155 }
156
testSiPrefixes()157 void UnitsTest::testSiPrefixes() {
158 IcuTestErrorCode status(*this, "Units testSiPrefixes");
159 // Test Cases
160 struct TestCase {
161 const char *source;
162 const char *target;
163 const double inputValue;
164 const double expectedValue;
165 } testCases[]{
166 {"gram", "kilogram", 1.0, 0.001}, //
167 {"milligram", "kilogram", 1.0, 0.000001}, //
168 {"microgram", "kilogram", 1.0, 0.000000001}, //
169 {"megagram", "gram", 1.0, 1000000}, //
170 {"megagram", "kilogram", 1.0, 1000}, //
171 {"gigabyte", "byte", 1.0, 1000000000}, //
172 // TODO: Fix `watt` probelms.
173 // {"megawatt", "watt", 1.0, 1000000}, //
174 // {"megawatt", "kilowatt", 1.0, 1000}, //
175 };
176
177 for (const auto &testCase : testCases) {
178 UErrorCode status = U_ZERO_ERROR;
179
180 MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
181 MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
182
183 ConversionRates conversionRates(status);
184 UnitConverter converter(source, target, conversionRates, status);
185
186 assertEqualsNear(UnicodeString("testSiPrefixes: ") + testCase.source + " to " + testCase.target,
187 testCase.expectedValue, converter.convert(testCase.inputValue),
188 0.0001 * testCase.expectedValue);
189 }
190 }
191
testMass()192 void UnitsTest::testMass() {
193 IcuTestErrorCode status(*this, "Units testMass");
194
195 // Test Cases
196 struct TestCase {
197 const char *source;
198 const char *target;
199 const double inputValue;
200 const double expectedValue;
201 } testCases[]{
202 {"gram", "kilogram", 1.0, 0.001}, //
203 {"pound", "kilogram", 1.0, 0.453592}, //
204 {"pound", "kilogram", 2.0, 0.907185}, //
205 {"ounce", "pound", 16.0, 1.0}, //
206 {"ounce", "kilogram", 16.0, 0.453592}, //
207 {"ton", "pound", 1.0, 2000}, //
208 {"stone", "pound", 1.0, 14}, //
209 {"stone", "kilogram", 1.0, 6.35029} //
210 };
211
212 for (const auto &testCase : testCases) {
213 UErrorCode status = U_ZERO_ERROR;
214
215 MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
216 MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
217
218 ConversionRates conversionRates(status);
219 UnitConverter converter(source, target, conversionRates, status);
220
221 assertEqualsNear(UnicodeString("testMass: ") + testCase.source + " to " + testCase.target,
222 testCase.expectedValue, converter.convert(testCase.inputValue),
223 0.0001 * testCase.expectedValue);
224 }
225 }
226
testTemperature()227 void UnitsTest::testTemperature() {
228 IcuTestErrorCode status(*this, "Units testTemperature");
229 // Test Cases
230 struct TestCase {
231 const char *source;
232 const char *target;
233 const double inputValue;
234 const double expectedValue;
235 } testCases[]{
236 {"celsius", "fahrenheit", 0.0, 32.0}, //
237 {"celsius", "fahrenheit", 10.0, 50.0}, //
238 {"fahrenheit", "celsius", 32.0, 0.0}, //
239 {"fahrenheit", "celsius", 89.6, 32}, //
240 {"kelvin", "fahrenheit", 0.0, -459.67}, //
241 {"kelvin", "fahrenheit", 300, 80.33}, //
242 {"kelvin", "celsius", 0.0, -273.15}, //
243 {"kelvin", "celsius", 300.0, 26.85} //
244 };
245
246 for (const auto &testCase : testCases) {
247 UErrorCode status = U_ZERO_ERROR;
248
249 MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
250 MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
251
252 ConversionRates conversionRates(status);
253 UnitConverter converter(source, target, conversionRates, status);
254
255 assertEqualsNear(UnicodeString("testTemperature: ") + testCase.source + " to " + testCase.target,
256 testCase.expectedValue, converter.convert(testCase.inputValue),
257 0.0001 * uprv_fabs(testCase.expectedValue));
258 }
259 }
260
testArea()261 void UnitsTest::testArea() {
262 IcuTestErrorCode status(*this, "Units Area");
263
264 // Test Cases
265 struct TestCase {
266 const char *source;
267 const char *target;
268 const double inputValue;
269 const double expectedValue;
270 } testCases[]{
271 {"square-meter", "square-yard", 10.0, 11.9599}, //
272 {"hectare", "square-yard", 1.0, 11959.9}, //
273 {"square-mile", "square-foot", 0.0001, 2787.84}, //
274 {"hectare", "square-yard", 1.0, 11959.9}, //
275 {"hectare", "square-meter", 1.0, 10000}, //
276 {"hectare", "square-meter", 0.0, 0.0}, //
277 {"square-mile", "square-foot", 0.0001, 2787.84}, //
278 {"square-yard", "square-foot", 10, 90}, //
279 {"square-yard", "square-foot", 0, 0}, //
280 {"square-yard", "square-foot", 0.000001, 0.000009}, //
281 {"square-mile", "square-foot", 0.0, 0.0}, //
282 };
283
284 for (const auto &testCase : testCases) {
285 UErrorCode status = U_ZERO_ERROR;
286
287 MeasureUnitImpl source = MeasureUnitImpl::forIdentifier(testCase.source, status);
288 MeasureUnitImpl target = MeasureUnitImpl::forIdentifier(testCase.target, status);
289
290 ConversionRates conversionRates(status);
291 UnitConverter converter(source, target, conversionRates, status);
292
293 assertEqualsNear(UnicodeString("testArea: ") + testCase.source + " to " + testCase.target,
294 testCase.expectedValue, converter.convert(testCase.inputValue),
295 0.0001 * testCase.expectedValue);
296 }
297 }
298
299 /**
300 * Trims whitespace off of the specified string.
301 * @param field is two pointers pointing at the start and end of the string.
302 * @return A StringPiece with initial and final space characters trimmed off.
303 */
trimField(char * (& field)[2])304 StringPiece trimField(char *(&field)[2]) {
305 const char *start = field[0];
306 start = u_skipWhitespace(start);
307 if (start >= field[1]) {
308 start = field[1];
309 }
310 const char *end = field[1];
311 while ((start < end) && U_IS_INV_WHITESPACE(*(end - 1))) {
312 end--;
313 }
314 int32_t length = (int32_t)(end - start);
315 return StringPiece(start, length);
316 }
317
318 // Used for passing context to unitsTestDataLineFn via u_parseDelimitedFile.
319 struct UnitsTestContext {
320 // Provides access to UnitsTest methods like logln.
321 UnitsTest *unitsTest;
322 // Conversion rates: does not take ownership.
323 ConversionRates *conversionRates;
324 };
325
326 /**
327 * Deals with a single data-driven unit test for unit conversions.
328 *
329 * This is a UParseLineFn as required by u_parseDelimitedFile, intended for
330 * parsing unitsTest.txt.
331 *
332 * @param context Must point at a UnitsTestContext struct.
333 * @param fields A list of pointer-pairs, each pair pointing at the start and
334 * end of each field. End pointers are important because these are *not*
335 * null-terminated strings. (Interpreted as a null-terminated string,
336 * fields[0][0] points at the whole line.)
337 * @param fieldCount The number of fields (pointer pairs) passed to the fields
338 * parameter.
339 * @param pErrorCode Receives status.
340 */
unitsTestDataLineFn(void * context,char * fields[][2],int32_t fieldCount,UErrorCode * pErrorCode)341 void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, UErrorCode *pErrorCode) {
342 if (U_FAILURE(*pErrorCode)) {
343 return;
344 }
345 UnitsTestContext *ctx = (UnitsTestContext *)context;
346 UnitsTest *unitsTest = ctx->unitsTest;
347 (void)fieldCount; // unused UParseLineFn variable
348 IcuTestErrorCode status(*unitsTest, "unitsTestDatalineFn");
349
350 StringPiece quantity = trimField(fields[0]);
351 StringPiece x = trimField(fields[1]);
352 StringPiece y = trimField(fields[2]);
353 StringPiece commentConversionFormula = trimField(fields[3]);
354 StringPiece utf8Expected = trimField(fields[4]);
355
356 UNumberFormat *nf = unum_open(UNUM_DEFAULT, NULL, -1, "en_US", NULL, status);
357 if (status.errIfFailureAndReset("unum_open failed")) {
358 return;
359 }
360 UnicodeString uExpected = UnicodeString::fromUTF8(utf8Expected);
361 double expected = unum_parseDouble(nf, uExpected.getBuffer(), uExpected.length(), 0, status);
362 unum_close(nf);
363 if (status.errIfFailureAndReset("unum_parseDouble(\"%s\") failed", utf8Expected)) {
364 return;
365 }
366
367 CharString sourceIdent(x, status);
368 MeasureUnitImpl sourceUnit = MeasureUnitImpl::forIdentifier(x, status);
369 if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", x.length(), x.data())) {
370 return;
371 }
372
373 CharString targetIdent(y, status);
374 MeasureUnitImpl targetUnit = MeasureUnitImpl::forIdentifier(y, status);
375 if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", y.length(), y.data())) {
376 return;
377 }
378
379 unitsTest->logln("Quantity (Category): \"%.*s\", "
380 "Expected value of \"1000 %.*s in %.*s\": %f, "
381 "commentConversionFormula: \"%.*s\", ",
382 quantity.length(), quantity.data(), x.length(), x.data(), y.length(), y.data(),
383 expected, commentConversionFormula.length(), commentConversionFormula.data());
384
385 // Convertibility:
386 auto convertibility = extractConvertibility(sourceUnit, targetUnit, *ctx->conversionRates, status);
387 if (status.errIfFailureAndReset("extractConvertibility(<%s>, <%s>, ...)",
388 sourceIdent.data(), targetIdent.data())) {
389 return;
390 }
391 CharString msg;
392 msg.append("convertible: ", status)
393 .append(sourceIdent.data(), status)
394 .append(" -> ", status)
395 .append(targetIdent.data(), status);
396 if (status.errIfFailureAndReset("msg construction")) {
397 return;
398 }
399 unitsTest->assertNotEquals(msg.data(), UNCONVERTIBLE, convertibility);
400
401 // Conversion:
402 UnitConverter converter(sourceUnit, targetUnit, *ctx->conversionRates, status);
403 if (status.errIfFailureAndReset("constructor: UnitConverter(<%s>, <%s>, status)",
404 sourceIdent.data(), targetIdent.data())) {
405 return;
406 }
407 double got = converter.convert(1000);
408 msg.clear();
409 msg.append("Converting 1000 ", status).append(x, status).append(" to ", status).append(y, status);
410 unitsTest->assertEqualsNear(msg.data(), expected, got, 0.0001 * expected);
411 double inverted = converter.convertInverse(got);
412 msg.clear();
413 msg.append("Converting back to ", status).append(x, status).append(" from ", status).append(y, status);
414 unitsTest->assertEqualsNear(msg.data(), 1000, inverted, 0.0001);
415 }
416
417 /**
418 * Runs data-driven unit tests for unit conversion. It looks for the test cases
419 * in source/test/testdata/cldr/units/unitsTest.txt, which originates in CLDR.
420 */
testConversions()421 void UnitsTest::testConversions() {
422 const char *filename = "unitsTest.txt";
423 const int32_t kNumFields = 5;
424 char *fields[kNumFields][2];
425
426 IcuTestErrorCode errorCode(*this, "UnitsTest::testConversions");
427 const char *sourceTestDataPath = getSourceTestData(errorCode);
428 if (errorCode.errIfFailureAndReset("unable to find the source/test/testdata "
429 "folder (getSourceTestData())")) {
430 return;
431 }
432
433 CharString path(sourceTestDataPath, errorCode);
434 path.appendPathPart("cldr/units", errorCode);
435 path.appendPathPart(filename, errorCode);
436
437 ConversionRates rates(errorCode);
438 UnitsTestContext ctx = {this, &rates};
439 u_parseDelimitedFile(path.data(), ';', fields, kNumFields, unitsTestDataLineFn, &ctx, errorCode);
440 if (errorCode.errIfFailureAndReset("error parsing %s: %s\n", path.data(), u_errorName(errorCode))) {
441 return;
442 }
443 }
444
testComplexUnitsConverter()445 void UnitsTest::testComplexUnitsConverter() {
446 IcuTestErrorCode status(*this, "UnitsTest::testComplexUnitsConverter");
447 ConversionRates rates(status);
448 MeasureUnit input = MeasureUnit::getFoot();
449 MeasureUnit output = MeasureUnit::forIdentifier("foot-and-inch", status);
450 MeasureUnitImpl tempInput, tempOutput;
451 const MeasureUnitImpl &inputImpl = MeasureUnitImpl::forMeasureUnit(input, tempInput, status);
452 const MeasureUnitImpl &outputImpl = MeasureUnitImpl::forMeasureUnit(output, tempOutput, status);
453 auto converter = ComplexUnitsConverter(inputImpl, outputImpl, rates, status);
454
455 // Significantly less than 2.0.
456 MaybeStackVector<Measure> measures = converter.convert(1.9999, nullptr, status);
457 assertEquals("measures length", 2, measures.length());
458 if (2 == measures.length()) {
459 assertEquals("1.9999: measures[0] value", 1.0, measures[0]->getNumber().getDouble(status));
460 assertEquals("1.9999: measures[0] unit", MeasureUnit::getFoot().getIdentifier(),
461 measures[0]->getUnit().getIdentifier());
462 assertEqualsNear("1.9999: measures[1] value", 11.9988,
463 measures[1]->getNumber().getDouble(status), 0.0001);
464 assertEquals("1.9999: measures[1] unit", MeasureUnit::getInch().getIdentifier(),
465 measures[1]->getUnit().getIdentifier());
466 }
467
468 // TODO(icu-units#100): consider factoring out the set of tests to make this function more
469 // data-driven.
470
471 // A minimal nudge under 2.0.
472 measures = converter.convert((2.0 - DBL_EPSILON), nullptr, status);
473 assertEquals("measures length", 2, measures.length());
474 if (2 == measures.length()) {
475 assertEquals("1 - eps: measures[0] value", 2.0, measures[0]->getNumber().getDouble(status));
476 assertEquals("1 - eps: measures[0] unit", MeasureUnit::getFoot().getIdentifier(),
477 measures[0]->getUnit().getIdentifier());
478 assertEquals("1 - eps: measures[1] value", 0.0, measures[1]->getNumber().getDouble(status));
479 assertEquals("1 - eps: measures[1] unit", MeasureUnit::getInch().getIdentifier(),
480 measures[1]->getUnit().getIdentifier());
481 }
482
483 // Testing precision with meter and light-year. 1e-16 light years is
484 // 0.946073 meters, and double precision can provide only ~15 decimal
485 // digits, so we don't expect to get anything less than 1 meter.
486
487 // An epsilon's nudge under one light-year: should give 1 ly, 0 m.
488 input = MeasureUnit::getLightYear();
489 output = MeasureUnit::forIdentifier("light-year-and-meter", status);
490 const MeasureUnitImpl &inputImpl3 = MeasureUnitImpl::forMeasureUnit(input, tempInput, status);
491 const MeasureUnitImpl &outputImpl3 = MeasureUnitImpl::forMeasureUnit(output, tempOutput, status);
492 converter = ComplexUnitsConverter(inputImpl3, outputImpl3, rates, status);
493 measures = converter.convert((2.0 - DBL_EPSILON), nullptr, status);
494 assertEquals("measures length", 2, measures.length());
495 if (2 == measures.length()) {
496 assertEquals("light-year test: measures[0] value", 2.0,
497 measures[0]->getNumber().getDouble(status));
498 assertEquals("light-year test: measures[0] unit", MeasureUnit::getLightYear().getIdentifier(),
499 measures[0]->getUnit().getIdentifier());
500 assertEquals("light-year test: measures[1] value", 0.0,
501 measures[1]->getNumber().getDouble(status));
502 assertEquals("light-year test: measures[1] unit", MeasureUnit::getMeter().getIdentifier(),
503 measures[1]->getUnit().getIdentifier());
504 }
505
506 // 1e-15 light years is 9.46073 meters (calculated using "bc" and the CLDR
507 // conversion factor). With double-precision maths, we get 10.5. In this
508 // case, we're off by almost 1 meter.
509 measures = converter.convert((1.0 + 1e-15), nullptr, status);
510 assertEquals("measures length", 2, measures.length());
511 if (2 == measures.length()) {
512 assertEquals("light-year test: measures[0] value", 1.0,
513 measures[0]->getNumber().getDouble(status));
514 assertEquals("light-year test: measures[0] unit", MeasureUnit::getLightYear().getIdentifier(),
515 measures[0]->getUnit().getIdentifier());
516 assertEqualsNear("light-year test: measures[1] value", 10,
517 measures[1]->getNumber().getDouble(status), 1);
518 assertEquals("light-year test: measures[1] unit", MeasureUnit::getMeter().getIdentifier(),
519 measures[1]->getUnit().getIdentifier());
520 }
521
522 // 2e-16 light years is 1.892146 meters. We consider this in the noise, and
523 // thus expect a 0. (This test fails when 2e-16 is increased to 4e-16.)
524 measures = converter.convert((1.0 + 2e-16), nullptr, status);
525 assertEquals("measures length", 2, measures.length());
526 if (2 == measures.length()) {
527 assertEquals("light-year test: measures[0] value", 1.0,
528 measures[0]->getNumber().getDouble(status));
529 assertEquals("light-year test: measures[0] unit", MeasureUnit::getLightYear().getIdentifier(),
530 measures[0]->getUnit().getIdentifier());
531 assertEquals("light-year test: measures[1] value", 0.0,
532 measures[1]->getNumber().getDouble(status));
533 assertEquals("light-year test: measures[1] unit", MeasureUnit::getMeter().getIdentifier(),
534 measures[1]->getUnit().getIdentifier());
535 }
536
537 // TODO(icu-units#63): test negative numbers!
538 }
539
testComplexUnitConverterSorting()540 void UnitsTest::testComplexUnitConverterSorting() {
541 IcuTestErrorCode status(*this, "UnitsTest::testComplexUnitConverterSorting");
542
543 MeasureUnitImpl source = MeasureUnitImpl::forIdentifier("meter", status);
544 MeasureUnitImpl target = MeasureUnitImpl::forIdentifier("inch-and-foot", status);
545 ConversionRates conversionRates(status);
546
547 ComplexUnitsConverter complexConverter(source, target, conversionRates, status);
548 auto measures = complexConverter.convert(10.0, nullptr, status);
549
550 if (2 == measures.length()) {
551 assertEquals("inch-and-foot unit 0", "inch", measures[0]->getUnit().getIdentifier());
552 assertEquals("inch-and-foot unit 1", "foot", measures[1]->getUnit().getIdentifier());
553
554 assertEqualsNear("inch-and-foot value 0", 9.7008, measures[0]->getNumber().getDouble(), 0.0001);
555 assertEqualsNear("inch-and-foot value 1", 32, measures[1]->getNumber().getInt64(), 0.00001);
556 }
557 }
558
559 /**
560 * This class represents the output fields from unitPreferencesTest.txt. Please
561 * see the documentation at the top of that file for details.
562 *
563 * For "mixed units" output, there are more (repeated) output fields. The last
564 * output unit has the expected output specified as both a rational fraction and
565 * a decimal fraction. This class ignores rational fractions, and expects to
566 * find a decimal fraction for each output unit.
567 */
568 class ExpectedOutput {
569 public:
570 // Counts number of units in the output. When this is more than one, we have
571 // "mixed units" in the expected output.
572 int _compoundCount = 0;
573
574 // Counts how many fields were skipped: we expect to skip only one per
575 // output unit type (the rational fraction).
576 int _skippedFields = 0;
577
578 // The expected output units: more than one for "mixed units".
579 MeasureUnit _measureUnits[3];
580
581 // The amounts of each of the output units.
582 double _amounts[3];
583
584 /**
585 * Parse an expected output field from the test data file.
586 *
587 * @param output may be a string representation of an integer, a rational
588 * fraction, a decimal fraction, or it may be a unit identifier. Whitespace
589 * should already be trimmed. This function ignores rational fractions,
590 * saving only decimal fractions and their unit identifiers.
591 * @return true if the field was successfully parsed, false if parsing
592 * failed.
593 */
parseOutputField(StringPiece output,UErrorCode & errorCode)594 void parseOutputField(StringPiece output, UErrorCode &errorCode) {
595 if (U_FAILURE(errorCode)) return;
596 DecimalQuantity dqOutputD;
597
598 dqOutputD.setToDecNumber(output, errorCode);
599 if (U_SUCCESS(errorCode)) {
600 _amounts[_compoundCount] = dqOutputD.toDouble();
601 return;
602 } else if (errorCode == U_DECIMAL_NUMBER_SYNTAX_ERROR) {
603 // Not a decimal fraction, it might be a rational fraction or a unit
604 // identifier: continue.
605 errorCode = U_ZERO_ERROR;
606 } else {
607 // Unexpected error, so we propagate it.
608 return;
609 }
610
611 _measureUnits[_compoundCount] = MeasureUnit::forIdentifier(output, errorCode);
612 if (U_SUCCESS(errorCode)) {
613 _compoundCount++;
614 _skippedFields = 0;
615 return;
616 }
617 _skippedFields++;
618 if (_skippedFields < 2) {
619 // We are happy skipping one field per output unit: we want to skip
620 // rational fraction fields like "11 / 10".
621 errorCode = U_ZERO_ERROR;
622 return;
623 } else {
624 // Propagate the error.
625 return;
626 }
627 }
628
629 /**
630 * Produces an output string for debug purposes.
631 */
toDebugString()632 std::string toDebugString() {
633 std::string result;
634 for (int i = 0; i < _compoundCount; i++) {
635 result += std::to_string(_amounts[i]);
636 result += " ";
637 result += _measureUnits[i].getIdentifier();
638 result += " ";
639 }
640 return result;
641 }
642 };
643
644 // Checks a vector of Measure instances against ExpectedOutput.
checkOutput(UnitsTest * unitsTest,const char * msg,ExpectedOutput expected,const MaybeStackVector<Measure> & actual,double precision)645 void checkOutput(UnitsTest *unitsTest, const char *msg, ExpectedOutput expected,
646 const MaybeStackVector<Measure> &actual, double precision) {
647 IcuTestErrorCode status(*unitsTest, "checkOutput");
648
649 CharString testMessage("Test case \"", status);
650 testMessage.append(msg, status);
651 testMessage.append("\": expected output: ", status);
652 testMessage.append(expected.toDebugString().c_str(), status);
653 testMessage.append(", obtained output:", status);
654 for (int i = 0; i < actual.length(); i++) {
655 testMessage.append(" ", status);
656 testMessage.append(std::to_string(actual[i]->getNumber().getDouble(status)), status);
657 testMessage.append(" ", status);
658 testMessage.appendInvariantChars(actual[i]->getUnit().getIdentifier(), status);
659 }
660 if (!unitsTest->assertEquals(testMessage.data(), expected._compoundCount, actual.length())) {
661 return;
662 };
663 for (int i = 0; i < actual.length(); i++) {
664 double permittedDiff = precision * expected._amounts[i];
665 if (permittedDiff == 0) {
666 // If 0 is expected, still permit a small delta.
667 // TODO: revisit this experimentally chosen value:
668 permittedDiff = 0.00000001;
669 }
670 unitsTest->assertEqualsNear(testMessage.data(), expected._amounts[i],
671 actual[i]->getNumber().getDouble(status), permittedDiff);
672 }
673 }
674
675 /**
676 * Runs a single data-driven unit test for unit preferences.
677 *
678 * This is a UParseLineFn as required by u_parseDelimitedFile, intended for
679 * parsing unitPreferencesTest.txt.
680 */
unitPreferencesTestDataLineFn(void * context,char * fields[][2],int32_t fieldCount,UErrorCode * pErrorCode)681 void unitPreferencesTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount,
682 UErrorCode *pErrorCode) {
683 if (U_FAILURE(*pErrorCode)) return;
684 UnitsTest *unitsTest = (UnitsTest *)context;
685 IcuTestErrorCode status(*unitsTest, "unitPreferencesTestDatalineFn");
686
687 if (!unitsTest->assertTrue(u"unitPreferencesTestDataLineFn expects 9 fields for simple and 11 "
688 u"fields for compound. Other field counts not yet supported. ",
689 fieldCount == 9 || fieldCount == 11)) {
690 return;
691 }
692
693 StringPiece quantity = trimField(fields[0]);
694 StringPiece usage = trimField(fields[1]);
695 StringPiece region = trimField(fields[2]);
696 // Unused // StringPiece inputR = trimField(fields[3]);
697 StringPiece inputD = trimField(fields[4]);
698 StringPiece inputUnit = trimField(fields[5]);
699 ExpectedOutput expected;
700 for (int i = 6; i < fieldCount; i++) {
701 expected.parseOutputField(trimField(fields[i]), status);
702 }
703 if (status.errIfFailureAndReset("parsing unitPreferencesTestData.txt test case: %s", fields[0][0])) {
704 return;
705 }
706
707 DecimalQuantity dqInputD;
708 dqInputD.setToDecNumber(inputD, status);
709 if (status.errIfFailureAndReset("parsing decimal quantity: \"%.*s\"", inputD.length(),
710 inputD.data())) {
711 return;
712 }
713 double inputAmount = dqInputD.toDouble();
714
715 MeasureUnit inputMeasureUnit = MeasureUnit::forIdentifier(inputUnit, status);
716 if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", inputUnit.length(), inputUnit.data())) {
717 return;
718 }
719
720 unitsTest->logln("Quantity (Category): \"%.*s\", Usage: \"%.*s\", Region: \"%.*s\", "
721 "Input: \"%f %s\", Expected Output: %s",
722 quantity.length(), quantity.data(), usage.length(), usage.data(), region.length(),
723 region.data(), inputAmount, inputMeasureUnit.getIdentifier(),
724 expected.toDebugString().c_str());
725
726 if (U_FAILURE(status)) {
727 return;
728 }
729
730 UnitsRouter router(inputMeasureUnit, region, usage, status);
731 if (status.errIfFailureAndReset("UnitsRouter(<%s>, \"%.*s\", \"%.*s\", status)",
732 inputMeasureUnit.getIdentifier(), region.length(), region.data(),
733 usage.length(), usage.data())) {
734 return;
735 }
736
737 CharString msg(quantity, status);
738 msg.append(" ", status);
739 msg.append(usage, status);
740 msg.append(" ", status);
741 msg.append(region, status);
742 msg.append(" ", status);
743 msg.append(inputD, status);
744 msg.append(" ", status);
745 msg.append(inputMeasureUnit.getIdentifier(), status);
746 if (status.errIfFailureAndReset("Failure before router.route")) {
747 return;
748 }
749 RouteResult routeResult = router.route(inputAmount, nullptr, status);
750 if (status.errIfFailureAndReset("router.route(inputAmount, ...)")) {
751 return;
752 }
753 // TODO: revisit this experimentally chosen precision:
754 checkOutput(unitsTest, msg.data(), expected, routeResult.measures, 0.0000000001);
755 }
756
757 /**
758 * Parses the format used by unitPreferencesTest.txt, calling lineFn for each
759 * line.
760 *
761 * This is a modified version of u_parseDelimitedFile, customized for
762 * unitPreferencesTest.txt, due to it having a variable number of fields per
763 * line.
764 */
parsePreferencesTests(const char * filename,char delimiter,char * fields[][2],int32_t maxFieldCount,UParseLineFn * lineFn,void * context,UErrorCode * pErrorCode)765 void parsePreferencesTests(const char *filename, char delimiter, char *fields[][2],
766 int32_t maxFieldCount, UParseLineFn *lineFn, void *context,
767 UErrorCode *pErrorCode) {
768 FileStream *file;
769 char line[10000];
770 char *start, *limit;
771 int32_t i;
772
773 if (U_FAILURE(*pErrorCode)) {
774 return;
775 }
776
777 if (fields == NULL || lineFn == NULL || maxFieldCount <= 0) {
778 *pErrorCode = U_ILLEGAL_ARGUMENT_ERROR;
779 return;
780 }
781
782 if (filename == NULL || *filename == 0 || (*filename == '-' && filename[1] == 0)) {
783 filename = NULL;
784 file = T_FileStream_stdin();
785 } else {
786 file = T_FileStream_open(filename, "r");
787 }
788 if (file == NULL) {
789 *pErrorCode = U_FILE_ACCESS_ERROR;
790 return;
791 }
792
793 while (T_FileStream_readLine(file, line, sizeof(line)) != NULL) {
794 /* remove trailing newline characters */
795 u_rtrim(line);
796
797 start = line;
798 *pErrorCode = U_ZERO_ERROR;
799
800 /* skip this line if it is empty or a comment */
801 if (*start == 0 || *start == '#') {
802 continue;
803 }
804
805 /* remove in-line comments */
806 limit = uprv_strchr(start, '#');
807 if (limit != NULL) {
808 /* get white space before the pound sign */
809 while (limit > start && U_IS_INV_WHITESPACE(*(limit - 1))) {
810 --limit;
811 }
812
813 /* truncate the line */
814 *limit = 0;
815 }
816
817 /* skip lines with only whitespace */
818 if (u_skipWhitespace(start)[0] == 0) {
819 continue;
820 }
821
822 /* for each field, call the corresponding field function */
823 for (i = 0; i < maxFieldCount; ++i) {
824 /* set the limit pointer of this field */
825 limit = start;
826 while (*limit != delimiter && *limit != 0) {
827 ++limit;
828 }
829
830 /* set the field start and limit in the fields array */
831 fields[i][0] = start;
832 fields[i][1] = limit;
833
834 /* set start to the beginning of the next field, if any */
835 start = limit;
836 if (*start != 0) {
837 ++start;
838 } else {
839 break;
840 }
841 }
842 if (i == maxFieldCount) {
843 *pErrorCode = U_PARSE_ERROR;
844 }
845 int fieldCount = i + 1;
846
847 /* call the field function */
848 lineFn(context, fields, fieldCount, pErrorCode);
849 if (U_FAILURE(*pErrorCode)) {
850 break;
851 }
852 }
853
854 if (filename != NULL) {
855 T_FileStream_close(file);
856 }
857 }
858
859 /**
860 * Runs data-driven unit tests for unit preferences. It looks for the test cases
861 * in source/test/testdata/cldr/units/unitPreferencesTest.txt, which originates
862 * in CLDR.
863 */
testPreferences()864 void UnitsTest::testPreferences() {
865 const char *filename = "unitPreferencesTest.txt";
866 const int32_t maxFields = 11;
867 char *fields[maxFields][2];
868
869 IcuTestErrorCode errorCode(*this, "UnitsTest::testPreferences");
870 const char *sourceTestDataPath = getSourceTestData(errorCode);
871 if (errorCode.errIfFailureAndReset("unable to find the source/test/testdata "
872 "folder (getSourceTestData())")) {
873 return;
874 }
875
876 CharString path(sourceTestDataPath, errorCode);
877 path.appendPathPart("cldr/units", errorCode);
878 path.appendPathPart(filename, errorCode);
879
880 parsePreferencesTests(path.data(), ';', fields, maxFields, unitPreferencesTestDataLineFn, this,
881 errorCode);
882 if (errorCode.errIfFailureAndReset("error parsing %s: %s\n", path.data(), u_errorName(errorCode))) {
883 return;
884 }
885 }
886
887 #endif /* #if !UCONFIG_NO_FORMATTING */
888