diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java
index 63fde87f06..dc67ad7b03 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java
@@ -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 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.
*
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java
index 2daa5c0a4d..171aa0ad7e 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java
@@ -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;
/**
*
* Defines rules for mapping non-negative numeric values onto a small set of keywords.
- *
+ *
*
* 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.
*
- *
*
* A PluralRules object is immutable. It contains caches for sample values, but those are synchronized.
- *
*
* PluralRules is Serializable so that it can be used in formatters, which are serializable.
- *
+ *
*
* For more information, details, and tips for writing rules, see the LDML spec, C.11 Language Plural
* Rules
*
- *
*
* Examples:
+ *
*
*
* "one: n is 1; few: n in 2..4"
*
- *
- *
*
* 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.
*
- *
*
*
* "zero: n is 0; one: n is 1; zero: n mod 100 in 1..19"
*
- *
+ *
* 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...
*
- *
*
*
* "one: n is 1; few: n mod 10 in 2..4 and n mod 100 not in 12..14"
*
- *
- *
*
* 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;
*
*
* Syntax:
- *
+ *
*
* 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
*
*
- *
*
- * The i, f, and v values are defined as follows.
+ * The i, f, and v values are defined as follows:
*
*
- * - v to be the number of visible fraction digits.
- * - f to be the visible fractional digits.
* - i to be the integer digits.
+ * - f to be the visible fractional digits, as an integer.
+ * - v to be the number of visible fraction digits.
+ * - j is defined to only match integers. That is j is 3 fails if v != 0 (eg for 3.1 or 3.0).
*
*
* Examples are in the following table:
*
- *
+ *
*
*
- * n |
- * f |
- * v |
+ * n |
+ * i |
+ * f |
+ * v |
*
*
* 1.0 |
- * 0 |
+ * 1 |
+ * 0 |
* 1 |
*
*
* 1.00 |
- * 0 |
+ * 1 |
+ * 0 |
* 2 |
*
*
* 1.3 |
- * 3 |
+ * 1 |
+ * 3 |
* 1 |
*
*
* 1.03 |
- * 3 |
+ * 1 |
+ * 3 |
* 2 |
*
*
* 1.23 |
- * 23 |
+ * 1 |
+ * 23 |
* 2 |
*
*
@@ -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> _keySamplesMap;
private transient Map _keyLimitedMap;
+ private transient Map> _keyFractionSamplesMap;
+ private transient Set _fractionSamples;
+
// Standard keywords.
@@ -262,6 +276,11 @@ public class PluralRules implements Serializable {
public int updateRepeatLimit(int limit) {
return limit;
}
+
+ public void getMentionedValues(Set 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 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;
+ }
- public NumberInfo(double number, int countVisibleFractionDigits, int fractionalDigits) {
- source = number;
- intValue = (long)number;
- this.fractionalDigits = fractionalDigits;
- this.countVisibleFractionDigits = countVisibleFractionDigits;
- }
- public NumberInfo(double number) {
- this(number, 0, 0);
- }
- final double source;
- final double intValue;
- final double fractionalDigits;
- final double countVisibleFractionDigits;
+ /**
+ * @deprecated This API is ICU internal only.
+ * @internal
+ */
+ public static class NumberInfo implements Comparable {
+ public final double source;
+ public final int fractionalDigits;
+ public final int visibleFractionDigitCount;
+ public final double intValue;
- public double get(int operand) {
- switch(operand) {
- default: return source;
- case 1: return intValue;
- case 2: return fractionalDigits;
- case 3: return countVisibleFractionDigits;
+ 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);
}
}
- public static int getOperand(String t) {
- return OPERAND_LIST.indexOf(t);
+
+ 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 n) {
+ this(n, decimals(n));
+ }
+
+ public NumberInfo(long n) {
+ this(n,0);
+ }
+
+ public double get(Operand operand) {
+ switch(operand) {
+ default: return source;
+ 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;
+ }
+ @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 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 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 getMentionedValues(Set 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 toAddTo) {
+ addRanges(toAddTo, mod);
+ if (mod != 0) {
+ addRanges(toAddTo, mod*2);
+ addRanges(toAddTo, mod*3);
+ }
+ }
+
+ private void addRanges(Set 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 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 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 toAddTo) {
+ constraint.getMentionedValues(toAddTo);
}
}
@@ -913,14 +1098,131 @@ public class PluralRules implements Serializable {
}
public String toString() {
- String s = rule.toString();
- if (next != null) {
- s = next.toString() + "; " + s;
+ StringBuilder builder = new StringBuilder();
+ Map ordered = new TreeMap(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);
}
- return s;
+ for (Entry 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 getMentionedValues(Set toAddTo) {
+ rule.getMentionedValues(toAddTo);
+ if (next != null) {
+ next.getMentionedValues(toAddTo);
+ } else {
+ // once done, manufacture values for the OTHER case
+ int otherCount = 3;
+ NumberInfo last = null;
+ Set others = new LinkedHashSet();
+ 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 toAddTo, Set 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 a;
+ }
+ }
+
+ /**
+ * @deprecated This API is ICU internal only.
+ * @internal
+ */
+ public static final Comparator KEYWORD_COMPARATOR = new Comparator () {
+ 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 temp = new TreeSet(KEYWORD_COMPARATOR);
+ temp.addAll(rules.getKeywords());
+ this.keywords = Collections.unmodifiableSet(new LinkedHashSet(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 PluralRules
@@ -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 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 getFractionSamples() {
+ initKeyMaps();
+ return _fractionSamples;
+ }
+
private Map 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> sampleFractionMap = new HashMap>();
+ Set mentioned = rules.getMentionedValues(new TreeSet());
+ for (NumberInfo s : mentioned) {
+ String keyword = select(s.source, s.visibleFractionDigitCount, s.fractionalDigits);
+ Set list = sampleFractionMap.get(keyword);
+ if (list == null) {
+ list = new LinkedHashSet(); // 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.emptyList());
- if (--keywordsRemaining == 0) {
- break;
- }
+ }
+ if (!sampleFractionMap.containsKey(k)) {
+ sampleFractionMap.put(k, Collections.emptySet());
}
}
}
@@ -1148,7 +1510,12 @@ public class PluralRules implements Serializable {
for (Entry> entry : sampleMap.entrySet()) {
sampleMap.put(entry.getKey(), Collections.unmodifiableList(entry.getValue()));
}
+ for (Entry> 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
index 0000000000..3d9d01fe63
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesFactory.java
@@ -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 OVERRIDES = new HashMap();
+ static Relation EXTRA_SAMPLES = Relation.of(new HashMap>(), 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 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;
+ }
+ };
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java
index 5dd46a3ce3..0fa7e33ce0 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java
@@ -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 samples = rules.getFractionSamples();
+ Map keywordToRule = new HashMap();
+ 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 collisionTest = new TreeMap();
+ for (NumberInfo sample : samples) {
+ collisionTest.clear();
+ for (Entry 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 uniqueRuleSet = new HashSet();
- 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 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 explicits = (Set) test[1];
- PluralRules pluralRules = PluralRules.forLocale(locale);
+ PluralRules pluralRules = factory.forLocale(locale);
LinkedHashSet 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 ALL = Collections.unmodifiableSet(EnumSet.allOf(StandardPluralCategories.class));
+ /**
+ * Return a mutable set
+ * @param source
+ * @return
+ */
+ static final EnumSet getSet(Collection source) {
+ EnumSet result = EnumSet.noneOf(StandardPluralCategories.class);
+ for (String s : source) {
+ result.add(StandardPluralCategories.valueOf(s));
+ }
+ return result;
+ }
+ static final Comparator> SHORTEST_FIRST = new Comparator>() {
+ public int compare(Set arg0, Set 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
index 0000000000..60e105f105
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/WritePluralRulesData.java
@@ -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, Relation> keywordsToData = new TreeMap(StandardPluralCategories.SHORTEST_FIRST);
+ for (ULocale locale : pluralRulesFactory.getAvailableULocales()) {
+ builder.setLength(0);
+ PluralRules rules = pluralRulesFactory.forLocale(locale);
+ boolean firstKeyword = true;
+ EnumSet keywords = StandardPluralCategories.getSet(rules.getKeywords());
+ Relation samplesToLocales = keywordsToData.get(keywords);
+ if (samplesToLocales == null) {
+ keywordsToData.put(keywords, samplesToLocales = Relation.of(
+ new LinkedHashMap>(), LinkedHashSet.class));
+ }
+ //System.out.println(locale);
+ for (StandardPluralCategories keyword : keywords) {
+ if (firstKeyword) {
+ firstKeyword = false;
+ } else {
+ builder.append(";\t");
+ }
+ Collection 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, Relation> keywordsAndData : keywordsToData.entrySet()) {
+ System.out.println("\n // " + keywordsAndData.getKey());
+ for (Entry> samplesAndLocales : keywordsAndData.getValue().keyValuesSet()) {
+ Set 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 {
+ 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