NumberPropertyMapper.java revision dcdfae8fec2b6153a1674e58a02144ca6ab477e8
1// © 2017 and later: Unicode, Inc. and others. 2// License & terms of use: http://www.unicode.org/copyright.html#License 3package com.ibm.icu.number; 4 5import java.math.BigDecimal; 6import java.math.MathContext; 7 8import com.ibm.icu.impl.StandardPlural; 9import com.ibm.icu.impl.number.AffixPatternProvider; 10import com.ibm.icu.impl.number.AffixUtils; 11import com.ibm.icu.impl.number.CustomSymbolCurrency; 12import com.ibm.icu.impl.number.DecimalFormatProperties; 13import com.ibm.icu.impl.number.MacroProps; 14import com.ibm.icu.impl.number.MultiplierImpl; 15import com.ibm.icu.impl.number.Padder; 16import com.ibm.icu.impl.number.PatternStringParser; 17import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo; 18import com.ibm.icu.impl.number.RoundingUtils; 19import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay; 20import com.ibm.icu.number.NumberFormatter.SignDisplay; 21import com.ibm.icu.number.Rounder.FractionRounderImpl; 22import com.ibm.icu.number.Rounder.IncrementRounderImpl; 23import com.ibm.icu.number.Rounder.SignificantRounderImpl; 24import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; 25import com.ibm.icu.text.CurrencyPluralInfo; 26import com.ibm.icu.text.DecimalFormatSymbols; 27import com.ibm.icu.util.Currency; 28import com.ibm.icu.util.Currency.CurrencyUsage; 29import com.ibm.icu.util.ULocale; 30 31/** 32 * <p> 33 * This class, as well as NumberFormatterImpl, could go into the impl package, but they depend on too many 34 * package-private members of the public APIs. 35 */ 36final class NumberPropertyMapper { 37 38 /** Convenience method to create a NumberFormatter directly from Properties. */ 39 public static UnlocalizedNumberFormatter create(DecimalFormatProperties properties, DecimalFormatSymbols symbols) { 40 MacroProps macros = oldToNew(properties, symbols, null); 41 return NumberFormatter.with().macros(macros); 42 } 43 44 /** 45 * Convenience method to create a NumberFormatter directly from a pattern string. Something like this could become 46 * public API if there is demand. 47 */ 48 public static UnlocalizedNumberFormatter create(String pattern, DecimalFormatSymbols symbols) { 49 DecimalFormatProperties properties = PatternStringParser.parseToProperties(pattern); 50 return create(properties, symbols); 51 } 52 53 /** 54 * Creates a new {@link MacroProps} object based on the content of a {@link DecimalFormatProperties} object. In 55 * other words, maps Properties to MacroProps. This function is used by the JDK-compatibility API to call into the 56 * ICU 60 fluent number formatting pipeline. 57 * 58 * @param properties 59 * The property bag to be mapped. 60 * @param symbols 61 * The symbols associated with the property bag. 62 * @param exportedProperties 63 * A property bag in which to store validated properties. 64 * @return A new MacroProps containing all of the information in the Properties. 65 */ 66 public static MacroProps oldToNew(DecimalFormatProperties properties, DecimalFormatSymbols symbols, 67 DecimalFormatProperties exportedProperties) { 68 MacroProps macros = new MacroProps(); 69 ULocale locale = symbols.getULocale(); 70 71 ///////////// 72 // SYMBOLS // 73 ///////////// 74 75 macros.symbols = symbols; 76 77 ////////////////// 78 // PLURAL RULES // 79 ////////////////// 80 81 macros.rules = properties.getPluralRules(); 82 83 ///////////// 84 // AFFIXES // 85 ///////////// 86 87 AffixPatternProvider affixProvider; 88 if (properties.getCurrencyPluralInfo() == null) { 89 affixProvider = new PropertiesAffixPatternProvider(properties); 90 } else { 91 affixProvider = new CurrencyPluralInfoAffixProvider(properties.getCurrencyPluralInfo()); 92 } 93 macros.affixProvider = affixProvider; 94 95 /////////// 96 // UNITS // 97 /////////// 98 99 boolean useCurrency = ((properties.getCurrency() != null) || properties.getCurrencyPluralInfo() != null 100 || properties.getCurrencyUsage() != null || affixProvider.hasCurrencySign()); 101 Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols); 102 CurrencyUsage currencyUsage = properties.getCurrencyUsage(); 103 boolean explicitCurrencyUsage = currencyUsage != null; 104 if (!explicitCurrencyUsage) { 105 currencyUsage = CurrencyUsage.STANDARD; 106 } 107 if (useCurrency) { 108 macros.unit = currency; 109 } 110 111 /////////////////////// 112 // ROUNDING STRATEGY // 113 /////////////////////// 114 115 int maxInt = properties.getMaximumIntegerDigits(); 116 int minInt = properties.getMinimumIntegerDigits(); 117 int maxFrac = properties.getMaximumFractionDigits(); 118 int minFrac = properties.getMinimumFractionDigits(); 119 int minSig = properties.getMinimumSignificantDigits(); 120 int maxSig = properties.getMaximumSignificantDigits(); 121 BigDecimal roundingIncrement = properties.getRoundingIncrement(); 122 MathContext mathContext = RoundingUtils.getMathContextOrUnlimited(properties); 123 boolean explicitMinMaxFrac = minFrac != -1 || maxFrac != -1; 124 boolean explicitMinMaxSig = minSig != -1 || maxSig != -1; 125 // Resolve min/max frac for currencies, required for the validation logic and for when minFrac or maxFrac was 126 // set (but not both) on a currency instance. 127 // NOTE: Increments are handled in "Rounder.constructCurrency()". 128 if (useCurrency) { 129 if (minFrac == -1 && maxFrac == -1) { 130 minFrac = currency.getDefaultFractionDigits(currencyUsage); 131 maxFrac = currency.getDefaultFractionDigits(currencyUsage); 132 } else if (minFrac == -1) { 133 minFrac = Math.min(maxFrac, currency.getDefaultFractionDigits(currencyUsage)); 134 } else if (maxFrac == -1) { 135 maxFrac = Math.max(minFrac, currency.getDefaultFractionDigits(currencyUsage)); 136 } else { 137 // No-op: user override for both minFrac and maxFrac 138 } 139 } 140 // Validate min/max int/frac. 141 // For backwards compatibility, minimum overrides maximum if the two conflict. 142 // The following logic ensures that there is always a minimum of at least one digit. 143 if (minInt == 0 && maxFrac != 0) { 144 // Force a digit after the decimal point. 145 minFrac = minFrac <= 0 ? 1 : minFrac; 146 maxFrac = maxFrac < 0 ? Integer.MAX_VALUE : maxFrac < minFrac ? minFrac : maxFrac; 147 minInt = 0; 148 maxInt = maxInt < 0 ? -1 : maxInt > RoundingUtils.MAX_INT_FRAC_SIG ? -1 : maxInt; 149 } else { 150 // Force a digit before the decimal point. 151 minFrac = minFrac < 0 ? 0 : minFrac; 152 maxFrac = maxFrac < 0 ? Integer.MAX_VALUE : maxFrac < minFrac ? minFrac : maxFrac; 153 minInt = minInt <= 0 ? 1 : minInt > RoundingUtils.MAX_INT_FRAC_SIG ? 1 : minInt; 154 maxInt = maxInt < 0 ? -1 : maxInt < minInt ? minInt : maxInt > RoundingUtils.MAX_INT_FRAC_SIG ? -1 : maxInt; 155 } 156 Rounder rounding = null; 157 if (explicitCurrencyUsage) { 158 rounding = Rounder.constructCurrency(currencyUsage).withCurrency(currency); 159 } else if (roundingIncrement != null) { 160 rounding = Rounder.constructIncrement(roundingIncrement); 161 } else if (explicitMinMaxSig) { 162 minSig = minSig < 1 ? 1 : minSig > RoundingUtils.MAX_INT_FRAC_SIG ? RoundingUtils.MAX_INT_FRAC_SIG : minSig; 163 maxSig = maxSig < 0 ? RoundingUtils.MAX_INT_FRAC_SIG 164 : maxSig < minSig ? minSig 165 : maxSig > RoundingUtils.MAX_INT_FRAC_SIG ? RoundingUtils.MAX_INT_FRAC_SIG : maxSig; 166 rounding = Rounder.constructSignificant(minSig, maxSig); 167 } else if (explicitMinMaxFrac) { 168 rounding = Rounder.constructFraction(minFrac, maxFrac); 169 } else if (useCurrency) { 170 rounding = Rounder.constructCurrency(currencyUsage); 171 } 172 if (rounding != null) { 173 rounding = rounding.withMode(mathContext); 174 macros.rounder = rounding; 175 } 176 177 /////////////////// 178 // INTEGER WIDTH // 179 /////////////////// 180 181 macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt); 182 183 /////////////////////// 184 // GROUPING STRATEGY // 185 /////////////////////// 186 187 int grouping1 = properties.getGroupingSize(); 188 int grouping2 = properties.getSecondaryGroupingSize(); 189 int minGrouping = properties.getMinimumGroupingDigits(); 190 assert grouping1 >= -2; // value of -2 means to forward no grouping information 191 grouping1 = grouping1 > 0 ? grouping1 : grouping2 > 0 ? grouping2 : grouping1; 192 grouping2 = grouping2 > 0 ? grouping2 : grouping1; 193 // TODO: Is it important to handle minGrouping > 2? 194 macros.grouper = Grouper.getInstance((byte) grouping1, (byte) grouping2, minGrouping == 2); 195 196 ///////////// 197 // PADDING // 198 ///////////// 199 200 if (properties.getFormatWidth() != -1) { 201 macros.padder = new Padder(properties.getPadString(), properties.getFormatWidth(), 202 properties.getPadPosition()); 203 } 204 205 /////////////////////////////// 206 // DECIMAL MARK ALWAYS SHOWN // 207 /////////////////////////////// 208 209 macros.decimal = properties.getDecimalSeparatorAlwaysShown() ? DecimalSeparatorDisplay.ALWAYS 210 : DecimalSeparatorDisplay.AUTO; 211 212 /////////////////////// 213 // SIGN ALWAYS SHOWN // 214 /////////////////////// 215 216 macros.sign = properties.getSignAlwaysShown() ? SignDisplay.ALWAYS : SignDisplay.AUTO; 217 218 ///////////////////////// 219 // SCIENTIFIC NOTATION // 220 ///////////////////////// 221 222 if (properties.getMinimumExponentDigits() != -1) { 223 // Scientific notation is required. 224 // This whole section feels like a hack, but it is needed for regression tests. 225 // The mapping from property bag to scientific notation is nontrivial due to LDML rules. 226 if (maxInt > 8) { 227 // But #13110: The maximum of 8 digits has unknown origins and is not in the spec. 228 // If maxInt is greater than 8, it is set to minInt, even if minInt is greater than 8. 229 maxInt = minInt; 230 macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt); 231 } else if (maxInt > minInt && minInt > 1) { 232 // Bug #13289: if maxInt > minInt > 1, then minInt should be 1. 233 minInt = 1; 234 macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt); 235 } 236 int engineering = maxInt < 0 ? -1 : maxInt; 237 macros.notation = new ScientificNotation( 238 // Engineering interval: 239 engineering, 240 // Enforce minimum integer digits (for patterns like "000.00E0"): 241 (engineering == minInt), 242 // Minimum exponent digits: 243 properties.getMinimumExponentDigits(), 244 // Exponent sign always shown: 245 properties.getExponentSignAlwaysShown() ? SignDisplay.ALWAYS : SignDisplay.AUTO); 246 // Scientific notation also involves overriding the rounding mode. 247 // TODO: Overriding here is a bit of a hack. Should this logic go earlier? 248 if (macros.rounder instanceof FractionRounder) { 249 // For the purposes of rounding, get the original min/max int/frac, since the local variables 250 // have been manipulated for display purposes. 251 int minInt_ = properties.getMinimumIntegerDigits(); 252 int minFrac_ = properties.getMinimumFractionDigits(); 253 int maxFrac_ = properties.getMaximumFractionDigits(); 254 if (minInt_ == 0 && maxFrac_ == 0) { 255 // Patterns like "#E0" and "##E0", which mean no rounding! 256 macros.rounder = Rounder.constructInfinite().withMode(mathContext); 257 } else if (minInt_ == 0 && minFrac_ == 0) { 258 // Patterns like "#.##E0" (no zeros in the mantissa), which mean round to maxFrac+1 259 macros.rounder = Rounder.constructSignificant(1, maxFrac_ + 1).withMode(mathContext); 260 } else { 261 // All other scientific patterns, which mean round to minInt+maxFrac 262 macros.rounder = Rounder.constructSignificant(minInt_ + minFrac_, minInt_ + maxFrac_) 263 .withMode(mathContext); 264 } 265 } 266 } 267 268 ////////////////////// 269 // COMPACT NOTATION // 270 ////////////////////// 271 272 if (properties.getCompactStyle() != null) { 273 if (properties.getCompactCustomData() != null) { 274 macros.notation = new CompactNotation(properties.getCompactCustomData()); 275 } else if (properties.getCompactStyle() == CompactStyle.LONG) { 276 macros.notation = Notation.compactLong(); 277 } else { 278 macros.notation = Notation.compactShort(); 279 } 280 // Do not forward the affix provider. 281 macros.affixProvider = null; 282 } 283 284 ///////////////// 285 // MULTIPLIERS // 286 ///////////////// 287 288 if (properties.getMagnitudeMultiplier() != 0) { 289 macros.multiplier = new MultiplierImpl(properties.getMagnitudeMultiplier()); 290 } else if (properties.getMultiplier() != null) { 291 macros.multiplier = new MultiplierImpl(properties.getMultiplier()); 292 } 293 294 ////////////////////// 295 // PROPERTY EXPORTS // 296 ////////////////////// 297 298 if (exportedProperties != null) { 299 300 exportedProperties.setMathContext(mathContext); 301 exportedProperties.setRoundingMode(mathContext.getRoundingMode()); 302 exportedProperties.setMinimumIntegerDigits(minInt); 303 exportedProperties.setMaximumIntegerDigits(maxInt == -1 ? Integer.MAX_VALUE : maxInt); 304 305 Rounder rounding_; 306 if (rounding instanceof CurrencyRounder) { 307 rounding_ = ((CurrencyRounder) rounding).withCurrency(currency); 308 } else { 309 rounding_ = rounding; 310 } 311 int minFrac_ = minFrac; 312 int maxFrac_ = maxFrac; 313 int minSig_ = minSig; 314 int maxSig_ = maxSig; 315 BigDecimal increment_ = null; 316 if (rounding_ instanceof FractionRounderImpl) { 317 minFrac_ = ((FractionRounderImpl) rounding_).minFrac; 318 maxFrac_ = ((FractionRounderImpl) rounding_).maxFrac; 319 } else if (rounding_ instanceof IncrementRounderImpl) { 320 increment_ = ((IncrementRounderImpl) rounding_).increment; 321 minFrac_ = increment_.scale(); 322 maxFrac_ = increment_.scale(); 323 } else if (rounding_ instanceof SignificantRounderImpl) { 324 minSig_ = ((SignificantRounderImpl) rounding_).minSig; 325 maxSig_ = ((SignificantRounderImpl) rounding_).maxSig; 326 } 327 328 exportedProperties.setMinimumFractionDigits(minFrac_); 329 exportedProperties.setMaximumFractionDigits(maxFrac_); 330 exportedProperties.setMinimumSignificantDigits(minSig_); 331 exportedProperties.setMaximumSignificantDigits(maxSig_); 332 exportedProperties.setRoundingIncrement(increment_); 333 } 334 335 return macros; 336 } 337 338 private static class PropertiesAffixPatternProvider implements AffixPatternProvider { 339 private final String posPrefix; 340 private final String posSuffix; 341 private final String negPrefix; 342 private final String negSuffix; 343 344 public PropertiesAffixPatternProvider(DecimalFormatProperties properties) { 345 // There are two ways to set affixes in DecimalFormat: via the pattern string (applyPattern), and via the 346 // explicit setters (setPositivePrefix and friends). The way to resolve the settings is as follows: 347 // 348 // 1) If the explicit setting is present for the field, use it. 349 // 2) Otherwise, follows UTS 35 rules based on the pattern string. 350 // 351 // Importantly, the explicit setters affect only the one field they override. If you set the positive 352 // prefix, that should not affect the negative prefix. Since it is impossible for the user of this class 353 // to know whether the origin for a string was the override or the pattern, we have to say that we always 354 // have a negative subpattern and perform all resolution logic here. 355 356 // Convenience: Extract the properties into local variables. 357 // Variables are named with three chars: [p/n][p/s][o/p] 358 // [p/n] => p for positive, n for negative 359 // [p/s] => p for prefix, s for suffix 360 // [o/p] => o for escaped custom override string, p for pattern string 361 String ppo = AffixUtils.escape(properties.getPositivePrefix()); 362 String pso = AffixUtils.escape(properties.getPositiveSuffix()); 363 String npo = AffixUtils.escape(properties.getNegativePrefix()); 364 String nso = AffixUtils.escape(properties.getNegativeSuffix()); 365 String ppp = properties.getPositivePrefixPattern(); 366 String psp = properties.getPositiveSuffixPattern(); 367 String npp = properties.getNegativePrefixPattern(); 368 String nsp = properties.getNegativeSuffixPattern(); 369 370 if (ppo != null) { 371 posPrefix = ppo; 372 } else if (ppp != null) { 373 posPrefix = ppp; 374 } else { 375 // UTS 35: Default positive prefix is empty string. 376 posPrefix = ""; 377 } 378 379 if (pso != null) { 380 posSuffix = pso; 381 } else if (psp != null) { 382 posSuffix = psp; 383 } else { 384 // UTS 35: Default positive suffix is empty string. 385 posSuffix = ""; 386 } 387 388 if (npo != null) { 389 negPrefix = npo; 390 } else if (npp != null) { 391 negPrefix = npp; 392 } else { 393 // UTS 35: Default negative prefix is "-" with positive prefix. 394 // Important: We prepend the "-" to the pattern, not the override! 395 negPrefix = ppp == null ? "-" : "-" + ppp; 396 } 397 398 if (nso != null) { 399 negSuffix = nso; 400 } else if (nsp != null) { 401 negSuffix = nsp; 402 } else { 403 // UTS 35: Default negative prefix is the positive prefix. 404 negSuffix = psp == null ? "" : psp; 405 } 406 } 407 408 @Override 409 public char charAt(int flags, int i) { 410 return getStringForFlags(flags).charAt(i); 411 } 412 413 @Override 414 public int length(int flags) { 415 return getStringForFlags(flags).length(); 416 } 417 418 private String getStringForFlags(int flags) { 419 boolean prefix = (flags & Flags.PREFIX) != 0; 420 boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0; 421 if (prefix && negative) { 422 return negPrefix; 423 } else if (prefix) { 424 return posPrefix; 425 } else if (negative) { 426 return negSuffix; 427 } else { 428 return posSuffix; 429 } 430 } 431 432 @Override 433 public boolean positiveHasPlusSign() { 434 return AffixUtils.containsType(posPrefix, AffixUtils.TYPE_PLUS_SIGN) 435 || AffixUtils.containsType(posSuffix, AffixUtils.TYPE_PLUS_SIGN); 436 } 437 438 @Override 439 public boolean hasNegativeSubpattern() { 440 // See comments in the constructor for more information on why this is always true. 441 return true; 442 } 443 444 @Override 445 public boolean negativeHasMinusSign() { 446 return AffixUtils.containsType(negPrefix, AffixUtils.TYPE_MINUS_SIGN) 447 || AffixUtils.containsType(negSuffix, AffixUtils.TYPE_MINUS_SIGN); 448 } 449 450 @Override 451 public boolean hasCurrencySign() { 452 return AffixUtils.hasCurrencySymbols(posPrefix) || AffixUtils.hasCurrencySymbols(posSuffix) 453 || AffixUtils.hasCurrencySymbols(negPrefix) || AffixUtils.hasCurrencySymbols(negSuffix); 454 } 455 456 @Override 457 public boolean containsSymbolType(int type) { 458 return AffixUtils.containsType(posPrefix, type) || AffixUtils.containsType(posSuffix, type) 459 || AffixUtils.containsType(negPrefix, type) || AffixUtils.containsType(negSuffix, type); 460 } 461 } 462 463 private static class CurrencyPluralInfoAffixProvider implements AffixPatternProvider { 464 private final AffixPatternProvider[] affixesByPlural; 465 466 public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) { 467 affixesByPlural = new ParsedPatternInfo[StandardPlural.COUNT]; 468 for (StandardPlural plural : StandardPlural.VALUES) { 469 affixesByPlural[plural.ordinal()] = PatternStringParser 470 .parseToPatternInfo(cpi.getCurrencyPluralPattern(plural.getKeyword())); 471 } 472 } 473 474 @Override 475 public char charAt(int flags, int i) { 476 int pluralOrdinal = (flags & Flags.PLURAL_MASK); 477 return affixesByPlural[pluralOrdinal].charAt(flags, i); 478 } 479 480 @Override 481 public int length(int flags) { 482 int pluralOrdinal = (flags & Flags.PLURAL_MASK); 483 return affixesByPlural[pluralOrdinal].length(flags); 484 } 485 486 @Override 487 public boolean positiveHasPlusSign() { 488 return affixesByPlural[StandardPlural.OTHER.ordinal()].positiveHasPlusSign(); 489 } 490 491 @Override 492 public boolean hasNegativeSubpattern() { 493 return affixesByPlural[StandardPlural.OTHER.ordinal()].hasNegativeSubpattern(); 494 } 495 496 @Override 497 public boolean negativeHasMinusSign() { 498 return affixesByPlural[StandardPlural.OTHER.ordinal()].negativeHasMinusSign(); 499 } 500 501 @Override 502 public boolean hasCurrencySign() { 503 return affixesByPlural[StandardPlural.OTHER.ordinal()].hasCurrencySign(); 504 } 505 506 @Override 507 public boolean containsSymbolType(int type) { 508 return affixesByPlural[StandardPlural.OTHER.ordinal()].containsSymbolType(type); 509 } 510 } 511} 512