1 /* GENERATED SOURCE. DO NOT MODIFY. */ 2 // © 2017 and later: Unicode, Inc. and others. 3 // License & terms of use: http://www.unicode.org/copyright.html#License 4 package ohos.global.icu.impl.number; 5 6 import java.math.BigDecimal; 7 8 import ohos.global.icu.impl.StandardPlural; 9 import ohos.global.icu.impl.number.Modifier.Signum; 10 import ohos.global.icu.impl.number.Padder.PadPosition; 11 import ohos.global.icu.number.NumberFormatter.SignDisplay; 12 import ohos.global.icu.text.DecimalFormatSymbols; 13 14 /** 15 * Assorted utilities relating to decimal formatting pattern strings. 16 * @hide exposed on OHOS 17 */ 18 public class PatternStringUtils { 19 20 // Note: the order of fields in this enum matters for parsing. 21 /** 22 * @hide exposed on OHOS 23 */ 24 public static enum PatternSignType { 25 // Render using normal positive subpattern rules 26 POS, 27 // Render using rules to force the display of a plus sign 28 POS_SIGN, 29 // Render using negative subpattern rules 30 NEG; 31 32 public static final PatternSignType[] VALUES = PatternSignType.values(); 33 }; 34 35 /** 36 * Determine whether a given roundingIncrement should be ignored for formatting 37 * based on the current maxFrac value (maximum fraction digits). For example a 38 * roundingIncrement of 0.01 should be ignored if maxFrac is 1, but not if maxFrac 39 * is 2 or more. Note that roundingIncrements are rounded up in significance, so 40 * a roundingIncrement of 0.006 is treated like 0.01 for this determination, i.e. 41 * it should not be ignored if maxFrac is 2 or more (but a roundingIncrement of 42 * 0.005 is treated like 0.001 for significance). 43 * 44 * This test is needed for both NumberPropertyMapper.oldToNew and 45 * PatternStringUtils.propertiesToPatternString, but NumberPropertyMapper 46 * is package-private so we have it here. 47 * 48 * @param roundIncrDec 49 * The roundingIncrement to be checked. Must be non-null. 50 * @param maxFrac 51 * The current maximum fraction digits value. 52 * @return true if roundIncr should be ignored for formatting. 53 */ ignoreRoundingIncrement(BigDecimal roundIncrDec, int maxFrac)54 public static boolean ignoreRoundingIncrement(BigDecimal roundIncrDec, int maxFrac) { 55 double roundIncr = roundIncrDec.doubleValue(); 56 if (roundIncr == 0.0) { 57 return true; 58 } 59 if (maxFrac < 0) { 60 return false; 61 } 62 int frac = 0; 63 roundIncr *= 2.0; // This handles the rounding up of values above e.g. 0.005 or 0.0005 64 for (frac = 0; frac <= maxFrac && roundIncr <= 1.0; frac++, roundIncr *= 10.0); 65 return (frac > maxFrac); 66 } 67 68 /** 69 * Creates a pattern string from a property bag. 70 * 71 * <p> 72 * Since pattern strings support only a subset of the functionality available in a property bag, a 73 * new property bag created from the string returned by this function may not be the same as the 74 * original property bag. 75 * 76 * @param properties 77 * The property bag to serialize. 78 * @return A pattern string approximately serializing the property bag. 79 */ propertiesToPatternString(DecimalFormatProperties properties)80 public static String propertiesToPatternString(DecimalFormatProperties properties) { 81 StringBuilder sb = new StringBuilder(); 82 83 // Convenience references 84 // The Math.min() calls prevent DoS 85 int dosMax = 100; 86 int grouping1 = Math.max(0, Math.min(properties.getGroupingSize(), dosMax)); 87 int grouping2 = Math.max(0, Math.min(properties.getSecondaryGroupingSize(), dosMax)); 88 boolean useGrouping = properties.getGroupingUsed(); 89 int paddingWidth = Math.min(properties.getFormatWidth(), dosMax); 90 PadPosition paddingLocation = properties.getPadPosition(); 91 String paddingString = properties.getPadString(); 92 int minInt = Math.max(0, Math.min(properties.getMinimumIntegerDigits(), dosMax)); 93 int maxInt = Math.min(properties.getMaximumIntegerDigits(), dosMax); 94 int minFrac = Math.max(0, Math.min(properties.getMinimumFractionDigits(), dosMax)); 95 int maxFrac = Math.min(properties.getMaximumFractionDigits(), dosMax); 96 int minSig = Math.min(properties.getMinimumSignificantDigits(), dosMax); 97 int maxSig = Math.min(properties.getMaximumSignificantDigits(), dosMax); 98 boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown(); 99 int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax); 100 boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown(); 101 AffixPatternProvider affixes = PropertiesAffixPatternProvider.forProperties(properties); 102 103 // Prefixes 104 sb.append(affixes.getString(AffixPatternProvider.FLAG_POS_PREFIX)); 105 int afterPrefixPos = sb.length(); 106 107 // Figure out the grouping sizes. 108 if (!useGrouping) { 109 grouping1 = 0; 110 grouping2 = 0; 111 } else if (grouping1 == grouping2) { 112 grouping1 = 0; 113 } 114 int groupingLength = grouping1 + grouping2 + 1; 115 116 // Figure out the digits we need to put in the pattern. 117 BigDecimal roundingInterval = properties.getRoundingIncrement(); 118 StringBuilder digitsString = new StringBuilder(); 119 int digitsStringScale = 0; 120 if (maxSig != Math.min(dosMax, -1)) { 121 // Significant Digits. 122 while (digitsString.length() < minSig) { 123 digitsString.append('@'); 124 } 125 while (digitsString.length() < maxSig) { 126 digitsString.append('#'); 127 } 128 } else if (roundingInterval != null && !ignoreRoundingIncrement(roundingInterval,maxFrac)) { 129 // Rounding Interval. 130 digitsStringScale = -roundingInterval.scale(); 131 // TODO: Check for DoS here? 132 String str = roundingInterval.scaleByPowerOfTen(roundingInterval.scale()).toPlainString(); 133 if (str.charAt(0) == '-') { 134 // TODO: Unsupported operation exception or fail silently? 135 digitsString.append(str, 1, str.length()); 136 } else { 137 digitsString.append(str); 138 } 139 } 140 while (digitsString.length() + digitsStringScale < minInt) { 141 digitsString.insert(0, '0'); 142 } 143 while (-digitsStringScale < minFrac) { 144 digitsString.append('0'); 145 digitsStringScale--; 146 } 147 148 // Write the digits to the string builder 149 int m0 = Math.max(groupingLength, digitsString.length() + digitsStringScale); 150 m0 = (maxInt != dosMax) ? Math.max(maxInt, m0) - 1 : m0 - 1; 151 int mN = (maxFrac != dosMax) ? Math.min(-maxFrac, digitsStringScale) : digitsStringScale; 152 for (int magnitude = m0; magnitude >= mN; magnitude--) { 153 int di = digitsString.length() + digitsStringScale - magnitude - 1; 154 if (di < 0 || di >= digitsString.length()) { 155 sb.append('#'); 156 } else { 157 sb.append(digitsString.charAt(di)); 158 } 159 // Decimal separator 160 if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) { 161 sb.append('.'); 162 } 163 if (!useGrouping) { 164 continue; 165 } 166 // Least-significant grouping separator 167 if (magnitude > 0 && magnitude == grouping1) { 168 sb.append(','); 169 } 170 // All other grouping separators 171 if (magnitude > grouping1 && grouping2 > 0 && (magnitude - grouping1) % grouping2 == 0) { 172 sb.append(','); 173 } 174 } 175 176 // Exponential notation 177 if (exponentDigits != Math.min(dosMax, -1)) { 178 sb.append('E'); 179 if (exponentShowPlusSign) { 180 sb.append('+'); 181 } 182 for (int i = 0; i < exponentDigits; i++) { 183 sb.append('0'); 184 } 185 } 186 187 // Suffixes 188 int beforeSuffixPos = sb.length(); 189 sb.append(affixes.getString(AffixPatternProvider.FLAG_POS_SUFFIX)); 190 191 // Resolve Padding 192 if (paddingWidth > 0) { 193 while (paddingWidth - sb.length() > 0) { 194 sb.insert(afterPrefixPos, '#'); 195 beforeSuffixPos++; 196 } 197 int addedLength; 198 switch (paddingLocation) { 199 case BEFORE_PREFIX: 200 addedLength = PatternStringUtils.escapePaddingString(paddingString, sb, 0); 201 sb.insert(0, '*'); 202 afterPrefixPos += addedLength + 1; 203 beforeSuffixPos += addedLength + 1; 204 break; 205 case AFTER_PREFIX: 206 addedLength = PatternStringUtils.escapePaddingString(paddingString, sb, afterPrefixPos); 207 sb.insert(afterPrefixPos, '*'); 208 afterPrefixPos += addedLength + 1; 209 beforeSuffixPos += addedLength + 1; 210 break; 211 case BEFORE_SUFFIX: 212 PatternStringUtils.escapePaddingString(paddingString, sb, beforeSuffixPos); 213 sb.insert(beforeSuffixPos, '*'); 214 break; 215 case AFTER_SUFFIX: 216 sb.append('*'); 217 PatternStringUtils.escapePaddingString(paddingString, sb, sb.length()); 218 break; 219 } 220 } 221 222 // Negative affixes 223 // Ignore if the negative prefix pattern is "-" and the negative suffix is empty 224 if (affixes.hasNegativeSubpattern()) { 225 sb.append(';'); 226 sb.append(affixes.getString(AffixPatternProvider.FLAG_NEG_PREFIX)); 227 // Copy the positive digit format into the negative. 228 // This is optional; the pattern is the same as if '#' were appended here instead. 229 sb.append(sb, afterPrefixPos, beforeSuffixPos); 230 sb.append(affixes.getString(AffixPatternProvider.FLAG_NEG_SUFFIX)); 231 } 232 233 return sb.toString(); 234 } 235 236 /** @return The number of chars inserted. */ escapePaddingString(CharSequence input, StringBuilder output, int startIndex)237 private static int escapePaddingString(CharSequence input, StringBuilder output, int startIndex) { 238 if (input == null || input.length() == 0) 239 input = Padder.FALLBACK_PADDING_STRING; 240 int startLength = output.length(); 241 if (input.length() == 1) { 242 if (input.equals("'")) { 243 output.insert(startIndex, "''"); 244 } else { 245 output.insert(startIndex, input); 246 } 247 } else { 248 output.insert(startIndex, '\''); 249 int offset = 1; 250 for (int i = 0; i < input.length(); i++) { 251 // it's okay to deal in chars here because the quote mark is the only interesting thing. 252 char ch = input.charAt(i); 253 if (ch == '\'') { 254 output.insert(startIndex + offset, "''"); 255 offset += 2; 256 } else { 257 output.insert(startIndex + offset, ch); 258 offset += 1; 259 } 260 } 261 output.insert(startIndex + offset, '\''); 262 } 263 return output.length() - startLength; 264 } 265 266 /** 267 * Converts a pattern between standard notation and localized notation. Localized notation means that 268 * instead of using generic placeholders in the pattern, you use the corresponding locale-specific 269 * characters instead. For example, in locale <em>fr-FR</em>, the period in the pattern "0.000" means 270 * "decimal" in standard notation (as it does in every other locale), but it means "grouping" in 271 * localized notation. 272 * 273 * <p> 274 * A greedy string-substitution strategy is used to substitute locale symbols. If two symbols are 275 * ambiguous or have the same prefix, the result is not well-defined. 276 * 277 * <p> 278 * Locale symbols are not allowed to contain the ASCII quote character. 279 * 280 * <p> 281 * This method is provided for backwards compatibility and should not be used in any new code. 282 * 283 * @param input 284 * The pattern to convert. 285 * @param symbols 286 * The symbols corresponding to the localized pattern. 287 * @param toLocalized 288 * true to convert from standard to localized notation; false to convert from localized to 289 * standard notation. 290 * @return The pattern expressed in the other notation. 291 */ convertLocalized( String input, DecimalFormatSymbols symbols, boolean toLocalized)292 public static String convertLocalized( 293 String input, 294 DecimalFormatSymbols symbols, 295 boolean toLocalized) { 296 if (input == null) 297 return null; 298 299 // Construct a table of strings to be converted between localized and standard. 300 String[][] table = new String[21][2]; 301 int standIdx = toLocalized ? 0 : 1; 302 int localIdx = toLocalized ? 1 : 0; 303 table[0][standIdx] = "%"; 304 table[0][localIdx] = symbols.getPercentString(); 305 table[1][standIdx] = "‰"; 306 table[1][localIdx] = symbols.getPerMillString(); 307 table[2][standIdx] = "."; 308 table[2][localIdx] = symbols.getDecimalSeparatorString(); 309 table[3][standIdx] = ","; 310 table[3][localIdx] = symbols.getGroupingSeparatorString(); 311 table[4][standIdx] = "-"; 312 table[4][localIdx] = symbols.getMinusSignString(); 313 table[5][standIdx] = "+"; 314 table[5][localIdx] = symbols.getPlusSignString(); 315 table[6][standIdx] = ";"; 316 table[6][localIdx] = Character.toString(symbols.getPatternSeparator()); 317 table[7][standIdx] = "@"; 318 table[7][localIdx] = Character.toString(symbols.getSignificantDigit()); 319 table[8][standIdx] = "E"; 320 table[8][localIdx] = symbols.getExponentSeparator(); 321 table[9][standIdx] = "*"; 322 table[9][localIdx] = Character.toString(symbols.getPadEscape()); 323 table[10][standIdx] = "#"; 324 table[10][localIdx] = Character.toString(symbols.getDigit()); 325 for (int i = 0; i < 10; i++) { 326 table[11 + i][standIdx] = Character.toString((char) ('0' + i)); 327 table[11 + i][localIdx] = symbols.getDigitStringsLocal()[i]; 328 } 329 330 // Special case: quotes are NOT allowed to be in any localIdx strings. 331 // Substitute them with '’' instead. 332 for (int i = 0; i < table.length; i++) { 333 table[i][localIdx] = table[i][localIdx].replace('\'', '’'); 334 } 335 336 // Iterate through the string and convert. 337 // State table: 338 // 0 => base state 339 // 1 => first char inside a quoted sequence in input and output string 340 // 2 => inside a quoted sequence in input and output string 341 // 3 => first char after a close quote in input string; 342 // close quote still needs to be written to output string 343 // 4 => base state in input string; inside quoted sequence in output string 344 // 5 => first char inside a quoted sequence in input string; 345 // inside quoted sequence in output string 346 StringBuilder result = new StringBuilder(); 347 int state = 0; 348 outer: for (int offset = 0; offset < input.length(); offset++) { 349 char ch = input.charAt(offset); 350 351 // Handle a quote character (state shift) 352 if (ch == '\'') { 353 if (state == 0) { 354 result.append('\''); 355 state = 1; 356 continue; 357 } else if (state == 1) { 358 result.append('\''); 359 state = 0; 360 continue; 361 } else if (state == 2) { 362 state = 3; 363 continue; 364 } else if (state == 3) { 365 result.append('\''); 366 result.append('\''); 367 state = 1; 368 continue; 369 } else if (state == 4) { 370 state = 5; 371 continue; 372 } else { 373 assert state == 5; 374 result.append('\''); 375 result.append('\''); 376 state = 4; 377 continue; 378 } 379 } 380 381 if (state == 0 || state == 3 || state == 4) { 382 for (String[] pair : table) { 383 // Perform a greedy match on this symbol string 384 if (input.regionMatches(offset, pair[0], 0, pair[0].length())) { 385 // Skip ahead past this region for the next iteration 386 offset += pair[0].length() - 1; 387 if (state == 3 || state == 4) { 388 result.append('\''); 389 state = 0; 390 } 391 result.append(pair[1]); 392 continue outer; 393 } 394 } 395 // No replacement found. Check if a special quote is necessary 396 for (String[] pair : table) { 397 if (input.regionMatches(offset, pair[1], 0, pair[1].length())) { 398 if (state == 0) { 399 result.append('\''); 400 state = 4; 401 } 402 result.append(ch); 403 continue outer; 404 } 405 } 406 // Still nothing. Copy the char verbatim. (Add a close quote if necessary) 407 if (state == 3 || state == 4) { 408 result.append('\''); 409 state = 0; 410 } 411 result.append(ch); 412 } else { 413 assert state == 1 || state == 2 || state == 5; 414 result.append(ch); 415 state = 2; 416 } 417 } 418 // Resolve final quotes 419 if (state == 3 || state == 4) { 420 result.append('\''); 421 state = 0; 422 } 423 if (state != 0) { 424 throw new IllegalArgumentException("Malformed localized pattern: unterminated quote"); 425 } 426 return result.toString(); 427 } 428 429 /** 430 * This method contains the heart of the logic for rendering LDML affix strings. It handles 431 * sign-always-shown resolution, whether to use the positive or negative subpattern, permille 432 * substitution, and plural forms for CurrencyPluralInfo. 433 */ patternInfoToStringBuilder( AffixPatternProvider patternInfo, boolean isPrefix, PatternSignType patternSignType, StandardPlural plural, boolean perMilleReplacesPercent, StringBuilder output)434 public static void patternInfoToStringBuilder( 435 AffixPatternProvider patternInfo, 436 boolean isPrefix, 437 PatternSignType patternSignType, 438 StandardPlural plural, 439 boolean perMilleReplacesPercent, 440 StringBuilder output) { 441 442 boolean plusReplacesMinusSign = (patternSignType == PatternSignType.POS_SIGN) 443 && !patternInfo.positiveHasPlusSign(); 444 445 // Should we use the affix from the negative subpattern? 446 // (If not, we will use the positive subpattern.) 447 boolean useNegativeAffixPattern = patternInfo.hasNegativeSubpattern() 448 && (patternSignType == PatternSignType.NEG 449 || (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign)); 450 451 // Resolve the flags for the affix pattern. 452 int flags = 0; 453 if (useNegativeAffixPattern) { 454 flags |= AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN; 455 } 456 if (isPrefix) { 457 flags |= AffixPatternProvider.Flags.PREFIX; 458 } 459 if (plural != null) { 460 assert plural.ordinal() == (AffixPatternProvider.Flags.PLURAL_MASK & plural.ordinal()); 461 flags |= plural.ordinal(); 462 } 463 464 // Should we prepend a sign to the pattern? 465 boolean prependSign; 466 if (!isPrefix || useNegativeAffixPattern) { 467 prependSign = false; 468 } else if (patternSignType == PatternSignType.NEG) { 469 prependSign = true; 470 } else { 471 prependSign = plusReplacesMinusSign; 472 } 473 474 // Compute the length of the affix pattern. 475 int length = patternInfo.length(flags) + (prependSign ? 1 : 0); 476 477 // Finally, set the result into the StringBuilder. 478 output.setLength(0); 479 for (int index = 0; index < length; index++) { 480 char candidate; 481 if (prependSign && index == 0) { 482 candidate = '-'; 483 } else if (prependSign) { 484 candidate = patternInfo.charAt(flags, index - 1); 485 } else { 486 candidate = patternInfo.charAt(flags, index); 487 } 488 if (plusReplacesMinusSign && candidate == '-') { 489 candidate = '+'; 490 } 491 if (perMilleReplacesPercent && candidate == '%') { 492 candidate = '‰'; 493 } 494 output.append(candidate); 495 } 496 } 497 resolveSignDisplay(SignDisplay signDisplay, Signum signum)498 public static PatternSignType resolveSignDisplay(SignDisplay signDisplay, Signum signum) { 499 switch (signDisplay) { 500 case AUTO: 501 case ACCOUNTING: 502 switch (signum) { 503 case NEG: 504 case NEG_ZERO: 505 return PatternSignType.NEG; 506 case POS_ZERO: 507 case POS: 508 return PatternSignType.POS; 509 } 510 break; 511 512 case ALWAYS: 513 case ACCOUNTING_ALWAYS: 514 switch (signum) { 515 case NEG: 516 case NEG_ZERO: 517 return PatternSignType.NEG; 518 case POS_ZERO: 519 case POS: 520 return PatternSignType.POS_SIGN; 521 } 522 break; 523 524 case EXCEPT_ZERO: 525 case ACCOUNTING_EXCEPT_ZERO: 526 switch (signum) { 527 case NEG: 528 return PatternSignType.NEG; 529 case NEG_ZERO: 530 case POS_ZERO: 531 return PatternSignType.POS; 532 case POS: 533 return PatternSignType.POS_SIGN; 534 } 535 break; 536 537 case NEVER: 538 return PatternSignType.POS; 539 540 default: 541 break; 542 } 543 544 throw new AssertionError("Unreachable"); 545 } 546 547 } 548