2 *******************************************************************************
\r
3 * Copyright (C) 2007-2009, International Business Machines Corporation and *
\r
4 * others. All Rights Reserved. *
\r
5 *******************************************************************************
\r
8 package com.ibm.icu.text;
\r
10 import com.ibm.icu.impl.PluralRulesLoader;
\r
11 import com.ibm.icu.impl.Utility;
\r
12 import com.ibm.icu.util.ULocale;
\r
14 import java.io.Serializable;
\r
16 import java.text.ParseException;
\r
17 import java.util.Collections;
\r
18 import java.util.HashSet;
\r
19 import java.util.Locale;
\r
20 import java.util.Set;
\r
23 * <p>Defines rules for mapping positive double values onto a small set of
\r
24 * keywords. Serializable so can be used in formatters, which are
\r
25 * serializable. Rules are constructed from a text description, consisting
\r
26 * of a series of keywords and conditions. The {@link #select} method
\r
27 * examines each condition in order and returns the keyword for the
\r
28 * first condition that matches the number. If none match,
\r
29 * {@link #KEYWORD_OTHER} is returned.</p>
\r
32 * "one: n is 1; few: n in 2..4"</pre></p>
\r
34 * This defines two rules, for 'one' and 'few'. The condition for
\r
35 * 'one' is "n is 1" which means that the number must be equal to
\r
36 * 1 for this condition to pass. The condition for 'few' is
\r
37 * "n in 2..4" which means that the number must be between 2 and
\r
38 * 4 inclusive - and be an integer - for this condition to pass. All other
\r
39 * numbers are assigned the keyword "other" by the default rule.</p>
\r
41 * "zero: n is 0; one: n is 1; zero: n mod 100 in 1..19"</pre>
\r
42 * This illustrates that the same keyword can be defined multiple times.
\r
43 * Each rule is examined in order, and the first keyword whose condition
\r
44 * passes is the one returned. Also notes that a modulus is applied
\r
45 * to n in the last rule. Thus its condition holds for 119, 219, 319...</p>
\r
47 * "one: n is 1; few: n mod 10 in 2..4 and n mod 100 not in 12..14"</pre></p>
\r
49 * This illustrates conjunction and negation. The condition for 'few'
\r
50 * has two parts, both of which must be met: "n mod 10 in 2..4" and
\r
51 * "n mod 100 not in 12..14". The first part applies a modulus to n
\r
52 * before the test as in the previous example. The second part applies
\r
53 * a different modulus and also uses negation, thus it matches all
\r
54 * numbers _not_ in 12, 13, 14, 112, 113, 114, 212, 213, 214...</p>
\r
57 * rules = rule (';' rule)*
\r
58 * rule = keyword ':' condition
\r
59 * keyword = <identifier>
\r
60 * condition = and_condition ('or' and_condition)*
\r
61 * and_condition = relation ('and' relation)*
\r
62 * relation = is_relation | in_relation | within_relation | 'n' <EOL>
\r
63 * is_relation = expr 'is' ('not')? value
\r
64 * in_relation = expr ('not')? 'in' range
\r
65 * within_relation = expr ('not')? 'within' range
\r
66 * expr = 'n' ('mod' value)?
\r
68 * digit = 0|1|2|3|4|5|6|7|8|9
\r
69 * range = value'..'value
\r
72 * The difference between 'in' and 'within' is that 'in' only includes
\r
73 * integers in the specified range, while 'within' includes all values.</p>
\r
76 public class PluralRules implements Serializable {
\r
77 private static final long serialVersionUID = 1;
\r
79 private final RuleList rules;
\r
80 private final Set keywords;
\r
81 private int repeatLimit; // for equality test
\r
83 // Standard keywords.
\r
86 * Common name for the 'zero' plural form.
\r
89 public static final String KEYWORD_ZERO = "zero";
\r
92 * Common name for the 'singular' plural form.
\r
95 public static final String KEYWORD_ONE = "one";
\r
98 * Common name for the 'dual' plural form.
\r
101 public static final String KEYWORD_TWO = "two";
\r
104 * Common name for the 'paucal' or other special plural form.
\r
107 public static final String KEYWORD_FEW = "few";
\r
110 * Common name for the arabic (11 to 99) plural form.
\r
113 public static final String KEYWORD_MANY = "many";
\r
116 * Common name for the default plural form. This name is returned
\r
117 * for values to which no other form in the rule applies. It
\r
118 * can additionally be assigned rules of its own.
\r
121 public static final String KEYWORD_OTHER = "other";
\r
124 * The set of all characters a valid keyword can start with.
\r
126 private static final UnicodeSet START_CHARS =
\r
127 new UnicodeSet("[[:ID_Start:][_]]");
\r
130 * The set of all characters a valid keyword can contain after
\r
131 * the first character.
\r
133 private static final UnicodeSet CONT_CHARS =
\r
134 new UnicodeSet("[:ID_Continue:]");
\r
137 * The default constraint that is always satisfied.
\r
139 private static final Constraint NO_CONSTRAINT = new Constraint() {
\r
140 private static final long serialVersionUID = 9163464945387899416L;
\r
142 public boolean isFulfilled(double n) {
\r
145 public String toString() {
\r
149 public int updateRepeatLimit(int limit) {
\r
155 * The default rule that always returns "other".
\r
157 private static final Rule DEFAULT_RULE = new Rule() {
\r
158 private static final long serialVersionUID = -5677499073940822149L;
\r
160 public String getKeyword() {
\r
161 return KEYWORD_OTHER;
\r
164 public boolean appliesTo(double n) {
\r
168 public String toString() {
\r
169 return "(" + KEYWORD_OTHER + ")";
\r
172 public int updateRepeatLimit(int limit) {
\r
179 * The default rules that accept any number and return
\r
180 * {@link #KEYWORD_OTHER}.
\r
183 public static final PluralRules DEFAULT =
\r
184 new PluralRules(new RuleChain(DEFAULT_RULE));
\r
187 * Parses a plural rules description and returns a PluralRules.
\r
188 * @param description the rule description.
\r
189 * @throws ParseException if the description cannot be parsed.
\r
190 * The exception index is typically not set, it will be -1.
\r
193 public static PluralRules parseDescription(String description)
\r
194 throws ParseException {
\r
196 description = description.trim();
\r
197 if (description.length() == 0) {
\r
201 return new PluralRules(parseRuleChain(description));
\r
205 * Creates a PluralRules from a description if it is parsable,
\r
206 * otherwise returns null.
\r
207 * @param description the rule description.
\r
208 * @return the PluralRules
\r
211 public static PluralRules createRules(String description) {
\r
213 return parseDescription(description);
\r
214 } catch(ParseException e) {
\r
220 * A constraint on a number.
\r
222 private interface Constraint extends Serializable {
\r
224 * Returns true if the number fulfills the constraint.
\r
225 * @param n the number to test, >= 0.
\r
227 boolean isFulfilled(double n);
\r
230 * Returns the larger of limit or the limit of this constraint.
\r
231 * If the constraint is a simple range test, this is the higher
\r
232 * end of the range; if it is a modulo test, this is the modulus.
\r
234 * @param limit the target limit
\r
235 * @return the new limit
\r
237 int updateRepeatLimit(int limit);
\r
241 * A pluralization rule. .
\r
243 private interface Rule extends Serializable {
\r
244 /* Returns the keyword that names this rule. */
\r
245 String getKeyword();
\r
246 /* Returns true if the rule applies to the number. */
\r
247 boolean appliesTo(double n);
\r
248 /* Returns the larger of limit and this rule's limit. */
\r
249 int updateRepeatLimit(int limit);
\r
253 * A list of rules to apply in order.
\r
255 private interface RuleList extends Serializable {
\r
256 /* Returns the keyword of the first rule that applies to the number. */
\r
257 String select(double n);
\r
259 /* Returns the set of defined keywords. */
\r
262 /* Return the value at which this rulelist starts repeating. */
\r
263 int getRepeatLimit();
\r
268 * condition : or_condition
\r
270 * or_condition : and_condition 'or' condition
\r
271 * and_condition : relation
\r
272 * relation 'and' relation
\r
273 * relation : is_relation
\r
277 * is_relation : expr 'is' value
\r
278 * expr 'is' 'not' value
\r
279 * in_relation : expr 'in' range
\r
280 * expr 'not' 'in' range
\r
281 * within_relation : expr 'within' range
\r
282 * expr 'not' 'within' range
\r
286 * digit : 0|1|2|3|4|5|6|7|8|9
\r
287 * range : value'..'value
\r
289 private static Constraint parseConstraint(String description)
\r
290 throws ParseException {
\r
292 description = description.trim().toLowerCase(Locale.ENGLISH);
\r
294 Constraint result = null;
\r
295 String[] or_together = Utility.splitString(description, "or");
\r
296 for (int i = 0; i < or_together.length; ++i) {
\r
297 Constraint andConstraint = null;
\r
298 String[] and_together = Utility.splitString(or_together[i], "and");
\r
299 for (int j = 0; j < and_together.length; ++j) {
\r
300 Constraint newConstraint = NO_CONSTRAINT;
\r
302 String condition = and_together[j].trim();
\r
303 String[] tokens = Utility.splitWhitespace(condition);
\r
306 boolean inRange = true;
\r
307 boolean integersOnly = true;
\r
308 long lowBound = -1;
\r
309 long highBound = -1;
\r
311 boolean isRange = false;
\r
314 String t = tokens[x++];
\r
315 if (!"n".equals(t)) {
\r
316 throw unexpected(t, condition);
\r
318 if (x < tokens.length) {
\r
320 if ("mod".equals(t)) {
\r
321 mod = Integer.parseInt(tokens[x++]);
\r
322 t = nextToken(tokens, x++, condition);
\r
324 if ("is".equals(t)) {
\r
325 t = nextToken(tokens, x++, condition);
\r
326 if ("not".equals(t)) {
\r
328 t = nextToken(tokens, x++, condition);
\r
332 if ("not".equals(t)) {
\r
334 t = nextToken(tokens, x++, condition);
\r
336 if ("in".equals(t)) {
\r
337 t = nextToken(tokens, x++, condition);
\r
338 } else if ("within".equals(t)) {
\r
339 integersOnly = false;
\r
340 t = nextToken(tokens, x++, condition);
\r
342 throw unexpected(t, condition);
\r
347 String[] pair = Utility.splitString(t, "..");
\r
348 if (pair.length == 2) {
\r
349 lowBound = Long.parseLong(pair[0]);
\r
350 highBound = Long.parseLong(pair[1]);
\r
352 throw unexpected(t, condition);
\r
355 lowBound = highBound = Long.parseLong(t);
\r
358 if (x != tokens.length) {
\r
359 throw unexpected(tokens[x], condition);
\r
363 new RangeConstraint(mod, inRange, integersOnly, lowBound, highBound);
\r
366 if (andConstraint == null) {
\r
367 andConstraint = newConstraint;
\r
369 andConstraint = new AndConstraint(andConstraint,
\r
374 if (result == null) {
\r
375 result = andConstraint;
\r
377 result = new OrConstraint(result, andConstraint);
\r
384 /* Returns a parse exception wrapping the token and context strings. */
\r
385 private static ParseException unexpected(String token, String context) {
\r
386 return new ParseException("unexpected token '" + token +
\r
387 "' in '" + context + "'", -1);
\r
391 * Returns the token at x if available, else throws a parse exception.
\r
393 private static String nextToken(String[] tokens, int x, String context)
\r
394 throws ParseException {
\r
395 if (x < tokens.length) {
\r
398 throw new ParseException("missing token at end of '" + context + "'", -1);
\r
403 * rule : keyword ':' condition
\r
404 * keyword: <identifier>
\r
406 private static Rule parseRule(String description) throws ParseException {
\r
407 int x = description.indexOf(':');
\r
409 throw new ParseException("missing ':' in rule description '" +
\r
410 description + "'", 0);
\r
413 String keyword = description.substring(0, x).trim();
\r
414 if (!isValidKeyword(keyword)) {
\r
415 throw new ParseException("keyword '" + keyword +
\r
416 " is not valid", 0);
\r
419 description = description.substring(x+1).trim();
\r
420 if (description.length() == 0) {
\r
421 throw new ParseException("missing constraint in '" +
\r
422 description + "'", x+1);
\r
424 Constraint constraint = parseConstraint(description);
\r
425 Rule rule = new ConstrainedRule(keyword, constraint);
\r
434 private static RuleChain parseRuleChain(String description)
\r
435 throws ParseException {
\r
437 RuleChain rc = null;
\r
438 String[] rules = Utility.split(description, ';');
\r
439 for (int i = 0; i < rules.length; ++i) {
\r
440 Rule r = parseRule(rules[i].trim());
\r
442 rc = new RuleChain(r);
\r
444 rc = rc.addRule(r);
\r
451 * An implementation of Constraint representing a modulus,
\r
452 * a range of values, and include/exclude. Provides lots of
\r
453 * convenience factory methods.
\r
455 private static class RangeConstraint implements Constraint, Serializable {
\r
456 private static final long serialVersionUID = 1;
\r
459 private boolean inRange;
\r
460 private boolean integersOnly;
\r
461 private long lowerBound;
\r
462 private long upperBound;
\r
464 public boolean isFulfilled(double n) {
\r
465 if (integersOnly && (n - (long)n) != 0.0) {
\r
469 n = n % mod; // java % handles double numerator the way we want
\r
471 return inRange == (n >= lowerBound && n <= upperBound);
\r
474 RangeConstraint(int mod, boolean inRange, boolean integersOnly,
\r
475 long lowerBound, long upperBound) {
\r
477 this.inRange = inRange;
\r
478 this.integersOnly = integersOnly;
\r
479 this.lowerBound = lowerBound;
\r
480 this.upperBound = upperBound;
\r
483 public int updateRepeatLimit(int limit) {
\r
484 int mylimit = mod == 0 ? (int)upperBound : mod;
\r
485 return Math.max(mylimit, limit);
\r
488 public String toString() {
\r
489 return "[mod: " + mod + " inRange: " + inRange +
\r
490 " integersOnly: " + integersOnly +
\r
491 " low: " + lowerBound + " high: " + upperBound + "]";
\r
495 /* Convenience base class for and/or constraints. */
\r
496 private static abstract class BinaryConstraint implements Constraint,
\r
498 private static final long serialVersionUID = 1;
\r
499 protected final Constraint a;
\r
500 protected final Constraint b;
\r
501 private final String conjunction;
\r
503 protected BinaryConstraint(Constraint a, Constraint b, String c) {
\r
506 this.conjunction = c;
\r
509 public int updateRepeatLimit(int limit) {
\r
510 return a.updateRepeatLimit(b.updateRepeatLimit(limit));
\r
513 public String toString() {
\r
514 return a.toString() + conjunction + b.toString();
\r
518 /* A constraint representing the logical and of two constraints. */
\r
519 private static class AndConstraint extends BinaryConstraint {
\r
520 private static final long serialVersionUID = 7766999779862263523L;
\r
522 AndConstraint(Constraint a, Constraint b) {
\r
523 super(a, b, " && ");
\r
526 public boolean isFulfilled(double n) {
\r
527 return a.isFulfilled(n) && b.isFulfilled(n);
\r
531 /* A constraint representing the logical or of two constraints. */
\r
532 private static class OrConstraint extends BinaryConstraint {
\r
533 private static final long serialVersionUID = 1405488568664762222L;
\r
535 OrConstraint(Constraint a, Constraint b) {
\r
536 super(a, b, " || ");
\r
539 public boolean isFulfilled(double n) {
\r
540 return a.isFulfilled(n) || b.isFulfilled(n);
\r
545 * Implementation of Rule that uses a constraint.
\r
546 * Provides 'and' and 'or' to combine constraints. Immutable.
\r
548 private static class ConstrainedRule implements Rule, Serializable {
\r
549 private static final long serialVersionUID = 1;
\r
550 private final String keyword;
\r
551 private final Constraint constraint;
\r
553 public ConstrainedRule(String keyword, Constraint constraint) {
\r
554 this.keyword = keyword;
\r
555 this.constraint = constraint;
\r
558 public Rule and(Constraint c) {
\r
559 return new ConstrainedRule(keyword, new AndConstraint(constraint, c));
\r
562 public Rule or(Constraint c) {
\r
563 return new ConstrainedRule(keyword, new OrConstraint(constraint, c));
\r
566 public String getKeyword() {
\r
570 public boolean appliesTo(double n) {
\r
571 return constraint.isFulfilled(n);
\r
574 public int updateRepeatLimit(int limit) {
\r
575 return constraint.updateRepeatLimit(limit);
\r
578 public String toString() {
\r
579 return keyword + ": " + constraint;
\r
584 * Implementation of RuleList that is itself a node in a linked list.
\r
585 * Immutable, but supports chaining with 'addRule'.
\r
587 private static class RuleChain implements RuleList, Serializable {
\r
588 private static final long serialVersionUID = 1;
\r
589 private final Rule rule;
\r
590 private final RuleChain next;
\r
592 /** Creates a rule chain with the single rule. */
\r
593 public RuleChain(Rule rule) {
\r
597 private RuleChain(Rule rule, RuleChain next) {
\r
602 public RuleChain addRule(Rule nextRule) {
\r
603 return new RuleChain(nextRule, this);
\r
606 private Rule selectRule(double n) {
\r
608 if (next != null) {
\r
609 r = next.selectRule(n);
\r
611 if (r == null && rule.appliesTo(n)) {
\r
617 public String select(double n) {
\r
618 Rule r = selectRule(n);
\r
620 return KEYWORD_OTHER;
\r
622 return r.getKeyword();
\r
625 public Set getKeywords() {
\r
626 Set result = new HashSet();
\r
627 result.add(KEYWORD_OTHER);
\r
628 RuleChain rc = this;
\r
629 while (rc != null) {
\r
630 result.add(rc.rule.getKeyword());
\r
636 public int getRepeatLimit() {
\r
638 RuleChain rc = this;
\r
639 while (rc != null) {
\r
640 result = rc.rule.updateRepeatLimit(result);
\r
646 public String toString() {
\r
647 String s = rule.toString();
\r
648 if (next != null) {
\r
649 s = next.toString() + "; " + s;
\r
655 // -------------------------------------------------------------------------
\r
656 // Static class methods.
\r
657 // -------------------------------------------------------------------------
\r
660 * Provides access to the predefined <code>PluralRules</code> for a given
\r
663 * @param locale The locale for which a <code>PluralRules</code> object is
\r
665 * @return The predefined <code>PluralRules</code> object for this locale.
\r
666 * If there's no predefined rules for this locale, the rules
\r
667 * for the closest parent in the locale hierarchy that has one will
\r
668 * be returned. The final fallback always returns the default
\r
672 public static PluralRules forLocale(ULocale locale) {
\r
673 return PluralRulesLoader.loader.forLocale(locale);
\r
677 * Checks whether a token is a valid keyword.
\r
679 * @param token the token to be checked
\r
680 * @return true if the token is a valid keyword.
\r
682 private static boolean isValidKeyword(String token) {
\r
683 if (token.length() > 0 && START_CHARS.contains(token.charAt(0))) {
\r
684 for (int i = 1; i < token.length(); ++i) {
\r
685 if (!CONT_CHARS.contains(token.charAt(i))) {
\r
695 * Creates a new <code>PluralRules</code> object. Immutable.
\r
697 private PluralRules(RuleList rules) {
\r
698 this.rules = rules;
\r
699 this.keywords = Collections.unmodifiableSet(rules.getKeywords());
\r
703 * Given a number, returns the keyword of the first rule that applies to
\r
706 * @param number The number for which the rule has to be determined.
\r
707 * @return The keyword of the selected rule.
\r
710 public String select(double number) {
\r
711 return rules.select(number);
\r
715 * Returns a set of all rule keywords used in this <code>PluralRules</code>
\r
716 * object. The rule "other" is always present by default.
\r
718 * @return The set of keywords.
\r
721 public Set getKeywords() {
\r
726 * Returns the set of locales for which PluralRules are known.
\r
727 * @return the set of locales for which PluralRules are known, as a list
\r
729 * @provisional This API might change or be removed in a future release.
\r
731 public static ULocale[] getAvailableULocales() {
\r
732 return PluralRulesLoader.loader.getAvailableULocales();
\r
736 * Returns the 'functionally equivalent' locale with respect to
\r
737 * plural rules. Calling PluralRules.forLocale with the functionally equivalent
\r
738 * locale, and with the provided locale, returns rules that behave the same.
\r
740 * All locales with the same functionally equivalent locale have
\r
741 * plural rules that behave the same. This is not exaustive;
\r
742 * there may be other locales whose plural rules behave the same
\r
743 * that do not have the same equivalent locale.
\r
745 * @param locale the locale to check
\r
746 * @param isAvailable if not null and of length > 0, this will hold 'true' at
\r
747 * index 0 if locale is directly defined (without fallback) as having plural rules
\r
748 * @return the functionally-equivalent locale
\r
750 * @provisional This API might change or be removed in a future release.
\r
752 public static ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable) {
\r
753 return PluralRulesLoader.loader.getFunctionalEquivalent(locale, isAvailable);
\r
760 public String toString() {
\r
761 return "keywords: " + keywords + " rules: " + rules.toString() +
\r
762 " limit: " + getRepeatLimit();
\r
769 public int hashCode() {
\r
770 return keywords.hashCode();
\r
777 public boolean equals(Object rhs) {
\r
778 return rhs instanceof PluralRules && equals((PluralRules)rhs);
\r
782 * Return tif rhs is equal to this.
\r
783 * @param rhs the PluralRules to compare to.
\r
784 * @return true if this and rhs are equal.
\r
787 public boolean equals(PluralRules rhs) {
\r
791 if (rhs == this) {
\r
794 if (!rhs.getKeywords().equals(keywords)) {
\r
798 int limit = Math.max(getRepeatLimit(), rhs.getRepeatLimit());
\r
799 for (int i = 0; i < limit; ++i) {
\r
800 if (!select(i).equals(rhs.select(i))) {
\r
807 private int getRepeatLimit() {
\r
808 if (repeatLimit == 0) {
\r
809 repeatLimit = rules.getRepeatLimit() + 1;
\r
811 return repeatLimit;
\r