1// © 2017 and later: Unicode, Inc. and others. 2// License & terms of use: http://www.unicode.org/copyright.html#License 3package com.ibm.icu.impl.number; 4 5import com.ibm.icu.impl.StandardPlural; 6import com.ibm.icu.impl.number.AffixUtils.SymbolProvider; 7import com.ibm.icu.number.NumberFormatter.SignDisplay; 8import com.ibm.icu.number.NumberFormatter.UnitWidth; 9import com.ibm.icu.text.DecimalFormatSymbols; 10import com.ibm.icu.text.PluralRules; 11import com.ibm.icu.util.Currency; 12 13/** 14 * This class is a {@link Modifier} that wraps a decimal format pattern. It applies the pattern's affixes in 15 * {@link Modifier#apply}. 16 * 17 * <p> 18 * In addition to being a Modifier, this class contains the business logic for substituting the correct locale symbols 19 * into the affixes of the decimal format pattern. 20 * 21 * <p> 22 * In order to use this class, create a new instance and call the following four setters: {@link #setPatternInfo}, 23 * {@link #setPatternAttributes}, {@link #setSymbols}, and {@link #setNumberProperties}. After calling these four 24 * setters, the instance will be ready for use as a Modifier. 25 * 26 * <p> 27 * This is a MUTABLE, NON-THREAD-SAFE class designed for performance. Do NOT save references to this or attempt to use 28 * it from multiple threads! Instead, you can obtain a safe, immutable decimal format pattern modifier by calling 29 * {@link MutablePatternModifier#createImmutable}, in effect treating this instance as a builder for the immutable 30 * variant. 31 */ 32public class MutablePatternModifier implements Modifier, SymbolProvider, CharSequence, MicroPropsGenerator { 33 34 // Modifier details 35 final boolean isStrong; 36 37 // Pattern details 38 AffixPatternProvider patternInfo; 39 SignDisplay signDisplay; 40 boolean perMilleReplacesPercent; 41 42 // Symbol details 43 DecimalFormatSymbols symbols; 44 UnitWidth unitWidth; 45 Currency currency; 46 PluralRules rules; 47 48 // Number details 49 boolean isNegative; 50 StandardPlural plural; 51 52 // QuantityChain details 53 MicroPropsGenerator parent; 54 55 // Transient CharSequence fields 56 boolean inCharSequenceMode; 57 int flags; 58 int length; 59 boolean prependSign; 60 boolean plusReplacesMinusSign; 61 62 /** 63 * @param isStrong 64 * Whether the modifier should be considered strong. For more information, see 65 * {@link Modifier#isStrong()}. Most of the time, decimal format pattern modifiers should be considered 66 * as non-strong. 67 */ 68 public MutablePatternModifier(boolean isStrong) { 69 this.isStrong = isStrong; 70 } 71 72 /** 73 * Sets a reference to the parsed decimal format pattern, usually obtained from 74 * {@link PatternStringParser#parseToPatternInfo(String)}, but any implementation of {@link AffixPatternProvider} is 75 * accepted. 76 */ 77 public void setPatternInfo(AffixPatternProvider patternInfo) { 78 this.patternInfo = patternInfo; 79 } 80 81 /** 82 * Sets attributes that imply changes to the literal interpretation of the pattern string affixes. 83 * 84 * @param signDisplay 85 * Whether to force a plus sign on positive numbers. 86 * @param perMille 87 * Whether to substitute the percent sign in the pattern with a permille sign. 88 */ 89 public void setPatternAttributes(SignDisplay signDisplay, boolean perMille) { 90 this.signDisplay = signDisplay; 91 this.perMilleReplacesPercent = perMille; 92 } 93 94 /** 95 * Sets locale-specific details that affect the symbols substituted into the pattern string affixes. 96 * 97 * @param symbols 98 * The desired instance of DecimalFormatSymbols. 99 * @param currency 100 * The currency to be used when substituting currency values into the affixes. 101 * @param unitWidth 102 * The width used to render currencies. 103 * @param rules 104 * Required if the triple currency sign, "¤¤¤", appears in the pattern, which can be determined from the 105 * convenience method {@link #needsPlurals()}. 106 */ 107 public void setSymbols(DecimalFormatSymbols symbols, Currency currency, UnitWidth unitWidth, PluralRules rules) { 108 assert (rules != null) == needsPlurals(); 109 this.symbols = symbols; 110 this.currency = currency; 111 this.unitWidth = unitWidth; 112 this.rules = rules; 113 } 114 115 /** 116 * Sets attributes of the current number being processed. 117 * 118 * @param isNegative 119 * Whether the number is negative. 120 * @param plural 121 * The plural form of the number, required only if the pattern contains the triple currency sign, "¤¤¤" 122 * (and as indicated by {@link #needsPlurals()}). 123 */ 124 public void setNumberProperties(boolean isNegative, StandardPlural plural) { 125 assert (plural != null) == needsPlurals(); 126 this.isNegative = isNegative; 127 this.plural = plural; 128 } 129 130 /** 131 * Returns true if the pattern represented by this MurkyModifier requires a plural keyword in order to localize. 132 * This is currently true only if there is a currency long name placeholder in the pattern ("¤¤¤"). 133 */ 134 public boolean needsPlurals() { 135 return patternInfo.containsSymbolType(AffixUtils.TYPE_CURRENCY_TRIPLE); 136 } 137 138 /** 139 * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but which is immutable 140 * and can be saved for future use. The number properties in the current instance are mutated; all other properties 141 * are left untouched. 142 * 143 * <p> 144 * The resulting modifier cannot be used in a QuantityChain. 145 * 146 * @return An immutable that supports both positive and negative numbers. 147 */ 148 public ImmutablePatternModifier createImmutable() { 149 return createImmutableAndChain(null); 150 } 151 152 /** 153 * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but which is immutable 154 * and can be saved for future use. The number properties in the current instance are mutated; all other properties 155 * are left untouched. 156 * 157 * @param parent 158 * The QuantityChain to which to chain this immutable. 159 * @return An immutable that supports both positive and negative numbers. 160 */ 161 public ImmutablePatternModifier createImmutableAndChain(MicroPropsGenerator parent) { 162 NumberStringBuilder a = new NumberStringBuilder(); 163 NumberStringBuilder b = new NumberStringBuilder(); 164 if (needsPlurals()) { 165 // Slower path when we require the plural keyword. 166 ParameterizedModifier pm = new ParameterizedModifier(); 167 for (StandardPlural plural : StandardPlural.VALUES) { 168 setNumberProperties(false, plural); 169 pm.setModifier(false, plural, createConstantModifier(a, b)); 170 setNumberProperties(true, plural); 171 pm.setModifier(true, plural, createConstantModifier(a, b)); 172 } 173 pm.freeze(); 174 return new ImmutablePatternModifier(pm, rules, parent); 175 } else { 176 // Faster path when plural keyword is not needed. 177 setNumberProperties(false, null); 178 Modifier positive = createConstantModifier(a, b); 179 setNumberProperties(true, null); 180 Modifier negative = createConstantModifier(a, b); 181 ParameterizedModifier pm = new ParameterizedModifier(positive, negative); 182 return new ImmutablePatternModifier(pm, null, parent); 183 } 184 } 185 186 /** 187 * Uses the current properties to create a single {@link ConstantMultiFieldModifier} with currency spacing support 188 * if required. 189 * 190 * @param a 191 * A working NumberStringBuilder object; passed from the outside to prevent the need to create many new 192 * instances if this method is called in a loop. 193 * @param b 194 * Another working NumberStringBuilder object. 195 * @return The constant modifier object. 196 */ 197 private ConstantMultiFieldModifier createConstantModifier(NumberStringBuilder a, NumberStringBuilder b) { 198 insertPrefix(a.clear(), 0); 199 insertSuffix(b.clear(), 0); 200 if (patternInfo.hasCurrencySign()) { 201 return new CurrencySpacingEnabledModifier(a, b, isStrong, symbols); 202 } else { 203 return new ConstantMultiFieldModifier(a, b, isStrong); 204 } 205 } 206 207 public static class ImmutablePatternModifier implements MicroPropsGenerator { 208 final ParameterizedModifier pm; 209 final PluralRules rules; 210 final MicroPropsGenerator parent; 211 212 ImmutablePatternModifier(ParameterizedModifier pm, PluralRules rules, MicroPropsGenerator parent) { 213 this.pm = pm; 214 this.rules = rules; 215 this.parent = parent; 216 } 217 218 @Override 219 public MicroProps processQuantity(DecimalQuantity quantity) { 220 MicroProps micros = parent.processQuantity(quantity); 221 applyToMicros(micros, quantity); 222 return micros; 223 } 224 225 public void applyToMicros(MicroProps micros, DecimalQuantity quantity) { 226 if (rules == null) { 227 micros.modMiddle = pm.getModifier(quantity.isNegative()); 228 } else { 229 // TODO: Fix this. Avoid the copy. 230 DecimalQuantity copy = quantity.createCopy(); 231 copy.roundToInfinity(); 232 StandardPlural plural = copy.getStandardPlural(rules); 233 micros.modMiddle = pm.getModifier(quantity.isNegative(), plural); 234 } 235 } 236 } 237 238 /** Used by the unsafe code path. */ 239 public MicroPropsGenerator addToChain(MicroPropsGenerator parent) { 240 this.parent = parent; 241 return this; 242 } 243 244 @Override 245 public MicroProps processQuantity(DecimalQuantity fq) { 246 MicroProps micros = parent.processQuantity(fq); 247 if (needsPlurals()) { 248 // TODO: Fix this. Avoid the copy. 249 DecimalQuantity copy = fq.createCopy(); 250 micros.rounding.apply(copy); 251 setNumberProperties(fq.isNegative(), copy.getStandardPlural(rules)); 252 } else { 253 setNumberProperties(fq.isNegative(), null); 254 } 255 micros.modMiddle = this; 256 return micros; 257 } 258 259 @Override 260 public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { 261 int prefixLen = insertPrefix(output, leftIndex); 262 int suffixLen = insertSuffix(output, rightIndex + prefixLen); 263 CurrencySpacingEnabledModifier.applyCurrencySpacing(output, leftIndex, prefixLen, rightIndex + prefixLen, 264 suffixLen, symbols); 265 return prefixLen + suffixLen; 266 } 267 268 @Override 269 public int getPrefixLength() { 270 // Enter and exit CharSequence Mode to get the length. 271 enterCharSequenceMode(true); 272 int result = AffixUtils.unescapedCodePointCount(this, this); // prefix length 273 exitCharSequenceMode(); 274 return result; 275 } 276 277 @Override 278 public int getCodePointCount() { 279 // Enter and exit CharSequence Mode to get the length. 280 enterCharSequenceMode(true); 281 int result = AffixUtils.unescapedCodePointCount(this, this); // prefix length 282 exitCharSequenceMode(); 283 enterCharSequenceMode(false); 284 result += AffixUtils.unescapedCodePointCount(this, this); // suffix length 285 exitCharSequenceMode(); 286 return result; 287 } 288 289 @Override 290 public boolean isStrong() { 291 return isStrong; 292 } 293 294 private int insertPrefix(NumberStringBuilder sb, int position) { 295 enterCharSequenceMode(true); 296 int length = AffixUtils.unescape(this, sb, position, this); 297 exitCharSequenceMode(); 298 return length; 299 } 300 301 private int insertSuffix(NumberStringBuilder sb, int position) { 302 enterCharSequenceMode(false); 303 int length = AffixUtils.unescape(this, sb, position, this); 304 exitCharSequenceMode(); 305 return length; 306 } 307 308 /** 309 * Returns the string that substitutes a given symbol type in a pattern. 310 */ 311 @Override 312 public CharSequence getSymbol(int type) { 313 switch (type) { 314 case AffixUtils.TYPE_MINUS_SIGN: 315 return symbols.getMinusSignString(); 316 case AffixUtils.TYPE_PLUS_SIGN: 317 return symbols.getPlusSignString(); 318 case AffixUtils.TYPE_PERCENT: 319 return symbols.getPercentString(); 320 case AffixUtils.TYPE_PERMILLE: 321 return symbols.getPerMillString(); 322 case AffixUtils.TYPE_CURRENCY_SINGLE: 323 // UnitWidth ISO, HIDDEN, or NARROW overrides the singular currency symbol. 324 if (unitWidth == UnitWidth.ISO_CODE) { 325 return currency.getCurrencyCode(); 326 } else if (unitWidth == UnitWidth.HIDDEN) { 327 return ""; 328 } else if (unitWidth == UnitWidth.NARROW) { 329 return currency.getName(symbols.getULocale(), Currency.NARROW_SYMBOL_NAME, null); 330 } else { 331 return currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null); 332 } 333 case AffixUtils.TYPE_CURRENCY_DOUBLE: 334 return currency.getCurrencyCode(); 335 case AffixUtils.TYPE_CURRENCY_TRIPLE: 336 // NOTE: This is the code path only for patterns containing "¤¤¤". 337 // Plural currencies set via the API are formatted in LongNameHandler. 338 // This code path is used by DecimalFormat via CurrencyPluralInfo. 339 assert plural != null; 340 return currency.getName(symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null); 341 case AffixUtils.TYPE_CURRENCY_QUAD: 342 return "\uFFFD"; 343 case AffixUtils.TYPE_CURRENCY_QUINT: 344 return currency.getName(symbols.getULocale(), Currency.NARROW_SYMBOL_NAME, null); 345 default: 346 throw new AssertionError(); 347 } 348 } 349 350 /** This method contains the heart of the logic for rendering LDML affix strings. */ 351 private void enterCharSequenceMode(boolean isPrefix) { 352 assert !inCharSequenceMode; 353 inCharSequenceMode = true; 354 355 // Should the output render '+' where '-' would normally appear in the pattern? 356 plusReplacesMinusSign = !isNegative 357 && (signDisplay == SignDisplay.ALWAYS || signDisplay == SignDisplay.ACCOUNTING_ALWAYS) 358 && patternInfo.positiveHasPlusSign() == false; 359 360 // Should we use the affix from the negative subpattern? (If not, we will use the positive subpattern.) 361 boolean useNegativeAffixPattern = patternInfo.hasNegativeSubpattern() 362 && (isNegative || (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign)); 363 364 // Resolve the flags for the affix pattern. 365 flags = 0; 366 if (useNegativeAffixPattern) { 367 flags |= AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN; 368 } 369 if (isPrefix) { 370 flags |= AffixPatternProvider.Flags.PREFIX; 371 } 372 if (plural != null) { 373 assert plural.ordinal() == (AffixPatternProvider.Flags.PLURAL_MASK & plural.ordinal()); 374 flags |= plural.ordinal(); 375 } 376 377 // Should we prepend a sign to the pattern? 378 if (!isPrefix || useNegativeAffixPattern) { 379 prependSign = false; 380 } else if (isNegative) { 381 prependSign = signDisplay != SignDisplay.NEVER; 382 } else { 383 prependSign = plusReplacesMinusSign; 384 } 385 386 // Finally, compute the length of the affix pattern. 387 length = patternInfo.length(flags) + (prependSign ? 1 : 0); 388 } 389 390 private void exitCharSequenceMode() { 391 assert inCharSequenceMode; 392 inCharSequenceMode = false; 393 } 394 395 @Override 396 public int length() { 397 assert inCharSequenceMode; 398 return length; 399 } 400 401 @Override 402 public char charAt(int index) { 403 assert inCharSequenceMode; 404 char candidate; 405 if (prependSign && index == 0) { 406 candidate = '-'; 407 } else if (prependSign) { 408 candidate = patternInfo.charAt(flags, index - 1); 409 } else { 410 candidate = patternInfo.charAt(flags, index); 411 } 412 if (plusReplacesMinusSign && candidate == '-') { 413 return '+'; 414 } 415 if (perMilleReplacesPercent && candidate == '%') { 416 return '‰'; 417 } 418 return candidate; 419 } 420 421 @Override 422 public CharSequence subSequence(int start, int end) { 423 // Never called by AffixUtils 424 throw new AssertionError(); 425 } 426} 427