mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-13 08:53:20 +00:00
ICU-21322 Add parse and format methods for DecimalQuantity with exponent
See #2012
This commit is contained in:
parent
f57ef9ebf7
commit
f79f03dad5
9 changed files with 294 additions and 0 deletions
|
@ -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;
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -961,4 +961,8 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity {
|
|||
public boolean isHasIntegerValue() {
|
||||
return scaleBigDecimal(toBigDecimal()) >= 0;
|
||||
}
|
||||
|
||||
@Override public String toExponentString() {
|
||||
return this.toPlainString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue