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;
}
!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) ||
if (symbol == isoCode) {
return UnicodeString(isoCode, 3);
} else {
- return UnicodeString(TRUE, symbol, symbolLen);
+ return UnicodeString(true, symbol, symbolLen);
}
}
if (symbol == isoCode) {
return UnicodeString(isoCode, 3);
} else {
- return UnicodeString(TRUE, symbol, symbolLen);
+ return UnicodeString(true, symbol, symbolLen);
}
}
decimalPatternMatchRequired = false;
decimalSeparatorAlwaysShown = false;
exponentSignAlwaysShown = false;
+ currencyAsDecimal = false;
formatFailIfMoreThanMaxDigits = false;
formatWidth = -1;
groupingSize = -1;
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;
bool decimalPatternMatchRequired;
bool decimalSeparatorAlwaysShown;
bool exponentSignAlwaysShown;
+ bool currencyAsDecimal;
bool formatFailIfMoreThanMaxDigits; // ICU4C-only
int32_t formatWidth;
int32_t groupingSize;
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(
return nullptr;
}
+ // currencyAsDecimal
+ if (affixProvider->currencyAsDecimal()) {
+ fMicros.currencyAsDecimal = patternModifier->getCurrencySymbolForUnitWidth(status);
+ }
+
// Outer modifier (CLDR units and currency long names)
if (isCldrUnit) {
const char *unitDisplayCase = "";
// 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
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 ¯os, bool safe, UErrorCode &status);
MicroProps& preProcessUnsafe(DecimalQuantity &inValue, UErrorCode &status);
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 {
return true;
}
+bool PropertiesAffixPatternProvider::currencyAsDecimal() const {
+ return fCurrencyAsDecimal;
+}
+
void CurrencyPluralInfoAffixProvider::setTo(const CurrencyPluralInfo& cpi,
const DecimalFormatProperties& properties,
return affixesByPlural[StandardPlural::OTHER].hasBody();
}
+bool CurrencyPluralInfoAffixProvider::currencyAsDecimal() const {
+ return affixesByPlural[StandardPlural::OTHER].currencyAsDecimal();
+}
+
#endif /* #if !UCONFIG_NO_FORMATTING */
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
bool hasBody() const U_OVERRIDE;
+ bool currencyAsDecimal() const U_OVERRIDE;
+
private:
PropertiesAffixPatternProvider affixesByPlural[StandardPlural::COUNT];
#include "number_roundingutils.h"
#include "decNumber.h"
#include "charstr.h"
+#include "util.h"
U_NAMESPACE_BEGIN namespace number {
namespace impl {
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.
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:
}
}
+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;
*/
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:
return positive.integerTotal > 0;
}
+bool ParsedPatternInfo::currencyAsDecimal() const {
+ return positive.hasCurrencyDecimal;
+}
+
/////////////////////////////////////////////////////
/// BEGIN RECURSIVE DESCENT PARSER IMPLEMENTATION ///
/////////////////////////////////////////////////////
}
}
+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;
}
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; }
}
}
properties.decimalSeparatorAlwaysShown = false;
}
+ // Persist the currency as decimal separator
+ properties.currencyAsDecimal = positive.hasCurrencyDecimal;
+
// Scientific notation settings
if (positive.exponentZeros > 0) {
properties.exponentSignAlwaysShown = positive.exponentHasPlusSign;
}
// Decimal separator
if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) {
- sb.append(u'.');
+ if (properties.currencyAsDecimal) {
+ sb.append(u'¤');
+ } else {
+ sb.append(u'.');
+ }
}
if (!useGrouping) {
continue;
bool hasPercentSign = false;
bool hasPerMilleSign = false;
bool hasCurrencySign = false;
+ bool hasCurrencyDecimal = false;
bool hasMinusSign = false;
bool hasPlusSign = false;
bool hasBody() const U_OVERRIDE;
+ bool currencyAsDecimal() const U_OVERRIDE;
+
private:
struct U_I18N_API ParserState {
const UnicodeString& pattern; // reference to the parent
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.
* 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;
};
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;
TESTCASE_AUTO(testToPatternSimple);
TESTCASE_AUTO(testExceptionOnInvalid);
TESTCASE_AUTO(testBug13117);
+ TESTCASE_AUTO(testCurrencyDecimal);
TESTCASE_AUTO_END;
}
{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"},
assertSuccess(input, status);
UnicodeString actual = PatternStringUtils::propertiesToPatternString(properties, status);
assertEquals(input, output, actual);
+ status = U_ZERO_ERROR;
}
}
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 */
TESTCASE_AUTO(Test20425_FractionWithIntegerIncrement);
TESTCASE_AUTO(Test21232_ParseTimeout);
TESTCASE_AUTO(Test10997_FormatCurrency);
+ TESTCASE_AUTO(Test21556_CurrencyAsDecimal);
TESTCASE_AUTO_END;
}
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);
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 */
void Test20425_FractionWithIntegerIncrement();
void Test21232_ParseTimeout();
void Test10997_FormatCurrency();
+ void Test21556_CurrencyAsDecimal();
private:
UBool testFormattableAsUFormattable(const char *file, int line, Formattable &f);
* number instead of rendering the number.
*/
public boolean hasBody();
+
+ /**
+ * True if the currency symbol should replace the decimal separator.
+ */
+ public boolean currencyAsDecimal();
}
public boolean hasBody() {
return affixesByPlural[StandardPlural.OTHER.ordinal()].hasBody();
}
+
+ @Override
+ public boolean currencyAsDecimal() {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].currencyAsDecimal();
+ }
}
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;
decimalPatternMatchRequired = false;
decimalSeparatorAlwaysShown = false;
exponentSignAlwaysShown = false;
+ currencyAsDecimal = false;
formatWidth = -1;
groupingSize = -1;
groupingUsed = true;
decimalPatternMatchRequired = other.decimalPatternMatchRequired;
decimalSeparatorAlwaysShown = other.decimalSeparatorAlwaysShown;
exponentSignAlwaysShown = other.exponentSignAlwaysShown;
+ currencyAsDecimal = other.currencyAsDecimal;
formatWidth = other.formatWidth;
groupingSize = other.groupingSize;
groupingUsed = other.groupingUsed;
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);
hashCode ^= _hashCodeHelper(decimalPatternMatchRequired);
hashCode ^= _hashCodeHelper(decimalSeparatorAlwaysShown);
hashCode ^= _hashCodeHelper(exponentSignAlwaysShown);
+ hashCode ^= _hashCodeHelper(currencyAsDecimal);
hashCode ^= _hashCodeHelper(formatWidth);
hashCode ^= _hashCodeHelper(groupingSize);
hashCode ^= _hashCodeHelper(groupingUsed);
return exponentSignAlwaysShown;
}
+ public boolean getCurrencyAsDecimal() {
+ return currencyAsDecimal;
+ }
+
public int getFormatWidth() {
return formatWidth;
}
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
public boolean useCurrency;
public String gender;
+ // Currency symbol to be used as the decimal separator
+ public String currencyAsDecimal;
+
// Internal fields:
private final boolean immutable;
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:
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);
+ }
+ }
}
public boolean hasBody() {
return positive.integerTotal > 0;
}
+
+ @Override
+ public boolean currencyAsDecimal() {
+ return positive.hasCurrencyDecimal;
+ }
}
public static class ParsedSubpatternInfo {
public boolean hasPercentSign = false;
public boolean hasPerMilleSign = false;
public boolean hasCurrencySign = false;
+ public boolean hasCurrencyDecimal = false;
public boolean hasMinusSign = false;
public boolean hasPlusSign = false;
this.offset = 0;
}
+ /** Returns the next code point, or -1 if string is too short. */
int peek() {
if (offset == pattern.length()) {
return -1;
}
}
+ /** 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);
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);
}
}
properties.setDecimalSeparatorAlwaysShown(false);
}
+ // Persist the currency as decimal separator
+ properties.setCurrencyAsDecimal(positive.hasCurrencyDecimal);
+
// Scientific notation settings
if (positive.exponentZeros > 0) {
properties.setExponentSignAlwaysShown(positive.exponentHasPlusSign);
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);
}
// Decimal separator
if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) {
- sb.append('.');
+ if (currencyAsDecimal) {
+ sb.append('¤');
+ } else {
+ sb.append('.');
+ }
}
if (!useGrouping) {
continue;
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) {
AffixUtils.hasCurrencySymbols(ppp) ||
AffixUtils.hasCurrencySymbols(psp) ||
AffixUtils.hasCurrencySymbols(npp) ||
- AffixUtils.hasCurrencySymbols(nsp));
+ AffixUtils.hasCurrencySymbols(nsp) ||
+ properties.getCurrencyAsDecimal());
+
+ currencyAsDecimal = properties.getCurrencyAsDecimal();
}
@Override
return true;
}
+ @Override
+ public boolean currencyAsDecimal() {
+ return currencyAsDecimal;
+ }
+
@Override
public String toString() {
return super.toString()
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;
// 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()) {
immPatternMod = patternMod.createImmutable();
}
+ // currencyAsDecimal
+ if (affixProvider.currencyAsDecimal()) {
+ micros.currencyAsDecimal = patternMod.getCurrencySymbolForUnitWidth();
+ }
+
// Outer modifier (CLDR units and currency long names)
if (isCldrUnit) {
String unitDisplayCase = null;
// 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
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())
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());
+ }
+ }
}
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 */
{ "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)" },
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
+ );
+ }
}