]> granicus.if.org Git - icu/commitdiff
ICU-21322 Add parse and format methods for DecimalQuantity with exponent
authorElango Cheran <elango@unicode.org>
Tue, 8 Mar 2022 18:50:01 +0000 (18:50 +0000)
committerElango <elango@unicode.org>
Tue, 8 Mar 2022 23:56:50 +0000 (15:56 -0800)
See #2012

icu4c/source/i18n/number_decimalquantity.cpp
icu4c/source/i18n/number_decimalquantity.h
icu4c/source/test/intltest/numbertest.h
icu4c/source/test/intltest/numbertest_decimalquantity.cpp
icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_DualStorageBCD.java
icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java

index ef53d3aa29b011454a2740b68bdf5ff41218f055..b40e1276c350fe4a8cdfca4d37fbad6828916991 100644 (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;
index 891478969dbe4e5e1dd492aa30972c17fbd9be25..862addf5d6cd9035e3df39971cb05d7fa16a405d 100644 (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. */
index a39f177299ae70f2f7f6610c3cd5d34bc52920e7..7556786c5022271798212b7508b1e21f60adfee6 100644 (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;
 
index 6a490088ed826179e25138c0322e9d8027413d17..87cd7707b55b8a82213742994ad7fc6108d9858e 100644 (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 */
index 7f020a582c59a36fbbfb7d3f4e94e8b28ebc71b1..43c8e969870a449f5d7600c4ce028b83791c00c3 100644 (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.
      *
index e43e1ee5323ae6ff728fffccb95b98e96e598468..a02a48e8213e92b8d5ffe260632455a6991dd859 100644 (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) {
index a10c0335b0072b0a6df4c134d340bbee6cef5977..414f62938eb5b199086b7ca508f56661c9fff5de 100644 (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) {
index b68ccae8ee863befedc629156a2c491e13a9692c..f38897f4af1eef2d12fcda4a182786fa5cb7aa17 100644 (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();
+  }
 }
index f8d961d0e98764ea816413bbfe983d20fbfad04c..8863e609257cb5d4c89d5380d352c72d758830e5 100644 (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);
     }