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 android.icu.impl.number; 5 6 import java.math.BigDecimal; 7 8 import android.icu.impl.number.Padder.PadPosition; 9 import android.icu.text.DecimalFormatSymbols; 10 11 /** 12 * Assorted utilities relating to decimal formatting pattern strings. 13 * @hide Only a subset of ICU is exposed in Android 14 */ 15 public class PatternStringUtils { 16 17 /** 18 * Creates a pattern string from a property bag. 19 * 20 * <p> 21 * Since pattern strings support only a subset of the functionality available in a property bag, a new property bag 22 * created from the string returned by this function may not be the same as the original property bag. 23 * 24 * @param properties 25 * The property bag to serialize. 26 * @return A pattern string approximately serializing the property bag. 27 */ 28 public static String propertiesToPatternString(DecimalFormatProperties properties) { 29 StringBuilder sb = new StringBuilder(); 30 31 // Convenience references 32 // The Math.min() calls prevent DoS 33 int dosMax = 100; 34 int groupingSize = Math.min(properties.getSecondaryGroupingSize(), dosMax); 35 int firstGroupingSize = Math.min(properties.getGroupingSize(), dosMax); 36 int paddingWidth = Math.min(properties.getFormatWidth(), dosMax); 37 PadPosition paddingLocation = properties.getPadPosition(); 38 String paddingString = properties.getPadString(); 39 int minInt = Math.max(Math.min(properties.getMinimumIntegerDigits(), dosMax), 0); 40 int maxInt = Math.min(properties.getMaximumIntegerDigits(), dosMax); 41 int minFrac = Math.max(Math.min(properties.getMinimumFractionDigits(), dosMax), 0); 42 int maxFrac = Math.min(properties.getMaximumFractionDigits(), dosMax); 43 int minSig = Math.min(properties.getMinimumSignificantDigits(), dosMax); 44 int maxSig = Math.min(properties.getMaximumSignificantDigits(), dosMax); 45 boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown(); 46 int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax); 47 boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown(); 48 String pp = properties.getPositivePrefix(); 49 String ppp = properties.getPositivePrefixPattern(); 50 String ps = properties.getPositiveSuffix(); 51 String psp = properties.getPositiveSuffixPattern(); 52 String np = properties.getNegativePrefix(); 53 String npp = properties.getNegativePrefixPattern(); 54 String ns = properties.getNegativeSuffix(); 55 String nsp = properties.getNegativeSuffixPattern(); 56 57 // Prefixes 58 if (ppp != null) { 59 sb.append(ppp); 60 } 61 AffixUtils.escape(pp, sb); 62 int afterPrefixPos = sb.length(); 63 64 // Figure out the grouping sizes. 65 int grouping1, grouping2, grouping; 66 if (groupingSize != Math.min(dosMax, -1) && firstGroupingSize != Math.min(dosMax, -1) 67 && groupingSize != firstGroupingSize) { 68 grouping = groupingSize; 69 grouping1 = groupingSize; 70 grouping2 = firstGroupingSize; 71 } else if (groupingSize != Math.min(dosMax, -1)) { 72 grouping = groupingSize; 73 grouping1 = 0; 74 grouping2 = groupingSize; 75 } else if (firstGroupingSize != Math.min(dosMax, -1)) { 76 grouping = groupingSize; 77 grouping1 = 0; 78 grouping2 = firstGroupingSize; 79 } else { 80 grouping = 0; 81 grouping1 = 0; 82 grouping2 = 0; 83 } 84 int groupingLength = grouping1 + grouping2 + 1; 85 86 // Figure out the digits we need to put in the pattern. 87 BigDecimal roundingInterval = properties.getRoundingIncrement(); 88 StringBuilder digitsString = new StringBuilder(); 89 int digitsStringScale = 0; 90 if (maxSig != Math.min(dosMax, -1)) { 91 // Significant Digits. 92 while (digitsString.length() < minSig) { 93 digitsString.append('@'); 94 } 95 while (digitsString.length() < maxSig) { 96 digitsString.append('#'); 97 } 98 } else if (roundingInterval != null) { 99 // Rounding Interval. 100 digitsStringScale = -roundingInterval.scale(); 101 // TODO: Check for DoS here? 102 String str = roundingInterval.scaleByPowerOfTen(roundingInterval.scale()).toPlainString(); 103 if (str.charAt(0) == '-') { 104 // TODO: Unsupported operation exception or fail silently? 105 digitsString.append(str, 1, str.length()); 106 } else { 107 digitsString.append(str); 108 } 109 } 110 while (digitsString.length() + digitsStringScale < minInt) { 111 digitsString.insert(0, '0'); 112 } 113 while (-digitsStringScale < minFrac) { 114 digitsString.append('0'); 115 digitsStringScale--; 116 } 117 118 // Write the digits to the string builder 119 int m0 = Math.max(groupingLength, digitsString.length() + digitsStringScale); 120 m0 = (maxInt != dosMax) ? Math.max(maxInt, m0) - 1 : m0 - 1; 121 int mN = (maxFrac != dosMax) ? Math.min(-maxFrac, digitsStringScale) : digitsStringScale; 122 for (int magnitude = m0; magnitude >= mN; magnitude--) { 123 int di = digitsString.length() + digitsStringScale - magnitude - 1; 124 if (di < 0 || di >= digitsString.length()) { 125 sb.append('#'); 126 } else { 127 sb.append(digitsString.charAt(di)); 128 } 129 if (magnitude > grouping2 && grouping > 0 && (magnitude - grouping2) % grouping == 0) { 130 sb.append(','); 131 } else if (magnitude > 0 && magnitude == grouping2) { 132 sb.append(','); 133 } else if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) { 134 sb.append('.'); 135 } 136 } 137 138 // Exponential notation 139 if (exponentDigits != Math.min(dosMax, -1)) { 140 sb.append('E'); 141 if (exponentShowPlusSign) { 142 sb.append('+'); 143 } 144 for (int i = 0; i < exponentDigits; i++) { 145 sb.append('0'); 146 } 147 } 148 149 // Suffixes 150 int beforeSuffixPos = sb.length(); 151 if (psp != null) { 152 sb.append(psp); 153 } 154 AffixUtils.escape(ps, sb); 155 156 // Resolve Padding 157 if (paddingWidth != -1) { 158 while (paddingWidth - sb.length() > 0) { 159 sb.insert(afterPrefixPos, '#'); 160 beforeSuffixPos++; 161 } 162 int addedLength; 163 switch (paddingLocation) { 164 case BEFORE_PREFIX: 165 addedLength = PatternStringUtils.escapePaddingString(paddingString, sb, 0); 166 sb.insert(0, '*'); 167 afterPrefixPos += addedLength + 1; 168 beforeSuffixPos += addedLength + 1; 169 break; 170 case AFTER_PREFIX: 171 addedLength = PatternStringUtils.escapePaddingString(paddingString, sb, afterPrefixPos); 172 sb.insert(afterPrefixPos, '*'); 173 afterPrefixPos += addedLength + 1; 174 beforeSuffixPos += addedLength + 1; 175 break; 176 case BEFORE_SUFFIX: 177 PatternStringUtils.escapePaddingString(paddingString, sb, beforeSuffixPos); 178 sb.insert(beforeSuffixPos, '*'); 179 break; 180 case AFTER_SUFFIX: 181 sb.append('*'); 182 PatternStringUtils.escapePaddingString(paddingString, sb, sb.length()); 183 break; 184 } 185 } 186 187 // Negative affixes 188 // Ignore if the negative prefix pattern is "-" and the negative suffix is empty 189 if (np != null || ns != null || (npp == null && nsp != null) 190 || (npp != null && (npp.length() != 1 || npp.charAt(0) != '-' || nsp.length() != 0))) { 191 sb.append(';'); 192 if (npp != null) 193 sb.append(npp); 194 AffixUtils.escape(np, sb); 195 // Copy the positive digit format into the negative. 196 // This is optional; the pattern is the same as if '#' were appended here instead. 197 sb.append(sb, afterPrefixPos, beforeSuffixPos); 198 if (nsp != null) 199 sb.append(nsp); 200 AffixUtils.escape(ns, sb); 201 } 202 203 return sb.toString(); 204 } 205 206 /** @return The number of chars inserted. */ 207 private static int escapePaddingString(CharSequence input, StringBuilder output, int startIndex) { 208 if (input == null || input.length() == 0) 209 input = Padder.FALLBACK_PADDING_STRING; 210 int startLength = output.length(); 211 if (input.length() == 1) { 212 if (input.equals("'")) { 213 output.insert(startIndex, "''"); 214 } else { 215 output.insert(startIndex, input); 216 } 217 } else { 218 output.insert(startIndex, '\''); 219 int offset = 1; 220 for (int i = 0; i < input.length(); i++) { 221 // it's okay to deal in chars here because the quote mark is the only interesting thing. 222 char ch = input.charAt(i); 223 if (ch == '\'') { 224 output.insert(startIndex + offset, "''"); 225 offset += 2; 226 } else { 227 output.insert(startIndex + offset, ch); 228 offset += 1; 229 } 230 } 231 output.insert(startIndex + offset, '\''); 232 } 233 return output.length() - startLength; 234 } 235 236 /** 237 * Converts a pattern between standard notation and localized notation. Localized notation means that instead of 238 * using generic placeholders in the pattern, you use the corresponding locale-specific characters instead. For 239 * example, in locale <em>fr-FR</em>, the period in the pattern "0.000" means "decimal" in standard notation (as it 240 * does in every other locale), but it means "grouping" in localized notation. 241 * 242 * <p> 243 * A greedy string-substitution strategy is used to substitute locale symbols. If two symbols are ambiguous or have 244 * the same prefix, the result is not well-defined. 245 * 246 * <p> 247 * Locale symbols are not allowed to contain the ASCII quote character. 248 * 249 * <p> 250 * This method is provided for backwards compatibility and should not be used in any new code. 251 * 252 * @param input 253 * The pattern to convert. 254 * @param symbols 255 * The symbols corresponding to the localized pattern. 256 * @param toLocalized 257 * true to convert from standard to localized notation; false to convert from localized to standard 258 * notation. 259 * @return The pattern expressed in the other notation. 260 */ 261 public static String convertLocalized(String input, DecimalFormatSymbols symbols, boolean toLocalized) { 262 if (input == null) 263 return null; 264 265 // Construct a table of strings to be converted between localized and standard. 266 String[][] table = new String[21][2]; 267 int standIdx = toLocalized ? 0 : 1; 268 int localIdx = toLocalized ? 1 : 0; 269 table[0][standIdx] = "%"; 270 table[0][localIdx] = symbols.getPercentString(); 271 table[1][standIdx] = ""; 272 table[1][localIdx] = symbols.getPerMillString(); 273 table[2][standIdx] = "."; 274 table[2][localIdx] = symbols.getDecimalSeparatorString(); 275 table[3][standIdx] = ","; 276 table[3][localIdx] = symbols.getGroupingSeparatorString(); 277 table[4][standIdx] = "-"; 278 table[4][localIdx] = symbols.getMinusSignString(); 279 table[5][standIdx] = "+"; 280 table[5][localIdx] = symbols.getPlusSignString(); 281 table[6][standIdx] = ";"; 282 table[6][localIdx] = Character.toString(symbols.getPatternSeparator()); 283 table[7][standIdx] = "@"; 284 table[7][localIdx] = Character.toString(symbols.getSignificantDigit()); 285 table[8][standIdx] = "E"; 286 table[8][localIdx] = symbols.getExponentSeparator(); 287 table[9][standIdx] = "*"; 288 table[9][localIdx] = Character.toString(symbols.getPadEscape()); 289 table[10][standIdx] = "#"; 290 table[10][localIdx] = Character.toString(symbols.getDigit()); 291 for (int i = 0; i < 10; i++) { 292 table[11 + i][standIdx] = Character.toString((char) ('0' + i)); 293 table[11 + i][localIdx] = symbols.getDigitStringsLocal()[i]; 294 } 295 296 // Special case: quotes are NOT allowed to be in any localIdx strings. 297 // Substitute them with '' instead. 298 for (int i = 0; i < table.length; i++) { 299 table[i][localIdx] = table[i][localIdx].replace('\'', ''); 300 } 301 302 // Iterate through the string and convert. 303 // State table: 304 // 0 => base state 305 // 1 => first char inside a quoted sequence in input and output string 306 // 2 => inside a quoted sequence in input and output string 307 // 3 => first char after a close quote in input string; 308 // close quote still needs to be written to output string 309 // 4 => base state in input string; inside quoted sequence in output string 310 // 5 => first char inside a quoted sequence in input string; 311 // inside quoted sequence in output string 312 StringBuilder result = new StringBuilder(); 313 int state = 0; 314 outer: for (int offset = 0; offset < input.length(); offset++) { 315 char ch = input.charAt(offset); 316 317 // Handle a quote character (state shift) 318 if (ch == '\'') { 319 if (state == 0) { 320 result.append('\''); 321 state = 1; 322 continue; 323 } else if (state == 1) { 324 result.append('\''); 325 state = 0; 326 continue; 327 } else if (state == 2) { 328 state = 3; 329 continue; 330 } else if (state == 3) { 331 result.append('\''); 332 result.append('\''); 333 state = 1; 334 continue; 335 } else if (state == 4) { 336 state = 5; 337 continue; 338 } else { 339 assert state == 5; 340 result.append('\''); 341 result.append('\''); 342 state = 4; 343 continue; 344 } 345 } 346 347 if (state == 0 || state == 3 || state == 4) { 348 for (String[] pair : table) { 349 // Perform a greedy match on this symbol string 350 if (input.regionMatches(offset, pair[0], 0, pair[0].length())) { 351 // Skip ahead past this region for the next iteration 352 offset += pair[0].length() - 1; 353 if (state == 3 || state == 4) { 354 result.append('\''); 355 state = 0; 356 } 357 result.append(pair[1]); 358 continue outer; 359 } 360 } 361 // No replacement found. Check if a special quote is necessary 362 for (String[] pair : table) { 363 if (input.regionMatches(offset, pair[1], 0, pair[1].length())) { 364 if (state == 0) { 365 result.append('\''); 366 state = 4; 367 } 368 result.append(ch); 369 continue outer; 370 } 371 } 372 // Still nothing. Copy the char verbatim. (Add a close quote if necessary) 373 if (state == 3 || state == 4) { 374 result.append('\''); 375 state = 0; 376 } 377 result.append(ch); 378 } else { 379 assert state == 1 || state == 2 || state == 5; 380 result.append(ch); 381 state = 2; 382 } 383 } 384 // Resolve final quotes 385 if (state == 3 || state == 4) { 386 result.append('\''); 387 state = 0; 388 } 389 if (state != 0) { 390 throw new IllegalArgumentException("Malformed localized pattern: unterminated quote"); 391 } 392 return result.toString(); 393 } 394 395 } 396