From b74178190393f71d986cc3b85748e1f1da75baa3 Mon Sep 17 00:00:00 2001 From: Mark Davis Date: Sun, 17 Aug 2014 15:26:18 +0000 Subject: [PATCH] ICU-10600 Add currencies (need some ugly hacks for that...) X-SVN-Rev: 36181 --- .../src/com/ibm/icu/text/MeasureFormat.java | 96 +++++++++++++++--- .../icu/dev/test/format/PluralRangesTest.java | 22 ++++- .../ibm/icu/dev/util/CollectionUtilities.java | 4 +- .../src/com/ibm/icu/dev/util/UnicodeMap.java | 99 ++++++++++++++++--- .../icu/dev/test/translit/UnicodeMapTest.java | 79 +++++++++++++-- 5 files changed, 259 insertions(+), 41 deletions(-) diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java index e6d990ee6b8..db9ce1d22f9 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java @@ -278,9 +278,8 @@ public class MeasureFormat extends UFormat { rules, unitToStyleToCountToFormat, formatters, - new ImmutableNumberFormat( - NumberFormat.getInstance(locale, formatWidth.getCurrencyStyle())), - new ImmutableNumberFormat(intFormat)); + new ImmutableNumberFormat(NumberFormat.getInstance(locale, formatWidth.getCurrencyStyle())), + new ImmutableNumberFormat(intFormat)); } /** @@ -401,16 +400,38 @@ public class MeasureFormat extends UFormat { } Number lowNumber = lowValue.getNumber(); Number highNumber = highValue.getNumber(); + final boolean isCurrency = unit instanceof Currency; + + UFieldPosition lowFpos = new UFieldPosition(); + UFieldPosition highFpos = new UFieldPosition(); + StringBuffer lowFormatted = null; + StringBuffer highFormatted = null; + + if (isCurrency) { + Currency currency = (Currency) unit; + int fracDigits = currency.getDefaultFractionDigits(); + int maxFrac = numberFormat.nf.getMaximumFractionDigits(); + int minFrac = numberFormat.nf.getMinimumFractionDigits(); + if (fracDigits != maxFrac || fracDigits != minFrac) { + DecimalFormat currentNumberFormat = (DecimalFormat) numberFormat.get(); + currentNumberFormat.setMaximumFractionDigits(fracDigits); + currentNumberFormat.setMinimumFractionDigits(fracDigits); + lowFormatted = currentNumberFormat.format(lowNumber, new StringBuffer(), lowFpos); + highFormatted = currentNumberFormat.format(highNumber, new StringBuffer(), highFpos); + } + } + if (lowFormatted == null) { + lowFormatted = numberFormat.format(lowNumber, new StringBuffer(), lowFpos); + highFormatted = numberFormat.format(highNumber, new StringBuffer(), highFpos); + } - UFieldPosition fpos = new UFieldPosition(); - StringBuffer lowFormatted = numberFormat.format(lowNumber, new StringBuffer(), fpos); final double lowDouble = lowNumber.doubleValue(); String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble, - fpos.getCountVisibleFractionDigits(), fpos.getFractionDigits())); + lowFpos.getCountVisibleFractionDigits(), lowFpos.getFractionDigits())); - StringBuffer highFormatted = numberFormat.format(highNumber, new StringBuffer(), fpos); - String keywordHigh = rules.select(new PluralRules.FixedDecimal(highNumber.doubleValue(), - fpos.getCountVisibleFractionDigits(), fpos.getFractionDigits())); + final double highDouble = highNumber.doubleValue(); + String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble, + highFpos.getCountVisibleFractionDigits(), highFpos.getFractionDigits())); final PluralRanges pluralRanges = Factory.getDefaultFactory().getPluralRanges(getLocale()); StandardPluralCategories resolvedCategory = pluralRanges.get( @@ -419,8 +440,25 @@ public class MeasureFormat extends UFormat { SimplePatternFormatter rangeFormatter = getRangeFormat(getLocale(), formatWidth); String formattedNumber = rangeFormatter.format(lowFormatted, highFormatted); - if (unit instanceof Currency) { - throw new IllegalArgumentException("Currency units not supported in ranges"); + if (isCurrency) { + // Nasty hack + currencyFormat.format(1d); // have to call this for the side effect + + Currency currencyUnit = (Currency) unit; + StringBuilder result = new StringBuilder(); + appendReplacingCurrency(currencyFormat.getPrefix(lowDouble >= 0), currencyUnit, resolvedCategory, result); + result.append(formattedNumber); + appendReplacingCurrency(currencyFormat.getSuffix(highDouble >= 0), currencyUnit, resolvedCategory, result); + return result.toString(); + // StringBuffer buffer = new StringBuffer(); + // CurrencyAmount currencyLow = (CurrencyAmount) lowValue; + // CurrencyAmount currencyHigh = (CurrencyAmount) highValue; + // FieldPosition pos = new FieldPosition(NumberFormat.INTEGER_FIELD); + // currencyFormat.format(currencyLow, buffer, pos); + // int startOfInteger = pos.getBeginIndex(); + // StringBuffer buffer2 = new StringBuffer(); + // FieldPosition pos2 = new FieldPosition(0); + // currencyFormat.format(currencyHigh, buffer2, pos2); } else { Map styleToCountToFormat = unitToStyleToCountToFormat.get(lowValue.getUnit()); QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth); @@ -429,6 +467,38 @@ public class MeasureFormat extends UFormat { } } + /** + * @param affix + * @param unit + * @param resolvedCategory + * @param result + * @return + */ + private void appendReplacingCurrency(String affix, Currency unit, StandardPluralCategories resolvedCategory, StringBuilder result) { + String replacement = "¤"; + int pos = affix.indexOf(replacement); + if (pos < 0) { + replacement = "XXX"; + pos = affix.indexOf(replacement); + } + if (pos < 0) { + result.append(affix); + } else { + // for now, just assume single + result.append(affix.substring(0,pos)); + // we have a mismatch between the number style and the currency style, so remap + int currentStyle = formatWidth.getCurrencyStyle(); + if (currentStyle == NumberFormat.ISOCURRENCYSTYLE) { + result.append(unit.getCurrencyCode()); + } else { + result.append(unit.getName(currencyFormat.nf.getLocale(ULocale.ACTUAL_LOCALE), + currentStyle == NumberFormat.CURRENCYSTYLE ? Currency.SYMBOL_NAME : Currency.PLURAL_LONG_NAME, + resolvedCategory.toString(), null)); + } + result.append(affix.substring(pos+replacement.length())); + } + } + /** * Formats a sequence of measures. * @@ -775,7 +845,7 @@ public class MeasureFormat extends UFormat { public synchronized String format(Number number) { return nf.format(number); } - + public String getPrefix(boolean positive) { return positive ? ((DecimalFormat)nf).getPositivePrefix() : ((DecimalFormat)nf).getNegativePrefix(); } @@ -1109,7 +1179,7 @@ public class MeasureFormat extends UFormat { public SimplePatternFormatter getRangeFormat(ULocale forLocale, FormatWidth width) { // TODO fix Hack for French - if (width != FormatWidth.WIDE && forLocale.getLanguage().equals("fr")) { + if (forLocale.getLanguage().equals("fr")) { return getRangeFormat(ULocale.ROOT, width); } SimplePatternFormatter result = localeIdToRangeFormat.get(forLocale); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java index f7b88427332..2edf1632e9c 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java @@ -52,17 +52,29 @@ public class PluralRangesTest extends TestFmwk { public void TestFormatting() { Object[][] tests = { - {0.0, 1.0, ULocale.FRANCE, FormatWidth.WIDE, MeasureUnit.FAHRENHEIT, "de 0 à 1 degré Fahrenheit"}, - {1.0, 2.0, ULocale.FRANCE, FormatWidth.WIDE, MeasureUnit.FAHRENHEIT, "de 1 à 2 degrés Fahrenheit"}, + {0.0, 1.0, ULocale.FRANCE, FormatWidth.WIDE, MeasureUnit.FAHRENHEIT, "0–1 degré Fahrenheit"}, + {1.0, 2.0, ULocale.FRANCE, FormatWidth.WIDE, MeasureUnit.FAHRENHEIT, "1–2 degrés Fahrenheit"}, {3.1, 4.25, ULocale.FRANCE, FormatWidth.SHORT, MeasureUnit.FAHRENHEIT, "3,1–4,25 °F"}, {3.1, 4.25, ULocale.ENGLISH, FormatWidth.SHORT, MeasureUnit.FAHRENHEIT, "3.1–4.25°F"}, {3.1, 4.25, ULocale.CHINESE, FormatWidth.WIDE, MeasureUnit.INCH, "3.1-4.25英寸"}, {0.0, 1.0, ULocale.ENGLISH, FormatWidth.WIDE, MeasureUnit.INCH, "0–1 inches"}, - {0.0, 1.0, ULocale.ENGLISH, FormatWidth.WIDE, Currency.getInstance("EUR"), - IllegalArgumentException.class}, + {0.0, 1.0, ULocale.ENGLISH, FormatWidth.NARROW, Currency.getInstance("EUR"), "€0.00–1.00"}, + {0.0, 1.0, ULocale.FRENCH, FormatWidth.NARROW, Currency.getInstance("EUR"), "0,00–1,00 €"}, + {0.0, 100.0, ULocale.FRENCH, FormatWidth.NARROW, Currency.getInstance("JPY"), "0–100\u00a0¥JP"}, + + {0.0, 1.0, ULocale.ENGLISH, FormatWidth.SHORT, Currency.getInstance("EUR"), "EUR0.00–1.00"}, + {0.0, 1.0, ULocale.FRENCH, FormatWidth.SHORT, Currency.getInstance("EUR"), "0,00–1,00\u00a0EUR"}, + {0.0, 100.0, ULocale.FRENCH, FormatWidth.SHORT, Currency.getInstance("JPY"), "0–100\u00a0JPY"}, + + {0.0, 1.0, ULocale.ENGLISH, FormatWidth.WIDE, Currency.getInstance("EUR"), "0.00–1.00 euros"}, + {0.0, 1.0, ULocale.FRENCH, FormatWidth.WIDE, Currency.getInstance("EUR"), "0,00–1,00 euro"}, + {0.0, 2.0, ULocale.FRENCH, FormatWidth.WIDE, Currency.getInstance("EUR"), "0,00–2,00 euros"}, + {0.0, 100.0, ULocale.FRENCH, FormatWidth.WIDE, Currency.getInstance("JPY"), "0–100 yens japonais"}, }; + int i = 0; for (Object[] test : tests) { + ++i; double low = (Double) test[0]; double high = (Double) test[1]; final ULocale locale = (ULocale) test[2]; @@ -77,7 +89,7 @@ public class PluralRangesTest extends TestFmwk { } catch (Exception e) { actual = e.getClass(); } - assertEquals("Formatting unit", expected, actual); + assertEquals(i + " Formatting unit", expected, actual); } } diff --git a/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/CollectionUtilities.java b/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/CollectionUtilities.java index 7849c274413..918eddf01aa 100644 --- a/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/CollectionUtilities.java +++ b/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/CollectionUtilities.java @@ -1,6 +1,6 @@ /* ******************************************************************************* - * Copyright (C) 1996-2013, International Business Machines Corporation and * + * Copyright (C) 1996-2014, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ @@ -51,7 +51,7 @@ public final class CollectionUtilities { * @param separator * @return string */ - public static >String join(U collection, String separator) { + public static >String join(U collection, String separator) { StringBuffer result = new StringBuffer(); boolean first = true; for (Iterator it = collection.iterator(); it.hasNext();) { diff --git a/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/UnicodeMap.java b/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/UnicodeMap.java index 801402456ec..792574564ac 100644 --- a/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/UnicodeMap.java +++ b/icu4j/main/tests/framework/src/com/ibm/icu/dev/util/UnicodeMap.java @@ -1,6 +1,6 @@ /* ******************************************************************************* - * Copyright (C) 1996-2012, International Business Machines Corporation and * + * Copyright (C) 1996-2014, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ @@ -387,6 +387,9 @@ public final class UnicodeMap implements Cloneable, Freezable, StringTransfor public UnicodeMap put(String string, T value) { int v = UnicodeSet.getSingleCodePoint(string); if (v == Integer.MAX_VALUE) { + if (locked) { + throw new UnsupportedOperationException("Attempt to modify locked object"); + } if (value != null) { if (stringMap == null) { stringMap = new TreeMap(); @@ -412,10 +415,11 @@ public final class UnicodeMap implements Cloneable, Freezable, StringTransfor public UnicodeMap putAll(UnicodeSet codepoints, T value) { UnicodeSetIterator it = new UnicodeSetIterator(codepoints); while (it.nextRange()) { - _putAll(it.codepoint, it.codepointEnd, value); - } - for (String key : codepoints.strings()) { - put(key, value); + if (it.string == null) { + _putAll(it.codepoint, it.codepointEnd, value); + } else { + put(it.string, value); + } } return this; } @@ -918,33 +922,56 @@ public final class UnicodeMap implements Cloneable, Freezable, StringTransfor } + /** + * Struct-like class used to iterate over a UnicodeMap in a for loop. + * Caution: The contents may change during the iteration! + */ public static class EntryRange { public int codepoint; public int codepointEnd; public String string; public T value; + @Override + public String toString() { + return (string != null ? Utility.hex(string) + : Utility.hex(codepoint) + (codepoint == codepointEnd ? "" : ".." + Utility.hex(codepointEnd))) + + "=" + value; + } } - public Iterable entryRanges() { + /** + * Returns an Iterable over EntryRange, designed for efficient for loops over UnicodeMaps. + * Caution: For efficiency, the EntryRange may be reused, so the EntryRange may change on each iteration! + * The value is guaranteed never to be null. + * @return entry range, for for loops + */ + public Iterable> entryRanges() { return new EntryRanges(); } - private class EntryRanges implements Iterable, Iterator { + private class EntryRanges implements Iterable>, Iterator> { int pos; EntryRange result = new EntryRange(); + int lastRealRange = values[length-2] == null ? length - 2 : length - 1; Iterator> stringIterator = stringMap == null ? null : stringMap.entrySet().iterator(); - public Iterator iterator() { + + public Iterator> iterator() { return this; } public boolean hasNext() { - return pos < length-1 || (stringIterator != null && stringIterator.hasNext()); + return pos < lastRealRange || (stringIterator != null && stringIterator.hasNext()); } public EntryRange next() { - if (pos < length-1) { + // a range may be null, but then the next one must not be (except the final range) + if (pos < lastRealRange) { + T temp = values[pos]; + if (temp == null) { + temp = values[++pos]; + } result.codepoint = transitions[pos]; result.codepointEnd = transitions[pos+1]-1; result.string = null; - result.value = values[pos]; + result.value = temp; ++pos; } else { Entry entry = stringIterator.next(); @@ -1096,4 +1123,54 @@ public final class UnicodeMap implements Cloneable, Freezable, StringTransfor // if (DEBUG_WRITE) System.out.println("Trans: " + transitions[i] + ",\t" + currentValue); // } // } + + public final UnicodeMap removeAll(UnicodeSet set) { + return putAll(set, null); + } + + public final UnicodeMap removeAll(UnicodeMap reference) { + return removeRetainAll(reference, true); + } + + public final UnicodeMap retainAll(UnicodeSet set) { + UnicodeSet toNuke = new UnicodeSet(); + // TODO Optimize + for (EntryRange ae : entryRanges()) { + if (ae.string != null) { + if (!set.contains(ae.string)) { + toNuke.add(ae.string); + } + } else { + for (int i = ae.codepoint; i <= ae.codepointEnd; ++i) { + if (!set.contains(i)) { + toNuke.add(i); + } + } + } + } + return putAll(toNuke, null); + } + + public final UnicodeMap retainAll(UnicodeMap reference) { + return removeRetainAll(reference, false); + } + + private final UnicodeMap removeRetainAll(UnicodeMap reference, boolean remove) { + UnicodeSet toNuke = new UnicodeSet(); + // TODO Optimize + for (EntryRange ae : entryRanges()) { + if (ae.string != null) { + if (ae.value.equals(reference.get(ae.string)) == remove) { + toNuke.add(ae.string); + } + } else { + for (int i = ae.codepoint; i <= ae.codepointEnd; ++i) { + if (ae.value.equals(reference.get(i)) == remove) { + toNuke.add(i); + } + } + } + } + return putAll(toNuke, null); + } } diff --git a/icu4j/main/tests/translit/src/com/ibm/icu/dev/test/translit/UnicodeMapTest.java b/icu4j/main/tests/translit/src/com/ibm/icu/dev/test/translit/UnicodeMapTest.java index ef5395fecf5..bb88ebc151c 100644 --- a/icu4j/main/tests/translit/src/com/ibm/icu/dev/test/translit/UnicodeMapTest.java +++ b/icu4j/main/tests/translit/src/com/ibm/icu/dev/test/translit/UnicodeMapTest.java @@ -1,6 +1,6 @@ /* ******************************************************************************* - * Copyright (C) 1996-2012, International Business Machines Corporation and * + * Copyright (C) 1996-2014, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ @@ -17,7 +17,9 @@ import java.util.TreeMap; import java.util.TreeSet; import com.ibm.icu.dev.test.TestFmwk; +import com.ibm.icu.dev.util.CollectionUtilities; import com.ibm.icu.dev.util.UnicodeMap; +import com.ibm.icu.dev.util.UnicodeMap.EntryRange; import com.ibm.icu.impl.Utility; import com.ibm.icu.text.UTF16; import com.ibm.icu.text.UnicodeSet; @@ -35,6 +37,63 @@ public class UnicodeMapTest extends TestFmwk { new UnicodeMapTest().run(args); } + public void TestIterations() { + UnicodeMap foo = new UnicodeMap(); + checkToString(foo, ""); + foo.put(3, 6d).put(5, 10d); + checkToString(foo, "0003=6.0\n0005=10.0\n"); + foo.put(0x10FFFF, 666d); + checkToString(foo, "0003=6.0\n0005=10.0\n10FFFF=666.0\n"); + foo.put("neg", -555d); + checkToString(foo, "0003=6.0\n0005=10.0\n10FFFF=666.0\n006E,0065,0067=-555.0\n"); + + double i = 0; + for (EntryRange entryRange : foo.entryRanges()) { + i += entryRange.value; + } + assertEquals("EntryRange", 127d, i); + } + + public void checkToString(UnicodeMap foo, String expected) { + assertEquals("EntryRange", expected, CollectionUtilities.join(foo.entryRanges(), "\n") + (foo.size() == 0 ? "" : "\n")); + assertEquals("EntryRange", expected, foo.toString()); + } + + public void TestRemove() { + UnicodeMap foo = new UnicodeMap() + .putAll(0x20, 0x29, -2d) + .put("abc", 3d) + .put("xy", 2d) + .put("mark", 4d) + .freeze(); + UnicodeMap fii = new UnicodeMap() + .putAll(0x21, 0x25, -2d) + .putAll(0x26, 0x28, -3d) + .put("abc", 3d) + .put("mark", 999d) + .freeze(); + + UnicodeMap afterFiiRemoval = new UnicodeMap() + .put(0x20, -2d) + .putAll(0x26, 0x29, -2d) + .put("xy", 2d) + .put("mark", 4d) + .freeze(); + + UnicodeMap afterFiiRetained = new UnicodeMap() + .putAll(0x21, 0x25, -2d) + .put("abc", 3d) + .freeze(); + + UnicodeMap test = new UnicodeMap().putAll(foo) + .removeAll(fii); + assertEquals("removeAll", afterFiiRemoval, test); + + test = new UnicodeMap().putAll(foo) + .retainAll(fii); + assertEquals("retainAll", afterFiiRetained, test); + } + public void TestAMonkey() { SortedMap stayWithMe = new TreeMap(OneFirstComparator); @@ -42,7 +101,7 @@ public class UnicodeMapTest extends TestFmwk { // check one special case, removal near end me.putAll(0x10FFFE, 0x10FFFF, 666); me.remove(0x10FFFF); - + int iterations = 100000; SortedMap test = new TreeMap(); @@ -65,9 +124,9 @@ public class UnicodeMapTest extends TestFmwk { break; case 2: case 3: case 4: case 5: case 6: case 7: case 8: other = getRandomKey(rand); -// if (other.equals("\uDBFF\uDFFF") && me.containsKey(0x10FFFF) && me.get(0x10FFFF).equals(me.get(0x10FFFE))) { -// System.out.println("Remove\t" + other + "\n" + me); -// } + // if (other.equals("\uDBFF\uDFFF") && me.containsKey(0x10FFFF) && me.get(0x10FFFF).equals(me.get(0x10FFFE))) { + // System.out.println("Remove\t" + other + "\n" + me); + // } logln("remove\t" + other); stayWithMe.remove(other); try { @@ -138,7 +197,7 @@ public class UnicodeMapTest extends TestFmwk { Set nonCodePointStrings = stayWithMe.tailMap("").keySet(); if (nonCodePointStrings.size() == 0) nonCodePointStrings = null; // for parallel api assertEquals("getNonRangeStrings", nonCodePointStrings, me.getNonRangeStrings()); - + TreeSet values = new TreeSet(stayWithMe.values()); TreeSet myValues = new TreeSet(me.values()); assertEquals("values", myValues, values); @@ -147,7 +206,7 @@ public class UnicodeMapTest extends TestFmwk { assertEquals("containsKey", stayWithMe.containsKey(key), me.containsKey(key)); } } - + static Comparator OneFirstComparator = new Comparator() { public int compare(String o1, String o2) { int cp1 = UnicodeSet.getSingleCodePoint(o1); @@ -161,7 +220,7 @@ public class UnicodeMapTest extends TestFmwk { } return 0; } - + }; /** @@ -177,8 +236,8 @@ public class UnicodeMapTest extends TestFmwk { return UTF16.valueOf('A'-1+r); } else if (r < 20) { return UTF16.valueOf(0x10FFFF - (r-10)); -// } else if (r == 20) { -// return ""; + // } else if (r == 20) { + // return ""; } return "a" + UTF16.valueOf(r + 'a'-1); }