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: *

* *

* Examples are in the following table: *

- * + *
* * - * - * - * + * + * + * + * * * * - * + * + * * * * * - * + * + * * * * * - * + * + * * * * * - * + * + * * * * * - * + * + * * * * @@ -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, String> rulesToLocale = Relation.of( + new TreeMap, Set>( + new CollectionUtilities.MapComparator()), 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 keywords = oldRules.getKeywords(); + if (newRules != null) { + TreeSet temp = new TreeSet(PluralRules.KEYWORD_COMPARATOR); + temp.addAll(keywords); + temp.addAll(newRules.getKeywords()); + keywords = temp; + } + Map temp = new LinkedHashMap(); + for (String keyword : keywords) { + Collection oldFractionSamples = oldRules.getFractionSamples(keyword); + Collection 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, Set> entry : rulesToLocale.keyValuesSet()) { + String localeList = CollectionUtilities.join(entry.getValue(), " "); + for (Entry 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 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 keywordToPattern = new TreeMap(PluralRules.KEYWORD_COMPARATOR); + final Map 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 set) { + final Map 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 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 skippedLocales = new LinkedHashSet(); + 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 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"}, + }; + +} diff --git a/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/CollectionUtilities.java b/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/CollectionUtilities.java index f6ce7835b2..7849c27441 100644 --- a/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/CollectionUtilities.java +++ b/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/CollectionUtilities.java @@ -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 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 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 int compare(Iterator iterator1, Iterator 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 > int compare(U o1, U o2) { + int diff = o1.size() - o2.size(); + if (diff != 0) { + return diff; + } + Iterator iterator1 = o1.iterator(); + Iterator iterator2 = o2.iterator(); + return compare(iterator1, iterator2); + } + + /** + * Compare, with shortest first, and otherwise lexicographically + * @param a + * @param b + * @return + */ + public static > 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 + implements Comparator> { + public int compare(Set o1, Set o2) { + return CollectionUtilities.compare(o1, o2); + } + }; + + + public static class CollectionComparator + implements Comparator> { + public int compare(Collection o1, Collection o2) { + return CollectionUtilities.compare(o1, o2); + } + }; + + /** + * Compare, allowing nulls and putting them first + * @param a + * @param b + * @return + */ + public static > 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 > int compareEntrySets(Collection o1, Collection o2) { + int diff = o1.size() - o2.size(); + if (diff != 0) { + return diff; + } + Iterator iterator1 = o1.iterator(); + Iterator 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 implements Comparator> { + public int compare(Map o1, Map o2) { + return CollectionUtilities.compareEntrySets(o1.entrySet(), o2.entrySet()); + } + }; + + public static class ComparableComparator implements Comparator { + public int compare(T arg0, T arg1) { + return CollectionUtilities.compare(arg0, arg1); + } + } } diff --git a/icu4j/main/tests/translit/src/com/ibm/icu/dev/util/UnicodeProperty.java b/icu4j/main/tests/translit/src/com/ibm/icu/dev/util/UnicodeProperty.java index 0752f3740e..9a2de6c9a9 100644 --- a/icu4j/main/tests/translit/src/com/ibm/icu/dev/util/UnicodeProperty.java +++ b/icu4j/main/tests/translit/src/com/ibm/icu/dev/util/UnicodeProperty.java @@ -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));
nfvnifv
1.00101
1.000102
1.33131
1.033132
1.23231232