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