blob: 8d302a559c99c47736d56b9f32d3890ca7d7b29c [file] [log] [blame]
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
*******************************************************************************
* Copyright (C) 2012-2016, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.text;
import java.util.HashMap;
import java.util.Map;
import java.util.MissingResourceException;
import com.ibm.icu.impl.ICUCache;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.SimpleCache;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.text.DecimalFormat.Unit;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
/**
* A cache containing data by locale for {@link CompactDecimalFormat}
*
* @author Travis Keep
*/
class CompactDecimalDataCache {
private static final String SHORT_STYLE = "short";
private static final String LONG_STYLE = "long";
private static final String SHORT_CURRENCY_STYLE = "shortCurrency";
private static final String NUMBER_ELEMENTS = "NumberElements";
private static final String PATTERNS_LONG = "patternsLong";
private static final String PATTERNS_SHORT = "patternsShort";
private static final String DECIMAL_FORMAT = "decimalFormat";
private static final String CURRENCY_FORMAT = "currencyFormat";
private static final String LATIN_NUMBERING_SYSTEM = "latn";
private static enum PatternsTableKey { PATTERNS_LONG, PATTERNS_SHORT };
private static enum FormatsTableKey { DECIMAL_FORMAT, CURRENCY_FORMAT };
public static final String OTHER = "other";
/**
* We can specify prefixes or suffixes for values with up to 15 digits,
* less than 10^15.
*/
static final int MAX_DIGITS = 15;
private final ICUCache<ULocale, DataBundle> cache =
new SimpleCache<ULocale, DataBundle>();
/**
* Data contains the compact decimal data for a particular locale. Data consists
* of one array and two hashmaps. The index of the divisors array as well
* as the arrays stored in the values of the two hashmaps correspond
* to log10 of the number being formatted, so when formatting 12,345, the 4th
* index of the arrays should be used. Divisors contain the number to divide
* by before doing formatting. In the case of english, <code>divisors[4]</code>
* is 1000. So to format 12,345, divide by 1000 to get 12. Then use
* PluralRules with the current locale to figure out which of the 6 plural variants
* 12 matches: "zero", "one", "two", "few", "many", or "other." Prefixes and
* suffixes are maps whose key is the plural variant and whose values are
* arrays of strings with indexes corresponding to log10 of the original number.
* these arrays contain the prefix or suffix to use.
*
* Each array in data is 15 in length, and every index is filled.
*
* @author Travis Keep
*
*/
static class Data {
long[] divisors;
Map<String, DecimalFormat.Unit[]> units;
boolean fromFallback;
Data(long[] divisors, Map<String, DecimalFormat.Unit[]> units)
{
this.divisors = divisors;
this.units = units;
}
public boolean isEmpty() {
return units == null || units.isEmpty();
}
}
/**
* DataBundle contains compact decimal data for all the styles in a particular
* locale. Currently available styles are short and long for decimals, and
* short only for currencies.
*
* @author Travis Keep
*/
static class DataBundle {
Data shortData;
Data longData;
Data shortCurrencyData;
private DataBundle(Data shortData, Data longData, Data shortCurrencyData) {
this.shortData = shortData;
this.longData = longData;
this.shortCurrencyData = shortCurrencyData;
}
private static DataBundle createEmpty() {
return new DataBundle(
new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>())
);
}
}
/**
* Sink for enumerating all of the compact decimal format patterns.
*
* More specific bundles (en_GB) are enumerated before their parents (en_001, en, root):
* Only store a value if it is still missing, that is, it has not been overridden.
*/
private static final class CompactDecimalDataSink extends UResource.Sink {
private DataBundle dataBundle; // Where to save values when they are read
private ULocale locale; // The locale we are traversing (for exception messages)
private boolean isLatin; // Whether or not we are traversing the Latin table
private boolean isFallback; // Whether or not we are traversing the Latin table as fallback
/*
* 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 CompactDecimalDataSink(DataBundle dataBundle, ULocale locale) {
this.dataBundle = dataBundle;
this.locale = locale;
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
// SPECIAL CASE: Don't consume root in the non-Latin numbering system
if (isRoot && !isLatin) { return; }
UResource.Table patternsTable = value.getTable();
for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
// patterns table: check for patternsShort or patternsLong
PatternsTableKey patternsTableKey;
if (key.contentEquals(PATTERNS_SHORT)) {
patternsTableKey = PatternsTableKey.PATTERNS_SHORT;
} else if (key.contentEquals(PATTERNS_LONG)) {
patternsTableKey = PatternsTableKey.PATTERNS_LONG;
} else {
continue;
}
// traverse into the table of formats
UResource.Table formatsTable = value.getTable();
for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
// formats table: check for decimalFormat or currencyFormat
FormatsTableKey formatsTableKey;
if (key.contentEquals(DECIMAL_FORMAT)) {
formatsTableKey = FormatsTableKey.DECIMAL_FORMAT;
} else if (key.contentEquals(CURRENCY_FORMAT)) {
formatsTableKey = FormatsTableKey.CURRENCY_FORMAT;
} else {
continue;
}
// Set the current style and destination based on the lvl1 and lvl2 keys
String style = null;
Data destination = null;
if (patternsTableKey == PatternsTableKey.PATTERNS_LONG
&& formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
style = LONG_STYLE;
destination = dataBundle.longData;
} else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
&& formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
style = SHORT_STYLE;
destination = dataBundle.shortData;
} else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
&& formatsTableKey == FormatsTableKey.CURRENCY_FORMAT) {
style = SHORT_CURRENCY_STYLE;
destination = dataBundle.shortCurrencyData;
} else {
// Silently ignore this case
continue;
}
// SPECIAL CASE: RULES FOR WHETHER OR NOT TO CONSUME THIS TABLE:
// 1) Don't consume longData if shortData was consumed from the non-Latin
// locale numbering system
// 2) Don't consume longData for the first time if this is the root bundle and
// shortData is already populated from a more specific locale. Note that if
// both longData and shortData are both only in root, longData will be
// consumed since it is alphabetically before shortData in the bundle.
if (isFallback
&& style == LONG_STYLE
&& !dataBundle.shortData.isEmpty()
&& !dataBundle.shortData.fromFallback) {
continue;
}
if (isRoot
&& style == LONG_STYLE
&& dataBundle.longData.isEmpty()
&& !dataBundle.shortData.isEmpty()) {
continue;
}
// Set the "fromFallback" flag on the data object
destination.fromFallback = isFallback;
// traverse into the table of powers of ten
UResource.Table powersOfTenTable = value.getTable();
for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
// This value will always be some even power of 10. e.g 10000.
long power10 = Long.parseLong(key.toString());
int log10Value = (int) Math.log10(power10);
// Silently ignore divisors that are too big.
if (log10Value >= MAX_DIGITS) continue;
// Iterate over the plural variants ("one", "other", etc)
UResource.Table pluralVariantsTable = value.getTable();
for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
// TODO: Use StandardPlural rather than String.
String pluralVariant = key.toString();
String template = value.toString();
// Copy the data into the in-memory data bundle (do not overwrite
// existing values)
int numZeros = populatePrefixSuffix(
pluralVariant, log10Value, template, locale, style, destination, false);
// If populatePrefixSuffix returns -1, it means that this key has been
// encountered already.
if (numZeros < 0) {
continue;
}
// Set the divisor, which is based on the number of zeros in the template
// string. If the divisor from here is different from the one previously
// stored, it means that the number of zeros in different plural variants
// differs; throw an exception.
long divisor = calculateDivisor(power10, numZeros);
if (destination.divisors[log10Value] != 0L
&& destination.divisors[log10Value] != divisor) {
throw new IllegalArgumentException("Plural variant '" + pluralVariant
+ "' template '" + template
+ "' for 10^" + log10Value
+ " has wrong number of zeros in " + localeAndStyle(locale, style));
}
destination.divisors[log10Value] = divisor;
}
}
}
}
}
}
/**
* Fetch data for a particular locale. Clients must not modify any part of the returned data. Portions of returned
* data may be shared so modifying it will have unpredictable results.
*/
DataBundle get(ULocale locale) {
DataBundle result = cache.get(locale);
if (result == null) {
result = load(locale);
cache.put(locale, result);
}
return result;
}
private static DataBundle load(ULocale ulocale) throws MissingResourceException {
DataBundle dataBundle = DataBundle.createEmpty();
String nsName = NumberingSystem.getInstance(ulocale).getName();
ICUResourceBundle r = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
ulocale);
CompactDecimalDataSink sink = new CompactDecimalDataSink(dataBundle, ulocale);
sink.isFallback = false;
// First load the number elements data from nsName if nsName is not Latin.
if (!nsName.equals(LATIN_NUMBERING_SYSTEM)) {
sink.isLatin = false;
try {
r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + nsName, sink);
} catch (MissingResourceException e) {
// Silently ignore and use Latin
}
// Set the "isFallback" flag for when we read Latin
sink.isFallback = true;
}
// Now load Latin, which will fill in things that were left out from above.
sink.isLatin = true;
r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + LATIN_NUMBERING_SYSTEM, sink);
// If longData is empty, default it to be equal to shortData
if (dataBundle.longData.isEmpty()) {
dataBundle.longData = dataBundle.shortData;
}
// Check for "other" variants in each of the three data classes
checkForOtherVariants(dataBundle.longData, ulocale, LONG_STYLE);
checkForOtherVariants(dataBundle.shortData, ulocale, SHORT_STYLE);
checkForOtherVariants(dataBundle.shortCurrencyData, ulocale, SHORT_CURRENCY_STYLE);
// Resolve missing elements
fillInMissing(dataBundle.longData);
fillInMissing(dataBundle.shortData);
fillInMissing(dataBundle.shortCurrencyData);
// Return the data bundle
return dataBundle;
}
/**
* Populates prefix and suffix information for a particular plural variant
* and index (log10 value).
* @param pluralVariant e.g "one", "other"
* @param idx the index (log10 value of the number) 0 <= idx < MAX_DIGITS
* @param template e.g "00K"
* @param locale the locale
* @param style the style
* @param destination Extracted prefix and suffix stored here.
* @return number of zeros found before any decimal point in template, or -1 if it was not saved.
*/
private static int populatePrefixSuffix(
String pluralVariant, int idx, String template, ULocale locale, String style,
Data destination, boolean overwrite) {
int firstIdx = template.indexOf("0");
int lastIdx = template.lastIndexOf("0");
if (firstIdx == -1) {
throw new IllegalArgumentException(
"Expect at least one zero in template '" + template +
"' for variant '" +pluralVariant + "' for 10^" + idx +
" in " + localeAndStyle(locale, style));
}
String prefix = template.substring(0, firstIdx);
String suffix = template.substring(lastIdx + 1);
// Save the unit, and return -1 if it was not saved
boolean saved = saveUnit(new DecimalFormat.Unit(prefix, suffix), pluralVariant, idx, destination.units, overwrite);
if (!saved) {
return -1;
}
// If there is effectively no prefix or suffix, ignore the actual
// number of 0's and act as if the number of 0's matches the size
// of the number
if (prefix.trim().length() == 0 && suffix.trim().length() == 0) {
return idx + 1;
}
// Calculate number of zeros before decimal point.
int i = firstIdx + 1;
while (i <= lastIdx && template.charAt(i) == '0') {
i++;
}
return i - firstIdx;
}
/**
* Calculate a divisor based on the magnitude and number of zeros in the
* template string.
* @param power10
* @param numZeros
* @return
*/
private static long calculateDivisor(long power10, int numZeros) {
// We craft our divisor such that when we divide by it, we get a
// number with the same number of digits as zeros found in the
// plural variant templates. If our magnitude is 10000 and we have
// two 0's in our plural variants, then we want a divisor of 1000.
// Note that if we have 43560 which is of same magnitude as 10000.
// When we divide by 1000 we a quotient which rounds to 44 (2 digits)
long divisor = power10;
for (int i = 1; i < numZeros; i++) {
divisor /= 10;
}
return divisor;
}
/**
* Returns locale and style. Used to form useful messages in thrown exceptions.
*
* Note: This is not covered by unit tests since no exceptions are thrown on the default CLDR data. It is too
* cumbersome to cover via reflection.
*
* @param locale the locale
* @param style the style
*/
private static String localeAndStyle(ULocale locale, String style) {
return "locale '" + locale + "' style '" + style + "'";
}
/**
* Checks to make sure that an "other" variant is present in all powers of 10.
* @param data
*/
private static void checkForOtherVariants(Data data, ULocale locale, String style) {
DecimalFormat.Unit[] otherByBase = data.units.get(OTHER);
if (otherByBase == null) {
throw new IllegalArgumentException("No 'other' plural variants defined in "
+ localeAndStyle(locale, style));
}
// Check all other plural variants, and make sure that if any of them are populated, then
// other is also populated
for (Map.Entry<String, Unit[]> entry : data.units.entrySet()) {
if (entry.getKey() == OTHER) continue;
DecimalFormat.Unit[] variantByBase = entry.getValue();
for (int log10Value = 0; log10Value < MAX_DIGITS; log10Value++) {
if (variantByBase[log10Value] != null && otherByBase[log10Value] == null) {
throw new IllegalArgumentException(
"No 'other' plural variant defined for 10^" + log10Value
+ " but a '" + entry.getKey() + "' variant is defined"
+ " in " +localeAndStyle(locale, style));
}
}
}
}
/**
* After reading information from resource bundle into a Data object, there
* is guarantee that it is complete.
*
* This method fixes any incomplete data it finds within <code>result</code>.
* It looks at each log10 value applying the two rules.
* <p>
* If no prefix is defined for the "other" variant, use the divisor, prefixes and
* suffixes for all defined variants from the previous log10. For log10 = 0,
* use all empty prefixes and suffixes and a divisor of 1.
* </p><p>
* Otherwise, examine each plural variant defined for the given log10 value.
* If it has no prefix and suffix for a particular variant, use the one from the
* "other" variant.
* </p>
*
* @param result this instance is fixed in-place.
*/
private static void fillInMissing(Data result) {
// Initially we assume that previous divisor is 1 with no prefix or suffix.
long lastDivisor = 1L;
for (int i = 0; i < result.divisors.length; i++) {
if (result.units.get(OTHER)[i] == null) {
result.divisors[i] = lastDivisor;
copyFromPreviousIndex(i, result.units);
} else {
lastDivisor = result.divisors[i];
propagateOtherToMissing(i, result.units);
}
}
}
private static void propagateOtherToMissing(
int idx, Map<String, DecimalFormat.Unit[]> units) {
DecimalFormat.Unit otherVariantValue = units.get(OTHER)[idx];
for (DecimalFormat.Unit[] byBase : units.values()) {
if (byBase[idx] == null) {
byBase[idx] = otherVariantValue;
}
}
}
private static void copyFromPreviousIndex(int idx, Map<String, DecimalFormat.Unit[]> units) {
for (DecimalFormat.Unit[] byBase : units.values()) {
if (idx == 0) {
byBase[idx] = DecimalFormat.NULL_UNIT;
} else {
byBase[idx] = byBase[idx - 1];
}
}
}
private static boolean saveUnit(
DecimalFormat.Unit unit, String pluralVariant, int idx,
Map<String, DecimalFormat.Unit[]> units,
boolean overwrite) {
DecimalFormat.Unit[] byBase = units.get(pluralVariant);
if (byBase == null) {
byBase = new DecimalFormat.Unit[MAX_DIGITS];
units.put(pluralVariant, byBase);
}
// Don't overwrite a pre-existing value unless the "overwrite" flag is true.
if (!overwrite && byBase[idx] != null) {
return false;
}
// Save the value and return
byBase[idx] = unit;
return true;
}
/**
* Fetches a prefix or suffix given a plural variant and log10 value. If it
* can't find the given variant, it falls back to "other".
* @param prefixOrSuffix the prefix or suffix map
* @param variant the plural variant
* @param base log10 value. 0 <= base < MAX_DIGITS.
* @return the prefix or suffix.
*/
static DecimalFormat.Unit getUnit(
Map<String, DecimalFormat.Unit[]> units, String variant, int base) {
DecimalFormat.Unit[] byBase = units.get(variant);
if (byBase == null) {
byBase = units.get(CompactDecimalDataCache.OTHER);
}
return byBase[base];
}
}