/* ********************************************************************** * Copyright (c) 2004-2015, 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.text.FieldPosition; import java.text.ParsePosition; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.EnumMap; 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.ICUData; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.SimpleCache; import com.ibm.icu.impl.SimplePatternFormatter; import com.ibm.icu.math.BigDecimal; import com.ibm.icu.text.PluralRules.Factory; import com.ibm.icu.text.PluralRules.StandardPluralCategories; import com.ibm.icu.util.Currency; import com.ibm.icu.util.CurrencyAmount; import com.ibm.icu.util.Measure; import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.TimeZone; 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. * *

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: *

 *      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));
 * 
*

* This class does not do conversions from one unit to another. It simply formats * whatever units it is given *

* 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 ImmutableNumberFormat numberFormat; private final transient FormatWidth formatWidth; // PluralRules is documented as being immutable which implies thread-safety. private final transient PluralRules rules; // Measure unit -> format width -> plural form -> pattern ("{0} meters") private final transient Map> unitToStyleToCountToFormat; private final transient NumericFormatters numericFormatters; private final transient ImmutableNumberFormat currencyFormat; private final transient ImmutableNumberFormat integerFormat; private final transient Map> unitToStyleToPerUnitPattern; private final transient EnumMap styleToPerPattern; private static final SimpleCache localeMeasureFormatData = new SimpleCache(); private static final SimpleCache localeToNumericDurationFormatters = new SimpleCache(); private static final Map 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("units", ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE), /** * Abbreviate when possible. * * @stable ICU 53 */ SHORT("unitsShort", ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE), /** * Brief. Use only a symbol for the unit when possible. * * @stable ICU 53 */ NARROW("unitsNarrow", ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE), /** * 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("unitsNarrow", ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE); // Be sure to update the toFormatWidth and fromFormatWidth() functions // when adding an enum value. final String resourceKey; private final ListFormatter.Style listFormatterStyle; private final int currencyStyle; private FormatWidth(String resourceKey, ListFormatter.Style style, int currencyStyle) { this.resourceKey = resourceKey; this.listFormatterStyle = style; this.currencyStyle = currencyStyle; } ListFormatter.Style getListFormatterStyle() { return listFormatterStyle; } int getCurrencyStyle() { return currencyStyle; } } /** * 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 JDK locale, formatWidth, and format. * * @param locale the JDK locale. * @param formatWidth hints how long formatted strings should be. * @return The new MeasureFormat object. * @draft ICU 54 * @provisional This API might change or be removed in a future release. */ 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) { PluralRules rules = PluralRules.forLocale(locale); NumericFormatters formatters = null; MeasureFormatData data = localeMeasureFormatData.get(locale); if (data == null) { data = loadLocaleData(locale); localeMeasureFormatData.put(locale, data); } if (formatWidth == FormatWidth.NUMERIC) { formatters = localeToNumericDurationFormatters.get(locale); if (formatters == null) { formatters = loadNumericFormatters(locale); localeToNumericDurationFormatters.put(locale, formatters); } } NumberFormat intFormat = NumberFormat.getInstance(locale); intFormat.setMaximumFractionDigits(0); intFormat.setMinimumFractionDigits(0); intFormat.setRoundingMode(BigDecimal.ROUND_DOWN); return new MeasureFormat( locale, formatWidth, new ImmutableNumberFormat(format), rules, data.unitToStyleToCountToFormat, formatters, new ImmutableNumberFormat(NumberFormat.getInstance(locale, formatWidth.getCurrencyStyle())), new ImmutableNumberFormat(intFormat), data.unitToStyleToPerUnitPattern, data.styleToPerPattern); } /** * Create a format from the JDK locale, formatWidth, and format. * * @param locale the JDK locale. * @param formatWidth hints how long formatted strings should be. * @param format This is defensively copied. * @return The new MeasureFormat object. * @draft ICU 54 * @provisional This API might change or be removed in a future release. */ 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 * formatMeasures method is preferred over calling * this method as they give better performance. * * @param obj must be a Collection, Measure[], or Measure object. * @param toAppendTo Formatted string appended here. * @param pos 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 pos) { int prevLength = toAppendTo.length(); FieldPosition fpos = new FieldPosition(pos.getFieldAttribute(), pos.getField()); 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; } toAppendTo.append(formatMeasures(new StringBuilder(), fpos, measures)); } else if (obj instanceof Measure[]) { toAppendTo.append(formatMeasures(new StringBuilder(), fpos, (Measure[]) obj)); } else if (obj instanceof Measure){ toAppendTo.append(formatMeasure((Measure) obj, numberFormat, new StringBuilder(), fpos)); } else { throw new IllegalArgumentException(obj.toString()); } if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) { pos.setBeginIndex(fpos.getBeginIndex() + prevLength); pos.setEndIndex(fpos.getEndIndex() + prevLength); } return toAppendTo; } /** * Parses text from a string to produce a Measure. * @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(); } /** * Format a range of measures, such as "3.4-5.1 meters". It is the caller’s * responsibility to have the appropriate values in appropriate order, * and using the appropriate Number values. *
Note: If the format doesn’t have enough decimals, or lowValue ≥ highValue, * the result will be a degenerate range, like “5-5 meters”. *
Currency Units are not yet supported. * * @param lowValue low value in range * @param highValue high value in range * @return the formatted string. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final String formatMeasureRange(Measure lowValue, Measure highValue) { MeasureUnit unit = lowValue.getUnit(); if (!unit.equals(highValue.getUnit())) { throw new IllegalArgumentException("Units must match: " + unit + " ≠ " + highValue.getUnit()); } Number lowNumber = lowValue.getNumber(); Number highNumber = highValue.getNumber(); final boolean isCurrency = unit instanceof Currency; UFieldPosition lowFpos = new UFieldPosition(); UFieldPosition highFpos = new UFieldPosition(); StringBuffer lowFormatted = null; StringBuffer highFormatted = null; if (isCurrency) { Currency currency = (Currency) unit; int fracDigits = currency.getDefaultFractionDigits(); int maxFrac = numberFormat.nf.getMaximumFractionDigits(); int minFrac = numberFormat.nf.getMinimumFractionDigits(); if (fracDigits != maxFrac || fracDigits != minFrac) { DecimalFormat currentNumberFormat = (DecimalFormat) numberFormat.get(); currentNumberFormat.setMaximumFractionDigits(fracDigits); currentNumberFormat.setMinimumFractionDigits(fracDigits); lowFormatted = currentNumberFormat.format(lowNumber, new StringBuffer(), lowFpos); highFormatted = currentNumberFormat.format(highNumber, new StringBuffer(), highFpos); } } if (lowFormatted == null) { lowFormatted = numberFormat.format(lowNumber, new StringBuffer(), lowFpos); highFormatted = numberFormat.format(highNumber, new StringBuffer(), highFpos); } final double lowDouble = lowNumber.doubleValue(); String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble, lowFpos.getCountVisibleFractionDigits(), lowFpos.getFractionDigits())); final double highDouble = highNumber.doubleValue(); String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble, highFpos.getCountVisibleFractionDigits(), highFpos.getFractionDigits())); final PluralRanges pluralRanges = Factory.getDefaultFactory().getPluralRanges(getLocale()); StandardPluralCategories resolvedCategory = pluralRanges.get( StandardPluralCategories.valueOf(keywordLow), StandardPluralCategories.valueOf(keywordHigh)); SimplePatternFormatter rangeFormatter = getRangeFormat(getLocale(), formatWidth); String formattedNumber = rangeFormatter.format(lowFormatted, highFormatted); if (isCurrency) { // Nasty hack currencyFormat.format(1d); // have to call this for the side effect Currency currencyUnit = (Currency) unit; StringBuilder result = new StringBuilder(); appendReplacingCurrency(currencyFormat.getPrefix(lowDouble >= 0), currencyUnit, resolvedCategory, result); result.append(formattedNumber); appendReplacingCurrency(currencyFormat.getSuffix(highDouble >= 0), currencyUnit, resolvedCategory, result); return result.toString(); // StringBuffer buffer = new StringBuffer(); // CurrencyAmount currencyLow = (CurrencyAmount) lowValue; // CurrencyAmount currencyHigh = (CurrencyAmount) highValue; // FieldPosition pos = new FieldPosition(NumberFormat.INTEGER_FIELD); // currencyFormat.format(currencyLow, buffer, pos); // int startOfInteger = pos.getBeginIndex(); // StringBuffer buffer2 = new StringBuffer(); // FieldPosition pos2 = new FieldPosition(0); // currencyFormat.format(currencyHigh, buffer2, pos2); } else { Map styleToCountToFormat = unitToStyleToCountToFormat.get(lowValue.getUnit()); QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth); SimplePatternFormatter formatter = countToFormat.getByVariant(resolvedCategory.toString()); return formatter.format(formattedNumber); } } private void appendReplacingCurrency(String affix, Currency unit, StandardPluralCategories resolvedCategory, StringBuilder result) { String replacement = "¤"; int pos = affix.indexOf(replacement); if (pos < 0) { replacement = "XXX"; pos = affix.indexOf(replacement); } if (pos < 0) { result.append(affix); } else { // for now, just assume single result.append(affix.substring(0,pos)); // we have a mismatch between the number style and the currency style, so remap int currentStyle = formatWidth.getCurrencyStyle(); if (currentStyle == NumberFormat.ISOCURRENCYSTYLE) { result.append(unit.getCurrencyCode()); } else { result.append(unit.getName(currencyFormat.nf.getLocale(ULocale.ACTUAL_LOCALE), currentStyle == NumberFormat.CURRENCYSTYLE ? Currency.SYMBOL_NAME : Currency.PLURAL_LONG_NAME, resolvedCategory.toString(), null)); } result.append(affix.substring(pos+replacement.length())); } } /** * 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. * @draft ICU 55 * @provisional This API might change or be removed in a future release. */ public StringBuilder formatMeasurePerUnit( Measure measure, MeasureUnit perUnit, StringBuilder appendTo, FieldPosition pos) { MeasureUnit resolvedUnit = MeasureUnit.resolveUnitPerUnit( measure.getUnit(), perUnit); if (resolvedUnit != null) { Measure newMeasure = new Measure(measure.getNumber(), resolvedUnit); return formatMeasure(newMeasure, numberFormat, appendTo, pos); } FieldPosition fpos = new FieldPosition( pos.getFieldAttribute(), pos.getField()); int offset = withPerUnitAndAppend( formatMeasure(measure, numberFormat, new StringBuilder(), fpos), perUnit, appendTo); if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) { pos.setBeginIndex(fpos.getBeginIndex() + offset); pos.setEndIndex(fpos.getEndIndex() + offset); } 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 fieldPosition 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 fieldPosition, Measure... measures) { // fast track for trivial cases if (measures.length == 0) { return appendTo; } if (measures.length == 1) { return formatMeasure(measures[0], numberFormat, appendTo, fieldPosition); } if (formatWidth == FormatWidth.NUMERIC) { // If we have just hour, minute, or second follow the numeric // track. Number[] hms = toHMS(measures); if (hms != null) { return formatNumeric(hms, appendTo); } } ListFormatter listFormatter = ListFormatter.getInstance( getLocale(), formatWidth.getListFormatterStyle()); if (fieldPosition != DontCareFieldPosition.INSTANCE) { return formatMeasuresSlowTrack(listFormatter, appendTo, fieldPosition, measures); } // Fast track: No field position. String[] results = new String[measures.length]; for (int i = 0; i < measures.length; i++) { results[i] = formatMeasure( measures[i], i == measures.length - 1 ? numberFormat : integerFormat); } return appendTo.append(listFormatter.format((Object[]) results)); } /** * Two MeasureFormats, a and b, are equal if and only if they have the same formatWidth, * locale, and equal number formats. * @stable ICU 53 */ @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()) && getNumberFormat().equals(rhs.getNumberFormat()); } /** * {@inheritDoc} * @stable ICU 53 */ @Override public final int hashCode() { // A very slow but safe implementation. return (getLocale().hashCode() * 31 + getNumberFormat().hashCode()) * 31 + getWidth().hashCode(); } /** * Get the format width this instance is using. * @stable ICU 53 */ public MeasureFormat.FormatWidth getWidth() { 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.get(); } /** * 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 * JDK locale. * @param locale desired JDK locale * @return a formatter object * @draft ICU 54 * @provisional This API might change or be removed in a future release. */ public static MeasureFormat getCurrencyFormat(Locale locale) { return getCurrencyFormat(ULocale.forLocale(locale)); } /** * Return a formatter for CurrencyAmount objects in the default * FORMAT 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, new ImmutableNumberFormat(format), this.rules, this.unitToStyleToCountToFormat, this.numericFormatters, this.currencyFormat, this.integerFormat, this.unitToStyleToPerUnitPattern, this.styleToPerPattern); } private MeasureFormat( ULocale locale, FormatWidth formatWidth, ImmutableNumberFormat format, PluralRules rules, Map> unitToStyleToCountToFormat, NumericFormatters formatters, ImmutableNumberFormat currencyFormat, ImmutableNumberFormat integerFormat, Map> unitToStyleToPerUnitPattern, EnumMap styleToPerPattern) { setLocale(locale, locale); this.formatWidth = formatWidth; this.numberFormat = format; this.rules = rules; this.unitToStyleToCountToFormat = unitToStyleToCountToFormat; this.numericFormatters = formatters; this.currencyFormat = currencyFormat; this.integerFormat = integerFormat; this.unitToStyleToPerUnitPattern = unitToStyleToPerUnitPattern; this.styleToPerPattern = styleToPerPattern; } MeasureFormat() { // Make compiler happy by setting final fields to null. this.formatWidth = null; this.numberFormat = null; this.rules = null; this.unitToStyleToCountToFormat = null; this.numericFormatters = null; this.currencyFormat = null; this.integerFormat = null; this.unitToStyleToPerUnitPattern = null; this.styleToPerPattern = null; } static class NumericFormatters { private DateFormat hourMinute; private DateFormat minuteSecond; private DateFormat hourMinuteSecond; public NumericFormatters( DateFormat hourMinute, DateFormat minuteSecond, DateFormat hourMinuteSecond) { this.hourMinute = hourMinute; this.minuteSecond = minuteSecond; this.hourMinuteSecond = hourMinuteSecond; } public DateFormat getHourMinute() { return hourMinute; } public DateFormat getMinuteSecond() { return minuteSecond; } public DateFormat 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")); } /** * Returns formatting data for all MeasureUnits except for currency ones. */ private static MeasureFormatData loadLocaleData( ULocale locale) { QuantityFormatter.Builder builder = new QuantityFormatter.Builder(); Map> unitToStyleToCountToFormat = new HashMap>(); Map> unitToStyleToPerUnitPattern = new HashMap>(); ICUResourceBundle resource = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale); EnumMap styleToPerPattern = new EnumMap(FormatWidth.class); for (FormatWidth styleItem : FormatWidth.values()) { try { ICUResourceBundle unitTypeRes = resource.getWithFallback(styleItem.resourceKey); ICUResourceBundle compoundRes = unitTypeRes.getWithFallback("compound"); ICUResourceBundle perRes = compoundRes.getWithFallback("per"); styleToPerPattern.put(styleItem, SimplePatternFormatter.compile(perRes.getString())); } catch (MissingResourceException e) { // may not have compound/per for every width. continue; } } fillInStyleMap(styleToPerPattern); for (MeasureUnit unit : MeasureUnit.getAvailable()) { // Currency data cannot be found here. Skip. if (unit instanceof Currency) { continue; } EnumMap styleToCountToFormat = unitToStyleToCountToFormat.get(unit); if (styleToCountToFormat == null) { unitToStyleToCountToFormat.put(unit, styleToCountToFormat = new EnumMap(FormatWidth.class)); } EnumMap styleToPerUnitPattern = new EnumMap(FormatWidth.class); unitToStyleToPerUnitPattern.put(unit, styleToPerUnitPattern); for (FormatWidth styleItem : FormatWidth.values()) { try { ICUResourceBundle unitTypeRes = resource.getWithFallback(styleItem.resourceKey); ICUResourceBundle unitsRes = unitTypeRes.getWithFallback(unit.getType()); ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(unit.getSubtype()); builder.reset(); boolean havePluralItem = false; int len = oneUnitRes.getSize(); for (int i = 0; i < len; i++) { UResourceBundle countBundle; try { countBundle = oneUnitRes.get(i); } catch (MissingResourceException e) { continue; } String resKey = countBundle.getKey(); if (resKey.equals("dnam")) { continue; // skip display name & per pattern (new in CLDR 26 / ICU 54) for now, not part of plurals } if (resKey.equals("per")) { styleToPerUnitPattern.put( styleItem, SimplePatternFormatter.compile(countBundle.getString())); continue; } havePluralItem = true; builder.add(resKey, countBundle.getString()); } if (havePluralItem) { // might not have any plural items if countBundle only has "dnam" display name, for instance, // as with fr unitsNarrow/light/lux in CLDR 26 styleToCountToFormat.put(styleItem, builder.build()); } } catch (MissingResourceException e) { continue; } } // TODO: if no fallback available, get from root. fillInStyleMap(styleToCountToFormat); fillInStyleMap(styleToPerUnitPattern); } return new MeasureFormatData(unitToStyleToCountToFormat, unitToStyleToPerUnitPattern, styleToPerPattern); } private static boolean fillInStyleMap(Map styleMap) { if (styleMap.size() == FormatWidth.values().length) { return true; } T fallback = styleMap.get(FormatWidth.SHORT); if (fallback == null) { fallback = styleMap.get(FormatWidth.WIDE); } if (fallback == null) { return false; } for (FormatWidth styleItem : FormatWidth.values()) { T item = styleMap.get(styleItem); if (item == null) { styleMap.put(styleItem, fallback); } } return true; } private int withPerUnitAndAppend( CharSequence formatted, MeasureUnit perUnit, StringBuilder appendTo) { int[] offsets = new int[1]; Map styleToPerUnitPattern = unitToStyleToPerUnitPattern.get(perUnit); SimplePatternFormatter perUnitPattern = styleToPerUnitPattern.get(formatWidth); if (perUnitPattern != null) { perUnitPattern.formatAndAppend(appendTo, offsets, formatted); return offsets[0]; } SimplePatternFormatter perPattern = styleToPerPattern.get(formatWidth); Map styleToCountToFormat = unitToStyleToCountToFormat.get(perUnit); QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth); String perUnitString = countToFormat.getByVariant("one").getPatternWithNoPlaceholders().trim(); perPattern.formatAndAppend(appendTo, offsets, formatted, perUnitString); return offsets[0]; } private String formatMeasure(Measure measure, ImmutableNumberFormat nf) { return formatMeasure( measure, nf, new StringBuilder(), DontCareFieldPosition.INSTANCE).toString(); } private StringBuilder formatMeasure( Measure measure, ImmutableNumberFormat nf, StringBuilder appendTo, FieldPosition fieldPosition) { if (measure.getUnit() instanceof Currency) { return appendTo.append( currencyFormat.format( new CurrencyAmount(measure.getNumber(), (Currency) measure.getUnit()), new StringBuffer(), fieldPosition)); } Number n = measure.getNumber(); MeasureUnit unit = measure.getUnit(); UFieldPosition fpos = new UFieldPosition(fieldPosition.getFieldAttribute(), fieldPosition.getField()); StringBuffer formattedNumber = nf.format(n, new StringBuffer(), fpos); String keyword = rules.select(new PluralRules.FixedDecimal(n.doubleValue(), fpos.getCountVisibleFractionDigits(), fpos.getFractionDigits())); Map styleToCountToFormat = unitToStyleToCountToFormat.get(unit); QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth); SimplePatternFormatter formatter = countToFormat.getByVariant(keyword); int[] offsets = new int[1]; formatter.formatAndAppend(appendTo, offsets, formattedNumber); if (offsets[0] != -1) { // there is a number (may not happen with, say, Arabic dual) // Fix field position if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) { fieldPosition.setBeginIndex(fpos.getBeginIndex() + offsets[0]); fieldPosition.setEndIndex(fpos.getEndIndex() + offsets[0]); } } return appendTo; } private static final class MeasureFormatData { MeasureFormatData( Map> unitToStyleToCountToFormat, Map> unitToStyleToPerUnitPattern, EnumMap styleToPerPattern) { this.unitToStyleToCountToFormat = unitToStyleToCountToFormat; this.unitToStyleToPerUnitPattern = unitToStyleToPerUnitPattern; this.styleToPerPattern = styleToPerPattern; } final Map> unitToStyleToCountToFormat; final Map> unitToStyleToPerUnitPattern; final EnumMap styleToPerPattern; } // Wrapper around NumberFormat that provides immutability and thread-safety. private static final class ImmutableNumberFormat { private NumberFormat nf; public ImmutableNumberFormat(NumberFormat nf) { this.nf = (NumberFormat) nf.clone(); } public synchronized NumberFormat get() { return (NumberFormat) nf.clone(); } public synchronized StringBuffer format( Number n, StringBuffer buffer, FieldPosition pos) { return nf.format(n, buffer, pos); } public synchronized StringBuffer format( CurrencyAmount n, StringBuffer buffer, FieldPosition pos) { return nf.format(n, buffer, pos); } @SuppressWarnings("unused") public synchronized String format(Number number) { return nf.format(number); } public String getPrefix(boolean positive) { return positive ? ((DecimalFormat)nf).getPositivePrefix() : ((DecimalFormat)nf).getNegativePrefix(); } public String getSuffix(boolean positive) { return positive ? ((DecimalFormat)nf).getPositiveSuffix() : ((DecimalFormat)nf).getPositiveSuffix(); } } static final class PatternData { final String prefix; final String suffix; public PatternData(String pattern) { int pos = pattern.indexOf("{0}"); if (pos < 0) { prefix = pattern; suffix = null; } else { prefix = pattern.substring(0,pos); suffix = pattern.substring(pos+3); } } public String toString() { return prefix + "; " + suffix; } } Object toTimeUnitProxy() { return new MeasureProxy(getLocale(), formatWidth, numberFormat.get(), TIME_UNIT_FORMAT); } Object toCurrencyProxy() { return new MeasureProxy(getLocale(), formatWidth, numberFormat.get(), CURRENCY_FORMAT); } private StringBuilder formatMeasuresSlowTrack( ListFormatter listFormatter, StringBuilder 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) { ImmutableNumberFormat nf = (i == measures.length - 1 ? numberFormat : integerFormat); if (fieldPositionFoundIndex == -1) { results[i] = formatMeasure(measures[i], nf, new StringBuilder(), fpos).toString(); if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) { fieldPositionFoundIndex = i; } } else { results[i] = formatMeasure(measures[i], nf); } } ListFormatter.FormattedListBuilder builder = listFormatter.format(Arrays.asList(results), fieldPositionFoundIndex); // Fix up FieldPosition indexes if our field is found. if (builder.getOffset() != -1) { fieldPosition.setBeginIndex(fpos.getBeginIndex() + builder.getOffset() + appendTo.length()); fieldPosition.setEndIndex(fpos.getEndIndex() + builder.getOffset() + appendTo.length()); } return appendTo.append(builder.toString()); } // type is one of "hm", "ms" or "hms" private static DateFormat 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. DateFormat result = new SimpleDateFormat(r.getString().replace("h", "H")); result.setTimeZone(TimeZone.GMT_ZONE); return result; } // 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 StringBuilder formatNumeric(Number[] hms, StringBuilder appendable) { // find the start and end of non-nil values in hms array. We have to know if we // have hour-minute; minute-second; or hour-minute-second. int startIndex = -1; int endIndex = -1; for (int i = 0; i < hms.length; i++) { if (hms[i] != null) { endIndex = i; if (startIndex == -1) { startIndex = endIndex; } } else { // Replace nil value with 0. hms[i] = Integer.valueOf(0); } } // convert hours, minutes, seconds into milliseconds. long millis = (long) (((Math.floor(hms[0].doubleValue()) * 60.0 + Math.floor(hms[1].doubleValue())) * 60.0 + Math.floor(hms[2].doubleValue())) * 1000.0); Date d = new Date(millis); // if hour-minute-second if (startIndex == 0 && endIndex == 2) { return formatNumeric( d, numericFormatters.getHourMinuteSecond(), DateFormat.Field.SECOND, hms[endIndex], appendable); } // if minute-second if (startIndex == 1 && endIndex == 2) { return formatNumeric( d, numericFormatters.getMinuteSecond(), DateFormat.Field.SECOND, hms[endIndex], appendable); } // if hour-minute if (startIndex == 0 && endIndex == 1) { return formatNumeric( d, numericFormatters.getHourMinute(), DateFormat.Field.MINUTE, hms[endIndex], appendable); } throw new IllegalStateException(); } // Formats a duration as 5:00:37 or 23:59. // duration is a particular duration after epoch. // formatter is a hour-minute-second, hour-minute, or minute-second formatter. // smallestField denotes what the smallest field is in duration: either // hour, minute, or second. // smallestAmount is the value of that smallest field. for 5:00:37.3, // smallestAmount is 37.3. This smallest field is formatted with this object's // NumberFormat instead of formatter. // appendTo is where the formatted string is appended. private StringBuilder formatNumeric( Date duration, DateFormat formatter, DateFormat.Field smallestField, Number smallestAmount, StringBuilder appendTo) { // Format the smallest amount ahead of time. String smallestAmountFormatted; // Format the smallest amount using this object's number format, but keep track // of the integer portion of this formatted amount. We have to replace just the // integer part with the corresponding value from formatting the date. Otherwise // when formatting 0 minutes 9 seconds, we may get "00:9" instead of "00:09" FieldPosition intFieldPosition = new FieldPosition(NumberFormat.INTEGER_FIELD); smallestAmountFormatted = numberFormat.format( smallestAmount, new StringBuffer(), intFieldPosition).toString(); // Give up if there is no integer field. if (intFieldPosition.getBeginIndex() == 0 && intFieldPosition.getEndIndex() == 0) { throw new IllegalStateException(); } // Format our duration as a date, but keep track of where the smallest field is // so that we can use it to replace the integer portion of the smallest value. FieldPosition smallestFieldPosition = new FieldPosition(smallestField); String draft = formatter.format( duration, new StringBuffer(), smallestFieldPosition).toString(); // If we find the smallest field if (smallestFieldPosition.getBeginIndex() != 0 || smallestFieldPosition.getEndIndex() != 0) { // add everything up to the start of the smallest field in duration. appendTo.append(draft, 0, smallestFieldPosition.getBeginIndex()); // add everything in the smallest field up to the integer portion appendTo.append(smallestAmountFormatted, 0, intFieldPosition.getBeginIndex()); // Add the smallest field in formatted duration in lieu of the integer portion // of smallest field appendTo.append( draft, smallestFieldPosition.getBeginIndex(), smallestFieldPosition.getEndIndex()); // Add the rest of the smallest field appendTo.append( smallestAmountFormatted, intFieldPosition.getEndIndex(), smallestAmountFormatted.length()); appendTo.append(draft, smallestFieldPosition.getEndIndex(), draft.length()); } else { // As fallback, just use the formatted duration. appendTo.append(draft); } return appendTo; } private Object writeReplace() throws ObjectStreamException { return new MeasureProxy( getLocale(), formatWidth, numberFormat.get(), 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 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() { } 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); } @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) 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 new CurrencyFormat(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.WIDE; } return values[ordinal]; } static final Map localeIdToRangeFormat = new ConcurrentHashMap(); /** * Return a simple pattern formatter 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 SimplePatternFormatter getRangeFormat(ULocale forLocale, FormatWidth width) { // TODO fix Hack for French if (forLocale.getLanguage().equals("fr")) { return getRangeFormat(ULocale.ROOT, width); } SimplePatternFormatter 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 = SimplePatternFormatter.compile(resultString); localeIdToRangeFormat.put(forLocale, result); if (!forLocale.equals(realLocale)) { localeIdToRangeFormat.put(realLocale, result); } } return result; } /** * Return a simple pattern pattern for a range, such as "{0}–{1}" or "{0}~{1}". * @param forLocale locale to get the range pattern for * @param width the format width. * @return range pattern * @internal * @deprecated This API is ICU internal only. */ @Deprecated public static String getRangePattern(ULocale forLocale, FormatWidth width) { return getRangeFormat(forLocale, width).toString(); } }