1 /* 2 ******************************************************************************* 3 * Copyright (C) 2007-2014, International Business Machines Corporation and 4 * others. All Rights Reserved. 5 ******************************************************************************* 6 */ 7 8 package com.ibm.icu.simple; 9 10 import java.io.IOException; 11 import java.io.NotSerializableException; 12 import java.io.ObjectInputStream; 13 import java.io.ObjectOutputStream; 14 import java.io.Serializable; 15 import java.text.ParseException; 16 import java.util.ArrayList; 17 import java.util.Collection; 18 import java.util.Collections; 19 import java.util.HashSet; 20 import java.util.Iterator; 21 import java.util.LinkedHashSet; 22 import java.util.List; 23 import java.util.Locale; 24 import java.util.Set; 25 import java.util.TreeSet; 26 import java.util.regex.Pattern; 27 28 import com.ibm.icu.util.Output; 29 30 /** 31 * <p> 32 * Defines rules for mapping non-negative numeric values onto a small set of keywords. 33 * </p> 34 * <p> 35 * Rules are constructed from a text description, consisting of a series of keywords and conditions. The {@link #select} 36 * method examines each condition in order and returns the keyword for the first condition that matches the number. If 37 * none match, {@link #KEYWORD_OTHER} is returned. 38 * </p> 39 * <p> 40 * A PluralRules object is immutable. It contains caches for sample values, but those are synchronized. 41 * <p> 42 * PluralRules is Serializable so that it can be used in formatters, which are serializable. 43 * </p> 44 * <p> 45 * For more information, details, and tips for writing rules, see the <a 46 * href="http://www.unicode.org/draft/reports/tr35/tr35.html#Language_Plural_Rules">LDML spec, C.11 Language Plural 47 * Rules</a> 48 * </p> 49 * <p> 50 * Examples: 51 * </p> 52 * 53 * <pre> 54 * "one: n is 1; few: n in 2..4" 55 * </pre> 56 * <p> 57 * This defines two rules, for 'one' and 'few'. The condition for 'one' is "n is 1" which means that the number must be 58 * equal to 1 for this condition to pass. The condition for 'few' is "n in 2..4" which means that the number must be 59 * between 2 and 4 inclusive - and be an integer - for this condition to pass. All other numbers are assigned the 60 * keyword "other" by the default rule. 61 * </p> 62 * 63 * <pre> 64 * "zero: n is 0; one: n is 1; zero: n mod 100 in 1..19" 65 * </pre> 66 * <p> 67 * This illustrates that the same keyword can be defined multiple times. Each rule is examined in order, and the first 68 * keyword whose condition passes is the one returned. Also notes that a modulus is applied to n in the last rule. Thus 69 * its condition holds for 119, 219, 319... 70 * </p> 71 * 72 * <pre> 73 * "one: n is 1; few: n mod 10 in 2..4 and n mod 100 not in 12..14" 74 * </pre> 75 * <p> 76 * This illustrates conjunction and negation. The condition for 'few' has two parts, both of which must be met: 77 * "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 78 * previous example. The second part applies a different modulus and also uses negation, thus it matches all numbers 79 * _not_ in 12, 13, 14, 112, 113, 114, 212, 213, 214... 80 * </p> 81 * <p> 82 * Syntax: 83 * </p> 84 * <pre> 85 * rules = rule (';' rule)* 86 * rule = keyword ':' condition 87 * keyword = <identifier> 88 * condition = and_condition ('or' and_condition)* 89 * and_condition = relation ('and' relation)* 90 * relation = not? expr not? rel not? range_list 91 * expr = ('n' | 'i' | 'f' | 'v' | 't') (mod value)? 92 * not = 'not' | '!' 93 * rel = 'in' | 'is' | '=' | '≠' | 'within' 94 * mod = 'mod' | '%' 95 * range_list = (range | value) (',' range_list)* 96 * value = digit+ 97 * digit = 0|1|2|3|4|5|6|7|8|9 98 * range = value'..'value 99 * </pre> 100 * <p>Each <b>not</b> term inverts the meaning; however, there should not be more than one of them.</p> 101 * <p> 102 * The i, f, t, and v values are defined as follows: 103 * </p> 104 * <ul> 105 * <li>i to be the integer digits.</li> 106 * <li>f to be the visible decimal digits, as an integer.</li> 107 * <li>t to be the visible decimal digits—without trailing zeros—as an integer.</li> 108 * <li>v to be the number of visible fraction digits.</li> 109 * <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> 110 * </ul> 111 * <p> 112 * Examples are in the following table: 113 * </p> 114 * <table border='1' style="border-collapse:collapse"> 115 * <tbody> 116 * <tr> 117 * <th>n</th> 118 * <th>i</th> 119 * <th>f</th> 120 * <th>v</th> 121 * </tr> 122 * <tr> 123 * <td>1.0</td> 124 * <td>1</td> 125 * <td align="right">0</td> 126 * <td>1</td> 127 * </tr> 128 * <tr> 129 * <td>1.00</td> 130 * <td>1</td> 131 * <td align="right">0</td> 132 * <td>2</td> 133 * </tr> 134 * <tr> 135 * <td>1.3</td> 136 * <td>1</td> 137 * <td align="right">3</td> 138 * <td>1</td> 139 * </tr> 140 * <tr> 141 * <td>1.03</td> 142 * <td>1</td> 143 * <td align="right">3</td> 144 * <td>2</td> 145 * </tr> 146 * <tr> 147 * <td>1.23</td> 148 * <td>1</td> 149 * <td align="right">23</td> 150 * <td>2</td> 151 * </tr> 152 * </tbody> 153 * </table> 154 * <p> 155 * An "identifier" is a sequence of characters that do not have the Unicode Pattern_Syntax or Pattern_White_Space 156 * properties. 157 * <p> 158 * The difference between 'in' and 'within' is that 'in' only includes integers in the specified range, while 'within' 159 * includes all values. Using 'within' with a range_list consisting entirely of values is the same as using 'in' (it's 160 * not an error). 161 * </p> 162 * 163 * @stable ICU 3.8 164 */ 165 public class PluralRules implements Serializable { 166 167 // static final UnicodeSet ALLOWED_ID = new UnicodeSet("[a-z]").freeze(); 168 169 // TODO Remove RulesList by moving its API and fields into PluralRules. 170 /** 171 * @internal 172 * @deprecated This API is ICU internal only. 173 */ 174 @Deprecated 175 public static final String CATEGORY_SEPARATOR = "; "; 176 /** 177 * @internal 178 * @deprecated This API is ICU internal only. 179 */ 180 @Deprecated 181 public static final String KEYWORD_RULE_SEPARATOR = ": "; 182 183 private static final long serialVersionUID = 1; 184 185 private final RuleList rules; 186 private final transient Set<String> keywords; 187 188 /** 189 * Provides a factory for returning plural rules 190 * 191 * @internal 192 * @deprecated This API is ICU internal only. 193 */ 194 @Deprecated 195 public static abstract class Factory { 196 /** 197 * Provides access to the predefined <code>PluralRules</code> for a given locale and the plural type. 198 * 199 * <p> 200 * ICU defines plural rules for many locales based on CLDR <i>Language Plural Rules</i>. For these predefined 201 * rules, see CLDR page at http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html 202 * 203 * @param locale 204 * The locale for which a <code>PluralRules</code> object is returned. 205 * @param type 206 * The plural type (e.g., cardinal or ordinal). 207 * @return The predefined <code>PluralRules</code> object for this locale. If there's no predefined rules for 208 * this locale, the rules for the closest parent in the locale hierarchy that has one will be returned. 209 * The final fallback always returns the default rules. 210 * @internal 211 * @deprecated This API is ICU internal only. 212 */ 213 @Deprecated forLocale(Locale locale, PluralType type)214 public abstract PluralRules forLocale(Locale locale, PluralType type); 215 216 /** 217 * Utility for getting CARDINAL rules. 218 * @param locale the locale 219 * @return plural rules. 220 * @internal 221 * @deprecated This API is ICU internal only. 222 */ 223 @Deprecated forLocale(Locale locale)224 public final PluralRules forLocale(Locale locale) { 225 return forLocale(locale, PluralType.CARDINAL); 226 } 227 228 /** 229 * Returns the locales for which there is plurals data. 230 * 231 * @internal 232 * @deprecated This API is ICU internal only. 233 @Deprecated 234 public abstract ULocale[] getAvailableULocales(); 235 */ 236 237 /** 238 * Returns the 'functionally equivalent' locale with respect to plural rules. Calling PluralRules.forLocale with 239 * the functionally equivalent locale, and with the provided locale, returns rules that behave the same. <br/> 240 * All locales with the same functionally equivalent locale have plural rules that behave the same. This is not 241 * exaustive; there may be other locales whose plural rules behave the same that do not have the same equivalent 242 * locale. 243 * 244 * @param locale 245 * the locale to check 246 * @param isAvailable 247 * if not null and of length > 0, this will hold 'true' at index 0 if locale is directly defined 248 * (without fallback) as having plural rules 249 * @return the functionally-equivalent locale 250 * @internal 251 * @deprecated This API is ICU internal only. 252 @Deprecated 253 public abstract ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable); 254 */ 255 256 /** 257 * Returns the default factory. 258 * @internal 259 * @deprecated This API is ICU internal only. 260 */ 261 @Deprecated getDefaultFactory()262 public static PluralRulesLoader getDefaultFactory() { 263 return PluralRulesLoader.loader; 264 } 265 266 /** 267 * Returns whether or not there are overrides. 268 * @internal 269 * @deprecated This API is ICU internal only. 270 @Deprecated 271 public abstract boolean hasOverride(ULocale locale); 272 */ 273 } 274 // Standard keywords. 275 276 /** 277 * Common name for the 'zero' plural form. 278 * @stable ICU 3.8 279 */ 280 public static final String KEYWORD_ZERO = "zero"; 281 282 /** 283 * Common name for the 'singular' plural form. 284 * @stable ICU 3.8 285 */ 286 public static final String KEYWORD_ONE = "one"; 287 288 /** 289 * Common name for the 'dual' plural form. 290 * @stable ICU 3.8 291 */ 292 public static final String KEYWORD_TWO = "two"; 293 294 /** 295 * Common name for the 'paucal' or other special plural form. 296 * @stable ICU 3.8 297 */ 298 public static final String KEYWORD_FEW = "few"; 299 300 /** 301 * Common name for the arabic (11 to 99) plural form. 302 * @stable ICU 3.8 303 */ 304 public static final String KEYWORD_MANY = "many"; 305 306 /** 307 * Common name for the default plural form. This name is returned 308 * for values to which no other form in the rule applies. It 309 * can additionally be assigned rules of its own. 310 * @stable ICU 3.8 311 */ 312 public static final String KEYWORD_OTHER = "other"; 313 314 /** 315 * Value returned by {@link #getUniqueKeywordValue} when there is no 316 * unique value to return. 317 * @stable ICU 4.8 318 */ 319 public static final double NO_UNIQUE_VALUE = -0.00123456777; 320 321 /** 322 * Type of plurals and PluralRules. 323 * @stable ICU 50 324 */ 325 public enum PluralType { 326 /** 327 * Plural rules for cardinal numbers: 1 file vs. 2 files. 328 * @stable ICU 50 329 */ 330 CARDINAL, 331 /** 332 * Plural rules for ordinal numbers: 1st file, 2nd file, 3rd file, 4th file, etc. 333 * @stable ICU 50 334 */ 335 ORDINAL 336 }; 337 338 /* 339 * The default constraint that is always satisfied. 340 */ 341 private static final Constraint NO_CONSTRAINT = new Constraint() { 342 private static final long serialVersionUID = 9163464945387899416L; 343 344 public boolean isFulfilled(FixedDecimal n) { 345 return true; 346 } 347 348 public boolean isLimited(SampleType sampleType) { 349 return false; 350 } 351 352 public String toString() { 353 return ""; 354 } 355 }; 356 357 /** 358 * 359 */ 360 private static final Rule DEFAULT_RULE = new Rule("other", NO_CONSTRAINT, null, null); 361 362 /** 363 * Parses a plural rules description and returns a PluralRules. 364 * @param description the rule description. 365 * @throws ParseException if the description cannot be parsed. 366 * The exception index is typically not set, it will be -1. 367 * @stable ICU 3.8 368 */ parseDescription(String description)369 public static PluralRules parseDescription(String description) 370 throws ParseException { 371 372 description = description.trim(); 373 return description.length() == 0 ? DEFAULT : new PluralRules(parseRuleChain(description)); 374 } 375 376 /** 377 * Creates a PluralRules from a description if it is parsable, 378 * otherwise returns null. 379 * @param description the rule description. 380 * @return the PluralRules 381 * @stable ICU 3.8 382 */ createRules(String description)383 public static PluralRules createRules(String description) { 384 try { 385 return parseDescription(description); 386 } catch(Exception e) { 387 return null; 388 } 389 } 390 391 /** 392 * The default rules that accept any number and return 393 * {@link #KEYWORD_OTHER}. 394 * @stable ICU 3.8 395 */ 396 public static final PluralRules DEFAULT = new PluralRules(new RuleList().addRule(DEFAULT_RULE)); 397 398 private enum Operand { 399 n, 400 i, 401 f, 402 t, 403 v, 404 w, 405 /* deprecated */ 406 j; 407 } 408 409 /** 410 * @internal 411 * @deprecated This API is ICU internal only. 412 */ 413 @Deprecated 414 public static class FixedDecimal extends Number implements Comparable<FixedDecimal> { 415 private static final long serialVersionUID = -4756200506571685661L; 416 /** 417 * @internal 418 * @deprecated This API is ICU internal only. 419 */ 420 @Deprecated 421 public final double source; 422 /** 423 * @internal 424 * @deprecated This API is ICU internal only. 425 */ 426 @Deprecated 427 public final int visibleDecimalDigitCount; 428 /** 429 * @internal 430 * @deprecated This API is ICU internal only. 431 */ 432 @Deprecated 433 public final int visibleDecimalDigitCountWithoutTrailingZeros; 434 /** 435 * @internal 436 * @deprecated This API is ICU internal only. 437 */ 438 @Deprecated 439 public final long decimalDigits; 440 /** 441 * @internal 442 * @deprecated This API is ICU internal only. 443 */ 444 @Deprecated 445 public final long decimalDigitsWithoutTrailingZeros; 446 /** 447 * @internal 448 * @deprecated This API is ICU internal only. 449 */ 450 @Deprecated 451 public final long integerValue; 452 /** 453 * @internal 454 * @deprecated This API is ICU internal only. 455 */ 456 @Deprecated 457 public final boolean hasIntegerValue; 458 /** 459 * @internal 460 * @deprecated This API is ICU internal only. 461 */ 462 @Deprecated 463 public final boolean isNegative; 464 private final int baseFactor; 465 466 /** 467 * @internal 468 * @deprecated This API is ICU internal only. 469 */ 470 @Deprecated getSource()471 public double getSource() { 472 return source; 473 } 474 475 /** 476 * @internal 477 * @deprecated This API is ICU internal only. 478 */ 479 @Deprecated getVisibleDecimalDigitCount()480 public int getVisibleDecimalDigitCount() { 481 return visibleDecimalDigitCount; 482 } 483 484 /** 485 * @internal 486 * @deprecated This API is ICU internal only. 487 */ 488 @Deprecated getVisibleDecimalDigitCountWithoutTrailingZeros()489 public int getVisibleDecimalDigitCountWithoutTrailingZeros() { 490 return visibleDecimalDigitCountWithoutTrailingZeros; 491 } 492 493 /** 494 * @internal 495 * @deprecated This API is ICU internal only. 496 */ 497 @Deprecated getDecimalDigits()498 public long getDecimalDigits() { 499 return decimalDigits; 500 } 501 502 /** 503 * @internal 504 * @deprecated This API is ICU internal only. 505 */ 506 @Deprecated getDecimalDigitsWithoutTrailingZeros()507 public long getDecimalDigitsWithoutTrailingZeros() { 508 return decimalDigitsWithoutTrailingZeros; 509 } 510 511 /** 512 * @internal 513 * @deprecated This API is ICU internal only. 514 */ 515 @Deprecated getIntegerValue()516 public long getIntegerValue() { 517 return integerValue; 518 } 519 520 /** 521 * @internal 522 * @deprecated This API is ICU internal only. 523 */ 524 @Deprecated isHasIntegerValue()525 public boolean isHasIntegerValue() { 526 return hasIntegerValue; 527 } 528 529 /** 530 * @internal 531 * @deprecated This API is ICU internal only. 532 */ 533 @Deprecated isNegative()534 public boolean isNegative() { 535 return isNegative; 536 } 537 538 /** 539 * @internal 540 * @deprecated This API is ICU internal only. 541 */ 542 @Deprecated getBaseFactor()543 public int getBaseFactor() { 544 return baseFactor; 545 } 546 547 static final long MAX = (long)1E18; 548 549 /** 550 * @internal 551 * @deprecated This API is ICU internal only. 552 * @param n is the original number 553 * @param v number of digits to the right of the decimal place. e.g 1.00 = 2 25. = 0 554 * @param f Corresponds to f in the plural rules grammar. 555 * The digits to the right of the decimal place as an integer. e.g 1.10 = 10 556 */ 557 @Deprecated FixedDecimal(double n, int v, long f)558 public FixedDecimal(double n, int v, long f) { 559 isNegative = n < 0; 560 source = isNegative ? -n : n; 561 visibleDecimalDigitCount = v; 562 decimalDigits = f; 563 integerValue = n > MAX 564 ? MAX 565 : (long)n; 566 hasIntegerValue = source == integerValue; 567 // check values. TODO make into unit test. 568 // 569 // long visiblePower = (int) Math.pow(10, v); 570 // if (fractionalDigits > visiblePower) { 571 // throw new IllegalArgumentException(); 572 // } 573 // double fraction = intValue + (fractionalDigits / (double) visiblePower); 574 // if (fraction != source) { 575 // double diff = Math.abs(fraction - source)/(Math.abs(fraction) + Math.abs(source)); 576 // if (diff > 0.00000001d) { 577 // throw new IllegalArgumentException(); 578 // } 579 // } 580 if (f == 0) { 581 decimalDigitsWithoutTrailingZeros = 0; 582 visibleDecimalDigitCountWithoutTrailingZeros = 0; 583 } else { 584 long fdwtz = f; 585 int trimmedCount = v; 586 while ((fdwtz%10) == 0) { 587 fdwtz /= 10; 588 --trimmedCount; 589 } 590 decimalDigitsWithoutTrailingZeros = fdwtz; 591 visibleDecimalDigitCountWithoutTrailingZeros = trimmedCount; 592 } 593 baseFactor = (int) Math.pow(10, v); 594 } 595 596 /** 597 * @internal 598 * @deprecated This API is ICU internal only. 599 */ 600 @Deprecated FixedDecimal(double n, int v)601 public FixedDecimal(double n, int v) { 602 this(n,v,getFractionalDigits(n, v)); 603 } 604 getFractionalDigits(double n, int v)605 private static int getFractionalDigits(double n, int v) { 606 if (v == 0) { 607 return 0; 608 } else { 609 if (n < 0) { 610 n = -n; 611 } 612 int baseFactor = (int) Math.pow(10, v); 613 long scaled = Math.round(n * baseFactor); 614 return (int) (scaled % baseFactor); 615 } 616 } 617 618 /** 619 * @internal 620 * @deprecated This API is ICU internal only. 621 */ 622 @Deprecated FixedDecimal(double n)623 public FixedDecimal(double n) { 624 this(n, decimals(n)); 625 } 626 627 /** 628 * @internal 629 * @deprecated This API is ICU internal only. 630 */ 631 @Deprecated FixedDecimal(long n)632 public FixedDecimal(long n) { 633 this(n,0); 634 } 635 636 private static final long MAX_INTEGER_PART = 1000000000; 637 /** 638 * Return a guess as to the number of decimals that would be displayed. This is only a guess; callers should 639 * always supply the decimals explicitly if possible. Currently, it is up to 6 decimals (without trailing zeros). 640 * Returns 0 for infinities and nans. 641 * @internal 642 * @deprecated This API is ICU internal only. 643 * 644 */ 645 @Deprecated decimals(double n)646 public static int decimals(double n) { 647 // Ugly... 648 if (Double.isInfinite(n) || Double.isNaN(n)) { 649 return 0; 650 } 651 if (n < 0) { 652 n = -n; 653 } 654 if (n < MAX_INTEGER_PART) { 655 long temp = (long)(n * 1000000) % 1000000; // get 6 decimals 656 for (int mask = 10, digits = 6; digits > 0; mask *= 10, --digits) { 657 if ((temp % mask) != 0) { 658 return digits; 659 } 660 } 661 return 0; 662 } else { 663 String buf = String.format(Locale.ENGLISH, "%1.15e", n); 664 int ePos = buf.lastIndexOf('e'); 665 int expNumPos = ePos + 1; 666 if (buf.charAt(expNumPos) == '+') { 667 expNumPos++; 668 } 669 String exponentStr = buf.substring(expNumPos); 670 int exponent = Integer.parseInt(exponentStr); 671 int numFractionDigits = ePos - 2 - exponent; 672 if (numFractionDigits < 0) { 673 return 0; 674 } 675 for (int i=ePos-1; numFractionDigits > 0; --i) { 676 if (buf.charAt(i) != '0') { 677 break; 678 } 679 --numFractionDigits; 680 } 681 return numFractionDigits; 682 } 683 } 684 685 /** 686 * @internal 687 * @deprecated This API is ICU internal only. 688 */ 689 @Deprecated FixedDecimal(String n)690 public FixedDecimal (String n) { 691 // Ugly, but for samples we don't care. 692 this(Double.parseDouble(n), getVisibleFractionCount(n)); 693 } 694 getVisibleFractionCount(String value)695 private static int getVisibleFractionCount(String value) { 696 value = value.trim(); 697 int decimalPos = value.indexOf('.') + 1; 698 if (decimalPos == 0) { 699 return 0; 700 } else { 701 return value.length() - decimalPos; 702 } 703 } 704 705 /** 706 * @internal 707 * @deprecated This API is ICU internal only. 708 */ 709 @Deprecated get(Operand operand)710 public double get(Operand operand) { 711 switch(operand) { 712 default: return source; 713 case i: return integerValue; 714 case f: return decimalDigits; 715 case t: return decimalDigitsWithoutTrailingZeros; 716 case v: return visibleDecimalDigitCount; 717 case w: return visibleDecimalDigitCountWithoutTrailingZeros; 718 } 719 } 720 721 /** 722 * @internal 723 * @deprecated This API is ICU internal only. 724 */ 725 @Deprecated getOperand(String t)726 public static Operand getOperand(String t) { 727 return Operand.valueOf(t); 728 } 729 730 /** 731 * We're not going to care about NaN. 732 * @internal 733 * @deprecated This API is ICU internal only. 734 */ 735 @Deprecated compareTo(FixedDecimal other)736 public int compareTo(FixedDecimal other) { 737 if (integerValue != other.integerValue) { 738 return integerValue < other.integerValue ? -1 : 1; 739 } 740 if (source != other.source) { 741 return source < other.source ? -1 : 1; 742 } 743 if (visibleDecimalDigitCount != other.visibleDecimalDigitCount) { 744 return visibleDecimalDigitCount < other.visibleDecimalDigitCount ? -1 : 1; 745 } 746 long diff = decimalDigits - other.decimalDigits; 747 if (diff != 0) { 748 return diff < 0 ? -1 : 1; 749 } 750 return 0; 751 } 752 753 /** 754 * @internal 755 * @deprecated This API is ICU internal only. 756 */ 757 @Deprecated 758 @Override equals(Object arg0)759 public boolean equals(Object arg0) { 760 if (arg0 == null) { 761 return false; 762 } 763 if (arg0 == this) { 764 return true; 765 } 766 if (!(arg0 instanceof FixedDecimal)) { 767 return false; 768 } 769 FixedDecimal other = (FixedDecimal)arg0; 770 return source == other.source && visibleDecimalDigitCount == other.visibleDecimalDigitCount && decimalDigits == other.decimalDigits; 771 } 772 773 /** 774 * @internal 775 * @deprecated This API is ICU internal only. 776 */ 777 @Deprecated 778 @Override hashCode()779 public int hashCode() { 780 // TODO Auto-generated method stub 781 return (int)(decimalDigits + 37 * (visibleDecimalDigitCount + (int)(37 * source))); 782 } 783 784 /** 785 * @internal 786 * @deprecated This API is ICU internal only. 787 */ 788 @Deprecated 789 @Override toString()790 public String toString() { 791 return String.format("%." + visibleDecimalDigitCount + "f", source); 792 } 793 794 /** 795 * @internal 796 * @deprecated This API is ICU internal only. 797 */ 798 @Deprecated hasIntegerValue()799 public boolean hasIntegerValue() { 800 return hasIntegerValue; 801 } 802 803 /** 804 * @internal 805 * @deprecated This API is ICU internal only. 806 */ 807 @Deprecated 808 @Override intValue()809 public int intValue() { 810 // TODO Auto-generated method stub 811 return (int)integerValue; 812 } 813 814 /** 815 * @internal 816 * @deprecated This API is ICU internal only. 817 */ 818 @Deprecated 819 @Override longValue()820 public long longValue() { 821 return integerValue; 822 } 823 824 /** 825 * @internal 826 * @deprecated This API is ICU internal only. 827 */ 828 @Deprecated 829 @Override floatValue()830 public float floatValue() { 831 return (float) source; 832 } 833 834 /** 835 * @internal 836 * @deprecated This API is ICU internal only. 837 */ 838 @Deprecated 839 @Override doubleValue()840 public double doubleValue() { 841 return isNegative ? -source : source; 842 } 843 844 /** 845 * @internal 846 * @deprecated This API is ICU internal only. 847 */ 848 @Deprecated getShiftedValue()849 public long getShiftedValue() { 850 return integerValue * baseFactor + decimalDigits; 851 } 852 writeObject( ObjectOutputStream out)853 private void writeObject( 854 ObjectOutputStream out) 855 throws IOException { 856 throw new NotSerializableException(); 857 } 858 readObject(ObjectInputStream in )859 private void readObject(ObjectInputStream in 860 ) throws IOException, ClassNotFoundException { 861 throw new NotSerializableException(); 862 } 863 } 864 865 /** 866 * Selection parameter for either integer-only or decimal-only. 867 * @internal 868 * @deprecated This API is ICU internal only. 869 */ 870 @Deprecated 871 public enum SampleType { 872 /** 873 * @internal 874 * @deprecated This API is ICU internal only. 875 */ 876 @Deprecated 877 INTEGER, 878 /** 879 * @internal 880 * @deprecated This API is ICU internal only. 881 */ 882 @Deprecated 883 DECIMAL 884 } 885 886 /** 887 * A range of NumberInfo that includes all values with the same visibleFractionDigitCount. 888 * @internal 889 * @deprecated This API is ICU internal only. 890 */ 891 @Deprecated 892 public static class FixedDecimalRange { 893 /** 894 * @internal 895 * @deprecated This API is ICU internal only. 896 */ 897 @Deprecated 898 public final FixedDecimal start; 899 /** 900 * @internal 901 * @deprecated This API is ICU internal only. 902 */ 903 @Deprecated 904 public final FixedDecimal end; 905 /** 906 * @internal 907 * @deprecated This API is ICU internal only. 908 */ 909 @Deprecated FixedDecimalRange(FixedDecimal start, FixedDecimal end)910 public FixedDecimalRange(FixedDecimal start, FixedDecimal end) { 911 if (start.visibleDecimalDigitCount != end.visibleDecimalDigitCount) { 912 throw new IllegalArgumentException("Ranges must have the same number of visible decimals: " + start + "~" + end); 913 } 914 this.start = start; 915 this.end = end; 916 } 917 /** 918 * @internal 919 * @deprecated This API is ICU internal only. 920 */ 921 @Deprecated 922 @Override toString()923 public String toString() { 924 return start + (end == start ? "" : "~" + end); 925 } 926 } 927 928 /** 929 * A list of NumberInfo that includes all values with the same visibleFractionDigitCount. 930 * @internal 931 * @deprecated This API is ICU internal only. 932 */ 933 @Deprecated 934 public static class FixedDecimalSamples { 935 /** 936 * @internal 937 * @deprecated This API is ICU internal only. 938 */ 939 @Deprecated 940 public final SampleType sampleType; 941 /** 942 * @internal 943 * @deprecated This API is ICU internal only. 944 */ 945 @Deprecated 946 public final Set<FixedDecimalRange> samples; 947 /** 948 * @internal 949 * @deprecated This API is ICU internal only. 950 */ 951 @Deprecated 952 public final boolean bounded; 953 /** 954 * The samples must be immutable. 955 * @param sampleType 956 * @param samples 957 */ FixedDecimalSamples(SampleType sampleType, Set<FixedDecimalRange> samples, boolean bounded)958 private FixedDecimalSamples(SampleType sampleType, Set<FixedDecimalRange> samples, boolean bounded) { 959 super(); 960 this.sampleType = sampleType; 961 this.samples = samples; 962 this.bounded = bounded; 963 } 964 /* 965 * Parse a list of the form described in CLDR. The source must be trimmed. 966 */ parse(String source)967 static FixedDecimalSamples parse(String source) { 968 SampleType sampleType2; 969 boolean bounded2 = true; 970 boolean haveBound = false; 971 Set<FixedDecimalRange> samples2 = new LinkedHashSet<FixedDecimalRange>(); 972 973 if (source.startsWith("integer")) { 974 sampleType2 = SampleType.INTEGER; 975 } else if (source.startsWith("decimal")) { 976 sampleType2 = SampleType.DECIMAL; 977 } else { 978 throw new IllegalArgumentException("Samples must start with 'integer' or 'decimal'"); 979 } 980 source = source.substring(7).trim(); // remove both 981 982 for (String range : COMMA_SEPARATED.split(source)) { 983 if (range.equals("…") || range.equals("...")) { 984 bounded2 = false; 985 haveBound = true; 986 continue; 987 } 988 if (haveBound) { 989 throw new IllegalArgumentException("Can only have … at the end of samples: " + range); 990 } 991 String[] rangeParts = TILDE_SEPARATED.split(range); 992 switch (rangeParts.length) { 993 case 1: 994 FixedDecimal sample = new FixedDecimal(rangeParts[0]); 995 checkDecimal(sampleType2, sample); 996 samples2.add(new FixedDecimalRange(sample, sample)); 997 break; 998 case 2: 999 FixedDecimal start = new FixedDecimal(rangeParts[0]); 1000 FixedDecimal end = new FixedDecimal(rangeParts[1]); 1001 checkDecimal(sampleType2, start); 1002 checkDecimal(sampleType2, end); 1003 samples2.add(new FixedDecimalRange(start, end)); 1004 break; 1005 default: throw new IllegalArgumentException("Ill-formed number range: " + range); 1006 } 1007 } 1008 return new FixedDecimalSamples(sampleType2, Collections.unmodifiableSet(samples2), bounded2); 1009 } 1010 checkDecimal(SampleType sampleType2, FixedDecimal sample)1011 private static void checkDecimal(SampleType sampleType2, FixedDecimal sample) { 1012 if ((sampleType2 == SampleType.INTEGER) != (sample.getVisibleDecimalDigitCount() == 0)) { 1013 throw new IllegalArgumentException("Ill-formed number range: " + sample); 1014 } 1015 } 1016 1017 /** 1018 * @internal 1019 * @deprecated This API is ICU internal only. 1020 */ 1021 @Deprecated addSamples(Set<Double> result)1022 public Set<Double> addSamples(Set<Double> result) { 1023 for (FixedDecimalRange item : samples) { 1024 // we have to convert to longs so we don't get strange double issues 1025 long startDouble = item.start.getShiftedValue(); 1026 long endDouble = item.end.getShiftedValue(); 1027 1028 for (long d = startDouble; d <= endDouble; d += 1) { 1029 result.add(d/(double)item.start.baseFactor); 1030 } 1031 } 1032 return result; 1033 } 1034 1035 /** 1036 * @internal 1037 * @deprecated This API is ICU internal only. 1038 */ 1039 @Deprecated 1040 @Override toString()1041 public String toString() { 1042 StringBuilder b = new StringBuilder("@").append(sampleType.toString().toLowerCase(Locale.ENGLISH)); 1043 boolean first = true; 1044 for (FixedDecimalRange item : samples) { 1045 if (first) { 1046 first = false; 1047 } else { 1048 b.append(","); 1049 } 1050 b.append(' ').append(item); 1051 } 1052 if (!bounded) { 1053 b.append(", …"); 1054 } 1055 return b.toString(); 1056 } 1057 1058 /** 1059 * @internal 1060 * @deprecated This API is ICU internal only. 1061 */ 1062 @Deprecated getSamples()1063 public Set<FixedDecimalRange> getSamples() { 1064 return samples; 1065 } 1066 1067 /** 1068 * @internal 1069 * @deprecated This API is ICU internal only. 1070 */ 1071 @Deprecated getStartEndSamples(Set<FixedDecimal> target)1072 public void getStartEndSamples(Set<FixedDecimal> target) { 1073 for (FixedDecimalRange item : samples) { 1074 target.add(item.start); 1075 target.add(item.end); 1076 } 1077 } 1078 } 1079 1080 /* 1081 * A constraint on a number. 1082 */ 1083 private interface Constraint extends Serializable { 1084 /* 1085 * Returns true if the number fulfills the constraint. 1086 * @param n the number to test, >= 0. 1087 */ isFulfilled(FixedDecimal n)1088 boolean isFulfilled(FixedDecimal n); 1089 1090 /* 1091 * Returns false if an unlimited number of values fulfills the 1092 * constraint. 1093 */ isLimited(SampleType sampleType)1094 boolean isLimited(SampleType sampleType); 1095 } 1096 isBreakAndIgnore(char c)1097 private static final boolean isBreakAndIgnore(char c) { 1098 return c <= 0x20 && (c == 0x20 || c == 9 || c == 0xa || c == 0xc || c == 0xd); 1099 } isBreakAndKeep(char c)1100 private static final boolean isBreakAndKeep(char c) { 1101 return c <= '=' && c >= '!' && (c == '!' || c == '%' || c == ',' || c == '.' || c == '='); 1102 } 1103 static class SimpleTokenizer { 1104 // static final UnicodeSet BREAK_AND_IGNORE = new UnicodeSet(0x09, 0x0a, 0x0c, 0x0d, 0x20, 0x20).freeze(); 1105 // static final UnicodeSet BREAK_AND_KEEP = new UnicodeSet('!', '!', '%', '%', ',', ',', '.', '.', '=', '=').freeze(); split(String source)1106 static String[] split(String source) { 1107 int last = -1; 1108 List<String> result = new ArrayList<String>(); 1109 for (int i = 0; i < source.length(); ++i) { 1110 char ch = source.charAt(i); 1111 if (isBreakAndIgnore(ch) /* BREAK_AND_IGNORE.contains(ch) */) { 1112 if (last >= 0) { 1113 result.add(source.substring(last,i)); 1114 last = -1; 1115 } 1116 } else if (isBreakAndKeep(ch) /* BREAK_AND_KEEP.contains(ch) */) { 1117 if (last >= 0) { 1118 result.add(source.substring(last,i)); 1119 } 1120 result.add(source.substring(i,i+1)); 1121 last = -1; 1122 } else if (last < 0) { 1123 last = i; 1124 } 1125 } 1126 if (last >= 0) { 1127 result.add(source.substring(last)); 1128 } 1129 return result.toArray(new String[result.size()]); 1130 } 1131 } 1132 1133 /* 1134 * syntax: 1135 * condition : or_condition 1136 * and_condition 1137 * or_condition : and_condition 'or' condition 1138 * and_condition : relation 1139 * relation 'and' relation 1140 * relation : in_relation 1141 * within_relation 1142 * in_relation : not? expr not? in not? range 1143 * within_relation : not? expr not? 'within' not? range 1144 * not : 'not' 1145 * '!' 1146 * expr : 'n' 1147 * 'n' mod value 1148 * mod : 'mod' 1149 * '%' 1150 * in : 'in' 1151 * 'is' 1152 * '=' 1153 * '≠' 1154 * value : digit+ 1155 * digit : 0|1|2|3|4|5|6|7|8|9 1156 * range : value'..'value 1157 */ parseConstraint(String description)1158 private static Constraint parseConstraint(String description) 1159 throws ParseException { 1160 1161 Constraint result = null; 1162 String[] or_together = OR_SEPARATED.split(description); 1163 for (int i = 0; i < or_together.length; ++i) { 1164 Constraint andConstraint = null; 1165 String[] and_together = AND_SEPARATED.split(or_together[i]); 1166 for (int j = 0; j < and_together.length; ++j) { 1167 Constraint newConstraint = NO_CONSTRAINT; 1168 1169 String condition = and_together[j].trim(); 1170 String[] tokens = SimpleTokenizer.split(condition); 1171 1172 int mod = 0; 1173 boolean inRange = true; 1174 boolean integersOnly = true; 1175 double lowBound = Long.MAX_VALUE; 1176 double highBound = Long.MIN_VALUE; 1177 long[] vals = null; 1178 1179 int x = 0; 1180 String t = tokens[x++]; 1181 boolean hackForCompatibility = false; 1182 Operand operand; 1183 try { 1184 operand = FixedDecimal.getOperand(t); 1185 } catch (Exception e) { 1186 throw unexpected(t, condition); 1187 } 1188 if (x < tokens.length) { 1189 t = tokens[x++]; 1190 if ("mod".equals(t) || "%".equals(t)) { 1191 mod = Integer.parseInt(tokens[x++]); 1192 t = nextToken(tokens, x++, condition); 1193 } 1194 if ("not".equals(t)) { 1195 inRange = !inRange; 1196 t = nextToken(tokens, x++, condition); 1197 if ("=".equals(t)) { 1198 throw unexpected(t, condition); 1199 } 1200 } else if ("!".equals(t)) { 1201 inRange = !inRange; 1202 t = nextToken(tokens, x++, condition); 1203 if (!"=".equals(t)) { 1204 throw unexpected(t, condition); 1205 } 1206 } 1207 if ("is".equals(t) || "in".equals(t) || "=".equals(t)) { 1208 hackForCompatibility = "is".equals(t); 1209 if (hackForCompatibility && !inRange) { 1210 throw unexpected(t, condition); 1211 } 1212 t = nextToken(tokens, x++, condition); 1213 } else if ("within".equals(t)) { 1214 integersOnly = false; 1215 t = nextToken(tokens, x++, condition); 1216 } else { 1217 throw unexpected(t, condition); 1218 } 1219 if ("not".equals(t)) { 1220 if (!hackForCompatibility && !inRange) { 1221 throw unexpected(t, condition); 1222 } 1223 inRange = !inRange; 1224 t = nextToken(tokens, x++, condition); 1225 } 1226 1227 List<Long> valueList = new ArrayList<Long>(); 1228 1229 // the token t is always one item ahead 1230 while (true) { 1231 long low = Long.parseLong(t); 1232 long high = low; 1233 if (x < tokens.length) { 1234 t = nextToken(tokens, x++, condition); 1235 if (t.equals(".")) { 1236 t = nextToken(tokens, x++, condition); 1237 if (!t.equals(".")) { 1238 throw unexpected(t, condition); 1239 } 1240 t = nextToken(tokens, x++, condition); 1241 high = Long.parseLong(t); 1242 if (x < tokens.length) { 1243 t = nextToken(tokens, x++, condition); 1244 if (!t.equals(",")) { // adjacent number: 1 2 1245 // no separator, fail 1246 throw unexpected(t, condition); 1247 } 1248 } 1249 } else if (!t.equals(",")) { // adjacent number: 1 2 1250 // no separator, fail 1251 throw unexpected(t, condition); 1252 } 1253 } 1254 // at this point, either we are out of tokens, or t is ',' 1255 if (low > high) { 1256 throw unexpected(low + "~" + high, condition); 1257 } else if (mod != 0 && high >= mod) { 1258 throw unexpected(high + ">mod=" + mod, condition); 1259 } 1260 valueList.add(low); 1261 valueList.add(high); 1262 lowBound = Math.min(lowBound, low); 1263 highBound = Math.max(highBound, high); 1264 if (x >= tokens.length) { 1265 break; 1266 } 1267 t = nextToken(tokens, x++, condition); 1268 } 1269 1270 if (t.equals(",")) { 1271 throw unexpected(t, condition); 1272 } 1273 1274 if (valueList.size() == 2) { 1275 vals = null; 1276 } else { 1277 vals = new long[valueList.size()]; 1278 for (int k = 0; k < vals.length; ++k) { 1279 vals[k] = valueList.get(k); 1280 } 1281 } 1282 1283 // Hack to exclude "is not 1,2" 1284 if (lowBound != highBound && hackForCompatibility && !inRange) { 1285 throw unexpected("is not <range>", condition); 1286 } 1287 1288 newConstraint = 1289 new RangeConstraint(mod, inRange, operand, integersOnly, lowBound, highBound, vals); 1290 } 1291 1292 if (andConstraint == null) { 1293 andConstraint = newConstraint; 1294 } else { 1295 andConstraint = new AndConstraint(andConstraint, 1296 newConstraint); 1297 } 1298 } 1299 1300 if (result == null) { 1301 result = andConstraint; 1302 } else { 1303 result = new OrConstraint(result, andConstraint); 1304 } 1305 } 1306 return result; 1307 } 1308 1309 static final Pattern AT_SEPARATED = Pattern.compile("\\s*\\Q\\E@\\s*"); 1310 static final Pattern OR_SEPARATED = Pattern.compile("\\s*or\\s*"); 1311 static final Pattern AND_SEPARATED = Pattern.compile("\\s*and\\s*"); 1312 static final Pattern COMMA_SEPARATED = Pattern.compile("\\s*,\\s*"); 1313 static final Pattern DOTDOT_SEPARATED = Pattern.compile("\\s*\\Q..\\E\\s*"); 1314 static final Pattern TILDE_SEPARATED = Pattern.compile("\\s*~\\s*"); 1315 static final Pattern SEMI_SEPARATED = Pattern.compile("\\s*;\\s*"); 1316 1317 1318 /* Returns a parse exception wrapping the token and context strings. */ unexpected(String token, String context)1319 private static ParseException unexpected(String token, String context) { 1320 return new ParseException("unexpected token '" + token + 1321 "' in '" + context + "'", -1); 1322 } 1323 1324 /* 1325 * Returns the token at x if available, else throws a parse exception. 1326 */ nextToken(String[] tokens, int x, String context)1327 private static String nextToken(String[] tokens, int x, String context) 1328 throws ParseException { 1329 if (x < tokens.length) { 1330 return tokens[x]; 1331 } 1332 throw new ParseException("missing token at end of '" + context + "'", -1); 1333 } 1334 1335 /* 1336 * Syntax: 1337 * rule : keyword ':' condition 1338 * keyword: <identifier> 1339 */ parseRule(String description)1340 private static Rule parseRule(String description) throws ParseException { 1341 if (description.length() == 0) { 1342 return DEFAULT_RULE; 1343 } 1344 1345 description = description.toLowerCase(Locale.ENGLISH); 1346 1347 int x = description.indexOf(':'); 1348 if (x == -1) { 1349 throw new ParseException("missing ':' in rule description '" + 1350 description + "'", 0); 1351 } 1352 1353 String keyword = description.substring(0, x).trim(); 1354 if (!isValidKeyword(keyword)) { 1355 throw new ParseException("keyword '" + keyword + 1356 " is not valid", 0); 1357 } 1358 1359 description = description.substring(x+1).trim(); 1360 String[] constraintOrSamples = AT_SEPARATED.split(description); 1361 boolean sampleFailure = false; 1362 FixedDecimalSamples integerSamples = null, decimalSamples = null; 1363 switch (constraintOrSamples.length) { 1364 case 1: break; 1365 case 2: 1366 integerSamples = FixedDecimalSamples.parse(constraintOrSamples[1]); 1367 if (integerSamples.sampleType == SampleType.DECIMAL) { 1368 decimalSamples = integerSamples; 1369 integerSamples = null; 1370 } 1371 break; 1372 case 3: 1373 integerSamples = FixedDecimalSamples.parse(constraintOrSamples[1]); 1374 decimalSamples = FixedDecimalSamples.parse(constraintOrSamples[2]); 1375 if (integerSamples.sampleType != SampleType.INTEGER || decimalSamples.sampleType != SampleType.DECIMAL) { 1376 throw new IllegalArgumentException("Must have @integer then @decimal in " + description); 1377 } 1378 break; 1379 default: 1380 throw new IllegalArgumentException("Too many samples in " + description); 1381 } 1382 if (sampleFailure) { 1383 throw new IllegalArgumentException("Ill-formed samples—'@' characters."); 1384 } 1385 1386 // 'other' is special, and must have no rules; all other keywords must have rules. 1387 boolean isOther = keyword.equals("other"); 1388 if (isOther != (constraintOrSamples[0].length() == 0)) { 1389 throw new IllegalArgumentException("The keyword 'other' must have no constraints, just samples."); 1390 } 1391 1392 Constraint constraint; 1393 if (isOther) { 1394 constraint = NO_CONSTRAINT; 1395 } else { 1396 constraint = parseConstraint(constraintOrSamples[0]); 1397 } 1398 return new Rule(keyword, constraint, integerSamples, decimalSamples); 1399 } 1400 1401 1402 /* 1403 * Syntax: 1404 * rules : rule 1405 * rule ';' rules 1406 */ parseRuleChain(String description)1407 private static RuleList parseRuleChain(String description) 1408 throws ParseException { 1409 RuleList result = new RuleList(); 1410 // remove trailing ; 1411 if (description.endsWith(";")) { 1412 description = description.substring(0,description.length()-1); 1413 } 1414 String[] rules = SEMI_SEPARATED.split(description); 1415 for (int i = 0; i < rules.length; ++i) { 1416 Rule rule = parseRule(rules[i].trim()); 1417 result.hasExplicitBoundingInfo |= rule.integerSamples != null || rule.decimalSamples != null; 1418 result.addRule(rule); 1419 } 1420 return result.finish(); 1421 } 1422 1423 /* 1424 * An implementation of Constraint representing a modulus, 1425 * a range of values, and include/exclude. Provides lots of 1426 * convenience factory methods. 1427 */ 1428 private static class RangeConstraint implements Constraint, Serializable { 1429 private static final long serialVersionUID = 1; 1430 1431 private final int mod; 1432 private final boolean inRange; 1433 private final boolean integersOnly; 1434 private final double lowerBound; 1435 private final double upperBound; 1436 private final long[] range_list; 1437 private final Operand operand; 1438 RangeConstraint(int mod, boolean inRange, Operand operand, boolean integersOnly, double lowBound, double highBound, long[] vals)1439 RangeConstraint(int mod, boolean inRange, Operand operand, boolean integersOnly, 1440 double lowBound, double highBound, long[] vals) { 1441 this.mod = mod; 1442 this.inRange = inRange; 1443 this.integersOnly = integersOnly; 1444 this.lowerBound = lowBound; 1445 this.upperBound = highBound; 1446 this.range_list = vals; 1447 this.operand = operand; 1448 } 1449 isFulfilled(FixedDecimal number)1450 public boolean isFulfilled(FixedDecimal number) { 1451 double n = number.get(operand); 1452 if ((integersOnly && (n - (long)n) != 0.0 1453 || operand == Operand.j && number.visibleDecimalDigitCount != 0)) { 1454 return !inRange; 1455 } 1456 if (mod != 0) { 1457 n = n % mod; // java % handles double numerator the way we want 1458 } 1459 boolean test = n >= lowerBound && n <= upperBound; 1460 if (test && range_list != null) { 1461 test = false; 1462 for (int i = 0; !test && i < range_list.length; i += 2) { 1463 test = n >= range_list[i] && n <= range_list[i+1]; 1464 } 1465 } 1466 return inRange == test; 1467 } 1468 isLimited(SampleType sampleType)1469 public boolean isLimited(SampleType sampleType) { 1470 boolean valueIsZero = lowerBound == upperBound && lowerBound == 0d; 1471 boolean hasDecimals = 1472 (operand == Operand.v || operand == Operand.w || operand == Operand.f || operand == Operand.t) 1473 && inRange != valueIsZero; // either NOT f = zero or f = non-zero 1474 switch (sampleType) { 1475 case INTEGER: 1476 return hasDecimals // will be empty 1477 || (operand == Operand.n || operand == Operand.i || operand == Operand.j) 1478 && mod == 0 1479 && inRange; 1480 1481 case DECIMAL: 1482 return (!hasDecimals || operand == Operand.n || operand == Operand.j) 1483 && (integersOnly || lowerBound == upperBound) 1484 && mod == 0 1485 && inRange; 1486 } 1487 return false; 1488 } 1489 toString()1490 public String toString() { 1491 StringBuilder result = new StringBuilder(); 1492 result.append(operand); 1493 if (mod != 0) { 1494 result.append(" % ").append(mod); 1495 } 1496 boolean isList = lowerBound != upperBound; 1497 result.append( 1498 !isList ? (inRange ? " = " : " != ") 1499 : integersOnly ? (inRange ? " = " : " != ") 1500 : (inRange ? " within " : " not within ") 1501 ); 1502 if (range_list != null) { 1503 for (int i = 0; i < range_list.length; i += 2) { 1504 addRange(result, range_list[i], range_list[i+1], i != 0); 1505 } 1506 } else { 1507 addRange(result, lowerBound, upperBound, false); 1508 } 1509 return result.toString(); 1510 } 1511 } 1512 addRange(StringBuilder result, double lb, double ub, boolean addSeparator)1513 private static void addRange(StringBuilder result, double lb, double ub, boolean addSeparator) { 1514 if (addSeparator) { 1515 result.append(","); 1516 } 1517 if (lb == ub) { 1518 result.append(format(lb)); 1519 } else { 1520 result.append(format(lb) + ".." + format(ub)); 1521 } 1522 } 1523 format(double lb)1524 private static String format(double lb) { 1525 long lbi = (long) lb; 1526 return lb == lbi ? String.valueOf(lbi) : String.valueOf(lb); 1527 } 1528 1529 /* Convenience base class for and/or constraints. */ 1530 private static abstract class BinaryConstraint implements Constraint, 1531 Serializable { 1532 private static final long serialVersionUID = 1; 1533 protected final Constraint a; 1534 protected final Constraint b; 1535 BinaryConstraint(Constraint a, Constraint b)1536 protected BinaryConstraint(Constraint a, Constraint b) { 1537 this.a = a; 1538 this.b = b; 1539 } 1540 } 1541 1542 /* A constraint representing the logical and of two constraints. */ 1543 private static class AndConstraint extends BinaryConstraint { 1544 private static final long serialVersionUID = 7766999779862263523L; 1545 AndConstraint(Constraint a, Constraint b)1546 AndConstraint(Constraint a, Constraint b) { 1547 super(a, b); 1548 } 1549 isFulfilled(FixedDecimal n)1550 public boolean isFulfilled(FixedDecimal n) { 1551 return a.isFulfilled(n) 1552 && b.isFulfilled(n); 1553 } 1554 isLimited(SampleType sampleType)1555 public boolean isLimited(SampleType sampleType) { 1556 // we ignore the case where both a and b are unlimited but no values 1557 // satisfy both-- we still consider this 'unlimited' 1558 return a.isLimited(sampleType) 1559 || b.isLimited(sampleType); 1560 } 1561 toString()1562 public String toString() { 1563 return a.toString() + " and " + b.toString(); 1564 } 1565 } 1566 1567 /* A constraint representing the logical or of two constraints. */ 1568 private static class OrConstraint extends BinaryConstraint { 1569 private static final long serialVersionUID = 1405488568664762222L; 1570 OrConstraint(Constraint a, Constraint b)1571 OrConstraint(Constraint a, Constraint b) { 1572 super(a, b); 1573 } 1574 isFulfilled(FixedDecimal n)1575 public boolean isFulfilled(FixedDecimal n) { 1576 return a.isFulfilled(n) 1577 || b.isFulfilled(n); 1578 } 1579 isLimited(SampleType sampleType)1580 public boolean isLimited(SampleType sampleType) { 1581 return a.isLimited(sampleType) 1582 && b.isLimited(sampleType); 1583 } 1584 toString()1585 public String toString() { 1586 return a.toString() + " or " + b.toString(); 1587 } 1588 } 1589 1590 /* 1591 * Implementation of Rule that uses a constraint. 1592 * Provides 'and' and 'or' to combine constraints. Immutable. 1593 */ 1594 private static class Rule implements Serializable { 1595 private static final long serialVersionUID = 1; 1596 private final String keyword; 1597 private final Constraint constraint; 1598 private final FixedDecimalSamples integerSamples; 1599 private final FixedDecimalSamples decimalSamples; 1600 Rule(String keyword, Constraint constraint, FixedDecimalSamples integerSamples, FixedDecimalSamples decimalSamples)1601 public Rule(String keyword, Constraint constraint, FixedDecimalSamples integerSamples, FixedDecimalSamples decimalSamples) { 1602 this.keyword = keyword; 1603 this.constraint = constraint; 1604 this.integerSamples = integerSamples; 1605 this.decimalSamples = decimalSamples; 1606 } 1607 1608 @SuppressWarnings("unused") and(Constraint c)1609 public Rule and(Constraint c) { 1610 return new Rule(keyword, new AndConstraint(constraint, c), integerSamples, decimalSamples); 1611 } 1612 1613 @SuppressWarnings("unused") or(Constraint c)1614 public Rule or(Constraint c) { 1615 return new Rule(keyword, new OrConstraint(constraint, c), integerSamples, decimalSamples); 1616 } 1617 getKeyword()1618 public String getKeyword() { 1619 return keyword; 1620 } 1621 appliesTo(FixedDecimal n)1622 public boolean appliesTo(FixedDecimal n) { 1623 return constraint.isFulfilled(n); 1624 } 1625 isLimited(SampleType sampleType)1626 public boolean isLimited(SampleType sampleType) { 1627 return constraint.isLimited(sampleType); 1628 } 1629 toString()1630 public String toString() { 1631 return keyword + ": " + constraint.toString() 1632 + (integerSamples == null ? "" : " " + integerSamples.toString()) 1633 + (decimalSamples == null ? "" : " " + decimalSamples.toString()); 1634 } 1635 1636 /** 1637 * @internal 1638 * @deprecated This API is ICU internal only. 1639 */ 1640 @Deprecated 1641 @Override hashCode()1642 public int hashCode() { 1643 return keyword.hashCode() ^ constraint.hashCode(); 1644 } 1645 getConstraint()1646 public String getConstraint() { 1647 return constraint.toString(); 1648 } 1649 } 1650 1651 private static class RuleList implements Serializable { 1652 private boolean hasExplicitBoundingInfo = false; 1653 private static final long serialVersionUID = 1; 1654 private final List<Rule> rules = new ArrayList<Rule>(); 1655 addRule(Rule nextRule)1656 public RuleList addRule(Rule nextRule) { 1657 String keyword = nextRule.getKeyword(); 1658 for (Rule rule : rules) { 1659 if (keyword.equals(rule.getKeyword())) { 1660 throw new IllegalArgumentException("Duplicate keyword: " + keyword); 1661 } 1662 } 1663 rules.add(nextRule); 1664 return this; 1665 } 1666 finish()1667 public RuleList finish() throws ParseException { 1668 // make sure that 'other' is present, and at the end. 1669 Rule otherRule = null; 1670 for (Iterator<Rule> it = rules.iterator(); it.hasNext();) { 1671 Rule rule = it.next(); 1672 if ("other".equals(rule.getKeyword())) { 1673 otherRule = rule; 1674 it.remove(); 1675 } 1676 } 1677 if (otherRule == null) { 1678 otherRule = parseRule("other:"); // make sure we have always have an 'other' a rule 1679 } 1680 rules.add(otherRule); 1681 return this; 1682 } 1683 selectRule(FixedDecimal n)1684 private Rule selectRule(FixedDecimal n) { 1685 for (Rule rule : rules) { 1686 if (rule.appliesTo(n)) { 1687 return rule; 1688 } 1689 } 1690 return null; 1691 } 1692 select(FixedDecimal n)1693 public String select(FixedDecimal n) { 1694 if (Double.isInfinite(n.source) || Double.isNaN(n.source)) { 1695 return KEYWORD_OTHER; 1696 } 1697 Rule r = selectRule(n); 1698 return r.getKeyword(); 1699 } 1700 getKeywords()1701 public Set<String> getKeywords() { 1702 Set<String> result = new LinkedHashSet<String>(); 1703 for (Rule rule : rules) { 1704 result.add(rule.getKeyword()); 1705 } 1706 // since we have explict 'other', we don't need this. 1707 //result.add(KEYWORD_OTHER); 1708 return result; 1709 } 1710 isLimited(String keyword, SampleType sampleType)1711 public boolean isLimited(String keyword, SampleType sampleType) { 1712 if (hasExplicitBoundingInfo) { 1713 FixedDecimalSamples mySamples = getDecimalSamples(keyword, sampleType); 1714 return mySamples == null ? true : mySamples.bounded; 1715 } 1716 1717 return computeLimited(keyword, sampleType); 1718 } 1719 computeLimited(String keyword, SampleType sampleType)1720 public boolean computeLimited(String keyword, SampleType sampleType) { 1721 // if all rules with this keyword are limited, it's limited, 1722 // and if there's no rule with this keyword, it's unlimited 1723 boolean result = false; 1724 for (Rule rule : rules) { 1725 if (keyword.equals(rule.getKeyword())) { 1726 if (!rule.isLimited(sampleType)) { 1727 return false; 1728 } 1729 result = true; 1730 } 1731 } 1732 return result; 1733 } 1734 toString()1735 public String toString() { 1736 StringBuilder builder = new StringBuilder(); 1737 for (Rule rule : rules) { 1738 if (builder.length() != 0) { 1739 builder.append(CATEGORY_SEPARATOR); 1740 } 1741 builder.append(rule); 1742 } 1743 return builder.toString(); 1744 } 1745 getRules(String keyword)1746 public String getRules(String keyword) { 1747 for (Rule rule : rules) { 1748 if (rule.getKeyword().equals(keyword)) { 1749 return rule.getConstraint(); 1750 } 1751 } 1752 return null; 1753 } 1754 select(FixedDecimal sample, String keyword)1755 public boolean select(FixedDecimal sample, String keyword) { 1756 for (Rule rule : rules) { 1757 if (rule.getKeyword().equals(keyword) && rule.appliesTo(sample)) { 1758 return true; 1759 } 1760 } 1761 return false; 1762 } 1763 getDecimalSamples(String keyword, SampleType sampleType)1764 public FixedDecimalSamples getDecimalSamples(String keyword, SampleType sampleType) { 1765 for (Rule rule : rules) { 1766 if (rule.getKeyword().equals(keyword)) { 1767 return sampleType == SampleType.INTEGER ? rule.integerSamples : rule.decimalSamples; 1768 } 1769 } 1770 return null; 1771 } 1772 } 1773 1774 /** 1775 * @deprecated This API is ICU internal only. 1776 * @internal 1777 */ 1778 @Deprecated 1779 public enum StandardPluralCategories { 1780 /** 1781 * @internal 1782 * @deprecated This API is ICU internal only. 1783 */ 1784 @Deprecated 1785 zero, 1786 /** 1787 * @internal 1788 * @deprecated This API is ICU internal only. 1789 */ 1790 @Deprecated 1791 one, 1792 /** 1793 * @internal 1794 * @deprecated This API is ICU internal only. 1795 */ 1796 @Deprecated 1797 two, 1798 /** 1799 * @internal 1800 * @deprecated This API is ICU internal only. 1801 */ 1802 @Deprecated 1803 few, 1804 /** 1805 * @internal 1806 * @deprecated This API is ICU internal only. 1807 */ 1808 @Deprecated 1809 many, 1810 /** 1811 * @internal 1812 * @deprecated This API is ICU internal only. 1813 */ 1814 @Deprecated 1815 other; forString(String s)1816 static StandardPluralCategories forString(String s) { 1817 StandardPluralCategories a; 1818 try { 1819 a = valueOf(s); 1820 } catch (Exception e) { 1821 return null; 1822 } 1823 return a; 1824 } 1825 } 1826 1827 @SuppressWarnings("unused") addConditional(Set<FixedDecimal> toAddTo, Set<FixedDecimal> others, double trial)1828 private boolean addConditional(Set<FixedDecimal> toAddTo, Set<FixedDecimal> others, double trial) { 1829 boolean added; 1830 FixedDecimal toAdd = new FixedDecimal(trial); 1831 if (!toAddTo.contains(toAdd) && !others.contains(toAdd)) { 1832 others.add(toAdd); 1833 added = true; 1834 } else { 1835 added = false; 1836 } 1837 return added; 1838 } 1839 1840 1841 1842 // ------------------------------------------------------------------------- 1843 // Static class methods. 1844 // ------------------------------------------------------------------------- 1845 1846 /** 1847 * Provides access to the predefined cardinal-number <code>PluralRules</code> for a given 1848 * locale. 1849 * Same as forLocale(locale, PluralType.CARDINAL). 1850 * 1851 * <p>ICU defines plural rules for many locales based on CLDR <i>Language Plural Rules</i>. 1852 * For these predefined rules, see CLDR page at 1853 * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html 1854 * 1855 * @param locale The locale for which a <code>PluralRules</code> object is 1856 * returned. 1857 * @return The predefined <code>PluralRules</code> object for this locale. 1858 * If there's no predefined rules for this locale, the rules 1859 * for the closest parent in the locale hierarchy that has one will 1860 * be returned. The final fallback always returns the default 1861 * rules. 1862 * @stable ICU 3.8 1863 */ forLocale(Locale locale)1864 public static PluralRules forLocale(Locale locale) { 1865 return forLocale(locale, PluralType.CARDINAL); 1866 } 1867 1868 /** 1869 * Provides access to the predefined <code>PluralRules</code> for a given 1870 * locale and the plural type. 1871 * 1872 * <p>ICU defines plural rules for many locales based on CLDR <i>Language Plural Rules</i>. 1873 * For these predefined rules, see CLDR page at 1874 * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html 1875 * 1876 * @param locale The locale for which a <code>PluralRules</code> object is 1877 * returned. 1878 * @param type The plural type (e.g., cardinal or ordinal). 1879 * @return The predefined <code>PluralRules</code> object for this locale. 1880 * If there's no predefined rules for this locale, the rules 1881 * for the closest parent in the locale hierarchy that has one will 1882 * be returned. The final fallback always returns the default 1883 * rules. 1884 * @stable ICU 50 1885 */ forLocale(Locale locale, PluralType type)1886 public static PluralRules forLocale(Locale locale, PluralType type) { 1887 return Factory.getDefaultFactory().forLocale(locale, type); 1888 } 1889 1890 /* 1891 * Checks whether a token is a valid keyword. 1892 * 1893 * @param token the token to be checked 1894 * @return true if the token is a valid keyword. 1895 */ isValidKeyword(String token)1896 private static boolean isValidKeyword(String token) { 1897 // return ALLOWED_ID.containsAll(token); 1898 for (int i = 0; i < token.length(); ++i) { 1899 char c = token.charAt(i); 1900 if (!('a' <= c && c <= 'z')) { 1901 return false; 1902 } 1903 } 1904 return true; 1905 } 1906 1907 /* 1908 * Creates a new <code>PluralRules</code> object. Immutable. 1909 */ PluralRules(RuleList rules)1910 private PluralRules(RuleList rules) { 1911 this.rules = rules; 1912 this.keywords = Collections.unmodifiableSet(rules.getKeywords()); 1913 } 1914 1915 /** 1916 * @internal 1917 * @deprecated This API is ICU internal only. 1918 */ 1919 @Deprecated 1920 @Override hashCode()1921 public int hashCode() { 1922 return rules.hashCode(); 1923 } 1924 /** 1925 * Given a number, returns the keyword of the first rule that applies to 1926 * the number. 1927 * 1928 * @param number The number for which the rule has to be determined. 1929 * @return The keyword of the selected rule. 1930 * @stable ICU 4.0 1931 */ select(double number)1932 public String select(double number) { 1933 return rules.select(new FixedDecimal(number)); 1934 } 1935 1936 /** 1937 * Given a number, returns the keyword of the first rule that applies to 1938 * the number. 1939 * 1940 * @param number The number for which the rule has to be determined. 1941 * @return The keyword of the selected rule. 1942 * @internal 1943 * @deprecated This API is ICU internal only. 1944 */ 1945 @Deprecated select(double number, int countVisibleFractionDigits, long fractionaldigits)1946 public String select(double number, int countVisibleFractionDigits, long fractionaldigits) { 1947 return rules.select(new FixedDecimal(number, countVisibleFractionDigits, fractionaldigits)); 1948 } 1949 1950 /** 1951 * Given a number information, returns the keyword of the first rule that applies to 1952 * the number. 1953 * 1954 * @param sample The number information for which the rule has to be determined. 1955 * @return The keyword of the selected rule. 1956 * @internal 1957 * @deprecated This API is ICU internal only. 1958 */ 1959 @Deprecated select(FixedDecimal sample)1960 public String select(FixedDecimal sample) { 1961 return rules.select(sample); 1962 } 1963 1964 1965 /** 1966 * Given a number information, and keyword, return whether the keyword would match the number. 1967 * 1968 * @param sample The number information for which the rule has to be determined. 1969 * @param keyword The keyword to filter on 1970 * @internal 1971 * @deprecated This API is ICU internal only. 1972 */ 1973 @Deprecated matches(FixedDecimal sample, String keyword)1974 public boolean matches(FixedDecimal sample, String keyword) { 1975 return rules.select(sample, keyword); 1976 } 1977 1978 /** 1979 * Returns a set of all rule keywords used in this <code>PluralRules</code> 1980 * object. The rule "other" is always present by default. 1981 * 1982 * @return The set of keywords. 1983 * @stable ICU 3.8 1984 */ getKeywords()1985 public Set<String> getKeywords() { 1986 return keywords; 1987 } 1988 1989 /** 1990 * Returns the unique value that this keyword matches, or {@link #NO_UNIQUE_VALUE} 1991 * if the keyword matches multiple values or is not defined for this PluralRules. 1992 * 1993 * @param keyword the keyword to check for a unique value 1994 * @return The unique value for the keyword, or NO_UNIQUE_VALUE. 1995 * @stable ICU 4.8 1996 */ getUniqueKeywordValue(String keyword)1997 public double getUniqueKeywordValue(String keyword) { 1998 Collection<Double> values = getAllKeywordValues(keyword); 1999 if (values != null && values.size() == 1) { 2000 return values.iterator().next(); 2001 } 2002 return NO_UNIQUE_VALUE; 2003 } 2004 2005 /** 2006 * Returns all the values that trigger this keyword, or null if the number of such 2007 * values is unlimited. 2008 * 2009 * @param keyword the keyword 2010 * @return the values that trigger this keyword, or null. The returned collection 2011 * is immutable. It will be empty if the keyword is not defined. 2012 * @stable ICU 4.8 2013 */ getAllKeywordValues(String keyword)2014 public Collection<Double> getAllKeywordValues(String keyword) { 2015 return getAllKeywordValues(keyword, SampleType.INTEGER); 2016 } 2017 2018 /** 2019 * Returns all the values that trigger this keyword, or null if the number of such 2020 * values is unlimited. 2021 * 2022 * @param keyword the keyword 2023 * @param type the type of samples requested, INTEGER or DECIMAL 2024 * @return the values that trigger this keyword, or null. The returned collection 2025 * is immutable. It will be empty if the keyword is not defined. 2026 * 2027 * @internal 2028 * @deprecated This API is ICU internal only. 2029 */ 2030 @Deprecated getAllKeywordValues(String keyword, SampleType type)2031 public Collection<Double> getAllKeywordValues(String keyword, SampleType type) { 2032 if (!isLimited(keyword, type)) { 2033 return null; 2034 } 2035 Collection<Double> samples = getSamples(keyword, type); 2036 return samples == null ? null : Collections.unmodifiableCollection(samples); 2037 } 2038 2039 /** 2040 * Returns a list of integer values for which select() would return that keyword, 2041 * or null if the keyword is not defined. The returned collection is unmodifiable. 2042 * The returned list is not complete, and there might be additional values that 2043 * would return the keyword. 2044 * 2045 * @param keyword the keyword to test 2046 * @return a list of values matching the keyword. 2047 * @stable ICU 4.8 2048 */ getSamples(String keyword)2049 public Collection<Double> getSamples(String keyword) { 2050 return getSamples(keyword, SampleType.INTEGER); 2051 } 2052 2053 /** 2054 * Returns a list of values for which select() would return that keyword, 2055 * or null if the keyword is not defined. 2056 * The returned collection is unmodifiable. 2057 * The returned list is not complete, and there might be additional values that 2058 * would return the keyword. The keyword might be defined, and yet have an empty set of samples, 2059 * IF there are samples for the other sampleType. 2060 * 2061 * @param keyword the keyword to test 2062 * @param sampleType the type of samples requested, INTEGER or DECIMAL 2063 * @return a list of values matching the keyword. 2064 * @internal 2065 * @deprecated ICU internal only 2066 */ 2067 @Deprecated getSamples(String keyword, SampleType sampleType)2068 public Collection<Double> getSamples(String keyword, SampleType sampleType) { 2069 if (!keywords.contains(keyword)) { 2070 return null; 2071 } 2072 Set<Double> result = new TreeSet<Double>(); 2073 2074 if (rules.hasExplicitBoundingInfo) { 2075 FixedDecimalSamples samples = rules.getDecimalSamples(keyword, sampleType); 2076 return samples == null ? Collections.unmodifiableSet(result) 2077 : Collections.unmodifiableSet(samples.addSamples(result)); 2078 } 2079 2080 // hack in case the rule is created without explicit samples 2081 int maxCount = isLimited(keyword, sampleType) ? Integer.MAX_VALUE : 20; 2082 2083 switch (sampleType) { 2084 case INTEGER: 2085 for (int i = 0; i < 200; ++i) { 2086 if (!addSample(keyword, i, maxCount, result)) { 2087 break; 2088 } 2089 } 2090 addSample(keyword, 1000000, maxCount, result); // hack for Welsh 2091 break; 2092 case DECIMAL: 2093 for (int i = 0; i < 2000; ++i) { 2094 if (!addSample(keyword, new FixedDecimal(i/10d, 1), maxCount, result)) { 2095 break; 2096 } 2097 } 2098 addSample(keyword, new FixedDecimal(1000000d, 1), maxCount, result); // hack for Welsh 2099 break; 2100 } 2101 return result.size() == 0 ? null : Collections.unmodifiableSet(result); 2102 } 2103 2104 /** 2105 * @internal 2106 * @deprecated This API is ICU internal only. 2107 */ 2108 @Deprecated addSample(String keyword, Number sample, int maxCount, Set<Double> result)2109 public boolean addSample(String keyword, Number sample, int maxCount, Set<Double> result) { 2110 String selectedKeyword = sample instanceof FixedDecimal ? select((FixedDecimal)sample) : select(sample.doubleValue()); 2111 if (selectedKeyword.equals(keyword)) { 2112 result.add(sample.doubleValue()); 2113 if (--maxCount < 0) { 2114 return false; 2115 } 2116 } 2117 return true; 2118 } 2119 2120 /** 2121 * Returns a list of values for which select() would return that keyword, 2122 * or null if the keyword is not defined or no samples are available. 2123 * The returned collection is unmodifiable. 2124 * The returned list is not complete, and there might be additional values that 2125 * would return the keyword. 2126 * 2127 * @param keyword the keyword to test 2128 * @param sampleType the type of samples requested, INTEGER or DECIMAL 2129 * @return a list of values matching the keyword. 2130 * @internal 2131 * @deprecated This API is ICU internal only. 2132 */ 2133 @Deprecated getDecimalSamples(String keyword, SampleType sampleType)2134 public FixedDecimalSamples getDecimalSamples(String keyword, SampleType sampleType) { 2135 return rules.getDecimalSamples(keyword, sampleType); 2136 } 2137 2138 /** 2139 * Returns the set of locales for which PluralRules are known. 2140 * @return the set of locales for which PluralRules are known, as a list 2141 * @draft ICU 4.2 2142 * @provisional This API might change or be removed in a future release. 2143 public static ULocale[] getAvailableULocales() { 2144 return Factory.getDefaultFactory().getAvailableULocales(); 2145 } 2146 */ 2147 2148 /** 2149 * Returns the 'functionally equivalent' locale with respect to 2150 * plural rules. Calling PluralRules.forLocale with the functionally equivalent 2151 * locale, and with the provided locale, returns rules that behave the same. 2152 * <br/> 2153 * All locales with the same functionally equivalent locale have 2154 * plural rules that behave the same. This is not exaustive; 2155 * there may be other locales whose plural rules behave the same 2156 * that do not have the same equivalent locale. 2157 * 2158 * @param locale the locale to check 2159 * @param isAvailable if not null and of length > 0, this will hold 'true' at 2160 * index 0 if locale is directly defined (without fallback) as having plural rules 2161 * @return the functionally-equivalent locale 2162 * @draft ICU 4.2 2163 * @provisional This API might change or be removed in a future release. 2164 public static ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable) { 2165 return Factory.getDefaultFactory().getFunctionalEquivalent(locale, isAvailable); 2166 } 2167 */ 2168 2169 /** 2170 * {@inheritDoc} 2171 * @stable ICU 3.8 2172 */ toString()2173 public String toString() { 2174 return rules.toString(); 2175 } 2176 2177 /** 2178 * {@inheritDoc} 2179 * @stable ICU 3.8 2180 */ equals(Object rhs)2181 public boolean equals(Object rhs) { 2182 return rhs instanceof PluralRules && equals((PluralRules)rhs); 2183 } 2184 2185 /** 2186 * Returns true if rhs is equal to this. 2187 * @param rhs the PluralRules to compare to. 2188 * @return true if this and rhs are equal. 2189 * @stable ICU 3.8 2190 */ 2191 // TODO Optimize this equals(PluralRules rhs)2192 public boolean equals(PluralRules rhs) { 2193 return rhs != null && toString().equals(rhs.toString()); 2194 } 2195 2196 /** 2197 * Status of the keyword for the rules, given a set of explicit values. 2198 * 2199 * @draft ICU 50 2200 * @provisional This API might change or be removed in a future release. 2201 */ 2202 public enum KeywordStatus { 2203 /** 2204 * The keyword is not valid for the rules. 2205 * 2206 * @draft ICU 50 2207 * @provisional This API might change or be removed in a future release. 2208 */ 2209 INVALID, 2210 /** 2211 * The keyword is valid, but unused (it is covered by the explicit values, OR has no values for the given {@link SampleType}). 2212 * 2213 * @draft ICU 50 2214 * @provisional This API might change or be removed in a future release. 2215 */ 2216 SUPPRESSED, 2217 /** 2218 * The keyword is valid, used, and has a single possible value (before considering explicit values). 2219 * 2220 * @draft ICU 50 2221 * @provisional This API might change or be removed in a future release. 2222 */ 2223 UNIQUE, 2224 /** 2225 * The keyword is valid, used, not unique, and has a finite set of values. 2226 * 2227 * @draft ICU 50 2228 * @provisional This API might change or be removed in a future release. 2229 */ 2230 BOUNDED, 2231 /** 2232 * The keyword is valid but not bounded; there indefinitely many matching values. 2233 * 2234 * @draft ICU 50 2235 * @provisional This API might change or be removed in a future release. 2236 */ 2237 UNBOUNDED 2238 } 2239 2240 /** 2241 * Find the status for the keyword, given a certain set of explicit values. 2242 * 2243 * @param keyword 2244 * the particular keyword (call rules.getKeywords() to get the valid ones) 2245 * @param offset 2246 * the offset used, or 0.0d if not. Internally, the offset is subtracted from each explicit value before 2247 * checking against the keyword values. 2248 * @param explicits 2249 * a set of Doubles that are used explicitly (eg [=0], "[=1]"). May be empty or null. 2250 * @param uniqueValue 2251 * If non null, set to the unique value. 2252 * @return the KeywordStatus 2253 * @draft ICU 50 2254 * @provisional This API might change or be removed in a future release. 2255 */ getKeywordStatus(String keyword, int offset, Set<Double> explicits, Output<Double> uniqueValue)2256 public KeywordStatus getKeywordStatus(String keyword, int offset, Set<Double> explicits, 2257 Output<Double> uniqueValue) { 2258 return getKeywordStatus(keyword, offset, explicits, uniqueValue, SampleType.INTEGER); 2259 } 2260 /** 2261 * Find the status for the keyword, given a certain set of explicit values. 2262 * 2263 * @param keyword 2264 * the particular keyword (call rules.getKeywords() to get the valid ones) 2265 * @param offset 2266 * the offset used, or 0.0d if not. Internally, the offset is subtracted from each explicit value before 2267 * checking against the keyword values. 2268 * @param explicits 2269 * a set of Doubles that are used explicitly (eg [=0], "[=1]"). May be empty or null. 2270 * @param sampleType 2271 * request KeywordStatus relative to INTEGER or DECIMAL values 2272 * @param uniqueValue 2273 * If non null, set to the unique value. 2274 * @return the KeywordStatus 2275 * @internal 2276 * @provisional This API might change or be removed in a future release. 2277 */ getKeywordStatus(String keyword, int offset, Set<Double> explicits, Output<Double> uniqueValue, SampleType sampleType)2278 public KeywordStatus getKeywordStatus(String keyword, int offset, Set<Double> explicits, 2279 Output<Double> uniqueValue, SampleType sampleType) { 2280 if (uniqueValue != null) { 2281 uniqueValue.value = null; 2282 } 2283 2284 if (!keywords.contains(keyword)) { 2285 return KeywordStatus.INVALID; 2286 } 2287 2288 if (!isLimited(keyword, sampleType)) { 2289 return KeywordStatus.UNBOUNDED; 2290 } 2291 2292 Collection<Double> values = getSamples(keyword, sampleType); 2293 2294 int originalSize = values.size(); 2295 2296 if (explicits == null) { 2297 explicits = Collections.emptySet(); 2298 } 2299 2300 // Quick check on whether there are multiple elements 2301 2302 if (originalSize > explicits.size()) { 2303 if (originalSize == 1) { 2304 if (uniqueValue != null) { 2305 uniqueValue.value = values.iterator().next(); 2306 } 2307 return KeywordStatus.UNIQUE; 2308 } 2309 return KeywordStatus.BOUNDED; 2310 } 2311 2312 // Compute if the quick test is insufficient. 2313 2314 HashSet<Double> subtractedSet = new HashSet<Double>(values); 2315 for (Double explicit : explicits) { 2316 subtractedSet.remove(explicit - offset); 2317 } 2318 if (subtractedSet.size() == 0) { 2319 return KeywordStatus.SUPPRESSED; 2320 } 2321 2322 if (uniqueValue != null && subtractedSet.size() == 1) { 2323 uniqueValue.value = subtractedSet.iterator().next(); 2324 } 2325 2326 return originalSize == 1 ? KeywordStatus.UNIQUE : KeywordStatus.BOUNDED; 2327 } 2328 2329 /** 2330 * @internal 2331 * @deprecated This API is ICU internal only. 2332 */ 2333 @Deprecated getRules(String keyword)2334 public String getRules(String keyword) { 2335 return rules.getRules(keyword); 2336 } 2337 /* 2338 private void writeObject( 2339 ObjectOutputStream out) 2340 throws IOException { 2341 throw new NotSerializableException(); 2342 } 2343 2344 private void readObject(ObjectInputStream in 2345 ) throws IOException, ClassNotFoundException { 2346 throw new NotSerializableException(); 2347 } 2348 2349 private Object writeReplace() throws ObjectStreamException { 2350 return new PluralRulesSerialProxy(toString()); 2351 } 2352 */ 2353 /** 2354 * @internal 2355 * @deprecated internal 2356 */ 2357 @Deprecated compareTo(PluralRules other)2358 public int compareTo(PluralRules other) { 2359 return toString().compareTo(other.toString()); 2360 } 2361 2362 /** 2363 * @internal 2364 * @deprecated internal 2365 */ 2366 @Deprecated isLimited(String keyword)2367 public Boolean isLimited(String keyword) { 2368 return rules.isLimited(keyword, SampleType.INTEGER); 2369 } 2370 2371 /** 2372 * @internal 2373 * @deprecated internal 2374 */ 2375 @Deprecated isLimited(String keyword, SampleType sampleType)2376 public boolean isLimited(String keyword, SampleType sampleType) { 2377 return rules.isLimited(keyword, sampleType); 2378 } 2379 2380 /** 2381 * @internal 2382 * @deprecated internal 2383 */ 2384 @Deprecated computeLimited(String keyword, SampleType sampleType)2385 public boolean computeLimited(String keyword, SampleType sampleType) { 2386 return rules.computeLimited(keyword, sampleType); 2387 } 2388 } 2389