com.ibm.icu.text.CompactDecimalDataCache Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of icu4j Show documentation
Show all versions of icu4j Show documentation
International Component for Unicode for Java (ICU4J) is a mature, widely used Java library
providing Unicode and Globalization support
/*
*******************************************************************************
* Copyright (C) 2012-2013, 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.ICUResourceBundle;
import com.ibm.icu.impl.SimpleCache;
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 NUMBER_ELEMENTS = "NumberElements";
private static final String PATTERN_LONG_PATH = "patternsLong/decimalFormat";
private static final String PATTERNS_SHORT_PATH = "patternsShort/decimalFormat";
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 static final String LATIN_NUMBERING_SYSTEM = "latn";
private final ICUCache cache =
new SimpleCache();
/**
* 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, divisors[4]
* 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 units;
Data(long[] divisors, Map units) {
this.divisors = divisors;
this.units = units;
}
}
/**
* DataBundle contains compact decimal data for all the styles in a particular
* locale. Currently available styles are short and long.
*
* @author Travis Keep
*/
static class DataBundle {
Data shortData;
Data longData;
DataBundle(Data shortData, Data longData) {
this.shortData = shortData;
this.longData = longData;
}
}
private static enum QuoteState {
OUTSIDE, // Outside single quote
INSIDE_EMPTY, // Just inside single quote
INSIDE_FULL // Inside single quote along with characters
}
// private static enum DataLocation { // Don't change order
// LOCAL, // In local numbering system
// LATIN, // In latin numbering system
// ROOT // In root locale
// }
private static enum UResFlags {
ANY, // Any locale will do.
NOT_ROOT // Locale cannot be root.
}
/**
* 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;
}
/**
* Loads the "patternsShort" and "patternsLong" data for a particular locale.
* We look for both of them in 3 places in this order:
* - local numbering system no ROOT fallback
* - latin numbering system no ROOT fallback
* - latin numbering system ROOT locale.
*
* If we find "patternsShort" data before finding "patternsLong" data, we
* make the "patternsLong" data be the same as "patternsShort."
* @param ulocale the locale for which we are loading the data.
* @return The returned data, never null.
*/
private static DataBundle load(ULocale ulocale) {
NumberingSystem ns = NumberingSystem.getInstance(ulocale);
ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, ulocale);
r = r.getWithFallback(NUMBER_ELEMENTS);
String numberingSystemName = ns.getName();
ICUResourceBundle shortDataBundle = null;
ICUResourceBundle longDataBundle = null;
if (!LATIN_NUMBERING_SYSTEM.equals(numberingSystemName)) {
ICUResourceBundle bundle = findWithFallback(r, numberingSystemName, UResFlags.NOT_ROOT);
shortDataBundle = findWithFallback(bundle, PATTERNS_SHORT_PATH, UResFlags.NOT_ROOT);
longDataBundle = findWithFallback(bundle, PATTERN_LONG_PATH, UResFlags.NOT_ROOT);
}
// If we haven't found, look in latin numbering system.
if (shortDataBundle == null) {
ICUResourceBundle bundle = getWithFallback(r, LATIN_NUMBERING_SYSTEM, UResFlags.ANY);
shortDataBundle = getWithFallback(bundle, PATTERNS_SHORT_PATH, UResFlags.ANY);
if (longDataBundle == null) {
longDataBundle = findWithFallback(bundle, PATTERN_LONG_PATH, UResFlags.ANY);
if (longDataBundle != null && isRoot(longDataBundle) && !isRoot(shortDataBundle)) {
longDataBundle = null;
}
}
}
Data shortData = loadStyle(shortDataBundle, ulocale, SHORT_STYLE);
Data longData;
if (longDataBundle == null) {
longData = shortData;
} else {
longData = loadStyle(longDataBundle, ulocale, LONG_STYLE);
}
return new DataBundle(shortData, longData);
}
/**
* findWithFallback finds a sub-resource bundle within r.
* @param r a resource bundle. It may be null in which case sub-resource bundle
* won't be found.
* @param path the path relative to r
* @param flags ANY or NOT_ROOT for locale of found sub-resource bundle.
* @return The sub-resource bundle or NULL if none found.
*/
private static ICUResourceBundle findWithFallback(
ICUResourceBundle r, String path, UResFlags flags) {
if (r == null) {
return null;
}
ICUResourceBundle result = r.findWithFallback(path);
if (result == null) {
return null;
}
switch (flags) {
case NOT_ROOT:
return isRoot(result) ? null : result;
case ANY:
return result;
default:
throw new IllegalArgumentException();
}
}
/**
* Like findWithFallback but throws MissingResourceException if no
* resource found instead of returning null.
*/
private static ICUResourceBundle getWithFallback(
ICUResourceBundle r, String path, UResFlags flags) {
ICUResourceBundle result = findWithFallback(r, path, flags);
if (result == null) {
throw new MissingResourceException(
"Cannot find " + path,
ICUResourceBundle.class.getName(), path);
}
return result;
}
/**
* isRoot returns true if r is in root locale or false otherwise.
*/
private static boolean isRoot(ICUResourceBundle r) {
ULocale bundleLocale = r.getULocale();
// Note: bundleLocale for root should be ULocale.ROOT, which is equivalent to new ULocale("").
// However, resource bundle might be initialized with locale ID "root", which should be
// actually normalized to "" in ICUResourceBundle. For now, this logic also compare to
// "root", not just ULocale.ROOT.
return bundleLocale.equals(ULocale.ROOT) || bundleLocale.toString().equals("root");
}
/**
* Loads the data
* @param r the main resource bundle.
* @param numberingSystemName The namespace name.
* @param allowNullResult If true, returns null if no data can be found
* for particular locale and style. If false, throws a runtime exception
* if data cannot be found.
* @return The loaded data or possibly null if allowNullResult is true.
*/
private static Data loadStyle(ICUResourceBundle r, ULocale locale, String style) {
int size = r.getSize();
Data result = new Data(
new long[MAX_DIGITS],
new HashMap());
for (int i = 0; i < size; i++) {
populateData(r.get(i), locale, style, result);
}
fillInMissing(result);
return result;
}
/**
* Populates Data object with data for a particular divisor from resource bundle.
* @param divisorData represents the rules for numbers of a particular size.
* This may look like:
*
* 10000{
* few{"00K"}
* many{"00K"}
* one{"00 xnb"}
* other{"00 xnb"}
* }
*
* @param locale the locale
* @param style the style
* @param result rule stored here.
*
*/
private static void populateData(
UResourceBundle divisorData, ULocale locale, String style, Data result) {
// This value will always be some even pwoer of 10. e.g 10000.
long magnitude = Long.parseLong(divisorData.getKey());
int thisIndex = (int) Math.log10(magnitude);
// Silently ignore divisors that are too big.
if (thisIndex >= MAX_DIGITS) {
return;
}
int size = divisorData.getSize();
// keep track of how many zeros are used in the plural variants.
// For "00K" this would be 2. This number must be the same for all
// plural variants. If they differ, we throw a runtime exception as
// such an anomaly is unrecoverable. We expect at least one zero.
int numZeros = 0;
// Keep track if this block defines "other" variant. If a block
// fails to define the "other" variant, we must immediately throw
// an exception as it is assumed that "other" variants are always
// defined.
boolean otherVariantDefined = false;
// Loop over all the plural variants. e.g one, other.
for (int i = 0; i < size; i++) {
UResourceBundle pluralVariantData = divisorData.get(i);
String pluralVariant = pluralVariantData.getKey();
String template = pluralVariantData.getString();
if (pluralVariant.equals(OTHER)) {
otherVariantDefined = true;
}
int nz = populatePrefixSuffix(
pluralVariant, thisIndex, template, locale, style, result);
if (nz != numZeros) {
if (numZeros != 0) {
throw new IllegalArgumentException(
"Plural variant '" + pluralVariant + "' template '" +
template + "' for 10^" + thisIndex +
" has wrong number of zeros in " + localeAndStyle(locale, style));
}
numZeros = nz;
}
}
if (!otherVariantDefined) {
throw new IllegalArgumentException(
"No 'other' plural variant defined for 10^" + thisIndex +
"in " +localeAndStyle(locale, style));
}
// 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 = magnitude;
for (int i = 1; i < numZeros; i++) {
divisor /= 10;
}
result.divisors[thisIndex] = divisor;
}
/**
* 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 result Extracted prefix and suffix stored here.
* @return number of zeros found before any decimal point in template.
*/
private static int populatePrefixSuffix(
String pluralVariant, int idx, String template, ULocale locale, String style,
Data result) {
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 = fixQuotes(template.substring(0, firstIdx));
String suffix = fixQuotes(template.substring(lastIdx + 1));
saveUnit(new DecimalFormat.Unit(prefix, suffix), pluralVariant, idx, result.units);
// 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;
}
private static String fixQuotes(String prefixOrSuffix) {
StringBuilder result = new StringBuilder();
int len = prefixOrSuffix.length();
QuoteState state = QuoteState.OUTSIDE;
for (int idx = 0; idx < len; idx++) {
char ch = prefixOrSuffix.charAt(idx);
if (ch == '\'') {
if (state == QuoteState.INSIDE_EMPTY) {
result.append('\'');
}
} else {
result.append(ch);
}
// Update state
switch (state) {
case OUTSIDE:
state = ch == '\'' ? QuoteState.INSIDE_EMPTY : QuoteState.OUTSIDE;
break;
case INSIDE_EMPTY:
case INSIDE_FULL:
state = ch == '\'' ? QuoteState.OUTSIDE : QuoteState.INSIDE_FULL;
break;
default:
throw new IllegalStateException();
}
}
return result.toString();
}
/**
* Returns locale and style. Used to form useful messages in thrown
* exceptions.
* @param locale the locale
* @param style the style
*/
private static String localeAndStyle(ULocale locale, String style) {
return "locale '" + locale + "' style '" + 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 result
.
* It looks at each log10 value applying the two rules.
*
* 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.
*
* 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.
*
*
* @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 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 units) {
for (DecimalFormat.Unit[] byBase : units.values()) {
if (idx == 0) {
byBase[idx] = DecimalFormat.NULL_UNIT;
} else {
byBase[idx] = byBase[idx - 1];
}
}
}
private static void saveUnit(
DecimalFormat.Unit unit, String pluralVariant, int idx,
Map units) {
DecimalFormat.Unit[] byBase = units.get(pluralVariant);
if (byBase == null) {
byBase = new DecimalFormat.Unit[MAX_DIGITS];
units.put(pluralVariant, byBase);
}
byBase[idx] = unit;
}
/**
* 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 units, String variant, int base) {
DecimalFormat.Unit[] byBase = units.get(variant);
if (byBase == null) {
byBase = units.get(CompactDecimalDataCache.OTHER);
}
return byBase[base];
}
}