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