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