From adc2570f1863012801d56252535eb7135ca71c9b Mon Sep 17 00:00:00 2001 From: Mark Davis Date: Thu, 3 Jul 2014 13:16:30 +0000 Subject: [PATCH] ICU-10600 add plural ranges and unit formatting X-SVN-Rev: 35997 --- .../com/ibm/icu/impl/PluralRulesLoader.java | 254 ++++++++++++++- .../src/com/ibm/icu/text/MeasureFormat.java | 274 ++++++++++------ .../src/com/ibm/icu/text/PluralRanges.java | 307 ++++++++++++++++++ .../src/com/ibm/icu/text/PluralRules.java | 52 +++ .../icu/dev/test/format/PluralRangesTest.java | 85 +++++ .../com/ibm/icu/dev/test/format/TestAll.java | 1 + 6 files changed, 879 insertions(+), 94 deletions(-) create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/text/PluralRanges.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/PluralRulesLoader.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/PluralRulesLoader.java index 24e23875af..217fa3b3ad 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/PluralRulesLoader.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/PluralRulesLoader.java @@ -1,6 +1,6 @@ /* ******************************************************************************* - * Copyright (C) 2008-2013, International Business Machines Corporation and * + * Copyright (C) 2008-2014, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ @@ -10,13 +10,18 @@ import java.text.ParseException; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import com.ibm.icu.text.NumberingSystem; +import com.ibm.icu.text.PluralRanges; import com.ibm.icu.text.PluralRules; import com.ibm.icu.text.PluralRules.PluralType; +import com.ibm.icu.text.PluralRules.StandardPluralCategories; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; @@ -29,6 +34,8 @@ public class PluralRulesLoader extends PluralRules.Factory { private Map localeIdToCardinalRulesId; private Map localeIdToOrdinalRulesId; private Map rulesIdToEquivalentULocale; + private static Map localeIdToPluralRanges; + /** * Access through singleton. @@ -140,7 +147,7 @@ public class PluralRulesLoader extends PluralRules.Factory { tempLocaleIdToOrdinalRulesId = Collections.emptyMap(); tempRulesIdToEquivalentULocale = Collections.emptyMap(); } - + synchronized(this) { if (localeIdToCardinalRulesId == null) { localeIdToCardinalRulesId = tempLocaleIdToCardinalRulesId; @@ -253,4 +260,245 @@ public class PluralRulesLoader extends PluralRules.Factory { public boolean hasOverride(ULocale locale) { return false; } -} + + private static final PluralRanges UNKNOWN_RANGE = new PluralRanges().freeze(); + + public PluralRanges getPluralRanges(ULocale locale) { + // TODO markdavis Fix the bad fallback, here and elsewhere in this file. + String localeId = ULocale.canonicalize(locale.getBaseName()); + PluralRanges result; + while (null == (result = localeIdToPluralRanges.get(localeId))) { + int ix = localeId.lastIndexOf("_"); + if (ix == -1) { + result = UNKNOWN_RANGE; + break; + } + localeId = localeId.substring(0, ix); + } + return result; + } + + public boolean isPluralRangesAvailable(ULocale locale) { + return getPluralRanges(locale) == UNKNOWN_RANGE; + } + + // TODO markdavis FIX HARD-CODED HACK once we have data from CLDR in the bundles + static { + String[][] pluralRangeData = { + {"locales", "id ja km ko lo ms my th vi zh"}, + {"other", "other", "other"}, + + {"locales", "am bn fr gu hi hy kn mr pa zu"}, + {"one", "one", "one"}, + {"one", "other", "other"}, + {"other", "other", "other"}, + + {"locales", "fa"}, + {"one", "one", "other"}, + {"one", "other", "other"}, + {"other", "other", "other"}, + + {"locales", "ka"}, + {"one", "other", "one"}, + {"other", "one", "other"}, + {"other", "other", "other"}, + + {"locales", "az de el gl hu it kk ky ml mn ne nl pt sq sw ta te tr ug uz"}, + {"one", "other", "other"}, + {"other", "one", "one"}, + {"other", "other", "other"}, + + {"locales", "af bg ca en es et eu fi nb sv ur"}, + {"one", "other", "other"}, + {"other", "one", "other"}, + {"other", "other", "other"}, + + {"locales", "da fil is"}, + {"one", "one", "one"}, + {"one", "other", "other"}, + {"other", "one", "one"}, + {"other", "other", "other"}, + + {"locales", "si"}, + {"one", "one", "one"}, + {"one", "other", "other"}, + {"other", "one", "other"}, + {"other", "other", "other"}, + + {"locales", "mk"}, + {"one", "one", "other"}, + {"one", "other", "other"}, + {"other", "one", "other"}, + {"other", "other", "other"}, + + {"locales", "lv"}, + {"zero", "zero", "other"}, + {"zero", "one", "one"}, + {"zero", "other", "other"}, + {"one", "zero", "other"}, + {"one", "one", "one"}, + {"one", "other", "other"}, + {"other", "zero", "other"}, + {"other", "one", "one"}, + {"other", "other", "other"}, + + {"locales", "ro"}, + {"one", "few", "few"}, + {"one", "other", "other"}, + {"few", "one", "few"}, + {"few", "few", "few"}, + {"few", "other", "other"}, + {"other", "few", "few"}, + {"other", "other", "other"}, + + {"locales", "hr sr bs"}, + {"one", "one", "one"}, + {"one", "few", "few"}, + {"one", "other", "other"}, + {"few", "one", "one"}, + {"few", "few", "few"}, + {"few", "other", "other"}, + {"other", "one", "one"}, + {"other", "few", "few"}, + {"other", "other", "other"}, + + {"locales", "sl"}, + {"one", "one", "few"}, + {"one", "two", "two"}, + {"one", "few", "few"}, + {"one", "other", "other"}, + {"two", "one", "few"}, + {"two", "two", "two"}, + {"two", "few", "few"}, + {"two", "other", "other"}, + {"few", "one", "few"}, + {"few", "two", "two"}, + {"few", "few", "few"}, + {"few", "other", "other"}, + {"other", "one", "few"}, + {"other", "two", "two"}, + {"other", "few", "few"}, + {"other", "other", "other"}, + + {"locales", "he"}, + {"one", "two", "other"}, + {"one", "many", "many"}, + {"one", "other", "other"}, + {"two", "many", "other"}, + {"two", "other", "other"}, + {"many", "many", "many"}, + {"many", "other", "many"}, + {"other", "one", "other"}, + {"other", "two", "other"}, + {"other", "many", "many"}, + {"other", "other", "other"}, + + {"locales", "cs pl sk"}, + {"one", "few", "few"}, + {"one", "many", "many"}, + {"one", "other", "other"}, + {"few", "few", "few"}, + {"few", "many", "many"}, + {"few", "other", "other"}, + {"many", "one", "one"}, + {"many", "few", "few"}, + {"many", "many", "many"}, + {"many", "other", "other"}, + {"other", "one", "one"}, + {"other", "few", "few"}, + {"other", "many", "many"}, + {"other", "other", "other"}, + + {"locales", "lt ru uk"}, + {"one", "one", "one"}, + {"one", "few", "few"}, + {"one", "many", "many"}, + {"one", "other", "other"}, + {"few", "one", "one"}, + {"few", "few", "few"}, + {"few", "many", "many"}, + {"few", "other", "other"}, + {"many", "one", "one"}, + {"many", "few", "few"}, + {"many", "many", "many"}, + {"many", "other", "other"}, + {"other", "one", "one"}, + {"other", "few", "few"}, + {"other", "many", "many"}, + {"other", "other", "other"}, + + {"locales", "cy"}, + {"zero", "one", "one"}, + {"zero", "two", "two"}, + {"zero", "few", "few"}, + {"zero", "many", "many"}, + {"zero", "other", "other"}, + {"one", "two", "two"}, + {"one", "few", "few"}, + {"one", "many", "many"}, + {"one", "other", "other"}, + {"two", "few", "few"}, + {"two", "many", "many"}, + {"two", "other", "other"}, + {"few", "many", "many"}, + {"few", "other", "other"}, + {"many", "other", "other"}, + {"other", "one", "one"}, + {"other", "two", "two"}, + {"other", "few", "few"}, + {"other", "many", "many"}, + {"other", "other", "other"}, + + {"locales", "ar"}, + {"zero", "one", "zero"}, + {"zero", "two", "zero"}, + {"zero", "few", "few"}, + {"zero", "many", "many"}, + {"zero", "other", "other"}, + {"one", "two", "other"}, + {"one", "few", "few"}, + {"one", "many", "many"}, + {"one", "other", "other"}, + {"two", "few", "few"}, + {"two", "many", "many"}, + {"two", "other", "other"}, + {"few", "few", "few"}, + {"few", "many", "many"}, + {"few", "other", "other"}, + {"many", "few", "few"}, + {"many", "many", "many"}, + {"many", "other", "other"}, + {"other", "one", "other"}, + {"other", "two", "other"}, + {"other", "few", "few"}, + {"other", "many", "many"}, + {"other", "other", "other"}, + }; + PluralRanges pr = null; + String[] locales = null; + HashMap tempLocaleIdToPluralRanges = new HashMap(); + for (String[] row : pluralRangeData) { + if (row[0].equals("locales")) { + if (pr != null) { + pr.freeze(); + for (String locale : locales) { + tempLocaleIdToPluralRanges.put(locale, pr); + } + } + locales = row[1].split(" "); + pr = new PluralRanges(); + } else { + pr.add( + StandardPluralCategories.valueOf(row[0]), + StandardPluralCategories.valueOf(row[1]), + StandardPluralCategories.valueOf(row[2])); + } + } + // do last one + for (String locale : locales) { + tempLocaleIdToPluralRanges.put(locale, pr); + } + // now make whole thing immutable + localeIdToPluralRanges = Collections.unmodifiableMap(tempLocaleIdToPluralRanges); + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java index c698e86e16..aea3ce58cf 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java @@ -1,13 +1,13 @@ /* -********************************************************************** -* Copyright (c) 2004-2014, International Business Machines -* Corporation and others. All Rights Reserved. -********************************************************************** -* Author: Alan Liu -* Created: April 20, 2004 -* Since: ICU 3.0 -********************************************************************** -*/ + ********************************************************************** + * Copyright (c) 2004-2014, 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; @@ -26,12 +26,14 @@ 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.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.StandardPluralCategories; import com.ibm.icu.util.Currency; import com.ibm.icu.util.CurrencyAmount; import com.ibm.icu.util.Measure; @@ -104,47 +106,47 @@ import com.ibm.icu.util.UResourceBundle; * @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 static final SimpleCache>> localeToUnitToStyleToCountToFormat - = new SimpleCache>>(); - + = new SimpleCache>>(); + private static final SimpleCache localeToNumericDurationFormatters - = new SimpleCache(); - + = 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. * @@ -154,7 +156,7 @@ public class MeasureFormat extends UFormat { // Be sure to update MeasureUnitTest.TestSerialFormatWidthEnum // when adding an enum value. public enum FormatWidth { - + /** * Spell out everything. * @@ -162,7 +164,7 @@ public class MeasureFormat extends UFormat { * @provisional This API might change or be removed in a future release. */ WIDE("units", ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE), - + /** * Abbreviate when possible. * @@ -170,7 +172,7 @@ public class MeasureFormat extends UFormat { * @provisional This API might change or be removed in a future release. */ SHORT("unitsShort", ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE), - + /** * Brief. Use only a symbol for the unit when possible. * @@ -178,7 +180,7 @@ public class MeasureFormat extends UFormat { * @provisional This API might change or be removed in a future release. */ 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. @@ -188,29 +190,29 @@ public class MeasureFormat extends UFormat { * @provisional This API might change or be removed in a future release. */ 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. * @@ -223,7 +225,7 @@ public class MeasureFormat extends UFormat { 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. * @@ -276,9 +278,9 @@ public class MeasureFormat extends UFormat { formatters, new ImmutableNumberFormat( NumberFormat.getInstance(locale, formatWidth.getCurrencyStyle())), - new ImmutableNumberFormat(intFormat)); + new ImmutableNumberFormat(intFormat)); } - + /** * Create a format from the JDK locale, formatWidth, and format. * @@ -341,7 +343,7 @@ public class MeasureFormat extends UFormat { } return toAppendTo; } - + /** * Parses text from a string to produce a Measure. * @see java.text.Format#parseObject(java.lang.String, java.text.ParsePosition) @@ -353,7 +355,7 @@ public class MeasureFormat extends UFormat { 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”. @@ -374,7 +376,48 @@ public class MeasureFormat extends UFormat { 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. Typically the units should be + * in ascending order. + * + * @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(); + + UFieldPosition fpos = new UFieldPosition(); + StringBuffer lowFormatted = numberFormat.format(lowNumber, new StringBuffer(), fpos); + String keywordLow = rules.select(new PluralRules.FixedDecimal(lowNumber.doubleValue(), fpos.getCountVisibleFractionDigits(), fpos.getFractionDigits())); + + StringBuffer highFormatted = numberFormat.format(highNumber, new StringBuffer(), fpos); + String keywordHigh = rules.select(new PluralRules.FixedDecimal(highNumber.doubleValue(), fpos.getCountVisibleFractionDigits(), fpos.getFractionDigits())); + + StandardPluralCategories resolvedCategory = PluralRules.getRange( + getLocale(), + StandardPluralCategories.valueOf(keywordLow), + StandardPluralCategories.valueOf(keywordHigh)); + + Map styleToCountToFormat = unitToStyleToCountToFormat.get(lowValue.getUnit()); + QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth); + SimplePatternFormatter formatter = countToFormat.getByVariant(resolvedCategory.toString()); + SimplePatternFormatter rangeFormatter = getRangeFormat(getLocale(), formatWidth); + String formattedNumber = rangeFormatter.format(lowFormatted, highFormatted); + return formatter.format(formattedNumber); + } + /** * Formats a sequence of measures. * @@ -399,7 +442,7 @@ public class MeasureFormat extends UFormat { 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. @@ -408,7 +451,7 @@ public class MeasureFormat extends UFormat { return formatNumeric(hms, appendTo); } } - + ListFormatter listFormatter = ListFormatter.getInstance( getLocale(), formatWidth.getListFormatterStyle()); if (fieldPosition != DontCareFieldPosition.INSTANCE) { @@ -422,9 +465,9 @@ public class MeasureFormat extends UFormat { 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. @@ -445,7 +488,7 @@ public class MeasureFormat extends UFormat { && getLocale().equals(rhs.getLocale()) && getNumberFormat().equals(rhs.getNumberFormat()); } - + /** * {@inheritDoc} * @draft ICU 53 @@ -457,7 +500,7 @@ public class MeasureFormat extends UFormat { return (getLocale().hashCode() * 31 + getNumberFormat().hashCode()) * 31 + getWidth().hashCode(); } - + /** * Get the format width this instance is using. * @draft ICU 53 @@ -466,7 +509,7 @@ public class MeasureFormat extends UFormat { public MeasureFormat.FormatWidth getWidth() { return formatWidth; } - + /** * Get the locale of this instance. * @draft ICU 53 @@ -475,7 +518,7 @@ public class MeasureFormat extends UFormat { public final ULocale getLocale() { return getLocale(ULocale.VALID_LOCALE); } - + /** * Get a copy of the number format. * @draft ICU 53 @@ -518,7 +561,7 @@ public class MeasureFormat extends UFormat { 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()); @@ -535,7 +578,7 @@ public class MeasureFormat extends UFormat { this.currencyFormat, this.integerFormat); } - + private MeasureFormat( ULocale locale, FormatWidth formatWidth, @@ -554,7 +597,7 @@ public class MeasureFormat extends UFormat { this.currencyFormat = currencyFormat; this.integerFormat = integerFormat; } - + MeasureFormat() { // Make compiler happy by setting final fields to null. this.formatWidth = null; @@ -565,12 +608,12 @@ public class MeasureFormat extends UFormat { this.currencyFormat = null; this.integerFormat = null; } - + static class NumericFormatters { private DateFormat hourMinute; private DateFormat minuteSecond; private DateFormat hourMinuteSecond; - + public NumericFormatters( DateFormat hourMinute, DateFormat minuteSecond, @@ -579,12 +622,12 @@ public class MeasureFormat extends UFormat { 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. @@ -594,7 +637,7 @@ public class MeasureFormat extends UFormat { loadNumericDurationFormat(r, "ms"), loadNumericDurationFormat(r, "hms")); } - + /** * Returns formatting data for all MeasureUnits except for currency ones. */ @@ -602,7 +645,7 @@ public class MeasureFormat extends UFormat { ULocale locale, PluralRules rules) { QuantityFormatter.Builder builder = new QuantityFormatter.Builder(); Map> unitToStyleToCountToFormat - = new HashMap>(); + = new HashMap>(); ICUResourceBundle resource = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, locale); for (MeasureUnit unit : MeasureUnit.getAvailable()) { // Currency data cannot be found here. Skip. @@ -654,13 +697,13 @@ public class MeasureFormat extends UFormat { } return unitToStyleToCountToFormat; } - + private String formatMeasure(Measure measure, ImmutableNumberFormat nf) { return formatMeasure( measure, nf, new StringBuilder(), DontCareFieldPosition.INSTANCE).toString(); } - + private StringBuilder formatMeasure( Measure measure, ImmutableNumberFormat nf, @@ -669,10 +712,10 @@ public class MeasureFormat extends UFormat { if (measure.getUnit() instanceof Currency) { return appendTo.append( currencyFormat.format( - new CurrencyAmount(measure.getNumber(), (Currency) measure.getUnit()), - new StringBuffer(), - fieldPosition)); - + new CurrencyAmount(measure.getNumber(), (Currency) measure.getUnit()), + new StringBuffer(), + fieldPosition)); + } Number n = measure.getNumber(); MeasureUnit unit = measure.getUnit(); @@ -694,24 +737,24 @@ public class MeasureFormat extends UFormat { } return appendTo; } - + // 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); @@ -722,7 +765,7 @@ public class MeasureFormat extends UFormat { return nf.format(number); } } - + static final class PatternData { final String prefix; final String suffix; @@ -741,26 +784,26 @@ public class MeasureFormat extends UFormat { } } - + 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); @@ -775,7 +818,7 @@ public class MeasureFormat extends UFormat { } 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()); @@ -783,7 +826,7 @@ public class MeasureFormat extends UFormat { } return appendTo.append(builder.toString()); } - + // type is one of "hm", "ms" or "hms" private static DateFormat loadNumericDurationFormat( ICUResourceBundle r, String type) { @@ -793,7 +836,7 @@ public class MeasureFormat extends UFormat { 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. @@ -820,11 +863,11 @@ public class MeasureFormat extends UFormat { } 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; @@ -874,7 +917,7 @@ public class MeasureFormat extends UFormat { } 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. @@ -892,7 +935,7 @@ public class MeasureFormat extends UFormat { 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 @@ -909,23 +952,23 @@ public class MeasureFormat extends UFormat { 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, @@ -938,15 +981,15 @@ public class MeasureFormat extends UFormat { } 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; @@ -988,7 +1031,7 @@ public class MeasureFormat extends UFormat { 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(); @@ -996,7 +1039,7 @@ public class MeasureFormat extends UFormat { throw new InvalidObjectException("Missing optional values map."); } } - + private TimeUnitFormat createTimeUnitFormat() throws InvalidObjectException { int style; if (formatWidth == FormatWidth.WIDE) { @@ -1024,7 +1067,7 @@ public class MeasureFormat extends UFormat { } } } - + private static FormatWidth fromFormatWidthOrdinal(int ordinal) { FormatWidth[] values = FormatWidth.values(); if (ordinal < 0 || ordinal >= values.length) { @@ -1032,4 +1075,53 @@ public class MeasureFormat extends UFormat { } 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 SimplePatternFormatter getRangeFormat(ULocale forLocale, FormatWidth width) { + // TODO fix Hack for French + if (width != FormatWidth.WIDE && forLocale.getLanguage().equals("fr")) { + return getRangeFormat(ULocale.ROOT, width); + } + SimplePatternFormatter result = localeIdToRangeFormat.get(forLocale); + if (result == null) { + ICUResourceBundle rb = (ICUResourceBundle)UResourceBundle. + getBundleInstance(ICUResourceBundle.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; + } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRanges.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRanges.java new file mode 100644 index 0000000000..543d06722b --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRanges.java @@ -0,0 +1,307 @@ +/* + ******************************************************************************* + * Copyright (C) 2008-2014, Google, International Business Machines Corporation and * + * others. All Rights Reserved. * + ******************************************************************************* + */ +package com.ibm.icu.text; + +import java.util.EnumSet; + +import com.ibm.icu.text.PluralRules.StandardPluralCategories; +import com.ibm.icu.util.Freezable; +import com.ibm.icu.util.Output; + +/** + * Utility class for returning the plural category for a range of numbers, such as 1–5, so that appropriate messages can + * be chosen. The rules for determining this value vary widely across locales. + * + * @author markdavis + * @internal + * @deprecated This API is ICU internal only. + */ +@Deprecated +public final class PluralRanges implements Freezable, Comparable { + + private volatile boolean isFrozen; + private Matrix matrix = new Matrix(); + private boolean[] explicit = new boolean[StandardPluralCategories.COUNT]; + + /** + * Internal class for mapping from two StandardPluralCategories values to another. + * + * @internal + * @deprecated This API is ICU internal only. + */ + public static final class Matrix implements Comparable, Cloneable { + private byte[] data = new byte[StandardPluralCategories.COUNT * StandardPluralCategories.COUNT]; + { + for (int i = 0; i < data.length; ++i) { + data[i] = -1; + } + } + + /** + * Internal method for setting. + * + * @internal + * @deprecated This API is ICU internal only. + */ + public void set(StandardPluralCategories start, StandardPluralCategories end, StandardPluralCategories result) { + data[start.ordinal() * StandardPluralCategories.COUNT + end.ordinal()] = result == null ? (byte) -1 + : (byte) result.ordinal(); + } + + /** + * Internal method for setting; throws exception if already set. + * + * @internal + * @deprecated This API is ICU internal only. + */ + public void setIfNew(StandardPluralCategories start, StandardPluralCategories end, + StandardPluralCategories result) { + byte old = data[start.ordinal() * StandardPluralCategories.COUNT + end.ordinal()]; + if (old >= 0) { + throw new IllegalArgumentException("Previously set value for <" + start + ", " + end + ", " + + StandardPluralCategories.VALUES.get(old) + ">"); + } + data[start.ordinal() * StandardPluralCategories.COUNT + end.ordinal()] = result == null ? (byte) -1 + : (byte) result.ordinal(); + } + + /** + * Internal method for getting. + * + * @internal + * @deprecated This API is ICU internal only. + */ + public StandardPluralCategories get(StandardPluralCategories start, StandardPluralCategories end) { + byte result = data[start.ordinal() * StandardPluralCategories.COUNT + end.ordinal()]; + return result < 0 ? null : StandardPluralCategories.VALUES.get(result); + } + + /** + * Internal method to see if <*,end> values are all the same. + * + * @internal + * @deprecated This API is ICU internal only. + */ + public StandardPluralCategories endSame(StandardPluralCategories end) { + StandardPluralCategories first = null; + for (StandardPluralCategories start : StandardPluralCategories.VALUES) { + StandardPluralCategories item = get(start, end); + if (item == null) { + continue; + } + if (first == null) { + first = item; + continue; + } + if (first != item) { + return null; + } + } + return first; + } + + /** + * Internal method to see if values are all the same. + * + * @internal + * @deprecated This API is ICU internal only. + */ + public StandardPluralCategories startSame(StandardPluralCategories start, + EnumSet endDone, Output emit) { + emit.value = false; + StandardPluralCategories first = null; + for (StandardPluralCategories end : StandardPluralCategories.VALUES) { + StandardPluralCategories item = get(start, end); + if (item == null) { + continue; + } + if (first == null) { + first = item; + continue; + } + if (first != item) { + return null; + } + // only emit if we didn't cover with the 'end' values + if (!endDone.contains(end)) { + emit.value = true; + } + } + return first; + } + + @Override + public int hashCode() { + int result = 0; + for (int i = 0; i < data.length; ++i) { + result = result * 37 + data[i]; + } + return result; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Matrix)) { + return false; + } + return 0 == compareTo((Matrix) other); + } + + public int compareTo(Matrix o) { + for (int i = 0; i < data.length; ++i) { + int diff = data[i] - o.data[i]; + if (diff != 0) { + return diff; + } + } + return 0; + } + + @Override + public Matrix clone() { + Matrix result = new Matrix(); + result.data = data.clone(); + return result; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + for (StandardPluralCategories i : StandardPluralCategories.values()) { + for (StandardPluralCategories j : StandardPluralCategories.values()) { + StandardPluralCategories x = get(i, j); + if (x != null) { + result.append(i + " & " + j + " → " + x + ";\n"); + } + } + } + return result.toString(); + } + } + + /** + * Internal method for building. If the start or end are null, it means everything of that type. + * + * @param rangeStart + * plural category for the start of the range + * @param rangeEnd + * plural category for the end of the range + * @param result + * the resulting plural category + * @internal + * @deprecated This API is ICU internal only. + */ + public void add(StandardPluralCategories rangeStart, StandardPluralCategories rangeEnd, + StandardPluralCategories result) { + if (isFrozen) { + throw new UnsupportedOperationException(); + } + explicit[result.ordinal()] = true; + if (rangeStart == null) { + for (StandardPluralCategories rs : StandardPluralCategories.values()) { + if (rangeEnd == null) { + for (StandardPluralCategories re : StandardPluralCategories.values()) { + matrix.setIfNew(rs, re, result); + } + } else { + explicit[rangeEnd.ordinal()] = true; + matrix.setIfNew(rs, rangeEnd, result); + } + } + } else if (rangeEnd == null) { + explicit[rangeStart.ordinal()] = true; + for (StandardPluralCategories re : StandardPluralCategories.values()) { + matrix.setIfNew(rangeStart, re, result); + } + } else { + explicit[rangeStart.ordinal()] = true; + explicit[rangeEnd.ordinal()] = true; + matrix.setIfNew(rangeStart, rangeEnd, result); + } + } + + /** + * Returns the appropriate plural category for a range from start to end. If there is no available data, then + * 'other' is returned. + * + * @param start + * plural category for the start of the range + * @param end + * plural category for the end of the range + * @return the resulting plural category, or 'end' if there is no data. + * @internal + * @deprecated This API is ICU internal only. + */ + public StandardPluralCategories get(StandardPluralCategories start, StandardPluralCategories end) { + StandardPluralCategories result = matrix.get(start, end); + return result == null ? end : result; + } + + /** + * Returns the appropriate plural category for a range from start to end. If the combination does not explicitly + * occur in the data, returns null. + * + * @param start + * plural category for the start of the range + * @param end + * plural category for the end of the range + * @return the resulting plural category + * @internal + * @deprecated This API is ICU internal only. + */ + public boolean isExplicit(StandardPluralCategories start, StandardPluralCategories end) { + return matrix.get(start, end) != null; + } + + /** + * Internal method to determines whether the StandardPluralCategories was explicitly used in any add statement. + * + * @param count + * plural category to test + * @return true if set + * @internal + * @deprecated This API is ICU internal only. + */ + public boolean isExplicitlySet(StandardPluralCategories count) { + return explicit[count.ordinal()]; + } + + @Override + public boolean equals(Object other) { + return other instanceof PluralRanges ? matrix.equals((PluralRanges) other) : false; + } + + @Override + public int hashCode() { + return matrix.hashCode(); + } + + public int compareTo(PluralRanges that) { + return matrix.compareTo(that.matrix); + } + + public boolean isFrozen() { + return isFrozen; + } + + public PluralRanges freeze() { + isFrozen = true; + return this; + } + + public PluralRanges cloneAsThawed() { + PluralRanges result = new PluralRanges(); + result.explicit = explicit.clone(); + result.matrix = matrix.clone(); + return result; + } + + @Override + public String toString() { + return matrix.toString(); + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java index 2540d746d1..68d784496e 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java @@ -15,6 +15,7 @@ import java.io.ObjectStreamException; import java.io.Serializable; import java.text.ParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -1819,6 +1820,20 @@ public class PluralRules implements Serializable { */ @Deprecated other; + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public static final List VALUES + = Collections.unmodifiableList(Arrays.asList(values())); + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public static final int COUNT = values().length; + static StandardPluralCategories forString(String s) { StandardPluralCategories a; try { @@ -2431,4 +2446,41 @@ public class PluralRules implements Serializable { public boolean computeLimited(String keyword, SampleType sampleType) { return rules.computeLimited(keyword, sampleType); } + + /** + * Return the plural category for a range, such as 3.1-4.2. + * @param locale locale for the range + * @param startPluralCategory the plural category of the low end of the range. + * @param endPluralCategory the plural category of the high end of the range. + * @return the plural category of the range + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + static public StandardPluralCategories getRange(ULocale locale, + StandardPluralCategories startPluralCategory, + StandardPluralCategories endPluralCategory) { + final PluralRanges pluralRanges = Factory.getDefaultFactory().getPluralRanges(locale); + return pluralRanges.get(startPluralCategory, endPluralCategory); + } + + /** + * Return the plural category for a range, such as 3.1-4.2. + * @param locale locale for the range + * @param startPluralCategory the plural category of the low end of the range. + * @param endPluralCategory the plural category of the high end of the range. + * @return the plural category of the range + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + static public String getRange(ULocale locale, + String startPluralCategory, + String endPluralCategory) { + final PluralRanges pluralRanges = Factory.getDefaultFactory().getPluralRanges(locale); + return pluralRanges.get( + StandardPluralCategories.valueOf(startPluralCategory), + StandardPluralCategories.valueOf(endPluralCategory)) + .toString(); + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java new file mode 100644 index 0000000000..364e86f0fb --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java @@ -0,0 +1,85 @@ +/* + ******************************************************************************* + * Copyright (C) 2008-2014, International Business Machines Corporation and * + * others. All Rights Reserved. * + ******************************************************************************* + */ +package com.ibm.icu.dev.test.format; + +import com.ibm.icu.dev.test.TestFmwk; +import com.ibm.icu.text.MeasureFormat; +import com.ibm.icu.text.MeasureFormat.FormatWidth; +import com.ibm.icu.text.PluralRanges; +import com.ibm.icu.text.PluralRules; +import com.ibm.icu.text.PluralRules.StandardPluralCategories; +import com.ibm.icu.util.Measure; +import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.ULocale; + +/** + * @author markdavis + * + */ +public class PluralRangesTest extends TestFmwk { + public static void main(String[] args) { + new PluralRangesTest().run(args); + } + + public void TestLocaleData() { + String[][] tests = { + {"de", "other", "one", "one"}, + {"xxx", "few", "few", "few" }, + {"de", "one", "other", "other"}, + {"de", "other", "one", "one"}, + {"de", "other", "other", "other"}, + {"ro", "one", "few", "few"}, + {"ro", "one", "other", "other"}, + {"ro", "few", "one", "few"}, + }; + for (String[] test : tests) { + final ULocale locale = new ULocale(test[0]); + final StandardPluralCategories start = StandardPluralCategories.valueOf(test[1]); + final StandardPluralCategories end = StandardPluralCategories.valueOf(test[2]); + final StandardPluralCategories expected = StandardPluralCategories.valueOf(test[3]); + + StandardPluralCategories actual = PluralRules.getRange(locale, start, end); + assertEquals("Deriving range category", expected, actual); + } + } + + public void TestFormatting() { + Object[][] tests = { + {0.0, 1.0, ULocale.FRANCE, FormatWidth.WIDE, MeasureUnit.FAHRENHEIT, "de 0 à 1 degré Fahrenheit"}, + {1.0, 2.0, ULocale.FRANCE, FormatWidth.WIDE, MeasureUnit.FAHRENHEIT, "de 1 à 2 degrés Fahrenheit"}, + {3.1, 4.25, ULocale.FRANCE, FormatWidth.SHORT, MeasureUnit.FAHRENHEIT, "3,1–4,25 °F"}, + {3.1, 4.25, ULocale.ENGLISH, FormatWidth.SHORT, MeasureUnit.FAHRENHEIT, "3.1–4.25°F"}, + {3.1, 4.25, ULocale.CHINESE, FormatWidth.WIDE, MeasureUnit.INCH, "3.1-4.25英寸"}, + {0.0, 1.0, new ULocale("xx"), FormatWidth.WIDE, MeasureUnit.INCH, "0–1 inches"}, + }; + for (Object[] test : tests) { + double low = (Double) test[0]; + double high = (Double) test[1]; + final ULocale locale = (ULocale) test[2]; + final FormatWidth width = (FormatWidth) test[3]; + final MeasureUnit unit = (MeasureUnit) test[4]; + final String expected = (String) test[5]; + + MeasureFormat mf = MeasureFormat.getInstance(locale, width); + String actual = mf.formatMeasureRange(new Measure(low, unit), new Measure(high, unit)); + assertEquals("Formatting unit", expected, actual); + } + } + + public void TestBasic() { + PluralRanges a = new PluralRanges(); + a.add(StandardPluralCategories.one, StandardPluralCategories.other, StandardPluralCategories.one); + StandardPluralCategories actual = a.get(StandardPluralCategories.one, StandardPluralCategories.other); + assertEquals("range", StandardPluralCategories.one, actual); + a.freeze(); + try { + a.add(StandardPluralCategories.one, StandardPluralCategories.one, StandardPluralCategories.one); + errln("Failed to cause exception on frozen instance"); + } catch (UnsupportedOperationException e) { + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestAll.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestAll.java index 5d6912899c..2726ceaa9e 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestAll.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestAll.java @@ -63,6 +63,7 @@ public class TestAll extends TestGroup { "IntlTestDecimalFormatAPIC", "IntlTestDecimalFormatSymbols", "IntlTestDecimalFormatSymbolsC", + "PluralRangesTest", }); } }