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