]> granicus.if.org Git - icu/commitdiff
ICU-8474 support plurals with decimals in MessageFormat and PluralFormat
authorMarkus Scherer <markus.icu@gmail.com>
Mon, 26 Aug 2013 20:29:07 +0000 (20:29 +0000)
committerMarkus Scherer <markus.icu@gmail.com>
Mon, 26 Aug 2013 20:29:07 +0000 (20:29 +0000)
X-SVN-Rev: 34087

icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/PluralFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralFormatUnitTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java

index 79ae7ce5268545909714ecf0c19f008c50218af7..e82b04832421b4b068fc15bc1d768367fd4b663c 100644 (file)
@@ -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);
index 7a46971fee9f4baab1c1c30e49b46345bf6f2cec..a1446996bc29c13e29b55c54c48e5b8e009d13ea 100644 (file)
@@ -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 {
      * <p>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<String, Object> 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<String, Object> 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(++partIndex<count);
+        return 0;
+    }
+
+    /**
+     * Returns the ARG_START index of the first occurrence of the plural number in a sub-message.
+     * Returns -1 if it is a REPLACE_NUMBER.
+     * Returns 0 if there is neither.
+     */
+    private int findFirstPluralNumberArg(int msgStart, String argName) {
+        for(int i=msgStart+1;; ++i) {
+            Part part=msgPattern.getPart(i);
+            Part.Type type=part.getType();
+            if(type==Part.Type.MSG_LIMIT) {
+                return 0;
+            }
+            if(type==Part.Type.REPLACE_NUMBER) {
+                return -1;
+            }
+            if(type==Part.Type.ARG_START) {
+                ArgType argType=part.getArgType();
+                if(argName.length()!=0 && (argType==ArgType.NONE || argType==ArgType.SIMPLE)) {
+                    part=msgPattern.getPart(i+1);  // ARG_NUMBER or ARG_NAME
+                    if(msgPattern.partSubstringMatches(part, argName)) {
+                        return i;
+                    }
+                }
+                i=msgPattern.getLimitPartIndex(i);
+            }
+        }
+    }
+
+    /**
+     * Mutable input/output values for the PluralSelectorProvider.
+     * Separate so that it is possible to make MessageFormat Freezable.
+     */
+    private static final class PluralSelectorContext {
+        private PluralSelectorContext(int start, String name, Number num, double off) {
+            startIndex = start;
+            argName = name;
+            // number needs to be set even when select() is not called.
+            // Keep it as a Number/Formattable:
+            // For format() methods, and to preserve information (e.g., BigDecimal).
+            if(off == 0) {
+                number = num;
+            } else {
+                number = num.doubleValue() - off;
+            }
+            offset = off;
+        }
+        @Override
+        public String toString() {
+            throw new AssertionError("PluralSelectorContext being formatted, rather than its number");
+        }
+
+        // Input values for plural selection with decimals.
+        int startIndex;
+        String argName;
+        /** argument number - plural offset */
+        Number number;
+        double offset;
+        // Output values for plural selection with decimals.
+        /** -1 if REPLACE_NUMBER, 0 arg not found, >0 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<AttributeAndPosition> attributes;
index 9c23ed94d9ca1f940775f83235cd322de4d43a31..a118b57602fd997593aa0af0cbc9c93c4592104e 100644 (file)
@@ -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 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 <code>StringBuffer</code>.
+     * @param number a number object (instance of <code>Number</code> for which
+     *        the plural message should be formatted. If no pattern has been
+     *        applied to this <code>PluralFormat</code> object yet, the
+     *        formatted number will be returned.
+     *        Note: If this object is not an instance of <code>Number</code>,
+     *              the <code>toAppendTo</code> will not be modified.
+     * @param toAppendTo the formatted message will be appended to this
+     *        <code>StringBuffer</code>.
+     * @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 <code>StringBuffer</code>.
-     * @param number a number object (instance of <code>Number</code> for which
-     *        the plural message should be formatted. If no pattern has been
-     *        applied to this <code>PluralFormat</code> object yet, the
-     *        formatted number will be returned.
-     *        Note: If this object is not an instance of <code>Number</code>,
-     *              the <code>toAppendTo</code> will not be modified.
-     * @param toAppendTo the formatted message will be appended to this
-     *        <code>StringBuffer</code>.
-     * @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 <code>PluralFormat</code>.
      * @param text the string to be parsed.
index 5851fa34ae7f7aabc9cdc1a80329abdc05d45bd0..df9329561588bc4eb24d987bcca1665850475e61 100644 (file)
@@ -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;
 
index 88300be61dafe9b94a60e08650a0eadce2503bc6..e5c5880e43789037289c8ebdbcce32cc7865a94f 100644 (file)
@@ -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));
+    }
 }
index 2e5ecc14e9ddac973a3957d6c8ff2cc3d8ca71db..7514c0e27e0f17ed5b12803fd9bf0a6e0bdea878 100644 (file)
@@ -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<Double> 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
+}
index caa7f3271e27858de7840f9bda0cded3fb9edd9b..c7fa56602ab9f001fa2e3d0b2fd8663cddb55a61 100644 (file)
@@ -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());
+    }
 }