2 *******************************************************************************
\r
3 * Copyright (C) 2007-2010, International Business Machines Corporation and *
\r
4 * others. All Rights Reserved. *
\r
5 *******************************************************************************
\r
8 package com.ibm.icu.text;
\r
10 import java.io.Serializable;
\r
11 import java.text.ParseException;
\r
12 import java.util.Collections;
\r
13 import java.util.HashSet;
\r
14 import java.util.Locale;
\r
15 import java.util.Set;
\r
17 import com.ibm.icu.impl.PluralRulesLoader;
\r
18 import com.ibm.icu.impl.Utility;
\r
19 import com.ibm.icu.util.ULocale;
\r
22 * <p>Defines rules for mapping positive double values onto a small set of
\r
23 * keywords. Serializable so can be used in formatters, which are
\r
24 * serializable. Rules are constructed from a text description, consisting
\r
25 * of a series of keywords and conditions. The {@link #select} method
\r
26 * examines each condition in order and returns the keyword for the
\r
27 * first condition that matches the number. If none match,
\r
28 * {@link #KEYWORD_OTHER} is returned.</p>
\r
31 * "one: n is 1; few: n in 2..4"</pre></p>
\r
33 * This defines two rules, for 'one' and 'few'. The condition for
\r
34 * 'one' is "n is 1" which means that the number must be equal to
\r
35 * 1 for this condition to pass. The condition for 'few' is
\r
36 * "n in 2..4" which means that the number must be between 2 and
\r
37 * 4 inclusive - and be an integer - for this condition to pass. All other
\r
38 * numbers are assigned the keyword "other" by the default rule.</p>
\r
40 * "zero: n is 0; one: n is 1; zero: n mod 100 in 1..19"</pre>
\r
41 * This illustrates that the same keyword can be defined multiple times.
\r
42 * Each rule is examined in order, and the first keyword whose condition
\r
43 * passes is the one returned. Also notes that a modulus is applied
\r
44 * to n in the last rule. Thus its condition holds for 119, 219, 319...</p>
\r
46 * "one: n is 1; few: n mod 10 in 2..4 and n mod 100 not in 12..14"</pre></p>
\r
48 * This illustrates conjunction and negation. The condition for 'few'
\r
49 * has two parts, both of which must be met: "n mod 10 in 2..4" and
\r
50 * "n mod 100 not in 12..14". The first part applies a modulus to n
\r
51 * before the test as in the previous example. The second part applies
\r
52 * a different modulus and also uses negation, thus it matches all
\r
53 * numbers _not_ in 12, 13, 14, 112, 113, 114, 212, 213, 214...</p>
\r
56 * rules = rule (';' rule)*
\r
57 * rule = keyword ':' condition
\r
58 * keyword = <identifier>
\r
59 * condition = and_condition ('or' and_condition)*
\r
60 * and_condition = relation ('and' relation)*
\r
61 * relation = is_relation | in_relation | within_relation | 'n' <EOL>
\r
62 * is_relation = expr 'is' ('not')? value
\r
63 * in_relation = expr ('not')? 'in' range
\r
64 * within_relation = expr ('not')? 'within' range
\r
65 * expr = 'n' ('mod' value)?
\r
67 * digit = 0|1|2|3|4|5|6|7|8|9
\r
68 * range = value'..'value
\r
71 * The difference between 'in' and 'within' is that 'in' only includes
\r
72 * integers in the specified range, while 'within' includes all values.</p>
\r
75 public class PluralRules implements Serializable {
\r
76 private static final long serialVersionUID = 1;
\r
78 private final RuleList rules;
\r
79 private final Set<String> keywords;
\r
80 private int repeatLimit; // for equality test
\r
82 // Standard keywords.
\r
85 * Common name for the 'zero' plural form.
\r
88 public static final String KEYWORD_ZERO = "zero";
\r
91 * Common name for the 'singular' plural form.
\r
94 public static final String KEYWORD_ONE = "one";
\r
97 * Common name for the 'dual' plural form.
\r
100 public static final String KEYWORD_TWO = "two";
\r
103 * Common name for the 'paucal' or other special plural form.
\r
106 public static final String KEYWORD_FEW = "few";
\r
109 * Common name for the arabic (11 to 99) plural form.
\r
112 public static final String KEYWORD_MANY = "many";
\r
115 * Common name for the default plural form. This name is returned
\r
116 * for values to which no other form in the rule applies. It
\r
117 * can additionally be assigned rules of its own.
\r
120 public static final String KEYWORD_OTHER = "other";
\r
123 * The set of all characters a valid keyword can start with.
\r
125 private static final UnicodeSet START_CHARS =
\r
126 new UnicodeSet("[[:ID_Start:][_]]");
\r
129 * The set of all characters a valid keyword can contain after
\r
130 * the first character.
\r
132 private static final UnicodeSet CONT_CHARS =
\r
133 new UnicodeSet("[:ID_Continue:]");
\r
136 * The default constraint that is always satisfied.
\r
138 private static final Constraint NO_CONSTRAINT = new Constraint() {
\r
139 private static final long serialVersionUID = 9163464945387899416L;
\r
141 public boolean isFulfilled(double n) {
\r
144 public String toString() {
\r
148 public int updateRepeatLimit(int limit) {
\r
154 * The default rule that always returns "other".
\r
156 private static final Rule DEFAULT_RULE = new Rule() {
\r
157 private static final long serialVersionUID = -5677499073940822149L;
\r
159 public String getKeyword() {
\r
160 return KEYWORD_OTHER;
\r
163 public boolean appliesTo(double n) {
\r
167 public String toString() {
\r
168 return "(" + KEYWORD_OTHER + ")";
\r
171 public int updateRepeatLimit(int limit) {
\r
178 * The default rules that accept any number and return
\r
179 * {@link #KEYWORD_OTHER}.
\r
182 public static final PluralRules DEFAULT =
\r
183 new PluralRules(new RuleChain(DEFAULT_RULE));
\r
186 * Parses a plural rules description and returns a PluralRules.
\r
187 * @param description the rule description.
\r
188 * @throws ParseException if the description cannot be parsed.
\r
189 * The exception index is typically not set, it will be -1.
\r
192 public static PluralRules parseDescription(String description)
\r
193 throws ParseException {
\r
195 description = description.trim();
\r
196 if (description.length() == 0) {
\r
200 return new PluralRules(parseRuleChain(description));
\r
204 * Creates a PluralRules from a description if it is parsable,
\r
205 * otherwise returns null.
\r
206 * @param description the rule description.
\r
207 * @return the PluralRules
\r
210 public static PluralRules createRules(String description) {
\r
212 return parseDescription(description);
\r
213 } catch(ParseException e) {
\r
219 * A constraint on a number.
\r
221 private interface Constraint extends Serializable {
\r
223 * Returns true if the number fulfills the constraint.
\r
224 * @param n the number to test, >= 0.
\r
226 boolean isFulfilled(double n);
\r
229 * Returns the larger of limit or the limit of this constraint.
\r
230 * If the constraint is a simple range test, this is the higher
\r
231 * end of the range; if it is a modulo test, this is the modulus.
\r
233 * @param limit the target limit
\r
234 * @return the new limit
\r
236 int updateRepeatLimit(int limit);
\r
240 * A pluralization rule. .
\r
242 private interface Rule extends Serializable {
\r
243 /* Returns the keyword that names this rule. */
\r
244 String getKeyword();
\r
245 /* Returns true if the rule applies to the number. */
\r
246 boolean appliesTo(double n);
\r
247 /* Returns the larger of limit and this rule's limit. */
\r
248 int updateRepeatLimit(int limit);
\r
252 * A list of rules to apply in order.
\r
254 private interface RuleList extends Serializable {
\r
255 /* Returns the keyword of the first rule that applies to the number. */
\r
256 String select(double n);
\r
258 /* Returns the set of defined keywords. */
\r
259 Set<String> getKeywords();
\r
261 /* Return the value at which this rulelist starts repeating. */
\r
262 int getRepeatLimit();
\r
267 * condition : or_condition
\r
269 * or_condition : and_condition 'or' condition
\r
270 * and_condition : relation
\r
271 * relation 'and' relation
\r
272 * relation : is_relation
\r
276 * is_relation : expr 'is' value
\r
277 * expr 'is' 'not' value
\r
278 * in_relation : expr 'in' range
\r
279 * expr 'not' 'in' range
\r
280 * within_relation : expr 'within' range
\r
281 * expr 'not' 'within' range
\r
285 * digit : 0|1|2|3|4|5|6|7|8|9
\r
286 * range : value'..'value
\r
288 private static Constraint parseConstraint(String description)
\r
289 throws ParseException {
\r
291 description = description.trim().toLowerCase(Locale.ENGLISH);
\r
293 Constraint result = null;
\r
294 String[] or_together = Utility.splitString(description, "or");
\r
295 for (int i = 0; i < or_together.length; ++i) {
\r
296 Constraint andConstraint = null;
\r
297 String[] and_together = Utility.splitString(or_together[i], "and");
\r
298 for (int j = 0; j < and_together.length; ++j) {
\r
299 Constraint newConstraint = NO_CONSTRAINT;
\r
301 String condition = and_together[j].trim();
\r
302 String[] tokens = Utility.splitWhitespace(condition);
\r
305 boolean inRange = true;
\r
306 boolean integersOnly = true;
\r
307 long lowBound = -1;
\r
308 long highBound = -1;
\r
310 boolean isRange = false;
\r
313 String t = tokens[x++];
\r
314 if (!"n".equals(t)) {
\r
315 throw unexpected(t, condition);
\r
317 if (x < tokens.length) {
\r
319 if ("mod".equals(t)) {
\r
320 mod = Integer.parseInt(tokens[x++]);
\r
321 t = nextToken(tokens, x++, condition);
\r
323 if ("is".equals(t)) {
\r
324 t = nextToken(tokens, x++, condition);
\r
325 if ("not".equals(t)) {
\r
327 t = nextToken(tokens, x++, condition);
\r
331 if ("not".equals(t)) {
\r
333 t = nextToken(tokens, x++, condition);
\r
335 if ("in".equals(t)) {
\r
336 t = nextToken(tokens, x++, condition);
\r
337 } else if ("within".equals(t)) {
\r
338 integersOnly = false;
\r
339 t = nextToken(tokens, x++, condition);
\r
341 throw unexpected(t, condition);
\r
346 String[] pair = Utility.splitString(t, "..");
\r
347 if (pair.length == 2) {
\r
348 lowBound = Long.parseLong(pair[0]);
\r
349 highBound = Long.parseLong(pair[1]);
\r
351 throw unexpected(t, condition);
\r
354 lowBound = highBound = Long.parseLong(t);
\r
357 if (x != tokens.length) {
\r
358 throw unexpected(tokens[x], condition);
\r
362 new RangeConstraint(mod, inRange, integersOnly, lowBound, highBound);
\r
365 if (andConstraint == null) {
\r
366 andConstraint = newConstraint;
\r
368 andConstraint = new AndConstraint(andConstraint,
\r
373 if (result == null) {
\r
374 result = andConstraint;
\r
376 result = new OrConstraint(result, andConstraint);
\r
383 /* Returns a parse exception wrapping the token and context strings. */
\r
384 private static ParseException unexpected(String token, String context) {
\r
385 return new ParseException("unexpected token '" + token +
\r
386 "' in '" + context + "'", -1);
\r
390 * Returns the token at x if available, else throws a parse exception.
\r
392 private static String nextToken(String[] tokens, int x, String context)
\r
393 throws ParseException {
\r
394 if (x < tokens.length) {
\r
397 throw new ParseException("missing token at end of '" + context + "'", -1);
\r
402 * rule : keyword ':' condition
\r
403 * keyword: <identifier>
\r
405 private static Rule parseRule(String description) throws ParseException {
\r
406 int x = description.indexOf(':');
\r
408 throw new ParseException("missing ':' in rule description '" +
\r
409 description + "'", 0);
\r
412 String keyword = description.substring(0, x).trim();
\r
413 if (!isValidKeyword(keyword)) {
\r
414 throw new ParseException("keyword '" + keyword +
\r
415 " is not valid", 0);
\r
418 description = description.substring(x+1).trim();
\r
419 if (description.length() == 0) {
\r
420 throw new ParseException("missing constraint in '" +
\r
421 description + "'", x+1);
\r
423 Constraint constraint = parseConstraint(description);
\r
424 Rule rule = new ConstrainedRule(keyword, constraint);
\r
433 private static RuleChain parseRuleChain(String description)
\r
434 throws ParseException {
\r
436 RuleChain rc = null;
\r
437 String[] rules = Utility.split(description, ';');
\r
438 for (int i = 0; i < rules.length; ++i) {
\r
439 Rule r = parseRule(rules[i].trim());
\r
441 rc = new RuleChain(r);
\r
443 rc = rc.addRule(r);
\r
450 * An implementation of Constraint representing a modulus,
\r
451 * a range of values, and include/exclude. Provides lots of
\r
452 * convenience factory methods.
\r
454 private static class RangeConstraint implements Constraint, Serializable {
\r
455 private static final long serialVersionUID = 1;
\r
458 private boolean inRange;
\r
459 private boolean integersOnly;
\r
460 private long lowerBound;
\r
461 private long upperBound;
\r
463 public boolean isFulfilled(double n) {
\r
464 if (integersOnly && (n - (long)n) != 0.0) {
\r
468 n = n % mod; // java % handles double numerator the way we want
\r
470 return inRange == (n >= lowerBound && n <= upperBound);
\r
473 RangeConstraint(int mod, boolean inRange, boolean integersOnly,
\r
474 long lowerBound, long upperBound) {
\r
476 this.inRange = inRange;
\r
477 this.integersOnly = integersOnly;
\r
478 this.lowerBound = lowerBound;
\r
479 this.upperBound = upperBound;
\r
482 public int updateRepeatLimit(int limit) {
\r
483 int mylimit = mod == 0 ? (int)upperBound : mod;
\r
484 return Math.max(mylimit, limit);
\r
487 public String toString() {
\r
488 return "[mod: " + mod + " inRange: " + inRange +
\r
489 " integersOnly: " + integersOnly +
\r
490 " low: " + lowerBound + " high: " + upperBound + "]";
\r
494 /* Convenience base class for and/or constraints. */
\r
495 private static abstract class BinaryConstraint implements Constraint,
\r
497 private static final long serialVersionUID = 1;
\r
498 protected final Constraint a;
\r
499 protected final Constraint b;
\r
500 private final String conjunction;
\r
502 protected BinaryConstraint(Constraint a, Constraint b, String c) {
\r
505 this.conjunction = c;
\r
508 public int updateRepeatLimit(int limit) {
\r
509 return a.updateRepeatLimit(b.updateRepeatLimit(limit));
\r
512 public String toString() {
\r
513 return a.toString() + conjunction + b.toString();
\r
517 /* A constraint representing the logical and of two constraints. */
\r
518 private static class AndConstraint extends BinaryConstraint {
\r
519 private static final long serialVersionUID = 7766999779862263523L;
\r
521 AndConstraint(Constraint a, Constraint b) {
\r
522 super(a, b, " && ");
\r
525 public boolean isFulfilled(double n) {
\r
526 return a.isFulfilled(n) && b.isFulfilled(n);
\r
530 /* A constraint representing the logical or of two constraints. */
\r
531 private static class OrConstraint extends BinaryConstraint {
\r
532 private static final long serialVersionUID = 1405488568664762222L;
\r
534 OrConstraint(Constraint a, Constraint b) {
\r
535 super(a, b, " || ");
\r
538 public boolean isFulfilled(double n) {
\r
539 return a.isFulfilled(n) || b.isFulfilled(n);
\r
544 * Implementation of Rule that uses a constraint.
\r
545 * Provides 'and' and 'or' to combine constraints. Immutable.
\r
547 private static class ConstrainedRule implements Rule, Serializable {
\r
548 private static final long serialVersionUID = 1;
\r
549 private final String keyword;
\r
550 private final Constraint constraint;
\r
552 public ConstrainedRule(String keyword, Constraint constraint) {
\r
553 this.keyword = keyword;
\r
554 this.constraint = constraint;
\r
557 @SuppressWarnings("unused")
\r
558 public Rule and(Constraint c) {
\r
559 return new ConstrainedRule(keyword, new AndConstraint(constraint, c));
\r
562 @SuppressWarnings("unused")
\r
563 public Rule or(Constraint c) {
\r
564 return new ConstrainedRule(keyword, new OrConstraint(constraint, c));
\r
567 public String getKeyword() {
\r
571 public boolean appliesTo(double n) {
\r
572 return constraint.isFulfilled(n);
\r
575 public int updateRepeatLimit(int limit) {
\r
576 return constraint.updateRepeatLimit(limit);
\r
579 public String toString() {
\r
580 return keyword + ": " + constraint;
\r
585 * Implementation of RuleList that is itself a node in a linked list.
\r
586 * Immutable, but supports chaining with 'addRule'.
\r
588 private static class RuleChain implements RuleList, Serializable {
\r
589 private static final long serialVersionUID = 1;
\r
590 private final Rule rule;
\r
591 private final RuleChain next;
\r
593 /** Creates a rule chain with the single rule. */
\r
594 public RuleChain(Rule rule) {
\r
598 private RuleChain(Rule rule, RuleChain next) {
\r
603 public RuleChain addRule(Rule nextRule) {
\r
604 return new RuleChain(nextRule, this);
\r
607 private Rule selectRule(double n) {
\r
609 if (next != null) {
\r
610 r = next.selectRule(n);
\r
612 if (r == null && rule.appliesTo(n)) {
\r
618 public String select(double n) {
\r
619 Rule r = selectRule(n);
\r
621 return KEYWORD_OTHER;
\r
623 return r.getKeyword();
\r
626 public Set<String> getKeywords() {
\r
627 Set<String> result = new HashSet<String>();
\r
628 result.add(KEYWORD_OTHER);
\r
629 RuleChain rc = this;
\r
630 while (rc != null) {
\r
631 result.add(rc.rule.getKeyword());
\r
637 public int getRepeatLimit() {
\r
639 RuleChain rc = this;
\r
640 while (rc != null) {
\r
641 result = rc.rule.updateRepeatLimit(result);
\r
647 public String toString() {
\r
648 String s = rule.toString();
\r
649 if (next != null) {
\r
650 s = next.toString() + "; " + s;
\r
656 // -------------------------------------------------------------------------
\r
657 // Static class methods.
\r
658 // -------------------------------------------------------------------------
\r
661 * Provides access to the predefined <code>PluralRules</code> for a given
\r
664 * @param locale The locale for which a <code>PluralRules</code> object is
\r
666 * @return The predefined <code>PluralRules</code> object for this locale.
\r
667 * If there's no predefined rules for this locale, the rules
\r
668 * for the closest parent in the locale hierarchy that has one will
\r
669 * be returned. The final fallback always returns the default
\r
673 public static PluralRules forLocale(ULocale locale) {
\r
674 return PluralRulesLoader.loader.forLocale(locale);
\r
678 * Checks whether a token is a valid keyword.
\r
680 * @param token the token to be checked
\r
681 * @return true if the token is a valid keyword.
\r
683 private static boolean isValidKeyword(String token) {
\r
684 if (token.length() > 0 && START_CHARS.contains(token.charAt(0))) {
\r
685 for (int i = 1; i < token.length(); ++i) {
\r
686 if (!CONT_CHARS.contains(token.charAt(i))) {
\r
696 * Creates a new <code>PluralRules</code> object. Immutable.
\r
698 private PluralRules(RuleList rules) {
\r
699 this.rules = rules;
\r
700 this.keywords = Collections.unmodifiableSet(rules.getKeywords());
\r
704 * Given a number, returns the keyword of the first rule that applies to
\r
707 * @param number The number for which the rule has to be determined.
\r
708 * @return The keyword of the selected rule.
\r
711 public String select(double number) {
\r
712 return rules.select(number);
\r
716 * Returns a set of all rule keywords used in this <code>PluralRules</code>
\r
717 * object. The rule "other" is always present by default.
\r
719 * @return The set of keywords.
\r
722 public Set<String> getKeywords() {
\r
727 * Returns the set of locales for which PluralRules are known.
\r
728 * @return the set of locales for which PluralRules are known, as a list
\r
730 * @provisional This API might change or be removed in a future release.
\r
732 public static ULocale[] getAvailableULocales() {
\r
733 return PluralRulesLoader.loader.getAvailableULocales();
\r
737 * Returns the 'functionally equivalent' locale with respect to
\r
738 * plural rules. Calling PluralRules.forLocale with the functionally equivalent
\r
739 * locale, and with the provided locale, returns rules that behave the same.
\r
741 * All locales with the same functionally equivalent locale have
\r
742 * plural rules that behave the same. This is not exaustive;
\r
743 * there may be other locales whose plural rules behave the same
\r
744 * that do not have the same equivalent locale.
\r
746 * @param locale the locale to check
\r
747 * @param isAvailable if not null and of length > 0, this will hold 'true' at
\r
748 * index 0 if locale is directly defined (without fallback) as having plural rules
\r
749 * @return the functionally-equivalent locale
\r
751 * @provisional This API might change or be removed in a future release.
\r
753 public static ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable) {
\r
754 return PluralRulesLoader.loader.getFunctionalEquivalent(locale, isAvailable);
\r
761 public String toString() {
\r
762 return "keywords: " + keywords + " rules: " + rules.toString() +
\r
763 " limit: " + getRepeatLimit();
\r
770 public int hashCode() {
\r
771 return keywords.hashCode();
\r
778 public boolean equals(Object rhs) {
\r
779 return rhs instanceof PluralRules && equals((PluralRules)rhs);
\r
783 * Return tif rhs is equal to this.
\r
784 * @param rhs the PluralRules to compare to.
\r
785 * @return true if this and rhs are equal.
\r
788 public boolean equals(PluralRules rhs) {
\r
792 if (rhs == this) {
\r
795 if (!rhs.getKeywords().equals(keywords)) {
\r
799 int limit = Math.max(getRepeatLimit(), rhs.getRepeatLimit());
\r
800 for (int i = 0; i < limit; ++i) {
\r
801 if (!select(i).equals(rhs.select(i))) {
\r
808 private int getRepeatLimit() {
\r
809 if (repeatLimit == 0) {
\r
810 repeatLimit = rules.getRepeatLimit() + 1;
\r
812 return repeatLimit;
\r