blob: a96071ec18ee9ebfef5b704c8fefff61621d06ba [file] [log] [blame]
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
*******************************************************************************
* Copyright (C) 2007-2015, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.dev.test.format;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
import com.ibm.icu.dev.util.CollectionUtilities;
import com.ibm.icu.impl.Relation;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
import com.ibm.icu.number.FormattedNumber;
import com.ibm.icu.number.FormattedNumberRange;
import com.ibm.icu.number.LocalizedNumberFormatter;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.NumberRangeFormatter;
import com.ibm.icu.number.Precision;
import com.ibm.icu.number.UnlocalizedNumberFormatter;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.text.PluralRules.DecimalQuantitySamples;
import com.ibm.icu.text.PluralRules.DecimalQuantitySamplesRange;
import com.ibm.icu.text.PluralRules.FixedDecimal;
import com.ibm.icu.text.PluralRules.KeywordStatus;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.text.PluralRules.SampleType;
import com.ibm.icu.text.UFieldPosition;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.ULocale;
/**
* @author dougfelt (Doug Felt)
* @author markdavis (Mark Davis) [for fractional support]
*/
@RunWith(JUnit4.class)
public class PluralRulesTest extends TestFmwk {
PluralRulesFactory factory = PluralRulesFactory.NORMAL;
@Test
public void testOverUnderflow() {
logln(String.valueOf(Long.MAX_VALUE + 1d));
for (double[] testDouble : new double[][] {
{ 1E18, 0, 0, 1E18 }, // check overflow
{ 10000000000000.1d, 1, 1, 10000000000000d }, { -0.00001d, 1, 5, 0 }, { 1d, 0, 0, 1 },
{ 1.1d, 1, 1, 1 }, { 12345d, 0, 0, 12345 }, { 12345.678912d, 678912, 6, 12345 },
{ 12345.6789123d, 678912, 6, 12345 }, // we only go out 6 digits
{ 1E18, 0, 0, 1E18 }, // check overflow
{ 1E19, 0, 0, 1E18 }, // check overflow
}) {
FixedDecimal fd = new FixedDecimal(testDouble[0]);
assertEquals(testDouble[0] + "=doubleValue()", testDouble[0], fd.doubleValue());
assertEquals(testDouble[0] + " decimalDigits", (int) testDouble[1], fd.getDecimalDigits());
assertEquals(testDouble[0] + " visibleDecimalDigitCount", (int) testDouble[2], fd.getVisibleDecimalDigitCount());
assertEquals(testDouble[0] + " decimalDigitsWithoutTrailingZeros", (int) testDouble[1],
fd.getDecimalDigitsWithoutTrailingZeros());
assertEquals(testDouble[0] + " visibleDecimalDigitCountWithoutTrailingZeros", (int) testDouble[2],
fd.getVisibleDecimalDigitCountWithoutTrailingZeros());
assertEquals(testDouble[0] + " integerValue", (long) testDouble[3], fd.getIntegerValue());
}
for (ULocale locale : new ULocale[] { ULocale.ENGLISH, new ULocale("cy"), new ULocale("ar") }) {
PluralRules rules = factory.forLocale(locale);
assertEquals(locale + " NaN", "other", rules.select(Double.NaN));
assertEquals(locale + " ∞", "other", rules.select(Double.POSITIVE_INFINITY));
assertEquals(locale + " -∞", "other", rules.select(Double.NEGATIVE_INFINITY));
}
}
@Test
public void testSyntaxRestrictions() {
Object[][] shouldFail = {
{ "a:n in 3..10,13..19" },
// = and != always work
{ "a:n=1" },
{ "a:n=1,3" },
{ "a:n!=1" },
{ "a:n!=1,3" },
// with spacing
{ "a: n = 1" },
{ "a: n = 1, 3" },
{ "a: n != 1" },
{ "a: n != 1, 3" },
{ "a: n ! = 1" },
{ "a: n ! = 1, 3" },
{ "a: n = 1 , 3" },
{ "a: n != 1 , 3" },
{ "a: n ! = 1 , 3" },
{ "a: n = 1 .. 3" },
{ "a: n != 1 .. 3" },
{ "a: n ! = 1 .. 3" },
// more complicated
{ "a:n in 3 .. 10 , 13 .. 19" },
// singles have special exceptions
{ "a: n is 1" },
{ "a: n is not 1" },
{ "a: n not is 1", ParseException.class }, // hacked to fail
{ "a: n in 1" },
{ "a: n not in 1" },
// multiples also have special exceptions
// TODO enable the following once there is an update to CLDR
// {"a: n is 1,3", ParseException.class},
{ "a: n is not 1,3", ParseException.class }, // hacked to fail
{ "a: n not is 1,3", ParseException.class }, // hacked to fail
{ "a: n in 1,3" },
{ "a: n not in 1,3" },
// disallow not with =
{ "a: n not= 1", ParseException.class }, // hacked to fail
{ "a: n not= 1,3", ParseException.class }, // hacked to fail
// disallow double negatives
{ "a: n ! is not 1", ParseException.class },
{ "a: n ! is not 1", ParseException.class },
{ "a: n not not in 1", ParseException.class },
{ "a: n is not not 1", NumberFormatException.class },
// disallow screwy cases
{ null, NullPointerException.class }, { "djkl;", ParseException.class },
{ "a: n = 1 .", ParseException.class }, { "a: n = 1 ..", ParseException.class },
{ "a: n = 1 2", ParseException.class }, { "a: n = 1 ,", ParseException.class },
{ "a:n in 3 .. 10 , 13 .. 19 ,", ParseException.class }, };
for (Object[] shouldFailTest : shouldFail) {
String rules = (String) shouldFailTest[0];
Class exception = shouldFailTest.length < 2 ? null : (Class) shouldFailTest[1];
Class actualException = null;
try {
PluralRules.parseDescription(rules);
} catch (Exception e) {
actualException = e.getClass();
}
assertEquals("Exception " + rules, exception, actualException);
}
}
@Test
public void testSamples() {
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, …";
PluralRules test = PluralRules.createRules(description);
checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 3, 19", true,
DecimalQuantity_DualStorageBCD.fromExponentString("3"));
checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 3.50~3.53, …", false,
DecimalQuantity_DualStorageBCD.fromExponentString("3.50"));
checkOldSamples(description, test, "one", SampleType.INTEGER,
DecimalQuantity_DualStorageBCD.fromExponentString("3"),
DecimalQuantity_DualStorageBCD.fromExponentString("19"));
checkOldSamples(description, test, "one", SampleType.DECIMAL,
DecimalQuantity_DualStorageBCD.fromExponentString("3.50"),
DecimalQuantity_DualStorageBCD.fromExponentString("3.51"),
DecimalQuantity_DualStorageBCD.fromExponentString("3.52"),
DecimalQuantity_DualStorageBCD.fromExponentString("3.53"));
checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "", true, null);
checkNewSamples(description, test, "other", PluralRules.SampleType.DECIMAL, "@decimal 99.0~99.2, 999.0, …",
false, DecimalQuantity_DualStorageBCD.fromExponentString("99.0"));
checkOldSamples(description, test, "other", SampleType.INTEGER);
checkOldSamples(description, test, "other", SampleType.DECIMAL,
DecimalQuantity_DualStorageBCD.fromExponentString("99.0"),
DecimalQuantity_DualStorageBCD.fromExponentString("99.1"),
DecimalQuantity_DualStorageBCD.fromExponentString("99.2"),
DecimalQuantity_DualStorageBCD.fromExponentString("999.0"));
}
/**
* This test is for the support of X.YeZ scientific notation of numbers in
* the plural sample string.
*/
@Test
public void testSamplesWithExponent() {
String description = "one: i = 0,1 @integer 0, 1, 1e5 @decimal 0.0~1.5, 1.1e5; "
+ "many: e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5"
+ " @integer 1000000, 2e6, 3e6, 4e6, 5e6, 6e6, 7e6, … @decimal 2.1e6, 3.1e6, 4.1e6, 5.1e6, 6.1e6, 7.1e6, …; "
+ "other: @integer 2~17, 100, 1000, 10000, 100000, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, …"
+ " @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, …"
;
// Creating the PluralRules object means being able to parse numbers
// like 1e5 and 1.1e5
PluralRules test = PluralRules.createRules(description);
// Currently, 'c' is the canonical representation of numbers with suppressed exponent, and 'e'
// is an alias. The test helpers here skip 'e' for round-trip sample string parsing and formatting.
checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 0, 1, 1e5", true,
DecimalQuantity_DualStorageBCD.fromExponentString("0"));
checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 0.0~1.5, 1.1e5", true,
DecimalQuantity_DualStorageBCD.fromExponentString("0.0"));
checkNewSamples(description, test, "many", PluralRules.SampleType.INTEGER, "@integer 1000000, 2e6, 3e6, 4e6, 5e6, 6e6, 7e6, …", false,
DecimalQuantity_DualStorageBCD.fromExponentString("1000000"));
checkNewSamples(description, test, "many", PluralRules.SampleType.DECIMAL, "@decimal 2.1e6, 3.1e6, 4.1e6, 5.1e6, 6.1e6, 7.1e6, …", false,
DecimalQuantity_DualStorageBCD.fromExponentString("2.1c6"));
checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "@integer 2~17, 100, 1000, 10000, 100000, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, …", false,
DecimalQuantity_DualStorageBCD.fromExponentString("2"));
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,
DecimalQuantity_DualStorageBCD.fromExponentString("2.0"));
}
/**
* This test is for the support of X.YcZ compact notation of numbers in
* the plural sample string.
*/
@Test
public void testSamplesWithCompactNotation() {
String description = "one: i = 0,1 @integer 0, 1, 1c5 @decimal 0.0~1.5, 1.1c5; "
+ "many: c = 0 and i != 0 and i % 1000000 = 0 and v = 0 or c != 0..5"
+ " @integer 1000000, 2c6, 3c6, 4c6, 5c6, 6c6, 7c6, … @decimal 2.1c6, 3.1c6, 4.1c6, 5.1c6, 6.1c6, 7.1c6, …; "
+ "other: @integer 2~17, 100, 1000, 10000, 100000, 2c5, 3c5, 4c5, 5c5, 6c5, 7c5, …"
+ " @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, …"
;
// Creating the PluralRules object means being able to parse numbers
// like 1c5 and 1.1c5.
// Note: Since `c` is currently an alias to `e`, the toString() of
// FixedDecimal will return "1e5" even when input is "1c5".
PluralRules test = PluralRules.createRules(description);
checkNewSamples(description, test, "one", PluralRules.SampleType.INTEGER, "@integer 0, 1, 1c5", true,
DecimalQuantity_DualStorageBCD.fromExponentString("0"));
checkNewSamples(description, test, "one", PluralRules.SampleType.DECIMAL, "@decimal 0.0~1.5, 1.1c5", true,
DecimalQuantity_DualStorageBCD.fromExponentString("0.0"));
checkNewSamples(description, test, "many", PluralRules.SampleType.INTEGER, "@integer 1000000, 2c6, 3c6, 4c6, 5c6, 6c6, 7c6, …", false,
DecimalQuantity_DualStorageBCD.fromExponentString("1000000"));
checkNewSamples(description, test, "many", PluralRules.SampleType.DECIMAL, "@decimal 2.1c6, 3.1c6, 4.1c6, 5.1c6, 6.1c6, 7.1c6, …", false,
DecimalQuantity_DualStorageBCD.fromExponentString("2.1c6"));
checkNewSamples(description, test, "other", PluralRules.SampleType.INTEGER, "@integer 2~17, 100, 1000, 10000, 100000, 2c5, 3c5, 4c5, 5c5, 6c5, 7c5, …", false,
DecimalQuantity_DualStorageBCD.fromExponentString("2"));
checkNewSamples(description, test, "other", PluralRules.SampleType.DECIMAL, "@decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 2.1c5, 3.1c5, 4.1c5, 5.1c5, 6.1c5, 7.1c5, …", false,
DecimalQuantity_DualStorageBCD.fromExponentString("2.0"));
}
public void checkOldSamples(String description, PluralRules rules, String keyword, SampleType sampleType,
DecimalQuantity... expected) {
Collection<DecimalQuantity> oldSamples = rules.getDecimalQuantitySamples(keyword, sampleType);
// Collect actual (oldSamples) and expected (expectedSamplesList) into the
// same concrete collection for comparison purposes.
ArrayList<DecimalQuantity> oldSamplesList = new ArrayList(oldSamples);
ArrayList<DecimalQuantity> expectedSamplesList = new ArrayList(Arrays.asList(expected));
if (!assertEquals("getOldSamples; " + keyword + "; " + description, expectedSamplesList,
oldSamplesList)) {
rules.getSamples(keyword, sampleType);
}
}
public void checkNewSamples(String description, PluralRules test, String keyword, SampleType sampleType,
String samplesString, boolean isBounded, DecimalQuantity firstInRange) {
String title = description + ", " + sampleType;
DecimalQuantitySamples samples = test.getDecimalSamples(keyword, sampleType);
if (samples != null) {
// For now, skip round-trip formatting test when samples string uses
// 'e' instead of 'c' for compact notation.
// We are skipping tests for 'e' by replacing 'e' with 'c' in exponent
// notation.
Pattern p = Pattern.compile("(\\d+)(e)([-]?\\d+)");
Matcher m = p.matcher(samplesString);
if (m.find()) {
samplesString = m.replaceAll("$1c$3");
}
assertEquals("samples; " + title, samplesString, samples.toString());
assertEquals("bounded; " + title, isBounded, samples.bounded);
assertEquals("first; " + title, firstInRange, samples.samples.iterator().next().start);
}
assertEquals("limited: " + title, isBounded, test.isLimited(keyword, sampleType));
}
private static final String[] parseTestData = { "a: n is 1", "a:1", "a: n mod 10 is 2", "a:2,12,22",
"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",
"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",
"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",
"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",
"a: n mod 10 in 2..3 or n mod 10 is 5", "a:2,3,5,12,13,15,22,23,25",
"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",
"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",
"a: n mod 2 is 1 and n is not 3 and n within 1..11", "a:1,5,7,9,11",
"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",
"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",
"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",
"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",
"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",
"a:2,3,5,6,7,13,15,16,17", "a: n in 2..6,3..7", "a:2,3,4,5,6,7", };
private String[] getTargetStrings(String targets) {
List list = new ArrayList(50);
String[] valSets = Utility.split(targets, ';');
for (int i = 0; i < valSets.length; ++i) {
String[] temp = Utility.split(valSets[i], ':');
String key = temp[0].trim();
String[] vals = Utility.split(temp[1], ',');
for (int j = 0; j < vals.length; ++j) {
String valString = vals[j].trim();
int val = Integer.parseInt(valString);
while (list.size() <= val) {
list.add(null);
}
if (list.get(val) != null) {
fail("test data error, key: " + list.get(val) + " already set for: " + val);
}
list.set(val, key);
}
}
String[] result = (String[]) list.toArray(new String[list.size()]);
for (int i = 0; i < result.length; ++i) {
if (result[i] == null) {
result[i] = "other";
}
}
return result;
}
private void checkTargets(PluralRules rules, String[] targets) {
for (int i = 0; i < targets.length; ++i) {
assertEquals("value " + i, targets[i], rules.select(i));
}
}
@Test
public void testParseEmpty() throws ParseException {
PluralRules rules = PluralRules.parseDescription("a:n");
assertEquals("empty", "a", rules.select(0));
}
@Test
public void testParsing() {
for (int i = 0; i < parseTestData.length; i += 2) {
String pattern = parseTestData[i];
String expected = parseTestData[i + 1];
logln("pattern[" + i + "] " + pattern);
try {
PluralRules rules = PluralRules.createRules(pattern);
String[] targets = getTargetStrings(expected);
checkTargets(rules, targets);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
}
private static String[][] operandTestData = { { "a: n 3", "FAIL" },
{ "a: n=1,2; b: n != 3..5; c:n!=5", "a:1,2; b:6,7; c:3,4" },
{ "a: n=1,2; b: n!=3..5; c:n!=5", "a:1,2; b:6,7; c:3,4" },
{ "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" },
{ "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" },
{ "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
// rules
{ "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
// visible
// digit
{ "one: j is 0", "one: 0; other: 0.0, 1.0, 3" }, // Last visible digit
// one → n is 1; few → n in 2..4;
};
@Test
public void testOperands() {
for (String[] pair : operandTestData) {
String pattern = pair[0].trim();
String categoriesAndExpected = pair[1].trim();
// logln("pattern[" + i + "] " + pattern);
boolean FAIL_EXPECTED = categoriesAndExpected.equalsIgnoreCase("fail");
try {
logln(pattern);
PluralRules rules = PluralRules.createRules(pattern);
if (FAIL_EXPECTED) {
assertNull("Should fail with 'null' return.", rules);
} else {
logln(rules == null ? "null rules" : rules.toString());
checkCategoriesAndExpected(pattern, categoriesAndExpected, rules);
}
} catch (Exception e) {
if (!FAIL_EXPECTED) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
}
}
@Test
public void testUniqueRules() {
main: for (ULocale locale : factory.getAvailableULocales()) {
PluralRules rules = factory.forLocale(locale);
Map<String, PluralRules> keywordToRule = new HashMap<>();
Collection<DecimalQuantitySamples> samples = new LinkedHashSet<>();
for (String keyword : rules.getKeywords()) {
for (SampleType sampleType : SampleType.values()) {
DecimalQuantitySamples samples2 = rules.getDecimalSamples(keyword, sampleType);
if (samples2 != null) {
samples.add(samples2);
}
}
if (keyword.equals("other")) {
continue;
}
String rules2 = keyword + ":" + rules.getRules(keyword);
PluralRules singleRule = PluralRules.createRules(rules2);
if (singleRule == null) {
errln("Can't generate single rule for " + rules2);
PluralRules.createRules(rules2); // for debugging
continue main;
}
keywordToRule.put(keyword, singleRule);
}
Map<DecimalQuantity, String> collisionTest = new LinkedHashMap();
for (DecimalQuantitySamples sample3 : samples) {
Set<DecimalQuantitySamplesRange> samples2 = sample3.getSamples();
if (samples2 == null) {
continue;
}
for (DecimalQuantitySamplesRange sample : samples2) {
for (int i = 0; i < 1; ++i) {
DecimalQuantity item = i == 0 ? sample.start : sample.end;
collisionTest.clear();
for (Entry<String, PluralRules> entry : keywordToRule.entrySet()) {
PluralRules rule = entry.getValue();
String foundKeyword = rule.select(item);
if (foundKeyword.equals("other")) {
continue;
}
String old = collisionTest.get(item);
if (old != null) {
errln(locale + "\tNon-unique rules: " + item + " => " + old + " & " + foundKeyword);
rule.select(item);
} else {
collisionTest.put(item, foundKeyword);
}
}
}
}
}
}
}
private void checkCategoriesAndExpected(String title1, String categoriesAndExpected, PluralRules rules) {
for (String categoryAndExpected : categoriesAndExpected.split("\\s*;\\s*")) {
String[] categoryFromExpected = categoryAndExpected.split("\\s*:\\s*");
String expected = categoryFromExpected[0];
for (String value : categoryFromExpected[1].split("\\s*,\\s*")) {
if (value.startsWith("@") || value.equals("…") || value.equals("null")) {
continue;
}
String[] values = value.split("\\s*~\\s*");
checkValue(title1, rules, expected, values[0]);
if (values.length > 1) {
checkValue(title1, rules, expected, values[1]);
}
}
}
}
public void checkValue(String title1, PluralRules rules, String expected, String value) {
DecimalQuantity dqNum = DecimalQuantity_DualStorageBCD.fromExponentString(value);
String result = rules.select(dqNum);
ULocale locale = null;
assertEquals(getAssertMessage(title1, locale, rules, expected) + "; value: " + value, expected, result);
}
/**
* Check the testing helper method checkValue(), which parses a plural
* rule's sample string as a {@link FormattedNumber} in order to call
* {@code PluralRules.select(FormattedNumber)}, which in turn can support
* the exponent in plural sample numbers like 1e6 and 2.8c3.
*/
@Test
public void testCheckValue() {
String ruleString =
"many: e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5"
+ " @integer 1000000, 1e6, 2e6, 3e6, 4e6, 5e6, 6e6, …"
+ " @decimal 1.0000001e6, 1.1e6, 2.0000001e6, 2.1e6, 3.0000001e6, 3.1e6, …; "
+ "one: i = 1 and v = 0"
+ " @integer 1; "
+ "other: "
+ " @integer 0, 2~16, 100, 1000, 10000, 100000, 1e3, 2e3, 3e3, 4e3, 5e3, 6e3, …"
+ " @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, …";
PluralRules rules = PluralRules.createRules(ruleString);
Object[][] casesData = {
// expected category, value string
{"many", "1000000"},
{"many", "1e6"},
{"many", "1.1e6"},
{"one", "1"},
{"other", "0"},
{"other", "1e5"},
{"other", "100000"},
{"other", "0.0"},
{"other", "100000.0"},
{"other", "1000000.0"}
};
for (Object[] caseDatum : casesData) {
String expCategory = (String) caseDatum[0];
String inputValueStr = (String) caseDatum[1];
String msg = "checkValue(" + inputValueStr + ")";
checkValue(msg, rules, expCategory, inputValueStr);
}
}
private static String[][] equalityTestData = {
// once we add fractions, we had to retract the "test all possibilities" for equality,
// so we only have a limited set of equality tests now.
{ "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" },
{ "b:n not in 5;", "b: n != 5" },
// { "a: n is 5",
// "a: n in 2..6 and n not in 2..4 and n is not 6" },
// { "a: n in 2..3",
// "a: n is 2 or n is 3",
// "a: n is 3 and n in 2..5 or n is 2" },
// { "a: n is 12; b:n mod 10 in 2..3",
// "b: n mod 10 in 2..3 and n is not 12; a: n in 12..12",
// "b: n is 13; a: n is 12; b: n mod 10 is 2 or n mod 10 is 3" },
};
private static String[][] inequalityTestData = { { "a: n mod 8 is 3", "a: n mod 7 is 3" },
{ "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" },
// the following are currently inequal, but we may make them equal in the future.
{ "a: n in 2..5", "a: n in 2..4,5" }, };
private void compareEquality(String id, Object[] objects, boolean shouldBeEqual) {
for (int i = 0; i < objects.length; ++i) {
Object lhs = objects[i];
int start = shouldBeEqual ? i : i + 1;
for (int j = start; j < objects.length; ++j) {
Object rhs = objects[j];
if (rhs == null || shouldBeEqual != lhs.equals(rhs)) {
String msg = shouldBeEqual ? "should be equal" : "should not be equal";
fail(id + " " + msg + " (" + i + ", " + j + "):\n " + lhs + "\n " + rhs);
}
// assertEquals("obj " + i + " and " + j, lhs, rhs);
}
}
}
private void compareEqualityTestSets(String[][] sets, boolean shouldBeEqual) {
for (int i = 0; i < sets.length; ++i) {
String[] patterns = sets[i];
PluralRules[] rules = new PluralRules[patterns.length];
for (int j = 0; j < patterns.length; ++j) {
rules[j] = PluralRules.createRules(patterns[j]);
}
compareEquality("test " + i, rules, shouldBeEqual);
}
}
@Test
public void testEquality() {
compareEqualityTestSets(equalityTestData, true);
}
@Test
public void testInequality() {
compareEqualityTestSets(inequalityTestData, false);
}
@Test
public void testBuiltInRules() {
Object[][] cases = {
{"en-US", PluralRules.KEYWORD_OTHER, 0},
{"en-US", PluralRules.KEYWORD_ONE, 1},
{"en-US", PluralRules.KEYWORD_OTHER, 2},
{"ja-JP", PluralRules.KEYWORD_OTHER, 0},
{"ja-JP", PluralRules.KEYWORD_OTHER, 1},
{"ja-JP", PluralRules.KEYWORD_OTHER, 2},
{"ru", PluralRules.KEYWORD_MANY, 0},
{"ru", PluralRules.KEYWORD_ONE, 1},
{"ru", PluralRules.KEYWORD_FEW, 2}
};
for (Object[] cas : cases) {
ULocale locale = new ULocale((String) cas[0]);
PluralRules rules = factory.forLocale(locale);
String expectedKeyword = (String) cas[1];
double number = (Integer) cas[2];
String message = locale + " " + number;
// Check both as double and as FormattedNumber.
assertEquals(message, expectedKeyword, rules.select(number));
FormattedNumber fn = NumberFormatter.withLocale(locale).format(number);
assertEquals(message, expectedKeyword, rules.select(fn));
}
}
@Test
public void testSelectTrailingZeros() {
UnlocalizedNumberFormatter unf = NumberFormatter.with()
.precision(Precision.fixedFraction(2));
Object[][] cases = {
// 1) locale
// 2) double expected keyword
// 3) formatted number expected keyword (2 fraction digits)
// 4) input number
{"bs", PluralRules.KEYWORD_FEW, PluralRules.KEYWORD_OTHER, 5.2}, // 5.2 => two, but 5.20 => other
{"si", PluralRules.KEYWORD_ONE, PluralRules.KEYWORD_ONE, 0.0},
{"si", PluralRules.KEYWORD_ONE, PluralRules.KEYWORD_ONE, 1.0},
{"si", PluralRules.KEYWORD_ONE, PluralRules.KEYWORD_OTHER, 0.1}, // 0.1 => one, but 0.10 => other
{"si", PluralRules.KEYWORD_ONE, PluralRules.KEYWORD_ONE, 0.01}, // 0.01 => one
{"hsb", PluralRules.KEYWORD_FEW, PluralRules.KEYWORD_FEW, 1.03}, // (f % 100 == 3) => few
{"hsb", PluralRules.KEYWORD_FEW, PluralRules.KEYWORD_OTHER, 1.3}, // 1.3 => few, but 1.30 => other
};
for (Object[] cas : cases) {
ULocale locale = new ULocale((String) cas[0]);
PluralRules rules = factory.forLocale(locale);
String expectedDoubleKeyword = (String) cas[1];
String expectedFormattedKeyword = (String) cas[2];
double number = (Double) cas[3];
String message = locale + " " + number;
// Check both as double and as FormattedNumber.
assertEquals(message, expectedDoubleKeyword, rules.select(number));
FormattedNumber fn = unf.locale(locale).format(number);
assertEquals(message, expectedFormattedKeyword, rules.select(fn));
}
}
private void compareLocaleResults(String loc1, String loc2, String loc3) {
PluralRules rules1 = PluralRules.forLocale(new ULocale(loc1));
PluralRules rules2 = PluralRules.forLocale(new ULocale(loc2));
PluralRules rules3 = PluralRules.forLocale(new ULocale(loc3));
for (int value = 0; value <= 12; value++) {
String result1 = rules1.select(value);
String result2 = rules2.select(value);
String result3 = rules3.select(value);
if (!result1.equals(result2) || !result1.equals(result3)) {
errln("PluralRules.select(" + value + ") does not return the same values for "
+ loc1 + ", " + loc2 + ", " + loc3);
}
}
}
@Test
public void testLocaleExtension() {
PluralRules rules = PluralRules.forLocale(new ULocale("pt@calendar=gregorian"));
String key = rules.select(1);
assertEquals("pt@calendar=gregorian select(1)", "one", key);
compareLocaleResults("ar", "ar_SA", "ar_SA@calendar=gregorian");
compareLocaleResults("ru", "ru_UA", "ru-u-cu-RUB");
compareLocaleResults("fr", "fr_CH", "fr@ms=uksystem");
}
@Test
public void testFunctionalEquivalent() {
// spot check
ULocale unknown = ULocale.createCanonical("zz_ZZ");
ULocale un_equiv = PluralRules.getFunctionalEquivalent(unknown, null);
assertEquals("unknown locales have root", ULocale.ROOT, un_equiv);
ULocale jp_equiv = PluralRules.getFunctionalEquivalent(ULocale.JAPAN, null);
ULocale cn_equiv = PluralRules.getFunctionalEquivalent(ULocale.CHINA, null);
assertEquals("japan and china equivalent locales", jp_equiv, cn_equiv);
boolean[] available = new boolean[1];
ULocale russia = ULocale.createCanonical("ru_RU");
ULocale ru_ru_equiv = PluralRules.getFunctionalEquivalent(russia, available);
assertFalse("ru_RU not listed", available[0]);
ULocale russian = ULocale.createCanonical("ru");
ULocale ru_equiv = PluralRules.getFunctionalEquivalent(russian, available);
assertTrue("ru listed", available[0]);
assertEquals("ru and ru_RU equivalent locales", ru_ru_equiv, ru_equiv);
}
@Test
public void testAvailableULocales() {
ULocale[] locales = factory.getAvailableULocales();
Set localeSet = new HashSet();
localeSet.addAll(Arrays.asList(locales));
assertEquals("locales are unique in list", locales.length, localeSet.size());
}
/*
* Test the method public static PluralRules parseDescription(String description)
*/
@Test
public void TestParseDescription() {
try {
if (PluralRules.DEFAULT != PluralRules.parseDescription("")) {
errln("PluralRules.parseDescription(String) was suppose "
+ "to return PluralRules.DEFAULT when String is of " + "length 0.");
}
} catch (ParseException e) {
errln("PluralRules.parseDescription(String) was not suppose " + "to return an exception.");
}
}
/*
* Tests the method public static PluralRules createRules(String description)
*/
@Test
public void TestCreateRules() {
try {
if (PluralRules.createRules(null) != null) {
errln("PluralRules.createRules(String) was suppose to "
+ "return null for an invalid String descrtiption.");
}
} catch (Exception e) {
}
}
/*
* Tests the method public int hashCode()
*/
@Test
public void TestHashCode() {
// Bad test, breaks whenever PluralRules implementation changes.
// PluralRules pr = PluralRules.DEFAULT;
// if (106069776 != pr.hashCode()) {
// errln("PluralRules.hashCode() was suppose to return 106069776 " + "when PluralRules.DEFAULT.");
// }
}
/*
* Tests the method public boolean equals(PluralRules rhs)
*/
@Test
public void TestEquals() {
PluralRules pr = PluralRules.DEFAULT;
if (pr.equals((PluralRules) null)) {
errln("PluralRules.equals(PluralRules) was supposed to return false " + "when passing null.");
}
}
private void assertRuleValue(String rule, DecimalQuantity value) {
assertRuleKeyValue("a:" + rule, "a", value);
}
private void assertRuleKeyValue(String rule, String key, DecimalQuantity value) {
PluralRules pr = PluralRules.createRules(rule);
// as a DecimalQuantity
assertEquals(rule, value, pr.getUniqueKeywordDecimalQuantityValue(key));
// as a double
double expDouble = value.equals(PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY) ?
PluralRules.NO_UNIQUE_VALUE : value.toDouble();
assertEquals(rule, expDouble, pr.getUniqueKeywordValue(key));
}
/*
* Tests getUniqueKeywordValue()
*/
@Test
public void TestGetUniqueKeywordValue() {
LocalizedNumberFormatter fmtr = NumberFormatter.withLocale(ULocale.ROOT);
assertRuleKeyValue("a: n is 1", "not_defined", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY); // key not defined
assertRuleValue("n within 2..2", new DecimalQuantity_DualStorageBCD(2));
assertRuleValue("n is 1", new DecimalQuantity_DualStorageBCD(1));
assertRuleValue("n in 2..2", new DecimalQuantity_DualStorageBCD(2));
assertRuleValue("n in 3..4", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
assertRuleValue("n within 3..4", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
assertRuleValue("n is 2 or n is 2", new DecimalQuantity_DualStorageBCD(2));
assertRuleValue("n is 2 and n is 2", new DecimalQuantity_DualStorageBCD(2));
assertRuleValue("n is 2 or n is 3", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
assertRuleValue("n is 2 and n is 3", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
assertRuleValue("n is 2 or n in 2..3", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
assertRuleValue("n is 2 and n in 2..3", new DecimalQuantity_DualStorageBCD(2));
assertRuleKeyValue("a: n is 1", "other", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY); // key matches default rule
assertRuleValue("n in 2,3", PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
assertRuleValue("n in 2,3..6 and n not in 2..3,5..6", new DecimalQuantity_DualStorageBCD(4));
}
/**
* The version in PluralFormatUnitTest is not really a test, and it's in the wrong place anyway, so I'm putting a
* variant of it here.
*
* Using the double API for getting plural samples, assert all samples match the keyword
* they are listed under, for all locales.
*
* Specifically, iterate over all locales, get plural rules for the locale, iterate over every rule,
* then iterate over every sample in the rule, parse sample to a number (double), use that number
* as an input to .select() for the rules object, and assert the actual return plural keyword matches
* what we expect based on the plural rule string.
*/
@Test
public void TestGetSamples() {
Set<ULocale> uniqueRuleSet = new HashSet<>();
for (ULocale locale : factory.getAvailableULocales()) {
uniqueRuleSet.add(PluralRules.getFunctionalEquivalent(locale, null));
}
for (ULocale locale : uniqueRuleSet) {
PluralRules rules = factory.forLocale(locale);
logln("\nlocale: " + (locale == ULocale.ROOT ? "root" : locale.toString()) + ", rules: " + rules);
Set<String> keywords = rules.getKeywords();
for (String keyword : keywords) {
Collection<Double> list = rules.getSamples(keyword);
logln("keyword: " + keyword + ", samples: " + list);
// with fractions, the samples can be empty and thus the list null. In that case, however, there will be
// FixedDecimal values.
// So patch the test for that.
if (list.size() == 0) {
// when the samples (meaning integer samples) are null, then then integerSamples must be, and the
// decimalSamples must not be
DecimalQuantitySamples integerSamples = rules.getDecimalSamples(keyword, SampleType.INTEGER);
DecimalQuantitySamples decimalSamples = rules.getDecimalSamples(keyword, SampleType.DECIMAL);
assertTrue(getAssertMessage("List is not null", locale, rules, keyword), integerSamples == null
&& decimalSamples != null && decimalSamples.samples.size() != 0);
} else {
if (!assertTrue(getAssertMessage("Test getSamples.isEmpty", locale, rules, keyword),
!list.isEmpty())) {
rules.getSamples(keyword);
}
if (rules.toString().contains(": j")) {
// hack until we remove j
} else {
for (double value : list) {
assertEquals(getAssertMessage("Match keyword", locale, rules, keyword) + "; value '"
+ value + "'", keyword, rules.select(value));
}
}
}
}
assertNull(locale + ", list is null", rules.getSamples("@#$%^&*"));
assertNull(locale + ", list is null", rules.getSamples("@#$%^&*", SampleType.DECIMAL));
}
}
/**
* This replicates the setup of TestGetSamples(), but parses samples as DecimalQuantity instead of double.
*
* Using the DecimalQuantity API for getting plural samples, assert all samples match the keyword
* they are listed under, for all locales.
*
* Specifically, iterate over all locales, get plural rules for the locale, iterate over every rule,
* then iterate over every sample in the rule, parse sample to a number (DecimalQuantity), use that number
* as an input to .select() for the rules object, and assert the actual return plural keyword matches
* what we expect based on the plural rule string.
*/
@Test
public void TestGetDecimalQuantitySamples() {
Set<ULocale> uniqueRuleSet = new HashSet<>();
for (ULocale locale : factory.getAvailableULocales()) {
uniqueRuleSet.add(PluralRules.getFunctionalEquivalent(locale, null));
}
for (ULocale locale : uniqueRuleSet) {
PluralRules rules = factory.forLocale(locale);
logln("\nlocale: " + (locale == ULocale.ROOT ? "root" : locale.toString()) + ", rules: " + rules);
Set<String> keywords = rules.getKeywords();
for (String keyword : keywords) {
Collection<DecimalQuantity> list = rules.getDecimalQuantitySamples(keyword);
logln("keyword: " + keyword + ", samples: " + list);
// with fractions, the samples can be empty and thus the list null. In that case, however, there will be
// FixedDecimal values.
// So patch the test for that.
if (list.size() == 0) {
// when the samples (meaning integer samples) are null, then then integerSamples must be, and the
// decimalSamples must not be
DecimalQuantitySamples integerSamples = rules.getDecimalSamples(keyword, SampleType.INTEGER);
DecimalQuantitySamples decimalSamples = rules.getDecimalSamples(keyword, SampleType.DECIMAL);
assertTrue(getAssertMessage("List is not null", locale, rules, keyword), integerSamples == null
&& decimalSamples != null && decimalSamples.samples.size() != 0);
} else {
if (!assertTrue(getAssertMessage("Test getSamples.isEmpty", locale, rules, keyword),
!list.isEmpty())) {
rules.getDecimalQuantitySamples(keyword);
}
if (rules.toString().contains(": j")) {
// hack until we remove j
} else {
for (DecimalQuantity value : list) {
assertEquals(getAssertMessage("Match keyword", locale, rules, keyword) + "; value '"
+ value + "'", keyword, rules.select(value));
}
}
}
}
assertNull(locale + ", list is null", rules.getDecimalQuantitySamples("@#$%^&*"));
assertNull(locale + ", list is null", rules.getDecimalQuantitySamples("@#$%^&*", SampleType.DECIMAL));
}
}
/**
* Test addSamples (Java) / getSamplesFromString (C++) to ensure the expansion of plural rule sample range
* expands to a sequence of sample numbers that is incremented as the right scale.
*
* Do this for numbers with fractional digits but no exponent.
*/
@Test
public void testGetOrAddSamplesFromString() {
PluralRules rules = PluralRules.createRules("testkeyword: e != 0 @decimal 2.0~4.0, …");
Set<String> keywords = rules.getKeywords();
assertTrue("At least parse the test keyword in the test rule string", 0 < keywords.size());
String expKeyword = "testkeyword";
Collection<DecimalQuantity> list = rules.getDecimalQuantitySamples(expKeyword, SampleType.DECIMAL);
String[] expDqStrs = {
"2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9",
"3.0", "3.1", "3.2", "3.3", "3.4", "3.5", "3.6", "3.7", "3.8", "3.9",
"4.0"
};
assertEquals("Number of parsed samples from test string incorrect", expDqStrs.length, list.size());
ArrayList<DecimalQuantity> actSamples = new ArrayList<>(list);
for (int i = 0; i < list.size(); i++) {
String expDqStr = expDqStrs[i];
DecimalQuantity sample = actSamples.get(i);
String sampleStr = sample.toExponentString();
assertEquals("Expansion of sample range to sequence of sample values should increment at the right scale",
expDqStr, sampleStr);
}
}
/**
* Test addSamples (Java) / getSamplesFromString (C++) to ensure the expansion of plural rule sample range
* expands to a sequence of sample numbers that is incremented as the right scale.
*
* Do this for numbers written in a notation that has an exponent, for which the number is an
* integer (also as defined in the UTS 35 spec for the plural operands) but whose representation
* has fractional digits in the significand written before the exponent.
*/
@Test
public void testGetOrAddSamplesFromStringCompactNotation() {
PluralRules rules = PluralRules.createRules("testkeyword: e != 0 @decimal 2.0c6~4.0c6, …");
Set<String> keywords = rules.getKeywords();
assertTrue("At least parse the test keyword in the test rule string", 0 < keywords.size());
String expKeyword = "testkeyword";
Collection<DecimalQuantity> list = rules.getDecimalQuantitySamples(expKeyword, SampleType.DECIMAL);
String[] expDqStrs = {
"2.0c6", "2.1c6", "2.2c6", "2.3c6", "2.4c6", "2.5c6", "2.6c6", "2.7c6", "2.8c6", "2.9c6",
"3.0c6", "3.1c6", "3.2c6", "3.3c6", "3.4c6", "3.5c6", "3.6c6", "3.7c6", "3.8c6", "3.9c6",
"4.0c6"
};
assertEquals("Number of parsed samples from test string incorrect", expDqStrs.length, list.size());
ArrayList<DecimalQuantity> actSamples = new ArrayList<>(list);
for (int i = 0; i < list.size(); i++) {
String expDqStr = expDqStrs[i];
DecimalQuantity sample = actSamples.get(i);
String sampleStr = sample.toExponentString();
assertEquals("Expansion of sample range to sequence of sample values should increment at the right scale",
expDqStr, sampleStr);
}
}
public String getAssertMessage(String message, ULocale locale, PluralRules rules, String keyword) {
String ruleString = "";
if (keyword != null) {
if (keyword.equals("other")) {
for (String keyword2 : rules.getKeywords()) {
ruleString += " NOR " + rules.getRules(keyword2).split("@")[0];
}
} else {
String rule = rules.getRules(keyword);
ruleString = rule == null ? null : rule.split("@")[0];
}
ruleString = "; rule: '" + keyword + ": " + ruleString + "'";
// !keyword.equals("other") ? "'; keyword: '" + keyword + "'; rule: '" + rules.getRules(keyword) + "'"
// : "'; keyword: '" + keyword + "'; rules: '" + rules.toString() + "'";
}
return message + (locale == null ? "" : "; locale: '" + locale + "'") + ruleString;
}
/**
* Returns the empty set if the keyword is not defined, null if there are an unlimited number of values for the
* keyword, or the set of values that trigger the keyword.
*/
@Test
public void TestGetAllKeywordValues() {
// data is pairs of strings, the rule, and the expected values as arguments
String[] data = {
"other: ; a: n mod 3 is 0",
"a: null",
"a: n in 2..5 and n within 5..8",
"a: 5",
"a: n in 2..5",
"a: 2,3,4,5; other: null",
"a: n not in 2..5",
"a: null; other: null",
"a: n within 2..5",
"a: 2,3,4,5; other: null",
"a: n not within 2..5",
"a: null; other: null",
"a: n in 2..5 or n within 6..8",
"a: 2,3,4,5,6,7,8", // ignore 'other' here on out, always null
"a: n in 2..5 and n within 6..8",
"a: null",
// we no longer support 'degenerate' rules
// "a: n within 2..5 and n within 6..8", "a:", // our sampling catches these
// "a: n within 2..5 and n within 5..8", "a: 5", // ''
// "a: n within 1..2 and n within 2..3 or n within 3..4 and n within 4..5", "a: 2,4",
// "a: n mod 3 is 0 and n within 0..5", "a: 0,3",
"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",
"a: 2,4,6", // but not this...
"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",
"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", };
for (int i = 0; i < data.length; i += 2) {
String ruleDescription = data[i];
String result = data[i + 1];
PluralRules p = PluralRules.createRules(ruleDescription);
if (p == null) { // for debugging
PluralRules.createRules(ruleDescription);
}
for (String ruleResult : result.split(";")) {
String[] ruleAndValues = ruleResult.split(":");
String keyword = ruleAndValues[0].trim();
String valueList = ruleAndValues.length < 2 ? null : ruleAndValues[1];
if (valueList != null) {
valueList = valueList.trim();
}
Collection<DecimalQuantity> values;
if (valueList == null || valueList.length() == 0) {
values = Collections.EMPTY_SET;
} else if ("null".equals(valueList)) {
values = null;
} else {
values = new LinkedHashSet<>();
for (String value : valueList.split(",")) {
values.add(DecimalQuantity_DualStorageBCD.fromExponentString(value));
}
}
Collection<DecimalQuantity> results = p.getAllKeywordDecimalQuantityValues(keyword);
// Convert DecimalQuantity using a 1:1 conversion to String for comparison purposes
Set<String> valuesForComparison = new HashSet<>();
if (values != null) {
for (DecimalQuantity dq : values) {
valuesForComparison.add(dq.toExponentString());
}
}
Set<String> resultsForComparison = new HashSet<>();
if (results != null) {
for (DecimalQuantity dq : results) {
resultsForComparison.add(dq.toExponentString());
}
}
assertEquals(keyword + " in " + ruleDescription,
values == null ? null : valuesForComparison,
results == null ? null : resultsForComparison
);
if (results != null) {
try {
results.add(PluralRules.NO_UNIQUE_VALUE_DECIMAL_QUANTITY);
fail("returned set is modifiable");
} catch (UnsupportedOperationException e) {
// pass
}
}
}
}
}
@Test
public void TestOrdinal() {
PluralRules pr = factory.forLocale(ULocale.ENGLISH, PluralType.ORDINAL);
assertEquals("PluralRules(en-ordinal).select(2)", "two", pr.select(2));
}
@Test
public void TestBasicFraction() {
String[][] tests = { { "en", "one: j is 1" }, { "1", "0", "1", "one" }, { "1", "2", "1.00", "other" }, };
ULocale locale = null;
NumberFormat nf = null;
PluralRules pr = null;
for (String[] row : tests) {
switch (row.length) {
case 2:
locale = ULocale.forLanguageTag(row[0]);
nf = NumberFormat.getInstance(locale);
pr = PluralRules.createRules(row[1]);
break;
case 4:
double n = Double.parseDouble(row[0]);
int minFracDigits = Integer.parseInt(row[1]);
nf.setMinimumFractionDigits(minFracDigits);
String expectedFormat = row[2];
String expectedKeyword = row[3];
UFieldPosition pos = new UFieldPosition();
String formatted = nf.format(1.0, new StringBuffer(), pos).toString();
int countVisibleFractionDigits = pos.getCountVisibleFractionDigits();
long fractionDigits = pos.getFractionDigits();
String keyword = pr.select(n, countVisibleFractionDigits, fractionDigits);
assertEquals("Formatted " + n + "\t" + minFracDigits, expectedFormat, formatted);
assertEquals("Keyword " + n + "\t" + minFracDigits, expectedKeyword, keyword);
break;
default:
throw new RuntimeException();
}
}
}
@Test
public void TestLimitedAndSamplesConsistency() {
for (ULocale locale : PluralRules.getAvailableULocales()) {
ULocale loc2 = PluralRules.getFunctionalEquivalent(locale, null);
if (!loc2.equals(locale)) {
continue; // only need "unique" rules
}
for (PluralType type : PluralType.values()) {
PluralRules rules = PluralRules.forLocale(locale, type);
for (SampleType sampleType : SampleType.values()) {
if (type == PluralType.ORDINAL) {
logKnownIssue("10783", "Fix issues with isLimited vs computeLimited on ordinals");
continue;
}
for (String keyword : rules.getKeywords()) {
boolean isLimited = rules.isLimited(keyword, sampleType);
boolean computeLimited = rules.computeLimited(keyword, sampleType);
Collection<DecimalQuantity> samples = rules.getDecimalQuantitySamples(keyword, sampleType);
assertNotNull(getAssertMessage("Samples must not be null", locale, rules, keyword), samples);
rules.getDecimalSamples(keyword, sampleType);
// assertNotNull(getAssertMessage("Decimal samples must be null if unlimited", locale, rules,
// keyword), decimalSamples);
}
}
}
}
}
@Test
public void TestKeywords() {
Set<String> possibleKeywords = new LinkedHashSet(Arrays.asList("zero", "one", "two", "few", "many", "other"));
DecimalQuantity ONE_INTEGER = DecimalQuantity_DualStorageBCD.fromExponentString("1");
Object[][][] tests = {
// format is locale, explicits, then triples of keyword, status, unique value.
{ { "en", null }, { "one", KeywordStatus.UNIQUE, ONE_INTEGER }, { "other", KeywordStatus.UNBOUNDED, null } },
{ { "pl", null }, { "one", KeywordStatus.UNIQUE, ONE_INTEGER }, { "few", KeywordStatus.UNBOUNDED, null },
{ "many", KeywordStatus.UNBOUNDED, null },
{ "other", KeywordStatus.SUPPRESSED, null, KeywordStatus.UNBOUNDED, null } // note that it is
// suppressed in
// INTEGER but not
// DECIMAL
}, { { "en", new HashSet<>(Arrays.asList(ONE_INTEGER)) }, // check that 1 is suppressed
{ "one", KeywordStatus.SUPPRESSED, null }, { "other", KeywordStatus.UNBOUNDED, null } }, };
Output<DecimalQuantity> uniqueValue = new Output<>();
for (Object[][] test : tests) {
ULocale locale = new ULocale((String) test[0][0]);
// NumberType numberType = (NumberType) test[1];
Set<DecimalQuantity> explicits = (Set<DecimalQuantity>) test[0][1];
PluralRules pluralRules = factory.forLocale(locale);
LinkedHashSet<String> remaining = new LinkedHashSet(possibleKeywords);
for (int i = 1; i < test.length; ++i) {
Object[] row = test[i];
String keyword = (String) row[0];
KeywordStatus statusExpected = (KeywordStatus) row[1];
DecimalQuantity uniqueExpected = (DecimalQuantity) row[2];
remaining.remove(keyword);
KeywordStatus status = pluralRules.getKeywordStatus(keyword, 0, explicits, uniqueValue);
assertEquals(getAssertMessage("Unique Value", locale, pluralRules, keyword), uniqueExpected,
uniqueValue.value);
assertEquals(getAssertMessage("Keyword Status", locale, pluralRules, keyword), statusExpected, status);
if (row.length > 3) {
statusExpected = (KeywordStatus) row[3];
uniqueExpected = (DecimalQuantity) row[4];
status = pluralRules.getKeywordStatus(keyword, 0, explicits, uniqueValue, SampleType.DECIMAL);
assertEquals(getAssertMessage("Unique Value - decimal", locale, pluralRules, keyword),
uniqueExpected, uniqueValue.value);
assertEquals(getAssertMessage("Keyword Status - decimal", locale, pluralRules, keyword),
statusExpected, status);
}
}
for (String keyword : remaining) {
KeywordStatus status = pluralRules.getKeywordStatus(keyword, 0, null, uniqueValue);
assertEquals("Invalid keyword " + keyword, status, KeywordStatus.INVALID);
assertNull("Invalid keyword " + keyword, uniqueValue.value);
}
}
}
// For the time being, the compact notation exponent operand `c` is an alias
// for the scientific exponent operand `e` and compact notation.
/**
* Test the proper plural rule keyword selection given an input number that is
* already formatted into scientific notation. This exercises the `e` plural operand
* for the formatted number.
*/
@Test
public void testScientificPluralKeyword() {
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 " +
"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, …");
ULocale locale = new ULocale("fr-FR");
Object[][] casesData = {
// unlocalized formatter skeleton, input, string output, plural rule keyword
{"", 0, "0", "one"},
{"scientific", 0, "0", "one"},
{"", 1, "1", "one"},
{"scientific", 1, "1", "one"},
{"", 2, "2", "other"},
{"scientific", 2, "2", "other"},
{"", 1000000, "1 000 000", "many"},
{"scientific", 1000000, "1 million", "many"},
{"", 1000001, "1 000 001", "other"},
{"scientific", 1000001, "1 million", "many"},
{"", 120000, "1 200 000", "other"},
{"scientific", 1200000, "1,2 millions", "many"},
{"", 1200001, "1 200 001", "other"},
{"scientific", 1200001, "1,2 millions", "many"},
{"", 2000000, "2 000 000", "many"},
{"scientific", 2000000, "2 millions", "many"},
};
for (Object[] caseDatum : casesData) {
String skeleton = (String) caseDatum[0];
int input = (int) caseDatum[1];
// String expectedString = (String) caseDatum[2];
String expectPluralRuleKeyword = (String) caseDatum[3];
String actualPluralRuleKeyword =
getPluralKeyword(rules, locale, input, skeleton);
assertEquals(
String.format("PluralRules select %s: %d", skeleton, input),
expectPluralRuleKeyword,
actualPluralRuleKeyword);
}
}
/**
* Test the proper plural rule keyword selection given an input number that is
* already formatted into compact notation. This exercises the `c` plural operand
* for the formatted number.
*/
@Test
public void testCompactDecimalPluralKeyword() {
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 " +
"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, …");
ULocale locale = new ULocale("fr-FR");
Object[][] casesData = {
// unlocalized formatter skeleton, input, string output, plural rule keyword
{"", 0, "0", "one"},
{"compact-long", 0, "0", "one"},
{"", 1, "1", "one"},
{"compact-long", 1, "1", "one"},
{"", 2, "2", "other"},
{"compact-long", 2, "2", "other"},
{"", 1000000, "1 000 000", "many"},
{"compact-long", 1000000, "1 million", "many"},
{"", 1000001, "1 000 001", "other"},
{"compact-long", 1000001, "1 million", "many"},
{"", 120000, "1 200 000", "other"},
{"compact-long", 1200000, "1,2 millions", "many"},
{"", 1200001, "1 200 001", "other"},
{"compact-long", 1200001, "1,2 millions", "many"},
{"", 2000000, "2 000 000", "many"},
{"compact-long", 2000000, "2 millions", "many"},
};
for (Object[] caseDatum : casesData) {
String skeleton = (String) caseDatum[0];
int input = (int) caseDatum[1];
// String expectedString = (String) caseDatum[2];
String expectPluralRuleKeyword = (String) caseDatum[3];
String actualPluralRuleKeyword =
getPluralKeyword(rules, locale, input, skeleton);
assertEquals(
String.format("PluralRules select %s: %d", skeleton, input),
expectPluralRuleKeyword,
actualPluralRuleKeyword);
}
}
private String getPluralKeyword(PluralRules rules, ULocale locale, double number, String skeleton) {
LocalizedNumberFormatter formatter =
NumberFormatter.forSkeleton(skeleton)
.locale(locale);
FormattedNumber fn = formatter.format(number);
String pluralKeyword = rules.select(fn);
return pluralKeyword;
}
@Test
public void testDoubleValue() {
Object[][] intCasesData = {
// source number, expected double value
{-101, -101.0},
{-100, -100.0},
{-1, -1.0},
{0, 0.0},
{1, 1.0},
{100, 100.0}
};
for (Object[] caseDatum : intCasesData) {
double inputNum = (int) caseDatum[0];
double expVal = (double) caseDatum[1];
FixedDecimal fd = new FixedDecimal(inputNum);
assertEquals("FixedDecimal.doubleValue() for " + inputNum, expVal, fd.doubleValue());
}
Object[][] doubleCasesData = {
// source number, expected double value
{-0.0, -0.0},
{0.1, 0.1},
{1.999, 1.999},
{2.0, 2.0},
{100.001, 100.001}
};
for (Object[] caseDatum : doubleCasesData) {
double inputNum = (double) caseDatum[0];
double expVal = (double) caseDatum[1];
FixedDecimal fd = new FixedDecimal(inputNum);
assertEquals("FixedDecimal.doubleValue() for " + inputNum, expVal, fd.doubleValue());
}
}
@Test
public void testLongValue() {
Object[][] intCasesData = {
// source number, expected double value
{-101, 101},
{-100, 100},
{-1, 1},
{0, 0},
{1, 1},
{100, 100}
};
for (Object[] caseDatum : intCasesData) {
long inputNum = (int) caseDatum[0];
long expVal = (int) caseDatum[1];
FixedDecimal fd = new FixedDecimal(inputNum);
assertEquals("FixedDecimal.longValue() for " + inputNum, expVal, fd.longValue());
}
Object[][] doubleCasesData = {
// source number, expected double value
{-0.0, 0},
{0.1, 0},
{1.999, 1},
{2.0, 2},
{100.001, 100}
};
for (Object[] caseDatum : doubleCasesData) {
double inputNum = (double) caseDatum[0];
long expVal = (int) caseDatum[1];
FixedDecimal fd = new FixedDecimal(inputNum);
assertEquals("FixedDecimal.longValue() for " + inputNum, expVal, fd.longValue());
}
}
enum StandardPluralCategories {
zero, one, two, few, many, other;
/**
*
*/
private static final Set<StandardPluralCategories> ALL = Collections.unmodifiableSet(EnumSet
.allOf(StandardPluralCategories.class));
/**
* Return a mutable set
*
* @param source
* @return
*/
static final EnumSet<StandardPluralCategories> getSet(Collection<String> source) {
EnumSet<StandardPluralCategories> result = EnumSet.noneOf(StandardPluralCategories.class);
for (String s : source) {
result.add(StandardPluralCategories.valueOf(s));
}
return result;
}
static final Comparator<Set<StandardPluralCategories>> SHORTEST_FIRST = new Comparator<Set<StandardPluralCategories>>() {
@Override
public int compare(Set<StandardPluralCategories> arg0, Set<StandardPluralCategories> arg1) {
int diff = arg0.size() - arg1.size();
if (diff != 0) {
return diff;
}
// otherwise first...
// could be optimized, but we don't care here.
for (StandardPluralCategories value : ALL) {
if (arg0.contains(value)) {
if (!arg1.contains(value)) {
return 1;
}
} else if (arg1.contains(value)) {
return -1;
}
}
return 0;
}
};
}
@Test
public void TestLocales() {
// This test will fail when the locale snapshot gets out of sync with the real CLDR data.
// In that case, temporarily use "if (true)",
// copy & paste the output into the initializer above,
// and revert to "if (false)" for normal testing.
if (false) {
generateLOCALE_SNAPSHOT();
return;
}
for (String test : LOCALE_SNAPSHOT) {
test = test.trim();
String[] parts = test.split("\\s*;\\s*");
for (String localeString : parts[0].split("\\s*,\\s*")) {
ULocale locale = new ULocale(localeString);
if (factory.hasOverride(locale)) {
continue; // skip for now
}
PluralRules rules = factory.forLocale(locale);
for (int i = 1; i < parts.length; ++i) {
checkCategoriesAndExpected(localeString, parts[i], rules);
}
}
}
}
private static final Comparator<PluralRules> PLURAL_RULE_COMPARATOR = new Comparator<PluralRules>() {
@Override
public int compare(PluralRules o1, PluralRules o2) {
return o1.compareTo(o2);
}
};
private void generateLOCALE_SNAPSHOT() {
Comparator c = new CollectionUtilities.CollectionComparator<>();
Relation<Set<StandardPluralCategories>, PluralRules> setsToRules = Relation.of(
new TreeMap<Set<StandardPluralCategories>, Set<PluralRules>>(c), TreeSet.class, PLURAL_RULE_COMPARATOR);
Relation<PluralRules, ULocale> data = Relation.of(
new TreeMap<PluralRules, Set<ULocale>>(PLURAL_RULE_COMPARATOR), TreeSet.class);
for (ULocale locale : PluralRules.getAvailableULocales()) {
PluralRules pr = PluralRules.forLocale(locale);
EnumSet<StandardPluralCategories> set = getCanonicalSet(pr.getKeywords());
setsToRules.put(set, pr);
data.put(pr, locale);
}
for (Entry<Set<StandardPluralCategories>, Set<PluralRules>> entry1 : setsToRules.keyValuesSet()) {
Set<StandardPluralCategories> set = entry1.getKey();
Set<PluralRules> rules = entry1.getValue();
System.out.println("\n // " + set);
for (PluralRules rule : rules) {
Set<ULocale> locales = data.get(rule);
System.out.print(" \"" + CollectionUtilities.join(locales, ","));
for (StandardPluralCategories spc : set) {
String keyword = spc.toString();
DecimalQuantitySamples samples = rule.getDecimalSamples(keyword, SampleType.INTEGER);
System.out.print("; " + spc + ": " + samples);
}
System.out.println("\",");
}
}
}
/**
* @param keywords
* @return
*/
private EnumSet<StandardPluralCategories> getCanonicalSet(Set<String> keywords) {
EnumSet<StandardPluralCategories> result = EnumSet.noneOf(StandardPluralCategories.class);
for (String s : keywords) {
result.add(StandardPluralCategories.valueOf(s));
}
return result;
}
static final String[] LOCALE_SNAPSHOT = {
// Last regenerated 2022-08-21 as part of CLDR 42 alpha2 integration
// [other]
"bm,bo,dz,hnj,id,ig,ii,ja,jbo,jv,kde,kea,km,ko,lkt,lo,ms,my,nqo,osa,root,sah,ses,sg,su,th,to,tpi,vi,wo,yo,yue,zh; other: @integer 0~15, 100, 1000, 10000, 100000, 1000000, …",
// [one, other]
"am,as,bn,doi,fa,gu,hi,kn,pcm,zu; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
"ff,hy,kab; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
"ast,de,en,et,fi,fy,gl,ia,io,lij,nl,sc,scn,sv,sw,ur,yi; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
"si; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
"ak,bho,guw,ln,mg,nso,pa,ti,wa; one: @integer 0, 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
"tzm; one: @integer 0, 1, 11~24; other: @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, …",
"af,an,asa,az,bal,bem,bez,bg,brx,ce,cgg,chr,ckb,dv,ee,el,eo,eu,fo,fur,gsw,ha,haw,hu,jgo,jmc,ka,kaj,kcg,kk,kkj,kl,ks,ksb,ku,ky,lb,lg,mas,mgo,ml,mn,mr,nah,nb,nd,ne,nn,nnh,no,nr,ny,nyn,om,or,os,pap,ps,rm,rof,rwk,saq,sd,sdh,seh,sn,so,sq,ss,ssy,st,syr,ta,te,teo,tig,tk,tn,tr,ts,ug,uz,ve,vo,vun,wae,xh,xog; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
"da; one: @integer 1; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
"is; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
"mk; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …",
"ceb,fil; one: @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, …; other: @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, …",
// [zero, one, other]
"lag; zero: @integer 0; one: @integer 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
"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, …",
"ksh; zero: @integer 0; one: @integer 1; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, …",
// [one, two, other]
"he; one: @integer 1; two: @integer 2; other: @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, …",
"iu,naq,sat,se,sma,smi,smj,smn,sms; one: @integer 1; two: @integer 2; other: @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, …",
// [one, few, other]
"shi; one: @integer 0, 1; few: @integer 2~10; other: @integer 11~26, 100, 1000, 10000, 100000, 1000000, …",
"ro; one: @integer 1; few: @integer 0, 2~16, 101, 1001, …; other: @integer 20~35, 100, 1000, 10000, 100000, 1000000, …",
"bs,hr,sr,sr_Latn; one: @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …; few: @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …; other: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
// [one, many, other]
"fr; one: @integer 0, 1; many: @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …; other: @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …",
"pt; one: @integer 0, 1; many: @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …; other: @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …",
"ca,it,pt_PT,vec; one: @integer 1; many: @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …",
"es; one: @integer 1; many: @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …; other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …",
// [one, two, few, other]
"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, …",
"dsb,hsb; one: @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, …; two: @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, …; few: @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, …; other: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
"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, …",
// [one, few, many, other]
"cs,sk; one: @integer 1; few: @integer 2~4; many: null; other: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
"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",
"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, …",
"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",
"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",
// [one, two, few, many, other]
"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, …",
"mt; one: @integer 1; two: @integer 2; few: @integer 0, 3~10, 103~109, 1003, …; many: @integer 11~19, 111~117, 1011, …; other: @integer 20~35, 100, 1000, 10000, 100000, 1000000, …",
"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, …",
"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, …",
// [zero, one, two, few, many, other]
"kw; zero: @integer 0; one: @integer 1; two: @integer 2, 22, 42, 62, 82, 102, 122, 142, 1000, 10000, 100000, …; few: @integer 3, 23, 43, 63, 83, 103, 123, 143, 1003, …; many: @integer 21, 41, 61, 81, 101, 121, 141, 161, 1001, …; other: @integer 4~19, 100, 1004, 1000000, …",
"ar,ars; zero: @integer 0; one: @integer 1; two: @integer 2; few: @integer 3~10, 103~110, 1003, …; many: @integer 11~26, 111, 1011, …; other: @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, …",
"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, …", };
private <T extends Serializable> T serializeAndDeserialize(T original, Output<Integer> size) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream ostream = new ObjectOutputStream(baos);
ostream.writeObject(original);
ostream.flush();
byte bytes[] = baos.toByteArray();
size.value = bytes.length;
ObjectInputStream istream = new ObjectInputStream(new ByteArrayInputStream(bytes));
T reconstituted = (T) istream.readObject();
return reconstituted;
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
@Test
public void TestSerialization() {
Output<Integer> size = new Output<>();
int max = 0;
for (ULocale locale : PluralRules.getAvailableULocales()) {
PluralRules item = PluralRules.forLocale(locale);
PluralRules item2 = serializeAndDeserialize(item, size);
logln(locale + "\tsize:\t" + size.value);
max = Math.max(max, size.value);
if (!assertEquals(locale + "\tPlural rules before and after serialization", item, item2)) {
// for debugging
PluralRules item3 = serializeAndDeserialize(item, size);
item.equals(item3);
}
}
logln("max \tsize:\t" + max);
}
public static class FixedDecimalHandler implements SerializableTestUtility.Handler {
@Override
public Object[] getTestObjects() {
FixedDecimal items[] = { new FixedDecimal(3d), new FixedDecimal(3d, 2), new FixedDecimal(3.1d, 1),
new FixedDecimal(3.1d, 2), };
return items;
}
@Override
public boolean hasSameBehavior(Object a, Object b) {
FixedDecimal a1 = (FixedDecimal) a;
FixedDecimal b1 = (FixedDecimal) b;
return a1.equals(b1);
}
}
@Test
public void TestSerial() {
PluralRules s = PluralRules.forLocale(ULocale.ENGLISH);
checkStreamingEquality(s);
}
public void checkStreamingEquality(PluralRules s) {
try {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOut);
objectOutputStream.writeObject(s);
objectOutputStream.close();
byte[] contents = byteOut.toByteArray();
logln(s.getClass() + ": " + showBytes(contents));
ByteArrayInputStream byteIn = new ByteArrayInputStream(contents);
ObjectInputStream objectInputStream = new ObjectInputStream(byteIn);
Object obj = objectInputStream.readObject();
assertEquals("Streamed Object equals ", s, obj);
} catch (Exception e) {
assertNull("TestSerial", e);
}
}
/**
* @param contents
* @return
*/
private String showBytes(byte[] contents) {
StringBuilder b = new StringBuilder("[");
for (int i = 0; i < contents.length; ++i) {
int item = contents[i] & 0xFF;
if (item >= 0x20 && item <= 0x7F) {
b.append((char) item);
} else {
b.append('(').append(Utility.hex(item, 2)).append(')');
}
}
return b.append(']').toString();
}
@Test
public void testJavaLocaleFactory() {
PluralRules rulesU0 = PluralRules.forLocale(ULocale.FRANCE);
PluralRules rulesJ0 = PluralRules.forLocale(Locale.FRANCE);
assertEquals("forLocale()", rulesU0, rulesJ0);
PluralRules rulesU1 = PluralRules.forLocale(ULocale.FRANCE, PluralType.ORDINAL);
PluralRules rulesJ1 = PluralRules.forLocale(Locale.FRANCE, PluralType.ORDINAL);
assertEquals("forLocale() with type", rulesU1, rulesJ1);
}
@Test
public void testBug20264() {
String expected = "1.23400";
FixedDecimal fd = new FixedDecimal(1.234, 5, 2);
assertEquals("FixedDecimal toString", expected, fd.toString());
Locale.setDefault(Locale.FRENCH);
assertEquals("FixedDecimal toString", expected, fd.toString());
Locale.setDefault(Locale.GERMAN);
assertEquals("FixedDecimal toString", expected, fd.toString());
}
@Test
public void testSelectRange() {
int d1 = 102;
int d2 = 201;
ULocale locale = new ULocale("sl");
// Locale sl has interesting data: one + two => few
FormattedNumberRange range = NumberRangeFormatter.withLocale(locale).formatRange(d1, d2);
PluralRules rules = PluralRules.forLocale(locale);
// For testing: get plural form of first and second numbers
FormattedNumber a = NumberFormatter.withLocale(locale).format(d1);
FormattedNumber b = NumberFormatter.withLocale(locale).format(d2);
assertEquals("First plural", "two", rules.select(a));
assertEquals("Second plural", "one", rules.select(b));
// Check the range plural now:
String form = rules.select(range);
assertEquals("Range plural", "few", form);
// Test when plural ranges data is unavailable:
PluralRules bare = PluralRules.createRules("a: i = 0,1");
try {
form = bare.select(range);
fail("Expected exception");
} catch (UnsupportedOperationException e) {}
// However, they should not throw when no data is available for a language.
PluralRules xyz = PluralRules.forLocale(new ULocale("xyz"));
form = xyz.select(range);
assertEquals("Fallback form", "other", form);
}
}