diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java index 5b45b47863b..7fd26ca9e4c 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java @@ -13,13 +13,13 @@ import java.util.concurrent.ConcurrentHashMap; import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.impl.TextTrieMap; -import com.ibm.icu.impl.number.Parse.ParseMode; import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier; import com.ibm.icu.impl.number.formatters.CurrencyFormat; import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier; import com.ibm.icu.impl.number.formatters.PaddingFormat; import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat; import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat; +import com.ibm.icu.impl.number.formatters.ScientificFormat; import com.ibm.icu.lang.UCharacter; import com.ibm.icu.text.CurrencyPluralInfo; import com.ibm.icu.text.DecimalFormatSymbols; @@ -102,7 +102,8 @@ public class Parse { CurrencyFormat.ICurrencyProperties, BigDecimalMultiplier.IProperties, MagnitudeMultiplier.IProperties, - PositiveDecimalFormat.IProperties { + PositiveDecimalFormat.IProperties, + ScientificFormat.IProperties { boolean DEFAULT_PARSE_INTEGER_ONLY = false; @@ -199,7 +200,7 @@ public class Parse { } /** - * @see #parse(String, ParsePosition, ParseMode, boolean, boolean, IProperties, + * @see Parse#parse(String, ParsePosition, ParseMode, boolean, boolean, IProperties, * DecimalFormatSymbols) */ private static enum StateName { @@ -303,6 +304,7 @@ public class Parse { boolean sawPrefix; boolean sawSuffix; boolean sawDecimalPoint; + boolean sawExponentDigit; // Data for intermediate parsing steps: StateName returnTo1; @@ -310,6 +312,7 @@ public class Parse { // For string literals: CharSequence currentString; int currentOffset; + boolean currentTrailing; // For affix patterns: CharSequence currentAffixPattern; long currentStepwiseParserTag; @@ -349,12 +352,14 @@ public class Parse { sawPrefix = false; sawSuffix = false; sawDecimalPoint = false; + sawExponentDigit = false; // Data for intermediate parsing steps: returnTo1 = null; returnTo2 = null; currentString = null; currentOffset = 0; + currentTrailing = false; currentAffixPattern = null; currentStepwiseParserTag = 0L; currentCurrencyTrieState = null; @@ -404,12 +409,14 @@ public class Parse { sawPrefix = other.sawPrefix; sawSuffix = other.sawSuffix; sawDecimalPoint = other.sawDecimalPoint; + sawExponentDigit = other.sawExponentDigit; // Data for intermediate parsing steps: returnTo1 = other.returnTo1; returnTo2 = other.returnTo2; currentString = other.currentString; currentOffset = other.currentOffset; + currentTrailing = other.currentTrailing; currentAffixPattern = other.currentAffixPattern; currentStepwiseParserTag = other.currentStepwiseParserTag; currentCurrencyTrieState = other.currentCurrencyTrieState; @@ -427,6 +434,7 @@ public class Parse { */ void appendDigit(byte digit, DigitType type) { if (type == DigitType.EXPONENT) { + sawExponentDigit = true; int newExponent = exponent * 10 + digit; if (newExponent < exponent) { // overflow @@ -606,7 +614,11 @@ public class Parse { int groupingCp2; SeparatorType decimalType1; SeparatorType decimalType2; + // TODO(sffc): Remove this field if it is not necessary. + @SuppressWarnings("unused") SeparatorType groupingType1; + // TODO(sffc): Remove this field if it is not necessary. + @SuppressWarnings("unused") SeparatorType groupingType2; TextTrieMap digitTrie; Set affixHolders = new HashSet(); @@ -1299,6 +1311,12 @@ public class Parse { continue; } + // Check for scientific notation. + if (properties.getMinimumExponentDigits() > 0 && !item.sawExponentDigit) { + if (DEBUGGING) System.out.println("-> reject due to lack of exponent"); + continue; + } + // Check that grouping sizes are valid. int grouping1 = properties.getGroupingSize(); int grouping2 = properties.getSecondaryGroupingSize(); @@ -1621,7 +1639,7 @@ public class Parse { private static void acceptNan(int cp, StateName nextName, ParserState state, StateItem item) { CharSequence nan = state.symbols.getNaN(); - long added = acceptString(cp, nextName, null, state, item, nan, 0); + long added = acceptString(cp, nextName, null, state, item, nan, 0, false); // Set state in the items that were added by the function call for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) { @@ -1634,7 +1652,7 @@ public class Parse { private static void acceptInfinity( int cp, StateName nextName, ParserState state, StateItem item) { CharSequence inf = state.symbols.getInfinity(); - long added = acceptString(cp, nextName, null, state, item, inf, 0); + long added = acceptString(cp, nextName, null, state, item, inf, 0, false); // Set state in the items that were added by the function call for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) { @@ -1647,7 +1665,7 @@ public class Parse { private static void acceptExponentSeparator( int cp, StateName nextName, ParserState state, StateItem item) { CharSequence exp = state.symbols.getExponentSeparator(); - acceptString(cp, nextName, null, state, item, exp, 0); + acceptString(cp, nextName, null, state, item, exp, 0, true); } private static void acceptPrefix(int cp, StateName nextName, ParserState state, StateItem item) { @@ -1676,7 +1694,7 @@ public class Parse { if (holder == null) return; String str = prefix ? holder.p : holder.s; if (holder.strings) { - long added = acceptString(cp, nextName, null, state, item, str, 0); + long added = acceptString(cp, nextName, null, state, item, str, 0, false); // At most one item can be added upon consuming a string. if (added != 0) { int i = state.lastInsertedIndex(); @@ -1705,7 +1723,14 @@ public class Parse { private static void acceptStringOffset(int cp, ParserState state, StateItem item) { acceptString( - cp, item.returnTo1, item.returnTo2, state, item, item.currentString, item.currentOffset); + cp, + item.returnTo1, + item.returnTo2, + state, + item, + item.currentString, + item.currentOffset, + item.currentTrailing); } /** @@ -1719,6 +1744,8 @@ public class Parse { * @param returnTo1 The state to return to after reaching the end of the string. * @param returnTo2 The state to save in returnTo1 after reaching the end of the * string. Set to null if returning to the main state loop. + * @param trailing true if this string should be ignored for the purposes of recording trailing + * code points; false if it trailing count should be reset after reading the string. * @param state The current {@link ParserState} * @param item The current {@link StateItem} * @param str The string against which to check for a match. @@ -1733,7 +1760,8 @@ public class Parse { ParserState state, StateItem item, CharSequence str, - int offset) { + int offset, + boolean trailing) { if (str == null || str.length() == 0) return 0L; // Fast path for fast mode @@ -1771,10 +1799,11 @@ public class Parse { next.returnTo2 = returnTo2; next.currentString = str; next.currentOffset = offset; + next.currentTrailing = trailing; } else { // We've reached the end of the string. next.name = returnTo1; - next.trailingCount = 0; + if (!trailing) next.trailingCount = 0; next.returnTo1 = returnTo2; next.returnTo2 = null; } @@ -1832,9 +1861,11 @@ public class Parse { resolvedPlusSign = true; break; case AffixPatternUtils.TYPE_PERCENT: + resolvedCp = '%'; // accept ASCII percent as well as locale percent resolvedStr = state.symbols.getPercentString(); break; case AffixPatternUtils.TYPE_PERMILLE: + resolvedCp = '‰'; // accept ASCII permille as well as locale permille resolvedStr = state.symbols.getPerMillString(); break; case AffixPatternUtils.TYPE_CURRENCY_SINGLE: @@ -1905,9 +1936,10 @@ public class Parse { // String symbol if (hasNext) { added |= - acceptString(cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item, resolvedStr, 0); + acceptString( + cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item, resolvedStr, 0, false); } else { - added |= acceptString(cp, returnTo, null, state, item, resolvedStr, 0); + added |= acceptString(cp, returnTo, null, state, item, resolvedStr, 0, false); } } if (resolvedCurrency) { @@ -1965,8 +1997,8 @@ public class Parse { str1 = state.symbols.getCurrencySymbol(); str2 = state.symbols.getInternationalCurrencySymbol(); } - added |= acceptString(cp, returnTo1, returnTo2, state, item, str1, 0); - added |= acceptString(cp, returnTo1, returnTo2, state, item, str2, 0); + added |= acceptString(cp, returnTo1, returnTo2, state, item, str1, 0, false); + added |= acceptString(cp, returnTo1, returnTo2, state, item, str2, 0, false); for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) { if (((1L << i) & added) != 0) { state.getItem(i).sawCurrency = true; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java index ad5c2872a5d..c5a3068ae19 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java @@ -556,7 +556,6 @@ public class PatternString { int exponentDigits = 0; boolean hasPercentSign = false; boolean hasPerMilleSign = false; - boolean hasCurrencySign = false; StringBuilder padding = new StringBuilder(); StringBuilder prefix = new StringBuilder(); @@ -690,7 +689,7 @@ public class PatternString { break; case '¤': - result.hasCurrencySign = true; + // no need to record that we saw it break; } consumeLiteral(state, destination); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java index 9abc90c2fd7..7818b2bcf29 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java @@ -617,7 +617,7 @@ public class Properties int count = ois.readInt(); // 2) Read each field by its name and value - for (int i=0; i"); + return result.toString(); + } + + /** + * Appends a string containing properties that differ from the default, but without being + * surrounded by <Properties>. + */ + public void toStringBare(StringBuilder result) { Field[] fields = Properties.class.getDeclaredFields(); for (Field field : fields) { Object myValue, defaultValue; @@ -938,8 +948,6 @@ public class Properties result.append(" " + field.getName() + ":" + myValue); } } - result.append(">"); - return result.toString(); } /** diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java index cd2f076bf1c..19b78eef97d 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java @@ -230,24 +230,6 @@ public abstract class Rounder extends Format.BeforeFormat { input.setIntegerFractionLength(minInt, maxInt, minFrac, maxFrac); } - private static final ThreadLocal threadLocalProperties = - new ThreadLocal() { - @Override - protected Properties initialValue() { - return new Properties(); - } - }; - - /** - * Gets a thread-local property bag that can be used to deliver properties to a constructor. - * Rounders themselves are guaranteed to not internally use a copy of this property bag. - * - * @return A clean, thread-local property bag. - */ - public static Properties getThreadLocalProperties() { - return threadLocalProperties.get().clear(); - } - @Override public void before(FormatQuantity input, ModifierHolder mods) { apply(input); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java index d3d8aef1147..1136a16bc5a 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java @@ -4,6 +4,7 @@ package com.ibm.icu.impl.number.formatters; import java.util.HashMap; import java.util.Map; +import java.util.MissingResourceException; import com.ibm.icu.impl.ICUData; import com.ibm.icu.impl.ICUResourceBundle; @@ -67,16 +68,45 @@ public class CompactDecimalFormat extends Format.BeforeFormat { return new CompactDecimalFormat(symbols, properties); } + private static final int DEFAULT_MIN_SIG = 1; + private static final int DEFAULT_MAX_SIG = 2; + private static final SignificantDigitsMode DEFAULT_SIG_MODE = + SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION; + + private static final ThreadLocal threadLocalProperties = + new ThreadLocal() { + @Override + protected Properties initialValue() { + return new Properties(); + } + }; + private static Rounder getRounder(IProperties properties) { // Use rounding settings if they were specified, or else use the default CDF rounder. - Rounder rounder = RoundingFormat.getDefaultOrNull(properties); + // TODO: Detecting and overriding significant digits here is a bit of a hack, since detection + // is also performed in the "RoundingFormat.getDefaultOrNull" method. + // It would be more elegant to call some sort of "fallback" copy method. + Rounder rounder = null; + if (!SignificantDigitsRounder.useSignificantDigits(properties)) { + rounder = RoundingFormat.getDefaultOrNull(properties); + } if (rounder == null) { - rounder = - SignificantDigitsRounder.getInstance( - SignificantDigitsRounder.getThreadLocalProperties() - .setMinimumSignificantDigits(1) - .setMaximumSignificantDigits(2) - .setSignificantDigitsMode(SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION)); + int _minSig = properties.getMinimumSignificantDigits(); + int _maxSig = properties.getMaximumSignificantDigits(); + SignificantDigitsMode _mode = properties.getSignificantDigitsMode(); + Properties rprops = threadLocalProperties.get().clear(); + // Settings needing possible override: + rprops.setMinimumSignificantDigits(_minSig > 0 ? _minSig : DEFAULT_MIN_SIG); + rprops.setMaximumSignificantDigits(_maxSig > 0 ? _maxSig : DEFAULT_MAX_SIG); + rprops.setSignificantDigitsMode(_mode != null ? _mode : DEFAULT_SIG_MODE); + // TODO: Should copyFrom() be used instead? It requires a cast. + // Settings to copy verbatim: + rprops.setRoundingMode(properties.getRoundingMode()); + rprops.setMinimumFractionDigits(properties.getMinimumFractionDigits()); + rprops.setMaximumFractionDigits(properties.getMaximumFractionDigits()); + rprops.setMinimumIntegerDigits(properties.getMinimumIntegerDigits()); + rprops.setMaximumIntegerDigits(properties.getMaximumIntegerDigits()); + rounder = SignificantDigitsRounder.getInstance(rprops); } return rounder; } @@ -101,17 +131,31 @@ public class CompactDecimalFormat extends Format.BeforeFormat { ULocale ulocale = symbols.getULocale(); CompactDecimalDataSink sink = new CompactDecimalDataSink(data, symbols, fingerprint); String nsName = NumberingSystem.getInstance(ulocale).getName(); - ICUResourceBundle r = + ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, ulocale); - r.getAllItemsWithFallback("NumberElements/" + nsName, sink); + internalPopulateData(nsName, rb, sink, data); + if (data.isEmpty() && fingerprint.compactStyle == CompactStyle.LONG) { + // No long data is available; load short data instead + sink.compactStyle = CompactStyle.SHORT; + internalPopulateData(nsName, rb, sink, data); + } + threadLocalDataCache.get().put(fingerprint, data); + return data; + } + + private static void internalPopulateData( + String nsName, ICUResourceBundle rb, CompactDecimalDataSink sink, CompactDecimalData data) { + try { + rb.getAllItemsWithFallback("NumberElements/" + nsName, sink); + } catch (MissingResourceException e) { + // Fall back to latn + } if (data.isEmpty() && !nsName.equals("latn")) { - r.getAllItemsWithFallback("NumberElements/latn", sink); + rb.getAllItemsWithFallback("NumberElements/latn", sink); } if (sink.exception != null) { throw sink.exception; } - threadLocalDataCache.get().put(fingerprint, data); - return data; } private static PositiveNegativeModifier getDefaultMod( @@ -302,7 +346,7 @@ public class CompactDecimalFormat extends Format.BeforeFormat { currencySymbol = CurrencyFormat.getCurrencySymbol(symbols, properties); } else { compactType = CompactType.DECIMAL; - currencySymbol = symbols.getCurrencySymbol(); // fallback; should remain unused + currencySymbol = ""; // fallback; should remain unused } compactStyle = properties.getCompactStyle(); uloc = symbols.getULocale(); @@ -337,12 +381,12 @@ public class CompactDecimalFormat extends Format.BeforeFormat { private static final class CompactDecimalDataSink extends UResource.Sink { - final CompactDecimalData data; - final DecimalFormatSymbols symbols; - final CompactStyle compactStyle; - final CompactType compactType; - final String currencySymbol; - final PNAffixGenerator pnag; + CompactDecimalData data; + DecimalFormatSymbols symbols; + CompactStyle compactStyle; + CompactType compactType; + String currencySymbol; + PNAffixGenerator pnag; IllegalArgumentException exception; /* diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java index b7575cc0e2f..f84d35a7259 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java @@ -13,6 +13,7 @@ import com.ibm.icu.impl.number.Rounder; import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier; import com.ibm.icu.impl.number.rounders.IncrementRounder; import com.ibm.icu.impl.number.rounders.MagnitudeRounder; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder; import com.ibm.icu.text.CurrencyPluralInfo; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.util.Currency; @@ -288,6 +289,9 @@ public class CurrencyFormat { }; public static Rounder getCurrencyRounder(DecimalFormatSymbols symbols, IProperties properties) { + if (SignificantDigitsRounder.useSignificantDigits(properties)) { + return SignificantDigitsRounder.getInstance(properties); + } Properties cprops = threadLocalProperties.get().clear(); populateCurrencyRounderProperties(cprops, symbols, properties); if (cprops.getRoundingIncrement() != null) { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java index f791ca46748..b4a33dcc8ac 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java @@ -86,12 +86,13 @@ public class PositiveDecimalFormat implements Format.TargetFormat { } public static boolean allowsDecimalPoint(IProperties properties) { - return properties.getDecimalSeparatorAlwaysShown() || properties.getMaximumFractionDigits() != 0; + return properties.getDecimalSeparatorAlwaysShown() + || properties.getMaximumFractionDigits() != 0; } // Properties private final boolean alwaysShowDecimal; - private final int groupingSize; + private final int primaryGroupingSize; private final int secondaryGroupingSize; private final int minimumGroupingDigits; @@ -104,14 +105,10 @@ public class PositiveDecimalFormat implements Format.TargetFormat { private final int codePointZero; public PositiveDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) { - groupingSize = - (properties.getGroupingSize() < 0) - ? properties.getSecondaryGroupingSize() - : properties.getGroupingSize(); - secondaryGroupingSize = - (properties.getSecondaryGroupingSize() < 0) - ? properties.getGroupingSize() - : properties.getSecondaryGroupingSize(); + int _primary = properties.getGroupingSize(); + int _secondary = properties.getSecondaryGroupingSize(); + primaryGroupingSize = _primary > 0 ? _primary : _secondary > 0 ? _secondary : 0; + secondaryGroupingSize = _secondary > 0 ? _secondary : primaryGroupingSize; minimumGroupingDigits = properties.getMinimumGroupingDigits(); alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown(); @@ -167,7 +164,9 @@ public class PositiveDecimalFormat implements Format.TargetFormat { // Add the decimal point if (input.getLowerDisplayMagnitude() < 0 || alwaysShowDecimal) { - length += string.insert(startIndex + length, decimalSeparator, NumberFormat.Field.DECIMAL_SEPARATOR); + length += + string.insert( + startIndex + length, decimalSeparator, NumberFormat.Field.DECIMAL_SEPARATOR); } // Add the fraction digits @@ -182,12 +181,16 @@ public class PositiveDecimalFormat implements Format.TargetFormat { int integerCount = input.getUpperDisplayMagnitude() + 1; for (int i = 0; i < integerCount; i++) { // Add grouping separator - if (groupingSize > 0 && i == groupingSize && integerCount - i >= minimumGroupingDigits) { - length += string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR); + if (primaryGroupingSize > 0 + && i == primaryGroupingSize + && integerCount - i >= minimumGroupingDigits) { + length += + string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR); } else if (secondaryGroupingSize > 0 - && i > groupingSize - && (i - groupingSize) % secondaryGroupingSize == 0) { - length += string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR); + && i > primaryGroupingSize + && (i - primaryGroupingSize) % secondaryGroupingSize == 0) { + length += + string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR); } // Get and append the next digit value @@ -219,9 +222,13 @@ public class PositiveDecimalFormat implements Format.TargetFormat { @Override public void export(Properties properties) { + // For backwards compatibility, export 0 as secondary grouping if primary and secondary are the same + int effectiveSecondaryGroupingSize = + secondaryGroupingSize == primaryGroupingSize ? 0 : secondaryGroupingSize; + properties.setDecimalSeparatorAlwaysShown(alwaysShowDecimal); - properties.setGroupingSize(groupingSize); - properties.setSecondaryGroupingSize(secondaryGroupingSize); + properties.setGroupingSize(primaryGroupingSize); + properties.setSecondaryGroupingSize(effectiveSecondaryGroupingSize); properties.setMinimumGroupingDigits(minimumGroupingDigits); } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java index 2f8c6b2fd78..102a38b7973 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java @@ -9,19 +9,11 @@ package com.ibm.icu.text; -import java.io.IOException; -import java.io.NotSerializableException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.text.AttributedCharacterIterator; -import java.text.FieldPosition; import java.text.ParsePosition; import java.util.Locale; -import com.ibm.icu.impl.number.FormatQuantity4; import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.util.CurrencyAmount; import com.ibm.icu.util.ULocale; /** @@ -55,7 +47,7 @@ import com.ibm.icu.util.ULocale; */ public class CompactDecimalFormat extends DecimalFormat { - private static final long serialVersionUID = 4716293295276629682L; + private static final long serialVersionUID = 4716293295276629682L; /** * Style parameter for CompactDecimalFormat. @@ -122,109 +114,6 @@ public class CompactDecimalFormat extends DecimalFormat { refreshFormatter(); } - /** - * {@inheritDoc} - * - * @stable ICU 49 - */ - @Override - public boolean equals(Object obj) { - return super.equals(obj); - } - - /** - * {@inheritDoc} - * - * @stable ICU 49 - */ - @Override - public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { - FormatQuantity4 fq = new FormatQuantity4(number); - formatter.format(fq, toAppendTo, pos); - fq.populateUFieldPosition(pos); - return toAppendTo; - } - - /** - * {@inheritDoc} - * - * @stable ICU 50 - */ - @Override - public AttributedCharacterIterator formatToCharacterIterator(Object obj) { - if (!(obj instanceof Number)) throw new IllegalArgumentException(); - Number number = (Number) obj; - FormatQuantity4 fq = new FormatQuantity4(number); - AttributedCharacterIterator result = formatter.formatToCharacterIterator(fq); - return result; - } - - /** - * {@inheritDoc} - * - * @stable ICU 49 - */ - @Override - public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { - FormatQuantity4 fq = new FormatQuantity4(number); - formatter.format(fq, toAppendTo, pos); - fq.populateUFieldPosition(pos); - return toAppendTo; - } - - /** - * {@inheritDoc} - * - * @stable ICU 49 - */ - @Override - public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) { - FormatQuantity4 fq = new FormatQuantity4(number); - formatter.format(fq, toAppendTo, pos); - fq.populateUFieldPosition(pos); - return toAppendTo; - } - - /** - * {@inheritDoc} - * - * @stable ICU 49 - */ - @Override - public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { - FormatQuantity4 fq = new FormatQuantity4(number); - formatter.format(fq, toAppendTo, pos); - fq.populateUFieldPosition(pos); - return toAppendTo; - } - - /** - * {@inheritDoc} - * - * @stable ICU 49 - */ - @Override - public StringBuffer format( - com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { - FormatQuantity4 fq = new FormatQuantity4(number.toBigDecimal()); - formatter.format(fq, toAppendTo, pos); - fq.populateUFieldPosition(pos); - return toAppendTo; - } - -// /** -// * {@inheritDoc} -// * -// * @internal ICU 57 technology preview -// * @deprecated This API might change or be removed in a future release. -// */ -// @Override -// @Deprecated -// public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) { -// // TODO(sffc) -// throw new UnsupportedOperationException(); -// } - /** * Parsing is currently unsupported, and throws an UnsupportedOperationException. * @@ -235,13 +124,13 @@ public class CompactDecimalFormat extends DecimalFormat { throw new UnsupportedOperationException(); } - // DISALLOW Serialization, at least while draft - - private void writeObject(ObjectOutputStream out) throws IOException { - throw new NotSerializableException(); - } - - private void readObject(ObjectInputStream in) throws IOException { - throw new NotSerializableException(); + /** + * Parsing is currently unsupported, and throws an UnsupportedOperationException. + * + * @stable ICU 49 + */ + @Override + public CurrencyAmount parseCurrency(CharSequence text, ParsePosition parsePosition) { + throw new UnsupportedOperationException(); } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java index 82e0cda2a5e..1fab6789e48 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java @@ -418,7 +418,11 @@ public class DecimalFormat extends NumberFormat { // FORMAT AND PARSE APIS // //=====================================================================================// - /** @stable ICU 2.0 */ + /** + * {@inheritDoc} + * + * @stable ICU 2.0 + */ @Override public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) { FormatQuantity4 fq = new FormatQuantity4(number); @@ -427,7 +431,11 @@ public class DecimalFormat extends NumberFormat { return result; } - /** @stable ICU 2.0 */ + /** + * {@inheritDoc} + * + * @stable ICU 2.0 + */ @Override public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) { FormatQuantity4 fq = new FormatQuantity4(number); @@ -436,7 +444,11 @@ public class DecimalFormat extends NumberFormat { return result; } - /** @stable ICU 2.0 */ + /** + * {@inheritDoc} + * + * @stable ICU 2.0 + */ @Override public StringBuffer format(BigInteger number, StringBuffer result, FieldPosition fieldPosition) { FormatQuantity4 fq = new FormatQuantity4(number); @@ -445,7 +457,11 @@ public class DecimalFormat extends NumberFormat { return result; } - /** @stable ICU 2.0 */ + /** + * {@inheritDoc} + * + * @stable ICU 2.0 + */ @Override public StringBuffer format( java.math.BigDecimal number, StringBuffer result, FieldPosition fieldPosition) { @@ -455,7 +471,11 @@ public class DecimalFormat extends NumberFormat { return result; } - /** @stable ICU 2.0 */ + /** + * {@inheritDoc} + * + * @stable ICU 2.0 + */ @Override public StringBuffer format(BigDecimal number, StringBuffer result, FieldPosition fieldPosition) { FormatQuantity4 fq = new FormatQuantity4(number.toBigDecimal()); @@ -464,7 +484,11 @@ public class DecimalFormat extends NumberFormat { return result; } - /** @stable ICU 3.6 */ + /** + * {@inheritDoc} + * + * @stable ICU 3.6 + */ @Override public AttributedCharacterIterator formatToCharacterIterator(Object obj) { if (!(obj instanceof Number)) throw new IllegalArgumentException(); @@ -474,7 +498,7 @@ public class DecimalFormat extends NumberFormat { return result; } - protected static final ThreadLocal threadLocalCurrencyProperties = + private static final ThreadLocal threadLocalCurrencyProperties = new ThreadLocal() { @Override protected Properties initialValue() { @@ -482,24 +506,42 @@ public class DecimalFormat extends NumberFormat { } }; + /** + * {@inheritDoc} + * + * @stable ICU 3.0 + */ @Override public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) { // TODO: This is ugly (although not as ugly as it was in ICU 58). // Currency should be a free parameter, not in property bag. Fix in ICU 60. Properties cprops = threadLocalCurrencyProperties.get(); + SingularFormat fmt = null; synchronized (this) { - cprops.copyFrom(properties); + // Use the pre-compiled formatter if possible. Otherwise, copy the properties + // and build our own formatter. + // TODO: Consider using a static format path here. + if (currAmt.getCurrency().equals(properties.getCurrency())) { + fmt = formatter; + } else { + cprops.copyFrom(properties); + } + } + if (fmt == null) { + cprops.setCurrency(currAmt.getCurrency()); + fmt = Endpoint.fromBTA(cprops, symbols); } - cprops.setCurrency(currAmt.getCurrency()); FormatQuantity4 fq = new FormatQuantity4(currAmt.getNumber()); - // TODO: Use a static format path here - SingularFormat fmt = Endpoint.fromBTA(cprops, symbols); fmt.format(fq, toAppendTo, pos); fq.populateUFieldPosition(pos); return toAppendTo; } - /** @stable ICU 2.0 */ + /** + * {@inheritDoc} + * + * @stable ICU 2.0 + */ @Override public Number parse(String text, ParsePosition parsePosition) { // Backwards compatibility: use currency parse mode if this is a currency instance @@ -511,7 +553,11 @@ public class DecimalFormat extends NumberFormat { return result; } - /** @stable ICU 49 */ + /** + * {@inheritDoc} + * + * @stable ICU 49 + */ @Override public CurrencyAmount parseCurrency(CharSequence text, ParsePosition parsePosition) { try { @@ -1922,7 +1968,11 @@ public class DecimalFormat extends NumberFormat { return properties.equals(other.properties) && symbols.equals(other.symbols); } - /** @stable ICU 2.0 */ + /** + * {@inheritDoc} + * + * @stable ICU 2.0 + */ @Override public synchronized int hashCode() { return properties.hashCode(); @@ -1936,9 +1986,26 @@ public class DecimalFormat extends NumberFormat { } }; + /** + * Returns the default value of toString() with extra DecimalFormat-specific information appended + * to the end of the string. This extra information is intended for debugging purposes, and the + * format is not guaranteed to be stable. + * + * @stable ICU 2.0 + */ @Override - public synchronized String toString() { - return ""; + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(getClass().getName()); + result.append("@"); + result.append(Integer.toHexString(hashCode())); + result.append(" { symbols@"); + result.append(Integer.toHexString(symbols.hashCode())); + synchronized (this) { + properties.toStringBare(result); + } + result.append(" }"); + return result.toString(); } /** @@ -2027,7 +2094,17 @@ public class DecimalFormat extends NumberFormat { refreshFormatter(); } + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated public static interface PropertySetter { + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated public void set(Properties props); } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/NumberFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/NumberFormat.java index 74d1928a7bb..80f8d944c00 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/NumberFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/NumberFormat.java @@ -1044,7 +1044,8 @@ public abstract class NumberFormat extends UFormat { // ===== End of factory stuff ===== /** - * Overrides hashCode. + * {@inheritDoc} + * * @stable ICU 2.0 */ @Override diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java index 90a8eda81dd..7c3aa236f7d 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java @@ -418,18 +418,73 @@ public class PluralRules implements Serializable { */ @Deprecated public static enum Operand { - /** The double value of the entire number. */ + /** + * The double value of the entire number. + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated n, - /** The integer value, with the fraction digits truncated off. */ + + /** + * The integer value, with the fraction digits truncated off. + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated i, - /** All visible fraction digits as an integer, including trailing zeros. */ + + /** + * All visible fraction digits as an integer, including trailing zeros. + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated f, - /** Visible fraction digits, not including trailing zeros. */ + + /** + * Visible fraction digits as an integer, not including trailing zeros. + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated t, - /** Number of visible fraction digits. */ + + /** + * Number of visible fraction digits. + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated v, + + /** + * Number of visible fraction digits, not including trailing zeros. + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated w, - /* deprecated */ + + /** + * THIS OPERAND IS DEPRECATED AND HAS BEEN REMOVED FROM THE SPEC. + * + *

Returns the integer value, but will fail if the number has fraction digits. + * That is, using "j" instead of "i" is like implicitly adding "v is 0". + * + *

For example, "j is 3" is equivalent to "i is 3 and v is 0": it matches + * "3" but not "3.1" or "3.0". + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated j; } @@ -439,8 +494,28 @@ public class PluralRules implements Serializable { */ @Deprecated public static interface IFixedDecimal { + /** + * Returns the value corresponding to the specified operand (n, i, f, t, v, or w). + * If the operand is 'n', returns a double; otherwise, returns an integer. + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated public double getPluralOperand(Operand operand); + + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated public boolean isNaN(); + + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated public boolean isInfinite(); } @@ -744,6 +819,8 @@ public class PluralRules implements Serializable { } /** + * {@inheritDoc} + * * @internal * @deprecated This API is ICU internal only. */ @@ -751,12 +828,13 @@ public class PluralRules implements Serializable { @Deprecated public double getPluralOperand(Operand operand) { switch(operand) { - default: return source; + case n: return source; case i: return integerValue; case f: return decimalDigits; case t: return decimalDigitsWithoutTrailingZeros; case v: return visibleDecimalDigitCount; case w: return visibleDecimalDigitCountWithoutTrailingZeros; + default: return source; } } @@ -904,17 +982,25 @@ public class PluralRules implements Serializable { throw new NotSerializableException(); } - /* (non-Javadoc) - * @see com.ibm.icu.text.PluralRules.IFixedDecimal#isNaN() + /** + * {@inheritDoc} + * + * @internal + * @deprecated This API is ICU internal only. */ + @Deprecated @Override public boolean isNaN() { return Double.isNaN(source); } - /* (non-Javadoc) - * @see com.ibm.icu.text.PluralRules.IFixedDecimal#isInfinite() + /** + * {@inheritDoc} + * + * @internal + * @deprecated This API is ICU internal only. */ + @Deprecated @Override public boolean isInfinite() { return Double.isInfinite(source); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/Currency.java b/icu4j/main/classes/core/src/com/ibm/icu/util/Currency.java index 8437c96884c..69dbba94ccf 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/Currency.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/Currency.java @@ -762,15 +762,30 @@ public class Currency extends MeasureUnit { private String isoCode; private String currencyString; + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated public CurrencyStringInfo(String isoCode, String currencyString) { this.isoCode = isoCode; this.currencyString = currencyString; } + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated public String getISOCode() { return isoCode; } + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated @SuppressWarnings("unused") public String getCurrencyString() { return currencyString; 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 7af68500597..eb7b094b18f 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 @@ -850,6 +850,17 @@ parse output breaks // JDK parses as -1945 (1,945d1) fail K +test parse strict scientific +set locale en +set pattern #E0 +set lenient 0 +begin +parse output breaks +123 fail JK +123E1 1230 +123E0 123 +123E fail JK + test parse strict without prefix/suffix set locale en set pattern # diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/CompactDecimalFormatTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/CompactDecimalFormatTest.java index ac8e8aca4dd..f50ea6c6a03 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/CompactDecimalFormatTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/CompactDecimalFormatTest.java @@ -553,6 +553,66 @@ public class CompactDecimalFormatTest extends TestFmwk { } } + @Test + public void TestLongShortFallback() { + // smn, dz have long but not short + // es_US, es_GT, es_419, ee have short but not long + ULocale[] locs = new ULocale[] { + new ULocale("smn"), + new ULocale("es_US"), + new ULocale("es_GT"), + new ULocale("es_419"), + new ULocale("ee"), + }; + double number = 12345.0; + // These expected values are the same in both ICU 58 and 59. + String[][] expectedShortLong = new String[][] { + { "12K", "12 tuhháát" }, + { "12k", "12 mil" }, + { "12k", "12 mil" }, + { "12k", "12 mil" }, + { "12K", "12K" }, + }; + + for (int i=0; i 0.01; d /= 10) { + String s1 = cdfShort.format(d); + String s2 = cdfLong.format(d); + assertNotNull("Short " + loc, s1); + assertNotNull("Long " + loc, s2); + assertNotEquals("Short " + loc, 0, s1.length()); + assertNotEquals("Long " + loc, 0, s2.length()); + } + } + } + + @Test + public void TestDigitDisplay() { + CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(ULocale.US, CompactStyle.SHORT); + cdf.setMinimumSignificantDigits(2); + String actual = cdf.format(70123.45678); + assertEquals("Should not display any extra fraction digits", "70K", actual); + } + @Test public void TestBug12422() { CompactDecimalFormat cdf; 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 d3f92feb643..0adeb32d36e 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 @@ -4683,7 +4683,9 @@ public class NumberFormatTest extends TestFmwk { NumberFormat fmt = NumberFormat.getInstance(new ULocale("en")); fmt.setMinimumIntegerDigits(10); FieldPosition pos = new FieldPosition(NumberFormat.Field.GROUPING_SEPARATOR); - fmt.format(1234, new StringBuffer(), pos); + StringBuffer sb = new StringBuffer(); + fmt.format(1234567, sb, pos); + assertEquals("Should have multiple grouping separators", "0,001,234,567", sb.toString()); assertEquals("FieldPosition should report the first occurence", 1, pos.getBeginIndex()); assertEquals("FieldPosition should report the first occurence", 2, pos.getEndIndex()); } @@ -4940,6 +4942,27 @@ public class NumberFormatTest extends TestFmwk { assertNotEquals("df2 != df1", df2, df1); } + @Test + public void Test13055() { + DecimalFormat df = (DecimalFormat) NumberFormat.getPercentInstance(); + df.setMaximumFractionDigits(0); + df.setRoundingMode(BigDecimal.ROUND_HALF_EVEN); + assertEquals("Should round percent toward even number", "216%", df.format(2.155)); + } + + @Test + public void Test13056() { + DecimalFormat df = new DecimalFormat("#,##0"); + assertEquals("Primary grouping should return 3", 3, df.getGroupingSize()); + assertEquals("Secondary grouping should return 0", 0, df.getSecondaryGroupingSize()); + df.setSecondaryGroupingSize(3); + assertEquals("Primary grouping should still return 3", 3, df.getGroupingSize()); + assertEquals("Secondary grouping should still return 0", 0, df.getSecondaryGroupingSize()); + df.setGroupingSize(4); + assertEquals("Primary grouping should return 4", 4, df.getGroupingSize()); + assertEquals("Secondary should remember explicit setting and return 3", 3, df.getSecondaryGroupingSize()); + } + @Test public void testPercentZero() { DecimalFormat df = (DecimalFormat) NumberFormat.getPercentInstance(); @@ -5017,6 +5040,89 @@ public class NumberFormatTest extends TestFmwk { assertEquals("Rounding mode ordinal from java.math.RoundingMode should be the same", df1, df2); } + @Test + public void testCurrencySignificantDigits() { + ULocale locale = new ULocale("en-US"); + DecimalFormat df = (DecimalFormat) NumberFormat.getCurrencyInstance(locale); + df.setMaximumSignificantDigits(2); + String result = df.format(1234); + assertEquals("Currency rounding should obey significant digits", "$1,200", result); + } + + @Test + public void testParseStrictScientific() { + // See ticket #13057 + DecimalFormat df = (DecimalFormat) NumberFormat.getScientificInstance(); + df.setParseStrict(true); + ParsePosition ppos = new ParsePosition(0); + Number result0 = df.parse("123E4", ppos); + assertEquals("Should accept number with exponent", 1230000L, result0); + assertEquals("Should consume the whole number", 5, ppos.getIndex()); + ppos.setIndex(0); + result0 = df.parse("123", ppos); + assertNull("Should reject number without exponent", result0); + ppos.setIndex(0); + CurrencyAmount result1 = df.parseCurrency("USD123", ppos); + assertNull("Should reject currency without exponent", result1); + } + + @Test + public void testParseLenientScientific() { + DecimalFormat df = (DecimalFormat) NumberFormat.getScientificInstance(); + ParsePosition ppos = new ParsePosition(0); + Number result0 = df.parse("123E", ppos); + assertEquals("Should parse the number in lenient mode", 123L, result0); + assertEquals("Should stop before the E", 3, ppos.getIndex()); + DecimalFormatSymbols dfs = df.getDecimalFormatSymbols(); + dfs.setExponentSeparator("EE"); + df.setDecimalFormatSymbols(dfs); + ppos.setIndex(0); + result0 = df.parse("123EE", ppos); + assertEquals("Should parse the number in lenient mode", 123L, result0); + assertEquals("Should stop before the EE", 3, ppos.getIndex()); + } + + @Test + public void testParseAcceptAsciiPercentPermilleFallback() { + ULocale loc = new ULocale("ar"); + DecimalFormat df = (DecimalFormat) NumberFormat.getPercentInstance(loc); + ParsePosition ppos = new ParsePosition(0); + Number result = df.parse("42%", ppos); + assertEquals("Should parse as 0.42 even in ar", new BigDecimal("0.42"), result); + assertEquals("Should consume the entire string even in ar", 3, ppos.getIndex()); + // TODO: Is there a better way to make a localized permille formatter? + df.applyPattern(df.toPattern().replace("%", "‰")); + ppos.setIndex(0); + result = df.parse("42‰", ppos); + assertEquals("Should parse as 0.042 even in ar", new BigDecimal("0.042"), result); + assertEquals("Should consume the entire string even in ar", 3, ppos.getIndex()); + } + + @Test + public void testParseSubtraction() { + // TODO: Is this a case we need to support? It prevents us from automatically parsing + // minus signs that appear after the number, like in "12-" vs "-12". + DecimalFormat df = new DecimalFormat(); + String str = "12 - 5"; + ParsePosition ppos = new ParsePosition(0); + Number n1 = df.parse(str, ppos); + Number n2 = df.parse(str, ppos); + assertEquals("Should parse 12 and -5", 7, n1.intValue() + n2.intValue()); + } + + @Test + public void testMultiCodePointPaddingInPattern() { + DecimalFormat df = new DecimalFormat("a*'நி'###0b"); + String result = df.format(12); + assertEquals("Multi-codepoint padding should not be split", "aநிநி12b", result); + df = new DecimalFormat("a*😁###0b"); + result = df.format(12); + assertEquals("Single-codepoint padding should not be split", "a😁😁12b", result); + df = new DecimalFormat("a*''###0b"); + result = df.format(12); + assertEquals("Quote should be escapable in padding syntax", "a''12b", result); + } + @Test public void testSignificantDigitsMode() { String[][] allExpected = {