}
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 {
}
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 {
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;
}
Derived move(std::move(*this));
move.fMacros.formatter1 = formatter;
move.fMacros.singleFormatter = true;
+ touchRangeLocales(move.fMacros);
return move;
}
Derived copy(*this);
copy.fMacros.formatter1 = std::move(formatter);
copy.fMacros.singleFormatter = true;
+ touchRangeLocales(copy.fMacros);
return copy;
}
Derived move(std::move(*this));
move.fMacros.formatter1 = std::move(formatter);
move.fMacros.singleFormatter = true;
+ touchRangeLocales(move.fMacros);
return move;
}
Derived copy(*this);
copy.fMacros.formatter1 = formatter;
copy.fMacros.singleFormatter = false;
+ touchRangeLocales(copy.fMacros);
return copy;
}
Derived move(std::move(*this));
move.fMacros.formatter1 = formatter;
move.fMacros.singleFormatter = false;
+ touchRangeLocales(move.fMacros);
return move;
}
Derived copy(*this);
copy.fMacros.formatter1 = std::move(formatter);
copy.fMacros.singleFormatter = false;
+ touchRangeLocales(copy.fMacros);
return copy;
}
Derived move(std::move(*this));
move.fMacros.formatter1 = std::move(formatter);
move.fMacros.singleFormatter = false;
+ touchRangeLocales(move.fMacros);
return move;
}
Derived copy(*this);
copy.fMacros.formatter2 = formatter;
copy.fMacros.singleFormatter = false;
+ touchRangeLocales(copy.fMacros);
return copy;
}
Derived move(std::move(*this));
move.fMacros.formatter2 = formatter;
move.fMacros.singleFormatter = false;
+ touchRangeLocales(move.fMacros);
return move;
}
Derived copy(*this);
copy.fMacros.formatter2 = std::move(formatter);
copy.fMacros.singleFormatter = false;
+ touchRangeLocales(copy.fMacros);
return copy;
}
Derived move(std::move(*this));
move.fMacros.formatter2 = std::move(formatter);
move.fMacros.singleFormatter = false;
+ touchRangeLocales(move.fMacros);
return move;
}
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& {
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!
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);
}
|| !(*micros1.modMiddle == *micros2.modMiddle)
|| !(*micros1.modOuter == *micros2.modOuter)) {
formatRange(data, micros1, micros2, status);
+ data.identityResult = UNUM_IDENTITY_RESULT_NOT_EQUAL;
return;
}
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);
}
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);
// (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)?
// 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);
}
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);
}
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);
class GeneratorHelpers;
class DecNum;
class NumberRangeFormatterImpl;
+struct RangeMacroProps;
+void touchRangeLocales(impl::RangeMacroProps& macros);
} // namespace impl
friend class UnlocalizedNumberFormatter;
// Give NumberRangeFormatter access to the MacroProps
+ friend void impl::touchRangeLocales(impl::RangeMacroProps& macros);
friend class impl::NumberRangeFormatterImpl;
};
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() {
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.
}
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);
// 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;
/**
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();
}
}
+ @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.
*
// 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
* @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);
}
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;
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);
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
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
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;
+ }
}
--- /dev/null
+// © 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;
+ }
+}
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;
* @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;
}
*/
@Override
public String toString() {
- return nsb.toString();
+ return string.toString();
}
/**
*/
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);
* @see NumberRangeFormatter
*/
public boolean nextFieldPosition(FieldPosition fieldPosition) {
- return nsb.nextFieldPosition(fieldPosition);
+ return string.nextFieldPosition(fieldPosition);
}
/**
* @see NumberRangeFormatter
*/
public AttributedCharacterIterator toCharacterIterator() {
- return nsb.toCharacterIterator();
+ return string.toCharacterIterator();
}
/**
* @see #getSecondBigDecimal
*/
public BigDecimal getFirstBigDecimal() {
- return first.toBigDecimal();
+ return quantity1.toBigDecimal();
}
/**
* @see #getFirstBigDecimal
*/
public BigDecimal getSecondBigDecimal() {
- return second.toBigDecimal();
+ return quantity2.toBigDecimal();
}
/**
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();
}
/**
// 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());
}
}
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);
}
// 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;
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.
*/
public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings<LocalizedNumberRangeFormatter> {
+ private volatile NumberRangeFormatterImpl fImpl;
+
LocalizedNumberRangeFormatter(NumberRangeFormatterSettings<?> parent, int key, Object value) {
super(parent, key, value);
}
}
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
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;
}
/**
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) {
//////////
/**
- * 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;
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);
// 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);
}
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);
}
}
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;
--- /dev/null
+// © 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());
+ }
+ }
+
+}
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;
*/
@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);
}
/**
* @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);
}
/**
* @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);
}
/**
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;
}
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;
}
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
@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
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);
// 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
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;
/**
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() {
@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(