1/* 2 ******************************************************************************* 3 * Copyright (C) 1996-2014, Google, International Business Machines Corporation and 4 * others. All Rights Reserved. * 5 ******************************************************************************* 6 */ 7 8package com.ibm.icu.text; 9 10import java.io.IOException; 11import java.io.NotSerializableException; 12import java.io.ObjectInputStream; 13import java.io.ObjectOutputStream; 14import java.math.BigDecimal; 15import java.math.BigInteger; 16import java.text.AttributedCharacterIterator; 17import java.text.FieldPosition; 18import java.text.ParsePosition; 19import java.util.Arrays; 20import java.util.Collection; 21import java.util.HashMap; 22import java.util.Locale; 23import java.util.Map; 24import java.util.Map.Entry; 25 26import com.ibm.icu.text.CompactDecimalDataCache.Data; 27import com.ibm.icu.text.PluralRules.FixedDecimal; 28import com.ibm.icu.util.Output; 29import com.ibm.icu.util.ULocale; 30 31/** 32 * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will limited real estate. 33 * For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will be appropriate for the given language, 34 * such as "1,2 Mrd." for German. 35 * <p> 36 * For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be short for supported 37 * languages. However, the result may sometimes exceed 7 characters, such as when there are combining marks or thin 38 * characters. In such cases, the visual width in fonts should still be short. 39 * <p> 40 * By default, there are 2 significant digits. After creation, if more than three significant digits are set (with 41 * setMaximumSignificantDigits), or if a fixed number of digits are set (with setMaximumIntegerDigits or 42 * setMaximumFractionDigits), then result may be wider. 43 * <p> 44 * At this time, negative numbers and parsing are not supported, and will produce an UnsupportedOperationException. 45 * Resetting the pattern prefixes or suffixes is not supported; the method calls are ignored. 46 * <p> 47 * Note that important methods, like setting the number of decimals, will be moved up from DecimalFormat to 48 * NumberFormat. 49 * 50 * @author markdavis 51 * @stable ICU 49 52 */ 53public class CompactDecimalFormat extends DecimalFormat { 54 55 private static final long serialVersionUID = 4716293295276629682L; 56 57// private static final int POSITIVE_PREFIX = 0, POSITIVE_SUFFIX = 1, AFFIX_SIZE = 2; 58 private static final CompactDecimalDataCache cache = new CompactDecimalDataCache(); 59 60 private final Map<String, DecimalFormat.Unit[]> units; 61 private final long[] divisor; 62 private final Map<String, Unit> pluralToCurrencyAffixes; 63 64 // null if created internally using explicit prefixes and suffixes. 65 private final PluralRules pluralRules; 66 67 /** 68 * Style parameter for CompactDecimalFormat. 69 * @stable ICU 50 70 */ 71 public enum CompactStyle { 72 /** 73 * Short version, like "1.2T" 74 * @stable ICU 50 75 */ 76 SHORT, 77 /** 78 * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not. 79 * @stable ICU 50 80 */ 81 LONG 82 } 83 84 /** 85 * Create a CompactDecimalFormat appropriate for a locale. The result may 86 * be affected by the number system in the locale, such as ar-u-nu-latn. 87 * 88 * @param locale the desired locale 89 * @param style the compact style 90 * @stable ICU 50 91 */ 92 public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) { 93 return new CompactDecimalFormat(locale, style); 94 } 95 96 /** 97 * Create a CompactDecimalFormat appropriate for a locale. The result may 98 * be affected by the number system in the locale, such as ar-u-nu-latn. 99 * 100 * @param locale the desired locale 101 * @param style the compact style 102 * @stable ICU 50 103 */ 104 public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) { 105 return new CompactDecimalFormat(ULocale.forLocale(locale), style); 106 } 107 108 /** 109 * The public mechanism is CompactDecimalFormat.getInstance(). 110 * 111 * @param locale 112 * the desired locale 113 * @param style 114 * the compact style 115 */ 116 CompactDecimalFormat(ULocale locale, CompactStyle style) { 117 this.pluralRules = PluralRules.forLocale(locale); 118 DecimalFormat format = (DecimalFormat) NumberFormat.getInstance(locale); 119 CompactDecimalDataCache.Data data = getData(locale, style); 120 this.units = data.units; 121 this.divisor = data.divisors; 122 pluralToCurrencyAffixes = null; 123 124// DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale); 125// // TODO fix to use plural-dependent affixes 126// Unit currency = new Unit(currencyFormat.getPositivePrefix(), currencyFormat.getPositiveSuffix()); 127// pluralToCurrencyAffixes = new HashMap<String,Unit>(); 128// for (String key : pluralRules.getKeywords()) { 129// pluralToCurrencyAffixes.put(key, currency); 130// } 131// // TODO fix to get right symbol for the count 132 133 finishInit(style, format.toPattern(), format.getDecimalFormatSymbols()); 134 } 135 136 /** 137 * Create a short number "from scratch". Intended for internal use. The prefix, suffix, and divisor arrays are 138 * parallel, and provide the information for each power of 10. When formatting a value, the correct power of 10 is 139 * found, then the value is divided by the divisor, and the prefix and suffix are set (using 140 * setPositivePrefix/Suffix). 141 * 142 * @param pattern 143 * A number format pattern. Note that the prefix and suffix are discarded, and the decimals are 144 * overridden by default. 145 * @param formatSymbols 146 * Decimal format symbols, typically from a locale. 147 * @param style 148 * compact style. 149 * @param divisor 150 * An array of prefix values, one for each power of 10 from 0 to 14 151 * @param pluralAffixes 152 * A map from plural categories to affixes. 153 * @param currencyAffixes 154 * A map from plural categories to currency affixes. 155 * @param debugCreationErrors 156 * A collection of strings for debugging. If null on input, then any errors found will be added to that 157 * collection instead of throwing exceptions. 158 * @internal 159 * @deprecated This API is ICU internal only. 160 */ 161 @Deprecated 162 public CompactDecimalFormat(String pattern, DecimalFormatSymbols formatSymbols, 163 CompactStyle style, PluralRules pluralRules, 164 long[] divisor, Map<String,String[][]> pluralAffixes, Map<String, String[]> currencyAffixes, 165 Collection<String> debugCreationErrors) { 166 167 this.pluralRules = pluralRules; 168 this.units = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors); 169 if (!pluralRules.getKeywords().equals(this.units.keySet())) { 170 debugCreationErrors.add("Missmatch in pluralCategories, should be: " + pluralRules.getKeywords() + ", was actually " + this.units.keySet()); 171 } 172 this.divisor = divisor.clone(); 173 if (currencyAffixes == null) { 174 pluralToCurrencyAffixes = null; 175 } else { 176 pluralToCurrencyAffixes = new HashMap<String,Unit>(); 177 for (Entry<String, String[]> s : currencyAffixes.entrySet()) { 178 String[] pair = s.getValue(); 179 pluralToCurrencyAffixes.put(s.getKey(), new Unit(pair[0], pair[1])); 180 } 181 } 182 finishInit(style, pattern, formatSymbols); 183 } 184 185 private void finishInit(CompactStyle style, String pattern, DecimalFormatSymbols formatSymbols) { 186 applyPattern(pattern); 187 setDecimalFormatSymbols(formatSymbols); 188 setMaximumSignificantDigits(2); // default significant digits 189 setSignificantDigitsUsed(true); 190 if (style == CompactStyle.SHORT) { 191 setGroupingUsed(false); 192 } 193 setCurrency(null); 194 } 195 196 /** 197 * {@inheritDoc} 198 * @stable ICU 49 199 */ 200 @Override 201 public boolean equals(Object obj) { 202 if (obj == null) 203 return false; 204 if (!super.equals(obj)) 205 return false; // super does class check 206 CompactDecimalFormat other = (CompactDecimalFormat) obj; 207 return mapsAreEqual(units, other.units) 208 && Arrays.equals(divisor, other.divisor) 209 && (pluralToCurrencyAffixes == other.pluralToCurrencyAffixes 210 || pluralToCurrencyAffixes != null && pluralToCurrencyAffixes.equals(other.pluralToCurrencyAffixes)) 211 && pluralRules.equals(other.pluralRules); 212 } 213 214 private boolean mapsAreEqual( 215 Map<String, DecimalFormat.Unit[]> lhs, Map<String, DecimalFormat.Unit[]> rhs) { 216 if (lhs.size() != rhs.size()) { 217 return false; 218 } 219 // For each MapEntry in lhs, see if there is a matching one in rhs. 220 for (Map.Entry<String, DecimalFormat.Unit[]> entry : lhs.entrySet()) { 221 DecimalFormat.Unit[] value = rhs.get(entry.getKey()); 222 if (value == null || !Arrays.equals(entry.getValue(), value)) { 223 return false; 224 } 225 } 226 return true; 227 } 228 229 /** 230 * {@inheritDoc} 231 * @stable ICU 49 232 */ 233 @Override 234 public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { 235 Output<Unit> currencyUnit = new Output<Unit>(); 236 Amount amount = toAmount(number, currencyUnit); 237 if (currencyUnit.value != null) { 238 currencyUnit.value.writePrefix(toAppendTo); 239 } 240 Unit unit = amount.getUnit(); 241 unit.writePrefix(toAppendTo); 242 super.format(amount.getQty(), toAppendTo, pos); 243 unit.writeSuffix(toAppendTo); 244 if (currencyUnit.value != null) { 245 currencyUnit.value.writeSuffix(toAppendTo); 246 } 247 return toAppendTo; 248 } 249 250 /** 251 * {@inheritDoc} 252 * @stable ICU 50 253 */ 254 @Override 255 public AttributedCharacterIterator formatToCharacterIterator(Object obj) { 256 if (!(obj instanceof Number)) { 257 throw new IllegalArgumentException(); 258 } 259 Number number = (Number) obj; 260 Amount amount = toAmount(number.doubleValue(), null); 261 return super.formatToCharacterIterator(amount.getQty(), amount.getUnit()); 262 } 263 264 /** 265 * {@inheritDoc} 266 * @stable ICU 49 267 */ 268 @Override 269 public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { 270 return format((double) number, toAppendTo, pos); 271 } 272 273 /** 274 * {@inheritDoc} 275 * @stable ICU 49 276 */ 277 @Override 278 public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) { 279 return format(number.doubleValue(), toAppendTo, pos); 280 } 281 282 /** 283 * {@inheritDoc} 284 * @stable ICU 49 285 */ 286 @Override 287 public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { 288 return format(number.doubleValue(), toAppendTo, pos); 289 } 290 291 /** 292 * {@inheritDoc} 293 * @stable ICU 49 294 */ 295 @Override 296 public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { 297 return format(number.doubleValue(), toAppendTo, pos); 298 } 299 300 /** 301 * Parsing is currently unsupported, and throws an UnsupportedOperationException. 302 * @stable ICU 49 303 */ 304 @Override 305 public Number parse(String text, ParsePosition parsePosition) { 306 throw new UnsupportedOperationException(); 307 } 308 309 // DISALLOW Serialization, at least while draft 310 311 private void writeObject(ObjectOutputStream out) throws IOException { 312 throw new NotSerializableException(); 313 } 314 315 private void readObject(ObjectInputStream in) throws IOException { 316 throw new NotSerializableException(); 317 } 318 319 /* INTERNALS */ 320 321 322 private Amount toAmount(double number, Output<Unit> currencyUnit) { 323 // We do this here so that the prefix or suffix we choose is always consistent 324 // with the rounding we do. This way, 999999 -> 1M instead of 1000K. 325 boolean negative = isNumberNegative(number); 326 number = adjustNumberAsInFormatting(number); 327 int base = number <= 1.0d ? 0 : (int) Math.log10(number); 328 if (base >= CompactDecimalDataCache.MAX_DIGITS) { 329 base = CompactDecimalDataCache.MAX_DIGITS - 1; 330 } 331 number /= divisor[base]; 332 String pluralVariant = getPluralForm(getFixedDecimal(number, toDigitList(number))); 333 if (pluralToCurrencyAffixes != null && currencyUnit != null) { 334 currencyUnit.value = pluralToCurrencyAffixes.get(pluralVariant); 335 } 336 if (negative) { 337 number = -number; 338 } 339 return new Amount( 340 number, 341 CompactDecimalDataCache.getUnit(units, pluralVariant, base)); 342 343 } 344 345 private void recordError(Collection<String> creationErrors, String errorMessage) { 346 if (creationErrors == null) { 347 throw new IllegalArgumentException(errorMessage); 348 } 349 creationErrors.add(errorMessage); 350 } 351 352 /** 353 * Manufacture the unit list from arrays 354 */ 355 private Map<String, DecimalFormat.Unit[]> otherPluralVariant(Map<String, String[][]> pluralCategoryToPower10ToAffix, 356 long[] divisor, Collection<String> debugCreationErrors) { 357 358 // check for bad divisors 359 if (divisor.length < CompactDecimalDataCache.MAX_DIGITS) { 360 recordError(debugCreationErrors, "Must have at least " + CompactDecimalDataCache.MAX_DIGITS + " prefix items."); 361 } 362 long oldDivisor = 0; 363 for (int i = 0; i < divisor.length; ++i) { 364 365 // divisor must be a power of 10, and must be less than or equal to 10^i 366 int log = (int) Math.log10(divisor[i]); 367 if (log > i) { 368 recordError(debugCreationErrors, "Divisor[" + i + "] must be less than or equal to 10^" + i 369 + ", but is: " + divisor[i]); 370 } 371 long roundTrip = (long) Math.pow(10.0d, log); 372 if (roundTrip != divisor[i]) { 373 recordError(debugCreationErrors, "Divisor[" + i + "] must be a power of 10, but is: " + divisor[i]); 374 } 375 376 if (divisor[i] < oldDivisor) { 377 recordError(debugCreationErrors, "Bad divisor, the divisor for 10E" + i + "(" + divisor[i] 378 + ") is less than the divisor for the divisor for 10E" + (i - 1) + "(" + oldDivisor + ")"); 379 } 380 oldDivisor = divisor[i]; 381 } 382 383 Map<String, DecimalFormat.Unit[]> result = new HashMap<String, DecimalFormat.Unit[]>(); 384 Map<String,Integer> seen = new HashMap<String,Integer>(); 385 386 String[][] defaultPower10ToAffix = pluralCategoryToPower10ToAffix.get("other"); 387 388 for (Entry<String, String[][]> pluralCategoryAndPower10ToAffix : pluralCategoryToPower10ToAffix.entrySet()) { 389 String pluralCategory = pluralCategoryAndPower10ToAffix.getKey(); 390 String[][] power10ToAffix = pluralCategoryAndPower10ToAffix.getValue(); 391 392 // we can't have one of the arrays be of different length 393 if (power10ToAffix.length != divisor.length) { 394 recordError(debugCreationErrors, "Prefixes & suffixes must be present for all divisors " + pluralCategory); 395 } 396 DecimalFormat.Unit[] units = new DecimalFormat.Unit[power10ToAffix.length]; 397 for (int i = 0; i < power10ToAffix.length; i++) { 398 String[] pair = power10ToAffix[i]; 399 if (pair == null) { 400 pair = defaultPower10ToAffix[i]; 401 } 402 403 // we can't have bad pair 404 if (pair.length != 2 || pair[0] == null || pair[1] == null) { 405 recordError(debugCreationErrors, "Prefix or suffix is null for " + pluralCategory + ", " + i + ", " + Arrays.asList(pair)); 406 continue; 407 } 408 409 // we can't have two different indexes with the same display 410 int log = (int) Math.log10(divisor[i]); 411 String key = pair[0] + "\uFFFF" + pair[1] + "\uFFFF" + (i - log); 412 Integer old = seen.get(key); 413 if (old == null) { 414 seen.put(key, i); 415 } else if (old != i) { 416 recordError(debugCreationErrors, "Collision between values for " + i + " and " + old 417 + " for [prefix/suffix/index-log(divisor)" + key.replace('\uFFFF', ';')); 418 } 419 420 units[i] = new Unit(pair[0], pair[1]); 421 } 422 result.put(pluralCategory, units); 423 } 424 return result; 425 } 426 427 private String getPluralForm(FixedDecimal fixedDecimal) { 428 if (pluralRules == null) { 429 return CompactDecimalDataCache.OTHER; 430 } 431 return pluralRules.select(fixedDecimal); 432 } 433 434 /** 435 * Gets the data for a particular locale and style. If style is unrecognized, 436 * we just return data for CompactStyle.SHORT. 437 * @param locale The locale. 438 * @param style The style. 439 * @return The data which must not be modified. 440 */ 441 private Data getData(ULocale locale, CompactStyle style) { 442 CompactDecimalDataCache.DataBundle bundle = cache.get(locale); 443 switch (style) { 444 case SHORT: 445 return bundle.shortData; 446 case LONG: 447 return bundle.longData; 448 default: 449 return bundle.shortData; 450 } 451 } 452 453 private static class Amount { 454 private final double qty; 455 private final Unit unit; 456 457 public Amount(double qty, Unit unit) { 458 this.qty = qty; 459 this.unit = unit; 460 } 461 462 public double getQty() { 463 return qty; 464 } 465 466 public Unit getUnit() { 467 return unit; 468 } 469 } 470} 471