]> granicus.if.org Git - icu/commitdiff
ICU-21556 Support currency as decimal separator in patterns
authorShane F. Carr <shane@unicode.org>
Tue, 21 Sep 2021 09:01:45 +0000 (09:01 +0000)
committerShane F. Carr <shane@unicode.org>
Wed, 22 Sep 2021 17:34:05 +0000 (12:34 -0500)
See #1711

30 files changed:
icu4c/source/i18n/decimfmt.cpp
icu4c/source/i18n/number_currencysymbols.cpp
icu4c/source/i18n/number_decimfmtprops.cpp
icu4c/source/i18n/number_decimfmtprops.h
icu4c/source/i18n/number_formatimpl.cpp
icu4c/source/i18n/number_formatimpl.h
icu4c/source/i18n/number_mapper.cpp
icu4c/source/i18n/number_mapper.h
icu4c/source/i18n/number_microprops.h
icu4c/source/i18n/number_patternmodifier.cpp
icu4c/source/i18n/number_patternmodifier.h
icu4c/source/i18n/number_patternstring.cpp
icu4c/source/i18n/number_patternstring.h
icu4c/source/i18n/number_types.h
icu4c/source/test/intltest/numbertest.h
icu4c/source/test/intltest/numbertest_patternstring.cpp
icu4c/source/test/intltest/numfmtst.cpp
icu4c/source/test/intltest/numfmtst.h
icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternProvider.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencyPluralInfoAffixProvider.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalFormatProperties.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternStringParser.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternStringUtils.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/PropertiesAffixPatternProvider.java
icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java
icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java

index 560fd8bd956cc1f04abb75d4774658a3550d297f..bca33366792705096df0ce57a536279852cc1318 100644 (file)
@@ -1077,7 +1077,7 @@ void DecimalFormat::setFormatWidth(int32_t width) {
 UnicodeString DecimalFormat::getPadCharacterString() const {
     if (fields == nullptr || fields->properties.padString.isBogus()) {
         // Readonly-alias the static string kFallbackPaddingString
-        return {TRUE, kFallbackPaddingString, -1};
+        return {true, kFallbackPaddingString, -1};
     } else {
         return fields->properties.padString;
     }
@@ -1322,6 +1322,7 @@ UnicodeString& DecimalFormat::toPattern(UnicodeString& result) const {
         !tprops.currency.isNull() ||
         !tprops.currencyPluralInfo.fPtr.isNull() ||
         !tprops.currencyUsage.isNull() ||
+        tprops.currencyAsDecimal ||
         AffixUtils::hasCurrencySymbols(tprops.positivePrefixPattern, localStatus) ||
         AffixUtils::hasCurrencySymbols(tprops.positiveSuffixPattern, localStatus) ||
         AffixUtils::hasCurrencySymbols(tprops.negativePrefixPattern, localStatus) ||
index 9208427904cceb6ee7c2f2f6f6f586c82f610efc..da1812f49f0b712514ff57ad02442cb331d79fab 100644 (file)
@@ -76,7 +76,7 @@ UnicodeString CurrencySymbols::loadSymbol(UCurrNameStyle selector, UErrorCode& s
     if (symbol == isoCode) {
         return UnicodeString(isoCode, 3);
     } else {
-        return UnicodeString(TRUE, symbol, symbolLen);
+        return UnicodeString(true, symbol, symbolLen);
     }
 }
 
@@ -104,7 +104,7 @@ UnicodeString CurrencySymbols::getPluralName(StandardPlural::Form plural, UError
     if (symbol == isoCode) {
         return UnicodeString(isoCode, 3);
     } else {
-        return UnicodeString(TRUE, symbol, symbolLen);
+        return UnicodeString(true, symbol, symbolLen);
     }
 }
 
index 8b2233c0556163bfa9d96d0ff06fc9f18166226c..7fa58bbc7ab66cae1dccd17740e09f40b4778262 100644 (file)
@@ -40,6 +40,7 @@ void DecimalFormatProperties::clear() {
     decimalPatternMatchRequired = false;
     decimalSeparatorAlwaysShown = false;
     exponentSignAlwaysShown = false;
+    currencyAsDecimal = false;
     formatFailIfMoreThanMaxDigits = false;
     formatWidth = -1;
     groupingSize = -1;
@@ -88,6 +89,7 @@ DecimalFormatProperties::_equals(const DecimalFormatProperties& other, bool igno
     eq = eq && currencyUsage == other.currencyUsage;
     eq = eq && decimalSeparatorAlwaysShown == other.decimalSeparatorAlwaysShown;
     eq = eq && exponentSignAlwaysShown == other.exponentSignAlwaysShown;
+    eq = eq && currencyAsDecimal == other.currencyAsDecimal;
     eq = eq && formatFailIfMoreThanMaxDigits == other.formatFailIfMoreThanMaxDigits;
     eq = eq && formatWidth == other.formatWidth;
     eq = eq && magnitudeMultiplier == other.magnitudeMultiplier;
index 0ace241adae8ab75dc58a2506328128a7df599f4..5f72f649842e66acc67ee2296ec6a57365282798 100644 (file)
@@ -105,6 +105,7 @@ struct U_I18N_API DecimalFormatProperties : public UMemory {
     bool decimalPatternMatchRequired;
     bool decimalSeparatorAlwaysShown;
     bool exponentSignAlwaysShown;
+    bool currencyAsDecimal;
     bool formatFailIfMoreThanMaxDigits; // ICU4C-only
     int32_t formatWidth;
     int32_t groupingSize;
index a57af93b3afd73e88bde0e4d93d23b09adad7f5d..96e3e9e7c6991020bb39910db78216364481079b 100644 (file)
@@ -352,10 +352,11 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps& macros, bool safe,
         return nullptr;
     }
     fPatternModifier.adoptInstead(patternModifier);
-    patternModifier->setPatternInfo(
-            macros.affixProvider != nullptr ? macros.affixProvider
-                                            : static_cast<const AffixPatternProvider*>(fPatternInfo.getAlias()),
-            kUndefinedField);
+    const AffixPatternProvider* affixProvider =
+        macros.affixProvider != nullptr
+            ? macros.affixProvider
+            : static_cast<const AffixPatternProvider*>(fPatternInfo.getAlias());
+    patternModifier->setPatternInfo(affixProvider, kUndefinedField);
     patternModifier->setPatternAttributes(fMicros.sign, isPermille, macros.approximately);
     if (patternModifier->needsPlurals()) {
         patternModifier->setSymbols(
@@ -375,6 +376,11 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps& macros, bool safe,
         return nullptr;
     }
 
+    // currencyAsDecimal
+    if (affixProvider->currencyAsDecimal()) {
+        fMicros.currencyAsDecimal = patternModifier->getCurrencySymbolForUnitWidth(status);
+    }
+
     // Outer modifier (CLDR units and currency long names)
     if (isCldrUnit) {
         const char *unitDisplayCase = "";
@@ -524,15 +530,27 @@ int32_t NumberFormatterImpl::writeNumber(const MicroProps& micros, DecimalQuanti
 
         // Add the decimal point
         if (quantity.getLowerDisplayMagnitude() < 0 || micros.decimal == UNUM_DECIMAL_SEPARATOR_ALWAYS) {
-            length += string.insert(
+            if (!micros.currencyAsDecimal.isBogus()) {
+                length += string.insert(
                     length + index,
-                    micros.useCurrency ? micros.symbols->getSymbol(
-                            DecimalFormatSymbols::ENumberFormatSymbol::kMonetarySeparatorSymbol) : micros
-                            .symbols
-                            ->getSymbol(
-                                    DecimalFormatSymbols::ENumberFormatSymbol::kDecimalSeparatorSymbol),
+                    micros.currencyAsDecimal,
+                    {UFIELD_CATEGORY_NUMBER, UNUM_CURRENCY_FIELD},
+                    status);
+            } else if (micros.useCurrency) {
+                length += string.insert(
+                    length + index,
+                    micros.symbols->getSymbol(
+                        DecimalFormatSymbols::ENumberFormatSymbol::kMonetarySeparatorSymbol),
                     {UFIELD_CATEGORY_NUMBER, UNUM_DECIMAL_SEPARATOR_FIELD},
                     status);
+            } else {
+                length += string.insert(
+                    length + index,
+                    micros.symbols->getSymbol(
+                        DecimalFormatSymbols::ENumberFormatSymbol::kDecimalSeparatorSymbol),
+                    {UFIELD_CATEGORY_NUMBER, UNUM_DECIMAL_SEPARATOR_FIELD},
+                    status);
+            }
         }
 
         // Add the fraction digits
index 9829edf716c7f50d8a2086f92fe70695806b4ec0..d7be1468b6d7af01ef807619f38a2967c05fd68a 100644 (file)
@@ -115,12 +115,6 @@ class NumberFormatterImpl : public UMemory {
     LocalPointer<const LongNameMultiplexer> fLongNameMultiplexer;
     LocalPointer<const CompactHandler> fCompactHandler;
 
-    // Value objects possibly used by the number formatting pipeline:
-    struct Warehouse {
-        CurrencySymbols fCurrencySymbols;
-    } fWarehouse;
-
-
     NumberFormatterImpl(const MacroProps &macros, bool safe, UErrorCode &status);
 
     MicroProps& preProcessUnsafe(DecimalQuantity &inValue, UErrorCode &status);
index e2a0d284b7cf5d4c50a494267fb7ff4533c25c66..2d4d47a094d999bc5c23405d7b991c86db293b96 100644 (file)
@@ -381,7 +381,10 @@ void PropertiesAffixPatternProvider::setTo(const DecimalFormatProperties& proper
         AffixUtils::hasCurrencySymbols(ppp, status) ||
         AffixUtils::hasCurrencySymbols(psp, status) ||
         AffixUtils::hasCurrencySymbols(npp, status) ||
-        AffixUtils::hasCurrencySymbols(nsp, status));
+        AffixUtils::hasCurrencySymbols(nsp, status) ||
+        properties.currencyAsDecimal);
+
+    fCurrencyAsDecimal = properties.currencyAsDecimal;
 }
 
 char16_t PropertiesAffixPatternProvider::charAt(int flags, int i) const {
@@ -446,6 +449,10 @@ bool PropertiesAffixPatternProvider::hasBody() const {
     return true;
 }
 
+bool PropertiesAffixPatternProvider::currencyAsDecimal() const {
+    return fCurrencyAsDecimal;
+}
+
 
 void CurrencyPluralInfoAffixProvider::setTo(const CurrencyPluralInfo& cpi,
                                             const DecimalFormatProperties& properties,
@@ -506,5 +513,9 @@ bool CurrencyPluralInfoAffixProvider::hasBody() const {
     return affixesByPlural[StandardPlural::OTHER].hasBody();
 }
 
+bool CurrencyPluralInfoAffixProvider::currencyAsDecimal() const {
+    return affixesByPlural[StandardPlural::OTHER].currencyAsDecimal();
+}
+
 
 #endif /* #if !UCONFIG_NO_FORMATTING */
index 9ecd776b3b4795512159c3e60c3b2c32cbb81c50..8879b7a94ea372d3a1db2c40c21072dc2d5337b7 100644 (file)
@@ -56,12 +56,15 @@ class PropertiesAffixPatternProvider : public AffixPatternProvider, public UMemo
 
     bool hasBody() const U_OVERRIDE;
 
+    bool currencyAsDecimal() const U_OVERRIDE;
+
   private:
     UnicodeString posPrefix;
     UnicodeString posSuffix;
     UnicodeString negPrefix;
     UnicodeString negSuffix;
     bool isCurrencyPattern;
+    bool fCurrencyAsDecimal;
 
     PropertiesAffixPatternProvider() = default; // puts instance in valid but undefined state
 
@@ -107,6 +110,8 @@ class CurrencyPluralInfoAffixProvider : public AffixPatternProvider, public UMem
 
     bool hasBody() const U_OVERRIDE;
 
+    bool currencyAsDecimal() const U_OVERRIDE;
+
   private:
     PropertiesAffixPatternProvider affixesByPlural[StandardPlural::COUNT];
 
index a18d5fc470eda1559bfd12f8bbd84a92b2e73590..c34e7c17e97aa5d82aaf36257af48d46e4ed648d 100644 (file)
@@ -18,6 +18,7 @@
 #include "number_roundingutils.h"
 #include "decNumber.h"
 #include "charstr.h"
+#include "util.h"
 
 U_NAMESPACE_BEGIN namespace number {
 namespace impl {
@@ -83,6 +84,9 @@ struct MicroProps : public MicroPropsGenerator {
     bool useCurrency;
     char nsName[9];
 
+    // Currency symbol to be used as the decimal separator
+    UnicodeString currencyAsDecimal = ICU_Utility::makeBogusString();
+
     // No ownership: must point at a string which will outlive MicroProps
     // instances, e.g. a string with static storage duration, or just a string
     // that will never be deallocated or modified.
index c35620a08b616f33c0fa528c7b4265dc4896608d..b6543b262b42db4227e96deaa07622f37344ac0e 100644 (file)
@@ -300,24 +300,8 @@ UnicodeString MutablePatternModifier::getSymbol(AffixPatternType type) const {
             return fSymbols->getSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kPercentSymbol);
         case AffixPatternType::TYPE_PERMILLE:
             return fSymbols->getSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kPerMillSymbol);
-        case AffixPatternType::TYPE_CURRENCY_SINGLE: {
-            switch (fUnitWidth) {
-            case UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW:
-                return fCurrencySymbols.getNarrowCurrencySymbol(localStatus);
-            case UNumberUnitWidth::UNUM_UNIT_WIDTH_SHORT:
-                return fCurrencySymbols.getCurrencySymbol(localStatus);
-            case UNumberUnitWidth::UNUM_UNIT_WIDTH_ISO_CODE:
-                return fCurrencySymbols.getIntlCurrencySymbol(localStatus);
-            case UNumberUnitWidth::UNUM_UNIT_WIDTH_FORMAL:
-                return fCurrencySymbols.getFormalCurrencySymbol(localStatus);
-            case UNumberUnitWidth::UNUM_UNIT_WIDTH_VARIANT:
-                return fCurrencySymbols.getVariantCurrencySymbol(localStatus);
-            case UNumberUnitWidth::UNUM_UNIT_WIDTH_HIDDEN:
-                return UnicodeString();
-            default:
-                return fCurrencySymbols.getCurrencySymbol(localStatus);
-            }
-        }
+        case AffixPatternType::TYPE_CURRENCY_SINGLE:
+            return getCurrencySymbolForUnitWidth(localStatus);
         case AffixPatternType::TYPE_CURRENCY_DOUBLE:
             return fCurrencySymbols.getIntlCurrencySymbol(localStatus);
         case AffixPatternType::TYPE_CURRENCY_TRIPLE:
@@ -335,6 +319,25 @@ UnicodeString MutablePatternModifier::getSymbol(AffixPatternType type) const {
     }
 }
 
+UnicodeString MutablePatternModifier::getCurrencySymbolForUnitWidth(UErrorCode& status) const {
+    switch (fUnitWidth) {
+    case UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW:
+        return fCurrencySymbols.getNarrowCurrencySymbol(status);
+    case UNumberUnitWidth::UNUM_UNIT_WIDTH_SHORT:
+        return fCurrencySymbols.getCurrencySymbol(status);
+    case UNumberUnitWidth::UNUM_UNIT_WIDTH_ISO_CODE:
+        return fCurrencySymbols.getIntlCurrencySymbol(status);
+    case UNumberUnitWidth::UNUM_UNIT_WIDTH_FORMAL:
+        return fCurrencySymbols.getFormalCurrencySymbol(status);
+    case UNumberUnitWidth::UNUM_UNIT_WIDTH_VARIANT:
+        return fCurrencySymbols.getVariantCurrencySymbol(status);
+    case UNumberUnitWidth::UNUM_UNIT_WIDTH_HIDDEN:
+        return UnicodeString();
+    default:
+        return fCurrencySymbols.getCurrencySymbol(status);
+    }
+}
+
 UnicodeString MutablePatternModifier::toUnicodeString() const {
     // Never called by AffixUtils
     UPRV_UNREACHABLE_EXIT;
index 0e2a2b557eae952b5357050224ea29c3084b9b4e..4f825e1ed2191646545c6ad8a4c0f81023e93133 100644 (file)
@@ -195,6 +195,11 @@ class U_I18N_API MutablePatternModifier
      */
     UnicodeString getSymbol(AffixPatternType type) const U_OVERRIDE;
 
+    /**
+     * Returns the currency symbol for the unit width specified in setSymbols()
+     */
+    UnicodeString getCurrencySymbolForUnitWidth(UErrorCode& status) const;
+
     UnicodeString toUnicodeString() const;
 
   private:
index c7f9e0e2d1ec68c4ed97ead5d7d77d55b4fb5df0..31b2b38506d6f5cffc89c9604627e80418551766 100644 (file)
@@ -115,6 +115,10 @@ bool ParsedPatternInfo::hasBody() const {
     return positive.integerTotal > 0;
 }
 
+bool ParsedPatternInfo::currencyAsDecimal() const {
+    return positive.hasCurrencyDecimal;
+}
+
 /////////////////////////////////////////////////////
 /// BEGIN RECURSIVE DESCENT PARSER IMPLEMENTATION ///
 /////////////////////////////////////////////////////
@@ -127,8 +131,20 @@ UChar32 ParsedPatternInfo::ParserState::peek() {
     }
 }
 
+UChar32 ParsedPatternInfo::ParserState::peek2() {
+    if (offset == pattern.length()) {
+        return -1;
+    }
+    int32_t cp1 = pattern.char32At(offset);
+    int32_t offset2 = offset + U16_LENGTH(cp1);
+    if (offset2 == pattern.length()) {
+        return -1;
+    }
+    return pattern.char32At(offset2);
+}
+
 UChar32 ParsedPatternInfo::ParserState::next() {
-    int codePoint = peek();
+    int32_t codePoint = peek();
     offset += U16_LENGTH(codePoint);
     return codePoint;
 }
@@ -286,6 +302,35 @@ void ParsedPatternInfo::consumeFormat(UErrorCode& status) {
         currentSubpattern->widthExceptAffixes += 1;
         consumeFractionFormat(status);
         if (U_FAILURE(status)) { return; }
+    } else if (state.peek() == u'¤') {
+        // Check if currency is a decimal separator
+        switch (state.peek2()) {
+            case u'#':
+            case u'0':
+            case u'1':
+            case u'2':
+            case u'3':
+            case u'4':
+            case u'5':
+            case u'6':
+            case u'7':
+            case u'8':
+            case u'9':
+                break;
+            default:
+                // Currency symbol followed by a non-numeric character;
+                // treat as a normal affix.
+                return;
+        }
+        // Currency symbol is followed by a numeric character;
+        // treat as a decimal separator.
+        currentSubpattern->hasCurrencySign = true;
+        currentSubpattern->hasCurrencyDecimal = true;
+        currentSubpattern->hasDecimal = true;
+        currentSubpattern->widthExceptAffixes += 1;
+        state.next(); // consume the symbol
+        consumeFractionFormat(status);
+        if (U_FAILURE(status)) { return; }
     }
 }
 
@@ -565,6 +610,9 @@ PatternParser::patternInfoToProperties(DecimalFormatProperties& properties, Pars
         properties.decimalSeparatorAlwaysShown = false;
     }
 
+    // Persist the currency as decimal separator
+    properties.currencyAsDecimal = positive.hasCurrencyDecimal;
+
     // Scientific notation settings
     if (positive.exponentZeros > 0) {
         properties.exponentSignAlwaysShown = positive.exponentHasPlusSign;
@@ -750,7 +798,11 @@ UnicodeString PatternStringUtils::propertiesToPatternString(const DecimalFormatP
         }
         // Decimal separator
         if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) {
-            sb.append(u'.');
+            if (properties.currencyAsDecimal) {
+                sb.append(u'¤');
+            } else {
+                sb.append(u'.');
+            }
         }
         if (!useGrouping) {
             continue;
index bf6b1adc2ec90120636883b3b27e3af534ad72da..94afda372296ae5daed57984d69e9b0954d69ce2 100644 (file)
@@ -62,6 +62,7 @@ struct U_I18N_API ParsedSubpatternInfo {
     bool hasPercentSign = false;
     bool hasPerMilleSign = false;
     bool hasCurrencySign = false;
+    bool hasCurrencyDecimal = false;
     bool hasMinusSign = false;
     bool hasPlusSign = false;
 
@@ -104,6 +105,8 @@ struct U_I18N_API ParsedPatternInfo : public AffixPatternProvider, public UMemor
 
     bool hasBody() const U_OVERRIDE;
 
+    bool currencyAsDecimal() const U_OVERRIDE;
+
   private:
     struct U_I18N_API ParserState {
         const UnicodeString& pattern; // reference to the parent
@@ -119,8 +122,13 @@ struct U_I18N_API ParsedPatternInfo : public AffixPatternProvider, public UMemor
             return *this;
         }
 
+        /** Returns the next code point, or -1 if string is too short. */
         UChar32 peek();
 
+        /** Returns the code point after the next code point, or -1 if string is too short. */
+        UChar32 peek2();
+
+        /** Returns the next code point and then steps forward. */
         UChar32 next();
 
         // TODO: We don't currently do anything with the message string.
index 6a6b3edaac52c19ff633bd4ce35a5378deca5433..84846efb9242ac4a89a6fd6c6c348e2f827774b3 100644 (file)
@@ -140,6 +140,11 @@ class U_I18N_API AffixPatternProvider {
      * number instead of rendering the number.
      */
     virtual bool hasBody() const = 0;
+
+    /**
+     * True if the currency symbol should replace the decimal separator.
+     */
+    virtual bool currencyAsDecimal() const = 0;
 };
 
 
index 309a42ae732f0f1fd56fdf2e6cca1729707c1892..f5a4ba75f1b3d9048582485976fdd5e5c11baf97 100644 (file)
@@ -254,12 +254,13 @@ class PatternModifierTest : public IntlTest {
     UnicodeString getSuffix(const MutablePatternModifier &mod, UErrorCode &status);
 };
 
-class PatternStringTest : public IntlTest {
+class PatternStringTest : public IntlTestWithFieldPosition {
   public:
     void testLocalized();
     void testToPatternSimple();
     void testExceptionOnInvalid();
     void testBug13117();
+    void testCurrencyDecimal();
 
     void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0) override;
 
index b89148df00d432a867858d756f4f94590bfa00bc..0bfd58016ffaf1b4ec437e03eda261ea6464a85d 100644 (file)
@@ -17,6 +17,7 @@ void PatternStringTest::runIndexedTest(int32_t index, UBool exec, const char*& n
         TESTCASE_AUTO(testToPatternSimple);
         TESTCASE_AUTO(testExceptionOnInvalid);
         TESTCASE_AUTO(testBug13117);
+        TESTCASE_AUTO(testCurrencyDecimal);
     TESTCASE_AUTO_END;
 }
 
@@ -56,6 +57,9 @@ void PatternStringTest::testToPatternSimple() {
                                   {u"0E0", u"0E0"},
                                   {u"#00E00", u"#00E00"},
                                   {u"#,##0", u"#,##0"},
+                                  {u"0¤", u"0¤"},
+                                  {u"0¤a", u"0¤a"},
+                                  {u"0¤00", u"0¤00"},
                                   {u"#;#", u"0;0"},
             // ignore a negative prefix pattern of '-' since that is the default:
                                   {u"#;-#", u"0"},
@@ -77,6 +81,7 @@ void PatternStringTest::testToPatternSimple() {
         assertSuccess(input, status);
         UnicodeString actual = PatternStringUtils::propertiesToPatternString(properties, status);
         assertEquals(input, output, actual);
+        status = U_ZERO_ERROR;
     }
 }
 
@@ -113,4 +118,34 @@ void PatternStringTest::testBug13117() {
     assertTrue("Should not consume negative subpattern", expected == actual);
 }
 
+void PatternStringTest::testCurrencyDecimal() {
+    IcuTestErrorCode status(*this, "testCurrencyDecimal");
+
+    // Manually create a NumberFormatter from a specific pattern
+    ParsedPatternInfo patternInfo;
+    PatternParser::parseToPatternInfo(u"a0¤00b", patternInfo, status);
+    MacroProps macros;
+    macros.unit = CurrencyUnit(u"EUR", status);
+    macros.affixProvider = &patternInfo;
+    LocalizedNumberFormatter nf = NumberFormatter::with().macros(macros).locale("und");
+
+    // Test that the output is as expected
+    FormattedNumber fn = nf.formatDouble(3.14, status);
+    assertEquals("Should substitute currency symbol", u"a3€14b", fn.toTempString(status));
+
+    // Test field positions
+    static const UFieldPosition expectedFieldPositions[] = {
+            {UNUM_INTEGER_FIELD, 1, 2},
+            {UNUM_CURRENCY_FIELD, 2, 3},
+            {UNUM_FRACTION_FIELD, 3, 5}};
+    checkFormattedValue(
+        u"Currency as decimal basic field positions",
+        fn,
+        u"a3€14b",
+        UFIELD_CATEGORY_NUMBER,
+        expectedFieldPositions,
+        UPRV_LENGTHOF(expectedFieldPositions)
+    );
+}
+
 #endif /* #if !UCONFIG_NO_FORMATTING */
index 34e81d473a6e60e2ec515fd5d95a1b7878188e59..011cc32dd34195a3b9652718152867f0c5977cf4 100644 (file)
@@ -251,6 +251,7 @@ void NumberFormatTest::runIndexedTest( int32_t index, UBool exec, const char* &n
   TESTCASE_AUTO(Test20425_FractionWithIntegerIncrement);
   TESTCASE_AUTO(Test21232_ParseTimeout);
   TESTCASE_AUTO(Test10997_FormatCurrency);
+  TESTCASE_AUTO(Test21556_CurrencyAsDecimal);
   TESTCASE_AUTO_END;
 }
 
@@ -10089,9 +10090,8 @@ void NumberFormatTest::Test21232_ParseTimeout() {
 void NumberFormatTest::Test10997_FormatCurrency() {
     IcuTestErrorCode status(*this, "Test10997_FormatCurrency");
 
-    UErrorCode error = U_ZERO_ERROR;
-    NumberFormat* fmt = NumberFormat::createCurrencyInstance(Locale::getUS(), error);
-    if (U_FAILURE(error)) {
+    LocalPointer<NumberFormat> fmt(NumberFormat::createCurrencyInstance(Locale::getUS(), status));
+    if (status.errDataIfFailureAndReset()) {
         return;
     }
     fmt->setMinimumFractionDigits(4);
@@ -10108,8 +10108,40 @@ void NumberFormatTest::Test10997_FormatCurrency() {
     Formattable eurAmnt(new CurrencyAmount(123.45, u"EUR", status));
     fmt->format(eurAmnt, str2, fp, status);
     assertEquals("minFrac 4 should be respected in different currency", u"€123.4500", str2);
+}
 
-    delete fmt;
+void NumberFormatTest::Test21556_CurrencyAsDecimal() {
+    IcuTestErrorCode status(*this, "Test21556_CurrencyAsDecimal");
+
+    {
+        DecimalFormat df(u"a0¤00b", status);
+        if (status.errDataIfFailureAndReset()) {
+            return;
+        }
+        df.setCurrency(u"EUR", status);
+        UnicodeString result;
+        FieldPosition fp(UNUM_CURRENCY_FIELD);
+        df.format(3.141, result, fp);
+        assertEquals("Basic test: format", u"a3€14b", result);
+        UnicodeString pattern;
+        assertEquals("Basic test: toPattern", u"a0¤00b", df.toPattern(pattern));
+        assertEquals("Basic test: field position begin", 2, fp.getBeginIndex());
+        assertEquals("Basic test: field position end", 3, fp.getEndIndex());
+    }
+
+    {
+        LocalPointer<NumberFormat> nf(NumberFormat::createCurrencyInstance("en-GB", status));
+        DecimalFormat* df = static_cast<DecimalFormat*>(nf.getAlias());
+        df->applyPattern(u"a0¤00b", status);
+        UnicodeString result;
+        FieldPosition fp(UNUM_CURRENCY_FIELD);
+        df->format(3.141, result, fp);
+        assertEquals("Via applyPattern: format", u"a3£14b", result);
+        UnicodeString pattern;
+        assertEquals("Via applyPattern: toPattern", u"a0¤00b", df->toPattern(pattern));
+        assertEquals("Via applyPattern: field position begin", 2, fp.getBeginIndex());
+        assertEquals("Via applyPattern: field position end", 3, fp.getEndIndex());
+    }
 }
 
 #endif /* #if !UCONFIG_NO_FORMATTING */
index 8af7d6e37ead1450fb5087ffbceedda70dad808b..abd828d644bccdcdd597fd391e7315f400d65142 100644 (file)
@@ -307,6 +307,7 @@ class NumberFormatTest: public CalendarTimeZoneTest {
     void Test20425_FractionWithIntegerIncrement();
     void Test21232_ParseTimeout();
     void Test10997_FormatCurrency();
+    void Test21556_CurrencyAsDecimal();
 
  private:
     UBool testFormattableAsUFormattable(const char *file, int line, Formattable &f);
index 1b805ae0695cbcea39f65619f1c3a828770adb66..4cf1e086de60e74a6676adb4d588a1dc8293da61 100644 (file)
@@ -38,4 +38,9 @@ public interface AffixPatternProvider {
      * number instead of rendering the number.
      */
     public boolean hasBody();
+
+    /**
+     * True if the currency symbol should replace the decimal separator.
+     */
+    public boolean currencyAsDecimal();
 }
index 8963608d6a51a0d7265160b5c4ebe95122f5382a..9544a2e0eb267fb0e4d03b9e5e0076735a5a3cdc 100644 (file)
@@ -68,4 +68,9 @@ public class CurrencyPluralInfoAffixProvider implements AffixPatternProvider {
     public boolean hasBody() {
         return affixesByPlural[StandardPlural.OTHER.ordinal()].hasBody();
     }
+
+    @Override
+    public boolean currencyAsDecimal() {
+        return affixesByPlural[StandardPlural.OTHER.ordinal()].currencyAsDecimal();
+    }
 }
index 10a358143b4ca7e1ef1b2b334023df053a0cce91..d38af94ed6c14781704e71a4663e6013827e9e69 100644 (file)
@@ -92,6 +92,7 @@ public class DecimalFormatProperties implements Cloneable, Serializable {
     private transient boolean decimalPatternMatchRequired;
     private transient boolean decimalSeparatorAlwaysShown;
     private transient boolean exponentSignAlwaysShown;
+    private transient boolean currencyAsDecimal;
     private transient int formatWidth;
     private transient int groupingSize;
     private transient boolean groupingUsed;
@@ -164,6 +165,7 @@ public class DecimalFormatProperties implements Cloneable, Serializable {
         decimalPatternMatchRequired = false;
         decimalSeparatorAlwaysShown = false;
         exponentSignAlwaysShown = false;
+        currencyAsDecimal = false;
         formatWidth = -1;
         groupingSize = -1;
         groupingUsed = true;
@@ -210,6 +212,7 @@ public class DecimalFormatProperties implements Cloneable, Serializable {
         decimalPatternMatchRequired = other.decimalPatternMatchRequired;
         decimalSeparatorAlwaysShown = other.decimalSeparatorAlwaysShown;
         exponentSignAlwaysShown = other.exponentSignAlwaysShown;
+        currencyAsDecimal = other.currencyAsDecimal;
         formatWidth = other.formatWidth;
         groupingSize = other.groupingSize;
         groupingUsed = other.groupingUsed;
@@ -257,6 +260,7 @@ public class DecimalFormatProperties implements Cloneable, Serializable {
         eq = eq && _equalsHelper(decimalPatternMatchRequired, other.decimalPatternMatchRequired);
         eq = eq && _equalsHelper(decimalSeparatorAlwaysShown, other.decimalSeparatorAlwaysShown);
         eq = eq && _equalsHelper(exponentSignAlwaysShown, other.exponentSignAlwaysShown);
+        eq = eq && _equalsHelper(currencyAsDecimal, other.currencyAsDecimal);
         eq = eq && _equalsHelper(formatWidth, other.formatWidth);
         eq = eq && _equalsHelper(groupingSize, other.groupingSize);
         eq = eq && _equalsHelper(groupingUsed, other.groupingUsed);
@@ -320,6 +324,7 @@ public class DecimalFormatProperties implements Cloneable, Serializable {
         hashCode ^= _hashCodeHelper(decimalPatternMatchRequired);
         hashCode ^= _hashCodeHelper(decimalSeparatorAlwaysShown);
         hashCode ^= _hashCodeHelper(exponentSignAlwaysShown);
+        hashCode ^= _hashCodeHelper(currencyAsDecimal);
         hashCode ^= _hashCodeHelper(formatWidth);
         hashCode ^= _hashCodeHelper(groupingSize);
         hashCode ^= _hashCodeHelper(groupingUsed);
@@ -443,6 +448,10 @@ public class DecimalFormatProperties implements Cloneable, Serializable {
         return exponentSignAlwaysShown;
     }
 
+    public boolean getCurrencyAsDecimal() {
+        return currencyAsDecimal;
+    }
+
     public int getFormatWidth() {
         return formatWidth;
     }
@@ -769,6 +778,18 @@ public class DecimalFormatProperties implements Cloneable, Serializable {
         return this;
     }
 
+    /**
+     * Sets whether the currency symbol should replace the decimal separator.
+     *
+     * @param currencyAsDecimal
+     *              Whether the currency symbol should replace the decimal separator.
+     * @return The property bag, for chaining.
+     */
+    public DecimalFormatProperties setCurrencyAsDecimal(boolean currencyAsDecimal) {
+        this.currencyAsDecimal = currencyAsDecimal;
+        return this;
+    }
+
     /**
      * Sets the minimum width of the string output by the formatting pipeline. For example, if padding is
      * enabled and paddingWidth is set to 6, formatting the number "3.14159" with the pattern "0.00" will
index 3b2b76d7f019c9931452b947a7c4cd5e9e8fd64c..1fb0a61e0f533dce7e119ff1c9576b9110874e74 100644 (file)
@@ -46,6 +46,9 @@ public class MicroProps implements Cloneable, MicroPropsGenerator {
     public boolean useCurrency;
     public String gender;
 
+    // Currency symbol to be used as the decimal separator
+    public String currencyAsDecimal;
+
     // Internal fields:
     private final boolean immutable;
 
index 14782921340db75d8329da1254e4dfcb33950d0c..ca0661f3d6e6352d5e1df3a83711f0c08d6caa2a 100644 (file)
@@ -402,31 +402,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
         case AffixUtils.TYPE_PERMILLE:
             return symbols.getPerMillString();
         case AffixUtils.TYPE_CURRENCY_SINGLE:
-            // UnitWidth ISO, HIDDEN, or NARROW overrides the singular currency symbol.
-            if (unitWidth == UnitWidth.ISO_CODE) {
-                return currency.getCurrencyCode();
-            } else if (unitWidth == UnitWidth.HIDDEN) {
-                return "";
-            } else {
-                int selector;
-                switch (unitWidth) {
-                    case SHORT:
-                        selector = Currency.SYMBOL_NAME;
-                        break;
-                    case NARROW:
-                        selector = Currency.NARROW_SYMBOL_NAME;
-                        break;
-                    case FORMAL:
-                        selector = Currency.FORMAL_SYMBOL_NAME;
-                        break;
-                    case VARIANT:
-                        selector = Currency.VARIANT_SYMBOL_NAME;
-                        break;
-                    default:
-                        throw new AssertionError();
-                }
-                return currency.getName(symbols.getULocale(), selector, null);
-            }
+            return getCurrencySymbolForUnitWidth();
         case AffixUtils.TYPE_CURRENCY_DOUBLE:
             return currency.getCurrencyCode();
         case AffixUtils.TYPE_CURRENCY_TRIPLE:
@@ -444,4 +420,35 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
             throw new AssertionError();
         }
     }
+
+    /**
+     * Returns the currency symbol for the unit width specified in setSymbols()
+     */
+    public String getCurrencySymbolForUnitWidth() {
+        // UnitWidth ISO, HIDDEN, or NARROW overrides the singular currency symbol.
+        if (unitWidth == UnitWidth.ISO_CODE) {
+            return currency.getCurrencyCode();
+        } else if (unitWidth == UnitWidth.HIDDEN) {
+            return "";
+        } else {
+            int selector;
+            switch (unitWidth) {
+                case SHORT:
+                    selector = Currency.SYMBOL_NAME;
+                    break;
+                case NARROW:
+                    selector = Currency.NARROW_SYMBOL_NAME;
+                    break;
+                case FORMAL:
+                    selector = Currency.FORMAL_SYMBOL_NAME;
+                    break;
+                case VARIANT:
+                    selector = Currency.VARIANT_SYMBOL_NAME;
+                    break;
+                default:
+                    throw new AssertionError();
+            }
+            return currency.getName(symbols.getULocale(), selector, null);
+        }
+    }
 }
index 43c54db368885437d7e4c0e497ee5c5e065d708a..0253035abbeb261255325b1335a8e7fc42f27103 100644 (file)
@@ -174,6 +174,11 @@ public class PatternStringParser {
         public boolean hasBody() {
             return positive.integerTotal > 0;
         }
+
+        @Override
+        public boolean currencyAsDecimal() {
+            return positive.hasCurrencyDecimal;
+        }
     }
 
     public static class ParsedSubpatternInfo {
@@ -195,6 +200,7 @@ public class PatternStringParser {
         public boolean hasPercentSign = false;
         public boolean hasPerMilleSign = false;
         public boolean hasCurrencySign = false;
+        public boolean hasCurrencyDecimal = false;
         public boolean hasMinusSign = false;
         public boolean hasPlusSign = false;
 
@@ -217,6 +223,7 @@ public class PatternStringParser {
             this.offset = 0;
         }
 
+        /** Returns the next code point, or -1 if string is too short. */
         int peek() {
             if (offset == pattern.length()) {
                 return -1;
@@ -225,6 +232,20 @@ public class PatternStringParser {
             }
         }
 
+        /** Returns the code point after the next code point, or -1 if string is too short. */
+        int peek2() {
+            if (offset == pattern.length()) {
+                return -1;
+            }
+            int cp1 = pattern.codePointAt(offset);
+            int offset2 = offset + Character.charCount(cp1);
+            if (offset2 == pattern.length()) {
+                return -1;
+            }
+            return pattern.codePointAt(offset2);
+        }
+
+        /** Returns the next code point and then steps forward. */
         int next() {
             int codePoint = peek();
             offset += Character.charCount(codePoint);
@@ -366,6 +387,34 @@ public class PatternStringParser {
             result.hasDecimal = true;
             result.widthExceptAffixes += 1;
             consumeFractionFormat(state, result);
+        } else if (state.peek() == '¤') {
+            // Check if currency is a decimal separator
+            switch (state.peek2()) {
+                case '#':
+                case '0':
+                case '1':
+                case '2':
+                case '3':
+                case '4':
+                case '5':
+                case '6':
+                case '7':
+                case '8':
+                case '9':
+                    break;
+                default:
+                    // Currency symbol followed by a non-numeric character;
+                    // treat as a normal affix.
+                    return;
+            }
+            // Currency symbol is followed by a numeric character;
+            // treat as a decimal separator.
+            result.hasCurrencySign = true;
+            result.hasCurrencyDecimal = true;
+            result.hasDecimal = true;
+            result.widthExceptAffixes += 1;
+            state.next(); // consume the symbol
+            consumeFractionFormat(state, result);
         }
     }
 
@@ -628,6 +677,9 @@ public class PatternStringParser {
             properties.setDecimalSeparatorAlwaysShown(false);
         }
 
+        // Persist the currency as decimal separator
+        properties.setCurrencyAsDecimal(positive.hasCurrencyDecimal);
+
         // Scientific notation settings
         if (positive.exponentZeros > 0) {
             properties.setExponentSignAlwaysShown(positive.exponentHasPlusSign);
index ec659fa82ffbec025aa7e8e39d86107b623e17fe..427177296d0eed63381b59661f31d6f6e2ffc5ba 100644 (file)
@@ -91,6 +91,7 @@ public class PatternStringUtils {
         int minSig = Math.min(properties.getMinimumSignificantDigits(), dosMax);
         int maxSig = Math.min(properties.getMaximumSignificantDigits(), dosMax);
         boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
+        boolean currencyAsDecimal = properties.getCurrencyAsDecimal();
         int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax);
         boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown();
         AffixPatternProvider affixes = PropertiesAffixPatternProvider.forProperties(properties);
@@ -153,7 +154,11 @@ public class PatternStringUtils {
             }
             // Decimal separator
             if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) {
-                sb.append('.');
+                if (currencyAsDecimal) {
+                    sb.append('¤');
+                } else {
+                    sb.append('.');
+                }
             }
             if (!useGrouping) {
                 continue;
index 870007ac34a4719dd64d2cd8615ed33b0a158ac2..9486fe37e1f6bd7abca987d3e3f954ab58a596a9 100644 (file)
@@ -8,6 +8,7 @@ public class PropertiesAffixPatternProvider implements AffixPatternProvider {
     private final String negPrefix;
     private final String negSuffix;
     private final boolean isCurrencyPattern;
+    private final boolean currencyAsDecimal;
 
     public static AffixPatternProvider forProperties(DecimalFormatProperties properties) {
         if (properties.getCurrencyPluralInfo() == null) {
@@ -84,7 +85,10 @@ public class PropertiesAffixPatternProvider implements AffixPatternProvider {
             AffixUtils.hasCurrencySymbols(ppp) ||
             AffixUtils.hasCurrencySymbols(psp) ||
             AffixUtils.hasCurrencySymbols(npp) ||
-            AffixUtils.hasCurrencySymbols(nsp));
+            AffixUtils.hasCurrencySymbols(nsp) ||
+            properties.getCurrencyAsDecimal());
+
+        currencyAsDecimal = properties.getCurrencyAsDecimal();
     }
 
     @Override
@@ -150,6 +154,11 @@ public class PropertiesAffixPatternProvider implements AffixPatternProvider {
         return true;
     }
 
+    @Override
+    public boolean currencyAsDecimal() {
+        return currencyAsDecimal;
+    }
+
     @Override
     public String toString() {
         return super.toString()
index d3eb5173b7d67cbbda482eb93c4882d0dd5b8e36..953ebc24749253f8529beabd5002a8cad47eab5c 100644 (file)
@@ -6,6 +6,7 @@ import com.ibm.icu.impl.FormattedStringBuilder;
 import com.ibm.icu.impl.IllegalIcuArgumentException;
 import com.ibm.icu.impl.StandardPlural;
 import com.ibm.icu.impl.number.CompactData.CompactType;
+import com.ibm.icu.impl.number.AffixPatternProvider;
 import com.ibm.icu.impl.number.ConstantAffixModifier;
 import com.ibm.icu.impl.number.DecimalQuantity;
 import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
@@ -361,7 +362,11 @@ class NumberFormatterImpl {
         // Middle modifier (patterns, positive/negative, currency symbols, percent)
         // The default middle modifier is weak (thus the false argument).
         MutablePatternModifier patternMod = new MutablePatternModifier(false);
-        patternMod.setPatternInfo((macros.affixProvider != null) ? macros.affixProvider : patternInfo, null);
+        AffixPatternProvider affixProvider =
+            (macros.affixProvider != null)
+                ? macros.affixProvider
+                : patternInfo;
+        patternMod.setPatternInfo(affixProvider, null);
         boolean approximately = (macros.approximately != null) ? macros.approximately : false;
         patternMod.setPatternAttributes(micros.sign, isPermille, approximately);
         if (patternMod.needsPlurals()) {
@@ -378,6 +383,11 @@ class NumberFormatterImpl {
             immPatternMod = patternMod.createImmutable();
         }
 
+        // currencyAsDecimal
+        if (affixProvider.currencyAsDecimal()) {
+            micros.currencyAsDecimal = patternMod.getCurrencySymbolForUnitWidth();
+        }
+
         // Outer modifier (CLDR units and currency long names)
         if (isCldrUnit) {
             String unitDisplayCase = null;
@@ -513,10 +523,24 @@ class NumberFormatterImpl {
             // Add the decimal point
             if (quantity.getLowerDisplayMagnitude() < 0
                     || micros.decimal == DecimalSeparatorDisplay.ALWAYS) {
-                length += string.insert(length + index,
-                        micros.useCurrency ? micros.symbols.getMonetaryDecimalSeparatorString()
-                                : micros.symbols.getDecimalSeparatorString(),
+                if (micros.currencyAsDecimal != null) {
+                    // Note: This unconditionally substitutes the standard short symbol.
+                    // TODO: Should we support narrow or other variants?
+                    length += string.insert(
+                        length + index,
+                        micros.currencyAsDecimal,
+                        NumberFormat.Field.CURRENCY);
+                } else if (micros.useCurrency) {
+                    length += string.insert(
+                        length + index,
+                        micros.symbols.getMonetaryDecimalSeparatorString(),
                         NumberFormat.Field.DECIMAL_SEPARATOR);
+                } else {
+                    length += string.insert(
+                        length + index,
+                        micros.symbols.getDecimalSeparatorString(),
+                        NumberFormat.Field.DECIMAL_SEPARATOR);
+                }
             }
 
             // Add the fraction digits
index 667e2951c91300db101bd0168b8a0ae93a8cb889..c076dc1a26123c1a318456452d49ae803f1dbdfd 100644 (file)
@@ -2504,6 +2504,7 @@ public synchronized void setParseStrictMode(ParseMode parseMode) {
     boolean useCurrency = ((tprops.getCurrency() != null)
             || tprops.getCurrencyPluralInfo() != null
             || tprops.getCurrencyUsage() != null
+            || tprops.getCurrencyAsDecimal()
             || AffixUtils.hasCurrencySymbols(tprops.getPositivePrefixPattern())
             || AffixUtils.hasCurrencySymbols(tprops.getPositiveSuffixPattern())
             || AffixUtils.hasCurrencySymbols(tprops.getNegativePrefixPattern())
index 4489446c53eb4e65ec67066aecf3839bfdc3f847..bbfdf66659f5b03012ca99031726264d5d532b07 100644 (file)
@@ -6938,4 +6938,32 @@ public class NumberFormatTest extends TestFmwk {
         df.parse(input.toString());
         // Should not hang
     }
+
+    @Test
+    public void Test21556_CurrencyAsDecimal() {
+        {
+            DecimalFormat df = new DecimalFormat("a0¤00b");
+            df.setCurrency(Currency.getInstance("EUR"));
+            StringBuffer result = new StringBuffer();
+            FieldPosition fp = new FieldPosition(NumberFormat.Field.CURRENCY);
+            df.format(3.141, result, fp);
+            assertEquals("Basic test: format", "a3€14b", result.toString());
+            assertEquals("Basic test: toPattern", "a0¤00b", df.toPattern());
+            assertEquals("Basic test: field position begin", 2, fp.getBeginIndex());
+            assertEquals("Basic test: field position end", 3, fp.getEndIndex());
+        }
+
+        {
+            NumberFormat nf = NumberFormat.getCurrencyInstance(new ULocale("en-GB"));
+            DecimalFormat df = (DecimalFormat) nf;
+            df.applyPattern("a0¤00b");
+            StringBuffer result = new StringBuffer();
+            FieldPosition fp = new FieldPosition(NumberFormat.Field.CURRENCY);
+            df.format(3.141, result, fp);
+            assertEquals("Via applyPattern: format", "a3£14b", result.toString());
+            assertEquals("Via applyPattern: toPattern", "a0¤00b", df.toPattern());
+            assertEquals("Via applyPattern: field position begin", 2, fp.getBeginIndex());
+            assertEquals("Via applyPattern: field position end", 3, fp.getEndIndex());
+        }
+    }
 }
index ffd3283482e16fa0ccbeb542284f05f8fd3ef055..96a1a3a6f0c2e018b7de90f83cba759903d8c3dd 100644 (file)
@@ -7,10 +7,17 @@ import static org.junit.Assert.fail;
 
 import org.junit.Test;
 
+import com.ibm.icu.dev.test.format.FormattedValueTest;
 import com.ibm.icu.impl.number.DecimalFormatProperties;
+import com.ibm.icu.impl.number.MacroProps;
 import com.ibm.icu.impl.number.PatternStringParser;
 import com.ibm.icu.impl.number.PatternStringUtils;
+import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo;
+import com.ibm.icu.number.FormattedNumber;
+import com.ibm.icu.number.LocalizedNumberFormatter;
+import com.ibm.icu.number.NumberFormatter;
 import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.ULocale;
 
 /** @author sffc */
@@ -47,6 +54,9 @@ public class PatternStringTest {
                 { "0E0", "0E0" },
                 { "#00E00", "#00E00" },
                 { "#,##0", "#,##0" },
+                { "0¤", "0¤"},
+                { "0¤a", "0¤a"},
+                { "0¤00", "0¤00"},
                 { "#;#", "0;0" },
                 { "#;-#", "0" }, // ignore a negative prefix pattern of '-' since that is the default
                 { "pp#,000;(#)", "pp#,000;(#,000)" },
@@ -127,4 +137,30 @@ public class PatternStringTest {
         DecimalFormatProperties actual = PatternStringParser.parseToProperties("0;");
         assertEquals("Should not consume negative subpattern", expected, actual);
     }
+
+    @Test
+    public void testCurrencyDecimal() {
+        // Manually create a NumberFormatter from a specific pattern
+        ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo("a0¤00b");
+        MacroProps macros = new MacroProps();
+        macros.unit = Currency.getInstance("EUR");
+        macros.affixProvider = patternInfo;
+        LocalizedNumberFormatter nf = NumberFormatter.with().macros(macros).locale(ULocale.ROOT);
+    
+        // Test that the output is as expected
+        FormattedNumber fn = nf.format(3.14);
+        assertEquals("Should substitute currency symbol", "a3€14b", fn.toString());
+    
+        // Test field positions
+        Object[][] expectedFieldPositions = new Object[][] {
+                {com.ibm.icu.text.NumberFormat.Field.INTEGER, 1, 2},
+                {com.ibm.icu.text.NumberFormat.Field.CURRENCY, 2, 3},
+                {com.ibm.icu.text.NumberFormat.Field.FRACTION, 3, 5}};
+        FormattedValueTest.checkFormattedValue(
+            "Currency as decimal basic field positions",
+            fn,
+            "a3€14b",
+            expectedFieldPositions
+        );
+    }
 }