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