blob: 503344a3b40add84777e16d7c6b65f85af2f6504 [file] [log] [blame]
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
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 = StandardPluralRanges.forLocale(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;
}
}