]> granicus.if.org Git - icu/commitdiff
ICU-11276 Adding Java NumberRangeFormatter implementation.
authorShane Carr <shane@unicode.org>
Fri, 7 Sep 2018 12:30:37 +0000 (05:30 -0700)
committerShane Carr <shane@unicode.org>
Thu, 27 Sep 2018 21:27:40 +0000 (14:27 -0700)
23 files changed:
icu4c/source/i18n/number_decimalquantity.cpp
icu4c/source/i18n/number_scientific.cpp
icu4c/source/i18n/numrange_fluent.cpp
icu4c/source/i18n/numrange_impl.cpp
icu4c/source/i18n/unicode/numberformatter.h
icu4c/source/test/intltest/numbertest_range.cpp
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/DecimalQuantity_AbstractBCD.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java
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/PrefixInfixSuffixLengthHelper.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java
icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java
icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java
icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberRangeFormatter.java
icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java
icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java
icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java
icu4j/main/classes/core/src/com/ibm/icu/number/ScientificNotation.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberRangeFormatterTest.java

index 9d80e3349cb8aa19e398f7ecb47e2a54152741c8..17ed7ff1884e4f37d5298b348b26749875d62af3 100644 (file)
@@ -1154,8 +1154,22 @@ const char16_t* DecimalQuantity::checkHealth() const {
 }
 
 bool DecimalQuantity::operator==(const DecimalQuantity& other) const {
-    // FIXME: Make a faster implementation.
-    return toString() == other.toString();
+    bool basicEquals = scale == other.scale && precision == other.precision && flags == other.flags
+            && lOptPos == other.lOptPos && lReqPos == other.lReqPos && rReqPos == other.rReqPos
+            && rOptPos == other.rOptPos;
+    if (!basicEquals) {
+        return false;
+    }
+
+    if (precision == 0) {
+        return true;
+    }
+    for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) {
+        if (getDigit(m) != other.getDigit(m)) {
+            return false;
+        }
+    }
+    return true;
 }
 
 UnicodeString DecimalQuantity::toString() const {
index b9043d39773da9b9c47de19da98732ed4f2c678b..c318eee6ffbcdc6851cf07a3ec662980d724af72 100644 (file)
@@ -82,9 +82,10 @@ int32_t ScientificModifier::getPrefixLength() const {
 }
 
 int32_t ScientificModifier::getCodePointCount() const {
-    // This method is not used for strong modifiers.
-    U_ASSERT(false);
-    return 0;
+    // NOTE: This method is only called one place, NumberRangeFormatterImpl.
+    // The call site only cares about != 0 and != 1.
+    // Return a very large value so that if this method is used elsewhere, we should notice.
+    return 999;
 }
 
 bool ScientificModifier::isStrong() const {
index d4df042b3f00fa485603a4722d8e52bfbe5b7661..ec00588502e729045ce503c393be7cc5392033ce 100644 (file)
@@ -18,11 +18,20 @@ using namespace icu::number;
 using namespace icu::number::impl;
 
 
+// This function needs to be declared in this namespace so it can be friended.
+// NOTE: In Java, this logic is handled in the resolve() function.
+void icu::number::impl::touchRangeLocales(RangeMacroProps& macros) {
+    macros.formatter1.fMacros.locale = macros.locale;
+    macros.formatter2.fMacros.locale = macros.locale;
+}
+
+
 template<typename Derived>
 Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(const UnlocalizedNumberFormatter& formatter) const& {
     Derived copy(*this);
     copy.fMacros.formatter1 = formatter;
     copy.fMacros.singleFormatter = true;
+    touchRangeLocales(copy.fMacros);
     return copy;
 }
 
@@ -31,6 +40,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(const Unlocal
     Derived move(std::move(*this));
     move.fMacros.formatter1 = formatter;
     move.fMacros.singleFormatter = true;
+    touchRangeLocales(move.fMacros);
     return move;
 }
 
@@ -39,6 +49,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(UnlocalizedNu
     Derived copy(*this);
     copy.fMacros.formatter1 = std::move(formatter);
     copy.fMacros.singleFormatter = true;
+    touchRangeLocales(copy.fMacros);
     return copy;
 }
 
@@ -47,6 +58,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(UnlocalizedNu
     Derived move(std::move(*this));
     move.fMacros.formatter1 = std::move(formatter);
     move.fMacros.singleFormatter = true;
+    touchRangeLocales(move.fMacros);
     return move;
 }
 
@@ -55,6 +67,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(const Unloca
     Derived copy(*this);
     copy.fMacros.formatter1 = formatter;
     copy.fMacros.singleFormatter = false;
+    touchRangeLocales(copy.fMacros);
     return copy;
 }
 
@@ -63,6 +76,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(const Unloca
     Derived move(std::move(*this));
     move.fMacros.formatter1 = formatter;
     move.fMacros.singleFormatter = false;
+    touchRangeLocales(move.fMacros);
     return move;
 }
 
@@ -71,6 +85,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(UnlocalizedN
     Derived copy(*this);
     copy.fMacros.formatter1 = std::move(formatter);
     copy.fMacros.singleFormatter = false;
+    touchRangeLocales(copy.fMacros);
     return copy;
 }
 
@@ -79,6 +94,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(UnlocalizedN
     Derived move(std::move(*this));
     move.fMacros.formatter1 = std::move(formatter);
     move.fMacros.singleFormatter = false;
+    touchRangeLocales(move.fMacros);
     return move;
 }
 
@@ -87,6 +103,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(const Unloc
     Derived copy(*this);
     copy.fMacros.formatter2 = formatter;
     copy.fMacros.singleFormatter = false;
+    touchRangeLocales(copy.fMacros);
     return copy;
 }
 
@@ -95,6 +112,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(const Unloc
     Derived move(std::move(*this));
     move.fMacros.formatter2 = formatter;
     move.fMacros.singleFormatter = false;
+    touchRangeLocales(move.fMacros);
     return move;
 }
 
@@ -103,6 +121,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(Unlocalized
     Derived copy(*this);
     copy.fMacros.formatter2 = std::move(formatter);
     copy.fMacros.singleFormatter = false;
+    touchRangeLocales(copy.fMacros);
     return copy;
 }
 
@@ -111,6 +130,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(Unlocalized
     Derived move(std::move(*this));
     move.fMacros.formatter2 = std::move(formatter);
     move.fMacros.singleFormatter = false;
+    touchRangeLocales(move.fMacros);
     return move;
 }
 
@@ -228,11 +248,13 @@ LocalizedNumberRangeFormatter::~LocalizedNumberRangeFormatter() {
 LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(const RangeMacroProps& macros, const Locale& locale) {
     fMacros = macros;
     fMacros.locale = locale;
+    touchRangeLocales(fMacros);
 }
 
 LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(RangeMacroProps&& macros, const Locale& locale) {
     fMacros = std::move(macros);
     fMacros.locale = locale;
+    touchRangeLocales(fMacros);
 }
 
 LocalizedNumberRangeFormatter UnlocalizedNumberRangeFormatter::locale(const Locale& locale) const& {
index be6d38c5b6fbfc7e99bbd9c44408313742c6c648..d076352bb060ae606159e724ea84a884c127c20e 100644 (file)
@@ -65,7 +65,7 @@ void getNumberRangeData(const char* localeName, const char* nsName, NumberRangeD
     ures_getAllItemsWithFallback(rb.getAlias(), dataPath.data(), sink, status);
     if (U_FAILURE(status)) { return; }
 
-    // TODO: Is it necessary to maually fall back to latn, or does the data sink take care of that?
+    // TODO: Is it necessary to manually fall back to latn, or does the data sink take care of that?
 
     if (data.rangePattern.getArgumentLimit() == 0) {
         // No data!
@@ -107,11 +107,10 @@ void NumberRangeFormatterImpl::format(UFormattedNumberRangeData& data, bool equa
 
     MicroProps micros1;
     MicroProps micros2;
+    formatterImpl1.preProcess(data.quantity1, micros1, status);
     if (fSameFormatters) {
-        formatterImpl1.preProcess(data.quantity1, micros1, status);
         formatterImpl1.preProcess(data.quantity2, micros2, status);
     } else {
-        formatterImpl1.preProcess(data.quantity1, micros1, status);
         formatterImpl2.preProcess(data.quantity2, micros2, status);
     }
 
@@ -124,6 +123,7 @@ void NumberRangeFormatterImpl::format(UFormattedNumberRangeData& data, bool equa
             || !(*micros1.modMiddle == *micros2.modMiddle)
             || !(*micros1.modOuter == *micros2.modOuter)) {
         formatRange(data, micros1, micros2, status);
+        data.identityResult = UNUM_IDENTITY_RESULT_NOT_EQUAL;
         return;
     }
 
@@ -182,8 +182,8 @@ void NumberRangeFormatterImpl::formatSingleValue(UFormattedNumberRangeData& data
                                                  UErrorCode& status) const {
     if (U_FAILURE(status)) { return; }
     if (fSameFormatters) {
-        int32_t length = formatterImpl1.writeNumber(micros1, data.quantity1, data.string, 0, status);
-        formatterImpl1.writeAffixes(micros1, data.string, 0, length, status);
+        int32_t length = NumberFormatterImpl::writeNumber(micros1, data.quantity1, data.string, 0, status);
+        NumberFormatterImpl::writeAffixes(micros1, data.string, 0, length, status);
     } else {
         formatRange(data, micros1, micros2, status);
     }
@@ -195,8 +195,8 @@ void NumberRangeFormatterImpl::formatApproximately (UFormattedNumberRangeData& d
                                                     UErrorCode& status) const {
     if (U_FAILURE(status)) { return; }
     if (fSameFormatters) {
-        int32_t length = formatterImpl1.writeNumber(micros1, data.quantity1, data.string, 0, status);
-        length += formatterImpl1.writeAffixes(micros1, data.string, 0, length, status);
+        int32_t length = NumberFormatterImpl::writeNumber(micros1, data.quantity1, data.string, 0, status);
+        length += NumberFormatterImpl::writeAffixes(micros1, data.string, 0, length, status);
         fApproximatelyModifier.apply(data.string, 0, length, status);
     } else {
         formatRange(data, micros1, micros2, status);
@@ -242,9 +242,7 @@ void NumberRangeFormatterImpl::formatRange(UFormattedNumberRangeData& data,
             // (could disable collapsing of the middle modifier)
             // The modifiers are equal by this point, so we can look at just one of them.
             const Modifier* mm = micros1.modMiddle;
-            if (mm == nullptr) {
-                // pass
-            } else if (fCollapse == UNUM_RANGE_COLLAPSE_UNIT) {
+            if (fCollapse == UNUM_RANGE_COLLAPSE_UNIT) {
                 // Only collapse if the modifier is a unit.
                 // TODO: Make a better way to check for a unit?
                 // TODO: Handle case where the modifier has both notation and unit (compact currency)?
@@ -321,6 +319,7 @@ void NumberRangeFormatterImpl::formatRange(UFormattedNumberRangeData& data,
     // TODO: Support padding?
 
     if (collapseInner) {
+        // Note: this is actually a mix of prefix and suffix, but adding to infix length works
         lengthInfix += micros1.modInner->apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status);
     } else {
         length1 += micros1.modInner->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status);
@@ -328,6 +327,7 @@ void NumberRangeFormatterImpl::formatRange(UFormattedNumberRangeData& data,
     }
 
     if (collapseMiddle) {
+        // Note: this is actually a mix of prefix and suffix, but adding to infix length works
         lengthInfix += micros1.modMiddle->apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status);
     } else {
         length1 += micros1.modMiddle->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status);
@@ -335,6 +335,7 @@ void NumberRangeFormatterImpl::formatRange(UFormattedNumberRangeData& data,
     }
 
     if (collapseOuter) {
+        // Note: this is actually a mix of prefix and suffix, but adding to infix length works
         lengthInfix += micros1.modOuter->apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status);
     } else {
         length1 += micros1.modOuter->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status);
index 2a9743e6bd5b42f12e97478bfa538b1315e46230..b742e9104f1c2ccaff2c6120e26a6f2994f057d7 100644 (file)
@@ -145,6 +145,8 @@ class CurrencySymbols;
 class GeneratorHelpers;
 class DecNum;
 class NumberRangeFormatterImpl;
+struct RangeMacroProps;
+void touchRangeLocales(impl::RangeMacroProps& macros);
 
 } // namespace impl
 
@@ -2112,6 +2114,7 @@ class U_I18N_API NumberFormatterSettings {
     friend class UnlocalizedNumberFormatter;
 
     // Give NumberRangeFormatter access to the MacroProps
+    friend void impl::touchRangeLocales(impl::RangeMacroProps& macros);
     friend class impl::NumberRangeFormatterImpl;
 };
 
index 7d5cb0467a5166147abfc1d2c15f095cb493bed9..e028be9a82aef7289a52fd798c28d2f84c5782e0 100644 (file)
@@ -107,6 +107,54 @@ void NumberRangeFormatterTest::testBasic() {
         u"4,999 m – 5,001 km",
         u"5,000 m – 5,000 km",
         u"5,000 m – 5,000,000 km");
+
+    assertFormatRange(
+        u"Basic long unit",
+        NumberRangeFormatter::with()
+            .numberFormatterBoth(NumberFormatter::with().unit(METER).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME)),
+        Locale("en-us"),
+        u"1 meter – 5 meters",  // TODO: This doesn't collapse because the plurals are different.  Fix?
+        u"~5 meters",
+        u"~5 meters",
+        u"0–3 meters",  // Note: It collapses when the plurals are the same
+        u"~0 meters",
+        u"3–3,000 meters",
+        u"3,000–5,000 meters",
+        u"4,999–5,001 meters",
+        u"~5,000 meters",
+        u"5,000–5,000,000 meters");
+
+    assertFormatRange(
+        u"Non-English locale and unit",
+        NumberRangeFormatter::with()
+            .numberFormatterBoth(NumberFormatter::with().unit(FAHRENHEIT).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME)),
+        Locale("fr-FR"),
+        u"1 degré Fahrenheit – 5 degrés Fahrenheit",
+        u"~5 degrés Fahrenheit",
+        u"~5 degrés Fahrenheit",
+        u"0 degré Fahrenheit – 3 degrés Fahrenheit",
+        u"~0 degré Fahrenheit",
+        u"3–3 000 degrés Fahrenheit",
+        u"3 000–5 000 degrés Fahrenheit",
+        u"4 999–5 001 degrés Fahrenheit",
+        u"~5 000 degrés Fahrenheit",
+        u"5 000–5 000 000 degrés Fahrenheit");
+
+    assertFormatRange(
+        u"Portuguese currency",
+        NumberRangeFormatter::with()
+            .numberFormatterBoth(NumberFormatter::with().unit(PTE)),
+        Locale("pt-PT"),
+        u"1$00 - 5$00 \u200B",
+        u"~5$00 \u200B",
+        u"~5$00 \u200B",
+        u"0$00 - 3$00 \u200B",
+        u"~0$00 \u200B",
+        u"3$00 - 3000$00 \u200B",
+        u"3000$00 - 5000$00 \u200B",
+        u"4999$00 - 5001$00 \u200B",
+        u"~5000$00 \u200B",
+        u"5000$00 - 5,000,000$00 \u200B");
 }
 
 void NumberRangeFormatterTest::testCollapse() {
@@ -392,6 +440,40 @@ void NumberRangeFormatterTest::testCollapse() {
         u"~5K m",
         u"5K – 5M m");
 
+    assertFormatRange(
+        u"No collapse on scientific notation",
+        NumberRangeFormatter::with()
+            .collapse(UNUM_RANGE_COLLAPSE_NONE)
+            .numberFormatterBoth(NumberFormatter::with().notation(Notation::scientific())),
+        Locale("en-us"),
+        u"1E0 – 5E0",
+        u"~5E0",
+        u"~5E0",
+        u"0E0 – 3E0",
+        u"~0E0",
+        u"3E0 – 3E3",
+        u"3E3 – 5E3",
+        u"4.999E3 – 5.001E3",
+        u"~5E3",
+        u"5E3 – 5E6");
+
+    assertFormatRange(
+        u"All collapse on scientific notation",
+        NumberRangeFormatter::with()
+            .collapse(UNUM_RANGE_COLLAPSE_ALL)
+            .numberFormatterBoth(NumberFormatter::with().notation(Notation::scientific())),
+        Locale("en-us"),
+        u"1–5E0",
+        u"~5E0",
+        u"~5E0",
+        u"0–3E0",
+        u"~0E0",
+        u"3E0 – 3E3",
+        u"3–5E3",
+        u"4.999–5.001E3",
+        u"~5E3",
+        u"5E3 – 5E6");
+
     // TODO: Test compact currency?
     // The code is not smart enough to differentiate the notation from the unit.
 }
index 4ebb3d0cc632ac431b1b876236b0ae5278185ca4..9b3a60ba5855d5c6dd4315a186b176c08ae026cb 100644 (file)
@@ -74,6 +74,23 @@ public class ConstantAffixModifier implements Modifier {
         return strong;
     }
 
+    @Override
+    public boolean containsField(Field field) {
+        // This method is not currently used.
+        assert false;
+        return false;
+    }
+
+    @Override
+    public boolean equalsModifier(Modifier other) {
+        if (!(other instanceof ConstantAffixModifier)) {
+            return false;
+        }
+        ConstantAffixModifier _other = (ConstantAffixModifier) other;
+        return prefix.equals(_other.prefix) && suffix.equals(_other.suffix) && field == _other.field
+                && strong == _other.strong;
+    }
+
     @Override
     public String toString() {
         return String.format("<ConstantAffixModifier prefix:'%s' suffix:'%s'>", prefix, suffix);
index cdd129c128f5c161884dbfef4428a03cac004bd9..ba12ada88a87e69382b106904e425e835f22cbc8 100644 (file)
@@ -2,6 +2,8 @@
 // License & terms of use: http://www.unicode.org/copyright.html#License
 package com.ibm.icu.impl.number;
 
+import java.util.Arrays;
+
 import com.ibm.icu.text.NumberFormat.Field;
 
 /**
@@ -59,6 +61,32 @@ public class ConstantMultiFieldModifier implements Modifier {
         return strong;
     }
 
+    @Override
+    public boolean containsField(Field field) {
+        for (int i = 0; i < prefixFields.length; i++) {
+            if (prefixFields[i] == field) {
+                return true;
+            }
+        }
+        for (int i = 0; i < suffixFields.length; i++) {
+            if (suffixFields[i] == field) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean equalsModifier(Modifier other) {
+        if (!(other instanceof ConstantMultiFieldModifier)) {
+            return false;
+        }
+        ConstantMultiFieldModifier _other = (ConstantMultiFieldModifier) other;
+        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;
+    }
+
     @Override
     public String toString() {
         NumberStringBuilder temp = new NumberStringBuilder();
index 00a64bbe2ff09d3b9bc46fc4700f6d744a30d02d..0efa2b79a34135742e64c0b51a00bc67663dc5d7 100644 (file)
@@ -1014,6 +1014,37 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
         }
     }
 
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null) {
+            return false;
+        }
+        if (!(other instanceof DecimalQuantity_AbstractBCD)) {
+            return false;
+        }
+        DecimalQuantity_AbstractBCD _other = (DecimalQuantity_AbstractBCD) other;
+
+        boolean basicEquals = scale == _other.scale && precision == _other.precision && flags == _other.flags
+                && lOptPos == _other.lOptPos && lReqPos == _other.lReqPos && rReqPos == _other.rReqPos
+                && rOptPos == _other.rOptPos;
+        if (!basicEquals) {
+            return false;
+        }
+
+        if (precision == 0) {
+            return true;
+        }
+        for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) {
+            if (getDigit(m) != _other.getDigit(m)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Returns a single digit from the BCD list. No internal state is changed by calling this method.
      *
index a7ab9b98a890717239cabf40f639b971ab279002..cf0b1f1b4b5fe65fba8193791f7188afa8f6ac2b 100644 (file)
@@ -2,6 +2,8 @@
 // License & terms of use: http://www.unicode.org/copyright.html#License
 package com.ibm.icu.impl.number;
 
+import com.ibm.icu.text.NumberFormat.Field;
+
 /**
  * A Modifier is an object that can be passed through the formatting pipeline until it is finally applied
  * to the string builder. A Modifier usually contains a prefix and a suffix that are applied, but it
@@ -48,4 +50,14 @@ public interface Modifier {
      * @return Whether the modifier is strong.
      */
     public boolean isStrong();
+
+    /**
+     * Whether the modifier contains at least one occurrence of the given field.
+     */
+    public boolean containsField(Field currency);
+
+    /**
+     * Returns whether the affixes owned by this modifier are equal to the ones owned by the given modifier.
+     */
+    public boolean equalsModifier(Modifier other);
 }
index 13d0ab2de100d8e6cfdcd1ec971f17a2ba7351c7..80525d663ed097c5396c415cb3882b35f837df46 100644 (file)
@@ -7,6 +7,7 @@ import com.ibm.icu.impl.number.AffixUtils.SymbolProvider;
 import com.ibm.icu.number.NumberFormatter.SignDisplay;
 import com.ibm.icu.number.NumberFormatter.UnitWidth;
 import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat.Field;
 import com.ibm.icu.text.PluralRules;
 import com.ibm.icu.util.Currency;
 
@@ -319,6 +320,20 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
         return isStrong;
     }
 
+    @Override
+    public boolean containsField(Field field) {
+        // This method is not currently used. (unsafe path not used in range formatting)
+        assert false;
+        return false;
+    }
+
+    @Override
+    public boolean equalsModifier(Modifier other) {
+        // This method is not currently used. (unsafe path not used in range formatting)
+        assert false;
+        return false;
+    }
+
     private int insertPrefix(NumberStringBuilder sb, int position) {
         prepareAffix(true);
         int length = AffixUtils.unescape(currentAffix, sb, position, this);
index 5ac1bcfa40828cdd59c1563947133df6c7612d5e..e23c5e917ef1ac8558da032510cd48589d3e126e 100644 (file)
@@ -3,7 +3,9 @@
 package com.ibm.icu.impl.number;
 
 import com.ibm.icu.impl.SimpleFormatterImpl;
+import com.ibm.icu.impl.number.range.PrefixInfixSuffixLengthHelper;
 import com.ibm.icu.text.NumberFormat.Field;
+import com.ibm.icu.util.ICUException;
 
 /**
  * The second primary implementation of {@link Modifier}, this one consuming a
@@ -80,6 +82,22 @@ public class SimpleModifier implements Modifier {
         return strong;
     }
 
+    @Override
+    public boolean containsField(Field field) {
+        // This method is not currently used.
+        assert false;
+        return false;
+    }
+
+    @Override
+    public boolean equalsModifier(Modifier other) {
+        if (!(other instanceof SimpleModifier)) {
+            return false;
+        }
+        SimpleModifier _other = (SimpleModifier) other;
+        return compiledPattern.equals(_other.compiledPattern) && field == _other.field && strong == _other.strong;
+    }
+
     /**
      * TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is because
      * DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it should not
@@ -123,4 +141,66 @@ public class SimpleModifier implements Modifier {
             return prefixLength + suffixLength;
         }
     }
+
+    /**
+     * TODO: Like above, this belongs with the rest of the SimpleFormatterImpl code.
+     * I put it here so that the SimpleFormatter uses in NumberStringBuilder are near each other.
+     *
+     * <p>
+     * Applies the compiled two-argument pattern to the NumberStringBuilder.
+     *
+     * <p>
+     * This method is optimized for the case where the prefix and suffix are often empty, such as
+     * in the range pattern like "{0}-{1}".
+     */
+    public static void formatTwoArgPattern(String compiledPattern, NumberStringBuilder result, int index, PrefixInfixSuffixLengthHelper h,
+            Field field) {
+        int argLimit = SimpleFormatterImpl.getArgumentLimit(compiledPattern);
+        if (argLimit != 2) {
+            throw new ICUException();
+        }
+        int offset = 1; // offset into compiledPattern
+        int length = 0; // chars added to result
+
+        int prefixLength = compiledPattern.charAt(offset);
+        offset++;
+        if (prefixLength < ARG_NUM_LIMIT) {
+            // No prefix
+            prefixLength = 0;
+        } else {
+            prefixLength -= ARG_NUM_LIMIT;
+            result.insert(index + length, compiledPattern, offset, offset + prefixLength, field);
+            offset += prefixLength;
+            length += prefixLength;
+            offset++;
+        }
+
+        int infixLength = compiledPattern.charAt(offset);
+        offset++;
+        if (infixLength < ARG_NUM_LIMIT) {
+            // No infix
+            infixLength = 0;
+        } else {
+            infixLength -= ARG_NUM_LIMIT;
+            result.insert(index + length, compiledPattern, offset, offset + infixLength, field);
+            offset += infixLength;
+            length += infixLength;
+            offset++;
+        }
+
+        int suffixLength;
+        if (offset == compiledPattern.length()) {
+            // No suffix
+            suffixLength = 0;
+        } else {
+            suffixLength = compiledPattern.charAt(offset) -  ARG_NUM_LIMIT;
+            offset++;
+            result.insert(index + length, compiledPattern, offset, offset + suffixLength, field);
+            length += suffixLength;
+        }
+
+        h.lengthPrefix = prefixLength;
+        h.lengthInfix = infixLength;
+        h.lengthSuffix = suffixLength;
+    }
 }
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/PrefixInfixSuffixLengthHelper.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/PrefixInfixSuffixLengthHelper.java
new file mode 100644 (file)
index 0000000..696f3f0
--- /dev/null
@@ -0,0 +1,30 @@
+// © 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;
+
+/**
+ * A small, mutable internal helper class for keeping track of offsets on range patterns.
+ */
+public class PrefixInfixSuffixLengthHelper {
+    public int lengthPrefix = 0;
+    public int length1 = 0;
+    public int lengthInfix = 0;
+    public int length2 = 0;
+    public int lengthSuffix = 0;
+
+    public int index0() {
+        return lengthPrefix;
+    }
+
+    public int index1() {
+        return lengthPrefix + length1;
+    }
+
+    public int index2() {
+        return lengthPrefix + length1 + lengthInfix;
+    }
+
+    public int index3() {
+        return lengthPrefix + length1 + lengthInfix + length2;
+    }
+}
index 800dcf6ab9bdd9731c387cead04214587af07e0c..e2872516a3073daee9eae13c688d0a936d1776e5 100644 (file)
@@ -16,6 +16,7 @@ import com.ibm.icu.util.ULocale;
 public class RangeMacroProps {
     public UnlocalizedNumberFormatter formatter1;
     public UnlocalizedNumberFormatter formatter2;
+    public int sameFormatters = -1; // -1 for unset, 0 for false, 1 for true
     public RangeCollapse collapse;
     public RangeIdentityFallback identityFallback;
     public ULocale loc;
index a759fa4ebd74cab844bd8216b94a27b897099406..ad1ab6b06eaa010f7f12789386d1c039195154bc 100644 (file)
@@ -23,16 +23,16 @@ import com.ibm.icu.util.ICUUncheckedIOException;
  * @see NumberRangeFormatter
  */
 public class FormattedNumberRange {
-    final NumberStringBuilder nsb;
-    final DecimalQuantity first;
-    final DecimalQuantity second;
+    final NumberStringBuilder string;
+    final DecimalQuantity quantity1;
+    final DecimalQuantity quantity2;
     final RangeIdentityResult identityResult;
 
-    FormattedNumberRange(NumberStringBuilder nsb, DecimalQuantity first, DecimalQuantity second,
+    FormattedNumberRange(NumberStringBuilder string, DecimalQuantity quantity1, DecimalQuantity quantity2,
             RangeIdentityResult identityResult) {
-        this.nsb = nsb;
-        this.first = first;
-        this.second = second;
+        this.string = string;
+        this.quantity1 = quantity1;
+        this.quantity2 = quantity2;
         this.identityResult = identityResult;
     }
 
@@ -46,7 +46,7 @@ public class FormattedNumberRange {
      */
     @Override
     public String toString() {
-        return nsb.toString();
+        return string.toString();
     }
 
     /**
@@ -67,7 +67,7 @@ public class FormattedNumberRange {
      */
     public <A extends Appendable> A appendTo(A appendable) {
         try {
-            appendable.append(nsb);
+            appendable.append(string);
         } catch (IOException e) {
             // Throw as an unchecked exception to avoid users needing try/catch
             throw new ICUUncheckedIOException(e);
@@ -105,7 +105,7 @@ public class FormattedNumberRange {
      * @see NumberRangeFormatter
      */
     public boolean nextFieldPosition(FieldPosition fieldPosition) {
-        return nsb.nextFieldPosition(fieldPosition);
+        return string.nextFieldPosition(fieldPosition);
     }
 
     /**
@@ -124,7 +124,7 @@ public class FormattedNumberRange {
      * @see NumberRangeFormatter
      */
     public AttributedCharacterIterator toCharacterIterator() {
-        return nsb.toCharacterIterator();
+        return string.toCharacterIterator();
     }
 
     /**
@@ -138,7 +138,7 @@ public class FormattedNumberRange {
      * @see #getSecondBigDecimal
      */
     public BigDecimal getFirstBigDecimal() {
-        return first.toBigDecimal();
+        return quantity1.toBigDecimal();
     }
 
     /**
@@ -152,7 +152,7 @@ public class FormattedNumberRange {
      * @see #getFirstBigDecimal
      */
     public BigDecimal getSecondBigDecimal() {
-        return second.toBigDecimal();
+        return quantity2.toBigDecimal();
     }
 
     /**
@@ -180,8 +180,8 @@ public class FormattedNumberRange {
     public int hashCode() {
         // NumberStringBuilder and BigDecimal are mutable, so we can't call
         // #equals() or #hashCode() on them directly.
-        return Arrays.hashCode(nsb.toCharArray()) ^ Arrays.hashCode(nsb.toFieldArray())
-                ^ first.toBigDecimal().hashCode() ^ second.toBigDecimal().hashCode();
+        return Arrays.hashCode(string.toCharArray()) ^ Arrays.hashCode(string.toFieldArray())
+                ^ quantity1.toBigDecimal().hashCode() ^ quantity2.toBigDecimal().hashCode();
     }
 
     /**
@@ -201,9 +201,9 @@ public class FormattedNumberRange {
         // NumberStringBuilder and BigDecimal are mutable, so we can't call
         // #equals() or #hashCode() on them directly.
         FormattedNumberRange _other = (FormattedNumberRange) other;
-        return Arrays.equals(nsb.toCharArray(), _other.nsb.toCharArray())
-                && Arrays.equals(nsb.toFieldArray(), _other.nsb.toFieldArray())
-                && first.toBigDecimal().equals(_other.first.toBigDecimal())
-                && second.toBigDecimal().equals(_other.second.toBigDecimal());
+        return Arrays.equals(string.toCharArray(), _other.string.toCharArray())
+                && Arrays.equals(string.toFieldArray(), _other.string.toFieldArray())
+                && quantity1.toBigDecimal().equals(_other.quantity1.toBigDecimal())
+                && quantity2.toBigDecimal().equals(_other.quantity2.toBigDecimal());
     }
 }
index c877fb800899517d1ca0e349e4c06a350932288b..29e9cb4ab759f9974cfbdec3780c03261b01ed0d 100644 (file)
@@ -152,9 +152,9 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
     public FormattedNumber format(DecimalQuantity fq) {
         NumberStringBuilder string = new NumberStringBuilder();
         if (computeCompiled()) {
-            compiled.apply(fq, string);
+            compiled.format(fq, string);
         } else {
-            NumberFormatterImpl.applyStatic(resolve(), fq, string);
+            NumberFormatterImpl.formatStatic(resolve(), fq, string);
         }
         return new FormattedNumber(string, fq);
     }
@@ -190,7 +190,7 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
         // Further benchmarking is required.
         long currentCount = callCount.incrementAndGet(this);
         if (currentCount == macros.threshold.longValue()) {
-            compiled = NumberFormatterImpl.fromMacros(macros);
+            compiled = new NumberFormatterImpl(macros);
             return true;
         } else if (compiled != null) {
             return true;
index 8973aae4233a71e71e2a8c62ecb913448e7923cf..27c787534d7cc324d88d42347a9374aa2a355f5b 100644 (file)
@@ -4,9 +4,6 @@ package com.ibm.icu.number;
 
 import com.ibm.icu.impl.number.DecimalQuantity;
 import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
-import com.ibm.icu.impl.number.NumberStringBuilder;
-import com.ibm.icu.impl.number.range.RangeMacroProps;
-import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult;
 
 /**
  * A NumberRangeFormatter that has a locale associated with it; this means .formatRange() methods are available.
@@ -18,6 +15,8 @@ import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult;
  */
 public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings<LocalizedNumberRangeFormatter> {
 
+    private volatile NumberRangeFormatterImpl fImpl;
+
     LocalizedNumberRangeFormatter(NumberRangeFormatterSettings<?> parent, int key, Object value) {
         super(parent, key, value);
     }
@@ -85,28 +84,10 @@ public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings<
     }
 
     FormattedNumberRange formatImpl(DecimalQuantity first, DecimalQuantity second, boolean equalBeforeRounding) {
-        // TODO: This is a placeholder implementation.
-        RangeMacroProps macros = resolve();
-        LocalizedNumberFormatter f1 , f2;
-        if (macros.formatter1 != null) {
-            f1 = macros.formatter1.locale(macros.loc);
-        } else {
-            f1 = NumberFormatter.withLocale(macros.loc);
-        }
-        if (macros.formatter2 != null) {
-            f2 = macros.formatter2.locale(macros.loc);
-        } else {
-            f2 = NumberFormatter.withLocale(macros.loc);
+        if (fImpl == null) {
+            fImpl = new NumberRangeFormatterImpl(resolve());
         }
-        FormattedNumber r1 = f1.format(first);
-        FormattedNumber r2 = f2.format(second);
-        NumberStringBuilder nsb = new NumberStringBuilder();
-        nsb.append(r1.nsb);
-        nsb.append(" --- ", null);
-        nsb.append(r2.nsb);
-        RangeIdentityResult identityResult = equalBeforeRounding ? RangeIdentityResult.EQUAL_BEFORE_ROUNDING
-                : RangeIdentityResult.NOT_EQUAL;
-        return new FormattedNumberRange(nsb, first, second, identityResult);
+        return fImpl.format(first, second, equalBeforeRounding);
     }
 
     @Override
index 06dad539f337fc9bb93aadebf19896037b2096d1..11f590f56c4fc9900fa08bf14004b83d6d6e6b8f 100644 (file)
@@ -42,21 +42,21 @@ import com.ibm.icu.util.MeasureUnit;
 class NumberFormatterImpl {
 
     /** Builds a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly. */
-    public static NumberFormatterImpl fromMacros(MacroProps macros) {
-        MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, true);
-        return new NumberFormatterImpl(microPropsGenerator);
+    public NumberFormatterImpl(MacroProps macros) {
+        this(macrosToMicroGenerator(macros, true));
     }
 
     /**
      * Builds and evaluates an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once.
      */
-    public static void applyStatic(
+    public static int formatStatic(
             MacroProps macros,
             DecimalQuantity inValue,
             NumberStringBuilder outString) {
-        MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false);
-        MicroProps micros = microPropsGenerator.processQuantity(inValue);
-        microsToString(micros, inValue, outString);
+        MicroProps micros = preProcessUnsafe(macros, inValue);
+        int length = writeNumber(micros, inValue, outString, 0);
+        length += writeAffixes(micros, outString, 0, length);
+        return length;
     }
 
     /**
@@ -82,9 +82,40 @@ class NumberFormatterImpl {
         this.microPropsGenerator = microPropsGenerator;
     }
 
-    public void apply(DecimalQuantity inValue, NumberStringBuilder outString) {
+    /**
+     * Evaluates the "safe" MicroPropsGenerator created by "fromMacros".
+     */
+    public int format(DecimalQuantity inValue, NumberStringBuilder outString) {
+        MicroProps micros = preProcess(inValue);
+        int length = writeNumber(micros, inValue, outString, 0);
+        length += writeAffixes(micros, outString, 0, length);
+        return length;
+    }
+
+    /**
+     * Like format(), but saves the result into an output MicroProps without additional processing.
+     */
+    public MicroProps preProcess(DecimalQuantity inValue) {
+        MicroProps micros = microPropsGenerator.processQuantity(inValue);
+        micros.rounder.apply(inValue);
+        if (micros.integerWidth.maxInt == -1) {
+            inValue.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE);
+        } else {
+            inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
+        }
+        return micros;
+    }
+
+    private static MicroProps preProcessUnsafe(MacroProps macros, DecimalQuantity inValue) {
+        MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false);
         MicroProps micros = microPropsGenerator.processQuantity(inValue);
-        microsToString(micros, inValue, outString);
+        micros.rounder.apply(inValue);
+        if (micros.integerWidth.maxInt == -1) {
+            inValue.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE);
+        } else {
+            inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
+        }
+        return micros;
     }
 
     public int getPrefixSuffix(byte signum, StandardPlural plural, NumberStringBuilder output) {
@@ -350,64 +381,55 @@ class NumberFormatterImpl {
     //////////
 
     /**
-     * Synthesizes the output string from a MicroProps and DecimalQuantity.
-     *
-     * @param micros
-     *            The MicroProps after the quantity has been consumed. Will not be mutated.
-     * @param quantity
-     *            The DecimalQuantity to be rendered. May be mutated.
-     * @param string
-     *            The output string. Will be mutated.
+     * Adds the affixes.  Intended to be called immediately after formatNumber.
      */
-    private static void microsToString(
+    public static int writeAffixes(
             MicroProps micros,
-            DecimalQuantity quantity,
-            NumberStringBuilder string) {
-        micros.rounder.apply(quantity);
-        if (micros.integerWidth.maxInt == -1) {
-            quantity.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE);
-        } else {
-            quantity.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
-        }
-        int length = writeNumber(micros, quantity, string);
-        // NOTE: When range formatting is added, these modifiers can bubble up.
-        // For now, apply them all here at once.
+            NumberStringBuilder string,
+            int start,
+            int end) {
         // Always apply the inner modifier (which is "strong").
-        length += micros.modInner.apply(string, 0, length);
+        int length = micros.modInner.apply(string, start, end);
         if (micros.padding.isValid()) {
-            micros.padding.padAndApply(micros.modMiddle, micros.modOuter, string, 0, length);
+            micros.padding.padAndApply(micros.modMiddle, micros.modOuter, string, start, end + length);
         } else {
-            length += micros.modMiddle.apply(string, 0, length);
-            length += micros.modOuter.apply(string, 0, length);
+            length += micros.modMiddle.apply(string, start, end + length);
+            length += micros.modOuter.apply(string, start, end + length);
         }
+        return length;
     }
 
-    private static int writeNumber(
+    /**
+     * Synthesizes the output string from a MicroProps and DecimalQuantity.
+     * This method formats only the main number, not affixes.
+     */
+    public static int writeNumber(
             MicroProps micros,
             DecimalQuantity quantity,
-            NumberStringBuilder string) {
+            NumberStringBuilder string,
+            int index) {
         int length = 0;
         if (quantity.isInfinite()) {
-            length += string.insert(length, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER);
+            length += string.insert(length + index, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER);
 
         } else if (quantity.isNaN()) {
-            length += string.insert(length, micros.symbols.getNaN(), NumberFormat.Field.INTEGER);
+            length += string.insert(length + index, micros.symbols.getNaN(), NumberFormat.Field.INTEGER);
 
         } else {
             // Add the integer digits
-            length += writeIntegerDigits(micros, quantity, string);
+            length += writeIntegerDigits(micros, quantity, string, length + index);
 
             // Add the decimal point
             if (quantity.getLowerDisplayMagnitude() < 0
                     || micros.decimal == DecimalSeparatorDisplay.ALWAYS) {
-                length += string.insert(length,
+                length += string.insert(length + index,
                         micros.useCurrency ? micros.symbols.getMonetaryDecimalSeparatorString()
                                 : micros.symbols.getDecimalSeparatorString(),
                         NumberFormat.Field.DECIMAL_SEPARATOR);
             }
 
             // Add the fraction digits
-            length += writeFractionDigits(micros, quantity, string);
+            length += writeFractionDigits(micros, quantity, string, length + index);
         }
 
         return length;
@@ -416,13 +438,14 @@ class NumberFormatterImpl {
     private static int writeIntegerDigits(
             MicroProps micros,
             DecimalQuantity quantity,
-            NumberStringBuilder string) {
+            NumberStringBuilder string,
+            int index) {
         int length = 0;
         int integerCount = quantity.getUpperDisplayMagnitude() + 1;
         for (int i = 0; i < integerCount; i++) {
             // Add grouping separator
             if (micros.grouping.groupAtPosition(i, quantity)) {
-                length += string.insert(0,
+                length += string.insert(index,
                         micros.useCurrency ? micros.symbols.getMonetaryGroupingSeparatorString()
                                 : micros.symbols.getGroupingSeparatorString(),
                         NumberFormat.Field.GROUPING_SEPARATOR);
@@ -431,11 +454,11 @@ class NumberFormatterImpl {
             // Get and append the next digit value
             byte nextDigit = quantity.getDigit(i);
             if (micros.symbols.getCodePointZero() != -1) {
-                length += string.insertCodePoint(0,
+                length += string.insertCodePoint(index,
                         micros.symbols.getCodePointZero() + nextDigit,
                         NumberFormat.Field.INTEGER);
             } else {
-                length += string.insert(0,
+                length += string.insert(index,
                         micros.symbols.getDigitStringsLocal()[nextDigit],
                         NumberFormat.Field.INTEGER);
             }
@@ -446,17 +469,18 @@ class NumberFormatterImpl {
     private static int writeFractionDigits(
             MicroProps micros,
             DecimalQuantity quantity,
-            NumberStringBuilder string) {
+            NumberStringBuilder string,
+            int index) {
         int length = 0;
         int fractionCount = -quantity.getLowerDisplayMagnitude();
         for (int i = 0; i < fractionCount; i++) {
             // Get and append the next digit value
             byte nextDigit = quantity.getDigit(-i - 1);
             if (micros.symbols.getCodePointZero() != -1) {
-                length += string.appendCodePoint(micros.symbols.getCodePointZero() + nextDigit,
+                length += string.insertCodePoint(length + index, micros.symbols.getCodePointZero() + nextDigit,
                         NumberFormat.Field.FRACTION);
             } else {
-                length += string.append(micros.symbols.getDigitStringsLocal()[nextDigit],
+                length += string.insert(length + index, micros.symbols.getDigitStringsLocal()[nextDigit],
                         NumberFormat.Field.FRACTION);
             }
         }
index 5867621c142fa3934ebb58d1837ad7ea9b402691..32b389a21ca2ecf3198486ec4a6517877ee045ad 100644 (file)
@@ -47,10 +47,10 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
     static final int KEY_PER_UNIT = 15;
     static final int KEY_MAX = 16;
 
-    final NumberFormatterSettings<?> parent;
-    final int key;
-    final Object value;
-    volatile MacroProps resolvedMacros;
+    private final NumberFormatterSettings<?> parent;
+    private final int key;
+    private final Object value;
+    private volatile MacroProps resolvedMacros;
 
     NumberFormatterSettings(NumberFormatterSettings<?> parent, int key, Object value) {
         this.parent = parent;
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java
new file mode 100644 (file)
index 0000000..b262990
--- /dev/null
@@ -0,0 +1,321 @@
+// © 2018 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+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.UResource;
+import com.ibm.icu.impl.number.DecimalQuantity;
+import com.ibm.icu.impl.number.MicroProps;
+import com.ibm.icu.impl.number.Modifier;
+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.number.NumberRangeFormatter.RangeCollapse;
+import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback;
+import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.UResourceBundle;
+
+/**
+ * Business logic behind NumberRangeFormatter.
+ */
+class NumberRangeFormatterImpl {
+
+    NumberFormatterImpl formatterImpl1;
+    NumberFormatterImpl formatterImpl2;
+    boolean fSameFormatters;
+
+    NumberRangeFormatter.RangeCollapse fCollapse;
+    NumberRangeFormatter.RangeIdentityFallback fIdentityFallback;
+
+    String fRangePattern;
+    SimpleModifier fApproximatelyModifier;
+
+     // Helper function for 2-dimensional switch statement
+     int identity2d(RangeIdentityFallback a, RangeIdentityResult b) {
+         return a.ordinal() | (b.ordinal() << 4);
+     }
+
+    private static final class NumberRangeDataSink extends UResource.Sink {
+
+        String rangePattern;
+        String approximatelyPattern;
+
+        // For use with SimpleFormatterImpl
+        StringBuilder sb;
+
+        NumberRangeDataSink(StringBuilder sb) {
+            this.sb = sb;
+        }
+
+        @Override
+        public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+            UResource.Table miscTable = value.getTable();
+            for (int i = 0; miscTable.getKeyAndValue(i, key, value); ++i) {
+                if (key.contentEquals("range") && rangePattern == null) {
+                    String pattern = value.getString();
+                    rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2);
+                }
+                if (key.contentEquals("approximately") && approximatelyPattern == null) {
+                    String pattern = value.getString();
+                    approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2);
+                }
+            }
+        }
+    }
+
+    private static void getNumberRangeData(
+            ULocale locale,
+            String nsName,
+            NumberRangeFormatterImpl out) {
+        StringBuilder sb = new StringBuilder();
+        NumberRangeDataSink sink = new NumberRangeDataSink(sb);
+        ICUResourceBundle resource;
+        resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
+        sb.append("NumberElements/");
+        sb.append(nsName);
+        sb.append("/miscPatterns");
+        String key = sb.toString();
+        resource.getAllItemsWithFallback(key, sink);
+
+        // TODO: Is it necessary to manually fall back to latn, or does the data sink take care of that?
+
+        if (sink.rangePattern == null) {
+            sink.rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments("{0} --- {1}", sb, 2, 2);
+        }
+        if (sink.approximatelyPattern == null) {
+            sink.approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments("~{0}", sb, 1, 1);
+        }
+
+        out.fRangePattern = sink.rangePattern;
+        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());
+        formatterImpl2 = new NumberFormatterImpl(macros.formatter2 != null ? macros.formatter2.resolve()
+                : NumberFormatter.withLocale(macros.loc).resolve());
+        fSameFormatters = macros.sameFormatters != 0;
+        fCollapse = macros.collapse != null ? macros.collapse : NumberRangeFormatter.RangeCollapse.AUTO;
+        fIdentityFallback = macros.identityFallback != null ? macros.identityFallback
+                : NumberRangeFormatter.RangeIdentityFallback.APPROXIMATELY;
+
+        // TODO: As of this writing (ICU 63), there is no locale that has different number miscPatterns
+        // based on numbering system. Therefore, data is loaded only from latn. If this changes,
+        // this part of the code should be updated to load from the local numbering system.
+        // The numbering system could come from the one specified in the NumberFormatter passed to
+        // numberFormatterBoth() or similar.
+
+        getNumberRangeData(macros.loc, "latn", this);
+    }
+
+    public FormattedNumberRange format(DecimalQuantity quantity1, DecimalQuantity quantity2, boolean equalBeforeRounding) {
+        NumberStringBuilder string = new NumberStringBuilder();
+        MicroProps micros1 = formatterImpl1.preProcess(quantity1);
+        MicroProps micros2;
+        if (fSameFormatters) {
+            micros2 = formatterImpl1.preProcess(quantity2);
+        } else {
+            micros2 = formatterImpl2.preProcess(quantity2);
+        }
+
+        // If any of the affixes are different, an identity is not possible
+        // and we must use formatRange().
+        // 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)) {
+            formatRange(quantity1, quantity2, string, micros1, micros2);
+            return new FormattedNumberRange(string, quantity1, quantity2, RangeIdentityResult.NOT_EQUAL);
+        }
+
+        // Check for identity
+        RangeIdentityResult identityResult;
+        if (equalBeforeRounding) {
+            identityResult = RangeIdentityResult.EQUAL_BEFORE_ROUNDING;
+        } else if (quantity1.equals(quantity2)) {
+            identityResult = RangeIdentityResult.EQUAL_AFTER_ROUNDING;
+        } else {
+            identityResult = RangeIdentityResult.NOT_EQUAL;
+        }
+
+        // Java does not let us use a constexpr like C++;
+        // we need to expand identity2d calls.
+        switch (identity2d(fIdentityFallback, identityResult)) {
+        case (3 | (2 << 4)): // RANGE, NOT_EQUAL
+        case (3 | (1 << 4)): // RANGE, EQUAL_AFTER_ROUNDING
+        case (3 | (0 << 4)): // RANGE, EQUAL_BEFORE_ROUNDING
+        case (2 | (2 << 4)): // APPROXIMATELY, NOT_EQUAL
+        case (1 | (2 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, NOT_EQUAL
+        case (0 | (2 << 4)): // SINGLE_VALUE, NOT_EQUAL
+            formatRange(quantity1, quantity2, string, micros1, micros2);
+            break;
+
+        case (2 | (1 << 4)): // APPROXIMATELY, EQUAL_AFTER_ROUNDING
+        case (2 | (0 << 4)): // APPROXIMATELY, EQUAL_BEFORE_ROUNDING
+        case (1 | (1 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, EQUAL_AFTER_ROUNDING
+            formatApproximately(quantity1, quantity2, string, micros1, micros2);
+            break;
+
+        case (1 | (0 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, EQUAL_BEFORE_ROUNDING
+        case (0 | (1 << 4)): // SINGLE_VALUE, EQUAL_AFTER_ROUNDING
+        case (0 | (0 << 4)): // SINGLE_VALUE, EQUAL_BEFORE_ROUNDING
+            formatSingleValue(quantity1, quantity2, string, micros1, micros2);
+            break;
+
+        default:
+            assert false;
+            break;
+        }
+
+        return new FormattedNumberRange(string, quantity1, quantity2, identityResult);
+    }
+
+    private void formatSingleValue(DecimalQuantity quantity1, DecimalQuantity quantity2, NumberStringBuilder string,
+            MicroProps micros1, MicroProps micros2) {
+        if (fSameFormatters) {
+            int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0);
+            NumberFormatterImpl.writeAffixes(micros1, string, 0, length);
+        } else {
+            formatRange(quantity1, quantity2, string, micros1, micros2);
+        }
+
+    }
+
+    private void formatApproximately(DecimalQuantity quantity1, DecimalQuantity quantity2, NumberStringBuilder string,
+            MicroProps micros1, MicroProps micros2) {
+        if (fSameFormatters) {
+            int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0);
+            length += NumberFormatterImpl.writeAffixes(micros1, string, 0, length);
+            fApproximatelyModifier.apply(string, 0, length);
+        } else {
+            formatRange(quantity1, quantity2, string, micros1, micros2);
+        }
+    }
+
+    private void formatRange(DecimalQuantity quantity1, DecimalQuantity quantity2, NumberStringBuilder string,
+            MicroProps micros1, MicroProps micros2) {
+        // modInner is always notation (scientific); collapsable in ALL.
+        // modOuter is always units; collapsable in ALL, AUTO, and UNIT.
+        // modMiddle could be either; collapsable in ALL and sometimes AUTO and UNIT.
+        // Never collapse an outer mod but not an inner mod.
+        boolean collapseOuter, collapseMiddle, collapseInner;
+        switch (fCollapse) {
+            case ALL:
+            case AUTO:
+            case UNIT:
+            {
+                // OUTER MODIFIER
+                collapseOuter = micros1.modOuter.equalsModifier(micros2.modOuter);
+
+                if (!collapseOuter) {
+                    // Never collapse inner mods if outer mods are not collapsable
+                    collapseMiddle = false;
+                    collapseInner = false;
+                    break;
+                }
+
+                // MIDDLE MODIFIER
+                collapseMiddle = micros1.modMiddle.equalsModifier(micros2.modMiddle);
+
+                if (!collapseMiddle) {
+                    // Never collapse inner mods if outer mods are not collapsable
+                    collapseInner = false;
+                    break;
+                }
+
+                // MIDDLE MODIFIER HEURISTICS
+                // (could disable collapsing of the middle modifier)
+                // The modifiers are equal by this point, so we can look at just one of them.
+                Modifier mm = micros1.modMiddle;
+                if (fCollapse == RangeCollapse.UNIT) {
+                    // Only collapse if the modifier is a unit.
+                    // TODO: Make a better way to check for a unit?
+                    // TODO: Handle case where the modifier has both notation and unit (compact currency)?
+                    if (mm.containsField(NumberFormat.Field.CURRENCY) && mm.containsField(NumberFormat.Field.PERCENT)) {
+                        collapseMiddle = false;
+                    }
+                } else if (fCollapse == RangeCollapse.AUTO) {
+                    // Heuristic as of ICU 63: collapse only if the modifier is more than one code point.
+                    if (mm.getCodePointCount() <= 1) {
+                        collapseMiddle = false;
+                    }
+                }
+
+                if (!collapseMiddle || fCollapse != RangeCollapse.ALL) {
+                    collapseInner = false;
+                    break;
+                }
+
+                // INNER MODIFIER
+                collapseInner = micros1.modInner.equalsModifier(micros2.modInner);
+
+                // All done checking for collapsability.
+                break;
+            }
+
+            default:
+                collapseOuter = false;
+                collapseMiddle = false;
+                collapseInner = false;
+                break;
+        }
+
+        // Java doesn't have macros, constexprs, or stack objects.
+        // Use a helper object instead.
+        PrefixInfixSuffixLengthHelper h = new PrefixInfixSuffixLengthHelper();
+
+        SimpleModifier.formatTwoArgPattern(fRangePattern, string, 0, h, null);
+
+        // SPACING HEURISTIC
+        // Add spacing unless all modifiers are collapsed.
+        // TODO: add API to control this?
+        {
+            boolean repeatInner = !collapseInner && micros1.modInner.getCodePointCount() > 0;
+            boolean repeatMiddle = !collapseMiddle && micros1.modMiddle.getCodePointCount() > 0;
+            boolean repeatOuter = !collapseOuter && micros1.modOuter.getCodePointCount() > 0;
+            if (repeatInner || repeatMiddle || repeatOuter) {
+                // Add spacing
+                h.lengthInfix += string.insertCodePoint(h.index1(), '\u0020', null);
+                h.lengthInfix += string.insertCodePoint(h.index2(), '\u0020', null);
+            }
+        }
+
+        h.length1 += NumberFormatterImpl.writeNumber(micros1, quantity1, string, h.index0());
+        h.length2 += NumberFormatterImpl.writeNumber(micros2, quantity2, string, h.index2());
+
+        // TODO: Support padding?
+
+        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());
+        } else {
+            h.length1 += micros1.modInner.apply(string, h.index0(), h.index1());
+            h.length2 += micros2.modInner.apply(string, h.index2(), h.index3());
+        }
+
+        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());
+        } else {
+            h.length1 += micros1.modMiddle.apply(string, h.index0(), h.index1());
+            h.length2 += micros2.modMiddle.apply(string, h.index2(), h.index3());
+        }
+
+        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());
+        } else {
+            h.length1 += micros1.modOuter.apply(string, h.index0(), h.index1());
+            h.length2 += micros2.modOuter.apply(string, h.index2(), h.index3());
+        }
+    }
+
+}
index 40ae7371d6a7604f50973a432868c3ed94b08b6d..50df9af21bcc5ea2b547cdbdce82f4a3c19b2a88 100644 (file)
@@ -23,14 +23,15 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
     static final int KEY_LOCALE = 1;
     static final int KEY_FORMATTER_1 = 2;
     static final int KEY_FORMATTER_2 = 3;
-    static final int KEY_COLLAPSE = 4;
-    static final int KEY_IDENTITY_FALLBACK = 5;
-    static final int KEY_MAX = 6;
+    static final int KEY_SAME_FORMATTERS = 4;
+    static final int KEY_COLLAPSE = 5;
+    static final int KEY_IDENTITY_FALLBACK = 6;
+    static final int KEY_MAX = 7;
 
-    final NumberRangeFormatterSettings<?> parent;
-    final int key;
-    final Object value;
-    volatile RangeMacroProps resolvedMacros;
+    private final NumberRangeFormatterSettings<?> parent;
+    private final int key;
+    private final Object value;
+    private volatile RangeMacroProps resolvedMacros;
 
     NumberRangeFormatterSettings(NumberRangeFormatterSettings<?> parent, int key, Object value) {
         this.parent = parent;
@@ -55,7 +56,7 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
      */
     @SuppressWarnings("unchecked")
     public T numberFormatterBoth(UnlocalizedNumberFormatter formatter) {
-        return (T) numberFormatterFirst(formatter).numberFormatterSecond(formatter);
+        return (T) create(KEY_SAME_FORMATTERS, true).create(KEY_FORMATTER_1, formatter);
     }
 
     /**
@@ -72,8 +73,9 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
      * @see NumberFormatter
      * @see NumberRangeFormatter
      */
+    @SuppressWarnings("unchecked")
     public T numberFormatterFirst(UnlocalizedNumberFormatter formatterFirst) {
-        return create(KEY_FORMATTER_1, formatterFirst);
+        return (T) create(KEY_SAME_FORMATTERS, false).create(KEY_FORMATTER_1, formatterFirst);
     }
 
     /**
@@ -90,8 +92,9 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
      * @see NumberFormatter
      * @see NumberRangeFormatter
      */
+    @SuppressWarnings("unchecked")
     public T numberFormatterSecond(UnlocalizedNumberFormatter formatterSecond) {
-        return create(KEY_FORMATTER_2, formatterSecond);
+        return (T) create(KEY_SAME_FORMATTERS, false).create(KEY_FORMATTER_2, formatterSecond);
     }
 
     /**
@@ -173,6 +176,11 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
                     macros.formatter2 = (UnlocalizedNumberFormatter) current.value;
                 }
                 break;
+            case KEY_SAME_FORMATTERS:
+                if (macros.sameFormatters == -1) {
+                    macros.sameFormatters = (boolean) current.value ? 1 : 0;
+                }
+                break;
             case KEY_COLLAPSE:
                 if (macros.collapse == null) {
                     macros.collapse = (RangeCollapse) current.value;
@@ -188,6 +196,13 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
             }
             current = current.parent;
         }
+        // Copy the locale into the children (see touchRangeLocales in C++)
+        if (macros.formatter1 != null) {
+            macros.formatter1.resolve().loc = macros.loc;
+        }
+        if (macros.formatter2 != null) {
+            macros.formatter2.resolve().loc = macros.loc;
+        }
         resolvedMacros = macros;
         return macros;
     }
index d3f62b963e349f1c9d205d4bbd09081b74b750eb..7b283734813dd383c39048c4564e991eb7aa02b9 100644 (file)
@@ -13,6 +13,7 @@ import com.ibm.icu.number.NumberFormatter.SignDisplay;
 import com.ibm.icu.number.Precision.SignificantRounderImpl;
 import com.ibm.icu.text.DecimalFormatSymbols;
 import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.NumberFormat.Field;
 
 /**
  * A class that defines the scientific notation style to be used when formatting numbers in
@@ -221,8 +222,10 @@ public class ScientificNotation extends Notation implements Cloneable {
 
         @Override
         public int getCodePointCount() {
-            // This method is not used for strong modifiers.
-            throw new AssertionError();
+            // NOTE: This method is only called one place, NumberRangeFormatterImpl.
+            // The call site only cares about != 0 and != 1.
+            // Return a very large value so that if this method is used elsewhere, we should notice.
+            return 999;
         }
 
         @Override
@@ -231,6 +234,20 @@ public class ScientificNotation extends Notation implements Cloneable {
             return true;
         }
 
+        @Override
+        public boolean containsField(Field field) {
+            // This method is not currently used. (unsafe path not used in range formatting)
+            assert false;
+            return false;
+        }
+
+        @Override
+        public boolean equalsModifier(Modifier other) {
+            // This method is not currently used. (unsafe path not used in range formatting)
+            assert false;
+            return false;
+        }
+
         @Override
         public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
             return doApply(exponent, output, rightIndex);
@@ -288,5 +305,22 @@ public class ScientificNotation extends Notation implements Cloneable {
             // Scientific is always strong
             return true;
         }
+
+        @Override
+        public boolean containsField(Field field) {
+            // This method is not used for inner modifiers.
+            assert false;
+            return false;
+        }
+
+        @Override
+        public boolean equalsModifier(Modifier other) {
+            if (!(other instanceof ScientificHandler)) {
+                return false;
+            }
+            ScientificHandler _other = (ScientificHandler) other;
+            // TODO: Check for locale symbols and settings as well? Could be less efficient.
+            return exponent == _other.exponent;
+        }
     }
 }
\ No newline at end of file
index 82510a7a531ac4ee9927bc93661f33c02aaa5f0d..af78f14dbdd9f35aa62817be4298fbda15f577da 100644 (file)
@@ -9,11 +9,16 @@ import java.util.Locale;
 import org.junit.Test;
 
 import com.ibm.icu.number.LocalizedNumberRangeFormatter;
+import com.ibm.icu.number.Notation;
 import com.ibm.icu.number.NumberFormatter;
-import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
+import com.ibm.icu.number.NumberFormatter.UnitWidth;
 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.UnlocalizedNumberRangeFormatter;
 import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.MeasureUnit;
 import com.ibm.icu.util.ULocale;
 
 /**
@@ -24,6 +29,7 @@ public class NumberRangeFormatterTest {
 
     private static final Currency USD = Currency.getInstance("USD");
     private static final Currency GBP = Currency.getInstance("GBP");
+    private static final Currency PTE = Currency.getInstance("PTE");
 
     @Test
     public void testSanity() {
@@ -42,86 +48,507 @@ public class NumberRangeFormatterTest {
     @Test
     public void testBasic() {
         assertFormatRange(
-                "Basic",
-                NumberRangeFormatter.with(),
-                ULocale.US,
-                "1 --- 5",
-                "5 --- 5",
-                "5 --- 5",
-                "0 --- 3",
-                "0 --- 0",
-                "3 --- 3,000",
-                "3,000 --- 5,000",
-                "4,999 --- 5,001",
-                "5,000 --- 5,000",
-                "5,000 --- 5,000,000");
+            "Basic",
+            NumberRangeFormatter.with(),
+            new ULocale("en-us"),
+            "1–5",
+            "~5",
+            "~5",
+            "0–3",
+            "~0",
+            "3–3,000",
+            "3,000–5,000",
+            "4,999–5,001",
+            "~5,000",
+            "5,000–5,000,000");
+
+        assertFormatRange(
+            "Basic with units",
+            NumberRangeFormatter.with()
+                .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)),
+            new ULocale("en-us"),
+            "1–5 m",
+            "~5 m",
+            "~5 m",
+            "0–3 m",
+            "~0 m",
+            "3–3,000 m",
+            "3,000–5,000 m",
+            "4,999–5,001 m",
+            "~5,000 m",
+            "5,000–5,000,000 m");
+
+        assertFormatRange(
+            "Basic with different units",
+            NumberRangeFormatter.with()
+                .numberFormatterFirst(NumberFormatter.with().unit(MeasureUnit.METER))
+                .numberFormatterSecond(NumberFormatter.with().unit(MeasureUnit.KILOMETER)),
+            new ULocale("en-us"),
+            "1 m – 5 km",
+            "5 m – 5 km",
+            "5 m – 5 km",
+            "0 m – 3 km",
+            "0 m – 0 km",
+            "3 m – 3,000 km",
+            "3,000 m – 5,000 km",
+            "4,999 m – 5,001 km",
+            "5,000 m – 5,000 km",
+            "5,000 m – 5,000,000 km");
+
+        assertFormatRange(
+            "Basic long unit",
+            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?
+            "~5 meters",
+            "~5 meters",
+            "0–3 meters",  // Note: It collapses when the plurals are the same
+            "~0 meters",
+            "3–3,000 meters",
+            "3,000–5,000 meters",
+            "4,999–5,001 meters",
+            "~5,000 meters",
+            "5,000–5,000,000 meters");
+
+        assertFormatRange(
+            "Non-English locale and unit",
+            NumberRangeFormatter.with()
+                .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.FULL_NAME)),
+            new ULocale("fr-FR"),
+            "1 degré Fahrenheit – 5 degrés Fahrenheit",
+            "~5 degrés Fahrenheit",
+            "~5 degrés Fahrenheit",
+            "0 degré Fahrenheit – 3 degrés Fahrenheit",
+            "~0 degré Fahrenheit",
+            "3–3 000 degrés Fahrenheit",
+            "3 000–5 000 degrés Fahrenheit",
+            "4 999–5 001 degrés Fahrenheit",
+            "~5 000 degrés Fahrenheit",
+            "5 000–5 000 000 degrés Fahrenheit");
+
+        assertFormatRange(
+            "Portuguese currency",
+            NumberRangeFormatter.with()
+                .numberFormatterBoth(NumberFormatter.with().unit(PTE)),
+            new ULocale("pt-PT"),
+            "1$00 - 5$00 \u200B",
+            "~5$00 \u200B",
+            "~5$00 \u200B",
+            "0$00 - 3$00 \u200B",
+            "~0$00 \u200B",
+            "3$00 - 3000$00 \u200B",
+            "3000$00 - 5000$00 \u200B",
+            "4999$00 - 5001$00 \u200B",
+            "~5000$00 \u200B",
+            "5000$00 - 5,000,000$00 \u200B");
     }
 
     @Test
-    public void testNullBehavior() {
-        assertFormatRange(
-                "Basic",
-                NumberRangeFormatter.with().numberFormatterBoth(null),
-                ULocale.US,
-                "1 --- 5",
-                "5 --- 5",
-                "5 --- 5",
-                "0 --- 3",
-                "0 --- 0",
-                "3 --- 3,000",
-                "3,000 --- 5,000",
-                "4,999 --- 5,001",
-                "5,000 --- 5,000",
-                "5,000 --- 5,000,000");
-
-        assertFormatRange(
-                "Basic",
-                NumberRangeFormatter.with().numberFormatterFirst(null),
-                ULocale.US,
-                "1 --- 5",
-                "5 --- 5",
-                "5 --- 5",
-                "0 --- 3",
-                "0 --- 0",
-                "3 --- 3,000",
-                "3,000 --- 5,000",
-                "4,999 --- 5,001",
-                "5,000 --- 5,000",
-                "5,000 --- 5,000,000");
-
-        assertFormatRange(
-                "Basic",
-                NumberRangeFormatter.with()
-                    .numberFormatterFirst(NumberFormatter.with().grouping(GroupingStrategy.OFF))
-                    .numberFormatterSecond(null),
-                ULocale.US,
-                "1 --- 5",
-                "5 --- 5",
-                "5 --- 5",
-                "0 --- 3",
-                "0 --- 0",
-                "3 --- 3,000",
-                "3000 --- 5,000",
-                "4999 --- 5,001",
-                "5000 --- 5,000",
-                "5000 --- 5,000,000");
-
-        assertFormatRange(
-                "Basic",
-                NumberRangeFormatter.with()
-                    .numberFormatterFirst(null)
-                    .numberFormatterSecond(NumberFormatter.with().grouping(GroupingStrategy.OFF)),
-                ULocale.US,
-                "1 --- 5",
-                "5 --- 5",
-                "5 --- 5",
-                "0 --- 3",
-                "0 --- 0",
-                "3 --- 3000",
-                "3,000 --- 5000",
-                "4,999 --- 5001",
-                "5,000 --- 5000",
-                "5,000 --- 5000000");
+    public void testCollapse() {
+        assertFormatRange(
+            "Default collapse on currency (default rounding)",
+            NumberRangeFormatter.with()
+                .numberFormatterBoth(NumberFormatter.with().unit(USD)),
+            new ULocale("en-us"),
+            "$1.00 – $5.00",
+            "~$5.00",
+            "~$5.00",
+            "$0.00 – $3.00",
+            "~$0.00",
+            "$3.00 – $3,000.00",
+            "$3,000.00 – $5,000.00",
+            "$4,999.00 – $5,001.00",
+            "~$5,000.00",
+            "$5,000.00 – $5,000,000.00");
+
+        assertFormatRange(
+            "Default collapse on currency",
+            NumberRangeFormatter.with()
+                .numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())),
+            new ULocale("en-us"),
+            "$1 – $5",
+            "~$5",
+            "~$5",
+            "$0 – $3",
+            "~$0",
+            "$3 – $3,000",
+            "$3,000 – $5,000",
+            "$4,999 – $5,001",
+            "~$5,000",
+            "$5,000 – $5,000,000");
+
+        assertFormatRange(
+            "No collapse on currency",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.NONE)
+                .numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())),
+            new ULocale("en-us"),
+            "$1 – $5",
+            "~$5",
+            "~$5",
+            "$0 – $3",
+            "~$0",
+            "$3 – $3,000",
+            "$3,000 – $5,000",
+            "$4,999 – $5,001",
+            "~$5,000",
+            "$5,000 – $5,000,000");
+
+        assertFormatRange(
+            "Unit collapse on currency",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.UNIT)
+                .numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())),
+            new ULocale("en-us"),
+            "$1–5",
+            "~$5",
+            "~$5",
+            "$0–3",
+            "~$0",
+            "$3–3,000",
+            "$3,000–5,000",
+            "$4,999–5,001",
+            "~$5,000",
+            "$5,000–5,000,000");
+
+        assertFormatRange(
+            "All collapse on currency",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.ALL)
+                .numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())),
+            new ULocale("en-us"),
+            "$1–5",
+            "~$5",
+            "~$5",
+            "$0–3",
+            "~$0",
+            "$3–3,000",
+            "$3,000–5,000",
+            "$4,999–5,001",
+            "~$5,000",
+            "$5,000–5,000,000");
+
+        assertFormatRange(
+            "Default collapse on currency ISO code",
+            NumberRangeFormatter.with()
+                .numberFormatterBoth(NumberFormatter.with()
+                    .unit(GBP)
+                    .unitWidth(UnitWidth.ISO_CODE)
+                    .precision(Precision.integer())),
+            new ULocale("en-us"),
+            "GBP 1–5",
+            "~GBP 5",  // TODO: Fix this at some point
+            "~GBP 5",
+            "GBP 0–3",
+            "~GBP 0",
+            "GBP 3–3,000",
+            "GBP 3,000–5,000",
+            "GBP 4,999–5,001",
+            "~GBP 5,000",
+            "GBP 5,000–5,000,000");
+
+        assertFormatRange(
+            "No collapse on currency ISO code",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.NONE)
+                .numberFormatterBoth(NumberFormatter.with()
+                    .unit(GBP)
+                    .unitWidth(UnitWidth.ISO_CODE)
+                    .precision(Precision.integer())),
+            new ULocale("en-us"),
+            "GBP 1 – GBP 5",
+            "~GBP 5",  // TODO: Fix this at some point
+            "~GBP 5",
+            "GBP 0 – GBP 3",
+            "~GBP 0",
+            "GBP 3 – GBP 3,000",
+            "GBP 3,000 – GBP 5,000",
+            "GBP 4,999 – GBP 5,001",
+            "~GBP 5,000",
+            "GBP 5,000 – GBP 5,000,000");
+
+        assertFormatRange(
+            "Unit collapse on currency ISO code",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.UNIT)
+                .numberFormatterBoth(NumberFormatter.with()
+                    .unit(GBP)
+                    .unitWidth(UnitWidth.ISO_CODE)
+                    .precision(Precision.integer())),
+            new ULocale("en-us"),
+            "GBP 1–5",
+            "~GBP 5",  // TODO: Fix this at some point
+            "~GBP 5",
+            "GBP 0–3",
+            "~GBP 0",
+            "GBP 3–3,000",
+            "GBP 3,000–5,000",
+            "GBP 4,999–5,001",
+            "~GBP 5,000",
+            "GBP 5,000–5,000,000");
+
+        assertFormatRange(
+            "All collapse on currency ISO code",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.ALL)
+                .numberFormatterBoth(NumberFormatter.with()
+                    .unit(GBP)
+                    .unitWidth(UnitWidth.ISO_CODE)
+                    .precision(Precision.integer())),
+            new ULocale("en-us"),
+            "GBP 1–5",
+            "~GBP 5",  // TODO: Fix this at some point
+            "~GBP 5",
+            "GBP 0–3",
+            "~GBP 0",
+            "GBP 3–3,000",
+            "GBP 3,000–5,000",
+            "GBP 4,999–5,001",
+            "~GBP 5,000",
+            "GBP 5,000–5,000,000");
+
+        // Default collapse on measurement unit is in testBasic()
+
+        assertFormatRange(
+            "No collapse on measurement unit",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.NONE)
+                .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)),
+            new ULocale("en-us"),
+            "1 m – 5 m",
+            "~5 m",
+            "~5 m",
+            "0 m – 3 m",
+            "~0 m",
+            "3 m – 3,000 m",
+            "3,000 m – 5,000 m",
+            "4,999 m – 5,001 m",
+            "~5,000 m",
+            "5,000 m – 5,000,000 m");
+
+        assertFormatRange(
+            "Unit collapse on measurement unit",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.UNIT)
+                .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)),
+            new ULocale("en-us"),
+            "1–5 m",
+            "~5 m",
+            "~5 m",
+            "0–3 m",
+            "~0 m",
+            "3–3,000 m",
+            "3,000–5,000 m",
+            "4,999–5,001 m",
+            "~5,000 m",
+            "5,000–5,000,000 m");
+
+        assertFormatRange(
+            "All collapse on measurement unit",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.ALL)
+                .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)),
+            new ULocale("en-us"),
+            "1–5 m",
+            "~5 m",
+            "~5 m",
+            "0–3 m",
+            "~0 m",
+            "3–3,000 m",
+            "3,000–5,000 m",
+            "4,999–5,001 m",
+            "~5,000 m",
+            "5,000–5,000,000 m");
+
+        assertFormatRange(
+            "Default collapse on measurement unit with compact-short notation",
+            NumberRangeFormatter.with()
+                .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)),
+            new ULocale("en-us"),
+            "1–5 m",
+            "~5 m",
+            "~5 m",
+            "0–3 m",
+            "~0 m",
+            "3–3K m",
+            "3K – 5K m",
+            "~5K m",
+            "~5K m",
+            "5K – 5M m");
+
+        assertFormatRange(
+            "No collapse on measurement unit with compact-short notation",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.NONE)
+                .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)),
+            new ULocale("en-us"),
+            "1 m – 5 m",
+            "~5 m",
+            "~5 m",
+            "0 m – 3 m",
+            "~0 m",
+            "3 m – 3K m",
+            "3K m – 5K m",
+            "~5K m",
+            "~5K m",
+            "5K m – 5M m");
+
+        assertFormatRange(
+            "Unit collapse on measurement unit with compact-short notation",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.UNIT)
+                .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)),
+            new ULocale("en-us"),
+            "1–5 m",
+            "~5 m",
+            "~5 m",
+            "0–3 m",
+            "~0 m",
+            "3–3K m",
+            "3K – 5K m",
+            "~5K m",
+            "~5K m",
+            "5K – 5M m");
+
+        assertFormatRange(
+            "All collapse on measurement unit with compact-short notation",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.ALL)
+                .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)),
+            new ULocale("en-us"),
+            "1–5 m",
+            "~5 m",
+            "~5 m",
+            "0–3 m",
+            "~0 m",
+            "3–3K m",
+            "3–5K m",  // this one is the key use case for ALL
+            "~5K m",
+            "~5K m",
+            "5K – 5M m");
+
+        assertFormatRange(
+            "No collapse on scientific notation",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.NONE)
+                .numberFormatterBoth(NumberFormatter.with().notation(Notation.scientific())),
+            new ULocale("en-us"),
+            "1E0 – 5E0",
+            "~5E0",
+            "~5E0",
+            "0E0 – 3E0",
+            "~0E0",
+            "3E0 – 3E3",
+            "3E3 – 5E3",
+            "4.999E3 – 5.001E3",
+            "~5E3",
+            "5E3 – 5E6");
+
+        assertFormatRange(
+            "All collapse on scientific notation",
+            NumberRangeFormatter.with()
+                .collapse(RangeCollapse.ALL)
+                .numberFormatterBoth(NumberFormatter.with().notation(Notation.scientific())),
+            new ULocale("en-us"),
+            "1–5E0",
+            "~5E0",
+            "~5E0",
+            "0–3E0",
+            "~0E0",
+            "3E0 – 3E3",
+            "3–5E3",
+            "4.999–5.001E3",
+            "~5E3",
+            "5E3 – 5E6");
+
+        // TODO: Test compact currency?
+        // The code is not smart enough to differentiate the notation from the unit.
+    }
+
+    @Test
+    public void testIdentity() {
+        assertFormatRange(
+            "Identity fallback Range",
+            NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.RANGE),
+            new ULocale("en-us"),
+            "1–5",
+            "5–5",
+            "5–5",
+            "0–3",
+            "0–0",
+            "3–3,000",
+            "3,000–5,000",
+            "4,999–5,001",
+            "5,000–5,000",
+            "5,000–5,000,000");
+
+        assertFormatRange(
+            "Identity fallback Approximately or Single Value",
+            NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE),
+            new ULocale("en-us"),
+            "1–5",
+            "~5",
+            "5",
+            "0–3",
+            "0",
+            "3–3,000",
+            "3,000–5,000",
+            "4,999–5,001",
+            "5,000",
+            "5,000–5,000,000");
+
+        assertFormatRange(
+            "Identity fallback  Single Value",
+            NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.SINGLE_VALUE),
+            new ULocale("en-us"),
+            "1–5",
+            "5",
+            "5",
+            "0–3",
+            "0",
+            "3–3,000",
+            "3,000–5,000",
+            "4,999–5,001",
+            "5,000",
+            "5,000–5,000,000");
+
+        assertFormatRange(
+            "Identity fallback Approximately or Single Value with compact notation",
+            NumberRangeFormatter.with()
+                .identityFallback(RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE)
+                .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort())),
+            new ULocale("en-us"),
+            "1–5",
+            "~5",
+            "5",
+            "0–3",
+            "0",
+            "3–3K",
+            "3K – 5K",
+            "~5K",
+            "5K",
+            "5K – 5M");
+    }
+
+    @Test
+    public void testDifferentFormatters() {
+        assertFormatRange(
+            "Different rounding rules",
+            NumberRangeFormatter.with()
+                .numberFormatterFirst(NumberFormatter.with().precision(Precision.integer()))
+                .numberFormatterSecond(NumberFormatter.with().precision(Precision.fixedDigits(2))),
+            new ULocale("en-us"),
+            "1–5.0",
+            "5–5.0",
+            "5–5.0",
+            "0–3.0",
+            "0–0.0",
+            "3–3,000",
+            "3,000–5,000",
+            "4,999–5,000",
+            "5,000–5,000",  // TODO: Should this one be ~5,000?
+            "5,000–5,000,000");
     }
 
     static void assertFormatRange(