]> granicus.if.org Git - icu/commitdiff
ICU-13678 Adding multiplier to skeleton string syntax.
authorShane Carr <shane@unicode.org>
Wed, 4 Apr 2018 01:26:18 +0000 (01:26 +0000)
committerShane Carr <shane@unicode.org>
Wed, 4 Apr 2018 01:26:18 +0000 (01:26 +0000)
X-SVN-Rev: 41193

icu4c/source/i18n/number_decimalquantity.cpp
icu4c/source/i18n/number_skeletons.cpp
icu4c/source/i18n/number_skeletons.h
icu4c/source/test/intltest/numbertest_api.cpp
icu4c/source/test/intltest/numbertest_skeletons.cpp
icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java
icu4j/main/classes/core/src/com/ibm/icu/number/Multiplier.java
icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberSkeletonTest.java

index ca91c839a69d53a2cbf5aa67444993472b87146c..f092bc10e4d29f5abb8f857738fa28725663266d 100644 (file)
@@ -757,6 +757,9 @@ UnicodeString DecimalQuantity::toPlainString() const {
     if (isNegative()) {
         sb.append(u'-');
     }
+    if (precision == 0) {
+        sb.append(u'0');
+    }
     for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) {
         if (m == -1) { sb.append(u'.'); }
         sb.append(getDigit(m) + u'0');
index 444471ea3bdda4fe1c08c6aed4d9d59f27422286..0976cb5d5b0226a5aa0037848ab9431185d8f354 100644 (file)
@@ -85,6 +85,7 @@ void U_CALLCONV initNumberSkeletons(UErrorCode& status) {
     b.add(u"currency", STEM_CURRENCY, status);
     b.add(u"integer-width", STEM_INTEGER_WIDTH, status);
     b.add(u"numbering-system", STEM_NUMBERING_SYSTEM, status);
+    b.add(u"multiply", STEM_MULTIPLY, status);
     if (U_FAILURE(status)) { return; }
 
     // Build the CharsTrie
@@ -443,6 +444,7 @@ MacroProps skeleton::parseSkeleton(const UnicodeString& skeletonString, UErrorCo
                 case STATE_CURRENCY_UNIT:
                 case STATE_INTEGER_WIDTH:
                 case STATE_NUMBERING_SYSTEM:
+                case STATE_MULTIPLY:
                     // segment.setLength(U16_LENGTH(cp)); // for error message
                     // throw new SkeletonSyntaxException("Stem requires an option", segment);
                     status = U_NUMBER_SKELETON_SYNTAX_ERROR;
@@ -592,6 +594,10 @@ skeleton::parseStem(const StringSegment& segment, const UCharsTrie& stemTrie, Se
         CHECK_NULL(seen, symbols, status);
             return STATE_NUMBERING_SYSTEM;
 
+        case STEM_MULTIPLY:
+        CHECK_NULL(seen, multiplier, status);
+            return STATE_MULTIPLY;
+
         default:
             U_ASSERT(false);
     }
@@ -621,6 +627,9 @@ ParseState skeleton::parseOption(ParseState stem, const StringSegment& segment,
         case STATE_NUMBERING_SYSTEM:
             blueprint_helpers::parseNumberingSystemOption(segment, macros, status);
             return STATE_NULL;
+        case STATE_MULTIPLY:
+            blueprint_helpers::parseMultiplierOption(segment, macros, status);
+            return STATE_NULL;
         default:
             break;
     }
@@ -712,6 +721,10 @@ void GeneratorHelpers::generateSkeleton(const MacroProps& macros, UnicodeString&
         sb.append(u' ');
     }
     if (U_FAILURE(status)) { return; }
+    if (GeneratorHelpers::multiplier(macros, sb, status)) {
+        sb.append(u' ');
+    }
+    if (U_FAILURE(status)) { return; }
 
     // Unsupported options
     if (!macros.padder.isBogus()) {
@@ -722,10 +735,6 @@ void GeneratorHelpers::generateSkeleton(const MacroProps& macros, UnicodeString&
         status = U_UNSUPPORTED_ERROR;
         return;
     }
-    if (macros.multiplier.isValid()) {
-        status = U_UNSUPPORTED_ERROR;
-        return;
-    }
     if (macros.rules != nullptr) {
         status = U_UNSUPPORTED_ERROR;
         return;
@@ -1175,6 +1184,35 @@ void blueprint_helpers::generateNumberingSystemOption(const NumberingSystem& ns,
     sb.append(UnicodeString(ns.getName(), -1, US_INV));
 }
 
+void blueprint_helpers::parseMultiplierOption(const StringSegment& segment, MacroProps& macros,
+                                              UErrorCode& status) {
+    // Need to do char <-> UChar conversion...
+    CharString buffer;
+    SKELETON_UCHAR_TO_CHAR(buffer, segment.toTempUnicodeString(), 0, segment.length(), status);
+
+    // Utilize DecimalQuantity/decNumber to parse this for us.
+    // TODO: Parse to a DecNumber directly.
+    DecimalQuantity dq;
+    UErrorCode localStatus = U_ZERO_ERROR;
+    dq.setToDecNumber({buffer.data(), buffer.length()}, localStatus);
+    if (U_FAILURE(localStatus)) {
+        // throw new SkeletonSyntaxException("Invalid rounding increment", segment, e);
+        status = U_NUMBER_SKELETON_SYNTAX_ERROR;
+        return;
+    }
+    macros.multiplier = Multiplier::arbitraryDouble(dq.toDouble());
+}
+
+void blueprint_helpers::generateMultiplierOption(int32_t magnitude, double arbitrary, UnicodeString& sb,
+                                                 UErrorCode&) {
+    // Utilize DecimalQuantity/double_conversion to format this for us.
+    DecimalQuantity dq;
+    dq.setToDouble(arbitrary);
+    dq.adjustMagnitude(magnitude);
+    dq.roundToInfinity();
+    sb.append(dq.toPlainString());
+}
+
 
 bool GeneratorHelpers::notation(const MacroProps& macros, UnicodeString& sb, UErrorCode& status) {
     if (macros.notation.fType == Notation::NTN_COMPACT) {
@@ -1376,5 +1414,18 @@ bool GeneratorHelpers::decimal(const MacroProps& macros, UnicodeString& sb, UErr
     return true;
 }
 
+bool GeneratorHelpers::multiplier(const MacroProps& macros, UnicodeString& sb, UErrorCode& status) {
+    if (!macros.multiplier.isValid()) {
+        return false; // Default or Bogus
+    }
+    sb.append(u"multiply/", -1);
+    blueprint_helpers::generateMultiplierOption(
+            macros.multiplier.fMagnitude,
+            macros.multiplier.fArbitrary,
+            sb,
+            status);
+    return true;
+}
+
 
 #endif /* #if !UCONFIG_NO_FORMATTING */
index 14f1bdbe70952a8d12b54bf7af22274438ba4c32..7954b99f2b227abeb929bda88e48f20687b81567 100644 (file)
@@ -45,6 +45,7 @@ enum ParseState {
     STATE_CURRENCY_UNIT,
     STATE_INTEGER_WIDTH,
     STATE_NUMBERING_SYSTEM,
+    STATE_MULTIPLY,
 };
 
 /**
@@ -99,6 +100,7 @@ enum StemEnum {
     STEM_CURRENCY,
     STEM_INTEGER_WIDTH,
     STEM_NUMBERING_SYSTEM,
+    STEM_MULTIPLY,
 };
 
 /**
@@ -236,6 +238,10 @@ void parseNumberingSystemOption(const StringSegment& segment, MacroProps& macros
 
 void generateNumberingSystemOption(const NumberingSystem& ns, UnicodeString& sb, UErrorCode& status);
 
+void parseMultiplierOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
+
+void generateMultiplierOption(int32_t magnitude, double arbitrary, UnicodeString& sb, UErrorCode& status);
+
 } // namespace blueprint_helpers
 
 /**
@@ -275,6 +281,8 @@ class GeneratorHelpers {
 
     static bool decimal(const MacroProps& macros, UnicodeString& sb, UErrorCode& status);
 
+    static bool multiplier(const MacroProps& macros, UnicodeString& sb, UErrorCode& status);
+
 };
 
 /**
@@ -293,6 +301,7 @@ struct SeenMacroProps {
     bool unitWidth = false;
     bool sign = false;
     bool decimal = false;
+    bool multiplier = false;
 };
 
 } // namespace impl
index bf650087886a3f50ddcf116389bb6644b0ace50b..947e970d6dfb3a7ad202cf2d45b104479331f9d4 100644 (file)
@@ -1913,7 +1913,7 @@ void NumberFormatterApiTest::decimal() {
 void NumberFormatterApiTest::multiplier() {
     assertFormatDescending(
             u"Multiplier None",
-            u"",
+            u"multiply/1",
             NumberFormatter::with().multiplier(Multiplier::none()),
             Locale::getEnglish(),
             u"87,650",
@@ -1928,7 +1928,7 @@ void NumberFormatterApiTest::multiplier() {
 
     assertFormatDescending(
             u"Multiplier Power of Ten",
-            nullptr,
+            u"multiply/1000000",
             NumberFormatter::with().multiplier(Multiplier::powerOfTen(6)),
             Locale::getEnglish(),
             u"87,650,000,000",
@@ -1943,7 +1943,7 @@ void NumberFormatterApiTest::multiplier() {
 
     assertFormatDescending(
             u"Multiplier Arbitrary Double",
-            nullptr,
+            u"multiply/5.2",
             NumberFormatter::with().multiplier(Multiplier::arbitraryDouble(5.2)),
             Locale::getEnglish(),
             u"455,780",
@@ -1958,7 +1958,7 @@ void NumberFormatterApiTest::multiplier() {
 
     assertFormatDescending(
             u"Multiplier Arbitrary BigDecimal",
-            nullptr,
+            u"multiply/5.2",
             NumberFormatter::with().multiplier(Multiplier::arbitraryDecimal({"5.2", -1})),
             Locale::getEnglish(),
             u"455,780",
@@ -1973,7 +1973,7 @@ void NumberFormatterApiTest::multiplier() {
 
     assertFormatDescending(
             u"Multiplier Zero",
-            nullptr,
+            u"multiply/0",
             NumberFormatter::with().multiplier(Multiplier::arbitraryDouble(0)),
             Locale::getEnglish(),
             u"0",
@@ -1985,6 +1985,22 @@ void NumberFormatterApiTest::multiplier() {
             u"0",
             u"0",
             u"0");
+
+    assertFormatSingle(
+            u"Multiplier Skeleton Scientific Notation and Percent",
+            u"percent multiply/1E2",
+            NumberFormatter::with().unit(NoUnit::percent()).multiplier(Multiplier::powerOfTen(2)),
+            Locale::getEnglish(),
+            0.5,
+            u"50%");
+
+    assertFormatSingle(
+            u"Negative Multiplier",
+            u"multiply/-5.2",
+            NumberFormatter::with().multiplier(Multiplier::arbitraryDouble(-5.2)),
+            Locale::getEnglish(),
+            2,
+            u"-10.4");
 }
 
 void NumberFormatterApiTest::locale() {
index 29d09f1a26e53ab36d0867b567072d197f403608..68138b9b08423b73538459660cc1fb59190fb052 100644 (file)
@@ -90,6 +90,11 @@ void NumberSkeletonTest::validTokens() {
             u"unit-width-hidden",
             u"decimal-auto",
             u"decimal-always",
+            u"multiply/5.2",
+            u"multiply/-5.2",
+            u"multiply/100",
+            u"multiply/1E2",
+            u"multiply/1",
             u"latin",
             u"numbering-system/arab",
             u"numbering-system/latn",
@@ -126,6 +131,9 @@ void NumberSkeletonTest::invalidTokens() {
             u"scientific/ee",
             u"round-increment/xxx",
             u"round-increment/0.1.2",
+            u"multiply/xxx",
+            u"multiply/0.1.2",
+            u"multiply/français", // non-invariant characters for C++
             u"currency/dummy",
             u"measure-unit/foo",
             u"integer-width/xxx",
@@ -174,7 +182,14 @@ void NumberSkeletonTest::duplicateValues() {
 }
 
 void NumberSkeletonTest::stemsRequiringOption() {
-    static const char16_t* stems[] = {u"round-increment", u"currency", u"measure-unit", u"integer-width",};
+    static const char16_t* stems[] = {
+            u"round-increment",
+            u"measure-unit",
+            u"per-unit",
+            u"currency",
+            u"integer-width",
+            u"numbering-system",
+            u"multiply"};
     static const char16_t* suffixes[] = {u"", u"/ceiling", u" scientific", u"/ceiling scientific"};
 
     for (auto& stem : stems) {
index 1366f0a411372f7330026cd7ed87a5a70cc67e6e..7a87d856e91ae15f170b838e9f3d8c7b6d23ef03 100644 (file)
@@ -946,6 +946,9 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
         if (isNegative()) {
             sb.append('-');
         }
+        if (precision == 0) {
+            sb.append('0');
+        }
         for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) {
             sb.append(getDigit(m));
             if (m == 0)
index d048221f5ceb96ca46e86b77cec2f58792543d7f..a40850ae19c173f94f31df6362470760155051ef 100644 (file)
@@ -3,6 +3,7 @@
 package com.ibm.icu.number;
 
 import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.math.MathContext;
 
 import com.ibm.icu.impl.number.DecimalQuantity;
@@ -37,6 +38,16 @@ public class Multiplier {
     }
 
     private Multiplier(int magnitude, BigDecimal arbitrary, MathContext mc) {
+        if (arbitrary != null) {
+            // Attempt to convert the BigDecimal to a magnitude multiplier.
+            arbitrary = arbitrary.stripTrailingZeros();
+            if (arbitrary.precision() == 1 && arbitrary.unscaledValue().equals(BigInteger.ONE)) {
+                // Success!
+                magnitude = -arbitrary.scale();
+                arbitrary = null;
+            }
+        }
+
         this.magnitude = magnitude;
         this.arbitrary = arbitrary;
         this.mc = mc;
@@ -130,6 +141,13 @@ public class Multiplier {
         }
     }
 
+    /**
+     * Returns whether the multiplier will change the number.
+     */
+    boolean isValid() {
+        return magnitude != 0 || arbitrary != null;
+    }
+
     /**
      * @internal
      * @deprecated ICU 62 This API is ICU internal only.
index 055bcc8f82d217811ecc4d930179b172d21b8a88..70df1dbcd1cbe5924ba7e4d2a5d866a404d39187 100644 (file)
@@ -52,6 +52,7 @@ class NumberSkeletonImpl {
         STATE_CURRENCY_UNIT,
         STATE_INTEGER_WIDTH,
         STATE_NUMBERING_SYSTEM,
+        STATE_MULTIPLY,
     }
 
     /**
@@ -103,6 +104,7 @@ class NumberSkeletonImpl {
         STEM_CURRENCY,
         STEM_INTEGER_WIDTH,
         STEM_NUMBERING_SYSTEM,
+        STEM_MULTIPLY,
     };
 
     /** For mapping from ordinal back to StemEnum in Java. */
@@ -155,6 +157,7 @@ class NumberSkeletonImpl {
         b.add("currency", StemEnum.STEM_CURRENCY.ordinal());
         b.add("integer-width", StemEnum.STEM_INTEGER_WIDTH.ordinal());
         b.add("numbering-system", StemEnum.STEM_NUMBERING_SYSTEM.ordinal());
+        b.add("multiply", StemEnum.STEM_MULTIPLY.ordinal());
 
         // Build the CharsTrie
         // TODO: Use SLOW or FAST here?
@@ -506,6 +509,7 @@ class NumberSkeletonImpl {
                 case STATE_CURRENCY_UNIT:
                 case STATE_INTEGER_WIDTH:
                 case STATE_NUMBERING_SYSTEM:
+                case STATE_MULTIPLY:
                     segment.setLength(Character.charCount(cp)); // for error message
                     throw new SkeletonSyntaxException("Stem requires an option", segment);
                 default:
@@ -653,6 +657,10 @@ class NumberSkeletonImpl {
             checkNull(macros.symbols, segment);
             return ParseState.STATE_NUMBERING_SYSTEM;
 
+        case STEM_MULTIPLY:
+            checkNull(macros.multiplier, segment);
+            return ParseState.STATE_MULTIPLY;
+
         default:
             throw new AssertionError();
         }
@@ -687,6 +695,9 @@ class NumberSkeletonImpl {
         case STATE_NUMBERING_SYSTEM:
             BlueprintHelpers.parseNumberingSystemOption(segment, macros);
             return ParseState.STATE_NULL;
+        case STATE_MULTIPLY:
+            BlueprintHelpers.parseMultiplierOption(segment, macros);
+            return ParseState.STATE_NULL;
         default:
             break;
         }
@@ -772,6 +783,9 @@ class NumberSkeletonImpl {
         if (macros.decimal != null && GeneratorHelpers.decimal(macros, sb)) {
             sb.append(' ');
         }
+        if (macros.multiplier != null && GeneratorHelpers.multiplier(macros, sb)) {
+            sb.append(' ');
+        }
 
         // Unsupported options
         if (macros.padder != null) {
@@ -782,10 +796,6 @@ class NumberSkeletonImpl {
             throw new UnsupportedOperationException(
                     "Cannot generate number skeleton with custom affix provider");
         }
-        if (macros.multiplier != null) {
-            throw new UnsupportedOperationException(
-                    "Cannot generate number skeleton with custom multiplier");
-        }
         if (macros.rules != null) {
             throw new UnsupportedOperationException(
                     "Cannot generate number skeleton with custom plural rules");
@@ -1152,6 +1162,28 @@ class NumberSkeletonImpl {
         private static void generateNumberingSystemOption(NumberingSystem ns, StringBuilder sb) {
             sb.append(ns.getName());
         }
+
+        private static void parseMultiplierOption(StringSegment segment, MacroProps macros) {
+            // Call segment.subSequence() because segment.toString() doesn't create a clean string.
+            String str = segment.subSequence(0, segment.length()).toString();
+            BigDecimal bd;
+            try {
+                bd = new BigDecimal(str);
+            } catch (NumberFormatException e) {
+                throw new SkeletonSyntaxException("Invalid multiplier", segment, e);
+            }
+            // NOTE: If bd is a power of ten, the Multiplier API optimizes it for us.
+            macros.multiplier = Multiplier.arbitrary(bd);
+        }
+
+        private static void generateMultiplierOption(Multiplier multiplier, StringBuilder sb) {
+            BigDecimal bd = multiplier.arbitrary;
+            if (bd == null) {
+                bd = BigDecimal.ONE;
+            }
+            bd = bd.scaleByPowerOfTen(multiplier.magnitude);
+            sb.append(bd.toPlainString());
+        }
     }
 
     ///// STEM GENERATION HELPER FUNCTIONS /////
@@ -1343,6 +1375,15 @@ class NumberSkeletonImpl {
             return true;
         }
 
+        private static boolean multiplier(MacroProps macros, StringBuilder sb) {
+            if (!macros.multiplier.isValid()) {
+                return false; // Default value
+            }
+            sb.append("multiply/");
+            BlueprintHelpers.generateMultiplierOption(macros.multiplier, sb);
+            return true;
+        }
+
     }
 
     ///// OTHER UTILITY FUNCTIONS /////
index 77291c929e08cea832b191815368796a0e8ba502..ac0a00f6d131379a53bd874ad0c155d4e6841928 100644 (file)
@@ -1887,7 +1887,7 @@ public class NumberFormatterApiTest {
     public void multiplier() {
         assertFormatDescending(
                 "Multiplier None",
-                null,
+                "multiply/1",
                 NumberFormatter.with().multiplier(Multiplier.none()),
                 ULocale.ENGLISH,
                 "87,650",
@@ -1902,7 +1902,7 @@ public class NumberFormatterApiTest {
 
         assertFormatDescending(
                 "Multiplier Power of Ten",
-                null,
+                "multiply/1000000",
                 NumberFormatter.with().multiplier(Multiplier.powerOfTen(6)),
                 ULocale.ENGLISH,
                 "87,650,000,000",
@@ -1917,7 +1917,7 @@ public class NumberFormatterApiTest {
 
         assertFormatDescending(
                 "Multiplier Arbitrary Double",
-                null,
+                "multiply/5.2",
                 NumberFormatter.with().multiplier(Multiplier.arbitrary(5.2)),
                 ULocale.ENGLISH,
                 "455,780",
@@ -1932,7 +1932,7 @@ public class NumberFormatterApiTest {
 
         assertFormatDescending(
                 "Multiplier Arbitrary BigDecimal",
-                null,
+                "multiply/5.2",
                 NumberFormatter.with().multiplier(Multiplier.arbitrary(new BigDecimal("5.2"))),
                 ULocale.ENGLISH,
                 "455,780",
@@ -1947,7 +1947,7 @@ public class NumberFormatterApiTest {
 
         assertFormatDescending(
                 "Multiplier Zero",
-                null,
+                "multiply/0",
                 NumberFormatter.with().multiplier(Multiplier.arbitrary(0)),
                 ULocale.ENGLISH,
                 "0",
@@ -1959,6 +1959,22 @@ public class NumberFormatterApiTest {
                 "0",
                 "0",
                 "0");
+
+        assertFormatSingle(
+                "Multiplier Skeleton Scientific Notation and Percent",
+                "percent multiply/1E2",
+                NumberFormatter.with().unit(NoUnit.PERCENT).multiplier(Multiplier.powerOfTen(2)),
+                ULocale.ENGLISH,
+                0.5,
+                "50%");
+
+        assertFormatSingle(
+                "Negative Multiplier",
+                "multiply/-5.2",
+                NumberFormatter.with().multiplier(Multiplier.arbitrary(-5.2)),
+                ULocale.ENGLISH,
+                2,
+                "-10.4");
     }
 
     @Test
index cd260d54a81187dc22683badb0ed22f6cc39b757..5e8847653297c2ce6324399745b960460cbface6 100644 (file)
@@ -82,6 +82,11 @@ public class NumberSkeletonTest {
                 "unit-width-hidden",
                 "decimal-auto",
                 "decimal-always",
+                "multiply/5.2",
+                "multiply/-5.2",
+                "multiply/100",
+                "multiply/1E2",
+                "multiply/1",
                 "latin",
                 "numbering-system/arab",
                 "numbering-system/latn",
@@ -120,6 +125,9 @@ public class NumberSkeletonTest {
                 "scientific/ee",
                 "round-increment/xxx",
                 "round-increment/0.1.2",
+                "multiply/xxx",
+                "multiply/0.1.2",
+                "multiply/français", // non-invariant characters for C++
                 "currency/dummy",
                 "measure-unit/foo",
                 "integer-width/xxx",
@@ -200,7 +208,14 @@ public class NumberSkeletonTest {
 
     @Test
     public void stemsRequiringOption() {
-        String[] stems = { "round-increment", "currency", "measure-unit", "integer-width", };
+        String[] stems = {
+                "round-increment",
+                "measure-unit",
+                "per-unit",
+                "currency",
+                "integer-width",
+                "numbering-system",
+                "multiply" };
         String[] suffixes = { "", "/ceiling", " scientific", "/ceiling scientific" };
 
         for (String stem : stems) {