blob: 8dad5fcfba12f04dd93bd7dfa0a45e7415d175d0 [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.impl.number;
import com.ibm.icu.impl.FormattedStringBuilder;
import com.ibm.icu.impl.StandardPlural;
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;
/**
* This class is a {@link Modifier} that wraps a decimal format pattern. It applies the pattern's affixes
* in {@link Modifier#apply}.
*
* <p>
* In addition to being a Modifier, this class contains the business logic for substituting the correct
* locale symbols into the affixes of the decimal format pattern.
*
* <p>
* In order to use this class, create a new instance and call the following four setters:
* {@link #setPatternInfo}, {@link #setPatternAttributes}, {@link #setSymbols}, and
* {@link #setNumberProperties}. After calling these four setters, the instance will be ready for use as
* a Modifier.
*
* <p>
* This is a MUTABLE, NON-THREAD-SAFE class designed for performance. Do NOT save references to this or
* attempt to use it from multiple threads! Instead, you can obtain a safe, immutable decimal format
* pattern modifier by calling {@link MutablePatternModifier#createImmutable}, in effect treating this
* instance as a builder for the immutable variant.
*/
public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPropsGenerator {
// Modifier details
final boolean isStrong;
// Pattern details
AffixPatternProvider patternInfo;
Field field;
SignDisplay signDisplay;
boolean perMilleReplacesPercent;
// Symbol details
DecimalFormatSymbols symbols;
UnitWidth unitWidth;
Currency currency;
PluralRules rules;
// Number details
Signum signum;
StandardPlural plural;
// QuantityChain details
MicroPropsGenerator parent;
// Transient fields for rendering
StringBuilder currentAffix;
/**
* @param isStrong
* Whether the modifier should be considered strong. For more information, see
* {@link Modifier#isStrong()}. Most of the time, decimal format pattern modifiers should
* be considered as non-strong.
*/
public MutablePatternModifier(boolean isStrong) {
this.isStrong = isStrong;
}
/**
* Sets a reference to the parsed decimal format pattern, usually obtained from
* {@link PatternStringParser#parseToPatternInfo(String)}, but any implementation of
* {@link AffixPatternProvider} is accepted.
*
* @param field
* Which field to use for literal characters in the pattern.
*/
public void setPatternInfo(AffixPatternProvider patternInfo, Field field) {
this.patternInfo = patternInfo;
this.field = field;
}
/**
* Sets attributes that imply changes to the literal interpretation of the pattern string affixes.
*
* @param signDisplay
* Whether to force a plus sign on positive numbers.
* @param perMille
* Whether to substitute the percent sign in the pattern with a permille sign.
*/
public void setPatternAttributes(SignDisplay signDisplay, boolean perMille) {
this.signDisplay = signDisplay;
this.perMilleReplacesPercent = perMille;
}
/**
* Sets locale-specific details that affect the symbols substituted into the pattern string affixes.
*
* @param symbols
* The desired instance of DecimalFormatSymbols.
* @param currency
* The currency to be used when substituting currency values into the affixes.
* @param unitWidth
* The width used to render currencies.
* @param rules
* Required if the triple currency sign, "¤¤¤", appears in the pattern, which can be
* determined from the convenience method {@link #needsPlurals()}.
*/
public void setSymbols(
DecimalFormatSymbols symbols,
Currency currency,
UnitWidth unitWidth,
PluralRules rules) {
assert (rules != null) == needsPlurals();
this.symbols = symbols;
this.currency = currency;
this.unitWidth = unitWidth;
this.rules = rules;
}
/**
* Sets attributes of the current number being processed.
*
* @param signum
* -1 if negative; +1 if positive; or 0 if zero.
* @param plural
* The plural form of the number, required only if the pattern contains the triple
* currency sign, "¤¤¤" (and as indicated by {@link #needsPlurals()}).
*/
public void setNumberProperties(Signum signum, StandardPlural plural) {
assert (plural != null) == needsPlurals();
this.signum = signum;
this.plural = plural;
}
/**
* Returns true if the pattern represented by this MurkyModifier requires a plural keyword in order
* to localize. This is currently true only if there is a currency long name placeholder in the
* pattern ("¤¤¤").
*/
public boolean needsPlurals() {
return patternInfo.containsSymbolType(AffixUtils.TYPE_CURRENCY_TRIPLE);
}
/**
* Creates a new quantity-dependent Modifier that behaves the same as the current instance, but which
* is immutable and can be saved for future use. The number properties in the current instance are
* mutated; all other properties are left untouched.
*
* <p>
* The resulting modifier cannot be used in a QuantityChain.
*
* @return An immutable that supports both positive and negative numbers.
*/
public ImmutablePatternModifier createImmutable() {
FormattedStringBuilder a = new FormattedStringBuilder();
FormattedStringBuilder b = new FormattedStringBuilder();
if (needsPlurals()) {
// Slower path when we require the plural keyword.
AdoptingModifierStore pm = new AdoptingModifierStore();
for (StandardPlural plural : StandardPlural.VALUES) {
setNumberProperties(Signum.POS, plural);
pm.setModifier(Signum.POS, plural, createConstantModifier(a, b));
setNumberProperties(Signum.POS_ZERO, plural);
pm.setModifier(Signum.POS_ZERO, plural, createConstantModifier(a, b));
setNumberProperties(Signum.NEG_ZERO, plural);
pm.setModifier(Signum.NEG_ZERO, plural, createConstantModifier(a, b));
setNumberProperties(Signum.NEG, plural);
pm.setModifier(Signum.NEG, plural, createConstantModifier(a, b));
}
pm.freeze();
return new ImmutablePatternModifier(pm, rules);
} else {
// Faster path when plural keyword is not needed.
setNumberProperties(Signum.POS, null);
Modifier positive = createConstantModifier(a, b);
setNumberProperties(Signum.POS_ZERO, null);
Modifier posZero = createConstantModifier(a, b);
setNumberProperties(Signum.NEG_ZERO, null);
Modifier negZero = createConstantModifier(a, b);
setNumberProperties(Signum.NEG, null);
Modifier negative = createConstantModifier(a, b);
AdoptingModifierStore pm = new AdoptingModifierStore(positive, posZero, negZero, negative);
return new ImmutablePatternModifier(pm, null);
}
}
/**
* Uses the current properties to create a single {@link ConstantMultiFieldModifier} with currency
* spacing support if required.
*
* @param a
* A working FormattedStringBuilder object; passed from the outside to prevent the need to
* create many new instances if this method is called in a loop.
* @param b
* Another working FormattedStringBuilder object.
* @return The constant modifier object.
*/
private ConstantMultiFieldModifier createConstantModifier(
FormattedStringBuilder a,
FormattedStringBuilder b) {
insertPrefix(a.clear(), 0);
insertSuffix(b.clear(), 0);
if (patternInfo.hasCurrencySign()) {
return new CurrencySpacingEnabledModifier(a, b, !patternInfo.hasBody(), isStrong, symbols);
} else {
return new ConstantMultiFieldModifier(a, b, !patternInfo.hasBody(), isStrong);
}
}
public static class ImmutablePatternModifier implements MicroPropsGenerator {
final AdoptingModifierStore pm;
final PluralRules rules;
/* final */ MicroPropsGenerator parent;
ImmutablePatternModifier(
AdoptingModifierStore pm,
PluralRules rules) {
this.pm = pm;
this.rules = rules;
this.parent = null;
}
public ImmutablePatternModifier addToChain(MicroPropsGenerator parent) {
this.parent = parent;
return this;
}
@Override
public MicroProps processQuantity(DecimalQuantity quantity) {
MicroProps micros = parent.processQuantity(quantity);
if (micros.modMiddle != null) {
return micros;
}
applyToMicros(micros, quantity);
return micros;
}
public void applyToMicros(MicroProps micros, DecimalQuantity quantity) {
if (rules == null) {
micros.modMiddle = pm.getModifierWithoutPlural(quantity.signum());
} else {
StandardPlural pluralForm = RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
micros.modMiddle = pm.getModifier(quantity.signum(), pluralForm);
}
}
// NOTE: This method is not used in ICU4J right now.
// In ICU4C, it is used by getPrefixSuffix().
// Un-comment this method when getPrefixSuffix() is cleaned up in ICU4J.
// public Modifier getModifier(byte signum, StandardPlural plural) {
// if (rules == null) {
// return pm.getModifier(signum);
// } else {
// return pm.getModifier(signum, plural);
// }
// }
}
/** Used by the unsafe code path. */
public MicroPropsGenerator addToChain(MicroPropsGenerator parent) {
this.parent = parent;
return this;
}
@Override
public MicroProps processQuantity(DecimalQuantity fq) {
MicroProps micros = parent.processQuantity(fq);
if (micros.modMiddle != null) {
return micros;
}
if (needsPlurals()) {
StandardPlural pluralForm = RoundingUtils.getPluralSafe(micros.rounder, rules, fq);
setNumberProperties(fq.signum(), pluralForm);
} else {
setNumberProperties(fq.signum(), null);
}
micros.modMiddle = this;
return micros;
}
@Override
public int apply(FormattedStringBuilder output, int leftIndex, int rightIndex) {
int prefixLen = insertPrefix(output, leftIndex);
int suffixLen = insertSuffix(output, rightIndex + prefixLen);
// If the pattern had no decimal stem body (like #,##0.00), overwrite the value.
int overwriteLen = 0;
if (!patternInfo.hasBody()) {
overwriteLen = output.splice(leftIndex + prefixLen, rightIndex + prefixLen, "", 0, 0, null);
}
CurrencySpacingEnabledModifier.applyCurrencySpacing(output,
leftIndex,
prefixLen,
rightIndex + prefixLen + overwriteLen,
suffixLen,
symbols);
return prefixLen + overwriteLen + suffixLen;
}
@Override
public int getPrefixLength() {
// Render the affix to get the length
prepareAffix(true);
int result = AffixUtils.unescapedCount(currentAffix, true, this); // prefix length
return result;
}
@Override
public int getCodePointCount() {
// Render the affixes to get the length
prepareAffix(true);
int result = AffixUtils.unescapedCount(currentAffix, false, this); // prefix length
prepareAffix(false);
result += AffixUtils.unescapedCount(currentAffix, false, this); // suffix length
return result;
}
@Override
public boolean isStrong() {
return isStrong;
}
@Override
public boolean containsField(java.text.Format.Field field) {
// This method is not currently used. (unsafe path not used in range formatting)
assert false;
return false;
}
@Override
public Parameters getParameters() {
// This method is not currently used.
assert false;
return null;
}
@Override
public boolean semanticallyEquivalent(Modifier other) {
// This method is not currently used. (unsafe path not used in range formatting)
assert false;
return false;
}
private int insertPrefix(FormattedStringBuilder sb, int position) {
prepareAffix(true);
int length = AffixUtils.unescape(currentAffix, sb, position, this, field);
return length;
}
private int insertSuffix(FormattedStringBuilder sb, int position) {
prepareAffix(false);
int length = AffixUtils.unescape(currentAffix, sb, position, this, field);
return length;
}
/**
* Pre-processes the prefix or suffix into the currentAffix field, creating and mutating that field
* if necessary. Calls down to {@link PatternStringUtils#affixPatternProviderToStringBuilder}.
*
* @param isPrefix
* true to prepare the prefix; false to prepare the suffix.
*/
private void prepareAffix(boolean isPrefix) {
if (currentAffix == null) {
currentAffix = new StringBuilder();
}
PatternStringUtils.patternInfoToStringBuilder(patternInfo,
isPrefix,
PatternStringUtils.resolveSignDisplay(signDisplay, signum),
plural,
perMilleReplacesPercent,
currentAffix);
}
/**
* Returns the string that substitutes a given symbol type in a pattern.
*/
@Override
public CharSequence getSymbol(int type) {
switch (type) {
case AffixUtils.TYPE_MINUS_SIGN:
return symbols.getMinusSignString();
case AffixUtils.TYPE_PLUS_SIGN:
return symbols.getPlusSignString();
case AffixUtils.TYPE_PERCENT:
return symbols.getPercentString();
case AffixUtils.TYPE_PERMILLE:
return symbols.getPerMillString();
case AffixUtils.TYPE_CURRENCY_SINGLE:
// UnitWidth ISO, HIDDEN, or NARROW overrides the singular currency symbol.
if (unitWidth == UnitWidth.ISO_CODE) {
return currency.getCurrencyCode();
} else if (unitWidth == UnitWidth.HIDDEN) {
return "";
} else {
int selector = unitWidth == UnitWidth.NARROW ? Currency.NARROW_SYMBOL_NAME
: Currency.SYMBOL_NAME;
return currency.getName(symbols.getULocale(), selector, null);
}
case AffixUtils.TYPE_CURRENCY_DOUBLE:
return currency.getCurrencyCode();
case AffixUtils.TYPE_CURRENCY_TRIPLE:
// NOTE: This is the code path only for patterns containing "¤¤¤".
// Plural currencies set via the API are formatted in LongNameHandler.
// This code path is used by DecimalFormat via CurrencyPluralInfo.
assert plural != null;
return currency
.getName(symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null);
case AffixUtils.TYPE_CURRENCY_QUAD:
return "\uFFFD";
case AffixUtils.TYPE_CURRENCY_QUINT:
return currency.getName(symbols.getULocale(), Currency.NARROW_SYMBOL_NAME, null);
default:
throw new AssertionError();
}
}
}