diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/ICUResourceBundle.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/ICUResourceBundle.java index 9c6558ba75..653391ca3a 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/ICUResourceBundle.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/ICUResourceBundle.java @@ -364,6 +364,14 @@ public class ICUResourceBundle extends UResourceBundle { return result; } + public void getAllItemsWithFallbackNoFail(String path, UResource.Sink sink) { + try { + getAllItemsWithFallback(path, sink); + } catch (MissingResourceException e) { + // Quietly ignore the exception. + } + } + public void getAllItemsWithFallback(String path, UResource.Sink sink) throws MissingResourceException { // Collect existing and parsed key objects into an array of keys, diff --git a/icu4j/main/classes/core/src/newapi/NumberFormatterImpl.java b/icu4j/main/classes/core/src/newapi/NumberFormatterImpl.java index 9e02cf697a..24a9b8e4f2 100644 --- a/icu4j/main/classes/core/src/newapi/NumberFormatterImpl.java +++ b/icu4j/main/classes/core/src/newapi/NumberFormatterImpl.java @@ -140,7 +140,8 @@ class NumberFormatterImpl { ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(innerPattern); // Symbols - // NOTE: C++ has a special class, SymbolsWrapper, in MacroProps. Java has all the resolution logic here directly. + // NOTE: C++ has a special class, SymbolsWrapper, in MacroProps. Java has all the resolution logic here + // directly. if (macros.symbols == null) { micros.symbols = DecimalFormatSymbols.getInstance(macros.loc); } else if (macros.symbols instanceof DecimalFormatSymbols) { @@ -238,7 +239,9 @@ class NumberFormatterImpl { // Lazily create PluralRules rules = PluralRules.forLocale(macros.loc); } - CompactType compactType = (macros.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL; + CompactType compactType = (macros.unit instanceof Currency && macros.unitWidth != UnitWidth.FULL_NAME) + ? CompactType.CURRENCY + : CompactType.DECIMAL; chain = ((CompactNotation) macros.notation).withLocaleData(macros.loc, compactType, rules, safe ? patternMod : null, chain); } diff --git a/icu4j/main/classes/core/src/newapi/ScientificNotation.java b/icu4j/main/classes/core/src/newapi/ScientificNotation.java index 283d0631ae..e31ec8af73 100644 --- a/icu4j/main/classes/core/src/newapi/ScientificNotation.java +++ b/icu4j/main/classes/core/src/newapi/ScientificNotation.java @@ -58,25 +58,39 @@ public class ScientificNotation extends Notation implements Cloneable { /* package-private */ MicroPropsGenerator withLocaleData(DecimalFormatSymbols symbols, boolean build, MicroPropsGenerator parent) { - return new MurkyScientificHandler(symbols, build, parent); + return new ScientificHandler(this, symbols, build, parent); } - private class MurkyScientificHandler implements MicroPropsGenerator, MultiplierProducer, Modifier { + // NOTE: The object lifecycle of ScientificModifier and ScientificHandler differ greatly in Java and C++. + // + // During formatting, we need to provide an object with state (the exponent) as the inner modifier. + // + // In Java, where the priority is put on reducing object creations, the unsafe code path re-uses the + // ScientificHandler as a ScientificModifier, and the safe code path pre-computes 25 ScientificModifier + // instances. This scheme reduces the number of object creations by 1 in both safe and unsafe. + // + // In C++, MicroProps provides a pre-allocated ScientificModifier, and ScientificHandler simply populates + // the state (the exponent) into that ScientificModifier. There is no difference between safe and unsafe. + private static class ScientificHandler implements MicroPropsGenerator, MultiplierProducer, Modifier { + + final ScientificNotation notation; final DecimalFormatSymbols symbols; - final ImmutableScientificModifier[] precomputedMods; + final ScientificModifier[] precomputedMods; final MicroPropsGenerator parent; /* unsafe */ int exponent; - private MurkyScientificHandler(DecimalFormatSymbols symbols, boolean safe, MicroPropsGenerator parent) { + private ScientificHandler(ScientificNotation notation, DecimalFormatSymbols symbols, boolean safe, + MicroPropsGenerator parent) { + this.notation = notation; this.symbols = symbols; this.parent = parent; if (safe) { // Pre-build the modifiers for exponents -12 through 12 - precomputedMods = new ImmutableScientificModifier[25]; + precomputedMods = new ScientificModifier[25]; for (int i = -12; i <= 12; i++) { - precomputedMods[i + 12] = new ImmutableScientificModifier(i); + precomputedMods[i + 12] = new ScientificModifier(i, this); } } else { precomputedMods = null; @@ -91,9 +105,9 @@ public class ScientificNotation extends Notation implements Cloneable { // Treat zero as if it had magnitude 0 int exponent; if (quantity.isZero()) { - if (requireMinInt && micros.rounding instanceof SignificantRounderImpl) { + if (notation.requireMinInt && micros.rounding instanceof SignificantRounderImpl) { // Show "00.000E0" on pattern "00.000E0" - ((SignificantRounderImpl) micros.rounding).apply(quantity, engineeringInterval); + ((SignificantRounderImpl) micros.rounding).apply(quantity, notation.engineeringInterval); exponent = 0; } else { micros.rounding.apply(quantity); @@ -109,7 +123,7 @@ public class ScientificNotation extends Notation implements Cloneable { micros.modInner = precomputedMods[exponent + 12]; } else if (precomputedMods != null) { // Safe code path B - micros.modInner = new ImmutableScientificModifier(exponent); + micros.modInner = new ScientificModifier(exponent, this); } else { // Unsafe code path: mutates the object and re-uses it as a Modifier! this.exponent = exponent; @@ -124,9 +138,9 @@ public class ScientificNotation extends Notation implements Cloneable { @Override public int getMultiplier(int magnitude) { - int interval = engineeringInterval; + int interval = notation.engineeringInterval; int digitsShown; - if (requireMinInt) { + if (notation.requireMinInt) { // For patterns like "000.00E0" and ".00E0" digitsShown = interval; } else if (interval <= 1) { @@ -141,7 +155,7 @@ public class ScientificNotation extends Notation implements Cloneable { @Override public int getPrefixLength() { - // FIXME: Localized exponent separator location. + // TODO: Localized exponent separator location. return 0; } @@ -153,6 +167,7 @@ public class ScientificNotation extends Notation implements Cloneable { @Override public boolean isStrong() { + // Scientific is always strong return true; } @@ -166,49 +181,52 @@ public class ScientificNotation extends Notation implements Cloneable { int i = rightIndex; // Append the exponent separator and sign i += output.insert(i, symbols.getExponentSeparator(), NumberFormat.Field.EXPONENT_SYMBOL); - if (exponent < 0 && exponentSignDisplay != SignDisplay.NEVER) { + if (exponent < 0 && notation.exponentSignDisplay != SignDisplay.NEVER) { i += output.insert(i, symbols.getMinusSignString(), NumberFormat.Field.EXPONENT_SIGN); - } else if (exponentSignDisplay == SignDisplay.ALWAYS) { + } else if (notation.exponentSignDisplay == SignDisplay.ALWAYS) { i += output.insert(i, symbols.getPlusSignString(), NumberFormat.Field.EXPONENT_SIGN); } // Append the exponent digits (using a simple inline algorithm) int disp = Math.abs(exponent); - for (int j = 0; j < minExponentDigits || disp > 0; j++, disp /= 10) { + for (int j = 0; j < notation.minExponentDigits || disp > 0; j++, disp /= 10) { int d = disp % 10; String digitString = symbols.getDigitStringsLocal()[d]; i += output.insert(i - j, digitString, NumberFormat.Field.EXPONENT); } return i - rightIndex; } + } - private class ImmutableScientificModifier implements Modifier { - final int exponent; + private static class ScientificModifier implements Modifier { + final int exponent; + final ScientificHandler handler; - ImmutableScientificModifier(int exponent) { - this.exponent = exponent; - } + ScientificModifier(int exponent, ScientificHandler handler) { + this.exponent = exponent; + this.handler = handler; + } - @Override - public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { - return doApply(exponent, output, rightIndex); - } + @Override + public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { + return handler.doApply(exponent, output, rightIndex); + } - @Override - public int getPrefixLength() { - // FIXME: Localized exponent separator location. - return 0; - } + @Override + public int getPrefixLength() { + // TODO: Localized exponent separator location. + return 0; + } - @Override - public int getCodePointCount() { - // This method is not used for strong modifiers. - throw new AssertionError(); - } + @Override + public int getCodePointCount() { + // This method is not used for strong modifiers. + throw new AssertionError(); + } - @Override - public boolean isStrong() { - return true; - } + @Override + public boolean isStrong() { + // Scientific is always strong + return true; } } } \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/impl/CompactData.java b/icu4j/main/classes/core/src/newapi/impl/CompactData.java index f2fb71d92d..8b1c1df801 100644 --- a/icu4j/main/classes/core/src/newapi/impl/CompactData.java +++ b/icu4j/main/classes/core/src/newapi/impl/CompactData.java @@ -5,7 +5,6 @@ package newapi.impl; import java.util.Arrays; import java.util.HashSet; import java.util.Map; -import java.util.MissingResourceException; import java.util.Set; import com.ibm.icu.impl.ICUData; @@ -20,257 +19,226 @@ import com.ibm.icu.util.UResourceBundle; public class CompactData implements MultiplierProducer { - public static CompactData getInstance( - ULocale locale, CompactType compactType, CompactStyle compactStyle) { - // TODO: Add a data cache? It would be keyed by locale, compact type, and compact style. - CompactData data = new CompactData(); - CompactDataSink sink = new CompactDataSink(data, compactType, compactStyle); - String nsName = NumberingSystem.getInstance(locale).getName(); - ICUResourceBundle rb = - (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale); - CompactData.internalPopulateData(nsName, rb, sink, data); - if (data.isEmpty() && compactStyle == CompactStyle.LONG) { - // No long data is available; load short data instead - sink.compactStyle = CompactStyle.SHORT; - CompactData.internalPopulateData(nsName, rb, sink, data); - } - return data; - } + public static CompactData getInstance(ULocale locale, CompactType compactType, CompactStyle compactStyle) { + // TODO: Add a data cache? It would be keyed by locale, compact type, and compact style. + CompactData data = new CompactData(); + CompactDataSink sink = new CompactDataSink(data); + String nsName = NumberingSystem.getInstance(locale).getName(); + ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale); - public static CompactData getInstance( - Map> powersToPluralsToPatterns) { - CompactData data = new CompactData(); - for (Map.Entry> magnitudeEntry : - powersToPluralsToPatterns.entrySet()) { - byte magnitude = (byte) (magnitudeEntry.getKey().length() - 1); - for (Map.Entry pluralEntry : magnitudeEntry.getValue().entrySet()) { - StandardPlural plural = StandardPlural.fromString(pluralEntry.getKey().toString()); - String patternString = pluralEntry.getValue().toString(); - data.setPattern(patternString, magnitude, plural); - int numZeros = countZeros(patternString); - if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun" - data.setMultiplier(magnitude, (byte) (numZeros - magnitude - 1)); + // Fall back to latn numbering system and/or short compact style. + String resourceKey = getResourceBundleKey(nsName, compactStyle, compactType); + rb.getAllItemsWithFallbackNoFail(resourceKey, sink); + if (data.isEmpty() && !nsName.equals("latn")) { + resourceKey = getResourceBundleKey("latn", compactStyle, compactType); + rb.getAllItemsWithFallbackNoFail(resourceKey, sink); } - } + if (data.isEmpty() && compactStyle != CompactStyle.SHORT) { + resourceKey = getResourceBundleKey(nsName, CompactStyle.SHORT, compactType); + rb.getAllItemsWithFallbackNoFail(resourceKey, sink); + } + if (data.isEmpty() && !nsName.equals("latn") && compactStyle != CompactStyle.SHORT) { + resourceKey = getResourceBundleKey("latn", CompactStyle.SHORT, compactType); + rb.getAllItemsWithFallbackNoFail(resourceKey, sink); + } + + // The last fallback is guaranteed to return data. + assert (!data.isEmpty()); + return data; } - return data; - } - private static void internalPopulateData( - String nsName, ICUResourceBundle rb, CompactDataSink sink, CompactData data) { - try { - rb.getAllItemsWithFallback("NumberElements/" + nsName, sink); - } catch (MissingResourceException e) { - // Fall back to latn + /** Returns a string like "NumberElements/latn/patternsShort/decimalFormat". */ + private static String getResourceBundleKey(String nsName, CompactStyle compactStyle, CompactType compactType) { + StringBuilder sb = new StringBuilder(); + sb.append("NumberElements/"); + sb.append(nsName); + sb.append(compactStyle == CompactStyle.SHORT ? "/patternsShort" : "/patternsLong"); + sb.append(compactType == CompactType.DECIMAL ? "/decimalFormat" : "/currencyFormat"); + return sb.toString(); } - if (data.isEmpty() && !nsName.equals("latn")) { - rb.getAllItemsWithFallback("NumberElements/latn", sink); + + /** Java-only method used by CLDR tooling. */ + public static CompactData getInstance(Map> powersToPluralsToPatterns) { + CompactData data = new CompactData(); + for (Map.Entry> magnitudeEntry : powersToPluralsToPatterns.entrySet()) { + byte magnitude = (byte) (magnitudeEntry.getKey().length() - 1); + for (Map.Entry pluralEntry : magnitudeEntry.getValue().entrySet()) { + StandardPlural plural = StandardPlural.fromString(pluralEntry.getKey().toString()); + String patternString = pluralEntry.getValue().toString(); + data.setPattern(patternString, magnitude, plural); + int numZeros = countZeros(patternString); + if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun" + data.setMultiplier(magnitude, (byte) (numZeros - magnitude - 1)); + } + } + } + return data; } - if (sink.exception != null) { - throw sink.exception; + + // A dummy object used when a "0" compact decimal entry is encountered. This is necessary + // in order to prevent falling back to root. Object equality ("==") is intended. + private static final String USE_FALLBACK = ""; + + private final String[] patterns; + private final byte[] multipliers; + private boolean isEmpty; + private int largestMagnitude; + + private static final int MAX_DIGITS = 15; + + private CompactData() { + patterns = new String[(CompactData.MAX_DIGITS + 1) * StandardPlural.COUNT]; + multipliers = new byte[CompactData.MAX_DIGITS + 1]; + isEmpty = true; + largestMagnitude = 0; } - } - // A dummy object used when a "0" compact decimal entry is encountered. This is necessary - // in order to prevent falling back to root. Object equality ("==") is intended. - private static final String USE_FALLBACK = ""; - - private final String[] patterns; - private final byte[] multipliers; - private boolean isEmpty; - private int largestMagnitude; - - private static final int MAX_DIGITS = 15; - - private CompactData() { - patterns = new String[(CompactData.MAX_DIGITS + 1) * StandardPlural.COUNT]; - multipliers = new byte[CompactData.MAX_DIGITS + 1]; - isEmpty = true; - largestMagnitude = 0; - } - - public boolean isEmpty() { - return isEmpty; - } - - @Override - public int getMultiplier(int magnitude) { - if (magnitude < 0) { - return 0; - } - if (magnitude > largestMagnitude) { - magnitude = largestMagnitude; - } - return multipliers[magnitude]; - } - - /** Returns the multiplier from the array directly without bounds checking. */ - public int getMultiplierDirect(int magnitude) { - return multipliers[magnitude]; - } - - private void setMultiplier(int magnitude, byte multiplier) { - if (multipliers[magnitude] != 0) { - assert multipliers[magnitude] == multiplier; - return; - } - multipliers[magnitude] = multiplier; - isEmpty = false; - if (magnitude > largestMagnitude) largestMagnitude = magnitude; - } - - public String getPattern(int magnitude, StandardPlural plural) { - if (magnitude < 0) { - return null; - } - if (magnitude > largestMagnitude) { - magnitude = largestMagnitude; - } - String patternString = patterns[getIndex(magnitude, plural)]; - if (patternString == null && plural != StandardPlural.OTHER) { - // Fall back to "other" plural variant - patternString = patterns[getIndex(magnitude, StandardPlural.OTHER)]; - } - if (patternString == USE_FALLBACK) { - // Return null if USE_FALLBACK is present - patternString = null; - } - return patternString; - } - - public Set getAllPatterns() { - Set result = new HashSet(); - result.addAll(Arrays.asList(patterns)); - result.remove(USE_FALLBACK); - result.remove(null); - return result; - } - - private boolean has(int magnitude, StandardPlural plural) { - // Return true if USE_FALLBACK is present - return patterns[getIndex(magnitude, plural)] != null; - } - - private void setPattern(String patternString, int magnitude, StandardPlural plural) { - patterns[getIndex(magnitude, plural)] = patternString; - isEmpty = false; - if (magnitude > largestMagnitude) largestMagnitude = magnitude; - } - - private void setNoFallback(int magnitude, StandardPlural plural) { - setPattern(USE_FALLBACK, magnitude, plural); - } - - private static final int getIndex(int magnitude, StandardPlural plural) { - return magnitude * StandardPlural.COUNT + plural.ordinal(); - } - - private static final class CompactDataSink extends UResource.Sink { - - CompactData data; - CompactStyle compactStyle; - CompactType compactType; - IllegalArgumentException exception; - - /* - * NumberElements{ <-- top (numbering system table) - * latn{ <-- patternsTable (one per numbering system) - * patternsLong{ <-- formatsTable (one per pattern) - * decimalFormat{ <-- powersOfTenTable (one per format) - * 1000{ <-- pluralVariantsTable (one per power of ten) - * one{"0 thousand"} <-- plural variant and template - */ - - public CompactDataSink(CompactData data, CompactType compactType, CompactStyle compactStyle) { - this.data = data; - this.compactType = compactType; - this.compactStyle = compactStyle; + public boolean isEmpty() { + return isEmpty; } @Override - public void put(UResource.Key key, UResource.Value value, boolean isRoot) { - UResource.Table patternsTable = value.getTable(); - for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) { - if (key.contentEquals("patternsShort") && compactStyle == CompactStyle.SHORT) { - } else if (key.contentEquals("patternsLong") && compactStyle == CompactStyle.LONG) { - } else { - continue; + public int getMultiplier(int magnitude) { + if (magnitude < 0) { + return 0; + } + if (magnitude > largestMagnitude) { + magnitude = largestMagnitude; + } + return multipliers[magnitude]; + } + + /** Returns the multiplier from the array directly without bounds checking. */ + public int getMultiplierDirect(int magnitude) { + return multipliers[magnitude]; + } + + private void setMultiplier(int magnitude, byte multiplier) { + if (multipliers[magnitude] != 0) { + assert multipliers[magnitude] == multiplier; + return; + } + multipliers[magnitude] = multiplier; + isEmpty = false; + if (magnitude > largestMagnitude) { + largestMagnitude = magnitude; + } + } + + public String getPattern(int magnitude, StandardPlural plural) { + if (magnitude < 0) { + return null; + } + if (magnitude > largestMagnitude) { + magnitude = largestMagnitude; + } + String patternString = patterns[getIndex(magnitude, plural)]; + if (patternString == null && plural != StandardPlural.OTHER) { + // Fall back to "other" plural variant + patternString = patterns[getIndex(magnitude, StandardPlural.OTHER)]; + } + if (patternString == USE_FALLBACK) { // == is intended + // Return null if USE_FALLBACK is present + patternString = null; + } + return patternString; + } + + public Set getAllPatterns() { + Set result = new HashSet(); + result.addAll(Arrays.asList(patterns)); + result.remove(USE_FALLBACK); + result.remove(null); + return result; + } + + private boolean has(int magnitude, StandardPlural plural) { + // Return true if USE_FALLBACK is present + return patterns[getIndex(magnitude, plural)] != null; + } + + private void setPattern(String patternString, int magnitude, StandardPlural plural) { + patterns[getIndex(magnitude, plural)] = patternString; + isEmpty = false; + if (magnitude > largestMagnitude) + largestMagnitude = magnitude; + } + + private void setNoFallback(int magnitude, StandardPlural plural) { + setPattern(USE_FALLBACK, magnitude, plural); + } + + private static final int getIndex(int magnitude, StandardPlural plural) { + return magnitude * StandardPlural.COUNT + plural.ordinal(); + } + + private static final class CompactDataSink extends UResource.Sink { + + CompactData data; + + public CompactDataSink(CompactData data) { + this.data = data; } - // traverse into the table of formats - UResource.Table formatsTable = value.getTable(); - for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) { - if (key.contentEquals("decimalFormat") && compactType == CompactType.DECIMAL) { - } else if (key.contentEquals("currencyFormat") && compactType == CompactType.CURRENCY) { - } else { - continue; - } + @Override + public void put(UResource.Key key, UResource.Value value, boolean isRoot) { + // traverse into the table of powers of ten + UResource.Table powersOfTenTable = value.getTable(); + for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) { - // traverse into the table of powers of ten - UResource.Table powersOfTenTable = value.getTable(); - for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) { + // Assumes that the keys are always of the form "10000" where the magnitude is the + // length of the key minus one. We expect magnitudes to be less than MAX_DIGITS. + byte magnitude = (byte) (key.length() - 1); + byte multiplier = (byte) data.getMultiplierDirect(magnitude); + assert magnitude < MAX_DIGITS; - // Assumes that the keys are always of the form "10000" where the magnitude is the - // length of the key minus one - byte magnitude = (byte) (key.length() - 1); - byte multiplier = (byte) data.getMultiplierDirect(magnitude); + // Iterate over the plural variants ("one", "other", etc) + UResource.Table pluralVariantsTable = value.getTable(); + for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) { - // Silently ignore divisors that are too big. - if (magnitude >= CompactData.MAX_DIGITS) continue; + // Skip this magnitude/plural if we already have it from a child locale. + StandardPlural plural = StandardPlural.fromString(key.toString()); + if (data.has(magnitude, plural)) { + continue; + } - // Iterate over the plural variants ("one", "other", etc) - UResource.Table pluralVariantsTable = value.getTable(); - for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) { + // The value "0" means that we need to use the default pattern and not fall back + // to parent locales. Example locale where this is relevant: 'it'. + String patternString = value.toString(); + if (patternString.equals("0")) { + data.setNoFallback(magnitude, plural); + continue; + } - // Skip this magnitude/plural if we already have it from a child locale. - StandardPlural plural = StandardPlural.fromString(key.toString()); - if (data.has(magnitude, plural)) { - continue; - } + // Save the pattern string. We will parse it lazily. + data.setPattern(patternString, magnitude, plural); - // The value "0" means that we need to use the default pattern and not fall back - // to parent locales. Example locale where this is relevant: 'it'. - String patternString = value.toString(); - if (patternString.equals("0")) { - data.setNoFallback(magnitude, plural); - continue; - } - - // Save the pattern string. We will parse it lazily. - data.setPattern(patternString, magnitude, plural); - - // If necessary, compute the multiplier: the difference between the magnitude - // and the number of zeros in the pattern. - if (multiplier == 0) { - int numZeros = countZeros(patternString); - if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun" - multiplier = (byte) (numZeros - magnitude - 1); + // If necessary, compute the multiplier: the difference between the magnitude + // and the number of zeros in the pattern. + if (multiplier == 0) { + int numZeros = countZeros(patternString); + if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun" + multiplier = (byte) (numZeros - magnitude - 1); + } + } } - } + + data.setMultiplier(magnitude, multiplier); } - - data.setMultiplier(magnitude, multiplier); - } - - // We want only one table of compact decimal formats, so if we get here, stop consuming. - // The data.isEmpty() check will prevent further bundles from being traversed. - return; } - } } - } - private static final int countZeros(String patternString) { - // NOTE: This strategy for computing the number of zeros is a hack for efficiency. - // It could break if there are any 0s that aren't part of the main pattern. - int numZeros = 0; - for (int i = 0; i < patternString.length(); i++) { - if (patternString.charAt(i) == '0') { - numZeros++; - } else if (numZeros > 0) { - break; // zeros should always be contiguous - } + private static final int countZeros(String patternString) { + // NOTE: This strategy for computing the number of zeros is a hack for efficiency. + // It could break if there are any 0s that aren't part of the main pattern. + int numZeros = 0; + for (int i = 0; i < patternString.length(); i++) { + if (patternString.charAt(i) == '0') { + numZeros++; + } else if (numZeros > 0) { + break; // zeros should always be contiguous + } + } + return numZeros; } - return numZeros; - } } diff --git a/icu4j/main/classes/core/src/newapi/impl/LongNameHandler.java b/icu4j/main/classes/core/src/newapi/impl/LongNameHandler.java index 8658104daa..cbb67f14d5 100644 --- a/icu4j/main/classes/core/src/newapi/impl/LongNameHandler.java +++ b/icu4j/main/classes/core/src/newapi/impl/LongNameHandler.java @@ -46,7 +46,9 @@ public class LongNameHandler implements MicroPropsGenerator { String pluralKeyword = e.getKey(); StandardPlural plural = StandardPlural.fromString(e.getKey()); String longName = currency.getName(loc, Currency.PLURAL_LONG_NAME, pluralKeyword, null); - String simpleFormat = e.getValue(); // e.g., "{0} {1}" + String simpleFormat = e.getValue(); + // Example pattern from data: "{0} {1}" + // Example output after find-and-replace: "{0} US dollars" simpleFormat = simpleFormat.replace("{1}", longName); String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 1, 1); SimpleModifier mod = new SimpleModifier(compiled, Field.CURRENCY, false); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java index 49d428e40a..44b651fb32 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java @@ -186,13 +186,50 @@ public class NumberFormatterTest { "$0.0088", "$0"); + assertFormatDescending( + "Compact Short with ISO Currency", + "C $USD unit-width=ISO_CODE", + NumberFormatter.with() + .notation(Notation.compactShort()) + .unit(USD) + .unitWidth(UnitWidth.ISO_CODE), + ULocale.ENGLISH, + "USD 88K", + "USD 8.8K", + "USD 876", + "USD 88", + "USD 8.8", + "USD 0.88", + "USD 0.088", + "USD 0.0088", + "USD 0"); + + assertFormatDescending( + "Compact Short with Long Name Currency", + "C $USD unit-width=FULL_NAME", + NumberFormatter.with() + .notation(Notation.compactShort()) + .unit(USD) + .unitWidth(UnitWidth.FULL_NAME), + ULocale.ENGLISH, + "88K US dollars", + "8.8K US dollars", + "876 US dollars", + "88 US dollars", + "8.8 US dollars", + "0.88 US dollars", + "0.088 US dollars", + "0.0088 US dollars", + "0 US dollars"); + // Note: Most locales don't have compact long currency, so this currently falls back to short. + // This test case should be fixed when proper compact long currency patterns are added. assertFormatDescending( "Compact Long Currency", "CC $USD", NumberFormatter.with().notation(Notation.compactLong()).unit(USD), ULocale.ENGLISH, - "$88K", + "$88K", // should be something like "$88 thousand" "$8.8K", "$876", "$88", @@ -202,6 +239,45 @@ public class NumberFormatterTest { "$0.0088", "$0"); + // Note: Most locales don't have compact long currency, so this currently falls back to short. + // This test case should be fixed when proper compact long currency patterns are added. + assertFormatDescending( + "Compact Long with ISO Currency", + "CC $USD unit-width=ISO_CODE", + NumberFormatter.with() + .notation(Notation.compactLong()) + .unit(USD) + .unitWidth(UnitWidth.ISO_CODE), + ULocale.ENGLISH, + "USD 88K", // should be something like "USD 88 thousand" + "USD 8.8K", + "USD 876", + "USD 88", + "USD 8.8", + "USD 0.88", + "USD 0.088", + "USD 0.0088", + "USD 0"); + + // TODO: This behavior could be improved and should be revisited. + assertFormatDescending( + "Compact Long with Long Name Currency", + "CC $USD unit-width=FULL_NAME", + NumberFormatter.with() + .notation(Notation.compactLong()) + .unit(USD) + .unitWidth(UnitWidth.FULL_NAME), + ULocale.ENGLISH, + "88 thousand US dollars", + "8.8 thousand US dollars", + "876 US dollars", + "88 US dollars", + "8.8 US dollars", + "0.88 US dollars", + "0.088 US dollars", + "0.0088 US dollars", + "0 US dollars"); + assertFormatSingle( "Compact Plural One", "CC",