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&lt;? extends Measure&gt;, 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