From: Mark Davis Date: Fri, 19 Jul 2013 19:38:17 +0000 (+0000) Subject: ICU-8474 made syntax fixes in accordance with doc, adjusted code for currencies to... X-Git-Tag: milestone-59-0-1~2761 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=50323c01c7ef5214a556817eba53b11fd66d6d18;p=icu ICU-8474 made syntax fixes in accordance with doc, adjusted code for currencies to work right. X-SVN-Rev: 33945 --- diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java index a99b4234ebd..32a60ea1bc0 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java @@ -27,6 +27,7 @@ import com.ibm.icu.impl.Utility; 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; @@ -1216,18 +1217,62 @@ public class DecimalFormat extends NumberFormat { 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); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java index 469e7e469f8..891070f186e 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java @@ -33,6 +33,7 @@ import com.ibm.icu.impl.PatternProps; 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; /** @@ -95,23 +96,24 @@ 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' - * 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 * - * + *

Each not term inverts the meaning; however, there should not be more than one of them.

*

- * The i, f, and v values are defined as follows: + * The i, f, t, and v values are defined as follows: *

* @@ -228,7 +230,7 @@ public class PluralRules implements Serializable { public final PluralRules forLocale(ULocale locale) { return forLocale(locale, PluralType.CARDINAL); } - + /** * Returns the locales for which there is plurals data. * @@ -254,7 +256,7 @@ public class PluralRules implements Serializable { * @internal */ public abstract ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable); - + /** * Returns the default factory. * @deprecated This API is ICU internal only. @@ -263,7 +265,7 @@ public class PluralRules implements Serializable { public static PluralRulesLoader getDefaultFactory() { return PluralRulesLoader.loader; } - + /** * Returns whether or not there are overrides. * @deprecated This API is ICU internal only. @@ -445,6 +447,7 @@ public class PluralRules implements Serializable { n, i, f, + t, v, j; } @@ -457,6 +460,7 @@ public class PluralRules implements Serializable { 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; @@ -485,6 +489,15 @@ public class PluralRules implements Serializable { // throw new IllegalArgumentException(); // } // } + if (f == 0) { + fractionalDigitsWithoutTrailingZeros = 0; + } else { + long fdwtz = f; + while ((fdwtz%10) == 0) { + fdwtz /= 10; + } + fractionalDigitsWithoutTrailingZeros = fdwtz; + } } /** @@ -492,7 +505,6 @@ public class PluralRules implements Serializable { * @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)); } @@ -519,7 +531,7 @@ public class PluralRules implements Serializable { * @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; } @@ -560,6 +572,7 @@ public class PluralRules implements Serializable { default: return source; case i: return intValue; case f: return fractionalDigits; + case t: return fractionalDigitsWithoutTrailingZeros; case v: return visibleFractionDigitCount; } } @@ -699,6 +712,36 @@ public class PluralRules implements Serializable { 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 result = new ArrayList(); + 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. // */ @@ -728,26 +771,28 @@ public class PluralRules implements Serializable { /* * 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 { @@ -763,20 +808,22 @@ public class PluralRules implements Serializable { 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) { @@ -784,60 +831,54 @@ public class PluralRules implements Serializable { } 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) { @@ -943,11 +984,11 @@ public class PluralRules implements Serializable { 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; @@ -1491,7 +1532,7 @@ public class PluralRules implements Serializable { return rules.select(new NumberInfo(number, countVisibleFractionDigits, fractionaldigits)); } - + /** * Given a number information, returns the keyword of the first rule that applies to * the number. diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java index 47dc01b1cbe..73e8090a73c 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java @@ -34,6 +34,8 @@ import com.ibm.icu.text.MeasureFormat; 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; @@ -3287,4 +3289,34 @@ public class NumberFormatTest extends com.ibm.icu.dev.test.TestFmwk { 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); + } + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java index b6165fcdffc..8759f348315 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java @@ -135,6 +135,11 @@ public class PluralRulesTest extends TestFmwk { } 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", @@ -156,13 +161,21 @@ public class PluralRulesTest extends TestFmwk { 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()); + } } } } @@ -234,14 +247,14 @@ public class PluralRulesTest extends TestFmwk { { "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 = {