OLD | NEW |
(Empty) | |
| 1 // © 2017 and later: Unicode, Inc. and others. |
| 2 // License & terms of use: http://www.unicode.org/copyright.html#License |
| 3 package com.ibm.icu.impl.number; |
| 4 |
| 5 import java.math.BigDecimal; |
| 6 |
| 7 import com.ibm.icu.impl.number.formatters.PaddingFormat; |
| 8 import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition; |
| 9 import com.ibm.icu.text.DecimalFormatSymbols; |
| 10 |
| 11 /** |
| 12 * Handles parsing and creation of the compact pattern string representation of
a decimal format. |
| 13 */ |
| 14 public class PatternString { |
| 15 |
| 16 /** |
| 17 * Parses a pattern string into a new property bag. |
| 18 * |
| 19 * @param pattern The pattern string, like "#,##0.00" |
| 20 * @param ignoreRounding Whether to leave out rounding information (minFrac, m
axFrac, and rounding |
| 21 * increment) when parsing the pattern. This may be desirable if a custom
rounding mode, such |
| 22 * as CurrencyUsage, is to be used instead. |
| 23 * @return A property bag object. |
| 24 * @throws IllegalArgumentException If there is a syntax error in the pattern
string. |
| 25 */ |
| 26 public static Properties parseToProperties(String pattern, boolean ignoreRound
ing) { |
| 27 Properties properties = new Properties(); |
| 28 LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding); |
| 29 return properties; |
| 30 } |
| 31 |
| 32 public static Properties parseToProperties(String pattern) { |
| 33 return parseToProperties(pattern, false); |
| 34 } |
| 35 |
| 36 /** |
| 37 * Parses a pattern string into an existing property bag. All properties that
can be encoded into |
| 38 * a pattern string will be overwritten with either their default value or wit
h the value coming |
| 39 * from the pattern string. Properties that cannot be encoded into a pattern s
tring, such as |
| 40 * rounding mode, are not modified. |
| 41 * |
| 42 * @param pattern The pattern string, like "#,##0.00" |
| 43 * @param properties The property bag object to overwrite. |
| 44 * @param ignoreRounding Whether to leave out rounding information (minFrac, m
axFrac, and rounding |
| 45 * increment) when parsing the pattern. This may be desirable if a custom
rounding mode, such |
| 46 * as CurrencyUsage, is to be used instead. |
| 47 * @throws IllegalArgumentException If there was a syntax error in the pattern
string. |
| 48 */ |
| 49 public static void parseToExistingProperties( |
| 50 String pattern, Properties properties, boolean ignoreRounding) { |
| 51 LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding); |
| 52 } |
| 53 |
| 54 public static void parseToExistingProperties(String pattern, Properties proper
ties) { |
| 55 parseToExistingProperties(pattern, properties, false); |
| 56 } |
| 57 |
| 58 /** |
| 59 * Creates a pattern string from a property bag. |
| 60 * |
| 61 * <p>Since pattern strings support only a subset of the functionality availab
le in a property |
| 62 * bag, a new property bag created from the string returned by this function m
ay not be the same |
| 63 * as the original property bag. |
| 64 * |
| 65 * @param properties The property bag to serialize. |
| 66 * @return A pattern string approximately serializing the property bag. |
| 67 */ |
| 68 public static String propertiesToString(Properties properties) { |
| 69 StringBuilder sb = new StringBuilder(); |
| 70 |
| 71 // Convenience references |
| 72 // The Math.min() calls prevent DoS |
| 73 int dosMax = 100; |
| 74 int groupingSize = Math.min(properties.getSecondaryGroupingSize(), dosMax); |
| 75 int firstGroupingSize = Math.min(properties.getGroupingSize(), dosMax); |
| 76 int paddingWidth = Math.min(properties.getFormatWidth(), dosMax); |
| 77 PadPosition paddingLocation = properties.getPadPosition(); |
| 78 String paddingString = properties.getPadString(); |
| 79 int minInt = Math.max(Math.min(properties.getMinimumIntegerDigits(), dosMax)
, 0); |
| 80 int maxInt = Math.min(properties.getMaximumIntegerDigits(), dosMax); |
| 81 int minFrac = Math.max(Math.min(properties.getMinimumFractionDigits(), dosMa
x), 0); |
| 82 int maxFrac = Math.min(properties.getMaximumFractionDigits(), dosMax); |
| 83 int minSig = Math.min(properties.getMinimumSignificantDigits(), dosMax); |
| 84 int maxSig = Math.min(properties.getMaximumSignificantDigits(), dosMax); |
| 85 boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown(); |
| 86 int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax)
; |
| 87 boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown(); |
| 88 String pp = properties.getPositivePrefix(); |
| 89 String ppp = properties.getPositivePrefixPattern(); |
| 90 String ps = properties.getPositiveSuffix(); |
| 91 String psp = properties.getPositiveSuffixPattern(); |
| 92 String np = properties.getNegativePrefix(); |
| 93 String npp = properties.getNegativePrefixPattern(); |
| 94 String ns = properties.getNegativeSuffix(); |
| 95 String nsp = properties.getNegativeSuffixPattern(); |
| 96 |
| 97 // Prefixes |
| 98 if (ppp != null) sb.append(ppp); |
| 99 AffixPatternUtils.escape(pp, sb); |
| 100 int afterPrefixPos = sb.length(); |
| 101 |
| 102 // Figure out the grouping sizes. |
| 103 int grouping1, grouping2, grouping; |
| 104 if (groupingSize != Math.min(dosMax, Properties.DEFAULT_SECONDARY_GROUPING_S
IZE) |
| 105 && firstGroupingSize != Math.min(dosMax, Properties.DEFAULT_GROUPING_SIZ
E) |
| 106 && groupingSize != firstGroupingSize) { |
| 107 grouping = groupingSize; |
| 108 grouping1 = groupingSize; |
| 109 grouping2 = firstGroupingSize; |
| 110 } else if (groupingSize != Math.min(dosMax, Properties.DEFAULT_SECONDARY_GRO
UPING_SIZE)) { |
| 111 grouping = groupingSize; |
| 112 grouping1 = 0; |
| 113 grouping2 = groupingSize; |
| 114 } else if (firstGroupingSize != Math.min(dosMax, Properties.DEFAULT_GROUPING
_SIZE)) { |
| 115 grouping = groupingSize; |
| 116 grouping1 = 0; |
| 117 grouping2 = firstGroupingSize; |
| 118 } else { |
| 119 grouping = 0; |
| 120 grouping1 = 0; |
| 121 grouping2 = 0; |
| 122 } |
| 123 int groupingLength = grouping1 + grouping2 + 1; |
| 124 |
| 125 // Figure out the digits we need to put in the pattern. |
| 126 BigDecimal roundingInterval = properties.getRoundingIncrement(); |
| 127 StringBuilder digitsString = new StringBuilder(); |
| 128 int digitsStringScale = 0; |
| 129 if (maxSig != Math.min(dosMax, Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS
)) { |
| 130 // Significant Digits. |
| 131 while (digitsString.length() < minSig) { |
| 132 digitsString.append('@'); |
| 133 } |
| 134 while (digitsString.length() < maxSig) { |
| 135 digitsString.append('#'); |
| 136 } |
| 137 } else if (roundingInterval != Properties.DEFAULT_ROUNDING_INCREMENT) { |
| 138 // Rounding Interval. |
| 139 digitsStringScale = -roundingInterval.scale(); |
| 140 // TODO: Check for DoS here? |
| 141 String str = roundingInterval.scaleByPowerOfTen(roundingInterval.scale()).
toPlainString(); |
| 142 if (str.charAt(0) == '\'') { |
| 143 // TODO: Unsupported operation exception or fail silently? |
| 144 digitsString.append(str, 1, str.length()); |
| 145 } else { |
| 146 digitsString.append(str); |
| 147 } |
| 148 } |
| 149 while (digitsString.length() + digitsStringScale < minInt) { |
| 150 digitsString.insert(0, '0'); |
| 151 } |
| 152 while (-digitsStringScale < minFrac) { |
| 153 digitsString.append('0'); |
| 154 digitsStringScale--; |
| 155 } |
| 156 |
| 157 // Write the digits to the string builder |
| 158 int m0 = Math.max(groupingLength, digitsString.length() + digitsStringScale)
; |
| 159 m0 = (maxInt != dosMax) ? Math.max(maxInt, m0) - 1 : m0 - 1; |
| 160 int mN = (maxFrac != dosMax) ? Math.min(-maxFrac, digitsStringScale) : digit
sStringScale; |
| 161 for (int magnitude = m0; magnitude >= mN; magnitude--) { |
| 162 int di = digitsString.length() + digitsStringScale - magnitude - 1; |
| 163 if (di < 0 || di >= digitsString.length()) { |
| 164 sb.append('#'); |
| 165 } else { |
| 166 sb.append(digitsString.charAt(di)); |
| 167 } |
| 168 if (magnitude > grouping2 && grouping > 0 && (magnitude - grouping2) % gro
uping == 0) { |
| 169 sb.append(','); |
| 170 } else if (magnitude > 0 && magnitude == grouping2) { |
| 171 sb.append(','); |
| 172 } else if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) { |
| 173 sb.append('.'); |
| 174 } |
| 175 } |
| 176 |
| 177 // Exponential notation |
| 178 if (exponentDigits != Math.min(dosMax, Properties.DEFAULT_MINIMUM_EXPONENT_D
IGITS)) { |
| 179 sb.append('E'); |
| 180 if (exponentShowPlusSign) { |
| 181 sb.append('+'); |
| 182 } |
| 183 for (int i = 0; i < exponentDigits; i++) { |
| 184 sb.append('0'); |
| 185 } |
| 186 } |
| 187 |
| 188 // Suffixes |
| 189 int beforeSuffixPos = sb.length(); |
| 190 if (psp != null) sb.append(psp); |
| 191 AffixPatternUtils.escape(ps, sb); |
| 192 |
| 193 // Resolve Padding |
| 194 if (paddingWidth != Properties.DEFAULT_FORMAT_WIDTH) { |
| 195 while (paddingWidth - sb.length() > 0) { |
| 196 sb.insert(afterPrefixPos, '#'); |
| 197 beforeSuffixPos++; |
| 198 } |
| 199 int addedLength; |
| 200 switch (paddingLocation) { |
| 201 case BEFORE_PREFIX: |
| 202 addedLength = escapePaddingString(paddingString, sb, 0); |
| 203 sb.insert(0, '*'); |
| 204 afterPrefixPos += addedLength + 1; |
| 205 beforeSuffixPos += addedLength + 1; |
| 206 break; |
| 207 case AFTER_PREFIX: |
| 208 addedLength = escapePaddingString(paddingString, sb, afterPrefixPos); |
| 209 sb.insert(afterPrefixPos, '*'); |
| 210 afterPrefixPos += addedLength + 1; |
| 211 beforeSuffixPos += addedLength + 1; |
| 212 break; |
| 213 case BEFORE_SUFFIX: |
| 214 escapePaddingString(paddingString, sb, beforeSuffixPos); |
| 215 sb.insert(beforeSuffixPos, '*'); |
| 216 break; |
| 217 case AFTER_SUFFIX: |
| 218 sb.append('*'); |
| 219 escapePaddingString(paddingString, sb, sb.length()); |
| 220 break; |
| 221 } |
| 222 } |
| 223 |
| 224 // Negative affixes |
| 225 // Ignore if the negative prefix pattern is "-" and the negative suffix is e
mpty |
| 226 if (np != null |
| 227 || ns != null |
| 228 || (npp == null && nsp != null) |
| 229 || (npp != null && (npp.length() != 1 || npp.charAt(0) != '-' || nsp.len
gth() != 0))) { |
| 230 sb.append(';'); |
| 231 if (npp != null) sb.append(npp); |
| 232 AffixPatternUtils.escape(np, sb); |
| 233 // Copy the positive digit format into the negative. |
| 234 // This is optional; the pattern is the same as if '#' were appended here
instead. |
| 235 sb.append(sb, afterPrefixPos, beforeSuffixPos); |
| 236 if (nsp != null) sb.append(nsp); |
| 237 AffixPatternUtils.escape(ns, sb); |
| 238 } |
| 239 |
| 240 return sb.toString(); |
| 241 } |
| 242 |
| 243 /** @return The number of chars inserted. */ |
| 244 private static int escapePaddingString(CharSequence input, StringBuilder outpu
t, int startIndex) { |
| 245 if (input == null || input.length() == 0) input = PaddingFormat.FALLBACK_PAD
DING_STRING; |
| 246 int startLength = output.length(); |
| 247 if (input.length() == 1) { |
| 248 if (input.equals("'")) { |
| 249 output.insert(startIndex, "''"); |
| 250 } else { |
| 251 output.insert(startIndex, input); |
| 252 } |
| 253 } else { |
| 254 output.insert(startIndex, '\''); |
| 255 int offset = 1; |
| 256 for (int i = 0; i < input.length(); i++) { |
| 257 // it's okay to deal in chars here because the quote mark is the only in
teresting thing. |
| 258 char ch = input.charAt(i); |
| 259 if (ch == '\'') { |
| 260 output.insert(startIndex + offset, "''"); |
| 261 offset += 2; |
| 262 } else { |
| 263 output.insert(startIndex + offset, ch); |
| 264 offset += 1; |
| 265 } |
| 266 } |
| 267 output.insert(startIndex + offset, '\''); |
| 268 } |
| 269 return output.length() - startLength; |
| 270 } |
| 271 |
| 272 /** |
| 273 * Converts a pattern between standard notation and localized notation. Locali
zed notation means |
| 274 * that instead of using generic placeholders in the pattern, you use the corr
esponding |
| 275 * locale-specific characters instead. For example, in locale <em>fr-FR</em>,
the period in the |
| 276 * pattern "0.000" means "decimal" in standard notation (as it does in every o
ther locale), but it |
| 277 * means "grouping" in localized notation. |
| 278 * |
| 279 * @param input The pattern to convert. |
| 280 * @param symbols The symbols corresponding to the localized pattern. |
| 281 * @param toLocalized true to convert from standard to localized notation; fal
se to convert from |
| 282 * localized to standard notation. |
| 283 * @return The pattern expressed in the other notation. |
| 284 * @deprecated ICU 59 This method is provided for backwards compatibility and
should not be used |
| 285 * in any new code. |
| 286 */ |
| 287 @Deprecated |
| 288 public static String convertLocalized( |
| 289 CharSequence input, DecimalFormatSymbols symbols, boolean toLocalized) { |
| 290 if (input == null) return null; |
| 291 |
| 292 /// This is not the prettiest function in the world, but it gets the job don
e. /// |
| 293 |
| 294 // Construct a table of code points to be converted between localized and st
andard. |
| 295 int[][] table = new int[6][2]; |
| 296 int standIdx = toLocalized ? 0 : 1; |
| 297 int localIdx = toLocalized ? 1 : 0; |
| 298 table[0][standIdx] = '%'; |
| 299 table[0][localIdx] = symbols.getPercent(); |
| 300 table[1][standIdx] = '‰'; |
| 301 table[1][localIdx] = symbols.getPerMill(); |
| 302 table[2][standIdx] = '.'; |
| 303 table[2][localIdx] = symbols.getDecimalSeparator(); |
| 304 table[3][standIdx] = ','; |
| 305 table[3][localIdx] = symbols.getGroupingSeparator(); |
| 306 table[4][standIdx] = '-'; |
| 307 table[4][localIdx] = symbols.getMinusSign(); |
| 308 table[5][standIdx] = '+'; |
| 309 table[5][localIdx] = symbols.getPlusSign(); |
| 310 |
| 311 // Special case: localIdx characters are NOT allowed to be quotes, like in d
e_CH. |
| 312 // Use '’' instead. |
| 313 for (int i = 0; i < table.length; i++) { |
| 314 if (table[i][localIdx] == '\'') { |
| 315 table[i][localIdx] = '’'; |
| 316 } |
| 317 } |
| 318 |
| 319 // Iterate through the string and convert |
| 320 int offset = 0; |
| 321 int state = 0; |
| 322 StringBuilder result = new StringBuilder(); |
| 323 for (; offset < input.length(); ) { |
| 324 int cp = Character.codePointAt(input, offset); |
| 325 int cpToAppend = cp; |
| 326 |
| 327 if (state == 1 || state == 3 || state == 4) { |
| 328 // Inside user-specified quote |
| 329 if (cp == '\'') { |
| 330 if (state == 1) { |
| 331 state = 0; |
| 332 } else if (state == 3) { |
| 333 state = 2; |
| 334 cpToAppend = -1; |
| 335 } else { |
| 336 state = 2; |
| 337 } |
| 338 } |
| 339 } else { |
| 340 // Base state or inside special character quote |
| 341 if (cp == '\'') { |
| 342 if (state == 2 && offset + 1 < input.length()) { |
| 343 int nextCp = Character.codePointAt(input, offset + 1); |
| 344 if (nextCp == '\'') { |
| 345 // escaped quote |
| 346 state = 4; |
| 347 } else { |
| 348 // begin user-specified quote sequence |
| 349 // we are already in a quote sequence, so omit the opening quote |
| 350 state = 3; |
| 351 cpToAppend = -1; |
| 352 } |
| 353 } else { |
| 354 state = 1; |
| 355 } |
| 356 } else { |
| 357 boolean needsSpecialQuote = false; |
| 358 for (int i = 0; i < table.length; i++) { |
| 359 if (table[i][0] == cp) { |
| 360 cpToAppend = table[i][1]; |
| 361 needsSpecialQuote = false; // in case an earlier translation trigg
ered it |
| 362 break; |
| 363 } else if (table[i][1] == cp) { |
| 364 needsSpecialQuote = true; |
| 365 } |
| 366 } |
| 367 if (state == 0 && needsSpecialQuote) { |
| 368 state = 2; |
| 369 result.appendCodePoint('\''); |
| 370 } else if (state == 2 && !needsSpecialQuote) { |
| 371 state = 0; |
| 372 result.appendCodePoint('\''); |
| 373 } |
| 374 } |
| 375 } |
| 376 if (cpToAppend != -1) { |
| 377 result.appendCodePoint(cpToAppend); |
| 378 } |
| 379 offset += Character.charCount(cp); |
| 380 } |
| 381 if (state == 2) { |
| 382 result.appendCodePoint('\''); |
| 383 } |
| 384 return result.toString(); |
| 385 } |
| 386 |
| 387 /** Implements a recursive descent parser for decimal format patterns. */ |
| 388 static class LdmlDecimalPatternParser { |
| 389 |
| 390 /** |
| 391 * An internal, intermediate data structure used for storing parse results b
efore they are |
| 392 * finalized into a DecimalFormatPattern.Builder. |
| 393 */ |
| 394 private static class PatternParseResult { |
| 395 SubpatternParseResult positive = new SubpatternParseResult(); |
| 396 SubpatternParseResult negative = null; |
| 397 |
| 398 /** Finalizes the temporary data stored in the PatternParseResult to the B
uilder. */ |
| 399 void saveToProperties(Properties properties, boolean ignoreRounding) { |
| 400 // Translate from PatternState to Properties. |
| 401 // Note that most data from "negative" is ignored per the specification
of DecimalFormat. |
| 402 |
| 403 // Grouping settings |
| 404 if (positive.groupingSizes[1] != -1) { |
| 405 properties.setGroupingSize(positive.groupingSizes[0]); |
| 406 } else { |
| 407 properties.setGroupingSize(Properties.DEFAULT_GROUPING_SIZE); |
| 408 } |
| 409 if (positive.groupingSizes[2] != -1) { |
| 410 properties.setSecondaryGroupingSize(positive.groupingSizes[1]); |
| 411 } else { |
| 412 properties.setSecondaryGroupingSize(Properties.DEFAULT_SECONDARY_GROUP
ING_SIZE); |
| 413 } |
| 414 |
| 415 // For backwards compatibility, require that the pattern emit at least o
ne min digit. |
| 416 int minInt, minFrac; |
| 417 if (positive.totalIntegerDigits == 0 && positive.maximumFractionDigits >
0) { |
| 418 // patterns like ".##" |
| 419 minInt = 0; |
| 420 minFrac = Math.max(1, positive.minimumFractionDigits); |
| 421 } else if (positive.minimumIntegerDigits == 0 && positive.minimumFractio
nDigits == 0) { |
| 422 // patterns like "#.##" |
| 423 minInt = 1; |
| 424 minFrac = 0; |
| 425 } else { |
| 426 minInt = positive.minimumIntegerDigits; |
| 427 minFrac = positive.minimumFractionDigits; |
| 428 } |
| 429 |
| 430 // Rounding settings |
| 431 // Don't set basic rounding when there is a currency sign; defer to Curr
encyUsage |
| 432 if (positive.minimumSignificantDigits > 0) { |
| 433 properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTIO
N_DIGITS); |
| 434 properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTIO
N_DIGITS); |
| 435 properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT)
; |
| 436 properties.setMinimumSignificantDigits(positive.minimumSignificantDigi
ts); |
| 437 properties.setMaximumSignificantDigits(positive.maximumSignificantDigi
ts); |
| 438 } else if (!positive.rounding.isZero()) { |
| 439 if (!ignoreRounding) { |
| 440 properties.setMinimumFractionDigits(minFrac); |
| 441 properties.setMaximumFractionDigits(positive.maximumFractionDigits); |
| 442 properties.setRoundingIncrement(positive.rounding.toBigDecimal()); |
| 443 } else { |
| 444 properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACT
ION_DIGITS); |
| 445 properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACT
ION_DIGITS); |
| 446 properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMEN
T); |
| 447 } |
| 448 properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGN
IFICANT_DIGITS); |
| 449 properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGN
IFICANT_DIGITS); |
| 450 } else { |
| 451 if (!ignoreRounding) { |
| 452 properties.setMinimumFractionDigits(minFrac); |
| 453 properties.setMaximumFractionDigits(positive.maximumFractionDigits); |
| 454 properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMEN
T); |
| 455 } else { |
| 456 properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACT
ION_DIGITS); |
| 457 properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACT
ION_DIGITS); |
| 458 properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMEN
T); |
| 459 } |
| 460 properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGN
IFICANT_DIGITS); |
| 461 properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGN
IFICANT_DIGITS); |
| 462 } |
| 463 |
| 464 // If the pattern ends with a '.' then force the decimal point. |
| 465 if (positive.hasDecimal && positive.maximumFractionDigits == 0) { |
| 466 properties.setDecimalSeparatorAlwaysShown(true); |
| 467 } else { |
| 468 properties.setDecimalSeparatorAlwaysShown(false); |
| 469 } |
| 470 |
| 471 // Scientific notation settings |
| 472 if (positive.exponentDigits > 0) { |
| 473 properties.setExponentSignAlwaysShown(positive.exponentShowPlusSign); |
| 474 properties.setMinimumExponentDigits(positive.exponentDigits); |
| 475 if (positive.minimumSignificantDigits == 0) { |
| 476 // patterns without '@' can define max integer digits, used for engi
neering notation |
| 477 properties.setMinimumIntegerDigits(positive.minimumIntegerDigits); |
| 478 properties.setMaximumIntegerDigits(positive.totalIntegerDigits); |
| 479 } else { |
| 480 // patterns with '@' cannot define max integer digits |
| 481 properties.setMinimumIntegerDigits(1); |
| 482 properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGE
R_DIGITS); |
| 483 } |
| 484 } else { |
| 485 properties.setExponentSignAlwaysShown(Properties.DEFAULT_EXPONENT_SIGN
_ALWAYS_SHOWN); |
| 486 properties.setMinimumExponentDigits(Properties.DEFAULT_MINIMUM_EXPONEN
T_DIGITS); |
| 487 properties.setMinimumIntegerDigits(minInt); |
| 488 properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_
DIGITS); |
| 489 } |
| 490 |
| 491 // Padding settings |
| 492 if (positive.padding.length() > 0) { |
| 493 // The width of the positive prefix and suffix templates are included
in the padding |
| 494 int paddingWidth = |
| 495 positive.paddingWidth |
| 496 + AffixPatternUtils.unescapedLength(positive.prefix) |
| 497 + AffixPatternUtils.unescapedLength(positive.suffix); |
| 498 properties.setFormatWidth(paddingWidth); |
| 499 if (positive.padding.length() == 1) { |
| 500 properties.setPadString(positive.padding.toString()); |
| 501 } else if (positive.padding.length() == 2) { |
| 502 if (positive.padding.charAt(0) == '\'') { |
| 503 properties.setPadString("'"); |
| 504 } else { |
| 505 properties.setPadString(positive.padding.toString()); |
| 506 } |
| 507 } else { |
| 508 properties.setPadString( |
| 509 positive.padding.subSequence(1, positive.padding.length() - 1).t
oString()); |
| 510 } |
| 511 assert positive.paddingLocation != null; |
| 512 properties.setPadPosition(positive.paddingLocation); |
| 513 } else { |
| 514 properties.setFormatWidth(Properties.DEFAULT_FORMAT_WIDTH); |
| 515 properties.setPadString(Properties.DEFAULT_PAD_STRING); |
| 516 properties.setPadPosition(Properties.DEFAULT_PAD_POSITION); |
| 517 } |
| 518 |
| 519 // Set the affixes |
| 520 // Always call the setter, even if the prefixes are empty, especially in
the case of the |
| 521 // negative prefix pattern, to prevent default values from overriding th
e pattern. |
| 522 properties.setPositivePrefixPattern(positive.prefix.toString()); |
| 523 properties.setPositiveSuffixPattern(positive.suffix.toString()); |
| 524 if (negative != null) { |
| 525 properties.setNegativePrefixPattern(negative.prefix.toString()); |
| 526 properties.setNegativeSuffixPattern(negative.suffix.toString()); |
| 527 } else { |
| 528 properties.setNegativePrefixPattern(null); |
| 529 properties.setNegativeSuffixPattern(null); |
| 530 } |
| 531 |
| 532 // Set the magnitude multiplier |
| 533 if (positive.hasPercentSign) { |
| 534 properties.setMagnitudeMultiplier(2); |
| 535 } else if (positive.hasPerMilleSign) { |
| 536 properties.setMagnitudeMultiplier(3); |
| 537 } else { |
| 538 properties.setMagnitudeMultiplier(Properties.DEFAULT_MAGNITUDE_MULTIPL
IER); |
| 539 } |
| 540 } |
| 541 } |
| 542 |
| 543 private static class SubpatternParseResult { |
| 544 int[] groupingSizes = new int[] {0, -1, -1}; |
| 545 int minimumIntegerDigits = 0; |
| 546 int totalIntegerDigits = 0; |
| 547 int minimumFractionDigits = 0; |
| 548 int maximumFractionDigits = 0; |
| 549 int minimumSignificantDigits = 0; |
| 550 int maximumSignificantDigits = 0; |
| 551 boolean hasDecimal = false; |
| 552 int paddingWidth = 0; |
| 553 PadPosition paddingLocation = null; |
| 554 FormatQuantity4 rounding = new FormatQuantity4(); |
| 555 boolean exponentShowPlusSign = false; |
| 556 int exponentDigits = 0; |
| 557 boolean hasPercentSign = false; |
| 558 boolean hasPerMilleSign = false; |
| 559 boolean hasCurrencySign = false; |
| 560 |
| 561 StringBuilder padding = new StringBuilder(); |
| 562 StringBuilder prefix = new StringBuilder(); |
| 563 StringBuilder suffix = new StringBuilder(); |
| 564 } |
| 565 |
| 566 /** An internal class used for tracking the cursor during parsing of a patte
rn string. */ |
| 567 private static class ParserState { |
| 568 final String pattern; |
| 569 int offset; |
| 570 |
| 571 ParserState(String pattern) { |
| 572 this.pattern = pattern; |
| 573 this.offset = 0; |
| 574 } |
| 575 |
| 576 int peek() { |
| 577 if (offset == pattern.length()) { |
| 578 return -1; |
| 579 } else { |
| 580 return pattern.codePointAt(offset); |
| 581 } |
| 582 } |
| 583 |
| 584 int next() { |
| 585 int codePoint = peek(); |
| 586 offset += Character.charCount(codePoint); |
| 587 return codePoint; |
| 588 } |
| 589 |
| 590 IllegalArgumentException toParseException(String message) { |
| 591 StringBuilder sb = new StringBuilder(); |
| 592 sb.append("Unexpected character in decimal format pattern: '"); |
| 593 sb.append(pattern); |
| 594 sb.append("': "); |
| 595 sb.append(message); |
| 596 sb.append(": "); |
| 597 if (peek() == -1) { |
| 598 sb.append("EOL"); |
| 599 } else { |
| 600 sb.append("'"); |
| 601 sb.append(Character.toChars(peek())); |
| 602 sb.append("'"); |
| 603 } |
| 604 return new IllegalArgumentException(sb.toString()); |
| 605 } |
| 606 } |
| 607 |
| 608 static void parse(String pattern, Properties properties, boolean ignoreRound
ing) { |
| 609 if (pattern == null || pattern.length() == 0) { |
| 610 // Backwards compatibility requires that we reset to the default values. |
| 611 // TODO: Only overwrite the properties that "saveToProperties" normally
touches? |
| 612 properties.clear(); |
| 613 return; |
| 614 } |
| 615 |
| 616 // TODO: Use whitespace characters from PatternProps |
| 617 // TODO: Use thread locals here. |
| 618 ParserState state = new ParserState(pattern); |
| 619 PatternParseResult result = new PatternParseResult(); |
| 620 consumePattern(state, result); |
| 621 result.saveToProperties(properties, ignoreRounding); |
| 622 } |
| 623 |
| 624 private static void consumePattern(ParserState state, PatternParseResult res
ult) { |
| 625 // pattern := subpattern (';' subpattern)? |
| 626 consumeSubpattern(state, result.positive); |
| 627 if (state.peek() == ';') { |
| 628 state.next(); // consume the ';' |
| 629 result.negative = new SubpatternParseResult(); |
| 630 consumeSubpattern(state, result.negative); |
| 631 } |
| 632 if (state.peek() != -1) { |
| 633 throw state.toParseException("pattern"); |
| 634 } |
| 635 } |
| 636 |
| 637 private static void consumeSubpattern(ParserState state, SubpatternParseResu
lt result) { |
| 638 // subpattern := literals? number exponent? literals? |
| 639 consumePadding(state, result, PadPosition.BEFORE_PREFIX); |
| 640 consumeAffix(state, result, result.prefix); |
| 641 consumePadding(state, result, PadPosition.AFTER_PREFIX); |
| 642 consumeFormat(state, result); |
| 643 consumeExponent(state, result); |
| 644 consumePadding(state, result, PadPosition.BEFORE_SUFFIX); |
| 645 consumeAffix(state, result, result.suffix); |
| 646 consumePadding(state, result, PadPosition.AFTER_SUFFIX); |
| 647 } |
| 648 |
| 649 private static void consumePadding( |
| 650 ParserState state, SubpatternParseResult result, PadPosition paddingLoca
tion) { |
| 651 if (state.peek() != '*') { |
| 652 return; |
| 653 } |
| 654 result.paddingLocation = paddingLocation; |
| 655 state.next(); // consume the '*' |
| 656 consumeLiteral(state, result.padding); |
| 657 } |
| 658 |
| 659 private static void consumeAffix( |
| 660 ParserState state, SubpatternParseResult result, StringBuilder destinati
on) { |
| 661 // literals := { literal } |
| 662 while (true) { |
| 663 switch (state.peek()) { |
| 664 case '#': |
| 665 case '@': |
| 666 case ';': |
| 667 case '*': |
| 668 case '.': |
| 669 case ',': |
| 670 case '0': |
| 671 case '1': |
| 672 case '2': |
| 673 case '3': |
| 674 case '4': |
| 675 case '5': |
| 676 case '6': |
| 677 case '7': |
| 678 case '8': |
| 679 case '9': |
| 680 case -1: |
| 681 // Characters that cannot appear unquoted in a literal |
| 682 return; |
| 683 |
| 684 case '%': |
| 685 result.hasPercentSign = true; |
| 686 break; |
| 687 |
| 688 case '‰': |
| 689 result.hasPerMilleSign = true; |
| 690 break; |
| 691 |
| 692 case '¤': |
| 693 result.hasCurrencySign = true; |
| 694 break; |
| 695 } |
| 696 consumeLiteral(state, destination); |
| 697 } |
| 698 } |
| 699 |
| 700 private static void consumeLiteral(ParserState state, StringBuilder destinat
ion) { |
| 701 if (state.peek() == -1) { |
| 702 throw state.toParseException("expected unquoted literal but found end of
string"); |
| 703 } else if (state.peek() == '\'') { |
| 704 destination.appendCodePoint(state.next()); // consume the starting quote |
| 705 while (state.peek() != '\'') { |
| 706 if (state.peek() == -1) { |
| 707 throw state.toParseException("expected quoted literal but found end
of string"); |
| 708 } else { |
| 709 destination.appendCodePoint(state.next()); // consume a quoted chara
cter |
| 710 } |
| 711 } |
| 712 destination.appendCodePoint(state.next()); // consume the ending quote |
| 713 } else { |
| 714 // consume a non-quoted literal character |
| 715 destination.appendCodePoint(state.next()); |
| 716 } |
| 717 } |
| 718 |
| 719 private static void consumeFormat(ParserState state, SubpatternParseResult r
esult) { |
| 720 consumeIntegerFormat(state, result); |
| 721 if (state.peek() == '.') { |
| 722 state.next(); // consume the decimal point |
| 723 result.hasDecimal = true; |
| 724 result.paddingWidth += 1; |
| 725 consumeFractionFormat(state, result); |
| 726 } |
| 727 } |
| 728 |
| 729 private static void consumeIntegerFormat(ParserState state, SubpatternParseR
esult result) { |
| 730 boolean seenSignificantDigitMarker = false; |
| 731 boolean seenDigit = false; |
| 732 |
| 733 while (true) { |
| 734 switch (state.peek()) { |
| 735 case ',': |
| 736 result.paddingWidth += 1; |
| 737 result.groupingSizes[2] = result.groupingSizes[1]; |
| 738 result.groupingSizes[1] = result.groupingSizes[0]; |
| 739 result.groupingSizes[0] = 0; |
| 740 break; |
| 741 |
| 742 case '#': |
| 743 if (seenDigit) throw state.toParseException("# cannot follow 0 befor
e decimal point"); |
| 744 result.paddingWidth += 1; |
| 745 result.groupingSizes[0] += 1; |
| 746 result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1); |
| 747 // no change to result.minimumIntegerDigits |
| 748 // no change to result.minimumSignificantDigits |
| 749 result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 :
0); |
| 750 result.rounding.appendDigit((byte) 0, 0, true); |
| 751 break; |
| 752 |
| 753 case '@': |
| 754 seenSignificantDigitMarker = true; |
| 755 if (seenDigit) throw state.toParseException("Can't mix @ and 0 in pa
ttern"); |
| 756 result.paddingWidth += 1; |
| 757 result.groupingSizes[0] += 1; |
| 758 result.totalIntegerDigits += 1; |
| 759 // no change to result.minimumIntegerDigits |
| 760 result.minimumSignificantDigits += 1; |
| 761 result.maximumSignificantDigits += 1; |
| 762 result.rounding.appendDigit((byte) 0, 0, true); |
| 763 break; |
| 764 |
| 765 case '0': |
| 766 case '1': |
| 767 case '2': |
| 768 case '3': |
| 769 case '4': |
| 770 case '5': |
| 771 case '6': |
| 772 case '7': |
| 773 case '8': |
| 774 case '9': |
| 775 seenDigit = true; |
| 776 if (seenSignificantDigitMarker) |
| 777 throw state.toParseException("Can't mix @ and 0 in pattern"); |
| 778 // TODO: Crash here if we've seen the significant digit marker? See
NumberFormatTestCases.txt |
| 779 result.paddingWidth += 1; |
| 780 result.groupingSizes[0] += 1; |
| 781 result.totalIntegerDigits += 1; |
| 782 result.minimumIntegerDigits += 1; |
| 783 // no change to result.minimumSignificantDigits |
| 784 result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 :
0); |
| 785 result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true); |
| 786 break; |
| 787 |
| 788 default: |
| 789 return; |
| 790 } |
| 791 state.next(); // consume the symbol |
| 792 } |
| 793 } |
| 794 |
| 795 private static void consumeFractionFormat(ParserState state, SubpatternParse
Result result) { |
| 796 int zeroCounter = 0; |
| 797 boolean seenHash = false; |
| 798 while (true) { |
| 799 switch (state.peek()) { |
| 800 case '#': |
| 801 seenHash = true; |
| 802 result.paddingWidth += 1; |
| 803 // no change to result.minimumFractionDigits |
| 804 result.maximumFractionDigits += 1; |
| 805 zeroCounter++; |
| 806 break; |
| 807 |
| 808 case '0': |
| 809 case '1': |
| 810 case '2': |
| 811 case '3': |
| 812 case '4': |
| 813 case '5': |
| 814 case '6': |
| 815 case '7': |
| 816 case '8': |
| 817 case '9': |
| 818 if (seenHash) throw state.toParseException("0 cannot follow # after
decimal point"); |
| 819 result.paddingWidth += 1; |
| 820 result.minimumFractionDigits += 1; |
| 821 result.maximumFractionDigits += 1; |
| 822 if (state.peek() == '0') { |
| 823 zeroCounter++; |
| 824 } else { |
| 825 result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCount
er, false); |
| 826 zeroCounter = 0; |
| 827 } |
| 828 break; |
| 829 |
| 830 default: |
| 831 return; |
| 832 } |
| 833 state.next(); // consume the symbol |
| 834 } |
| 835 } |
| 836 |
| 837 private static void consumeExponent(ParserState state, SubpatternParseResult
result) { |
| 838 if (state.peek() != 'E') { |
| 839 return; |
| 840 } |
| 841 state.next(); // consume the E |
| 842 result.paddingWidth++; |
| 843 if (state.peek() == '+') { |
| 844 state.next(); // consume the + |
| 845 result.exponentShowPlusSign = true; |
| 846 result.paddingWidth++; |
| 847 } |
| 848 while (state.peek() == '0') { |
| 849 state.next(); // consume the 0 |
| 850 result.exponentDigits += 1; |
| 851 result.paddingWidth++; |
| 852 } |
| 853 } |
| 854 } |
| 855 } |
OLD | NEW |