blob: b9f072a3a18bcfdc0371398bf5d8ce33a0072fb7 [file] [log] [blame]
// © 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, 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.
*
* 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.
*
* 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)
* @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(),
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) 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;
}
}