• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* GENERATED SOURCE. DO NOT MODIFY. */
2 // © 2018 and later: Unicode, Inc. and others.
3 // License & terms of use: http://www.unicode.org/copyright.html#License
4 package ohos.global.icu.number;
5 
6 import java.math.BigDecimal;
7 import java.math.RoundingMode;
8 import java.util.Set;
9 
10 import ohos.global.icu.impl.CacheBase;
11 import ohos.global.icu.impl.PatternProps;
12 import ohos.global.icu.impl.SoftCache;
13 import ohos.global.icu.impl.StringSegment;
14 import ohos.global.icu.impl.number.MacroProps;
15 import ohos.global.icu.impl.number.RoundingUtils;
16 import ohos.global.icu.number.NumberFormatter.DecimalSeparatorDisplay;
17 import ohos.global.icu.number.NumberFormatter.GroupingStrategy;
18 import ohos.global.icu.number.NumberFormatter.SignDisplay;
19 import ohos.global.icu.number.NumberFormatter.UnitWidth;
20 import ohos.global.icu.text.DecimalFormatSymbols;
21 import ohos.global.icu.text.NumberingSystem;
22 import ohos.global.icu.util.BytesTrie;
23 import ohos.global.icu.util.CharsTrie;
24 import ohos.global.icu.util.CharsTrieBuilder;
25 import ohos.global.icu.util.Currency;
26 import ohos.global.icu.util.Currency.CurrencyUsage;
27 import ohos.global.icu.util.MeasureUnit;
28 import ohos.global.icu.util.NoUnit;
29 import ohos.global.icu.util.StringTrieBuilder;
30 
31 /**
32  * @author sffc
33  *
34  */
35 class NumberSkeletonImpl {
36 
37     ///////////////////////////////////////////////////////////////////////////////////////
38     // NOTE: For an example of how to add a new stem to the number skeleton parser, see: //
39     // http://bugs.icu-project.org/trac/changeset/41193                                  //
40     ///////////////////////////////////////////////////////////////////////////////////////
41 
42     /**
43      * While parsing a skeleton, this enum records what type of option we expect to find next.
44      */
45     static enum ParseState {
46         // Section 0: We expect whitespace or a stem, but not an option:
47         STATE_NULL,
48 
49         // Section 1: We might accept an option, but it is not required:
50         STATE_SCIENTIFIC,
51         STATE_FRACTION_PRECISION,
52 
53         // Section 2: An option is required:
54         STATE_INCREMENT_PRECISION,
55         STATE_MEASURE_UNIT,
56         STATE_PER_MEASURE_UNIT,
57         STATE_IDENTIFIER_UNIT,
58         STATE_CURRENCY_UNIT,
59         STATE_INTEGER_WIDTH,
60         STATE_NUMBERING_SYSTEM,
61         STATE_SCALE,
62     }
63 
64     /**
65      * All possible stem literals have an entry in the StemEnum. The enum name is the kebab case stem
66      * string literal written in upper snake case.
67      *
68      * @see StemToObject
69      * @see #SERIALIZED_STEM_TRIE
70      */
71     static enum StemEnum {
72         // Section 1: Stems that do not require an option:
73         STEM_COMPACT_SHORT,
74         STEM_COMPACT_LONG,
75         STEM_SCIENTIFIC,
76         STEM_ENGINEERING,
77         STEM_NOTATION_SIMPLE,
78         STEM_BASE_UNIT,
79         STEM_PERCENT,
80         STEM_PERMILLE,
81         STEM_PERCENT_100, // concise-only
82         STEM_PRECISION_INTEGER,
83         STEM_PRECISION_UNLIMITED,
84         STEM_PRECISION_CURRENCY_STANDARD,
85         STEM_PRECISION_CURRENCY_CASH,
86         STEM_ROUNDING_MODE_CEILING,
87         STEM_ROUNDING_MODE_FLOOR,
88         STEM_ROUNDING_MODE_DOWN,
89         STEM_ROUNDING_MODE_UP,
90         STEM_ROUNDING_MODE_HALF_EVEN,
91         STEM_ROUNDING_MODE_HALF_DOWN,
92         STEM_ROUNDING_MODE_HALF_UP,
93         STEM_ROUNDING_MODE_UNNECESSARY,
94         STEM_GROUP_OFF,
95         STEM_GROUP_MIN2,
96         STEM_GROUP_AUTO,
97         STEM_GROUP_ON_ALIGNED,
98         STEM_GROUP_THOUSANDS,
99         STEM_LATIN,
100         STEM_UNIT_WIDTH_NARROW,
101         STEM_UNIT_WIDTH_SHORT,
102         STEM_UNIT_WIDTH_FULL_NAME,
103         STEM_UNIT_WIDTH_ISO_CODE,
104         STEM_UNIT_WIDTH_FORMAL,
105         STEM_UNIT_WIDTH_VARIANT,
106         STEM_UNIT_WIDTH_HIDDEN,
107         STEM_SIGN_AUTO,
108         STEM_SIGN_ALWAYS,
109         STEM_SIGN_NEVER,
110         STEM_SIGN_ACCOUNTING,
111         STEM_SIGN_ACCOUNTING_ALWAYS,
112         STEM_SIGN_EXCEPT_ZERO,
113         STEM_SIGN_ACCOUNTING_EXCEPT_ZERO,
114         STEM_DECIMAL_AUTO,
115         STEM_DECIMAL_ALWAYS,
116 
117         // Section 2: Stems that DO require an option:
118         STEM_PRECISION_INCREMENT,
119         STEM_MEASURE_UNIT,
120         STEM_PER_MEASURE_UNIT,
121         STEM_UNIT,
122         STEM_CURRENCY,
123         STEM_INTEGER_WIDTH,
124         STEM_NUMBERING_SYSTEM,
125         STEM_SCALE,
126     };
127 
128     /** Default wildcard char, accepted on input and printed in output */
129     static final char WILDCARD_CHAR = '*';
130 
131     /** Alternative wildcard char, accept on input but not printed in output */
132     static final char ALT_WILDCARD_CHAR = '+';
133 
134     /** Checks whether the char is a wildcard on input */
isWildcardChar(char c)135     static boolean isWildcardChar(char c) {
136         return c == WILDCARD_CHAR || c == ALT_WILDCARD_CHAR;
137     }
138 
139     /** For mapping from ordinal back to StemEnum in Java. */
140     static final StemEnum[] STEM_ENUM_VALUES = StemEnum.values();
141 
142     /** A data structure for mapping from stem strings to the stem enum. Built at startup. */
143     static final String SERIALIZED_STEM_TRIE = buildStemTrie();
144 
buildStemTrie()145     static String buildStemTrie() {
146         CharsTrieBuilder b = new CharsTrieBuilder();
147 
148         // Section 1:
149         b.add("compact-short", StemEnum.STEM_COMPACT_SHORT.ordinal());
150         b.add("compact-long", StemEnum.STEM_COMPACT_LONG.ordinal());
151         b.add("scientific", StemEnum.STEM_SCIENTIFIC.ordinal());
152         b.add("engineering", StemEnum.STEM_ENGINEERING.ordinal());
153         b.add("notation-simple", StemEnum.STEM_NOTATION_SIMPLE.ordinal());
154         b.add("base-unit", StemEnum.STEM_BASE_UNIT.ordinal());
155         b.add("percent", StemEnum.STEM_PERCENT.ordinal());
156         b.add("permille", StemEnum.STEM_PERMILLE.ordinal());
157         b.add("precision-integer", StemEnum.STEM_PRECISION_INTEGER.ordinal());
158         b.add("precision-unlimited", StemEnum.STEM_PRECISION_UNLIMITED.ordinal());
159         b.add("precision-currency-standard", StemEnum.STEM_PRECISION_CURRENCY_STANDARD.ordinal());
160         b.add("precision-currency-cash", StemEnum.STEM_PRECISION_CURRENCY_CASH.ordinal());
161         b.add("rounding-mode-ceiling", StemEnum.STEM_ROUNDING_MODE_CEILING.ordinal());
162         b.add("rounding-mode-floor", StemEnum.STEM_ROUNDING_MODE_FLOOR.ordinal());
163         b.add("rounding-mode-down", StemEnum.STEM_ROUNDING_MODE_DOWN.ordinal());
164         b.add("rounding-mode-up", StemEnum.STEM_ROUNDING_MODE_UP.ordinal());
165         b.add("rounding-mode-half-even", StemEnum.STEM_ROUNDING_MODE_HALF_EVEN.ordinal());
166         b.add("rounding-mode-half-down", StemEnum.STEM_ROUNDING_MODE_HALF_DOWN.ordinal());
167         b.add("rounding-mode-half-up", StemEnum.STEM_ROUNDING_MODE_HALF_UP.ordinal());
168         b.add("rounding-mode-unnecessary", StemEnum.STEM_ROUNDING_MODE_UNNECESSARY.ordinal());
169         b.add("group-off", StemEnum.STEM_GROUP_OFF.ordinal());
170         b.add("group-min2", StemEnum.STEM_GROUP_MIN2.ordinal());
171         b.add("group-auto", StemEnum.STEM_GROUP_AUTO.ordinal());
172         b.add("group-on-aligned", StemEnum.STEM_GROUP_ON_ALIGNED.ordinal());
173         b.add("group-thousands", StemEnum.STEM_GROUP_THOUSANDS.ordinal());
174         b.add("latin", StemEnum.STEM_LATIN.ordinal());
175         b.add("unit-width-narrow", StemEnum.STEM_UNIT_WIDTH_NARROW.ordinal());
176         b.add("unit-width-short", StemEnum.STEM_UNIT_WIDTH_SHORT.ordinal());
177         b.add("unit-width-full-name", StemEnum.STEM_UNIT_WIDTH_FULL_NAME.ordinal());
178         b.add("unit-width-iso-code", StemEnum.STEM_UNIT_WIDTH_ISO_CODE.ordinal());
179         b.add("unit-width-formal", StemEnum.STEM_UNIT_WIDTH_FORMAL.ordinal());
180         b.add("unit-width-variant", StemEnum.STEM_UNIT_WIDTH_VARIANT.ordinal());
181         b.add("unit-width-hidden", StemEnum.STEM_UNIT_WIDTH_HIDDEN.ordinal());
182         b.add("sign-auto", StemEnum.STEM_SIGN_AUTO.ordinal());
183         b.add("sign-always", StemEnum.STEM_SIGN_ALWAYS.ordinal());
184         b.add("sign-never", StemEnum.STEM_SIGN_NEVER.ordinal());
185         b.add("sign-accounting", StemEnum.STEM_SIGN_ACCOUNTING.ordinal());
186         b.add("sign-accounting-always", StemEnum.STEM_SIGN_ACCOUNTING_ALWAYS.ordinal());
187         b.add("sign-except-zero", StemEnum.STEM_SIGN_EXCEPT_ZERO.ordinal());
188         b.add("sign-accounting-except-zero", StemEnum.STEM_SIGN_ACCOUNTING_EXCEPT_ZERO.ordinal());
189         b.add("decimal-auto", StemEnum.STEM_DECIMAL_AUTO.ordinal());
190         b.add("decimal-always", StemEnum.STEM_DECIMAL_ALWAYS.ordinal());
191 
192         // Section 2:
193         b.add("precision-increment", StemEnum.STEM_PRECISION_INCREMENT.ordinal());
194         b.add("measure-unit", StemEnum.STEM_MEASURE_UNIT.ordinal());
195         b.add("per-measure-unit", StemEnum.STEM_PER_MEASURE_UNIT.ordinal());
196         b.add("unit", StemEnum.STEM_UNIT.ordinal());
197         b.add("currency", StemEnum.STEM_CURRENCY.ordinal());
198         b.add("integer-width", StemEnum.STEM_INTEGER_WIDTH.ordinal());
199         b.add("numbering-system", StemEnum.STEM_NUMBERING_SYSTEM.ordinal());
200         b.add("scale", StemEnum.STEM_SCALE.ordinal());
201 
202         // Section 3 (concise tokens):
203         b.add("K", StemEnum.STEM_COMPACT_SHORT.ordinal());
204         b.add("KK", StemEnum.STEM_COMPACT_LONG.ordinal());
205         b.add("%", StemEnum.STEM_PERCENT.ordinal());
206         b.add("%x100", StemEnum.STEM_PERCENT_100.ordinal());
207         b.add(",_", StemEnum.STEM_GROUP_OFF.ordinal());
208         b.add(",?", StemEnum.STEM_GROUP_MIN2.ordinal());
209         b.add(",!", StemEnum.STEM_GROUP_ON_ALIGNED.ordinal());
210         b.add("+!", StemEnum.STEM_SIGN_ALWAYS.ordinal());
211         b.add("+_", StemEnum.STEM_SIGN_NEVER.ordinal());
212         b.add("()", StemEnum.STEM_SIGN_ACCOUNTING.ordinal());
213         b.add("()!", StemEnum.STEM_SIGN_ACCOUNTING_ALWAYS.ordinal());
214         b.add("+?", StemEnum.STEM_SIGN_EXCEPT_ZERO.ordinal());
215         b.add("()?", StemEnum.STEM_SIGN_ACCOUNTING_EXCEPT_ZERO.ordinal());
216 
217         // Build the CharsTrie
218         // TODO: Use SLOW or FAST here?
219         return b.buildCharSequence(StringTrieBuilder.Option.FAST).toString();
220     }
221 
222     /**
223      * Utility class for methods that convert from StemEnum to corresponding objects or enums. This
224      * applies to only the "Section 1" stems, those that are well-defined without an option.
225      */
226     static final class StemToObject {
227 
notation(StemEnum stem)228         private static Notation notation(StemEnum stem) {
229             switch (stem) {
230             case STEM_COMPACT_SHORT:
231                 return Notation.compactShort();
232             case STEM_COMPACT_LONG:
233                 return Notation.compactLong();
234             case STEM_SCIENTIFIC:
235                 return Notation.scientific();
236             case STEM_ENGINEERING:
237                 return Notation.engineering();
238             case STEM_NOTATION_SIMPLE:
239                 return Notation.simple();
240             default:
241                 throw new AssertionError();
242             }
243         }
244 
unit(StemEnum stem)245         private static MeasureUnit unit(StemEnum stem) {
246             switch (stem) {
247             case STEM_BASE_UNIT:
248                 return NoUnit.BASE;
249             case STEM_PERCENT:
250                 return NoUnit.PERCENT;
251             case STEM_PERMILLE:
252                 return NoUnit.PERMILLE;
253             default:
254                 throw new AssertionError();
255             }
256         }
257 
precision(StemEnum stem)258         private static Precision precision(StemEnum stem) {
259             switch (stem) {
260             case STEM_PRECISION_INTEGER:
261                 return Precision.integer();
262             case STEM_PRECISION_UNLIMITED:
263                 return Precision.unlimited();
264             case STEM_PRECISION_CURRENCY_STANDARD:
265                 return Precision.currency(CurrencyUsage.STANDARD);
266             case STEM_PRECISION_CURRENCY_CASH:
267                 return Precision.currency(CurrencyUsage.CASH);
268             default:
269                 throw new AssertionError();
270             }
271         }
272 
roundingMode(StemEnum stem)273         private static RoundingMode roundingMode(StemEnum stem) {
274             switch (stem) {
275             case STEM_ROUNDING_MODE_CEILING:
276                 return RoundingMode.CEILING;
277             case STEM_ROUNDING_MODE_FLOOR:
278                 return RoundingMode.FLOOR;
279             case STEM_ROUNDING_MODE_DOWN:
280                 return RoundingMode.DOWN;
281             case STEM_ROUNDING_MODE_UP:
282                 return RoundingMode.UP;
283             case STEM_ROUNDING_MODE_HALF_EVEN:
284                 return RoundingMode.HALF_EVEN;
285             case STEM_ROUNDING_MODE_HALF_DOWN:
286                 return RoundingMode.HALF_DOWN;
287             case STEM_ROUNDING_MODE_HALF_UP:
288                 return RoundingMode.HALF_UP;
289             case STEM_ROUNDING_MODE_UNNECESSARY:
290                 return RoundingMode.UNNECESSARY;
291             default:
292                 throw new AssertionError();
293             }
294         }
295 
groupingStrategy(StemEnum stem)296         private static GroupingStrategy groupingStrategy(StemEnum stem) {
297             switch (stem) {
298             case STEM_GROUP_OFF:
299                 return GroupingStrategy.OFF;
300             case STEM_GROUP_MIN2:
301                 return GroupingStrategy.MIN2;
302             case STEM_GROUP_AUTO:
303                 return GroupingStrategy.AUTO;
304             case STEM_GROUP_ON_ALIGNED:
305                 return GroupingStrategy.ON_ALIGNED;
306             case STEM_GROUP_THOUSANDS:
307                 return GroupingStrategy.THOUSANDS;
308             default:
309                 return null; // for objects, throw; for enums, return null
310             }
311         }
312 
unitWidth(StemEnum stem)313         private static UnitWidth unitWidth(StemEnum stem) {
314             switch (stem) {
315             case STEM_UNIT_WIDTH_NARROW:
316                 return UnitWidth.NARROW;
317             case STEM_UNIT_WIDTH_SHORT:
318                 return UnitWidth.SHORT;
319             case STEM_UNIT_WIDTH_FULL_NAME:
320                 return UnitWidth.FULL_NAME;
321             case STEM_UNIT_WIDTH_ISO_CODE:
322                 return UnitWidth.ISO_CODE;
323             case STEM_UNIT_WIDTH_FORMAL:
324                 return UnitWidth.FORMAL;
325             case STEM_UNIT_WIDTH_VARIANT:
326                 return UnitWidth.VARIANT;
327             case STEM_UNIT_WIDTH_HIDDEN:
328                 return UnitWidth.HIDDEN;
329             default:
330                 return null; // for objects, throw; for enums, return null
331             }
332         }
333 
signDisplay(StemEnum stem)334         private static SignDisplay signDisplay(StemEnum stem) {
335             switch (stem) {
336             case STEM_SIGN_AUTO:
337                 return SignDisplay.AUTO;
338             case STEM_SIGN_ALWAYS:
339                 return SignDisplay.ALWAYS;
340             case STEM_SIGN_NEVER:
341                 return SignDisplay.NEVER;
342             case STEM_SIGN_ACCOUNTING:
343                 return SignDisplay.ACCOUNTING;
344             case STEM_SIGN_ACCOUNTING_ALWAYS:
345                 return SignDisplay.ACCOUNTING_ALWAYS;
346             case STEM_SIGN_EXCEPT_ZERO:
347                 return SignDisplay.EXCEPT_ZERO;
348             case STEM_SIGN_ACCOUNTING_EXCEPT_ZERO:
349                 return SignDisplay.ACCOUNTING_EXCEPT_ZERO;
350             default:
351                 return null; // for objects, throw; for enums, return null
352             }
353         }
354 
decimalSeparatorDisplay(StemEnum stem)355         private static DecimalSeparatorDisplay decimalSeparatorDisplay(StemEnum stem) {
356             switch (stem) {
357             case STEM_DECIMAL_AUTO:
358                 return DecimalSeparatorDisplay.AUTO;
359             case STEM_DECIMAL_ALWAYS:
360                 return DecimalSeparatorDisplay.ALWAYS;
361             default:
362                 return null; // for objects, throw; for enums, return null
363             }
364         }
365     }
366 
367     /**
368      * Utility class for methods that convert from enums to stem strings. More complex object conversions
369      * take place in ObjectToStemString.
370      */
371     static final class EnumToStemString {
372 
roundingMode(RoundingMode value, StringBuilder sb)373         private static void roundingMode(RoundingMode value, StringBuilder sb) {
374             switch (value) {
375             case CEILING:
376                 sb.append("rounding-mode-ceiling");
377                 break;
378             case FLOOR:
379                 sb.append("rounding-mode-floor");
380                 break;
381             case DOWN:
382                 sb.append("rounding-mode-down");
383                 break;
384             case UP:
385                 sb.append("rounding-mode-up");
386                 break;
387             case HALF_EVEN:
388                 sb.append("rounding-mode-half-even");
389                 break;
390             case HALF_DOWN:
391                 sb.append("rounding-mode-half-down");
392                 break;
393             case HALF_UP:
394                 sb.append("rounding-mode-half-up");
395                 break;
396             case UNNECESSARY:
397                 sb.append("rounding-mode-unnecessary");
398                 break;
399             default:
400                 throw new AssertionError();
401             }
402         }
403 
groupingStrategy(GroupingStrategy value, StringBuilder sb)404         private static void groupingStrategy(GroupingStrategy value, StringBuilder sb) {
405             switch (value) {
406             case OFF:
407                 sb.append("group-off");
408                 break;
409             case MIN2:
410                 sb.append("group-min2");
411                 break;
412             case AUTO:
413                 sb.append("group-auto");
414                 break;
415             case ON_ALIGNED:
416                 sb.append("group-on-aligned");
417                 break;
418             case THOUSANDS:
419                 sb.append("group-thousands");
420                 break;
421             default:
422                 throw new AssertionError();
423             }
424         }
425 
unitWidth(UnitWidth value, StringBuilder sb)426         private static void unitWidth(UnitWidth value, StringBuilder sb) {
427             switch (value) {
428             case NARROW:
429                 sb.append("unit-width-narrow");
430                 break;
431             case SHORT:
432                 sb.append("unit-width-short");
433                 break;
434             case FULL_NAME:
435                 sb.append("unit-width-full-name");
436                 break;
437             case ISO_CODE:
438                 sb.append("unit-width-iso-code");
439                 break;
440             case FORMAL:
441                 sb.append("unit-width-formal");
442                 break;
443             case VARIANT:
444                 sb.append("unit-width-variant");
445                 break;
446             case HIDDEN:
447                 sb.append("unit-width-hidden");
448                 break;
449             default:
450                 throw new AssertionError();
451             }
452         }
453 
signDisplay(SignDisplay value, StringBuilder sb)454         private static void signDisplay(SignDisplay value, StringBuilder sb) {
455             switch (value) {
456             case AUTO:
457                 sb.append("sign-auto");
458                 break;
459             case ALWAYS:
460                 sb.append("sign-always");
461                 break;
462             case NEVER:
463                 sb.append("sign-never");
464                 break;
465             case ACCOUNTING:
466                 sb.append("sign-accounting");
467                 break;
468             case ACCOUNTING_ALWAYS:
469                 sb.append("sign-accounting-always");
470                 break;
471             case EXCEPT_ZERO:
472                 sb.append("sign-except-zero");
473                 break;
474             case ACCOUNTING_EXCEPT_ZERO:
475                 sb.append("sign-accounting-except-zero");
476                 break;
477             default:
478                 throw new AssertionError();
479             }
480         }
481 
decimalSeparatorDisplay(DecimalSeparatorDisplay value, StringBuilder sb)482         private static void decimalSeparatorDisplay(DecimalSeparatorDisplay value, StringBuilder sb) {
483             switch (value) {
484             case AUTO:
485                 sb.append("decimal-auto");
486                 break;
487             case ALWAYS:
488                 sb.append("decimal-always");
489                 break;
490             default:
491                 throw new AssertionError();
492             }
493         }
494     }
495 
496     ///// ENTRYPOINT FUNCTIONS /////
497 
498     /** Cache for parsed skeleton strings. */
499     private static final CacheBase<String, UnlocalizedNumberFormatter, Void> cache = new SoftCache<String, UnlocalizedNumberFormatter, Void>() {
500         @Override
501         protected UnlocalizedNumberFormatter createInstance(String skeletonString, Void unused) {
502             return create(skeletonString);
503         }
504     };
505 
506     /**
507      * Gets the number formatter for the given number skeleton string from the cache, creating it if it
508      * does not exist in the cache.
509      *
510      * @param skeletonString
511      *            A number skeleton string, possibly not in its shortest form.
512      * @return An UnlocalizedNumberFormatter with behavior defined by the given skeleton string.
513      */
getOrCreate(String skeletonString)514     public static UnlocalizedNumberFormatter getOrCreate(String skeletonString) {
515         // TODO: This does not currently check the cache for the normalized form of the skeleton.
516         // A new cache implementation would be required for that to work.
517         return cache.getInstance(skeletonString, null);
518     }
519 
520     /**
521      * Creates a NumberFormatter corresponding to the given skeleton string.
522      *
523      * @param skeletonString
524      *            A number skeleton string, possibly not in its shortest form.
525      * @return An UnlocalizedNumberFormatter with behavior defined by the given skeleton string.
526      */
create(String skeletonString)527     public static UnlocalizedNumberFormatter create(String skeletonString) {
528         MacroProps macros = parseSkeleton(skeletonString);
529         return NumberFormatter.with().macros(macros);
530     }
531 
532     /**
533      * Create a skeleton string corresponding to the given NumberFormatter.
534      *
535      * @param macros
536      *            The NumberFormatter options object.
537      * @return A skeleton string in normalized form.
538      */
generate(MacroProps macros)539     public static String generate(MacroProps macros) {
540         StringBuilder sb = new StringBuilder();
541         generateSkeleton(macros, sb);
542         return sb.toString();
543     }
544 
545     ///// MAIN PARSING FUNCTIONS /////
546 
547     /**
548      * Converts from a skeleton string to a MacroProps. This method contains the primary parse loop.
549      */
parseSkeleton(String skeletonString)550     private static MacroProps parseSkeleton(String skeletonString) {
551         // Add a trailing whitespace to the end of the skeleton string to make code cleaner.
552         skeletonString += " ";
553 
554         MacroProps macros = new MacroProps();
555         StringSegment segment = new StringSegment(skeletonString, false);
556         CharsTrie stemTrie = new CharsTrie(SERIALIZED_STEM_TRIE, 0);
557         ParseState stem = ParseState.STATE_NULL;
558         int offset = 0;
559 
560         // Primary skeleton parse loop:
561         while (offset < segment.length()) {
562             int cp = segment.codePointAt(offset);
563             boolean isTokenSeparator = PatternProps.isWhiteSpace(cp);
564             boolean isOptionSeparator = (cp == '/');
565 
566             if (!isTokenSeparator && !isOptionSeparator) {
567                 // Non-separator token; consume it.
568                 offset += Character.charCount(cp);
569                 if (stem == ParseState.STATE_NULL) {
570                     // We are currently consuming a stem.
571                     // Go to the next state in the stem trie.
572                     stemTrie.nextForCodePoint(cp);
573                 }
574                 continue;
575             }
576 
577             // We are looking at a token or option separator.
578             // If the segment is nonempty, parse it and reset the segment.
579             // Otherwise, make sure it is a valid repeating separator.
580             if (offset != 0) {
581                 segment.setLength(offset);
582                 if (stem == ParseState.STATE_NULL) {
583                     // The first separator after the start of a token. Parse it as a stem.
584                     stem = parseStem(segment, stemTrie, macros);
585                     stemTrie.reset();
586                 } else {
587                     // A separator after the first separator of a token. Parse it as an option.
588                     stem = parseOption(stem, segment, macros);
589                 }
590                 segment.resetLength();
591 
592                 // Consume the segment:
593                 segment.adjustOffset(offset);
594                 offset = 0;
595 
596             } else if (stem != ParseState.STATE_NULL) {
597                 // A separator ('/' or whitespace) following an option separator ('/')
598                 segment.setLength(Character.charCount(cp)); // for error message
599                 throw new SkeletonSyntaxException("Unexpected separator character", segment);
600 
601             } else {
602                 // Two spaces in a row; this is OK.
603             }
604 
605             // Does the current stem forbid options?
606             if (isOptionSeparator && stem == ParseState.STATE_NULL) {
607                 segment.setLength(Character.charCount(cp)); // for error message
608                 throw new SkeletonSyntaxException("Unexpected option separator", segment);
609             }
610 
611             // Does the current stem require an option?
612             if (isTokenSeparator && stem != ParseState.STATE_NULL) {
613                 switch (stem) {
614                 case STATE_INCREMENT_PRECISION:
615                 case STATE_MEASURE_UNIT:
616                 case STATE_PER_MEASURE_UNIT:
617                 case STATE_CURRENCY_UNIT:
618                 case STATE_INTEGER_WIDTH:
619                 case STATE_NUMBERING_SYSTEM:
620                 case STATE_SCALE:
621                     segment.setLength(Character.charCount(cp)); // for error message
622                     throw new SkeletonSyntaxException("Stem requires an option", segment);
623                 default:
624                     break;
625                 }
626                 stem = ParseState.STATE_NULL;
627             }
628 
629             // Consume the separator:
630             segment.adjustOffset(Character.charCount(cp));
631         }
632         assert stem == ParseState.STATE_NULL;
633         return macros;
634     }
635 
636     /**
637      * Given that the current segment represents a stem, parse it and save the result.
638      *
639      * @return The next state after parsing this stem, corresponding to what subset of options to expect.
640      */
parseStem(StringSegment segment, CharsTrie stemTrie, MacroProps macros)641     private static ParseState parseStem(StringSegment segment, CharsTrie stemTrie, MacroProps macros) {
642         // First check for "blueprint" stems, which start with a "signal char"
643         switch (segment.charAt(0)) {
644         case '.':
645             checkNull(macros.precision, segment);
646             BlueprintHelpers.parseFractionStem(segment, macros);
647             return ParseState.STATE_FRACTION_PRECISION;
648         case '@':
649             checkNull(macros.precision, segment);
650             BlueprintHelpers.parseDigitsStem(segment, macros);
651             return ParseState.STATE_NULL;
652         case 'E':
653             checkNull(macros.notation, segment);
654             BlueprintHelpers.parseScientificStem(segment, macros);
655             return ParseState.STATE_NULL;
656         case '0':
657             checkNull(macros.notation, segment);
658             BlueprintHelpers.parseIntegerStem(segment, macros);
659             return ParseState.STATE_NULL;
660         }
661 
662         // Now look at the stemsTrie, which is already be pointing at our stem.
663         BytesTrie.Result stemResult = stemTrie.current();
664 
665         if (stemResult != BytesTrie.Result.INTERMEDIATE_VALUE
666                 && stemResult != BytesTrie.Result.FINAL_VALUE) {
667             throw new SkeletonSyntaxException("Unknown stem", segment);
668         }
669 
670         StemEnum stem = STEM_ENUM_VALUES[stemTrie.getValue()];
671         switch (stem) {
672 
673         // Stems with meaning on their own, not requiring an option:
674 
675         case STEM_COMPACT_SHORT:
676         case STEM_COMPACT_LONG:
677         case STEM_SCIENTIFIC:
678         case STEM_ENGINEERING:
679         case STEM_NOTATION_SIMPLE:
680             checkNull(macros.notation, segment);
681             macros.notation = StemToObject.notation(stem);
682             switch (stem) {
683             case STEM_SCIENTIFIC:
684             case STEM_ENGINEERING:
685                 return ParseState.STATE_SCIENTIFIC; // allows for scientific options
686             default:
687                 return ParseState.STATE_NULL;
688             }
689 
690         case STEM_BASE_UNIT:
691         case STEM_PERCENT:
692         case STEM_PERMILLE:
693             checkNull(macros.unit, segment);
694             macros.unit = StemToObject.unit(stem);
695             return ParseState.STATE_NULL;
696 
697         case STEM_PERCENT_100:
698             checkNull(macros.scale, segment);
699             checkNull(macros.unit, segment);
700             macros.scale = Scale.powerOfTen(2);
701             macros.unit = NoUnit.PERCENT;
702             return ParseState.STATE_NULL;
703 
704         case STEM_PRECISION_INTEGER:
705         case STEM_PRECISION_UNLIMITED:
706         case STEM_PRECISION_CURRENCY_STANDARD:
707         case STEM_PRECISION_CURRENCY_CASH:
708             checkNull(macros.precision, segment);
709             macros.precision = StemToObject.precision(stem);
710             switch (stem) {
711             case STEM_PRECISION_INTEGER:
712                 return ParseState.STATE_FRACTION_PRECISION; // allows for "precision-integer/@##"
713             default:
714                 return ParseState.STATE_NULL;
715             }
716 
717         case STEM_ROUNDING_MODE_CEILING:
718         case STEM_ROUNDING_MODE_FLOOR:
719         case STEM_ROUNDING_MODE_DOWN:
720         case STEM_ROUNDING_MODE_UP:
721         case STEM_ROUNDING_MODE_HALF_EVEN:
722         case STEM_ROUNDING_MODE_HALF_DOWN:
723         case STEM_ROUNDING_MODE_HALF_UP:
724         case STEM_ROUNDING_MODE_UNNECESSARY:
725             checkNull(macros.roundingMode, segment);
726             macros.roundingMode = StemToObject.roundingMode(stem);
727             return ParseState.STATE_NULL;
728 
729         case STEM_GROUP_OFF:
730         case STEM_GROUP_MIN2:
731         case STEM_GROUP_AUTO:
732         case STEM_GROUP_ON_ALIGNED:
733         case STEM_GROUP_THOUSANDS:
734             checkNull(macros.grouping, segment);
735             macros.grouping = StemToObject.groupingStrategy(stem);
736             return ParseState.STATE_NULL;
737 
738         case STEM_LATIN:
739             checkNull(macros.symbols, segment);
740             macros.symbols = NumberingSystem.LATIN;
741             return ParseState.STATE_NULL;
742 
743         case STEM_UNIT_WIDTH_NARROW:
744         case STEM_UNIT_WIDTH_SHORT:
745         case STEM_UNIT_WIDTH_FULL_NAME:
746         case STEM_UNIT_WIDTH_ISO_CODE:
747         case STEM_UNIT_WIDTH_FORMAL:
748         case STEM_UNIT_WIDTH_VARIANT:
749         case STEM_UNIT_WIDTH_HIDDEN:
750             checkNull(macros.unitWidth, segment);
751             macros.unitWidth = StemToObject.unitWidth(stem);
752             return ParseState.STATE_NULL;
753 
754         case STEM_SIGN_AUTO:
755         case STEM_SIGN_ALWAYS:
756         case STEM_SIGN_NEVER:
757         case STEM_SIGN_ACCOUNTING:
758         case STEM_SIGN_ACCOUNTING_ALWAYS:
759         case STEM_SIGN_EXCEPT_ZERO:
760         case STEM_SIGN_ACCOUNTING_EXCEPT_ZERO:
761             checkNull(macros.sign, segment);
762             macros.sign = StemToObject.signDisplay(stem);
763             return ParseState.STATE_NULL;
764 
765         case STEM_DECIMAL_AUTO:
766         case STEM_DECIMAL_ALWAYS:
767             checkNull(macros.decimal, segment);
768             macros.decimal = StemToObject.decimalSeparatorDisplay(stem);
769             return ParseState.STATE_NULL;
770 
771         // Stems requiring an option:
772 
773         case STEM_PRECISION_INCREMENT:
774             checkNull(macros.precision, segment);
775             return ParseState.STATE_INCREMENT_PRECISION;
776 
777         case STEM_MEASURE_UNIT:
778             checkNull(macros.unit, segment);
779             return ParseState.STATE_MEASURE_UNIT;
780 
781         case STEM_PER_MEASURE_UNIT:
782             checkNull(macros.perUnit, segment);
783             return ParseState.STATE_PER_MEASURE_UNIT;
784 
785         case STEM_UNIT:
786             checkNull(macros.unit, segment);
787             checkNull(macros.perUnit, segment);
788             return ParseState.STATE_IDENTIFIER_UNIT;
789 
790         case STEM_CURRENCY:
791             checkNull(macros.unit, segment);
792             return ParseState.STATE_CURRENCY_UNIT;
793 
794         case STEM_INTEGER_WIDTH:
795             checkNull(macros.integerWidth, segment);
796             return ParseState.STATE_INTEGER_WIDTH;
797 
798         case STEM_NUMBERING_SYSTEM:
799             checkNull(macros.symbols, segment);
800             return ParseState.STATE_NUMBERING_SYSTEM;
801 
802         case STEM_SCALE:
803             checkNull(macros.scale, segment);
804             return ParseState.STATE_SCALE;
805 
806         default:
807             throw new AssertionError();
808         }
809     }
810 
811     /**
812      * Given that the current segment represents an option, parse it and save the result.
813      *
814      * @return The next state after parsing this option, corresponding to what subset of options to
815      *         expect next.
816      */
parseOption(ParseState stem, StringSegment segment, MacroProps macros)817     private static ParseState parseOption(ParseState stem, StringSegment segment, MacroProps macros) {
818 
819         ///// Required options: /////
820 
821         switch (stem) {
822         case STATE_CURRENCY_UNIT:
823             BlueprintHelpers.parseCurrencyOption(segment, macros);
824             return ParseState.STATE_NULL;
825         case STATE_MEASURE_UNIT:
826             BlueprintHelpers.parseMeasureUnitOption(segment, macros);
827             return ParseState.STATE_NULL;
828         case STATE_PER_MEASURE_UNIT:
829             BlueprintHelpers.parseMeasurePerUnitOption(segment, macros);
830             return ParseState.STATE_NULL;
831         case STATE_IDENTIFIER_UNIT:
832             BlueprintHelpers.parseIdentifierUnitOption(segment, macros);
833             return ParseState.STATE_NULL;
834         case STATE_INCREMENT_PRECISION:
835             BlueprintHelpers.parseIncrementOption(segment, macros);
836             return ParseState.STATE_NULL;
837         case STATE_INTEGER_WIDTH:
838             BlueprintHelpers.parseIntegerWidthOption(segment, macros);
839             return ParseState.STATE_NULL;
840         case STATE_NUMBERING_SYSTEM:
841             BlueprintHelpers.parseNumberingSystemOption(segment, macros);
842             return ParseState.STATE_NULL;
843         case STATE_SCALE:
844             BlueprintHelpers.parseScaleOption(segment, macros);
845             return ParseState.STATE_NULL;
846         default:
847             break;
848         }
849 
850         ///// Non-required options: /////
851 
852         // Scientific options
853         switch (stem) {
854         case STATE_SCIENTIFIC:
855             if (BlueprintHelpers.parseExponentWidthOption(segment, macros)) {
856                 return ParseState.STATE_SCIENTIFIC;
857             }
858             if (BlueprintHelpers.parseExponentSignOption(segment, macros)) {
859                 return ParseState.STATE_SCIENTIFIC;
860             }
861             break;
862         default:
863             break;
864         }
865 
866         // Frac-sig option
867         switch (stem) {
868         case STATE_FRACTION_PRECISION:
869             if (BlueprintHelpers.parseFracSigOption(segment, macros)) {
870                 return ParseState.STATE_NULL;
871             }
872             break;
873         default:
874             break;
875         }
876 
877         // Unknown option
878         throw new SkeletonSyntaxException("Invalid option", segment);
879     }
880 
881     ///// MAIN SKELETON GENERATION FUNCTION /////
882 
883     /**
884      * Main skeleton generator function. Appends the normalized skeleton for the MacroProps to the given
885      * StringBuilder.
886      */
generateSkeleton(MacroProps macros, StringBuilder sb)887     private static void generateSkeleton(MacroProps macros, StringBuilder sb) {
888         // Supported options
889         if (macros.notation != null && GeneratorHelpers.notation(macros, sb)) {
890             sb.append(' ');
891         }
892         if (macros.unit != null && GeneratorHelpers.unit(macros, sb)) {
893             sb.append(' ');
894         }
895         if (macros.perUnit != null && GeneratorHelpers.perUnit(macros, sb)) {
896             sb.append(' ');
897         }
898         if (macros.precision != null && GeneratorHelpers.precision(macros, sb)) {
899             sb.append(' ');
900         }
901         if (macros.roundingMode != null && GeneratorHelpers.roundingMode(macros, sb)) {
902             sb.append(' ');
903         }
904         if (macros.grouping != null && GeneratorHelpers.grouping(macros, sb)) {
905             sb.append(' ');
906         }
907         if (macros.integerWidth != null && GeneratorHelpers.integerWidth(macros, sb)) {
908             sb.append(' ');
909         }
910         if (macros.symbols != null && GeneratorHelpers.symbols(macros, sb)) {
911             sb.append(' ');
912         }
913         if (macros.unitWidth != null && GeneratorHelpers.unitWidth(macros, sb)) {
914             sb.append(' ');
915         }
916         if (macros.sign != null && GeneratorHelpers.sign(macros, sb)) {
917             sb.append(' ');
918         }
919         if (macros.decimal != null && GeneratorHelpers.decimal(macros, sb)) {
920             sb.append(' ');
921         }
922         if (macros.scale != null && GeneratorHelpers.scale(macros, sb)) {
923             sb.append(' ');
924         }
925 
926         // Unsupported options
927         if (macros.padder != null) {
928             throw new UnsupportedOperationException(
929                     "Cannot generate number skeleton with custom padder");
930         }
931         if (macros.affixProvider != null) {
932             throw new UnsupportedOperationException(
933                     "Cannot generate number skeleton with custom affix provider");
934         }
935         if (macros.rules != null) {
936             throw new UnsupportedOperationException(
937                     "Cannot generate number skeleton with custom plural rules");
938         }
939 
940         // Remove the trailing space
941         if (sb.length() > 0) {
942             sb.setLength(sb.length() - 1);
943         }
944     }
945 
946     ///// BLUEPRINT HELPER FUNCTIONS /////
947 
948     /**
949      * Utility class for methods for processing stems and options that cannot be interpreted literally.
950      */
951     static final class BlueprintHelpers {
952 
953         /** @return Whether we successfully found and parsed an exponent width option. */
parseExponentWidthOption(StringSegment segment, MacroProps macros)954         private static boolean parseExponentWidthOption(StringSegment segment, MacroProps macros) {
955             if (!isWildcardChar(segment.charAt(0))) {
956                 return false;
957             }
958             int offset = 1;
959             int minExp = 0;
960             for (; offset < segment.length(); offset++) {
961                 if (segment.charAt(offset) == 'e') {
962                     minExp++;
963                 } else {
964                     break;
965                 }
966             }
967             if (offset < segment.length()) {
968                 return false;
969             }
970             // Use the public APIs to enforce bounds checking
971             macros.notation = ((ScientificNotation) macros.notation).withMinExponentDigits(minExp);
972             return true;
973         }
974 
generateExponentWidthOption(int minExponentDigits, StringBuilder sb)975         private static void generateExponentWidthOption(int minExponentDigits, StringBuilder sb) {
976             sb.append(WILDCARD_CHAR);
977             appendMultiple(sb, 'e', minExponentDigits);
978         }
979 
980         /** @return Whether we successfully found and parsed an exponent sign option. */
parseExponentSignOption(StringSegment segment, MacroProps macros)981         private static boolean parseExponentSignOption(StringSegment segment, MacroProps macros) {
982             // Get the sign display type out of the CharsTrie data structure.
983             // TODO: Make this more efficient (avoid object allocation)? It shouldn't be very hot code.
984             CharsTrie tempStemTrie = new CharsTrie(SERIALIZED_STEM_TRIE, 0);
985             BytesTrie.Result result = tempStemTrie.next(segment, 0, segment.length());
986             if (result != BytesTrie.Result.INTERMEDIATE_VALUE
987                     && result != BytesTrie.Result.FINAL_VALUE) {
988                 return false;
989             }
990             SignDisplay sign = StemToObject.signDisplay(STEM_ENUM_VALUES[tempStemTrie.getValue()]);
991             if (sign == null) {
992                 return false;
993             }
994             macros.notation = ((ScientificNotation) macros.notation).withExponentSignDisplay(sign);
995             return true;
996         }
997 
parseCurrencyOption(StringSegment segment, MacroProps macros)998         private static void parseCurrencyOption(StringSegment segment, MacroProps macros) {
999             String currencyCode = segment.subSequence(0, segment.length()).toString();
1000             Currency currency;
1001             try {
1002                 currency = Currency.getInstance(currencyCode);
1003             } catch (IllegalArgumentException e) {
1004                 // Not 3 ascii chars
1005                 throw new SkeletonSyntaxException("Invalid currency", segment, e);
1006             }
1007             macros.unit = currency;
1008         }
1009 
generateCurrencyOption(Currency currency, StringBuilder sb)1010         private static void generateCurrencyOption(Currency currency, StringBuilder sb) {
1011             sb.append(currency.getCurrencyCode());
1012         }
1013 
parseMeasureUnitOption(StringSegment segment, MacroProps macros)1014         private static void parseMeasureUnitOption(StringSegment segment, MacroProps macros) {
1015             // NOTE: The category (type) of the unit is guaranteed to be a valid subtag (alphanumeric)
1016             // http://unicode.org/reports/tr35/#Validity_Data
1017             int firstHyphen = 0;
1018             while (firstHyphen < segment.length() && segment.charAt(firstHyphen) != '-') {
1019                 firstHyphen++;
1020             }
1021             if (firstHyphen == segment.length()) {
1022                 throw new SkeletonSyntaxException("Invalid measure unit option", segment);
1023             }
1024             String type = segment.subSequence(0, firstHyphen).toString();
1025             String subType = segment.subSequence(firstHyphen + 1, segment.length()).toString();
1026             Set<MeasureUnit> units = MeasureUnit.getAvailable(type);
1027             for (MeasureUnit unit : units) {
1028                 if (subType.equals(unit.getSubtype())) {
1029                     macros.unit = unit;
1030                     return;
1031                 }
1032             }
1033             throw new SkeletonSyntaxException("Unknown measure unit", segment);
1034         }
1035 
generateMeasureUnitOption(MeasureUnit unit, StringBuilder sb)1036         private static void generateMeasureUnitOption(MeasureUnit unit, StringBuilder sb) {
1037             sb.append(unit.getType());
1038             sb.append("-");
1039             sb.append(unit.getSubtype());
1040         }
1041 
parseMeasurePerUnitOption(StringSegment segment, MacroProps macros)1042         private static void parseMeasurePerUnitOption(StringSegment segment, MacroProps macros) {
1043             // A little bit of a hack: save the current unit (numerator), call the main measure unit
1044             // parsing code, put back the numerator unit, and put the new unit into per-unit.
1045             MeasureUnit numerator = macros.unit;
1046             parseMeasureUnitOption(segment, macros);
1047             macros.perUnit = macros.unit;
1048             macros.unit = numerator;
1049         }
1050 
parseIdentifierUnitOption(StringSegment segment, MacroProps macros)1051         private static void parseIdentifierUnitOption(StringSegment segment, MacroProps macros) {
1052             MeasureUnit[] units = MeasureUnit.parseCoreUnitIdentifier(segment.asString());
1053             if (units == null) {
1054                 throw new SkeletonSyntaxException("Invalid core unit identifier", segment);
1055             }
1056             macros.unit = units[0];
1057             if (units.length == 2) {
1058                 macros.perUnit = units[1];
1059             }
1060         }
1061 
parseFractionStem(StringSegment segment, MacroProps macros)1062         private static void parseFractionStem(StringSegment segment, MacroProps macros) {
1063             assert segment.charAt(0) == '.';
1064             int offset = 1;
1065             int minFrac = 0;
1066             int maxFrac;
1067             for (; offset < segment.length(); offset++) {
1068                 if (segment.charAt(offset) == '0') {
1069                     minFrac++;
1070                 } else {
1071                     break;
1072                 }
1073             }
1074             if (offset < segment.length()) {
1075                 if (isWildcardChar(segment.charAt(offset))) {
1076                     maxFrac = -1;
1077                     offset++;
1078                 } else {
1079                     maxFrac = minFrac;
1080                     for (; offset < segment.length(); offset++) {
1081                         if (segment.charAt(offset) == '#') {
1082                             maxFrac++;
1083                         } else {
1084                             break;
1085                         }
1086                     }
1087                 }
1088             } else {
1089                 maxFrac = minFrac;
1090             }
1091             if (offset < segment.length()) {
1092                 throw new SkeletonSyntaxException("Invalid fraction stem", segment);
1093             }
1094             // Use the public APIs to enforce bounds checking
1095             if (maxFrac == -1) {
1096                 if (minFrac == 0) {
1097                     macros.precision = Precision.unlimited();
1098                 } else {
1099                     macros.precision = Precision.minFraction(minFrac);
1100                 }
1101             } else {
1102                 macros.precision = Precision.minMaxFraction(minFrac, maxFrac);
1103             }
1104         }
1105 
generateFractionStem(int minFrac, int maxFrac, StringBuilder sb)1106         private static void generateFractionStem(int minFrac, int maxFrac, StringBuilder sb) {
1107             if (minFrac == 0 && maxFrac == 0) {
1108                 sb.append("precision-integer");
1109                 return;
1110             }
1111             sb.append('.');
1112             appendMultiple(sb, '0', minFrac);
1113             if (maxFrac == -1) {
1114                 sb.append(WILDCARD_CHAR);
1115             } else {
1116                 appendMultiple(sb, '#', maxFrac - minFrac);
1117             }
1118         }
1119 
parseDigitsStem(StringSegment segment, MacroProps macros)1120         private static void parseDigitsStem(StringSegment segment, MacroProps macros) {
1121             assert segment.charAt(0) == '@';
1122             int offset = 0;
1123             int minSig = 0;
1124             int maxSig;
1125             for (; offset < segment.length(); offset++) {
1126                 if (segment.charAt(offset) == '@') {
1127                     minSig++;
1128                 } else {
1129                     break;
1130                 }
1131             }
1132             if (offset < segment.length()) {
1133                 if (isWildcardChar(segment.charAt(offset))) {
1134                     maxSig = -1;
1135                     offset++;
1136                 } else {
1137                     maxSig = minSig;
1138                     for (; offset < segment.length(); offset++) {
1139                         if (segment.charAt(offset) == '#') {
1140                             maxSig++;
1141                         } else {
1142                             break;
1143                         }
1144                     }
1145                 }
1146             } else {
1147                 maxSig = minSig;
1148             }
1149             if (offset < segment.length()) {
1150                 throw new SkeletonSyntaxException("Invalid significant digits stem", segment);
1151             }
1152             // Use the public APIs to enforce bounds checking
1153             if (maxSig == -1) {
1154                 macros.precision = Precision.minSignificantDigits(minSig);
1155             } else {
1156                 macros.precision = Precision.minMaxSignificantDigits(minSig, maxSig);
1157             }
1158         }
1159 
generateDigitsStem(int minSig, int maxSig, StringBuilder sb)1160         private static void generateDigitsStem(int minSig, int maxSig, StringBuilder sb) {
1161             appendMultiple(sb, '@', minSig);
1162             if (maxSig == -1) {
1163                 sb.append(WILDCARD_CHAR);
1164             } else {
1165                 appendMultiple(sb, '#', maxSig - minSig);
1166             }
1167         }
1168 
parseScientificStem(StringSegment segment, MacroProps macros)1169         private static void parseScientificStem(StringSegment segment, MacroProps macros) {
1170             assert(segment.charAt(0) == 'E');
1171             block:
1172             {
1173                 int offset = 1;
1174                 if (segment.length() == offset) {
1175                     break block;
1176                 }
1177                 boolean isEngineering = false;
1178                 if (segment.charAt(offset) == 'E') {
1179                     isEngineering = true;
1180                     offset++;
1181                     if (segment.length() == offset) {
1182                         break block;
1183                     }
1184                 }
1185                 SignDisplay signDisplay = SignDisplay.AUTO;
1186                 if (segment.charAt(offset) == '+') {
1187                     offset++;
1188                     if (segment.length() == offset) {
1189                         break block;
1190                     }
1191                     if (segment.charAt(offset) == '!') {
1192                         signDisplay = SignDisplay.ALWAYS;
1193                     } else if (segment.charAt(offset) == '?') {
1194                         signDisplay = SignDisplay.EXCEPT_ZERO;
1195                     } else {
1196                         break block;
1197                     }
1198                     offset++;
1199                     if (segment.length() == offset) {
1200                         break block;
1201                     }
1202                 }
1203                 int minDigits = 0;
1204                 for (; offset < segment.length(); offset++) {
1205                     if (segment.charAt(offset) != '0') {
1206                         break block;
1207                     }
1208                     minDigits++;
1209                 }
1210                 macros.notation = (isEngineering ? Notation.engineering() : Notation.scientific())
1211                     .withExponentSignDisplay(signDisplay)
1212                     .withMinExponentDigits(minDigits);
1213                 return;
1214             }
1215             throw new SkeletonSyntaxException("Invalid scientific stem", segment);
1216         }
1217 
parseIntegerStem(StringSegment segment, MacroProps macros)1218         private static void parseIntegerStem(StringSegment segment, MacroProps macros) {
1219             assert(segment.charAt(0) == '0');
1220             int offset = 1;
1221             for (; offset < segment.length(); offset++) {
1222                 if (segment.charAt(offset) != '0') {
1223                     offset--;
1224                     break;
1225                 }
1226             }
1227             if (offset < segment.length()) {
1228                  throw new SkeletonSyntaxException("Invalid integer stem", segment);
1229             }
1230             macros.integerWidth = IntegerWidth.zeroFillTo(offset);
1231             return;
1232         }
1233 
1234         /** @return Whether we successfully found and parsed a frac-sig option. */
parseFracSigOption(StringSegment segment, MacroProps macros)1235         private static boolean parseFracSigOption(StringSegment segment, MacroProps macros) {
1236             if (segment.charAt(0) != '@') {
1237                 return false;
1238             }
1239             int offset = 0;
1240             int minSig = 0;
1241             int maxSig;
1242             for (; offset < segment.length(); offset++) {
1243                 if (segment.charAt(offset) == '@') {
1244                     minSig++;
1245                 } else {
1246                     break;
1247                 }
1248             }
1249             // For the frac-sig option, there must be minSig or maxSig but not both.
1250             // Valid: @+, @@+, @@@+
1251             // Valid: @#, @##, @###
1252             // Invalid: @, @@, @@@
1253             // Invalid: @@#, @@##, @@@#
1254             if (offset < segment.length()) {
1255                 if (isWildcardChar(segment.charAt(offset))) {
1256                     maxSig = -1;
1257                     offset++;
1258                 } else if (minSig > 1) {
1259                     // @@#, @@##, @@@#
1260                     throw new SkeletonSyntaxException("Invalid digits option for fraction rounder",
1261                             segment);
1262                 } else {
1263                     maxSig = minSig;
1264                     for (; offset < segment.length(); offset++) {
1265                         if (segment.charAt(offset) == '#') {
1266                             maxSig++;
1267                         } else {
1268                             break;
1269                         }
1270                     }
1271                 }
1272             } else {
1273                 // @, @@, @@@
1274                 throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
1275             }
1276             if (offset < segment.length()) {
1277                 throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
1278             }
1279 
1280             FractionPrecision oldRounder = (FractionPrecision) macros.precision;
1281             if (maxSig == -1) {
1282                 macros.precision = oldRounder.withMinDigits(minSig);
1283             } else {
1284                 macros.precision = oldRounder.withMaxDigits(maxSig);
1285             }
1286             return true;
1287         }
1288 
parseIncrementOption(StringSegment segment, MacroProps macros)1289         private static void parseIncrementOption(StringSegment segment, MacroProps macros) {
1290             // Call segment.subSequence() because segment.toString() doesn't create a clean string.
1291             String str = segment.subSequence(0, segment.length()).toString();
1292             BigDecimal increment;
1293             try {
1294                 increment = new BigDecimal(str);
1295             } catch (NumberFormatException e) {
1296                 throw new SkeletonSyntaxException("Invalid rounding increment", segment, e);
1297             }
1298             macros.precision = Precision.increment(increment);
1299         }
1300 
generateIncrementOption(BigDecimal increment, StringBuilder sb)1301         private static void generateIncrementOption(BigDecimal increment, StringBuilder sb) {
1302             sb.append(increment.toPlainString());
1303         }
1304 
parseIntegerWidthOption(StringSegment segment, MacroProps macros)1305         private static void parseIntegerWidthOption(StringSegment segment, MacroProps macros) {
1306             int offset = 0;
1307             int minInt = 0;
1308             int maxInt;
1309             if (isWildcardChar(segment.charAt(0))) {
1310                 maxInt = -1;
1311                 offset++;
1312             } else {
1313                 maxInt = 0;
1314             }
1315             for (; offset < segment.length(); offset++) {
1316                 if (maxInt != -1 && segment.charAt(offset) == '#') {
1317                     maxInt++;
1318                 } else {
1319                     break;
1320                 }
1321             }
1322             if (offset < segment.length()) {
1323                 for (; offset < segment.length(); offset++) {
1324                     if (segment.charAt(offset) == '0') {
1325                         minInt++;
1326                     } else {
1327                         break;
1328                     }
1329                 }
1330             }
1331             if (maxInt != -1) {
1332                 maxInt += minInt;
1333             }
1334             if (offset < segment.length()) {
1335                 throw new SkeletonSyntaxException("Invalid integer width stem", segment);
1336             }
1337             // Use the public APIs to enforce bounds checking
1338             if (maxInt == -1) {
1339                 macros.integerWidth = IntegerWidth.zeroFillTo(minInt);
1340             } else {
1341                 macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
1342             }
1343         }
1344 
generateIntegerWidthOption(int minInt, int maxInt, StringBuilder sb)1345         private static void generateIntegerWidthOption(int minInt, int maxInt, StringBuilder sb) {
1346             if (maxInt == -1) {
1347                 sb.append(WILDCARD_CHAR);
1348             } else {
1349                 appendMultiple(sb, '#', maxInt - minInt);
1350             }
1351             appendMultiple(sb, '0', minInt);
1352         }
1353 
parseNumberingSystemOption(StringSegment segment, MacroProps macros)1354         private static void parseNumberingSystemOption(StringSegment segment, MacroProps macros) {
1355             String nsName = segment.subSequence(0, segment.length()).toString();
1356             NumberingSystem ns = NumberingSystem.getInstanceByName(nsName);
1357             if (ns == null) {
1358                 throw new SkeletonSyntaxException("Unknown numbering system", segment);
1359             }
1360             macros.symbols = ns;
1361         }
1362 
generateNumberingSystemOption(NumberingSystem ns, StringBuilder sb)1363         private static void generateNumberingSystemOption(NumberingSystem ns, StringBuilder sb) {
1364             sb.append(ns.getName());
1365         }
1366 
parseScaleOption(StringSegment segment, MacroProps macros)1367         private static void parseScaleOption(StringSegment segment, MacroProps macros) {
1368             // Call segment.subSequence() because segment.toString() doesn't create a clean string.
1369             String str = segment.subSequence(0, segment.length()).toString();
1370             BigDecimal bd;
1371             try {
1372                 bd = new BigDecimal(str);
1373             } catch (NumberFormatException e) {
1374                 throw new SkeletonSyntaxException("Invalid scale", segment, e);
1375             }
1376             // NOTE: If bd is a power of ten, the Scale API optimizes it for us.
1377             macros.scale = Scale.byBigDecimal(bd);
1378         }
1379 
generateScaleOption(Scale scale, StringBuilder sb)1380         private static void generateScaleOption(Scale scale, StringBuilder sb) {
1381             BigDecimal bd = scale.arbitrary;
1382             if (bd == null) {
1383                 bd = BigDecimal.ONE;
1384             }
1385             bd = bd.scaleByPowerOfTen(scale.magnitude);
1386             sb.append(bd.toPlainString());
1387         }
1388     }
1389 
1390     ///// STEM GENERATION HELPER FUNCTIONS /////
1391 
1392     /**
1393      * Utility class for methods for generating a token corresponding to each macro-prop. Each method
1394      * returns whether or not a token was written to the string builder.
1395      */
1396     static final class GeneratorHelpers {
1397 
notation(MacroProps macros, StringBuilder sb)1398         private static boolean notation(MacroProps macros, StringBuilder sb) {
1399             if (macros.notation instanceof CompactNotation) {
1400                 if (macros.notation == Notation.compactLong()) {
1401                     sb.append("compact-long");
1402                     return true;
1403                 } else if (macros.notation == Notation.compactShort()) {
1404                     sb.append("compact-short");
1405                     return true;
1406                 } else {
1407                     // Compact notation generated from custom data (not supported in skeleton)
1408                     // The other compact notations are literals
1409                     throw new UnsupportedOperationException(
1410                             "Cannot generate number skeleton with custom compact data");
1411                 }
1412             } else if (macros.notation instanceof ScientificNotation) {
1413                 ScientificNotation impl = (ScientificNotation) macros.notation;
1414                 if (impl.engineeringInterval == 3) {
1415                     sb.append("engineering");
1416                 } else {
1417                     sb.append("scientific");
1418                 }
1419                 if (impl.minExponentDigits > 1) {
1420                     sb.append('/');
1421                     BlueprintHelpers.generateExponentWidthOption(impl.minExponentDigits, sb);
1422                 }
1423                 if (impl.exponentSignDisplay != SignDisplay.AUTO) {
1424                     sb.append('/');
1425                     EnumToStemString.signDisplay(impl.exponentSignDisplay, sb);
1426                 }
1427                 return true;
1428             } else {
1429                 assert macros.notation instanceof SimpleNotation;
1430                 // Default value is not shown in normalized form
1431                 return false;
1432             }
1433         }
1434 
unit(MacroProps macros, StringBuilder sb)1435         private static boolean unit(MacroProps macros, StringBuilder sb) {
1436             if (macros.unit instanceof Currency) {
1437                 sb.append("currency/");
1438                 BlueprintHelpers.generateCurrencyOption((Currency) macros.unit, sb);
1439                 return true;
1440             } else if (macros.unit instanceof NoUnit) {
1441                 if (macros.unit == NoUnit.PERCENT) {
1442                     sb.append("percent");
1443                     return true;
1444                 } else if (macros.unit == NoUnit.PERMILLE) {
1445                     sb.append("permille");
1446                     return true;
1447                 } else {
1448                     assert macros.unit == NoUnit.BASE;
1449                     // Default value is not shown in normalized form
1450                     return false;
1451                 }
1452             } else {
1453                 sb.append("measure-unit/");
1454                 BlueprintHelpers.generateMeasureUnitOption(macros.unit, sb);
1455                 return true;
1456             }
1457         }
1458 
perUnit(MacroProps macros, StringBuilder sb)1459         private static boolean perUnit(MacroProps macros, StringBuilder sb) {
1460             // Per-units are currently expected to be only MeasureUnits.
1461             if (macros.perUnit instanceof Currency || macros.perUnit instanceof NoUnit) {
1462                 throw new UnsupportedOperationException(
1463                         "Cannot generate number skeleton with per-unit that is not a standard measure unit");
1464             } else {
1465                 sb.append("per-measure-unit/");
1466                 BlueprintHelpers.generateMeasureUnitOption(macros.perUnit, sb);
1467                 return true;
1468             }
1469         }
1470 
precision(MacroProps macros, StringBuilder sb)1471         private static boolean precision(MacroProps macros, StringBuilder sb) {
1472             if (macros.precision instanceof Precision.InfiniteRounderImpl) {
1473                 sb.append("precision-unlimited");
1474             } else if (macros.precision instanceof Precision.FractionRounderImpl) {
1475                 Precision.FractionRounderImpl impl = (Precision.FractionRounderImpl) macros.precision;
1476                 BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
1477             } else if (macros.precision instanceof Precision.SignificantRounderImpl) {
1478                 Precision.SignificantRounderImpl impl = (Precision.SignificantRounderImpl) macros.precision;
1479                 BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
1480             } else if (macros.precision instanceof Precision.FracSigRounderImpl) {
1481                 Precision.FracSigRounderImpl impl = (Precision.FracSigRounderImpl) macros.precision;
1482                 BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
1483                 sb.append('/');
1484                 if (impl.minSig == -1) {
1485                     BlueprintHelpers.generateDigitsStem(1, impl.maxSig, sb);
1486                 } else {
1487                     BlueprintHelpers.generateDigitsStem(impl.minSig, -1, sb);
1488                 }
1489             } else if (macros.precision instanceof Precision.IncrementRounderImpl) {
1490                 Precision.IncrementRounderImpl impl = (Precision.IncrementRounderImpl) macros.precision;
1491                 sb.append("precision-increment/");
1492                 BlueprintHelpers.generateIncrementOption(impl.increment, sb);
1493             } else {
1494                 assert macros.precision instanceof Precision.CurrencyRounderImpl;
1495                 Precision.CurrencyRounderImpl impl = (Precision.CurrencyRounderImpl) macros.precision;
1496                 if (impl.usage == CurrencyUsage.STANDARD) {
1497                     sb.append("precision-currency-standard");
1498                 } else {
1499                     sb.append("precision-currency-cash");
1500                 }
1501             }
1502 
1503             // NOTE: Always return true for rounding because the default value depends on other options.
1504             return true;
1505         }
1506 
roundingMode(MacroProps macros, StringBuilder sb)1507         private static boolean roundingMode(MacroProps macros, StringBuilder sb) {
1508             if (macros.roundingMode == RoundingUtils.DEFAULT_ROUNDING_MODE) {
1509                 return false; // Default value
1510             }
1511             EnumToStemString.roundingMode(macros.roundingMode, sb);
1512             return true;
1513         }
1514 
grouping(MacroProps macros, StringBuilder sb)1515         private static boolean grouping(MacroProps macros, StringBuilder sb) {
1516             if (macros.grouping instanceof GroupingStrategy) {
1517                 if (macros.grouping == GroupingStrategy.AUTO) {
1518                     return false; // Default value
1519                 }
1520                 EnumToStemString.groupingStrategy((GroupingStrategy) macros.grouping, sb);
1521                 return true;
1522             } else {
1523                 throw new UnsupportedOperationException(
1524                         "Cannot generate number skeleton with custom Grouper");
1525             }
1526         }
1527 
integerWidth(MacroProps macros, StringBuilder sb)1528         private static boolean integerWidth(MacroProps macros, StringBuilder sb) {
1529             if (macros.integerWidth.equals(IntegerWidth.DEFAULT)) {
1530                 return false; // Default
1531             }
1532             sb.append("integer-width/");
1533             BlueprintHelpers.generateIntegerWidthOption(macros.integerWidth.minInt,
1534                     macros.integerWidth.maxInt,
1535                     sb);
1536             return true;
1537         }
1538 
symbols(MacroProps macros, StringBuilder sb)1539         private static boolean symbols(MacroProps macros, StringBuilder sb) {
1540             if (macros.symbols instanceof NumberingSystem) {
1541                 NumberingSystem ns = (NumberingSystem) macros.symbols;
1542                 if (ns.getName().equals("latn")) {
1543                     sb.append("latin");
1544                 } else {
1545                     sb.append("numbering-system/");
1546                     BlueprintHelpers.generateNumberingSystemOption(ns, sb);
1547                 }
1548                 return true;
1549             } else {
1550                 assert macros.symbols instanceof DecimalFormatSymbols;
1551                 throw new UnsupportedOperationException(
1552                         "Cannot generate number skeleton with custom DecimalFormatSymbols");
1553             }
1554         }
1555 
unitWidth(MacroProps macros, StringBuilder sb)1556         private static boolean unitWidth(MacroProps macros, StringBuilder sb) {
1557             if (macros.unitWidth == UnitWidth.SHORT) {
1558                 return false; // Default value
1559             }
1560             EnumToStemString.unitWidth(macros.unitWidth, sb);
1561             return true;
1562         }
1563 
sign(MacroProps macros, StringBuilder sb)1564         private static boolean sign(MacroProps macros, StringBuilder sb) {
1565             if (macros.sign == SignDisplay.AUTO) {
1566                 return false; // Default value
1567             }
1568             EnumToStemString.signDisplay(macros.sign, sb);
1569             return true;
1570         }
1571 
decimal(MacroProps macros, StringBuilder sb)1572         private static boolean decimal(MacroProps macros, StringBuilder sb) {
1573             if (macros.decimal == DecimalSeparatorDisplay.AUTO) {
1574                 return false; // Default value
1575             }
1576             EnumToStemString.decimalSeparatorDisplay(macros.decimal, sb);
1577             return true;
1578         }
1579 
scale(MacroProps macros, StringBuilder sb)1580         private static boolean scale(MacroProps macros, StringBuilder sb) {
1581             if (!macros.scale.isValid()) {
1582                 return false; // Default value
1583             }
1584             sb.append("scale/");
1585             BlueprintHelpers.generateScaleOption(macros.scale, sb);
1586             return true;
1587         }
1588 
1589     }
1590 
1591     ///// OTHER UTILITY FUNCTIONS /////
1592 
checkNull(Object value, CharSequence content)1593     private static void checkNull(Object value, CharSequence content) {
1594         if (value != null) {
1595             throw new SkeletonSyntaxException("Duplicated setting", content);
1596         }
1597     }
1598 
appendMultiple(StringBuilder sb, int cp, int count)1599     private static void appendMultiple(StringBuilder sb, int cp, int count) {
1600         for (int i = 0; i < count; i++) {
1601             sb.appendCodePoint(cp);
1602         }
1603     }
1604 }
1605