From: Shane Carr Date: Fri, 7 Sep 2018 12:30:37 +0000 (-0700) Subject: ICU-11276 Adding Java NumberRangeFormatter implementation. X-Git-Tag: release-63-rc~63^2~16 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=55974b2fb62b6d062f8eacc35554b4e6b7ea106e;p=icu ICU-11276 Adding Java NumberRangeFormatter implementation. --- diff --git a/icu4c/source/i18n/number_decimalquantity.cpp b/icu4c/source/i18n/number_decimalquantity.cpp index 9d80e3349cb..17ed7ff1884 100644 --- a/icu4c/source/i18n/number_decimalquantity.cpp +++ b/icu4c/source/i18n/number_decimalquantity.cpp @@ -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 { diff --git a/icu4c/source/i18n/number_scientific.cpp b/icu4c/source/i18n/number_scientific.cpp index b9043d39773..c318eee6ffb 100644 --- a/icu4c/source/i18n/number_scientific.cpp +++ b/icu4c/source/i18n/number_scientific.cpp @@ -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 { diff --git a/icu4c/source/i18n/numrange_fluent.cpp b/icu4c/source/i18n/numrange_fluent.cpp index d4df042b3f0..ec00588502e 100644 --- a/icu4c/source/i18n/numrange_fluent.cpp +++ b/icu4c/source/i18n/numrange_fluent.cpp @@ -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 Derived NumberRangeFormatterSettings::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::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::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::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::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::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::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::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::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::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::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::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& { diff --git a/icu4c/source/i18n/numrange_impl.cpp b/icu4c/source/i18n/numrange_impl.cpp index be6d38c5b6f..d076352bb06 100644 --- a/icu4c/source/i18n/numrange_impl.cpp +++ b/icu4c/source/i18n/numrange_impl.cpp @@ -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); diff --git a/icu4c/source/i18n/unicode/numberformatter.h b/icu4c/source/i18n/unicode/numberformatter.h index 2a9743e6bd5..b742e9104f1 100644 --- a/icu4c/source/i18n/unicode/numberformatter.h +++ b/icu4c/source/i18n/unicode/numberformatter.h @@ -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; }; diff --git a/icu4c/source/test/intltest/numbertest_range.cpp b/icu4c/source/test/intltest/numbertest_range.cpp index 7d5cb0467a5..e028be9a82a 100644 --- a/icu4c/source/test/intltest/numbertest_range.cpp +++ b/icu4c/source/test/intltest/numbertest_range.cpp @@ -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. } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java index 4ebb3d0cc63..9b3a60ba585 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java @@ -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("", prefix, suffix); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java index cdd129c128f..ba12ada88a8 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java @@ -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(); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java index 00a64bbe2ff..0efa2b79a34 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java @@ -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. * diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java index a7ab9b98a89..cf0b1f1b4b5 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java @@ -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); } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java index 13d0ab2de10..80525d663ed 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java @@ -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); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java index 5ac1bcfa408..e23c5e917ef 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java @@ -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. + * + *

+ * Applies the compiled two-argument pattern to the NumberStringBuilder. + * + *

+ * 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 index 00000000000..696f3f082c3 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/PrefixInfixSuffixLengthHelper.java @@ -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; + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java index 800dcf6ab9b..e2872516a30 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java @@ -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; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java index a759fa4ebd7..ad1ab6b06ea 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java @@ -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 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()); } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java index c877fb80089..29e9cb4ab75 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java @@ -152,9 +152,9 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings { + 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 diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java index 06dad539f33..11f590f56c4 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java @@ -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); } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java index 5867621c142..32b389a21ca 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java @@ -47,10 +47,10 @@ public abstract class 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 index 00000000000..b2629903922 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java @@ -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()); + } + } + +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java index 40ae7371d6a..50df9af21bc 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java @@ -23,14 +23,15 @@ public abstract class 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