aboutsummaryrefslogtreecommitdiff
path: root/src/com/ibm/icu/simple/PluralRules.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/ibm/icu/simple/PluralRules.java')
-rw-r--r--src/com/ibm/icu/simple/PluralRules.java2388
1 files changed, 2388 insertions, 0 deletions
diff --git a/src/com/ibm/icu/simple/PluralRules.java b/src/com/ibm/icu/simple/PluralRules.java
new file mode 100644
index 0000000..ab0039d
--- /dev/null
+++ b/src/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>
+ * &quot;one: n is 1; few: n in 2..4&quot;
+ * </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>
+ * &quot;zero: n is 0; one: n is 1; zero: n mod 100 in 1..19&quot;
+ * </pre>
+ * <p>
+ * This illustrates that the same keyword can be defined multiple times. Each rule is examined in order, and the first
+ * keyword whose condition passes is the one returned. Also notes that a modulus is applied to n in the last rule. Thus
+ * its condition holds for 119, 219, 319...
+ * </p>
+ *
+ * <pre>
+ * &quot;one: n is 1; few: n mod 10 in 2..4 and n mod 100 not in 12..14&quot;
+ * </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 = &lt;identifier&gt;
+ * 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);
+ }
+}