import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.math.BigDecimal;
import com.ibm.icu.math.MathContext;
+import com.ibm.icu.text.PluralRules.NumberInfo;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.ULocale;
private StringBuffer subformat(int number, StringBuffer result, FieldPosition fieldPosition,
boolean isNegative, boolean isInteger, boolean parseAttr) {
if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
- return subformat(currencyPluralInfo.select(number), result, fieldPosition, isNegative,
+ // compute the plural category from the digitList plus other settings
+ return subformat(getPluralCategory(number), result, fieldPosition, isNegative,
isInteger, parseAttr);
} else {
return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
}
}
+ /**
+ * This is ugly, but don't see a better way to do it without major restructuring of the code.
+ */
+ private String getPluralCategory(double number) {
+ // get the visible fractions and the number of fraction digits.
+ int fractionalDigitsInDigitList = digitList.count - digitList.decimalAt;
+ int v;
+ long f;
+ int maxFractionalDigits;
+ int minFractionalDigits;
+ if (useSignificantDigits) {
+ maxFractionalDigits = maxSignificantDigits - digitList.decimalAt;
+ minFractionalDigits = minSignificantDigits - digitList.decimalAt;
+ if (minFractionalDigits < 0) {
+ minFractionalDigits = 0;
+ }
+ if (maxFractionalDigits < 0) {
+ maxFractionalDigits = 0;
+ }
+ } else {
+ maxFractionalDigits = getMaximumFractionDigits();
+ minFractionalDigits = getMinimumFractionDigits();
+ }
+ v = fractionalDigitsInDigitList;
+ if (v < minFractionalDigits) {
+ v = minFractionalDigits;
+ } else if (v > maxFractionalDigits) {
+ v = maxFractionalDigits;
+ }
+ f = 0;
+ if (v > 0) {
+ for (int i = digitList.decimalAt; i < digitList.count; ++i) {
+ f *= 10;
+ f += digitList.digits[i];
+ }
+ for (int i = v; i < fractionalDigitsInDigitList; ++i) {
+ f *= 10;
+ }
+ }
+ return currencyPluralInfo.select(new NumberInfo(number, v, f));
+ }
+
private StringBuffer subformat(double number, StringBuffer result, FieldPosition fieldPosition,
boolean isNegative,
boolean isInteger, boolean parseAttr) {
if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
- return subformat(currencyPluralInfo.select(number), result, fieldPosition, isNegative,
+ // compute the plural category from the digitList plus other settings
+ return subformat(getPluralCategory(number), result, fieldPosition, isNegative,
isInteger, parseAttr);
} else {
return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
import com.ibm.icu.impl.PluralRulesLoader;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.util.Output;
+import com.ibm.icu.util.StringTokenizer;
import com.ibm.icu.util.ULocale;
/**
* keyword = <identifier>
* condition = and_condition ('or' and_condition)*
* and_condition = relation ('and' relation)*
- * relation = is_relation | in_relation | within_relation | 'n' <EOL>
- * is_relation = expr 'is' ('not')? value
- * in_relation = expr ('not')? 'in' range_list
- * within_relation = expr ('not')? 'within' range_list
- * expr = ('n' | 'i' | 'f' | 'v') ('mod' value)?
+ * relation = not? expr not? rel not? range_list
+ * expr = ('n' | 'i' | 'f' | 'v' | 't') (mod value)?
+ * not = 'not' | '!'
+ * rel = 'in' | 'is' | '=' | '≠' | 'within'
+ * mod = 'mod' | '%'
* range_list = (range | value) (',' range_list)*
- * value = digit+ ('.' digit+)?
+ * value = digit+
* digit = 0|1|2|3|4|5|6|7|8|9
* range = value'..'value
* </pre>
- *
+ * <p>Each <b>not</b> term inverts the meaning; however, there should not be more than one of them.</p>
* <p>
- * The i, f, and v values are defined as follows:
+ * The i, f, t, and v values are defined as follows:
* </p>
* <ul>
* <li>i to be the integer digits.</li>
* <li>f to be the visible fractional digits, as an integer.</li>
+ * <li>t to be the visible fractional digits—without trailing zeros—as an integer.</li>
* <li>v to be the number of visible fraction digits.</li>
* <li>j is defined to only match integers. That is j is 3 fails if v != 0 (eg for 3.1 or 3.0).</li>
* </ul>
public final PluralRules forLocale(ULocale locale) {
return forLocale(locale, PluralType.CARDINAL);
}
-
+
/**
* Returns the locales for which there is plurals data.
*
* @internal
*/
public abstract ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable);
-
+
/**
* Returns the default factory.
* @deprecated This API is ICU internal only.
public static PluralRulesLoader getDefaultFactory() {
return PluralRulesLoader.loader;
}
-
+
/**
* Returns whether or not there are overrides.
* @deprecated This API is ICU internal only.
n,
i,
f,
+ t,
v,
j;
}
private final double source;
private final int visibleFractionDigitCount;
private final long fractionalDigits;
+ private final long fractionalDigitsWithoutTrailingZeros;
private final long intValue;
private final boolean hasIntegerValue;
private final boolean isNegative;
// throw new IllegalArgumentException();
// }
// }
+ if (f == 0) {
+ fractionalDigitsWithoutTrailingZeros = 0;
+ } else {
+ long fdwtz = f;
+ while ((fdwtz%10) == 0) {
+ fdwtz /= 10;
+ }
+ fractionalDigitsWithoutTrailingZeros = fdwtz;
+ }
}
/**
* @deprecated This API is ICU internal only.
*/
public NumberInfo(double n, int v) {
- // Ugly, but for samples we don't care.
this(n,v,getFractionalDigits(n, v));
}
* @deprecated This API is ICU internal only.
*/
public static int decimals(double n) {
- // Ugly, but for samples we don't care.
+ // Ugly...
String temp = String.valueOf(n);
return temp.endsWith(".0") ? 0 : temp.length() - temp.indexOf('.') - 1;
}
default: return source;
case i: return intValue;
case f: return fractionalDigits;
+ case t: return fractionalDigitsWithoutTrailingZeros;
case v: return visibleFractionDigitCount;
}
}
public String getConstraint();
}
+ static class SimpleTokenizer {
+ static final UnicodeSet BREAK_AND_IGNORE = new UnicodeSet(0x09, 0x0a, 0x0c, 0x0d, 0x20, 0x20).freeze();
+ static final UnicodeSet BREAK_AND_KEEP = new UnicodeSet('!', '!', '%', '%', '=', '=', '≠', '≠').freeze();
+ static String[] split(String source) {
+ int last = -1;
+ List<String> result = new ArrayList<String>();
+ for (int i = 0; i < source.length(); ++i) {
+ char ch = source.charAt(i);
+ if (BREAK_AND_IGNORE.contains(ch)) {
+ if (last >= 0) {
+ result.add(source.substring(last,i));
+ last = -1;
+ }
+ } else if (BREAK_AND_KEEP.contains(ch)) {
+ if (last >= 0) {
+ result.add(source.substring(last,i));
+ }
+ result.add(source.substring(i,i+1));
+ last = -1;
+ } else if (last < 0) {
+ last = i;
+ }
+ }
+ if (last >= 0) {
+ result.add(source.substring(last));
+ }
+ return result.toArray(new String[result.size()]);
+ }
+ }
+
// /*
// * A list of rules to apply in order.
// */
/*
* syntax:
- * condition : or_condition
- * and_condition
- * or_condition : and_condition 'or' condition
- * and_condition : relation
- * relation 'and' relation
- * relation : is_relation
- * in_relation
- * within_relation
- * 'n' EOL
- * is_relation : expr 'is' value
- * expr 'is' 'not' value
- * in_relation : expr 'in' range
- * expr 'not' 'in' range
- * within_relation : expr 'within' range
- * expr 'not' 'within' range
- * expr : 'n'
- * 'n' 'mod' value
- * value : digit+
- * digit : 0|1|2|3|4|5|6|7|8|9
- * range : value'..'value
+ * condition : or_condition
+ * and_condition
+ * or_condition : and_condition 'or' condition
+ * and_condition : relation
+ * relation 'and' relation
+ * relation : in_relation
+ * within_relation
+ * in_relation : not? expr not? in not? range
+ * within_relation : not? expr not? 'within' not? range
+ * not : 'not'
+ * '!'
+ * expr : 'n'
+ * 'n' mod value
+ * mod : 'mod'
+ * '%'
+ * in : 'in'
+ * 'is'
+ * '='
+ * '≠'
+ * value : digit+
+ * digit : 0|1|2|3|4|5|6|7|8|9
+ * range : value'..'value
*/
private static Constraint parseConstraint(String description)
throws ParseException {
Constraint newConstraint = NO_CONSTRAINT;
String condition = and_together[j].trim();
- String[] tokens = Utility.splitWhitespace(condition);
+ String[] tokens = SimpleTokenizer.split(condition);
int mod = 0;
boolean inRange = true;
boolean integersOnly = true;
double lowBound = Long.MAX_VALUE;
double highBound = Long.MIN_VALUE;
- double[] vals = null;
-
- boolean isRange = false;
+ long[] vals = null;
int x = 0;
String t = tokens[x++];
Operand operand;
+ if ("not".equals(t) || "!".equals(t)) {
+ inRange = !inRange;
+ t = nextToken(tokens, x++, condition);
+ }
try {
operand = NumberInfo.getOperand(t);
} catch (Exception e) {
}
if (x < tokens.length) {
t = tokens[x++];
- if ("mod".equals(t)) {
+ if ("mod".equals(t) || "%".equals(t)) {
mod = Integer.parseInt(tokens[x++]);
t = nextToken(tokens, x++, condition);
}
- if ("is".equals(t)) {
+ if ("not".equals(t) || "!".equals(t)) {
+ inRange = !inRange;
+ t = nextToken(tokens, x++, condition);
+ }
+ if ("is".equals(t) || "in".equals(t) || "=".equals(t)) {
+ t = nextToken(tokens, x++, condition);
+ } else if ("≠".equals(t) || "!=".equals(t)) {
+ inRange = !inRange;
+ t = nextToken(tokens, x++, condition);
+ } else if ("within".equals(t)) {
+ integersOnly = false;
t = nextToken(tokens, x++, condition);
- if ("not".equals(t)) {
- inRange = false;
- t = nextToken(tokens, x++, condition);
- }
} else {
- isRange = true;
- if ("not".equals(t)) {
- inRange = false;
- t = nextToken(tokens, x++, condition);
- }
- if ("in".equals(t)) {
- t = nextToken(tokens, x++, condition);
- } else if ("within".equals(t)) {
- integersOnly = false;
- t = nextToken(tokens, x++, condition);
- } else {
- throw unexpected(t, condition);
- }
+ throw unexpected(t, condition);
+ }
+ if ("not".equals(t) || "!".equals(t)) {
+ inRange = !inRange;
+ t = nextToken(tokens, x++, condition);
}
- if (isRange) {
- String[] range_list = Utility.splitString(t, ",");
- vals = new double[range_list.length * 2];
- for (int k1 = 0, k2 = 0; k1 < range_list.length; ++k1, k2 += 2) {
- String range = range_list[k1];
- String[] pair = Utility.splitString(range, "..");
- double low, high;
- if (pair.length == 2) {
- low = Double.parseDouble(pair[0]);
- high = Double.parseDouble(pair[1]);
- if (low > high) {
- throw unexpected(range, condition);
- }
- } else if (pair.length == 1) {
- low = high = Double.parseDouble(pair[0]);
- } else {
+ String[] range_list = Utility.splitString(t, ",");
+ vals = new long[range_list.length * 2];
+ for (int k1 = 0, k2 = 0; k1 < range_list.length; ++k1, k2 += 2) {
+ String range = range_list[k1];
+ String[] pair = Utility.splitString(range, "..");
+ long low, high;
+ if (pair.length == 2) {
+ low = Long.parseLong(pair[0]);
+ high = Long.parseLong(pair[1]);
+ if (low > high) {
throw unexpected(range, condition);
}
- vals[k2] = low;
- vals[k2+1] = high;
- lowBound = Math.min(lowBound, low);
- highBound = Math.max(highBound, high);
- }
- if (vals.length == 2) {
- vals = null;
+ } else if (pair.length == 1) {
+ low = high = Long.parseLong(pair[0]);
+ } else {
+ throw unexpected(range, condition);
}
- } else {
- lowBound = highBound = Double.parseDouble(t);
+ vals[k2] = low;
+ vals[k2+1] = high;
+ lowBound = Math.min(lowBound, low);
+ highBound = Math.max(highBound, high);
+ }
+ if (vals.length == 2) {
+ vals = null;
}
if (x != tokens.length) {
private final boolean integersOnly;
private final double lowerBound;
private final double upperBound;
- private final double[] range_list;
+ private final long[] range_list;
private final Operand operand;
RangeConstraint(int mod, boolean inRange, Operand operand, boolean integersOnly,
- double lowBound, double highBound, double[] vals) {
+ double lowBound, double highBound, long[] vals) {
this.mod = mod;
this.inRange = inRange;
this.integersOnly = integersOnly;
return rules.select(new NumberInfo(number, countVisibleFractionDigits, fractionaldigits));
}
-
+
/**
* Given a number information, returns the keyword of the first rule that applies to
* the number.
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberFormat.NumberFormatFactory;
import com.ibm.icu.text.NumberFormat.SimpleNumberFormatFactory;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.text.PluralRules.NumberInfo;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.ULocale;
numberFormat.setMaximumSignificantDigits(3);
assertEquals("TestShowZero", "0", numberFormat.format(0.0));
}
+
+ public void TestCurrencyPlurals() {
+ String[][] tests = {
+ {"en", "USD", "1", "1 US dollar"},
+ {"en", "USD", "1.0", "1.0 US dollars"},
+ {"en", "USD", "1.00", "1.00 US dollars"},
+ {"en", "AUD", "1", "1 Australian dollar"},
+ {"en", "AUD", "1.00", "1.00 Australian dollars"},
+ {"sl", "USD", "1", "1 ameriški dolar"},
+ {"sl", "USD", "2", "2 ameriška dolarja"},
+ {"sl", "USD", "3", "3 ameriški dolarji"},
+ {"sl", "USD", "5", "5 ameriških dolarjev"},
+ };
+ for (String test[] : tests) {
+ DecimalFormat numberFormat = (DecimalFormat) DecimalFormat.getInstance(new ULocale(test[0]), NumberFormat.PLURALCURRENCYSTYLE);
+ numberFormat.setCurrency(Currency.getInstance(test[1]));
+ double number = Double.parseDouble(test[2]);
+ int dotPos = test[2].indexOf('.');
+ int decimals = dotPos < 0 ? 0 : test[2].length() - dotPos - 1;
+ int digits = dotPos < 0 ? test[2].length() : test[2].length() - 1;
+ numberFormat.setMaximumFractionDigits(decimals);
+ numberFormat.setMinimumFractionDigits(decimals);
+ String actual = numberFormat.format(number);
+ assertEquals(test[0] + "\t" + test[1] + "\t" + test[2], test[3], actual);
+ numberFormat.setMaximumSignificantDigits(digits);
+ numberFormat.setMinimumSignificantDigits(digits);
+ actual = numberFormat.format(number);
+ assertEquals(test[0] + "\t" + test[1] + "\t" + test[2], test[3], actual);
+ }
+ }
}
}
private static String[][] operandTestData = {
+ {"a: n 3", "FAIL"},
+ {"a: !n!≠!1,2; b: not n is 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",
String categoriesAndExpected = pair[1].trim();
// logln("pattern[" + i + "] " + pattern);
+ boolean FAIL_EXPECTED = categoriesAndExpected.equalsIgnoreCase("fail");
try {
+ logln(pattern);
PluralRules rules = PluralRules.createRules(pattern);
- logln(rules.toString());
- checkCategoriesAndExpected(pattern, categoriesAndExpected, rules);
+ if (FAIL_EXPECTED) {
+ assertNull("Should fail with 'null' return.", rules);
+ } else {
+ logln(rules.toString());
+ checkCategoriesAndExpected(pattern, categoriesAndExpected, rules);
+ }
} catch (Exception e) {
- e.printStackTrace();
- throw new RuntimeException(e.getMessage());
+ if (!FAIL_EXPECTED) {
+ e.printStackTrace();
+ throw new RuntimeException(e.getMessage());
+ }
}
}
}
{ "a:n in 2;b:n in 5",
"b: n in 5;a: n in 2;" },
-// { "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" },
+ // { "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 = {