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