From 2a36e2f226d6e95f85a24a02c68b75e4aabca54d Mon Sep 17 00:00:00 2001 From: Markus Scherer Date: Mon, 26 Aug 2013 20:29:07 +0000 Subject: [PATCH] ICU-8474 support plurals with decimals in MessageFormat and PluralFormat X-SVN-Rev: 34087 --- .../src/com/ibm/icu/text/DecimalFormat.java | 10 +- .../src/com/ibm/icu/text/MessageFormat.java | 258 ++++++++++++++---- .../src/com/ibm/icu/text/PluralFormat.java | 95 ++++--- .../src/com/ibm/icu/text/PluralRules.java | 1 - .../dev/test/format/PluralFormatUnitTest.java | 56 ++-- .../icu/dev/test/format/PluralRulesTest.java | 47 +++- .../dev/test/format/TestMessageFormat.java | 69 +++++ 7 files changed, 398 insertions(+), 138 deletions(-) diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java index 79ae7ce5268..e82b0483242 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java @@ -1218,7 +1218,8 @@ public class DecimalFormat extends NumberFormat { boolean isNegative, boolean isInteger, boolean parseAttr) { if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) { // compute the plural category from the digitList plus other settings - return subformat(getPluralCategory(number), result, fieldPosition, isNegative, + return subformat(currencyPluralInfo.select(getFixedDecimal(number)), + result, fieldPosition, isNegative, isInteger, parseAttr); } else { return subformat(result, fieldPosition, isNegative, isInteger, parseAttr); @@ -1228,7 +1229,7 @@ public class DecimalFormat extends NumberFormat { /** * This is ugly, but don't see a better way to do it without major restructuring of the code. */ - private String getPluralCategory(double number) { + /*package*/ FixedDecimal getFixedDecimal(double number) { // get the visible fractions and the number of fraction digits. int fractionalDigitsInDigitList = digitList.count - digitList.decimalAt; int v; @@ -1264,7 +1265,7 @@ public class DecimalFormat extends NumberFormat { f *= 10; } } - return currencyPluralInfo.select(new FixedDecimal(number, v, f)); + return new FixedDecimal(number, v, f); } private StringBuffer subformat(double number, StringBuffer result, FieldPosition fieldPosition, @@ -1272,7 +1273,8 @@ public class DecimalFormat extends NumberFormat { boolean isInteger, boolean parseAttr) { if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) { // compute the plural category from the digitList plus other settings - return subformat(getPluralCategory(number), result, fieldPosition, isNegative, + return subformat(currencyPluralInfo.select(getFixedDecimal(number)), + result, fieldPosition, isNegative, isInteger, parseAttr); } else { return subformat(result, fieldPosition, isNegative, isInteger, parseAttr); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java index 7a46971fee9..a1446996bc2 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java @@ -1,6 +1,6 @@ /* ********************************************************************** -* Copyright (c) 2004-2012, International Business Machines +* Copyright (c) 2004-2013, International Business Machines * Corporation and others. All Rights Reserved. ********************************************************************** * Author: Alan Liu @@ -36,7 +36,7 @@ import com.ibm.icu.impl.PatternProps; import com.ibm.icu.impl.Utility; import com.ibm.icu.text.MessagePattern.ArgType; import com.ibm.icu.text.MessagePattern.Part; -import com.ibm.icu.text.PluralFormat.PluralSelector; +import com.ibm.icu.text.PluralRules.FixedDecimal; import com.ibm.icu.text.PluralRules.PluralType; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.ULocale.Category; @@ -409,7 +409,8 @@ public class MessageFormat extends UFormat { this.ulocale = locale; // Invalidate all stock formatters. They are no longer valid since // the locale has changed. - stockNumberFormatter = stockDateFormatter = null; + stockDateFormatter = null; + stockNumberFormatter = null; pluralProvider = null; ordinalProvider = null; applyPattern(existingPattern); /*ibm.3550*/ @@ -1439,8 +1440,10 @@ public class MessageFormat extends UFormat { } other.msgPattern = msgPattern == null ? null : (MessagePattern)msgPattern.clone(); - other.stockDateFormatter = stockDateFormatter == null ? null : (Format) stockDateFormatter.clone(); - other.stockNumberFormatter = stockNumberFormatter == null ? null : (Format) stockNumberFormatter.clone(); + other.stockDateFormatter = + stockDateFormatter == null ? null : (DateFormat) stockDateFormatter.clone(); + other.stockNumberFormatter = + stockNumberFormatter == null ? null : (NumberFormat) stockNumberFormatter.clone(); other.pluralProvider = null; other.ordinalProvider = null; @@ -1559,12 +1562,26 @@ public class MessageFormat extends UFormat { * Stock formatters. Those are used when a format is not explicitly mentioned in * the message. The format is inferred from the argument. */ - private transient Format stockDateFormatter; - private transient Format stockNumberFormatter; + private transient DateFormat stockDateFormatter; + private transient NumberFormat stockNumberFormatter; private transient PluralSelectorProvider pluralProvider; private transient PluralSelectorProvider ordinalProvider; + private DateFormat getStockDateFormatter() { + if (stockDateFormatter == null) { + stockDateFormatter = DateFormat.getDateTimeInstance( + DateFormat.SHORT, DateFormat.SHORT, ulocale);//fix + } + return stockDateFormatter; + } + private NumberFormat getStockNumberFormatter() { + if (stockNumberFormatter == null) { + stockNumberFormatter = NumberFormat.getInstance(ulocale); + } + return stockNumberFormatter; + } + // *Important*: All fields must be declared *transient*. // See the longer comment above ulocale. @@ -1575,7 +1592,7 @@ public class MessageFormat extends UFormat { *

Exactly one of args and argsMap must be null, the other non-null. * * @param msgStart Index to msgPattern part to start formatting from. - * @param pluralNumber Zero except when formatting a plural argument sub-message + * @param pluralNumber null except when formatting a plural argument sub-message. * where a '#' is replaced by the format string for this number. * @param args The formattable objects array. Non-null iff numbered values are used. * @param argsMap The key-value map of formattable objects. Non-null iff named values are used. @@ -1583,7 +1600,7 @@ public class MessageFormat extends UFormat { * The result (string & attributes) is appended to existing contents. * @param fp Field position status. */ - private void format(int msgStart, double pluralNumber, + private void format(int msgStart, PluralSelectorContext pluralNumber, Object[] args, Map argsMap, AppendableWrapper dest, FieldPosition fp) { String msgString=msgPattern.getPatternString(); @@ -1598,10 +1615,13 @@ public class MessageFormat extends UFormat { } prevIndex=part.getLimit(); if(type==Part.Type.REPLACE_NUMBER) { - if (stockNumberFormatter == null) { - stockNumberFormatter = NumberFormat.getInstance(ulocale); + if(pluralNumber.forReplaceNumber) { + // number-offset was already formatted. + dest.formatAndAppend(pluralNumber.formatter, + pluralNumber.number, pluralNumber.numberString); + } else { + dest.formatAndAppend(getStockNumberFormatter(), pluralNumber.number); } - dest.formatAndAppend(stockNumberFormatter, pluralNumber); continue; } if(type!=Part.Type.ARG_START) { @@ -1613,6 +1633,7 @@ public class MessageFormat extends UFormat { Object arg; String noArg=null; Object argId=null; + String argName=msgPattern.getSubstring(part); if(args!=null) { int argNumber=part.getValue(); // ARG_NUMBER if (dest.attributes != null) { @@ -1626,18 +1647,12 @@ public class MessageFormat extends UFormat { noArg="{"+argNumber+"}"; } } else { - String key; - if(part.getType()==MessagePattern.Part.Type.ARG_NAME) { - key=msgPattern.getSubstring(part); - } else /* ARG_NUMBER */ { - key=Integer.toString(part.getValue()); - } - argId = key; - if(argsMap!=null && argsMap.containsKey(key)) { - arg=argsMap.get(key); + argId = argName; + if(argsMap!=null && argsMap.containsKey(argName)) { + arg=argsMap.get(argName); } else { arg=null; - noArg="{"+key+"}"; + noArg="{"+argName+"}"; } } ++i; @@ -1647,6 +1662,15 @@ public class MessageFormat extends UFormat { dest.append(noArg); } else if (arg == null) { dest.append("null"); + } else if(pluralNumber!=null && pluralNumber.numberArgIndex==(i-2)) { + if(pluralNumber.offset == 0) { + // The number was already formatted with this formatter. + dest.formatAndAppend(pluralNumber.formatter, pluralNumber.number, pluralNumber.numberString); + } else { + // Do not use the formatted (number-offset) string for a named argument + // that formats the number without subtracting the offset. + dest.formatAndAppend(pluralNumber.formatter, arg); + } } else if(cachedFormatters!=null && (formatter=cachedFormatters.get(i - 2))!=null) { // Handles all ArgType.SIMPLE, and formatters from setFormat() and its siblings. if ( formatter instanceof ChoiceFormat || @@ -1658,7 +1682,7 @@ public class MessageFormat extends UFormat { if (subMsgString.indexOf('{') >= 0 || (subMsgString.indexOf('\'') >= 0 && !msgPattern.jdkAposMode())) { MessageFormat subMsgFormat = new MessageFormat(subMsgString, ulocale); - subMsgFormat.format(0, 0, args, argsMap, dest, null); + subMsgFormat.format(0, null, args, argsMap, dest, null); } else if (dest.attributes == null) { dest.append(subMsgString); } else { @@ -1680,17 +1704,10 @@ public class MessageFormat extends UFormat { // any argument which got reset to null via setFormat() or its siblings. if (arg instanceof Number) { // format number if can - if (stockNumberFormatter == null) { - stockNumberFormatter = NumberFormat.getInstance(ulocale); - } - dest.formatAndAppend(stockNumberFormatter, arg); + dest.formatAndAppend(getStockNumberFormatter(), arg); } else if (arg instanceof Date) { // format a Date if can - if (stockDateFormatter == null) { - stockDateFormatter = DateFormat.getDateTimeInstance( - DateFormat.SHORT, DateFormat.SHORT, ulocale);//fix - } - dest.formatAndAppend(stockDateFormatter, arg); + dest.formatAndAppend(getStockDateFormatter(), arg); } else { dest.append(arg.toString()); } @@ -1700,30 +1717,33 @@ public class MessageFormat extends UFormat { } double number = ((Number)arg).doubleValue(); int subMsgStart=findChoiceSubMessage(msgPattern, i, number); - formatComplexSubMessage(subMsgStart, 0, args, argsMap, dest); + formatComplexSubMessage(subMsgStart, null, args, argsMap, dest); } else if(argType.hasPluralStyle()) { if (!(arg instanceof Number)) { throw new IllegalArgumentException("'" + arg + "' is not a Number"); } - double number = ((Number)arg).doubleValue(); - PluralSelector selector; + PluralSelectorProvider selector; if(argType == ArgType.PLURAL) { if (pluralProvider == null) { - pluralProvider = new PluralSelectorProvider(ulocale, PluralType.CARDINAL); + pluralProvider = new PluralSelectorProvider(this, PluralType.CARDINAL); } selector = pluralProvider; } else { if (ordinalProvider == null) { - ordinalProvider = new PluralSelectorProvider(ulocale, PluralType.ORDINAL); + ordinalProvider = new PluralSelectorProvider(this, PluralType.ORDINAL); } selector = ordinalProvider; } - int subMsgStart=PluralFormat.findSubMessage(msgPattern, i, selector, number); + Number number = (Number)arg; double offset=msgPattern.getPluralOffset(i); - formatComplexSubMessage(subMsgStart, number-offset, args, argsMap, dest); + PluralSelectorContext context = + new PluralSelectorContext(i, argName, number, offset); + int subMsgStart=PluralFormat.findSubMessage( + msgPattern, i, selector, context, number.doubleValue()); + formatComplexSubMessage(subMsgStart, context, args, argsMap, dest); } else if(argType==ArgType.SELECT) { int subMsgStart=SelectFormat.findSubMessage(msgPattern, i, arg.toString()); - formatComplexSubMessage(subMsgStart, 0, args, argsMap, dest); + formatComplexSubMessage(subMsgStart, null, args, argsMap, dest); } else { // This should never happen. throw new IllegalStateException("unexpected argType "+argType); @@ -1735,7 +1755,7 @@ public class MessageFormat extends UFormat { } private void formatComplexSubMessage( - int msgStart, double pluralNumber, + int msgStart, PluralSelectorContext pluralNumber, Object[] args, Map argsMap, AppendableWrapper dest) { if (!msgPattern.jdkAposMode()) { @@ -1768,10 +1788,12 @@ public class MessageFormat extends UFormat { } sb.append(msgString, prevIndex, index); if (type == Part.Type.REPLACE_NUMBER) { - if (stockNumberFormatter == null) { - stockNumberFormatter = NumberFormat.getInstance(ulocale); + if(pluralNumber.forReplaceNumber) { + // number-offset was already formatted. + sb.append(pluralNumber.numberString); + } else { + sb.append(getStockNumberFormatter().format(pluralNumber.number)); } - sb.append(stockNumberFormatter.format(pluralNumber)); } prevIndex = part.getLimit(); } else if (type == Part.Type.ARG_START) { @@ -1789,7 +1811,7 @@ public class MessageFormat extends UFormat { if (subMsgString.indexOf('{') >= 0) { MessageFormat subMsgFormat = new MessageFormat("", ulocale); subMsgFormat.applyPattern(subMsgString, MessagePattern.ApostropheMode.DOUBLE_REQUIRED); - subMsgFormat.format(0, 0, args, argsMap, dest, null); + subMsgFormat.format(0, null, args, argsMap, dest, null); } else { dest.append(subMsgString); } @@ -1946,6 +1968,105 @@ public class MessageFormat extends UFormat { } } + /** + * Finds the "other" sub-message. + * @param partIndex the index of the first PluralFormat argument style part. + * @return the "other" sub-message start part index. + */ + private int findOtherSubMessage(int partIndex) { + int count=msgPattern.countParts(); + MessagePattern.Part part=msgPattern.getPart(partIndex); + if(part.getType().hasNumericValue()) { + ++partIndex; + } + // Iterate over (ARG_SELECTOR [ARG_INT|ARG_DOUBLE] message) tuples + // until ARG_LIMIT or end of plural-only pattern. + do { + part=msgPattern.getPart(partIndex++); + MessagePattern.Part.Type type=part.getType(); + if(type==MessagePattern.Part.Type.ARG_LIMIT) { + break; + } + assert type==MessagePattern.Part.Type.ARG_SELECTOR; + // part is an ARG_SELECTOR followed by an optional explicit value, and then a message + if(msgPattern.partSubstringMatches(part, "other")) { + return partIndex; + } + if(msgPattern.getPartType(partIndex).hasNumericValue()) { + ++partIndex; // skip the numeric-value part of "=1" etc. + } + partIndex=msgPattern.getLimitPartIndex(partIndex); + } while(++partIndex0 ARG_START index */ + int numberArgIndex; + Format formatter; + /** formatted argument number - plural offset */ + String numberString; + /** true if number-offset was formatted with the stock number formatter */ + boolean forReplaceNumber; + } + /** * This provider helps defer instantiation of a PluralRules object * until we actually need to select a keyword. @@ -1953,17 +2074,40 @@ public class MessageFormat extends UFormat { * we do not need any PluralRules. */ private static final class PluralSelectorProvider implements PluralFormat.PluralSelector { - public PluralSelectorProvider(ULocale loc, PluralType type) { - locale=loc; - this.type=type; + public PluralSelectorProvider(MessageFormat mf, PluralType type) { + msgFormat = mf; + this.type = type; } - public String select(double number) { + public String select(Object ctx, double number) { if(rules == null) { - rules = PluralRules.forLocale(locale, type); + rules = PluralRules.forLocale(msgFormat.ulocale, type); + } + // Select a sub-message according to how the number is formatted, + // which is specified in the selected sub-message. + // We avoid this circle by looking at how + // the number is formatted in the "other" sub-message + // which must always be present and usually contains the number. + // Message authors should be consistent across sub-messages. + PluralSelectorContext context = (PluralSelectorContext)ctx; + int otherIndex = msgFormat.findOtherSubMessage(context.startIndex); + context.numberArgIndex = msgFormat.findFirstPluralNumberArg(otherIndex, context.argName); + if(context.numberArgIndex > 0 && msgFormat.cachedFormatters != null) { + context.formatter = msgFormat.cachedFormatters.get(context.numberArgIndex); + } + if(context.formatter == null) { + context.formatter = msgFormat.getStockNumberFormatter(); + context.forReplaceNumber = true; + } + assert context.number.doubleValue() == number; // argument number minus the offset + context.numberString = context.formatter.format(context.number); + if(context.formatter instanceof DecimalFormat) { + FixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number); + return rules.select(dec); + } else { + return rules.select(number); } - return rules.select(number); } - private ULocale locale; + private MessageFormat msgFormat; private PluralRules rules; private PluralType type; } @@ -1991,7 +2135,7 @@ public class MessageFormat extends UFormat { "This method is not available in MessageFormat objects " + "that use alphanumeric argument names."); } - format(0, 0, arguments, argsMap, dest, fp); + format(0, null, arguments, argsMap, dest, fp); } private void resetPattern() { @@ -2489,6 +2633,14 @@ public class MessageFormat extends UFormat { } } + public void formatAndAppend(Format formatter, Object arg, String argString) { + if (attributes == null && argString != null) { + append(argString); + } else { + formatAndAppend(formatter, arg); + } + } + private Appendable app; private int length; private List attributes; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralFormat.java index 9c23ed94d9c..a118b57602f 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralFormat.java @@ -14,6 +14,7 @@ import java.text.ParsePosition; import java.util.Map; import com.ibm.icu.impl.Utility; +import com.ibm.icu.text.PluralRules.FixedDecimal; import com.ibm.icu.text.PluralRules.PluralType; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.ULocale.Category; @@ -386,13 +387,14 @@ public class PluralFormat extends UFormat { * @param pattern A MessagePattern. * @param partIndex the index of the first PluralFormat argument style part. * @param selector the PluralSelector for mapping the number (minus offset) to a keyword. + * @param context worker object for the selector. * @param number a number to be matched to one of the PluralFormat argument's explicit values, * or mapped via the PluralSelector. * @return the sub-message start part index. */ /*package*/ static int findSubMessage( MessagePattern pattern, int partIndex, - PluralSelector selector, double number) { + PluralSelector selector, Object context, double number) { int count=pattern.countParts(); double offset; MessagePattern.Part part=pattern.getPart(partIndex); @@ -402,7 +404,7 @@ public class PluralFormat extends UFormat { } else { offset=0; } - // The keyword is null until we need to match against non-explicit, not-"other" value. + // The keyword is null until we need to match against a non-explicit, not-"other" value. // Then we get the keyword from the selector. // (In other words, we never call the selector if we match against an explicit value, // or if the only non-explicit keyword is "other".) @@ -454,7 +456,7 @@ public class PluralFormat extends UFormat { } } else { if(keyword==null) { - keyword=selector.select(number-offset); + keyword=selector.select(context, number-offset); if(msgStart!=0 && keyword.equals("other")) { // We have already seen an "other" sub-message. // Do not match "other" again. @@ -488,18 +490,21 @@ public class PluralFormat extends UFormat { /** * Given a number, returns the appropriate PluralFormat keyword. * + * @param context worker object for the selector. * @param number The number to be plural-formatted. * @return The selected PluralFormat keyword. */ - public String select(double number); + public String select(Object context, double number); } // See PluralSelector: // We could avoid this adapter class if we made PluralSelector public // (or at least publicly visible) and had PluralRules implement PluralSelector. private final class PluralSelectorAdapter implements PluralSelector { - public String select(double number) { - return pluralRules.select(number); + public String select(Object context, double number) { + FixedDecimal dec = (FixedDecimal) context; + assert dec.source == number; + return pluralRules.select(dec); } } transient private PluralSelectorAdapter pluralRulesWrapper = new PluralSelectorAdapter(); @@ -515,16 +520,60 @@ public class PluralFormat extends UFormat { * @stable ICU 4.0 */ public final String format(double number) { + return format(number, number); + } + + /** + * Formats a plural message for a given number and appends the formatted + * message to the given StringBuffer. + * @param number a number object (instance of Number for which + * the plural message should be formatted. If no pattern has been + * applied to this PluralFormat object yet, the + * formatted number will be returned. + * Note: If this object is not an instance of Number, + * the toAppendTo will not be modified. + * @param toAppendTo the formatted message will be appended to this + * StringBuffer. + * @param pos will be ignored by this method. + * @return the string buffer passed in as toAppendTo, with formatted text + * appended. + * @throws IllegalArgumentException if number is not an instance of Number + * @stable ICU 3.8 + */ + public StringBuffer format(Object number, StringBuffer toAppendTo, + FieldPosition pos) { + if (!(number instanceof Number)) { + throw new IllegalArgumentException("'" + number + "' is not a Number"); + } + Number numberObject = (Number) number; + toAppendTo.append(format(numberObject, numberObject.doubleValue())); + return toAppendTo; + } + + private final String format(Number numberObject, double number) { // If no pattern was applied, return the formatted number. if (msgPattern == null || msgPattern.countParts() == 0) { - return numberFormat.format(number); + return numberFormat.format(numberObject); } // Get the appropriate sub-message. - int partIndex = findSubMessage(msgPattern, 0, pluralRulesWrapper, number); + // Select it based on the formatted number-offset. + double numberMinusOffset = number - offset; + String numberString; + if (offset == 0) { + numberString = numberFormat.format(numberObject); // could be BigDecimal etc. + } else { + numberString = numberFormat.format(numberMinusOffset); + } + FixedDecimal dec; + if(numberFormat instanceof DecimalFormat) { + dec = ((DecimalFormat) numberFormat).getFixedDecimal(numberMinusOffset); + } else { + dec = new FixedDecimal(numberMinusOffset); + } + int partIndex = findSubMessage(msgPattern, 0, pluralRulesWrapper, dec, number); // Replace syntactic # signs in the top level of this sub-message // (not in nested arguments) with the formatted number-offset. - number -= offset; StringBuilder result = null; int prevIndex = msgPattern.getPart(partIndex).getLimit(); for (;;) { @@ -545,7 +594,7 @@ public class PluralFormat extends UFormat { } result.append(pattern, prevIndex, index); if (type == MessagePattern.Part.Type.REPLACE_NUMBER) { - result.append(numberFormat.format(number)); + result.append(numberString); } prevIndex = part.getLimit(); } else if (type == MessagePattern.Part.Type.ARG_START) { @@ -562,32 +611,6 @@ public class PluralFormat extends UFormat { } } - /** - * Formats a plural message for a given number and appends the formatted - * message to the given StringBuffer. - * @param number a number object (instance of Number for which - * the plural message should be formatted. If no pattern has been - * applied to this PluralFormat object yet, the - * formatted number will be returned. - * Note: If this object is not an instance of Number, - * the toAppendTo will not be modified. - * @param toAppendTo the formatted message will be appended to this - * StringBuffer. - * @param pos will be ignored by this method. - * @return the string buffer passed in as toAppendTo, with formatted text - * appended. - * @throws IllegalArgumentException if number is not an instance of Number - * @stable ICU 3.8 - */ - public StringBuffer format(Object number, StringBuffer toAppendTo, - FieldPosition pos) { - if (number instanceof Number) { - toAppendTo.append(format(((Number) number).doubleValue())); - return toAppendTo; - } - throw new IllegalArgumentException("'" + number + "' is not a Number"); - } - /** * This method is not yet supported by PluralFormat. * @param text the string to be parsed. 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 5851fa34ae7..df932956158 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 @@ -27,7 +27,6 @@ import java.util.TreeSet; import java.util.regex.Pattern; import com.ibm.icu.impl.PluralRulesLoader; -import com.ibm.icu.impl.Utility; import com.ibm.icu.util.Output; import com.ibm.icu.util.ULocale; diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralFormatUnitTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralFormatUnitTest.java index 88300be61da..e5c5880e437 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralFormatUnitTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralFormatUnitTest.java @@ -15,13 +15,14 @@ import java.util.Map; import java.util.Set; import com.ibm.icu.dev.test.TestFmwk; +import com.ibm.icu.text.DecimalFormat; +import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.MessageFormat; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.PluralFormat; import com.ibm.icu.text.PluralRules; import com.ibm.icu.text.PluralRules.PluralType; import com.ibm.icu.text.PluralRules.SampleType; -import com.ibm.icu.text.UFieldPosition; import com.ibm.icu.util.ULocale; /** @@ -302,7 +303,7 @@ public class PluralFormatUnitTest extends TestFmwk { PluralFormat pf = new PluralFormat(ULocale.ENGLISH, pluralStyle); MessageFormat mf = new MessageFormat("{0,plural," + pluralStyle + "}", ULocale.ENGLISH); Integer args[] = new Integer[1]; - for (int i = 0; i < 7; ++i) { + for (int i = 0; i <= 7; ++i) { String result = pf.format(i); assertEquals("PluralFormat.format(value " + i + ")", targets[i], result); args[0] = i; @@ -346,43 +347,18 @@ public class PluralFormatUnitTest extends TestFmwk { assertEquals("PluralFormat.format(456)", "456th file", pf.format(456)); assertEquals("PluralFormat.format(111)", "111th file", pf.format(111)); } - - public void TestBasicFraction() { - String[][] tests = { - {"en", "one: j is 1"}, - {"1", "0", "1", "one"}, - {"1", "2", "1.00", "other"}, - }; - ULocale locale = null; - NumberFormat nf = null; - PluralRules pr = null; - - for (String[] row : tests) { - switch(row.length) { - case 2: - locale = ULocale.forLanguageTag(row[0]); - nf = NumberFormat.getInstance(locale); - pr = PluralRules.createRules(row[1]); - break; - case 4: - double n = Double.parseDouble(row[0]); - int minFracDigits = Integer.parseInt(row[1]); - nf.setMinimumFractionDigits(minFracDigits); - String expectedFormat = row[2]; - String expectedKeyword = row[3]; - - UFieldPosition pos = new UFieldPosition(); - String formatted = nf.format(1.0, new StringBuffer(), pos).toString(); - int countVisibleFractionDigits = pos.getCountVisibleFractionDigits(); - long fractionDigits = pos.getFractionDigits(); - String keyword = pr.select(n, countVisibleFractionDigits, fractionDigits); - assertEquals("Formatted " + n + "\t" + minFracDigits, expectedFormat, formatted); - assertEquals("Keyword " + n + "\t" + minFracDigits, expectedKeyword, keyword); - break; - default: - throw new RuntimeException(); - } - } - } + public void TestDecimals() { + // Simple number replacement. + PluralFormat pf = new PluralFormat(ULocale.ENGLISH, "one{one meter}other{# meters}"); + assertEquals("simple format(1)", "one meter", pf.format(1)); + assertEquals("simple format(1.5)", "1.5 meters", pf.format(1.5)); + + PluralFormat pf2 = new PluralFormat(ULocale.ENGLISH, + "offset:1 one{another meter}other{another # meters}"); + pf2.setNumberFormat(new DecimalFormat("0.0", new DecimalFormatSymbols(ULocale.ENGLISH))); + assertEquals("offset-decimals format(1)", "another 0.0 meters", pf2.format(1)); + assertEquals("offset-decimals format(2)", "another 1.0 meters", pf2.format(2)); + assertEquals("offset-decimals format(2.5)", "another 1.5 meters", pf2.format(2.5)); + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java index 2e5ecc14e9d..7514c0e27e0 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java @@ -33,7 +33,9 @@ import com.ibm.icu.dev.test.TestFmwk; import com.ibm.icu.dev.util.CollectionUtilities; import com.ibm.icu.dev.util.Relation; import com.ibm.icu.impl.Utility; +import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.PluralRules; +import com.ibm.icu.text.UFieldPosition; import com.ibm.icu.text.PluralRules.FixedDecimalRange; import com.ibm.icu.text.PluralRules.FixedDecimalSamples; import com.ibm.icu.text.PluralRules.KeywordStatus; @@ -123,7 +125,7 @@ public class PluralRulesTest extends TestFmwk { Class exception = shouldFailTest.length < 2 ? null : (Class) shouldFailTest[1]; Class actualException = null; try { - PluralRules test = PluralRules.parseDescription(rules); + PluralRules.parseDescription(rules); } catch (Exception e) { actualException = e.getClass(); } @@ -587,7 +589,6 @@ public class PluralRulesTest extends TestFmwk { integerSamples == null && decimalSamples != null && decimalSamples.samples.size() != 0); } else { if (!assertTrue(getAssertMessage("Test getSamples.isEmpty", locale, rules, keyword), !list.isEmpty())) { - int debugHere = 0; rules.getSamples(keyword); } if (rules.toString().contains(": j")) { @@ -698,6 +699,44 @@ public class PluralRulesTest extends TestFmwk { assertEquals("PluralRules(en-ordinal).select(2)", "two", pr.select(2)); } + public void TestBasicFraction() { + String[][] tests = { + {"en", "one: j is 1"}, + {"1", "0", "1", "one"}, + {"1", "2", "1.00", "other"}, + }; + ULocale locale = null; + NumberFormat nf = null; + PluralRules pr = null; + + for (String[] row : tests) { + switch(row.length) { + case 2: + locale = ULocale.forLanguageTag(row[0]); + nf = NumberFormat.getInstance(locale); + pr = PluralRules.createRules(row[1]); + break; + case 4: + double n = Double.parseDouble(row[0]); + int minFracDigits = Integer.parseInt(row[1]); + nf.setMinimumFractionDigits(minFracDigits); + String expectedFormat = row[2]; + String expectedKeyword = row[3]; + + UFieldPosition pos = new UFieldPosition(); + String formatted = nf.format(1.0, new StringBuffer(), pos).toString(); + int countVisibleFractionDigits = pos.getCountVisibleFractionDigits(); + long fractionDigits = pos.getFractionDigits(); + String keyword = pr.select(n, countVisibleFractionDigits, fractionDigits); + assertEquals("Formatted " + n + "\t" + minFracDigits, expectedFormat, formatted); + assertEquals("Keyword " + n + "\t" + minFracDigits, expectedKeyword, keyword); + break; + default: + throw new RuntimeException(); + } + } + } + public void TestLimitedAndSamplesConsistency() { for (ULocale locale : PluralRules.getAvailableULocales()) { ULocale loc2 = PluralRules.getFunctionalEquivalent(locale, null); @@ -714,8 +753,8 @@ public class PluralRulesTest extends TestFmwk { assertEquals(getAssertMessage("computeLimited == isLimited", locale, rules, keyword), computeLimited, isLimited); } Collection samples = rules.getSamples(keyword, sampleType); - FixedDecimalSamples decimalSamples = rules.getDecimalSamples(keyword, sampleType); assertNotNull(getAssertMessage("Samples must not be null", locale, rules, keyword), samples); + /*FixedDecimalSamples decimalSamples = */ rules.getDecimalSamples(keyword, sampleType); //assertNotNull(getAssertMessage("Decimal samples must be null if unlimited", locale, rules, keyword), decimalSamples); } } @@ -981,4 +1020,4 @@ public class PluralRulesTest extends TestFmwk { } logln("max \tsize:\t" + max); } -} \ No newline at end of file +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java index caa7f3271e2..c7fa56602ab 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java @@ -1861,4 +1861,73 @@ public class TestMessageFormat extends com.ibm.icu.dev.test.TestFmwk { assertEquals("plural-and-ordinal format(3) failed", "3 files, 3rd file", m.format(args, result, ignore).toString()); } + + public void TestDecimals() { + // Simple number replacement. + MessageFormat m = new MessageFormat( + "{0,plural,one{one meter}other{# meters}}", + ULocale.ENGLISH); + Object[] args = new Object[] { 1 }; + FieldPosition ignore = null; + StringBuffer result = new StringBuffer(); + assertEquals("simple format(1)", "one meter", + m.format(args, result, ignore).toString()); + + args[0] = 1.5; + result.delete(0, result.length()); + assertEquals("simple format(1.5)", "1.5 meters", + m.format(args, result, ignore).toString()); + + // Simple but explicit. + MessageFormat m0 = new MessageFormat( + "{0,plural,one{one meter}other{{0} meters}}", + ULocale.ENGLISH); + args[0] = 1; + result.delete(0, result.length()); + assertEquals("explicit format(1)", "one meter", + m0.format(args, result, ignore).toString()); + + args[0] = 1.5; + result.delete(0, result.length()); + assertEquals("explicit format(1.5)", "1.5 meters", + m0.format(args, result, ignore).toString()); + + // With offset and specific simple format with optional decimals. + MessageFormat m1 = new MessageFormat( + "{0,plural,offset:1 one{another meter}other{{0,number,00.#} meters}}", + ULocale.ENGLISH); + args[0] = 1; + result.delete(0, result.length()); + assertEquals("offset format(1)", "01 meters", + m1.format(args, result, ignore).toString()); + + args[0] = 2; + result.delete(0, result.length()); + assertEquals("offset format(1)", "another meter", + m1.format(args, result, ignore).toString()); + + args[0] = 2.5; + result.delete(0, result.length()); + assertEquals("offset format(1)", "02.5 meters", + m1.format(args, result, ignore).toString()); + + // With offset and specific simple format with forced decimals. + MessageFormat m2 = new MessageFormat( + "{0,plural,offset:1 one{another meter}other{{0,number,0.0} meters}}", + ULocale.ENGLISH); + args[0] = 1; + result.delete(0, result.length()); + assertEquals("offset-decimals format(1)", "1.0 meters", + m2.format(args, result, ignore).toString()); + + args[0] = 2; + result.delete(0, result.length()); + assertEquals("offset-decimals format(1)", "2.0 meters", + m2.format(args, result, ignore).toString()); + + args[0] = 2.5; + result.delete(0, result.length()); + assertEquals("offset-decimals format(1)", "2.5 meters", + m2.format(args, result, ignore).toString()); + } } -- 2.40.0