Home | History | Annotate | Download | only in format
      1 /* GENERATED SOURCE. DO NOT MODIFY. */
      2 //  2016 and later: Unicode, Inc. and others.
      3 // License & terms of use: http://www.unicode.org/copyright.html#License
      4 /*
      5  *******************************************************************************
      6  * Copyright (C) 2007-2015, International Business Machines Corporation and
      7  * others. All Rights Reserved.
      8  *******************************************************************************
      9  */
     10 package android.icu.dev.test.format;
     11 
     12 import java.io.ByteArrayInputStream;
     13 import java.io.ByteArrayOutputStream;
     14 import java.io.IOException;
     15 import java.io.ObjectInputStream;
     16 import java.io.ObjectOutputStream;
     17 import java.io.Serializable;
     18 import java.text.ParseException;
     19 import java.util.ArrayList;
     20 import java.util.Arrays;
     21 import java.util.Collection;
     22 import java.util.Collections;
     23 import java.util.Comparator;
     24 import java.util.EnumSet;
     25 import java.util.HashMap;
     26 import java.util.HashSet;
     27 import java.util.LinkedHashSet;
     28 import java.util.List;
     29 import java.util.Locale;
     30 import java.util.Map;
     31 import java.util.Map.Entry;
     32 import java.util.Set;
     33 import java.util.TreeMap;
     34 import java.util.TreeSet;
     35 
     36 import org.junit.Test;
     37 import org.junit.runner.RunWith;
     38 import org.junit.runners.JUnit4;
     39 
     40 import android.icu.dev.test.TestFmwk;
     41 import android.icu.dev.test.serializable.SerializableTestUtility;
     42 import android.icu.dev.util.CollectionUtilities;
     43 import android.icu.impl.Relation;
     44 import android.icu.impl.Utility;
     45 import android.icu.text.NumberFormat;
     46 import android.icu.text.PluralRules;
     47 import android.icu.text.PluralRules.FixedDecimal;
     48 import android.icu.text.PluralRules.FixedDecimalRange;
     49 import android.icu.text.PluralRules.FixedDecimalSamples;
     50 import android.icu.text.PluralRules.KeywordStatus;
     51 import android.icu.text.PluralRules.PluralType;
     52 import android.icu.text.PluralRules.SampleType;
     53 import android.icu.text.UFieldPosition;
     54 import android.icu.util.Output;
     55 import android.icu.util.ULocale;
     56 import android.icu.testsharding.MainTestShard;
     57 
     58 /**
     59  * @author dougfelt (Doug Felt)
     60  * @author markdavis (Mark Davis) [for fractional support]
     61  */
     62 @MainTestShard
     63 @RunWith(JUnit4.class)
     64 public class PluralRulesTest extends TestFmwk {
     65 
     66     PluralRulesFactory factory = PluralRulesFactory.NORMAL;
     67 
     68     @Test
     69     public void testOverUnderflow() {
     70         logln(String.valueOf(Long.MAX_VALUE + 1d));
     71         for (double[] testDouble : new double[][] {
     72                 { 1E18, 0, 0, 1E18 }, // check overflow
     73                 { 10000000000000.1d, 1, 1, 10000000000000d }, { -0.00001d, 1, 5, 0 }, { 1d, 0, 0, 1 },
     74                 { 1.1d, 1, 1, 1 }, { 12345d, 0, 0, 12345 }, { 12345.678912d, 678912, 6, 12345 },
     75                 { 12345.6789123d, 678912, 6, 12345 }, // we only go out 6 digits
     76                 { 1E18, 0, 0, 1E18 }, // check overflow
     77                 { 1E19, 0, 0, 1E18 }, // check overflow
     78         }) {
     79             FixedDecimal fd = new FixedDecimal(testDouble[0]);
     80             assertEquals(testDouble[0] + "=doubleValue()", testDouble[0], fd.doubleValue());
     81             assertEquals(testDouble[0] + " decimalDigits", (int) testDouble[1], fd.getDecimalDigits());
     82             assertEquals(testDouble[0] + " visibleDecimalDigitCount", (int) testDouble[2], fd.getVisibleDecimalDigitCount());
     83             assertEquals(testDouble[0] + " decimalDigitsWithoutTrailingZeros", (int) testDouble[1],
     84                     fd.getDecimalDigitsWithoutTrailingZeros());
     85             assertEquals(testDouble[0] + " visibleDecimalDigitCountWithoutTrailingZeros", (int) testDouble[2],
     86                     fd.getVisibleDecimalDigitCountWithoutTrailingZeros());
     87             assertEquals(testDouble[0] + " integerValue", (long) testDouble[3], fd.getIntegerValue());
     88         }
     89 
     90         for (ULocale locale : new ULocale[] { ULocale.ENGLISH, new ULocale("cy"), new ULocale("ar") }) {
     91             PluralRules rules = factory.forLocale(locale);
     92 
     93             assertEquals(locale + " NaN", "other", rules.select(Double.NaN));
     94             assertEquals(locale + " ", "other", rules.select(Double.POSITIVE_INFINITY));
     95             assertEquals(locale + " -", "other", rules.select(Double.NEGATIVE_INFINITY));
     96         }
     97     }
     98 
     99     @Test
    100     public void testSyntaxRestrictions() {
    101         Object[][] shouldFail = {
    102                 { "a:n in 3..10,13..19" },
    103 
    104                 // = and != always work
    105                 { "a:n=1" },
    106                 { "a:n=1,3" },
    107                 { "a:n!=1" },
    108                 { "a:n!=1,3" },
    109 
    110                 // with spacing
    111                 { "a: n = 1" },
    112                 { "a: n = 1, 3" },
    113                 { "a: n != 1" },
    114                 { "a: n != 1, 3" },
    115                 { "a: n ! = 1" },
    116                 { "a: n ! = 1, 3" },
    117                 { "a: n = 1 , 3" },
    118                 { "a: n != 1 , 3" },
    119                 { "a: n ! = 1 , 3" },
    120                 { "a: n = 1 .. 3" },
    121                 { "a: n != 1 .. 3" },
    122                 { "a: n ! = 1 .. 3" },
    123 
    124                 // more complicated
    125                 { "a:n in 3 .. 10 , 13 .. 19" },
    126 
    127                 // singles have special exceptions
    128                 { "a: n is 1" },
    129                 { "a: n is not 1" },
    130                 { "a: n not is 1", ParseException.class }, // hacked to fail
    131                 { "a: n in 1" },
    132                 { "a: n not in 1" },
    133 
    134                 // multiples also have special exceptions
    135                 // TODO enable the following once there is an update to CLDR
    136                 // {"a: n is 1,3", ParseException.class},
    137                 { "a: n is not 1,3", ParseException.class }, // hacked to fail
    138                 { "a: n not is 1,3", ParseException.class }, // hacked to fail
    139                 { "a: n in 1,3" },
    140                 { "a: n not in 1,3" },
    141 
    142                 // disallow not with =
    143                 { "a: n not= 1", ParseException.class }, // hacked to fail
    144                 { "a: n not= 1,3", ParseException.class }, // hacked to fail
    145 
    146                 // disallow double negatives
    147                 { "a: n ! is not 1", ParseException.class },
    148                 { "a: n ! is not 1", ParseException.class },
    149                 { "a: n not not in 1", ParseException.class },
    150                 { "a: n is not not 1", NumberFormatException.class },
    151 
    152                 // disallow screwy cases
    153                 { null, NullPointerException.class }, { "djkl;", ParseException.class },
    154                 { "a: n = 1 .", ParseException.class }, { "a: n = 1 ..", ParseException.class },
    155                 { "a: n = 1 2", ParseException.class }, { "a: n = 1 ,", ParseException.class },
    156                 { "a:n in 3 .. 10 , 13 .. 19 ,", ParseException.class }, };
    157         for (Object[] shouldFailTest : shouldFail) {
    158             String rules = (String) shouldFailTest[0];
    159             Class exception = shouldFailTest.length < 2 ? null : (Class) shouldFailTest[1];
    160             Class actualException = null;
    161             try {
    162                 PluralRules.parseDescription(rules);
    163             } catch (Exception e) {
    164                 actualException = e.getClass();
    165             }
    166             assertEquals("Exception " + rules, exception, actualException);
    167         }
    168     }
    169 
    170     @Test
    171     public void testSamples() {
    172         String description = "one: n is 3 or f is 5 @integer  3,19, @decimal 3.50 ~ 3.53,   ; other:  @decimal 99.0~99.2, 999.0, ";
    173         PluralRules test = PluralRules.createRules(description);
    174 
    175         checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 3, 19", true,
    176                 new FixedDecimal(3));
    177         checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 3.50~3.53, ", false,
    178                 new FixedDecimal(3.5, 2));
    179         checkOldSamples(description, test, "one", SampleType.INTEGER, 3d, 19d);
    180         checkOldSamples(description, test, "one", SampleType.DECIMAL, 3.5d, 3.51d, 3.52d, 3.53d);
    181 
    182         checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "", true, null);
    183         checkNewSamples(description, test, "other", PluralRules.SampleType.DECIMAL, "@decimal 99.0~99.2, 999.0, ",
    184                 false, new FixedDecimal(99d, 1));
    185         checkOldSamples(description, test, "other", SampleType.INTEGER);
    186         checkOldSamples(description, test, "other", SampleType.DECIMAL, 99d, 99.1, 99.2d, 999d);
    187     }
    188 
    189     public void checkOldSamples(String description, PluralRules rules, String keyword, SampleType sampleType,
    190             Double... expected) {
    191         Collection<Double> oldSamples = rules.getSamples(keyword, sampleType);
    192         if (!assertEquals("getOldSamples; " + keyword + "; " + description, new HashSet(Arrays.asList(expected)),
    193                 oldSamples)) {
    194             rules.getSamples(keyword, sampleType);
    195         }
    196     }
    197 
    198     public void checkNewSamples(String description, PluralRules test, String keyword, SampleType sampleType,
    199             String samplesString, boolean isBounded, FixedDecimal firstInRange) {
    200         String title = description + ", " + sampleType;
    201         FixedDecimalSamples samples = test.getDecimalSamples(keyword, sampleType);
    202         if (samples != null) {
    203             assertEquals("samples; " + title, samplesString, samples.toString());
    204             assertEquals("bounded; " + title, isBounded, samples.bounded);
    205             assertEquals("first; " + title, firstInRange, samples.samples.iterator().next().start);
    206         }
    207         assertEquals("limited: " + title, isBounded, test.isLimited(keyword, sampleType));
    208     }
    209 
    210     private static final String[] parseTestData = { "a: n is 1", "a:1", "a: n mod 10 is 2", "a:2,12,22",
    211             "a: n is not 1", "a:0,2,3,4,5", "a: n mod 3 is not 1", "a:0,2,3,5,6,8,9", "a: n in 2..5", "a:2,3,4,5",
    212             "a: n within 2..5", "a:2,3,4,5", "a: n not in 2..5", "a:0,1,6,7,8", "a: n not within 2..5", "a:0,1,6,7,8",
    213             "a: n mod 10 in 2..5", "a:2,3,4,5,12,13,14,15,22,23,24,25", "a: n mod 10 within 2..5",
    214             "a:2,3,4,5,12,13,14,15,22,23,24,25", "a: n mod 10 is 2 and n is not 12", "a:2,22,32,42",
    215             "a: n mod 10 in 2..3 or n mod 10 is 5", "a:2,3,5,12,13,15,22,23,25",
    216             "a: n mod 10 within 2..3 or n mod 10 is 5", "a:2,3,5,12,13,15,22,23,25", "a: n is 1 or n is 4 or n is 23",
    217             "a:1,4,23", "a: n mod 2 is 1 and n is not 3 and n in 1..11", "a:1,5,7,9,11",
    218             "a: n mod 2 is 1 and n is not 3 and n within 1..11", "a:1,5,7,9,11",
    219             "a: n mod 2 is 1 or n mod 5 is 1 and n is not 6", "a:1,3,5,7,9,11,13,15,16",
    220             "a: n in 2..5; b: n in 5..8; c: n mod 2 is 1", "a:2,3,4,5;b:6,7,8;c:1,9,11",
    221             "a: n within 2..5; b: n within 5..8; c: n mod 2 is 1", "a:2,3,4,5;b:6,7,8;c:1,9,11",
    222             "a: n in 2,4..6; b: n within 7..9,11..12,20", "a:2,4,5,6;b:7,8,9,11,12,20",
    223             "a: n in 2..8,12 and n not in 4..6", "a:2,3,7,8,12", "a: n mod 10 in 2,3,5..7 and n is not 12",
    224             "a:2,3,5,6,7,13,15,16,17", "a: n in 2..6,3..7", "a:2,3,4,5,6,7", };
    225 
    226     private String[] getTargetStrings(String targets) {
    227         List list = new ArrayList(50);
    228         String[] valSets = Utility.split(targets, ';');
    229         for (int i = 0; i < valSets.length; ++i) {
    230             String[] temp = Utility.split(valSets[i], ':');
    231             String key = temp[0].trim();
    232             String[] vals = Utility.split(temp[1], ',');
    233             for (int j = 0; j < vals.length; ++j) {
    234                 String valString = vals[j].trim();
    235                 int val = Integer.parseInt(valString);
    236                 while (list.size() <= val) {
    237                     list.add(null);
    238                 }
    239                 if (list.get(val) != null) {
    240                     fail("test data error, key: " + list.get(val) + " already set for: " + val);
    241                 }
    242                 list.set(val, key);
    243             }
    244         }
    245 
    246         String[] result = (String[]) list.toArray(new String[list.size()]);
    247         for (int i = 0; i < result.length; ++i) {
    248             if (result[i] == null) {
    249                 result[i] = "other";
    250             }
    251         }
    252         return result;
    253     }
    254 
    255     private void checkTargets(PluralRules rules, String[] targets) {
    256         for (int i = 0; i < targets.length; ++i) {
    257             assertEquals("value " + i, targets[i], rules.select(i));
    258         }
    259     }
    260 
    261     @Test
    262     public void testParseEmpty() throws ParseException {
    263         PluralRules rules = PluralRules.parseDescription("a:n");
    264         assertEquals("empty", "a", rules.select(0));
    265     }
    266 
    267     @Test
    268     public void testParsing() {
    269         for (int i = 0; i < parseTestData.length; i += 2) {
    270             String pattern = parseTestData[i];
    271             String expected = parseTestData[i + 1];
    272 
    273             logln("pattern[" + i + "] " + pattern);
    274             try {
    275                 PluralRules rules = PluralRules.createRules(pattern);
    276                 String[] targets = getTargetStrings(expected);
    277                 checkTargets(rules, targets);
    278             } catch (Exception e) {
    279                 e.printStackTrace();
    280                 throw new RuntimeException(e.getMessage());
    281             }
    282         }
    283     }
    284 
    285     private static String[][] operandTestData = { { "a: n 3", "FAIL" },
    286             { "a: n=1,2; b: n != 3..5; c:n!=5", "a:1,2; b:6,7; c:3,4" },
    287             { "a: n=1,2; b: n!=3..5; c:n!=5", "a:1,2; b:6,7; c:3,4" },
    288             { "a: t is 1", "a:1.1,1.1000,99.100; other:1.2,1.0" }, { "a: f is 1", "a:1.1; other:1.1000,99.100" },
    289             { "a: i is 2; b:i is 3", "b: 3.5; a: 2.5" }, { "a: f is 0; b:f is 50", "a: 1.00; b: 1.50" },
    290             { "a: v is 1; b:v is 2", "a: 1.0; b: 1.00" }, { "one: n is 1 AND v is 0", "one: 1 ; other: 1.00,1.0" }, // English
    291                                                                                                                     // rules
    292             { "one: v is 0 and i mod 10 is 1 or f mod 10 is 1", "one: 1, 1.1, 3.1; other: 1.0, 3.2, 5" }, // Last
    293                                                                                                           // visible
    294                                                                                                           // digit
    295             { "one: j is 0", "one: 0; other: 0.0, 1.0, 3" }, // Last visible digit
    296     // one  n is 1; few  n in 2..4;
    297     };
    298 
    299     @Test
    300     public void testOperands() {
    301         for (String[] pair : operandTestData) {
    302             String pattern = pair[0].trim();
    303             String categoriesAndExpected = pair[1].trim();
    304 
    305             // logln("pattern[" + i + "] " + pattern);
    306             boolean FAIL_EXPECTED = categoriesAndExpected.equalsIgnoreCase("fail");
    307             try {
    308                 logln(pattern);
    309                 PluralRules rules = PluralRules.createRules(pattern);
    310                 if (FAIL_EXPECTED) {
    311                     assertNull("Should fail with 'null' return.", rules);
    312                 } else {
    313                     logln(rules == null ? "null rules" : rules.toString());
    314                     checkCategoriesAndExpected(pattern, categoriesAndExpected, rules);
    315                 }
    316             } catch (Exception e) {
    317                 if (!FAIL_EXPECTED) {
    318                     e.printStackTrace();
    319                     throw new RuntimeException(e.getMessage());
    320                 }
    321             }
    322         }
    323     }
    324 
    325     @Test
    326     public void testUniqueRules() {
    327         main: for (ULocale locale : factory.getAvailableULocales()) {
    328             PluralRules rules = factory.forLocale(locale);
    329             Map<String, PluralRules> keywordToRule = new HashMap<String, PluralRules>();
    330             Collection<FixedDecimalSamples> samples = new LinkedHashSet<FixedDecimalSamples>();
    331 
    332             for (String keyword : rules.getKeywords()) {
    333                 for (SampleType sampleType : SampleType.values()) {
    334                     FixedDecimalSamples samples2 = rules.getDecimalSamples(keyword, sampleType);
    335                     if (samples2 != null) {
    336                         samples.add(samples2);
    337                     }
    338                 }
    339                 if (keyword.equals("other")) {
    340                     continue;
    341                 }
    342                 String rules2 = keyword + ":" + rules.getRules(keyword);
    343                 PluralRules singleRule = PluralRules.createRules(rules2);
    344                 if (singleRule == null) {
    345                     errln("Can't generate single rule for " + rules2);
    346                     PluralRules.createRules(rules2); // for debugging
    347                     continue main;
    348                 }
    349                 keywordToRule.put(keyword, singleRule);
    350             }
    351             Map<FixedDecimal, String> collisionTest = new TreeMap();
    352             for (FixedDecimalSamples sample3 : samples) {
    353                 Set<FixedDecimalRange> samples2 = sample3.getSamples();
    354                 if (samples2 == null) {
    355                     continue;
    356                 }
    357                 for (FixedDecimalRange sample : samples2) {
    358                     for (int i = 0; i < 1; ++i) {
    359                         FixedDecimal item = i == 0 ? sample.start : sample.end;
    360                         collisionTest.clear();
    361                         for (Entry<String, PluralRules> entry : keywordToRule.entrySet()) {
    362                             PluralRules rule = entry.getValue();
    363                             String foundKeyword = rule.select(item);
    364                             if (foundKeyword.equals("other")) {
    365                                 continue;
    366                             }
    367                             String old = collisionTest.get(item);
    368                             if (old != null) {
    369                                 errln(locale + "\tNon-unique rules: " + item + " => " + old + " & " + foundKeyword);
    370                                 rule.select(item);
    371                             } else {
    372                                 collisionTest.put(item, foundKeyword);
    373                             }
    374                         }
    375                     }
    376                 }
    377             }
    378         }
    379     }
    380 
    381     private void checkCategoriesAndExpected(String title1, String categoriesAndExpected, PluralRules rules) {
    382         for (String categoryAndExpected : categoriesAndExpected.split("\\s*;\\s*")) {
    383             String[] categoryFromExpected = categoryAndExpected.split("\\s*:\\s*");
    384             String expected = categoryFromExpected[0];
    385             for (String value : categoryFromExpected[1].split("\\s*,\\s*")) {
    386                 if (value.startsWith("@") || value.equals("") || value.equals("null")) {
    387                     continue;
    388                 }
    389                 String[] values = value.split("\\s*~\\s*");
    390                 checkValue(title1, rules, expected, values[0]);
    391                 if (values.length > 1) {
    392                     checkValue(title1, rules, expected, values[1]);
    393                 }
    394             }
    395         }
    396     }
    397 
    398     public void checkValue(String title1, PluralRules rules, String expected, String value) {
    399         double number = Double.parseDouble(value);
    400         int decimalPos = value.indexOf('.') + 1;
    401         int countVisibleFractionDigits;
    402         int fractionaldigits;
    403         if (decimalPos == 0) {
    404             countVisibleFractionDigits = fractionaldigits = 0;
    405         } else {
    406             countVisibleFractionDigits = value.length() - decimalPos;
    407             fractionaldigits = Integer.parseInt(value.substring(decimalPos));
    408         }
    409         String result = rules.select(number, countVisibleFractionDigits, fractionaldigits);
    410         ULocale locale = null;
    411         assertEquals(getAssertMessage(title1, locale, rules, expected) + "; value: " + value, expected, result);
    412     }
    413 
    414     private static String[][] equalityTestData = {
    415             // once we add fractions, we had to retract the "test all possibilities" for equality,
    416             // so we only have a limited set of equality tests now.
    417             { "c: n%11!=5", "c: n mod 11 is not 5" }, { "c: n is not 7", "c: n != 7" }, { "a:n in 2;", "a: n = 2" },
    418             { "b:n not in 5;", "b: n != 5" },
    419 
    420     // { "a: n is 5",
    421     // "a: n in 2..6 and n not in 2..4 and n is not 6" },
    422     // { "a: n in 2..3",
    423     // "a: n is 2 or n is 3",
    424     // "a: n is 3 and n in 2..5 or n is 2" },
    425     // { "a: n is 12; b:n mod 10 in 2..3",
    426     // "b: n mod 10 in 2..3 and n is not 12; a: n in 12..12",
    427     // "b: n is 13; a: n is 12; b: n mod 10 is 2 or n mod 10 is 3" },
    428     };
    429 
    430     private static String[][] inequalityTestData = { { "a: n mod 8 is 3", "a: n mod 7 is 3" },
    431             { "a: n mod 3 is 2 and n is not 5", "a: n mod 6 is 2 or n is 8 or n is 11" },
    432             // the following are currently inequal, but we may make them equal in the future.
    433             { "a: n in 2..5", "a: n in 2..4,5" }, };
    434 
    435     private void compareEquality(String id, Object[] objects, boolean shouldBeEqual) {
    436         for (int i = 0; i < objects.length; ++i) {
    437             Object lhs = objects[i];
    438             int start = shouldBeEqual ? i : i + 1;
    439             for (int j = start; j < objects.length; ++j) {
    440                 Object rhs = objects[j];
    441                 if (rhs == null || shouldBeEqual != lhs.equals(rhs)) {
    442                     String msg = shouldBeEqual ? "should be equal" : "should not be equal";
    443                     fail(id + " " + msg + " (" + i + ", " + j + "):\n    " + lhs + "\n    " + rhs);
    444                 }
    445                 // assertEquals("obj " + i + " and " + j, lhs, rhs);
    446             }
    447         }
    448     }
    449 
    450     private void compareEqualityTestSets(String[][] sets, boolean shouldBeEqual) {
    451         for (int i = 0; i < sets.length; ++i) {
    452             String[] patterns = sets[i];
    453             PluralRules[] rules = new PluralRules[patterns.length];
    454             for (int j = 0; j < patterns.length; ++j) {
    455                 rules[j] = PluralRules.createRules(patterns[j]);
    456             }
    457             compareEquality("test " + i, rules, shouldBeEqual);
    458         }
    459     }
    460 
    461     @Test
    462     public void testEquality() {
    463         compareEqualityTestSets(equalityTestData, true);
    464     }
    465 
    466     @Test
    467     public void testInequality() {
    468         compareEqualityTestSets(inequalityTestData, false);
    469     }
    470 
    471     @Test
    472     public void testBuiltInRules() {
    473         // spot check
    474         PluralRules rules = factory.forLocale(ULocale.US);
    475         assertEquals("us 0", PluralRules.KEYWORD_OTHER, rules.select(0));
    476         assertEquals("us 1", PluralRules.KEYWORD_ONE, rules.select(1));
    477         assertEquals("us 2", PluralRules.KEYWORD_OTHER, rules.select(2));
    478 
    479         rules = factory.forLocale(ULocale.JAPAN);
    480         assertEquals("ja 0", PluralRules.KEYWORD_OTHER, rules.select(0));
    481         assertEquals("ja 1", PluralRules.KEYWORD_OTHER, rules.select(1));
    482         assertEquals("ja 2", PluralRules.KEYWORD_OTHER, rules.select(2));
    483 
    484         rules = factory.forLocale(ULocale.createCanonical("ru"));
    485         assertEquals("ru 0", PluralRules.KEYWORD_MANY, rules.select(0));
    486         assertEquals("ru 1", PluralRules.KEYWORD_ONE, rules.select(1));
    487         assertEquals("ru 2", PluralRules.KEYWORD_FEW, rules.select(2));
    488     }
    489 
    490     @Test
    491     public void testFunctionalEquivalent() {
    492         // spot check
    493         ULocale unknown = ULocale.createCanonical("zz_ZZ");
    494         ULocale un_equiv = PluralRules.getFunctionalEquivalent(unknown, null);
    495         assertEquals("unknown locales have root", ULocale.ROOT, un_equiv);
    496 
    497         ULocale jp_equiv = PluralRules.getFunctionalEquivalent(ULocale.JAPAN, null);
    498         ULocale cn_equiv = PluralRules.getFunctionalEquivalent(ULocale.CHINA, null);
    499         assertEquals("japan and china equivalent locales", jp_equiv, cn_equiv);
    500 
    501         boolean[] available = new boolean[1];
    502         ULocale russia = ULocale.createCanonical("ru_RU");
    503         ULocale ru_ru_equiv = PluralRules.getFunctionalEquivalent(russia, available);
    504         assertFalse("ru_RU not listed", available[0]);
    505 
    506         ULocale russian = ULocale.createCanonical("ru");
    507         ULocale ru_equiv = PluralRules.getFunctionalEquivalent(russian, available);
    508         assertTrue("ru listed", available[0]);
    509         assertEquals("ru and ru_RU equivalent locales", ru_ru_equiv, ru_equiv);
    510     }
    511 
    512     @Test
    513     public void testAvailableULocales() {
    514         ULocale[] locales = factory.getAvailableULocales();
    515         Set localeSet = new HashSet();
    516         localeSet.addAll(Arrays.asList(locales));
    517 
    518         assertEquals("locales are unique in list", locales.length, localeSet.size());
    519     }
    520 
    521     /*
    522      * Test the method public static PluralRules parseDescription(String description)
    523      */
    524     @Test
    525     public void TestParseDescription() {
    526         try {
    527             if (PluralRules.DEFAULT != PluralRules.parseDescription("")) {
    528                 errln("PluralRules.parseDescription(String) was suppose "
    529                         + "to return PluralRules.DEFAULT when String is of " + "length 0.");
    530             }
    531         } catch (ParseException e) {
    532             errln("PluralRules.parseDescription(String) was not suppose " + "to return an exception.");
    533         }
    534     }
    535 
    536     /*
    537      * Tests the method public static PluralRules createRules(String description)
    538      */
    539     @Test
    540     public void TestCreateRules() {
    541         try {
    542             if (PluralRules.createRules(null) != null) {
    543                 errln("PluralRules.createRules(String) was suppose to "
    544                         + "return null for an invalid String descrtiption.");
    545             }
    546         } catch (Exception e) {
    547         }
    548     }
    549 
    550     /*
    551      * Tests the method public int hashCode()
    552      */
    553     @Test
    554     public void TestHashCode() {
    555         // Bad test, breaks whenever PluralRules implementation changes.
    556         // PluralRules pr = PluralRules.DEFAULT;
    557         // if (106069776 != pr.hashCode()) {
    558         // errln("PluralRules.hashCode() was suppose to return 106069776 " + "when PluralRules.DEFAULT.");
    559         // }
    560     }
    561 
    562     /*
    563      * Tests the method public boolean equals(PluralRules rhs)
    564      */
    565     @Test
    566     public void TestEquals() {
    567         PluralRules pr = PluralRules.DEFAULT;
    568 
    569         if (pr.equals((PluralRules) null)) {
    570             errln("PluralRules.equals(PluralRules) was supposed to return false " + "when passing null.");
    571         }
    572     }
    573 
    574     private void assertRuleValue(String rule, double value) {
    575         assertRuleKeyValue("a:" + rule, "a", value);
    576     }
    577 
    578     private void assertRuleKeyValue(String rule, String key, double value) {
    579         PluralRules pr = PluralRules.createRules(rule);
    580         assertEquals(rule, value, pr.getUniqueKeywordValue(key));
    581     }
    582 
    583     /*
    584      * Tests getUniqueKeywordValue()
    585      */
    586     @Test
    587     public void TestGetUniqueKeywordValue() {
    588         assertRuleKeyValue("a: n is 1", "not_defined", PluralRules.NO_UNIQUE_VALUE); // key not defined
    589         assertRuleValue("n within 2..2", 2);
    590         assertRuleValue("n is 1", 1);
    591         assertRuleValue("n in 2..2", 2);
    592         assertRuleValue("n in 3..4", PluralRules.NO_UNIQUE_VALUE);
    593         assertRuleValue("n within 3..4", PluralRules.NO_UNIQUE_VALUE);
    594         assertRuleValue("n is 2 or n is 2", 2);
    595         assertRuleValue("n is 2 and n is 2", 2);
    596         assertRuleValue("n is 2 or n is 3", PluralRules.NO_UNIQUE_VALUE);
    597         assertRuleValue("n is 2 and n is 3", PluralRules.NO_UNIQUE_VALUE);
    598         assertRuleValue("n is 2 or n in 2..3", PluralRules.NO_UNIQUE_VALUE);
    599         assertRuleValue("n is 2 and n in 2..3", 2);
    600         assertRuleKeyValue("a: n is 1", "other", PluralRules.NO_UNIQUE_VALUE); // key matches default rule
    601         assertRuleValue("n in 2,3", PluralRules.NO_UNIQUE_VALUE);
    602         assertRuleValue("n in 2,3..6 and n not in 2..3,5..6", 4);
    603     }
    604 
    605     /**
    606      * The version in PluralFormatUnitTest is not really a test, and it's in the wrong place anyway, so I'm putting a
    607      * variant of it here.
    608      */
    609     @Test
    610     public void TestGetSamples() {
    611         Set<ULocale> uniqueRuleSet = new HashSet<ULocale>();
    612         for (ULocale locale : factory.getAvailableULocales()) {
    613             uniqueRuleSet.add(PluralRules.getFunctionalEquivalent(locale, null));
    614         }
    615         for (ULocale locale : uniqueRuleSet) {
    616             PluralRules rules = factory.forLocale(locale);
    617             logln("\nlocale: " + (locale == ULocale.ROOT ? "root" : locale.toString()) + ", rules: " + rules);
    618             Set<String> keywords = rules.getKeywords();
    619             for (String keyword : keywords) {
    620                 Collection<Double> list = rules.getSamples(keyword);
    621                 logln("keyword: " + keyword + ", samples: " + list);
    622                 // with fractions, the samples can be empty and thus the list null. In that case, however, there will be
    623                 // FixedDecimal values.
    624                 // So patch the test for that.
    625                 if (list.size() == 0) {
    626                     // when the samples (meaning integer samples) are null, then then integerSamples must be, and the
    627                     // decimalSamples must not be
    628                     FixedDecimalSamples integerSamples = rules.getDecimalSamples(keyword, SampleType.INTEGER);
    629                     FixedDecimalSamples decimalSamples = rules.getDecimalSamples(keyword, SampleType.DECIMAL);
    630                     assertTrue(getAssertMessage("List is not null", locale, rules, keyword), integerSamples == null
    631                             && decimalSamples != null && decimalSamples.samples.size() != 0);
    632                 } else {
    633                     if (!assertTrue(getAssertMessage("Test getSamples.isEmpty", locale, rules, keyword),
    634                             !list.isEmpty())) {
    635                         rules.getSamples(keyword);
    636                     }
    637                     if (rules.toString().contains(": j")) {
    638                         // hack until we remove j
    639                     } else {
    640                         for (double value : list) {
    641                             assertEquals(getAssertMessage("Match keyword", locale, rules, keyword) + "; value '"
    642                                     + value + "'", keyword, rules.select(value));
    643                         }
    644                     }
    645                 }
    646             }
    647 
    648             assertNull(locale + ", list is null", rules.getSamples("@#$%^&*"));
    649             assertNull(locale + ", list is null", rules.getSamples("@#$%^&*", SampleType.DECIMAL));
    650         }
    651     }
    652 
    653     public String getAssertMessage(String message, ULocale locale, PluralRules rules, String keyword) {
    654         String ruleString = "";
    655         if (keyword != null) {
    656             if (keyword.equals("other")) {
    657                 for (String keyword2 : rules.getKeywords()) {
    658                     ruleString += " NOR " + rules.getRules(keyword2).split("@")[0];
    659                 }
    660             } else {
    661                 String rule = rules.getRules(keyword);
    662                 ruleString = rule == null ? null : rule.split("@")[0];
    663             }
    664             ruleString = "; rule: '" + keyword + ": " + ruleString + "'";
    665             // !keyword.equals("other") ? "'; keyword: '" + keyword + "'; rule: '" + rules.getRules(keyword) + "'"
    666             // : "'; keyword: '" + keyword + "'; rules: '" + rules.toString() + "'";
    667         }
    668         return message + (locale == null ? "" : "; locale: '" + locale + "'") + ruleString;
    669     }
    670 
    671     /**
    672      * Returns the empty set if the keyword is not defined, null if there are an unlimited number of values for the
    673      * keyword, or the set of values that trigger the keyword.
    674      */
    675     @Test
    676     public void TestGetAllKeywordValues() {
    677         // data is pairs of strings, the rule, and the expected values as arguments
    678         String[] data = {
    679                 "other: ; a: n mod 3 is 0",
    680                 "a: null",
    681                 "a: n in 2..5 and n within 5..8",
    682                 "a: 5",
    683                 "a: n in 2..5",
    684                 "a: 2,3,4,5; other: null",
    685                 "a: n not in 2..5",
    686                 "a: null; other: null",
    687                 "a: n within 2..5",
    688                 "a: 2,3,4,5; other: null",
    689                 "a: n not within 2..5",
    690                 "a: null; other: null",
    691                 "a: n in 2..5 or n within 6..8",
    692                 "a: 2,3,4,5,6,7,8", // ignore 'other' here on out, always null
    693                 "a: n in 2..5 and n within 6..8",
    694                 "a: null",
    695                 // we no longer support 'degenerate' rules
    696                 // "a: n within 2..5 and n within 6..8", "a:", // our sampling catches these
    697                 // "a: n within 2..5 and n within 5..8", "a: 5", // ''
    698                 // "a: n within 1..2 and n within 2..3 or n within 3..4 and n within 4..5", "a: 2,4",
    699                 // "a: n mod 3 is 0 and n within 0..5", "a: 0,3",
    700                 "a: n within 1..2 and n within 2..3 or n within 3..4 and n within 4..5 or n within 5..6 and n within 6..7",
    701                 "a: 2,4,6", // but not this...
    702                 "a: n mod 3 is 0 and n within 1..2", "a: null", "a: n mod 3 is 0 and n within 0..6", "a: 0,3,6",
    703                 "a: n mod 3 is 0 and n in 3..12", "a: 3,6,9,12", "a: n in 2,4..6 and n is not 5", "a: 2,4,6", };
    704         for (int i = 0; i < data.length; i += 2) {
    705             String ruleDescription = data[i];
    706             String result = data[i + 1];
    707 
    708             PluralRules p = PluralRules.createRules(ruleDescription);
    709             if (p == null) { // for debugging
    710                 PluralRules.createRules(ruleDescription);
    711             }
    712             for (String ruleResult : result.split(";")) {
    713                 String[] ruleAndValues = ruleResult.split(":");
    714                 String keyword = ruleAndValues[0].trim();
    715                 String valueList = ruleAndValues.length < 2 ? null : ruleAndValues[1];
    716                 if (valueList != null) {
    717                     valueList = valueList.trim();
    718                 }
    719                 Collection<Double> values;
    720                 if (valueList == null || valueList.length() == 0) {
    721                     values = Collections.EMPTY_SET;
    722                 } else if ("null".equals(valueList)) {
    723                     values = null;
    724                 } else {
    725                     values = new TreeSet<Double>();
    726                     for (String value : valueList.split(",")) {
    727                         values.add(Double.parseDouble(value));
    728                     }
    729                 }
    730 
    731                 Collection<Double> results = p.getAllKeywordValues(keyword);
    732                 assertEquals(keyword + " in " + ruleDescription, values, results == null ? null : new HashSet(results));
    733 
    734                 if (results != null) {
    735                     try {
    736                         results.add(PluralRules.NO_UNIQUE_VALUE);
    737                         fail("returned set is modifiable");
    738                     } catch (UnsupportedOperationException e) {
    739                         // pass
    740                     }
    741                 }
    742             }
    743         }
    744     }
    745 
    746     @Test
    747     public void TestOrdinal() {
    748         PluralRules pr = factory.forLocale(ULocale.ENGLISH, PluralType.ORDINAL);
    749         assertEquals("PluralRules(en-ordinal).select(2)", "two", pr.select(2));
    750     }
    751 
    752     @Test
    753     public void TestBasicFraction() {
    754         String[][] tests = { { "en", "one: j is 1" }, { "1", "0", "1", "one" }, { "1", "2", "1.00", "other" }, };
    755         ULocale locale = null;
    756         NumberFormat nf = null;
    757         PluralRules pr = null;
    758 
    759         for (String[] row : tests) {
    760             switch (row.length) {
    761             case 2:
    762                 locale = ULocale.forLanguageTag(row[0]);
    763                 nf = NumberFormat.getInstance(locale);
    764                 pr = PluralRules.createRules(row[1]);
    765                 break;
    766             case 4:
    767                 double n = Double.parseDouble(row[0]);
    768                 int minFracDigits = Integer.parseInt(row[1]);
    769                 nf.setMinimumFractionDigits(minFracDigits);
    770                 String expectedFormat = row[2];
    771                 String expectedKeyword = row[3];
    772 
    773                 UFieldPosition pos = new UFieldPosition();
    774                 String formatted = nf.format(1.0, new StringBuffer(), pos).toString();
    775                 int countVisibleFractionDigits = pos.getCountVisibleFractionDigits();
    776                 long fractionDigits = pos.getFractionDigits();
    777                 String keyword = pr.select(n, countVisibleFractionDigits, fractionDigits);
    778                 assertEquals("Formatted " + n + "\t" + minFracDigits, expectedFormat, formatted);
    779                 assertEquals("Keyword " + n + "\t" + minFracDigits, expectedKeyword, keyword);
    780                 break;
    781             default:
    782                 throw new RuntimeException();
    783             }
    784         }
    785     }
    786 
    787     @Test
    788     public void TestLimitedAndSamplesConsistency() {
    789         for (ULocale locale : PluralRules.getAvailableULocales()) {
    790             ULocale loc2 = PluralRules.getFunctionalEquivalent(locale, null);
    791             if (!loc2.equals(locale)) {
    792                 continue; // only need "unique" rules
    793             }
    794             for (PluralType type : PluralType.values()) {
    795                 PluralRules rules = PluralRules.forLocale(locale, type);
    796                 for (SampleType sampleType : SampleType.values()) {
    797                     if (type == PluralType.ORDINAL) {
    798                         logKnownIssue("10783", "Fix issues with isLimited vs computeLimited on ordinals");
    799                         continue;
    800                     }
    801                     for (String keyword : rules.getKeywords()) {
    802                         boolean isLimited = rules.isLimited(keyword, sampleType);
    803                         boolean computeLimited = rules.computeLimited(keyword, sampleType);
    804                         if (!keyword.equals("other")) {
    805                             assertEquals(getAssertMessage("computeLimited == isLimited", locale, rules, keyword),
    806                                     computeLimited, isLimited);
    807                         }
    808                         Collection<Double> samples = rules.getSamples(keyword, sampleType);
    809                         assertNotNull(getAssertMessage("Samples must not be null", locale, rules, keyword), samples);
    810                         /* FixedDecimalSamples decimalSamples = */rules.getDecimalSamples(keyword, sampleType);
    811                         // assertNotNull(getAssertMessage("Decimal samples must be null if unlimited", locale, rules,
    812                         // keyword), decimalSamples);
    813                     }
    814                 }
    815             }
    816         }
    817     }
    818 
    819     @Test
    820     public void TestKeywords() {
    821         Set<String> possibleKeywords = new LinkedHashSet(Arrays.asList("zero", "one", "two", "few", "many", "other"));
    822         Object[][][] tests = {
    823                 // format is locale, explicits, then triples of keyword, status, unique value.
    824                 { { "en", null }, { "one", KeywordStatus.UNIQUE, 1.0d }, { "other", KeywordStatus.UNBOUNDED, null } },
    825                 { { "pl", null }, { "one", KeywordStatus.UNIQUE, 1.0d }, { "few", KeywordStatus.UNBOUNDED, null },
    826                         { "many", KeywordStatus.UNBOUNDED, null },
    827                         { "other", KeywordStatus.SUPPRESSED, null, KeywordStatus.UNBOUNDED, null } // note that it is
    828                                                                                                    // suppressed in
    829                                                                                                    // INTEGER but not
    830                                                                                                    // DECIMAL
    831                 }, { { "en", new HashSet<Double>(Arrays.asList(1.0d)) }, // check that 1 is suppressed
    832                         { "one", KeywordStatus.SUPPRESSED, null }, { "other", KeywordStatus.UNBOUNDED, null } }, };
    833         Output<Double> uniqueValue = new Output<Double>();
    834         for (Object[][] test : tests) {
    835             ULocale locale = new ULocale((String) test[0][0]);
    836             // NumberType numberType = (NumberType) test[1];
    837             Set<Double> explicits = (Set<Double>) test[0][1];
    838             PluralRules pluralRules = factory.forLocale(locale);
    839             LinkedHashSet<String> remaining = new LinkedHashSet(possibleKeywords);
    840             for (int i = 1; i < test.length; ++i) {
    841                 Object[] row = test[i];
    842                 String keyword = (String) row[0];
    843                 KeywordStatus statusExpected = (KeywordStatus) row[1];
    844                 Double uniqueExpected = (Double) row[2];
    845                 remaining.remove(keyword);
    846                 KeywordStatus status = pluralRules.getKeywordStatus(keyword, 0, explicits, uniqueValue);
    847                 assertEquals(getAssertMessage("Unique Value", locale, pluralRules, keyword), uniqueExpected,
    848                         uniqueValue.value);
    849                 assertEquals(getAssertMessage("Keyword Status", locale, pluralRules, keyword), statusExpected, status);
    850                 if (row.length > 3) {
    851                     statusExpected = (KeywordStatus) row[3];
    852                     uniqueExpected = (Double) row[4];
    853                     status = pluralRules.getKeywordStatus(keyword, 0, explicits, uniqueValue, SampleType.DECIMAL);
    854                     assertEquals(getAssertMessage("Unique Value - decimal", locale, pluralRules, keyword),
    855                             uniqueExpected, uniqueValue.value);
    856                     assertEquals(getAssertMessage("Keyword Status - decimal", locale, pluralRules, keyword),
    857                             statusExpected, status);
    858                 }
    859             }
    860             for (String keyword : remaining) {
    861                 KeywordStatus status = pluralRules.getKeywordStatus(keyword, 0, null, uniqueValue);
    862                 assertEquals("Invalid keyword " + keyword, status, KeywordStatus.INVALID);
    863                 assertNull("Invalid keyword " + keyword, uniqueValue.value);
    864             }
    865         }
    866     }
    867 
    868     enum StandardPluralCategories {
    869         zero, one, two, few, many, other;
    870         /**
    871          *
    872          */
    873         private static final Set<StandardPluralCategories> ALL = Collections.unmodifiableSet(EnumSet
    874                 .allOf(StandardPluralCategories.class));
    875 
    876         /**
    877          * Return a mutable set
    878          *
    879          * @param source
    880          * @return
    881          */
    882         static final EnumSet<StandardPluralCategories> getSet(Collection<String> source) {
    883             EnumSet<StandardPluralCategories> result = EnumSet.noneOf(StandardPluralCategories.class);
    884             for (String s : source) {
    885                 result.add(StandardPluralCategories.valueOf(s));
    886             }
    887             return result;
    888         }
    889 
    890         static final Comparator<Set<StandardPluralCategories>> SHORTEST_FIRST = new Comparator<Set<StandardPluralCategories>>() {
    891             @Override
    892             public int compare(Set<StandardPluralCategories> arg0, Set<StandardPluralCategories> arg1) {
    893                 int diff = arg0.size() - arg1.size();
    894                 if (diff != 0) {
    895                     return diff;
    896                 }
    897                 // otherwise first...
    898                 // could be optimized, but we don't care here.
    899                 for (StandardPluralCategories value : ALL) {
    900                     if (arg0.contains(value)) {
    901                         if (!arg1.contains(value)) {
    902                             return 1;
    903                         }
    904                     } else if (arg1.contains(value)) {
    905                         return -1;
    906                     }
    907 
    908                 }
    909                 return 0;
    910             }
    911 
    912         };
    913     }
    914 
    915     @Test
    916     public void TestLocales() {
    917         if (false) {
    918             generateLOCALE_SNAPSHOT();
    919         }
    920         for (String test : LOCALE_SNAPSHOT) {
    921             test = test.trim();
    922             String[] parts = test.split("\\s*;\\s*");
    923             for (String localeString : parts[0].split("\\s*,\\s*")) {
    924                 ULocale locale = new ULocale(localeString);
    925                 if (factory.hasOverride(locale)) {
    926                     continue; // skip for now
    927                 }
    928                 PluralRules rules = factory.forLocale(locale);
    929                 for (int i = 1; i < parts.length; ++i) {
    930                     checkCategoriesAndExpected(localeString, parts[i], rules);
    931                 }
    932             }
    933         }
    934     }
    935 
    936     private static final Comparator<PluralRules> PLURAL_RULE_COMPARATOR = new Comparator<PluralRules>() {
    937         @Override
    938         public int compare(PluralRules o1, PluralRules o2) {
    939             return o1.compareTo(o2);
    940         }
    941     };
    942 
    943     private void generateLOCALE_SNAPSHOT() {
    944         Comparator c = new CollectionUtilities.CollectionComparator<Comparable>();
    945         Relation<Set<StandardPluralCategories>, PluralRules> setsToRules = Relation.of(
    946                 new TreeMap<Set<StandardPluralCategories>, Set<PluralRules>>(c), TreeSet.class, PLURAL_RULE_COMPARATOR);
    947         Relation<PluralRules, ULocale> data = Relation.of(
    948                 new TreeMap<PluralRules, Set<ULocale>>(PLURAL_RULE_COMPARATOR), TreeSet.class);
    949         for (ULocale locale : PluralRules.getAvailableULocales()) {
    950             PluralRules pr = PluralRules.forLocale(locale);
    951             EnumSet<StandardPluralCategories> set = getCanonicalSet(pr.getKeywords());
    952             setsToRules.put(set, pr);
    953             data.put(pr, locale);
    954         }
    955         for (Entry<Set<StandardPluralCategories>, Set<PluralRules>> entry1 : setsToRules.keyValuesSet()) {
    956             Set<StandardPluralCategories> set = entry1.getKey();
    957             Set<PluralRules> rules = entry1.getValue();
    958             System.out.println("\n        // " + set);
    959             for (PluralRules rule : rules) {
    960                 Set<ULocale> locales = data.get(rule);
    961                 System.out.print("        \"" + CollectionUtilities.join(locales, ","));
    962                 for (StandardPluralCategories spc : set) {
    963                     String keyword = spc.toString();
    964                     FixedDecimalSamples samples = rule.getDecimalSamples(keyword, SampleType.INTEGER);
    965                     System.out.print("; " + spc + ": " + samples);
    966                 }
    967                 System.out.println("\",");
    968             }
    969         }
    970     }
    971 
    972     /**
    973      * @param keywords
    974      * @return
    975      */
    976     private EnumSet<StandardPluralCategories> getCanonicalSet(Set<String> keywords) {
    977         EnumSet<StandardPluralCategories> result = EnumSet.noneOf(StandardPluralCategories.class);
    978         for (String s : keywords) {
    979             result.add(StandardPluralCategories.valueOf(s));
    980         }
    981         return result;
    982     }
    983 
    984     static final String[] LOCALE_SNAPSHOT = {
    985             // [other]
    986             "bm,bo,dz,id,ig,ii,in,ja,jbo,jv,jw,kde,kea,km,ko,lkt,lo,ms,my,nqo,root,sah,ses,sg,th,to,vi,wo,yo,zh; other: @integer 0~15, 100, 1000, 10000, 100000, 1000000, ",
    987 
    988             // [one, other]
    989             "am,bn,fa,gu,hi,kn,mr,zu; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, ",
    990             "ff,fr,hy,kab; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, ",
    991             "ast,ca,de,en,et,fi,fy,gl,it,ji,nl,sv,sw,ur,yi; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, ",
    992             "pt; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, ",
    993             "si; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, ",
    994             "ak,bh,guw,ln,mg,nso,pa,ti,wa; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, ",
    995             "tzm; one: @integer 0, 1, 11~24; other: @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, ",
    996             "af,asa,az,bem,bez,bg,brx,cgg,chr,ckb,dv,ee,el,eo,es,eu,fo,fur,gsw,ha,haw,hu,jgo,jmc,ka,kaj,kcg,kk,kkj,kl,ks,ksb,ku,ky,lb,lg,mas,mgo,ml,mn,nah,nb,nd,ne,nn,nnh,no,nr,ny,nyn,om,or,os,pap,ps,rm,rof,rwk,saq,seh,sn,so,sq,ss,ssy,st,syr,ta,te,teo,tig,tk,tn,tr,ts,ug,uz,ve,vo,vun,wae,xh,xog; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, ",
    997             "pt_PT; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, ",
    998             "da; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, ",
    999             "is; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, ; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, ",
   1000             "mk; one: @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, ; other: @integer 0, 2~10, 12~17, 100, 1000, 10000, 100000, 1000000, ",
   1001             "fil,tl; one: @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, ; other: @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, ",
   1002 
   1003             // [zero, one, other]
   1004             "lag; zero: @integer 0; one: @integer 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, ",
   1005             "lv,prg; zero: @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, ; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, ; other: @integer 2~9, 22~29, 102, 1002, ",
   1006             "ksh; zero: @integer 0; one: @integer 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, ",
   1007 
   1008             // [one, two, other]
   1009             "iu,kw,naq,se,sma,smi,smj,smn,sms; one: @integer 1; two: @integer 2; other: @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, ",
   1010 
   1011             // [one, few, other]
   1012             "shi; one: @integer 0, 1; few: @integer 2~10; other: @integer 11~26, 100, 1000, 10000, 100000, 1000000, ",
   1013             "mo,ro; one: @integer 1; few: @integer 0, 2~16, 101, 1001, ; other: @integer 20~35, 100, 1000, 10000, 100000, 1000000, ",
   1014             "bs,hr,sh,sr; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, ; few: @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, ; other: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, ",
   1015 
   1016             // [one, two, few, other]
   1017             "gd; one: @integer 1, 11; two: @integer 2, 12; few: @integer 3~10, 13~19; other: @integer 0, 20~34, 100, 1000, 10000, 100000, 1000000, ",
   1018             "sl; one: @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, ; two: @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, ; few: @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, ; other: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, ",
   1019 
   1020             // [one, two, many, other]
   1021             "he,iw; one: @integer 1; two: @integer 2; many: @integer 20, 30, 40, 50, 60, 70, 80, 90, 100, 1000, 10000, 100000, 1000000, ; other: @integer 0, 3~17, 101, 1001, ",
   1022 
   1023             // [one, few, many, other]
   1024             "cs,sk; one: @integer 1; few: @integer 2~4; many: null; other: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, ",
   1025             "be; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, ; few: @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, ; many: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, ; other: null",
   1026             "lt; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, ; few: @integer 2~9, 22~29, 102, 1002, ; many: null; other: @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, ",
   1027             "mt; one: @integer 1; few: @integer 0, 2~10, 102~107, 1002, ; many: @integer 11~19, 111~117, 1011, ; other: @integer 20~35, 100, 1000, 10000, 100000, 1000000, ",
   1028             "pl; one: @integer 1; few: @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, ; many: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, ; other: null",
   1029             "ru,uk; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, ; few: @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, ; many: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, ; other: null",
   1030 
   1031             // [one, two, few, many, other]
   1032             "br; one: @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, ; two: @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, ; few: @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, ; many: @integer 1000000, ; other: @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, ",
   1033             "ga; one: @integer 1; two: @integer 2; few: @integer 3~6; many: @integer 7~10; other: @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, ",
   1034             "gv; one: @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, ; two: @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, ; few: @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, ; many: null; other: @integer 3~10, 13~19, 23, 103, 1003, ",
   1035 
   1036             // [zero, one, two, few, many, other]
   1037             "ar; zero: @integer 0; one: @integer 1; two: @integer 2; few: @integer 3~10, 103~110, 1003, ; many: @integer 11~26, 111, 1011, ; other: @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, ",
   1038             "cy; zero: @integer 0; one: @integer 1; two: @integer 2; few: @integer 3; many: @integer 6; other: @integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, ", };
   1039 
   1040     private <T extends Serializable> T serializeAndDeserialize(T original, Output<Integer> size) {
   1041         try {
   1042             ByteArrayOutputStream baos = new ByteArrayOutputStream();
   1043             ObjectOutputStream ostream = new ObjectOutputStream(baos);
   1044             ostream.writeObject(original);
   1045             ostream.flush();
   1046             byte bytes[] = baos.toByteArray();
   1047             size.value = bytes.length;
   1048             ObjectInputStream istream = new ObjectInputStream(new ByteArrayInputStream(bytes));
   1049             T reconstituted = (T) istream.readObject();
   1050             return reconstituted;
   1051         } catch (IOException e) {
   1052             throw new RuntimeException(e);
   1053         } catch (ClassNotFoundException e) {
   1054             throw new RuntimeException(e);
   1055         }
   1056     }
   1057 
   1058     @Test
   1059     public void TestSerialization() {
   1060         Output<Integer> size = new Output<Integer>();
   1061         int max = 0;
   1062         for (ULocale locale : PluralRules.getAvailableULocales()) {
   1063             PluralRules item = PluralRules.forLocale(locale);
   1064             PluralRules item2 = serializeAndDeserialize(item, size);
   1065             logln(locale + "\tsize:\t" + size.value);
   1066             max = Math.max(max, size.value);
   1067             if (!assertEquals(locale + "\tPlural rules before and after serialization", item, item2)) {
   1068                 // for debugging
   1069                 PluralRules item3 = serializeAndDeserialize(item, size);
   1070                 item.equals(item3);
   1071             }
   1072         }
   1073         logln("max \tsize:\t" + max);
   1074     }
   1075 
   1076     public static class FixedDecimalHandler implements SerializableTestUtility.Handler {
   1077         @Override
   1078         public Object[] getTestObjects() {
   1079             FixedDecimal items[] = { new FixedDecimal(3d), new FixedDecimal(3d, 2), new FixedDecimal(3.1d, 1),
   1080                     new FixedDecimal(3.1d, 2), };
   1081             return items;
   1082         }
   1083 
   1084         @Override
   1085         public boolean hasSameBehavior(Object a, Object b) {
   1086             FixedDecimal a1 = (FixedDecimal) a;
   1087             FixedDecimal b1 = (FixedDecimal) b;
   1088             return a1.equals(b1);
   1089         }
   1090     }
   1091 
   1092     @Test
   1093     public void TestSerial() {
   1094         PluralRules s = PluralRules.forLocale(ULocale.ENGLISH);
   1095         checkStreamingEquality(s);
   1096     }
   1097 
   1098     public void checkStreamingEquality(PluralRules s) {
   1099         try {
   1100             ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
   1101             ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOut);
   1102             objectOutputStream.writeObject(s);
   1103             objectOutputStream.close();
   1104             byte[] contents = byteOut.toByteArray();
   1105             logln(s.getClass() + ": " + showBytes(contents));
   1106             ByteArrayInputStream byteIn = new ByteArrayInputStream(contents);
   1107             ObjectInputStream objectInputStream = new ObjectInputStream(byteIn);
   1108             Object obj = objectInputStream.readObject();
   1109             assertEquals("Streamed Object equals ", s, obj);
   1110         } catch (Exception e) {
   1111             assertNull("TestSerial", e);
   1112         }
   1113     }
   1114 
   1115     /**
   1116      * @param contents
   1117      * @return
   1118      */
   1119     private String showBytes(byte[] contents) {
   1120         StringBuilder b = new StringBuilder('[');
   1121         for (int i = 0; i < contents.length; ++i) {
   1122             int item = contents[i] & 0xFF;
   1123             if (item >= 0x20 && item <= 0x7F) {
   1124                 b.append((char) item);
   1125             } else {
   1126                 b.append('(').append(Utility.hex(item, 2)).append(')');
   1127             }
   1128         }
   1129         return b.append(']').toString();
   1130     }
   1131 
   1132     @Test
   1133     public void testJavaLocaleFactory() {
   1134         PluralRules rulesU0 = PluralRules.forLocale(ULocale.FRANCE);
   1135         PluralRules rulesJ0 = PluralRules.forLocale(Locale.FRANCE);
   1136         assertEquals("forLocale()", rulesU0, rulesJ0);
   1137 
   1138         PluralRules rulesU1 = PluralRules.forLocale(ULocale.FRANCE, PluralType.ORDINAL);
   1139         PluralRules rulesJ1 = PluralRules.forLocale(Locale.FRANCE, PluralType.ORDINAL);
   1140         assertEquals("forLocale() with type", rulesU1, rulesJ1);
   1141     }
   1142 }
   1143