• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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