diff --git a/icu4c/source/i18n/measunit.cpp b/icu4c/source/i18n/measunit.cpp index 2741b84aabf0..75fb64711bb2 100644 --- a/icu4c/source/i18n/measunit.cpp +++ b/icu4c/source/i18n/measunit.cpp @@ -2400,6 +2400,7 @@ MeasureUnitImpl MeasureUnitImpl::copy(UErrorCode &status) const { MeasureUnitImpl result; result.complexity = complexity; result.identifier.append(identifier, status); + result.constantDenominator = constantDenominator; for (int32_t i = 0; i < singleUnits.length(); i++) { SingleUnitImpl *item = result.singleUnits.emplaceBack(*singleUnits[i]); if (!item) { diff --git a/icu4c/source/i18n/measunit_extra.cpp b/icu4c/source/i18n/measunit_extra.cpp index a6348422738b..18bd82c4f359 100644 --- a/icu4c/source/i18n/measunit_extra.cpp +++ b/icu4c/source/i18n/measunit_extra.cpp @@ -15,6 +15,7 @@ #include "charstr.h" #include "cmemory.h" #include "cstring.h" +#include "double-conversion-string-to-double.h" #include "measunit_impl.h" #include "resource.h" #include "uarrsort.h" @@ -30,13 +31,15 @@ #include "unicode/ustringtrie.h" #include "uresimp.h" #include "util.h" +#include #include - U_NAMESPACE_BEGIN namespace { +using icu::double_conversion::StringToDoubleConverter; + // TODO: Propose a new error code for this? constexpr UErrorCode kUnitIdentifierSyntaxError = U_ILLEGAL_ARGUMENT_ERROR; @@ -467,37 +470,55 @@ void U_CALLCONV initUnitExtras(UErrorCode& status) { class Token { public: - Token(int32_t match) : fMatch(match) {} - - enum Type { - TYPE_UNDEFINED, - TYPE_PREFIX, - // Token type for "-per-", "-", and "-and-". - TYPE_COMPOUND_PART, - // Token type for "per-". - TYPE_INITIAL_COMPOUND_PART, - TYPE_POWER_PART, - TYPE_SIMPLE_UNIT, - }; - - // Calling getType() is invalid, resulting in an assertion failure, if Token - // value isn't positive. - Type getType() const { - U_ASSERT(fMatch > 0); - if (fMatch < kCompoundPartOffset) { - return TYPE_PREFIX; - } - if (fMatch < kInitialCompoundPartOffset) { - return TYPE_COMPOUND_PART; - } - if (fMatch < kPowerPartOffset) { - return TYPE_INITIAL_COMPOUND_PART; - } - if (fMatch < kSimpleUnitOffset) { - return TYPE_POWER_PART; - } - return TYPE_SIMPLE_UNIT; - } + Token(int64_t match) : fMatch(match) { + if (fMatch < kCompoundPartOffset) { + this->fType = TYPE_PREFIX; + } else if (fMatch < kInitialCompoundPartOffset) { + this->fType = TYPE_COMPOUND_PART; + } else if (fMatch < kPowerPartOffset) { + this->fType = TYPE_INITIAL_COMPOUND_PART; + } else if (fMatch < kSimpleUnitOffset) { + this->fType = TYPE_POWER_PART; + } else { + this->fType = TYPE_SIMPLE_UNIT; + } + } + + static Token constantToken(StringPiece str, UErrorCode &status) { + Token result; + auto value = Token::parseStringToLong(str, status); + if (U_FAILURE(status)) { + return result; + } + result.fMatch = value; + result.fType = TYPE_CONSTANT_DENOMINATOR; + return result; + } + + enum Type { + TYPE_UNDEFINED, + TYPE_PREFIX, + // Token type for "-per-", "-", and "-and-". + TYPE_COMPOUND_PART, + // Token type for "per-". + TYPE_INITIAL_COMPOUND_PART, + TYPE_POWER_PART, + TYPE_SIMPLE_UNIT, + TYPE_CONSTANT_DENOMINATOR, + }; + + // Calling getType() is invalid, resulting in an assertion failure, if Token + // value isn't positive. + Type getType() const { + U_ASSERT(fMatch >= 0); + return this->fType; + } + + // Retrieve the value of the constant denominator if the token is of type TYPE_CONSTANT_DENOMINATOR. + uint64_t getConstantDenominator() const { + U_ASSERT(getType() == TYPE_CONSTANT_DENOMINATOR); + return static_cast(fMatch); + } UMeasurePrefix getUnitPrefix() const { U_ASSERT(getType() == TYPE_PREFIX); @@ -530,8 +551,41 @@ class Token { return fMatch - kSimpleUnitOffset; } + // TODO: Consider moving this to a separate utility class. + // Utility function to parse a string into an unsigned long value. + // The value must be a positive integer within the range [1, INT64_MAX]. + // The input can be in integer or scientific notation. + static uint64_t parseStringToLong(const StringPiece strNum, UErrorCode &status) { + // We are processing well-formed input, so we don't need any special options to + // StringToDoubleConverter. + StringToDoubleConverter converter(0, 0, 0, "", ""); + int32_t count; + double double_result = converter.StringToDouble(strNum.data(), strNum.length(), &count); + if (count != strNum.length()) { + status = kUnitIdentifierSyntaxError; + return 0; + } + + if (U_FAILURE(status) || double_result < 1.0 || double_result > static_cast(INT64_MAX)) { + status = kUnitIdentifierSyntaxError; + return 0; + } + + // Check if the value is integer. + uint64_t int_result = static_cast(double_result); + const double kTolerance = 1e-9; + if (abs(double_result - int_result) > kTolerance) { + status = kUnitIdentifierSyntaxError; + return 0; + } + + return int_result; + } + private: - int32_t fMatch; + Token() = default; + int64_t fMatch; + Type fType = TYPE_UNDEFINED; }; class Parser { @@ -555,6 +609,50 @@ class Parser { return {source}; } + /** + * A single unit or a constant denominator. + */ + struct SingleUnitOrConstant { + enum ValueType { + kSingleUnit, + kConstantDenominator, + }; + + ValueType type = kSingleUnit; + SingleUnitImpl singleUnit; + uint64_t constantDenominator; + + static SingleUnitOrConstant singleUnitValue(SingleUnitImpl singleUnit) { + SingleUnitOrConstant result; + result.type = kSingleUnit; + result.singleUnit = singleUnit; + result.constantDenominator = 0; + return result; + } + + static SingleUnitOrConstant constantDenominatorValue(uint64_t constant) { + SingleUnitOrConstant result; + result.type = kConstantDenominator; + result.singleUnit = {}; + result.constantDenominator = constant; + return result; + } + + uint64_t getConstantDenominator() const { + U_ASSERT(type == kConstantDenominator); + return constantDenominator; + } + + SingleUnitImpl getSingleUnit() const { + U_ASSERT(type == kSingleUnit); + return singleUnit; + } + + bool isSingleUnit() const { return type == kSingleUnit; } + + bool isConstantDenominator() const { return type == kConstantDenominator; } + }; + MeasureUnitImpl parse(UErrorCode& status) { MeasureUnitImpl result; @@ -569,12 +667,19 @@ class Parser { while (hasNext()) { bool sawAnd = false; - SingleUnitImpl singleUnit = nextSingleUnit(sawAnd, status); + auto singleUnitOrConstant = nextSingleUnitOrConstant(sawAnd, status); if (U_FAILURE(status)) { return result; } - bool added = result.appendSingleUnit(singleUnit, status); + if (singleUnitOrConstant.isConstantDenominator()) { + result.constantDenominator = singleUnitOrConstant.getConstantDenominator(); + result.complexity = UMEASURE_UNIT_COMPOUND; + continue; + } + + U_ASSERT(singleUnitOrConstant.isSingleUnit()); + bool added = result.appendSingleUnit(singleUnitOrConstant.getSingleUnit(), status); if (U_FAILURE(status)) { return result; } @@ -604,6 +709,12 @@ class Parser { } } + if (result.singleUnits.length() == 0) { + // The identifier was empty or only had a constant denominator. + status = kUnitIdentifierSyntaxError; + return result; // add it for code consistency. + } + return result; } @@ -622,6 +733,10 @@ class Parser { // identifier is invalid pending TODO(CLDR-13701). bool fAfterPer = false; + // Set to true when we've just seen a "per-". This is used to determine if + // the next token can be a constant denominator token. + bool fJustSawPer = false; + Parser() : fSource(""), fTrie(u"") {} Parser(StringPiece source) @@ -640,6 +755,10 @@ class Parser { // Saves the position in the fSource string for the end of the most // recent matching token. int32_t previ = -1; + + // Saves the position in the fSource string for later use in case of unit constant found. + int32_t currentFIndex = fIndex; + // Find the longest token that matches a value in the trie: while (fIndex < fSource.length()) { auto result = fTrie.next(fSource.data()[fIndex++]); @@ -658,12 +777,25 @@ class Parser { // continue; } - if (match < 0) { - status = kUnitIdentifierSyntaxError; - } else { + if (match >= 0) { fIndex = previ; + return {match}; } - return {match}; + + // If no match was found, we check if the token is a constant denominator. + // 1. We find the index of the start of the next token or the end of the string. + int32_t endOfConstantIndex = fSource.find("-", currentFIndex); + endOfConstantIndex = (endOfConstantIndex == -1) ? fSource.length() : endOfConstantIndex; + if (endOfConstantIndex <= currentFIndex) { + status = kUnitIdentifierSyntaxError; + return {match}; + } + + // 2. We extract the substring from the start of the constant to the end of the constant. + StringPiece constantDenominatorStr = + fSource.substr(currentFIndex, endOfConstantIndex - currentFIndex); + fIndex = endOfConstantIndex; + return Token::constantToken(constantDenominatorStr, status); } /** @@ -680,10 +812,10 @@ class Parser { * unit", sawAnd is set to true. If not, it is left as is. * @param status ICU error code. */ - SingleUnitImpl nextSingleUnit(bool &sawAnd, UErrorCode &status) { - SingleUnitImpl result; + SingleUnitOrConstant nextSingleUnitOrConstant(bool &sawAnd, UErrorCode &status) { + SingleUnitImpl singleUnitResult; if (U_FAILURE(status)) { - return result; + return {}; } // state: @@ -695,19 +827,22 @@ class Parser { bool atStart = fIndex == 0; Token token = nextToken(status); if (U_FAILURE(status)) { - return result; + return {}; } + fJustSawPer = false; + if (atStart) { // Identifiers optionally start with "per-". if (token.getType() == Token::TYPE_INITIAL_COMPOUND_PART) { U_ASSERT(token.getInitialCompoundPart() == INITIAL_COMPOUND_PART_PER); fAfterPer = true; - result.dimensionality = -1; + fJustSawPer = true; + singleUnitResult.dimensionality = -1; token = nextToken(status); if (U_FAILURE(status)) { - return result; + return {}; } } } else { @@ -715,7 +850,7 @@ class Parser { // via a compound part: if (token.getType() != Token::TYPE_COMPOUND_PART) { status = kUnitIdentifierSyntaxError; - return result; + return {}; } switch (token.getMatch()) { @@ -724,15 +859,16 @@ class Parser { // Mixed compound units not yet supported, // TODO(CLDR-13701). status = kUnitIdentifierSyntaxError; - return result; + return {}; } fAfterPer = true; - result.dimensionality = -1; + fJustSawPer = true; + singleUnitResult.dimensionality = -1; break; case COMPOUND_PART_TIMES: if (fAfterPer) { - result.dimensionality = -1; + singleUnitResult.dimensionality = -1; } break; @@ -741,7 +877,7 @@ class Parser { // Can't start with "-and-", and mixed compound units // not yet supported, TODO(CLDR-13701). status = kUnitIdentifierSyntaxError; - return result; + return {}; } sawAnd = true; break; @@ -749,52 +885,65 @@ class Parser { token = nextToken(status); if (U_FAILURE(status)) { - return result; + return {}; + } + } + + if (token.getType() == Token::TYPE_CONSTANT_DENOMINATOR) { + if (!fJustSawPer) { + status = kUnitIdentifierSyntaxError; + return {}; } + + return SingleUnitOrConstant::constantDenominatorValue(token.getConstantDenominator()); } // Read tokens until we have a complete SingleUnit or we reach the end. while (true) { switch (token.getType()) { - case Token::TYPE_POWER_PART: - if (state > 0) { - status = kUnitIdentifierSyntaxError; - return result; - } - result.dimensionality *= token.getPower(); - state = 1; - break; - - case Token::TYPE_PREFIX: - if (state > 1) { - status = kUnitIdentifierSyntaxError; - return result; - } - result.unitPrefix = token.getUnitPrefix(); - state = 2; - break; - - case Token::TYPE_SIMPLE_UNIT: - result.index = token.getSimpleUnitIndex(); - return result; + case Token::TYPE_POWER_PART: + if (state > 0) { + status = kUnitIdentifierSyntaxError; + return {}; + } + singleUnitResult.dimensionality *= token.getPower(); + state = 1; + break; - default: + case Token::TYPE_PREFIX: + if (state > 1) { status = kUnitIdentifierSyntaxError; - return result; + return {}; + } + singleUnitResult.unitPrefix = token.getUnitPrefix(); + state = 2; + break; + + case Token::TYPE_SIMPLE_UNIT: + singleUnitResult.index = token.getSimpleUnitIndex(); + break; + + default: + status = kUnitIdentifierSyntaxError; + return {}; + } + + if (token.getType() == Token::TYPE_SIMPLE_UNIT) { + break; } if (!hasNext()) { // We ran out of tokens before finding a complete single unit. status = kUnitIdentifierSyntaxError; - return result; + return {}; } token = nextToken(status); if (U_FAILURE(status)) { - return result; + return {}; } } - return result; + return SingleUnitOrConstant::singleUnitValue(singleUnitResult); } }; @@ -1145,6 +1294,7 @@ void MeasureUnitImpl::serialize(UErrorCode &status) { CharString result; bool beforePer = true; bool firstTimeNegativeDimension = false; + bool constantDenominatorAppended = false; for (int32_t i = 0; i < this->singleUnits.length(); i++) { if (beforePer && (*this->singleUnits[i]).dimensionality < 0) { beforePer = false; @@ -1168,43 +1318,103 @@ void MeasureUnitImpl::serialize(UErrorCode &status) { } else { result.append(StringPiece("-per-"), status); } - } else { - if (result.length() != 0) { + + if (this->constantDenominator != 0) { + result.appendNumber(this->constantDenominator, status); result.append(StringPiece("-"), status); + constantDenominatorAppended = true; } + + } else if (result.length() != 0) { + result.append(StringPiece("-"), status); } } this->singleUnits[i]->appendNeutralIdentifier(result, status); } + if (!constantDenominatorAppended && this->constantDenominator != 0) { + result.append(StringPiece("-per-"), status); + result.appendNumber(this->constantDenominator, status); + } + + if (U_FAILURE(status)) { + return; + } this->identifier = CharString(result, status); } -MeasureUnit MeasureUnitImpl::build(UErrorCode& status) && { +MeasureUnit MeasureUnitImpl::build(UErrorCode &status) && { this->serialize(status); return MeasureUnit(std::move(*this)); } -MeasureUnit MeasureUnit::forIdentifier(StringPiece identifier, UErrorCode& status) { +MeasureUnit MeasureUnit::forIdentifier(StringPiece identifier, UErrorCode &status) { return Parser::from(identifier, status).parse(status).build(status); } -UMeasureUnitComplexity MeasureUnit::getComplexity(UErrorCode& status) const { +UMeasureUnitComplexity MeasureUnit::getComplexity(UErrorCode &status) const { MeasureUnitImpl temp; return MeasureUnitImpl::forMeasureUnit(*this, temp, status).complexity; } -UMeasurePrefix MeasureUnit::getPrefix(UErrorCode& status) const { +UMeasurePrefix MeasureUnit::getPrefix(UErrorCode &status) const { return SingleUnitImpl::forMeasureUnit(*this, status).unitPrefix; } -MeasureUnit MeasureUnit::withPrefix(UMeasurePrefix prefix, UErrorCode& status) const UPRV_NO_SANITIZE_UNDEFINED { +MeasureUnit MeasureUnit::withPrefix(UMeasurePrefix prefix, + UErrorCode &status) const UPRV_NO_SANITIZE_UNDEFINED { SingleUnitImpl singleUnit = SingleUnitImpl::forMeasureUnit(*this, status); singleUnit.unitPrefix = prefix; return singleUnit.build(status); } +uint64_t MeasureUnit::getConstantDenominator(UErrorCode &status) const { + auto complexity = this->getComplexity(status); + if (U_FAILURE(status)) { + return 0; + } + + if (complexity != UMEASURE_UNIT_SINGLE && complexity != UMEASURE_UNIT_COMPOUND) { + status = U_ILLEGAL_ARGUMENT_ERROR; + return 0; + } + + if (this->fImpl == nullptr) { + return 0; + } + + return this->fImpl->constantDenominator; +} + +MeasureUnit MeasureUnit::withConstantDenominator(uint64_t denominator, UErrorCode &status) const { + // To match the behavior of the Java API, we do not allow a constant denominator + // bigger than LONG_MAX. + if (denominator > LONG_MAX) { + status = U_ILLEGAL_ARGUMENT_ERROR; + return {}; + } + + auto complexity = this->getComplexity(status); + if (U_FAILURE(status)) { + return {}; + } + if (complexity != UMEASURE_UNIT_SINGLE && complexity != UMEASURE_UNIT_COMPOUND) { + status = U_ILLEGAL_ARGUMENT_ERROR; + return {}; + } + + MeasureUnitImpl impl = MeasureUnitImpl::forMeasureUnitMaybeCopy(*this, status); + if (U_FAILURE(status)) { + return {}; + } + + impl.constantDenominator = denominator; + impl.complexity = (impl.singleUnits.length() < 2 && denominator == 0) ? UMEASURE_UNIT_SINGLE + : UMEASURE_UNIT_COMPOUND; + return std::move(impl).build(status); +} + int32_t MeasureUnit::getDimensionality(UErrorCode& status) const { SingleUnitImpl singleUnit = SingleUnitImpl::forMeasureUnit(*this, status); if (U_FAILURE(status)) { return 0; } @@ -1222,6 +1432,11 @@ MeasureUnit MeasureUnit::withDimensionality(int32_t dimensionality, UErrorCode& MeasureUnit MeasureUnit::reciprocal(UErrorCode& status) const { MeasureUnitImpl impl = MeasureUnitImpl::forMeasureUnitMaybeCopy(*this, status); + // The reciprocal of a unit that has a constant denominator is not allowed. + if (impl.constantDenominator != 0) { + status = U_ILLEGAL_ARGUMENT_ERROR; + return {}; + } impl.takeReciprocal(status); return std::move(impl).build(status); } diff --git a/icu4c/source/i18n/measunit_impl.h b/icu4c/source/i18n/measunit_impl.h index f6a8f90dc94f..db31435944c2 100644 --- a/icu4c/source/i18n/measunit_impl.h +++ b/icu4c/source/i18n/measunit_impl.h @@ -328,6 +328,14 @@ class U_I18N_API MeasureUnitImpl : public UMemory { */ CharString identifier; + /** + * Represents the unit constant denominator. + * + * NOTE: + * if set to 0, it means that the constant is not set. + */ + uint64_t constantDenominator = 0; + // For calling serialize // TODO(icu-units#147): revisit serialization friend class number::impl::LongNameHandler; diff --git a/icu4c/source/i18n/unicode/measunit.h b/icu4c/source/i18n/unicode/measunit.h index b23897192eb4..b213166cf9e9 100644 --- a/icu4c/source/i18n/unicode/measunit.h +++ b/icu4c/source/i18n/unicode/measunit.h @@ -552,6 +552,44 @@ class U_I18N_API MeasureUnit: public UObject { */ UMeasurePrefix getPrefix(UErrorCode& status) const; +#ifndef U_HIDE_DRAFT_API + + /** + * Creates a new MeasureUnit with a specified constant denominator. + * + * This method is applicable only to COMPOUND and SINGLE units. If invoked on a + * MIXED unit, an error will be set in the status. + * + * NOTE: If the constant denominator is set to 0, it means that you are removing + * the constant denominator. + * + * @param denominator The constant denominator to set. + * @param status Set if this is not a COMPOUND or SINGLE unit or if another error occurs. + * @return A new MeasureUnit with the specified constant denominator. + * @draft ICU 77 + */ + MeasureUnit withConstantDenominator(uint64_t denominator, UErrorCode &status) const; + + /** + * Retrieves the constant denominator for this COMPOUND unit. + * + * Examples: + * - For the unit "liter-per-1000-kiloliter", the constant denominator is 1000. + * - For the unit "liter-per-kilometer", the constant denominator is zero. + * + * This method is applicable only to COMPOUND and SINGLE units. If invoked on + * a MIXED unit, an error will be set in the status. + * + * NOTE: If no constant denominator exists, the method returns 0. + * + * @param status Set if this is not a COMPOUND or SINGLE unit or if another error occurs. + * @return The value of the constant denominator. + * @draft ICU 77 + */ + uint64_t getConstantDenominator(UErrorCode &status) const; + +#endif /* U_HIDE_DRAFT_API */ + /** * Creates a MeasureUnit which is this SINGLE unit augmented with the specified dimensionality * (power). For example, if dimensionality is 2, the unit will be squared. @@ -591,7 +629,9 @@ class U_I18N_API MeasureUnit: public UObject { * NOTE: Only works on SINGLE and COMPOUND units. If this is a MIXED unit, an error will * occur. For more information, see UMeasureUnitComplexity. * - * @param status Set if this is a MIXED unit or if another error occurs. + * NOTE: An Error will be returned for units that have a constant denominator. + * + * @param status Set if this is a MIXED unit, has a constant denominator or if another error occurs. * @return The reciprocal of the target unit. * @stable ICU 67 */ @@ -627,6 +667,10 @@ class U_I18N_API MeasureUnit: public UObject { * * If this is a SINGLE unit, an array of length 1 will be returned. * + * NOTE: For units with a constant denominator, the returned single units will + * not include the constant denominator. To obtain the constant denominator, + * retrieve it from the original unit. + * * @param status Set if an error occurs. * @return A pair with the list of units as a LocalArray and the number of units in the list. * @stable ICU 68 diff --git a/icu4c/source/test/depstest/dependencies.txt b/icu4c/source/test/depstest/dependencies.txt index 7e23ce700bf6..caf3c57cdbe8 100644 --- a/icu4c/source/test/depstest/dependencies.txt +++ b/icu4c/source/test/depstest/dependencies.txt @@ -1123,6 +1123,7 @@ group: units_extra measunit_extra.o deps units bytestriebuilder bytestrie resourcebundle uclean_i18n + double_conversion group: units measunit.o currunit.o diff --git a/icu4c/source/test/intltest/measfmttest.cpp b/icu4c/source/test/intltest/measfmttest.cpp index fbe71bdf8873..e302f75eaba6 100644 --- a/icu4c/source/test/intltest/measfmttest.cpp +++ b/icu4c/source/test/intltest/measfmttest.cpp @@ -5450,7 +5450,7 @@ void MeasureFormatTest::TestUnitPerUnitResolution() { actual, pos, status); - assertEquals("", "50 psi", actual); + assertEquals("TestUnitPerUnitResolution", "50 psi", actual); } void MeasureFormatTest::TestIndividualPluralFallback() { @@ -5708,6 +5708,19 @@ void MeasureFormatTest::TestInvalidIdentifiers() { // Compound units not supported in mixed units yet. TODO(CLDR-13701). "kilonewton-meter-and-newton-meter", + + // Invalid identifiers with constants. + "meter-per--20--second", + "meter-per-1000-1e9-second", + "meter-per-1e20-second", + "per-1000", + "meter-per-1000-1000", + "meter-per-1000-second-1000-kilometer", + "1000-meter", + "meter-1000", + "meter-per-1000-1000", + "meter-per-1000-second-1000-kilometer", + "per-1000-and-per-1000", }; for (const auto& input : inputs) { diff --git a/icu4c/source/test/intltest/units_test.cpp b/icu4c/source/test/intltest/units_test.cpp index add612c27678..147d19c55e6a 100644 --- a/icu4c/source/test/intltest/units_test.cpp +++ b/icu4c/source/test/intltest/units_test.cpp @@ -26,6 +26,7 @@ #include "units_router.h" #include "uparse.h" #include "uresimp.h" +#include struct UnitConversionTestCase { const StringPiece source; @@ -50,6 +51,8 @@ class UnitsTest : public IntlTest { void testComplexUnitsConverter(); void testComplexUnitsConverterSorting(); void testUnitPreferencesWithCLDRTests(); + void testUnitsConstantsDenomenator(); + void testMeasureUnit_withConstantDenominator(); void testConverter(); }; @@ -67,6 +70,8 @@ void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, cha TESTCASE_AUTO(testComplexUnitsConverter); TESTCASE_AUTO(testComplexUnitsConverterSorting); TESTCASE_AUTO(testUnitPreferencesWithCLDRTests); + TESTCASE_AUTO(testUnitsConstantsDenomenator); + TESTCASE_AUTO(testMeasureUnit_withConstantDenominator); TESTCASE_AUTO(testConverter); TESTCASE_AUTO_END; } @@ -1157,4 +1162,166 @@ void UnitsTest::testUnitPreferencesWithCLDRTests() { } } +void UnitsTest::testUnitsConstantsDenomenator() { + IcuTestErrorCode status(*this, "UnitTests::testUnitsConstantsDenomenator"); + + // Test Cases + struct TestCase { + const char *source; + const uint64_t expectedConstant; + } testCases[]{ + {"meter-per-1000", 1000}, + {"liter-per-1000-kiloliter", 1000}, + {"liter-per-kilometer", 0}, + {"second-per-1000-minute", 1000}, + {"gram-per-1000-kilogram", 1000}, + {"meter-per-100", 100}, + {"portion-per-1", 1}, + {"portion-per-2", 2}, + {"portion-per-3", 3}, + {"portion-per-4", 4}, + {"portion-per-5", 5}, + {"portion-per-6", 6}, + {"portion-per-7", 7}, + {"portion-per-8", 8}, + {"portion-per-9", 9}, + // Test for constant denominators that are powers of 10 + {"portion-per-10", 10}, + {"portion-per-100", 100}, + {"portion-per-1000", 1000}, + {"portion-per-10000", 10000}, + {"portion-per-100000", 100000}, + {"portion-per-1000000", 1000000}, + {"portion-per-10000000", 10000000}, + {"portion-per-100000000", 100000000}, + {"portion-per-1000000000", 1000000000}, + {"portion-per-10000000000", 10000000000}, + {"portion-per-100000000000", 100000000000}, + {"portion-per-1000000000000", 1000000000000}, + {"portion-per-10000000000000", 10000000000000}, + {"portion-per-100000000000000", 100000000000000}, + {"portion-per-1000000000000000", 1000000000000000}, + {"portion-per-10000000000000000", 10000000000000000}, + {"portion-per-100000000000000000", 100000000000000000}, + {"portion-per-1000000000000000000", 1000000000000000000}, + // Test for constant denominators that are represented as scientific notation + // numbers. + {"portion-per-1e1", 10}, + {"portion-per-1E1", 10}, + {"portion-per-1e2", 100}, + {"portion-per-1E2", 100}, + {"portion-per-1e3", 1000}, + {"portion-per-1E3", 1000}, + {"portion-per-1e4", 10000}, + {"portion-per-1E4", 10000}, + {"portion-per-1e5", 100000}, + {"portion-per-1E5", 100000}, + {"portion-per-1e6", 1000000}, + {"portion-per-1E6", 1000000}, + {"portion-per-1e10", 10000000000}, + {"portion-per-1E10", 10000000000}, + {"portion-per-1e18", 1000000000000000000}, + {"portion-per-1E18", 1000000000000000000}, + // Test for constant denominators that are randomly selected. + {"liter-per-12345-kilometer", 12345}, + {"per-1000-kilometer", 1000}, + {"liter-per-1000-kiloliter", 1000}, + // Test for constant denominators that give 0. + {"meter", 0}, + {"meter-per-second", 0}, + {"meter-per-square-second", 0}, + // NOTE: The following constant denominator should be 0. However, since + // `100-kilometer` is treated as a unit in CLDR, + // the unit does not have a constant denominator. + // This issue should be addressed in CLDR. + {"meter-per-100-kilometer", 0}, + // NOTE: the following CLDR identifier should be invalid, but because + // `100-kilometer` is considered a unit in CLDR, + // one `100` will be considered as a unit constant denominator and the other + // `100` will be considered part of the unit. + // This issue should be addressed in CLDR. + {"meter-per-100-100-kilometer", 100}, + }; + + for (const auto &testCase : testCases) { + MeasureUnit unit = MeasureUnit::forIdentifier(testCase.source, status); + if (status.errIfFailureAndReset("forIdentifier(\"%s\")", testCase.source)) { + continue; + } + + uint64_t constant = unit.getConstantDenominator(status); + if (status.errIfFailureAndReset("getConstantDenominator(\"%s\")", testCase.source)) { + continue; + } + + auto complexity = unit.getComplexity(status); + if (status.errIfFailureAndReset("getComplexity(\"%s\")", testCase.source)) { + continue; + } + + if (constant != testCase.expectedConstant) { + assertTrue("getConstantDenominator(\"%s\")", false); + } + if (constant != 0) { + assertEquals("getComplexity(\"%s\")", UMEASURE_UNIT_COMPOUND, complexity); + } + } +} + +void UnitsTest::testMeasureUnit_withConstantDenominator() { + IcuTestErrorCode status(*this, "UnitsTest::testMeasureUnit_withConstantDenominator"); + + // Test Cases + struct TestCase { + const char *source; + const uint64_t constantDenominator; + const UMeasureUnitComplexity expectedComplexity; + } testCases[]{ + {"meter-per-second", 100, UMEASURE_UNIT_COMPOUND}, + {"meter-per-100-second", 0, UMEASURE_UNIT_COMPOUND}, + {"portion", 100, UMEASURE_UNIT_COMPOUND}, + {"portion-per-100", 0, UMEASURE_UNIT_SINGLE}, + + }; + + for (auto testCase : testCases) { + auto unit = MeasureUnit::forIdentifier(testCase.source, status); + if (status.errIfFailureAndReset("forIdentifier(\"%s\")", testCase.source)) { + continue; + } + + unit = unit.withConstantDenominator(testCase.constantDenominator, status); + if (status.errIfFailureAndReset("withConstantDenominator(\"%s\")", testCase.source)) { + continue; + } + + auto actualConstantDenominator = unit.getConstantDenominator(status); + if (status.errIfFailureAndReset("getConstantDenominator(\"%s\")", testCase.source)) { + continue; + } + + auto actualComplexity = unit.getComplexity(status); + if (status.errIfFailureAndReset("getComplexity(\"%s\")", testCase.source)) { + continue; + } + + if (actualConstantDenominator != testCase.constantDenominator) { + assertTrue("getConstantDenominator(\"%s\")", false); + } + assertEquals("getComplexity(\"%s\")", testCase.expectedComplexity, actualComplexity); + } + + // Test for invalid constant denominator + auto unit = MeasureUnit::forIdentifier("portion", status); + if (status.errIfFailureAndReset("forIdentifier(\"portion\")")) { + return; + } + + uint64_t denominator = LONG_MAX; + denominator++; + unit = unit.withConstantDenominator(denominator, status); + assertTrue("There is a failure caused by withConstantDenominator(\"portion\")", status.isFailure()); + status.reset(); +} + #endif /* #if !UCONFIG_NO_FORMATTING */