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