ICU-13177 Small Java changes relating to Compact and Padding.

X-SVN-Rev: 40423
This commit is contained in:
Shane Carr 2017-09-16 06:57:08 +00:00
parent c842f7426d
commit 6a1bbcaa58
8 changed files with 158 additions and 171 deletions

View file

@ -69,26 +69,6 @@ public class CompactDecimalFormat extends DecimalFormat {
LONG
}
/**
* Type parameter for CompactDecimalFormat.
*
* @draft ICU 60
*/
public enum CompactType {
/**
* Standard compact format, like "1.2T"
*
* @draft ICU 60
*/
DECIMAL,
/**
* Compact format with currency, like "$1.2T"
*
* @draft ICU 60
*/
CURRENCY
}
/**
* Creates a CompactDecimalFormat appropriate for a locale. The result may be affected by the
* number system in the locale, such as ar-u-nu-latn.

View file

@ -3,6 +3,7 @@
package newapi;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@ -11,7 +12,6 @@ import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.PatternStringParser;
import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CompactDecimalFormat.CompactType;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.ULocale;
@ -26,6 +26,10 @@ public class CompactNotation extends Notation {
final CompactStyle compactStyle;
final Map<String, Map<String, String>> compactCustomData;
public enum CompactType {
DECIMAL, CURRENCY
}
public CompactNotation(CompactStyle compactStyle) {
compactCustomData = null;
this.compactStyle = compactStyle;
@ -36,18 +40,13 @@ public class CompactNotation extends Notation {
this.compactCustomData = compactCustomData;
}
/* package-private */ MicroPropsGenerator withLocaleData(ULocale dataLocale, CompactType compactType, PluralRules rules,
MutablePatternModifier buildReference, MicroPropsGenerator parent) {
CompactData data;
if (compactStyle != null) {
data = CompactData.getInstance(dataLocale, compactType, compactStyle);
} else {
data = CompactData.getInstance(compactCustomData);
}
return new CompactImpl(data, rules, buildReference, parent);
/* package-private */ MicroPropsGenerator withLocaleData(ULocale locale, String nsName, CompactType compactType,
PluralRules rules, MutablePatternModifier buildReference, MicroPropsGenerator parent) {
// TODO: Add a data cache? It would be keyed by locale, nsName, compact type, and compact style.
return new CompactHandler(this, locale, nsName, compactType, rules, buildReference, parent);
}
private static class CompactImpl implements MicroPropsGenerator {
private static class CompactHandler implements MicroPropsGenerator {
private static class CompactModInfo {
public ImmutablePatternModifier mod;
@ -55,28 +54,35 @@ public class CompactNotation extends Notation {
}
final PluralRules rules;
final CompactData data;
final Map<String, CompactModInfo> precomputedMods;
final MicroPropsGenerator parent;
final Map<String, CompactModInfo> precomputedMods;
final CompactData data;
private CompactImpl(CompactData data, PluralRules rules, MutablePatternModifier buildReference, MicroPropsGenerator parent) {
this.data = data;
private CompactHandler(CompactNotation notation, ULocale locale, String nsName, CompactType compactType,
PluralRules rules, MutablePatternModifier buildReference, MicroPropsGenerator parent) {
this.rules = rules;
this.parent = parent;
this.data = new CompactData();
if (notation.compactStyle != null) {
data.populate(locale, nsName, notation.compactStyle, compactType);
} else {
data.populate(notation.compactCustomData);
}
if (buildReference != null) {
// Safe code path
precomputedMods = precomputeAllModifiers(data, buildReference);
precomputedMods = new HashMap<String, CompactModInfo>();
precomputeAllModifiers(buildReference);
} else {
// Unsafe code path
precomputedMods = null;
}
this.parent = parent;
}
/** Used by the safe code path */
private static Map<String, CompactModInfo> precomputeAllModifiers(CompactData data,
MutablePatternModifier buildReference) {
Map<String, CompactModInfo> precomputedMods = new HashMap<String, CompactModInfo>();
Set<String> allPatterns = data.getAllPatterns();
private void precomputeAllModifiers(MutablePatternModifier buildReference) {
Set<String> allPatterns = new HashSet<String>();
data.getUniquePatterns(allPatterns);
for (String patternString : allPatterns) {
CompactModInfo info = new CompactModInfo();
ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(patternString);
@ -85,27 +91,26 @@ public class CompactNotation extends Notation {
info.numDigits = patternInfo.positive.integerTotal;
precomputedMods.put(patternString, info);
}
return precomputedMods;
}
@Override
public MicroProps processQuantity(DecimalQuantity input) {
MicroProps micros = parent.processQuantity(input);
public MicroProps processQuantity(DecimalQuantity quantity) {
MicroProps micros = parent.processQuantity(quantity);
assert micros.rounding != null;
// Treat zero as if it had magnitude 0
int magnitude;
if (input.isZero()) {
if (quantity.isZero()) {
magnitude = 0;
micros.rounding.apply(input);
micros.rounding.apply(quantity);
} else {
// TODO: Revisit chooseMultiplierAndApply
int multiplier = micros.rounding.chooseMultiplierAndApply(input, data);
magnitude = input.isZero() ? 0 : input.getMagnitude();
int multiplier = micros.rounding.chooseMultiplierAndApply(quantity, data);
magnitude = quantity.isZero() ? 0 : quantity.getMagnitude();
magnitude -= multiplier;
}
StandardPlural plural = input.getStandardPlural(rules);
StandardPlural plural = quantity.getStandardPlural(rules);
String patternString = data.getPattern(magnitude, plural);
int numDigits = -1;
if (patternString == null) {
@ -113,12 +118,13 @@ public class CompactNotation extends Notation {
// 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, input);
info.mod.applyToMicros(micros, quantity);
numDigits = info.numDigits;
} else {
// Unsafe code path.
// Overwrite the PatternInfo in the existing modMiddle
// Overwrite the PatternInfo in the existing modMiddle.
assert micros.modMiddle instanceof MutablePatternModifier;
ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(patternString);
((MutablePatternModifier) micros.modMiddle).setPatternInfo(patternInfo);

View file

@ -71,6 +71,7 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
public FormattedNumber format(DecimalQuantity fq) {
MacroProps macros = resolve();
NumberStringBuilder string = new NumberStringBuilder();
// TODO: Make this more like C++, where we get and then conditionally atomic-increment?
long currentCount = callCount.incrementAndGet(this);
MicroProps micros;
if (currentCount == macros.threshold.longValue()) {

View file

@ -31,6 +31,12 @@ public final class NumberFormatter {
AUTO, ALWAYS, NEVER, ACCOUNTING, ACCOUNTING_ALWAYS,
}
/**
* Use a default threshold of 3. This means that the third time .format() is called, the data structures get built
* using the "safe" code path. The first two calls to .format() will trigger the unsafe code path.
*/
static final long DEFAULT_THRESHOLD = 3;
public static UnlocalizedNumberFormatter with() {
return BASE;
}

View file

@ -7,7 +7,6 @@ import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.PatternStringParser;
import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo;
import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
import com.ibm.icu.text.CompactDecimalFormat.CompactType;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberingSystem;
@ -17,6 +16,7 @@ import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.NoUnit;
import com.ibm.icu.util.ULocale;
import newapi.CompactNotation.CompactType;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.SignDisplay;
import newapi.NumberFormatter.UnitWidth;
@ -90,6 +90,9 @@ class NumberFormatterImpl {
boolean perMille = false;
PluralRules rules = macros.rules;
// FIXME
String nsName = NumberingSystem.getInstance(macros.loc).getName();
MicroProps micros = new MicroProps(safe);
MicroPropsGenerator chain = micros;
@ -242,7 +245,7 @@ class NumberFormatterImpl {
CompactType compactType = (macros.unit instanceof Currency && macros.unitWidth != UnitWidth.FULL_NAME)
? CompactType.CURRENCY
: CompactType.DECIMAL;
chain = ((CompactNotation) macros.notation).withLocaleData(macros.loc, compactType, rules,
chain = ((CompactNotation) macros.notation).withLocaleData(macros.loc, nsName, compactType, rules,
safe ? patternMod : null, chain);
}

View file

@ -3,7 +3,6 @@
package newapi.impl;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@ -12,89 +11,92 @@ import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CompactDecimalFormat.CompactType;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.ICUException;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
import newapi.CompactNotation.CompactType;
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);
String nsName = NumberingSystem.getInstance(locale).getName();
ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
// 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;
}
/** 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();
}
/** Java-only method used by CLDR tooling. */
public static CompactData getInstance(Map<String, Map<String, String>> powersToPluralsToPatterns) {
CompactData data = new CompactData();
for (Map.Entry<String, Map<String, String>> magnitudeEntry : powersToPluralsToPatterns.entrySet()) {
byte magnitude = (byte) (magnitudeEntry.getKey().length() - 1);
for (Map.Entry<String, String> 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;
}
// 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 = "<USE FALLBACK>";
private final String[] patterns;
private final byte[] multipliers;
private byte largestMagnitude;
private boolean isEmpty;
private int largestMagnitude;
private static final int MAX_DIGITS = 15;
private static final int COMPACT_MAX_DIGITS = 15;
private CompactData() {
patterns = new String[(CompactData.MAX_DIGITS + 1) * StandardPlural.COUNT];
multipliers = new byte[CompactData.MAX_DIGITS + 1];
isEmpty = true;
public CompactData() {
patterns = new String[(CompactData.COMPACT_MAX_DIGITS + 1) * StandardPlural.COUNT];
multipliers = new byte[CompactData.COMPACT_MAX_DIGITS + 1];
largestMagnitude = 0;
isEmpty = true;
}
public boolean isEmpty() {
return isEmpty;
public void populate(ULocale locale, String nsName, CompactStyle compactStyle, CompactType compactType) {
assert isEmpty;
CompactDataSink sink = new CompactDataSink(this);
ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
boolean nsIsLatn = nsName.equals("latn");
boolean compactIsShort = compactStyle == CompactStyle.SHORT;
// Fall back to latn numbering system and/or short compact style.
StringBuilder resourceKey = new StringBuilder();
getResourceBundleKey(nsName, compactStyle, compactType, resourceKey);
rb.getAllItemsWithFallbackNoFail(resourceKey.toString(), sink);
if (isEmpty && !nsIsLatn) {
getResourceBundleKey("latn", compactStyle, compactType, resourceKey);
rb.getAllItemsWithFallbackNoFail(resourceKey.toString(), sink);
}
if (isEmpty && !compactIsShort) {
getResourceBundleKey(nsName, CompactStyle.SHORT, compactType, resourceKey);
rb.getAllItemsWithFallbackNoFail(resourceKey.toString(), sink);
}
if (isEmpty && !nsIsLatn && !compactIsShort) {
getResourceBundleKey("latn", CompactStyle.SHORT, compactType, resourceKey);
rb.getAllItemsWithFallbackNoFail(resourceKey.toString(), sink);
}
// The last fallback should be guaranteed to return data.
if (isEmpty) {
throw new ICUException("Could not load compact decimal data for locale " + locale);
}
}
/** Produces a string like "NumberElements/latn/patternsShort/decimalFormat". */
private static void getResourceBundleKey(String nsName, CompactStyle compactStyle, CompactType compactType, StringBuilder sb) {
sb.setLength(0);
sb.append("NumberElements/");
sb.append(nsName);
sb.append(compactStyle == CompactStyle.SHORT ? "/patternsShort" : "/patternsLong");
sb.append(compactType == CompactType.DECIMAL ? "/decimalFormat" : "/currencyFormat");
}
/** Java-only method used by CLDR tooling. */
public void populate(Map<String, Map<String, String>> powersToPluralsToPatterns) {
assert isEmpty;
for (Map.Entry<String, Map<String, String>> magnitudeEntry : powersToPluralsToPatterns.entrySet()) {
byte magnitude = (byte) (magnitudeEntry.getKey().length() - 1);
for (Map.Entry<String, String> pluralEntry : magnitudeEntry.getValue().entrySet()) {
StandardPlural plural = StandardPlural.fromString(pluralEntry.getKey().toString());
String patternString = pluralEntry.getValue().toString();
patterns[getIndex(magnitude, plural)] = patternString;
int numZeros = countZeros(patternString);
if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun"
// Save the multiplier.
multipliers[magnitude] = (byte) (numZeros - magnitude - 1);
if (magnitude > largestMagnitude) {
largestMagnitude = magnitude;
}
isEmpty = false;
}
}
}
}
@Override
@ -108,23 +110,6 @@ public class CompactData implements MultiplierProducer {
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;
@ -144,32 +129,13 @@ public class CompactData implements MultiplierProducer {
return patternString;
}
public Set<String> getAllPatterns() {
Set<String> result = new HashSet<String>();
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();
public void getUniquePatterns(Set<String> output) {
assert output.isEmpty();
// NOTE: In C++, this is done more manually with a UVector.
// In Java, we can take advantage of JDK HashSet.
output.addAll(Arrays.asList(patterns));
output.remove(USE_FALLBACK);
output.remove(null);
}
private static final class CompactDataSink extends UResource.Sink {
@ -189,16 +155,17 @@ public class CompactData implements MultiplierProducer {
// 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;
byte multiplier = data.multipliers[magnitude];
assert magnitude < COMPACT_MAX_DIGITS;
// Iterate over the plural variants ("one", "other", etc)
UResource.Table pluralVariantsTable = value.getTable();
for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
// Skip this magnitude/plural if we already have it from a child locale.
// Note: This also skips USE_FALLBACK entries.
StandardPlural plural = StandardPlural.fromString(key.toString());
if (data.has(magnitude, plural)) {
if (data.patterns[getIndex(magnitude, plural)] != null) {
continue;
}
@ -206,12 +173,11 @@ public class CompactData implements MultiplierProducer {
// to parent locales. Example locale where this is relevant: 'it'.
String patternString = value.toString();
if (patternString.equals("0")) {
data.setNoFallback(magnitude, plural);
continue;
patternString = USE_FALLBACK;
}
// Save the pattern string. We will parse it lazily.
data.setPattern(patternString, magnitude, plural);
data.patterns[getIndex(magnitude, plural)] = patternString;
// If necessary, compute the multiplier: the difference between the magnitude
// and the number of zeros in the pattern.
@ -223,11 +189,24 @@ public class CompactData implements MultiplierProducer {
}
}
data.setMultiplier(magnitude, multiplier);
// Save the multiplier.
if (data.multipliers[magnitude] == 0) {
data.multipliers[magnitude] = multiplier;
if (magnitude > data.largestMagnitude) {
data.largestMagnitude = magnitude;
}
data.isEmpty = false;
} else {
assert data.multipliers[magnitude] == multiplier;
}
}
}
}
private static final int getIndex(int magnitude, StandardPlural plural) {
return magnitude * StandardPlural.COUNT + plural.ordinal();
}
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.

View file

@ -117,7 +117,7 @@ public class Padder {
// Should not happen since currency spacing is always on the inside.
throw new AssertionError();
}
length += string.insert(insertIndex, paddingString, null);
addPaddingHelper(paddingString, requiredPadding, string, insertIndex);
}
return length;
@ -126,6 +126,7 @@ public class Padder {
private static int addPaddingHelper(String paddingString, int requiredPadding, NumberStringBuilder string,
int index) {
for (int i = 0; i < requiredPadding; i++) {
// TODO: If appending to the end, this will cause actual insertion operations. Improve.
string.insert(index, paddingString, null);
}
return paddingString.length() * requiredPadding;

View file

@ -1015,6 +1015,15 @@ public class NumberFormatterTest {
ULocale.ENGLISH,
0.8888,
"88.88%**");
assertFormatSingle(
"Currency Spacing with Zero Digit Padding Broken",
"$GBP unit-width=ISO_CODE",
NumberFormatter.with().padding(Padder.codePoints('0', 12, PadPosition.AFTER_PREFIX)).unit(GBP)
.unitWidth(UnitWidth.ISO_CODE),
ULocale.ENGLISH,
514.23,
"GBP 000514.23"); // TODO: This is broken; it renders too wide (13 instead of 12).
}
@Test
@ -1330,6 +1339,8 @@ public class NumberFormatterTest {
public void locale() {
// Coverage for the locale setters.
assertEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.with().locale(Locale.ENGLISH));
assertEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.withLocale(ULocale.ENGLISH));
assertEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.withLocale(Locale.ENGLISH));
assertNotEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.with().locale(Locale.FRENCH));
}