]> granicus.if.org Git - icu/commitdiff
merge sffc units-staging branch
authorYounies Mahmoud <youniesmahmoud@youniess-imac.home>
Mon, 9 Mar 2020 19:27:40 +0000 (20:27 +0100)
committerYounies Mahmoud <youniesmahmoud@youniess-imac.home>
Mon, 9 Mar 2020 19:27:40 +0000 (20:27 +0100)
1  2 
icu4c/source/i18n/unitconverter.cpp
icu4c/source/test/intltest/unitstest.cpp
tools/cldr/cldr-to-icu/src/main/java/org/unicode/icu/tool/cldrtoicu/LdmlConverter.java

index 15c1ecbec9098340ca7995d3a1e96a0e4180f93d,0e281344bf083f25f5dd53df87a7b485f21432f3..111ee32493e961012431a6c1e4df24f315df5b90
  
  #if !UCONFIG_NO_FORMATTING
  
 +#include <cmath>
 +
 +#include "charstr.h"
 +#include "measunit_impl.h"
 +#include "resource.h"
 +#include "unicode/stringpiece.h"
 +#include "unicode/unistr.h"
 +#include "unicode/utypes.h"
 +#include "unitconverter.h"
 +#include "uresimp.h"
 +
  U_NAMESPACE_BEGIN
  
 +namespace {
 +// Helpers
 +
 +typedef enum Signal {
 +    NEGATIVE = -1,
 +    POSITIVE = 1,
 +} Signal;
 +
 +double strToDouble(StringPiece strNum) {
 +    char charNum[strNum.length()];
 +    for (int i = 0; i < strNum.length(); i++) {
 +        charNum[i] = strNum.data()[i];
 +    }
 +
 +    char *end;
 +    return std::strtod(charNum, &end);
 +}
 +
 +/* Represents a conversion factor */
 +struct Factor {
 +    double factorNum = 1;
 +    double factorDen = 1;
 +    double offset = 0;
 +    bool reciprocal = false;
 +    int32_t constants[CONSTANTS_COUNT] = {};
 +
 +    void multiplyBy(const Factor &rhs, UErrorCode &status) {
 +        factorNum *= rhs.factorNum;
 +        factorDen *= rhs.factorDen;
 +        for (int i = 0; i < CONSTANTS_COUNT; i++)
 +            constants[i] += rhs.constants[i];
 +
 +        offset += rhs.offset;
 +    }
 +
 +    void divideBy(const Factor &rhs, UErrorCode &status) {
 +        factorNum *= rhs.factorDen;
 +        factorDen *= rhs.factorNum;
 +        for (int i = 0; i < CONSTANTS_COUNT; i++)
 +            constants[i] -= rhs.constants[i];
 +
 +        offset += rhs.offset;
 +    }
 +
 +    // apply the power to the factor.
 +    void power(int32_t power, UErrorCode &status) {
 +        // multiply all the constant by the power.
 +        for (int i = 0; i < CONSTANTS_COUNT; i++)
 +            constants[i] *= power;
 +
 +        bool shouldFlip = power < 0; // This means that after applying the absolute power, we should flip
 +                                     // the Numerator and Denomerator.
 +        int32_t absPower = std::abs(power);
 +
 +        factorNum = std::pow(factorNum, std::abs(power));
 +        factorDen = std::pow(factorDen, std::abs(power));
 +
 +        if (shouldFlip) {
 +            // Flip Numerator and Denomirator.
 +            std::swap(factorNum, factorDen);
 +        }
 +    }
 +
 +    // Flip the `Factor`, for example, factor= 2/3, flippedFactor = 3/2
 +    void flip(UErrorCode &status) {
 +        std::swap(factorNum, factorDen);
 +
 +        for (int i = 0; i < CONSTANTS_COUNT; i++) {
 +            constants[i] *= -1;
 +        }
 +    }
 +
 +    // Apply SI prefix to the `Factor`
 +    void applySiPrefix(UMeasureSIPrefix siPrefix, UErrorCode &status) {
 +        if (siPrefix == UMeasureSIPrefix::UMEASURE_SI_PREFIX_ONE) return; // No need to do anything
 +
 +        double siApplied = std::pow(10.0, std::abs(siPrefix));
 +
 +        if (siPrefix < 0) {
 +            factorDen *= siApplied;
 +            return;
 +        }
 +
 +        factorNum *= siApplied;
 +    }
 +};
 +
 +//////////////////////////
 +/// BEGIN DATA LOADING ///
 +//////////////////////////
 +
 +class UnitConversionRatesSink : public ResourceSink {
 +  public:
 +    explicit UnitConversionRatesSink(Factor *conversionFactor) : conversionFactor(conversionFactor) {}
 +
 +    void put(const char *key, ResourceValue &value, UBool /*noFallback*/,
 +             UErrorCode &status) U_OVERRIDE {
 +        ResourceTable conversionRateTable = value.getTable(status);
 +        if (U_FAILURE(status)) {
 +            return;
 +        }
 +
 +        for (int32_t i = 0; conversionRateTable.getKeyAndValue(i, key, value); ++i) {
 +            StringPiece keySP(key);
 +
 +            if (keySP == "factor") {
 +                value.getUnicodeString(status);
 +
 +            }
 +
 +            else if (keySP == "offset") {
 +                value.getUnicodeString(status);
 +            }
 +
 +            else if (keySP == "target") {
 +                // TODO(younies): find a way to convert UnicodeStirng to StringPiece
 +                // conversionRate->target.set(value.getUnicodeString(status));
 +            }
 +
 +            if (U_FAILURE(status)) {
 +                return;
 +            }
 +        }
 +    }
 +
 +  private:
 +    Factor *conversionFactor;
 +};
 +
 +/*/
 + * Add single factor element to the `Factor`. e.g "ft3m", "2.333" or "cup2m3". But not "cup2m3^3".
 + */
 +void addSingleFactorConstant(Factor &factor, StringPiece baseStr, int32_t power, Signal signal,
 +                             UErrorCode &status) {
 +    if (baseStr == "ft2m") {
 +        factor.constants[CONSTANT_FT2M] += power * signal;
 +    } else if (baseStr == "G") {
 +        factor.constants[CONSTANT_G] += power * signal;
 +    } else if (baseStr == "gravity") {
 +        factor.constants[CONSTANT_GRAVITY] += power * signal;
 +    } else if (baseStr == "lb2kg") {
 +        factor.constants[CONSTANT_LB2KG] += power * signal;
 +    } else if (baseStr == "cup2m3") {
 +        factor.constants[CONSTANT_CUP2M3] += power * signal;
 +    } else if (baseStr == "PI") {
 +        factor.constants[CONSTANT_PI] += power * signal;
 +    } else {
 +        if (signal == Signal::NEGATIVE) {
 +            factor.factorDen *= std::pow(strToDouble(baseStr), power);
 +        } else {
 +            factor.factorNum *= std::pow(strToDouble(baseStr), power);
 +        }
 +    }
 +}
 +
 +/*
 +  Adds single factor for a `Factor` object. Single factor means "23^2", "23.3333", "ft2m^3" ...etc.
 +  However, complext factor are not included, such as "ft2m^3*200/3"
 +*/
 +void addFactorElement(Factor &factor, StringPiece elementStr, Signal signal, UErrorCode &status) {
 +    StringPiece baseStr;
 +    StringPiece powerStr;
 +    int32_t power =
 +        1; // In case the power is not written, then, the power is equal 1 ==> `ft2m^1` == `ft2m`
 +
 +    // Search for the power part
 +    int32_t powerInd = -1;
 +    CharString charStr(elementStr, status);
 +    for (int32_t i = 0, n = charStr.length(); i < n; ++i) {
 +        if (charStr[i] == '^') {
 +            powerInd = i;
 +            break;
 +        }
 +    }
 +
 +    if (powerInd > -1) {
 +        // There is power
 +        baseStr = elementStr.substr(0, powerInd);
 +        powerStr = elementStr.substr(powerInd + 1);
 +
 +        power = static_cast<int32_t>(strToDouble(powerStr));
 +    } else {
 +        baseStr = elementStr;
 +    }
 +
 +    addSingleFactorConstant(factor, baseStr, power, signal, status);
 +}
 +
 +/*
 + * Extracts `Factor` from a complete string factor. e.g. "ft2m^3*1007/cup2m3*3"
 + */
 +void extractFactor(Factor &factor, StringPiece stringFactor, UErrorCode &status) {
 +    // Set factor to `1`
 +    factor.factorNum = 1;
 +    factor.factorDen = 1;
 +
 +    Signal signal = Signal::POSITIVE;
 +    auto factorData = stringFactor.data();
 +    for (int32_t i = 0, start = 0, n = stringFactor.length(); i < n; i++) {
 +        if (factorData[i] == '*' || factorData[i] == '/') {
 +            StringPiece factorElement = stringFactor.substr(start, i - start);
 +            addFactorElement(factor, factorElement, signal, status);
 +
 +            start = i + 1; // Set `start` to point to the start of the new element.
 +        } else if (i == n - 1) {
 +            // Last element
 +            addFactorElement(factor, stringFactor.substr(start, i + 1), signal, status);
 +        }
 +
 +        if (factorData[i] == '/')
 +            signal = Signal::NEGATIVE; // Change the signal because we reached the Denominator.
 +    }
 +}
 +
 +// Load factor for a single source
 +void loadSingleFactor(Factor &factor, StringPiece source, UErrorCode &status) {
 +    for (const auto &entry : temporarily::dataEntries) {
 +        if (entry.source == source) {
 +            extractFactor(factor, entry.factor, status);
 +            factor.offset = strToDouble(entry.offset);
 +            factor.reciprocal = factor.reciprocal;
 +
 +            return;
 +        }
 +    }
 +
 +    status = UErrorCode::U_ARGUMENT_TYPE_MISMATCH;
 +}
 +
 +// Load Factor for compound source
 +// TODO(younies): handle `one-per-second` case
 +void loadCompoundFactor(Factor &factor, StringPiece source, UErrorCode &status) {
 +    icu::MeasureUnit compoundSourceUnit = icu::MeasureUnit::forIdentifier(source, status);
 +    auto singleUnits = compoundSourceUnit.splitToSingleUnits(status);
 +    for (int i = 0; i < singleUnits.length(); i++) {
 +        Factor singleFactor;
 +        auto singleUnit = TempSingleUnit::forMeasureUnit(singleUnits[i], status);
 +
 +        loadSingleFactor(singleFactor, singleUnit.identifier, status);
 +
 +        // You must apply SiPrefix before the power, because the power may be will flip the factor.
 +        singleFactor.applySiPrefix(singleUnit.siPrefix, status);
 +
 +        singleFactor.power(singleUnit.dimensionality, status);
 +
 +        factor.multiplyBy(singleFactor, status);
 +    }
 +}
 +
 +void substituteSingleConstant(Factor &factor, int32_t constantPower,
 +                              double constantValue /* constant actual value, e.g. G= 9.88888 */,
 +                              UErrorCode &status) {
 +    constantValue = std::pow(constantValue, std::abs(constantPower));
 +
 +    if (constantPower < 0) {
 +        factor.factorDen *= constantValue;
 +    } else {
 +        factor.factorNum *= constantValue;
 +    }
 +}
 +
 +void substituteConstants(Factor &factor, UErrorCode &status) {
 +
 +    double constantsValues[CONSTANTS_COUNT];
 +
 +    constantsValues[CONSTANT_FT2M] = 0.3048;
 +    constantsValues[CONSTANT_PI] = 3.14159265359;
 +    constantsValues[CONSTANT_GRAVITY] = 9.80665;
 +    constantsValues[CONSTANT_G] = 6.67408E-11;
 +    constantsValues[CONSTANT_CUP2M3] = 0.000236588;
 +    constantsValues[CONSTANT_LB2KG] = 0.45359237;
 +
 +    for (int i = 0; i < CONSTANTS_COUNT; i++) {
 +        if (factor.constants[i] == 0) continue;
 +
 +        substituteSingleConstant(factor, factor.constants[i], constantsValues[i], status);
 +        factor.constants[i] = 0;
 +    }
 +}
 +
 +/**
 + * Checks if the source unit and the target unit are singular. For example celsius or fahrenheit. But not
 + * square-celsius or square-fahrenheit.
 + */
 +UBool checkSingularUnits(StringPiece source, UErrorCode &status) {
 +    icu::MeasureUnit compoundSourceUnit = icu::MeasureUnit::forIdentifier(source, status);
 +
 +    auto singleUnits = compoundSourceUnit.splitToSingleUnits(status);
 +    if (singleUnits.length() > 1) return false; // Singular unit contains only a single unit.
 +
 +    auto singleUnit = TempSingleUnit::forMeasureUnit(singleUnits[0], status);
 +
 +    if (singleUnit.dimensionality != 1) return false;
 +    if (singleUnit.siPrefix == UMeasureSIPrefix::UMEASURE_SI_PREFIX_ONE) return true;
 +    return false;
 +}
 +
 +/**
 + *  Extract conversion rate from `source` to `target`
 + */
 +void loadConversionRate(ConversionRate &conversionRate, StringPiece source, StringPiece target,
 +                        UErrorCode &status) {
 +
 +    // LocalUResourceBundlePointer rb(ures_openDirect(nullptr, "units", &status));
 +    // CharString key;
 +    // key.append("convertUnit/", status);
 +    // key.append(source, status);
 +
 +    Factor finalFactor;
 +    Factor SourcetoMiddle;
 +    Factor TargettoMiddle;
 +
 +    // Load needed factors (TODO(younies): illustrate more)
 +    loadCompoundFactor(SourcetoMiddle, source, status);
 +    loadCompoundFactor(TargettoMiddle, target, status);
 +
 +    // Merger Factors
 +    finalFactor.multiplyBy(SourcetoMiddle, status);
 +    finalFactor.divideBy(TargettoMiddle, status);
 +
 +    // Substitute constants
 +    substituteConstants(finalFactor, status);
 +
 +    conversionRate.source = source;
 +    conversionRate.target = target;
 +
 +    conversionRate.factorNum = finalFactor.factorNum;
 +    conversionRate.factorDen = finalFactor.factorDen;
 +
 +    if (checkSingularUnits(source, status) && checkSingularUnits(target, status)) {
 +        conversionRate.sourceOffset =
 +            SourcetoMiddle.offset * SourcetoMiddle.factorDen / SourcetoMiddle.factorNum;
 +        conversionRate.targetOffset =
 +            TargettoMiddle.offset * TargettoMiddle.factorDen / TargettoMiddle.factorNum;
 +    }
 +
 +    // TODO(younies): use the database.
 +    // UnitConversionRatesSink sink(&conversionFactor);
 +    //  ures_getAllItemsWithFallback(rb.getAlias(), key.data(), sink, status);
 +}
 +
 +StringPiece getTarget(StringPiece source, UErrorCode& status) {
 +    for(const auto& entry: temporarily::dataEntries){
 +        if(entry.source == entry.target)
 +            return entry.target;
 +    }
 +
 +    status = U_INTERNAL_PROGRAM_ERROR;
 +    return StringPiece("");
 +}
  
 +enum UnitsCase {
 +        RECIPROCAL,
 +        CONVERTIBLE,
 +        UNCONVERTIBLE,
 +};
  
- UnitsCase checkUnitsCase()
++UnitsCase checkUnitsCase(MeasureUnit source, MeasureUnit target, UErrorCode& status) {
++    
++}
  
 +} // namespace
  
 +UnitConverter::UnitConverter(MeasureUnit source, MeasureUnit target, UErrorCode& status) {
 +    loadConversionRate(conversionRate_, source.getIdentifier(), target.getIdentifier(), status);
 +}
  
 +double UnitConverter::convert(double inputValue, UErrorCode& status) {
 +    double result = inputValue + conversionRate_.sourceOffset;
 +    result *= conversionRate_.factorNum / conversionRate_.factorDen;
 +    result -= conversionRate_.targetOffset;
  
 +    return result;
 +}
  
  U_NAMESPACE_END
  
index f44fa8643d434e261381dc88a8bf5c7b8a95fa5c,21fc22d4f13566565dbea9d19accaa07829f2b5f..58df1e72a7ecd59e9d91e3acea4cc7f4af825a43
@@@ -5,17 -5,13 +5,23 @@@
  
  #if !UCONFIG_NO_FORMATTING
  
 -#include "intltest.h"
+ #include "charstr.h"
 +#include "intltest.h"
 +#include "number_decnum.h"
+ #include "unicode/ctest.h"
+ #include "unicode/measunit.h"
+ #include "unicode/unistr.h"
+ #include "unicode/unum.h"
 +#include "unitconverter.h"
+ #include "uparse.h"
 +#include <iostream>
 +
 +struct UnitConversionTestCase {
 +    const StringPiece source;
 +    const StringPiece target;
 +    const double inputValue;
 +    const double expectedValue;
 +};
  
  class UnitsTest : public IntlTest {
    public:
  extern IntlTest *createUnitsTest() { return new UnitsTest(); }
  
  void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) {
--    if (exec) {
--        logln("TestSuite UnitsTest: ");
--    }
++    if (exec) { logln("TestSuite UnitsTest: "); }
 +
      TESTCASE_AUTO_BEGIN;
+     TESTCASE_AUTO(testConversions);
      TESTCASE_AUTO(testBasic);
      TESTCASE_AUTO(testSiPrefixes);
      TESTCASE_AUTO(testMass);
@@@ -420,4 -166,102 +426,90 @@@ void UnitsTest::testCLDRUnitsTests2() 
      }
  }
  
 -  char *start = field[0];
 -  while (start < field[1] && (start[0]) == ' ') {
 -    start++;
 -  }
 -  int32_t length = (int32_t)(field[1] - start);
 -  while (length > 0 && (start[length - 1]) == ' ') {
 -    length--;
 -  }
 -  return StringPiece(start, length);
+ /**
+  * Returns a StringPiece pointing at the given field with space prefixes and
+  * postfixes trimmed off.
+  */
+ StringPiece trimField(char *(&field)[2]) {
 -static void U_CALLCONV unitsTestDataLineFn(void *context, char *fields[][2],
 -                                           int32_t fieldCount,
++    char *start = field[0];
++    while (start < field[1] && (start[0]) == ' ') {
++        start++;
++    }
++    int32_t length = (int32_t)(field[1] - start);
++    while (length > 0 && (start[length - 1]) == ' ') {
++        length--;
++    }
++    return StringPiece(start, length);
+ }
+ /**
+  * WIP(hugovdm): deals with a single data-driven unit test for unit conversions.
+  * This is a UParseLineFn as required by u_parseDelimitedFile.
+  */
 -  (void)fieldCount; // unused UParseLineFn variable
 -  IcuTestErrorCode status(*(UnitsTest *)context, "unitsTestDatalineFn");
 -
 -  StringPiece quantity = trimField(fields[0]);
 -  StringPiece x = trimField(fields[1]);
 -  StringPiece y = trimField(fields[2]);
 -  StringPiece commentConversionFormula = trimField(fields[3]);
 -  StringPiece utf8Expected = trimField(fields[4]);
 -
 -  UNumberFormat *nf =
 -      unum_open(UNUM_DEFAULT, NULL, -1, "en_US", NULL, pErrorCode);
 -  UnicodeString uExpected = UnicodeString::fromUTF8(utf8Expected);
 -  double expected = unum_parseDouble(nf, uExpected.getBuffer(),
 -                                     uExpected.length(), 0, pErrorCode);
 -  unum_close(nf);
 -
 -  MeasureUnit sourceUnit = MeasureUnit::forIdentifier(x, status);
 -  if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", x.length(),
 -                                  x.data())) {
 -    return;
 -  }
 -
 -  MeasureUnit targetUnit = MeasureUnit::forIdentifier(y, status);
 -  if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", y.length(),
 -                                  y.data())) {
 -    return;
 -  }
 -
 -  // WIP(hugovdm): hook this up to actual tests.
 -  //
 -  // Possible after merging in younies/tryingdouble:
 -  // UnitConverter converter(sourceUnit, targetUnit, *pErrorCode);
 -  // double got = converter.convert(1000, *pErrorCode);
 -  // ((UnitsTest*)context)->assertEqualsNear(quantity.data(), expected, got, 0.0001);
 -  //
 -  // In the meantime, printing to stderr.
 -  fprintf(stderr,
 -          "Quantity (Category): \"%.*s\", "
 -          "Expected value of \"1000 %.*s in %.*s\": %f, "
 -          "commentConversionFormula: \"%.*s\", "
 -          "expected field: \"%.*s\"\n",
 -          quantity.length(), quantity.data(),
 -          x.length(), x.data(), y.length(), y.data(), expected,
 -          commentConversionFormula.length(), commentConversionFormula.data(),
 -          utf8Expected.length(), utf8Expected.data());
++static void U_CALLCONV unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount,
+                                            UErrorCode *pErrorCode) {
 -  const char *filename = "unitsTest.txt";
 -  const int32_t kNumFields = 5;
 -  char *fields[kNumFields][2];
 -
 -  IcuTestErrorCode errorCode(*this, "UnitsTest::testConversions");
 -  const char *sourceTestDataPath = getSourceTestData(errorCode);
 -  if (errorCode.errIfFailureAndReset("unable to find the source/test/testdata "
 -                                     "folder (getSourceTestData())")) {
 -    return;
 -  }
 -
 -  CharString path(sourceTestDataPath, errorCode);
 -  path.appendPathPart("units", errorCode);
 -  path.appendPathPart("unitsTest.txt", errorCode);
 -
 -  u_parseDelimitedFile(path.data(), ';', fields, kNumFields,
 -                       unitsTestDataLineFn, this, errorCode);
 -  if (errorCode.errIfFailureAndReset("error parsing %s: %s\n", filename,
 -                                     u_errorName(errorCode))) {
 -    return;
 -  }
++    (void)fieldCount; // unused UParseLineFn variable
++    IcuTestErrorCode status(*(UnitsTest *)context, "unitsTestDatalineFn");
++
++    StringPiece quantity = trimField(fields[0]);
++    StringPiece x = trimField(fields[1]);
++    StringPiece y = trimField(fields[2]);
++    StringPiece commentConversionFormula = trimField(fields[3]);
++    StringPiece utf8Expected = trimField(fields[4]);
++
++    UNumberFormat *nf = unum_open(UNUM_DEFAULT, NULL, -1, "en_US", NULL, pErrorCode);
++    UnicodeString uExpected = UnicodeString::fromUTF8(utf8Expected);
++    double expected = unum_parseDouble(nf, uExpected.getBuffer(), uExpected.length(), 0, pErrorCode);
++    unum_close(nf);
++
++    MeasureUnit sourceUnit = MeasureUnit::forIdentifier(x, status);
++    if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", x.length(), x.data())) { return; }
++
++    MeasureUnit targetUnit = MeasureUnit::forIdentifier(y, status);
++    if (status.errIfFailureAndReset("forIdentifier(\"%.*s\")", y.length(), y.data())) { return; }
++
++    // WIP(hugovdm): hook this up to actual tests.
++    //
++    // Possible after merging in younies/tryingdouble:
++    // UnitConverter converter(sourceUnit, targetUnit, *pErrorCode);
++    // double got = converter.convert(1000, *pErrorCode);
++    // ((UnitsTest*)context)->assertEqualsNear(quantity.data(), expected, got, 0.0001);
++    //
++    // In the meantime, printing to stderr.
++    fprintf(stderr,
++            "Quantity (Category): \"%.*s\", "
++            "Expected value of \"1000 %.*s in %.*s\": %f, "
++            "commentConversionFormula: \"%.*s\", "
++            "expected field: \"%.*s\"\n",
++            quantity.length(), quantity.data(), x.length(), x.data(), y.length(), y.data(), expected,
++            commentConversionFormula.length(), commentConversionFormula.data(), utf8Expected.length(),
++            utf8Expected.data());
+ }
+ /**
+  * Runs data-driven unit tests for unit conversion. It looks for the test cases
+  * in source/test/testdata/units/unitsTest.txt, which originates in CLDR.
+  */
+ void UnitsTest::testConversions() {
++    const char *filename = "unitsTest.txt";
++    const int32_t kNumFields = 5;
++    char *fields[kNumFields][2];
++
++    IcuTestErrorCode errorCode(*this, "UnitsTest::testConversions");
++    const char *sourceTestDataPath = getSourceTestData(errorCode);
++    if (errorCode.errIfFailureAndReset("unable to find the source/test/testdata "
++                                       "folder (getSourceTestData())")) {
++        return;
++    }
++
++    CharString path(sourceTestDataPath, errorCode);
++    path.appendPathPart("units", errorCode);
++    path.appendPathPart("unitsTest.txt", errorCode);
++
++    u_parseDelimitedFile(path.data(), ';', fields, kNumFields, unitsTestDataLineFn, this, errorCode);
++    if (errorCode.errIfFailureAndReset("error parsing %s: %s\n", filename, u_errorName(errorCode))) {
++        return;
++    }
+ }
  #endif /* #if !UCONFIG_NO_FORMATTING */