Home | History | Annotate | Download | only in number
      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