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