1/* 2 ********************************************************************** 3 * Copyright (c) 2004-2015, International Business Machines 4 * Corporation and others. All Rights Reserved. 5 ********************************************************************** 6 * Author: Alan Liu 7 * Created: April 20, 2004 8 * Since: ICU 3.0 9 ********************************************************************** 10 */ 11package com.ibm.icu.text; 12 13import java.io.Externalizable; 14import java.io.IOException; 15import java.io.InvalidObjectException; 16import java.io.ObjectInput; 17import java.io.ObjectOutput; 18import java.io.ObjectStreamException; 19import java.text.FieldPosition; 20import java.text.ParsePosition; 21import java.util.Arrays; 22import java.util.Collection; 23import java.util.Date; 24import java.util.EnumMap; 25import java.util.HashMap; 26import java.util.Locale; 27import java.util.Map; 28import java.util.MissingResourceException; 29import java.util.concurrent.ConcurrentHashMap; 30 31import com.ibm.icu.impl.DontCareFieldPosition; 32import com.ibm.icu.impl.ICUData; 33import com.ibm.icu.impl.ICUResourceBundle; 34import com.ibm.icu.impl.SimpleCache; 35import com.ibm.icu.impl.SimplePatternFormatter; 36import com.ibm.icu.math.BigDecimal; 37import com.ibm.icu.text.PluralRules.Factory; 38import com.ibm.icu.text.PluralRules.StandardPluralCategories; 39import com.ibm.icu.util.Currency; 40import com.ibm.icu.util.CurrencyAmount; 41import com.ibm.icu.util.Measure; 42import com.ibm.icu.util.MeasureUnit; 43import com.ibm.icu.util.TimeZone; 44import com.ibm.icu.util.ULocale; 45import com.ibm.icu.util.ULocale.Category; 46import com.ibm.icu.util.UResourceBundle; 47 48// If you update the examples in the doc, don't forget to update MesaureUnitTest.TestExamplesInDocs too. 49/** 50 * A formatter for Measure objects. 51 * 52 * <p>To format a Measure object, first create a formatter 53 * object using a MeasureFormat factory method. Then use that 54 * object's format or formatMeasures methods. 55 * 56 * Here is sample code: 57 * <pre> 58 * MeasureFormat fmtFr = MeasureFormat.getInstance( 59 * ULocale.FRENCH, FormatWidth.SHORT); 60 * Measure measure = new Measure(23, MeasureUnit.CELSIUS); 61 * 62 * // Output: 23 °C 63 * System.out.println(fmtFr.format(measure)); 64 * 65 * Measure measureF = new Measure(70, MeasureUnit.FAHRENHEIT); 66 * 67 * // Output: 70 °F 68 * System.out.println(fmtFr.format(measureF)); 69 * 70 * MeasureFormat fmtFrFull = MeasureFormat.getInstance( 71 * ULocale.FRENCH, FormatWidth.WIDE); 72 * // Output: 70 pieds et 5,3 pouces 73 * System.out.println(fmtFrFull.formatMeasures( 74 * new Measure(70, MeasureUnit.FOOT), 75 * new Measure(5.3, MeasureUnit.INCH))); 76 * 77 * // Output: 1 pied et 1 pouce 78 * System.out.println(fmtFrFull.formatMeasures( 79 * new Measure(1, MeasureUnit.FOOT), 80 * new Measure(1, MeasureUnit.INCH))); 81 * 82 * MeasureFormat fmtFrNarrow = MeasureFormat.getInstance( 83 ULocale.FRENCH, FormatWidth.NARROW); 84 * // Output: 1′ 1″ 85 * System.out.println(fmtFrNarrow.formatMeasures( 86 * new Measure(1, MeasureUnit.FOOT), 87 * new Measure(1, MeasureUnit.INCH))); 88 * 89 * 90 * MeasureFormat fmtEn = MeasureFormat.getInstance(ULocale.ENGLISH, FormatWidth.WIDE); 91 * 92 * // Output: 1 inch, 2 feet 93 * fmtEn.formatMeasures( 94 * new Measure(1, MeasureUnit.INCH), 95 * new Measure(2, MeasureUnit.FOOT)); 96 * </pre> 97 * <p> 98 * This class does not do conversions from one unit to another. It simply formats 99 * whatever units it is given 100 * <p> 101 * This class is immutable and thread-safe so long as its deprecated subclass, 102 * TimeUnitFormat, is never used. TimeUnitFormat is not thread-safe, and is 103 * mutable. Although this class has existing subclasses, this class does not support new 104 * sub-classes. 105 * 106 * @see com.ibm.icu.text.UFormat 107 * @author Alan Liu 108 * @stable ICU 3.0 109 */ 110public class MeasureFormat extends UFormat { 111 112 113 // Generated by serialver from JDK 1.4.1_01 114 static final long serialVersionUID = -7182021401701778240L; 115 116 private final transient ImmutableNumberFormat numberFormat; 117 118 private final transient FormatWidth formatWidth; 119 120 // PluralRules is documented as being immutable which implies thread-safety. 121 private final transient PluralRules rules; 122 123 // Measure unit -> format width -> plural form -> pattern ("{0} meters") 124 private final transient Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> unitToStyleToCountToFormat; 125 126 private final transient NumericFormatters numericFormatters; 127 128 private final transient ImmutableNumberFormat currencyFormat; 129 130 private final transient ImmutableNumberFormat integerFormat; 131 132 private final transient Map<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>> unitToStyleToPerUnitPattern; 133 134 private final transient EnumMap<FormatWidth, SimplePatternFormatter> styleToPerPattern; 135 136 private static final SimpleCache<ULocale, MeasureFormatData> localeMeasureFormatData 137 = new SimpleCache<ULocale, MeasureFormatData>(); 138 139 private static final SimpleCache<ULocale, NumericFormatters> localeToNumericDurationFormatters 140 = new SimpleCache<ULocale,NumericFormatters>(); 141 142 private static final Map<MeasureUnit, Integer> hmsTo012 = 143 new HashMap<MeasureUnit, Integer>(); 144 145 static { 146 hmsTo012.put(MeasureUnit.HOUR, 0); 147 hmsTo012.put(MeasureUnit.MINUTE, 1); 148 hmsTo012.put(MeasureUnit.SECOND, 2); 149 } 150 151 // For serialization: sub-class types. 152 private static final int MEASURE_FORMAT = 0; 153 private static final int TIME_UNIT_FORMAT = 1; 154 private static final int CURRENCY_FORMAT = 2; 155 156 /** 157 * Formatting width enum. 158 * 159 * @stable ICU 53 160 */ 161 // Be sure to update MeasureUnitTest.TestSerialFormatWidthEnum 162 // when adding an enum value. 163 public enum FormatWidth { 164 165 /** 166 * Spell out everything. 167 * 168 * @stable ICU 53 169 */ 170 WIDE("units", ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE), 171 172 /** 173 * Abbreviate when possible. 174 * 175 * @stable ICU 53 176 */ 177 SHORT("unitsShort", ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE), 178 179 /** 180 * Brief. Use only a symbol for the unit when possible. 181 * 182 * @stable ICU 53 183 */ 184 NARROW("unitsNarrow", ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE), 185 186 /** 187 * Identical to NARROW except when formatMeasures is called with 188 * an hour and minute; minute and second; or hour, minute, and second Measures. 189 * In these cases formatMeasures formats as 5:37:23 instead of 5h, 37m, 23s. 190 * 191 * @stable ICU 53 192 */ 193 NUMERIC("unitsNarrow", ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE); 194 195 // Be sure to update the toFormatWidth and fromFormatWidth() functions 196 // when adding an enum value. 197 198 final String resourceKey; 199 private final ListFormatter.Style listFormatterStyle; 200 private final int currencyStyle; 201 202 private FormatWidth(String resourceKey, ListFormatter.Style style, int currencyStyle) { 203 this.resourceKey = resourceKey; 204 this.listFormatterStyle = style; 205 this.currencyStyle = currencyStyle; 206 } 207 208 ListFormatter.Style getListFormatterStyle() { 209 return listFormatterStyle; 210 } 211 212 int getCurrencyStyle() { 213 return currencyStyle; 214 } 215 } 216 217 /** 218 * Create a format from the locale, formatWidth, and format. 219 * 220 * @param locale the locale. 221 * @param formatWidth hints how long formatted strings should be. 222 * @return The new MeasureFormat object. 223 * @stable ICU 53 224 */ 225 public static MeasureFormat getInstance(ULocale locale, FormatWidth formatWidth) { 226 return getInstance(locale, formatWidth, NumberFormat.getInstance(locale)); 227 } 228 229 /** 230 * Create a format from the JDK locale, formatWidth, and format. 231 * 232 * @param locale the JDK locale. 233 * @param formatWidth hints how long formatted strings should be. 234 * @return The new MeasureFormat object. 235 * @draft ICU 54 236 * @provisional This API might change or be removed in a future release. 237 */ 238 public static MeasureFormat getInstance(Locale locale, FormatWidth formatWidth) { 239 return getInstance(ULocale.forLocale(locale), formatWidth); 240 } 241 242 /** 243 * Create a format from the locale, formatWidth, and format. 244 * 245 * @param locale the locale. 246 * @param formatWidth hints how long formatted strings should be. 247 * @param format This is defensively copied. 248 * @return The new MeasureFormat object. 249 * @stable ICU 53 250 */ 251 public static MeasureFormat getInstance(ULocale locale, FormatWidth formatWidth, NumberFormat format) { 252 PluralRules rules = PluralRules.forLocale(locale); 253 NumericFormatters formatters = null; 254 MeasureFormatData data = localeMeasureFormatData.get(locale); 255 if (data == null) { 256 data = loadLocaleData(locale); 257 localeMeasureFormatData.put(locale, data); 258 } 259 if (formatWidth == FormatWidth.NUMERIC) { 260 formatters = localeToNumericDurationFormatters.get(locale); 261 if (formatters == null) { 262 formatters = loadNumericFormatters(locale); 263 localeToNumericDurationFormatters.put(locale, formatters); 264 } 265 } 266 NumberFormat intFormat = NumberFormat.getInstance(locale); 267 intFormat.setMaximumFractionDigits(0); 268 intFormat.setMinimumFractionDigits(0); 269 intFormat.setRoundingMode(BigDecimal.ROUND_DOWN); 270 return new MeasureFormat( 271 locale, 272 formatWidth, 273 new ImmutableNumberFormat(format), 274 rules, 275 data.unitToStyleToCountToFormat, 276 formatters, 277 new ImmutableNumberFormat(NumberFormat.getInstance(locale, formatWidth.getCurrencyStyle())), 278 new ImmutableNumberFormat(intFormat), 279 data.unitToStyleToPerUnitPattern, 280 data.styleToPerPattern); 281 } 282 283 /** 284 * Create a format from the JDK locale, formatWidth, and format. 285 * 286 * @param locale the JDK locale. 287 * @param formatWidth hints how long formatted strings should be. 288 * @param format This is defensively copied. 289 * @return The new MeasureFormat object. 290 * @draft ICU 54 291 * @provisional This API might change or be removed in a future release. 292 */ 293 public static MeasureFormat getInstance(Locale locale, FormatWidth formatWidth, NumberFormat format) { 294 return getInstance(ULocale.forLocale(locale), formatWidth, format); 295 } 296 297 /** 298 * Able to format Collection<? extends Measure>, Measure[], and Measure 299 * by delegating to formatMeasures. 300 * If the pos argument identifies a NumberFormat field, 301 * then its indices are set to the beginning and end of the first such field 302 * encountered. MeasureFormat itself does not supply any fields. 303 * 304 * Calling a 305 * <code>formatMeasures</code> method is preferred over calling 306 * this method as they give better performance. 307 * 308 * @param obj must be a Collection<? extends Measure>, Measure[], or Measure object. 309 * @param toAppendTo Formatted string appended here. 310 * @param pos Identifies a field in the formatted text. 311 * @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer, java.text.FieldPosition) 312 * 313 * @stable ICU53 314 */ 315 @Override 316 public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { 317 int prevLength = toAppendTo.length(); 318 FieldPosition fpos = 319 new FieldPosition(pos.getFieldAttribute(), pos.getField()); 320 if (obj instanceof Collection) { 321 Collection<?> coll = (Collection<?>) obj; 322 Measure[] measures = new Measure[coll.size()]; 323 int idx = 0; 324 for (Object o : coll) { 325 if (!(o instanceof Measure)) { 326 throw new IllegalArgumentException(obj.toString()); 327 } 328 measures[idx++] = (Measure) o; 329 } 330 toAppendTo.append(formatMeasures(new StringBuilder(), fpos, measures)); 331 } else if (obj instanceof Measure[]) { 332 toAppendTo.append(formatMeasures(new StringBuilder(), fpos, (Measure[]) obj)); 333 } else if (obj instanceof Measure){ 334 toAppendTo.append(formatMeasure((Measure) obj, numberFormat, new StringBuilder(), fpos)); 335 } else { 336 throw new IllegalArgumentException(obj.toString()); 337 } 338 if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) { 339 pos.setBeginIndex(fpos.getBeginIndex() + prevLength); 340 pos.setEndIndex(fpos.getEndIndex() + prevLength); 341 } 342 return toAppendTo; 343 } 344 345 /** 346 * Parses text from a string to produce a <code>Measure</code>. 347 * @see java.text.Format#parseObject(java.lang.String, java.text.ParsePosition) 348 * @throws UnsupportedOperationException Not supported. 349 * @draft ICU 53 (Retain) 350 * @provisional This API might change or be removed in a future release. 351 */ 352 @Override 353 public Measure parseObject(String source, ParsePosition pos) { 354 throw new UnsupportedOperationException(); 355 } 356 357 /** 358 * Format a sequence of measures. Uses the ListFormatter unit lists. 359 * So, for example, one could format “3 feet, 2 inches”. 360 * Zero values are formatted (eg, “3 feet, 0 inches”). It is the caller’s 361 * responsibility to have the appropriate values in appropriate order, 362 * and using the appropriate Number values. Typically the units should be 363 * in descending order, with all but the last Measure having integer values 364 * (eg, not “3.2 feet, 2 inches”). 365 * 366 * @param measures a sequence of one or more measures. 367 * @return the formatted string. 368 * @stable ICU 53 369 */ 370 public final String formatMeasures(Measure... measures) { 371 return formatMeasures( 372 new StringBuilder(), 373 DontCareFieldPosition.INSTANCE, 374 measures).toString(); 375 } 376 377 /** 378 * Format a range of measures, such as "3.4-5.1 meters". It is the caller’s 379 * responsibility to have the appropriate values in appropriate order, 380 * and using the appropriate Number values. 381 * <br>Note: If the format doesn’t have enough decimals, or lowValue ≥ highValue, 382 * the result will be a degenerate range, like “5-5 meters”. 383 * <br>Currency Units are not yet supported. 384 * 385 * @param lowValue low value in range 386 * @param highValue high value in range 387 * @return the formatted string. 388 * @internal 389 * @deprecated This API is ICU internal only. 390 */ 391 @Deprecated 392 public final String formatMeasureRange(Measure lowValue, Measure highValue) { 393 MeasureUnit unit = lowValue.getUnit(); 394 if (!unit.equals(highValue.getUnit())) { 395 throw new IllegalArgumentException("Units must match: " + unit + " ≠ " + highValue.getUnit()); 396 } 397 Number lowNumber = lowValue.getNumber(); 398 Number highNumber = highValue.getNumber(); 399 final boolean isCurrency = unit instanceof Currency; 400 401 UFieldPosition lowFpos = new UFieldPosition(); 402 UFieldPosition highFpos = new UFieldPosition(); 403 StringBuffer lowFormatted = null; 404 StringBuffer highFormatted = null; 405 406 if (isCurrency) { 407 Currency currency = (Currency) unit; 408 int fracDigits = currency.getDefaultFractionDigits(); 409 int maxFrac = numberFormat.nf.getMaximumFractionDigits(); 410 int minFrac = numberFormat.nf.getMinimumFractionDigits(); 411 if (fracDigits != maxFrac || fracDigits != minFrac) { 412 DecimalFormat currentNumberFormat = (DecimalFormat) numberFormat.get(); 413 currentNumberFormat.setMaximumFractionDigits(fracDigits); 414 currentNumberFormat.setMinimumFractionDigits(fracDigits); 415 lowFormatted = currentNumberFormat.format(lowNumber, new StringBuffer(), lowFpos); 416 highFormatted = currentNumberFormat.format(highNumber, new StringBuffer(), highFpos); 417 } 418 } 419 if (lowFormatted == null) { 420 lowFormatted = numberFormat.format(lowNumber, new StringBuffer(), lowFpos); 421 highFormatted = numberFormat.format(highNumber, new StringBuffer(), highFpos); 422 } 423 424 final double lowDouble = lowNumber.doubleValue(); 425 String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble, 426 lowFpos.getCountVisibleFractionDigits(), lowFpos.getFractionDigits())); 427 428 final double highDouble = highNumber.doubleValue(); 429 String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble, 430 highFpos.getCountVisibleFractionDigits(), highFpos.getFractionDigits())); 431 432 final PluralRanges pluralRanges = Factory.getDefaultFactory().getPluralRanges(getLocale()); 433 StandardPluralCategories resolvedCategory = pluralRanges.get( 434 StandardPluralCategories.valueOf(keywordLow), StandardPluralCategories.valueOf(keywordHigh)); 435 436 SimplePatternFormatter rangeFormatter = getRangeFormat(getLocale(), formatWidth); 437 String formattedNumber = rangeFormatter.format(lowFormatted, highFormatted); 438 439 if (isCurrency) { 440 // Nasty hack 441 currencyFormat.format(1d); // have to call this for the side effect 442 443 Currency currencyUnit = (Currency) unit; 444 StringBuilder result = new StringBuilder(); 445 appendReplacingCurrency(currencyFormat.getPrefix(lowDouble >= 0), currencyUnit, resolvedCategory, result); 446 result.append(formattedNumber); 447 appendReplacingCurrency(currencyFormat.getSuffix(highDouble >= 0), currencyUnit, resolvedCategory, result); 448 return result.toString(); 449 // StringBuffer buffer = new StringBuffer(); 450 // CurrencyAmount currencyLow = (CurrencyAmount) lowValue; 451 // CurrencyAmount currencyHigh = (CurrencyAmount) highValue; 452 // FieldPosition pos = new FieldPosition(NumberFormat.INTEGER_FIELD); 453 // currencyFormat.format(currencyLow, buffer, pos); 454 // int startOfInteger = pos.getBeginIndex(); 455 // StringBuffer buffer2 = new StringBuffer(); 456 // FieldPosition pos2 = new FieldPosition(0); 457 // currencyFormat.format(currencyHigh, buffer2, pos2); 458 } else { 459 Map<FormatWidth, QuantityFormatter> styleToCountToFormat = unitToStyleToCountToFormat.get(lowValue.getUnit()); 460 QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth); 461 SimplePatternFormatter formatter = countToFormat.getByVariant(resolvedCategory.toString()); 462 return formatter.format(formattedNumber); 463 } 464 } 465 466 private void appendReplacingCurrency(String affix, Currency unit, StandardPluralCategories resolvedCategory, StringBuilder result) { 467 String replacement = "¤"; 468 int pos = affix.indexOf(replacement); 469 if (pos < 0) { 470 replacement = "XXX"; 471 pos = affix.indexOf(replacement); 472 } 473 if (pos < 0) { 474 result.append(affix); 475 } else { 476 // for now, just assume single 477 result.append(affix.substring(0,pos)); 478 // we have a mismatch between the number style and the currency style, so remap 479 int currentStyle = formatWidth.getCurrencyStyle(); 480 if (currentStyle == NumberFormat.ISOCURRENCYSTYLE) { 481 result.append(unit.getCurrencyCode()); 482 } else { 483 result.append(unit.getName(currencyFormat.nf.getLocale(ULocale.ACTUAL_LOCALE), 484 currentStyle == NumberFormat.CURRENCYSTYLE ? Currency.SYMBOL_NAME : Currency.PLURAL_LONG_NAME, 485 resolvedCategory.toString(), null)); 486 } 487 result.append(affix.substring(pos+replacement.length())); 488 } 489 } 490 491 /** 492 * Formats a single measure per unit. 493 * 494 * An example of such a formatted string is "3.5 meters per second." 495 * 496 * @param measure the measure object. In above example, 3.5 meters. 497 * @param perUnit the per unit. In above example, it is MeasureUnit.SECOND 498 * @param appendTo formatted string appended here. 499 * @param pos The field position. 500 * @return appendTo. 501 * @draft ICU 55 502 * @provisional This API might change or be removed in a future release. 503 */ 504 public StringBuilder formatMeasurePerUnit( 505 Measure measure, 506 MeasureUnit perUnit, 507 StringBuilder appendTo, 508 FieldPosition pos) { 509 MeasureUnit resolvedUnit = MeasureUnit.resolveUnitPerUnit( 510 measure.getUnit(), perUnit); 511 if (resolvedUnit != null) { 512 Measure newMeasure = new Measure(measure.getNumber(), resolvedUnit); 513 return formatMeasure(newMeasure, numberFormat, appendTo, pos); 514 } 515 FieldPosition fpos = new FieldPosition( 516 pos.getFieldAttribute(), pos.getField()); 517 int offset = withPerUnitAndAppend( 518 formatMeasure(measure, numberFormat, new StringBuilder(), fpos), 519 perUnit, 520 appendTo); 521 if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) { 522 pos.setBeginIndex(fpos.getBeginIndex() + offset); 523 pos.setEndIndex(fpos.getEndIndex() + offset); 524 } 525 return appendTo; 526 } 527 528 /** 529 * Formats a sequence of measures. 530 * 531 * If the fieldPosition argument identifies a NumberFormat field, 532 * then its indices are set to the beginning and end of the first such field 533 * encountered. MeasureFormat itself does not supply any fields. 534 * 535 * @param appendTo the formatted string appended here. 536 * @param fieldPosition Identifies a field in the formatted text. 537 * @param measures the measures to format. 538 * @return appendTo. 539 * @see MeasureFormat#formatMeasures(Measure...) 540 * @stable ICU 53 541 */ 542 public StringBuilder formatMeasures( 543 StringBuilder appendTo, FieldPosition fieldPosition, Measure... measures) { 544 // fast track for trivial cases 545 if (measures.length == 0) { 546 return appendTo; 547 } 548 if (measures.length == 1) { 549 return formatMeasure(measures[0], numberFormat, appendTo, fieldPosition); 550 } 551 552 if (formatWidth == FormatWidth.NUMERIC) { 553 // If we have just hour, minute, or second follow the numeric 554 // track. 555 Number[] hms = toHMS(measures); 556 if (hms != null) { 557 return formatNumeric(hms, appendTo); 558 } 559 } 560 561 ListFormatter listFormatter = ListFormatter.getInstance( 562 getLocale(), formatWidth.getListFormatterStyle()); 563 if (fieldPosition != DontCareFieldPosition.INSTANCE) { 564 return formatMeasuresSlowTrack(listFormatter, appendTo, fieldPosition, measures); 565 } 566 // Fast track: No field position. 567 String[] results = new String[measures.length]; 568 for (int i = 0; i < measures.length; i++) { 569 results[i] = formatMeasure( 570 measures[i], 571 i == measures.length - 1 ? numberFormat : integerFormat); 572 } 573 return appendTo.append(listFormatter.format((Object[]) results)); 574 575 } 576 577 /** 578 * Two MeasureFormats, a and b, are equal if and only if they have the same formatWidth, 579 * locale, and equal number formats. 580 * @stable ICU 53 581 */ 582 @Override 583 public final boolean equals(Object other) { 584 if (this == other) { 585 return true; 586 } 587 if (!(other instanceof MeasureFormat)) { 588 return false; 589 } 590 MeasureFormat rhs = (MeasureFormat) other; 591 // A very slow but safe implementation. 592 return getWidth() == rhs.getWidth() 593 && getLocale().equals(rhs.getLocale()) 594 && getNumberFormat().equals(rhs.getNumberFormat()); 595 } 596 597 /** 598 * {@inheritDoc} 599 * @stable ICU 53 600 */ 601 @Override 602 public final int hashCode() { 603 // A very slow but safe implementation. 604 return (getLocale().hashCode() * 31 605 + getNumberFormat().hashCode()) * 31 + getWidth().hashCode(); 606 } 607 608 /** 609 * Get the format width this instance is using. 610 * @stable ICU 53 611 */ 612 public MeasureFormat.FormatWidth getWidth() { 613 return formatWidth; 614 } 615 616 /** 617 * Get the locale of this instance. 618 * @stable ICU 53 619 */ 620 public final ULocale getLocale() { 621 return getLocale(ULocale.VALID_LOCALE); 622 } 623 624 /** 625 * Get a copy of the number format. 626 * @stable ICU 53 627 */ 628 public NumberFormat getNumberFormat() { 629 return numberFormat.get(); 630 } 631 632 /** 633 * Return a formatter for CurrencyAmount objects in the given 634 * locale. 635 * @param locale desired locale 636 * @return a formatter object 637 * @stable ICU 3.0 638 */ 639 public static MeasureFormat getCurrencyFormat(ULocale locale) { 640 return new CurrencyFormat(locale); 641 } 642 643 /** 644 * Return a formatter for CurrencyAmount objects in the given 645 * JDK locale. 646 * @param locale desired JDK locale 647 * @return a formatter object 648 * @draft ICU 54 649 * @provisional This API might change or be removed in a future release. 650 */ 651 public static MeasureFormat getCurrencyFormat(Locale locale) { 652 return getCurrencyFormat(ULocale.forLocale(locale)); 653 } 654 655 /** 656 * Return a formatter for CurrencyAmount objects in the default 657 * <code>FORMAT</code> locale. 658 * @return a formatter object 659 * @see Category#FORMAT 660 * @stable ICU 3.0 661 */ 662 public static MeasureFormat getCurrencyFormat() { 663 return getCurrencyFormat(ULocale.getDefault(Category.FORMAT)); 664 } 665 666 // This method changes the NumberFormat object as well to match the new locale. 667 MeasureFormat withLocale(ULocale locale) { 668 return MeasureFormat.getInstance(locale, getWidth()); 669 } 670 671 MeasureFormat withNumberFormat(NumberFormat format) { 672 return new MeasureFormat( 673 getLocale(), 674 this.formatWidth, 675 new ImmutableNumberFormat(format), 676 this.rules, 677 this.unitToStyleToCountToFormat, 678 this.numericFormatters, 679 this.currencyFormat, 680 this.integerFormat, 681 this.unitToStyleToPerUnitPattern, 682 this.styleToPerPattern); 683 } 684 685 private MeasureFormat( 686 ULocale locale, 687 FormatWidth formatWidth, 688 ImmutableNumberFormat format, 689 PluralRules rules, 690 Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> unitToStyleToCountToFormat, 691 NumericFormatters formatters, 692 ImmutableNumberFormat currencyFormat, 693 ImmutableNumberFormat integerFormat, 694 Map<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>> unitToStyleToPerUnitPattern, 695 EnumMap<FormatWidth, SimplePatternFormatter> styleToPerPattern) { 696 setLocale(locale, locale); 697 this.formatWidth = formatWidth; 698 this.numberFormat = format; 699 this.rules = rules; 700 this.unitToStyleToCountToFormat = unitToStyleToCountToFormat; 701 this.numericFormatters = formatters; 702 this.currencyFormat = currencyFormat; 703 this.integerFormat = integerFormat; 704 this.unitToStyleToPerUnitPattern = unitToStyleToPerUnitPattern; 705 this.styleToPerPattern = styleToPerPattern; 706 } 707 708 MeasureFormat() { 709 // Make compiler happy by setting final fields to null. 710 this.formatWidth = null; 711 this.numberFormat = null; 712 this.rules = null; 713 this.unitToStyleToCountToFormat = null; 714 this.numericFormatters = null; 715 this.currencyFormat = null; 716 this.integerFormat = null; 717 this.unitToStyleToPerUnitPattern = null; 718 this.styleToPerPattern = null; 719 } 720 721 static class NumericFormatters { 722 private DateFormat hourMinute; 723 private DateFormat minuteSecond; 724 private DateFormat hourMinuteSecond; 725 726 public NumericFormatters( 727 DateFormat hourMinute, 728 DateFormat minuteSecond, 729 DateFormat hourMinuteSecond) { 730 this.hourMinute = hourMinute; 731 this.minuteSecond = minuteSecond; 732 this.hourMinuteSecond = hourMinuteSecond; 733 } 734 735 public DateFormat getHourMinute() { return hourMinute; } 736 public DateFormat getMinuteSecond() { return minuteSecond; } 737 public DateFormat getHourMinuteSecond() { return hourMinuteSecond; } 738 } 739 740 private static NumericFormatters loadNumericFormatters( 741 ULocale locale) { 742 ICUResourceBundle r = (ICUResourceBundle)UResourceBundle. 743 getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale); 744 return new NumericFormatters( 745 loadNumericDurationFormat(r, "hm"), 746 loadNumericDurationFormat(r, "ms"), 747 loadNumericDurationFormat(r, "hms")); 748 } 749 750 /** 751 * Returns formatting data for all MeasureUnits except for currency ones. 752 */ 753 private static MeasureFormatData loadLocaleData( 754 ULocale locale) { 755 QuantityFormatter.Builder builder = new QuantityFormatter.Builder(); 756 Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> unitToStyleToCountToFormat 757 = new HashMap<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>>(); 758 Map<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>> unitToStyleToPerUnitPattern 759 = new HashMap<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>>(); 760 ICUResourceBundle resource = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale); 761 EnumMap<FormatWidth, SimplePatternFormatter> styleToPerPattern = new EnumMap<FormatWidth, SimplePatternFormatter>(FormatWidth.class); 762 for (FormatWidth styleItem : FormatWidth.values()) { 763 try { 764 ICUResourceBundle unitTypeRes = resource.getWithFallback(styleItem.resourceKey); 765 ICUResourceBundle compoundRes = unitTypeRes.getWithFallback("compound"); 766 ICUResourceBundle perRes = compoundRes.getWithFallback("per"); 767 styleToPerPattern.put(styleItem, SimplePatternFormatter.compile(perRes.getString())); 768 } catch (MissingResourceException e) { 769 // may not have compound/per for every width. 770 continue; 771 } 772 } 773 fillInStyleMap(styleToPerPattern); 774 for (MeasureUnit unit : MeasureUnit.getAvailable()) { 775 // Currency data cannot be found here. Skip. 776 if (unit instanceof Currency) { 777 continue; 778 } 779 EnumMap<FormatWidth, QuantityFormatter> styleToCountToFormat = unitToStyleToCountToFormat.get(unit); 780 if (styleToCountToFormat == null) { 781 unitToStyleToCountToFormat.put(unit, styleToCountToFormat = new EnumMap<FormatWidth, QuantityFormatter>(FormatWidth.class)); 782 } 783 EnumMap<FormatWidth, SimplePatternFormatter> styleToPerUnitPattern = new EnumMap<FormatWidth, SimplePatternFormatter>(FormatWidth.class); 784 unitToStyleToPerUnitPattern.put(unit, styleToPerUnitPattern); 785 for (FormatWidth styleItem : FormatWidth.values()) { 786 try { 787 ICUResourceBundle unitTypeRes = resource.getWithFallback(styleItem.resourceKey); 788 ICUResourceBundle unitsRes = unitTypeRes.getWithFallback(unit.getType()); 789 ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(unit.getSubtype()); 790 builder.reset(); 791 boolean havePluralItem = false; 792 int len = oneUnitRes.getSize(); 793 for (int i = 0; i < len; i++) { 794 UResourceBundle countBundle; 795 try { 796 countBundle = oneUnitRes.get(i); 797 } catch (MissingResourceException e) { 798 continue; 799 } 800 String resKey = countBundle.getKey(); 801 if (resKey.equals("dnam")) { 802 continue; // skip display name & per pattern (new in CLDR 26 / ICU 54) for now, not part of plurals 803 } 804 if (resKey.equals("per")) { 805 styleToPerUnitPattern.put( 806 styleItem, SimplePatternFormatter.compile(countBundle.getString())); 807 continue; 808 } 809 havePluralItem = true; 810 builder.add(resKey, countBundle.getString()); 811 } 812 if (havePluralItem) { 813 // might not have any plural items if countBundle only has "dnam" display name, for instance, 814 // as with fr unitsNarrow/light/lux in CLDR 26 815 styleToCountToFormat.put(styleItem, builder.build()); 816 } 817 } catch (MissingResourceException e) { 818 continue; 819 } 820 } 821 // TODO: if no fallback available, get from root. 822 fillInStyleMap(styleToCountToFormat); 823 fillInStyleMap(styleToPerUnitPattern); 824 } 825 return new MeasureFormatData(unitToStyleToCountToFormat, unitToStyleToPerUnitPattern, styleToPerPattern); 826 } 827 828 private static <T> boolean fillInStyleMap(Map<FormatWidth, T> styleMap) { 829 if (styleMap.size() == FormatWidth.values().length) { 830 return true; 831 } 832 T fallback = styleMap.get(FormatWidth.SHORT); 833 if (fallback == null) { 834 fallback = styleMap.get(FormatWidth.WIDE); 835 } 836 if (fallback == null) { 837 return false; 838 } 839 for (FormatWidth styleItem : FormatWidth.values()) { 840 T item = styleMap.get(styleItem); 841 if (item == null) { 842 styleMap.put(styleItem, fallback); 843 } 844 } 845 return true; 846 } 847 848 private int withPerUnitAndAppend( 849 CharSequence formatted, MeasureUnit perUnit, StringBuilder appendTo) { 850 int[] offsets = new int[1]; 851 Map<FormatWidth, SimplePatternFormatter> styleToPerUnitPattern = 852 unitToStyleToPerUnitPattern.get(perUnit); 853 SimplePatternFormatter perUnitPattern = styleToPerUnitPattern.get(formatWidth); 854 if (perUnitPattern != null) { 855 perUnitPattern.formatAndAppend(appendTo, offsets, formatted); 856 return offsets[0]; 857 } 858 SimplePatternFormatter perPattern = styleToPerPattern.get(formatWidth); 859 Map<FormatWidth, QuantityFormatter> styleToCountToFormat = unitToStyleToCountToFormat.get(perUnit); 860 QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth); 861 String perUnitString = countToFormat.getByVariant("one").getPatternWithNoPlaceholders().trim(); 862 perPattern.formatAndAppend(appendTo, offsets, formatted, perUnitString); 863 return offsets[0]; 864 } 865 866 private String formatMeasure(Measure measure, ImmutableNumberFormat nf) { 867 return formatMeasure( 868 measure, nf, new StringBuilder(), 869 DontCareFieldPosition.INSTANCE).toString(); 870 } 871 872 private StringBuilder formatMeasure( 873 Measure measure, 874 ImmutableNumberFormat nf, 875 StringBuilder appendTo, 876 FieldPosition fieldPosition) { 877 if (measure.getUnit() instanceof Currency) { 878 return appendTo.append( 879 currencyFormat.format( 880 new CurrencyAmount(measure.getNumber(), (Currency) measure.getUnit()), 881 new StringBuffer(), 882 fieldPosition)); 883 884 } 885 Number n = measure.getNumber(); 886 MeasureUnit unit = measure.getUnit(); 887 UFieldPosition fpos = new UFieldPosition(fieldPosition.getFieldAttribute(), fieldPosition.getField()); 888 StringBuffer formattedNumber = nf.format(n, new StringBuffer(), fpos); 889 String keyword = rules.select(new PluralRules.FixedDecimal(n.doubleValue(), fpos.getCountVisibleFractionDigits(), fpos.getFractionDigits())); 890 891 Map<FormatWidth, QuantityFormatter> styleToCountToFormat = unitToStyleToCountToFormat.get(unit); 892 QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth); 893 SimplePatternFormatter formatter = countToFormat.getByVariant(keyword); 894 int[] offsets = new int[1]; 895 formatter.formatAndAppend(appendTo, offsets, formattedNumber); 896 if (offsets[0] != -1) { // there is a number (may not happen with, say, Arabic dual) 897 // Fix field position 898 if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) { 899 fieldPosition.setBeginIndex(fpos.getBeginIndex() + offsets[0]); 900 fieldPosition.setEndIndex(fpos.getEndIndex() + offsets[0]); 901 } 902 } 903 return appendTo; 904 } 905 906 private static final class MeasureFormatData { 907 MeasureFormatData( 908 Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> unitToStyleToCountToFormat, 909 Map<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>> unitToStyleToPerUnitPattern, 910 EnumMap<FormatWidth, SimplePatternFormatter> styleToPerPattern) { 911 this.unitToStyleToCountToFormat = unitToStyleToCountToFormat; 912 this.unitToStyleToPerUnitPattern = unitToStyleToPerUnitPattern; 913 this.styleToPerPattern = styleToPerPattern; 914 } 915 final Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> unitToStyleToCountToFormat; 916 final Map<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>> unitToStyleToPerUnitPattern; 917 final EnumMap<FormatWidth, SimplePatternFormatter> styleToPerPattern; 918 } 919 920 // Wrapper around NumberFormat that provides immutability and thread-safety. 921 private static final class ImmutableNumberFormat { 922 private NumberFormat nf; 923 924 public ImmutableNumberFormat(NumberFormat nf) { 925 this.nf = (NumberFormat) nf.clone(); 926 } 927 928 public synchronized NumberFormat get() { 929 return (NumberFormat) nf.clone(); 930 } 931 932 public synchronized StringBuffer format( 933 Number n, StringBuffer buffer, FieldPosition pos) { 934 return nf.format(n, buffer, pos); 935 } 936 937 public synchronized StringBuffer format( 938 CurrencyAmount n, StringBuffer buffer, FieldPosition pos) { 939 return nf.format(n, buffer, pos); 940 } 941 942 @SuppressWarnings("unused") 943 public synchronized String format(Number number) { 944 return nf.format(number); 945 } 946 947 public String getPrefix(boolean positive) { 948 return positive ? ((DecimalFormat)nf).getPositivePrefix() : ((DecimalFormat)nf).getNegativePrefix(); 949 } 950 public String getSuffix(boolean positive) { 951 return positive ? ((DecimalFormat)nf).getPositiveSuffix() : ((DecimalFormat)nf).getPositiveSuffix(); 952 } 953 } 954 955 static final class PatternData { 956 final String prefix; 957 final String suffix; 958 public PatternData(String pattern) { 959 int pos = pattern.indexOf("{0}"); 960 if (pos < 0) { 961 prefix = pattern; 962 suffix = null; 963 } else { 964 prefix = pattern.substring(0,pos); 965 suffix = pattern.substring(pos+3); 966 } 967 } 968 public String toString() { 969 return prefix + "; " + suffix; 970 } 971 972 } 973 974 Object toTimeUnitProxy() { 975 return new MeasureProxy(getLocale(), formatWidth, numberFormat.get(), TIME_UNIT_FORMAT); 976 } 977 978 Object toCurrencyProxy() { 979 return new MeasureProxy(getLocale(), formatWidth, numberFormat.get(), CURRENCY_FORMAT); 980 } 981 982 private StringBuilder formatMeasuresSlowTrack( 983 ListFormatter listFormatter, 984 StringBuilder appendTo, 985 FieldPosition fieldPosition, 986 Measure... measures) { 987 String[] results = new String[measures.length]; 988 989 // Zero out our field position so that we can tell when we find our field. 990 FieldPosition fpos = new FieldPosition( 991 fieldPosition.getFieldAttribute(), fieldPosition.getField()); 992 993 int fieldPositionFoundIndex = -1; 994 for (int i = 0; i < measures.length; ++i) { 995 ImmutableNumberFormat nf = (i == measures.length - 1 ? numberFormat : integerFormat); 996 if (fieldPositionFoundIndex == -1) { 997 results[i] = formatMeasure(measures[i], nf, new StringBuilder(), fpos).toString(); 998 if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) { 999 fieldPositionFoundIndex = i; 1000 } 1001 } else { 1002 results[i] = formatMeasure(measures[i], nf); 1003 } 1004 } 1005 ListFormatter.FormattedListBuilder builder = 1006 listFormatter.format(Arrays.asList(results), fieldPositionFoundIndex); 1007 1008 // Fix up FieldPosition indexes if our field is found. 1009 if (builder.getOffset() != -1) { 1010 fieldPosition.setBeginIndex(fpos.getBeginIndex() + builder.getOffset() + appendTo.length()); 1011 fieldPosition.setEndIndex(fpos.getEndIndex() + builder.getOffset() + appendTo.length()); 1012 } 1013 return appendTo.append(builder.toString()); 1014 } 1015 1016 // type is one of "hm", "ms" or "hms" 1017 private static DateFormat loadNumericDurationFormat( 1018 ICUResourceBundle r, String type) { 1019 r = r.getWithFallback(String.format("durationUnits/%s", type)); 1020 // We replace 'h' with 'H' because 'h' does not make sense in the context of durations. 1021 DateFormat result = new SimpleDateFormat(r.getString().replace("h", "H")); 1022 result.setTimeZone(TimeZone.GMT_ZONE); 1023 return result; 1024 } 1025 1026 // Returns hours in [0]; minutes in [1]; seconds in [2] out of measures array. If 1027 // unsuccessful, e.g measures has other measurements besides hours, minutes, seconds; 1028 // hours, minutes, seconds are out of order; or have negative values, returns null. 1029 // If hours, minutes, or seconds is missing from measures the corresponding element in 1030 // returned array will be null. 1031 private static Number[] toHMS(Measure[] measures) { 1032 Number[] result = new Number[3]; 1033 int lastIdx = -1; 1034 for (Measure m : measures) { 1035 if (m.getNumber().doubleValue() < 0.0) { 1036 return null; 1037 } 1038 Integer idxObj = hmsTo012.get(m.getUnit()); 1039 if (idxObj == null) { 1040 return null; 1041 } 1042 int idx = idxObj.intValue(); 1043 if (idx <= lastIdx) { 1044 // hour before minute before second 1045 return null; 1046 } 1047 lastIdx = idx; 1048 result[idx] = m.getNumber(); 1049 } 1050 return result; 1051 } 1052 1053 // Formats numeric time duration as 5:00:47 or 3:54. In the process, it replaces any null 1054 // values in hms with 0. 1055 private StringBuilder formatNumeric(Number[] hms, StringBuilder appendable) { 1056 1057 // find the start and end of non-nil values in hms array. We have to know if we 1058 // have hour-minute; minute-second; or hour-minute-second. 1059 int startIndex = -1; 1060 int endIndex = -1; 1061 for (int i = 0; i < hms.length; i++) { 1062 if (hms[i] != null) { 1063 endIndex = i; 1064 if (startIndex == -1) { 1065 startIndex = endIndex; 1066 } 1067 } else { 1068 // Replace nil value with 0. 1069 hms[i] = Integer.valueOf(0); 1070 } 1071 } 1072 // convert hours, minutes, seconds into milliseconds. 1073 long millis = (long) (((Math.floor(hms[0].doubleValue()) * 60.0 1074 + Math.floor(hms[1].doubleValue())) * 60.0 1075 + Math.floor(hms[2].doubleValue())) * 1000.0); 1076 Date d = new Date(millis); 1077 // if hour-minute-second 1078 if (startIndex == 0 && endIndex == 2) { 1079 return formatNumeric( 1080 d, 1081 numericFormatters.getHourMinuteSecond(), 1082 DateFormat.Field.SECOND, 1083 hms[endIndex], 1084 appendable); 1085 } 1086 // if minute-second 1087 if (startIndex == 1 && endIndex == 2) { 1088 return formatNumeric( 1089 d, 1090 numericFormatters.getMinuteSecond(), 1091 DateFormat.Field.SECOND, 1092 hms[endIndex], 1093 appendable); 1094 } 1095 // if hour-minute 1096 if (startIndex == 0 && endIndex == 1) { 1097 return formatNumeric( 1098 d, 1099 numericFormatters.getHourMinute(), 1100 DateFormat.Field.MINUTE, 1101 hms[endIndex], 1102 appendable); 1103 } 1104 throw new IllegalStateException(); 1105 } 1106 1107 // Formats a duration as 5:00:37 or 23:59. 1108 // duration is a particular duration after epoch. 1109 // formatter is a hour-minute-second, hour-minute, or minute-second formatter. 1110 // smallestField denotes what the smallest field is in duration: either 1111 // hour, minute, or second. 1112 // smallestAmount is the value of that smallest field. for 5:00:37.3, 1113 // smallestAmount is 37.3. This smallest field is formatted with this object's 1114 // NumberFormat instead of formatter. 1115 // appendTo is where the formatted string is appended. 1116 private StringBuilder formatNumeric( 1117 Date duration, 1118 DateFormat formatter, 1119 DateFormat.Field smallestField, 1120 Number smallestAmount, 1121 StringBuilder appendTo) { 1122 // Format the smallest amount ahead of time. 1123 String smallestAmountFormatted; 1124 1125 // Format the smallest amount using this object's number format, but keep track 1126 // of the integer portion of this formatted amount. We have to replace just the 1127 // integer part with the corresponding value from formatting the date. Otherwise 1128 // when formatting 0 minutes 9 seconds, we may get "00:9" instead of "00:09" 1129 FieldPosition intFieldPosition = new FieldPosition(NumberFormat.INTEGER_FIELD); 1130 smallestAmountFormatted = numberFormat.format( 1131 smallestAmount, new StringBuffer(), intFieldPosition).toString(); 1132 // Give up if there is no integer field. 1133 if (intFieldPosition.getBeginIndex() == 0 && intFieldPosition.getEndIndex() == 0) { 1134 throw new IllegalStateException(); 1135 } 1136 // Format our duration as a date, but keep track of where the smallest field is 1137 // so that we can use it to replace the integer portion of the smallest value. 1138 FieldPosition smallestFieldPosition = new FieldPosition(smallestField); 1139 String draft = formatter.format( 1140 duration, new StringBuffer(), smallestFieldPosition).toString(); 1141 1142 // If we find the smallest field 1143 if (smallestFieldPosition.getBeginIndex() != 0 1144 || smallestFieldPosition.getEndIndex() != 0) { 1145 // add everything up to the start of the smallest field in duration. 1146 appendTo.append(draft, 0, smallestFieldPosition.getBeginIndex()); 1147 1148 // add everything in the smallest field up to the integer portion 1149 appendTo.append(smallestAmountFormatted, 0, intFieldPosition.getBeginIndex()); 1150 1151 // Add the smallest field in formatted duration in lieu of the integer portion 1152 // of smallest field 1153 appendTo.append( 1154 draft, 1155 smallestFieldPosition.getBeginIndex(), 1156 smallestFieldPosition.getEndIndex()); 1157 1158 // Add the rest of the smallest field 1159 appendTo.append( 1160 smallestAmountFormatted, 1161 intFieldPosition.getEndIndex(), 1162 smallestAmountFormatted.length()); 1163 appendTo.append(draft, smallestFieldPosition.getEndIndex(), draft.length()); 1164 } else { 1165 // As fallback, just use the formatted duration. 1166 appendTo.append(draft); 1167 } 1168 return appendTo; 1169 } 1170 1171 private Object writeReplace() throws ObjectStreamException { 1172 return new MeasureProxy( 1173 getLocale(), formatWidth, numberFormat.get(), MEASURE_FORMAT); 1174 } 1175 1176 static class MeasureProxy implements Externalizable { 1177 private static final long serialVersionUID = -6033308329886716770L; 1178 1179 private ULocale locale; 1180 private FormatWidth formatWidth; 1181 private NumberFormat numberFormat; 1182 private int subClass; 1183 private HashMap<Object, Object> keyValues; 1184 1185 public MeasureProxy( 1186 ULocale locale, 1187 FormatWidth width, 1188 NumberFormat numberFormat, 1189 int subClass) { 1190 this.locale = locale; 1191 this.formatWidth = width; 1192 this.numberFormat = numberFormat; 1193 this.subClass = subClass; 1194 this.keyValues = new HashMap<Object, Object>(); 1195 } 1196 1197 // Must have public constructor, to enable Externalizable 1198 public MeasureProxy() { 1199 } 1200 1201 public void writeExternal(ObjectOutput out) throws IOException { 1202 out.writeByte(0); // version 1203 out.writeUTF(locale.toLanguageTag()); 1204 out.writeByte(formatWidth.ordinal()); 1205 out.writeObject(numberFormat); 1206 out.writeByte(subClass); 1207 out.writeObject(keyValues); 1208 } 1209 1210 @SuppressWarnings("unchecked") 1211 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 1212 in.readByte(); // version. 1213 locale = ULocale.forLanguageTag(in.readUTF()); 1214 formatWidth = fromFormatWidthOrdinal(in.readByte() & 0xFF); 1215 numberFormat = (NumberFormat) in.readObject(); 1216 if (numberFormat == null) { 1217 throw new InvalidObjectException("Missing number format."); 1218 } 1219 subClass = in.readByte() & 0xFF; 1220 1221 // This cast is safe because the serialized form of hashtable can have 1222 // any object as the key and any object as the value. 1223 keyValues = (HashMap<Object, Object>) in.readObject(); 1224 if (keyValues == null) { 1225 throw new InvalidObjectException("Missing optional values map."); 1226 } 1227 } 1228 1229 private TimeUnitFormat createTimeUnitFormat() throws InvalidObjectException { 1230 int style; 1231 if (formatWidth == FormatWidth.WIDE) { 1232 style = TimeUnitFormat.FULL_NAME; 1233 } else if (formatWidth == FormatWidth.SHORT) { 1234 style = TimeUnitFormat.ABBREVIATED_NAME; 1235 } else { 1236 throw new InvalidObjectException("Bad width: " + formatWidth); 1237 } 1238 TimeUnitFormat result = new TimeUnitFormat(locale, style); 1239 result.setNumberFormat(numberFormat); 1240 return result; 1241 } 1242 1243 private Object readResolve() throws ObjectStreamException { 1244 switch (subClass) { 1245 case MEASURE_FORMAT: 1246 return MeasureFormat.getInstance(locale, formatWidth, numberFormat); 1247 case TIME_UNIT_FORMAT: 1248 return createTimeUnitFormat(); 1249 case CURRENCY_FORMAT: 1250 return new CurrencyFormat(locale); 1251 default: 1252 throw new InvalidObjectException("Unknown subclass: " + subClass); 1253 } 1254 } 1255 } 1256 1257 private static FormatWidth fromFormatWidthOrdinal(int ordinal) { 1258 FormatWidth[] values = FormatWidth.values(); 1259 if (ordinal < 0 || ordinal >= values.length) { 1260 return FormatWidth.WIDE; 1261 } 1262 return values[ordinal]; 1263 } 1264 1265 static final Map<ULocale, SimplePatternFormatter> localeIdToRangeFormat 1266 = new ConcurrentHashMap<ULocale, SimplePatternFormatter>(); 1267 1268 /** 1269 * Return a simple pattern formatter for a range, such as "{0}–{1}". 1270 * @param forLocale locale to get the format for 1271 * @param width the format width 1272 * @return range formatter, such as "{0}–{1}" 1273 * @internal 1274 * @deprecated This API is ICU internal only. 1275 */ 1276 @Deprecated 1277 1278 public static SimplePatternFormatter getRangeFormat(ULocale forLocale, FormatWidth width) { 1279 // TODO fix Hack for French 1280 if (forLocale.getLanguage().equals("fr")) { 1281 return getRangeFormat(ULocale.ROOT, width); 1282 } 1283 SimplePatternFormatter result = localeIdToRangeFormat.get(forLocale); 1284 if (result == null) { 1285 ICUResourceBundle rb = (ICUResourceBundle)UResourceBundle. 1286 getBundleInstance(ICUData.ICU_BASE_NAME, forLocale); 1287 ULocale realLocale = rb.getULocale(); 1288 if (!forLocale.equals(realLocale)) { // if the child would inherit, then add a cache entry for it. 1289 result = localeIdToRangeFormat.get(forLocale); 1290 if (result != null) { 1291 localeIdToRangeFormat.put(forLocale, result); 1292 return result; 1293 } 1294 } 1295 // At this point, both the forLocale and the realLocale don't have an item 1296 // So we have to make one. 1297 NumberingSystem ns = NumberingSystem.getInstance(forLocale); 1298 1299 String resultString = null; 1300 try { 1301 resultString = rb.getStringWithFallback("NumberElements/" + ns.getName() + "/miscPatterns/range"); 1302 } catch ( MissingResourceException ex ) { 1303 resultString = rb.getStringWithFallback("NumberElements/latn/patterns/range"); 1304 } 1305 result = SimplePatternFormatter.compile(resultString); 1306 localeIdToRangeFormat.put(forLocale, result); 1307 if (!forLocale.equals(realLocale)) { 1308 localeIdToRangeFormat.put(realLocale, result); 1309 } 1310 } 1311 return result; 1312 } 1313 1314 /** 1315 * Return a simple pattern pattern for a range, such as "{0}–{1}" or "{0}~{1}". 1316 * @param forLocale locale to get the range pattern for 1317 * @param width the format width. 1318 * @return range pattern 1319 * @internal 1320 * @deprecated This API is ICU internal only. 1321 */ 1322 @Deprecated 1323 public static String getRangePattern(ULocale forLocale, FormatWidth width) { 1324 return getRangeFormat(forLocale, width).toString(); 1325 } 1326} 1327