blob: 15ecee7c3dadff011aafdc3dd0afdc972d4c2209 [file] [log] [blame]
/*
**********************************************************************
* 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.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.SimpleFormatterImpl;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.math.BigDecimal;
import com.ibm.icu.text.PluralRules.Factory;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.ICUException;
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.
*
* <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 MeasureFormatData cache;
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;
private final transient NumericFormatters numericFormatters;
private final transient ImmutableNumberFormat currencyFormat;
private final transient ImmutableNumberFormat integerFormat;
private static final SimpleCache<ULocale, MeasureFormatData> localeMeasureFormatData
= new SimpleCache<ULocale, MeasureFormatData>();
private static final SimpleCache<ULocale, NumericFormatters> localeToNumericDurationFormatters
= new SimpleCache<ULocale,NumericFormatters>();
private static final Map<MeasureUnit, Integer> hmsTo012 =
new HashMap<MeasureUnit, Integer>();
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.DURATION, NumberFormat.PLURALCURRENCYSTYLE),
/**
* Abbreviate when possible.
*
* @stable ICU 53
*/
SHORT(ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE),
/**
* Brief. Use only a symbol for the unit when possible.
*
* @stable ICU 53
*/
NARROW(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(ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE);
// Be sure to update the toFormatWidth and fromFormatWidth() functions
// when adding an enum value.
private static final int INDEX_COUNT = 3; // NARROW.ordinal() + 1
private final ListFormatter.Style listFormatterStyle;
private final int currencyStyle;
private FormatWidth(ListFormatter.Style style, int currencyStyle) {
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.
* @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) {
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,
data,
formatWidth,
new ImmutableNumberFormat(format),
rules,
formatters,
new ImmutableNumberFormat(NumberFormat.getInstance(locale, formatWidth.getCurrencyStyle())),
new ImmutableNumberFormat(intFormat));
}
/**
* 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.
* @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<? extends Measure>, 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 <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();
}
/**
* 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.
* <br>Note: If the format doesn’t have enough decimals, or lowValue ≥ highValue,
* the result will be a degenerate range, like “5-5 meters”.
* <br>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());
StandardPlural resolvedPlural = pluralRanges.get(
StandardPlural.fromString(keywordLow),
StandardPlural.fromString(keywordHigh));
String rangeFormatter = getRangeFormat(getLocale(), formatWidth);
String formattedNumber = SimpleFormatterImpl.formatCompiledPattern(
rangeFormatter, 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, resolvedPlural, result);
result.append(formattedNumber);
appendReplacingCurrency(currencyFormat.getSuffix(highDouble >= 0), currencyUnit, resolvedPlural, 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 {
String formatter =
getPluralFormatter(lowValue.getUnit(), formatWidth, resolvedPlural.ordinal());
return SimpleFormatterImpl.formatCompiledPattern(formatter, formattedNumber);
}
}
private void appendReplacingCurrency(String affix, Currency unit, StandardPlural resolvedPlural, 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,
resolvedPlural.getKeyword(), 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
* @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.cache,
this.formatWidth,
new ImmutableNumberFormat(format),
this.rules,
this.numericFormatters,
this.currencyFormat,
this.integerFormat);
}
private MeasureFormat(
ULocale locale,
MeasureFormatData data,
FormatWidth formatWidth,
ImmutableNumberFormat format,
PluralRules rules,
NumericFormatters formatters,
ImmutableNumberFormat currencyFormat,
ImmutableNumberFormat integerFormat) {
setLocale(locale, locale);
this.cache = data;
this.formatWidth = formatWidth;
this.numberFormat = format;
this.rules = rules;
this.numericFormatters = formatters;
this.currencyFormat = currencyFormat;
this.integerFormat = integerFormat;
}
MeasureFormat() {
// Make compiler happy by setting final fields to null.
this.cache = null;
this.formatWidth = null;
this.numberFormat = null;
this.rules = null;
this.numericFormatters = null;
this.currencyFormat = null;
this.integerFormat = 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"));
}
/**
* Sink for enumerating all of the measurement unit display names.
* Contains inner sink classes, each one corresponding to a type of resource table.
* The outer sink handles the top-level units, unitsNarrow, and unitsShort tables.
*
* More specific bundles (en_GB) are enumerated before their parents (en_001, en, root):
* Only store a value if it is still missing, that is, it has not been overridden.
*
* C++: Each inner sink class has a reference to the main outer sink.
* Java: Use non-static inner classes instead.
*/
private static final class UnitDataSink extends UResource.TableSink {
/**
* Sink for a table of display patterns. For example,
* unitsShort/duration/hour contains other{"{0} hrs"}.
*/
class UnitPatternSink extends UResource.TableSink {
String[] patterns;
void setFormatterIfAbsent(int index, UResource.Value value, int minPlaceholders) {
if (patterns == null) {
EnumMap<FormatWidth, String[]> styleToPatterns =
cacheData.unitToStyleToPatterns.get(unit);
if (styleToPatterns == null) {
styleToPatterns =
new EnumMap<FormatWidth, String[]>(FormatWidth.class);
cacheData.unitToStyleToPatterns.put(unit, styleToPatterns);
} else {
patterns = styleToPatterns.get(width);
}
if (patterns == null) {
patterns = new String[MeasureFormatData.PATTERN_COUNT];
styleToPatterns.put(width, patterns);
}
}
if (patterns[index] == null) {
patterns[index] = SimpleFormatterImpl.compileToStringMinMaxArguments(
value.getString(), sb, minPlaceholders, 1);
}
}
@Override
public void put(UResource.Key key, UResource.Value value) {
if (key.contentEquals("dnam")) {
// Skip the unit display name for now.
} else if (key.contentEquals("per")) {
// For example, "{0}/h".
setFormatterIfAbsent(MeasureFormatData.PER_UNIT_INDEX, value, 1);
} else {
// The key must be one of the plural form strings. For example:
// one{"{0} hr"}
// other{"{0} hrs"}
setFormatterIfAbsent(StandardPlural.indexFromString(key), value, 0);
}
}
}
UnitPatternSink patternSink = new UnitPatternSink();
/**
* Sink for a table of per-unit tables. For example,
* unitsShort/duration contains tables for duration-unit subtypes day & hour.
*/
class UnitSubtypeSink extends UResource.TableSink {
@Override
public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) {
// Should we ignore or reject unknown units?
unit = MeasureUnit.internalGetInstance(type, key.toString()); // never null
// Trigger a fresh lookup of the patterns for this unit+width.
patternSink.patterns = null;
return patternSink;
}
}
UnitSubtypeSink subtypeSink = new UnitSubtypeSink();
/**
* Sink for compound x-per-y display pattern. For example,
* unitsShort/compound/per may be "{0}/{1}".
*/
class UnitCompoundSink extends UResource.TableSink {
@Override
public void put(UResource.Key key, UResource.Value value) {
if (key.contentEquals("per")) {
cacheData.styleToPerPattern.put(width,
SimpleFormatterImpl.compileToStringMinMaxArguments(
value.getString(), sb, 2, 2));
}
}
}
UnitCompoundSink compoundSink = new UnitCompoundSink();
/**
* Sink for a table of unit type tables. For example,
* unitsShort contains tables for area & duration.
* It also contains a table for the compound/per pattern.
*/
class UnitTypeSink extends UResource.TableSink {
@Override
public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) {
if (key.contentEquals("currency")) {
// Skip.
} else if (key.contentEquals("compound")) {
if (!cacheData.hasPerFormatter(width)) {
return compoundSink;
}
} else {
type = key.toString();
return subtypeSink;
}
return null;
}
}
UnitTypeSink typeSink = new UnitTypeSink();
UnitDataSink(MeasureFormatData outputData) {
cacheData = outputData;
}
@Override
public void put(UResource.Key key, UResource.Value value) {
// Handle aliases like
// units:alias{"/LOCALE/unitsShort"}
// which should only occur in the root bundle.
if (value.getType() != ICUResourceBundle.ALIAS) { return; }
FormatWidth sourceWidth = widthFromKey(key);
if (sourceWidth == null) {
// Alias from something we don't care about.
return;
}
FormatWidth targetWidth = widthFromAlias(value);
if (targetWidth == null) {
// We do not recognize what to fall back to.
throw new ICUException("Units data fallback from " + key +
" to unknown " + value.getAliasString());
}
// Check that we do not fall back to another fallback.
if (cacheData.widthFallback[targetWidth.ordinal()] != null) {
throw new ICUException("Units data fallback from " + key +
" to " + value.getAliasString() + " which falls back to something else");
}
cacheData.widthFallback[sourceWidth.ordinal()] = targetWidth;
}
@Override
public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) {
if ((width = widthFromKey(key)) != null) {
return typeSink;
}
return null;
}
static FormatWidth widthFromKey(UResource.Key key) {
if (key.startsWith("units")) {
if (key.length() == 5) {
return FormatWidth.WIDE;
} else if (key.regionMatches(5, "Short")) {
return FormatWidth.SHORT;
} else if (key.regionMatches(5, "Narrow")) {
return FormatWidth.NARROW;
}
}
return null;
}
static FormatWidth widthFromAlias(UResource.Value value) {
String s = value.getAliasString();
// For example: "/LOCALE/unitsShort"
if (s.startsWith("/LOCALE/units")) {
if (s.length() == 13) {
return FormatWidth.WIDE;
} else if (s.length() == 18 && s.endsWith("Short")) {
return FormatWidth.SHORT;
} else if (s.length() == 19 && s.endsWith("Narrow")) {
return FormatWidth.NARROW;
}
}
return null;
}
// Output data.
MeasureFormatData cacheData;
// Path to current data.
FormatWidth width;
String type;
MeasureUnit unit;
// Temporary
StringBuilder sb = new StringBuilder();
}
/**
* Returns formatting data for all MeasureUnits except for currency ones.
*/
private static MeasureFormatData loadLocaleData(ULocale locale) {
ICUResourceBundle resource =
(ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
MeasureFormatData cacheData = new MeasureFormatData();
UnitDataSink sink = new UnitDataSink(cacheData);
resource.getAllTableItemsWithFallback("", sink);
return cacheData;
}
private static final FormatWidth getRegularWidth(FormatWidth width) {
if (width == FormatWidth.NUMERIC) {
return FormatWidth.NARROW;
}
return width;
}
private String getFormatterOrNull(MeasureUnit unit, FormatWidth width, int index) {
width = getRegularWidth(width);
Map<FormatWidth, String[]> styleToPatterns = cache.unitToStyleToPatterns.get(unit);
String[] patterns = styleToPatterns.get(width);
if (patterns != null && patterns[index] != null) {
return patterns[index];
}
FormatWidth fallbackWidth = cache.widthFallback[width.ordinal()];
if (fallbackWidth != null) {
patterns = styleToPatterns.get(fallbackWidth);
if (patterns != null && patterns[index] != null) {
return patterns[index];
}
}
return null;
}
private String getFormatter(MeasureUnit unit, FormatWidth width, int index) {
String pattern = getFormatterOrNull(unit, width, index);
if (pattern == null) {
throw new MissingResourceException(
"no formatting pattern for " + unit + ", width " + width + ", index " + index,
null, null);
}
return pattern;
}
private String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) {
if (index != StandardPlural.OTHER_INDEX) {
String pattern = getFormatterOrNull(unit, width, index);
if (pattern != null) {
return pattern;
}
}
return getFormatter(unit, width, StandardPlural.OTHER_INDEX);
}
private String getPerFormatter(FormatWidth width) {
width = getRegularWidth(width);
String perPattern = cache.styleToPerPattern.get(width);
if (perPattern != null) {
return perPattern;
}
FormatWidth fallbackWidth = cache.widthFallback[width.ordinal()];
if (fallbackWidth != null) {
perPattern = cache.styleToPerPattern.get(fallbackWidth);
if (perPattern != null) {
return perPattern;
}
}
throw new MissingResourceException("no x-per-y pattern for width " + width, null, null);
}
private int withPerUnitAndAppend(
CharSequence formatted, MeasureUnit perUnit, StringBuilder appendTo) {
int[] offsets = new int[1];
String perUnitPattern =
getFormatterOrNull(perUnit, formatWidth, MeasureFormatData.PER_UNIT_INDEX);
if (perUnitPattern != null) {
SimpleFormatterImpl.formatAndAppend(perUnitPattern, appendTo, offsets, formatted);
return offsets[0];
}
String perPattern = getPerFormatter(formatWidth);
String pattern = getPluralFormatter(perUnit, formatWidth, StandardPlural.ONE.ordinal());
String perUnitString = SimpleFormatterImpl.getTextWithNoArguments(pattern).trim();
SimpleFormatterImpl.formatAndAppend(
perPattern, 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) {
Number n = measure.getNumber();
MeasureUnit unit = measure.getUnit();
if (unit instanceof Currency) {
return appendTo.append(
currencyFormat.format(
new CurrencyAmount(n, (Currency) unit),
new StringBuffer(),
fieldPosition));
}
StringBuffer formattedNumber = new StringBuffer();
StandardPlural pluralForm = QuantityFormatter.selectPlural(
n, nf.nf, rules, formattedNumber, fieldPosition);
String formatter = getPluralFormatter(unit, formatWidth, pluralForm.ordinal());
return QuantityFormatter.format(formatter, formattedNumber, appendTo, fieldPosition);
}
/**
* Instances contain all MeasureFormat specific data for a particular locale.
* This data is cached. It is never copied, but is shared via shared pointers.
*
* Note: We might change the cache data to have
* an array[WIDTH_INDEX_COUNT] or EnumMap<FormatWidth, ...> of
* complete sets of unit & per patterns,
* to correspond to the resource data and its aliases.
*/
private static final class MeasureFormatData {
static final int PER_UNIT_INDEX = StandardPlural.COUNT;
static final int PATTERN_COUNT = PER_UNIT_INDEX + 1;
boolean hasPerFormatter(FormatWidth width) {
return styleToPerPattern.containsKey(width);
}
/**
* Redirection data from root-bundle, top-level sideways aliases.
* - null: initial value, just fall back to root
* - FormatWidth.WIDE/SHORT/NARROW: sideways alias for missing data
*/
final FormatWidth widthFallback[] = new FormatWidth[FormatWidth.INDEX_COUNT];
/** Measure unit -> format width -> array of patterns ("{0} meters") (plurals + PER_UNIT_INDEX) */
final Map<MeasureUnit, EnumMap<FormatWidth, String[]>> unitToStyleToPatterns =
new HashMap<MeasureUnit, EnumMap<FormatWidth, String[]>>();
final EnumMap<FormatWidth, String> styleToPerPattern =
new EnumMap<FormatWidth, String>(FormatWidth.class);;
}
// 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<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<Object, Object>();
}
// 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<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 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.SHORT;
}
return values[ordinal];
}
private static final Map<ULocale, String> localeIdToRangeFormat =
new ConcurrentHashMap<ULocale, String>();
/**
* 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;
}
}