diff --git a/icu4c/source/i18n/number_decimalquantity.cpp b/icu4c/source/i18n/number_decimalquantity.cpp index ef53d3aa29b..b40e1276c35 100644 --- a/icu4c/source/i18n/number_decimalquantity.cpp +++ b/icu4c/source/i18n/number_decimalquantity.cpp @@ -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; diff --git a/icu4c/source/i18n/number_decimalquantity.h b/icu4c/source/i18n/number_decimalquantity.h index 891478969db..862addf5d6c 100644 --- a/icu4c/source/i18n/number_decimalquantity.h +++ b/icu4c/source/i18n/number_decimalquantity.h @@ -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. */ diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index a39f177299a..7556786c502 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -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; diff --git a/icu4c/source/test/intltest/numbertest_decimalquantity.cpp b/icu4c/source/test/intltest/numbertest_decimalquantity.cpp index 6a490088ed8..87cd7707b55 100644 --- a/icu4c/source/test/intltest/numbertest_decimalquantity.cpp +++ b/icu4c/source/test/intltest/numbertest_decimalquantity.cpp @@ -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 */ diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java index 7f020a582c5..43c8e969870 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java @@ -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. * diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java index e43e1ee5323..a02a48e8213 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java @@ -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) { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_DualStorageBCD.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_DualStorageBCD.java index a10c0335b00..414f62938eb 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_DualStorageBCD.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_DualStorageBCD.java @@ -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) { diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java index b68ccae8ee8..f38897f4af1 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java @@ -961,4 +961,8 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { public boolean isHasIntegerValue() { return scaleBigDecimal(toBigDecimal()) >= 0; } + + @Override public String toExponentString() { + return this.toPlainString(); + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java index f8d961d0e98..8863e609257 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java @@ -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); }