Index: icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java |
=================================================================== |
--- icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java (revision 39589) |
+++ icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java (working copy) |
@@ -1,524 +0,0 @@ |
-// © 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]; |
- } |
-} |