ICU-22781 Adding support for constant denominators (C++)

See #3337
This commit is contained in:
Younies Mahmoud 2025-01-24 00:33:04 +00:00
parent 242bf9655f
commit ba4d4d3ac2
7 changed files with 534 additions and 85 deletions

View file

@ -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) {

View file

@ -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 <climits>
#include <cstdlib>
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) {}
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;
}
}
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,
};
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;
}
// 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;
}
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<uint64_t>(fMatch);
}
UMeasurePrefix getUnitPrefix() const {
U_ASSERT(getType() == TYPE_PREFIX);
@ -530,8 +551,41 @@ public:
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<double>(INT64_MAX)) {
status = kUnitIdentifierSyntaxError;
return 0;
}
// Check if the value is integer.
uint64_t int_result = static_cast<uint64_t>(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 @@ public:
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 @@ public:
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 @@ public:
}
}
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 @@ private:
// 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 @@ private:
// 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 @@ private:
// 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 @@ private:
* 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 @@ private:
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 @@ private:
// via a compound part:
if (token.getType() != Token::TYPE_COMPOUND_PART) {
status = kUnitIdentifierSyntaxError;
return result;
return {};
}
switch (token.getMatch()) {
@ -724,15 +859,16 @@ private:
// 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 @@ private:
// 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 @@ private:
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;
default:
case Token::TYPE_POWER_PART:
if (state > 0) {
status = kUnitIdentifierSyntaxError;
return result;
return {};
}
singleUnitResult.dimensionality *= token.getPower();
state = 1;
break;
case Token::TYPE_PREFIX:
if (state > 1) {
status = kUnitIdentifierSyntaxError;
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);
}

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -26,6 +26,7 @@
#include "units_router.h"
#include "uparse.h"
#include "uresimp.h"
#include <climits>
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 */