ICU-11276 Adding Java NumberRangeFormatter implementation.

This commit is contained in:
Shane Carr 2018-09-07 05:30:37 -07:00
parent 57f448e93c
commit 55974b2fb6
No known key found for this signature in database
GPG key ID: FCED3B24AAB18B5C
23 changed files with 1334 additions and 195 deletions

View file

@ -1154,8 +1154,22 @@ const char16_t* DecimalQuantity::checkHealth() const {
}
bool DecimalQuantity::operator==(const DecimalQuantity& other) const {
// FIXME: Make a faster implementation.
return toString() == other.toString();
bool basicEquals = scale == other.scale && precision == other.precision && flags == other.flags
&& lOptPos == other.lOptPos && lReqPos == other.lReqPos && rReqPos == other.rReqPos
&& rOptPos == other.rOptPos;
if (!basicEquals) {
return false;
}
if (precision == 0) {
return true;
}
for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) {
if (getDigit(m) != other.getDigit(m)) {
return false;
}
}
return true;
}
UnicodeString DecimalQuantity::toString() const {

View file

@ -82,9 +82,10 @@ int32_t ScientificModifier::getPrefixLength() const {
}
int32_t ScientificModifier::getCodePointCount() const {
// This method is not used for strong modifiers.
U_ASSERT(false);
return 0;
// NOTE: This method is only called one place, NumberRangeFormatterImpl.
// The call site only cares about != 0 and != 1.
// Return a very large value so that if this method is used elsewhere, we should notice.
return 999;
}
bool ScientificModifier::isStrong() const {

View file

@ -18,11 +18,20 @@ using namespace icu::number;
using namespace icu::number::impl;
// This function needs to be declared in this namespace so it can be friended.
// NOTE: In Java, this logic is handled in the resolve() function.
void icu::number::impl::touchRangeLocales(RangeMacroProps& macros) {
macros.formatter1.fMacros.locale = macros.locale;
macros.formatter2.fMacros.locale = macros.locale;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(const UnlocalizedNumberFormatter& formatter) const& {
Derived copy(*this);
copy.fMacros.formatter1 = formatter;
copy.fMacros.singleFormatter = true;
touchRangeLocales(copy.fMacros);
return copy;
}
@ -31,6 +40,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(const Unlocal
Derived move(std::move(*this));
move.fMacros.formatter1 = formatter;
move.fMacros.singleFormatter = true;
touchRangeLocales(move.fMacros);
return move;
}
@ -39,6 +49,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(UnlocalizedNu
Derived copy(*this);
copy.fMacros.formatter1 = std::move(formatter);
copy.fMacros.singleFormatter = true;
touchRangeLocales(copy.fMacros);
return copy;
}
@ -47,6 +58,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(UnlocalizedNu
Derived move(std::move(*this));
move.fMacros.formatter1 = std::move(formatter);
move.fMacros.singleFormatter = true;
touchRangeLocales(move.fMacros);
return move;
}
@ -55,6 +67,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(const Unloca
Derived copy(*this);
copy.fMacros.formatter1 = formatter;
copy.fMacros.singleFormatter = false;
touchRangeLocales(copy.fMacros);
return copy;
}
@ -63,6 +76,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(const Unloca
Derived move(std::move(*this));
move.fMacros.formatter1 = formatter;
move.fMacros.singleFormatter = false;
touchRangeLocales(move.fMacros);
return move;
}
@ -71,6 +85,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(UnlocalizedN
Derived copy(*this);
copy.fMacros.formatter1 = std::move(formatter);
copy.fMacros.singleFormatter = false;
touchRangeLocales(copy.fMacros);
return copy;
}
@ -79,6 +94,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(UnlocalizedN
Derived move(std::move(*this));
move.fMacros.formatter1 = std::move(formatter);
move.fMacros.singleFormatter = false;
touchRangeLocales(move.fMacros);
return move;
}
@ -87,6 +103,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(const Unloc
Derived copy(*this);
copy.fMacros.formatter2 = formatter;
copy.fMacros.singleFormatter = false;
touchRangeLocales(copy.fMacros);
return copy;
}
@ -95,6 +112,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(const Unloc
Derived move(std::move(*this));
move.fMacros.formatter2 = formatter;
move.fMacros.singleFormatter = false;
touchRangeLocales(move.fMacros);
return move;
}
@ -103,6 +121,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(Unlocalized
Derived copy(*this);
copy.fMacros.formatter2 = std::move(formatter);
copy.fMacros.singleFormatter = false;
touchRangeLocales(copy.fMacros);
return copy;
}
@ -111,6 +130,7 @@ Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(Unlocalized
Derived move(std::move(*this));
move.fMacros.formatter2 = std::move(formatter);
move.fMacros.singleFormatter = false;
touchRangeLocales(move.fMacros);
return move;
}
@ -228,11 +248,13 @@ LocalizedNumberRangeFormatter::~LocalizedNumberRangeFormatter() {
LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(const RangeMacroProps& macros, const Locale& locale) {
fMacros = macros;
fMacros.locale = locale;
touchRangeLocales(fMacros);
}
LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(RangeMacroProps&& macros, const Locale& locale) {
fMacros = std::move(macros);
fMacros.locale = locale;
touchRangeLocales(fMacros);
}
LocalizedNumberRangeFormatter UnlocalizedNumberRangeFormatter::locale(const Locale& locale) const& {

View file

@ -65,7 +65,7 @@ void getNumberRangeData(const char* localeName, const char* nsName, NumberRangeD
ures_getAllItemsWithFallback(rb.getAlias(), dataPath.data(), sink, status);
if (U_FAILURE(status)) { return; }
// TODO: Is it necessary to maually fall back to latn, or does the data sink take care of that?
// TODO: Is it necessary to manually fall back to latn, or does the data sink take care of that?
if (data.rangePattern.getArgumentLimit() == 0) {
// No data!
@ -107,11 +107,10 @@ void NumberRangeFormatterImpl::format(UFormattedNumberRangeData& data, bool equa
MicroProps micros1;
MicroProps micros2;
formatterImpl1.preProcess(data.quantity1, micros1, status);
if (fSameFormatters) {
formatterImpl1.preProcess(data.quantity1, micros1, status);
formatterImpl1.preProcess(data.quantity2, micros2, status);
} else {
formatterImpl1.preProcess(data.quantity1, micros1, status);
formatterImpl2.preProcess(data.quantity2, micros2, status);
}
@ -124,6 +123,7 @@ void NumberRangeFormatterImpl::format(UFormattedNumberRangeData& data, bool equa
|| !(*micros1.modMiddle == *micros2.modMiddle)
|| !(*micros1.modOuter == *micros2.modOuter)) {
formatRange(data, micros1, micros2, status);
data.identityResult = UNUM_IDENTITY_RESULT_NOT_EQUAL;
return;
}
@ -182,8 +182,8 @@ void NumberRangeFormatterImpl::formatSingleValue(UFormattedNumberRangeData& data
UErrorCode& status) const {
if (U_FAILURE(status)) { return; }
if (fSameFormatters) {
int32_t length = formatterImpl1.writeNumber(micros1, data.quantity1, data.string, 0, status);
formatterImpl1.writeAffixes(micros1, data.string, 0, length, status);
int32_t length = NumberFormatterImpl::writeNumber(micros1, data.quantity1, data.string, 0, status);
NumberFormatterImpl::writeAffixes(micros1, data.string, 0, length, status);
} else {
formatRange(data, micros1, micros2, status);
}
@ -195,8 +195,8 @@ void NumberRangeFormatterImpl::formatApproximately (UFormattedNumberRangeData& d
UErrorCode& status) const {
if (U_FAILURE(status)) { return; }
if (fSameFormatters) {
int32_t length = formatterImpl1.writeNumber(micros1, data.quantity1, data.string, 0, status);
length += formatterImpl1.writeAffixes(micros1, data.string, 0, length, status);
int32_t length = NumberFormatterImpl::writeNumber(micros1, data.quantity1, data.string, 0, status);
length += NumberFormatterImpl::writeAffixes(micros1, data.string, 0, length, status);
fApproximatelyModifier.apply(data.string, 0, length, status);
} else {
formatRange(data, micros1, micros2, status);
@ -242,9 +242,7 @@ void NumberRangeFormatterImpl::formatRange(UFormattedNumberRangeData& data,
// (could disable collapsing of the middle modifier)
// The modifiers are equal by this point, so we can look at just one of them.
const Modifier* mm = micros1.modMiddle;
if (mm == nullptr) {
// pass
} else if (fCollapse == UNUM_RANGE_COLLAPSE_UNIT) {
if (fCollapse == UNUM_RANGE_COLLAPSE_UNIT) {
// Only collapse if the modifier is a unit.
// TODO: Make a better way to check for a unit?
// TODO: Handle case where the modifier has both notation and unit (compact currency)?
@ -321,6 +319,7 @@ void NumberRangeFormatterImpl::formatRange(UFormattedNumberRangeData& data,
// TODO: Support padding?
if (collapseInner) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
lengthInfix += micros1.modInner->apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status);
} else {
length1 += micros1.modInner->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status);
@ -328,6 +327,7 @@ void NumberRangeFormatterImpl::formatRange(UFormattedNumberRangeData& data,
}
if (collapseMiddle) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
lengthInfix += micros1.modMiddle->apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status);
} else {
length1 += micros1.modMiddle->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status);
@ -335,6 +335,7 @@ void NumberRangeFormatterImpl::formatRange(UFormattedNumberRangeData& data,
}
if (collapseOuter) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
lengthInfix += micros1.modOuter->apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status);
} else {
length1 += micros1.modOuter->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status);

View file

@ -145,6 +145,8 @@ class CurrencySymbols;
class GeneratorHelpers;
class DecNum;
class NumberRangeFormatterImpl;
struct RangeMacroProps;
void touchRangeLocales(impl::RangeMacroProps& macros);
} // namespace impl
@ -2112,6 +2114,7 @@ class U_I18N_API NumberFormatterSettings {
friend class UnlocalizedNumberFormatter;
// Give NumberRangeFormatter access to the MacroProps
friend void impl::touchRangeLocales(impl::RangeMacroProps& macros);
friend class impl::NumberRangeFormatterImpl;
};

View file

@ -107,6 +107,54 @@ void NumberRangeFormatterTest::testBasic() {
u"4,999 m 5,001 km",
u"5,000 m 5,000 km",
u"5,000 m 5,000,000 km");
assertFormatRange(
u"Basic long unit",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().unit(METER).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME)),
Locale("en-us"),
u"1 meter 5 meters", // TODO: This doesn't collapse because the plurals are different. Fix?
u"~5 meters",
u"~5 meters",
u"03 meters", // Note: It collapses when the plurals are the same
u"~0 meters",
u"33,000 meters",
u"3,0005,000 meters",
u"4,9995,001 meters",
u"~5,000 meters",
u"5,0005,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"33 000 degrés Fahrenheit",
u"3 0005 000 degrés Fahrenheit",
u"4 9995 001 degrés Fahrenheit",
u"~5 000 degrés Fahrenheit",
u"5 0005 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"15E0",
u"~5E0",
u"~5E0",
u"03E0",
u"~0E0",
u"3E0 3E3",
u"35E3",
u"4.9995.001E3",
u"~5E3",
u"5E3 5E6");
// TODO: Test compact currency?
// The code is not smart enough to differentiate the notation from the unit.
}

View file

@ -74,6 +74,23 @@ public class ConstantAffixModifier implements Modifier {
return strong;
}
@Override
public boolean containsField(Field field) {
// This method is not currently used.
assert false;
return false;
}
@Override
public boolean equalsModifier(Modifier other) {
if (!(other instanceof ConstantAffixModifier)) {
return false;
}
ConstantAffixModifier _other = (ConstantAffixModifier) other;
return prefix.equals(_other.prefix) && suffix.equals(_other.suffix) && field == _other.field
&& strong == _other.strong;
}
@Override
public String toString() {
return String.format("<ConstantAffixModifier prefix:'%s' suffix:'%s'>", prefix, suffix);

View file

@ -2,6 +2,8 @@
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.util.Arrays;
import com.ibm.icu.text.NumberFormat.Field;
/**
@ -59,6 +61,32 @@ public class ConstantMultiFieldModifier implements Modifier {
return strong;
}
@Override
public boolean containsField(Field field) {
for (int i = 0; i < prefixFields.length; i++) {
if (prefixFields[i] == field) {
return true;
}
}
for (int i = 0; i < suffixFields.length; i++) {
if (suffixFields[i] == field) {
return true;
}
}
return false;
}
@Override
public boolean equalsModifier(Modifier other) {
if (!(other instanceof ConstantMultiFieldModifier)) {
return false;
}
ConstantMultiFieldModifier _other = (ConstantMultiFieldModifier) other;
return Arrays.equals(prefixChars, _other.prefixChars) && Arrays.equals(prefixFields, _other.prefixFields)
&& Arrays.equals(suffixChars, _other.suffixChars) && Arrays.equals(suffixFields, _other.suffixFields)
&& overwrite == _other.overwrite && strong == _other.strong;
}
@Override
public String toString() {
NumberStringBuilder temp = new NumberStringBuilder();

View file

@ -1014,6 +1014,37 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
}
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null) {
return false;
}
if (!(other instanceof DecimalQuantity_AbstractBCD)) {
return false;
}
DecimalQuantity_AbstractBCD _other = (DecimalQuantity_AbstractBCD) other;
boolean basicEquals = scale == _other.scale && precision == _other.precision && flags == _other.flags
&& lOptPos == _other.lOptPos && lReqPos == _other.lReqPos && rReqPos == _other.rReqPos
&& rOptPos == _other.rOptPos;
if (!basicEquals) {
return false;
}
if (precision == 0) {
return true;
}
for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) {
if (getDigit(m) != _other.getDigit(m)) {
return false;
}
}
return true;
}
/**
* Returns a single digit from the BCD list. No internal state is changed by calling this method.
*

View file

@ -2,6 +2,8 @@
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import com.ibm.icu.text.NumberFormat.Field;
/**
* A Modifier is an object that can be passed through the formatting pipeline until it is finally applied
* to the string builder. A Modifier usually contains a prefix and a suffix that are applied, but it
@ -48,4 +50,14 @@ public interface Modifier {
* @return Whether the modifier is strong.
*/
public boolean isStrong();
/**
* Whether the modifier contains at least one occurrence of the given field.
*/
public boolean containsField(Field currency);
/**
* Returns whether the affixes owned by this modifier are equal to the ones owned by the given modifier.
*/
public boolean equalsModifier(Modifier other);
}

View file

@ -7,6 +7,7 @@ import com.ibm.icu.impl.number.AffixUtils.SymbolProvider;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat.Field;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
@ -319,6 +320,20 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
return isStrong;
}
@Override
public boolean containsField(Field field) {
// This method is not currently used. (unsafe path not used in range formatting)
assert false;
return false;
}
@Override
public boolean equalsModifier(Modifier other) {
// This method is not currently used. (unsafe path not used in range formatting)
assert false;
return false;
}
private int insertPrefix(NumberStringBuilder sb, int position) {
prepareAffix(true);
int length = AffixUtils.unescape(currentAffix, sb, position, this);

View file

@ -3,7 +3,9 @@
package com.ibm.icu.impl.number;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.number.range.PrefixInfixSuffixLengthHelper;
import com.ibm.icu.text.NumberFormat.Field;
import com.ibm.icu.util.ICUException;
/**
* The second primary implementation of {@link Modifier}, this one consuming a
@ -80,6 +82,22 @@ public class SimpleModifier implements Modifier {
return strong;
}
@Override
public boolean containsField(Field field) {
// This method is not currently used.
assert false;
return false;
}
@Override
public boolean equalsModifier(Modifier other) {
if (!(other instanceof SimpleModifier)) {
return false;
}
SimpleModifier _other = (SimpleModifier) other;
return compiledPattern.equals(_other.compiledPattern) && field == _other.field && strong == _other.strong;
}
/**
* TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is because
* DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it should not
@ -123,4 +141,66 @@ public class SimpleModifier implements Modifier {
return prefixLength + suffixLength;
}
}
/**
* TODO: Like above, this belongs with the rest of the SimpleFormatterImpl code.
* I put it here so that the SimpleFormatter uses in NumberStringBuilder are near each other.
*
* <p>
* Applies the compiled two-argument pattern to the NumberStringBuilder.
*
* <p>
* This method is optimized for the case where the prefix and suffix are often empty, such as
* in the range pattern like "{0}-{1}".
*/
public static void formatTwoArgPattern(String compiledPattern, NumberStringBuilder result, int index, PrefixInfixSuffixLengthHelper h,
Field field) {
int argLimit = SimpleFormatterImpl.getArgumentLimit(compiledPattern);
if (argLimit != 2) {
throw new ICUException();
}
int offset = 1; // offset into compiledPattern
int length = 0; // chars added to result
int prefixLength = compiledPattern.charAt(offset);
offset++;
if (prefixLength < ARG_NUM_LIMIT) {
// No prefix
prefixLength = 0;
} else {
prefixLength -= ARG_NUM_LIMIT;
result.insert(index + length, compiledPattern, offset, offset + prefixLength, field);
offset += prefixLength;
length += prefixLength;
offset++;
}
int infixLength = compiledPattern.charAt(offset);
offset++;
if (infixLength < ARG_NUM_LIMIT) {
// No infix
infixLength = 0;
} else {
infixLength -= ARG_NUM_LIMIT;
result.insert(index + length, compiledPattern, offset, offset + infixLength, field);
offset += infixLength;
length += infixLength;
offset++;
}
int suffixLength;
if (offset == compiledPattern.length()) {
// No suffix
suffixLength = 0;
} else {
suffixLength = compiledPattern.charAt(offset) - ARG_NUM_LIMIT;
offset++;
result.insert(index + length, compiledPattern, offset, offset + suffixLength, field);
length += suffixLength;
}
h.lengthPrefix = prefixLength;
h.lengthInfix = infixLength;
h.lengthSuffix = suffixLength;
}
}

View file

@ -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;
}
}

View file

@ -16,6 +16,7 @@ import com.ibm.icu.util.ULocale;
public class RangeMacroProps {
public UnlocalizedNumberFormatter formatter1;
public UnlocalizedNumberFormatter formatter2;
public int sameFormatters = -1; // -1 for unset, 0 for false, 1 for true
public RangeCollapse collapse;
public RangeIdentityFallback identityFallback;
public ULocale loc;

View file

@ -23,16 +23,16 @@ import com.ibm.icu.util.ICUUncheckedIOException;
* @see NumberRangeFormatter
*/
public class FormattedNumberRange {
final NumberStringBuilder nsb;
final DecimalQuantity first;
final DecimalQuantity second;
final NumberStringBuilder string;
final DecimalQuantity quantity1;
final DecimalQuantity quantity2;
final RangeIdentityResult identityResult;
FormattedNumberRange(NumberStringBuilder nsb, DecimalQuantity first, DecimalQuantity second,
FormattedNumberRange(NumberStringBuilder string, DecimalQuantity quantity1, DecimalQuantity quantity2,
RangeIdentityResult identityResult) {
this.nsb = nsb;
this.first = first;
this.second = second;
this.string = string;
this.quantity1 = quantity1;
this.quantity2 = quantity2;
this.identityResult = identityResult;
}
@ -46,7 +46,7 @@ public class FormattedNumberRange {
*/
@Override
public String toString() {
return nsb.toString();
return string.toString();
}
/**
@ -67,7 +67,7 @@ public class FormattedNumberRange {
*/
public <A extends Appendable> A appendTo(A appendable) {
try {
appendable.append(nsb);
appendable.append(string);
} catch (IOException e) {
// Throw as an unchecked exception to avoid users needing try/catch
throw new ICUUncheckedIOException(e);
@ -105,7 +105,7 @@ public class FormattedNumberRange {
* @see NumberRangeFormatter
*/
public boolean nextFieldPosition(FieldPosition fieldPosition) {
return nsb.nextFieldPosition(fieldPosition);
return string.nextFieldPosition(fieldPosition);
}
/**
@ -124,7 +124,7 @@ public class FormattedNumberRange {
* @see NumberRangeFormatter
*/
public AttributedCharacterIterator toCharacterIterator() {
return nsb.toCharacterIterator();
return string.toCharacterIterator();
}
/**
@ -138,7 +138,7 @@ public class FormattedNumberRange {
* @see #getSecondBigDecimal
*/
public BigDecimal getFirstBigDecimal() {
return first.toBigDecimal();
return quantity1.toBigDecimal();
}
/**
@ -152,7 +152,7 @@ public class FormattedNumberRange {
* @see #getFirstBigDecimal
*/
public BigDecimal getSecondBigDecimal() {
return second.toBigDecimal();
return quantity2.toBigDecimal();
}
/**
@ -180,8 +180,8 @@ public class FormattedNumberRange {
public int hashCode() {
// NumberStringBuilder and BigDecimal are mutable, so we can't call
// #equals() or #hashCode() on them directly.
return Arrays.hashCode(nsb.toCharArray()) ^ Arrays.hashCode(nsb.toFieldArray())
^ first.toBigDecimal().hashCode() ^ second.toBigDecimal().hashCode();
return Arrays.hashCode(string.toCharArray()) ^ Arrays.hashCode(string.toFieldArray())
^ quantity1.toBigDecimal().hashCode() ^ quantity2.toBigDecimal().hashCode();
}
/**
@ -201,9 +201,9 @@ public class FormattedNumberRange {
// NumberStringBuilder and BigDecimal are mutable, so we can't call
// #equals() or #hashCode() on them directly.
FormattedNumberRange _other = (FormattedNumberRange) other;
return Arrays.equals(nsb.toCharArray(), _other.nsb.toCharArray())
&& Arrays.equals(nsb.toFieldArray(), _other.nsb.toFieldArray())
&& first.toBigDecimal().equals(_other.first.toBigDecimal())
&& second.toBigDecimal().equals(_other.second.toBigDecimal());
return Arrays.equals(string.toCharArray(), _other.string.toCharArray())
&& Arrays.equals(string.toFieldArray(), _other.string.toFieldArray())
&& quantity1.toBigDecimal().equals(_other.quantity1.toBigDecimal())
&& quantity2.toBigDecimal().equals(_other.quantity2.toBigDecimal());
}
}

View file

@ -152,9 +152,9 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
public FormattedNumber format(DecimalQuantity fq) {
NumberStringBuilder string = new NumberStringBuilder();
if (computeCompiled()) {
compiled.apply(fq, string);
compiled.format(fq, string);
} else {
NumberFormatterImpl.applyStatic(resolve(), fq, string);
NumberFormatterImpl.formatStatic(resolve(), fq, string);
}
return new FormattedNumber(string, fq);
}
@ -190,7 +190,7 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
// Further benchmarking is required.
long currentCount = callCount.incrementAndGet(this);
if (currentCount == macros.threshold.longValue()) {
compiled = NumberFormatterImpl.fromMacros(macros);
compiled = new NumberFormatterImpl(macros);
return true;
} else if (compiled != null) {
return true;

View file

@ -4,9 +4,6 @@ package com.ibm.icu.number;
import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.range.RangeMacroProps;
import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult;
/**
* A NumberRangeFormatter that has a locale associated with it; this means .formatRange() methods are available.
@ -18,6 +15,8 @@ import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult;
*/
public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings<LocalizedNumberRangeFormatter> {
private volatile NumberRangeFormatterImpl fImpl;
LocalizedNumberRangeFormatter(NumberRangeFormatterSettings<?> parent, int key, Object value) {
super(parent, key, value);
}
@ -85,28 +84,10 @@ public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings<
}
FormattedNumberRange formatImpl(DecimalQuantity first, DecimalQuantity second, boolean equalBeforeRounding) {
// TODO: This is a placeholder implementation.
RangeMacroProps macros = resolve();
LocalizedNumberFormatter f1 , f2;
if (macros.formatter1 != null) {
f1 = macros.formatter1.locale(macros.loc);
} else {
f1 = NumberFormatter.withLocale(macros.loc);
if (fImpl == null) {
fImpl = new NumberRangeFormatterImpl(resolve());
}
if (macros.formatter2 != null) {
f2 = macros.formatter2.locale(macros.loc);
} else {
f2 = NumberFormatter.withLocale(macros.loc);
}
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

View file

@ -42,21 +42,21 @@ import com.ibm.icu.util.MeasureUnit;
class NumberFormatterImpl {
/** Builds a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly. */
public static NumberFormatterImpl fromMacros(MacroProps macros) {
MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, true);
return new NumberFormatterImpl(microPropsGenerator);
public NumberFormatterImpl(MacroProps macros) {
this(macrosToMicroGenerator(macros, true));
}
/**
* Builds and evaluates an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once.
*/
public static void applyStatic(
public static int formatStatic(
MacroProps macros,
DecimalQuantity inValue,
NumberStringBuilder outString) {
MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false);
MicroProps micros = microPropsGenerator.processQuantity(inValue);
microsToString(micros, inValue, outString);
MicroProps micros = preProcessUnsafe(macros, inValue);
int length = writeNumber(micros, inValue, outString, 0);
length += writeAffixes(micros, outString, 0, length);
return length;
}
/**
@ -82,9 +82,40 @@ class NumberFormatterImpl {
this.microPropsGenerator = microPropsGenerator;
}
public void apply(DecimalQuantity inValue, NumberStringBuilder outString) {
/**
* Evaluates the "safe" MicroPropsGenerator created by "fromMacros".
*/
public int format(DecimalQuantity inValue, NumberStringBuilder outString) {
MicroProps micros = preProcess(inValue);
int length = writeNumber(micros, inValue, outString, 0);
length += writeAffixes(micros, outString, 0, length);
return length;
}
/**
* Like format(), but saves the result into an output MicroProps without additional processing.
*/
public MicroProps preProcess(DecimalQuantity inValue) {
MicroProps micros = microPropsGenerator.processQuantity(inValue);
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;
}
private static MicroProps preProcessUnsafe(MacroProps macros, DecimalQuantity inValue) {
MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false);
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;
}
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);
}
}

View file

@ -47,10 +47,10 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
static final int KEY_PER_UNIT = 15;
static final int KEY_MAX = 16;
final NumberFormatterSettings<?> parent;
final int key;
final Object value;
volatile MacroProps resolvedMacros;
private final NumberFormatterSettings<?> parent;
private final int key;
private final Object value;
private volatile MacroProps resolvedMacros;
NumberFormatterSettings(NumberFormatterSettings<?> parent, int key, Object value) {
this.parent = parent;

View file

@ -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());
}
}
}

View file

@ -23,14 +23,15 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
static final int KEY_LOCALE = 1;
static final int KEY_FORMATTER_1 = 2;
static final int KEY_FORMATTER_2 = 3;
static final int KEY_COLLAPSE = 4;
static final int KEY_IDENTITY_FALLBACK = 5;
static final int KEY_MAX = 6;
static final int KEY_SAME_FORMATTERS = 4;
static final int KEY_COLLAPSE = 5;
static final int KEY_IDENTITY_FALLBACK = 6;
static final int KEY_MAX = 7;
final NumberRangeFormatterSettings<?> parent;
final int key;
final Object value;
volatile RangeMacroProps resolvedMacros;
private final NumberRangeFormatterSettings<?> parent;
private final int key;
private final Object value;
private volatile RangeMacroProps resolvedMacros;
NumberRangeFormatterSettings(NumberRangeFormatterSettings<?> parent, int key, Object value) {
this.parent = parent;
@ -55,7 +56,7 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
*/
@SuppressWarnings("unchecked")
public T numberFormatterBoth(UnlocalizedNumberFormatter formatter) {
return (T) numberFormatterFirst(formatter).numberFormatterSecond(formatter);
return (T) create(KEY_SAME_FORMATTERS, true).create(KEY_FORMATTER_1, formatter);
}
/**
@ -72,8 +73,9 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
* @see NumberFormatter
* @see NumberRangeFormatter
*/
@SuppressWarnings("unchecked")
public T numberFormatterFirst(UnlocalizedNumberFormatter formatterFirst) {
return create(KEY_FORMATTER_1, formatterFirst);
return (T) create(KEY_SAME_FORMATTERS, false).create(KEY_FORMATTER_1, formatterFirst);
}
/**
@ -90,8 +92,9 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
* @see NumberFormatter
* @see NumberRangeFormatter
*/
@SuppressWarnings("unchecked")
public T numberFormatterSecond(UnlocalizedNumberFormatter formatterSecond) {
return create(KEY_FORMATTER_2, formatterSecond);
return (T) create(KEY_SAME_FORMATTERS, false).create(KEY_FORMATTER_2, formatterSecond);
}
/**
@ -173,6 +176,11 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
macros.formatter2 = (UnlocalizedNumberFormatter) current.value;
}
break;
case KEY_SAME_FORMATTERS:
if (macros.sameFormatters == -1) {
macros.sameFormatters = (boolean) current.value ? 1 : 0;
}
break;
case KEY_COLLAPSE:
if (macros.collapse == null) {
macros.collapse = (RangeCollapse) current.value;
@ -188,6 +196,13 @@ public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatte
}
current = current.parent;
}
// Copy the locale into the children (see touchRangeLocales in C++)
if (macros.formatter1 != null) {
macros.formatter1.resolve().loc = macros.loc;
}
if (macros.formatter2 != null) {
macros.formatter2.resolve().loc = macros.loc;
}
resolvedMacros = macros;
return macros;
}

View file

@ -13,6 +13,7 @@ import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.Precision.SignificantRounderImpl;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberFormat.Field;
/**
* A class that defines the scientific notation style to be used when formatting numbers in
@ -221,8 +222,10 @@ public class ScientificNotation extends Notation implements Cloneable {
@Override
public int getCodePointCount() {
// This method is not used for strong modifiers.
throw new AssertionError();
// NOTE: This method is only called one place, NumberRangeFormatterImpl.
// The call site only cares about != 0 and != 1.
// Return a very large value so that if this method is used elsewhere, we should notice.
return 999;
}
@Override
@ -231,6 +234,20 @@ public class ScientificNotation extends Notation implements Cloneable {
return true;
}
@Override
public boolean containsField(Field field) {
// This method is not currently used. (unsafe path not used in range formatting)
assert false;
return false;
}
@Override
public boolean equalsModifier(Modifier other) {
// This method is not currently used. (unsafe path not used in range formatting)
assert false;
return false;
}
@Override
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
return doApply(exponent, output, rightIndex);
@ -288,5 +305,22 @@ public class ScientificNotation extends Notation implements Cloneable {
// Scientific is always strong
return true;
}
@Override
public boolean containsField(Field field) {
// This method is not used for inner modifiers.
assert false;
return false;
}
@Override
public boolean equalsModifier(Modifier other) {
if (!(other instanceof ScientificHandler)) {
return false;
}
ScientificHandler _other = (ScientificHandler) other;
// TODO: Check for locale symbols and settings as well? Could be less efficient.
return exponent == _other.exponent;
}
}
}

View file

@ -9,11 +9,16 @@ import java.util.Locale;
import org.junit.Test;
import com.ibm.icu.number.LocalizedNumberRangeFormatter;
import com.ibm.icu.number.Notation;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.number.NumberRangeFormatter;
import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse;
import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback;
import com.ibm.icu.number.Precision;
import com.ibm.icu.number.UnlocalizedNumberRangeFormatter;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
/**
@ -24,6 +29,7 @@ public class NumberRangeFormatterTest {
private static final Currency USD = Currency.getInstance("USD");
private static final Currency GBP = Currency.getInstance("GBP");
private static final Currency PTE = Currency.getInstance("PTE");
@Test
public void testSanity() {
@ -42,86 +48,507 @@ public class NumberRangeFormatterTest {
@Test
public void testBasic() {
assertFormatRange(
"Basic",
NumberRangeFormatter.with(),
ULocale.US,
"1 --- 5",
"5 --- 5",
"5 --- 5",
"0 --- 3",
"0 --- 0",
"3 --- 3,000",
"3,000 --- 5,000",
"4,999 --- 5,001",
"5,000 --- 5,000",
"5,000 --- 5,000,000");
"Basic",
NumberRangeFormatter.with(),
new ULocale("en-us"),
"15",
"~5",
"~5",
"03",
"~0",
"33,000",
"3,0005,000",
"4,9995,001",
"~5,000",
"5,0005,000,000");
assertFormatRange(
"Basic with units",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)),
new ULocale("en-us"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33,000 m",
"3,0005,000 m",
"4,9995,001 m",
"~5,000 m",
"5,0005,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",
"03 meters", // Note: It collapses when the plurals are the same
"~0 meters",
"33,000 meters",
"3,0005,000 meters",
"4,9995,001 meters",
"~5,000 meters",
"5,0005,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",
"33 000 degrés Fahrenheit",
"3 0005 000 degrés Fahrenheit",
"4 9995 001 degrés Fahrenheit",
"~5 000 degrés Fahrenheit",
"5 0005 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() {
public void testCollapse() {
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");
"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(
"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");
"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(
"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");
"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(
"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");
"Unit collapse on currency",
NumberRangeFormatter.with()
.collapse(RangeCollapse.UNIT)
.numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())),
new ULocale("en-us"),
"$15",
"~$5",
"~$5",
"$03",
"~$0",
"$33,000",
"$3,0005,000",
"$4,9995,001",
"~$5,000",
"$5,0005,000,000");
assertFormatRange(
"All collapse on currency",
NumberRangeFormatter.with()
.collapse(RangeCollapse.ALL)
.numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())),
new ULocale("en-us"),
"$15",
"~$5",
"~$5",
"$03",
"~$0",
"$33,000",
"$3,0005,000",
"$4,9995,001",
"~$5,000",
"$5,0005,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 15",
"~GBP 5", // TODO: Fix this at some point
"~GBP 5",
"GBP 03",
"~GBP 0",
"GBP 33,000",
"GBP 3,0005,000",
"GBP 4,9995,001",
"~GBP 5,000",
"GBP 5,0005,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 15",
"~GBP 5", // TODO: Fix this at some point
"~GBP 5",
"GBP 03",
"~GBP 0",
"GBP 33,000",
"GBP 3,0005,000",
"GBP 4,9995,001",
"~GBP 5,000",
"GBP 5,0005,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 15",
"~GBP 5", // TODO: Fix this at some point
"~GBP 5",
"GBP 03",
"~GBP 0",
"GBP 33,000",
"GBP 3,0005,000",
"GBP 4,9995,001",
"~GBP 5,000",
"GBP 5,0005,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"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33,000 m",
"3,0005,000 m",
"4,9995,001 m",
"~5,000 m",
"5,0005,000,000 m");
assertFormatRange(
"All collapse on measurement unit",
NumberRangeFormatter.with()
.collapse(RangeCollapse.ALL)
.numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)),
new ULocale("en-us"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33,000 m",
"3,0005,000 m",
"4,9995,001 m",
"~5,000 m",
"5,0005,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"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33K 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"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33K 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"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33K m",
"35K 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"),
"15E0",
"~5E0",
"~5E0",
"03E0",
"~0E0",
"3E0 3E3",
"35E3",
"4.9995.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"),
"15",
"55",
"55",
"03",
"00",
"33,000",
"3,0005,000",
"4,9995,001",
"5,0005,000",
"5,0005,000,000");
assertFormatRange(
"Identity fallback Approximately or Single Value",
NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE),
new ULocale("en-us"),
"15",
"~5",
"5",
"03",
"0",
"33,000",
"3,0005,000",
"4,9995,001",
"5,000",
"5,0005,000,000");
assertFormatRange(
"Identity fallback Single Value",
NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.SINGLE_VALUE),
new ULocale("en-us"),
"15",
"5",
"5",
"03",
"0",
"33,000",
"3,0005,000",
"4,9995,001",
"5,000",
"5,0005,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"),
"15",
"~5",
"5",
"03",
"0",
"33K",
"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"),
"15.0",
"55.0",
"55.0",
"03.0",
"00.0",
"33,000",
"3,0005,000",
"4,9995,000",
"5,0005,000", // TODO: Should this one be ~5,000?
"5,0005,000,000");
}
static void assertFormatRange(