From e5cc630590b96f5f4362e3200cc729afe088b799 Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Tue, 6 Feb 2018 03:08:17 +0000 Subject: [PATCH] ICU-13568 ICU-13400 ICU-13389 ICU-13075 NumberFormatter assorted fixes: Adding custom pattern support for currencies. Upgrading grouping API. Adding narrow currency symbol support to ICU4C and API. Fixing behavior when pattern does not have a number placeholder. X-SVN-Rev: 40838 --- icu4c/source/common/ucurr.cpp | 27 ++- icu4c/source/common/unicode/ucurr.h | 12 +- icu4c/source/i18n/number_compact.cpp | 7 - icu4c/source/i18n/number_compact.h | 1 - icu4c/source/i18n/number_fluent.cpp | 6 +- icu4c/source/i18n/number_formatimpl.cpp | 107 ++++++--- icu4c/source/i18n/number_grouping.cpp | 60 +++-- icu4c/source/i18n/number_modifiers.cpp | 15 +- icu4c/source/i18n/number_modifiers.h | 21 +- icu4c/source/i18n/number_patternmodifier.cpp | 27 ++- icu4c/source/i18n/number_patternstring.cpp | 4 + icu4c/source/i18n/number_patternstring.h | 2 + icu4c/source/i18n/number_types.h | 7 + icu4c/source/i18n/unicode/numberformatter.h | 215 +++++++++++------ icu4c/source/test/intltest/numbertest.h | 3 + icu4c/source/test/intltest/numbertest_api.cpp | 179 ++++++++++++++- .../test/intltest/numbertest_modifiers.cpp | 10 +- .../intltest/numbertest_patternmodifier.cpp | 37 +++ icu4c/source/test/intltest/numfmtst.cpp | 217 +++++++++++------- icu4c/source/test/intltest/numfmtst.h | 1 + .../src/com/ibm/icu/impl/CurrencyData.java | 1 - .../icu/impl/number/AffixPatternProvider.java | 7 + .../number/ConstantMultiFieldModifier.java | 11 +- .../CurrencyPluralInfoAffixProvider.java | 5 + .../CurrencySpacingEnabledModifier.java | 3 +- .../src/com/ibm/icu/impl/number/Grouper.java | 164 +++++++++++++ .../com/ibm/icu/impl/number/MacroProps.java | 11 +- .../com/ibm/icu/impl/number/MicroProps.java | 1 - .../impl/number/MutablePatternModifier.java | 21 +- .../icu/impl/number/PatternStringParser.java | 5 + .../PropertiesAffixPatternProvider.java | 5 + .../icu/impl/number/parse/DecimalMatcher.java | 2 +- .../impl/number/parse/NumberParserImpl.java | 7 +- .../impl/number/parse/ScientificMatcher.java | 2 +- .../com/ibm/icu/number/CompactNotation.java | 36 ++- .../core/src/com/ibm/icu/number/Grouper.java | 157 ------------- .../com/ibm/icu/number/NumberFormatter.java | 118 +++++++++- .../ibm/icu/number/NumberFormatterImpl.java | 76 +++--- .../icu/number/NumberFormatterSettings.java | 30 +-- .../ibm/icu/number/NumberPropertyMapper.java | 3 +- .../ibm/icu/text/CurrencyDisplayNames.java | 20 +- .../core/src/com/ibm/icu/util/Currency.java | 23 +- .../data/numberformattestspecification.txt | 3 +- .../icu/dev/test/format/NumberFormatTest.java | 27 +++ .../ibm/icu/dev/test/number/ModifierTest.java | 10 +- .../number/MutablePatternModifierTest.java | 26 +++ .../test/number/NumberFormatterApiTest.java | 196 +++++++++++++++- .../ibm/icu/dev/test/util/CurrencyTest.java | 49 +++- 48 files changed, 1459 insertions(+), 518 deletions(-) create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/Grouper.java delete mode 100644 icu4j/main/classes/core/src/com/ibm/icu/number/Grouper.java diff --git a/icu4c/source/common/ucurr.cpp b/icu4c/source/common/ucurr.cpp index a772da9a29..37c3d79e4d 100644 --- a/icu4c/source/common/ucurr.cpp +++ b/icu4c/source/common/ucurr.cpp @@ -17,6 +17,7 @@ #include "unicode/ustring.h" #include "unicode/parsepos.h" #include "ustr_imp.h" +#include "charstr.h" #include "cmemory.h" #include "cstring.h" #include "uassert.h" @@ -28,9 +29,12 @@ #include "uinvchar.h" #include "uresimp.h" #include "ulist.h" +#include "uresimp.h" #include "ureslocs.h" #include "ulocimp.h" +using namespace icu; + //#define UCURR_DEBUG_EQUIV 1 #ifdef UCURR_DEBUG_EQUIV #include "stdio.h" @@ -104,6 +108,7 @@ static const char VAR_DELIM_STR[] = "_"; // Tag for localized display names (symbols) of currencies static const char CURRENCIES[] = "Currencies"; +static const char CURRENCIES_NARROW[] = "Currencies%narrow"; static const char CURRENCYPLURALS[] = "CurrencyPlurals"; static const UChar EUR_STR[] = {0x0045,0x0055,0x0052,0}; @@ -698,7 +703,7 @@ ucurr_getName(const UChar* currency, } int32_t choice = (int32_t) nameStyle; - if (choice < 0 || choice > 1) { + if (choice < 0 || choice > 2) { *ec = U_ILLEGAL_ARGUMENT_ERROR; return 0; } @@ -731,15 +736,19 @@ ucurr_getName(const UChar* currency, const UChar* s = NULL; ec2 = U_ZERO_ERROR; - UResourceBundle* rb = ures_open(U_ICUDATA_CURR, loc, &ec2); + LocalUResourceBundlePointer rb(ures_open(U_ICUDATA_CURR, loc, &ec2)); - rb = ures_getByKey(rb, CURRENCIES, rb, &ec2); - - // Fetch resource with multi-level resource inheritance fallback - rb = ures_getByKeyWithFallback(rb, buf, rb, &ec2); - - s = ures_getStringByIndex(rb, choice, len, &ec2); - ures_close(rb); + if (nameStyle == UCURR_NARROW_SYMBOL_NAME) { + CharString key; + key.append(CURRENCIES_NARROW, ec2); + key.append("/", ec2); + key.append(buf, ec2); + s = ures_getStringByKeyWithFallback(rb.getAlias(), key.data(), len, &ec2); + } else { + ures_getByKey(rb.getAlias(), CURRENCIES, rb.getAlias(), &ec2); + ures_getByKeyWithFallback(rb.getAlias(), buf, rb.getAlias(), &ec2); + s = ures_getStringByIndex(rb.getAlias(), choice, len, &ec2); + } // If we've succeeded we're done. Otherwise, try to fallback. // If that fails (because we are already at root) then exit. diff --git a/icu4c/source/common/unicode/ucurr.h b/icu4c/source/common/unicode/ucurr.h index 1abb3b22e9..e3831bba15 100644 --- a/icu4c/source/common/unicode/ucurr.h +++ b/icu4c/source/common/unicode/ucurr.h @@ -102,7 +102,17 @@ typedef enum UCurrNameStyle { * currency, such as "US Dollar" for USD. * @stable ICU 2.6 */ - UCURR_LONG_NAME + UCURR_LONG_NAME, + + /** + * Selector for getName() indicating the narrow currency symbol. + * The narrow currency symbol is similar to the regular currency + * symbol, but it always takes the shortest form: for example, + * "$" instead of "US$" for USD in en-CA. + * + * @draft ICU 61 + */ + UCURR_NARROW_SYMBOL_NAME } UCurrNameStyle; #if !UCONFIG_NO_SERVICE diff --git a/icu4c/source/i18n/number_compact.cpp b/icu4c/source/i18n/number_compact.cpp index 8ceee1378b..cc0d8fd2a2 100644 --- a/icu4c/source/i18n/number_compact.cpp +++ b/icu4c/source/i18n/number_compact.cpp @@ -262,7 +262,6 @@ void CompactHandler::precomputeAllModifiers(MutablePatternModifier &buildReferen buildReference.setPatternInfo(&patternInfo); info.mod = buildReference.createImmutable(status); if (U_FAILURE(status)) { return; } - info.numDigits = patternInfo.positive.integerTotal; info.patternString = patternString; } } @@ -286,7 +285,6 @@ void CompactHandler::processQuantity(DecimalQuantity &quantity, MicroProps &micr StandardPlural::Form plural = quantity.getStandardPlural(rules); const UChar *patternString = data.getPattern(magnitude, plural); - int numDigits = -1; if (patternString == nullptr) { // Use the default (non-compact) modifier. // No need to take any action. @@ -299,7 +297,6 @@ void CompactHandler::processQuantity(DecimalQuantity &quantity, MicroProps &micr const CompactModInfo &info = precomputedMods[i]; if (u_strcmp(patternString, info.patternString) == 0) { info.mod->applyToMicros(micros, quantity); - numDigits = info.numDigits; break; } } @@ -313,12 +310,8 @@ void CompactHandler::processQuantity(DecimalQuantity &quantity, MicroProps &micr PatternParser::parseToPatternInfo(UnicodeString(patternString), patternInfo, status); static_cast(const_cast(micros.modMiddle)) ->setPatternInfo(&patternInfo); - numDigits = patternInfo.positive.integerTotal; } - // FIXME: Deal with numDigits == 0 (Awaiting a test case) - (void)numDigits; - // We already performed rounding. Do not perform it again. micros.rounding = Rounder::constructPassThrough(); } diff --git a/icu4c/source/i18n/number_compact.h b/icu4c/source/i18n/number_compact.h index 2344abf535..f7adf36416 100644 --- a/icu4c/source/i18n/number_compact.h +++ b/icu4c/source/i18n/number_compact.h @@ -52,7 +52,6 @@ class CompactData : public MultiplierProducer { struct CompactModInfo { const ImmutablePatternModifier *mod; const UChar* patternString; - int32_t numDigits; }; class CompactHandler : public MicroPropsGenerator, public UMemory { diff --git a/icu4c/source/i18n/number_fluent.cpp b/icu4c/source/i18n/number_fluent.cpp index 5e59c4c14e..3be3401ef3 100644 --- a/icu4c/source/i18n/number_fluent.cpp +++ b/icu4c/source/i18n/number_fluent.cpp @@ -73,9 +73,11 @@ Derived NumberFormatterSettings::rounding(const Rounder &rounder) const } template -Derived NumberFormatterSettings::grouping(const Grouper &grouper) const { +Derived NumberFormatterSettings::grouping(const UGroupingStrategy &strategy) const { Derived copy(*this); - copy.fMacros.grouper = grouper; + // NOTE: This is slightly different than how the setting is stored in Java + // because we want to put it on the stack. + copy.fMacros.grouper = Grouper::forStrategy(strategy); return copy; } diff --git a/icu4c/source/i18n/number_formatimpl.cpp b/icu4c/source/i18n/number_formatimpl.cpp index e2fc4f20be..bc96cb15da 100644 --- a/icu4c/source/i18n/number_formatimpl.cpp +++ b/icu4c/source/i18n/number_formatimpl.cpp @@ -17,6 +17,8 @@ #include "unicode/dcfmtsym.h" #include "number_scientific.h" #include "number_compact.h" +#include "uresimp.h" +#include "ureslocs.h" using namespace icu; using namespace icu::number; @@ -88,6 +90,37 @@ const char16_t *getPatternForStyle(const Locale &locale, const char *nsName, Cld return pattern; } +struct CurrencyFormatInfoResult { + bool exists; + const char16_t* pattern; + const char16_t* decimalSeparator; + const char16_t* groupingSeparator; +}; +CurrencyFormatInfoResult getCurrencyFormatInfo(const Locale& locale, const char* isoCode, UErrorCode& status) { + // TODO: Load this data in a centralized location like ICU4J? + // TODO: Parts of this same data are loaded in dcfmtsym.cpp; should clean up. + CurrencyFormatInfoResult result = { false, nullptr, nullptr, nullptr }; + if (U_FAILURE(status)) return result; + CharString key; + key.append("Currencies/", status); + key.append(isoCode, status); + UErrorCode localStatus = status; + LocalUResourceBundlePointer bundle(ures_open(U_ICUDATA_CURR, locale.getName(), &localStatus)); + ures_getByKeyWithFallback(bundle.getAlias(), key.data(), bundle.getAlias(), &localStatus); + if (U_SUCCESS(localStatus) && ures_getSize(bundle.getAlias())>2) { // the length is 3 if more data is present + ures_getByIndex(bundle.getAlias(), 2, bundle.getAlias(), &localStatus); + int32_t dummy; + result.exists = true; + result.pattern = ures_getStringByIndex(bundle.getAlias(), 0, &dummy, &localStatus); + result.decimalSeparator = ures_getStringByIndex(bundle.getAlias(), 1, &dummy, &localStatus); + result.groupingSeparator = ures_getStringByIndex(bundle.getAlias(), 2, &dummy, &localStatus); + status = localStatus; + } else if (localStatus != U_MISSING_RESOURCE_ERROR) { + status = localStatus; + } + return result; +} + inline bool unitIsCurrency(const MeasureUnit &unit) { return uprv_strcmp("currency", unit.getType()) == 0; } @@ -186,29 +219,7 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps ¯os, bool safe, } const char *nsName = U_SUCCESS(status) ? ns->getName() : "latn"; - // Load and parse the pattern string. It is used for grouping sizes and affixes only. - CldrPatternStyle patternStyle; - if (isPercent || isPermille) { - patternStyle = CLDR_PATTERN_STYLE_PERCENT; - } else if (!isCurrency || unitWidth == UNUM_UNIT_WIDTH_FULL_NAME) { - patternStyle = CLDR_PATTERN_STYLE_DECIMAL; - } else if (isAccounting) { - // NOTE: Although ACCOUNTING and ACCOUNTING_ALWAYS are only supported in currencies right now, - // the API contract allows us to add support to other units in the future. - patternStyle = CLDR_PATTERN_STYLE_ACCOUNTING; - } else { - patternStyle = CLDR_PATTERN_STYLE_CURRENCY; - } - const char16_t *pattern = getPatternForStyle(macros.locale, nsName, patternStyle, status); - auto patternInfo = new ParsedPatternInfo(); - fPatternInfo.adoptInstead(patternInfo); - PatternParser::parseToPatternInfo(UnicodeString(pattern), *patternInfo, status); - - ///////////////////////////////////////////////////////////////////////////////////// - /// START POPULATING THE DEFAULT MICROPROPS AND BUILDING THE MICROPROPS GENERATOR /// - ///////////////////////////////////////////////////////////////////////////////////// - - // Symbols + // Resolve the symbols. Do this here because currency may need to customize them. if (macros.symbols.isDecimalFormatSymbols()) { fMicros.symbols = macros.symbols.getDecimalFormatSymbols(); } else { @@ -217,6 +228,50 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps ¯os, bool safe, fSymbols.adoptInstead(fMicros.symbols); } + // Load and parse the pattern string. It is used for grouping sizes and affixes only. + // If we are formatting currency, check for a currency-specific pattern. + const char16_t* pattern = nullptr; + if (isCurrency) { + CurrencyFormatInfoResult info = getCurrencyFormatInfo(macros.locale, currency.getSubtype(), status); + if (info.exists) { + pattern = info.pattern; + // It's clunky to clone an object here, but this code is not frequently executed. + DecimalFormatSymbols* symbols = new DecimalFormatSymbols(*fMicros.symbols); + fMicros.symbols = symbols; + fSymbols.adoptInstead(symbols); + symbols->setSymbol( + DecimalFormatSymbols::ENumberFormatSymbol::kMonetarySeparatorSymbol, + UnicodeString(info.decimalSeparator), + FALSE); + symbols->setSymbol( + DecimalFormatSymbols::ENumberFormatSymbol::kMonetaryGroupingSeparatorSymbol, + UnicodeString(info.groupingSeparator), + FALSE); + } + } + if (pattern == nullptr) { + CldrPatternStyle patternStyle; + if (isPercent || isPermille) { + patternStyle = CLDR_PATTERN_STYLE_PERCENT; + } else if (!isCurrency || unitWidth == UNUM_UNIT_WIDTH_FULL_NAME) { + patternStyle = CLDR_PATTERN_STYLE_DECIMAL; + } else if (isAccounting) { + // NOTE: Although ACCOUNTING and ACCOUNTING_ALWAYS are only supported in currencies right now, + // the API contract allows us to add support to other units in the future. + patternStyle = CLDR_PATTERN_STYLE_ACCOUNTING; + } else { + patternStyle = CLDR_PATTERN_STYLE_CURRENCY; + } + pattern = getPatternForStyle(macros.locale, nsName, patternStyle, status); + } + auto patternInfo = new ParsedPatternInfo(); + fPatternInfo.adoptInstead(patternInfo); + PatternParser::parseToPatternInfo(UnicodeString(pattern), *patternInfo, status); + + ///////////////////////////////////////////////////////////////////////////////////// + /// START POPULATING THE DEFAULT MICROPROPS AND BUILDING THE MICROPROPS GENERATOR /// + ///////////////////////////////////////////////////////////////////////////////////// + // Rounding strategy if (!macros.rounder.isBogus()) { fMicros.rounding = macros.rounder; @@ -234,11 +289,11 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps ¯os, bool safe, fMicros.grouping = macros.grouper; } else if (macros.notation.fType == Notation::NTN_COMPACT) { // Compact notation uses minGrouping by default since ICU 59 - fMicros.grouping = Grouper::minTwoDigits(); + fMicros.grouping = Grouper::forStrategy(UNUM_GROUPING_MIN2); } else { - fMicros.grouping = Grouper::defaults(); + fMicros.grouping = Grouper::forStrategy(UNUM_GROUPING_AUTO); } - fMicros.grouping.setLocaleData(*fPatternInfo); + fMicros.grouping.setLocaleData(*fPatternInfo, macros.locale); // Padding strategy if (!macros.padder.isBogus()) { diff --git a/icu4c/source/i18n/number_grouping.cpp b/icu4c/source/i18n/number_grouping.cpp index 15362825cc..67fd4c9431 100644 --- a/icu4c/source/i18n/number_grouping.cpp +++ b/icu4c/source/i18n/number_grouping.cpp @@ -7,36 +7,70 @@ #include "unicode/numberformatter.h" #include "number_patternstring.h" +#include "uresimp.h" using namespace icu; using namespace icu::number; using namespace icu::number::impl; -Grouper Grouper::defaults() { - return {-2, -2, false}; +namespace { + +int16_t getMinGroupingForLocale(const Locale& locale) { + // TODO: Cache this? + UErrorCode localStatus = U_ZERO_ERROR; + LocalUResourceBundlePointer bundle(ures_open(NULL, locale.getName(), &localStatus)); + int32_t resultLen = 0; + const char16_t* result = ures_getStringByKeyWithFallback( + bundle.getAlias(), + "NumberElements/minimumGroupingDigits", + &resultLen, + &localStatus); + // TODO: Is it safe to assume resultLen == 1? Would locales set minGrouping >= 10? + if (U_FAILURE(localStatus) || resultLen != 1) { + return 1; + } + return result[0] - u'0'; } -Grouper Grouper::minTwoDigits() { - return {-2, -2, true}; } -Grouper Grouper::none() { - return {-1, -1, false}; +Grouper Grouper::forStrategy(UGroupingStrategy grouping) { + switch (grouping) { + case UNUM_GROUPING_OFF: + return {-1, -1, -2}; + case UNUM_GROUPING_AUTO: + return {-2, -2, -2}; + case UNUM_GROUPING_MIN2: + return {-2, -2, -3}; + case UNUM_GROUPING_ON_ALIGNED: + return {-4, -4, 1}; + case UNUM_GROUPING_WESTERN: + return {3, 3, 1}; + default: + U_ASSERT(FALSE); + } } -void Grouper::setLocaleData(const impl::ParsedPatternInfo &patternInfo) { - if (fGrouping1 != -2) { +void Grouper::setLocaleData(const impl::ParsedPatternInfo &patternInfo, const Locale& locale) { + if (fGrouping1 != -2 && fGrouping2 != -4) { return; } - auto grouping1 = static_cast (patternInfo.positive.groupingSizes & 0xffff); - auto grouping2 = static_cast ((patternInfo.positive.groupingSizes >> 16) & 0xffff); - auto grouping3 = static_cast ((patternInfo.positive.groupingSizes >> 32) & 0xffff); + auto grouping1 = static_cast (patternInfo.positive.groupingSizes & 0xffff); + auto grouping2 = static_cast ((patternInfo.positive.groupingSizes >> 16) & 0xffff); + auto grouping3 = static_cast ((patternInfo.positive.groupingSizes >> 32) & 0xffff); if (grouping2 == -1) { - grouping1 = -1; + grouping1 = fGrouping1 == -4 ? (short) 3 : (short) -1; } if (grouping3 == -1) { grouping2 = grouping1; } + if (fMinGrouping == -2) { + fMinGrouping = getMinGroupingForLocale(locale); + } else if (fMinGrouping == -3) { + fMinGrouping = uprv_max(2, getMinGroupingForLocale(locale)); + } else { + // leave fMinGrouping alone + } fGrouping1 = grouping1; fGrouping2 = grouping2; } @@ -49,7 +83,7 @@ bool Grouper::groupAtPosition(int32_t position, const impl::DecimalQuantity &val } position -= fGrouping1; return position >= 0 && (position % fGrouping2) == 0 - && value.getUpperDisplayMagnitude() - fGrouping1 + 1 >= (fMin2 ? 2 : 1); + && value.getUpperDisplayMagnitude() - fGrouping1 + 1 >= fMinGrouping; } #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/number_modifiers.cpp b/icu4c/source/i18n/number_modifiers.cpp index 0162489509..872b97010d 100644 --- a/icu4c/source/i18n/number_modifiers.cpp +++ b/icu4c/source/i18n/number_modifiers.cpp @@ -155,9 +155,15 @@ SimpleModifier::formatAsPrefixSuffix(NumberStringBuilder &result, int32_t startI int32_t ConstantMultiFieldModifier::apply(NumberStringBuilder &output, int leftIndex, int rightIndex, UErrorCode &status) const { - // Insert the suffix first since inserting the prefix will change the rightIndex - int32_t length = output.insert(rightIndex, fSuffix, status); - length += output.insert(leftIndex, fPrefix, status); + int32_t length = output.insert(leftIndex, fPrefix, status); + if (fOverwrite) { + length += output.splice( + leftIndex + length, + rightIndex + length, + UnicodeString(), 0, 0, + UNUM_FIELD_COUNT, status); + } + length += output.insert(rightIndex + length, fSuffix, status); return length; } @@ -177,10 +183,11 @@ bool ConstantMultiFieldModifier::isStrong() const { CurrencySpacingEnabledModifier::CurrencySpacingEnabledModifier(const NumberStringBuilder &prefix, const NumberStringBuilder &suffix, + bool overwrite, bool strong, const DecimalFormatSymbols &symbols, UErrorCode &status) - : ConstantMultiFieldModifier(prefix, suffix, strong) { + : ConstantMultiFieldModifier(prefix, suffix, overwrite, strong) { // Check for currency spacing. Do not build the UnicodeSets unless there is // a currency code point at a boundary. if (prefix.length() > 0 && prefix.fieldAt(prefix.length() - 1) == UNUM_CURRENCY_FIELD) { diff --git a/icu4c/source/i18n/number_modifiers.h b/icu4c/source/i18n/number_modifiers.h index 962d17b574..4762a6f6d3 100644 --- a/icu4c/source/i18n/number_modifiers.h +++ b/icu4c/source/i18n/number_modifiers.h @@ -103,8 +103,15 @@ class U_I18N_API SimpleModifier : public Modifier, public UMemory { */ class U_I18N_API ConstantMultiFieldModifier : public Modifier, public UMemory { public: - ConstantMultiFieldModifier(const NumberStringBuilder &prefix, const NumberStringBuilder &suffix, - bool strong) : fPrefix(prefix), fSuffix(suffix), fStrong(strong) {} + ConstantMultiFieldModifier( + const NumberStringBuilder &prefix, + const NumberStringBuilder &suffix, + bool overwrite, + bool strong) + : fPrefix(prefix), + fSuffix(suffix), + fOverwrite(overwrite), + fStrong(strong) {} int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex, UErrorCode &status) const U_OVERRIDE; @@ -120,6 +127,7 @@ class U_I18N_API ConstantMultiFieldModifier : public Modifier, public UMemory { // value and is treated internally as immutable. NumberStringBuilder fPrefix; NumberStringBuilder fSuffix; + bool fOverwrite; bool fStrong; }; @@ -127,8 +135,13 @@ class U_I18N_API ConstantMultiFieldModifier : public Modifier, public UMemory { class U_I18N_API CurrencySpacingEnabledModifier : public ConstantMultiFieldModifier { public: /** Safe code path */ - CurrencySpacingEnabledModifier(const NumberStringBuilder &prefix, const NumberStringBuilder &suffix, - bool strong, const DecimalFormatSymbols &symbols, UErrorCode &status); + CurrencySpacingEnabledModifier( + const NumberStringBuilder &prefix, + const NumberStringBuilder &suffix, + bool overwrite, + bool strong, + const DecimalFormatSymbols &symbols, + UErrorCode &status); int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex, UErrorCode &status) const U_OVERRIDE; diff --git a/icu4c/source/i18n/number_patternmodifier.cpp b/icu4c/source/i18n/number_patternmodifier.cpp index 0866285e45..e182104c91 100644 --- a/icu4c/source/i18n/number_patternmodifier.cpp +++ b/icu4c/source/i18n/number_patternmodifier.cpp @@ -109,9 +109,9 @@ ConstantMultiFieldModifier *MutablePatternModifier::createConstantModifier(UErro insertPrefix(a, 0, status); insertSuffix(b, 0, status); if (patternInfo->hasCurrencySign()) { - return new CurrencySpacingEnabledModifier(a, b, fStrong, *symbols, status); + return new CurrencySpacingEnabledModifier(a, b, !patternInfo->hasBody(), fStrong, *symbols, status); } else { - return new ConstantMultiFieldModifier(a, b, fStrong); + return new ConstantMultiFieldModifier(a, b, !patternInfo->hasBody(), fStrong); } } @@ -167,9 +167,23 @@ int32_t MutablePatternModifier::apply(NumberStringBuilder &output, int32_t leftI auto nonConstThis = const_cast(this); int32_t prefixLen = nonConstThis->insertPrefix(output, leftIndex, status); int32_t suffixLen = nonConstThis->insertSuffix(output, rightIndex + prefixLen, status); + // If the pattern had no decimal stem body (like #,##0.00), overwrite the value. + int32_t overwriteLen = 0; + if (!patternInfo->hasBody()) { + overwriteLen = output.splice( + leftIndex + prefixLen, rightIndex + prefixLen, + UnicodeString(), 0, 0, UNUM_FIELD_COUNT, + status); + } CurrencySpacingEnabledModifier::applyCurrencySpacing( - output, leftIndex, prefixLen, rightIndex + prefixLen, suffixLen, *symbols, status); - return prefixLen + suffixLen; + output, + leftIndex, + prefixLen, + rightIndex + overwriteLen + prefixLen, + suffixLen, + *symbols, + status); + return prefixLen + overwriteLen + suffixLen; } int32_t MutablePatternModifier::getPrefixLength(UErrorCode &status) const { @@ -234,13 +248,16 @@ UnicodeString MutablePatternModifier::getSymbol(AffixPatternType type) const { } else if (unitWidth == UNumberUnitWidth::UNUM_UNIT_WIDTH_HIDDEN) { return UnicodeString(); } else { + UCurrNameStyle selector = (unitWidth == UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW) + ? UCurrNameStyle::UCURR_NARROW_SYMBOL_NAME + : UCurrNameStyle::UCURR_SYMBOL_NAME; UErrorCode status = U_ZERO_ERROR; UBool isChoiceFormat = FALSE; int32_t symbolLen = 0; const char16_t *symbol = ucurr_getName( currencyCode, symbols->getLocale().getName(), - UCurrNameStyle::UCURR_SYMBOL_NAME, + selector, &isChoiceFormat, &symbolLen, &status); diff --git a/icu4c/source/i18n/number_patternstring.cpp b/icu4c/source/i18n/number_patternstring.cpp index c67e354181..20178824b0 100644 --- a/icu4c/source/i18n/number_patternstring.cpp +++ b/icu4c/source/i18n/number_patternstring.cpp @@ -95,6 +95,10 @@ bool ParsedPatternInfo::containsSymbolType(AffixPatternType type, UErrorCode &st return AffixUtils::containsType(UnicodeStringCharSequence(pattern), type, status); } +bool ParsedPatternInfo::hasBody() const { + return positive.integerTotal > 0; +} + ///////////////////////////////////////////////////// /// BEGIN RECURSIVE DESCENT PARSER IMPLEMENTATION /// ///////////////////////////////////////////////////// diff --git a/icu4c/source/i18n/number_patternstring.h b/icu4c/source/i18n/number_patternstring.h index 6e1bb7f44d..ec44290d66 100644 --- a/icu4c/source/i18n/number_patternstring.h +++ b/icu4c/source/i18n/number_patternstring.h @@ -84,6 +84,8 @@ struct U_I18N_API ParsedPatternInfo : public AffixPatternProvider, public UMemor bool containsSymbolType(AffixPatternType type, UErrorCode &status) const U_OVERRIDE; + bool hasBody() const U_OVERRIDE; + private: struct U_I18N_API ParserState { const UnicodeString &pattern; // reference to the parent diff --git a/icu4c/source/i18n/number_types.h b/icu4c/source/i18n/number_types.h index a404ef9686..e914ef71ac 100644 --- a/icu4c/source/i18n/number_types.h +++ b/icu4c/source/i18n/number_types.h @@ -142,6 +142,13 @@ class U_I18N_API AffixPatternProvider { virtual bool negativeHasMinusSign() const = 0; virtual bool containsSymbolType(AffixPatternType, UErrorCode &) const = 0; + + /** + * True if the pattern has a number placeholder like "0" or "#,##0.00"; false if the pattern does not + * have one. This is used in cases like compact notation, where the pattern replaces the entire + * number instead of rendering the number. + */ + virtual bool hasBody() const = 0; }; /** diff --git a/icu4c/source/i18n/unicode/numberformatter.h b/icu4c/source/i18n/unicode/numberformatter.h index 397181536b..4c4f542b4d 100644 --- a/icu4c/source/i18n/unicode/numberformatter.h +++ b/icu4c/source/i18n/unicode/numberformatter.h @@ -88,10 +88,6 @@ * * *

- * * The narrow format for currencies is not currently supported; this is a known issue that will be fixed in a - * future version. See #11666 for more information. - * - *

* This enum is similar to {@link com.ibm.icu.text.MeasureFormat.FormatWidth}. * * @draft ICU 60 @@ -165,6 +161,97 @@ typedef enum UNumberUnitWidth { UNUM_UNIT_WIDTH_COUNT } UNumberUnitWidth; +/** + * An enum declaring the strategy for when and how to display grouping separators (i.e., the + * separator, often a comma or period, after every 2-3 powers of ten). The choices are several + * pre-built strategies for different use cases that employ locale data whenever possible. Example + * outputs for 1234 and 1234567 in en-IN: + * + *

    + *
  • OFF: 1234 and 12345 + *
  • MIN2: 1234 and 12,34,567 + *
  • AUTO: 1,234 and 12,34,567 + *
  • ON_ALIGNED: 1,234 and 12,34,567 + *
  • WESTERN: 1,234 and 1,234,567 + *
+ * + *

+ * The default is AUTO, which displays grouping separators unless the locale data says that grouping + * is not customary. To force grouping for all numbers greater than 1000 consistently across locales, + * use ON_ALIGNED. On the other hand, to display grouping less frequently than the default, use MIN2 + * or OFF. See the docs of each option for details. + * + *

+ * Note: This enum specifies the strategy for grouping sizes. To set which character to use as the + * grouping separator, use the "symbols" setter. + * + * @draft ICU 61 + */ +typedef enum UGroupingStrategy { + /** + * Do not display grouping separators in any locale. + * + * @draft ICU 61 + */ + UNUM_GROUPING_OFF, + + /** + * Display grouping using locale defaults, except do not show grouping on values smaller than + * 10000 (such that there is a minimum of two digits before the first separator). + * + *

+ * Note that locales may restrict grouping separators to be displayed only on 1 million or + * greater (for example, ee and hu) or disable grouping altogether (for example, bg currency). + * + *

+ * Locale data is used to determine whether to separate larger numbers into groups of 2 + * (customary in South Asia) or groups of 3 (customary in Europe and the Americas). + * + * @draft ICU 61 + */ + UNUM_GROUPING_MIN2, + + /** + * Display grouping using the default strategy for all locales. This is the default behavior. + * + *

+ * Note that locales may restrict grouping separators to be displayed only on 1 million or + * greater (for example, ee and hu) or disable grouping altogether (for example, bg currency). + * + *

+ * Locale data is used to determine whether to separate larger numbers into groups of 2 + * (customary in South Asia) or groups of 3 (customary in Europe and the Americas). + * + * @draft ICU 61 + */ + UNUM_GROUPING_AUTO, + + /** + * Always display the grouping separator on values of at least 1000. + * + *

+ * This option ignores the locale data that restricts or disables grouping, described in MIN2 and + * AUTO. This option may be useful to normalize the alignment of numbers, such as in a + * spreadsheet. + * + *

+ * Locale data is used to determine whether to separate larger numbers into groups of 2 + * (customary in South Asia) or groups of 3 (customary in Europe and the Americas). + * + * @draft ICU 61 + */ + UNUM_GROUPING_ON_ALIGNED, + + /** + * Use the Western defaults: groups of 3 and enabled for all numbers 1000 or greater. Do not use + * locale data for determining the grouping strategy. + * + * @draft ICU 61 + */ + UNUM_GROUPING_WESTERN + +} UGroupingStrategy; + /** * An enum declaring how to denote positive and negative numbers. Example outputs when formatting 123 and -123 in * en-US: @@ -303,7 +390,6 @@ class Rounder; class FractionRounder; class CurrencyRounder; class IncrementRounder; -class Grouper; class IntegerWidth; namespace impl { @@ -1036,53 +1122,6 @@ class U_I18N_API IncrementRounder : public Rounder { friend class Rounder; }; -/** - * @internal This API is a technical preview. It is likely to change in an upcoming release. - */ -class U_I18N_API Grouper : public UMemory { - public: - /** - * @internal This API is a technical preview. It is likely to change in an upcoming release. - */ - static Grouper defaults(); - - /** - * @internal This API is a technical preview. It is likely to change in an upcoming release. - */ - static Grouper minTwoDigits(); - - /** - * @internal This API is a technical preview. It is likely to change in an upcoming release. - */ - static Grouper none(); - - private: - int8_t fGrouping1; // -3 means "bogus"; -2 means "needs locale data"; -1 means "no grouping" - int8_t fGrouping2; - bool fMin2; - - Grouper(int8_t grouping1, int8_t grouping2, bool min2) - : fGrouping1(grouping1), fGrouping2(grouping2), fMin2(min2) {} - - Grouper() : fGrouping1(-3) {}; - - bool isBogus() const { - return fGrouping1 == -3; - } - - /** NON-CONST: mutates the current instance. */ - void setLocaleData(const impl::ParsedPatternInfo &patternInfo); - - bool groupAtPosition(int32_t position, const impl::DecimalQuantity &value) const; - - // To allow MacroProps/MicroProps to initialize empty instances: - friend struct impl::MacroProps; - friend struct impl::MicroProps; - - // To allow NumberFormatterImpl to access isBogus() and perform other operations: - friend class impl::NumberFormatterImpl; -}; - /** * A class that defines the strategy for padding and truncating integers before the decimal separator. * @@ -1252,6 +1291,58 @@ class U_I18N_API SymbolsWrapper : public UMemory { void doCleanup(); }; +/** @internal */ +class U_I18N_API Grouper : public UMemory { + public: + /** @internal */ + static Grouper forStrategy(UGroupingStrategy grouping); + + // Future: static Grouper forProperties(DecimalFormatProperties& properties); + + /** @internal */ + Grouper(int16_t grouping1, int16_t grouping2, int16_t minGrouping) + : fGrouping1(grouping1), fGrouping2(grouping2), fMinGrouping(minGrouping) {} + + private: + /** + * The grouping sizes, with the following special values: + *

    + *
  • -1 = no grouping + *
  • -2 = needs locale data + *
  • -4 = fall back to Western grouping if not in locale + *
+ */ + int16_t fGrouping1; + int16_t fGrouping2; + + /** + * The minimum gropuing size, with the following special values: + *
    + *
  • -2 = needs locale data + *
  • -3 = no less than 2 + *
+ */ + int16_t fMinGrouping; + + Grouper() : fGrouping1(-3) {}; + + bool isBogus() const { + return fGrouping1 == -3; + } + + /** NON-CONST: mutates the current instance. */ + void setLocaleData(const impl::ParsedPatternInfo &patternInfo, const Locale& locale); + + bool groupAtPosition(int32_t position, const impl::DecimalQuantity &value) const; + + // To allow MacroProps/MicroProps to initialize empty instances: + friend struct MacroProps; + friend struct MicroProps; + + // To allow NumberFormatterImpl to access isBogus() and perform other operations: + friend class NumberFormatterImpl; +}; + /** @internal */ class U_I18N_API Padder : public UMemory { public: @@ -1531,8 +1622,6 @@ class U_I18N_API NumberFormatterSettings { */ Derived rounding(const Rounder &rounder) const; -#ifndef U_HIDE_INTERNAL_API - /** * Specifies the grouping strategy to use when formatting numbers. * @@ -1546,25 +1635,21 @@ class U_I18N_API NumberFormatterSettings { * The exact grouping widths will be chosen based on the locale. * *

- * Pass this method the return value of one of the factory methods on {@link Grouper}. For example: + * Pass this method an element from the {@link UGroupingStrategy} enum. For example: * *

-     * NumberFormatter::with().grouping(Grouper::min2())
+     * NumberFormatter::with().grouping(UNUM_GROUPING_MIN2)
      * 
* - * The default is to perform grouping without concern for the minimum grouping digits. + * The default is to perform grouping according to locale data; most locales, but not all locales, + * enable it by default. * - * @param grouper + * @param strategy * The grouping strategy to use. * @return The fluent chain. - * @see Grouper - * @see Notation - * @internal - * @internal ICU 60: This API is technical preview. + * @draft ICU 61 */ - Derived grouping(const Grouper &grouper) const; - -#endif /* U_HIDE_INTERNAL_API */ + Derived grouping(const UGroupingStrategy &strategy) const; /** * Specifies the minimum and maximum number of digits to render before the decimal mark. diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index 614cc96b20..9d4ffb7cef 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -71,6 +71,8 @@ class NumberFormatterApiTest : public IntlTest { CurrencyUnit GBP; CurrencyUnit CZK; CurrencyUnit CAD; + CurrencyUnit ESP; + CurrencyUnit PTE; MeasureUnit METER; MeasureUnit DAY; @@ -139,6 +141,7 @@ class ModifiersTest : public IntlTest { class PatternModifierTest : public IntlTest { public: void testBasic(); + void testPatternWithNoPlaceholder(); void testMutableEqualsImmutable(); void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0); diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp index 1c1487372c..62db705eac 100644 --- a/icu4c/source/test/intltest/numbertest_api.cpp +++ b/icu4c/source/test/intltest/numbertest_api.cpp @@ -22,6 +22,7 @@ NumberFormatterApiTest::NumberFormatterApiTest() NumberFormatterApiTest::NumberFormatterApiTest(UErrorCode &status) : USD(u"USD", status), GBP(u"GBP", status), CZK(u"CZK", status), CAD(u"CAD", status), + ESP(u"ESP", status), PTE(u"PTE", status), FRENCH_SYMBOLS(Locale::getFrench(), status), SWISS_SYMBOLS(Locale("de-CH"), status), MYANMAR_SYMBOLS(Locale("my"), status) { @@ -349,6 +350,9 @@ void NumberFormatterApiTest::notationCompact() { Locale::getEnglish(), 9990000, u"10M"); + + // NOTE: There is no API for compact custom data in C++ + // and thus no "Compact Somali No Figure" test } void NumberFormatterApiTest::unitMeasure() { @@ -608,6 +612,66 @@ void NumberFormatterApiTest::unitCurrency() { Locale::getEnglish(), -9876543.21, u"-£9,876,543.21"); + + // The full currency symbol is not shown in NARROW format. + // NOTE: This example is in the documentation. + assertFormatSingle( + u"Currency Difference between Narrow and Short (Narrow Version)", + NumberFormatter::with().unit(USD).unitWidth(UNUM_UNIT_WIDTH_NARROW), + Locale("en-CA"), + 5.43, + u"$5.43"); + + assertFormatSingle( + u"Currency Difference between Narrow and Short (Short Version)", + NumberFormatter::with().unit(USD).unitWidth(UNUM_UNIT_WIDTH_SHORT), + Locale("en-CA"), + 5.43, + u"US$5.43"); + + assertFormatSingle( + u"Currency-dependent format (Control)", + NumberFormatter::with().unit(USD).unitWidth(UNUM_UNIT_WIDTH_SHORT), + Locale("ca"), + 444444.55, + u"444.444,55 USD"); + + assertFormatSingle( + u"Currency-dependent format (Test)", + NumberFormatter::with().unit(ESP).unitWidth(UNUM_UNIT_WIDTH_SHORT), + Locale("ca"), + 444444.55, + u"₧ 444.445"); + + assertFormatSingle( + u"Currency-dependent symbols (Control)", + NumberFormatter::with().unit(USD).unitWidth(UNUM_UNIT_WIDTH_SHORT), + Locale("pt-PT"), + 444444.55, + u"444 444,55 US$"); + + // NOTE: This is a bit of a hack on CLDR's part. They set the currency symbol to U+200B (zero- + // width space), and they set the decimal separator to the $ symbol. + assertFormatSingle( + u"Currency-dependent symbols (Test Short)", + NumberFormatter::with().unit(PTE).unitWidth(UNUM_UNIT_WIDTH_SHORT), + Locale("pt-PT"), + 444444.55, + u"444,444$55 \u200B"); + + assertFormatSingle( + u"Currency-dependent symbols (Test Narrow)", + NumberFormatter::with().unit(PTE).unitWidth(UNUM_UNIT_WIDTH_NARROW), + Locale("pt-PT"), + 444444.55, + u"444,444$55 PTE"); + + assertFormatSingle( + u"Currency-dependent symbols (Test ISO Code)", + NumberFormatter::with().unit(PTE).unitWidth(UNUM_UNIT_WIDTH_ISO_CODE), + Locale("pt-PT"), + 444444.55, + u"444,444$55 PTE"); } void NumberFormatterApiTest::unitPercent() { @@ -826,6 +890,20 @@ void NumberFormatterApiTest::roundingFractionFigures() { u"0.09", u"0.01", u"0.00"); + + assertFormatSingle( + "FracSig with trailing zeros A", + NumberFormatter::with().rounding(Rounder::fixedFraction(2).withMinDigits(3)), + Locale::getEnglish(), + 0.1, + u"0.10"); + + assertFormatSingle( + "FracSig with trailing zeros B", + NumberFormatter::with().rounding(Rounder::fixedFraction(2).withMinDigits(3)), + Locale::getEnglish(), + 0.0999999, + u"0.10"); } void NumberFormatterApiTest::roundingOther() { @@ -950,7 +1028,7 @@ void NumberFormatterApiTest::roundingOther() { void NumberFormatterApiTest::grouping() { assertFormatDescendingBig( u"Western Grouping", - NumberFormatter::with().grouping(Grouper::defaults()), + NumberFormatter::with().grouping(UNUM_GROUPING_AUTO), Locale::getEnglish(), u"87,650,000", u"8,765,000", @@ -964,7 +1042,7 @@ void NumberFormatterApiTest::grouping() { assertFormatDescendingBig( u"Indic Grouping", - NumberFormatter::with().grouping(Grouper::defaults()), + NumberFormatter::with().grouping(UNUM_GROUPING_AUTO), Locale("en-IN"), u"8,76,50,000", u"87,65,000", @@ -978,7 +1056,7 @@ void NumberFormatterApiTest::grouping() { assertFormatDescendingBig( u"Western Grouping, Wide", - NumberFormatter::with().grouping(Grouper::minTwoDigits()), + NumberFormatter::with().grouping(UNUM_GROUPING_MIN2), Locale::getEnglish(), u"87,650,000", u"8,765,000", @@ -992,7 +1070,7 @@ void NumberFormatterApiTest::grouping() { assertFormatDescendingBig( u"Indic Grouping, Wide", - NumberFormatter::with().grouping(Grouper::minTwoDigits()), + NumberFormatter::with().grouping(UNUM_GROUPING_MIN2), Locale("en-IN"), u"8,76,50,000", u"87,65,000", @@ -1006,7 +1084,7 @@ void NumberFormatterApiTest::grouping() { assertFormatDescendingBig( u"No Grouping", - NumberFormatter::with().grouping(Grouper::none()), + NumberFormatter::with().grouping(UNUM_GROUPING_OFF), Locale("en-IN"), u"87650000", u"8765000", @@ -1017,6 +1095,97 @@ void NumberFormatterApiTest::grouping() { u"87.65", u"8.765", u"0"); + + // NOTE: Hungarian is interesting because it has minimumGroupingDigits=4 in locale data + // If this test breaks due to data changes, find another locale that has minimumGroupingDigits. + assertFormatDescendingBig( + u"Hungarian Grouping", + NumberFormatter::with().grouping(UNUM_GROUPING_AUTO), + Locale("hu"), + u"87 650 000", + u"8 765 000", + u"876500", + u"87650", + u"8765", + u"876,5", + u"87,65", + u"8,765", + u"0"); + + assertFormatDescendingBig( + u"Hungarian Grouping, Min 2", + NumberFormatter::with().grouping(UNUM_GROUPING_MIN2), + Locale("hu"), + u"87 650 000", + u"8 765 000", + u"876500", + u"87650", + u"8765", + u"876,5", + u"87,65", + u"8,765", + u"0"); + + assertFormatDescendingBig( + u"Hungarian Grouping, Always", + NumberFormatter::with().grouping(UNUM_GROUPING_ON_ALIGNED), + Locale("hu"), + u"87 650 000", + u"8 765 000", + u"876 500", + u"87 650", + u"8 765", + u"876,5", + u"87,65", + u"8,765", + u"0"); + + // NOTE: Bulgarian is interesting because it has no grouping in the default currency format. + // If this test breaks due to data changes, find another locale that has no default grouping. + assertFormatDescendingBig( + u"Bulgarian Currency Grouping", + NumberFormatter::with().grouping(UNUM_GROUPING_AUTO).unit(USD), + Locale("bg"), + u"87650000,00 щ.д.", + u"8765000,00 щ.д.", + u"876500,00 щ.д.", + u"87650,00 щ.д.", + u"8765,00 щ.д.", + u"876,50 щ.д.", + u"87,65 щ.д.", + u"8,76 щ.д.", + u"0,00 щ.д."); + + assertFormatDescendingBig( + u"Bulgarian Currency Grouping, Always", + NumberFormatter::with().grouping(UNUM_GROUPING_ON_ALIGNED).unit(USD), + Locale("bg"), + u"87 650 000,00 щ.д.", + u"8 765 000,00 щ.д.", + u"876 500,00 щ.д.", + u"87 650,00 щ.д.", + u"8 765,00 щ.д.", + u"876,50 щ.д.", + u"87,65 щ.д.", + u"8,76 щ.д.", + u"0,00 щ.д."); + + // TODO: Enable this test when macro-setter is available in C++ + // MacroProps macros; + // macros.grouping = Grouper(4, 1, 3); + // assertFormatDescendingBig( + // u"Custom Grouping via Internal API", + // NumberFormatter::with().macros(macros), + // Locale::getEnglish(), + // u"8,7,6,5,0000", + // u"8,7,6,5000", + // u"876500", + // u"87650", + // u"8765", + // u"876.5", + // u"87.65", + // u"8.765", + // u"0"); } void NumberFormatterApiTest::padding() { diff --git a/icu4c/source/test/intltest/numbertest_modifiers.cpp b/icu4c/source/test/intltest/numbertest_modifiers.cpp index 279df757f6..bebb3f8b2b 100644 --- a/icu4c/source/test/intltest/numbertest_modifiers.cpp +++ b/icu4c/source/test/intltest/numbertest_modifiers.cpp @@ -38,13 +38,13 @@ void ModifiersTest::testConstantMultiFieldModifier() { UErrorCode status = U_ZERO_ERROR; NumberStringBuilder prefix; NumberStringBuilder suffix; - ConstantMultiFieldModifier mod1(prefix, suffix, true); + ConstantMultiFieldModifier mod1(prefix, suffix, false, true); assertModifierEquals(mod1, 0, true, u"|", u"n", status); assertSuccess("Spot 1", status); prefix.append(u"a📻", UNUM_PERCENT_FIELD, status); suffix.append(u"b", UNUM_CURRENCY_FIELD, status); - ConstantMultiFieldModifier mod2(prefix, suffix, true); + ConstantMultiFieldModifier mod2(prefix, suffix, false, true); assertModifierEquals(mod2, 3, true, u"a📻|b", u"%%%n$", status); assertSuccess("Spot 2", status); @@ -105,14 +105,14 @@ void ModifiersTest::testCurrencySpacingEnabledModifier() { NumberStringBuilder prefix; NumberStringBuilder suffix; - CurrencySpacingEnabledModifier mod1(prefix, suffix, true, symbols, status); + CurrencySpacingEnabledModifier mod1(prefix, suffix, false, true, symbols, status); assertSuccess("Spot 2", status); assertModifierEquals(mod1, 0, true, u"|", u"n", status); assertSuccess("Spot 3", status); prefix.append(u"USD", UNUM_CURRENCY_FIELD, status); assertSuccess("Spot 4", status); - CurrencySpacingEnabledModifier mod2(prefix, suffix, true, symbols, status); + CurrencySpacingEnabledModifier mod2(prefix, suffix, false, true, symbols, status); assertSuccess("Spot 5", status); assertModifierEquals(mod2, 3, true, u"USD|", u"$$$n", status); assertSuccess("Spot 6", status); @@ -138,7 +138,7 @@ void ModifiersTest::testCurrencySpacingEnabledModifier() { symbols.setPatternForCurrencySpacing(UNUM_CURRENCY_SURROUNDING_MATCH, true, u"[|]"); suffix.append("XYZ", UNUM_CURRENCY_FIELD, status); assertSuccess("Spot 11", status); - CurrencySpacingEnabledModifier mod3(prefix, suffix, true, symbols, status); + CurrencySpacingEnabledModifier mod3(prefix, suffix, false, true, symbols, status); assertSuccess("Spot 12", status); assertModifierEquals(mod3, 3, true, u"USD|\u00A0XYZ", u"$$$nn$$$", status); assertSuccess("Spot 13", status); diff --git a/icu4c/source/test/intltest/numbertest_patternmodifier.cpp b/icu4c/source/test/intltest/numbertest_patternmodifier.cpp index 2d0d9d75cd..07f840576b 100644 --- a/icu4c/source/test/intltest/numbertest_patternmodifier.cpp +++ b/icu4c/source/test/intltest/numbertest_patternmodifier.cpp @@ -14,6 +14,7 @@ void PatternModifierTest::runIndexedTest(int32_t index, UBool exec, const char * } TESTCASE_AUTO_BEGIN; TESTCASE_AUTO(testBasic); + TESTCASE_AUTO(testPatternWithNoPlaceholder); TESTCASE_AUTO(testMutableEqualsImmutable); TESTCASE_AUTO_END; } @@ -78,6 +79,42 @@ void PatternModifierTest::testBasic() { assertSuccess("Spot 5", status); } +void PatternModifierTest::testPatternWithNoPlaceholder() { + UErrorCode status = U_ZERO_ERROR; + MutablePatternModifier mod(false); + ParsedPatternInfo patternInfo; + PatternParser::parseToPatternInfo(u"abc", patternInfo, status); + assertSuccess("Spot 1", status); + mod.setPatternInfo(&patternInfo); + mod.setPatternAttributes(UNUM_SIGN_AUTO, false); + DecimalFormatSymbols symbols(Locale::getEnglish(), status); + CurrencyUnit currency(u"USD", status); + assertSuccess("Spot 2", status); + mod.setSymbols(&symbols, currency, UNUM_UNIT_WIDTH_SHORT, nullptr); + mod.setNumberProperties(1, StandardPlural::Form::COUNT); + + // Unsafe Code Path + NumberStringBuilder nsb; + nsb.append(u"x123y", UNUM_FIELD_COUNT, status); + assertSuccess("Spot 3", status); + mod.apply(nsb, 1, 4, status); + assertSuccess("Spot 4", status); + assertEquals("Unsafe Path", u"xabcy", nsb.toUnicodeString()); + + // Safe Code Path + nsb.clear(); + nsb.append(u"x123y", UNUM_FIELD_COUNT, status); + assertSuccess("Spot 5", status); + MicroProps micros; + LocalPointer imod(mod.createImmutable(status)); + assertSuccess("Spot 6", status); + DecimalQuantity quantity; + imod->applyToMicros(micros, quantity); + micros.modMiddle->apply(nsb, 1, 4, status); + assertSuccess("Spot 7", status); + assertEquals("Safe Path", u"xabcy", nsb.toUnicodeString()); +} + void PatternModifierTest::testMutableEqualsImmutable() { UErrorCode status = U_ZERO_ERROR; MutablePatternModifier mod(false); diff --git a/icu4c/source/test/intltest/numfmtst.cpp b/icu4c/source/test/intltest/numfmtst.cpp index bfa714d228..c65859b873 100644 --- a/icu4c/source/test/intltest/numfmtst.cpp +++ b/icu4c/source/test/intltest/numfmtst.cpp @@ -42,7 +42,7 @@ #include "unicode/msgfmt.h" #if (U_PLATFORM == U_PF_AIX) || (U_PLATFORM == U_PF_OS390) -// These should not be macros. If they are, +// These should not be macros. If they are, // replace them with std::isnan and std::isinf #if defined(isnan) #undef isnan @@ -581,8 +581,8 @@ void NumberFormatTest::runIndexedTest( int32_t index, UBool exec, const char* &n TESTCASE_AUTO(TestFieldPositionIterator); TESTCASE_AUTO(TestDecimal); TESTCASE_AUTO(TestCurrencyFractionDigits); - TESTCASE_AUTO(TestExponentParse); - TESTCASE_AUTO(TestExplicitParents); + TESTCASE_AUTO(TestExponentParse); + TESTCASE_AUTO(TestExplicitParents); TESTCASE_AUTO(TestLenientParse); TESTCASE_AUTO(TestAvailableNumberingSystems); TESTCASE_AUTO(TestRoundingPattern); @@ -623,6 +623,7 @@ void NumberFormatTest::runIndexedTest( int32_t index, UBool exec, const char* &n TESTCASE_AUTO(Test11649_toPatternWithMultiCurrency); TESTCASE_AUTO(Test13327_numberingSystemBufferOverflow); TESTCASE_AUTO(Test13391_chakmaParsing); + TESTCASE_AUTO(Test11035_FormatCurrencyAmount); TESTCASE_AUTO_END; } @@ -1458,19 +1459,19 @@ NumberFormatTest::TestLenientParse(void) Locale en_US("en_US"); Locale sv_SE("sv_SE"); - + NumberFormat *mFormat = NumberFormat::createInstance(sv_SE, UNUM_DECIMAL, status); - + if (mFormat == NULL || U_FAILURE(status)) { dataerrln("Unable to create NumberFormat (sv_SE, UNUM_DECIMAL) - %s", u_errorName(status)); } else { mFormat->setLenient(TRUE); for (int32_t t = 0; t < UPRV_LENGTHOF(lenientMinusTestCases); t += 1) { UnicodeString testCase = ctou(lenientMinusTestCases[t]); - + mFormat->parse(testCase, n, status); logln((UnicodeString)"parse(" + testCase + ") = " + n.getLong()); - + if (U_FAILURE(status) || n.getType() != Formattable::kLong || n.getLong() != -5) { errln((UnicodeString)"Lenient parse failed for \"" + (UnicodeString) lenientMinusTestCases[t] + (UnicodeString) "\""); status = U_ZERO_ERROR; @@ -1478,19 +1479,19 @@ NumberFormatTest::TestLenientParse(void) } delete mFormat; } - + mFormat = NumberFormat::createInstance(en_US, UNUM_DECIMAL, status); - + if (mFormat == NULL || U_FAILURE(status)) { dataerrln("Unable to create NumberFormat (en_US, UNUM_DECIMAL) - %s", u_errorName(status)); } else { mFormat->setLenient(TRUE); for (int32_t t = 0; t < UPRV_LENGTHOF(lenientMinusTestCases); t += 1) { UnicodeString testCase = ctou(lenientMinusTestCases[t]); - + mFormat->parse(testCase, n, status); logln((UnicodeString)"parse(" + testCase + ") = " + n.getLong()); - + if (U_FAILURE(status) || n.getType() != Formattable::kLong || n.getLong() != -5) { errln((UnicodeString)"Lenient parse failed for \"" + (UnicodeString) lenientMinusTestCases[t] + (UnicodeString) "\""); status = U_ZERO_ERROR; @@ -1498,7 +1499,7 @@ NumberFormatTest::TestLenientParse(void) } delete mFormat; } - + NumberFormat *cFormat = NumberFormat::createInstance(en_US, UNUM_CURRENCY, status); if (cFormat == NULL || U_FAILURE(status)) { @@ -1572,10 +1573,10 @@ NumberFormatTest::TestLenientParse(void) // Test cases that should fail with a strict parse and pass with a // lenient parse. NumberFormat *nFormat = NumberFormat::createInstance(en_US, status); - + if (nFormat == NULL || U_FAILURE(status)) { dataerrln("Unable to create NumberFormat (en_US) - %s", u_errorName(status)); - } else { + } else { // first, make sure that they fail with a strict parse for (int32_t t = 0; t < UPRV_LENGTHOF(strictFailureTestCases); t += 1) { UnicodeString testCase = ctou(strictFailureTestCases[t]); @@ -2311,30 +2312,54 @@ void NumberFormatTest::TestCurrencyNames(void) { const UBool possibleDataError = TRUE; // Warning: HARD-CODED LOCALE DATA in this test. If it fails, CHECK // THE LOCALE DATA before diving into the code. - assertEquals("USD.getName(SYMBOL_NAME)", + assertEquals("USD.getName(SYMBOL_NAME, en)", UnicodeString("$"), UnicodeString(ucurr_getName(USD, "en", UCURR_SYMBOL_NAME, &isChoiceFormat, &len, &ec)), possibleDataError); - assertEquals("USD.getName(LONG_NAME)", + assertEquals("USD.getName(NARROW_SYMBOL_NAME, en)", + UnicodeString("$"), + UnicodeString(ucurr_getName(USD, "en", + UCURR_NARROW_SYMBOL_NAME, + &isChoiceFormat, &len, &ec)), + possibleDataError); + assertEquals("USD.getName(LONG_NAME, en)", UnicodeString("US Dollar"), UnicodeString(ucurr_getName(USD, "en", UCURR_LONG_NAME, &isChoiceFormat, &len, &ec)), possibleDataError); - assertEquals("CAD.getName(SYMBOL_NAME)", + assertEquals("CAD.getName(SYMBOL_NAME, en)", UnicodeString("CA$"), UnicodeString(ucurr_getName(CAD, "en", UCURR_SYMBOL_NAME, &isChoiceFormat, &len, &ec)), possibleDataError); - assertEquals("CAD.getName(SYMBOL_NAME)", + assertEquals("CAD.getName(NARROW_SYMBOL_NAME, en)", + UnicodeString("$"), + UnicodeString(ucurr_getName(CAD, "en", + UCURR_NARROW_SYMBOL_NAME, + &isChoiceFormat, &len, &ec)), + possibleDataError); + assertEquals("CAD.getName(SYMBOL_NAME, en_CA)", UnicodeString("$"), UnicodeString(ucurr_getName(CAD, "en_CA", UCURR_SYMBOL_NAME, &isChoiceFormat, &len, &ec)), possibleDataError); + assertEquals("USD.getName(SYMBOL_NAME, en_CA)", + UnicodeString("US$"), + UnicodeString(ucurr_getName(USD, "en_CA", + UCURR_SYMBOL_NAME, + &isChoiceFormat, &len, &ec)), + possibleDataError); + assertEquals("USD.getName(NARROW_SYMBOL_NAME, en_CA)", + UnicodeString("$"), + UnicodeString(ucurr_getName(USD, "en_CA", + UCURR_NARROW_SYMBOL_NAME, + &isChoiceFormat, &len, &ec)), + possibleDataError); assertEquals("USD.getName(SYMBOL_NAME) in en_NZ", UnicodeString("US$"), UnicodeString(ucurr_getName(USD, "en_NZ", @@ -2347,6 +2372,18 @@ void NumberFormatTest::TestCurrencyNames(void) { UCURR_SYMBOL_NAME, &isChoiceFormat, &len, &ec)), possibleDataError); + assertEquals("USX.getName(SYMBOL_NAME)", + UnicodeString("USX"), + UnicodeString(ucurr_getName(USX, "en_US", + UCURR_SYMBOL_NAME, + &isChoiceFormat, &len, &ec)), + possibleDataError); + assertEquals("USX.getName(NARROW_SYMBOL_NAME)", + UnicodeString("USX"), + UnicodeString(ucurr_getName(USX, "en_US", + UCURR_NARROW_SYMBOL_NAME, + &isChoiceFormat, &len, &ec)), + possibleDataError); assertEquals("USX.getName(LONG_NAME)", UnicodeString("USX"), UnicodeString(ucurr_getName(USX, "en_US", @@ -2497,7 +2534,7 @@ void NumberFormatTest::TestSymbolsWithBadLocale(void) { UnicodeString intlCurrencySymbol((UChar)0xa4); intlCurrencySymbol.append((UChar)0xa4); - + logln("Current locale is %s", Locale::getDefault().getName()); Locale::setDefault(locBad, status); logln("Current locale is %s", Locale::getDefault().getName()); @@ -3208,7 +3245,7 @@ void NumberFormatTest::TestCompatibleCurrencies() { expectParseCurrency(*fmtJP, JPY, 1235, "\\u00A51,235"); logln("%s:%d - testing parse of fullwidth yen sign in JP\n", __FILE__, __LINE__); expectParseCurrency(*fmtJP, JPY, 1235, "\\uFFE51,235"); - + // more.. */ } @@ -3228,7 +3265,7 @@ void NumberFormatTest::expectParseCurrency(const NumberFormat &fmt, const UChar* fmt.getLocale(ULOC_ACTUAL_LOCALE, status).getBaseName(), text); u_austrcpy(theInfo+uprv_strlen(theInfo), currency); - + char theOperation[100]; uprv_strcpy(theOperation, theInfo); @@ -3239,7 +3276,7 @@ void NumberFormatTest::expectParseCurrency(const NumberFormat &fmt, const UChar* uprv_strcat(theOperation, ", check currency:"); assertEquals(theOperation, currency, currencyAmount->getISOCurrency()); } - + void NumberFormatTest::TestJB3832(){ const char* localeID = "pt_PT@currency=PTE"; @@ -3672,7 +3709,7 @@ void NumberFormatTest::TestNumberingSystems() { NumberFormat *fmt = (NumberFormat *) origFmt->clone(); delete origFmt; - + if (item->isRBNF) { expect3(*fmt,item->value,CharsToUnicodeString(item->expectedResult)); } else { @@ -4044,7 +4081,7 @@ for (;;) { UErrorCode status = U_ZERO_ERROR; NumberFormat* numFmt = NumberFormat::createInstance(locale, k, status); logln("#%d NumberFormat(%s, %s) Currency=%s\n", - i, localeString, currencyStyleNames[kIndex], + i, localeString, currencyStyleNames[kIndex], currencyISOCode); if (U_FAILURE(status)) { @@ -6804,7 +6841,7 @@ void NumberFormatTest::TestFormatAttributes() { DecimalFormat *decFmt = (DecimalFormat *) NumberFormat::createInstance(locale, UNUM_CURRENCY, status); if (failure(status, "NumberFormat::createInstance", TRUE)) return; double val = 12345.67; - + { int32_t expected[] = { UNUM_CURRENCY_FIELD, 0, 1, @@ -6889,7 +6926,7 @@ const char* attrString(int32_t attrId) { // // Test formatting & parsing of big decimals. -// API test, not a comprehensive test. +// API test, not a comprehensive test. // See DecimalFormatTest/DataDrivenTests // #define ASSERT_SUCCESS(status) {if (U_FAILURE(status)) errln("file %s, line %d: status: %s", \ @@ -7022,7 +7059,7 @@ void NumberFormatTest::TestDecimal() { delete fmtr; } } - + #if U_PLATFORM != U_PF_CYGWIN || defined(CYGWINMSVC) /* * This test fails on Cygwin (1.7.16) using GCC because of a rounding issue with strtod(). @@ -7074,38 +7111,38 @@ void NumberFormatTest::TestCurrencyFractionDigits() { } } -void NumberFormatTest::TestExponentParse() { - - UErrorCode status = U_ZERO_ERROR; - Formattable result; - ParsePosition parsePos(0); - - // set the exponent symbol - status = U_ZERO_ERROR; - DecimalFormatSymbols *symbols = new DecimalFormatSymbols(Locale::getDefault(), status); - if(U_FAILURE(status)) { - dataerrln((UnicodeString)"ERROR: Could not create DecimalFormatSymbols (Default)"); - return; - } - - // create format instance - status = U_ZERO_ERROR; - DecimalFormat fmt("#####", symbols, status); - if(U_FAILURE(status)) { - errln((UnicodeString)"ERROR: Could not create DecimalFormat (pattern, symbols*)"); - } - - // parse the text - fmt.parse("5.06e-27", result, parsePos); - if(result.getType() != Formattable::kDouble && - result.getDouble() != 5.06E-27 && - parsePos.getIndex() != 8 - ) - { - errln("ERROR: parse failed - expected 5.06E-27, 8 - returned %d, %i", - result.getDouble(), parsePos.getIndex()); - } -} +void NumberFormatTest::TestExponentParse() { + + UErrorCode status = U_ZERO_ERROR; + Formattable result; + ParsePosition parsePos(0); + + // set the exponent symbol + status = U_ZERO_ERROR; + DecimalFormatSymbols *symbols = new DecimalFormatSymbols(Locale::getDefault(), status); + if(U_FAILURE(status)) { + dataerrln((UnicodeString)"ERROR: Could not create DecimalFormatSymbols (Default)"); + return; + } + + // create format instance + status = U_ZERO_ERROR; + DecimalFormat fmt("#####", symbols, status); + if(U_FAILURE(status)) { + errln((UnicodeString)"ERROR: Could not create DecimalFormat (pattern, symbols*)"); + } + + // parse the text + fmt.parse("5.06e-27", result, parsePos); + if(result.getType() != Formattable::kDouble && + result.getDouble() != 5.06E-27 && + parsePos.getIndex() != 8 + ) + { + errln("ERROR: parse failed - expected 5.06E-27, 8 - returned %d, %i", + result.getDouble(), parsePos.getIndex()); + } +} void NumberFormatTest::TestExplicitParents() { @@ -7188,13 +7225,13 @@ NumberFormatTest::Test9087(void) { U_STRING_DECL(pattern,"#",1); U_STRING_INIT(pattern,"#",1); - + U_STRING_DECL(infstr,"INF",3); U_STRING_INIT(infstr,"INF",3); U_STRING_DECL(nanstr,"NAN",3); U_STRING_INIT(nanstr,"NAN",3); - + UChar outputbuf[50] = {0}; UErrorCode status = U_ZERO_ERROR; UNumberFormat* fmt = unum_open(UNUM_PATTERN_DECIMAL,pattern,1,NULL,NULL,&status); @@ -7216,7 +7253,7 @@ NumberFormatTest::Test9087(void) UFieldPosition position = { 0, 0, 0}; unum_formatDouble(fmt,inf,outputbuf,50,&position,&status); - + if ( u_strcmp(infstr, outputbuf)) { errln((UnicodeString)"FAIL: unexpected result for infinity - expected " + infstr + " got " + outputbuf); } @@ -7237,7 +7274,7 @@ void NumberFormatTest::TestFormatFastpaths() { } #else infoln("NOTE: UCONFIG_FORMAT_FASTPATHS not set, test skipped."); -#endif +#endif // get some additional case { @@ -7500,9 +7537,9 @@ UBool NumberFormatTest::testFormattableAsUFormattable(const char *file, int line UErrorCode int64ConversionU = U_ZERO_ERROR; int64_t r = ufmt_getInt64(u, &int64ConversionU); - if( (l==r) + if( (l==r) && ( uType != UFMT_INT64 ) // int64 better not overflow - && (U_INVALID_FORMAT_ERROR==int64ConversionU) + && (U_INVALID_FORMAT_ERROR==int64ConversionU) && (U_INVALID_FORMAT_ERROR==int64ConversionF) ) { logln("%s:%d: OK: 64 bit overflow", file, line); } else { @@ -7627,7 +7664,7 @@ void NumberFormatTest::TestSignificantDigits(void) { numberFormat->setMinimumSignificantDigits(3); numberFormat->setMaximumSignificantDigits(5); numberFormat->setGroupingUsed(false); - + UnicodeString result; UnicodeString expectedResult; for (unsigned int i = 0; i < UPRV_LENGTHOF(input); ++i) { @@ -7649,7 +7686,7 @@ void NumberFormatTest::TestShowZero() { numberFormat->setSignificantDigitsUsed(TRUE); numberFormat->setMaximumSignificantDigits(3); - + UnicodeString result; numberFormat->format(0.0, result); if (result != "0") { @@ -7666,7 +7703,7 @@ void NumberFormatTest::TestBug9936() { dataerrln("File %s, Line %d: status = %s.\n", __FILE__, __LINE__, u_errorName(status)); return; } - + if (numberFormat->areSignificantDigitsUsed() == TRUE) { errln("File %s, Line %d: areSignificantDigitsUsed() was TRUE, expected FALSE.\n", __FILE__, __LINE__); } @@ -7690,7 +7727,7 @@ void NumberFormatTest::TestBug9936() { if (numberFormat->areSignificantDigitsUsed() == FALSE) { errln("File %s, Line %d: areSignificantDigitsUsed() was FALSE, expected TRUE.\n", __FILE__, __LINE__); } - + } void NumberFormatTest::TestParseNegativeWithFaLocale() { @@ -7781,7 +7818,7 @@ void NumberFormatTest::TestParseSignsAndMarks() { { "en@numbers=arabext", FALSE, CharsToUnicodeString("\\u200E-\\u200E\\u06F6\\u06F7"), -67 }, { "en@numbers=arabext", TRUE, CharsToUnicodeString("\\u200E-\\u200E\\u06F6\\u06F7"), -67 }, { "en@numbers=arabext", TRUE, CharsToUnicodeString("\\u200E-\\u200E \\u06F6\\u06F7"), -67 }, - + { "he", FALSE, CharsToUnicodeString("12"), 12 }, { "he", TRUE, CharsToUnicodeString("12"), 12 }, { "he", FALSE, CharsToUnicodeString("-23"), -23 }, @@ -7910,7 +7947,7 @@ void NumberFormatTest::Test10468ApplyPattern() { // explicit padding char is specified in the new pattern. fmt.applyPattern("AA#,##0.00ZZ", status); - // Oops this still prints 'a' even though we changed the pattern. + // Oops this still prints 'a' even though we changed the pattern. if (fmt.getPadCharacterString() != UnicodeString(" ")) { errln("applyPattern did not clear padding character."); } @@ -7923,7 +7960,7 @@ void NumberFormatTest::TestRoundingScientific10542() { errcheckln(status, "DecimalFormat constructor failed - %s", u_errorName(status)); return; } - + DecimalFormat::ERoundingMode roundingModes[] = { DecimalFormat::kRoundCeiling, DecimalFormat::kRoundDown, @@ -7940,7 +7977,7 @@ void NumberFormatTest::TestRoundingScientific10542() { "Round half even", "Round half up", "Round up"}; - + { double values[] = {-0.003006, -0.003005, -0.003004, 0.003014, 0.003015, 0.003016}; // The order of these expected values correspond to the order of roundingModes and the order of values. @@ -8233,7 +8270,7 @@ void NumberFormatTest::TestCurrencyUsage() { assertEquals("Test Currency Usage 3", UnicodeString("CA$123.57"), original_rounding); fmt->setCurrencyUsage(UCURR_USAGE_CASH, &status); }else{ - fmt = (DecimalFormat *) NumberFormat::createInstance(enUS_CAD, UNUM_CASH_CURRENCY, status); + fmt = (DecimalFormat *) NumberFormat::createInstance(enUS_CAD, UNUM_CASH_CURRENCY, status); if (assertSuccess("en_US@currency=CAD/CASH", status, TRUE) == FALSE) { continue; } @@ -8634,7 +8671,7 @@ void NumberFormatTest::Test10727_RoundingZero() { DigitList d; d.set(-0.0); assertFalse("", d.isPositive()); - d.round(3); + d.round(3); assertFalse("", d.isPositive()); } @@ -8650,7 +8687,7 @@ void NumberFormatTest::Test11376_getAndSetPositivePrefix() { DecimalFormat *dfmt = (DecimalFormat *) fmt.getAlias(); dfmt->setCurrency(USD); UnicodeString result; - + // This line should be a no-op. I am setting the positive prefix // to be the same thing it was before. dfmt->setPositivePrefix(dfmt->getPositivePrefix(result)); @@ -8774,7 +8811,7 @@ void NumberFormatTest::Test11649_toPatternWithMultiCurrency() { static UChar USD[] = {0x55, 0x53, 0x44, 0x0}; fmt.setCurrency(USD); UnicodeString appendTo; - + assertEquals("", "US dollars 12.34", fmt.format(12.34, appendTo)); UnicodeString topattern; @@ -8784,7 +8821,7 @@ void NumberFormatTest::Test11649_toPatternWithMultiCurrency() { return; } fmt2.setCurrency(USD); - + appendTo.remove(); assertEquals("", "US dollars 12.34", fmt2.format(12.34, appendTo)); } @@ -8885,4 +8922,28 @@ void NumberFormatTest::checkExceptionIssue11735() { assertEquals("Issue11735 ppos", 0, ppos.getIndex()); } +void NumberFormatTest::Test11035_FormatCurrencyAmount() { + UErrorCode status; + double amount = 12345.67; + const char16_t* expected = u"12,345$67 ​"; + + // Test two ways to set a currency via API + + Locale loc1 = Locale("pt_PT"); + NumberFormat* fmt1 = NumberFormat::createCurrencyInstance(loc1, status); + fmt1->setCurrency(u"PTE", status); + UnicodeString actualSetCurrency; + fmt1->format(amount, actualSetCurrency); + + Locale loc2 = Locale("pt_PT@currency=PTE"); + NumberFormat* fmt2 = NumberFormat::createCurrencyInstance(loc2, status); + UnicodeString actualLocaleString; + fmt2->format(amount, actualLocaleString); + + // TODO: The following test fill fail until DecimalFormat wraps NumberFormatter. + if (!logKnownIssue("13574")) { + assertEquals("Custom Currency Pattern, Set Currency", expected, actualSetCurrency); + } +} + #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/intltest/numfmtst.h b/icu4c/source/test/intltest/numfmtst.h index 8477fcbcdb..05d05c86cf 100644 --- a/icu4c/source/test/intltest/numfmtst.h +++ b/icu4c/source/test/intltest/numfmtst.h @@ -219,6 +219,7 @@ class NumberFormatTest: public CalendarTimeZoneTest { void Test13391_chakmaParsing(); void checkExceptionIssue11735(); + void Test11035_FormatCurrencyAmount(); private: UBool testFormattableAsUFormattable(const char *file, int line, Formattable &f); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/CurrencyData.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/CurrencyData.java index b656a8b725..df4272574f 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/CurrencyData.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/CurrencyData.java @@ -29,7 +29,6 @@ public class CurrencyData { public abstract Map getUnitPatterns(); public abstract CurrencyFormatInfo getFormatInfo(String isoCode); public abstract CurrencySpacingInfo getSpacingInfo(); - public abstract String getNarrowSymbol(String isoCode); } public static final class CurrencyFormatInfo { 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 01d137fcee..e7519d5036 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 @@ -31,4 +31,11 @@ public interface AffixPatternProvider { public boolean negativeHasMinusSign(); public boolean containsSymbolType(int type); + + /** + * True if the pattern has a number placeholder like "0" or "#,##0.00"; false if the pattern does not + * have one. This is used in cases like compact notation, where the pattern replaces the entire + * number instead of rendering the number. + */ + public boolean hasBody(); } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java index 557d0b80f8..cdd129c128 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java @@ -17,24 +17,29 @@ public class ConstantMultiFieldModifier implements Modifier { protected final char[] suffixChars; protected final Field[] prefixFields; protected final Field[] suffixFields; + private final boolean overwrite; private final boolean strong; public ConstantMultiFieldModifier( NumberStringBuilder prefix, NumberStringBuilder suffix, + boolean overwrite, boolean strong) { prefixChars = prefix.toCharArray(); suffixChars = suffix.toCharArray(); prefixFields = prefix.toFieldArray(); suffixFields = suffix.toFieldArray(); + this.overwrite = overwrite; this.strong = strong; } @Override public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { - // Insert the suffix first since inserting the prefix will change the rightIndex - int length = output.insert(rightIndex, suffixChars, suffixFields); - length += output.insert(leftIndex, prefixChars, prefixFields); + int length = output.insert(leftIndex, prefixChars, prefixFields); + if (overwrite) { + length += output.splice(leftIndex + length, rightIndex + length, "", 0, 0, null); + } + length += output.insert(rightIndex + length, suffixChars, suffixFields); return length; } 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 ada6acf025..2540899cda 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 @@ -59,4 +59,9 @@ public class CurrencyPluralInfoAffixProvider implements AffixPatternProvider { public boolean containsSymbolType(int type) { return affixesByPlural[StandardPlural.OTHER.ordinal()].containsSymbolType(type); } + + @Override + public boolean hasBody() { + return affixesByPlural[StandardPlural.OTHER.ordinal()].hasBody(); + } } \ No newline at end of file diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java index 9fb35f7546..2cf875884e 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java @@ -30,9 +30,10 @@ public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier { public CurrencySpacingEnabledModifier( NumberStringBuilder prefix, NumberStringBuilder suffix, + boolean overwrite, boolean strong, DecimalFormatSymbols symbols) { - super(prefix, suffix, strong); + super(prefix, suffix, overwrite, strong); // Check for currency spacing. Do not build the UnicodeSets unless there is // a currency code point at a boundary. diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Grouper.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Grouper.java new file mode 100644 index 0000000000..fee5564de6 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Grouper.java @@ -0,0 +1,164 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import com.ibm.icu.impl.ICUData; +import com.ibm.icu.impl.ICUResourceBundle; +import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo; +import com.ibm.icu.number.NumberFormatter.GroupingStrategy; +import com.ibm.icu.util.ULocale; +import com.ibm.icu.util.UResourceBundle; + +/** + * A full options object for grouping sizes. + */ +public class Grouper { + + private static final Grouper GROUPER_NEVER = new Grouper((short) -1, (short) -1, (short) -2); + private static final Grouper GROUPER_MIN2 = new Grouper((short) -2, (short) -2, (short) -3); + private static final Grouper GROUPER_AUTO = new Grouper((short) -2, (short) -2, (short) -2); + private static final Grouper GROUPER_ON_ALIGNED = new Grouper((short) -4, (short) -4, (short) 1); + + private static final Grouper GROUPER_WESTERN = new Grouper((short) 3, (short) 3, (short) 1); + private static final Grouper GROUPER_INDIC = new Grouper((short) 3, (short) 2, (short) 1); + private static final Grouper GROUPER_WESTERN_MIN2 = new Grouper((short) 3, (short) 3, (short) 2); + private static final Grouper GROUPER_INDIC_MIN2 = new Grouper((short) 3, (short) 2, (short) 2); + + /** + * Convert from the GroupingStrategy enum to a Grouper object. + */ + public static Grouper forStrategy(GroupingStrategy grouping) { + switch (grouping) { + case OFF: + return GROUPER_NEVER; + case MIN2: + return GROUPER_MIN2; + case AUTO: + return GROUPER_AUTO; + case ON_ALIGNED: + return GROUPER_ON_ALIGNED; + case WESTERN: + return GROUPER_WESTERN; + default: + throw new AssertionError(); + } + } + + /** + * Resolve the values in Properties to a Grouper object. + */ + public static Grouper forProperties(DecimalFormatProperties properties) { + short grouping1 = (short) properties.getGroupingSize(); + short grouping2 = (short) properties.getSecondaryGroupingSize(); + short minGrouping = (short) properties.getMinimumGroupingDigits(); + grouping1 = grouping1 > 0 ? grouping1 : grouping2 > 0 ? grouping2 : grouping1; + grouping2 = grouping2 > 0 ? grouping2 : grouping1; + return getInstance(grouping1, grouping2, minGrouping); + } + + public static Grouper getInstance(short grouping1, short grouping2, short minGrouping) { + if (grouping1 == -1) { + return GROUPER_NEVER; + } else if (grouping1 == 3 && grouping2 == 3 && minGrouping == 1) { + return GROUPER_WESTERN; + } else if (grouping1 == 3 && grouping2 == 2 && minGrouping == 1) { + return GROUPER_INDIC; + } else if (grouping1 == 3 && grouping2 == 3 && minGrouping == 1) { + return GROUPER_WESTERN_MIN2; + } else if (grouping1 == 3 && grouping2 == 2 && minGrouping == 1) { + return GROUPER_INDIC_MIN2; + } else { + return new Grouper(grouping1, grouping2, minGrouping); + } + } + + private static short getMinGroupingForLocale(ULocale locale) { + // TODO: Cache this? + ICUResourceBundle resource = (ICUResourceBundle) UResourceBundle + .getBundleInstance(ICUData.ICU_BASE_NAME, locale); + String result = resource.getStringWithFallback("NumberElements/minimumGroupingDigits"); + return Short.valueOf(result); + } + + /** + * The primary grouping size, with the following special values: + *
    + *
  • -1 = no grouping + *
  • -2 = needs locale data + *
  • -4 = fall back to Western grouping if not in locale + *
+ */ + private final short grouping1; + + /** + * The secondary grouping size, with the following special values: + *
    + *
  • -1 = no grouping + *
  • -2 = needs locale data + *
  • -4 = fall back to Western grouping if not in locale + *
+ */ + private final short grouping2; + + /** + * The minimum gropuing size, with the following special values: + *
    + *
  • -2 = needs locale data + *
  • -3 = no less than 2 + *
+ */ + private final short minGrouping; + + private Grouper(short grouping1, short grouping2, short minGrouping) { + this.grouping1 = grouping1; + this.grouping2 = grouping2; + this.minGrouping = minGrouping; + } + + public Grouper withLocaleData(ULocale locale, ParsedPatternInfo patternInfo) { + if (this.grouping1 != -2 && this.grouping1 != -4) { + return this; + } + + short grouping1 = (short) (patternInfo.positive.groupingSizes & 0xffff); + short grouping2 = (short) ((patternInfo.positive.groupingSizes >>> 16) & 0xffff); + short grouping3 = (short) ((patternInfo.positive.groupingSizes >>> 32) & 0xffff); + if (grouping2 == -1) { + grouping1 = this.grouping1 == -4 ? (short) 3 : (short) -1; + } + if (grouping3 == -1) { + grouping2 = grouping1; + } + + short minGrouping; + if (this.minGrouping == -2) { + minGrouping = getMinGroupingForLocale(locale); + } else if (this.minGrouping == -3) { + minGrouping = (short) Math.max(2, getMinGroupingForLocale(locale)); + } else { + minGrouping = this.minGrouping; + } + + return getInstance(grouping1, grouping2, minGrouping); + } + + public boolean groupAtPosition(int position, DecimalQuantity value) { + assert grouping1 != -2 && grouping1 != -4; + if (grouping1 == -1 || grouping1 == 0) { + // Either -1 or 0 means "no grouping" + return false; + } + position -= grouping1; + return position >= 0 + && (position % grouping2) == 0 + && value.getUpperDisplayMagnitude() - grouping1 + 1 >= minGrouping; + } + + public short getPrimary() { + return grouping1; + } + + public short getSecondary() { + return grouping2; + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MacroProps.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MacroProps.java index e1f1cf7794..fa0f7648ca 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MacroProps.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MacroProps.java @@ -3,7 +3,6 @@ package com.ibm.icu.impl.number; import com.ibm.icu.impl.Utility; -import com.ibm.icu.number.Grouper; import com.ibm.icu.number.IntegerWidth; import com.ibm.icu.number.Notation; import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay; @@ -19,7 +18,7 @@ public class MacroProps implements Cloneable { public MeasureUnit unit; public MeasureUnit perUnit; public Rounder rounder; - public Grouper grouper; + public Object grouping; public Padder padder; public IntegerWidth integerWidth; public Object symbols; @@ -47,8 +46,8 @@ public class MacroProps implements Cloneable { perUnit = fallback.perUnit; if (rounder == null) rounder = fallback.rounder; - if (grouper == null) - grouper = fallback.grouper; + if (grouping == null) + grouping = fallback.grouping; if (padder == null) padder = fallback.padder; if (integerWidth == null) @@ -77,7 +76,7 @@ public class MacroProps implements Cloneable { unit, perUnit, rounder, - grouper, + grouping, padder, integerWidth, symbols, @@ -103,7 +102,7 @@ public class MacroProps implements Cloneable { && Utility.equals(unit, other.unit) && Utility.equals(perUnit, other.perUnit) && Utility.equals(rounder, other.rounder) - && Utility.equals(grouper, other.grouper) + && Utility.equals(grouping, other.grouping) && Utility.equals(padder, other.padder) && Utility.equals(integerWidth, other.integerWidth) && Utility.equals(symbols, other.symbols) 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 019a10168a..d5e3ba44f3 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 @@ -2,7 +2,6 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number; -import com.ibm.icu.number.Grouper; import com.ibm.icu.number.IntegerWidth; import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay; import com.ibm.icu.number.NumberFormatter.SignDisplay; 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 cc894bebfd..609259ab07 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 @@ -206,9 +206,9 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr insertPrefix(a.clear(), 0); insertSuffix(b.clear(), 0); if (patternInfo.hasCurrencySign()) { - return new CurrencySpacingEnabledModifier(a, b, isStrong, symbols); + return new CurrencySpacingEnabledModifier(a, b, !patternInfo.hasBody(), isStrong, symbols); } else { - return new ConstantMultiFieldModifier(a, b, isStrong); + return new ConstantMultiFieldModifier(a, b, !patternInfo.hasBody(), isStrong); } } @@ -271,13 +271,18 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { int prefixLen = insertPrefix(output, leftIndex); int suffixLen = insertSuffix(output, rightIndex + prefixLen); + // If the pattern had no decimal stem body (like #,##0.00), overwrite the value. + int overwriteLen = 0; + if (!patternInfo.hasBody()) { + overwriteLen = output.splice(leftIndex + prefixLen, rightIndex + prefixLen, "", 0, 0, null); + } CurrencySpacingEnabledModifier.applyCurrencySpacing(output, leftIndex, prefixLen, - rightIndex + prefixLen, + rightIndex + prefixLen + overwriteLen, suffixLen, symbols); - return prefixLen + suffixLen; + return prefixLen + overwriteLen + suffixLen; } @Override @@ -317,7 +322,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr /** * Pre-processes the prefix or suffix into the currentAffix field, creating and mutating that field - * if necessary. Calls down to {@link PatternStringUtils#affixPatternProviderToStringBuilder}. + * if necessary. Calls down to {@link PatternStringUtils#affixPatternProviderToStringBuilder}. * * @param isPrefix * true to prepare the prefix; false to prepare the suffix. @@ -355,10 +360,10 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr return currency.getCurrencyCode(); } else if (unitWidth == UnitWidth.HIDDEN) { return ""; - } else if (unitWidth == UnitWidth.NARROW) { - return currency.getName(symbols.getULocale(), Currency.NARROW_SYMBOL_NAME, null); } else { - return currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null); + int selector = unitWidth == UnitWidth.NARROW ? Currency.NARROW_SYMBOL_NAME + : Currency.SYMBOL_NAME; + return currency.getName(symbols.getULocale(), selector, null); } case AffixUtils.TYPE_CURRENCY_DOUBLE: return currency.getCurrencyCode(); 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 216427c5de..53dd482baa 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 @@ -169,6 +169,11 @@ public class PatternStringParser { public boolean containsSymbolType(int type) { return AffixUtils.containsType(pattern, type); } + + @Override + public boolean hasBody() { + return positive.integerTotal > 0; + } } public static class ParsedSubpatternInfo { 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 c6b57ba920..6d2eab65c8 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 @@ -127,6 +127,11 @@ public class PropertiesAffixPatternProvider implements AffixPatternProvider { || AffixUtils.containsType(negPrefix, type) || AffixUtils.containsType(negSuffix, type); } + @Override + public boolean hasBody() { + return true; + } + @Override public String toString() { return super.toString() diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/DecimalMatcher.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/DecimalMatcher.java index 4a12af11c2..ae09550561 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/DecimalMatcher.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/DecimalMatcher.java @@ -3,9 +3,9 @@ package com.ibm.icu.impl.number.parse; import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD; +import com.ibm.icu.impl.number.Grouper; import com.ibm.icu.impl.number.parse.UnicodeSetStaticCache.Key; import com.ibm.icu.lang.UCharacter; -import com.ibm.icu.number.Grouper; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.UnicodeSet; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/NumberParserImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/NumberParserImpl.java index 73654686db..acb96411b1 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/NumberParserImpl.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/NumberParserImpl.java @@ -12,11 +12,12 @@ import com.ibm.icu.impl.number.AffixPatternProvider; import com.ibm.icu.impl.number.AffixUtils; import com.ibm.icu.impl.number.CustomSymbolCurrency; import com.ibm.icu.impl.number.DecimalFormatProperties; +import com.ibm.icu.impl.number.Grouper; import com.ibm.icu.impl.number.PatternStringParser; import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo; import com.ibm.icu.impl.number.PropertiesAffixPatternProvider; import com.ibm.icu.impl.number.RoundingUtils; -import com.ibm.icu.number.Grouper; +import com.ibm.icu.number.NumberFormatter.GroupingStrategy; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.UnicodeSet; import com.ibm.icu.util.Currency; @@ -93,7 +94,7 @@ public class NumberParserImpl { ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(pattern); AffixMatcher.newGenerate(patternInfo, parser, factory, ignorables, parseFlags); - Grouper grouper = Grouper.defaults().withLocaleData(patternInfo); + Grouper grouper = Grouper.forStrategy(GroupingStrategy.AUTO).withLocaleData(locale, patternInfo); parser.addMatcher(ignorables); parser.addMatcher(DecimalMatcher.getInstance(symbols, grouper, parseFlags)); @@ -166,7 +167,7 @@ public class NumberParserImpl { AffixPatternProvider patternInfo = new PropertiesAffixPatternProvider(properties); Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols); boolean isStrict = properties.getParseMode() == ParseMode.STRICT; - Grouper grouper = Grouper.defaults().withProperties(properties); + Grouper grouper = Grouper.forProperties(properties); int parseFlags = 0; // Fraction grouping is disabled by default because it has never been supported in DecimalFormat parseFlags |= ParsingUtils.PARSE_FLAG_FRACTION_GROUPING_DISABLED; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ScientificMatcher.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ScientificMatcher.java index c05e75fa80..a6c053af7e 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ScientificMatcher.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ScientificMatcher.java @@ -2,7 +2,7 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number.parse; -import com.ibm.icu.number.Grouper; +import com.ibm.icu.impl.number.Grouper; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.UnicodeSet; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/CompactNotation.java b/icu4j/main/classes/core/src/com/ibm/icu/number/CompactNotation.java index c176ee2501..5219f0091d 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/CompactNotation.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/CompactNotation.java @@ -10,6 +10,7 @@ import java.util.Set; import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.impl.number.CompactData; import com.ibm.icu.impl.number.CompactData.CompactType; +import com.ibm.icu.impl.number.DecimalFormatProperties; import com.ibm.icu.impl.number.DecimalQuantity; import com.ibm.icu.impl.number.MicroProps; import com.ibm.icu.impl.number.MicroPropsGenerator; @@ -38,6 +39,17 @@ public class CompactNotation extends Notation { final CompactStyle compactStyle; final Map> compactCustomData; + /** + * Create a compact notation with custom data. + * @internal + * @deprecated This API is ICU internal only. + * @see DecimalFormatProperties#setCompactCustomData + */ + @Deprecated + public static CompactNotation forCustomData(Map> compactCustomData) { + return new CompactNotation(compactCustomData); + } + /* package-private */ CompactNotation(CompactStyle compactStyle) { compactCustomData = null; this.compactStyle = compactStyle; @@ -61,14 +73,9 @@ public class CompactNotation extends Notation { private static class CompactHandler implements MicroPropsGenerator { - private static class CompactModInfo { - public ImmutablePatternModifier mod; - public int numDigits; - } - final PluralRules rules; final MicroPropsGenerator parent; - final Map precomputedMods; + final Map precomputedMods; final CompactData data; private CompactHandler( @@ -89,7 +96,7 @@ public class CompactNotation extends Notation { } if (buildReference != null) { // Safe code path - precomputedMods = new HashMap(); + precomputedMods = new HashMap(); precomputeAllModifiers(buildReference); } else { // Unsafe code path @@ -103,12 +110,9 @@ public class CompactNotation extends Notation { data.getUniquePatterns(allPatterns); for (String patternString : allPatterns) { - CompactModInfo info = new CompactModInfo(); ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(patternString); buildReference.setPatternInfo(patternInfo); - info.mod = buildReference.createImmutable(); - info.numDigits = patternInfo.positive.integerTotal; - precomputedMods.put(patternString, info); + precomputedMods.put(patternString, buildReference.createImmutable()); } } @@ -131,28 +135,22 @@ public class CompactNotation extends Notation { StandardPlural plural = quantity.getStandardPlural(rules); String patternString = data.getPattern(magnitude, plural); - @SuppressWarnings("unused") // see #13075 - int numDigits = -1; if (patternString == null) { // Use the default (non-compact) modifier. // No need to take any action. } else if (precomputedMods != null) { // Safe code path. // Java uses a hash set here for O(1) lookup. C++ uses a linear search. - CompactModInfo info = precomputedMods.get(patternString); - info.mod.applyToMicros(micros, quantity); - numDigits = info.numDigits; + ImmutablePatternModifier mod = precomputedMods.get(patternString); + mod.applyToMicros(micros, quantity); } else { // Unsafe code path. // Overwrite the PatternInfo in the existing modMiddle. assert micros.modMiddle instanceof MutablePatternModifier; ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(patternString); ((MutablePatternModifier) micros.modMiddle).setPatternInfo(patternInfo); - numDigits = patternInfo.positive.integerTotal; } - // FIXME: Deal with numDigits == 0 (Awaiting a test case) - // We already performed rounding. Do not perform it again. micros.rounding = Rounder.constructPassThrough(); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/Grouper.java b/icu4j/main/classes/core/src/com/ibm/icu/number/Grouper.java deleted file mode 100644 index 24d46f597b..0000000000 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/Grouper.java +++ /dev/null @@ -1,157 +0,0 @@ -// © 2017 and later: Unicode, Inc. and others. -// License & terms of use: http://www.unicode.org/copyright.html#License -package com.ibm.icu.number; - -import com.ibm.icu.impl.number.DecimalFormatProperties; -import com.ibm.icu.impl.number.DecimalQuantity; -import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo; - -/** - * @internal - * @deprecated This API is a technical preview. It is likely to change in an upcoming release. - */ -@Deprecated -public class Grouper { - - // Conveniences for Java handling of bytes - private static final byte N2 = -2; - private static final byte N1 = -1; - private static final byte B2 = 2; - private static final byte B3 = 3; - - private static final Grouper DEFAULTS = new Grouper(N2, N2, false); - private static final Grouper MIN2 = new Grouper(N2, N2, true); - private static final Grouper NONE = new Grouper(N1, N1, false); - - private final byte grouping1; // -2 means "needs locale data"; -1 means "no grouping" - private final byte grouping2; - private final boolean min2; - - private Grouper(byte grouping1, byte grouping2, boolean min2) { - this.grouping1 = grouping1; - this.grouping2 = grouping2; - this.min2 = min2; - } - - /** - * @internal - * @deprecated This API is a technical preview. It is likely to change in an upcoming release. - */ - @Deprecated - public static Grouper defaults() { - return DEFAULTS; - } - - /** - * @internal - * @deprecated This API is a technical preview. It is likely to change in an upcoming release. - */ - @Deprecated - public static Grouper minTwoDigits() { - return MIN2; - } - - /** - * @internal - * @deprecated This API is a technical preview. It is likely to change in an upcoming release. - */ - @Deprecated - public static Grouper none() { - return NONE; - } - - ////////////////////////// - // PACKAGE-PRIVATE APIS // - ////////////////////////// - - private static final Grouper GROUPING_3 = new Grouper(B3, B3, false); - private static final Grouper GROUPING_3_2 = new Grouper(B3, B2, false); - private static final Grouper GROUPING_3_MIN2 = new Grouper(B3, B3, true); - private static final Grouper GROUPING_3_2_MIN2 = new Grouper(B3, B2, true); - - static Grouper getInstance(byte grouping1, byte grouping2, boolean min2) { - if (grouping1 == -1) { - return NONE; - } else if (!min2 && grouping1 == 3 && grouping2 == 3) { - return GROUPING_3; - } else if (!min2 && grouping1 == 3 && grouping2 == 2) { - return GROUPING_3_2; - } else if (min2 && grouping1 == 3 && grouping2 == 3) { - return GROUPING_3_MIN2; - } else if (min2 && grouping1 == 3 && grouping2 == 2) { - return GROUPING_3_2_MIN2; - } else { - return new Grouper(grouping1, grouping2, min2); - } - } - - /** - * @internal - * @deprecated This API is ICU internal only. - */ - @Deprecated - public Grouper withProperties(DecimalFormatProperties properties) { - if (grouping1 != -2) { - return this; - } - byte grouping1 = (byte) properties.getGroupingSize(); - byte grouping2 = (byte) properties.getSecondaryGroupingSize(); - int minGrouping = properties.getMinimumGroupingDigits(); - grouping1 = grouping1 > 0 ? grouping1 : grouping2 > 0 ? grouping2 : grouping1; - grouping2 = grouping2 > 0 ? grouping2 : grouping1; - // TODO: Is it important to handle minGrouping > 2? - return getInstance(grouping1, grouping2, minGrouping == 2); - } - - /** - * @internal - * @deprecated This API is ICU internal only. - */ - @Deprecated - public Grouper withLocaleData(ParsedPatternInfo patternInfo) { - if (grouping1 != -2) { - return this; - } - // TODO: short or byte? - byte grouping1 = (byte) (patternInfo.positive.groupingSizes & 0xffff); - byte grouping2 = (byte) ((patternInfo.positive.groupingSizes >>> 16) & 0xffff); - byte grouping3 = (byte) ((patternInfo.positive.groupingSizes >>> 32) & 0xffff); - if (grouping2 == -1) { - grouping1 = -1; - } - if (grouping3 == -1) { - grouping2 = grouping1; - } - return getInstance(grouping1, grouping2, min2); - } - - boolean groupAtPosition(int position, DecimalQuantity value) { - assert grouping1 != -2; - if (grouping1 == -1 || grouping1 == 0) { - // Either -1 or 0 means "no grouping" - return false; - } - position -= grouping1; - return position >= 0 - && (position % grouping2) == 0 - && value.getUpperDisplayMagnitude() - grouping1 + 1 >= (min2 ? 2 : 1); - } - - /** - * @internal - * @deprecated This API is ICU internal only. - */ - @Deprecated - public byte getPrimary() { - return grouping1; - } - - /** - * @internal - * @deprecated This API is ICU internal only. - */ - @Deprecated - public byte getSecondary() { - return grouping2; - } -} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java index 7a819b18ae..d66cb2afde 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java @@ -161,15 +161,119 @@ public final class NumberFormatter { } /** - * An enum declaring how to denote positive and negative numbers. Example outputs when formatting 123 - * and -123 in en-US: + * An enum declaring the strategy for when and how to display grouping separators (i.e., the + * separator, often a comma or period, after every 2-3 powers of ten). The choices are several + * pre-built strategies for different use cases that employ locale data whenever possible. Example + * outputs for 1234 and 1234567 in en-IN: * *
    - *
  • AUTO: "123" and "-123" - *
  • ALWAYS: "+123" and "-123" - *
  • NEVER: "123" and "123" - *
  • ACCOUNTING: "$123" and "($123)" - *
  • ACCOUNTING_ALWAYS: "+$123" and "($123)" + *
  • OFF: 1234 and 12345 + *
  • MIN2: 1234 and 12,34,567 + *
  • AUTO: 1,234 and 12,34,567 + *
  • ON_ALIGNED: 1,234 and 12,34,567 + *
  • WESTERN: 1,234 and 1,234,567 + *
+ * + *

+ * The default is AUTO, which displays grouping separators unless the locale data says that grouping + * is not customary. To force grouping for all numbers greater than 1000 consistently across locales, + * use ON_ALIGNED. On the other hand, to display grouping less frequently than the default, use MIN2 + * or OFF. See the docs of each option for details. + * + *

+ * Note: This enum specifies the strategy for grouping sizes. To set which character to use as the + * grouping separator, use the "symbols" setter. + * + * @draft ICU 61 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + */ + public static enum GroupingStrategy { + /** + * Do not display grouping separators in any locale. + * + * @draft ICU 61 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + */ + OFF, + + /** + * Display grouping using locale defaults, except do not show grouping on values smaller than + * 10000 (such that there is a minimum of two digits before the first separator). + * + *

+ * Note that locales may restrict grouping separators to be displayed only on 1 million or + * greater (for example, ee and hu) or disable grouping altogether (for example, bg currency). + * + *

+ * Locale data is used to determine whether to separate larger numbers into groups of 2 + * (customary in South Asia) or groups of 3 (customary in Europe and the Americas). + * + * @draft ICU 61 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + */ + MIN2, + + /** + * Display grouping using the default strategy for all locales. This is the default behavior. + * + *

+ * Note that locales may restrict grouping separators to be displayed only on 1 million or + * greater (for example, ee and hu) or disable grouping altogether (for example, bg currency). + * + *

+ * Locale data is used to determine whether to separate larger numbers into groups of 2 + * (customary in South Asia) or groups of 3 (customary in Europe and the Americas). + * + * @draft ICU 61 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + */ + AUTO, + + /** + * Always display the grouping separator on values of at least 1000. + * + *

+ * This option ignores the locale data that restricts or disables grouping, described in MIN2 and + * AUTO. This option may be useful to normalize the alignment of numbers, such as in a + * spreadsheet. + * + *

+ * Locale data is used to determine whether to separate larger numbers into groups of 2 + * (customary in South Asia) or groups of 3 (customary in Europe and the Americas). + * + * @draft ICU 61 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + */ + ON_ALIGNED, + + /** + * Use the Western defaults: groups of 3 and enabled for all numbers 1000 or greater. Do not use + * locale data for determining the grouping strategy. + * + * @draft ICU 61 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + */ + WESTERN + } + + /** + * An enum declaring how to denote positive and negative numbers. Example outputs when formatting + * 123, 0, and -123 in en-US: + * + *

    + *
  • AUTO: "123", "0", and "-123" + *
  • ALWAYS: "+123", "+0", and "-123" + *
  • NEVER: "123", "0", and "123" + *
  • ACCOUNTING: "$123", "$0", and "($123)" + *
  • ACCOUNTING_ALWAYS: "+$123", "+$0", and "($123)" + *
  • EXCEPT_ZERO: "+123", "0", and "-123" + *
  • ACCOUNTING_EXCEPT_ZERO: "+$123", "$0", and "($123)" *
* *

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 d612ab27bf..c0cf83f2de 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 @@ -2,9 +2,12 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.number; +import com.ibm.icu.impl.CurrencyData; +import com.ibm.icu.impl.CurrencyData.CurrencyFormatInfo; import com.ibm.icu.impl.number.CompactData.CompactType; import com.ibm.icu.impl.number.ConstantAffixModifier; import com.ibm.icu.impl.number.DecimalQuantity; +import com.ibm.icu.impl.number.Grouper; import com.ibm.icu.impl.number.LongNameHandler; import com.ibm.icu.impl.number.MacroProps; import com.ibm.icu.impl.number.MicroProps; @@ -15,6 +18,7 @@ import com.ibm.icu.impl.number.Padder; import com.ibm.icu.impl.number.PatternStringParser; import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo; import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay; +import com.ibm.icu.number.NumberFormatter.GroupingStrategy; import com.ibm.icu.number.NumberFormatter.SignDisplay; import com.ibm.icu.number.NumberFormatter.UnitWidth; import com.ibm.icu.text.DecimalFormatSymbols; @@ -134,35 +138,49 @@ class NumberFormatterImpl { } String nsName = ns.getName(); - // Load and parse the pattern string. It is used for grouping sizes and affixes only. - int patternStyle; - if (isPercent || isPermille) { - patternStyle = NumberFormat.PERCENTSTYLE; - } else if (!isCurrency || unitWidth == UnitWidth.FULL_NAME) { - patternStyle = NumberFormat.NUMBERSTYLE; - } else if (isAccounting) { - // NOTE: Although ACCOUNTING and ACCOUNTING_ALWAYS are only supported in currencies right - // now, - // the API contract allows us to add support to other units in the future. - patternStyle = NumberFormat.ACCOUNTINGCURRENCYSTYLE; - } else { - patternStyle = NumberFormat.CURRENCYSTYLE; - } - String pattern = NumberFormat - .getPatternForStyleAndNumberingSystem(macros.loc, nsName, patternStyle); - ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(pattern); - - ///////////////////////////////////////////////////////////////////////////////////// - /// START POPULATING THE DEFAULT MICROPROPS AND BUILDING THE MICROPROPS GENERATOR /// - ///////////////////////////////////////////////////////////////////////////////////// - - // Symbols + // Resolve the symbols. Do this here because currency may need to customize them. if (macros.symbols instanceof DecimalFormatSymbols) { micros.symbols = (DecimalFormatSymbols) macros.symbols; } else { micros.symbols = DecimalFormatSymbols.forNumberingSystem(macros.loc, ns); } + // Load and parse the pattern string. It is used for grouping sizes and affixes only. + // If we are formatting currency, check for a currency-specific pattern. + String pattern = null; + if (isCurrency) { + CurrencyFormatInfo info = CurrencyData.provider.getInstance(macros.loc, true) + .getFormatInfo(currency.getCurrencyCode()); + if (info != null) { + pattern = info.currencyPattern; + // It's clunky to clone an object here, but this code is not frequently executed. + micros.symbols = (DecimalFormatSymbols) micros.symbols.clone(); + micros.symbols.setMonetaryDecimalSeparatorString(info.monetaryDecimalSeparator); + micros.symbols.setMonetaryGroupingSeparatorString(info.monetaryGroupingSeparator); + } + } + if (pattern == null) { + int patternStyle; + if (isPercent || isPermille) { + patternStyle = NumberFormat.PERCENTSTYLE; + } else if (!isCurrency || unitWidth == UnitWidth.FULL_NAME) { + patternStyle = NumberFormat.NUMBERSTYLE; + } else if (isAccounting) { + // NOTE: Although ACCOUNTING and ACCOUNTING_ALWAYS are only supported in currencies + // right now, the API contract allows us to add support to other units in the future. + patternStyle = NumberFormat.ACCOUNTINGCURRENCYSTYLE; + } else { + patternStyle = NumberFormat.CURRENCYSTYLE; + } + pattern = NumberFormat + .getPatternForStyleAndNumberingSystem(macros.loc, nsName, patternStyle); + } + ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(pattern); + + ///////////////////////////////////////////////////////////////////////////////////// + /// START POPULATING THE DEFAULT MICROPROPS AND BUILDING THE MICROPROPS GENERATOR /// + ///////////////////////////////////////////////////////////////////////////////////// + // Multiplier (compatibility mode value). if (macros.multiplier != null) { chain = macros.multiplier.copyAndChain(chain); @@ -181,15 +199,17 @@ class NumberFormatterImpl { micros.rounding = micros.rounding.withLocaleData(currency); // Grouping strategy - if (macros.grouper != null) { - micros.grouping = macros.grouper; + if (macros.grouping instanceof Grouper) { + micros.grouping = (Grouper) macros.grouping; + } else if (macros.grouping instanceof GroupingStrategy) { + micros.grouping = Grouper.forStrategy((GroupingStrategy) macros.grouping); } else if (macros.notation instanceof CompactNotation) { // Compact notation uses minGrouping by default since ICU 59 - micros.grouping = Grouper.minTwoDigits(); + micros.grouping = Grouper.forStrategy(GroupingStrategy.MIN2); } else { - micros.grouping = Grouper.defaults(); + micros.grouping = Grouper.forStrategy(GroupingStrategy.AUTO); } - micros.grouping = micros.grouping.withLocaleData(patternInfo); + micros.grouping = micros.grouping.withLocaleData(macros.loc, patternInfo); // Padding strategy if (macros.padder != null) { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java index df5d94bca9..0143f2e48b 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java @@ -5,6 +5,7 @@ package com.ibm.icu.number; import com.ibm.icu.impl.number.MacroProps; import com.ibm.icu.impl.number.Padder; import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay; +import com.ibm.icu.number.NumberFormatter.GroupingStrategy; import com.ibm.icu.number.NumberFormatter.SignDisplay; import com.ibm.icu.number.NumberFormatter.UnitWidth; import com.ibm.icu.text.DecimalFormatSymbols; @@ -31,7 +32,7 @@ public abstract class NumberFormatterSettings - * Pass this method the return value of one of the factory methods on {@link Grouper}. For example: + * Pass this method an element from the {@link GroupingStrategy} enum. For example: * *

-     * NumberFormatter.with().grouping(Grouper.min2())
+     * NumberFormatter.with().grouping(GroupingStrategy.MIN2)
      * 
* - * The default is to perform grouping without concern for the minimum grouping digits. + * The default is to perform grouping according to locale data; most locales, but not all locales, + * enable it by default. * - * @param grouper + * @param strategy * The grouping strategy to use. * @return The fluent chain. - * @see Grouper - * @see Notation - * @internal - * @deprecated ICU 60 This API is technical preview; see #7861. + * @see GroupingStrategy + * @draft ICU 61 + * @provisional This API might change or be removed in a future release. */ - @Deprecated - public T grouping(Grouper grouper) { - return create(KEY_GROUPER, grouper); + public T grouping(GroupingStrategy strategy) { + return create(KEY_GROUPING, strategy); } /** @@ -512,9 +512,9 @@ public abstract class NumberFormatterSettings EQUIVALENT_CURRENCY_SYMBOLS = @@ -568,8 +563,8 @@ public class Currency extends MeasureUnit { * currency object in the en_US locale is "$". * @param locale locale in which to display currency * @param nameStyle selector for which kind of name to return. - * The nameStyle should be either SYMBOL_NAME or - * LONG_NAME. Otherwise, throw IllegalArgumentException. + * The nameStyle should be SYMBOL_NAME, NARROW_SYMBOL_NAME, + * or LONG_NAME. Otherwise, throw IllegalArgumentException. * @param isChoiceFormat fill-in; isChoiceFormat[0] is set to true * if the returned value is a ChoiceFormat pattern; otherwise it * is set to false @@ -597,13 +592,7 @@ public class Currency extends MeasureUnit { case SYMBOL_NAME: return names.getSymbol(subType); case NARROW_SYMBOL_NAME: - // CurrencyDisplayNames is the public interface. - // CurrencyDisplayInfo is ICU's standard implementation. - if (!(names instanceof CurrencyDisplayInfo)) { - throw new UnsupportedOperationException( - "Cannot get narrow symbol from custom currency display name provider"); - } - return ((CurrencyDisplayInfo) names).getNarrowSymbol(subType); + return names.getNarrowSymbol(subType); case LONG_NAME: return names.getName(subType); default: diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt b/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt index 5d3deb758f..e358953ac6 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt @@ -465,9 +465,8 @@ output grouping breaks grouping2 minGroupingDigits 1,2345,6789 4 1,23,45,6789 4 K 2 1,23,45,6789 4 K 2 2 -// Q only supports minGrouping<=2 123,456789 6 6 3 -123456789 6 JKQ 6 4 +123456789 6 JK 6 4 test multiplier setters set locale en_US 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 8a85f23a4d..f390d1e040 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 @@ -5330,6 +5330,33 @@ public class NumberFormatTest extends TestFmwk { assertEquals("Grouping should be off", false, df.isGroupingUsed()); } + @Test + public void Test11035_FormatCurrencyAmount() { + double amount = 12345.67; + String expected = "12,345$67 ​"; + Currency cur = Currency.getInstance("PTE"); + + // Test three ways to set currency via API + + ULocale loc1 = new ULocale("pt_PT"); + NumberFormat fmt1 = NumberFormat.getCurrencyInstance(loc1); + fmt1.setCurrency(cur); + String actualSetCurrency = fmt1.format(amount); + + ULocale loc2 = new ULocale("pt_PT@currency=PTE"); + NumberFormat fmt2 = NumberFormat.getCurrencyInstance(loc2); + String actualLocaleString = fmt2.format(amount); + + ULocale loc3 = new ULocale("pt_PT"); + NumberFormat fmt3 = NumberFormat.getCurrencyInstance(loc3); + CurrencyAmount curAmt = new CurrencyAmount(amount, cur); + String actualCurrencyAmount = fmt3.format(curAmt); + + assertEquals("Custom Currency Pattern, Set Currency", expected, actualSetCurrency); + assertEquals("Custom Currency Pattern, Locale String", expected, actualCurrencyAmount); + assertEquals("Custom Currency Pattern, CurrencyAmount", expected, actualLocaleString); + } + @Test public void testPercentZero() { DecimalFormat df = (DecimalFormat) NumberFormat.getPercentInstance(); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ModifierTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ModifierTest.java index 10d768080a..fee90dae83 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ModifierTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ModifierTest.java @@ -31,12 +31,12 @@ public class ModifierTest { public void testConstantMultiFieldModifier() { NumberStringBuilder prefix = new NumberStringBuilder(); NumberStringBuilder suffix = new NumberStringBuilder(); - Modifier mod1 = new ConstantMultiFieldModifier(prefix, suffix, true); + Modifier mod1 = new ConstantMultiFieldModifier(prefix, suffix, false, true); assertModifierEquals(mod1, 0, true, "|", "n"); prefix.append("a📻", NumberFormat.Field.PERCENT); suffix.append("b", NumberFormat.Field.CURRENCY); - Modifier mod2 = new ConstantMultiFieldModifier(prefix, suffix, true); + Modifier mod2 = new ConstantMultiFieldModifier(prefix, suffix, false, true); assertModifierEquals(mod2, 3, true, "a📻|b", "%%%n$"); // Make sure the first modifier is still the same (that it stayed constant) @@ -91,11 +91,11 @@ public class ModifierTest { DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH); NumberStringBuilder prefix = new NumberStringBuilder(); NumberStringBuilder suffix = new NumberStringBuilder(); - Modifier mod1 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols); + Modifier mod1 = new CurrencySpacingEnabledModifier(prefix, suffix, false, true, symbols); assertModifierEquals(mod1, 0, true, "|", "n"); prefix.append("USD", NumberFormat.Field.CURRENCY); - Modifier mod2 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols); + Modifier mod2 = new CurrencySpacingEnabledModifier(prefix, suffix, false, true, symbols); assertModifierEquals(mod2, 3, true, "USD|", "$$$n"); // Test the default currency spacing rules @@ -116,7 +116,7 @@ public class ModifierTest { true, "[|]"); suffix.append("XYZ", NumberFormat.Field.CURRENCY); - Modifier mod3 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols); + Modifier mod3 = new CurrencySpacingEnabledModifier(prefix, suffix, false, true, symbols); assertModifierEquals(mod3, 3, true, "USD|\u00A0XYZ", "$$$nn$$$"); } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MutablePatternModifierTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MutablePatternModifierTest.java index 7069073c14..7f70a55415 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MutablePatternModifierTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MutablePatternModifierTest.java @@ -103,6 +103,32 @@ public class MutablePatternModifierTest { assertFalse(nsb1 + " vs. " + nsb3, nsb1.contentEquals(nsb3)); } + @Test + public void patternWithNoPlaceholder() { + MutablePatternModifier mod = new MutablePatternModifier(false); + mod.setPatternInfo(PatternStringParser.parseToPatternInfo("abc")); + mod.setPatternAttributes(SignDisplay.AUTO, false); + mod.setSymbols(DecimalFormatSymbols.getInstance(ULocale.ENGLISH), + Currency.getInstance("USD"), + UnitWidth.SHORT, + null); + mod.setNumberProperties(1, null); + + // Unsafe Code Path + NumberStringBuilder nsb = new NumberStringBuilder(); + nsb.append("x123y", null); + mod.apply(nsb, 1, 4); + assertEquals("Unsafe Path", "xabcy", nsb.toString()); + + // Safe Code Path + nsb.clear(); + nsb.append("x123y", null); + MicroProps micros = new MicroProps(false); + mod.createImmutable().applyToMicros(micros, new DecimalQuantity_DualStorageBCD()); + micros.modMiddle.apply(nsb, 1, 4); + assertEquals("Safe Path", "xabcy", nsb.toString()); + } + private static String getPrefix(MutablePatternModifier mod) { NumberStringBuilder nsb = new NumberStringBuilder(); mod.apply(nsb, 0, 0); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java index e117875264..0bfca024c1 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java @@ -19,17 +19,20 @@ import java.util.Set; import org.junit.Ignore; import org.junit.Test; +import com.ibm.icu.impl.number.Grouper; +import com.ibm.icu.impl.number.MacroProps; import com.ibm.icu.impl.number.Padder; import com.ibm.icu.impl.number.Padder.PadPosition; import com.ibm.icu.impl.number.PatternStringParser; +import com.ibm.icu.number.CompactNotation; import com.ibm.icu.number.FormattedNumber; import com.ibm.icu.number.FractionRounder; -import com.ibm.icu.number.Grouper; import com.ibm.icu.number.IntegerWidth; import com.ibm.icu.number.LocalizedNumberFormatter; import com.ibm.icu.number.Notation; import com.ibm.icu.number.NumberFormatter; import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay; +import com.ibm.icu.number.NumberFormatter.GroupingStrategy; import com.ibm.icu.number.NumberFormatter.SignDisplay; import com.ibm.icu.number.NumberFormatter.UnitWidth; import com.ibm.icu.number.Rounder; @@ -51,6 +54,8 @@ public class NumberFormatterApiTest { private static final Currency GBP = Currency.getInstance("GBP"); private static final Currency CZK = Currency.getInstance("CZK"); private static final Currency CAD = Currency.getInstance("CAD"); + private static final Currency ESP = Currency.getInstance("ESP"); + private static final Currency PTE = Currency.getInstance("PTE"); @Test public void notationSimple() { @@ -354,6 +359,19 @@ public class NumberFormatterApiTest { ULocale.ENGLISH, 9990000, "10M"); + + Map> compactCustomData = new HashMap>(); + Map entry = new HashMap(); + entry.put("one", "Kun"); + entry.put("other", "0KK"); + compactCustomData.put("1000", entry); + assertFormatSingle( + "Compact Somali No Figure", + "", + NumberFormatter.with().notation(CompactNotation.forCustomData(compactCustomData)), + ULocale.ENGLISH, + 1000, + "Kun"); } @Test @@ -645,9 +663,59 @@ public class NumberFormatterApiTest { "Currency Difference between Narrow and Short (Short Version)", "", NumberFormatter.with().unit(USD).unitWidth(UnitWidth.SHORT), - ULocale.forLanguageTag("en_CA"), + ULocale.forLanguageTag("en-CA"), 5.43, - "US$ 5.43"); + "US$5.43"); + + assertFormatSingle( + "Currency-dependent format (Control)", + "", + NumberFormatter.with().unit(USD).unitWidth(UnitWidth.SHORT), + ULocale.forLanguageTag("ca"), + 444444.55, + "444.444,55 USD"); + + assertFormatSingle( + "Currency-dependent format (Test)", + "", + NumberFormatter.with().unit(ESP).unitWidth(UnitWidth.SHORT), + ULocale.forLanguageTag("ca"), + 444444.55, + "₧ 444.445"); + + assertFormatSingle( + "Currency-dependent symbols (Control)", + "", + NumberFormatter.with().unit(USD).unitWidth(UnitWidth.SHORT), + ULocale.forLanguageTag("pt-PT"), + 444444.55, + "444 444,55 US$"); + + // NOTE: This is a bit of a hack on CLDR's part. They set the currency symbol to U+200B (zero- + // width space), and they set the decimal separator to the $ symbol. + assertFormatSingle( + "Currency-dependent symbols (Test)", + "", + NumberFormatter.with().unit(PTE).unitWidth(UnitWidth.SHORT), + ULocale.forLanguageTag("pt-PT"), + 444444.55, + "444,444$55 \u200B"); + + assertFormatSingle( + "Currency-dependent symbols (Test)", + "", + NumberFormatter.with().unit(PTE).unitWidth(UnitWidth.NARROW), + ULocale.forLanguageTag("pt-PT"), + 444444.55, + "444,444$55 PTE"); + + assertFormatSingle( + "Currency-dependent symbols (Test)", + "", + NumberFormatter.with().unit(PTE).unitWidth(UnitWidth.ISO_CODE), + ULocale.forLanguageTag("pt-PT"), + 444444.55, + "444,444$55 PTE"); } @Test @@ -889,6 +957,22 @@ public class NumberFormatterApiTest { "0.09", "0.01", "0.00"); + + assertFormatSingle( + "FracSig with trailing zeros A", + "", + NumberFormatter.with().rounding(Rounder.fixedFraction(2).withMinDigits(3)), + ULocale.ENGLISH, + 0.1, + "0.10"); + + assertFormatSingle( + "FracSig with trailing zeros B", + "", + NumberFormatter.with().rounding(Rounder.fixedFraction(2).withMinDigits(3)), + ULocale.ENGLISH, + 0.0999999, + "0.10"); } @Test @@ -1020,7 +1104,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Western Grouping", "grouping=defaults", - NumberFormatter.with().grouping(Grouper.defaults()), + NumberFormatter.with().grouping(GroupingStrategy.AUTO), ULocale.ENGLISH, "87,650,000", "8,765,000", @@ -1035,7 +1119,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Indic Grouping", "grouping=defaults", - NumberFormatter.with().grouping(Grouper.defaults()), + NumberFormatter.with().grouping(GroupingStrategy.AUTO), new ULocale("en-IN"), "8,76,50,000", "87,65,000", @@ -1050,7 +1134,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Western Grouping, Min 2", "grouping=min2", - NumberFormatter.with().grouping(Grouper.minTwoDigits()), + NumberFormatter.with().grouping(GroupingStrategy.MIN2), ULocale.ENGLISH, "87,650,000", "8,765,000", @@ -1065,7 +1149,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Indic Grouping, Min 2", "grouping=min2", - NumberFormatter.with().grouping(Grouper.minTwoDigits()), + NumberFormatter.with().grouping(GroupingStrategy.MIN2), new ULocale("en-IN"), "8,76,50,000", "87,65,000", @@ -1080,7 +1164,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "No Grouping", "grouping=none", - NumberFormatter.with().grouping(Grouper.none()), + NumberFormatter.with().grouping(GroupingStrategy.OFF), new ULocale("en-IN"), "87650000", "8765000", @@ -1091,6 +1175,102 @@ public class NumberFormatterApiTest { "87.65", "8.765", "0"); + + // NOTE: Hungarian is interesting because it has minimumGroupingDigits=4 in locale data + // If this test breaks due to data changes, find another locale that has minimumGroupingDigits. + assertFormatDescendingBig( + "Hungarian Grouping", + "", + NumberFormatter.with().grouping(GroupingStrategy.AUTO), + new ULocale("hu"), + "87 650 000", + "8 765 000", + "876500", + "87650", + "8765", + "876,5", + "87,65", + "8,765", + "0"); + + assertFormatDescendingBig( + "Hungarian Grouping, Min 2", + "", + NumberFormatter.with().grouping(GroupingStrategy.MIN2), + new ULocale("hu"), + "87 650 000", + "8 765 000", + "876500", + "87650", + "8765", + "876,5", + "87,65", + "8,765", + "0"); + + assertFormatDescendingBig( + "Hungarian Grouping, Always", + "", + NumberFormatter.with().grouping(GroupingStrategy.ON_ALIGNED), + new ULocale("hu"), + "87 650 000", + "8 765 000", + "876 500", + "87 650", + "8 765", + "876,5", + "87,65", + "8,765", + "0"); + + // NOTE: Bulgarian is interesting because it has no grouping in the default currency format. + // If this test breaks due to data changes, find another locale that has no default grouping. + assertFormatDescendingBig( + "Bulgarian Currency Grouping", + "", + NumberFormatter.with().grouping(GroupingStrategy.AUTO).unit(USD), + new ULocale("bg"), + "87650000,00 щ.д.", + "8765000,00 щ.д.", + "876500,00 щ.д.", + "87650,00 щ.д.", + "8765,00 щ.д.", + "876,50 щ.д.", + "87,65 щ.д.", + "8,76 щ.д.", + "0,00 щ.д."); + + assertFormatDescendingBig( + "Bulgarian Currency Grouping, Always", + "", + NumberFormatter.with().grouping(GroupingStrategy.ON_ALIGNED).unit(USD), + new ULocale("bg"), + "87 650 000,00 щ.д.", + "8 765 000,00 щ.д.", + "876 500,00 щ.д.", + "87 650,00 щ.д.", + "8 765,00 щ.д.", + "876,50 щ.д.", + "87,65 щ.д.", + "8,76 щ.д.", + "0,00 щ.д."); + + MacroProps macros = new MacroProps(); + macros.grouping = Grouper.getInstance((short) 4, (short) 1, (short) 3); + assertFormatDescendingBig( + "Custom Grouping via Internal API", + "", + NumberFormatter.with().macros(macros), + ULocale.ENGLISH, + "8,7,6,5,0000", + "8,7,6,5000", + "876500", + "87650", + "8765", + "876.5", + "87.65", + "8.765", + "0"); } @Test diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/CurrencyTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/CurrencyTest.java index 9e4347feb7..8a96666927 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/CurrencyTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/CurrencyTest.java @@ -190,17 +190,54 @@ public class CurrencyTest extends TestFmwk { // Do a basic check of getName() // USD { "US$", "US Dollar" } // 04/04/1792- ULocale en = ULocale.ENGLISH; + ULocale en_CA = ULocale.forLanguageTag("en-CA"); + ULocale en_US = ULocale.forLanguageTag("en-US"); + ULocale en_NZ = ULocale.forLanguageTag("en-NZ"); boolean[] isChoiceFormat = new boolean[1]; - Currency usd = Currency.getInstance("USD"); + Currency USD = Currency.getInstance("USD"); + Currency CAD = Currency.getInstance("CAD"); + Currency USX = Currency.getInstance("USX"); // Warning: HARD-CODED LOCALE DATA in this test. If it fails, CHECK // THE LOCALE DATA before diving into the code. - assertEquals("USD.getName(SYMBOL_NAME)", + assertEquals("USD.getName(SYMBOL_NAME, en)", "$", - usd.getName(en, Currency.SYMBOL_NAME, isChoiceFormat)); - assertEquals("USD.getName(LONG_NAME)", + USD.getName(en, Currency.SYMBOL_NAME, isChoiceFormat)); + assertEquals("USD.getName(NARROW_SYMBOL_NAME, en)", + "$", + USD.getName(en, Currency.NARROW_SYMBOL_NAME, isChoiceFormat)); + assertEquals("USD.getName(LONG_NAME, en)", "US Dollar", - usd.getName(en, Currency.LONG_NAME, isChoiceFormat)); - // TODO add more tests later + USD.getName(en, Currency.LONG_NAME, isChoiceFormat)); + assertEquals("CAD.getName(SYMBOL_NAME, en)", + "CA$", + CAD.getName(en, Currency.SYMBOL_NAME, isChoiceFormat)); + assertEquals("CAD.getName(NARROW_SYMBOL_NAME, en)", + "$", + CAD.getName(en, Currency.NARROW_SYMBOL_NAME, isChoiceFormat)); + assertEquals("CAD.getName(SYMBOL_NAME, en_CA)", + "$", + CAD.getName(en_CA, Currency.SYMBOL_NAME, isChoiceFormat)); + assertEquals("USD.getName(SYMBOL_NAME, en_CA)", + "US$", + USD.getName(en_CA, Currency.SYMBOL_NAME, isChoiceFormat)); + assertEquals("USD.getName(NARROW_SYMBOL_NAME, en_CA)", + "$", + USD.getName(en_CA, Currency.NARROW_SYMBOL_NAME, isChoiceFormat)); + assertEquals("USD.getName(SYMBOL_NAME) in en_NZ", + "US$", + USD.getName(en_NZ, Currency.SYMBOL_NAME, isChoiceFormat)); + assertEquals("CAD.getName(SYMBOL_NAME)", + "CA$", + CAD.getName(en_NZ, Currency.SYMBOL_NAME, isChoiceFormat)); + assertEquals("USX.getName(SYMBOL_NAME)", + "USX", + USX.getName(en_US, Currency.SYMBOL_NAME, isChoiceFormat)); + assertEquals("USX.getName(NARROW_SYMBOL_NAME)", + "USX", + USX.getName(en_US, Currency.NARROW_SYMBOL_NAME, isChoiceFormat)); + assertEquals("USX.getName(LONG_NAME)", + "USX", + USX.getName(en_US, Currency.LONG_NAME, isChoiceFormat)); } @Test