blob: 3a5fcdcda0c6c6828df4651acf6a8e472c8cf1b3 [file] [log] [blame]
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.number;
import java.math.BigDecimal;
import java.math.MathContext;
import com.ibm.icu.impl.number.AffixPatternProvider;
import com.ibm.icu.impl.number.CustomSymbolCurrency;
import com.ibm.icu.impl.number.DecimalFormatProperties;
import com.ibm.icu.impl.number.Grouper;
import com.ibm.icu.impl.number.MacroProps;
import com.ibm.icu.impl.number.Padder;
import com.ibm.icu.impl.number.PatternStringParser;
import com.ibm.icu.impl.number.PatternStringUtils;
import com.ibm.icu.impl.number.PropertiesAffixPatternProvider;
import com.ibm.icu.impl.number.RoundingUtils;
import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.Precision.FractionRounderImpl;
import com.ibm.icu.number.Precision.IncrementRounderImpl;
import com.ibm.icu.number.Precision.SignificantRounderImpl;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.ULocale;
/**
* <p>
* This class, as well as NumberFormatterImpl, could go into the impl package, but they depend on too
* many package-private members of the public APIs.
*/
final class NumberPropertyMapper {
/** Convenience method to create a NumberFormatter directly from Properties. */
public static UnlocalizedNumberFormatter create(
DecimalFormatProperties properties,
DecimalFormatSymbols symbols) {
MacroProps macros = oldToNew(properties, symbols, null);
return NumberFormatter.with().macros(macros);
}
/** Convenience method to create a NumberFormatter directly from Properties. */
public static UnlocalizedNumberFormatter create(
DecimalFormatProperties properties,
DecimalFormatSymbols symbols,
DecimalFormatProperties exportedProperties) {
MacroProps macros = oldToNew(properties, symbols, exportedProperties);
return NumberFormatter.with().macros(macros);
}
/**
* Convenience method to create a NumberFormatter directly from a pattern string. Something like this
* could become public API if there is demand.
*
* NOTE: This appears to be dead code.
*/
public static UnlocalizedNumberFormatter create(String pattern, DecimalFormatSymbols symbols) {
DecimalFormatProperties properties = PatternStringParser.parseToProperties(pattern);
return create(properties, symbols);
}
/**
* Creates a new {@link MacroProps} object based on the content of a {@link DecimalFormatProperties}
* object. In other words, maps Properties to MacroProps. This function is used by the
* JDK-compatibility API to call into the ICU 60 fluent number formatting pipeline.
*
* @param properties
* The property bag to be mapped.
* @param symbols
* The symbols associated with the property bag.
* @param exportedProperties
* A property bag in which to store validated properties. Used by some DecimalFormat
* getters.
* @return A new MacroProps containing all of the information in the Properties.
*/
public static MacroProps oldToNew(
DecimalFormatProperties properties,
DecimalFormatSymbols symbols,
DecimalFormatProperties exportedProperties) {
MacroProps macros = new MacroProps();
ULocale locale = symbols.getULocale();
/////////////
// SYMBOLS //
/////////////
macros.symbols = symbols;
//////////////////
// PLURAL RULES //
//////////////////
PluralRules rules = properties.getPluralRules();
if (rules == null && properties.getCurrencyPluralInfo() != null) {
rules = properties.getCurrencyPluralInfo().getPluralRules();
}
macros.rules = rules;
/////////////
// AFFIXES //
/////////////
AffixPatternProvider affixProvider = PropertiesAffixPatternProvider.forProperties(properties);
macros.affixProvider = affixProvider;
///////////
// UNITS //
///////////
boolean useCurrency = ((properties.getCurrency() != null)
|| properties.getCurrencyPluralInfo() != null
|| properties.getCurrencyUsage() != null
|| affixProvider.hasCurrencySign());
Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols);
CurrencyUsage currencyUsage = properties.getCurrencyUsage();
boolean explicitCurrencyUsage = currencyUsage != null;
if (!explicitCurrencyUsage) {
currencyUsage = CurrencyUsage.STANDARD;
}
if (useCurrency) {
macros.unit = currency;
}
///////////////////////
// ROUNDING STRATEGY //
///////////////////////
int maxInt = properties.getMaximumIntegerDigits();
int minInt = properties.getMinimumIntegerDigits();
int maxFrac = properties.getMaximumFractionDigits();
int minFrac = properties.getMinimumFractionDigits();
int minSig = properties.getMinimumSignificantDigits();
int maxSig = properties.getMaximumSignificantDigits();
BigDecimal roundingIncrement = properties.getRoundingIncrement();
MathContext mathContext = RoundingUtils.getMathContextOrUnlimited(properties);
boolean explicitMinMaxFrac = minFrac != -1 || maxFrac != -1;
boolean explicitMinMaxSig = minSig != -1 || maxSig != -1;
// Resolve min/max frac for currencies, required for the validation logic and for when minFrac or
// maxFrac was set (but not both) on a currency instance.
// NOTE: Increments are handled in "Rounder.constructCurrency()".
if (useCurrency) {
if (minFrac == -1 && maxFrac == -1) {
minFrac = currency.getDefaultFractionDigits(currencyUsage);
maxFrac = currency.getDefaultFractionDigits(currencyUsage);
} else if (minFrac == -1) {
minFrac = Math.min(maxFrac, currency.getDefaultFractionDigits(currencyUsage));
} else if (maxFrac == -1) {
maxFrac = Math.max(minFrac, currency.getDefaultFractionDigits(currencyUsage));
} else {
// No-op: user override for both minFrac and maxFrac
}
}
// Validate min/max int/frac.
// For backwards compatibility, minimum overrides maximum if the two conflict.
// The following logic ensures that there is always a minimum of at least one digit.
if (minInt == 0 && maxFrac != 0) {
// Force a digit after the decimal point.
minFrac = minFrac <= 0 ? 1 : minFrac;
maxFrac = maxFrac < 0 ? -1 : maxFrac < minFrac ? minFrac : maxFrac;
minInt = 0;
maxInt = maxInt < 0 ? -1 : maxInt > RoundingUtils.MAX_INT_FRAC_SIG ? -1 : maxInt;
} else {
// Force a digit before the decimal point.
minFrac = minFrac < 0 ? 0 : minFrac;
maxFrac = maxFrac < 0 ? -1 : maxFrac < minFrac ? minFrac : maxFrac;
minInt = minInt <= 0 ? 1 : minInt > RoundingUtils.MAX_INT_FRAC_SIG ? 1 : minInt;
maxInt = maxInt < 0 ? -1
: maxInt < minInt ? minInt : maxInt > RoundingUtils.MAX_INT_FRAC_SIG ? -1 : maxInt;
}
Precision rounding = null;
if (explicitCurrencyUsage) {
rounding = Precision.constructCurrency(currencyUsage).withCurrency(currency);
} else if (roundingIncrement != null) {
if (PatternStringUtils.ignoreRoundingIncrement(roundingIncrement, maxFrac)) {
rounding = Precision.constructFraction(minFrac, maxFrac);
} else {
rounding = Precision.constructIncrement(roundingIncrement);
}
} else if (explicitMinMaxSig) {
minSig = minSig < 1 ? 1
: minSig > RoundingUtils.MAX_INT_FRAC_SIG ? RoundingUtils.MAX_INT_FRAC_SIG : minSig;
maxSig = maxSig < 0 ? RoundingUtils.MAX_INT_FRAC_SIG
: maxSig < minSig ? minSig
: maxSig > RoundingUtils.MAX_INT_FRAC_SIG ? RoundingUtils.MAX_INT_FRAC_SIG
: maxSig;
rounding = Precision.constructSignificant(minSig, maxSig);
} else if (explicitMinMaxFrac) {
rounding = Precision.constructFraction(minFrac, maxFrac);
} else if (useCurrency) {
rounding = Precision.constructCurrency(currencyUsage);
}
if (rounding != null) {
rounding = rounding.withMode(mathContext);
macros.precision = rounding;
}
///////////////////
// INTEGER WIDTH //
///////////////////
macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
///////////////////////
// GROUPING STRATEGY //
///////////////////////
macros.grouping = Grouper.forProperties(properties);
/////////////
// PADDING //
/////////////
if (properties.getFormatWidth() > 0) {
macros.padder = Padder.forProperties(properties);
}
///////////////////////////////
// DECIMAL MARK ALWAYS SHOWN //
///////////////////////////////
macros.decimal = properties.getDecimalSeparatorAlwaysShown() ? DecimalSeparatorDisplay.ALWAYS
: DecimalSeparatorDisplay.AUTO;
///////////////////////
// SIGN ALWAYS SHOWN //
///////////////////////
macros.sign = properties.getSignAlwaysShown() ? SignDisplay.ALWAYS : SignDisplay.AUTO;
/////////////////////////
// SCIENTIFIC NOTATION //
/////////////////////////
if (properties.getMinimumExponentDigits() != -1) {
// Scientific notation is required.
// This whole section feels like a hack, but it is needed for regression tests.
// The mapping from property bag to scientific notation is nontrivial due to LDML rules.
if (maxInt > 8) {
// But #13110: The maximum of 8 digits has unknown origins and is not in the spec.
// If maxInt is greater than 8, it is set to minInt, even if minInt is greater than 8.
maxInt = minInt;
macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
} else if (maxInt > minInt && minInt > 1) {
// Bug #13289: if maxInt > minInt > 1, then minInt should be 1.
minInt = 1;
macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
}
int engineering = maxInt < 0 ? -1 : maxInt;
macros.notation = new ScientificNotation(
// Engineering interval:
engineering,
// Enforce minimum integer digits (for patterns like "000.00E0"):
(engineering == minInt),
// Minimum exponent digits:
properties.getMinimumExponentDigits(),
// Exponent sign always shown:
properties.getExponentSignAlwaysShown() ? SignDisplay.ALWAYS : SignDisplay.AUTO);
// Scientific notation also involves overriding the rounding mode.
// TODO: Overriding here is a bit of a hack. Should this logic go earlier?
if (macros.precision instanceof FractionPrecision) {
// For the purposes of rounding, get the original min/max int/frac, since the local
// variables have been manipulated for display purposes.
int maxInt_ = properties.getMaximumIntegerDigits();
int minInt_ = properties.getMinimumIntegerDigits();
int minFrac_ = properties.getMinimumFractionDigits();
int maxFrac_ = properties.getMaximumFractionDigits();
if (minInt_ == 0 && maxFrac_ == 0) {
// Patterns like "#E0" and "##E0", which mean no rounding!
macros.precision = Precision.constructInfinite().withMode(mathContext);
} else if (minInt_ == 0 && minFrac_ == 0) {
// Patterns like "#.##E0" (no zeros in the mantissa), which mean round to maxFrac+1
macros.precision = Precision.constructSignificant(1, maxFrac_ + 1).withMode(mathContext);
} else {
int maxSig_ = minInt_ + maxFrac_;
// Bug #20058: if maxInt_ > minInt_ > 1, then minInt_ should be 1.
if (maxInt_ > minInt_ && minInt_ > 1) {
minInt_ = 1;
}
int minSig_ = minInt_ + minFrac_;
// To avoid regression, maxSig is not reset when minInt_ set to 1.
// TODO: Reset maxSig_ = 1 + minFrac_ to follow the spec.
macros.precision = Precision.constructSignificant(minSig_, maxSig_)
.withMode(mathContext);
}
}
}
//////////////////////
// COMPACT NOTATION //
//////////////////////
if (properties.getCompactStyle() != null) {
if (properties.getCompactCustomData() != null) {
macros.notation = new CompactNotation(properties.getCompactCustomData());
} else if (properties.getCompactStyle() == CompactStyle.LONG) {
macros.notation = Notation.compactLong();
} else {
macros.notation = Notation.compactShort();
}
// Do not forward the affix provider.
macros.affixProvider = null;
}
/////////////////
// MULTIPLIERS //
/////////////////
macros.scale = RoundingUtils.scaleFromProperties(properties);
//////////////////////
// PROPERTY EXPORTS //
//////////////////////
if (exportedProperties != null) {
exportedProperties.setCurrency(currency);
exportedProperties.setMathContext(mathContext);
exportedProperties.setRoundingMode(mathContext.getRoundingMode());
exportedProperties.setMinimumIntegerDigits(minInt);
exportedProperties.setMaximumIntegerDigits(maxInt == -1 ? Integer.MAX_VALUE : maxInt);
Precision rounding_;
if (rounding instanceof CurrencyPrecision) {
rounding_ = ((CurrencyPrecision) rounding).withCurrency(currency);
} else {
rounding_ = rounding;
}
int minFrac_ = minFrac;
int maxFrac_ = maxFrac;
int minSig_ = minSig;
int maxSig_ = maxSig;
BigDecimal increment_ = null;
if (rounding_ instanceof FractionRounderImpl) {
minFrac_ = ((FractionRounderImpl) rounding_).minFrac;
maxFrac_ = ((FractionRounderImpl) rounding_).maxFrac;
} else if (rounding_ instanceof IncrementRounderImpl) {
increment_ = ((IncrementRounderImpl) rounding_).increment;
minFrac_ = increment_.scale();
maxFrac_ = increment_.scale();
} else if (rounding_ instanceof SignificantRounderImpl) {
minSig_ = ((SignificantRounderImpl) rounding_).minSig;
maxSig_ = ((SignificantRounderImpl) rounding_).maxSig;
}
exportedProperties.setMinimumFractionDigits(minFrac_);
exportedProperties.setMaximumFractionDigits(maxFrac_);
exportedProperties.setMinimumSignificantDigits(minSig_);
exportedProperties.setMaximumSignificantDigits(maxSig_);
exportedProperties.setRoundingIncrement(increment_);
}
return macros;
}
}