com.ibm.icu.number.NumberRangeFormatterImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of icu4j Show documentation
Show all versions of icu4j Show documentation
International Component for Unicode for Java (ICU4J) is a mature, widely used Java library
providing Unicode and Globalization support
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.number;
import java.util.MissingResourceException;
import com.ibm.icu.impl.FormattedStringBuilder;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.MicroProps;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.SimpleModifier;
import com.ibm.icu.impl.number.range.PrefixInfixSuffixLengthHelper;
import com.ibm.icu.impl.number.range.RangeMacroProps;
import com.ibm.icu.impl.number.range.StandardPluralRanges;
import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse;
import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback;
import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
/**
* Business logic behind NumberRangeFormatter.
*/
class NumberRangeFormatterImpl {
final NumberFormatterImpl formatterImpl1;
final NumberFormatterImpl formatterImpl2;
final boolean fSameFormatters;
final NumberRangeFormatter.RangeCollapse fCollapse;
final NumberRangeFormatter.RangeIdentityFallback fIdentityFallback;
// Should be final, but they are set in a helper function, not the constructor proper.
// TODO: Clean up to make these fields actually final.
/* final */ String fRangePattern;
/* final */ SimpleModifier fApproximatelyModifier;
final StandardPluralRanges fPluralRanges;
////////////////////
// Helper function for 2-dimensional switch statement
int identity2d(RangeIdentityFallback a, RangeIdentityResult b) {
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") && !hasRangeData()) {
String pattern = value.getString();
rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2);
}
if (key.contentEquals("approximately") && !hasApproxData()) {
String pattern = value.getString();
approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 1, 1); // 1 arg, as in "~{0}"
}
}
}
private boolean hasRangeData() {
return rangePattern != null;
}
private boolean hasApproxData() {
return approximatelyPattern != null;
}
public boolean isComplete() {
return hasRangeData() && hasApproxData();
}
public void fillInDefaults() {
if (!hasRangeData()) {
rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments("{0}–{1}", sb, 2, 2);
}
if (!hasApproxData()) {
approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments("~{0}", sb, 1, 1);
}
}
}
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();
try {
resource.getAllItemsWithFallback(key, sink);
} catch (MissingResourceException e) {
// ignore; fall back to latn
}
// Fall back to latn if necessary
if (!sink.isComplete()) {
resource.getAllItemsWithFallback("NumberElements/latn/miscPatterns", sink);
}
sink.fillInDefaults();
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;
String nsName = formatterImpl1.getRawMicroProps().nsName;
if (nsName == null || !nsName.equals(formatterImpl2.getRawMicroProps().nsName)) {
throw new IllegalArgumentException("Both formatters must have same numbering system");
}
getNumberRangeData(macros.loc, nsName, this);
// TODO: Get locale from PluralRules instead?
fPluralRanges = new StandardPluralRanges(macros.loc);
}
public FormattedNumberRange format(DecimalQuantity quantity1, DecimalQuantity quantity2, boolean equalBeforeRounding) {
FormattedStringBuilder string = new FormattedStringBuilder();
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.semanticallyEquivalent(micros2.modInner)
|| !micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle)
|| !micros1.modOuter.semanticallyEquivalent(micros2.modOuter)) {
formatRange(quantity1, quantity2, string, micros1, micros2);
return new FormattedNumberRange(string, quantity1, quantity2, RangeIdentityResult.NOT_EQUAL);
}
// 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, FormattedStringBuilder 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, FormattedStringBuilder string,
MicroProps micros1, MicroProps micros2) {
if (fSameFormatters) {
int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0);
// HEURISTIC: Desired modifier order: inner, middle, approximately, outer.
length += micros1.modInner.apply(string, 0, length);
length += micros1.modMiddle.apply(string, 0, length);
length += fApproximatelyModifier.apply(string, 0, length);
micros1.modOuter.apply(string, 0, length);
} else {
formatRange(quantity1, quantity2, string, micros1, micros2);
}
}
private void formatRange(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder 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.semanticallyEquivalent(micros2.modOuter);
if (!collapseOuter) {
// Never collapse inner mods if outer mods are not collapsable
collapseMiddle = false;
collapseInner = false;
break;
}
// MIDDLE MODIFIER
collapseMiddle = micros1.modMiddle.semanticallyEquivalent(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.semanticallyEquivalent(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);
assert h.lengthInfix > 0;
// SPACING HEURISTIC
// Add spacing unless all modifiers are collapsed.
// TODO: add API to control this?
// TODO: Use a data-driven heuristic like currency spacing?
// TODO: Use Unicode [:whitespace:] instead of PatternProps whitespace? (consider speed implications)
{
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 if there is not already spacing
if (!PatternProps.isWhiteSpace(string.charAt(h.index1()))) {
h.lengthInfix += string.insertCodePoint(h.index1(), '\u0020', null);
}
if (!PatternProps.isWhiteSpace(string.charAt(h.index2() - 1))) {
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
Modifier mod = resolveModifierPlurals(micros1.modInner, micros2.modInner);
h.lengthInfix += mod.apply(string, h.index0(), h.index3());
} else {
h.length1 += micros1.modInner.apply(string, h.index0(), h.index1());
h.length2 += micros2.modInner.apply(string, h.index2(), h.index3());
}
if (collapseMiddle) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
Modifier mod = resolveModifierPlurals(micros1.modMiddle, micros2.modMiddle);
h.lengthInfix += mod.apply(string, h.index0(), h.index3());
} else {
h.length1 += micros1.modMiddle.apply(string, h.index0(), h.index1());
h.length2 += micros2.modMiddle.apply(string, h.index2(), h.index3());
}
if (collapseOuter) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
Modifier mod = resolveModifierPlurals(micros1.modOuter, micros2.modOuter);
h.lengthInfix += mod.apply(string, h.index0(), h.index3());
} else {
h.length1 += micros1.modOuter.apply(string, h.index0(), h.index1());
h.length2 += micros2.modOuter.apply(string, h.index2(), h.index3());
}
}
Modifier resolveModifierPlurals(Modifier first, Modifier second) {
Modifier.Parameters firstParameters = first.getParameters();
if (firstParameters == null) {
// No plural form; return a fallback (e.g., the first)
return first;
}
Modifier.Parameters secondParameters = second.getParameters();
if (secondParameters == null) {
// No plural form; return a fallback (e.g., the first)
return first;
}
// Get the required plural form from data
StandardPlural resultPlural = fPluralRanges.resolve(firstParameters.plural, secondParameters.plural);
// Get and return the new Modifier
assert firstParameters.obj == secondParameters.obj;
assert firstParameters.signum == secondParameters.signum;
Modifier mod = firstParameters.obj.getModifier(firstParameters.signum, resultPlural);
assert mod != null;
return mod;
}
}