• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html
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.number.FormattedNumber;
45 import com.ibm.icu.number.FormattedNumberRange;
46 import com.ibm.icu.number.LocalizedNumberFormatter;
47 import com.ibm.icu.number.NumberFormatter;
48 import com.ibm.icu.number.NumberRangeFormatter;
49 import com.ibm.icu.number.Precision;
50 import com.ibm.icu.number.UnlocalizedNumberFormatter;
51 import com.ibm.icu.text.NumberFormat;
52 import com.ibm.icu.text.PluralRules;
53 import com.ibm.icu.text.PluralRules.FixedDecimal;
54 import com.ibm.icu.text.PluralRules.FixedDecimalRange;
55 import com.ibm.icu.text.PluralRules.FixedDecimalSamples;
56 import com.ibm.icu.text.PluralRules.KeywordStatus;
57 import com.ibm.icu.text.PluralRules.PluralType;
58 import com.ibm.icu.text.PluralRules.SampleType;
59 import com.ibm.icu.text.UFieldPosition;
60 import com.ibm.icu.util.Output;
61 import com.ibm.icu.util.ULocale;
62 
63 /**
64  * @author dougfelt (Doug Felt)
65  * @author markdavis (Mark Davis) [for fractional support]
66  */
67 @RunWith(JUnit4.class)
68 public class PluralRulesTest extends TestFmwk {
69 
70     PluralRulesFactory factory = PluralRulesFactory.NORMAL;
71 
72     @Test
testOverUnderflow()73     public void testOverUnderflow() {
74         logln(String.valueOf(Long.MAX_VALUE + 1d));
75         for (double[] testDouble : new double[][] {
76                 { 1E18, 0, 0, 1E18 }, // check overflow
77                 { 10000000000000.1d, 1, 1, 10000000000000d }, { -0.00001d, 1, 5, 0 }, { 1d, 0, 0, 1 },
78                 { 1.1d, 1, 1, 1 }, { 12345d, 0, 0, 12345 }, { 12345.678912d, 678912, 6, 12345 },
79                 { 12345.6789123d, 678912, 6, 12345 }, // we only go out 6 digits
80                 { 1E18, 0, 0, 1E18 }, // check overflow
81                 { 1E19, 0, 0, 1E18 }, // check overflow
82         }) {
83             FixedDecimal fd = new FixedDecimal(testDouble[0]);
84             assertEquals(testDouble[0] + "=doubleValue()", testDouble[0], fd.doubleValue());
85             assertEquals(testDouble[0] + " decimalDigits", (int) testDouble[1], fd.getDecimalDigits());
86             assertEquals(testDouble[0] + " visibleDecimalDigitCount", (int) testDouble[2], fd.getVisibleDecimalDigitCount());
87             assertEquals(testDouble[0] + " decimalDigitsWithoutTrailingZeros", (int) testDouble[1],
88                     fd.getDecimalDigitsWithoutTrailingZeros());
89             assertEquals(testDouble[0] + " visibleDecimalDigitCountWithoutTrailingZeros", (int) testDouble[2],
90                     fd.getVisibleDecimalDigitCountWithoutTrailingZeros());
91             assertEquals(testDouble[0] + " integerValue", (long) testDouble[3], fd.getIntegerValue());
92         }
93 
94         for (ULocale locale : new ULocale[] { ULocale.ENGLISH, new ULocale("cy"), new ULocale("ar") }) {
95             PluralRules rules = factory.forLocale(locale);
96 
97             assertEquals(locale + " NaN", "other", rules.select(Double.NaN));
98             assertEquals(locale + " ∞", "other", rules.select(Double.POSITIVE_INFINITY));
99             assertEquals(locale + " -∞", "other", rules.select(Double.NEGATIVE_INFINITY));
100         }
101     }
102 
103     @Test
testSyntaxRestrictions()104     public void testSyntaxRestrictions() {
105         Object[][] shouldFail = {
106                 { "a:n in 3..10,13..19" },
107 
108                 // = and != always work
109                 { "a:n=1" },
110                 { "a:n=1,3" },
111                 { "a:n!=1" },
112                 { "a:n!=1,3" },
113 
114                 // with spacing
115                 { "a: n = 1" },
116                 { "a: n = 1, 3" },
117                 { "a: n != 1" },
118                 { "a: n != 1, 3" },
119                 { "a: n ! = 1" },
120                 { "a: n ! = 1, 3" },
121                 { "a: n = 1 , 3" },
122                 { "a: n != 1 , 3" },
123                 { "a: n ! = 1 , 3" },
124                 { "a: n = 1 .. 3" },
125                 { "a: n != 1 .. 3" },
126                 { "a: n ! = 1 .. 3" },
127 
128                 // more complicated
129                 { "a:n in 3 .. 10 , 13 .. 19" },
130 
131                 // singles have special exceptions
132                 { "a: n is 1" },
133                 { "a: n is not 1" },
134                 { "a: n not is 1", ParseException.class }, // hacked to fail
135                 { "a: n in 1" },
136                 { "a: n not in 1" },
137 
138                 // multiples also have special exceptions
139                 // TODO enable the following once there is an update to CLDR
140                 // {"a: n is 1,3", ParseException.class},
141                 { "a: n is not 1,3", ParseException.class }, // hacked to fail
142                 { "a: n not is 1,3", ParseException.class }, // hacked to fail
143                 { "a: n in 1,3" },
144                 { "a: n not in 1,3" },
145 
146                 // disallow not with =
147                 { "a: n not= 1", ParseException.class }, // hacked to fail
148                 { "a: n not= 1,3", ParseException.class }, // hacked to fail
149 
150                 // disallow double negatives
151                 { "a: n ! is not 1", ParseException.class },
152                 { "a: n ! is not 1", ParseException.class },
153                 { "a: n not not in 1", ParseException.class },
154                 { "a: n is not not 1", NumberFormatException.class },
155 
156                 // disallow screwy cases
157                 { null, NullPointerException.class }, { "djkl;", ParseException.class },
158                 { "a: n = 1 .", ParseException.class }, { "a: n = 1 ..", ParseException.class },
159                 { "a: n = 1 2", ParseException.class }, { "a: n = 1 ,", ParseException.class },
160                 { "a:n in 3 .. 10 , 13 .. 19 ,", ParseException.class }, };
161         for (Object[] shouldFailTest : shouldFail) {
162             String rules = (String) shouldFailTest[0];
163             Class exception = shouldFailTest.length < 2 ? null : (Class) shouldFailTest[1];
164             Class actualException = null;
165             try {
166                 PluralRules.parseDescription(rules);
167             } catch (Exception e) {
168                 actualException = e.getClass();
169             }
170             assertEquals("Exception " + rules, exception, actualException);
171         }
172     }
173 
174     @Test
175     public void testSamples() {
176         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, …";
177         PluralRules test = PluralRules.createRules(description);
178 
179         checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 3, 19", true,
180                 new FixedDecimal(3));
181         checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 3.50~3.53, …", false,
182                 new FixedDecimal(3.5, 2));
183         checkOldSamples(description, test, "one", SampleType.INTEGER, 3d, 19d);
184         checkOldSamples(description, test, "one", SampleType.DECIMAL, 3.5d, 3.51d, 3.52d, 3.53d);
185 
186         checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "", true, null);
187         checkNewSamples(description, test, "other", PluralRules.SampleType.DECIMAL, "@decimal 99.0~99.2, 999.0, …",
188                 false, new FixedDecimal(99d, 1));
189         checkOldSamples(description, test, "other", SampleType.INTEGER);
190         checkOldSamples(description, test, "other", SampleType.DECIMAL, 99d, 99.1, 99.2d, 999d);
191     }
192 
193     /**
194      * This test is for the support of X.YeZ scientific notation of numbers in
195      * the plural sample string.
196      */
197     @Test
198     public void testSamplesWithExponent() {
199         String description = "one: i = 0,1 @integer 0, 1, 1e5 @decimal 0.0~1.5, 1.1e5; "
200                 + "many: e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5"
201                 + " @integer 1000000, 2e6, 3e6, 4e6, 5e6, 6e6, 7e6, … @decimal 2.1e6, 3.1e6, 4.1e6, 5.1e6, 6.1e6, 7.1e6, …; "
202                 + "other:  @integer 2~17, 100, 1000, 10000, 100000, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, …"
203                 + " @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1e5, 3.1e5, 4.1e5, 5.1e5, 6.1e5, 7.1e5, …"
204                 ;
205         // Creating the PluralRules object means being able to parse numbers
206         // like 1e5 and 1.1e5
207         PluralRules test = PluralRules.createRules(description);
208         checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 0, 1, 1e5", true,
209                 new FixedDecimal(0));
210         checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 0.0~1.5, 1.1e5", true,
211                 new FixedDecimal(0, 1));
212         checkNewSamples(description, test, "many", PluralRules.SampleType.INTEGER, "@integer 1000000, 2e6, 3e6, 4e6, 5e6, 6e6, 7e6, …", false,
213                 new FixedDecimal(1000000));
214         checkNewSamples(description, test, "many", PluralRules.SampleType.DECIMAL, "@decimal 2.1e6, 3.1e6, 4.1e6, 5.1e6, 6.1e6, 7.1e6, …", false,
215                 FixedDecimal.createWithExponent(2.1, 1, 6));
216         checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "@integer 2~17, 100, 1000, 10000, 100000, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, …", false,
217                 new FixedDecimal(2));
218         checkNewSamples(description, test, "other", PluralRules.SampleType.DECIMAL, "@decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1e5, 3.1e5, 4.1e5, 5.1e5, 6.1e5, 7.1e5, …", false,
219                 new FixedDecimal(2.0, 1));
220     }
221 
222     /**
223      * This test is for the support of X.YcZ compactnotation of numbers in
224      * the plural sample string.
225      */
226     @Test
227     public void testSamplesWithCompactNotation() {
228         String description = "one: i = 0,1 @integer 0, 1, 1c5 @decimal 0.0~1.5, 1.1c5; "
229                 + "many: c = 0 and i != 0 and i % 1000000 = 0 and v = 0 or c != 0..5"
230                 + " @integer 1000000, 2c6, 3c6, 4c6, 5c6, 6c6, 7c6, … @decimal 2.1c6, 3.1c6, 4.1c6, 5.1c6, 6.1c6, 7.1c6, …; "
231                 + "other:  @integer 2~17, 100, 1000, 10000, 100000, 2c5, 3c5, 4c5, 5c5, 6c5, 7c5, …"
232                 + " @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1c5, 3.1c5, 4.1c5, 5.1c5, 6.1c5, 7.1c5, …"
233                 ;
234         // Creating the PluralRules object means being able to parse numbers
235         // like 1c5 and 1.1c5.
236         // Note: Since `c` is currently an alias to `e`, the toString() of
237         // FixedDecimal will return "1e5" even when input is "1c5".
238         PluralRules test = PluralRules.createRules(description);
239         checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 0, 1, 1e5", true,
240                 new FixedDecimal(0));
241         checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 0.0~1.5, 1.1e5", true,
242                 new FixedDecimal(0, 1));
243         checkNewSamples(description, test, "many", PluralRules.SampleType.INTEGER, "@integer 1000000, 2e6, 3e6, 4e6, 5e6, 6e6, 7e6, …", false,
244                 new FixedDecimal(1000000));
245         checkNewSamples(description, test, "many", PluralRules.SampleType.DECIMAL, "@decimal 2.1e6, 3.1e6, 4.1e6, 5.1e6, 6.1e6, 7.1e6, …", false,
246                 FixedDecimal.createWithExponent(2.1, 1, 6));
247         checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "@integer 2~17, 100, 1000, 10000, 100000, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, …", false,
248                 new FixedDecimal(2));
249         checkNewSamples(description, test, "other", PluralRules.SampleType.DECIMAL, "@decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1e5, 3.1e5, 4.1e5, 5.1e5, 6.1e5, 7.1e5, …", false,
250                 new FixedDecimal(2.0, 1));
251     }
252 
253     public void checkOldSamples(String description, PluralRules rules, String keyword, SampleType sampleType,
254             Double... expected) {
255         Collection<Double> oldSamples = rules.getSamples(keyword, sampleType);
256         if (!assertEquals("getOldSamples; " + keyword + "; " + description, new HashSet(Arrays.asList(expected)),
257                 oldSamples)) {
258             rules.getSamples(keyword, sampleType);
259         }
260     }
261 
262     public void checkNewSamples(String description, PluralRules test, String keyword, SampleType sampleType,
263             String samplesString, boolean isBounded, FixedDecimal firstInRange) {
264         String title = description + ", " + sampleType;
265         FixedDecimalSamples samples = test.getDecimalSamples(keyword, sampleType);
266         if (samples != null) {
267             assertEquals("samples; " + title, samplesString, samples.toString());
268             assertEquals("bounded; " + title, isBounded, samples.bounded);
269             assertEquals("first; " + title, firstInRange, samples.samples.iterator().next().start);
270         }
271         assertEquals("limited: " + title, isBounded, test.isLimited(keyword, sampleType));
272     }
273 
274     private static final String[] parseTestData = { "a: n is 1", "a:1", "a: n mod 10 is 2", "a:2,12,22",
275             "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",
276             "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",
277             "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",
278             "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",
279             "a: n mod 10 in 2..3 or n mod 10 is 5", "a:2,3,5,12,13,15,22,23,25",
280             "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",
281             "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",
282             "a: n mod 2 is 1 and n is not 3 and n within 1..11", "a:1,5,7,9,11",
283             "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",
284             "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",
285             "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",
286             "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",
287             "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",
288             "a:2,3,5,6,7,13,15,16,17", "a: n in 2..6,3..7", "a:2,3,4,5,6,7", };
289 
290     private String[] getTargetStrings(String targets) {
291         List list = new ArrayList(50);
292         String[] valSets = Utility.split(targets, ';');
293         for (int i = 0; i < valSets.length; ++i) {
294             String[] temp = Utility.split(valSets[i], ':');
295             String key = temp[0].trim();
296             String[] vals = Utility.split(temp[1], ',');
297             for (int j = 0; j < vals.length; ++j) {
298                 String valString = vals[j].trim();
299                 int val = Integer.parseInt(valString);
300                 while (list.size() <= val) {
301                     list.add(null);
302                 }
303                 if (list.get(val) != null) {
304                     fail("test data error, key: " + list.get(val) + " already set for: " + val);
305                 }
306                 list.set(val, key);
307             }
308         }
309 
310         String[] result = (String[]) list.toArray(new String[list.size()]);
311         for (int i = 0; i < result.length; ++i) {
312             if (result[i] == null) {
313                 result[i] = "other";
314             }
315         }
316         return result;
317     }
318 
319     private void checkTargets(PluralRules rules, String[] targets) {
320         for (int i = 0; i < targets.length; ++i) {
321             assertEquals("value " + i, targets[i], rules.select(i));
322         }
323     }
324 
325     @Test
326     public void testParseEmpty() throws ParseException {
327         PluralRules rules = PluralRules.parseDescription("a:n");
328         assertEquals("empty", "a", rules.select(0));
329     }
330 
331     @Test
332     public void testParsing() {
333         for (int i = 0; i < parseTestData.length; i += 2) {
334             String pattern = parseTestData[i];
335             String expected = parseTestData[i + 1];
336 
337             logln("pattern[" + i + "] " + pattern);
338             try {
339                 PluralRules rules = PluralRules.createRules(pattern);
340                 String[] targets = getTargetStrings(expected);
341                 checkTargets(rules, targets);
342             } catch (Exception e) {
343                 e.printStackTrace();
344                 throw new RuntimeException(e.getMessage());
345             }
346         }
347     }
348 
349     private static String[][] operandTestData = { { "a: n 3", "FAIL" },
350             { "a: n=1,2; b: n != 3..5; c:n!=5", "a:1,2; b:6,7; c:3,4" },
351             { "a: n=1,2; b: n!=3..5; c:n!=5", "a:1,2; b:6,7; c:3,4" },
352             { "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" },
353             { "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" },
354             { "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
355                                                                                                                     // rules
356             { "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
357                                                                                                           // visible
358                                                                                                           // digit
359             { "one: j is 0", "one: 0; other: 0.0, 1.0, 3" }, // Last visible digit
360     // one → n is 1; few → n in 2..4;
361     };
362 
363     @Test
364     public void testOperands() {
365         for (String[] pair : operandTestData) {
366             String pattern = pair[0].trim();
367             String categoriesAndExpected = pair[1].trim();
368 
369             // logln("pattern[" + i + "] " + pattern);
370             boolean FAIL_EXPECTED = categoriesAndExpected.equalsIgnoreCase("fail");
371             try {
372                 logln(pattern);
373                 PluralRules rules = PluralRules.createRules(pattern);
374                 if (FAIL_EXPECTED) {
375                     assertNull("Should fail with 'null' return.", rules);
376                 } else {
377                     logln(rules == null ? "null rules" : rules.toString());
378                     checkCategoriesAndExpected(pattern, categoriesAndExpected, rules);
379                 }
380             } catch (Exception e) {
381                 if (!FAIL_EXPECTED) {
382                     e.printStackTrace();
383                     throw new RuntimeException(e.getMessage());
384                 }
385             }
386         }
387     }
388 
389     private static final Set<String> compactExponentLocales = new HashSet(Arrays.asList("es", "fr", "it", "pt"));
390 
391     @Test
392     public void testUniqueRules() {
393         main: for (ULocale locale : factory.getAvailableULocales()) {
394             PluralRules rules = factory.forLocale(locale);
395             Map<String, PluralRules> keywordToRule = new HashMap<>();
396             Collection<FixedDecimalSamples> samples = new LinkedHashSet<>();
397 
398             for (String keyword : rules.getKeywords()) {
399                 for (SampleType sampleType : SampleType.values()) {
400                     FixedDecimalSamples samples2 = rules.getDecimalSamples(keyword, sampleType);
401                     if (samples2 != null) {
402                         samples.add(samples2);
403                     }
404                 }
405                 if (keyword.equals("other")) {
406                     continue;
407                 }
408                 String rules2 = keyword + ":" + rules.getRules(keyword);
409                 PluralRules singleRule = PluralRules.createRules(rules2);
410                 if (singleRule == null) {
411                     errln("Can't generate single rule for " + rules2);
412                     PluralRules.createRules(rules2); // for debugging
413                     continue main;
414                 }
415                 keywordToRule.put(keyword, singleRule);
416             }
417             if (compactExponentLocales.contains(locale.getLanguage()) && logKnownIssue("21714", "PluralRules.select treats 1c6 as 1")) {
418                 continue;
419             }
420             Map<FixedDecimal, String> collisionTest = new TreeMap();
421             for (FixedDecimalSamples sample3 : samples) {
422                 Set<FixedDecimalRange> samples2 = sample3.getSamples();
423                 if (samples2 == null) {
424                     continue;
425                 }
426                 for (FixedDecimalRange sample : samples2) {
427                     for (int i = 0; i < 1; ++i) {
428                         FixedDecimal item = i == 0 ? sample.start : sample.end;
429                         collisionTest.clear();
430                         for (Entry<String, PluralRules> entry : keywordToRule.entrySet()) {
431                             PluralRules rule = entry.getValue();
432                             String foundKeyword = rule.select(item);
433                             if (foundKeyword.equals("other")) {
434                                 continue;
435                             }
436                             String old = collisionTest.get(item);
437                             if (old != null) {
438                                 errln(locale + "\tNon-unique rules: " + item + " => " + old + " & " + foundKeyword);
439                                 rule.select(item);
440                             } else {
441                                 collisionTest.put(item, foundKeyword);
442                             }
443                         }
444                     }
445                 }
446             }
447         }
448     }
449 
450     private void checkCategoriesAndExpected(String title1, String categoriesAndExpected, PluralRules rules) {
451         for (String categoryAndExpected : categoriesAndExpected.split("\\s*;\\s*")) {
452             String[] categoryFromExpected = categoryAndExpected.split("\\s*:\\s*");
453             String expected = categoryFromExpected[0];
454             for (String value : categoryFromExpected[1].split("\\s*,\\s*")) {
455                 if (value.startsWith("@") || value.equals("…") || value.equals("null")) {
456                     continue;
457                 }
458                 String[] values = value.split("\\s*~\\s*");
459                 checkValue(title1, rules, expected, values[0]);
460                 if (values.length > 1) {
461                     checkValue(title1, rules, expected, values[1]);
462                 }
463             }
464         }
465     }
466 
467     public void checkValue(String title1, PluralRules rules, String expected, String value) {
468         FixedDecimal fdNum = new FixedDecimal(value);
469 
470         String result = rules.select(fdNum);
471         ULocale locale = null;
472         assertEquals(getAssertMessage(title1, locale, rules, expected) + "; value: " + value, expected, result);
473     }
474 
475     /**
476      * Check the testing helper method checkValue(), which parses a plural
477      * rule's sample string as a {@link FormattedNumber} in order to call
478      * {@code PluralRules.select(FormattedNumber)}, which in turn can support
479      * the exponent in plural sample numbers like 1e6 and 2.8c3.
480      */
481     @Test
482     public void testCheckValue() {
483         String ruleString =
484             "many: e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5"
485             + " @integer 1000000, 1e6, 2e6, 3e6, 4e6, 5e6, 6e6, …"
486             + " @decimal 1.0000001e6, 1.1e6, 2.0000001e6, 2.1e6, 3.0000001e6, 3.1e6, …;  "
487             + "one: i = 1 and v = 0"
488             + " @integer 1;  "
489             + "other: "
490             + " @integer 0, 2~16, 100, 1000, 10000, 100000, 1e3, 2e3, 3e3, 4e3, 5e3, 6e3, …"
491             + " @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001e3, 1.1e3, 2.0001e3, 2.1e3, 3.0001e3, 3.1e3, …";
492         PluralRules rules = PluralRules.createRules(ruleString);
493 
494         Object[][] casesData = {
495                 // expected category, value string
496                 {"many",  "1000000"},
497                 {"many",  "1e6"},
498                 {"many",  "1.1e6"},
499                 {"one",   "1"},
500                 {"other", "0"},
501                 {"other", "1e5"},
502                 {"other", "100000"},
503                 {"other", "0.0"},
504                 {"other", "100000.0"},
505                 {"other", "1000000.0"}
506         };
507 
508         for (Object[] caseDatum : casesData) {
509             String expCategory = (String) caseDatum[0];
510             String inputValueStr = (String) caseDatum[1];
511 
512             String msg = "checkValue(" + inputValueStr + ")";
513 
514             checkValue(msg, rules, expCategory, inputValueStr);
515         }
516     }
517 
518     private static String[][] equalityTestData = {
519             // once we add fractions, we had to retract the "test all possibilities" for equality,
520             // so we only have a limited set of equality tests now.
521             { "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" },
522             { "b:n not in 5;", "b: n != 5" },
523 
524     // { "a: n is 5",
525     // "a: n in 2..6 and n not in 2..4 and n is not 6" },
526     // { "a: n in 2..3",
527     // "a: n is 2 or n is 3",
528     // "a: n is 3 and n in 2..5 or n is 2" },
529     // { "a: n is 12; b:n mod 10 in 2..3",
530     // "b: n mod 10 in 2..3 and n is not 12; a: n in 12..12",
531     // "b: n is 13; a: n is 12; b: n mod 10 is 2 or n mod 10 is 3" },
532     };
533 
534     private static String[][] inequalityTestData = { { "a: n mod 8 is 3", "a: n mod 7 is 3" },
535             { "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" },
536             // the following are currently inequal, but we may make them equal in the future.
537             { "a: n in 2..5", "a: n in 2..4,5" }, };
538 
539     private void compareEquality(String id, Object[] objects, boolean shouldBeEqual) {
540         for (int i = 0; i < objects.length; ++i) {
541             Object lhs = objects[i];
542             int start = shouldBeEqual ? i : i + 1;
543             for (int j = start; j < objects.length; ++j) {
544                 Object rhs = objects[j];
545                 if (rhs == null || shouldBeEqual != lhs.equals(rhs)) {
546                     String msg = shouldBeEqual ? "should be equal" : "should not be equal";
547                     fail(id + " " + msg + " (" + i + ", " + j + "):\n    " + lhs + "\n    " + rhs);
548                 }
549                 // assertEquals("obj " + i + " and " + j, lhs, rhs);
550             }
551         }
552     }
553 
554     private void compareEqualityTestSets(String[][] sets, boolean shouldBeEqual) {
555         for (int i = 0; i < sets.length; ++i) {
556             String[] patterns = sets[i];
557             PluralRules[] rules = new PluralRules[patterns.length];
558             for (int j = 0; j < patterns.length; ++j) {
559                 rules[j] = PluralRules.createRules(patterns[j]);
560             }
561             compareEquality("test " + i, rules, shouldBeEqual);
562         }
563     }
564 
565     @Test
566     public void testEquality() {
567         compareEqualityTestSets(equalityTestData, true);
568     }
569 
570     @Test
571     public void testInequality() {
572         compareEqualityTestSets(inequalityTestData, false);
573     }
574 
575     @Test
576     public void testBuiltInRules() {
577         Object[][] cases = {
578                 {"en-US", PluralRules.KEYWORD_OTHER, 0},
579                 {"en-US", PluralRules.KEYWORD_ONE, 1},
580                 {"en-US", PluralRules.KEYWORD_OTHER, 2},
581                 {"ja-JP", PluralRules.KEYWORD_OTHER, 0},
582                 {"ja-JP", PluralRules.KEYWORD_OTHER, 1},
583                 {"ja-JP", PluralRules.KEYWORD_OTHER, 2},
584                 {"ru", PluralRules.KEYWORD_MANY, 0},
585                 {"ru", PluralRules.KEYWORD_ONE, 1},
586                 {"ru", PluralRules.KEYWORD_FEW, 2}
587         };
588         for (Object[] cas : cases) {
589             ULocale locale = new ULocale((String) cas[0]);
590             PluralRules rules = factory.forLocale(locale);
591             String expectedKeyword = (String) cas[1];
592             double number = (Integer) cas[2];
593             String message = locale + " " + number;
594             // Check both as double and as FormattedNumber.
595             assertEquals(message, expectedKeyword, rules.select(number));
596             FormattedNumber fn = NumberFormatter.withLocale(locale).format(number);
597             assertEquals(message, expectedKeyword, rules.select(fn));
598         }
599     }
600 
601     @Test
602     public void testSelectTrailingZeros() {
603         UnlocalizedNumberFormatter unf = NumberFormatter.with()
604                 .precision(Precision.fixedFraction(2));
605         Object[][] cases = {
606                 // 1) locale
607                 // 2) double expected keyword
608                 // 3) formatted number expected keyword (2 fraction digits)
609                 // 4) input number
610                 {"bs",  PluralRules.KEYWORD_FEW,   PluralRules.KEYWORD_OTHER, 5.2},  // 5.2 => two, but 5.20 => other
611                 {"si",  PluralRules.KEYWORD_ONE,   PluralRules.KEYWORD_ONE,   0.0},
612                 {"si",  PluralRules.KEYWORD_ONE,   PluralRules.KEYWORD_ONE,   1.0},
613                 {"si",  PluralRules.KEYWORD_ONE,   PluralRules.KEYWORD_OTHER, 0.1},  // 0.1 => one, but 0.10 => other
614                 {"si",  PluralRules.KEYWORD_ONE,   PluralRules.KEYWORD_ONE,   0.01}, // 0.01 => one
615                 {"hsb", PluralRules.KEYWORD_FEW,   PluralRules.KEYWORD_FEW,   1.03}, // (f % 100 == 3) => few
616                 {"hsb", PluralRules.KEYWORD_FEW,   PluralRules.KEYWORD_OTHER, 1.3},  // 1.3 => few, but 1.30 => other
617         };
618         for (Object[] cas : cases) {
619             ULocale locale = new ULocale((String) cas[0]);
620             PluralRules rules = factory.forLocale(locale);
621             String expectedDoubleKeyword = (String) cas[1];
622             String expectedFormattedKeyword = (String) cas[2];
623             double number = (Double) cas[3];
624             String message = locale + " " + number;
625             // Check both as double and as FormattedNumber.
626             assertEquals(message, expectedDoubleKeyword, rules.select(number));
627             FormattedNumber fn = unf.locale(locale).format(number);
628             assertEquals(message, expectedFormattedKeyword, rules.select(fn));
629         }
630     }
631 
632     private void compareLocaleResults(String loc1, String loc2, String loc3) {
633         PluralRules rules1 = PluralRules.forLocale(new ULocale(loc1));
634         PluralRules rules2 = PluralRules.forLocale(new ULocale(loc2));
635         PluralRules rules3 = PluralRules.forLocale(new ULocale(loc3));
636         for (int value = 0; value <= 12; value++) {
637             String result1 = rules1.select(value);
638             String result2 = rules2.select(value);
639             String result3 = rules3.select(value);
640             if (!result1.equals(result2) || !result1.equals(result3)) {
641                 errln("PluralRules.select(" + value + ") does not return the same values for "
642                         + loc1 + ", " + loc2 + ", " + loc3);
643             }
644         }
645     }
646 
647     @Test
648     public void testLocaleExtension() {
649         PluralRules rules = PluralRules.forLocale(new ULocale("pt@calendar=gregorian"));
650         String key = rules.select(1);
651         assertEquals("pt@calendar=gregorian select(1)", "one", key);
652         compareLocaleResults("ar", "ar_SA", "ar_SA@calendar=gregorian");
653         compareLocaleResults("ru", "ru_UA", "ru-u-cu-RUB");
654         compareLocaleResults("fr", "fr_CH", "fr@ms=uksystem");
655     }
656 
657     @Test
658     public void testFunctionalEquivalent() {
659         // spot check
660         ULocale unknown = ULocale.createCanonical("zz_ZZ");
661         ULocale un_equiv = PluralRules.getFunctionalEquivalent(unknown, null);
662         assertEquals("unknown locales have root", ULocale.ROOT, un_equiv);
663 
664         ULocale jp_equiv = PluralRules.getFunctionalEquivalent(ULocale.JAPAN, null);
665         ULocale cn_equiv = PluralRules.getFunctionalEquivalent(ULocale.CHINA, null);
666         assertEquals("japan and china equivalent locales", jp_equiv, cn_equiv);
667 
668         boolean[] available = new boolean[1];
669         ULocale russia = ULocale.createCanonical("ru_RU");
670         ULocale ru_ru_equiv = PluralRules.getFunctionalEquivalent(russia, available);
671         assertFalse("ru_RU not listed", available[0]);
672 
673         ULocale russian = ULocale.createCanonical("ru");
674         ULocale ru_equiv = PluralRules.getFunctionalEquivalent(russian, available);
675         assertTrue("ru listed", available[0]);
676         assertEquals("ru and ru_RU equivalent locales", ru_ru_equiv, ru_equiv);
677     }
678 
679     @Test
680     public void testAvailableULocales() {
681         ULocale[] locales = factory.getAvailableULocales();
682         Set localeSet = new HashSet();
683         localeSet.addAll(Arrays.asList(locales));
684 
685         assertEquals("locales are unique in list", locales.length, localeSet.size());
686     }
687 
688     /*
689      * Test the method public static PluralRules parseDescription(String description)
690      */
691     @Test
692     public void TestParseDescription() {
693         try {
694             if (PluralRules.DEFAULT != PluralRules.parseDescription("")) {
695                 errln("PluralRules.parseDescription(String) was suppose "
696                         + "to return PluralRules.DEFAULT when String is of " + "length 0.");
697             }
698         } catch (ParseException e) {
699             errln("PluralRules.parseDescription(String) was not suppose " + "to return an exception.");
700         }
701     }
702 
703     /*
704      * Tests the method public static PluralRules createRules(String description)
705      */
706     @Test
707     public void TestCreateRules() {
708         try {
709             if (PluralRules.createRules(null) != null) {
710                 errln("PluralRules.createRules(String) was suppose to "
711                         + "return null for an invalid String descrtiption.");
712             }
713         } catch (Exception e) {
714         }
715     }
716 
717     /*
718      * Tests the method public int hashCode()
719      */
720     @Test
721     public void TestHashCode() {
722         // Bad test, breaks whenever PluralRules implementation changes.
723         // PluralRules pr = PluralRules.DEFAULT;
724         // if (106069776 != pr.hashCode()) {
725         // errln("PluralRules.hashCode() was suppose to return 106069776 " + "when PluralRules.DEFAULT.");
726         // }
727     }
728 
729     /*
730      * Tests the method public boolean equals(PluralRules rhs)
731      */
732     @Test
733     public void TestEquals() {
734         PluralRules pr = PluralRules.DEFAULT;
735 
736         if (pr.equals((PluralRules) null)) {
737             errln("PluralRules.equals(PluralRules) was supposed to return false " + "when passing null.");
738         }
739     }
740 
741     private void assertRuleValue(String rule, double value) {
742         assertRuleKeyValue("a:" + rule, "a", value);
743     }
744 
745     private void assertRuleKeyValue(String rule, String key, double value) {
746         PluralRules pr = PluralRules.createRules(rule);
747         assertEquals(rule, value, pr.getUniqueKeywordValue(key));
748     }
749 
750     /*
751      * Tests getUniqueKeywordValue()
752      */
753     @Test
754     public void TestGetUniqueKeywordValue() {
755         assertRuleKeyValue("a: n is 1", "not_defined", PluralRules.NO_UNIQUE_VALUE); // key not defined
756         assertRuleValue("n within 2..2", 2);
757         assertRuleValue("n is 1", 1);
758         assertRuleValue("n in 2..2", 2);
759         assertRuleValue("n in 3..4", PluralRules.NO_UNIQUE_VALUE);
760         assertRuleValue("n within 3..4", PluralRules.NO_UNIQUE_VALUE);
761         assertRuleValue("n is 2 or n is 2", 2);
762         assertRuleValue("n is 2 and n is 2", 2);
763         assertRuleValue("n is 2 or n is 3", PluralRules.NO_UNIQUE_VALUE);
764         assertRuleValue("n is 2 and n is 3", PluralRules.NO_UNIQUE_VALUE);
765         assertRuleValue("n is 2 or n in 2..3", PluralRules.NO_UNIQUE_VALUE);
766         assertRuleValue("n is 2 and n in 2..3", 2);
767         assertRuleKeyValue("a: n is 1", "other", PluralRules.NO_UNIQUE_VALUE); // key matches default rule
768         assertRuleValue("n in 2,3", PluralRules.NO_UNIQUE_VALUE);
769         assertRuleValue("n in 2,3..6 and n not in 2..3,5..6", 4);
770     }
771 
772     /**
773      * The version in PluralFormatUnitTest is not really a test, and it's in the wrong place anyway, so I'm putting a
774      * variant of it here.
775      */
776     @Test
777     public void TestGetSamples() {
778         Set<ULocale> uniqueRuleSet = new HashSet<>();
779         for (ULocale locale : factory.getAvailableULocales()) {
780             uniqueRuleSet.add(PluralRules.getFunctionalEquivalent(locale, null));
781         }
782         for (ULocale locale : uniqueRuleSet) {
783             //if (locale.getLanguage().equals("fr") &&
784             //        logKnownIssue("21322", "PluralRules::getSamples cannot distinguish 1e5 from 100000")) {
785             //    continue;
786             //}
787             PluralRules rules = factory.forLocale(locale);
788             logln("\nlocale: " + (locale == ULocale.ROOT ? "root" : locale.toString()) + ", rules: " + rules);
789             Set<String> keywords = rules.getKeywords();
790             for (String keyword : keywords) {
791                 Collection<Double> list = rules.getSamples(keyword);
792                 logln("keyword: " + keyword + ", samples: " + list);
793                 // with fractions, the samples can be empty and thus the list null. In that case, however, there will be
794                 // FixedDecimal values.
795                 // So patch the test for that.
796                 if (list.size() == 0) {
797                     // when the samples (meaning integer samples) are null, then then integerSamples must be, and the
798                     // decimalSamples must not be
799                     FixedDecimalSamples integerSamples = rules.getDecimalSamples(keyword, SampleType.INTEGER);
800                     FixedDecimalSamples decimalSamples = rules.getDecimalSamples(keyword, SampleType.DECIMAL);
801                     assertTrue(getAssertMessage("List is not null", locale, rules, keyword), integerSamples == null
802                             && decimalSamples != null && decimalSamples.samples.size() != 0);
803                 } else {
804                     if (!assertTrue(getAssertMessage("Test getSamples.isEmpty", locale, rules, keyword),
805                             !list.isEmpty())) {
806                         rules.getSamples(keyword);
807                     }
808                     if (rules.toString().contains(": j")) {
809                         // hack until we remove j
810                     } else {
811                         for (double value : list) {
812                             assertEquals(getAssertMessage("Match keyword", locale, rules, keyword) + "; value '"
813                                     + value + "'", keyword, rules.select(value));
814                         }
815                     }
816                 }
817             }
818 
819             assertNull(locale + ", list is null", rules.getSamples("@#$%^&*"));
820             assertNull(locale + ", list is null", rules.getSamples("@#$%^&*", SampleType.DECIMAL));
821         }
822     }
823 
824     public String getAssertMessage(String message, ULocale locale, PluralRules rules, String keyword) {
825         String ruleString = "";
826         if (keyword != null) {
827             if (keyword.equals("other")) {
828                 for (String keyword2 : rules.getKeywords()) {
829                     ruleString += " NOR " + rules.getRules(keyword2).split("@")[0];
830                 }
831             } else {
832                 String rule = rules.getRules(keyword);
833                 ruleString = rule == null ? null : rule.split("@")[0];
834             }
835             ruleString = "; rule: '" + keyword + ": " + ruleString + "'";
836             // !keyword.equals("other") ? "'; keyword: '" + keyword + "'; rule: '" + rules.getRules(keyword) + "'"
837             // : "'; keyword: '" + keyword + "'; rules: '" + rules.toString() + "'";
838         }
839         return message + (locale == null ? "" : "; locale: '" + locale + "'") + ruleString;
840     }
841 
842     /**
843      * Returns the empty set if the keyword is not defined, null if there are an unlimited number of values for the
844      * keyword, or the set of values that trigger the keyword.
845      */
846     @Test
847     public void TestGetAllKeywordValues() {
848         // data is pairs of strings, the rule, and the expected values as arguments
849         String[] data = {
850                 "other: ; a: n mod 3 is 0",
851                 "a: null",
852                 "a: n in 2..5 and n within 5..8",
853                 "a: 5",
854                 "a: n in 2..5",
855                 "a: 2,3,4,5; other: null",
856                 "a: n not in 2..5",
857                 "a: null; other: null",
858                 "a: n within 2..5",
859                 "a: 2,3,4,5; other: null",
860                 "a: n not within 2..5",
861                 "a: null; other: null",
862                 "a: n in 2..5 or n within 6..8",
863                 "a: 2,3,4,5,6,7,8", // ignore 'other' here on out, always null
864                 "a: n in 2..5 and n within 6..8",
865                 "a: null",
866                 // we no longer support 'degenerate' rules
867                 // "a: n within 2..5 and n within 6..8", "a:", // our sampling catches these
868                 // "a: n within 2..5 and n within 5..8", "a: 5", // ''
869                 // "a: n within 1..2 and n within 2..3 or n within 3..4 and n within 4..5", "a: 2,4",
870                 // "a: n mod 3 is 0 and n within 0..5", "a: 0,3",
871                 "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",
872                 "a: 2,4,6", // but not this...
873                 "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",
874                 "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", };
875         for (int i = 0; i < data.length; i += 2) {
876             String ruleDescription = data[i];
877             String result = data[i + 1];
878 
879             PluralRules p = PluralRules.createRules(ruleDescription);
880             if (p == null) { // for debugging
881                 PluralRules.createRules(ruleDescription);
882             }
883             for (String ruleResult : result.split(";")) {
884                 String[] ruleAndValues = ruleResult.split(":");
885                 String keyword = ruleAndValues[0].trim();
886                 String valueList = ruleAndValues.length < 2 ? null : ruleAndValues[1];
887                 if (valueList != null) {
888                     valueList = valueList.trim();
889                 }
890                 Collection<Double> values;
891                 if (valueList == null || valueList.length() == 0) {
892                     values = Collections.EMPTY_SET;
893                 } else if ("null".equals(valueList)) {
894                     values = null;
895                 } else {
896                     values = new TreeSet<>();
897                     for (String value : valueList.split(",")) {
898                         values.add(Double.parseDouble(value));
899                     }
900                 }
901 
902                 Collection<Double> results = p.getAllKeywordValues(keyword);
903                 assertEquals(keyword + " in " + ruleDescription, values, results == null ? null : new HashSet(results));
904 
905                 if (results != null) {
906                     try {
907                         results.add(PluralRules.NO_UNIQUE_VALUE);
908                         fail("returned set is modifiable");
909                     } catch (UnsupportedOperationException e) {
910                         // pass
911                     }
912                 }
913             }
914         }
915     }
916 
917     @Test
918     public void TestOrdinal() {
919         PluralRules pr = factory.forLocale(ULocale.ENGLISH, PluralType.ORDINAL);
920         assertEquals("PluralRules(en-ordinal).select(2)", "two", pr.select(2));
921     }
922 
923     @Test
924     public void TestBasicFraction() {
925         String[][] tests = { { "en", "one: j is 1" }, { "1", "0", "1", "one" }, { "1", "2", "1.00", "other" }, };
926         ULocale locale = null;
927         NumberFormat nf = null;
928         PluralRules pr = null;
929 
930         for (String[] row : tests) {
931             switch (row.length) {
932             case 2:
933                 locale = ULocale.forLanguageTag(row[0]);
934                 nf = NumberFormat.getInstance(locale);
935                 pr = PluralRules.createRules(row[1]);
936                 break;
937             case 4:
938                 double n = Double.parseDouble(row[0]);
939                 int minFracDigits = Integer.parseInt(row[1]);
940                 nf.setMinimumFractionDigits(minFracDigits);
941                 String expectedFormat = row[2];
942                 String expectedKeyword = row[3];
943 
944                 UFieldPosition pos = new UFieldPosition();
945                 String formatted = nf.format(1.0, new StringBuffer(), pos).toString();
946                 int countVisibleFractionDigits = pos.getCountVisibleFractionDigits();
947                 long fractionDigits = pos.getFractionDigits();
948                 String keyword = pr.select(n, countVisibleFractionDigits, fractionDigits);
949                 assertEquals("Formatted " + n + "\t" + minFracDigits, expectedFormat, formatted);
950                 assertEquals("Keyword " + n + "\t" + minFracDigits, expectedKeyword, keyword);
951                 break;
952             default:
953                 throw new RuntimeException();
954             }
955         }
956     }
957 
958     @Test
959     public void TestLimitedAndSamplesConsistency() {
960         for (ULocale locale : PluralRules.getAvailableULocales()) {
961             ULocale loc2 = PluralRules.getFunctionalEquivalent(locale, null);
962             if (!loc2.equals(locale)) {
963                 continue; // only need "unique" rules
964             }
965             for (PluralType type : PluralType.values()) {
966                 PluralRules rules = PluralRules.forLocale(locale, type);
967                 for (SampleType sampleType : SampleType.values()) {
968                     if (type == PluralType.ORDINAL) {
969                         logKnownIssue("10783", "Fix issues with isLimited vs computeLimited on ordinals");
970                         continue;
971                     }
972                     for (String keyword : rules.getKeywords()) {
973                         boolean isLimited = rules.isLimited(keyword, sampleType);
974                         boolean computeLimited = rules.computeLimited(keyword, sampleType);
975                         if (!keyword.equals("other") && !(locale.getLanguage().equals("fr") && logKnownIssue("ICU-21322", "fr plurals many case computeLimited == isLimited"))) {
976                             assertEquals(getAssertMessage("computeLimited == isLimited", locale, rules, keyword),
977                                     computeLimited, isLimited);
978                         }
979                         Collection<Double> samples = rules.getSamples(keyword, sampleType);
980                         assertNotNull(getAssertMessage("Samples must not be null", locale, rules, keyword), samples);
981                         /* FixedDecimalSamples decimalSamples = */rules.getDecimalSamples(keyword, sampleType);
982                         // assertNotNull(getAssertMessage("Decimal samples must be null if unlimited", locale, rules,
983                         // keyword), decimalSamples);
984                     }
985                 }
986             }
987         }
988     }
989 
990     @Test
991     public void TestKeywords() {
992         Set<String> possibleKeywords = new LinkedHashSet(Arrays.asList("zero", "one", "two", "few", "many", "other"));
993         Object[][][] tests = {
994                 // format is locale, explicits, then triples of keyword, status, unique value.
995                 { { "en", null }, { "one", KeywordStatus.UNIQUE, 1.0d }, { "other", KeywordStatus.UNBOUNDED, null } },
996                 { { "pl", null }, { "one", KeywordStatus.UNIQUE, 1.0d }, { "few", KeywordStatus.UNBOUNDED, null },
997                         { "many", KeywordStatus.UNBOUNDED, null },
998                         { "other", KeywordStatus.SUPPRESSED, null, KeywordStatus.UNBOUNDED, null } // note that it is
999                                                                                                    // suppressed in
1000                                                                                                    // INTEGER but not
1001                                                                                                    // DECIMAL
1002                 }, { { "en", new HashSet<>(Arrays.asList(1.0d)) }, // check that 1 is suppressed
1003                         { "one", KeywordStatus.SUPPRESSED, null }, { "other", KeywordStatus.UNBOUNDED, null } }, };
1004         Output<Double> uniqueValue = new Output<>();
1005         for (Object[][] test : tests) {
1006             ULocale locale = new ULocale((String) test[0][0]);
1007             // NumberType numberType = (NumberType) test[1];
1008             Set<Double> explicits = (Set<Double>) test[0][1];
1009             PluralRules pluralRules = factory.forLocale(locale);
1010             LinkedHashSet<String> remaining = new LinkedHashSet(possibleKeywords);
1011             for (int i = 1; i < test.length; ++i) {
1012                 Object[] row = test[i];
1013                 String keyword = (String) row[0];
1014                 KeywordStatus statusExpected = (KeywordStatus) row[1];
1015                 Double uniqueExpected = (Double) row[2];
1016                 remaining.remove(keyword);
1017                 KeywordStatus status = pluralRules.getKeywordStatus(keyword, 0, explicits, uniqueValue);
1018                 assertEquals(getAssertMessage("Unique Value", locale, pluralRules, keyword), uniqueExpected,
1019                         uniqueValue.value);
1020                 assertEquals(getAssertMessage("Keyword Status", locale, pluralRules, keyword), statusExpected, status);
1021                 if (row.length > 3) {
1022                     statusExpected = (KeywordStatus) row[3];
1023                     uniqueExpected = (Double) row[4];
1024                     status = pluralRules.getKeywordStatus(keyword, 0, explicits, uniqueValue, SampleType.DECIMAL);
1025                     assertEquals(getAssertMessage("Unique Value - decimal", locale, pluralRules, keyword),
1026                             uniqueExpected, uniqueValue.value);
1027                     assertEquals(getAssertMessage("Keyword Status - decimal", locale, pluralRules, keyword),
1028                             statusExpected, status);
1029                 }
1030             }
1031             for (String keyword : remaining) {
1032                 KeywordStatus status = pluralRules.getKeywordStatus(keyword, 0, null, uniqueValue);
1033                 assertEquals("Invalid keyword " + keyword, status, KeywordStatus.INVALID);
1034                 assertNull("Invalid keyword " + keyword, uniqueValue.value);
1035             }
1036         }
1037     }
1038 
1039     // For the time being, the compact notation exponent operand `c` is an alias
1040     // for the scientific exponent operand `e` and compact notation.
1041     @Test
1042     public void testScientificPluralKeyword() {
1043         PluralRules rules = PluralRules.createRules("one: i = 0,1 @integer 0, 1 @decimal 0.0~1.5;  many: e = 0 and i % 1000000 = 0 and v = 0 or " +
1044                 "e != 0 .. 5;  other:  @integer 2~17, 100, 1000, 10000, 100000, 1000000, @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …");
1045         ULocale locale = new ULocale("fr-FR");
1046 
1047         Object[][] casesData = {
1048                 // unlocalized formatter skeleton, input, string output, plural rule keyword
1049                 {"",           0, "0", "one"},
1050                 {"scientific", 0, "0", "one"},
1051 
1052                 {"",           1, "1", "one"},
1053                 {"scientific", 1, "1", "one"},
1054 
1055                 {"",           2, "2", "other"},
1056                 {"scientific", 2, "2", "other"},
1057 
1058                 {"",           1000000, "1 000 000", "many"},
1059                 {"scientific", 1000000, "1 million", "many"},
1060 
1061                 {"",           1000001, "1 000 001", "other"},
1062                 {"scientific", 1000001, "1 million", "many"},
1063 
1064                 {"",           120000, "1 200 000", "other"},
1065                 {"scientific", 1200000, "1,2 millions", "many"},
1066 
1067                 {"",           1200001, "1 200 001", "other"},
1068                 {"scientific", 1200001, "1,2 millions", "many"},
1069 
1070                 {"",           2000000, "2 000 000", "many"},
1071                 {"scientific", 2000000, "2 millions", "many"},
1072         };
1073 
1074         for (Object[] caseDatum : casesData) {
1075             String skeleton = (String) caseDatum[0];
1076             int input = (int) caseDatum[1];
1077             // String expectedString = (String) caseDatum[2];
1078             String expectPluralRuleKeyword = (String) caseDatum[3];
1079 
1080             String actualPluralRuleKeyword =
1081                     getPluralKeyword(rules, locale, input, skeleton);
1082 
1083             assertEquals(
1084                     String.format("PluralRules select %s: %d", skeleton, input),
1085                     expectPluralRuleKeyword,
1086                     actualPluralRuleKeyword);
1087         }
1088     }
1089 
1090     @Test
1091     public void testCompactDecimalPluralKeyword() {
1092         PluralRules rules = PluralRules.createRules("one: i = 0,1 @integer 0, 1 @decimal 0.0~1.5;  many: c = 0 and i % 1000000 = 0 and v = 0 or " +
1093                 "c != 0 .. 5;  other:  @integer 2~17, 100, 1000, 10000, 100000, 1000000, @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …");
1094         ULocale locale = new ULocale("fr-FR");
1095 
1096         Object[][] casesData = {
1097                 // unlocalized formatter skeleton, input, string output, plural rule keyword
1098                 {"",             0, "0", "one"},
1099                 {"compact-long", 0, "0", "one"},
1100 
1101                 {"",             1, "1", "one"},
1102                 {"compact-long", 1, "1", "one"},
1103 
1104                 {"",             2, "2", "other"},
1105                 {"compact-long", 2, "2", "other"},
1106 
1107                 {"",             1000000, "1 000 000", "many"},
1108                 {"compact-long", 1000000, "1 million", "many"},
1109 
1110                 {"",             1000001, "1 000 001", "other"},
1111                 {"compact-long", 1000001, "1 million", "many"},
1112 
1113                 {"",             120000, "1 200 000", "other"},
1114                 {"compact-long", 1200000, "1,2 millions", "many"},
1115 
1116                 {"",             1200001, "1 200 001", "other"},
1117                 {"compact-long", 1200001, "1,2 millions", "many"},
1118 
1119                 {"",             2000000, "2 000 000", "many"},
1120                 {"compact-long", 2000000, "2 millions", "many"},
1121         };
1122 
1123         for (Object[] caseDatum : casesData) {
1124             String skeleton = (String) caseDatum[0];
1125             int input = (int) caseDatum[1];
1126             // String expectedString = (String) caseDatum[2];
1127             String expectPluralRuleKeyword = (String) caseDatum[3];
1128 
1129             String actualPluralRuleKeyword =
1130                     getPluralKeyword(rules, locale, input, skeleton);
1131 
1132             assertEquals(
1133                     String.format("PluralRules select %s: %d", skeleton, input),
1134                     expectPluralRuleKeyword,
1135                     actualPluralRuleKeyword);
1136         }
1137     }
1138 
1139     private String getPluralKeyword(PluralRules rules, ULocale locale, double number, String skeleton) {
1140         LocalizedNumberFormatter formatter =
1141                 NumberFormatter.forSkeleton(skeleton)
1142                     .locale(locale);
1143         FormattedNumber fn = formatter.format(number);
1144         String pluralKeyword = rules.select(fn);
1145         return pluralKeyword;
1146     }
1147 
1148     @Test
1149     public void testDoubleValue() {
1150         Object[][] intCasesData = {
1151                 // source number, expected double value
1152                 {-101, -101.0},
1153                 {-100, -100.0},
1154                 {-1,   -1.0},
1155                 {0,     0.0},
1156                 {1,     1.0},
1157                 {100,   100.0}
1158         };
1159 
1160         for (Object[] caseDatum : intCasesData) {
1161             double inputNum = (int) caseDatum[0];
1162             double expVal = (double) caseDatum[1];
1163             FixedDecimal fd = new FixedDecimal(inputNum);
1164             assertEquals("FixedDecimal.doubleValue() for " + inputNum, expVal, fd.doubleValue());
1165         }
1166 
1167         Object[][] doubleCasesData = {
1168                 // source number, expected double value
1169                 {-0.0,     -0.0},
1170                 {0.1,       0.1},
1171                 {1.999,     1.999},
1172                 {2.0,       2.0},
1173                 {100.001, 100.001}
1174         };
1175 
1176         for (Object[] caseDatum : doubleCasesData) {
1177             double inputNum = (double) caseDatum[0];
1178             double expVal = (double) caseDatum[1];
1179             FixedDecimal fd = new FixedDecimal(inputNum);
1180             assertEquals("FixedDecimal.doubleValue() for " + inputNum, expVal, fd.doubleValue());
1181         }
1182     }
1183 
1184     @Test
1185     public void testLongValue() {
1186         Object[][] intCasesData = {
1187                 // source number, expected double value
1188                 {-101,  101},
1189                 {-100,  100},
1190                 {-1,    1},
1191                 {0,     0},
1192                 {1,     1},
1193                 {100,   100}
1194         };
1195 
1196         for (Object[] caseDatum : intCasesData) {
1197             long inputNum = (int) caseDatum[0];
1198             long expVal = (int) caseDatum[1];
1199             FixedDecimal fd = new FixedDecimal(inputNum);
1200             assertEquals("FixedDecimal.longValue() for " + inputNum, expVal, fd.longValue());
1201         }
1202 
1203         Object[][] doubleCasesData = {
1204                 // source number, expected double value
1205                 {-0.0,      0},
1206                 {0.1,       0},
1207                 {1.999,     1},
1208                 {2.0,       2},
1209                 {100.001,   100}
1210         };
1211 
1212         for (Object[] caseDatum : doubleCasesData) {
1213             double inputNum = (double) caseDatum[0];
1214             long expVal = (int) caseDatum[1];
1215             FixedDecimal fd = new FixedDecimal(inputNum);
1216             assertEquals("FixedDecimal.longValue() for " + inputNum, expVal, fd.longValue());
1217         }
1218     }
1219 
1220     enum StandardPluralCategories {
1221         zero, one, two, few, many, other;
1222         /**
1223          *
1224          */
1225         private static final Set<StandardPluralCategories> ALL = Collections.unmodifiableSet(EnumSet
1226                 .allOf(StandardPluralCategories.class));
1227 
1228         /**
1229          * Return a mutable set
1230          *
1231          * @param source
1232          * @return
1233          */
1234         static final EnumSet<StandardPluralCategories> getSet(Collection<String> source) {
1235             EnumSet<StandardPluralCategories> result = EnumSet.noneOf(StandardPluralCategories.class);
1236             for (String s : source) {
1237                 result.add(StandardPluralCategories.valueOf(s));
1238             }
1239             return result;
1240         }
1241 
1242         static final Comparator<Set<StandardPluralCategories>> SHORTEST_FIRST = new Comparator<Set<StandardPluralCategories>>() {
1243             @Override
1244             public int compare(Set<StandardPluralCategories> arg0, Set<StandardPluralCategories> arg1) {
1245                 int diff = arg0.size() - arg1.size();
1246                 if (diff != 0) {
1247                     return diff;
1248                 }
1249                 // otherwise first...
1250                 // could be optimized, but we don't care here.
1251                 for (StandardPluralCategories value : ALL) {
1252                     if (arg0.contains(value)) {
1253                         if (!arg1.contains(value)) {
1254                             return 1;
1255                         }
1256                     } else if (arg1.contains(value)) {
1257                         return -1;
1258                     }
1259 
1260                 }
1261                 return 0;
1262             }
1263 
1264         };
1265     }
1266 
1267     @Test
1268     public void TestLocales() {
1269         if (false) {
1270             generateLOCALE_SNAPSHOT();
1271         }
1272         for (String test : LOCALE_SNAPSHOT) {
1273             test = test.trim();
1274             String[] parts = test.split("\\s*;\\s*");
1275             for (String localeString : parts[0].split("\\s*,\\s*")) {
1276                 ULocale locale = new ULocale(localeString);
1277                 if (factory.hasOverride(locale)) {
1278                     continue; // skip for now
1279                 }
1280                 if (compactExponentLocales.contains(locale.getLanguage()) && logKnownIssue("21322", "PluralRules::getSamples cannot distinguish 1e5 from 100000")) {
1281                     // or logKnownIssue("21714", "PluralRules.select treats 1c6 as 1") ?
1282                     continue;
1283                 }
1284                 PluralRules rules = factory.forLocale(locale);
1285                 for (int i = 1; i < parts.length; ++i) {
1286                     checkCategoriesAndExpected(localeString, parts[i], rules);
1287                 }
1288             }
1289         }
1290     }
1291 
1292     private static final Comparator<PluralRules> PLURAL_RULE_COMPARATOR = new Comparator<PluralRules>() {
1293         @Override
1294         public int compare(PluralRules o1, PluralRules o2) {
1295             return o1.compareTo(o2);
1296         }
1297     };
1298 
1299     private void generateLOCALE_SNAPSHOT() {
1300         Comparator c = new CollectionUtilities.CollectionComparator<>();
1301         Relation<Set<StandardPluralCategories>, PluralRules> setsToRules = Relation.of(
1302                 new TreeMap<Set<StandardPluralCategories>, Set<PluralRules>>(c), TreeSet.class, PLURAL_RULE_COMPARATOR);
1303         Relation<PluralRules, ULocale> data = Relation.of(
1304                 new TreeMap<PluralRules, Set<ULocale>>(PLURAL_RULE_COMPARATOR), TreeSet.class);
1305         for (ULocale locale : PluralRules.getAvailableULocales()) {
1306             PluralRules pr = PluralRules.forLocale(locale);
1307             EnumSet<StandardPluralCategories> set = getCanonicalSet(pr.getKeywords());
1308             setsToRules.put(set, pr);
1309             data.put(pr, locale);
1310         }
1311         for (Entry<Set<StandardPluralCategories>, Set<PluralRules>> entry1 : setsToRules.keyValuesSet()) {
1312             Set<StandardPluralCategories> set = entry1.getKey();
1313             Set<PluralRules> rules = entry1.getValue();
1314             System.out.println("\n        // " + set);
1315             for (PluralRules rule : rules) {
1316                 Set<ULocale> locales = data.get(rule);
1317                 System.out.print("        \"" + CollectionUtilities.join(locales, ","));
1318                 for (StandardPluralCategories spc : set) {
1319                     String keyword = spc.toString();
1320                     FixedDecimalSamples samples = rule.getDecimalSamples(keyword, SampleType.INTEGER);
1321                     System.out.print("; " + spc + ": " + samples);
1322                 }
1323                 System.out.println("\",");
1324             }
1325         }
1326     }
1327 
1328     /**
1329      * @param keywords
1330      * @return
1331      */
1332     private EnumSet<StandardPluralCategories> getCanonicalSet(Set<String> keywords) {
1333         EnumSet<StandardPluralCategories> result = EnumSet.noneOf(StandardPluralCategories.class);
1334         for (String s : keywords) {
1335             result.add(StandardPluralCategories.valueOf(s));
1336         }
1337         return result;
1338     }
1339 
1340     static final String[] LOCALE_SNAPSHOT = {
1341             // [other]
1342             "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, …",
1343 
1344             // [one, other]
1345             "am,bn,fa,gu,hi,kn,mr,zu; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1346             "ff,hy,kab; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1347             "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, …",
1348             "pt; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
1349             "si; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1350             "ak,bho,guw,ln,mg,nso,pa,ti,wa; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1351             "tzm; one: @integer 0, 1, 11~24; other: @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, …",
1352             "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, …",
1353             "pt_PT; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
1354             "da; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
1355             "is; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
1356             "mk; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
1357             "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, …",
1358 
1359             // [zero, one, other]
1360             "lag; zero: @integer 0; one: @integer 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1361             "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, …",
1362             "ksh; zero: @integer 0; one: @integer 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1363 
1364             // [one, two, other]
1365             "iu,naq,se,sma,smi,smj,smn,sms; one: @integer 1; two: @integer 2; other: @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, …",
1366 
1367             // [one, many, other]
1368             "fr; one: @integer 0, 1; many: @integer 1000000; other: @integer 2~17, 100, 1000, 10000, 100000, …",
1369 
1370             // [one, few, other]
1371             "shi; one: @integer 0, 1; few: @integer 2~10; other: @integer 11~26, 100, 1000, 10000, 100000, 1000000, …",
1372             "mo,ro; one: @integer 1; few: @integer 0, 2~16, 102, 1002, …; other: @integer 20~35, 100, 1000, 10000, 100000, 1000000, …",
1373             "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, …",
1374 
1375             // [one, two, few, other]
1376             "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, …",
1377             "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, …",
1378 
1379             // [one, two, many, other]
1380             "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, …",
1381 
1382             // [one, few, many, other]
1383             "cs,sk; one: @integer 1; few: @integer 2~4; many: null; other: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
1384             "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",
1385             "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, …",
1386             "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, …",
1387             "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",
1388             "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",
1389 
1390             // [one, two, few, many, other]
1391             "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, …",
1392             "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, …",
1393             "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, …",
1394 
1395             // [zero, one, two, few, many, other]
1396             "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, …",
1397             "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, …",
1398             "kw; zero: @integer 0; one: @integer 1; two: @integer 2, 22, 42, 62, 82, 102, 122, 142, 1002, …; few: @integer 3, 23, 43, 63, 83, 103, 123, 143, 1003, …; many: @integer 21, 41, 61, 81, 101, 121, 141, 161, 1001, …; other: @integer 4~19, 100, 1000000, …", };
1399 
1400     private <T extends Serializable> T serializeAndDeserialize(T original, Output<Integer> size) {
1401         try {
1402             ByteArrayOutputStream baos = new ByteArrayOutputStream();
1403             ObjectOutputStream ostream = new ObjectOutputStream(baos);
1404             ostream.writeObject(original);
1405             ostream.flush();
1406             byte bytes[] = baos.toByteArray();
1407             size.value = bytes.length;
1408             ObjectInputStream istream = new ObjectInputStream(new ByteArrayInputStream(bytes));
1409             T reconstituted = (T) istream.readObject();
1410             return reconstituted;
1411         } catch (IOException e) {
1412             throw new RuntimeException(e);
1413         } catch (ClassNotFoundException e) {
1414             throw new RuntimeException(e);
1415         }
1416     }
1417 
1418     @Test
1419     public void TestSerialization() {
1420         Output<Integer> size = new Output<>();
1421         int max = 0;
1422         for (ULocale locale : PluralRules.getAvailableULocales()) {
1423             PluralRules item = PluralRules.forLocale(locale);
1424             PluralRules item2 = serializeAndDeserialize(item, size);
1425             logln(locale + "\tsize:\t" + size.value);
1426             max = Math.max(max, size.value);
1427             if (!assertEquals(locale + "\tPlural rules before and after serialization", item, item2)) {
1428                 // for debugging
1429                 PluralRules item3 = serializeAndDeserialize(item, size);
1430                 item.equals(item3);
1431             }
1432         }
1433         logln("max \tsize:\t" + max);
1434     }
1435 
1436     public static class FixedDecimalHandler implements SerializableTestUtility.Handler {
1437         @Override
1438         public Object[] getTestObjects() {
1439             FixedDecimal items[] = { new FixedDecimal(3d), new FixedDecimal(3d, 2), new FixedDecimal(3.1d, 1),
1440                     new FixedDecimal(3.1d, 2), };
1441             return items;
1442         }
1443 
1444         @Override
1445         public boolean hasSameBehavior(Object a, Object b) {
1446             FixedDecimal a1 = (FixedDecimal) a;
1447             FixedDecimal b1 = (FixedDecimal) b;
1448             return a1.equals(b1);
1449         }
1450     }
1451 
1452     @Test
1453     public void TestSerial() {
1454         PluralRules s = PluralRules.forLocale(ULocale.ENGLISH);
1455         checkStreamingEquality(s);
1456     }
1457 
1458     public void checkStreamingEquality(PluralRules s) {
1459         try {
1460             ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
1461             ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOut);
1462             objectOutputStream.writeObject(s);
1463             objectOutputStream.close();
1464             byte[] contents = byteOut.toByteArray();
1465             logln(s.getClass() + ": " + showBytes(contents));
1466             ByteArrayInputStream byteIn = new ByteArrayInputStream(contents);
1467             ObjectInputStream objectInputStream = new ObjectInputStream(byteIn);
1468             Object obj = objectInputStream.readObject();
1469             assertEquals("Streamed Object equals ", s, obj);
1470         } catch (Exception e) {
1471             assertNull("TestSerial", e);
1472         }
1473     }
1474 
1475     /**
1476      * @param contents
1477      * @return
1478      */
1479     private String showBytes(byte[] contents) {
1480         StringBuilder b = new StringBuilder("[");
1481         for (int i = 0; i < contents.length; ++i) {
1482             int item = contents[i] & 0xFF;
1483             if (item >= 0x20 && item <= 0x7F) {
1484                 b.append((char) item);
1485             } else {
1486                 b.append('(').append(Utility.hex(item, 2)).append(')');
1487             }
1488         }
1489         return b.append(']').toString();
1490     }
1491 
1492     @Test
1493     public void testJavaLocaleFactory() {
1494         PluralRules rulesU0 = PluralRules.forLocale(ULocale.FRANCE);
1495         PluralRules rulesJ0 = PluralRules.forLocale(Locale.FRANCE);
1496         assertEquals("forLocale()", rulesU0, rulesJ0);
1497 
1498         PluralRules rulesU1 = PluralRules.forLocale(ULocale.FRANCE, PluralType.ORDINAL);
1499         PluralRules rulesJ1 = PluralRules.forLocale(Locale.FRANCE, PluralType.ORDINAL);
1500         assertEquals("forLocale() with type", rulesU1, rulesJ1);
1501     }
1502 
1503     @Test
1504     public void testBug20264() {
1505         String expected = "1.23400";
1506         FixedDecimal fd = new FixedDecimal(1.234, 5, 2);
1507         assertEquals("FixedDecimal toString", expected, fd.toString());
1508         Locale.setDefault(Locale.FRENCH);
1509         assertEquals("FixedDecimal toString", expected, fd.toString());
1510         Locale.setDefault(Locale.GERMAN);
1511         assertEquals("FixedDecimal toString", expected, fd.toString());
1512     }
1513 
1514     @Test
1515     public void testSelectRange() {
1516         int d1 = 102;
1517         int d2 = 201;
1518         ULocale locale = new ULocale("sl");
1519 
1520         // Locale sl has interesting data: one + two => few
1521         FormattedNumberRange range = NumberRangeFormatter.withLocale(locale).formatRange(d1, d2);
1522         PluralRules rules = PluralRules.forLocale(locale);
1523 
1524         // For testing: get plural form of first and second numbers
1525         FormattedNumber a = NumberFormatter.withLocale(locale).format(d1);
1526         FormattedNumber b = NumberFormatter.withLocale(locale).format(d2);
1527         assertEquals("First plural", "two", rules.select(a));
1528         assertEquals("Second plural", "one", rules.select(b));
1529 
1530         // Check the range plural now:
1531         String form = rules.select(range);
1532         assertEquals("Range plural", "few", form);
1533 
1534         // Test when plural ranges data is unavailable:
1535         PluralRules bare = PluralRules.createRules("a: i = 0,1");
1536         try {
1537             form = bare.select(range);
1538             fail("Expected exception");
1539         } catch (UnsupportedOperationException e) {}
1540 
1541         // However, they should not throw when no data is available for a language.
1542         PluralRules xyz = PluralRules.forLocale(new ULocale("xyz"));
1543         form = xyz.select(range);
1544         assertEquals("Fallback form", "other", form);
1545     }
1546 }
1547