]> granicus.if.org Git - icu/commitdiff
ICU-11276 Porting pluralRanges support to Java.
authorShane Carr <shane@unicode.org>
Sat, 15 Sep 2018 00:54:51 +0000 (17:54 -0700)
committerShane Carr <shane@unicode.org>
Thu, 27 Sep 2018 21:27:40 +0000 (14:27 -0700)
14 files changed:
icu4j/main/classes/core/src/com/ibm/icu/impl/number/AdoptingModifierStore.java [moved from icu4j/main/classes/core/src/com/ibm/icu/impl/number/ParameterizedModifier.java with 80% similarity]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierStore.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/StandardPluralRanges.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java
icu4j/main/classes/core/src/com/ibm/icu/number/ScientificNotation.java
icu4j/main/shared/data/icudata.jar
icu4j/main/shared/data/icutzdata.jar
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberRangeFormatterTest.java

similarity index 80%
rename from icu4j/main/classes/core/src/com/ibm/icu/impl/number/ParameterizedModifier.java
rename to icu4j/main/classes/core/src/com/ibm/icu/impl/number/AdoptingModifierStore.java
index a553be4f30efb628f806f3f4a758cf0224aee640..7e3459d19677c50e8acc8f085eb09d1f46b214d4 100644 (file)
@@ -5,10 +5,11 @@ package com.ibm.icu.impl.number;
 import com.ibm.icu.impl.StandardPlural;
 
 /**
- * A ParameterizedModifier by itself is NOT a Modifier. Rather, it wraps a data structure containing two
- * or more Modifiers and returns the modifier appropriate for the current situation.
+ * This implementation of ModifierStore adopts references to Modifiers.
+ *
+ * (This is named "adopting" because in C++, this class takes ownership of the Modifiers.)
  */
-public class ParameterizedModifier {
+public class AdoptingModifierStore implements ModifierStore {
     private final Modifier positive;
     private final Modifier zero;
     private final Modifier negative;
@@ -21,7 +22,7 @@ public class ParameterizedModifier {
      * <p>
      * If this constructor is used, a plural form CANNOT be passed to {@link #getModifier}.
      */
-    public ParameterizedModifier(Modifier positive, Modifier zero, Modifier negative) {
+    public AdoptingModifierStore(Modifier positive, Modifier zero, Modifier negative) {
         this.positive = positive;
         this.zero = zero;
         this.negative = negative;
@@ -36,7 +37,7 @@ public class ParameterizedModifier {
      * <p>
      * If this constructor is used, a plural form MUST be passed to {@link #getModifier}.
      */
-    public ParameterizedModifier() {
+    public AdoptingModifierStore() {
         this.positive = null;
         this.zero = null;
         this.negative = null;
@@ -53,7 +54,7 @@ public class ParameterizedModifier {
         frozen = true;
     }
 
-    public Modifier getModifier(int signum) {
+    public Modifier getModifierWithoutPlural(int signum) {
         assert frozen;
         assert mods == null;
         return signum == 0 ? zero : signum < 0 ? negative : positive;
@@ -66,6 +67,8 @@ public class ParameterizedModifier {
     }
 
     private static int getModIndex(int signum, StandardPlural plural) {
+        assert signum >= -1 && signum <= 1;
+        assert plural != null;
         return plural.ordinal() * 3 + (signum + 1);
     }
 }
index 9b3a60ba5855d5c6dd4315a186b176c08ae026cb..fc881a2c50425c94f517eef4f22b1aab9e7dfe14 100644 (file)
@@ -82,7 +82,12 @@ public class ConstantAffixModifier implements Modifier {
     }
 
     @Override
-    public boolean equalsModifier(Modifier other) {
+    public Parameters getParameters() {
+        return null;
+    }
+
+    @Override
+    public boolean semanticallyEquivalent(Modifier other) {
         if (!(other instanceof ConstantAffixModifier)) {
             return false;
         }
index ba12ada88a87e69382b106904e425e835f22cbc8..d53349204be9656e72ecb7156c47212e7853e375 100644 (file)
@@ -22,17 +22,30 @@ public class ConstantMultiFieldModifier implements Modifier {
     private final boolean overwrite;
     private final boolean strong;
 
+    // Parameters: used for number range formatting
+    private final Parameters parameters;
+
     public ConstantMultiFieldModifier(
             NumberStringBuilder prefix,
             NumberStringBuilder suffix,
             boolean overwrite,
             boolean strong) {
+        this(prefix, suffix, overwrite, strong, null);
+    }
+
+    public ConstantMultiFieldModifier(
+            NumberStringBuilder prefix,
+            NumberStringBuilder suffix,
+            boolean overwrite,
+            boolean strong,
+            Parameters parameters) {
         prefixChars = prefix.toCharArray();
         suffixChars = suffix.toCharArray();
         prefixFields = prefix.toFieldArray();
         suffixFields = suffix.toFieldArray();
         this.overwrite = overwrite;
         this.strong = strong;
+        this.parameters = parameters;
     }
 
     @Override
@@ -77,11 +90,19 @@ public class ConstantMultiFieldModifier implements Modifier {
     }
 
     @Override
-    public boolean equalsModifier(Modifier other) {
+    public Parameters getParameters() {
+        return parameters;
+    }
+
+    @Override
+    public boolean semanticallyEquivalent(Modifier other) {
         if (!(other instanceof ConstantMultiFieldModifier)) {
             return false;
         }
         ConstantMultiFieldModifier _other = (ConstantMultiFieldModifier) other;
+        if (parameters != null && _other.parameters != null && parameters.obj == _other.parameters.obj) {
+            return true;
+        }
         return Arrays.equals(prefixChars, _other.prefixChars) && Arrays.equals(prefixFields, _other.prefixFields)
                 && Arrays.equals(suffixChars, _other.suffixChars) && Arrays.equals(suffixFields, _other.suffixFields)
                 && overwrite == _other.overwrite && strong == _other.strong;
index bb86ba1ef5be14638af188b74412112819ca80a0..61921390ac6f19f983de5f0301009a5e60203a33 100644 (file)
@@ -21,7 +21,7 @@ import com.ibm.icu.util.MeasureUnit;
 import com.ibm.icu.util.ULocale;
 import com.ibm.icu.util.UResourceBundle;
 
-public class LongNameHandler implements MicroPropsGenerator {
+public class LongNameHandler implements MicroPropsGenerator, ModifierStore {
 
     private static final int DNAM_INDEX = StandardPlural.COUNT;
     private static final int PER_INDEX = StandardPlural.COUNT + 1;
@@ -175,10 +175,11 @@ public class LongNameHandler implements MicroPropsGenerator {
         String[] simpleFormats = new String[ARRAY_LENGTH];
         getCurrencyLongNameData(locale, currency, simpleFormats);
         // TODO(ICU4J): Reduce the number of object creations here?
-        Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<StandardPlural, SimpleModifier>(
+        Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(
                 StandardPlural.class);
-        simpleFormatsToModifiers(simpleFormats, null, modifiers);
-        return new LongNameHandler(modifiers, rules, parent);
+        LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
+        result.simpleFormatsToModifiers(simpleFormats, null);
+        return result;
     }
 
     public static LongNameHandler forMeasureUnit(
@@ -203,10 +204,11 @@ public class LongNameHandler implements MicroPropsGenerator {
         getMeasureData(locale, unit, width, simpleFormats);
         // TODO: What field to use for units?
         // TODO(ICU4J): Reduce the number of object creations here?
-        Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<StandardPlural, SimpleModifier>(
+        Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(
                 StandardPlural.class);
-        simpleFormatsToModifiers(simpleFormats, null, modifiers);
-        return new LongNameHandler(modifiers, rules, parent);
+        LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
+        result.simpleFormatsToModifiers(simpleFormats, null);
+        return result;
     }
 
     private static LongNameHandler forCompoundUnit(
@@ -238,29 +240,32 @@ public class LongNameHandler implements MicroPropsGenerator {
             perUnitFormat = SimpleFormatterImpl.formatCompiledPattern(compiled, "{0}", secondaryString);
         }
         // TODO: What field to use for units?
-        Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<StandardPlural, SimpleModifier>(
+        Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(
                 StandardPlural.class);
-        multiSimpleFormatsToModifiers(primaryData, perUnitFormat, null, modifiers);
-        return new LongNameHandler(modifiers, rules, parent);
+        LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
+        result.multiSimpleFormatsToModifiers(primaryData, perUnitFormat, null);
+        return result;
     }
 
-    private static void simpleFormatsToModifiers(
+    private void simpleFormatsToModifiers(
             String[] simpleFormats,
-            NumberFormat.Field field,
-            Map<StandardPlural, SimpleModifier> output) {
+            NumberFormat.Field field) {
         StringBuilder sb = new StringBuilder();
         for (StandardPlural plural : StandardPlural.VALUES) {
             String simpleFormat = getWithPlural(simpleFormats, plural);
             String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 0, 1);
-            output.put(plural, new SimpleModifier(compiled, field, false));
+            Modifier.Parameters parameters = new Modifier.Parameters();
+            parameters.obj = this;
+            parameters.signum = 0;
+            parameters.plural = plural;
+            modifiers.put(plural, new SimpleModifier(compiled, field, false, parameters));
         }
     }
 
-    private static void multiSimpleFormatsToModifiers(
+    private void multiSimpleFormatsToModifiers(
             String[] leadFormats,
             String trailFormat,
-            NumberFormat.Field field,
-            Map<StandardPlural, SimpleModifier> output) {
+            NumberFormat.Field field) {
         StringBuilder sb = new StringBuilder();
         String trailCompiled = SimpleFormatterImpl.compileToStringMinMaxArguments(trailFormat, sb, 1, 1);
         for (StandardPlural plural : StandardPlural.VALUES) {
@@ -268,7 +273,11 @@ public class LongNameHandler implements MicroPropsGenerator {
             String compoundFormat = SimpleFormatterImpl.formatCompiledPattern(trailCompiled, leadFormat);
             String compoundCompiled = SimpleFormatterImpl
                     .compileToStringMinMaxArguments(compoundFormat, sb, 0, 1);
-            output.put(plural, new SimpleModifier(compoundCompiled, field, false));
+            Modifier.Parameters parameters = new Modifier.Parameters();
+            parameters.obj = this;
+            parameters.signum = 0;
+            parameters.plural = plural;
+            modifiers.put(plural, new SimpleModifier(compoundCompiled, field, false, parameters));
         }
     }
 
@@ -281,4 +290,9 @@ public class LongNameHandler implements MicroPropsGenerator {
         micros.modOuter = modifiers.get(copy.getStandardPlural(rules));
         return micros;
     }
+
+    @Override
+    public Modifier getModifier(int signum, StandardPlural plural) {
+        return modifiers.get(plural);
+    }
 }
index cf0b1f1b4b5fe65fba8193791f7188afa8f6ac2b..d0e74bbe093d1e1a8bf739d1c9b7b3d6cf270aec 100644 (file)
@@ -2,6 +2,7 @@
 // License & terms of use: http://www.unicode.org/copyright.html#License
 package com.ibm.icu.impl.number;
 
+import com.ibm.icu.impl.StandardPlural;
 import com.ibm.icu.text.NumberFormat.Field;
 
 /**
@@ -57,7 +58,23 @@ public interface Modifier {
     public boolean containsField(Field currency);
 
     /**
-     * Returns whether the affixes owned by this modifier are equal to the ones owned by the given modifier.
+     * A fill-in for getParameters(). obj will always be set; if non-null, the other
+     * two fields are also safe to read.
      */
-    public boolean equalsModifier(Modifier other);
+    public static class Parameters {
+        public ModifierStore obj;
+        public int signum;
+        public StandardPlural plural;
+    }
+
+    /**
+     * Gets a set of "parameters" for this Modifier.
+     */
+    public Parameters getParameters();
+
+    /**
+     * Returns whether this Modifier is *semantically equivalent* to the other Modifier;
+     * in many cases, this is the same as equal, but parameters should be ignored.
+     */
+    public boolean semanticallyEquivalent(Modifier other);
 }
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierStore.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierStore.java
new file mode 100644 (file)
index 0000000..1751c1c
--- /dev/null
@@ -0,0 +1,18 @@
+// © 2018 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.impl.StandardPlural;
+
+/**
+ * This is *not* a modifier; rather, it is an object that can return modifiers
+ * based on given parameters.
+ *
+ * @author sffc
+ */
+public interface ModifierStore {
+    /**
+     * Returns a Modifier with the given parameters (best-effort).
+     */
+    Modifier getModifier(int signum, StandardPlural plural);
+}
index 80525d663ed097c5396c415cb3882b35f837df46..a651b3907d92a1aadf54d64132e804002fc2cd28 100644 (file)
@@ -166,7 +166,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
         NumberStringBuilder b = new NumberStringBuilder();
         if (needsPlurals()) {
             // Slower path when we require the plural keyword.
-            ParameterizedModifier pm = new ParameterizedModifier();
+            AdoptingModifierStore pm = new AdoptingModifierStore();
             for (StandardPlural plural : StandardPlural.VALUES) {
                 setNumberProperties(1, plural);
                 pm.setModifier(1, plural, createConstantModifier(a, b));
@@ -185,7 +185,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
             Modifier zero = createConstantModifier(a, b);
             setNumberProperties(-1, null);
             Modifier negative = createConstantModifier(a, b);
-            ParameterizedModifier pm = new ParameterizedModifier(positive, zero, negative);
+            AdoptingModifierStore pm = new AdoptingModifierStore(positive, zero, negative);
             return new ImmutablePatternModifier(pm, null, parent);
         }
     }
@@ -214,12 +214,12 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
     }
 
     public static class ImmutablePatternModifier implements MicroPropsGenerator {
-        final ParameterizedModifier pm;
+        final AdoptingModifierStore pm;
         final PluralRules rules;
         final MicroPropsGenerator parent;
 
         ImmutablePatternModifier(
-                ParameterizedModifier pm,
+                AdoptingModifierStore pm,
                 PluralRules rules,
                 MicroPropsGenerator parent) {
             this.pm = pm;
@@ -236,7 +236,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
 
         public void applyToMicros(MicroProps micros, DecimalQuantity quantity) {
             if (rules == null) {
-                micros.modMiddle = pm.getModifier(quantity.signum());
+                micros.modMiddle = pm.getModifierWithoutPlural(quantity.signum());
             } else {
                 // TODO: Fix this. Avoid the copy.
                 DecimalQuantity copy = quantity.createCopy();
@@ -328,7 +328,14 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
     }
 
     @Override
-    public boolean equalsModifier(Modifier other) {
+    public Parameters getParameters() {
+        // This method is not currently used.
+        assert false;
+        return null;
+    }
+
+    @Override
+    public boolean semanticallyEquivalent(Modifier other) {
         // This method is not currently used. (unsafe path not used in range formatting)
         assert false;
         return false;
index e23c5e917ef1ac8558da032510cd48589d3e126e..30c12d61a82eb145998609a1bc54bd860597f81c 100644 (file)
@@ -19,15 +19,24 @@ public class SimpleModifier implements Modifier {
     private final int suffixOffset;
     private final int suffixLength;
 
+    // Parameters: used for number range formatting
+    private final Parameters parameters;
+
     /** TODO: This is copied from SimpleFormatterImpl. */
     private static final int ARG_NUM_LIMIT = 0x100;
 
     /** Creates a modifier that uses the SimpleFormatter string formats. */
     public SimpleModifier(String compiledPattern, Field field, boolean strong) {
+        this(compiledPattern, field, strong, null);
+    }
+
+    /** Creates a modifier that uses the SimpleFormatter string formats. */
+    public SimpleModifier(String compiledPattern, Field field, boolean strong, Parameters parameters) {
         assert compiledPattern != null;
         this.compiledPattern = compiledPattern;
         this.field = field;
         this.strong = strong;
+        this.parameters = parameters;
 
         int argLimit = SimpleFormatterImpl.getArgumentLimit(compiledPattern);
         if (argLimit == 0) {
@@ -90,11 +99,19 @@ public class SimpleModifier implements Modifier {
     }
 
     @Override
-    public boolean equalsModifier(Modifier other) {
+    public Parameters getParameters() {
+        return parameters;
+    }
+
+    @Override
+    public boolean semanticallyEquivalent(Modifier other) {
         if (!(other instanceof SimpleModifier)) {
             return false;
         }
         SimpleModifier _other = (SimpleModifier) other;
+        if (parameters != null && _other.parameters != null && parameters.obj == _other.parameters.obj) {
+            return true;
+        }
         return compiledPattern.equals(_other.compiledPattern) && field == _other.field && strong == _other.strong;
     }
 
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/StandardPluralRanges.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/StandardPluralRanges.java
new file mode 100644 (file)
index 0000000..8a9819a
--- /dev/null
@@ -0,0 +1,104 @@
+// © 2018 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.range;
+
+import java.util.MissingResourceException;
+
+import com.ibm.icu.impl.ICUData;
+import com.ibm.icu.impl.ICUResourceBundle;
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.UResource;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.UResourceBundle;
+
+/**
+ * @author sffc
+ *
+ */
+public class StandardPluralRanges {
+
+    StandardPlural[] flatTriples;
+    int numTriples = 0;
+
+    ////////////////////
+
+    private static final class PluralRangesDataSink extends UResource.Sink {
+
+        StandardPluralRanges output;
+
+        PluralRangesDataSink(StandardPluralRanges output) {
+            this.output = output;
+        }
+
+        @Override
+        public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+            UResource.Array entriesArray = value.getArray();
+            output.setCapacity(entriesArray.getSize());
+            for (int i = 0; entriesArray.getValue(i, value); ++i) {
+                UResource.Array pluralFormsArray = value.getArray();
+                pluralFormsArray.getValue(0, value);
+                StandardPlural first = StandardPlural.fromString(value.getString());
+                pluralFormsArray.getValue(1, value);
+                StandardPlural second = StandardPlural.fromString(value.getString());
+                pluralFormsArray.getValue(2, value);
+                StandardPlural result = StandardPlural.fromString(value.getString());
+                output.addPluralRange(first, second, result);
+            }
+        }
+    }
+
+    private static void getPluralRangesData(
+            ULocale locale,
+            StandardPluralRanges out) {
+        StringBuilder sb = new StringBuilder();
+        ICUResourceBundle resource;
+        resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "pluralRanges");
+        sb.append("locales/");
+        sb.append(locale.getLanguage());
+        String key = sb.toString();
+        String set;
+        try {
+            set = resource.getStringWithFallback(key);
+        } catch (MissingResourceException e) {
+            // Not all languages are covered: fail gracefully
+            return;
+        }
+
+        sb.setLength(0);
+        sb.append("rules/");
+        sb.append(set);
+        key = sb.toString();
+        PluralRangesDataSink sink = new PluralRangesDataSink(out);
+        resource.getAllItemsWithFallback(key, sink);
+    }
+
+    ////////////////////
+
+    public StandardPluralRanges(ULocale locale) {
+        getPluralRangesData(locale, this);
+    }
+
+    /** Used for data loading. */
+    private void addPluralRange(StandardPlural first, StandardPlural second, StandardPlural result) {
+        flatTriples[3 * numTriples] = first;
+        flatTriples[3 * numTriples + 1] = second;
+        flatTriples[3 * numTriples + 2] = result;
+        numTriples++;
+    }
+
+    /** Used for data loading. */
+    private void setCapacity(int length) {
+        flatTriples = new StandardPlural[length*3];
+    }
+
+    public StandardPlural resolve(StandardPlural first, StandardPlural second) {
+        for (int i = 0; i < numTriples; i++) {
+            if (first == flatTriples[3 * i] && second == flatTriples[3 * i + 1]) {
+                return flatTriples[3 * i + 2];
+            }
+        }
+        // Default fallback
+        return StandardPlural.OTHER;
+    }
+
+}
index ca842748a109c03e1a5f037f0c1de10f931abf46..1a43f85f36e1a5aa03d9dc66d32051632570ad6a 100644 (file)
@@ -5,6 +5,7 @@ package com.ibm.icu.number;
 import com.ibm.icu.impl.ICUData;
 import com.ibm.icu.impl.ICUResourceBundle;
 import com.ibm.icu.impl.SimpleFormatterImpl;
+import com.ibm.icu.impl.StandardPlural;
 import com.ibm.icu.impl.UResource;
 import com.ibm.icu.impl.number.DecimalQuantity;
 import com.ibm.icu.impl.number.MicroProps;
@@ -13,6 +14,7 @@ import com.ibm.icu.impl.number.NumberStringBuilder;
 import com.ibm.icu.impl.number.SimpleModifier;
 import com.ibm.icu.impl.number.range.PrefixInfixSuffixLengthHelper;
 import com.ibm.icu.impl.number.range.RangeMacroProps;
+import com.ibm.icu.impl.number.range.StandardPluralRanges;
 import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse;
 import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback;
 import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult;
@@ -25,15 +27,21 @@ import com.ibm.icu.util.UResourceBundle;
  */
 class NumberRangeFormatterImpl {
 
-    NumberFormatterImpl formatterImpl1;
-    NumberFormatterImpl formatterImpl2;
-    boolean fSameFormatters;
+    final NumberFormatterImpl formatterImpl1;
+    final NumberFormatterImpl formatterImpl2;
+    final boolean fSameFormatters;
 
-    NumberRangeFormatter.RangeCollapse fCollapse;
-    NumberRangeFormatter.RangeIdentityFallback fIdentityFallback;
+    final NumberRangeFormatter.RangeCollapse fCollapse;
+    final NumberRangeFormatter.RangeIdentityFallback fIdentityFallback;
 
-    String fRangePattern;
-    SimpleModifier fApproximatelyModifier;
+    // Should be final, but they are set in a helper function, not the constructor proper.
+    // TODO: Clean up to make these fields actually final.
+    /* final */ String fRangePattern;
+    /* final */ SimpleModifier fApproximatelyModifier;
+
+    final StandardPluralRanges fPluralRanges;
+
+    ////////////////////
 
      // Helper function for 2-dimensional switch statement
      int identity2d(RangeIdentityFallback a, RangeIdentityResult b) {
@@ -95,6 +103,8 @@ class NumberRangeFormatterImpl {
         out.fApproximatelyModifier = new SimpleModifier(sink.approximatelyPattern, null, false);
     }
 
+    ////////////////////
+
     public NumberRangeFormatterImpl(RangeMacroProps macros) {
         formatterImpl1 = new NumberFormatterImpl(macros.formatter1 != null ? macros.formatter1.resolve()
                 : NumberFormatter.withLocale(macros.loc).resolve());
@@ -112,6 +122,9 @@ class NumberRangeFormatterImpl {
         // numberFormatterBoth() or similar.
 
         getNumberRangeData(macros.loc, "latn", this);
+
+        // TODO: Get locale from PluralRules instead?
+        fPluralRanges = new StandardPluralRanges(macros.loc);
     }
 
     public FormattedNumberRange format(DecimalQuantity quantity1, DecimalQuantity quantity2, boolean equalBeforeRounding) {
@@ -129,9 +142,9 @@ class NumberRangeFormatterImpl {
         // TODO: Write this as MicroProps operator==() ?
         // TODO: Avoid the redundancy of these equality operations with the
         // ones in formatRange?
-        if (!micros1.modInner.equalsModifier(micros2.modInner)
-                || !micros1.modMiddle.equalsModifier(micros2.modMiddle)
-                || !micros1.modOuter.equalsModifier(micros2.modOuter)) {
+        if (!micros1.modInner.semanticallyEquivalent(micros2.modInner)
+                || !micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle)
+                || !micros1.modOuter.semanticallyEquivalent(micros2.modOuter)) {
             formatRange(quantity1, quantity2, string, micros1, micros2);
             return new FormattedNumberRange(string, quantity1, quantity2, RangeIdentityResult.NOT_EQUAL);
         }
@@ -213,7 +226,7 @@ class NumberRangeFormatterImpl {
             case UNIT:
             {
                 // OUTER MODIFIER
-                collapseOuter = micros1.modOuter.equalsModifier(micros2.modOuter);
+                collapseOuter = micros1.modOuter.semanticallyEquivalent(micros2.modOuter);
 
                 if (!collapseOuter) {
                     // Never collapse inner mods if outer mods are not collapsable
@@ -223,7 +236,7 @@ class NumberRangeFormatterImpl {
                 }
 
                 // MIDDLE MODIFIER
-                collapseMiddle = micros1.modMiddle.equalsModifier(micros2.modMiddle);
+                collapseMiddle = micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle);
 
                 if (!collapseMiddle) {
                     // Never collapse inner mods if outer mods are not collapsable
@@ -255,7 +268,7 @@ class NumberRangeFormatterImpl {
                 }
 
                 // INNER MODIFIER
-                collapseInner = micros1.modInner.equalsModifier(micros2.modInner);
+                collapseInner = micros1.modInner.semanticallyEquivalent(micros2.modInner);
 
                 // All done checking for collapsability.
                 break;
@@ -295,7 +308,8 @@ class NumberRangeFormatterImpl {
 
         if (collapseInner) {
             // Note: this is actually a mix of prefix and suffix, but adding to infix length works
-            h.lengthInfix += micros1.modInner.apply(string, h.index0(), h.index3());
+            Modifier mod = resolveModifierPlurals(micros1.modInner, micros2.modInner);
+            h.lengthInfix += mod.apply(string, h.index0(), h.index3());
         } else {
             h.length1 += micros1.modInner.apply(string, h.index0(), h.index1());
             h.length2 += micros2.modInner.apply(string, h.index2(), h.index3());
@@ -303,7 +317,8 @@ class NumberRangeFormatterImpl {
 
         if (collapseMiddle) {
             // Note: this is actually a mix of prefix and suffix, but adding to infix length works
-            h.lengthInfix += micros1.modMiddle.apply(string, h.index0(), h.index3());
+            Modifier mod = resolveModifierPlurals(micros1.modMiddle, micros2.modMiddle);
+            h.lengthInfix += mod.apply(string, h.index0(), h.index3());
         } else {
             h.length1 += micros1.modMiddle.apply(string, h.index0(), h.index1());
             h.length2 += micros2.modMiddle.apply(string, h.index2(), h.index3());
@@ -311,11 +326,36 @@ class NumberRangeFormatterImpl {
 
         if (collapseOuter) {
             // Note: this is actually a mix of prefix and suffix, but adding to infix length works
-            h.lengthInfix += micros1.modOuter.apply(string, h.index0(), h.index3());
+            Modifier mod = resolveModifierPlurals(micros1.modOuter, micros2.modOuter);
+            h.lengthInfix += mod.apply(string, h.index0(), h.index3());
         } else {
             h.length1 += micros1.modOuter.apply(string, h.index0(), h.index1());
             h.length2 += micros2.modOuter.apply(string, h.index2(), h.index3());
         }
     }
 
+    Modifier resolveModifierPlurals(Modifier first, Modifier second) {
+        Modifier.Parameters firstParameters = first.getParameters();
+        if (firstParameters == null) {
+            // No plural form; return a fallback (e.g., the first)
+            return first;
+        }
+
+        Modifier.Parameters secondParameters = second.getParameters();
+        if (secondParameters == null) {
+            // No plural form; return a fallback (e.g., the first)
+            return first;
+        }
+
+        // Get the required plural form from data
+        StandardPlural resultPlural = fPluralRanges.resolve(firstParameters.plural, secondParameters.plural);
+
+        // Get and return the new Modifier
+        assert firstParameters.obj == secondParameters.obj;
+        assert firstParameters.signum == secondParameters.signum;
+        Modifier mod = firstParameters.obj.getModifier(firstParameters.signum, resultPlural);
+        assert mod != null;
+        return mod;
+    }
+
 }
index 3854ff36f1c6644819c5a7e21afcc159a6ce8b4f..bd0c723b859483f3e895bf928ca5576657226a54 100644 (file)
@@ -242,7 +242,14 @@ public class ScientificNotation extends Notation implements Cloneable {
         }
 
         @Override
-        public boolean equalsModifier(Modifier other) {
+        public Parameters getParameters() {
+            // This method is not currently used.
+            assert false;
+            return null;
+        }
+
+        @Override
+        public boolean semanticallyEquivalent(Modifier other) {
             // This method is not currently used. (unsafe path not used in range formatting)
             assert false;
             return false;
@@ -316,7 +323,12 @@ public class ScientificNotation extends Notation implements Cloneable {
         }
 
         @Override
-        public boolean equalsModifier(Modifier other) {
+        public Parameters getParameters() {
+            return null;
+        }
+
+        @Override
+        public boolean semanticallyEquivalent(Modifier other) {
             if (!(other instanceof ScientificModifier)) {
                 return false;
             }
index e2be2f06c93b0e839d21efdab7baeb91a3eaf43f..19219d0d605f78b4e303c727752e773438afc292 100644 (file)
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:92dc0a5ca71ac54537a6c7c42c2f80ccbd3298d9ebf69c3d732199230d5100c6
-size 12487439
+oid sha256:6dadae4ae83d956325f4e089327e824bb32dbf75c98e9c2815ffa7521f3505ca
+size 12512661
index 6d6bf299cc0b63da2834b2f89e9a4fe910ce0a4c..d4ac91fdfbbfe12238e5ef645fa9b151a89a4e3a 100755 (executable)
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:d2308b3498ce1c2b869b60b5a0f7cea6f08aff2fac046ae260e10688c15c60b2
+oid sha256:b5ffb95eb91501a9f61ee31333e392ff183e0f7dddefb592f04b90fe2ac3490c
 size 92857
index af78f14dbdd9f35aa62817be4298fbda15f577da..21e599d71b04f58a3505b5b6f428222a3f2fe10b 100644 (file)
@@ -8,6 +8,7 @@ import java.util.Locale;
 
 import org.junit.Test;
 
+import com.ibm.icu.number.LocalizedNumberFormatter;
 import com.ibm.icu.number.LocalizedNumberRangeFormatter;
 import com.ibm.icu.number.Notation;
 import com.ibm.icu.number.NumberFormatter;
@@ -16,6 +17,7 @@ import com.ibm.icu.number.NumberRangeFormatter;
 import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse;
 import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback;
 import com.ibm.icu.number.Precision;
+import com.ibm.icu.number.UnlocalizedNumberFormatter;
 import com.ibm.icu.number.UnlocalizedNumberRangeFormatter;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.MeasureUnit;
@@ -100,10 +102,10 @@ public class NumberRangeFormatterTest {
             NumberRangeFormatter.with()
                 .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(UnitWidth.FULL_NAME)),
             new ULocale("en-us"),
-            "1 meter – 5 meters",  // TODO: This doesn't collapse because the plurals are different.  Fix?
+            "1–5 meters",
             "~5 meters",
             "~5 meters",
-            "0–3 meters",  // Note: It collapses when the plurals are the same
+            "0–3 meters",
             "~0 meters",
             "3–3,000 meters",
             "3,000–5,000 meters",
@@ -116,10 +118,10 @@ public class NumberRangeFormatterTest {
             NumberRangeFormatter.with()
                 .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.FULL_NAME)),
             new ULocale("fr-FR"),
-            "1 degré Fahrenheit – 5 degrés Fahrenheit",
+            "15 degrés Fahrenheit",
             "~5 degrés Fahrenheit",
             "~5 degrés Fahrenheit",
-            "0 degré Fahrenheit – 3 degrés Fahrenheit",
+            "03 degrés Fahrenheit",
             "~0 degré Fahrenheit",
             "3–3 000 degrés Fahrenheit",
             "3 000–5 000 degrés Fahrenheit",
@@ -361,6 +363,39 @@ public class NumberRangeFormatterTest {
             "~5,000 m",
             "5,000–5,000,000 m");
 
+        assertFormatRange(
+            "Default collapse, long-form compact notation",
+            NumberRangeFormatter.with()
+                .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactLong())),
+            new ULocale("de-CH"),
+            "1–5",
+            "~5",
+            "~5",
+            "0–3",
+            "~0",
+            "3–3 Tausend",
+            "3–5 Tausend",
+            "~5 Tausend",
+            "~5 Tausend",
+            "5 Tausend – 5 Millionen");
+
+        assertFormatRange(
+            "Unit collapse, long-form compact notation",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.UNIT)
+                .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactLong())),
+                new ULocale("de-CH"),
+            "1–5",
+            "~5",
+            "~5",
+            "0–3",
+            "~0",
+            "3–3 Tausend",
+            "3 Tausend – 5 Tausend",
+            "~5 Tausend",
+            "~5 Tausend",
+            "5 Tausend – 5 Millionen");
+
         assertFormatRange(
             "Default collapse on measurement unit with compact-short notation",
             NumberRangeFormatter.with()
@@ -551,6 +586,62 @@ public class NumberRangeFormatterTest {
             "5,000–5,000,000");
     }
 
+    @Test
+    public void testPlurals() {
+        // Locale sl has interesting plural forms:
+        // GBP{
+        //     one{"britanski funt"}
+        //     two{"britanska funta"}
+        //     few{"britanski funti"}
+        //     other{"britanskih funtov"}
+        // }
+        ULocale locale = new ULocale("sl");
+
+        UnlocalizedNumberFormatter unf = NumberFormatter.with()
+            .unit(GBP)
+            .unitWidth(UnitWidth.FULL_NAME)
+            .precision(Precision.integer());
+        LocalizedNumberFormatter lnf = unf.locale(locale);
+
+        // For comparison, run the non-range version of the formatter
+        assertEquals(Integer.toString(1), "1 britanski funt", lnf.format(1).toString());
+        assertEquals(Integer.toString(2), "2 britanska funta", lnf.format(2).toString());
+        assertEquals(Integer.toString(3), "3 britanski funti", lnf.format(3).toString());
+        assertEquals(Integer.toString(5), "5 britanskih funtov", lnf.format(5).toString());
+
+        LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter.with()
+            .numberFormatterBoth(unf)
+            .identityFallback(RangeIdentityFallback.RANGE)
+            .locale(locale);
+
+        Object[][] cases = new Object[][] {
+            {1, 1, "1–1 britanski funti"}, // one + one -> few
+            {1, 2, "1–2 britanska funta"}, // one + two -> two
+            {1, 3, "1–3 britanski funti"}, // one + few -> few
+            {1, 5, "1–5 britanskih funtov"}, // one + other -> other
+            {2, 1, "2–1 britanski funti"}, // two + one -> few
+            {2, 2, "2–2 britanska funta"}, // two + two -> two
+            {2, 3, "2–3 britanski funti"}, // two + few -> few
+            {2, 5, "2–5 britanskih funtov"}, // two + other -> other
+            {3, 1, "3–1 britanski funti"}, // few + one -> few
+            {3, 2, "3–2 britanska funta"}, // few + two -> two
+            {3, 3, "3–3 britanski funti"}, // few + few -> few
+            {3, 5, "3–5 britanskih funtov"}, // few + other -> other
+            {5, 1, "5–1 britanski funti"}, // other + one -> few
+            {5, 2, "5–2 britanska funta"}, // other + two -> two
+            {5, 3, "5–3 britanski funti"}, // other + few -> few
+            {5, 5, "5–5 britanskih funtov"}, // other + other -> other
+        };
+        for (Object[] cas : cases) {
+            int first = (Integer) cas[0];
+            int second = (Integer) cas[1];
+            String expected = (String) cas[2];
+            String message = Integer.toString(first) + " " + Integer.toString(second);
+            String actual = lnrf.formatRange(first, second).toString();
+            assertEquals(message, expected, actual);
+        }
+    }
+
     static void assertFormatRange(
             String message,
             UnlocalizedNumberRangeFormatter f,