ICU-21322 Add parse and format methods for DecimalQuantity with exponent

See #2012
This commit is contained in:
Elango Cheran 2022-03-08 18:50:01 +00:00 committed by Elango
parent f57ef9ebf7
commit f79f03dad5
9 changed files with 294 additions and 0 deletions

View file

@ -557,6 +557,65 @@ void DecimalQuantity::_setToDecNum(const DecNum& decnum, UErrorCode& status) {
}
}
DecimalQuantity DecimalQuantity::fromExponentString(UnicodeString num, UErrorCode& status) {
if (num.indexOf(u'e') >= 0 || num.indexOf(u'c') >= 0
|| num.indexOf(u'E') >= 0 || num.indexOf(u'C') >= 0) {
int32_t ePos = num.lastIndexOf('e');
if (ePos < 0) {
ePos = num.lastIndexOf('c');
}
if (ePos < 0) {
ePos = num.lastIndexOf('E');
}
if (ePos < 0) {
ePos = num.lastIndexOf('C');
}
int32_t expNumPos = ePos + 1;
UnicodeString exponentStr = num.tempSubString(expNumPos, num.length() - expNumPos);
// parse exponentStr into exponent, but note that parseAsciiInteger doesn't handle the minus sign
bool isExpStrNeg = num[expNumPos] == u'-';
int32_t exponentParsePos = isExpStrNeg ? 1 : 0;
int32_t exponent = ICU_Utility::parseAsciiInteger(exponentStr, exponentParsePos);
exponent = isExpStrNeg ? -exponent : exponent;
// Compute the decNumber representation
UnicodeString fractionStr = num.tempSubString(0, ePos);
CharString fracCharStr = CharString();
fracCharStr.appendInvariantChars(fractionStr, status);
DecNum decnum;
decnum.setTo(fracCharStr.toStringPiece(), status);
// Clear and set this DecimalQuantity instance
DecimalQuantity dq;
dq.setToDecNum(decnum, status);
int32_t numFracDigit = getVisibleFractionCount(fractionStr);
dq.setMinFraction(numFracDigit);
dq.adjustExponent(exponent);
return dq;
} else {
DecimalQuantity dq;
int numFracDigit = getVisibleFractionCount(num);
CharString numCharStr = CharString();
numCharStr.appendInvariantChars(num, status);
dq.setToDecNumber(numCharStr.toStringPiece(), status);
dq.setMinFraction(numFracDigit);
return dq;
}
}
int32_t DecimalQuantity::getVisibleFractionCount(UnicodeString value) {
int decimalPos = value.indexOf('.') + 1;
if (decimalPos == 0) {
return 0;
} else {
return value.length() - decimalPos;
}
}
int64_t DecimalQuantity::toLong(bool truncateIfOverflow) const {
// NOTE: Call sites should be guarded by fitsInLong(), like this:
// if (dq.fitsInLong()) { /* use dq.toLong() */ } else { /* use some fallback */ }
@ -956,6 +1015,44 @@ UnicodeString DecimalQuantity::toPlainString() const {
return sb;
}
UnicodeString DecimalQuantity::toExponentString() const {
U_ASSERT(!isApproximate);
UnicodeString sb;
if (isNegative()) {
sb.append(u'-');
}
int32_t upper = scale + precision - 1;
int32_t lower = scale;
if (upper < lReqPos - 1) {
upper = lReqPos - 1;
}
if (lower > rReqPos) {
lower = rReqPos;
}
int32_t p = upper;
if (p < 0) {
sb.append(u'0');
}
for (; p >= 0; p--) {
sb.append(u'0' + getDigitPos(p - scale));
}
if (lower < 0) {
sb.append(u'.');
}
for(; p >= lower; p--) {
sb.append(u'0' + getDigitPos(p - scale));
}
if (exponent != 0) {
sb.append(u'c');
ICU_Utility::appendNumber(sb, exponent);
}
return sb;
}
UnicodeString DecimalQuantity::toScientificString() const {
U_ASSERT(!isApproximate);
UnicodeString result;

View file

@ -245,6 +245,9 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory {
/** Internal method if the caller already has a DecNum. */
DecimalQuantity &setToDecNum(const DecNum& n, UErrorCode& status);
/** Returns a DecimalQuantity after parsing the input string. */
static DecimalQuantity fromExponentString(UnicodeString n, UErrorCode& status);
/**
* Appends a digit, optionally with one or more leading zeros, to the end of the value represented
* by this DecimalQuantity.
@ -326,6 +329,10 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory {
/** Returns the string without exponential notation. Slightly slower than toScientificString(). */
UnicodeString toPlainString() const;
/** Returns the string using ASCII digits and using exponential notation for non-zero
exponents, following the UTS 35 specification for plural rule samples. */
UnicodeString toExponentString() const;
/** Visible for testing */
inline bool isUsingBytes() { return usingBytes; }
@ -529,6 +536,8 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory {
void _setToDecNum(const DecNum& dn, UErrorCode& status);
static int32_t getVisibleFractionCount(UnicodeString value);
void convertToAccurateDouble();
/** Ensure that a byte array of at least 40 digits is allocated. */

View file

@ -208,6 +208,7 @@ class DecimalQuantityTest : public IntlTest {
void testNickelRounding();
void testScientificAndCompactSuppressedExponent();
void testSuppressedExponentUnchangedByInitialScaling();
void testDecimalQuantityParseFormatRoundTrip();
void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0) override;

View file

@ -33,6 +33,7 @@ void DecimalQuantityTest::runIndexedTest(int32_t index, UBool exec, const char *
TESTCASE_AUTO(testNickelRounding);
TESTCASE_AUTO(testScientificAndCompactSuppressedExponent);
TESTCASE_AUTO(testSuppressedExponentUnchangedByInitialScaling);
TESTCASE_AUTO(testDecimalQuantityParseFormatRoundTrip);
TESTCASE_AUTO_END;
}
@ -724,4 +725,52 @@ void DecimalQuantityTest::testSuppressedExponentUnchangedByInitialScaling() {
}
}
void DecimalQuantityTest::testDecimalQuantityParseFormatRoundTrip() {
IcuTestErrorCode status(*this, "testDecimalQuantityParseFormatRoundTrip");
struct TestCase {
UnicodeString numberString;
} cases[] = {
// number string
{ u"0" },
{ u"1" },
{ u"1.0" },
{ u"1.00" },
{ u"1.1" },
{ u"1.10" },
{ u"-1.10" },
{ u"0.0" },
{ u"1c5" },
{ u"1.0c5" },
{ u"1.1c5" },
{ u"1.10c5" },
{ u"0.00" },
{ u"0.1" },
{ u"1c-1" },
{ u"1.0c-1" }
};
for (const auto& cas : cases) {
UnicodeString numberString = cas.numberString;
DecimalQuantity dq = DecimalQuantity::fromExponentString(numberString, status);
UnicodeString roundTrip = dq.toExponentString();
assertEquals("DecimalQuantity format(parse(s)) should equal original s", numberString, roundTrip);
}
DecimalQuantity dq = DecimalQuantity::fromExponentString(u"1c0", status);
assertEquals("Zero ignored for visible exponent",
u"1",
dq.toExponentString());
dq.clear();
dq = DecimalQuantity::fromExponentString(u"1.0c0", status);
assertEquals("Zero ignored for visible exponent",
u"1.0",
dq.toExponentString());
}
#endif /* #if !UCONFIG_NO_FORMATTING */

View file

@ -230,6 +230,12 @@ public interface DecimalQuantity extends PluralRules.IFixedDecimal {
*/
public String toPlainString();
/**
* Returns the string using ASCII digits and using exponential notation for non-zero
* exponents, following the UTS 35 specification for plural rule samples.
*/
public String toExponentString();
/**
* Like clone, but without the restrictions of the Cloneable interface clone.
*

View file

@ -1121,6 +1121,48 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
}
}
@Override
public String toExponentString() {
StringBuilder sb = new StringBuilder();
toExponentString(sb);
return sb.toString();
}
private void toExponentString(StringBuilder result) {
assert(!isApproximate);
if (isNegative()) {
result.append('-');
}
int upper = scale + precision - 1;
int lower = scale;
if (upper < lReqPos - 1) {
upper = lReqPos - 1;
}
if (lower > rReqPos) {
lower = rReqPos;
}
int p = upper;
if (p < 0) {
result.append('0');
}
for (; p >= 0; p--) {
result.append((char) ('0' + getDigitPos(p - scale)));
}
if (lower < 0) {
result.append('.');
}
for(; p >= lower; p--) {
result.append((char) ('0' + getDigitPos(p - scale)));
}
if (exponent != 0) {
result.append('c');
result.append(exponent);
}
}
@Override
public boolean equals(Object other) {
if (this == other) {

View file

@ -88,6 +88,54 @@ public final class DecimalQuantity_DualStorageBCD extends DecimalQuantity_Abstra
return new DecimalQuantity_DualStorageBCD(this);
}
/**
* Returns a DecimalQuantity after parsing the input string.
*
* @param s The String to parse
*/
public static DecimalQuantity fromExponentString(String num) {
if (num.contains("e") || num.contains("c")
|| num.contains("E") || num.contains("C")) {
int ePos = num.lastIndexOf('e');
if (ePos < 0) {
ePos = num.lastIndexOf('c');
}
if (ePos < 0) {
ePos = num.lastIndexOf('E');
}
if (ePos < 0) {
ePos = num.lastIndexOf('C');
}
int expNumPos = ePos + 1;
String exponentStr = num.substring(expNumPos);
int exponent = Integer.parseInt(exponentStr);
String fractionStr = num.substring(0, ePos);
BigDecimal fraction = new BigDecimal(fractionStr);
DecimalQuantity_DualStorageBCD dq = new DecimalQuantity_DualStorageBCD(fraction);
int numFracDigit = getVisibleFractionCount(fractionStr);
dq.setMinFraction(numFracDigit);
dq.adjustExponent(exponent);
return dq;
} else {
int numFracDigit = getVisibleFractionCount(num);
DecimalQuantity_DualStorageBCD dq = new DecimalQuantity_DualStorageBCD(new BigDecimal(num));
dq.setMinFraction(numFracDigit);
return dq;
}
}
private static int getVisibleFractionCount(String value) {
int decimalPos = value.indexOf('.') + 1;
if (decimalPos == 0) {
return 0;
} else {
return value.length() - decimalPos;
}
}
@Override
protected byte getDigitPos(int position) {
if (usingBytes) {

View file

@ -961,4 +961,8 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity {
public boolean isHasIntegerValue() {
return scaleBigDecimal(toBigDecimal()) >= 0;
}
@Override public String toExponentString() {
return this.toPlainString();
}
}

View file

@ -907,6 +907,44 @@ public class DecimalQuantityTest extends TestFmwk {
}
}
@Test
public void testDecimalQuantityParseFormatRoundTrip() {
Object[] casesData = {
// number string
"0",
"1",
"1.0",
"1.00",
"1.1",
"1.10",
"-1.10",
"0.0",
"1c5",
"1.0c5",
"1.1c5",
"1.10c5",
"0.00",
"0.1",
"1c-1",
"1.0c-1"
};
for (Object caseDatum : casesData) {
String numStr = (String) caseDatum;
DecimalQuantity dq = DecimalQuantity_DualStorageBCD.fromExponentString(numStr);
String roundTrip = dq.toExponentString();
assertEquals("DecimalQuantity format(parse(s)) should equal original s", numStr, roundTrip);
}
assertEquals("Zero ignored for visible exponent",
"1",
DecimalQuantity_DualStorageBCD.fromExponentString("1c0").toExponentString());
assertEquals("Zero ignored for visible exponent",
"1.0",
DecimalQuantity_DualStorageBCD.fromExponentString("1.0c0").toExponentString());
}
static boolean doubleEquals(double d1, double d2) {
return (Math.abs(d1 - d2) < 1e-6) || (Math.abs((d1 - d2) / d1) < 1e-6);
}