| // © 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 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; |
| |
| 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; |
| |
| // 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. |
| * <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. |
| * |
| * 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.Style.UNIT, UnitWidth.FULL_NAME, UnitWidth.FULL_NAME), |
| |
| /** |
| * Abbreviate when possible. |
| * |
| * @stable ICU 53 |
| */ |
| SHORT(ListFormatter.Style.UNIT_SHORT, UnitWidth.SHORT, UnitWidth.ISO_CODE), |
| |
| /** |
| * Brief. Use only a symbol for the unit when possible. |
| * |
| * @stable ICU 53 |
| */ |
| NARROW(ListFormatter.Style.UNIT_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.Style.UNIT_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.Style.UNIT, UnitWidth.FULL_NAME, UnitWidth.SHORT); |
| |
| private final ListFormatter.Style listFormatterStyle; |
| |
| /** |
| * 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.Style style, UnitWidth unitWidth, UnitWidth currencyWidth) { |
| this.listFormatterStyle = style; |
| this.unitWidth = unitWidth; |
| this.currencyWidth = currencyWidth; |
| } |
| |
| ListFormatter.Style getListFormatterStyle() { |
| return listFormatterStyle; |
| } |
| } |
| |
| /** |
| * 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<? extends Measure>, 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. |
| * |
| * Calling a <code>formatMeasures</code> method is preferred over calling this method as they give |
| * better performance. |
| * |
| * @param obj |
| * must be a Collection<? extends Measure>, 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) |
| * @provisional This API might change or be removed in a future release. |
| */ |
| @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 http://bugs.icu-project.org/trac/ticket/12454 |
| |
| /** |
| * Formats a single measure per unit. |
| * |
| * 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. |
| * |
| * 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(), |
| formatWidth.getListFormatterStyle()); |
| 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) 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) 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; |
| } |
| } |