From: Shane F. Carr Date: Tue, 21 Sep 2021 09:01:45 +0000 (+0000) Subject: ICU-21556 Support currency as decimal separator in patterns X-Git-Tag: release-70-rc~20 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=f39cf84d628cf5e1916268cedd374ac0f05add1b;p=icu ICU-21556 Support currency as decimal separator in patterns See #1711 --- diff --git a/icu4c/source/i18n/decimfmt.cpp b/icu4c/source/i18n/decimfmt.cpp index 560fd8bd956..bca33366792 100644 --- a/icu4c/source/i18n/decimfmt.cpp +++ b/icu4c/source/i18n/decimfmt.cpp @@ -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) || diff --git a/icu4c/source/i18n/number_currencysymbols.cpp b/icu4c/source/i18n/number_currencysymbols.cpp index 9208427904c..da1812f49f0 100644 --- a/icu4c/source/i18n/number_currencysymbols.cpp +++ b/icu4c/source/i18n/number_currencysymbols.cpp @@ -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); } } diff --git a/icu4c/source/i18n/number_decimfmtprops.cpp b/icu4c/source/i18n/number_decimfmtprops.cpp index 8b2233c0556..7fa58bbc7ab 100644 --- a/icu4c/source/i18n/number_decimfmtprops.cpp +++ b/icu4c/source/i18n/number_decimfmtprops.cpp @@ -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; diff --git a/icu4c/source/i18n/number_decimfmtprops.h b/icu4c/source/i18n/number_decimfmtprops.h index 0ace241adae..5f72f649842 100644 --- a/icu4c/source/i18n/number_decimfmtprops.h +++ b/icu4c/source/i18n/number_decimfmtprops.h @@ -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; diff --git a/icu4c/source/i18n/number_formatimpl.cpp b/icu4c/source/i18n/number_formatimpl.cpp index a57af93b3af..96e3e9e7c69 100644 --- a/icu4c/source/i18n/number_formatimpl.cpp +++ b/icu4c/source/i18n/number_formatimpl.cpp @@ -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(fPatternInfo.getAlias()), - kUndefinedField); + const AffixPatternProvider* affixProvider = + macros.affixProvider != nullptr + ? macros.affixProvider + : static_cast(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 diff --git a/icu4c/source/i18n/number_formatimpl.h b/icu4c/source/i18n/number_formatimpl.h index 9829edf716c..d7be1468b6d 100644 --- a/icu4c/source/i18n/number_formatimpl.h +++ b/icu4c/source/i18n/number_formatimpl.h @@ -115,12 +115,6 @@ class NumberFormatterImpl : public UMemory { LocalPointer fLongNameMultiplexer; LocalPointer fCompactHandler; - // Value objects possibly used by the number formatting pipeline: - struct Warehouse { - CurrencySymbols fCurrencySymbols; - } fWarehouse; - - NumberFormatterImpl(const MacroProps ¯os, bool safe, UErrorCode &status); MicroProps& preProcessUnsafe(DecimalQuantity &inValue, UErrorCode &status); diff --git a/icu4c/source/i18n/number_mapper.cpp b/icu4c/source/i18n/number_mapper.cpp index e2a0d284b7c..2d4d47a094d 100644 --- a/icu4c/source/i18n/number_mapper.cpp +++ b/icu4c/source/i18n/number_mapper.cpp @@ -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 */ diff --git a/icu4c/source/i18n/number_mapper.h b/icu4c/source/i18n/number_mapper.h index 9ecd776b3b4..8879b7a94ea 100644 --- a/icu4c/source/i18n/number_mapper.h +++ b/icu4c/source/i18n/number_mapper.h @@ -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]; diff --git a/icu4c/source/i18n/number_microprops.h b/icu4c/source/i18n/number_microprops.h index a18d5fc470e..c34e7c17e97 100644 --- a/icu4c/source/i18n/number_microprops.h +++ b/icu4c/source/i18n/number_microprops.h @@ -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. diff --git a/icu4c/source/i18n/number_patternmodifier.cpp b/icu4c/source/i18n/number_patternmodifier.cpp index c35620a08b6..b6543b262b4 100644 --- a/icu4c/source/i18n/number_patternmodifier.cpp +++ b/icu4c/source/i18n/number_patternmodifier.cpp @@ -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; diff --git a/icu4c/source/i18n/number_patternmodifier.h b/icu4c/source/i18n/number_patternmodifier.h index 0e2a2b557ea..4f825e1ed21 100644 --- a/icu4c/source/i18n/number_patternmodifier.h +++ b/icu4c/source/i18n/number_patternmodifier.h @@ -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: diff --git a/icu4c/source/i18n/number_patternstring.cpp b/icu4c/source/i18n/number_patternstring.cpp index c7f9e0e2d1e..31b2b38506d 100644 --- a/icu4c/source/i18n/number_patternstring.cpp +++ b/icu4c/source/i18n/number_patternstring.cpp @@ -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; diff --git a/icu4c/source/i18n/number_patternstring.h b/icu4c/source/i18n/number_patternstring.h index bf6b1adc2ec..94afda37229 100644 --- a/icu4c/source/i18n/number_patternstring.h +++ b/icu4c/source/i18n/number_patternstring.h @@ -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. diff --git a/icu4c/source/i18n/number_types.h b/icu4c/source/i18n/number_types.h index 6a6b3edaac5..84846efb924 100644 --- a/icu4c/source/i18n/number_types.h +++ b/icu4c/source/i18n/number_types.h @@ -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; }; diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index 309a42ae732..f5a4ba75f1b 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -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; diff --git a/icu4c/source/test/intltest/numbertest_patternstring.cpp b/icu4c/source/test/intltest/numbertest_patternstring.cpp index b89148df00d..0bfd58016ff 100644 --- a/icu4c/source/test/intltest/numbertest_patternstring.cpp +++ b/icu4c/source/test/intltest/numbertest_patternstring.cpp @@ -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 */ diff --git a/icu4c/source/test/intltest/numfmtst.cpp b/icu4c/source/test/intltest/numfmtst.cpp index 34e81d473a6..011cc32dd34 100644 --- a/icu4c/source/test/intltest/numfmtst.cpp +++ b/icu4c/source/test/intltest/numfmtst.cpp @@ -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 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 nf(NumberFormat::createCurrencyInstance("en-GB", status)); + DecimalFormat* df = static_cast(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 */ diff --git a/icu4c/source/test/intltest/numfmtst.h b/icu4c/source/test/intltest/numfmtst.h index 8af7d6e37ea..abd828d644b 100644 --- a/icu4c/source/test/intltest/numfmtst.h +++ b/icu4c/source/test/intltest/numfmtst.h @@ -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); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternProvider.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternProvider.java index 1b805ae0695..4cf1e086de6 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternProvider.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternProvider.java @@ -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(); } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencyPluralInfoAffixProvider.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencyPluralInfoAffixProvider.java index 8963608d6a5..9544a2e0eb2 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencyPluralInfoAffixProvider.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencyPluralInfoAffixProvider.java @@ -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(); + } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalFormatProperties.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalFormatProperties.java index 10a358143b4..d38af94ed6c 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalFormatProperties.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalFormatProperties.java @@ -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 diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java index 3b2b76d7f01..1fb0a61e0f5 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java @@ -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; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java index 14782921340..ca0661f3d6e 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java @@ -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); + } + } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternStringParser.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternStringParser.java index 43c54db3688..0253035abbe 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternStringParser.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternStringParser.java @@ -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); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternStringUtils.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternStringUtils.java index ec659fa82ff..427177296d0 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternStringUtils.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternStringUtils.java @@ -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; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PropertiesAffixPatternProvider.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PropertiesAffixPatternProvider.java index 870007ac34a..9486fe37e1f 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PropertiesAffixPatternProvider.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PropertiesAffixPatternProvider.java @@ -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() diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java index d3eb5173b7d..953ebc24749 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java @@ -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 diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java index 667e2951c91..c076dc1a261 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java @@ -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()) diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java index 4489446c53e..bbfdf66659f 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java @@ -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()); + } + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java index ffd3283482e..96a1a3a6f0c 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java @@ -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 + ); + } }