diff options
Diffstat (limited to 'java/com/ibm/icu/simple/PluralRules.java')
-rw-r--r-- | java/com/ibm/icu/simple/PluralRules.java | 2388 |
1 files changed, 2388 insertions, 0 deletions
diff --git a/java/com/ibm/icu/simple/PluralRules.java b/java/com/ibm/icu/simple/PluralRules.java new file mode 100644 index 0000000..ab0039d --- /dev/null +++ b/java/com/ibm/icu/simple/PluralRules.java @@ -0,0 +1,2388 @@ +/* + ******************************************************************************* + * 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; + +/** + * <p> + * Defines rules for mapping non-negative numeric values onto a small set of keywords. + * </p> + * <p> + * Rules are constructed from a text description, consisting of a series of keywords and conditions. The {@link #select} + * method examines each condition in order and returns the keyword for the first condition that matches the number. If + * none match, {@link #KEYWORD_OTHER} is returned. + * </p> + * <p> + * A PluralRules object is immutable. It contains caches for sample values, but those are synchronized. + * <p> + * PluralRules is Serializable so that it can be used in formatters, which are serializable. + * </p> + * <p> + * For more information, details, and tips for writing rules, see the <a + * href="http://www.unicode.org/draft/reports/tr35/tr35.html#Language_Plural_Rules">LDML spec, C.11 Language Plural + * Rules</a> + * </p> + * <p> + * Examples: + * </p> + * + * <pre> + * "one: n is 1; few: n in 2..4" + * </pre> + * <p> + * This defines two rules, for 'one' and 'few'. The condition for 'one' is "n is 1" which means that the number must be + * equal to 1 for this condition to pass. The condition for 'few' is "n in 2..4" which means that the number must be + * between 2 and 4 inclusive - and be an integer - for this condition to pass. All other numbers are assigned the + * keyword "other" by the default rule. + * </p> + * + * <pre> + * "zero: n is 0; one: n is 1; zero: n mod 100 in 1..19" + * </pre> + * <p> + * This illustrates that the same keyword can be defined multiple times. Each rule is examined in order, and the first + * keyword whose condition passes is the one returned. Also notes that a modulus is applied to n in the last rule. Thus + * its condition holds for 119, 219, 319... + * </p> + * + * <pre> + * "one: n is 1; few: n mod 10 in 2..4 and n mod 100 not in 12..14" + * </pre> + * <p> + * This illustrates conjunction and negation. The condition for 'few' has two parts, both of which must be met: + * "n mod 10 in 2..4" and "n mod 100 not in 12..14". The first part applies a modulus to n before the test as in the + * 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... + * </p> + * <p> + * Syntax: + * </p> + * <pre> + * 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 + * </pre> + * <p>Each <b>not</b> term inverts the meaning; however, there should not be more than one of them.</p> + * <p> + * The i, f, t, and v values are defined as follows: + * </p> + * <ul> + * <li>i to be the integer digits.</li> + * <li>f to be the visible decimal digits, as an integer.</li> + * <li>t to be the visible decimal digits—without trailing zeros—as an integer.</li> + * <li>v to be the number of visible fraction digits.</li> + * <li>j is defined to only match integers. That is j is 3 fails if v != 0 (eg for 3.1 or 3.0).</li> + * </ul> + * <p> + * Examples are in the following table: + * </p> + * <table border='1' style="border-collapse:collapse"> + * <tbody> + * <tr> + * <th>n</th> + * <th>i</th> + * <th>f</th> + * <th>v</th> + * </tr> + * <tr> + * <td>1.0</td> + * <td>1</td> + * <td align="right">0</td> + * <td>1</td> + * </tr> + * <tr> + * <td>1.00</td> + * <td>1</td> + * <td align="right">0</td> + * <td>2</td> + * </tr> + * <tr> + * <td>1.3</td> + * <td>1</td> + * <td align="right">3</td> + * <td>1</td> + * </tr> + * <tr> + * <td>1.03</td> + * <td>1</td> + * <td align="right">3</td> + * <td>2</td> + * </tr> + * <tr> + * <td>1.23</td> + * <td>1</td> + * <td align="right">23</td> + * <td>2</td> + * </tr> + * </tbody> + * </table> + * <p> + * An "identifier" is a sequence of characters that do not have the Unicode Pattern_Syntax or Pattern_White_Space + * properties. + * <p> + * 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). + * </p> + * + * @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<String> 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 <code>PluralRules</code> for a given locale and the plural type. + * + * <p> + * ICU defines plural rules for many locales based on CLDR <i>Language Plural Rules</i>. 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 <code>PluralRules</code> object is returned. + * @param type + * The plural type (e.g., cardinal or ordinal). + * @return The predefined <code>PluralRules</code> 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. <br/> + * 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<FixedDecimal> { + 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<FixedDecimalRange> 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<FixedDecimalRange> 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<FixedDecimalRange> samples2 = new LinkedHashSet<FixedDecimalRange>(); + + 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<Double> addSamples(Set<Double> 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<FixedDecimalRange> getSamples() { + return samples; + } + + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public void getStartEndSamples(Set<FixedDecimal> 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<String> result = new ArrayList<String>(); + 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<Long> valueList = new ArrayList<Long>(); + + // 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 <range>", 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: <identifier> + */ + 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<Rule> rules = new ArrayList<Rule>(); + + 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<Rule> 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<String> getKeywords() { + Set<String> result = new LinkedHashSet<String>(); + 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<FixedDecimal> toAddTo, Set<FixedDecimal> 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 <code>PluralRules</code> for a given + * locale. + * Same as forLocale(locale, PluralType.CARDINAL). + * + * <p>ICU defines plural rules for many locales based on CLDR <i>Language Plural Rules</i>. + * 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 <code>PluralRules</code> object is + * returned. + * @return The predefined <code>PluralRules</code> 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 <code>PluralRules</code> for a given + * locale and the plural type. + * + * <p>ICU defines plural rules for many locales based on CLDR <i>Language Plural Rules</i>. + * 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 <code>PluralRules</code> object is + * returned. + * @param type The plural type (e.g., cardinal or ordinal). + * @return The predefined <code>PluralRules</code> 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 <code>PluralRules</code> 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 <code>PluralRules</code> + * object. The rule "other" is always present by default. + * + * @return The set of keywords. + * @stable ICU 3.8 + */ + public Set<String> 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<Double> 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<Double> 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<Double> getAllKeywordValues(String keyword, SampleType type) { + if (!isLimited(keyword, type)) { + return null; + } + Collection<Double> 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<Double> 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<Double> getSamples(String keyword, SampleType sampleType) { + if (!keywords.contains(keyword)) { + return null; + } + Set<Double> result = new TreeSet<Double>(); + + 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<Double> 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. + * <br/> + * 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<Double> explicits, + Output<Double> 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<Double> explicits, + Output<Double> uniqueValue, SampleType sampleType) { + if (uniqueValue != null) { + uniqueValue.value = null; + } + + if (!keywords.contains(keyword)) { + return KeywordStatus.INVALID; + } + + if (!isLimited(keyword, sampleType)) { + return KeywordStatus.UNBOUNDED; + } + + Collection<Double> 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<Double> subtractedSet = new HashSet<Double>(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); + } +} |