]> granicus.if.org Git - icu/commitdiff
ICU-8474 updates to fix new rules, separate out test generation class, factory class...
authorMark Davis <mark@macchiato.com>
Mon, 8 Apr 2013 14:11:49 +0000 (14:11 +0000)
committerMark Davis <mark@macchiato.com>
Mon, 8 Apr 2013 14:11:49 +0000 (14:11 +0000)
X-SVN-Rev: 33499

icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesFactory.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/WritePluralRulesData.java [new file with mode: 0644]
icu4j/main/tests/framework/src/com/ibm/icu/dev/util/CollectionUtilities.java
icu4j/main/tests/translit/src/com/ibm/icu/dev/util/UnicodeProperty.java

index 63fde87f06f09aea78b458793e9cdaa890c02c93..dc67ad7b0341a623c3b5c324d99b9d547d07e51b 100644 (file)
@@ -20,6 +20,7 @@ import com.ibm.icu.impl.SimpleCache;
 import com.ibm.icu.text.DateIntervalInfo.PatternInfo;
 import com.ibm.icu.util.Calendar;
 import com.ibm.icu.util.DateInterval;
+import com.ibm.icu.util.Output;
 import com.ibm.icu.util.ULocale;
 import com.ibm.icu.util.ULocale.Category;
 
@@ -603,7 +604,43 @@ public class DateIntervalFormat extends UFormat {
         return format(fFromCalendar, fToCalendar, appendTo, fieldPosition);
     }
 
-
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public String getPatterns(Calendar fromCalendar,
+            Calendar toCalendar, 
+            Output<String> part2) {
+        // First, find the largest different calendar field.
+        int field;
+        if ( fromCalendar.get(Calendar.ERA) != toCalendar.get(Calendar.ERA) ) {
+            field = Calendar.ERA;
+        } else if ( fromCalendar.get(Calendar.YEAR) != 
+                    toCalendar.get(Calendar.YEAR) ) {
+            field = Calendar.YEAR;
+        } else if ( fromCalendar.get(Calendar.MONTH) !=
+                    toCalendar.get(Calendar.MONTH) ) {
+            field = Calendar.MONTH;
+        } else if ( fromCalendar.get(Calendar.DATE) !=
+                    toCalendar.get(Calendar.DATE) ) {
+            field = Calendar.DATE;
+        } else if ( fromCalendar.get(Calendar.AM_PM) !=
+                    toCalendar.get(Calendar.AM_PM) ) {
+            field = Calendar.AM_PM;
+        } else if ( fromCalendar.get(Calendar.HOUR) !=
+                    toCalendar.get(Calendar.HOUR) ) {
+            field = Calendar.HOUR;
+        } else if ( fromCalendar.get(Calendar.MINUTE) !=
+                    toCalendar.get(Calendar.MINUTE) ) {
+            field = Calendar.MINUTE;
+        } else {
+            return null;
+        }
+        PatternInfo intervalPattern = fIntervalPatterns.get(
+                DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field]);
+        part2.value = intervalPattern.getSecondPart();
+        return intervalPattern.getFirstPart();
+    }
     /**
      * Format 2 Calendars to produce a string. 
      *
index 2daa5c0a4d0f33efd65ca4f701c6c26cb7656dd7..171aa0ad7e873dd018561a296bb2ac9b9edb882d 100644 (file)
@@ -10,16 +10,19 @@ package com.ibm.icu.text;
 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.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
 
 import com.ibm.icu.impl.PatternProps;
 import com.ibm.icu.impl.PluralRulesLoader;
@@ -30,56 +33,48 @@ import com.ibm.icu.util.ULocale;
 /**
  * <p>
  * Defines rules for mapping non-negative numeric values onto a small set of keywords.
- * 
+ * </p>
  * <p>
  * Rules are constructed from a text description, consisting of a series of keywords and conditions. The {@link #select}
  * method examines each condition in order and returns the keyword for the first condition that matches the number. If
  * none match, {@link #KEYWORD_OTHER} is returned.
  * </p>
- * 
  * <p>
  * A PluralRules object is immutable. It contains caches for sample values, but those are synchronized.
- * 
  * <p>
  * PluralRules is Serializable so that it can be used in formatters, which are serializable.
- * 
+ * </p>
  * <p>
  * For more information, details, and tips for writing rules, see the <a
  * href="http://www.unicode.org/draft/reports/tr35/tr35.html#Language_Plural_Rules">LDML spec, C.11 Language Plural
  * Rules</a>
  * </p>
- * 
  * <p>
  * Examples:
+ * </p>
  * 
  * <pre>
  * &quot;one: n is 1; few: n in 2..4&quot;
  * </pre>
- * 
- * </p>
  * <p>
  * This defines two rules, for 'one' and 'few'. The condition for 'one' is "n is 1" which means that the number must be
  * equal to 1 for this condition to pass. The condition for 'few' is "n in 2..4" which means that the number must be
  * between 2 and 4 inclusive - and be an integer - for this condition to pass. All other numbers are assigned the
  * keyword "other" by the default rule.
  * </p>
- * <p>
  * 
  * <pre>
  * &quot;zero: n is 0; one: n is 1; zero: n mod 100 in 1..19&quot;
  * </pre>
- * 
+ * <p>
  * This illustrates that the same keyword can be defined multiple times. Each rule is examined in order, and the first
  * keyword whose condition passes is the one returned. Also notes that a modulus is applied to n in the last rule. Thus
  * its condition holds for 119, 219, 319...
  * </p>
- * <p>
  * 
  * <pre>
  * &quot;one: n is 1; few: n mod 10 in 2..4 and n mod 100 not in 12..14&quot;
  * </pre>
- * 
- * </p>
  * <p>
  * This illustrates conjunction and negation. The condition for 'few' has two parts, both of which must be met:
  * "n mod 10 in 2..4" and "n mod 100 not in 12..14". The first part applies a modulus to n before the test as in the
@@ -88,7 +83,7 @@ import com.ibm.icu.util.ULocale;
  * </p>
  * <p>
  * Syntax:
- * 
+ * </p>
  * <pre>
  * rules         = rule (';' rule)*
  * rule          = keyword ':' condition
@@ -101,53 +96,59 @@ import com.ibm.icu.util.ULocale;
  * within_relation = expr ('not')? 'within' range_list
  * expr          = ('n' | 'i' | 'f' | 'v') ('mod' value)?
  * range_list    = (range | value) (',' range_list)*
- * value         = digit+
+ * value         = digit+ ('.' digit+)?
  * digit         = 0|1|2|3|4|5|6|7|8|9
  * range         = value'..'value
  * </pre>
  * 
- * </p>
  * <p>
- * The i, f, and v values are defined as follows.
+ * The i, f, and v values are defined as follows:
  * </p>
  * <ul>
- * <li>v to be the number of visible fraction digits.</li>
- * <li>f to be the visible fractional digits.</li>
  * <li>i to be the integer digits.</li>
+ * <li>f to be the visible fractional digits, as an integer.</li>
+ * <li>v to be the number of visible fraction digits.</li>
+ * <li>j is defined to only match integers. That is j is 3 fails if v != 0 (eg for 3.1 or 3.0).</li>
  * </ul>
  * <p>
  * Examples are in the following table:
  * </p>
- * <table border='1'>
+ * <table border='1' style="border-collapse:collapse">
  * <tbody>
  * <tr>
- * <td>n</td>
- * <td>f</td>
- * <td>v</td>
+ * <th>n</th>
+ * <th>i</th>
+ * <th>f</th>
+ * <th>v</th>
  * </tr>
  * <tr>
  * <td>1.0</td>
- * <td>0</td>
+ * <td>1</td>
+ * <td align="right">0</td>
  * <td>1</td>
  * </tr>
  * <tr>
  * <td>1.00</td>
- * <td>0</td>
+ * <td>1</td>
+ * <td align="right">0</td>
  * <td>2</td>
  * </tr>
  * <tr>
  * <td>1.3</td>
- * <td>3</td>
+ * <td>1</td>
+ * <td align="right">3</td>
  * <td>1</td>
  * </tr>
  * <tr>
  * <td>1.03</td>
- * <td>3</td>
+ * <td>1</td>
+ * <td align="right">3</td>
  * <td>2</td>
  * </tr>
  * <tr>
  * <td>1.23</td>
- * <td>23</td>
+ * <td>1</td>
+ * <td align="right">23</td>
  * <td>2</td>
  * </tr>
  * </tbody>
@@ -164,6 +165,16 @@ import com.ibm.icu.util.ULocale;
  * @stable ICU 3.8
  */
 public class PluralRules implements Serializable {
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public static final String CATEGORY_SEPARATOR = ";  ";
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public static final String KEYWORD_RULE_SEPARATOR = ": ";
 
     private static final long serialVersionUID = 1;
 
@@ -173,6 +184,9 @@ public class PluralRules implements Serializable {
     private transient int hashCode;
     private transient Map<String, List<Double>> _keySamplesMap;
     private transient Map<String, Boolean> _keyLimitedMap;
+    private transient Map<String, Set<NumberInfo>> _keyFractionSamplesMap;
+    private transient Set<NumberInfo> _fractionSamples;
+
 
     // Standard keywords.
 
@@ -262,6 +276,11 @@ public class PluralRules implements Serializable {
         public int updateRepeatLimit(int limit) {
             return limit;
         }
+
+        public void getMentionedValues(Set<NumberInfo> toAddTo) {
+            toAddTo.add(new NumberInfo(0));
+            toAddTo.add(new NumberInfo(9999.9999));
+        }
     };
 
     /*
@@ -283,12 +302,19 @@ public class PluralRules implements Serializable {
         }
 
         public String toString() {
-            return "(" + KEYWORD_OTHER + ")";
+            return "";
         }
 
         public int updateRepeatLimit(int limit) {
             return limit;
         }
+
+        public void getMentionedValues(Set<NumberInfo> toAddTo) {
+        }
+
+        public String getConstraint() {
+            return null;
+        }
     };
 
     /**
@@ -332,35 +358,116 @@ public class PluralRules implements Serializable {
         }
     }
 
-    private static class NumberInfo {
-        private static final String OPERAND_LIST = "nifv";
+    private enum Operand {
+        n,
+        i,
+        f,
+        v,
+        j;
+    }
+
+    /**
+     * @deprecated This API is ICU internal only.
+     * @internal
+     */
+    public static class NumberInfo implements Comparable<NumberInfo> {
+        public final double source;
+        public final int fractionalDigits;
+        public final int visibleFractionDigitCount;
+        public final double intValue;
+
+        public NumberInfo(double n, int v, int f) {
+            source = n;
+            visibleFractionDigitCount = v;
+            fractionalDigits = f;
+            intValue = (long)n;
+        }
+
+        // Ugly, but for samples we don't care.
+        public NumberInfo(double n, int v) {
+            this(n,v,getFractionalDigits(n, v));
+        }
+
+        // Ugly, but for samples we don't care.
+        public static int decimals(double n) {
+            String temp = String.valueOf(n);
+            return temp.endsWith(".0") ? 0 : temp.length() - temp.indexOf('.') - 1;
+        }
+
+        // Ugly, but for samples we don't care.
+        public NumberInfo (String n) {
+            this(Double.parseDouble(n), getVisibleFractionCount(n));
+        }
+
+        private static int getFractionalDigits(double n, int v) {
+            if (v == 0) {
+                return 0;
+            } else {
+                int base = (int) Math.pow(10, v);
+                long scaled = Math.round(n * base);
+                return (int) (scaled % base);
+            }
+        }
+
+        private static int getVisibleFractionCount(String value) {
+            int decimalPos = value.trim().indexOf('.') + 1;
+            if (decimalPos == 0) {
+                return 0;
+            } else {
+                return value.length() - decimalPos - 1;
+            }
+        }
 
-        public NumberInfo(double number, int countVisibleFractionDigits, int fractionalDigits) {
-            source = number;
-            intValue = (long)number;
-            this.fractionalDigits = fractionalDigits;
-            this.countVisibleFractionDigits = countVisibleFractionDigits;
+        public NumberInfo(double n) {
+            this(n, decimals(n));
         }
-        public NumberInfo(double number) {
-            this(number, 0, 0);
+
+        public NumberInfo(long n) {
+            this(n,0);
         }
-        final double source;
-        final double intValue;
-        final double fractionalDigits;
-        final double countVisibleFractionDigits;
 
-        public double get(int operand) {
+        public double get(Operand operand) {
             switch(operand) {
             default: return source;
-            case 1: return intValue;
-            case 2: return fractionalDigits;
-            case 3: return countVisibleFractionDigits;
+            case i: return intValue;
+            case f: return fractionalDigits;
+            case v: return visibleFractionDigitCount;
+            }
+        }
+
+        public static Operand getOperand(String t) {
+            return Operand.valueOf(t);
+        }
+
+        /**
+         * We're not going to care about NaN.
+         */
+        public int compareTo(NumberInfo other) {
+            if (source != other.source) {
+                return source < other.source ? -1 : 1;
             }
+            if (visibleFractionDigitCount != other.visibleFractionDigitCount) {
+                return visibleFractionDigitCount < other.visibleFractionDigitCount ? -1 : 1;
+            }
+            return fractionalDigits - other.fractionalDigits;
+        }
+        @Override
+        public boolean equals(Object arg0) {
+            NumberInfo other = (NumberInfo)arg0;
+            return source == other.source && visibleFractionDigitCount == other.visibleFractionDigitCount && fractionalDigits == other.fractionalDigits;
         }
-        public static int getOperand(String t) {
-            return OPERAND_LIST.indexOf(t);
+        @Override
+        public int hashCode() {
+            // TODO Auto-generated method stub
+            return fractionalDigits + 37 * (visibleFractionDigitCount + (int)(37 * source));
+        }
+        @Override
+        public String toString() {
+            return String.format("%." + visibleFractionDigitCount + "f", source);
         }
     }
+
+
     /*
      * A constraint on a number.
      */
@@ -386,6 +493,12 @@ public class PluralRules implements Serializable {
          * @return the new limit
          */
         int updateRepeatLimit(int limit);
+
+        /**
+         * Gets samples of significant numbers
+         */
+        void getMentionedValues(Set<NumberInfo> toAddTo);
+
     }
 
     /*
@@ -403,6 +516,13 @@ public class PluralRules implements Serializable {
 
         /* Returns the larger of limit and this rule's limit. */
         int updateRepeatLimit(int limit);
+
+        /**
+         * Gets samples of significant numbers
+         */
+        void getMentionedValues(Set<NumberInfo> toAddTo);
+
+        public String getConstraint();
     }
 
     /*
@@ -421,6 +541,15 @@ public class PluralRules implements Serializable {
         /* Return true if the values for this keyword are limited. */
         boolean isLimited(String keyword);
 
+        /**
+         * Get mentioned samples
+         */
+        Set<NumberInfo> getMentionedValues(Set<NumberInfo> toAddTo);
+
+        /**
+         * keyword: rules mapping
+         */
+        String getRules(String keyword);
     }
 
     /*
@@ -465,16 +594,18 @@ public class PluralRules implements Serializable {
                 int mod = 0;
                 boolean inRange = true;
                 boolean integersOnly = true;
-                long lowBound = Long.MAX_VALUE;
-                long highBound = Long.MIN_VALUE;
-                long[] vals = null;
+                double lowBound = Long.MAX_VALUE;
+                double highBound = Long.MIN_VALUE;
+                double[] vals = null;
 
                 boolean isRange = false;
 
                 int x = 0;
                 String t = tokens[x++];
-                int operand = NumberInfo.getOperand(t);
-                if (operand < 0) {
+                Operand operand;
+                try {
+                    operand = NumberInfo.getOperand(t);
+                } catch (Exception e) {
                     throw unexpected(t, condition);
                 }
                 if (x < tokens.length) {
@@ -507,11 +638,11 @@ public class PluralRules implements Serializable {
 
                     if (isRange) {
                         String[] range_list = Utility.splitString(t, ",");
-                        vals = new long[range_list.length * 2];
+                        vals = new double[range_list.length * 2];
                         for (int k1 = 0, k2 = 0; k1 < range_list.length; ++k1, k2 += 2) {
                             String range = range_list[k1];
                             String[] pair = Utility.splitString(range, "..");
-                            long low, high;
+                            double low, high;
                             if (pair.length == 2) {
                                 low = Long.parseLong(pair[0]);
                                 high = Long.parseLong(pair[1]);
@@ -638,25 +769,65 @@ public class PluralRules implements Serializable {
         private final int mod;
         private final boolean inRange;
         private final boolean integersOnly;
-        private final long lowerBound;
-        private final long upperBound;
-        private final long[] range_list;
-        private final int operand;
+        private final double lowerBound;
+        private final double upperBound;
+        private final double[] range_list;
+        private final Operand operand;
 
-        RangeConstraint(int mod, boolean inRange, int operand, boolean integersOnly,
-                long lowerBound, long upperBound, long[] range_list) {
+        RangeConstraint(int mod, boolean inRange, Operand operand, boolean integersOnly,
+                double lowBound, double highBound, double[] vals) {
             this.mod = mod;
             this.inRange = inRange;
             this.integersOnly = integersOnly;
-            this.lowerBound = lowerBound;
-            this.upperBound = upperBound;
-            this.range_list = range_list;
+            this.lowerBound = lowBound;
+            this.upperBound = highBound;
+            this.range_list = vals;
             this.operand = operand;
         }
 
+        public void getMentionedValues(Set<NumberInfo> toAddTo) {
+            addRanges(toAddTo, mod);
+            if (mod != 0) {
+                addRanges(toAddTo, mod*2);
+                addRanges(toAddTo, mod*3);
+            }
+        }
+
+        private void addRanges(Set<NumberInfo> toAddTo, int offset) {
+            toAddTo.add(new NumberInfo(lowerBound + offset));
+            if (upperBound != lowerBound) {
+                toAddTo.add(new NumberInfo(upperBound + offset));
+            }
+            if (range_list != null) {
+                // we will just add one value from the middle
+                for (double value : range_list) {
+                    if (value == lowerBound || value == upperBound) {
+                        continue;
+                    }
+                    toAddTo.add(new NumberInfo(value + offset)); 
+                    break;
+                }
+            }
+            if (!integersOnly) {
+                double average = (lowerBound + upperBound) / 2.0d;
+                toAddTo.add(new NumberInfo(average + offset));
+                if (range_list != null) {
+                    // we will just add one value from the middle
+                    for (double value : range_list) {
+                        if (value == lowerBound || value == upperBound) {
+                            continue;
+                        }
+                        toAddTo.add(new NumberInfo(value + 0.33 + offset)); 
+                        break;
+                    }
+                }
+            }
+        }
+
         public boolean isFulfilled(NumberInfo number) {
             double n = number.get(operand);
-            if (integersOnly && (n - (long)n) != 0.0) {
+            if ((integersOnly && (n - (long)n) != 0.0
+                    || operand == Operand.j && number.visibleFractionDigitCount != 0)) {
                 return !inRange;
             }
             if (mod != 0) {
@@ -682,73 +853,59 @@ public class PluralRules implements Serializable {
         }
 
         public String toString() {
-            class ListBuilder {
-                StringBuilder sb = new StringBuilder("[");
-                ListBuilder add(String s) {
-                    return add(s, null);
-                }
-                ListBuilder add(String s, Object o) {
-                    if (sb.length() > 1) {
-                        sb.append(", ");
-                    }
-                    sb.append(s);
-                    if (o != null) {
-                        sb.append(": ").append(o.toString());
-                    }
-                    return this;
-                }
-                public String toString() {
-                    String s = sb.append(']').toString();
-                    sb = null;
-                    return s;
-                }
-            }
-            ListBuilder lb = new ListBuilder();
-            lb.add(NumberInfo.OPERAND_LIST.substring(operand, operand+1));
-            if (mod > 1) {
-                lb.add("mod", mod);
-            }
-            if (inRange) {
-                lb.add("in");
-            } else {
-                lb.add("except");
-            }
-            if (integersOnly) {
-                lb.add("ints");
-            }
-            if (lowerBound == upperBound) {
-                lb.add(String.valueOf(lowerBound));
-            } else {
-                lb.add(String.valueOf(lowerBound) + "-" + String.valueOf(upperBound));
+            StringBuilder result = new StringBuilder();
+            result.append(operand);
+            if (mod != 0) {
+                result.append(" mod ").append(mod);
             }
+            boolean isList = lowerBound != upperBound;
+            result.append(
+                    !isList ? (inRange ? " is " : " is not ")
+                            : integersOnly ? (inRange ? " in " : " not in ")
+                                    : (inRange ? " within " : " not within ") 
+                    );
             if (range_list != null) {
-                lb.add(Arrays.toString(range_list));
+                for (int i = 0; i < range_list.length; i += 2) {
+                    addRange(result, range_list[i], range_list[i+1], i != 0);
+                }
+            } else {
+                addRange(result, lowerBound, upperBound, false);
             }
-            return lb.toString();
+            return result.toString();
         }
     }
 
+    private static void addRange(StringBuilder result, double lb, double ub, boolean addSeparator) {
+        if (addSeparator) {
+            result.append(",");
+        }
+        if (lb == ub) {
+            result.append(format(lb));
+        } else {
+            result.append(format(lb) + ".." + format(ub));
+        }
+    }
+
+    private static String format(double lb) {
+        long lbi = (long) lb;
+        return lb == lbi ? String.valueOf(lbi) : String.valueOf(lb);
+    }
+
     /* Convenience base class for and/or constraints. */
     private static abstract class BinaryConstraint implements Constraint,
     Serializable {
         private static final long serialVersionUID = 1;
         protected final Constraint a;
         protected final Constraint b;
-        private final String conjunction;
 
-        protected BinaryConstraint(Constraint a, Constraint b, String c) {
+        protected BinaryConstraint(Constraint a, Constraint b) {
             this.a = a;
             this.b = b;
-            this.conjunction = c;
         }
 
         public int updateRepeatLimit(int limit) {
             return a.updateRepeatLimit(b.updateRepeatLimit(limit));
         }
-
-        public String toString() {
-            return a.toString() + conjunction + b.toString();
-        }
     }
 
     /* A constraint representing the logical and of two constraints. */
@@ -756,7 +913,7 @@ public class PluralRules implements Serializable {
         private static final long serialVersionUID = 7766999779862263523L;
 
         AndConstraint(Constraint a, Constraint b) {
-            super(a, b, " && ");
+            super(a, b);
         }
 
         public boolean isFulfilled(NumberInfo n) {
@@ -768,6 +925,15 @@ public class PluralRules implements Serializable {
             // satisfy both-- we still consider this 'unlimited'
             return a.isLimited() || b.isLimited();
         }
+
+        public void getMentionedValues(Set<NumberInfo> toAddTo) {
+            a.getMentionedValues(toAddTo);
+            b.getMentionedValues(toAddTo);
+        }
+
+        public String toString() {
+            return a.toString() + " and " + b.toString();
+        }
     }
 
     /* A constraint representing the logical or of two constraints. */
@@ -775,7 +941,7 @@ public class PluralRules implements Serializable {
         private static final long serialVersionUID = 1405488568664762222L;
 
         OrConstraint(Constraint a, Constraint b) {
-            super(a, b, " || ");
+            super(a, b);
         }
 
         public boolean isFulfilled(NumberInfo n) {
@@ -785,6 +951,14 @@ public class PluralRules implements Serializable {
         public boolean isLimited() {
             return a.isLimited() && b.isLimited();
         }
+
+        public void getMentionedValues(Set<NumberInfo> toAddTo) {
+            a.getMentionedValues(toAddTo);
+            b.getMentionedValues(toAddTo);
+        }
+        public String toString() {
+            return a.toString() + " or " + b.toString();
+        }
     }
 
     /*
@@ -828,7 +1002,18 @@ public class PluralRules implements Serializable {
         }
 
         public String toString() {
-            return keyword + ": " + constraint;
+            return keyword + ": " + constraint.toString();
+        }
+
+        public String getConstraint() {
+            return constraint.toString();
+        }
+
+        /**
+         * Gets samples of significant numbers
+         */
+        public void getMentionedValues(Set<NumberInfo> toAddTo) {
+            constraint.getMentionedValues(toAddTo);
         }
     }
 
@@ -913,14 +1098,131 @@ public class PluralRules implements Serializable {
         }
 
         public String toString() {
-            String s = rule.toString();
+            StringBuilder builder = new StringBuilder();
+            Map<String, String> ordered = new TreeMap<String, String>(KEYWORD_COMPARATOR);
+            for (RuleChain current = this; current != null; current = current.next) {
+                String keyword = current.rule.getKeyword();
+                String constraint = current.rule.getConstraint();
+                ordered.put(keyword, constraint);
+            }
+            for (Entry<String, String> entry : ordered.entrySet()) {
+                if (builder.length() != 0) {
+                    builder.append(CATEGORY_SEPARATOR);
+                }
+                builder.append(entry.getKey()).append(KEYWORD_RULE_SEPARATOR).append(entry.getValue());
+            }
+            return builder.toString();
+        }
+
+        /* (non-Javadoc)
+         * @see com.ibm.icu.text.PluralRules.RuleList#getMentionedSamples(java.util.Set)
+         */
+        public Set<NumberInfo> getMentionedValues(Set<NumberInfo> toAddTo) {
+            rule.getMentionedValues(toAddTo);
             if (next != null) {
-                s = next.toString() + "; " + s;
+                next.getMentionedValues(toAddTo);
+            } else {
+                // once done, manufacture values for the OTHER case
+                int otherCount = 3;
+                NumberInfo last = null;
+                Set<NumberInfo> others = new LinkedHashSet<NumberInfo>();
+                for (NumberInfo s : toAddTo) {
+                    double trial;
+                    if (last == null) {
+                        trial = s.source-0.5;
+                    } else {
+                        double diff = s.source - last.source;
+                        if (diff > 1.0d) {
+                            trial = Math.floor(s.source);
+                            if (trial == s.source) {
+                                --trial;
+                            }
+                        } else {
+                            trial = (s.source + last.source) / 2;
+                        }                     
+                    }
+                    if (trial >= 0) {
+                        addConditional(toAddTo, others, trial);
+                    }
+                    last = s;
+                }
+                double trial = last == null ? 0 : last.source;
+                double fraction = 0;
+                while (otherCount > 0) {
+                    if (addConditional(toAddTo, others, trial = trial * 2 + 1 + fraction)) {
+                        --otherCount;
+                    }
+                    fraction += 0.125;
+                }
+                toAddTo.addAll(others);
+            }
+            toAddTo.add(new NumberInfo(0)); // always there
+            toAddTo.add(new NumberInfo(0,1)); // always there
+            toAddTo.add(new NumberInfo(0.1,1)); // always there
+            toAddTo.add(new NumberInfo(1)); // always there
+            toAddTo.add(new NumberInfo(1,2)); // always there
+            toAddTo.add(new NumberInfo(1.01,2)); // always there
+            toAddTo.add(new NumberInfo(2,2)); // always there
+            toAddTo.add(new NumberInfo(2.01,2)); // always there
+            toAddTo.add(new NumberInfo(2.10,2)); // always there
+            return toAddTo;
+        }
+
+        private boolean addConditional(Set<NumberInfo> toAddTo, Set<NumberInfo> others, double trial) {
+            boolean added;
+            NumberInfo toAdd = new NumberInfo(trial);
+            if (!toAddTo.contains(toAdd) && !others.contains(toAdd)) {
+                others.add(toAdd);
+                added = true;
+            } else {
+                added = false;
+            }
+            return added;
+        }
+
+        public String getRules(String keyword) {
+            for (RuleChain current = this; current != null; current = current.next) {
+                if (current.rule.getKeyword().equals(keyword)) {
+                    return current.rule.getConstraint();
+                }
+            }
+            return null;
+        }
+    }
+
+    enum StandardPluralCategories {
+        zero,
+        one,
+        two,
+        few,
+        many,
+        other;
+        static StandardPluralCategories forString(String s) {
+            StandardPluralCategories a;
+            try {
+                a = valueOf(s);
+            } catch (Exception e) {
+                return null;
             }
-            return s;
+            return a;
         }
     }
 
+    /**
+     * @deprecated This API is ICU internal only.
+     * @internal
+     */
+    public static final Comparator<String> KEYWORD_COMPARATOR = new Comparator<String> () {
+        public int compare(String arg0, String arg1) {
+            StandardPluralCategories a = StandardPluralCategories.forString(arg0);
+            StandardPluralCategories b = StandardPluralCategories.forString(arg1);
+            return a == null 
+                    ? (b == null ? arg0.compareTo(arg1) : -1)
+                            : (b == null ? 1 : a.compareTo(b));
+        }
+    };
+
+
     // -------------------------------------------------------------------------
     // Static class methods.
     // -------------------------------------------------------------------------
@@ -985,7 +1287,9 @@ public class PluralRules implements Serializable {
      */
     private PluralRules(RuleList rules) {
         this.rules = rules;
-        this.keywords = Collections.unmodifiableSet(rules.getKeywords());
+        TreeSet<String> temp = new TreeSet<String>(KEYWORD_COMPARATOR);
+        temp.addAll(rules.getKeywords());
+        this.keywords = Collections.unmodifiableSet(new LinkedHashSet<String>(temp));
     }
 
     /**
@@ -1007,10 +1311,24 @@ public class PluralRules implements Serializable {
      * @param number The number for which the rule has to be determined.
      * @return The keyword of the selected rule.
      * @internal
+     * @deprecated This API is ICU internal only.
      */
     public String select(double number, int countVisibleFractionDigits, int fractionaldigits) {
         return rules.select(new NumberInfo(number, countVisibleFractionDigits, fractionaldigits));
     }
+    
+    /**
+     * Given a number, returns the keyword of the first rule that applies to
+     * the number.
+     *
+     * @param number The number for which the rule has to be determined.
+     * @return The keyword of the selected rule.
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public String select(NumberInfo sample) {
+        return rules.select(new NumberInfo(sample.source, sample.visibleFractionDigitCount, sample.fractionalDigits));
+    }
 
     /**
      * Returns a set of all rule keywords used in this <code>PluralRules</code>
@@ -1085,6 +1403,36 @@ public class PluralRules implements Serializable {
         return getKeySamplesMap().get(keyword);
     }
 
+    /**
+     * Returns a list of values for which select() would return that keyword,
+     * or null if the keyword is not defined. The returned collection is unmodifiable.
+     * The returned list is not complete, and there might be additional values that
+     * would return the keyword.
+     *
+     * @param keyword the keyword to test
+     * @return a list of values matching the keyword.
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public Collection<NumberInfo> getFractionSamples(String keyword) {
+        if (!keywords.contains(keyword)) {
+            return null;
+        }
+        initKeyMaps();
+        return _keyFractionSamplesMap.get(keyword);
+    }
+
+    /**
+     * Returns a list of values that includes at least one value for each keyword.
+     *
+     * @return a list of values
+     * @internal
+     */
+    public Collection<NumberInfo> getFractionSamples() {
+        initKeyMaps();
+        return _fractionSamples;
+    }
+
     private Map<String, Boolean> getKeyLimitedMap() {
         initKeyMaps();
         return _keyLimitedMap;
@@ -1114,6 +1462,7 @@ public class PluralRules implements Serializable {
             int keywordsRemaining = keywords.size();
 
             int limit = Math.max(5, getRepeatLimit() * MAX_SAMPLES) * 2;
+
             for (int i = 0; keywordsRemaining > 0 && i < limit; ++i) {
                 double val = i / 2.0;
                 String keyword = select(val);
@@ -1133,13 +1482,26 @@ public class PluralRules implements Serializable {
                 }
             }
 
+            // collect explicit samples
+            Map<String, Set<NumberInfo>> sampleFractionMap = new HashMap<String, Set<NumberInfo>>();
+            Set<NumberInfo> mentioned = rules.getMentionedValues(new TreeSet<NumberInfo>());
+            for (NumberInfo s : mentioned) {
+                String keyword = select(s.source, s.visibleFractionDigitCount, s.fractionalDigits);
+                Set<NumberInfo> list = sampleFractionMap.get(keyword);
+                if (list == null) {
+                    list = new LinkedHashSet<NumberInfo>(); // will be sorted because the iteration is
+                    sampleFractionMap.put(keyword, list);
+                }
+                list.add(s);
+            }
+
             if (keywordsRemaining > 0) {
                 for (String k : keywords) {
                     if (!sampleMap.containsKey(k)) {
                         sampleMap.put(k, Collections.<Double>emptyList());
-                        if (--keywordsRemaining == 0) {
-                            break;
-                        }
+                    }
+                    if (!sampleFractionMap.containsKey(k)) {
+                        sampleFractionMap.put(k, Collections.<NumberInfo>emptySet());
                     }
                 }
             }
@@ -1148,7 +1510,12 @@ public class PluralRules implements Serializable {
             for (Entry<String, List<Double>> entry : sampleMap.entrySet()) {
                 sampleMap.put(entry.getKey(), Collections.unmodifiableList(entry.getValue()));
             }
+            for (Entry<String, Set<NumberInfo>> entry : sampleFractionMap.entrySet()) {
+                sampleFractionMap.put(entry.getKey(), Collections.unmodifiableSet(entry.getValue()));
+            }
             _keySamplesMap = sampleMap;
+            _keyFractionSamplesMap = sampleFractionMap;
+            _fractionSamples = Collections.unmodifiableSet(mentioned);
         }
     }
 
@@ -1188,11 +1555,10 @@ public class PluralRules implements Serializable {
      * @stable ICU 3.8
      */
     public String toString() {
-        return "keywords: " + keywords +
-                " limit: " + getRepeatLimit() +
-                " rules: " + rules.toString();
+        return rules.toString();
     }
 
+
     /**
      * {@inheritDoc}
      * @stable ICU 3.8
@@ -1366,4 +1732,12 @@ public class PluralRules implements Serializable {
 
         return originalSize == 1 ? KeywordStatus.UNIQUE : KeywordStatus.BOUNDED;
     }
+
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public String getRules(String keyword) {
+        return rules.getRules(keyword);
+    }
 }
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesFactory.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesFactory.java
new file mode 100644 (file)
index 0000000..3d9d01f
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ *******************************************************************************
+ * Copyright (C) 2013, Google Inc, International Business Machines Corporation and         *
+ * others. All Rights Reserved.                                                *
+ *******************************************************************************
+ */
+package com.ibm.icu.dev.test.format;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.ibm.icu.dev.util.Relation;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.text.PluralRules.NumberInfo;
+import com.ibm.icu.text.PluralRules.PluralType;
+import com.ibm.icu.util.ULocale;
+
+/**
+ * @author markdavis
+ *
+ */
+public abstract class PluralRulesFactory {
+
+    abstract boolean hasOverride(ULocale locale);
+
+    abstract PluralRules forLocale(ULocale locale, PluralType ordinal);
+    
+    PluralRules forLocale(ULocale locale) {
+        return forLocale(locale, PluralType.CARDINAL);
+    }
+    
+    abstract ULocale[] getAvailableULocales();
+
+    abstract ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable);
+
+    static final PluralRulesFactory NORMAL = new PluralRulesFactoryVanilla();
+
+    static final PluralRulesFactory ALTERNATE = new PluralRulesFactoryWithOverrides();
+
+    private PluralRulesFactory() {}
+    
+    static class PluralRulesFactoryVanilla extends PluralRulesFactory {
+        @Override
+        boolean hasOverride(ULocale locale) {
+            return false;
+        }
+        @Override
+        PluralRules forLocale(ULocale locale, PluralType ordinal) {
+            return PluralRules.forLocale(locale, ordinal);
+        }
+        @Override
+        ULocale[] getAvailableULocales() {
+            return PluralRules.getAvailableULocales();
+        }
+        @Override
+        ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable) {
+            return PluralRules.getFunctionalEquivalent(locale, isAvailable);
+        }
+    }
+
+    static class PluralRulesFactoryWithOverrides extends PluralRulesFactory {
+        static Map<ULocale,PluralRules> OVERRIDES = new HashMap<ULocale,PluralRules>(); 
+        static Relation<ULocale,NumberInfo> EXTRA_SAMPLES = Relation.of(new HashMap<ULocale,Set<NumberInfo>>(), HashSet.class); 
+        static {
+            String[][] overrides = {
+                    {"en,ca,de,et,fi,gl,it,nl,pt,sv,sw,ta,te,ur", "one: j is 1"},
+                    {"cs,sk", "one: j is 1;  few: j in 2..4; many: v is not 0"},
+                    //{"el", "one: j is 1 or i is 0 and f is 1"},
+                    {"da,is", "one: j is 1 or f is 1"},
+                    {"fil", "one: j in 0..1"},
+                    {"he", "one: j is 1;  two: j is 2", "10,20"},
+                    {"hi", "one: n within 0..1"},
+                    {"hr", "one: j mod 10 is 1 and j mod 100 is not 11;  few: j mod 10 in 2..4 and j mod 100 not in 12..14;  many: j mod 10 is 0 or j mod 10 in 5..9 or j mod 100 in 11..14"},
+                    {"lv", "zero: n mod 10 is 0" +
+                            " or n mod 10 in 11..19" +
+                            " or v in 1..6 and f is not 0 and f mod 10 is 0" +
+                            " or v in 1..6 and f mod 10 in 11..19;" +
+                            "one: n mod 10 is 1 and n mod 100 is not 11" +
+                            " or v in 1..6 and f mod 10 is 1 and f mod 100 is not 11" +
+                    " or v not in 0..6 and f mod 10 is 1"},
+                    {"pl", "one: j is 1;  few: j mod 10 in 2..4 and j mod 100 not in 12..14;  many: j is not 1 and j mod 10 in 0..1 or j mod 10 in 5..9 or j mod 100 in 12..14"},
+                    {"sl", "one: j mod 100 is 1;  two: j mod 100 is 2;  few: j mod 100 in 3..4 or v is not 0"},
+                    {"sr", "one: j mod 10 is 1 and j mod 100 is not 11" +
+                            " or v in 1..6 and f mod 10 is 1 and f mod 100 is not 11" +
+                            " or v not in 0..6 and f mod 10 is 1;" +
+                            " few: j mod 10 in 2..4 and j mod 100 not in 12..14" +
+                            " or v in 1..6 and f mod 10 in 2..4 and f mod 100 not in 12..14" +
+                            " or v not in 0..6 and f mod 10 in 2..4;" +
+                            " many: j mod 10 is 0 or j mod 10 in 5..9 or j mod 100 in 11..14" +
+                            " or v in 1..6 and f mod 10 in 5..9" +
+                            " or v in 1..6 and f mod 100 in 11..14" +
+                    " or v not in 0..6 and f mod 10 in 5..9"},
+                    {"ro", "one: j is 1; few: n is 0 or n is not 1 and n mod 100 in 1..19"},
+                    {"ru,uk", "one: j mod 10 is 1 and j mod 100 is not 11;" +
+                            " few: j mod 10 in 2..4 and j mod 100 not in 12..14;" +
+                    " many: j mod 10 is 0 or j mod 10 in 5..9 or j mod 100 in 11..14"},
+            };
+            for (String[] pair : overrides) {
+                for (String locale : pair[0].split("\\s*,\\s*")) {
+                    ULocale uLocale = new ULocale(locale);
+                    if (OVERRIDES.containsKey(uLocale)) {
+                        throw new IllegalArgumentException("Duplicate locale: " + uLocale);
+                    }
+                    OVERRIDES.put(uLocale, PluralRules.createRules(pair[1]));
+                    if (pair.length==3) {
+                        for (String item : pair[2].split("\\s*,\\s*")) {
+                            EXTRA_SAMPLES.put(uLocale, new PluralRules.NumberInfo(item));
+                        }
+                    }
+                }
+            }
+        }
+        @Override
+        boolean hasOverride(ULocale locale) {
+            return OVERRIDES.containsKey(locale);
+        }
+
+        @Override
+        PluralRules forLocale(ULocale locale, PluralType ordinal) {
+            PluralRules override = ordinal != PluralType.CARDINAL ? null : OVERRIDES.get(locale);
+            return override != null ? override: PluralRules.forLocale(locale, ordinal);
+        }
+
+        @Override
+        ULocale[] getAvailableULocales() {
+            return PluralRules.getAvailableULocales(); // TODO fix if we add more locales
+        }
+
+        static final Map<String,ULocale> rulesToULocale = new HashMap();
+        
+        @Override
+        ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable) {
+            if (rulesToULocale.isEmpty()) {
+                for (ULocale locale2 : getAvailableULocales()) {
+                    String rules = forLocale(locale2).toString();
+                    ULocale old = rulesToULocale.get(rules);
+                    if (old == null) {
+                        rulesToULocale.put(rules, locale2);
+                    }
+                }
+            }
+            String rules = forLocale(locale).toString();
+            ULocale result = rulesToULocale.get(rules);
+            return result == null ? ULocale.ROOT : result;
+        }
+    };
+}
index 5dd46a3ce3f3be7b1d034cd0c6a99382cb9893f5..0fa7e33ce0582d7fec1acdc0a41facdb882adbfb 100644 (file)
@@ -11,23 +11,36 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
+import java.util.TreeMap;
 
 import com.ibm.icu.dev.test.TestFmwk;
 import com.ibm.icu.impl.Utility;
 import com.ibm.icu.text.PluralRules;
 import com.ibm.icu.text.PluralRules.KeywordStatus;
 import com.ibm.icu.text.PluralRules.PluralType;
+import com.ibm.icu.text.PluralRules.NumberInfo;
 import com.ibm.icu.util.Output;
 import com.ibm.icu.util.ULocale;
 
 /**
  * @author dougfelt (Doug Felt)
+ * @author markdavis (Mark Davis) [for fractional support]
  */
 public class PluralRulesTest extends TestFmwk {
+    
+    static boolean USE_ALT = System.getProperty("alt_plurals") != null;
+    
+    PluralRulesFactory factory = USE_ALT ? PluralRulesFactory.ALTERNATE : PluralRulesFactory.NORMAL;
+    
     public static void main(String[] args) throws Exception {
         new PluralRulesTest().run(args);
     }
@@ -117,17 +130,17 @@ public class PluralRulesTest extends TestFmwk {
 
     private static String[][] operandTestData = {
         {"a: i is 2; b:i is 3", 
-            "b: 3.5; a: 2.5"},
+        "b: 3.5; a: 2.5"},
         {"a: f is 0; b:f is 50", 
-            "a: 1.00; b: 1.50"},
+        "a: 1.00; b: 1.50"},
         {"a: v is 1; b:v is 2", 
-            "a: 1.0; b: 1.00"},
+        "a: 1.0; b: 1.00"},
         {"one: n is 1 AND v is 0", 
-            "one: 1 ; other: 1.00,1.0"}, // English rules
+        "one: 1 ; other: 1.00,1.0"}, // English rules
         {"one: v is 0 and i mod 10 is 1 or f mod 10 is 1", 
-            "one: 1, 1.1, 3.1; other: 1.0, 3.2, 5"}, // Last visible digit
-        {"one: n is 1 and v is 0; few: n in 2..4 and v is 0; many: v is not 0", 
-            "one: 1; few: 2, 3, 4; many: 0.5, 1.0, 2.0, 2.1, 3.0, 4.999, 5.3; other:0,5,1001"}, // Last visible digit
+        "one: 1, 1.1, 3.1; other: 1.0, 3.2, 5"}, // Last visible digit
+        {"one: j is 0", 
+        "one: 0; other: 0.0, 1.0, 3"}, // Last visible digit
         // one → n is 1; few → n in 2..4;
     };
 
@@ -140,24 +153,7 @@ public class PluralRulesTest extends TestFmwk {
             try {
                 PluralRules rules = PluralRules.createRules(pattern);
                 logln(rules.toString());
-                for (String categoryAndExpected : categoriesAndExpected.split("\\s*;\\s*")) {
-                    String[] categoryFromExpected = categoryAndExpected.split("\\s*:\\s*");
-                    String expected = categoryFromExpected[0];
-                    for (String value : categoryFromExpected[1].split("\\s*,\\s*")) {
-                        double number = Double.parseDouble(value);
-                        int decimalPos = value.indexOf('.') + 1;
-                        int countVisibleFractionDigits;
-                        int fractionaldigits;
-                        if (decimalPos == 0) {
-                            countVisibleFractionDigits = fractionaldigits = 0;
-                        } else {
-                            countVisibleFractionDigits = value.length() - decimalPos;
-                            fractionaldigits = Integer.parseInt(value.substring(decimalPos));
-                        }
-                        String result = rules.select(number, countVisibleFractionDigits, fractionaldigits);
-                        assertEquals("testing <" + pair[0] + "> with <" + value + ">", expected, result);
-                    }
-                }
+                checkCategoriesAndExpected(pattern, categoriesAndExpected, rules);
             } catch (Exception e) {
                 e.printStackTrace();
                 throw new RuntimeException(e.getMessage());
@@ -165,6 +161,68 @@ public class PluralRulesTest extends TestFmwk {
         }
     }
 
+    public void testUniqueRules() {
+        main:
+        for (ULocale locale : factory.getAvailableULocales()) {
+            PluralRules rules = factory.forLocale(locale);
+            Collection<NumberInfo> samples = rules.getFractionSamples();
+            Map<String,PluralRules> keywordToRule = new HashMap<String,PluralRules>();
+            for (String keyword : rules.getKeywords()) {
+                if (keyword.equals("other")) {
+                    continue;
+                }
+                String rules2 = keyword + ":" + rules.getRules(keyword);
+                PluralRules singleRule = PluralRules.createRules(rules2);
+                if (singleRule == null) {
+                    errln("Can't generate single rule for " + rules2);
+                    PluralRules.createRules(rules2); // for debugging
+                    continue main;
+                }
+                keywordToRule.put(keyword, singleRule);
+            }
+            Map<NumberInfo, String> collisionTest = new TreeMap();
+            for (NumberInfo sample : samples) {
+                collisionTest.clear();
+                for (Entry<String, PluralRules> entry: keywordToRule.entrySet()) {
+                    String keyword = entry.getKey();
+                    PluralRules rule = entry.getValue();
+                    String foundKeyword = rule.select(sample);
+                    if (foundKeyword.equals("other")) {
+                        continue;
+                    }
+                    String old = collisionTest.get(sample);
+                    if (old != null) {
+                        errln(locale + "\tNon-unique rules: " + sample + " => " + old + " & " + foundKeyword);
+                        rule.select(sample);
+                    } else {
+                        collisionTest.put(sample, foundKeyword);
+                    }
+                }
+            }
+        }
+    }
+
+    private void checkCategoriesAndExpected(String title, String categoriesAndExpected, PluralRules rules) {
+        for (String categoryAndExpected : categoriesAndExpected.split("\\s*;\\s*")) {
+            String[] categoryFromExpected = categoryAndExpected.split("\\s*:\\s*");
+            String expected = categoryFromExpected[0];
+            for (String value : categoryFromExpected[1].split("\\s*,\\s*")) {
+                double number = Double.parseDouble(value);
+                int decimalPos = value.indexOf('.') + 1;
+                int countVisibleFractionDigits;
+                int fractionaldigits;
+                if (decimalPos == 0) {
+                    countVisibleFractionDigits = fractionaldigits = 0;
+                } else {
+                    countVisibleFractionDigits = value.length() - decimalPos;
+                    fractionaldigits = Integer.parseInt(value.substring(decimalPos));
+                }
+                String result = rules.select(number, countVisibleFractionDigits, fractionaldigits);
+                assertEquals("testing <" + title + "> with <" + value + ">", expected, result);
+            }
+        }
+    }
+
     private static String[][] equalityTestData = {
         { "a: n is 5",
         "a: n in 2..6 and n not in 2..4 and n is not 6" },
@@ -221,17 +279,17 @@ public class PluralRulesTest extends TestFmwk {
 
     public void testBuiltInRules() {
         // spot check
-        PluralRules rules = PluralRules.forLocale(ULocale.US);
+        PluralRules rules = factory.forLocale(ULocale.US);
         assertEquals("us 0", PluralRules.KEYWORD_OTHER, rules.select(0));
         assertEquals("us 1", PluralRules.KEYWORD_ONE, rules.select(1));
         assertEquals("us 2", PluralRules.KEYWORD_OTHER, rules.select(2));
 
-        rules = PluralRules.forLocale(ULocale.JAPAN);
+        rules = factory.forLocale(ULocale.JAPAN);
         assertEquals("ja 0", PluralRules.KEYWORD_OTHER, rules.select(0));
         assertEquals("ja 1", PluralRules.KEYWORD_OTHER, rules.select(1));
         assertEquals("ja 2", PluralRules.KEYWORD_OTHER, rules.select(2));
 
-        rules = PluralRules.forLocale(ULocale.createCanonical("ru"));
+        rules = factory.forLocale(ULocale.createCanonical("ru"));
         assertEquals("ru 0", PluralRules.KEYWORD_MANY, rules.select(0));
         assertEquals("ru 1", PluralRules.KEYWORD_ONE, rules.select(1));
         assertEquals("ru 2", PluralRules.KEYWORD_FEW, rules.select(2));
@@ -259,7 +317,7 @@ public class PluralRulesTest extends TestFmwk {
     }
 
     public void testAvailableULocales() {
-        ULocale[] locales = PluralRules.getAvailableULocales();
+        ULocale[] locales = factory.getAvailableULocales();
         Set localeSet = new HashSet();
         localeSet.addAll(Arrays.asList(locales));
 
@@ -351,11 +409,11 @@ public class PluralRulesTest extends TestFmwk {
      */
     public void TestGetSamples() {
         Set<ULocale> uniqueRuleSet = new HashSet<ULocale>();
-        for (ULocale locale : PluralRules.getAvailableULocales()) {
+        for (ULocale locale : factory.getAvailableULocales()) {
             uniqueRuleSet.add(PluralRules.getFunctionalEquivalent(locale, null));
         }
         for (ULocale locale : uniqueRuleSet) {
-            PluralRules rules = PluralRules.forLocale(locale);
+            PluralRules rules = factory.forLocale(locale);
             logln("\nlocale: " + (locale == ULocale.ROOT ? "root" : locale.toString()) + ", rules: " + rules);
             Set<String> keywords = rules.getKeywords();
             for (String keyword : keywords) {
@@ -442,7 +500,7 @@ public class PluralRulesTest extends TestFmwk {
     }
 
     public void TestOrdinal() {
-        PluralRules pr = PluralRules.forLocale(ULocale.ENGLISH, PluralType.ORDINAL);
+        PluralRules pr = factory.forLocale(ULocale.ENGLISH, PluralType.ORDINAL);
         assertEquals("PluralRules(en-ordinal).select(2)", "two", pr.select(2));
     }
 
@@ -467,7 +525,7 @@ public class PluralRulesTest extends TestFmwk {
             ULocale locale = new ULocale((String) test[0]);
             // NumberType numberType = (NumberType) test[1];
             Set<Double> explicits = (Set<Double>) test[1];
-            PluralRules pluralRules = PluralRules.forLocale(locale);
+            PluralRules pluralRules = factory.forLocale(locale);
             LinkedHashSet<String> remaining = new LinkedHashSet(possibleKeywords);
             for (int i = 2; i < test.length; i += 3) {
                 String keyword = (String) test[i];
@@ -485,4 +543,116 @@ public class PluralRulesTest extends TestFmwk {
             }
         }
     }
+
+    enum StandardPluralCategories {
+        zero,
+        one,
+        two,
+        few,
+        many,
+        other;
+        /**
+         * 
+         */
+        private static final Set<StandardPluralCategories> ALL = Collections.unmodifiableSet(EnumSet.allOf(StandardPluralCategories.class));
+        /**
+         * Return a mutable set
+         * @param source
+         * @return
+         */
+        static final EnumSet<StandardPluralCategories> getSet(Collection<String> source) {
+            EnumSet<StandardPluralCategories> result = EnumSet.noneOf(StandardPluralCategories.class);
+            for (String s : source) {
+                result.add(StandardPluralCategories.valueOf(s));
+            }
+            return result;
+        }
+        static final Comparator<Set<StandardPluralCategories>> SHORTEST_FIRST = new Comparator<Set<StandardPluralCategories>>() {
+            public int compare(Set<StandardPluralCategories> arg0, Set<StandardPluralCategories> arg1) {
+                int diff = arg0.size() - arg1.size();
+                if (diff != 0) {
+                    return diff;
+                }
+                // otherwise first...
+                // could be optimized, but we don't care here.
+                for (StandardPluralCategories value : ALL) {
+                    if (arg0.contains(value)) {
+                        if (!arg1.contains(value)) {
+                            return 1;
+                        }
+                    } else if (arg1.contains(value)) {
+                        return -1;
+                    }
+
+                }
+                return 0;
+            }
+
+        };
+    }
+
+    public void TestLocales() {
+        for (String test : LOCALE_SNAPSHOT) {
+            test = test.trim();
+            String[] parts = test.split("\\s*;\\s*");
+            for (String localeString : parts[0].split("\\s*,\\s*")) {
+                ULocale locale = new ULocale(localeString);
+                if (factory.hasOverride(locale)) {
+                    continue; // skip for now
+                }
+                PluralRules rules = factory.forLocale(locale);
+                for (int i = 1; i < parts.length; ++i) {
+                    checkCategoriesAndExpected(localeString, parts[i], rules);
+                }
+            }
+        }
+    }
+
+    static final String[] LOCALE_SNAPSHOT = {
+        // [other]
+        "az,bm,bo,dz,fa,hu,id,ig,ii,ja,jv,ka,kde,kea,km,kn,ko,lo,ms,my,sah,ses,sg,th,to,tr,vi,wo,yo,zh; other: 0, 0.0, 0.1, 1, 1.0, 3, 7",
+
+        // [one, other]
+        "af,asa,ast,bem,bez,bg,bn,brx,ca,cgg,chr,ckb,da,de,dv,ee,el,en,eo,es,et,eu,fi,fo,fur,fy,gl,gsw,gu,ha,haw,hy,is,it,jgo,jmc,kaj,kcg,kk,kkj,kl,ks,ksb,ku,ky,lb,lg,mas,mgo,ml,mn,mr,nah,nb,nd,ne,nl,nn,nnh,no,nr,ny,nyn,om,or,os,pa,pap,ps,pt,rm,rof,rwk,saq,seh,sn,so,sq,ss,ssy,st,sv,sw,syr,ta,te,teo,tig,tk,tn,ts,ur,ve,vo,vun,wae,xh,xog,zu;    one: 1, 1.0;    other: 0, 0.0, 0.1, 0.5, 3, 7",
+        "ak,am,bh,fil,guw,hi,ln,mg,nso,ti,tl,wa;    one: 0, 0.0, 1, 1.0;    other: 0.1, 0.5, 3, 7",
+        "ff,fr,kab; one: 0, 0.0, 0.1, 0.5, 1, 1.0, 1.5; other: 2, 5",
+        "gv;    one: 0, 0.0, 1, 1.0, 11, 12, 20, 21, 22, 31, 32, 40, 60;    other: 0.1, 15.5, 39.5, 59",
+        "mk;    one: 1, 1.0, 21, 31;    other: 0, 0.0, 0.1, 10.5, 11, 26, 30",
+        "tzm;   one: 0, 0.0, 1, 1.0, 11, 98, 99;    other: 0.1, 0.5, 10",
+
+        // [one, few, other]
+        "cs,sk; one: 1, 1.0;    few: 2, 3, 4;   other: 0, 0.0, 0.1, 0.5, 5",
+        "lt;    one: 1, 1.0, 21, 31;    few: 22, 29, 32, 39, 65;    other: 0, 0.0, 0.1, 11, 12, 19, 110, 111, 119, 211, 219, 311, 318.5, 319",
+        "mo,ro; one: 1, 1.0;    few: 0, 0.0, 101, 118, 119, 201, 219, 301, 318, 319;    other: 0.1, 160",
+        "shi;   one: 0, 0.0, 0.1, 0.5, 1, 1.0;  few: 2, 9, 10;  other: 1.5, 5.5",
+
+        // [one, two, other]
+        "iu,kw,naq,se,sma,smi,smj,smn,sms;  one: 1, 1.0;    two: 2; other: 0, 0.0, 0.1, 0.5, 1.5, 5",
+
+        // [zero, one, other]
+        "ksh;   zero: 0, 0.0;   one: 1, 1.0;    other: 0.1, 0.5, 3, 7",
+        "lag;   zero: 0, 0.0;   one: 0.1, 0.5, 1, 1.0, 1.5; other: 2, 5",
+        "lv;    zero: 0, 0.0;   one: 1, 1.0, 21, 31, 161;   other: 0.1, 11, 110, 111, 211, 310, 311",
+
+        // [one, few, many, other]
+        "be,bs,hr,ru,sh,sr,uk;  one: 1, 1.0, 21, 31;    few: 22, 24, 32, 34;    many: 0, 0.0, 10, 11, 12, 14, 15, 19, 20, 25, 29, 30, 35, 39, 110, 111, 112, 114, 211, 212, 214, 311, 312, 314; other: 0.1, 9.5, 15.5",
+        "mt;    one: 1, 1.0;    few: 0, 0.0, 102, 109, 110, 202, 210, 302, 310; many: 111, 119, 211, 219, 311, 319; other: 0.1, 55.5, 101",
+        "pl;    one: 1, 1.0;    few: 22, 24, 32, 34;    many: 0, 0.0, 10, 11, 12, 14, 15, 19, 20, 21, 25, 29, 30, 31, 35, 39, 112, 114, 212, 214, 312, 314; other: 0.1, 5.5, 9.5, 15.5",
+
+        // [one, two, many, other]
+        "he;    one: 1, 1.0;    two: 2; many: 10, 20, 30;   other: 0, 0.0, 0.1, 5.5, 19, 29",
+
+        // [one, two, few, other]
+        "gd;    one: 1, 1.0, 11;    two: 2, 12; few: 3, 10, 13, 19; other: 0, 0.0, 0.1, 5.5, 11.5, 12.5",
+        "sl;    one: 1, 1.0, 101, 201, 301; two: 102, 202, 302; few: 103, 104, 203, 204, 303, 304;  other: 0, 0.0, 0.1, 103.5, 152.5, 203.5",
+
+        // [one, two, few, many, other]
+        "br;    one: 1, 1.0, 21, 31;    two: 22, 32;    few: 23, 24, 29, 33, 34, 39, 369, 389;  many: 1000000, 2000000, 3000000;    other: 0, 0.0, 0.1, 11, 12, 13, 14, 19, 110, 111, 112, 119, 170, 171, 172, 179, 190, 191, 192, 199, 210, 211, 212, 219, 270, 271, 272, 279, 290, 291, 292, 299, 310, 311, 312, 319, 334.5, 370, 371, 372, 379, 390, 391, 392, 399",
+        "ga;    one: 1, 1.0;    two: 2; few: 3, 6;  many: 7, 8, 9, 10;  other: 0, 0.0, 0.1, 6.5",
+
+        // [zero, one, two, few, many, other]
+        "ar;    zero: 0, 0.0;   one: 1, 1.0;    two: 2; few: 103, 109, 110, 203, 210, 303, 310; many: 111, 150, 199, 211, 298, 299, 311, 399;   other: 0.1",
+        "cy;    zero: 0, 0.0;   one: 1, 1.0;    two: 2; few: 3; many: 6;    other: 0.1, 2.5, 3.5, 5",
+    };
+
 }
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/WritePluralRulesData.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/WritePluralRulesData.java
new file mode 100644 (file)
index 0000000..60e105f
--- /dev/null
@@ -0,0 +1,549 @@
+/*
+ *******************************************************************************
+ * Copyright (C) 2013, Google Inc. and International Business Machines Corporation and  *
+ * others. All Rights Reserved.                                                *
+ *******************************************************************************
+ */
+package com.ibm.icu.dev.test.format;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.Map.Entry;
+
+import com.ibm.icu.dev.test.format.PluralRulesTest.StandardPluralCategories;
+import com.ibm.icu.dev.util.CollectionUtilities;
+import com.ibm.icu.dev.util.Relation;
+import com.ibm.icu.impl.Row;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.text.PluralRules.NumberInfo;
+import com.ibm.icu.util.ULocale;
+
+/**
+ * @author markdavis
+ */
+public class WritePluralRulesData {
+    
+    public static void main(String[] args) throws Exception {
+        if (args.length == 0) {
+            args = new String[] {"rules"};
+        }
+        for (String arg : args) {
+            if (arg.equalsIgnoreCase("samples")) {
+                generateSamples();
+            } else if (arg.equalsIgnoreCase("rules")) {
+                showRules();
+            } else if (arg.equalsIgnoreCase("oldSnap")) {
+                generateLOCALE_SNAPSHOT(PluralRulesFactory.NORMAL);
+            } else if (arg.equalsIgnoreCase("newSnap")) {
+                generateLOCALE_SNAPSHOT(PluralRulesFactory.ALTERNATE);
+            } else {
+                throw new IllegalArgumentException();
+            }
+        }
+    }
+
+    public static void generateLOCALE_SNAPSHOT(PluralRulesFactory pluralRulesFactory) {
+        StringBuilder builder = new StringBuilder();
+        Map<Set<StandardPluralCategories>, Relation<String, ULocale>> keywordsToData = new TreeMap(StandardPluralCategories.SHORTEST_FIRST);
+        for (ULocale locale : pluralRulesFactory.getAvailableULocales()) {
+            builder.setLength(0);
+            PluralRules rules = pluralRulesFactory.forLocale(locale);
+            boolean firstKeyword = true;
+            EnumSet<StandardPluralCategories> keywords = StandardPluralCategories.getSet(rules.getKeywords());
+            Relation<String, ULocale> samplesToLocales = keywordsToData.get(keywords);
+            if (samplesToLocales == null) {
+                keywordsToData.put(keywords, samplesToLocales = Relation.of(
+                        new LinkedHashMap<String,Set<ULocale>>(), LinkedHashSet.class));
+            }
+            //System.out.println(locale);
+            for (StandardPluralCategories keyword : keywords) {
+                if (firstKeyword) {
+                    firstKeyword = false;
+                } else {
+                    builder.append(";\t");
+                }
+                Collection<NumberInfo> samples = rules.getFractionSamples(keyword.toString());
+                if (samples.size() == 0) {
+                    throw new IllegalArgumentException();
+                }
+                builder.append(keyword).append(": ");
+                boolean first = true;
+                for (NumberInfo n : samples) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        builder.append(", ");
+                    }
+                    builder.append(n);
+                    //                    for (double j : samples) {
+                    //                        double sample = i + j/100;
+                    //                    }
+                }
+            }
+            samplesToLocales.put(builder.toString(), locale);
+        }
+        System.out.println("    static final String[] LOCALE_SNAPSHOT = {");
+        for (Entry<Set<StandardPluralCategories>, Relation<String, ULocale>> keywordsAndData : keywordsToData.entrySet()) {
+            System.out.println("\n        // " + keywordsAndData.getKey());
+            for (Entry<String, Set<ULocale>> samplesAndLocales : keywordsAndData.getValue().keyValuesSet()) {
+                Set<ULocale> locales = samplesAndLocales.getValue();
+                // check functional equivalence
+                boolean[] isAvailable = new boolean[1];
+                for (ULocale locale : locales) {
+                    ULocale base = pluralRulesFactory.getFunctionalEquivalent(locale, isAvailable);
+                    if (!locales.contains(base) && !base.toString().isEmpty()) {
+                        System.out.println("**" + locales + " doesn't contain " + base);
+                    }
+                }
+
+                System.out.println(
+                        "        \"" + CollectionUtilities.join(locales, ",")
+                        + ";\t" + samplesAndLocales.getKey() + "\",");
+            }
+        }
+        System.out.println("    };");
+    }
+
+    private static class OldNewData extends Row.R4<String, String, String, String> {
+        public OldNewData(String oldRules, String oldSamples, String newRules, String newSamples) {
+            super(oldRules, oldSamples, newRules, newSamples);
+        }
+    }
+
+    static final String[] FOCUS_LOCALES = ("af,am,ar,az,bg,bn,ca,cs,cy,da,de,el,en,es,et,eu,fa,fi,fil,fr,gl,gu," +
+            "hi,hr,hu,hy,id,is,it,he,ja,ka,kk,km,kn,ko,ky,lo,lt,lv,mk,ml,mn,mr,ms,my,ne,nl,nb," +
+            "pa,pl,ps,pt,ro,ru,si,sk,sl,sq,sr,sv,sw,ta,te,th,tr,uk,ur,uz,vi,zh,zu").split("\\s*,\\s*");
+
+    public static void showRules() {
+        if (true) {
+            // for debugging
+            PluralRules rules = PluralRulesFactory.ALTERNATE.forLocale(new ULocale("lv"));
+            rules.select(2.0d, 2, 0);
+        }
+        // System.out.println(new TreeSet(Arrays.asList(locales)));
+        Relation<Map<String,OldNewData>, String> rulesToLocale = Relation.of(
+                new TreeMap<Map<String,OldNewData>, Set<String>>(
+                        new CollectionUtilities.MapComparator<String,OldNewData>()), TreeSet.class);
+        for (String localeString : FOCUS_LOCALES) {
+            ULocale locale = new ULocale(localeString);
+            PluralRules oldRules = PluralRules.forLocale(locale);
+            PluralRules newRules = PluralRulesFactory.ALTERNATE.hasOverride(locale) ? PluralRulesFactory.ALTERNATE.forLocale(locale) : null;
+            Set<String> keywords = oldRules.getKeywords();
+            if (newRules != null) {
+                TreeSet<String> temp = new TreeSet<String>(PluralRules.KEYWORD_COMPARATOR);
+                temp.addAll(keywords);
+                temp.addAll(newRules.getKeywords());
+                keywords = temp;
+            }
+            Map<String,OldNewData> temp = new LinkedHashMap();
+            for (String keyword : keywords) {
+                Collection<NumberInfo> oldFractionSamples = oldRules.getFractionSamples(keyword);
+                Collection<NumberInfo> newFractionSamples = newRules == null ? null : newRules.getFractionSamples(keyword);
+
+                // add extra samples if we have some, or if the rules differ
+
+                if (newRules != null) {
+                    oldFractionSamples = oldFractionSamples == null ? new TreeSet()
+                    : new TreeSet(oldFractionSamples);
+                    newFractionSamples = newFractionSamples == null ? new TreeSet()
+                    : new TreeSet(newFractionSamples);
+                    //                    if (extraSamples != null) {
+                    //                        for (NumberPlus sample : extraSamples) {
+                    //                            if (oldRules.select(sample.source, sample.visibleFractionDigitCount, sample.fractionalDigits).equals(keyword)) {
+                    //                                oldFractionSamples.add(sample);
+                    //                            }
+                    //                            if (newRules != null && newRules.select(sample.source, sample.visibleFractionDigitCount, sample.fractionalDigits).equals(keyword)) {
+                    //                                newFractionSamples.add(sample);
+                    //                            }
+                    //                        }
+                    //                    }
+
+                    // if the rules differ, then add samples from each to the other
+                    if (newRules != null) {
+                        for (NumberInfo sample : oldRules.getFractionSamples()) {
+                            if (newRules.select(sample.source, sample.visibleFractionDigitCount, sample.fractionalDigits).equals(keyword)) {
+                                newFractionSamples.add(sample);
+                            }
+                        }
+                        for (NumberInfo sample : newRules.getFractionSamples()) {
+                            if (oldRules.select(sample.source, sample.visibleFractionDigitCount, sample.fractionalDigits).equals(keyword)) {
+                                oldFractionSamples.add(sample);
+                            }
+                        }
+                    }
+                }
+                String oldRulesString = oldRules.getRules(keyword);
+                if (oldRulesString == null) {
+                    oldRulesString = "";
+                }
+                String newRulesString = newRules == null ? "" : newRules.getRules(keyword);
+                if (newRulesString == null) {
+                    newRulesString = "";
+                }
+                temp.put(keyword, new OldNewData(
+                        oldRulesString, 
+                        oldFractionSamples == null ? "" : "'" + CollectionUtilities.join(oldFractionSamples, ", "),
+                                newRulesString, 
+                                newFractionSamples == null ? "" : "'" + CollectionUtilities.join(newFractionSamples, ", ")
+                        ));
+            }
+            rulesToLocale.put(temp, locale.toString());
+        }
+        System.out.println("Locales\tPC\tOld Rules\tOld Samples\tNew Rules\tNew Samples");
+        for (Entry<Map<String, OldNewData>, Set<String>> entry : rulesToLocale.keyValuesSet()) {
+            String localeList = CollectionUtilities.join(entry.getValue(), " ");
+            for (Entry<String, OldNewData> keywordRulesSamples : entry.getKey().entrySet()) {
+                System.out.println(
+                        localeList // locale
+                        + "\t" + keywordRulesSamples.getKey() // keyword
+                        + "\t" + keywordRulesSamples.getValue().get0() // rules
+                        + "\t" + keywordRulesSamples.getValue().get1() // samples
+                        + "\t" + keywordRulesSamples.getValue().get2() // rules
+                        + "\t" + keywordRulesSamples.getValue().get3() // samples
+                        );
+                localeList = "";
+            }
+        }
+
+        if (false) {
+            System.out.println("\n\nOld Rules for Locales");
+            for (String localeString : FOCUS_LOCALES) {
+                ULocale locale = new ULocale(localeString);
+                PluralRules oldRules = PluralRules.forLocale(locale);
+                System.out.println("{\"" + locale.toString() + "\", \"" + oldRules.toString() + "\"},");
+            }
+        }
+    }
+
+    static String[][] SAMPLE_PATTERNS = {
+        {"af", "one", "{0} dag"},
+        {"af", "other", "{0} dae"},
+        {"am", "one", "{0} ቀን"},
+        {"am", "other", "{0} ቀናት"}, // fixed to 'other'
+        {"ar", "few", "{0} ساعات"},
+        {"ar", "many", "{0}  ساعة"},
+        {"ar", "one", "ساعة"},
+        {"ar", "other", "{0} ساعة"},
+        {"ar", "two", "ساعتان"},
+        {"ar", "zero", "{0} ساعة"},
+        {"bg", "one", "{0} ден"},
+        {"bg", "other", "{0} дена"},
+        {"bn", "one", "{0} টি আপেল"},
+        {"bn", "other", "আমার অনেকগুলি আপেল আছে"},
+        {"br", "few", "{0} deiz"},
+        {"br", "many", "{0} a zeizioù"},
+        {"br", "one", "{0} deiz"},
+        {"br", "other", "{0} deiz"},
+        {"br", "two", "{0} zeiz"},
+        {"ca", "one", "{0} dia"},
+        {"ca", "other", "{0} dies"},
+        {"cs", "few", "{0} dny"},
+        {"cs", "one", "{0} den"},
+        {"cs", "other", "{0} dní"},
+        {"cs", "many", "{0} dne"}, // added from spreadsheet
+        {"cy", "zero", "{0} cadair (f) {0} peint (m)"},
+        {"cy", "one", "{0} gadair (f) {0} peint (m)"},
+        {"cy", "two", "{0} gadair (f) {0} beint (m)"},
+        {"cy", "few", "{0} cadair (f) {0} pheint (m)"},
+        {"cy", "many", "{0} chadair (f) {0} pheint (m)"},
+        {"cy", "other", "{0} cadair (f) {0} peint (m)"},
+        {"da", "one", "{0} dag"},
+        {"da", "other", "{0} dage"},
+        {"de", "one", "{0} Tag"},
+        {"de", "other", "{0} Tage"},
+        {"dz", "other", "ཉིནམ་ {0} "},
+        {"el", "one", "{0} ημέρα"},
+        {"el", "other", "{0} ημέρες"},
+        {"es", "one", "{0} día"},
+        {"es", "other", "{0} días"},
+        {"et", "one", "{0} ööpäev"},
+        {"et", "other", "{0} ööpäeva"},
+        {"eu", "one", "Nire {0} lagunarekin nago"},
+        {"eu", "other", "Nire {0} lagunekin nago"},
+        {"fa", "other", "{0} روز"},
+        {"fi", "one", "{0} päivä"},
+        {"fi", "other", "{0} päivää"},
+        {"fil", "one", "sa {0} araw"},
+        {"fil", "other", "sa {0} (na) araw"},
+        {"fr", "one", "{0} jour"},
+        {"fr", "other", "{0} jours"},
+        {"gl", "one", "{0} día"},
+        {"gl", "other", "{0} días"},
+        {"gu", "one", "{0} અઠવાડિયું"},
+        {"gu", "other", "{0} અઠવાડિયા"},
+        {"he", "many", "{0} ימים"},
+        {"he", "one", " יום {0}"},
+        {"he", "other", "{0} ימים"},
+        {"he", "two", "יומיים"},
+        {"hi", "one", "{0} घंटा"},
+        {"hi", "other", "{0} घंटे"},
+        {"hr", "few", "za {0} mjeseca"},
+        {"hr", "many", "za {0} mjeseci"},
+        {"hr", "one", "za {0} mjesec"},
+        {"hr", "other", "za sljedeći broj mjeseci: {0}"},
+        {"hu", "other", "{0} nap"},
+        {"hy", "few", "{0} օր"},
+        {"hy", "many", "{0} օր"},
+        {"hy", "one", "{0} օր"},
+        {"hy", "other", "{0} օր"},
+        {"hy", "two", "{0} օր"},
+        {"hy", "zero", "{0} օր"},
+        {"id", "other", "{0} hari"},
+        {"is", "one", "{0} dagur"},
+        {"is", "other", "{0} dagar"},
+        {"it", "one", "{0} giorno"},
+        {"it", "other", "{0} giorni"},
+        {"ja", "other", "{0}日"},
+        {"km", "other", "{0} ថ្ងៃ"},
+        {"kn", "other", "{0} ದಿನಗಳು"},
+        {"ko", "other", "{0}일"},
+        {"lo", "other", "{0} ມື້"},
+        {"lt", "few", "{0} dienos"},
+        {"lt", "one", "{0} diena"},
+        {"lt", "other", "{0} dienų"},
+        {"lv", "one", "{0} diennakts"},
+        {"lv", "other", "{0} diennaktis"},
+        {"lv", "zero", "{0} diennakšu"},
+        {"ml", "one", "{0} വ്യക്തി"},
+        {"ml", "other", "{0} വ്യക്തികൾ"},
+        {"mr", "one", "{0} घर"},
+        {"mr", "other", "{0} घरे"},
+        {"ms", "other", "{0} hari"},
+        {"nb", "one", "{0} dag"},
+        {"nb", "other", "{0} dager"},
+        {"ne", "one", "तपाईंसँग {0} निमन्त्रणा छ"},
+        {"ne", "other", "तपाईँसँग {0} निमन्त्रणाहरू छन्"},
+        //        {"ne", "", "{0} दिन बाँकी छ ।"},
+        //        {"ne", "", "{0} दिन बाँकी छ ।"},
+        //        {"ne", "", "{0} दिन बाँकी छ ।"},
+        //        {"ne", "", "{0} जनाहरू पाहुना बाँकी छ ।"},
+        {"nl", "one", "{0} dag"},
+        {"nl", "other", "{0} dagen"},
+        {"pl", "few", "{0} miesiące"},
+        {"pl", "many", "{0} miesięcy"},
+        {"pl", "one", "{0} miesiąc"},
+        {"pl", "other", "{0} miesiąca"},
+        {"pt", "one", "{0} dia"},
+        {"pt", "other", "{0} dias"},
+        {"pt_PT", "one", "{0} dia"},
+        {"pt_PT", "other", "{0} dias"},
+        {"ro", "few", "{0} zile"},
+        {"ro", "one", "{0} zi"},
+        {"ro", "other", "{0} de zile"},
+        {"ru", "few", "{0} года"},
+        {"ru", "many", "{0} лет"},
+        {"ru", "one", "{0} год"},
+        {"ru", "other", "{0} года"},
+        {"si", "other", "දින {0}ක්"},
+        {"sk", "few", "{0} dni"},
+        {"sk", "one", "{0} deň"},
+        {"sk", "other", "{0} dní"},
+        {"sk", "many", "{0} dňa"}, // added from spreadsheet
+        {"sl", "few", "{0} ure"},
+        {"sl", "one", "{0} ura"},
+        {"sl", "other", "{0} ur"},
+        {"sl", "two", "{0} uri"},
+        {"sr", "few", "{0} сата"},
+        {"sr", "many", "{0} сати"},
+        {"sr", "one", "{0} сат"},
+        {"sr", "other", "{0} сати"},
+        {"sv", "one", "om {0} dag"},
+        {"sv", "other", "om {0} dagar"},
+        {"sw", "one", "siku {0} iliyopita"},
+        {"sw", "other", "siku {0} zilizopita"},
+        {"ta", "one", "{0} நாள்"},
+        {"ta", "other", "{0} நாட்கள்"},
+        {"te", "one", "{0} రోజు"},
+        {"te", "other", "{0} రోజులు"},
+        {"th", "other", "{0} วัน"},
+        {"tr", "other", "{0} gün"},
+        {"uk", "few", "{0} дні"},
+        {"uk", "many", "{0} днів"},
+        {"uk", "one", "{0} день"},
+        {"uk", "other", "{0} дня"},
+        {"ur", "one", "{0} گھنٹہ"},
+        {"ur", "other", "{0} گھنٹے"},
+        {"vi", "other", "{0} ngày"},
+        {"zh", "other", "{0} 天"},
+        {"zh_Hant", "other", "{0} 日"},     
+        {"en", "one", "{0} day"},        // added from spreadsheet  
+        {"en", "other", "{0} days"},       // added from spreadsheet   
+        {"zu", "one", "{0} usuku"},     // added from spreadsheet
+        {"zu", "other", "{0} izinsuku"},          // added from spreadsheet
+    };
+    static final Set<String> NEW_LOCALES = new HashSet(Arrays.asList("az,ka,kk,ky,mk,mn,my,pa,ps,sq,uz".split("\\s*,\\s*")));
+
+    static class SamplePatterns {
+        final Map<String,String> keywordToPattern = new TreeMap(PluralRules.KEYWORD_COMPARATOR);
+        final Map<String,String> keywordToErrors = new HashMap();
+        public void put(String keyword, String sample) {
+            if (keywordToPattern.containsKey(keyword)) {
+                throw new IllegalArgumentException("Duplicate keyword <" + keyword + ">");
+            } else {
+                keywordToPattern.put(keyword, sample);
+            }
+        }
+        public void checkErrors(Set<String> set) {
+            final Map<String,String> skeletonToKeyword = new HashMap();
+            for (String keyword : set) {
+                String error = "";
+                String sample = keywordToPattern.get(keyword);
+                String skeleton = sample.replace(" ", "").replace("{0}", "");
+                String oldSkeletonKeyword = skeletonToKeyword.get(skeleton);
+                if (oldSkeletonKeyword != null) {
+                    if (!error.isEmpty()) {
+                        error += ", ";
+                    }
+                    error += "Duplicate keyword skeleton <" + keyword + ", " + skeleton + ">, same as for: <" + oldSkeletonKeyword + ">";
+                } else {
+                    skeletonToKeyword.put(skeleton, keyword);
+                }
+                if (error.isEmpty()) {
+                    keywordToErrors.put(keyword, "");
+                } else {
+                    keywordToErrors.put(keyword, "\tERROR: " + error);
+                }
+            }
+        }
+    }
+
+    static void generateSamples() {
+        Map<ULocale, SamplePatterns> localeToSamplePatterns = new LinkedHashMap();
+        for (String[] row : SAMPLE_PATTERNS) {
+            ULocale locale = new ULocale(row[0]);
+            String keyword = row[1];
+            String sample = row[2];
+            SamplePatterns samplePatterns = localeToSamplePatterns.get(locale);
+            if (samplePatterns == null) {
+                localeToSamplePatterns.put(locale, samplePatterns = new SamplePatterns());
+            }
+            samplePatterns.put(keyword, sample);
+        }
+        LinkedHashSet<ULocale> skippedLocales = new LinkedHashSet<ULocale>();
+        System.out.println("Locale\tPC\tPattern\tSample\tErrors");
+        for (String localeString : FOCUS_LOCALES) {
+            ULocale locale = new ULocale(localeString);
+            PluralRules newRules = PluralRulesFactory.ALTERNATE.forLocale(locale);
+            SamplePatterns samplePatterns = localeToSamplePatterns.get(locale);
+            if (samplePatterns == null && NEW_LOCALES.contains(localeString)) {
+                skippedLocales.add(locale);
+                continue;
+            }
+            // check for errors. Changes state so that we get an error map
+            samplePatterns.checkErrors(newRules.getKeywords());
+            // now print.
+            for (String keyword : newRules.getKeywords()) {
+                String pattern = null;
+                String error = null;
+                Collection<NumberInfo> samples = newRules.getFractionSamples(keyword);
+                NumberInfo first = samples.iterator().next();
+                String sample = "??? " + first.toString();
+                if (samplePatterns == null) {
+                    pattern = "???";
+                    error = "\tERROR: Locale data missing";
+                } else {
+                    pattern = samplePatterns.keywordToPattern.get(keyword);
+                    error = samplePatterns.keywordToErrors.get(keyword);
+                    if (pattern == null) {
+                        pattern = "???";
+                        error = "\tERROR: Needed for new rules";
+                    } else {
+                        sample = pattern.replace("{0}", first.toString());
+                    }
+                }
+                System.out.println(locale + "\t" + keyword
+                        + "\t" + pattern
+                        + "\t" + sample
+                        + error
+                        );
+            }
+        }
+        System.out.println("SKIP:\t\t\t" + skippedLocales);
+    }
+
+
+    static String[][] OLDRULES = {
+        {"af", "one: n is 1"},
+        {"am", "one: n in 0..1"},
+        {"ar", "zero: n is 0;  one: n is 1;  two: n is 2;  few: n mod 100 in 3..10;  many: n mod 100 in 11..99"},
+        {"az", "other: null"},
+        {"bg", "one: n is 1"},
+        {"bn", "one: n is 1"},
+        {"ca", "one: n is 1"},
+        {"cs", "one: n is 1;  few: n in 2..4"},
+        {"cy", "zero: n is 0;  one: n is 1;  two: n is 2;  few: n is 3;  many: n is 6"},
+        {"da", "one: n is 1"},
+        {"de", "one: n is 1"},
+        {"el", "one: n is 1"},
+        {"en", "one: n is 1"},
+        {"es", "one: n is 1"},
+        {"et", "one: n is 1"},
+        {"eu", "one: n is 1"},
+        {"fa", "other: null"},
+        {"fi", "one: n is 1"},
+        {"fil", "one: n in 0..1"},
+        {"fr", "one: n within 0..2 and n is not 2"},
+        {"gl", "one: n is 1"},
+        {"gu", "one: n is 1"},
+        {"hi", "one: n in 0..1"},
+        {"hr", "one: n mod 10 is 1 and n mod 100 is not 11;  few: n mod 10 in 2..4 and n mod 100 not in 12..14;  many: n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14"},
+        {"hu", "other: null"},
+        {"hy", "one: n is 1"},
+        {"id", "other: null"},
+        {"is", "one: n is 1"},
+        {"it", "one: n is 1"},
+        {"he", "one: n is 1;  two: n is 2;  many: n is not 0 and n mod 10 is 0"},
+        {"ja", "other: null"},
+        {"ka", "other: null"},
+        {"kk", "one: n is 1"},
+        {"km", "other: null"},
+        {"kn", "other: null"},
+        {"ko", "other: null"},
+        {"ky", "one: n is 1"},
+        {"lo", "other: null"},
+        {"lt", "one: n mod 10 is 1 and n mod 100 not in 11..19;  few: n mod 10 in 2..9 and n mod 100 not in 11..19"},
+        {"lv", "zero: n is 0;  one: n mod 10 is 1 and n mod 100 is not 11"},
+        {"mk", "one: n mod 10 is 1 and n is not 11"},
+        {"ml", "one: n is 1"},
+        {"mn", "one: n is 1"},
+        {"mr", "one: n is 1"},
+        {"ms", "other: null"},
+        {"my", "other: null"},
+        {"ne", "one: n is 1"},
+        {"nl", "one: n is 1"},
+        {"nb", "one: n is 1"},
+        {"pa", "one: n is 1"},
+        {"pl", "one: n is 1;  few: n mod 10 in 2..4 and n mod 100 not in 12..14;  many: n is not 1 and n mod 10 in 0..1 or n mod 10 in 5..9 or n mod 100 in 12..14"},
+        {"ps", "one: n is 1"},
+        {"pt", "one: n is 1"},
+        {"ro", "one: n is 1;  few: n is 0 or n is not 1 and n mod 100 in 1..19"},
+        {"ru", "one: n mod 10 is 1 and n mod 100 is not 11;  few: n mod 10 in 2..4 and n mod 100 not in 12..14;  many: n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14"},
+        {"si", "other: null"},
+        {"sk", "one: n is 1;  few: n in 2..4"},
+        {"sl", "one: n mod 100 is 1;  two: n mod 100 is 2;  few: n mod 100 in 3..4"},
+        {"sq", "one: n is 1"},
+        {"sr", "one: n mod 10 is 1 and n mod 100 is not 11;  few: n mod 10 in 2..4 and n mod 100 not in 12..14;  many: n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14"},
+        {"sv", "one: n is 1"},
+        {"sw", "one: n is 1"},
+        {"ta", "one: n is 1"},
+        {"te", "one: n is 1"},
+        {"th", "other: null"},
+        {"tr", "other: null"},
+        {"uk", "one: n mod 10 is 1 and n mod 100 is not 11;  few: n mod 10 in 2..4 and n mod 100 not in 12..14;  many: n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14"},
+        {"ur", "one: n is 1"},
+        {"uz", "other: null"},
+        {"vi", "other: null"},
+        {"zh", "other: null"},
+        {"zu", "one: n is 1"},
+    };
+
+}
index f6ce7835b2b434baac7b904fc6811db945b10105..7849c27441304028ead0000621b3dbcb68334397 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *******************************************************************************
- * Copyright (C) 1996-2012, International Business Machines Corporation and    *
+ * Copyright (C) 1996-2013, International Business Machines Corporation and    *
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
@@ -11,7 +11,10 @@ import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
 import java.util.SortedSet;
+import java.util.TreeSet;
 import java.util.regex.Matcher;
 
 import com.ibm.icu.text.UTF16;
@@ -542,4 +545,146 @@ public final class CollectionUtilities {
         }
     }
 
+    /**
+     * Compare, allowing nulls
+     * @param a
+     * @param b
+     * @return
+     */
+    public static <T> boolean equals(T a, T b) {
+        return a == null 
+                ? b == null 
+                : b == null ? false : a.equals(b);
+    }
+
+    /**
+     * Compare, allowing nulls and putting them first
+     * @param a
+     * @param b
+     * @return
+     */
+    public static <T extends Comparable> int compare(T a, T b) {
+        return a == null 
+                ? b == null ? 0 : -1 
+                        : b == null ? 1 : a.compareTo(b);
+    }
+
+    /**
+     * Compare iterators
+     * @param iterator1
+     * @param iterator2
+     * @return
+     */
+    public static <T extends Comparable> int compare(Iterator<T> iterator1, Iterator<T> iterator2) {
+        int diff;
+        while (true) {
+            if (!iterator1.hasNext()) {
+                return iterator2.hasNext() ? -1 : 0;
+            } else if (!iterator2.hasNext()) {
+                return 1;
+            }
+            diff = CollectionUtilities.compare(iterator1.next(), iterator2.next());
+            if (diff != 0) {
+                return diff;
+            }
+        }
+    }
+
+    /**
+     * Compare, with shortest first, and otherwise lexicographically
+     * @param a
+     * @param b
+     * @return
+     */
+    public static <T extends Comparable, U extends Collection<T>> int compare(U o1, U o2) {
+        int diff = o1.size() - o2.size();
+        if (diff != 0) {
+            return diff;
+        }
+        Iterator<T> iterator1 = o1.iterator();
+        Iterator<T> iterator2 = o2.iterator();
+        return compare(iterator1, iterator2);
+    }
+
+    /**
+     * Compare, with shortest first, and otherwise lexicographically
+     * @param a
+     * @param b
+     * @return
+     */
+    public static <T extends Comparable, U extends Set<T>> int compare(U o1, U o2) {
+        int diff = o1.size() - o2.size();
+        if (diff != 0) {
+            return diff;
+        }
+        return compare(new TreeSet(o1), new TreeSet(o2));
+    }
+
+    public static class SetComparator<T extends Comparable> 
+    implements Comparator<Set<T>> {
+        public int compare(Set<T> o1, Set<T> o2) {
+            return CollectionUtilities.compare(o1, o2);
+        }
+    };
+
+
+    public static class CollectionComparator<T extends Comparable> 
+    implements Comparator<Collection<T>> {
+        public int compare(Collection<T> o1, Collection<T> o2) {
+            return CollectionUtilities.compare(o1, o2);
+        }
+    };
+
+    /**
+     * Compare, allowing nulls and putting them first
+     * @param a
+     * @param b
+     * @return
+     */
+    public static <K extends Comparable, V extends Comparable, T extends Entry<K, V>> int compare(T a, T b) {
+        if (a == null) {
+            return b == null ? 0 : -1;
+        } else if (b == null) {
+            return 1;
+        }
+        int diff = compare(a.getKey(), b.getKey());
+        if (diff != 0) {
+            return diff;
+        }
+        return compare(a.getValue(), b.getValue());
+    }
+
+    public static <K extends Comparable, V extends Comparable, T extends Entry<K, V>> int compareEntrySets(Collection<T> o1, Collection<T> o2) {
+        int diff = o1.size() - o2.size();
+        if (diff != 0) {
+            return diff;
+        }
+        Iterator<T> iterator1 = o1.iterator();
+        Iterator<T> iterator2 = o2.iterator();
+        while (true) {
+            if (!iterator1.hasNext()) {
+                return iterator2.hasNext() ? -1 : 0;
+            } else if (!iterator2.hasNext()) {
+                return 1;
+            }
+            T item1 = iterator1.next();
+            T item2 = iterator2.next();
+            diff = CollectionUtilities.compare(item1, item2);
+            if (diff != 0) {
+                return diff;
+            }
+        }
+    }
+
+    public static class MapComparator<K extends Comparable, V extends Comparable> implements Comparator<Map<K,V>> {
+        public int compare(Map<K, V> o1, Map<K, V> o2) {
+            return CollectionUtilities.compareEntrySets(o1.entrySet(), o2.entrySet());
+        }
+    };
+    
+    public static class ComparableComparator<T extends Comparable> implements Comparator<T> {
+        public int compare(T arg0, T arg1) {
+            return CollectionUtilities.compare(arg0, arg1);
+        }
+    }
 }
index 0752f3740e9bde5d42d107d75006d48a3d36ac62..9a2de6c9a9864fe084df2024205a1e266eca82d7 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *******************************************************************************
- * Copyright (C) 1996-2012, International Business Machines Corporation and    *
+ * Copyright (C) 1996-2013, International Business Machines Corporation and    *
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
@@ -736,6 +736,9 @@ public abstract class UnicodeProperty extends UnicodeLabel {
 
         public final Factory add(UnicodeProperty sp) {
             String name2 = sp.getName();
+            if (name2.isEmpty()) {
+                throw new IllegalArgumentException();
+            }
             canonicalNames.put(name2, sp);
             skeletonNames.put(toSkeleton(name2), sp);
             List c = sp.getNameAliases(new ArrayList(1));