ICU-21556 Support currency as decimal separator in patterns

See #1711
This commit is contained in:
Shane F. Carr 2021-09-21 09:01:45 +00:00
parent fd5c76edbd
commit f39cf84d62
30 changed files with 450 additions and 76 deletions

View 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) ||

View 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);
}
}

View 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;

View 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;

View 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

View 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);

View 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 */

View 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];

View 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.

View 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;

View 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:

View 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;

View 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.

View 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;
};

View 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;

View 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"", u""},
{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 */

View 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 */

View 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);

View 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();
}

View 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();
}
}

View 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

View 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;

View 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);
}
}
}

View 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);

View 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;

View 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()

View 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

View 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())

View 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());
}
}
}

View 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¤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
);
}
}