• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html#License
3 /*
4  *******************************************************************************
5  * Copyright (C) 2012-2016, International Business Machines Corporation and
6  * others. All Rights Reserved.
7  *******************************************************************************
8  */
9 package com.ibm.icu.text;
10 
11 import java.util.HashMap;
12 import java.util.Map;
13 import java.util.MissingResourceException;
14 
15 import com.ibm.icu.impl.ICUCache;
16 import com.ibm.icu.impl.ICUData;
17 import com.ibm.icu.impl.ICUResourceBundle;
18 import com.ibm.icu.impl.SimpleCache;
19 import com.ibm.icu.impl.UResource;
20 import com.ibm.icu.text.DecimalFormat.Unit;
21 import com.ibm.icu.util.ULocale;
22 import com.ibm.icu.util.UResourceBundle;
23 
24 /**
25  * A cache containing data by locale for {@link CompactDecimalFormat}
26  *
27  * @author Travis Keep
28  */
29 class CompactDecimalDataCache {
30 
31     private static final String SHORT_STYLE = "short";
32     private static final String LONG_STYLE = "long";
33     private static final String SHORT_CURRENCY_STYLE = "shortCurrency";
34     private static final String NUMBER_ELEMENTS = "NumberElements";
35     private static final String PATTERNS_LONG = "patternsLong";
36     private static final String PATTERNS_SHORT = "patternsShort";
37     private static final String DECIMAL_FORMAT = "decimalFormat";
38     private static final String CURRENCY_FORMAT = "currencyFormat";
39     private static final String LATIN_NUMBERING_SYSTEM = "latn";
40 
41     private static enum PatternsTableKey { PATTERNS_LONG, PATTERNS_SHORT };
42     private static enum FormatsTableKey { DECIMAL_FORMAT, CURRENCY_FORMAT };
43 
44     public static final String OTHER = "other";
45 
46     /**
47      * We can specify prefixes or suffixes for values with up to 15 digits,
48      * less than 10^15.
49      */
50     static final int MAX_DIGITS = 15;
51 
52     private final ICUCache<ULocale, DataBundle> cache =
53             new SimpleCache<ULocale, DataBundle>();
54 
55     /**
56      * Data contains the compact decimal data for a particular locale. Data consists
57      * of one array and two hashmaps. The index of the divisors array as well
58      * as the arrays stored in the values of the two hashmaps correspond
59      * to log10 of the number being formatted, so when formatting 12,345, the 4th
60      * index of the arrays should be used. Divisors contain the number to divide
61      * by before doing formatting. In the case of english, <code>divisors[4]</code>
62      * is 1000.  So to format 12,345, divide by 1000 to get 12. Then use
63      * PluralRules with the current locale to figure out which of the 6 plural variants
64      * 12 matches: "zero", "one", "two", "few", "many", or "other." Prefixes and
65      * suffixes are maps whose key is the plural variant and whose values are
66      * arrays of strings with indexes corresponding to log10 of the original number.
67      * these arrays contain the prefix or suffix to use.
68      *
69      * Each array in data is 15 in length, and every index is filled.
70      *
71      * @author Travis Keep
72      *
73      */
74     static class Data {
75         long[] divisors;
76         Map<String, DecimalFormat.Unit[]> units;
77         boolean fromFallback;
78 
Data(long[] divisors, Map<String, DecimalFormat.Unit[]> units)79         Data(long[] divisors, Map<String, DecimalFormat.Unit[]> units)
80         {
81             this.divisors = divisors;
82             this.units = units;
83         }
84 
isEmpty()85         public boolean isEmpty() {
86             return units == null || units.isEmpty();
87         }
88     }
89 
90     /**
91      * DataBundle contains compact decimal data for all the styles in a particular
92      * locale. Currently available styles are short and long for decimals, and
93      * short only for currencies.
94      *
95      * @author Travis Keep
96      */
97     static class DataBundle {
98         Data shortData;
99         Data longData;
100         Data shortCurrencyData;
101 
DataBundle(Data shortData, Data longData, Data shortCurrencyData)102         private DataBundle(Data shortData, Data longData, Data shortCurrencyData) {
103             this.shortData = shortData;
104             this.longData = longData;
105             this.shortCurrencyData = shortCurrencyData;
106         }
107 
createEmpty()108         private static DataBundle createEmpty() {
109             return new DataBundle(
110                 new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
111                 new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
112                 new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>())
113             );
114         }
115     }
116 
117     /**
118      * Sink for enumerating all of the compact decimal format patterns.
119      *
120      * More specific bundles (en_GB) are enumerated before their parents (en_001, en, root):
121      * Only store a value if it is still missing, that is, it has not been overridden.
122      */
123     private static final class CompactDecimalDataSink extends UResource.Sink {
124 
125         private DataBundle dataBundle; // Where to save values when they are read
126         private ULocale locale; // The locale we are traversing (for exception messages)
127         private boolean isLatin; // Whether or not we are traversing the Latin table
128         private boolean isFallback; // Whether or not we are traversing the Latin table as fallback
129 
130         /*
131          * NumberElements{              <-- top (numbering system table)
132          *  latn{                       <-- patternsTable (one per numbering system)
133          *    patternsLong{             <-- formatsTable (one per pattern)
134          *      decimalFormat{          <-- powersOfTenTable (one per format)
135          *        1000{                 <-- pluralVariantsTable (one per power of ten)
136          *          one{"0 thousand"}   <-- plural variant and template
137          */
138 
CompactDecimalDataSink(DataBundle dataBundle, ULocale locale)139         public CompactDecimalDataSink(DataBundle dataBundle, ULocale locale) {
140             this.dataBundle = dataBundle;
141             this.locale = locale;
142         }
143 
144         @Override
put(UResource.Key key, UResource.Value value, boolean isRoot)145         public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
146             // SPECIAL CASE: Don't consume root in the non-Latin numbering system
147             if (isRoot && !isLatin) { return; }
148 
149             UResource.Table patternsTable = value.getTable();
150             for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
151 
152                 // patterns table: check for patternsShort or patternsLong
153                 PatternsTableKey patternsTableKey;
154                 if (key.contentEquals(PATTERNS_SHORT)) {
155                     patternsTableKey = PatternsTableKey.PATTERNS_SHORT;
156                 } else if (key.contentEquals(PATTERNS_LONG)) {
157                     patternsTableKey = PatternsTableKey.PATTERNS_LONG;
158                 } else {
159                     continue;
160                 }
161 
162                 // traverse into the table of formats
163                 UResource.Table formatsTable = value.getTable();
164                 for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
165 
166                     // formats table: check for decimalFormat or currencyFormat
167                     FormatsTableKey formatsTableKey;
168                     if (key.contentEquals(DECIMAL_FORMAT)) {
169                         formatsTableKey = FormatsTableKey.DECIMAL_FORMAT;
170                     } else if (key.contentEquals(CURRENCY_FORMAT)) {
171                         formatsTableKey = FormatsTableKey.CURRENCY_FORMAT;
172                     } else {
173                         continue;
174                     }
175 
176                     // Set the current style and destination based on the lvl1 and lvl2 keys
177                     String style = null;
178                     Data destination = null;
179                     if (patternsTableKey == PatternsTableKey.PATTERNS_LONG
180                             && formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
181                         style = LONG_STYLE;
182                         destination = dataBundle.longData;
183                     } else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
184                             && formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
185                         style = SHORT_STYLE;
186                         destination = dataBundle.shortData;
187                     } else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
188                             && formatsTableKey == FormatsTableKey.CURRENCY_FORMAT) {
189                         style = SHORT_CURRENCY_STYLE;
190                         destination = dataBundle.shortCurrencyData;
191                     } else {
192                         // Silently ignore this case
193                         continue;
194                     }
195 
196                     // SPECIAL CASE: RULES FOR WHETHER OR NOT TO CONSUME THIS TABLE:
197                     //   1) Don't consume longData if shortData was consumed from the non-Latin
198                     //      locale numbering system
199                     //   2) Don't consume longData for the first time if this is the root bundle and
200                     //      shortData is already populated from a more specific locale. Note that if
201                     //      both longData and shortData are both only in root, longData will be
202                     //      consumed since it is alphabetically before shortData in the bundle.
203                     if (isFallback
204                             && style == LONG_STYLE
205                             && !dataBundle.shortData.isEmpty()
206                             && !dataBundle.shortData.fromFallback) {
207                         continue;
208                     }
209                     if (isRoot
210                             && style == LONG_STYLE
211                             && dataBundle.longData.isEmpty()
212                             && !dataBundle.shortData.isEmpty()) {
213                         continue;
214                     }
215 
216                     // Set the "fromFallback" flag on the data object
217                     destination.fromFallback = isFallback;
218 
219                     // traverse into the table of powers of ten
220                     UResource.Table powersOfTenTable = value.getTable();
221                     for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
222 
223                         // This value will always be some even power of 10. e.g 10000.
224                         long power10 = Long.parseLong(key.toString());
225                         int log10Value = (int) Math.log10(power10);
226 
227                         // Silently ignore divisors that are too big.
228                         if (log10Value >= MAX_DIGITS) continue;
229 
230                         // Iterate over the plural variants ("one", "other", etc)
231                         UResource.Table pluralVariantsTable = value.getTable();
232                         for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
233                             // TODO: Use StandardPlural rather than String.
234                             String pluralVariant = key.toString();
235                             String template = value.toString();
236 
237                             // Copy the data into the in-memory data bundle (do not overwrite
238                             // existing values)
239                             int numZeros = populatePrefixSuffix(
240                                     pluralVariant, log10Value, template, locale, style, destination, false);
241 
242                             // If populatePrefixSuffix returns -1, it means that this key has been
243                             // encountered already.
244                             if (numZeros < 0) {
245                                 continue;
246                             }
247 
248                             // Set the divisor, which is based on the number of zeros in the template
249                             // string.  If the divisor from here is different from the one previously
250                             // stored, it means that the number of zeros in different plural variants
251                             // differs; throw an exception.
252                             long divisor = calculateDivisor(power10, numZeros);
253                             if (destination.divisors[log10Value] != 0L
254                                     && destination.divisors[log10Value] != divisor) {
255                                 throw new IllegalArgumentException("Plural variant '" + pluralVariant
256                                         + "' template '" + template
257                                         + "' for 10^" + log10Value
258                                         + " has wrong number of zeros in " + localeAndStyle(locale, style));
259                             }
260                             destination.divisors[log10Value] = divisor;
261                         }
262                     }
263                 }
264             }
265         }
266     }
267 
268     /**
269      * Fetch data for a particular locale. Clients must not modify any part of the returned data. Portions of returned
270      * data may be shared so modifying it will have unpredictable results.
271      */
get(ULocale locale)272     DataBundle get(ULocale locale) {
273         DataBundle result = cache.get(locale);
274         if (result == null) {
275             result = load(locale);
276             cache.put(locale, result);
277         }
278         return result;
279     }
280 
load(ULocale ulocale)281     private static DataBundle load(ULocale ulocale) throws MissingResourceException {
282         DataBundle dataBundle = DataBundle.createEmpty();
283         String nsName = NumberingSystem.getInstance(ulocale).getName();
284         ICUResourceBundle r = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
285                 ulocale);
286         CompactDecimalDataSink sink = new CompactDecimalDataSink(dataBundle, ulocale);
287         sink.isFallback = false;
288 
289         // First load the number elements data from nsName if nsName is not Latin.
290         if (!nsName.equals(LATIN_NUMBERING_SYSTEM)) {
291             sink.isLatin = false;
292 
293             try {
294                 r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + nsName, sink);
295             } catch (MissingResourceException e) {
296                 // Silently ignore and use Latin
297             }
298 
299             // Set the "isFallback" flag for when we read Latin
300             sink.isFallback = true;
301         }
302 
303         // Now load Latin, which will fill in things that were left out from above.
304         sink.isLatin = true;
305         r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + LATIN_NUMBERING_SYSTEM, sink);
306 
307         // If longData is empty, default it to be equal to shortData
308         if (dataBundle.longData.isEmpty()) {
309             dataBundle.longData = dataBundle.shortData;
310         }
311 
312         // Check for "other" variants in each of the three data classes
313         checkForOtherVariants(dataBundle.longData, ulocale, LONG_STYLE);
314         checkForOtherVariants(dataBundle.shortData, ulocale, SHORT_STYLE);
315         checkForOtherVariants(dataBundle.shortCurrencyData, ulocale, SHORT_CURRENCY_STYLE);
316 
317         // Resolve missing elements
318         fillInMissing(dataBundle.longData);
319         fillInMissing(dataBundle.shortData);
320         fillInMissing(dataBundle.shortCurrencyData);
321 
322         // Return the data bundle
323         return dataBundle;
324     }
325 
326 
327     /**
328      * Populates prefix and suffix information for a particular plural variant
329      * and index (log10 value).
330      * @param pluralVariant e.g "one", "other"
331      * @param idx the index (log10 value of the number) 0 <= idx < MAX_DIGITS
332      * @param template e.g "00K"
333      * @param locale the locale
334      * @param style the style
335      * @param destination Extracted prefix and suffix stored here.
336      * @return number of zeros found before any decimal point in template, or -1 if it was not saved.
337      */
populatePrefixSuffix( String pluralVariant, int idx, String template, ULocale locale, String style, Data destination, boolean overwrite)338     private static int populatePrefixSuffix(
339             String pluralVariant, int idx, String template, ULocale locale, String style,
340             Data destination, boolean overwrite) {
341         int firstIdx = template.indexOf("0");
342         int lastIdx = template.lastIndexOf("0");
343         if (firstIdx == -1) {
344             throw new IllegalArgumentException(
345                 "Expect at least one zero in template '" + template +
346                 "' for variant '" +pluralVariant + "' for 10^" + idx +
347                 " in " + localeAndStyle(locale, style));
348         }
349         String prefix = template.substring(0, firstIdx);
350         String suffix = template.substring(lastIdx + 1);
351 
352         // Save the unit, and return -1 if it was not saved
353         boolean saved = saveUnit(new DecimalFormat.Unit(prefix, suffix), pluralVariant, idx, destination.units, overwrite);
354         if (!saved) {
355             return -1;
356         }
357 
358         // If there is effectively no prefix or suffix, ignore the actual
359         // number of 0's and act as if the number of 0's matches the size
360         // of the number
361         if (prefix.trim().length() == 0 && suffix.trim().length() == 0) {
362           return idx + 1;
363         }
364 
365         // Calculate number of zeros before decimal point.
366         int i = firstIdx + 1;
367         while (i <= lastIdx && template.charAt(i) == '0') {
368             i++;
369         }
370         return i - firstIdx;
371     }
372 
373     /**
374      * Calculate a divisor based on the magnitude and number of zeros in the
375      * template string.
376      * @param power10
377      * @param numZeros
378      * @return
379      */
calculateDivisor(long power10, int numZeros)380     private static long calculateDivisor(long power10, int numZeros) {
381         // We craft our divisor such that when we divide by it, we get a
382         // number with the same number of digits as zeros found in the
383         // plural variant templates. If our magnitude is 10000 and we have
384         // two 0's in our plural variants, then we want a divisor of 1000.
385         // Note that if we have 43560 which is of same magnitude as 10000.
386         // When we divide by 1000 we a quotient which rounds to 44 (2 digits)
387         long divisor = power10;
388         for (int i = 1; i < numZeros; i++) {
389             divisor /= 10;
390         }
391         return divisor;
392     }
393 
394 
395     /**
396      * Returns locale and style. Used to form useful messages in thrown exceptions.
397      *
398      * Note: This is not covered by unit tests since no exceptions are thrown on the default CLDR data.  It is too
399      * cumbersome to cover via reflection.
400      *
401      * @param locale the locale
402      * @param style the style
403      */
localeAndStyle(ULocale locale, String style)404     private static String localeAndStyle(ULocale locale, String style) {
405         return "locale '" + locale + "' style '" + style + "'";
406     }
407 
408     /**
409      * Checks to make sure that an "other" variant is present in all powers of 10.
410      * @param data
411      */
checkForOtherVariants(Data data, ULocale locale, String style)412     private static void checkForOtherVariants(Data data, ULocale locale, String style) {
413         DecimalFormat.Unit[] otherByBase = data.units.get(OTHER);
414 
415         if (otherByBase == null) {
416             throw new IllegalArgumentException("No 'other' plural variants defined in "
417                     + localeAndStyle(locale, style));
418         }
419 
420         // Check all other plural variants, and make sure that if any of them are populated, then
421         // other is also populated
422         for (Map.Entry<String, Unit[]> entry : data.units.entrySet()) {
423             if (entry.getKey() == OTHER) continue;
424             DecimalFormat.Unit[] variantByBase = entry.getValue();
425             for (int log10Value = 0; log10Value < MAX_DIGITS; log10Value++) {
426                 if (variantByBase[log10Value] != null && otherByBase[log10Value] == null) {
427                     throw new IllegalArgumentException(
428                             "No 'other' plural variant defined for 10^" + log10Value
429                             + " but a '" + entry.getKey() + "' variant is defined"
430                             + " in " +localeAndStyle(locale, style));
431                 }
432             }
433         }
434     }
435 
436     /**
437      * After reading information from resource bundle into a Data object, there
438      * is guarantee that it is complete.
439      *
440      * This method fixes any incomplete data it finds within <code>result</code>.
441      * It looks at each log10 value applying the two rules.
442      *   <p>
443      *   If no prefix is defined for the "other" variant, use the divisor, prefixes and
444      *   suffixes for all defined variants from the previous log10. For log10 = 0,
445      *   use all empty prefixes and suffixes and a divisor of 1.
446      *   </p><p>
447      *   Otherwise, examine each plural variant defined for the given log10 value.
448      *   If it has no prefix and suffix for a particular variant, use the one from the
449      *   "other" variant.
450      *   </p>
451      *
452      * @param result this instance is fixed in-place.
453      */
fillInMissing(Data result)454     private static void fillInMissing(Data result) {
455         // Initially we assume that previous divisor is 1 with no prefix or suffix.
456         long lastDivisor = 1L;
457         for (int i = 0; i < result.divisors.length; i++) {
458             if (result.units.get(OTHER)[i] == null) {
459                 result.divisors[i] = lastDivisor;
460                 copyFromPreviousIndex(i, result.units);
461             } else {
462                 lastDivisor = result.divisors[i];
463                 propagateOtherToMissing(i, result.units);
464             }
465         }
466     }
467 
propagateOtherToMissing( int idx, Map<String, DecimalFormat.Unit[]> units)468     private static void propagateOtherToMissing(
469             int idx, Map<String, DecimalFormat.Unit[]> units) {
470         DecimalFormat.Unit otherVariantValue = units.get(OTHER)[idx];
471         for (DecimalFormat.Unit[] byBase : units.values()) {
472             if (byBase[idx] == null) {
473                 byBase[idx] = otherVariantValue;
474             }
475         }
476     }
477 
copyFromPreviousIndex(int idx, Map<String, DecimalFormat.Unit[]> units)478     private static void copyFromPreviousIndex(int idx, Map<String, DecimalFormat.Unit[]> units) {
479         for (DecimalFormat.Unit[] byBase : units.values()) {
480             if (idx == 0) {
481                 byBase[idx] = DecimalFormat.NULL_UNIT;
482             } else {
483                 byBase[idx] = byBase[idx - 1];
484             }
485         }
486     }
487 
saveUnit( DecimalFormat.Unit unit, String pluralVariant, int idx, Map<String, DecimalFormat.Unit[]> units, boolean overwrite)488     private static boolean saveUnit(
489             DecimalFormat.Unit unit, String pluralVariant, int idx,
490             Map<String, DecimalFormat.Unit[]> units,
491             boolean overwrite) {
492         DecimalFormat.Unit[] byBase = units.get(pluralVariant);
493         if (byBase == null) {
494             byBase = new DecimalFormat.Unit[MAX_DIGITS];
495             units.put(pluralVariant, byBase);
496         }
497 
498         // Don't overwrite a pre-existing value unless the "overwrite" flag is true.
499         if (!overwrite && byBase[idx] != null) {
500             return false;
501         }
502 
503         // Save the value and return
504         byBase[idx] = unit;
505         return true;
506     }
507 
508     /**
509      * Fetches a prefix or suffix given a plural variant and log10 value. If it
510      * can't find the given variant, it falls back to "other".
511      * @param prefixOrSuffix the prefix or suffix map
512      * @param variant the plural variant
513      * @param base log10 value. 0 <= base < MAX_DIGITS.
514      * @return the prefix or suffix.
515      */
getUnit( Map<String, DecimalFormat.Unit[]> units, String variant, int base)516     static DecimalFormat.Unit getUnit(
517             Map<String, DecimalFormat.Unit[]> units, String variant, int base) {
518         DecimalFormat.Unit[] byBase = units.get(variant);
519         if (byBase == null) {
520             byBase = units.get(CompactDecimalDataCache.OTHER);
521         }
522         return byBase[base];
523     }
524 }
525