MeasureFormat.java

// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 **********************************************************************
 * Copyright (c) 2004-2016, International Business Machines
 * Corporation and others.  All Rights Reserved.
 **********************************************************************
 * Author: Alan Liu
 * Created: April 20, 2004
 * Since: ICU 3.0
 **********************************************************************
 */
package com.ibm.icu.text;

import com.ibm.icu.impl.DontCareFieldPosition;
import com.ibm.icu.impl.FormattedStringBuilder;
import com.ibm.icu.impl.FormattedValueStringBuilderImpl;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.SimpleCache;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
import com.ibm.icu.impl.number.LongNameHandler;
import com.ibm.icu.impl.number.RoundingUtils;
import com.ibm.icu.number.IntegerWidth;
import com.ibm.icu.number.LocalizedNumberFormatter;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.number.Precision;
import com.ibm.icu.text.ListFormatter.FormattedListBuilder;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;
import com.ibm.icu.util.UResourceBundle;
import java.io.Externalizable;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.ObjectStreamException;
import java.math.RoundingMode;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.concurrent.ConcurrentHashMap;

// If you update the examples in the doc, don't forget to update MesaureUnitTest.TestExamplesInDocs
// too.
/**
 * A formatter for Measure objects.
 *
 * <p><strong>IMPORTANT:</strong> New users are strongly encouraged to see if {@link
 * NumberFormatter} fits their use case. Although not deprecated, this class, MeasureFormat, is
 * provided for backwards compatibility only, and has much more limited capabilities. <hr>
 *
 * <p>To format a Measure object, first create a formatter object using a MeasureFormat factory
 * method. Then use that object's format or formatMeasures methods.
 *
 * <p>Here is sample code:
 *
 * <pre>
 * MeasureFormat fmtFr = MeasureFormat.getInstance(ULocale.FRENCH, FormatWidth.SHORT);
 * Measure measure = new Measure(23, MeasureUnit.CELSIUS);
 *
 * // Output: 23 °C
 * System.out.println(fmtFr.format(measure));
 *
 * Measure measureF = new Measure(70, MeasureUnit.FAHRENHEIT);
 *
 * // Output: 70 °F
 * System.out.println(fmtFr.format(measureF));
 *
 * MeasureFormat fmtFrFull = MeasureFormat.getInstance(ULocale.FRENCH, FormatWidth.WIDE);
 * // Output: 70 pieds et 5,3 pouces
 * System.out.println(fmtFrFull.formatMeasures(new Measure(70, MeasureUnit.FOOT),
 *         new Measure(5.3, MeasureUnit.INCH)));
 *
 * // Output: 1 pied et 1 pouce
 * System.out.println(
 *         fmtFrFull.formatMeasures(new Measure(1, MeasureUnit.FOOT), new Measure(1, MeasureUnit.INCH)));
 *
 * MeasureFormat fmtFrNarrow = MeasureFormat.getInstance(ULocale.FRENCH, FormatWidth.NARROW);
 * // Output: 1′ 1″
 * System.out.println(fmtFrNarrow.formatMeasures(new Measure(1, MeasureUnit.FOOT),
 *         new Measure(1, MeasureUnit.INCH)));
 *
 * MeasureFormat fmtEn = MeasureFormat.getInstance(ULocale.ENGLISH, FormatWidth.WIDE);
 *
 * // Output: 1 inch, 2 feet
 * fmtEn.formatMeasures(new Measure(1, MeasureUnit.INCH), new Measure(2, MeasureUnit.FOOT));
 * </pre>
 *
 * <p>This class does not do conversions from one unit to another. It simply formats whatever units
 * it is given
 *
 * <p>This class is immutable and thread-safe so long as its deprecated subclass, TimeUnitFormat, is
 * never used. TimeUnitFormat is not thread-safe, and is mutable. Although this class has existing
 * subclasses, this class does not support new sub-classes.
 *
 * @see com.ibm.icu.text.UFormat
 * @author Alan Liu
 * @stable ICU 3.0
 */
public class MeasureFormat extends UFormat {

    // Generated by serialver from JDK 1.4.1_01
    static final long serialVersionUID = -7182021401701778240L;

    private final transient FormatWidth formatWidth;

    // PluralRules is documented as being immutable which implies thread-safety.
    private final transient PluralRules rules;

    private final transient NumericFormatters numericFormatters;

    private final transient NumberFormat numberFormat;

    private final transient LocalizedNumberFormatter numberFormatter;

    private static final SimpleCache<ULocale, NumericFormatters> localeToNumericDurationFormatters =
            new SimpleCache<>();

    private static final Map<MeasureUnit, Integer> hmsTo012 = new HashMap<>();

    static {
        hmsTo012.put(MeasureUnit.HOUR, 0);
        hmsTo012.put(MeasureUnit.MINUTE, 1);
        hmsTo012.put(MeasureUnit.SECOND, 2);
    }

    // For serialization: sub-class types.
    private static final int MEASURE_FORMAT = 0;
    private static final int TIME_UNIT_FORMAT = 1;
    private static final int CURRENCY_FORMAT = 2;

    /**
     * Formatting width enum.
     *
     * @stable ICU 53
     */
    // Be sure to update MeasureUnitTest.TestSerialFormatWidthEnum
    // when adding an enum value.
    public enum FormatWidth {

        /**
         * Spell out everything.
         *
         * @stable ICU 53
         */
        WIDE(ListFormatter.Width.WIDE, UnitWidth.FULL_NAME, UnitWidth.FULL_NAME),

        /**
         * Abbreviate when possible.
         *
         * @stable ICU 53
         */
        SHORT(ListFormatter.Width.SHORT, UnitWidth.SHORT, UnitWidth.ISO_CODE),

        /**
         * Brief. Use only a symbol for the unit when possible.
         *
         * @stable ICU 53
         */
        NARROW(ListFormatter.Width.NARROW, UnitWidth.NARROW, UnitWidth.SHORT),

        /**
         * Identical to NARROW except when formatMeasures is called with an hour and minute; minute
         * and second; or hour, minute, and second Measures. In these cases formatMeasures formats
         * as 5:37:23 instead of 5h, 37m, 23s.
         *
         * @stable ICU 53
         */
        NUMERIC(ListFormatter.Width.NARROW, UnitWidth.NARROW, UnitWidth.SHORT),

        /**
         * The default format width for getCurrencyFormat(), which is to show the symbol for
         * currency (UnitWidth.SHORT) but wide for other units.
         *
         * @internal Use {@link #getCurrencyFormat()}
         * @deprecated ICU 61 This API is ICU internal only.
         */
        @Deprecated
        DEFAULT_CURRENCY(ListFormatter.Width.SHORT, UnitWidth.FULL_NAME, UnitWidth.SHORT);

        final ListFormatter.Width listWidth;

        /**
         * The {@link UnitWidth} (used for newer NumberFormatter API) that corresponds to this
         * FormatWidth (used for the older APIs) for all units except currencies.
         */
        final UnitWidth unitWidth;

        /**
         * The {@link UnitWidth} (used for newer NumberFormatter API) that corresponds to this
         * FormatWidth (used for the older APIs) for currencies.
         */
        final UnitWidth currencyWidth;

        private FormatWidth(
                ListFormatter.Width listWidth, UnitWidth unitWidth, UnitWidth currencyWidth) {
            this.listWidth = listWidth;
            this.unitWidth = unitWidth;
            this.currencyWidth = currencyWidth;
        }
    }

    /**
     * Create a format from the locale, formatWidth, and format.
     *
     * @param locale the locale.
     * @param formatWidth hints how long formatted strings should be.
     * @return The new MeasureFormat object.
     * @stable ICU 53
     */
    public static MeasureFormat getInstance(ULocale locale, FormatWidth formatWidth) {
        return getInstance(locale, formatWidth, NumberFormat.getInstance(locale));
    }

    /**
     * Create a format from the {@link java.util.Locale} and formatWidth.
     *
     * @param locale the {@link java.util.Locale}.
     * @param formatWidth hints how long formatted strings should be.
     * @return The new MeasureFormat object.
     * @stable ICU 54
     */
    public static MeasureFormat getInstance(Locale locale, FormatWidth formatWidth) {
        return getInstance(ULocale.forLocale(locale), formatWidth);
    }

    /**
     * Create a format from the locale, formatWidth, and format.
     *
     * @param locale the locale.
     * @param formatWidth hints how long formatted strings should be.
     * @param format This is defensively copied.
     * @return The new MeasureFormat object.
     * @stable ICU 53
     */
    public static MeasureFormat getInstance(
            ULocale locale, FormatWidth formatWidth, NumberFormat format) {
        return new MeasureFormat(locale, formatWidth, format, null, null);
    }

    /**
     * Create a format from the {@link java.util.Locale}, formatWidth, and format.
     *
     * @param locale the {@link java.util.Locale}.
     * @param formatWidth hints how long formatted strings should be.
     * @param format This is defensively copied.
     * @return The new MeasureFormat object.
     * @stable ICU 54
     */
    public static MeasureFormat getInstance(
            Locale locale, FormatWidth formatWidth, NumberFormat format) {
        return getInstance(ULocale.forLocale(locale), formatWidth, format);
    }

    /**
     * Able to format Collection&lt;? extends Measure&gt;, Measure[], and Measure by delegating to
     * formatMeasures. If the pos argument identifies a NumberFormat field, then its indices are set
     * to the beginning and end of the first such field encountered. MeasureFormat itself does not
     * supply any fields.
     *
     * <p>Calling a <code>formatMeasures</code> method is preferred over calling this method as they
     * give better performance.
     *
     * @param obj must be a Collection&lt;? extends Measure&gt;, Measure[], or Measure object.
     * @param toAppendTo Formatted string appended here.
     * @param fpos Identifies a field in the formatted text.
     * @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer,
     *     java.text.FieldPosition)
     * @stable ICU53
     */
    @Override
    public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition fpos) {
        int prevLength = toAppendTo.length();
        fpos.setBeginIndex(0);
        fpos.setEndIndex(0);
        if (obj instanceof Collection) {
            Collection<?> coll = (Collection<?>) obj;
            Measure[] measures = new Measure[coll.size()];
            int idx = 0;
            for (Object o : coll) {
                if (!(o instanceof Measure)) {
                    throw new IllegalArgumentException(obj.toString());
                }
                measures[idx++] = (Measure) o;
            }
            formatMeasuresInternal(toAppendTo, fpos, measures);
        } else if (obj instanceof Measure[]) {
            formatMeasuresInternal(toAppendTo, fpos, (Measure[]) obj);
        } else if (obj instanceof Measure) {
            FormattedStringBuilder result = formatMeasure((Measure) obj);
            // No offset: toAppendTo.length() is considered below
            FormattedValueStringBuilderImpl.nextFieldPosition(result, fpos);
            Utility.appendTo(result, toAppendTo);
        } else {
            throw new IllegalArgumentException(obj.toString());
        }
        if (prevLength > 0 && fpos.getEndIndex() != 0) {
            fpos.setBeginIndex(fpos.getBeginIndex() + prevLength);
            fpos.setEndIndex(fpos.getEndIndex() + prevLength);
        }
        return toAppendTo;
    }

    /**
     * Parses text from a string to produce a <code>Measure</code>.
     *
     * @see java.text.Format#parseObject(java.lang.String, java.text.ParsePosition)
     * @throws UnsupportedOperationException Not supported.
     * @draft ICU 53 (Retain)
     */
    @Override
    public Measure parseObject(String source, ParsePosition pos) {
        throw new UnsupportedOperationException();
    }

    /**
     * Format a sequence of measures. Uses the ListFormatter unit lists. So, for example, one could
     * format “3 feet, 2 inches”. Zero values are formatted (eg, “3 feet, 0 inches”). It is the
     * caller’s responsibility to have the appropriate values in appropriate order, and using the
     * appropriate Number values. Typically the units should be in descending order, with all but
     * the last Measure having integer values (eg, not “3.2 feet, 2 inches”).
     *
     * @param measures a sequence of one or more measures.
     * @return the formatted string.
     * @stable ICU 53
     */
    public final String formatMeasures(Measure... measures) {
        return formatMeasures(new StringBuilder(), DontCareFieldPosition.INSTANCE, measures)
                .toString();
    }

    // NOTE: For formatMeasureRange(), see https://unicode-org.atlassian.net/browse/ICU-12454

    /**
     * Formats a single measure per unit.
     *
     * <p>An example of such a formatted string is "3.5 meters per second."
     *
     * @param measure the measure object. In above example, 3.5 meters.
     * @param perUnit the per unit. In above example, it is MeasureUnit.SECOND
     * @param appendTo formatted string appended here.
     * @param pos The field position.
     * @return appendTo.
     * @stable ICU 55
     */
    public StringBuilder formatMeasurePerUnit(
            Measure measure, MeasureUnit perUnit, StringBuilder appendTo, FieldPosition pos) {
        DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(measure.getNumber());
        FormattedStringBuilder string = new FormattedStringBuilder();
        getUnitFormatterFromCache(NUMBER_FORMATTER_STANDARD, measure.getUnit(), perUnit)
                .formatImpl(dq, string);
        DecimalFormat.fieldPositionHelper(dq, string, pos, appendTo.length());
        Utility.appendTo(string, appendTo);
        return appendTo;
    }

    /**
     * Formats a sequence of measures.
     *
     * <p>If the fieldPosition argument identifies a NumberFormat field, then its indices are set to
     * the beginning and end of the first such field encountered. MeasureFormat itself does not
     * supply any fields.
     *
     * @param appendTo the formatted string appended here.
     * @param fpos Identifies a field in the formatted text.
     * @param measures the measures to format.
     * @return appendTo.
     * @see MeasureFormat#formatMeasures(Measure...)
     * @stable ICU 53
     */
    public StringBuilder formatMeasures(
            StringBuilder appendTo, FieldPosition fpos, Measure... measures) {
        int prevLength = appendTo.length();
        formatMeasuresInternal(appendTo, fpos, measures);
        if (prevLength > 0 && fpos.getEndIndex() > 0) {
            fpos.setBeginIndex(fpos.getBeginIndex() + prevLength);
            fpos.setEndIndex(fpos.getEndIndex() + prevLength);
        }
        return appendTo;
    }

    private void formatMeasuresInternal(
            Appendable appendTo, FieldPosition fieldPosition, Measure... measures) {
        // fast track for trivial cases
        if (measures.length == 0) {
            return;
        }
        if (measures.length == 1) {
            FormattedStringBuilder result = formatMeasure(measures[0]);
            FormattedValueStringBuilderImpl.nextFieldPosition(result, fieldPosition);
            Utility.appendTo(result, appendTo);
            return;
        }

        if (formatWidth == FormatWidth.NUMERIC) {
            // If we have just hour, minute, or second follow the numeric
            // track.
            Number[] hms = toHMS(measures);
            if (hms != null) {
                formatNumeric(hms, appendTo);
                return;
            }
        }

        ListFormatter listFormatter =
                ListFormatter.getInstance(
                        getLocale(), ListFormatter.Type.UNITS, formatWidth.listWidth);
        if (fieldPosition != DontCareFieldPosition.INSTANCE) {
            formatMeasuresSlowTrack(listFormatter, appendTo, fieldPosition, measures);
            return;
        }
        // Fast track: No field position.
        String[] results = new String[measures.length];
        for (int i = 0; i < measures.length; i++) {
            if (i == measures.length - 1) {
                results[i] = formatMeasure(measures[i]).toString();
            } else {
                results[i] = formatMeasureInteger(measures[i]).toString();
            }
        }
        FormattedListBuilder builder = listFormatter.formatImpl(Arrays.asList(results), false);
        builder.appendTo(appendTo);
    }

    /**
     * Gets the display name of the specified {@link MeasureUnit} corresponding to the current
     * locale and format width.
     *
     * @param unit The unit for which to get a display name.
     * @return The display name in the locale and width specified in {@link
     *     MeasureFormat#getInstance}, or null if there is no display name available for the
     *     specified unit.
     * @stable ICU 58
     */
    public String getUnitDisplayName(MeasureUnit unit) {
        return LongNameHandler.getUnitDisplayName(getLocale(), unit, formatWidth.unitWidth);
    }

    /**
     * Two MeasureFormats, a and b, are equal if and only if they have the same formatWidth, locale,
     * and equal number formats.
     *
     * @stable ICU 3.0
     */
    @Override
    public final boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof MeasureFormat)) {
            return false;
        }
        MeasureFormat rhs = (MeasureFormat) other;
        // A very slow but safe implementation.
        return getWidth() == rhs.getWidth()
                && getLocale().equals(rhs.getLocale())
                && getNumberFormatInternal().equals(rhs.getNumberFormatInternal());
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 3.0
     */
    @Override
    public final int hashCode() {
        // A very slow but safe implementation.
        return (getLocale().hashCode() * 31 + getNumberFormatInternal().hashCode()) * 31
                + getWidth().hashCode();
    }

    /**
     * Get the format width this instance is using.
     *
     * @stable ICU 53
     */
    public MeasureFormat.FormatWidth getWidth() {
        if (formatWidth == MeasureFormat.FormatWidth.DEFAULT_CURRENCY) {
            return MeasureFormat.FormatWidth.WIDE;
        }
        return formatWidth;
    }

    /**
     * Get the locale of this instance.
     *
     * @stable ICU 53
     */
    public final ULocale getLocale() {
        return getLocale(ULocale.VALID_LOCALE);
    }

    /**
     * Get a copy of the number format.
     *
     * @stable ICU 53
     */
    public NumberFormat getNumberFormat() {
        return numberFormat.clone();
    }

    /** Get a copy of the number format without cloning. Internal method. */
    NumberFormat getNumberFormatInternal() {
        return numberFormat;
    }

    /**
     * Return a formatter for CurrencyAmount objects in the given locale.
     *
     * @param locale desired locale
     * @return a formatter object
     * @stable ICU 3.0
     */
    public static MeasureFormat getCurrencyFormat(ULocale locale) {
        return new CurrencyFormat(locale);
    }

    /**
     * Return a formatter for CurrencyAmount objects in the given {@link java.util.Locale}.
     *
     * @param locale desired {@link java.util.Locale}
     * @return a formatter object
     * @stable ICU 54
     */
    public static MeasureFormat getCurrencyFormat(Locale locale) {
        return getCurrencyFormat(ULocale.forLocale(locale));
    }

    /**
     * Return a formatter for CurrencyAmount objects in the default <code>FORMAT</code> locale.
     *
     * @return a formatter object
     * @see Category#FORMAT
     * @stable ICU 3.0
     */
    public static MeasureFormat getCurrencyFormat() {
        return getCurrencyFormat(ULocale.getDefault(Category.FORMAT));
    }

    // This method changes the NumberFormat object as well to match the new locale.
    MeasureFormat withLocale(ULocale locale) {
        return MeasureFormat.getInstance(locale, getWidth());
    }

    MeasureFormat withNumberFormat(NumberFormat format) {
        return new MeasureFormat(
                getLocale(), this.formatWidth, format, this.rules, this.numericFormatters);
    }

    MeasureFormat(ULocale locale, FormatWidth formatWidth) {
        this(locale, formatWidth, null, null, null);
    }

    private MeasureFormat(
            ULocale locale,
            FormatWidth formatWidth,
            NumberFormat numberFormat,
            PluralRules rules,
            NumericFormatters formatters) {
        // Needed for getLocale(ULocale.VALID_LOCALE).
        setLocale(locale, locale);
        this.formatWidth = formatWidth;

        if (rules == null) {
            rules = PluralRules.forLocale(locale);
        }
        this.rules = rules;

        if (numberFormat == null) {
            numberFormat = NumberFormat.getInstance(locale);
        } else {
            numberFormat = numberFormat.clone();
        }
        this.numberFormat = numberFormat;

        if (formatters == null && formatWidth == FormatWidth.NUMERIC) {
            formatters = localeToNumericDurationFormatters.get(locale);
            if (formatters == null) {
                formatters = loadNumericFormatters(locale);
                localeToNumericDurationFormatters.put(locale, formatters);
            }
        }
        this.numericFormatters = formatters;

        if (!(numberFormat instanceof DecimalFormat)) {
            throw new IllegalArgumentException();
        }
        numberFormatter =
                ((DecimalFormat) numberFormat).toNumberFormatter().unitWidth(formatWidth.unitWidth);
    }

    MeasureFormat(
            ULocale locale, FormatWidth formatWidth, NumberFormat numberFormat, PluralRules rules) {
        this(locale, formatWidth, numberFormat, rules, null);
        if (formatWidth == FormatWidth.NUMERIC) {
            throw new IllegalArgumentException(
                    "The format width 'numeric' is not allowed by this constructor");
        }
    }

    static class NumericFormatters {
        private String hourMinute;
        private String minuteSecond;
        private String hourMinuteSecond;

        public NumericFormatters(String hourMinute, String minuteSecond, String hourMinuteSecond) {
            this.hourMinute = hourMinute;
            this.minuteSecond = minuteSecond;
            this.hourMinuteSecond = hourMinuteSecond;
        }

        public String getHourMinute() {
            return hourMinute;
        }

        public String getMinuteSecond() {
            return minuteSecond;
        }

        public String getHourMinuteSecond() {
            return hourMinuteSecond;
        }
    }

    private static NumericFormatters loadNumericFormatters(ULocale locale) {
        ICUResourceBundle r =
                (ICUResourceBundle)
                        UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
        return new NumericFormatters(
                loadNumericDurationFormat(r, "hm"),
                loadNumericDurationFormat(r, "ms"),
                loadNumericDurationFormat(r, "hms"));
    }

    /// BEGIN NUMBER FORMATTER CACHING MACHINERY ///

    static final int NUMBER_FORMATTER_STANDARD = 1;
    static final int NUMBER_FORMATTER_CURRENCY = 2;
    static final int NUMBER_FORMATTER_INTEGER = 3;

    static class NumberFormatterCacheEntry {
        int type;
        MeasureUnit unit;
        MeasureUnit perUnit;
        LocalizedNumberFormatter formatter;
    }

    // formatter1 is most recently used.
    private transient NumberFormatterCacheEntry formatter1 = null;
    private transient NumberFormatterCacheEntry formatter2 = null;
    private transient NumberFormatterCacheEntry formatter3 = null;

    private synchronized LocalizedNumberFormatter getUnitFormatterFromCache(
            int type, MeasureUnit unit, MeasureUnit perUnit) {
        if (formatter1 != null) {
            if (formatter1.type == type
                    && formatter1.unit == unit
                    && formatter1.perUnit == perUnit) {
                return formatter1.formatter;
            }
            if (formatter2 != null) {
                if (formatter2.type == type
                        && formatter2.unit == unit
                        && formatter2.perUnit == perUnit) {
                    return formatter2.formatter;
                }
                if (formatter3 != null) {
                    if (formatter3.type == type
                            && formatter3.unit == unit
                            && formatter3.perUnit == perUnit) {
                        return formatter3.formatter;
                    }
                }
            }
        }

        // No hit; create a new formatter.
        LocalizedNumberFormatter formatter;
        if (type == NUMBER_FORMATTER_STANDARD) {
            formatter =
                    getNumberFormatter()
                            .unit(unit)
                            .perUnit(perUnit)
                            .unitWidth(formatWidth.unitWidth);
        } else if (type == NUMBER_FORMATTER_CURRENCY) {
            formatter =
                    NumberFormatter.withLocale(getLocale())
                            .unit(unit)
                            .perUnit(perUnit)
                            .unitWidth(formatWidth.currencyWidth);
        } else {
            assert type == NUMBER_FORMATTER_INTEGER;
            formatter =
                    getNumberFormatter()
                            .unit(unit)
                            .perUnit(perUnit)
                            .unitWidth(formatWidth.unitWidth)
                            .precision(
                                    Precision.integer()
                                            .withMode(
                                                    RoundingUtils.mathContextUnlimited(
                                                            RoundingMode.DOWN)));
        }
        formatter3 = formatter2;
        formatter2 = formatter1;
        formatter1 = new NumberFormatterCacheEntry();
        formatter1.type = type;
        formatter1.unit = unit;
        formatter1.perUnit = perUnit;
        formatter1.formatter = formatter;
        return formatter;
    }

    synchronized void clearCache() {
        formatter1 = null;
        formatter2 = null;
        formatter3 = null;
    }

    // Can be overridden by subclasses:
    LocalizedNumberFormatter getNumberFormatter() {
        return numberFormatter;
    }

    /// END NUMBER FORMATTER CACHING MACHINERY ///

    private FormattedStringBuilder formatMeasure(Measure measure) {
        MeasureUnit unit = measure.getUnit();
        DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(measure.getNumber());
        FormattedStringBuilder string = new FormattedStringBuilder();
        if (unit instanceof Currency) {
            getUnitFormatterFromCache(NUMBER_FORMATTER_CURRENCY, unit, null).formatImpl(dq, string);
        } else {
            getUnitFormatterFromCache(NUMBER_FORMATTER_STANDARD, unit, null).formatImpl(dq, string);
        }
        return string;
    }

    private FormattedStringBuilder formatMeasureInteger(Measure measure) {
        DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(measure.getNumber());
        FormattedStringBuilder string = new FormattedStringBuilder();
        getUnitFormatterFromCache(NUMBER_FORMATTER_INTEGER, measure.getUnit(), null)
                .formatImpl(dq, string);
        return string;
    }

    private void formatMeasuresSlowTrack(
            ListFormatter listFormatter,
            Appendable appendTo,
            FieldPosition fieldPosition,
            Measure... measures) {
        String[] results = new String[measures.length];

        // Zero out our field position so that we can tell when we find our field.
        FieldPosition fpos =
                new FieldPosition(fieldPosition.getFieldAttribute(), fieldPosition.getField());

        int fieldPositionFoundIndex = -1;
        for (int i = 0; i < measures.length; ++i) {
            FormattedStringBuilder result;
            if (i == measures.length - 1) {
                result = formatMeasure(measures[i]);
            } else {
                result = formatMeasureInteger(measures[i]);
            }
            if (fieldPositionFoundIndex == -1) {
                FormattedValueStringBuilderImpl.nextFieldPosition(result, fpos);
                if (fpos.getEndIndex() != 0) {
                    fieldPositionFoundIndex = i;
                }
            }
            results[i] = result.toString();
        }
        ListFormatter.FormattedListBuilder builder =
                listFormatter.formatImpl(Arrays.asList(results), true);

        // Fix up FieldPosition indexes if our field is found.
        int offset = builder.getOffset(fieldPositionFoundIndex);
        if (offset != -1) {
            fieldPosition.setBeginIndex(fpos.getBeginIndex() + offset);
            fieldPosition.setEndIndex(fpos.getEndIndex() + offset);
        }
        builder.appendTo(appendTo);
    }

    // type is one of "hm", "ms" or "hms"
    private static String loadNumericDurationFormat(ICUResourceBundle r, String type) {
        r = r.getWithFallback(String.format("durationUnits/%s", type));
        // We replace 'h' with 'H' because 'h' does not make sense in the context of durations.
        return r.getString().replace("h", "H");
    }

    // Returns hours in [0]; minutes in [1]; seconds in [2] out of measures array. If
    // unsuccessful, e.g measures has other measurements besides hours, minutes, seconds;
    // hours, minutes, seconds are out of order; or have negative values, returns null.
    // If hours, minutes, or seconds is missing from measures the corresponding element in
    // returned array will be null.
    private static Number[] toHMS(Measure[] measures) {
        Number[] result = new Number[3];
        int lastIdx = -1;
        for (Measure m : measures) {
            if (m.getNumber().doubleValue() < 0.0) {
                return null;
            }
            Integer idxObj = hmsTo012.get(m.getUnit());
            if (idxObj == null) {
                return null;
            }
            int idx = idxObj.intValue();
            if (idx <= lastIdx) {
                // hour before minute before second
                return null;
            }
            lastIdx = idx;
            result[idx] = m.getNumber();
        }
        return result;
    }

    // Formats numeric time duration as 5:00:47 or 3:54. In the process, it replaces any null
    // values in hms with 0.
    private void formatNumeric(Number[] hms, Appendable appendable) {
        String pattern;

        // All possible combinations: "h", "m", "s", "hm", "hs", "ms", "hms"
        if (hms[0] != null && hms[2] != null) { // "hms" & "hs" (we add minutes if "hs")
            pattern = numericFormatters.getHourMinuteSecond();
            if (hms[1] == null) hms[1] = 0;
            hms[1] = Math.floor(hms[1].doubleValue());
            hms[0] = Math.floor(hms[0].doubleValue());
        } else if (hms[0] != null && hms[1] != null) { // "hm"
            pattern = numericFormatters.getHourMinute();
            hms[0] = Math.floor(hms[0].doubleValue());
        } else if (hms[1] != null && hms[2] != null) { // "ms"
            pattern = numericFormatters.getMinuteSecond();
            hms[1] = Math.floor(hms[1].doubleValue());
        } else { // h m s, handled outside formatNumeric. No value is also an error.
            throw new IllegalStateException();
        }

        // We can create it on demand, but all of the patterns (right now) have mm and ss.
        // So unless it is hours only we will need a 0-padded 2 digits formatter.
        LocalizedNumberFormatter numberFormatter2 =
                numberFormatter.integerWidth(IntegerWidth.zeroFillTo(2));
        FormattedStringBuilder fsb = new FormattedStringBuilder();

        boolean protect = false;
        for (int i = 0; i < pattern.length(); i++) {
            char c = pattern.charAt(i);

            // Also set the proper field in this switch
            // We don't use DateFormat.Field because this is not a date / time, is a duration.
            Number value = 0;
            switch (c) {
                case 'H':
                    value = hms[0];
                    break;
                case 'm':
                    value = hms[1];
                    break;
                case 's':
                    value = hms[2];
                    break;
            }

            // There is not enough info to add Field(s) for the unit because all we have are plain
            // text patterns. For example in "21:51" there is no text for something like "hour",
            // while in something like "21h51" there is ("h"). But we can't really tell...
            switch (c) {
                case 'H':
                case 'm':
                case 's':
                    if (protect) {
                        fsb.appendChar16(c, null);
                    } else {
                        if ((i + 1 < pattern.length()) && pattern.charAt(i + 1) == c) { // doubled
                            fsb.append(
                                    numberFormatter2.format(value), null); // TODO: Use proper Field
                            i++;
                        } else {
                            fsb.append(
                                    numberFormatter.format(value), null); // TODO: Use proper Field
                        }
                    }
                    break;
                case '\'':
                    // '' is escaped apostrophe
                    if ((i + 1 < pattern.length()) && pattern.charAt(i + 1) == c) {
                        fsb.appendChar16(c, null);
                        i++;
                    } else {
                        protect = !protect;
                    }
                    break;
                default:
                    fsb.appendChar16(c, null);
            }
        }

        try {
            appendable.append(fsb);
        } catch (IOException e) {
            throw new ICUUncheckedIOException(e);
        }
    }

    Object toTimeUnitProxy() {
        return new MeasureProxy(
                getLocale(), formatWidth, getNumberFormatInternal(), TIME_UNIT_FORMAT);
    }

    Object toCurrencyProxy() {
        return new MeasureProxy(
                getLocale(), formatWidth, getNumberFormatInternal(), CURRENCY_FORMAT);
    }

    private Object writeReplace() throws ObjectStreamException {
        return new MeasureProxy(
                getLocale(), formatWidth, getNumberFormatInternal(), MEASURE_FORMAT);
    }

    static class MeasureProxy implements Externalizable {
        private static final long serialVersionUID = -6033308329886716770L;

        private ULocale locale;
        private FormatWidth formatWidth;
        private NumberFormat numberFormat;
        private int subClass;
        private HashMap<Object, Object> keyValues;

        public MeasureProxy(
                ULocale locale, FormatWidth width, NumberFormat numberFormat, int subClass) {
            this.locale = locale;
            this.formatWidth = width;
            this.numberFormat = numberFormat;
            this.subClass = subClass;
            this.keyValues = new HashMap<>();
        }

        // Must have public constructor, to enable Externalizable
        public MeasureProxy() {}

        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeByte(0); // version
            out.writeUTF(locale.toLanguageTag());
            out.writeByte(formatWidth.ordinal());
            out.writeObject(numberFormat);
            out.writeByte(subClass);
            out.writeObject(keyValues);
        }

        @Override
        @SuppressWarnings("unchecked")
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            in.readByte(); // version.
            locale = ULocale.forLanguageTag(in.readUTF());
            formatWidth = fromFormatWidthOrdinal(in.readByte() & 0xFF);
            numberFormat = (NumberFormat) in.readObject();
            if (numberFormat == null) {
                throw new InvalidObjectException("Missing number format.");
            }
            subClass = in.readByte() & 0xFF;

            // This cast is safe because the serialized form of hashtable can have
            // any object as the key and any object as the value.
            keyValues = (HashMap<Object, Object>) in.readObject();
            if (keyValues == null) {
                throw new InvalidObjectException("Missing optional values map.");
            }
        }

        private TimeUnitFormat createTimeUnitFormat() throws InvalidObjectException {
            int style;
            if (formatWidth == FormatWidth.WIDE) {
                style = TimeUnitFormat.FULL_NAME;
            } else if (formatWidth == FormatWidth.SHORT) {
                style = TimeUnitFormat.ABBREVIATED_NAME;
            } else {
                throw new InvalidObjectException("Bad width: " + formatWidth);
            }
            TimeUnitFormat result = new TimeUnitFormat(locale, style);
            result.setNumberFormat(numberFormat);
            return result;
        }

        private Object readResolve() throws ObjectStreamException {
            switch (subClass) {
                case MEASURE_FORMAT:
                    return MeasureFormat.getInstance(locale, formatWidth, numberFormat);
                case TIME_UNIT_FORMAT:
                    return createTimeUnitFormat();
                case CURRENCY_FORMAT:
                    return MeasureFormat.getCurrencyFormat(locale);
                default:
                    throw new InvalidObjectException("Unknown subclass: " + subClass);
            }
        }
    }

    private static FormatWidth fromFormatWidthOrdinal(int ordinal) {
        FormatWidth[] values = FormatWidth.values();
        if (ordinal < 0 || ordinal >= values.length) {
            return FormatWidth.SHORT;
        }
        return values[ordinal];
    }

    private static final Map<ULocale, String> localeIdToRangeFormat = new ConcurrentHashMap<>();

    /**
     * Return a formatter (compiled SimpleFormatter pattern) for a range, such as "{0}–{1}".
     *
     * @param forLocale locale to get the format for
     * @param width the format width
     * @return range formatter, such as "{0}–{1}"
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static String getRangeFormat(ULocale forLocale, FormatWidth width) {
        // TODO fix Hack for French
        if (forLocale.getLanguage().equals("fr")) {
            return getRangeFormat(ULocale.ROOT, width);
        }
        String result = localeIdToRangeFormat.get(forLocale);
        if (result == null) {
            ICUResourceBundle rb =
                    (ICUResourceBundle)
                            UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, forLocale);
            ULocale realLocale = rb.getULocale();
            if (!forLocale.equals(
                    realLocale)) { // if the child would inherit, then add a cache entry
                // for it.
                result = localeIdToRangeFormat.get(forLocale);
                if (result != null) {
                    localeIdToRangeFormat.put(forLocale, result);
                    return result;
                }
            }
            // At this point, both the forLocale and the realLocale don't have an item
            // So we have to make one.
            NumberingSystem ns = NumberingSystem.getInstance(forLocale);

            String resultString = null;
            try {
                resultString =
                        rb.getStringWithFallback(
                                "NumberElements/" + ns.getName() + "/miscPatterns/range");
            } catch (MissingResourceException ex) {
                resultString = rb.getStringWithFallback("NumberElements/latn/patterns/range");
            }
            result =
                    SimpleFormatterImpl.compileToStringMinMaxArguments(
                            resultString, new StringBuilder(), 2, 2);
            localeIdToRangeFormat.put(forLocale, result);
            if (!forLocale.equals(realLocale)) {
                localeIdToRangeFormat.put(realLocale, result);
            }
        }
        return result;
    }
}