/* ******************************************************************************* * Copyright (C) 2007-2014, International Business Machines Corporation and * others. All Rights Reserved. ******************************************************************************* */ package com.ibm.icu.simple; import java.io.IOException; import java.io.NotSerializableException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TreeSet; import java.util.regex.Pattern; import com.ibm.icu.util.Output; /** *

* 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 * previous example. The second part applies a different modulus and also uses negation, thus it matches all numbers * _not_ in 12, 13, 14, 112, 113, 114, 212, 213, 214... *

*

* Syntax: *

*
 * rules         = rule (';' rule)*
 * rule          = keyword ':' condition
 * keyword       = <identifier>
 * condition     = and_condition ('or' and_condition)*
 * and_condition = relation ('and' relation)*
 * relation      = not? expr not? rel not? range_list
 * expr          = ('n' | 'i' | 'f' | 'v' | 't') (mod value)?
 * not           = 'not' | '!'
 * rel           = 'in' | 'is' | '=' | '≠' | 'within'
 * mod           = 'mod' | '%'
 * range_list    = (range | value) (',' range_list)*
 * value         = digit+
 * digit         = 0|1|2|3|4|5|6|7|8|9
 * range         = value'..'value
 * 
*

Each not term inverts the meaning; however, there should not be more than one of them.

*

* The i, f, t, and v values are defined as follows: *

* *

* Examples are in the following table: *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
nifv
1.0101
1.00102
1.3131
1.03132
1.231232
*

* An "identifier" is a sequence of characters that do not have the Unicode Pattern_Syntax or Pattern_White_Space * properties. *

* The difference between 'in' and 'within' is that 'in' only includes integers in the specified range, while 'within' * includes all values. Using 'within' with a range_list consisting entirely of values is the same as using 'in' (it's * not an error). *

* * @stable ICU 3.8 */ public class PluralRules implements Serializable { // static final UnicodeSet ALLOWED_ID = new UnicodeSet("[a-z]").freeze(); // TODO Remove RulesList by moving its API and fields into PluralRules. /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public static final String CATEGORY_SEPARATOR = "; "; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public static final String KEYWORD_RULE_SEPARATOR = ": "; private static final long serialVersionUID = 1; private final RuleList rules; private final transient Set keywords; /** * Provides a factory for returning plural rules * * @internal * @deprecated This API is ICU internal only. */ @Deprecated public static abstract class Factory { /** * Provides access to the predefined PluralRules for a given locale and the plural type. * *

* ICU defines plural rules for many locales based on CLDR Language Plural Rules. For these predefined * rules, see CLDR page at http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html * * @param locale * The locale for which a PluralRules object is returned. * @param type * The plural type (e.g., cardinal or ordinal). * @return The predefined PluralRules object for this locale. If there's no predefined rules for * this locale, the rules for the closest parent in the locale hierarchy that has one will be returned. * The final fallback always returns the default rules. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public abstract PluralRules forLocale(Locale locale, PluralType type); /** * Utility for getting CARDINAL rules. * @param locale the locale * @return plural rules. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final PluralRules forLocale(Locale locale) { return forLocale(locale, PluralType.CARDINAL); } /** * Returns the locales for which there is plurals data. * * @internal * @deprecated This API is ICU internal only. @Deprecated public abstract ULocale[] getAvailableULocales(); */ /** * Returns the 'functionally equivalent' locale with respect to plural rules. Calling PluralRules.forLocale with * the functionally equivalent locale, and with the provided locale, returns rules that behave the same.
* All locales with the same functionally equivalent locale have plural rules that behave the same. This is not * exaustive; there may be other locales whose plural rules behave the same that do not have the same equivalent * locale. * * @param locale * the locale to check * @param isAvailable * if not null and of length > 0, this will hold 'true' at index 0 if locale is directly defined * (without fallback) as having plural rules * @return the functionally-equivalent locale * @internal * @deprecated This API is ICU internal only. @Deprecated public abstract ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable); */ /** * Returns the default factory. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public static PluralRulesLoader getDefaultFactory() { return PluralRulesLoader.loader; } /** * Returns whether or not there are overrides. * @internal * @deprecated This API is ICU internal only. @Deprecated public abstract boolean hasOverride(ULocale locale); */ } // Standard keywords. /** * Common name for the 'zero' plural form. * @stable ICU 3.8 */ public static final String KEYWORD_ZERO = "zero"; /** * Common name for the 'singular' plural form. * @stable ICU 3.8 */ public static final String KEYWORD_ONE = "one"; /** * Common name for the 'dual' plural form. * @stable ICU 3.8 */ public static final String KEYWORD_TWO = "two"; /** * Common name for the 'paucal' or other special plural form. * @stable ICU 3.8 */ public static final String KEYWORD_FEW = "few"; /** * Common name for the arabic (11 to 99) plural form. * @stable ICU 3.8 */ public static final String KEYWORD_MANY = "many"; /** * Common name for the default plural form. This name is returned * for values to which no other form in the rule applies. It * can additionally be assigned rules of its own. * @stable ICU 3.8 */ public static final String KEYWORD_OTHER = "other"; /** * Value returned by {@link #getUniqueKeywordValue} when there is no * unique value to return. * @stable ICU 4.8 */ public static final double NO_UNIQUE_VALUE = -0.00123456777; /** * Type of plurals and PluralRules. * @stable ICU 50 */ public enum PluralType { /** * Plural rules for cardinal numbers: 1 file vs. 2 files. * @stable ICU 50 */ CARDINAL, /** * Plural rules for ordinal numbers: 1st file, 2nd file, 3rd file, 4th file, etc. * @stable ICU 50 */ ORDINAL }; /* * The default constraint that is always satisfied. */ private static final Constraint NO_CONSTRAINT = new Constraint() { private static final long serialVersionUID = 9163464945387899416L; public boolean isFulfilled(FixedDecimal n) { return true; } public boolean isLimited(SampleType sampleType) { return false; } public String toString() { return ""; } }; /** * */ private static final Rule DEFAULT_RULE = new Rule("other", NO_CONSTRAINT, null, null); /** * Parses a plural rules description and returns a PluralRules. * @param description the rule description. * @throws ParseException if the description cannot be parsed. * The exception index is typically not set, it will be -1. * @stable ICU 3.8 */ public static PluralRules parseDescription(String description) throws ParseException { description = description.trim(); return description.length() == 0 ? DEFAULT : new PluralRules(parseRuleChain(description)); } /** * Creates a PluralRules from a description if it is parsable, * otherwise returns null. * @param description the rule description. * @return the PluralRules * @stable ICU 3.8 */ public static PluralRules createRules(String description) { try { return parseDescription(description); } catch(Exception e) { return null; } } /** * The default rules that accept any number and return * {@link #KEYWORD_OTHER}. * @stable ICU 3.8 */ public static final PluralRules DEFAULT = new PluralRules(new RuleList().addRule(DEFAULT_RULE)); private enum Operand { n, i, f, t, v, w, /* deprecated */ j; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public static class FixedDecimal extends Number implements Comparable { private static final long serialVersionUID = -4756200506571685661L; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final double source; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final int visibleDecimalDigitCount; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final int visibleDecimalDigitCountWithoutTrailingZeros; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final long decimalDigits; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final long decimalDigitsWithoutTrailingZeros; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final long integerValue; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final boolean hasIntegerValue; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final boolean isNegative; private final int baseFactor; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public double getSource() { return source; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public int getVisibleDecimalDigitCount() { return visibleDecimalDigitCount; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public int getVisibleDecimalDigitCountWithoutTrailingZeros() { return visibleDecimalDigitCountWithoutTrailingZeros; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public long getDecimalDigits() { return decimalDigits; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public long getDecimalDigitsWithoutTrailingZeros() { return decimalDigitsWithoutTrailingZeros; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public long getIntegerValue() { return integerValue; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public boolean isHasIntegerValue() { return hasIntegerValue; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public boolean isNegative() { return isNegative; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public int getBaseFactor() { return baseFactor; } static final long MAX = (long)1E18; /** * @internal * @deprecated This API is ICU internal only. * @param n is the original number * @param v number of digits to the right of the decimal place. e.g 1.00 = 2 25. = 0 * @param f Corresponds to f in the plural rules grammar. * The digits to the right of the decimal place as an integer. e.g 1.10 = 10 */ @Deprecated public FixedDecimal(double n, int v, long f) { isNegative = n < 0; source = isNegative ? -n : n; visibleDecimalDigitCount = v; decimalDigits = f; integerValue = n > MAX ? MAX : (long)n; hasIntegerValue = source == integerValue; // check values. TODO make into unit test. // // long visiblePower = (int) Math.pow(10, v); // if (fractionalDigits > visiblePower) { // throw new IllegalArgumentException(); // } // double fraction = intValue + (fractionalDigits / (double) visiblePower); // if (fraction != source) { // double diff = Math.abs(fraction - source)/(Math.abs(fraction) + Math.abs(source)); // if (diff > 0.00000001d) { // throw new IllegalArgumentException(); // } // } if (f == 0) { decimalDigitsWithoutTrailingZeros = 0; visibleDecimalDigitCountWithoutTrailingZeros = 0; } else { long fdwtz = f; int trimmedCount = v; while ((fdwtz%10) == 0) { fdwtz /= 10; --trimmedCount; } decimalDigitsWithoutTrailingZeros = fdwtz; visibleDecimalDigitCountWithoutTrailingZeros = trimmedCount; } baseFactor = (int) Math.pow(10, v); } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public FixedDecimal(double n, int v) { this(n,v,getFractionalDigits(n, v)); } private static int getFractionalDigits(double n, int v) { if (v == 0) { return 0; } else { if (n < 0) { n = -n; } int baseFactor = (int) Math.pow(10, v); long scaled = Math.round(n * baseFactor); return (int) (scaled % baseFactor); } } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public FixedDecimal(double n) { this(n, decimals(n)); } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public FixedDecimal(long n) { this(n,0); } private static final long MAX_INTEGER_PART = 1000000000; /** * Return a guess as to the number of decimals that would be displayed. This is only a guess; callers should * always supply the decimals explicitly if possible. Currently, it is up to 6 decimals (without trailing zeros). * Returns 0 for infinities and nans. * @internal * @deprecated This API is ICU internal only. * */ @Deprecated public static int decimals(double n) { // Ugly... if (Double.isInfinite(n) || Double.isNaN(n)) { return 0; } if (n < 0) { n = -n; } if (n < MAX_INTEGER_PART) { long temp = (long)(n * 1000000) % 1000000; // get 6 decimals for (int mask = 10, digits = 6; digits > 0; mask *= 10, --digits) { if ((temp % mask) != 0) { return digits; } } return 0; } else { String buf = String.format(Locale.ENGLISH, "%1.15e", n); int ePos = buf.lastIndexOf('e'); int expNumPos = ePos + 1; if (buf.charAt(expNumPos) == '+') { expNumPos++; } String exponentStr = buf.substring(expNumPos); int exponent = Integer.parseInt(exponentStr); int numFractionDigits = ePos - 2 - exponent; if (numFractionDigits < 0) { return 0; } for (int i=ePos-1; numFractionDigits > 0; --i) { if (buf.charAt(i) != '0') { break; } --numFractionDigits; } return numFractionDigits; } } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public FixedDecimal (String n) { // Ugly, but for samples we don't care. this(Double.parseDouble(n), getVisibleFractionCount(n)); } private static int getVisibleFractionCount(String value) { value = value.trim(); int decimalPos = value.indexOf('.') + 1; if (decimalPos == 0) { return 0; } else { return value.length() - decimalPos; } } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public double get(Operand operand) { switch(operand) { default: return source; case i: return integerValue; case f: return decimalDigits; case t: return decimalDigitsWithoutTrailingZeros; case v: return visibleDecimalDigitCount; case w: return visibleDecimalDigitCountWithoutTrailingZeros; } } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public static Operand getOperand(String t) { return Operand.valueOf(t); } /** * We're not going to care about NaN. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public int compareTo(FixedDecimal other) { if (integerValue != other.integerValue) { return integerValue < other.integerValue ? -1 : 1; } if (source != other.source) { return source < other.source ? -1 : 1; } if (visibleDecimalDigitCount != other.visibleDecimalDigitCount) { return visibleDecimalDigitCount < other.visibleDecimalDigitCount ? -1 : 1; } long diff = decimalDigits - other.decimalDigits; if (diff != 0) { return diff < 0 ? -1 : 1; } return 0; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public boolean equals(Object arg0) { if (arg0 == null) { return false; } if (arg0 == this) { return true; } if (!(arg0 instanceof FixedDecimal)) { return false; } FixedDecimal other = (FixedDecimal)arg0; return source == other.source && visibleDecimalDigitCount == other.visibleDecimalDigitCount && decimalDigits == other.decimalDigits; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public int hashCode() { // TODO Auto-generated method stub return (int)(decimalDigits + 37 * (visibleDecimalDigitCount + (int)(37 * source))); } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public String toString() { return String.format("%." + visibleDecimalDigitCount + "f", source); } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public boolean hasIntegerValue() { return hasIntegerValue; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public int intValue() { // TODO Auto-generated method stub return (int)integerValue; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public long longValue() { return integerValue; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public float floatValue() { return (float) source; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public double doubleValue() { return isNegative ? -source : source; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public long getShiftedValue() { return integerValue * baseFactor + decimalDigits; } private void writeObject( ObjectOutputStream out) throws IOException { throw new NotSerializableException(); } private void readObject(ObjectInputStream in ) throws IOException, ClassNotFoundException { throw new NotSerializableException(); } } /** * Selection parameter for either integer-only or decimal-only. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public enum SampleType { /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated INTEGER, /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated DECIMAL } /** * A range of NumberInfo that includes all values with the same visibleFractionDigitCount. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public static class FixedDecimalRange { /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final FixedDecimal start; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final FixedDecimal end; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public FixedDecimalRange(FixedDecimal start, FixedDecimal end) { if (start.visibleDecimalDigitCount != end.visibleDecimalDigitCount) { throw new IllegalArgumentException("Ranges must have the same number of visible decimals: " + start + "~" + end); } this.start = start; this.end = end; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public String toString() { return start + (end == start ? "" : "~" + end); } } /** * A list of NumberInfo that includes all values with the same visibleFractionDigitCount. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public static class FixedDecimalSamples { /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final SampleType sampleType; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final Set samples; /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final boolean bounded; /** * The samples must be immutable. * @param sampleType * @param samples */ private FixedDecimalSamples(SampleType sampleType, Set samples, boolean bounded) { super(); this.sampleType = sampleType; this.samples = samples; this.bounded = bounded; } /* * Parse a list of the form described in CLDR. The source must be trimmed. */ static FixedDecimalSamples parse(String source) { SampleType sampleType2; boolean bounded2 = true; boolean haveBound = false; Set samples2 = new LinkedHashSet(); if (source.startsWith("integer")) { sampleType2 = SampleType.INTEGER; } else if (source.startsWith("decimal")) { sampleType2 = SampleType.DECIMAL; } else { throw new IllegalArgumentException("Samples must start with 'integer' or 'decimal'"); } source = source.substring(7).trim(); // remove both for (String range : COMMA_SEPARATED.split(source)) { if (range.equals("…") || range.equals("...")) { bounded2 = false; haveBound = true; continue; } if (haveBound) { throw new IllegalArgumentException("Can only have … at the end of samples: " + range); } String[] rangeParts = TILDE_SEPARATED.split(range); switch (rangeParts.length) { case 1: FixedDecimal sample = new FixedDecimal(rangeParts[0]); checkDecimal(sampleType2, sample); samples2.add(new FixedDecimalRange(sample, sample)); break; case 2: FixedDecimal start = new FixedDecimal(rangeParts[0]); FixedDecimal end = new FixedDecimal(rangeParts[1]); checkDecimal(sampleType2, start); checkDecimal(sampleType2, end); samples2.add(new FixedDecimalRange(start, end)); break; default: throw new IllegalArgumentException("Ill-formed number range: " + range); } } return new FixedDecimalSamples(sampleType2, Collections.unmodifiableSet(samples2), bounded2); } private static void checkDecimal(SampleType sampleType2, FixedDecimal sample) { if ((sampleType2 == SampleType.INTEGER) != (sample.getVisibleDecimalDigitCount() == 0)) { throw new IllegalArgumentException("Ill-formed number range: " + sample); } } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public Set addSamples(Set result) { for (FixedDecimalRange item : samples) { // we have to convert to longs so we don't get strange double issues long startDouble = item.start.getShiftedValue(); long endDouble = item.end.getShiftedValue(); for (long d = startDouble; d <= endDouble; d += 1) { result.add(d/(double)item.start.baseFactor); } } return result; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public String toString() { StringBuilder b = new StringBuilder("@").append(sampleType.toString().toLowerCase(Locale.ENGLISH)); boolean first = true; for (FixedDecimalRange item : samples) { if (first) { first = false; } else { b.append(","); } b.append(' ').append(item); } if (!bounded) { b.append(", …"); } return b.toString(); } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public Set getSamples() { return samples; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public void getStartEndSamples(Set target) { for (FixedDecimalRange item : samples) { target.add(item.start); target.add(item.end); } } } /* * A constraint on a number. */ private interface Constraint extends Serializable { /* * Returns true if the number fulfills the constraint. * @param n the number to test, >= 0. */ boolean isFulfilled(FixedDecimal n); /* * Returns false if an unlimited number of values fulfills the * constraint. */ boolean isLimited(SampleType sampleType); } private static final boolean isBreakAndIgnore(char c) { return c <= 0x20 && (c == 0x20 || c == 9 || c == 0xa || c == 0xc || c == 0xd); } private static final boolean isBreakAndKeep(char c) { return c <= '=' && c >= '!' && (c == '!' || c == '%' || c == ',' || c == '.' || c == '='); } static class SimpleTokenizer { // static final UnicodeSet BREAK_AND_IGNORE = new UnicodeSet(0x09, 0x0a, 0x0c, 0x0d, 0x20, 0x20).freeze(); // static final UnicodeSet BREAK_AND_KEEP = new UnicodeSet('!', '!', '%', '%', ',', ',', '.', '.', '=', '=').freeze(); static String[] split(String source) { int last = -1; List result = new ArrayList(); for (int i = 0; i < source.length(); ++i) { char ch = source.charAt(i); if (isBreakAndIgnore(ch) /* BREAK_AND_IGNORE.contains(ch) */) { if (last >= 0) { result.add(source.substring(last,i)); last = -1; } } else if (isBreakAndKeep(ch) /* BREAK_AND_KEEP.contains(ch) */) { if (last >= 0) { result.add(source.substring(last,i)); } result.add(source.substring(i,i+1)); last = -1; } else if (last < 0) { last = i; } } if (last >= 0) { result.add(source.substring(last)); } return result.toArray(new String[result.size()]); } } /* * syntax: * condition : or_condition * and_condition * or_condition : and_condition 'or' condition * and_condition : relation * relation 'and' relation * relation : in_relation * within_relation * in_relation : not? expr not? in not? range * within_relation : not? expr not? 'within' not? range * not : 'not' * '!' * expr : 'n' * 'n' mod value * mod : 'mod' * '%' * in : 'in' * 'is' * '=' * '≠' * value : digit+ * digit : 0|1|2|3|4|5|6|7|8|9 * range : value'..'value */ private static Constraint parseConstraint(String description) throws ParseException { Constraint result = null; String[] or_together = OR_SEPARATED.split(description); for (int i = 0; i < or_together.length; ++i) { Constraint andConstraint = null; String[] and_together = AND_SEPARATED.split(or_together[i]); for (int j = 0; j < and_together.length; ++j) { Constraint newConstraint = NO_CONSTRAINT; String condition = and_together[j].trim(); String[] tokens = SimpleTokenizer.split(condition); int mod = 0; boolean inRange = true; boolean integersOnly = true; double lowBound = Long.MAX_VALUE; double highBound = Long.MIN_VALUE; long[] vals = null; int x = 0; String t = tokens[x++]; boolean hackForCompatibility = false; Operand operand; try { operand = FixedDecimal.getOperand(t); } catch (Exception e) { throw unexpected(t, condition); } if (x < tokens.length) { t = tokens[x++]; if ("mod".equals(t) || "%".equals(t)) { mod = Integer.parseInt(tokens[x++]); t = nextToken(tokens, x++, condition); } if ("not".equals(t)) { inRange = !inRange; t = nextToken(tokens, x++, condition); if ("=".equals(t)) { throw unexpected(t, condition); } } else if ("!".equals(t)) { inRange = !inRange; t = nextToken(tokens, x++, condition); if (!"=".equals(t)) { throw unexpected(t, condition); } } if ("is".equals(t) || "in".equals(t) || "=".equals(t)) { hackForCompatibility = "is".equals(t); if (hackForCompatibility && !inRange) { throw unexpected(t, condition); } t = nextToken(tokens, x++, condition); } else if ("within".equals(t)) { integersOnly = false; t = nextToken(tokens, x++, condition); } else { throw unexpected(t, condition); } if ("not".equals(t)) { if (!hackForCompatibility && !inRange) { throw unexpected(t, condition); } inRange = !inRange; t = nextToken(tokens, x++, condition); } List valueList = new ArrayList(); // the token t is always one item ahead while (true) { long low = Long.parseLong(t); long high = low; if (x < tokens.length) { t = nextToken(tokens, x++, condition); if (t.equals(".")) { t = nextToken(tokens, x++, condition); if (!t.equals(".")) { throw unexpected(t, condition); } t = nextToken(tokens, x++, condition); high = Long.parseLong(t); if (x < tokens.length) { t = nextToken(tokens, x++, condition); if (!t.equals(",")) { // adjacent number: 1 2 // no separator, fail throw unexpected(t, condition); } } } else if (!t.equals(",")) { // adjacent number: 1 2 // no separator, fail throw unexpected(t, condition); } } // at this point, either we are out of tokens, or t is ',' if (low > high) { throw unexpected(low + "~" + high, condition); } else if (mod != 0 && high >= mod) { throw unexpected(high + ">mod=" + mod, condition); } valueList.add(low); valueList.add(high); lowBound = Math.min(lowBound, low); highBound = Math.max(highBound, high); if (x >= tokens.length) { break; } t = nextToken(tokens, x++, condition); } if (t.equals(",")) { throw unexpected(t, condition); } if (valueList.size() == 2) { vals = null; } else { vals = new long[valueList.size()]; for (int k = 0; k < vals.length; ++k) { vals[k] = valueList.get(k); } } // Hack to exclude "is not 1,2" if (lowBound != highBound && hackForCompatibility && !inRange) { throw unexpected("is not ", condition); } newConstraint = new RangeConstraint(mod, inRange, operand, integersOnly, lowBound, highBound, vals); } if (andConstraint == null) { andConstraint = newConstraint; } else { andConstraint = new AndConstraint(andConstraint, newConstraint); } } if (result == null) { result = andConstraint; } else { result = new OrConstraint(result, andConstraint); } } return result; } static final Pattern AT_SEPARATED = Pattern.compile("\\s*\\Q\\E@\\s*"); static final Pattern OR_SEPARATED = Pattern.compile("\\s*or\\s*"); static final Pattern AND_SEPARATED = Pattern.compile("\\s*and\\s*"); static final Pattern COMMA_SEPARATED = Pattern.compile("\\s*,\\s*"); static final Pattern DOTDOT_SEPARATED = Pattern.compile("\\s*\\Q..\\E\\s*"); static final Pattern TILDE_SEPARATED = Pattern.compile("\\s*~\\s*"); static final Pattern SEMI_SEPARATED = Pattern.compile("\\s*;\\s*"); /* Returns a parse exception wrapping the token and context strings. */ private static ParseException unexpected(String token, String context) { return new ParseException("unexpected token '" + token + "' in '" + context + "'", -1); } /* * Returns the token at x if available, else throws a parse exception. */ private static String nextToken(String[] tokens, int x, String context) throws ParseException { if (x < tokens.length) { return tokens[x]; } throw new ParseException("missing token at end of '" + context + "'", -1); } /* * Syntax: * rule : keyword ':' condition * keyword: */ private static Rule parseRule(String description) throws ParseException { if (description.length() == 0) { return DEFAULT_RULE; } description = description.toLowerCase(Locale.ENGLISH); int x = description.indexOf(':'); if (x == -1) { throw new ParseException("missing ':' in rule description '" + description + "'", 0); } String keyword = description.substring(0, x).trim(); if (!isValidKeyword(keyword)) { throw new ParseException("keyword '" + keyword + " is not valid", 0); } description = description.substring(x+1).trim(); String[] constraintOrSamples = AT_SEPARATED.split(description); boolean sampleFailure = false; FixedDecimalSamples integerSamples = null, decimalSamples = null; switch (constraintOrSamples.length) { case 1: break; case 2: integerSamples = FixedDecimalSamples.parse(constraintOrSamples[1]); if (integerSamples.sampleType == SampleType.DECIMAL) { decimalSamples = integerSamples; integerSamples = null; } break; case 3: integerSamples = FixedDecimalSamples.parse(constraintOrSamples[1]); decimalSamples = FixedDecimalSamples.parse(constraintOrSamples[2]); if (integerSamples.sampleType != SampleType.INTEGER || decimalSamples.sampleType != SampleType.DECIMAL) { throw new IllegalArgumentException("Must have @integer then @decimal in " + description); } break; default: throw new IllegalArgumentException("Too many samples in " + description); } if (sampleFailure) { throw new IllegalArgumentException("Ill-formed samples—'@' characters."); } // 'other' is special, and must have no rules; all other keywords must have rules. boolean isOther = keyword.equals("other"); if (isOther != (constraintOrSamples[0].length() == 0)) { throw new IllegalArgumentException("The keyword 'other' must have no constraints, just samples."); } Constraint constraint; if (isOther) { constraint = NO_CONSTRAINT; } else { constraint = parseConstraint(constraintOrSamples[0]); } return new Rule(keyword, constraint, integerSamples, decimalSamples); } /* * Syntax: * rules : rule * rule ';' rules */ private static RuleList parseRuleChain(String description) throws ParseException { RuleList result = new RuleList(); // remove trailing ; if (description.endsWith(";")) { description = description.substring(0,description.length()-1); } String[] rules = SEMI_SEPARATED.split(description); for (int i = 0; i < rules.length; ++i) { Rule rule = parseRule(rules[i].trim()); result.hasExplicitBoundingInfo |= rule.integerSamples != null || rule.decimalSamples != null; result.addRule(rule); } return result.finish(); } /* * An implementation of Constraint representing a modulus, * a range of values, and include/exclude. Provides lots of * convenience factory methods. */ private static class RangeConstraint implements Constraint, Serializable { private static final long serialVersionUID = 1; private final int mod; private final boolean inRange; private final boolean integersOnly; private final double lowerBound; private final double upperBound; private final long[] range_list; private final Operand operand; RangeConstraint(int mod, boolean inRange, Operand operand, boolean integersOnly, double lowBound, double highBound, long[] vals) { this.mod = mod; this.inRange = inRange; this.integersOnly = integersOnly; this.lowerBound = lowBound; this.upperBound = highBound; this.range_list = vals; this.operand = operand; } public boolean isFulfilled(FixedDecimal number) { double n = number.get(operand); if ((integersOnly && (n - (long)n) != 0.0 || operand == Operand.j && number.visibleDecimalDigitCount != 0)) { return !inRange; } if (mod != 0) { n = n % mod; // java % handles double numerator the way we want } boolean test = n >= lowerBound && n <= upperBound; if (test && range_list != null) { test = false; for (int i = 0; !test && i < range_list.length; i += 2) { test = n >= range_list[i] && n <= range_list[i+1]; } } return inRange == test; } public boolean isLimited(SampleType sampleType) { boolean valueIsZero = lowerBound == upperBound && lowerBound == 0d; boolean hasDecimals = (operand == Operand.v || operand == Operand.w || operand == Operand.f || operand == Operand.t) && inRange != valueIsZero; // either NOT f = zero or f = non-zero switch (sampleType) { case INTEGER: return hasDecimals // will be empty || (operand == Operand.n || operand == Operand.i || operand == Operand.j) && mod == 0 && inRange; case DECIMAL: return (!hasDecimals || operand == Operand.n || operand == Operand.j) && (integersOnly || lowerBound == upperBound) && mod == 0 && inRange; } return false; } public String toString() { StringBuilder result = new StringBuilder(); result.append(operand); if (mod != 0) { result.append(" % ").append(mod); } boolean isList = lowerBound != upperBound; result.append( !isList ? (inRange ? " = " : " != ") : integersOnly ? (inRange ? " = " : " != ") : (inRange ? " within " : " not within ") ); if (range_list != null) { 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 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; protected BinaryConstraint(Constraint a, Constraint b) { this.a = a; this.b = b; } } /* A constraint representing the logical and of two constraints. */ private static class AndConstraint extends BinaryConstraint { private static final long serialVersionUID = 7766999779862263523L; AndConstraint(Constraint a, Constraint b) { super(a, b); } public boolean isFulfilled(FixedDecimal n) { return a.isFulfilled(n) && b.isFulfilled(n); } public boolean isLimited(SampleType sampleType) { // we ignore the case where both a and b are unlimited but no values // satisfy both-- we still consider this 'unlimited' return a.isLimited(sampleType) || b.isLimited(sampleType); } public String toString() { return a.toString() + " and " + b.toString(); } } /* A constraint representing the logical or of two constraints. */ private static class OrConstraint extends BinaryConstraint { private static final long serialVersionUID = 1405488568664762222L; OrConstraint(Constraint a, Constraint b) { super(a, b); } public boolean isFulfilled(FixedDecimal n) { return a.isFulfilled(n) || b.isFulfilled(n); } public boolean isLimited(SampleType sampleType) { return a.isLimited(sampleType) && b.isLimited(sampleType); } public String toString() { return a.toString() + " or " + b.toString(); } } /* * Implementation of Rule that uses a constraint. * Provides 'and' and 'or' to combine constraints. Immutable. */ private static class Rule implements Serializable { private static final long serialVersionUID = 1; private final String keyword; private final Constraint constraint; private final FixedDecimalSamples integerSamples; private final FixedDecimalSamples decimalSamples; public Rule(String keyword, Constraint constraint, FixedDecimalSamples integerSamples, FixedDecimalSamples decimalSamples) { this.keyword = keyword; this.constraint = constraint; this.integerSamples = integerSamples; this.decimalSamples = decimalSamples; } @SuppressWarnings("unused") public Rule and(Constraint c) { return new Rule(keyword, new AndConstraint(constraint, c), integerSamples, decimalSamples); } @SuppressWarnings("unused") public Rule or(Constraint c) { return new Rule(keyword, new OrConstraint(constraint, c), integerSamples, decimalSamples); } public String getKeyword() { return keyword; } public boolean appliesTo(FixedDecimal n) { return constraint.isFulfilled(n); } public boolean isLimited(SampleType sampleType) { return constraint.isLimited(sampleType); } public String toString() { return keyword + ": " + constraint.toString() + (integerSamples == null ? "" : " " + integerSamples.toString()) + (decimalSamples == null ? "" : " " + decimalSamples.toString()); } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public int hashCode() { return keyword.hashCode() ^ constraint.hashCode(); } public String getConstraint() { return constraint.toString(); } } private static class RuleList implements Serializable { private boolean hasExplicitBoundingInfo = false; private static final long serialVersionUID = 1; private final List rules = new ArrayList(); public RuleList addRule(Rule nextRule) { String keyword = nextRule.getKeyword(); for (Rule rule : rules) { if (keyword.equals(rule.getKeyword())) { throw new IllegalArgumentException("Duplicate keyword: " + keyword); } } rules.add(nextRule); return this; } public RuleList finish() throws ParseException { // make sure that 'other' is present, and at the end. Rule otherRule = null; for (Iterator it = rules.iterator(); it.hasNext();) { Rule rule = it.next(); if ("other".equals(rule.getKeyword())) { otherRule = rule; it.remove(); } } if (otherRule == null) { otherRule = parseRule("other:"); // make sure we have always have an 'other' a rule } rules.add(otherRule); return this; } private Rule selectRule(FixedDecimal n) { for (Rule rule : rules) { if (rule.appliesTo(n)) { return rule; } } return null; } public String select(FixedDecimal n) { if (Double.isInfinite(n.source) || Double.isNaN(n.source)) { return KEYWORD_OTHER; } Rule r = selectRule(n); return r.getKeyword(); } public Set getKeywords() { Set result = new LinkedHashSet(); for (Rule rule : rules) { result.add(rule.getKeyword()); } // since we have explict 'other', we don't need this. //result.add(KEYWORD_OTHER); return result; } public boolean isLimited(String keyword, SampleType sampleType) { if (hasExplicitBoundingInfo) { FixedDecimalSamples mySamples = getDecimalSamples(keyword, sampleType); return mySamples == null ? true : mySamples.bounded; } return computeLimited(keyword, sampleType); } public boolean computeLimited(String keyword, SampleType sampleType) { // if all rules with this keyword are limited, it's limited, // and if there's no rule with this keyword, it's unlimited boolean result = false; for (Rule rule : rules) { if (keyword.equals(rule.getKeyword())) { if (!rule.isLimited(sampleType)) { return false; } result = true; } } return result; } public String toString() { StringBuilder builder = new StringBuilder(); for (Rule rule : rules) { if (builder.length() != 0) { builder.append(CATEGORY_SEPARATOR); } builder.append(rule); } return builder.toString(); } public String getRules(String keyword) { for (Rule rule : rules) { if (rule.getKeyword().equals(keyword)) { return rule.getConstraint(); } } return null; } public boolean select(FixedDecimal sample, String keyword) { for (Rule rule : rules) { if (rule.getKeyword().equals(keyword) && rule.appliesTo(sample)) { return true; } } return false; } public FixedDecimalSamples getDecimalSamples(String keyword, SampleType sampleType) { for (Rule rule : rules) { if (rule.getKeyword().equals(keyword)) { return sampleType == SampleType.INTEGER ? rule.integerSamples : rule.decimalSamples; } } return null; } } /** * @deprecated This API is ICU internal only. * @internal */ @Deprecated public enum StandardPluralCategories { /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated zero, /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated one, /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated two, /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated few, /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated many, /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated other; static StandardPluralCategories forString(String s) { StandardPluralCategories a; try { a = valueOf(s); } catch (Exception e) { return null; } return a; } } @SuppressWarnings("unused") private boolean addConditional(Set toAddTo, Set others, double trial) { boolean added; FixedDecimal toAdd = new FixedDecimal(trial); if (!toAddTo.contains(toAdd) && !others.contains(toAdd)) { others.add(toAdd); added = true; } else { added = false; } return added; } // ------------------------------------------------------------------------- // Static class methods. // ------------------------------------------------------------------------- /** * Provides access to the predefined cardinal-number PluralRules for a given * locale. * Same as forLocale(locale, PluralType.CARDINAL). * *

ICU defines plural rules for many locales based on CLDR Language Plural Rules. * For these predefined rules, see CLDR page at * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html * * @param locale The locale for which a PluralRules object is * returned. * @return The predefined PluralRules object for this locale. * If there's no predefined rules for this locale, the rules * for the closest parent in the locale hierarchy that has one will * be returned. The final fallback always returns the default * rules. * @stable ICU 3.8 */ public static PluralRules forLocale(Locale locale) { return forLocale(locale, PluralType.CARDINAL); } /** * Provides access to the predefined PluralRules for a given * locale and the plural type. * *

ICU defines plural rules for many locales based on CLDR Language Plural Rules. * For these predefined rules, see CLDR page at * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html * * @param locale The locale for which a PluralRules object is * returned. * @param type The plural type (e.g., cardinal or ordinal). * @return The predefined PluralRules object for this locale. * If there's no predefined rules for this locale, the rules * for the closest parent in the locale hierarchy that has one will * be returned. The final fallback always returns the default * rules. * @stable ICU 50 */ public static PluralRules forLocale(Locale locale, PluralType type) { return Factory.getDefaultFactory().forLocale(locale, type); } /* * Checks whether a token is a valid keyword. * * @param token the token to be checked * @return true if the token is a valid keyword. */ private static boolean isValidKeyword(String token) { // return ALLOWED_ID.containsAll(token); for (int i = 0; i < token.length(); ++i) { char c = token.charAt(i); if (!('a' <= c && c <= 'z')) { return false; } } return true; } /* * Creates a new PluralRules object. Immutable. */ private PluralRules(RuleList rules) { this.rules = rules; this.keywords = Collections.unmodifiableSet(rules.getKeywords()); } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated @Override public int hashCode() { return rules.hashCode(); } /** * 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. * @stable ICU 4.0 */ public String select(double number) { return rules.select(new FixedDecimal(number)); } /** * 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. */ @Deprecated public String select(double number, int countVisibleFractionDigits, long fractionaldigits) { return rules.select(new FixedDecimal(number, countVisibleFractionDigits, fractionaldigits)); } /** * Given a number information, returns the keyword of the first rule that applies to * the number. * * @param sample The number information for which the rule has to be determined. * @return The keyword of the selected rule. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public String select(FixedDecimal sample) { return rules.select(sample); } /** * Given a number information, and keyword, return whether the keyword would match the number. * * @param sample The number information for which the rule has to be determined. * @param keyword The keyword to filter on * @internal * @deprecated This API is ICU internal only. */ @Deprecated public boolean matches(FixedDecimal sample, String keyword) { return rules.select(sample, keyword); } /** * Returns a set of all rule keywords used in this PluralRules * object. The rule "other" is always present by default. * * @return The set of keywords. * @stable ICU 3.8 */ public Set getKeywords() { return keywords; } /** * Returns the unique value that this keyword matches, or {@link #NO_UNIQUE_VALUE} * if the keyword matches multiple values or is not defined for this PluralRules. * * @param keyword the keyword to check for a unique value * @return The unique value for the keyword, or NO_UNIQUE_VALUE. * @stable ICU 4.8 */ public double getUniqueKeywordValue(String keyword) { Collection values = getAllKeywordValues(keyword); if (values != null && values.size() == 1) { return values.iterator().next(); } return NO_UNIQUE_VALUE; } /** * Returns all the values that trigger this keyword, or null if the number of such * values is unlimited. * * @param keyword the keyword * @return the values that trigger this keyword, or null. The returned collection * is immutable. It will be empty if the keyword is not defined. * @stable ICU 4.8 */ public Collection getAllKeywordValues(String keyword) { return getAllKeywordValues(keyword, SampleType.INTEGER); } /** * Returns all the values that trigger this keyword, or null if the number of such * values is unlimited. * * @param keyword the keyword * @param type the type of samples requested, INTEGER or DECIMAL * @return the values that trigger this keyword, or null. The returned collection * is immutable. It will be empty if the keyword is not defined. * * @internal * @deprecated This API is ICU internal only. */ @Deprecated public Collection getAllKeywordValues(String keyword, SampleType type) { if (!isLimited(keyword, type)) { return null; } Collection samples = getSamples(keyword, type); return samples == null ? null : Collections.unmodifiableCollection(samples); } /** * Returns a list of integer 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. * @stable ICU 4.8 */ public Collection getSamples(String keyword) { return getSamples(keyword, SampleType.INTEGER); } /** * 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. The keyword might be defined, and yet have an empty set of samples, * IF there are samples for the other sampleType. * * @param keyword the keyword to test * @param sampleType the type of samples requested, INTEGER or DECIMAL * @return a list of values matching the keyword. * @internal * @deprecated ICU internal only */ @Deprecated public Collection getSamples(String keyword, SampleType sampleType) { if (!keywords.contains(keyword)) { return null; } Set result = new TreeSet(); if (rules.hasExplicitBoundingInfo) { FixedDecimalSamples samples = rules.getDecimalSamples(keyword, sampleType); return samples == null ? Collections.unmodifiableSet(result) : Collections.unmodifiableSet(samples.addSamples(result)); } // hack in case the rule is created without explicit samples int maxCount = isLimited(keyword, sampleType) ? Integer.MAX_VALUE : 20; switch (sampleType) { case INTEGER: for (int i = 0; i < 200; ++i) { if (!addSample(keyword, i, maxCount, result)) { break; } } addSample(keyword, 1000000, maxCount, result); // hack for Welsh break; case DECIMAL: for (int i = 0; i < 2000; ++i) { if (!addSample(keyword, new FixedDecimal(i/10d, 1), maxCount, result)) { break; } } addSample(keyword, new FixedDecimal(1000000d, 1), maxCount, result); // hack for Welsh break; } return result.size() == 0 ? null : Collections.unmodifiableSet(result); } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public boolean addSample(String keyword, Number sample, int maxCount, Set result) { String selectedKeyword = sample instanceof FixedDecimal ? select((FixedDecimal)sample) : select(sample.doubleValue()); if (selectedKeyword.equals(keyword)) { result.add(sample.doubleValue()); if (--maxCount < 0) { return false; } } return true; } /** * Returns a list of values for which select() would return that keyword, * or null if the keyword is not defined or no samples are available. * 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 * @param sampleType the type of samples requested, INTEGER or DECIMAL * @return a list of values matching the keyword. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public FixedDecimalSamples getDecimalSamples(String keyword, SampleType sampleType) { return rules.getDecimalSamples(keyword, sampleType); } /** * Returns the set of locales for which PluralRules are known. * @return the set of locales for which PluralRules are known, as a list * @draft ICU 4.2 * @provisional This API might change or be removed in a future release. public static ULocale[] getAvailableULocales() { return Factory.getDefaultFactory().getAvailableULocales(); } */ /** * Returns the 'functionally equivalent' locale with respect to * plural rules. Calling PluralRules.forLocale with the functionally equivalent * locale, and with the provided locale, returns rules that behave the same. *
* All locales with the same functionally equivalent locale have * plural rules that behave the same. This is not exaustive; * there may be other locales whose plural rules behave the same * that do not have the same equivalent locale. * * @param locale the locale to check * @param isAvailable if not null and of length > 0, this will hold 'true' at * index 0 if locale is directly defined (without fallback) as having plural rules * @return the functionally-equivalent locale * @draft ICU 4.2 * @provisional This API might change or be removed in a future release. public static ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable) { return Factory.getDefaultFactory().getFunctionalEquivalent(locale, isAvailable); } */ /** * {@inheritDoc} * @stable ICU 3.8 */ public String toString() { return rules.toString(); } /** * {@inheritDoc} * @stable ICU 3.8 */ public boolean equals(Object rhs) { return rhs instanceof PluralRules && equals((PluralRules)rhs); } /** * Returns true if rhs is equal to this. * @param rhs the PluralRules to compare to. * @return true if this and rhs are equal. * @stable ICU 3.8 */ // TODO Optimize this public boolean equals(PluralRules rhs) { return rhs != null && toString().equals(rhs.toString()); } /** * Status of the keyword for the rules, given a set of explicit values. * * @draft ICU 50 * @provisional This API might change or be removed in a future release. */ public enum KeywordStatus { /** * The keyword is not valid for the rules. * * @draft ICU 50 * @provisional This API might change or be removed in a future release. */ INVALID, /** * The keyword is valid, but unused (it is covered by the explicit values, OR has no values for the given {@link SampleType}). * * @draft ICU 50 * @provisional This API might change or be removed in a future release. */ SUPPRESSED, /** * The keyword is valid, used, and has a single possible value (before considering explicit values). * * @draft ICU 50 * @provisional This API might change or be removed in a future release. */ UNIQUE, /** * The keyword is valid, used, not unique, and has a finite set of values. * * @draft ICU 50 * @provisional This API might change or be removed in a future release. */ BOUNDED, /** * The keyword is valid but not bounded; there indefinitely many matching values. * * @draft ICU 50 * @provisional This API might change or be removed in a future release. */ UNBOUNDED } /** * Find the status for the keyword, given a certain set of explicit values. * * @param keyword * the particular keyword (call rules.getKeywords() to get the valid ones) * @param offset * the offset used, or 0.0d if not. Internally, the offset is subtracted from each explicit value before * checking against the keyword values. * @param explicits * a set of Doubles that are used explicitly (eg [=0], "[=1]"). May be empty or null. * @param uniqueValue * If non null, set to the unique value. * @return the KeywordStatus * @draft ICU 50 * @provisional This API might change or be removed in a future release. */ public KeywordStatus getKeywordStatus(String keyword, int offset, Set explicits, Output uniqueValue) { return getKeywordStatus(keyword, offset, explicits, uniqueValue, SampleType.INTEGER); } /** * Find the status for the keyword, given a certain set of explicit values. * * @param keyword * the particular keyword (call rules.getKeywords() to get the valid ones) * @param offset * the offset used, or 0.0d if not. Internally, the offset is subtracted from each explicit value before * checking against the keyword values. * @param explicits * a set of Doubles that are used explicitly (eg [=0], "[=1]"). May be empty or null. * @param sampleType * request KeywordStatus relative to INTEGER or DECIMAL values * @param uniqueValue * If non null, set to the unique value. * @return the KeywordStatus * @internal * @provisional This API might change or be removed in a future release. */ public KeywordStatus getKeywordStatus(String keyword, int offset, Set explicits, Output uniqueValue, SampleType sampleType) { if (uniqueValue != null) { uniqueValue.value = null; } if (!keywords.contains(keyword)) { return KeywordStatus.INVALID; } if (!isLimited(keyword, sampleType)) { return KeywordStatus.UNBOUNDED; } Collection values = getSamples(keyword, sampleType); int originalSize = values.size(); if (explicits == null) { explicits = Collections.emptySet(); } // Quick check on whether there are multiple elements if (originalSize > explicits.size()) { if (originalSize == 1) { if (uniqueValue != null) { uniqueValue.value = values.iterator().next(); } return KeywordStatus.UNIQUE; } return KeywordStatus.BOUNDED; } // Compute if the quick test is insufficient. HashSet subtractedSet = new HashSet(values); for (Double explicit : explicits) { subtractedSet.remove(explicit - offset); } if (subtractedSet.size() == 0) { return KeywordStatus.SUPPRESSED; } if (uniqueValue != null && subtractedSet.size() == 1) { uniqueValue.value = subtractedSet.iterator().next(); } return originalSize == 1 ? KeywordStatus.UNIQUE : KeywordStatus.BOUNDED; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public String getRules(String keyword) { return rules.getRules(keyword); } /* private void writeObject( ObjectOutputStream out) throws IOException { throw new NotSerializableException(); } private void readObject(ObjectInputStream in ) throws IOException, ClassNotFoundException { throw new NotSerializableException(); } private Object writeReplace() throws ObjectStreamException { return new PluralRulesSerialProxy(toString()); } */ /** * @internal * @deprecated internal */ @Deprecated public int compareTo(PluralRules other) { return toString().compareTo(other.toString()); } /** * @internal * @deprecated internal */ @Deprecated public Boolean isLimited(String keyword) { return rules.isLimited(keyword, SampleType.INTEGER); } /** * @internal * @deprecated internal */ @Deprecated public boolean isLimited(String keyword, SampleType sampleType) { return rules.isLimited(keyword, sampleType); } /** * @internal * @deprecated internal */ @Deprecated public boolean computeLimited(String keyword, SampleType sampleType) { return rules.computeLimited(keyword, sampleType); } }