| /* |
| ******************************************************************************* |
| * 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.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 SHORT_CURRENCY_STYLE = "shortCurrency"; |
| 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"; |
| private static final String PATTERNS_SHORT_CURRENCY_PATH = "patternsShort/currencyFormat"; |
| |
| 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<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; |
| |
| Data(long[] divisors, Map<String, DecimalFormat.Unit[]> 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 for decimals, and |
| * short only for currencies. |
| * |
| * @author Travis Keep |
| */ |
| static class DataBundle { |
| Data shortData; |
| Data longData; |
| Data shortCurrencyData; |
| |
| DataBundle(Data shortData, Data longData, Data shortCurrencyData) { |
| this.shortData = shortData; |
| this.longData = longData; |
| this.shortCurrencyData = shortCurrencyData; |
| } |
| } |
| |
| 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:<ol> |
| * <li>local numbering system no ROOT fallback</li> |
| * <li>latin numbering system no ROOT fallback</li> |
| * <li>latin numbering system ROOT locale.</li> |
| * </ol> |
| * 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; |
| ICUResourceBundle shortCurrencyDataBundle = 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); |
| shortCurrencyDataBundle = findWithFallback(bundle, PATTERNS_SHORT_CURRENCY_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; |
| } |
| } |
| } |
| if (shortCurrencyDataBundle == null) { |
| ICUResourceBundle bundle = getWithFallback(r, LATIN_NUMBERING_SYSTEM, UResFlags.ANY); |
| shortCurrencyDataBundle = getWithFallback(bundle, PATTERNS_SHORT_CURRENCY_PATH, UResFlags.ANY); |
| } |
| Data shortData = loadStyle(shortDataBundle, ulocale, SHORT_STYLE); |
| Data longData; |
| if (longDataBundle == null) { |
| longData = shortData; |
| } else { |
| longData = loadStyle(longDataBundle, ulocale, LONG_STYLE); |
| } |
| Data shortCurrencyData = loadStyle(shortCurrencyDataBundle, ulocale, SHORT_CURRENCY_STYLE); |
| return new DataBundle(shortData, longData, shortCurrencyData); |
| } |
| |
| /** |
| * 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<String, DecimalFormat.Unit[]>()); |
| 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: |
| * <pre> |
| * 10000{ |
| * few{"00K"} |
| * many{"00K"} |
| * one{"00 xnb"} |
| * other{"00 xnb"} |
| * } |
| * </pre> |
| * @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 = template.substring(0, firstIdx); |
| String suffix = 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; |
| } |
| |
| |
| /** |
| * 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 <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 void saveUnit( |
| DecimalFormat.Unit unit, String pluralVariant, int idx, |
| Map<String, DecimalFormat.Unit[]> 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<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]; |
| } |
| } |