• 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     @Test
390     public void testUniqueRules() {
391         main: for (ULocale locale : factory.getAvailableULocales()) {
392             PluralRules rules = factory.forLocale(locale);
393             Map<String, PluralRules> keywordToRule = new HashMap<>();
394             Collection<FixedDecimalSamples> samples = new LinkedHashSet<>();
395 
396             for (String keyword : rules.getKeywords()) {
397                 for (SampleType sampleType : SampleType.values()) {
398                     FixedDecimalSamples samples2 = rules.getDecimalSamples(keyword, sampleType);
399                     if (samples2 != null) {
400                         samples.add(samples2);
401                     }
402                 }
403                 if (keyword.equals("other")) {
404                     continue;
405                 }
406                 String rules2 = keyword + ":" + rules.getRules(keyword);
407                 PluralRules singleRule = PluralRules.createRules(rules2);
408                 if (singleRule == null) {
409                     errln("Can't generate single rule for " + rules2);
410                     PluralRules.createRules(rules2); // for debugging
411                     continue main;
412                 }
413                 keywordToRule.put(keyword, singleRule);
414             }
415             Map<FixedDecimal, String> collisionTest = new TreeMap();
416             for (FixedDecimalSamples sample3 : samples) {
417                 Set<FixedDecimalRange> samples2 = sample3.getSamples();
418                 if (samples2 == null) {
419                     continue;
420                 }
421                 for (FixedDecimalRange sample : samples2) {
422                     for (int i = 0; i < 1; ++i) {
423                         FixedDecimal item = i == 0 ? sample.start : sample.end;
424                         collisionTest.clear();
425                         for (Entry<String, PluralRules> entry : keywordToRule.entrySet()) {
426                             PluralRules rule = entry.getValue();
427                             String foundKeyword = rule.select(item);
428                             if (foundKeyword.equals("other")) {
429                                 continue;
430                             }
431                             String old = collisionTest.get(item);
432                             if (old != null) {
433                                 if (!locale.getLanguage().equals("fr") ||
434                                         !logKnownIssue("21322", "fr Non-unique rules: 1e6 => one & many")) {
435                                     errln(locale + "\tNon-unique rules: " + item + " => " + old + " & " + foundKeyword);
436                                 }
437                                 rule.select(item);
438                             } else {
439                                 collisionTest.put(item, foundKeyword);
440                             }
441                         }
442                     }
443                 }
444             }
445         }
446     }
447 
448     private void checkCategoriesAndExpected(String title1, String categoriesAndExpected, PluralRules rules) {
449         for (String categoryAndExpected : categoriesAndExpected.split("\\s*;\\s*")) {
450             String[] categoryFromExpected = categoryAndExpected.split("\\s*:\\s*");
451             String expected = categoryFromExpected[0];
452             for (String value : categoryFromExpected[1].split("\\s*,\\s*")) {
453                 if (value.startsWith("@") || value.equals("…") || value.equals("null")) {
454                     continue;
455                 }
456                 String[] values = value.split("\\s*~\\s*");
457                 checkValue(title1, rules, expected, values[0]);
458                 if (values.length > 1) {
459                     checkValue(title1, rules, expected, values[1]);
460                 }
461             }
462         }
463     }
464 
465     public void checkValue(String title1, PluralRules rules, String expected, String value) {
466         double number = Double.parseDouble(value);
467         int decimalPos = value.indexOf('.') + 1;
468         int countVisibleFractionDigits;
469         int fractionaldigits;
470         if (decimalPos == 0) {
471             countVisibleFractionDigits = fractionaldigits = 0;
472         } else {
473             countVisibleFractionDigits = value.length() - decimalPos;
474             fractionaldigits = Integer.parseInt(value.substring(decimalPos));
475         }
476         String result = rules.select(number, countVisibleFractionDigits, fractionaldigits);
477         ULocale locale = null;
478         assertEquals(getAssertMessage(title1, locale, rules, expected) + "; value: " + value, expected, result);
479     }
480 
481     private static String[][] equalityTestData = {
482             // once we add fractions, we had to retract the "test all possibilities" for equality,
483             // so we only have a limited set of equality tests now.
484             { "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" },
485             { "b:n not in 5;", "b: n != 5" },
486 
487     // { "a: n is 5",
488     // "a: n in 2..6 and n not in 2..4 and n is not 6" },
489     // { "a: n in 2..3",
490     // "a: n is 2 or n is 3",
491     // "a: n is 3 and n in 2..5 or n is 2" },
492     // { "a: n is 12; b:n mod 10 in 2..3",
493     // "b: n mod 10 in 2..3 and n is not 12; a: n in 12..12",
494     // "b: n is 13; a: n is 12; b: n mod 10 is 2 or n mod 10 is 3" },
495     };
496 
497     private static String[][] inequalityTestData = { { "a: n mod 8 is 3", "a: n mod 7 is 3" },
498             { "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" },
499             // the following are currently inequal, but we may make them equal in the future.
500             { "a: n in 2..5", "a: n in 2..4,5" }, };
501 
502     private void compareEquality(String id, Object[] objects, boolean shouldBeEqual) {
503         for (int i = 0; i < objects.length; ++i) {
504             Object lhs = objects[i];
505             int start = shouldBeEqual ? i : i + 1;
506             for (int j = start; j < objects.length; ++j) {
507                 Object rhs = objects[j];
508                 if (rhs == null || shouldBeEqual != lhs.equals(rhs)) {
509                     String msg = shouldBeEqual ? "should be equal" : "should not be equal";
510                     fail(id + " " + msg + " (" + i + ", " + j + "):\n    " + lhs + "\n    " + rhs);
511                 }
512                 // assertEquals("obj " + i + " and " + j, lhs, rhs);
513             }
514         }
515     }
516 
517     private void compareEqualityTestSets(String[][] sets, boolean shouldBeEqual) {
518         for (int i = 0; i < sets.length; ++i) {
519             String[] patterns = sets[i];
520             PluralRules[] rules = new PluralRules[patterns.length];
521             for (int j = 0; j < patterns.length; ++j) {
522                 rules[j] = PluralRules.createRules(patterns[j]);
523             }
524             compareEquality("test " + i, rules, shouldBeEqual);
525         }
526     }
527 
528     @Test
529     public void testEquality() {
530         compareEqualityTestSets(equalityTestData, true);
531     }
532 
533     @Test
534     public void testInequality() {
535         compareEqualityTestSets(inequalityTestData, false);
536     }
537 
538     @Test
539     public void testBuiltInRules() {
540         Object[][] cases = {
541                 {"en-US", PluralRules.KEYWORD_OTHER, 0},
542                 {"en-US", PluralRules.KEYWORD_ONE, 1},
543                 {"en-US", PluralRules.KEYWORD_OTHER, 2},
544                 {"ja-JP", PluralRules.KEYWORD_OTHER, 0},
545                 {"ja-JP", PluralRules.KEYWORD_OTHER, 1},
546                 {"ja-JP", PluralRules.KEYWORD_OTHER, 2},
547                 {"ru", PluralRules.KEYWORD_MANY, 0},
548                 {"ru", PluralRules.KEYWORD_ONE, 1},
549                 {"ru", PluralRules.KEYWORD_FEW, 2}
550         };
551         for (Object[] cas : cases) {
552             ULocale locale = new ULocale((String) cas[0]);
553             PluralRules rules = factory.forLocale(locale);
554             String expectedKeyword = (String) cas[1];
555             double number = (Integer) cas[2];
556             String message = locale + " " + number;
557             // Check both as double and as FormattedNumber.
558             assertEquals(message, expectedKeyword, rules.select(number));
559             FormattedNumber fn = NumberFormatter.withLocale(locale).format(number);
560             assertEquals(message, expectedKeyword, rules.select(fn));
561         }
562     }
563 
564     @Test
565     public void testSelectTrailingZeros() {
566         UnlocalizedNumberFormatter unf = NumberFormatter.with()
567                 .precision(Precision.fixedFraction(2));
568         Object[][] cases = {
569                 // 1) locale
570                 // 2) double expected keyword
571                 // 3) formatted number expected keyword (2 fraction digits)
572                 // 4) input number
573                 {"bs",  PluralRules.KEYWORD_FEW,   PluralRules.KEYWORD_OTHER, 5.2},  // 5.2 => two, but 5.20 => other
574                 {"si",  PluralRules.KEYWORD_ONE,   PluralRules.KEYWORD_ONE,   0.0},
575                 {"si",  PluralRules.KEYWORD_ONE,   PluralRules.KEYWORD_ONE,   1.0},
576                 {"si",  PluralRules.KEYWORD_ONE,   PluralRules.KEYWORD_OTHER, 0.1},  // 0.1 => one, but 0.10 => other
577                 {"si",  PluralRules.KEYWORD_ONE,   PluralRules.KEYWORD_ONE,   0.01}, // 0.01 => one
578                 {"hsb", PluralRules.KEYWORD_FEW,   PluralRules.KEYWORD_FEW,   1.03}, // (f % 100 == 3) => few
579                 {"hsb", PluralRules.KEYWORD_FEW,   PluralRules.KEYWORD_OTHER, 1.3},  // 1.3 => few, but 1.30 => other
580         };
581         for (Object[] cas : cases) {
582             ULocale locale = new ULocale((String) cas[0]);
583             PluralRules rules = factory.forLocale(locale);
584             String expectedDoubleKeyword = (String) cas[1];
585             String expectedFormattedKeyword = (String) cas[2];
586             double number = (Double) cas[3];
587             String message = locale + " " + number;
588             // Check both as double and as FormattedNumber.
589             assertEquals(message, expectedDoubleKeyword, rules.select(number));
590             FormattedNumber fn = unf.locale(locale).format(number);
591             assertEquals(message, expectedFormattedKeyword, rules.select(fn));
592         }
593     }
594 
595     private void compareLocaleResults(String loc1, String loc2, String loc3) {
596         PluralRules rules1 = PluralRules.forLocale(new ULocale(loc1));
597         PluralRules rules2 = PluralRules.forLocale(new ULocale(loc2));
598         PluralRules rules3 = PluralRules.forLocale(new ULocale(loc3));
599         for (int value = 0; value <= 12; value++) {
600             String result1 = rules1.select(value);
601             String result2 = rules2.select(value);
602             String result3 = rules3.select(value);
603             if (!result1.equals(result2) || !result1.equals(result3)) {
604                 errln("PluralRules.select(" + value + ") does not return the same values for "
605                         + loc1 + ", " + loc2 + ", " + loc3);
606             }
607         }
608     }
609 
610     @Test
611     public void testLocaleExtension() {
612         PluralRules rules = PluralRules.forLocale(new ULocale("pt@calendar=gregorian"));
613         String key = rules.select(1);
614         assertEquals("pt@calendar=gregorian select(1)", "one", key);
615         compareLocaleResults("ar", "ar_SA", "ar_SA@calendar=gregorian");
616         compareLocaleResults("ru", "ru_UA", "ru-u-cu-RUB");
617         compareLocaleResults("fr", "fr_CH", "fr@ms=uksystem");
618     }
619 
620     @Test
621     public void testFunctionalEquivalent() {
622         // spot check
623         ULocale unknown = ULocale.createCanonical("zz_ZZ");
624         ULocale un_equiv = PluralRules.getFunctionalEquivalent(unknown, null);
625         assertEquals("unknown locales have root", ULocale.ROOT, un_equiv);
626 
627         ULocale jp_equiv = PluralRules.getFunctionalEquivalent(ULocale.JAPAN, null);
628         ULocale cn_equiv = PluralRules.getFunctionalEquivalent(ULocale.CHINA, null);
629         assertEquals("japan and china equivalent locales", jp_equiv, cn_equiv);
630 
631         boolean[] available = new boolean[1];
632         ULocale russia = ULocale.createCanonical("ru_RU");
633         ULocale ru_ru_equiv = PluralRules.getFunctionalEquivalent(russia, available);
634         assertFalse("ru_RU not listed", available[0]);
635 
636         ULocale russian = ULocale.createCanonical("ru");
637         ULocale ru_equiv = PluralRules.getFunctionalEquivalent(russian, available);
638         assertTrue("ru listed", available[0]);
639         assertEquals("ru and ru_RU equivalent locales", ru_ru_equiv, ru_equiv);
640     }
641 
642     @Test
643     public void testAvailableULocales() {
644         ULocale[] locales = factory.getAvailableULocales();
645         Set localeSet = new HashSet();
646         localeSet.addAll(Arrays.asList(locales));
647 
648         assertEquals("locales are unique in list", locales.length, localeSet.size());
649     }
650 
651     /*
652      * Test the method public static PluralRules parseDescription(String description)
653      */
654     @Test
655     public void TestParseDescription() {
656         try {
657             if (PluralRules.DEFAULT != PluralRules.parseDescription("")) {
658                 errln("PluralRules.parseDescription(String) was suppose "
659                         + "to return PluralRules.DEFAULT when String is of " + "length 0.");
660             }
661         } catch (ParseException e) {
662             errln("PluralRules.parseDescription(String) was not suppose " + "to return an exception.");
663         }
664     }
665 
666     /*
667      * Tests the method public static PluralRules createRules(String description)
668      */
669     @Test
670     public void TestCreateRules() {
671         try {
672             if (PluralRules.createRules(null) != null) {
673                 errln("PluralRules.createRules(String) was suppose to "
674                         + "return null for an invalid String descrtiption.");
675             }
676         } catch (Exception e) {
677         }
678     }
679 
680     /*
681      * Tests the method public int hashCode()
682      */
683     @Test
684     public void TestHashCode() {
685         // Bad test, breaks whenever PluralRules implementation changes.
686         // PluralRules pr = PluralRules.DEFAULT;
687         // if (106069776 != pr.hashCode()) {
688         // errln("PluralRules.hashCode() was suppose to return 106069776 " + "when PluralRules.DEFAULT.");
689         // }
690     }
691 
692     /*
693      * Tests the method public boolean equals(PluralRules rhs)
694      */
695     @Test
696     public void TestEquals() {
697         PluralRules pr = PluralRules.DEFAULT;
698 
699         if (pr.equals((PluralRules) null)) {
700             errln("PluralRules.equals(PluralRules) was supposed to return false " + "when passing null.");
701         }
702     }
703 
704     private void assertRuleValue(String rule, double value) {
705         assertRuleKeyValue("a:" + rule, "a", value);
706     }
707 
708     private void assertRuleKeyValue(String rule, String key, double value) {
709         PluralRules pr = PluralRules.createRules(rule);
710         assertEquals(rule, value, pr.getUniqueKeywordValue(key));
711     }
712 
713     /*
714      * Tests getUniqueKeywordValue()
715      */
716     @Test
717     public void TestGetUniqueKeywordValue() {
718         assertRuleKeyValue("a: n is 1", "not_defined", PluralRules.NO_UNIQUE_VALUE); // key not defined
719         assertRuleValue("n within 2..2", 2);
720         assertRuleValue("n is 1", 1);
721         assertRuleValue("n in 2..2", 2);
722         assertRuleValue("n in 3..4", PluralRules.NO_UNIQUE_VALUE);
723         assertRuleValue("n within 3..4", PluralRules.NO_UNIQUE_VALUE);
724         assertRuleValue("n is 2 or n is 2", 2);
725         assertRuleValue("n is 2 and n is 2", 2);
726         assertRuleValue("n is 2 or n is 3", PluralRules.NO_UNIQUE_VALUE);
727         assertRuleValue("n is 2 and n is 3", PluralRules.NO_UNIQUE_VALUE);
728         assertRuleValue("n is 2 or n in 2..3", PluralRules.NO_UNIQUE_VALUE);
729         assertRuleValue("n is 2 and n in 2..3", 2);
730         assertRuleKeyValue("a: n is 1", "other", PluralRules.NO_UNIQUE_VALUE); // key matches default rule
731         assertRuleValue("n in 2,3", PluralRules.NO_UNIQUE_VALUE);
732         assertRuleValue("n in 2,3..6 and n not in 2..3,5..6", 4);
733     }
734 
735     /**
736      * The version in PluralFormatUnitTest is not really a test, and it's in the wrong place anyway, so I'm putting a
737      * variant of it here.
738      */
739     @Test
740     public void TestGetSamples() {
741         Set<ULocale> uniqueRuleSet = new HashSet<>();
742         for (ULocale locale : factory.getAvailableULocales()) {
743             uniqueRuleSet.add(PluralRules.getFunctionalEquivalent(locale, null));
744         }
745         for (ULocale locale : uniqueRuleSet) {
746             if (locale.getLanguage().equals("fr") &&
747                     logKnownIssue("21322", "PluralRules::getSamples cannot distinguish 1e5 from 100000")) {
748                 continue;
749             }
750             PluralRules rules = factory.forLocale(locale);
751             logln("\nlocale: " + (locale == ULocale.ROOT ? "root" : locale.toString()) + ", rules: " + rules);
752             Set<String> keywords = rules.getKeywords();
753             for (String keyword : keywords) {
754                 Collection<Double> list = rules.getSamples(keyword);
755                 logln("keyword: " + keyword + ", samples: " + list);
756                 // with fractions, the samples can be empty and thus the list null. In that case, however, there will be
757                 // FixedDecimal values.
758                 // So patch the test for that.
759                 if (list.size() == 0) {
760                     // when the samples (meaning integer samples) are null, then then integerSamples must be, and the
761                     // decimalSamples must not be
762                     FixedDecimalSamples integerSamples = rules.getDecimalSamples(keyword, SampleType.INTEGER);
763                     FixedDecimalSamples decimalSamples = rules.getDecimalSamples(keyword, SampleType.DECIMAL);
764                     assertTrue(getAssertMessage("List is not null", locale, rules, keyword), integerSamples == null
765                             && decimalSamples != null && decimalSamples.samples.size() != 0);
766                 } else {
767                     if (!assertTrue(getAssertMessage("Test getSamples.isEmpty", locale, rules, keyword),
768                             !list.isEmpty())) {
769                         rules.getSamples(keyword);
770                     }
771                     if (rules.toString().contains(": j")) {
772                         // hack until we remove j
773                     } else {
774                         for (double value : list) {
775                             assertEquals(getAssertMessage("Match keyword", locale, rules, keyword) + "; value '"
776                                     + value + "'", keyword, rules.select(value));
777                         }
778                     }
779                 }
780             }
781 
782             assertNull(locale + ", list is null", rules.getSamples("@#$%^&*"));
783             assertNull(locale + ", list is null", rules.getSamples("@#$%^&*", SampleType.DECIMAL));
784         }
785     }
786 
787     public String getAssertMessage(String message, ULocale locale, PluralRules rules, String keyword) {
788         String ruleString = "";
789         if (keyword != null) {
790             if (keyword.equals("other")) {
791                 for (String keyword2 : rules.getKeywords()) {
792                     ruleString += " NOR " + rules.getRules(keyword2).split("@")[0];
793                 }
794             } else {
795                 String rule = rules.getRules(keyword);
796                 ruleString = rule == null ? null : rule.split("@")[0];
797             }
798             ruleString = "; rule: '" + keyword + ": " + ruleString + "'";
799             // !keyword.equals("other") ? "'; keyword: '" + keyword + "'; rule: '" + rules.getRules(keyword) + "'"
800             // : "'; keyword: '" + keyword + "'; rules: '" + rules.toString() + "'";
801         }
802         return message + (locale == null ? "" : "; locale: '" + locale + "'") + ruleString;
803     }
804 
805     /**
806      * Returns the empty set if the keyword is not defined, null if there are an unlimited number of values for the
807      * keyword, or the set of values that trigger the keyword.
808      */
809     @Test
810     public void TestGetAllKeywordValues() {
811         // data is pairs of strings, the rule, and the expected values as arguments
812         String[] data = {
813                 "other: ; a: n mod 3 is 0",
814                 "a: null",
815                 "a: n in 2..5 and n within 5..8",
816                 "a: 5",
817                 "a: n in 2..5",
818                 "a: 2,3,4,5; other: null",
819                 "a: n not in 2..5",
820                 "a: null; other: null",
821                 "a: n within 2..5",
822                 "a: 2,3,4,5; other: null",
823                 "a: n not within 2..5",
824                 "a: null; other: null",
825                 "a: n in 2..5 or n within 6..8",
826                 "a: 2,3,4,5,6,7,8", // ignore 'other' here on out, always null
827                 "a: n in 2..5 and n within 6..8",
828                 "a: null",
829                 // we no longer support 'degenerate' rules
830                 // "a: n within 2..5 and n within 6..8", "a:", // our sampling catches these
831                 // "a: n within 2..5 and n within 5..8", "a: 5", // ''
832                 // "a: n within 1..2 and n within 2..3 or n within 3..4 and n within 4..5", "a: 2,4",
833                 // "a: n mod 3 is 0 and n within 0..5", "a: 0,3",
834                 "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",
835                 "a: 2,4,6", // but not this...
836                 "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",
837                 "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", };
838         for (int i = 0; i < data.length; i += 2) {
839             String ruleDescription = data[i];
840             String result = data[i + 1];
841 
842             PluralRules p = PluralRules.createRules(ruleDescription);
843             if (p == null) { // for debugging
844                 PluralRules.createRules(ruleDescription);
845             }
846             for (String ruleResult : result.split(";")) {
847                 String[] ruleAndValues = ruleResult.split(":");
848                 String keyword = ruleAndValues[0].trim();
849                 String valueList = ruleAndValues.length < 2 ? null : ruleAndValues[1];
850                 if (valueList != null) {
851                     valueList = valueList.trim();
852                 }
853                 Collection<Double> values;
854                 if (valueList == null || valueList.length() == 0) {
855                     values = Collections.EMPTY_SET;
856                 } else if ("null".equals(valueList)) {
857                     values = null;
858                 } else {
859                     values = new TreeSet<>();
860                     for (String value : valueList.split(",")) {
861                         values.add(Double.parseDouble(value));
862                     }
863                 }
864 
865                 Collection<Double> results = p.getAllKeywordValues(keyword);
866                 assertEquals(keyword + " in " + ruleDescription, values, results == null ? null : new HashSet(results));
867 
868                 if (results != null) {
869                     try {
870                         results.add(PluralRules.NO_UNIQUE_VALUE);
871                         fail("returned set is modifiable");
872                     } catch (UnsupportedOperationException e) {
873                         // pass
874                     }
875                 }
876             }
877         }
878     }
879 
880     @Test
881     public void TestOrdinal() {
882         PluralRules pr = factory.forLocale(ULocale.ENGLISH, PluralType.ORDINAL);
883         assertEquals("PluralRules(en-ordinal).select(2)", "two", pr.select(2));
884     }
885 
886     @Test
887     public void TestBasicFraction() {
888         String[][] tests = { { "en", "one: j is 1" }, { "1", "0", "1", "one" }, { "1", "2", "1.00", "other" }, };
889         ULocale locale = null;
890         NumberFormat nf = null;
891         PluralRules pr = null;
892 
893         for (String[] row : tests) {
894             switch (row.length) {
895             case 2:
896                 locale = ULocale.forLanguageTag(row[0]);
897                 nf = NumberFormat.getInstance(locale);
898                 pr = PluralRules.createRules(row[1]);
899                 break;
900             case 4:
901                 double n = Double.parseDouble(row[0]);
902                 int minFracDigits = Integer.parseInt(row[1]);
903                 nf.setMinimumFractionDigits(minFracDigits);
904                 String expectedFormat = row[2];
905                 String expectedKeyword = row[3];
906 
907                 UFieldPosition pos = new UFieldPosition();
908                 String formatted = nf.format(1.0, new StringBuffer(), pos).toString();
909                 int countVisibleFractionDigits = pos.getCountVisibleFractionDigits();
910                 long fractionDigits = pos.getFractionDigits();
911                 String keyword = pr.select(n, countVisibleFractionDigits, fractionDigits);
912                 assertEquals("Formatted " + n + "\t" + minFracDigits, expectedFormat, formatted);
913                 assertEquals("Keyword " + n + "\t" + minFracDigits, expectedKeyword, keyword);
914                 break;
915             default:
916                 throw new RuntimeException();
917             }
918         }
919     }
920 
921     @Test
922     public void TestLimitedAndSamplesConsistency() {
923         for (ULocale locale : PluralRules.getAvailableULocales()) {
924             ULocale loc2 = PluralRules.getFunctionalEquivalent(locale, null);
925             if (!loc2.equals(locale)) {
926                 continue; // only need "unique" rules
927             }
928             for (PluralType type : PluralType.values()) {
929                 PluralRules rules = PluralRules.forLocale(locale, type);
930                 for (SampleType sampleType : SampleType.values()) {
931                     if (type == PluralType.ORDINAL) {
932                         logKnownIssue("10783", "Fix issues with isLimited vs computeLimited on ordinals");
933                         continue;
934                     }
935                     for (String keyword : rules.getKeywords()) {
936                         boolean isLimited = rules.isLimited(keyword, sampleType);
937                         boolean computeLimited = rules.computeLimited(keyword, sampleType);
938                         if (!keyword.equals("other") && !(locale.getLanguage().equals("fr") && logKnownIssue("ICU-21322", "fr plurals many case computeLimited == isLimited"))) {
939                             assertEquals(getAssertMessage("computeLimited == isLimited", locale, rules, keyword),
940                                     computeLimited, isLimited);
941                         }
942                         Collection<Double> samples = rules.getSamples(keyword, sampleType);
943                         assertNotNull(getAssertMessage("Samples must not be null", locale, rules, keyword), samples);
944                         /* FixedDecimalSamples decimalSamples = */rules.getDecimalSamples(keyword, sampleType);
945                         // assertNotNull(getAssertMessage("Decimal samples must be null if unlimited", locale, rules,
946                         // keyword), decimalSamples);
947                     }
948                 }
949             }
950         }
951     }
952 
953     @Test
954     public void TestKeywords() {
955         Set<String> possibleKeywords = new LinkedHashSet(Arrays.asList("zero", "one", "two", "few", "many", "other"));
956         Object[][][] tests = {
957                 // format is locale, explicits, then triples of keyword, status, unique value.
958                 { { "en", null }, { "one", KeywordStatus.UNIQUE, 1.0d }, { "other", KeywordStatus.UNBOUNDED, null } },
959                 { { "pl", null }, { "one", KeywordStatus.UNIQUE, 1.0d }, { "few", KeywordStatus.UNBOUNDED, null },
960                         { "many", KeywordStatus.UNBOUNDED, null },
961                         { "other", KeywordStatus.SUPPRESSED, null, KeywordStatus.UNBOUNDED, null } // note that it is
962                                                                                                    // suppressed in
963                                                                                                    // INTEGER but not
964                                                                                                    // DECIMAL
965                 }, { { "en", new HashSet<>(Arrays.asList(1.0d)) }, // check that 1 is suppressed
966                         { "one", KeywordStatus.SUPPRESSED, null }, { "other", KeywordStatus.UNBOUNDED, null } }, };
967         Output<Double> uniqueValue = new Output<>();
968         for (Object[][] test : tests) {
969             ULocale locale = new ULocale((String) test[0][0]);
970             // NumberType numberType = (NumberType) test[1];
971             Set<Double> explicits = (Set<Double>) test[0][1];
972             PluralRules pluralRules = factory.forLocale(locale);
973             LinkedHashSet<String> remaining = new LinkedHashSet(possibleKeywords);
974             for (int i = 1; i < test.length; ++i) {
975                 Object[] row = test[i];
976                 String keyword = (String) row[0];
977                 KeywordStatus statusExpected = (KeywordStatus) row[1];
978                 Double uniqueExpected = (Double) row[2];
979                 remaining.remove(keyword);
980                 KeywordStatus status = pluralRules.getKeywordStatus(keyword, 0, explicits, uniqueValue);
981                 assertEquals(getAssertMessage("Unique Value", locale, pluralRules, keyword), uniqueExpected,
982                         uniqueValue.value);
983                 assertEquals(getAssertMessage("Keyword Status", locale, pluralRules, keyword), statusExpected, status);
984                 if (row.length > 3) {
985                     statusExpected = (KeywordStatus) row[3];
986                     uniqueExpected = (Double) row[4];
987                     status = pluralRules.getKeywordStatus(keyword, 0, explicits, uniqueValue, SampleType.DECIMAL);
988                     assertEquals(getAssertMessage("Unique Value - decimal", locale, pluralRules, keyword),
989                             uniqueExpected, uniqueValue.value);
990                     assertEquals(getAssertMessage("Keyword Status - decimal", locale, pluralRules, keyword),
991                             statusExpected, status);
992                 }
993             }
994             for (String keyword : remaining) {
995                 KeywordStatus status = pluralRules.getKeywordStatus(keyword, 0, null, uniqueValue);
996                 assertEquals("Invalid keyword " + keyword, status, KeywordStatus.INVALID);
997                 assertNull("Invalid keyword " + keyword, uniqueValue.value);
998             }
999         }
1000     }
1001 
1002     // For the time being, the compact notation exponent operand `c` is an alias
1003     // for the scientific exponent operand `e` and compact notation.
1004     @Test
1005     public void testScientificPluralKeyword() {
1006         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 " +
1007                 "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, …");
1008         ULocale locale = new ULocale("fr-FR");
1009 
1010         Object[][] casesData = {
1011                 // unlocalized formatter skeleton, input, string output, plural rule keyword
1012                 {"",           0, "0", "one"},
1013                 {"scientific", 0, "0", "one"},
1014 
1015                 {"",           1, "1", "one"},
1016                 {"scientific", 1, "1", "one"},
1017 
1018                 {"",           2, "2", "other"},
1019                 {"scientific", 2, "2", "other"},
1020 
1021                 {"",           1000000, "1 000 000", "many"},
1022                 {"scientific", 1000000, "1 million", "many"},
1023 
1024                 {"",           1000001, "1 000 001", "other"},
1025                 {"scientific", 1000001, "1 million", "many"},
1026 
1027                 {"",           120000, "1 200 000", "other"},
1028                 {"scientific", 1200000, "1,2 millions", "many"},
1029 
1030                 {"",           1200001, "1 200 001", "other"},
1031                 {"scientific", 1200001, "1,2 millions", "many"},
1032 
1033                 {"",           2000000, "2 000 000", "many"},
1034                 {"scientific", 2000000, "2 millions", "many"},
1035         };
1036 
1037         for (Object[] caseDatum : casesData) {
1038             String skeleton = (String) caseDatum[0];
1039             int input = (int) caseDatum[1];
1040             // String expectedString = (String) caseDatum[2];
1041             String expectPluralRuleKeyword = (String) caseDatum[3];
1042 
1043             String actualPluralRuleKeyword =
1044                     getPluralKeyword(rules, locale, input, skeleton);
1045 
1046             assertEquals(
1047                     String.format("PluralRules select %s: %d", skeleton, input),
1048                     expectPluralRuleKeyword,
1049                     actualPluralRuleKeyword);
1050         }
1051     }
1052 
1053     @Test
1054     public void testCompactDecimalPluralKeyword() {
1055         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 " +
1056                 "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, …");
1057         ULocale locale = new ULocale("fr-FR");
1058 
1059         Object[][] casesData = {
1060                 // unlocalized formatter skeleton, input, string output, plural rule keyword
1061                 {"",             0, "0", "one"},
1062                 {"compact-long", 0, "0", "one"},
1063 
1064                 {"",             1, "1", "one"},
1065                 {"compact-long", 1, "1", "one"},
1066 
1067                 {"",             2, "2", "other"},
1068                 {"compact-long", 2, "2", "other"},
1069 
1070                 {"",             1000000, "1 000 000", "many"},
1071                 {"compact-long", 1000000, "1 million", "many"},
1072 
1073                 {"",             1000001, "1 000 001", "other"},
1074                 {"compact-long", 1000001, "1 million", "many"},
1075 
1076                 {"",             120000, "1 200 000", "other"},
1077                 {"compact-long", 1200000, "1,2 millions", "many"},
1078 
1079                 {"",             1200001, "1 200 001", "other"},
1080                 {"compact-long", 1200001, "1,2 millions", "many"},
1081 
1082                 {"",             2000000, "2 000 000", "many"},
1083                 {"compact-long", 2000000, "2 millions", "many"},
1084         };
1085 
1086         for (Object[] caseDatum : casesData) {
1087             String skeleton = (String) caseDatum[0];
1088             int input = (int) caseDatum[1];
1089             // String expectedString = (String) caseDatum[2];
1090             String expectPluralRuleKeyword = (String) caseDatum[3];
1091 
1092             String actualPluralRuleKeyword =
1093                     getPluralKeyword(rules, locale, input, skeleton);
1094 
1095             assertEquals(
1096                     String.format("PluralRules select %s: %d", skeleton, input),
1097                     expectPluralRuleKeyword,
1098                     actualPluralRuleKeyword);
1099         }
1100     }
1101 
1102     private String getPluralKeyword(PluralRules rules, ULocale locale, double number, String skeleton) {
1103         LocalizedNumberFormatter formatter =
1104                 NumberFormatter.forSkeleton(skeleton)
1105                     .locale(locale);
1106         FormattedNumber fn = formatter.format(number);
1107         String pluralKeyword = rules.select(fn);
1108         return pluralKeyword;
1109     }
1110 
1111     enum StandardPluralCategories {
1112         zero, one, two, few, many, other;
1113         /**
1114          *
1115          */
1116         private static final Set<StandardPluralCategories> ALL = Collections.unmodifiableSet(EnumSet
1117                 .allOf(StandardPluralCategories.class));
1118 
1119         /**
1120          * Return a mutable set
1121          *
1122          * @param source
1123          * @return
1124          */
1125         static final EnumSet<StandardPluralCategories> getSet(Collection<String> source) {
1126             EnumSet<StandardPluralCategories> result = EnumSet.noneOf(StandardPluralCategories.class);
1127             for (String s : source) {
1128                 result.add(StandardPluralCategories.valueOf(s));
1129             }
1130             return result;
1131         }
1132 
1133         static final Comparator<Set<StandardPluralCategories>> SHORTEST_FIRST = new Comparator<Set<StandardPluralCategories>>() {
1134             @Override
1135             public int compare(Set<StandardPluralCategories> arg0, Set<StandardPluralCategories> arg1) {
1136                 int diff = arg0.size() - arg1.size();
1137                 if (diff != 0) {
1138                     return diff;
1139                 }
1140                 // otherwise first...
1141                 // could be optimized, but we don't care here.
1142                 for (StandardPluralCategories value : ALL) {
1143                     if (arg0.contains(value)) {
1144                         if (!arg1.contains(value)) {
1145                             return 1;
1146                         }
1147                     } else if (arg1.contains(value)) {
1148                         return -1;
1149                     }
1150 
1151                 }
1152                 return 0;
1153             }
1154 
1155         };
1156     }
1157 
1158     @Test
1159     public void TestLocales() {
1160         if (false) {
1161             generateLOCALE_SNAPSHOT();
1162         }
1163         for (String test : LOCALE_SNAPSHOT) {
1164             test = test.trim();
1165             String[] parts = test.split("\\s*;\\s*");
1166             for (String localeString : parts[0].split("\\s*,\\s*")) {
1167                 ULocale locale = new ULocale(localeString);
1168                 if (factory.hasOverride(locale)) {
1169                     continue; // skip for now
1170                 }
1171                 PluralRules rules = factory.forLocale(locale);
1172                 for (int i = 1; i < parts.length; ++i) {
1173                     checkCategoriesAndExpected(localeString, parts[i], rules);
1174                 }
1175             }
1176         }
1177     }
1178 
1179     private static final Comparator<PluralRules> PLURAL_RULE_COMPARATOR = new Comparator<PluralRules>() {
1180         @Override
1181         public int compare(PluralRules o1, PluralRules o2) {
1182             return o1.compareTo(o2);
1183         }
1184     };
1185 
1186     private void generateLOCALE_SNAPSHOT() {
1187         Comparator c = new CollectionUtilities.CollectionComparator<>();
1188         Relation<Set<StandardPluralCategories>, PluralRules> setsToRules = Relation.of(
1189                 new TreeMap<Set<StandardPluralCategories>, Set<PluralRules>>(c), TreeSet.class, PLURAL_RULE_COMPARATOR);
1190         Relation<PluralRules, ULocale> data = Relation.of(
1191                 new TreeMap<PluralRules, Set<ULocale>>(PLURAL_RULE_COMPARATOR), TreeSet.class);
1192         for (ULocale locale : PluralRules.getAvailableULocales()) {
1193             PluralRules pr = PluralRules.forLocale(locale);
1194             EnumSet<StandardPluralCategories> set = getCanonicalSet(pr.getKeywords());
1195             setsToRules.put(set, pr);
1196             data.put(pr, locale);
1197         }
1198         for (Entry<Set<StandardPluralCategories>, Set<PluralRules>> entry1 : setsToRules.keyValuesSet()) {
1199             Set<StandardPluralCategories> set = entry1.getKey();
1200             Set<PluralRules> rules = entry1.getValue();
1201             System.out.println("\n        // " + set);
1202             for (PluralRules rule : rules) {
1203                 Set<ULocale> locales = data.get(rule);
1204                 System.out.print("        \"" + CollectionUtilities.join(locales, ","));
1205                 for (StandardPluralCategories spc : set) {
1206                     String keyword = spc.toString();
1207                     FixedDecimalSamples samples = rule.getDecimalSamples(keyword, SampleType.INTEGER);
1208                     System.out.print("; " + spc + ": " + samples);
1209                 }
1210                 System.out.println("\",");
1211             }
1212         }
1213     }
1214 
1215     /**
1216      * @param keywords
1217      * @return
1218      */
1219     private EnumSet<StandardPluralCategories> getCanonicalSet(Set<String> keywords) {
1220         EnumSet<StandardPluralCategories> result = EnumSet.noneOf(StandardPluralCategories.class);
1221         for (String s : keywords) {
1222             result.add(StandardPluralCategories.valueOf(s));
1223         }
1224         return result;
1225     }
1226 
1227     static final String[] LOCALE_SNAPSHOT = {
1228             // [other]
1229             "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, …",
1230 
1231             // [one, other]
1232             "am,bn,fa,gu,hi,kn,mr,zu; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1233             "ff,hy,kab; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1234             "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, …",
1235             "pt; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
1236             "si; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1237             "ak,bho,guw,ln,mg,nso,pa,ti,wa; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1238             "tzm; one: @integer 0, 1, 11~24; other: @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, …",
1239             "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, …",
1240             "pt_PT; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
1241             "da; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
1242             "is; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
1243             "mk; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
1244             "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, …",
1245 
1246             // [zero, one, other]
1247             "lag; zero: @integer 0; one: @integer 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1248             "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, …",
1249             "ksh; zero: @integer 0; one: @integer 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
1250 
1251             // [one, two, other]
1252             "iu,naq,se,sma,smi,smj,smn,sms; one: @integer 1; two: @integer 2; other: @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, …",
1253 
1254             // [one, many, other]
1255             "fr; one: @integer 0, 1; many: @integer 1000000; other: @integer 2~17, 100, 1000, 10000, 100000, …",
1256 
1257             // [one, few, other]
1258             "shi; one: @integer 0, 1; few: @integer 2~10; other: @integer 11~26, 100, 1000, 10000, 100000, 1000000, …",
1259             "mo,ro; one: @integer 1; few: @integer 0, 2~16, 102, 1002, …; other: @integer 20~35, 100, 1000, 10000, 100000, 1000000, …",
1260             "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, …",
1261 
1262             // [one, two, few, other]
1263             "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, …",
1264             "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, …",
1265 
1266             // [one, two, many, other]
1267             "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, …",
1268 
1269             // [one, few, many, other]
1270             "cs,sk; one: @integer 1; few: @integer 2~4; many: null; other: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
1271             "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",
1272             "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, …",
1273             "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, …",
1274             "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",
1275             "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",
1276 
1277             // [one, two, few, many, other]
1278             "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, …",
1279             "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, …",
1280             "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, …",
1281 
1282             // [zero, one, two, few, many, other]
1283             "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, …",
1284             "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, …",
1285             "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, …", };
1286 
1287     private <T extends Serializable> T serializeAndDeserialize(T original, Output<Integer> size) {
1288         try {
1289             ByteArrayOutputStream baos = new ByteArrayOutputStream();
1290             ObjectOutputStream ostream = new ObjectOutputStream(baos);
1291             ostream.writeObject(original);
1292             ostream.flush();
1293             byte bytes[] = baos.toByteArray();
1294             size.value = bytes.length;
1295             ObjectInputStream istream = new ObjectInputStream(new ByteArrayInputStream(bytes));
1296             T reconstituted = (T) istream.readObject();
1297             return reconstituted;
1298         } catch (IOException e) {
1299             throw new RuntimeException(e);
1300         } catch (ClassNotFoundException e) {
1301             throw new RuntimeException(e);
1302         }
1303     }
1304 
1305     @Test
1306     public void TestSerialization() {
1307         Output<Integer> size = new Output<>();
1308         int max = 0;
1309         for (ULocale locale : PluralRules.getAvailableULocales()) {
1310             PluralRules item = PluralRules.forLocale(locale);
1311             PluralRules item2 = serializeAndDeserialize(item, size);
1312             logln(locale + "\tsize:\t" + size.value);
1313             max = Math.max(max, size.value);
1314             if (!assertEquals(locale + "\tPlural rules before and after serialization", item, item2)) {
1315                 // for debugging
1316                 PluralRules item3 = serializeAndDeserialize(item, size);
1317                 item.equals(item3);
1318             }
1319         }
1320         logln("max \tsize:\t" + max);
1321     }
1322 
1323     public static class FixedDecimalHandler implements SerializableTestUtility.Handler {
1324         @Override
1325         public Object[] getTestObjects() {
1326             FixedDecimal items[] = { new FixedDecimal(3d), new FixedDecimal(3d, 2), new FixedDecimal(3.1d, 1),
1327                     new FixedDecimal(3.1d, 2), };
1328             return items;
1329         }
1330 
1331         @Override
1332         public boolean hasSameBehavior(Object a, Object b) {
1333             FixedDecimal a1 = (FixedDecimal) a;
1334             FixedDecimal b1 = (FixedDecimal) b;
1335             return a1.equals(b1);
1336         }
1337     }
1338 
1339     @Test
1340     public void TestSerial() {
1341         PluralRules s = PluralRules.forLocale(ULocale.ENGLISH);
1342         checkStreamingEquality(s);
1343     }
1344 
1345     public void checkStreamingEquality(PluralRules s) {
1346         try {
1347             ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
1348             ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOut);
1349             objectOutputStream.writeObject(s);
1350             objectOutputStream.close();
1351             byte[] contents = byteOut.toByteArray();
1352             logln(s.getClass() + ": " + showBytes(contents));
1353             ByteArrayInputStream byteIn = new ByteArrayInputStream(contents);
1354             ObjectInputStream objectInputStream = new ObjectInputStream(byteIn);
1355             Object obj = objectInputStream.readObject();
1356             assertEquals("Streamed Object equals ", s, obj);
1357         } catch (Exception e) {
1358             assertNull("TestSerial", e);
1359         }
1360     }
1361 
1362     /**
1363      * @param contents
1364      * @return
1365      */
1366     private String showBytes(byte[] contents) {
1367         StringBuilder b = new StringBuilder("[");
1368         for (int i = 0; i < contents.length; ++i) {
1369             int item = contents[i] & 0xFF;
1370             if (item >= 0x20 && item <= 0x7F) {
1371                 b.append((char) item);
1372             } else {
1373                 b.append('(').append(Utility.hex(item, 2)).append(')');
1374             }
1375         }
1376         return b.append(']').toString();
1377     }
1378 
1379     @Test
1380     public void testJavaLocaleFactory() {
1381         PluralRules rulesU0 = PluralRules.forLocale(ULocale.FRANCE);
1382         PluralRules rulesJ0 = PluralRules.forLocale(Locale.FRANCE);
1383         assertEquals("forLocale()", rulesU0, rulesJ0);
1384 
1385         PluralRules rulesU1 = PluralRules.forLocale(ULocale.FRANCE, PluralType.ORDINAL);
1386         PluralRules rulesJ1 = PluralRules.forLocale(Locale.FRANCE, PluralType.ORDINAL);
1387         assertEquals("forLocale() with type", rulesU1, rulesJ1);
1388     }
1389 
1390     @Test
1391     public void testBug20264() {
1392         String expected = "1.23400";
1393         FixedDecimal fd = new FixedDecimal(1.234, 5, 2);
1394         assertEquals("FixedDecimal toString", expected, fd.toString());
1395         Locale.setDefault(Locale.FRENCH);
1396         assertEquals("FixedDecimal toString", expected, fd.toString());
1397         Locale.setDefault(Locale.GERMAN);
1398         assertEquals("FixedDecimal toString", expected, fd.toString());
1399     }
1400 
1401     @Test
1402     public void testSelectRange() {
1403         int d1 = 102;
1404         int d2 = 201;
1405         ULocale locale = new ULocale("sl");
1406 
1407         // Locale sl has interesting data: one + two => few
1408         FormattedNumberRange range = NumberRangeFormatter.withLocale(locale).formatRange(d1, d2);
1409         PluralRules rules = PluralRules.forLocale(locale);
1410 
1411         // For testing: get plural form of first and second numbers
1412         FormattedNumber a = NumberFormatter.withLocale(locale).format(d1);
1413         FormattedNumber b = NumberFormatter.withLocale(locale).format(d2);
1414         assertEquals("First plural", "two", rules.select(a));
1415         assertEquals("Second plural", "one", rules.select(b));
1416 
1417         // Check the range plural now:
1418         String form = rules.select(range);
1419         assertEquals("Range plural", "few", form);
1420 
1421         // Test when plural ranges data is unavailable:
1422         PluralRules bare = PluralRules.createRules("a: i = 0,1");
1423         try {
1424             form = bare.select(range);
1425             fail("Expected exception");
1426         } catch (UnsupportedOperationException e) {}
1427 
1428         // However, they should not throw when no data is available for a language.
1429         PluralRules xyz = PluralRules.forLocale(new ULocale("xyz"));
1430         form = xyz.select(range);
1431         assertEquals("Fallback form", "other", form);
1432     }
1433 }
1434