• 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.parse;
5 
6 import java.text.ParsePosition;
7 import java.util.ArrayList;
8 import java.util.Collection;
9 import java.util.List;
10 
11 import ohos.global.icu.impl.StringSegment;
12 import ohos.global.icu.impl.number.AffixPatternProvider;
13 import ohos.global.icu.impl.number.AffixUtils;
14 import ohos.global.icu.impl.number.CustomSymbolCurrency;
15 import ohos.global.icu.impl.number.DecimalFormatProperties;
16 import ohos.global.icu.impl.number.DecimalFormatProperties.ParseMode;
17 import ohos.global.icu.impl.number.Grouper;
18 import ohos.global.icu.impl.number.PatternStringParser;
19 import ohos.global.icu.impl.number.PatternStringParser.ParsedPatternInfo;
20 import ohos.global.icu.impl.number.PropertiesAffixPatternProvider;
21 import ohos.global.icu.impl.number.RoundingUtils;
22 import ohos.global.icu.number.NumberFormatter.GroupingStrategy;
23 import ohos.global.icu.number.Scale;
24 import ohos.global.icu.text.DecimalFormatSymbols;
25 import ohos.global.icu.util.Currency;
26 import ohos.global.icu.util.CurrencyAmount;
27 import ohos.global.icu.util.ULocale;
28 
29 /**
30  * Primary number parsing implementation class.
31  *
32  * @author sffc
33  * @hide exposed on OHOS
34  *
35  */
36 public class NumberParserImpl {
37 
38     /**
39      * Creates a parser with most default options. Used for testing, not production.
40      */
createSimpleParser(ULocale locale, String pattern, int parseFlags)41     public static NumberParserImpl createSimpleParser(ULocale locale, String pattern, int parseFlags) {
42 
43         NumberParserImpl parser = new NumberParserImpl(parseFlags);
44         Currency currency = Currency.getInstance("USD");
45         DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
46         IgnorablesMatcher ignorables = IgnorablesMatcher.getInstance(parseFlags);
47 
48         AffixTokenMatcherFactory factory = new AffixTokenMatcherFactory();
49         factory.currency = currency;
50         factory.symbols = symbols;
51         factory.ignorables = ignorables;
52         factory.locale = locale;
53         factory.parseFlags = parseFlags;
54 
55         ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(pattern);
56         AffixMatcher.createMatchers(patternInfo, parser, factory, ignorables, parseFlags);
57 
58         Grouper grouper = Grouper.forStrategy(GroupingStrategy.AUTO).withLocaleData(locale, patternInfo);
59 
60         parser.addMatcher(ignorables);
61         parser.addMatcher(DecimalMatcher.getInstance(symbols, grouper, parseFlags));
62         parser.addMatcher(MinusSignMatcher.getInstance(symbols, false));
63         parser.addMatcher(PlusSignMatcher.getInstance(symbols, false));
64         parser.addMatcher(PercentMatcher.getInstance(symbols));
65         parser.addMatcher(PermilleMatcher.getInstance(symbols));
66         parser.addMatcher(NanMatcher.getInstance(symbols, parseFlags));
67         parser.addMatcher(InfinityMatcher.getInstance(symbols));
68         parser.addMatcher(PaddingMatcher.getInstance("@"));
69         parser.addMatcher(ScientificMatcher.getInstance(symbols, grouper));
70         parser.addMatcher(CombinedCurrencyMatcher.getInstance(currency, symbols, parseFlags));
71         parser.addMatcher(new RequireNumberValidator());
72 
73         parser.freeze();
74         return parser;
75     }
76 
77     /**
78      * Parses the string without returning a NumberParserImpl. Used for testing, not production.
79      */
parseStatic( String input, ParsePosition ppos, DecimalFormatProperties properties, DecimalFormatSymbols symbols)80     public static Number parseStatic(
81             String input,
82             ParsePosition ppos,
83             DecimalFormatProperties properties,
84             DecimalFormatSymbols symbols) {
85         NumberParserImpl parser = createParserFromProperties(properties, symbols, false);
86         ParsedNumber result = new ParsedNumber();
87         parser.parse(input, true, result);
88         if (result.success()) {
89             ppos.setIndex(result.charEnd);
90             return result.getNumber();
91         } else {
92             ppos.setErrorIndex(result.charEnd);
93             return null;
94         }
95     }
96 
97     /**
98      * Parses the string without returning a NumberParserImpl. Used for testing, not production.
99      */
parseStaticCurrency( String input, ParsePosition ppos, DecimalFormatProperties properties, DecimalFormatSymbols symbols)100     public static CurrencyAmount parseStaticCurrency(
101             String input,
102             ParsePosition ppos,
103             DecimalFormatProperties properties,
104             DecimalFormatSymbols symbols) {
105         NumberParserImpl parser = createParserFromProperties(properties, symbols, true);
106         ParsedNumber result = new ParsedNumber();
107         parser.parse(input, true, result);
108         if (result.success()) {
109             ppos.setIndex(result.charEnd);
110             assert result.currencyCode != null;
111             return new CurrencyAmount(result.getNumber(), Currency.getInstance(result.currencyCode));
112         } else {
113             ppos.setErrorIndex(result.charEnd);
114             return null;
115         }
116     }
117 
createDefaultParserForLocale(ULocale loc)118     public static NumberParserImpl createDefaultParserForLocale(ULocale loc) {
119         DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(loc);
120         DecimalFormatProperties properties = PatternStringParser.parseToProperties("0");
121         return createParserFromProperties(properties, symbols, false);
122     }
123 
124     /**
125      * Creates a parser from the given DecimalFormatProperties. This is the endpoint used by
126      * DecimalFormat in production code.
127      *
128      * @param properties
129      *            The property bag.
130      * @param symbols
131      *            The locale's symbols.
132      * @param parseCurrency
133      *            True to force a currency match and use monetary separators; false otherwise.
134      * @return An immutable parser object.
135      */
createParserFromProperties( DecimalFormatProperties properties, DecimalFormatSymbols symbols, boolean parseCurrency)136     public static NumberParserImpl createParserFromProperties(
137             DecimalFormatProperties properties,
138             DecimalFormatSymbols symbols,
139             boolean parseCurrency) {
140 
141         ULocale locale = symbols.getULocale();
142         AffixPatternProvider affixProvider = PropertiesAffixPatternProvider.forProperties(properties);
143         Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols);
144         ParseMode parseMode = properties.getParseMode();
145         if (parseMode == null) {
146             parseMode = ParseMode.LENIENT;
147         }
148         Grouper grouper = Grouper.forProperties(properties);
149         int parseFlags = 0;
150         if (!properties.getParseCaseSensitive()) {
151             parseFlags |= ParsingUtils.PARSE_FLAG_IGNORE_CASE;
152         }
153         if (properties.getParseIntegerOnly()) {
154             parseFlags |= ParsingUtils.PARSE_FLAG_INTEGER_ONLY;
155         }
156         if (properties.getParseToBigDecimal()) {
157             parseFlags |= ParsingUtils.PARSE_FLAG_FORCE_BIG_DECIMAL;
158         }
159         if (properties.getSignAlwaysShown()) {
160             parseFlags |= ParsingUtils.PARSE_FLAG_PLUS_SIGN_ALLOWED;
161         }
162         if (parseMode == ParseMode.JAVA_COMPATIBILITY) {
163             parseFlags |= ParsingUtils.PARSE_FLAG_STRICT_SEPARATORS;
164             parseFlags |= ParsingUtils.PARSE_FLAG_USE_FULL_AFFIXES;
165             parseFlags |= ParsingUtils.PARSE_FLAG_EXACT_AFFIX;
166             parseFlags |= ParsingUtils.PARSE_FLAG_JAVA_COMPATIBILITY_IGNORABLES;
167         } else if (parseMode == ParseMode.STRICT) {
168             parseFlags |= ParsingUtils.PARSE_FLAG_STRICT_GROUPING_SIZE;
169             parseFlags |= ParsingUtils.PARSE_FLAG_STRICT_SEPARATORS;
170             parseFlags |= ParsingUtils.PARSE_FLAG_USE_FULL_AFFIXES;
171             parseFlags |= ParsingUtils.PARSE_FLAG_EXACT_AFFIX;
172             parseFlags |= ParsingUtils.PARSE_FLAG_STRICT_IGNORABLES;
173         } else {
174             parseFlags |= ParsingUtils.PARSE_FLAG_INCLUDE_UNPAIRED_AFFIXES;
175         }
176         if (grouper.getPrimary() <= 0) {
177             parseFlags |= ParsingUtils.PARSE_FLAG_GROUPING_DISABLED;
178         }
179         if (parseCurrency || affixProvider.hasCurrencySign()) {
180             parseFlags |= ParsingUtils.PARSE_FLAG_MONETARY_SEPARATORS;
181         }
182         if (!parseCurrency) {
183             parseFlags |= ParsingUtils.PARSE_FLAG_NO_FOREIGN_CURRENCIES;
184         }
185 
186         NumberParserImpl parser = new NumberParserImpl(parseFlags);
187         IgnorablesMatcher ignorables = IgnorablesMatcher.getInstance(parseFlags);
188 
189         AffixTokenMatcherFactory factory = new AffixTokenMatcherFactory();
190         factory.currency = currency;
191         factory.symbols = symbols;
192         factory.ignorables = ignorables;
193         factory.locale = locale;
194         factory.parseFlags = parseFlags;
195 
196         //////////////////////
197         /// AFFIX MATCHERS ///
198         //////////////////////
199 
200         // Set up a pattern modifier with mostly defaults to generate AffixMatchers.
201         AffixMatcher.createMatchers(affixProvider, parser, factory, ignorables, parseFlags);
202 
203         ////////////////////////
204         /// CURRENCY MATCHER ///
205         ////////////////////////
206 
207         if (parseCurrency || affixProvider.hasCurrencySign()) {
208             parser.addMatcher(CombinedCurrencyMatcher.getInstance(currency, symbols, parseFlags));
209         }
210 
211         ///////////////
212         /// PERCENT ///
213         ///////////////
214 
215         // ICU-TC meeting, April 11, 2018: accept percent/permille only if it is in the pattern,
216         // and to maintain regressive behavior, divide by 100 even if no percent sign is present.
217         if (parseMode == ParseMode.LENIENT && affixProvider.containsSymbolType(AffixUtils.TYPE_PERCENT)) {
218             parser.addMatcher(PercentMatcher.getInstance(symbols));
219         }
220         if (parseMode == ParseMode.LENIENT && affixProvider.containsSymbolType(AffixUtils.TYPE_PERMILLE)) {
221             parser.addMatcher(PermilleMatcher.getInstance(symbols));
222         }
223 
224         ///////////////////////////////
225         /// OTHER STANDARD MATCHERS ///
226         ///////////////////////////////
227 
228         if (parseMode == ParseMode.LENIENT) {
229             parser.addMatcher(PlusSignMatcher.getInstance(symbols, false));
230             parser.addMatcher(MinusSignMatcher.getInstance(symbols, false));
231         }
232         parser.addMatcher(NanMatcher.getInstance(symbols, parseFlags));
233         parser.addMatcher(InfinityMatcher.getInstance(symbols));
234         String padString = properties.getPadString();
235         if (padString != null && !ignorables.getSet().contains(padString)) {
236             parser.addMatcher(PaddingMatcher.getInstance(padString));
237         }
238         parser.addMatcher(ignorables);
239         parser.addMatcher(DecimalMatcher.getInstance(symbols, grouper, parseFlags));
240         // NOTE: parseNoExponent doesn't disable scientific parsing if we have a scientific formatter
241         if (!properties.getParseNoExponent() || properties.getMinimumExponentDigits() > 0) {
242             parser.addMatcher(ScientificMatcher.getInstance(symbols, grouper));
243         }
244 
245         //////////////////
246         /// VALIDATORS ///
247         //////////////////
248 
249         parser.addMatcher(new RequireNumberValidator());
250         if (parseMode != ParseMode.LENIENT) {
251             parser.addMatcher(new RequireAffixValidator());
252         }
253         if (parseCurrency) {
254             parser.addMatcher(new RequireCurrencyValidator());
255         }
256         if (properties.getDecimalPatternMatchRequired()) {
257             boolean patternHasDecimalSeparator = properties.getDecimalSeparatorAlwaysShown()
258                     || properties.getMaximumFractionDigits() != 0;
259             parser.addMatcher(RequireDecimalSeparatorValidator.getInstance(patternHasDecimalSeparator));
260         }
261         // The multiplier takes care of scaling percentages.
262         Scale multiplier = RoundingUtils.scaleFromProperties(properties);
263         if (multiplier != null) {
264             parser.addMatcher(new MultiplierParseHandler(multiplier));
265         }
266 
267         parser.freeze();
268         return parser;
269     }
270 
271     private final int parseFlags;
272     private final List<NumberParseMatcher> matchers;
273     private boolean frozen;
274 
275     /**
276      * Creates a new, empty parser.
277      *
278      * @param parseFlags
279      *            The parser settings defined in the PARSE_FLAG_* fields.
280      */
NumberParserImpl(int parseFlags)281     public NumberParserImpl(int parseFlags) {
282         matchers = new ArrayList<>();
283         this.parseFlags = parseFlags;
284         frozen = false;
285     }
286 
addMatcher(NumberParseMatcher matcher)287     public void addMatcher(NumberParseMatcher matcher) {
288         assert !frozen;
289         this.matchers.add(matcher);
290     }
291 
addMatchers(Collection<? extends NumberParseMatcher> matchers)292     public void addMatchers(Collection<? extends NumberParseMatcher> matchers) {
293         assert !frozen;
294         this.matchers.addAll(matchers);
295     }
296 
freeze()297     public void freeze() {
298         frozen = true;
299     }
300 
getParseFlags()301     public int getParseFlags() {
302         return parseFlags;
303     }
304 
parse(String input, boolean greedy, ParsedNumber result)305     public void parse(String input, boolean greedy, ParsedNumber result) {
306         parse(input, 0, greedy, result);
307     }
308 
309     /**
310      * Primary entrypoint to parsing code path.
311      *
312      * @param input
313      *            The string to parse. This is a String, not CharSequence, to enforce assumptions about
314      *            immutability (CharSequences are not guaranteed to be immutable).
315      * @param start
316      *            The index into the string at which to start parsing.
317      * @param greedy
318      *            Whether to use the faster but potentially less accurate greedy code path.
319      * @param result
320      *            Output variable to store results.
321      */
parse(String input, int start, boolean greedy, ParsedNumber result)322     public void parse(String input, int start, boolean greedy, ParsedNumber result) {
323         assert frozen;
324         assert start >= 0 && start < input.length();
325         StringSegment segment = new StringSegment(input,
326                 0 != (parseFlags & ParsingUtils.PARSE_FLAG_IGNORE_CASE));
327         segment.adjustOffset(start);
328         if (greedy) {
329             parseGreedy(segment, result);
330         } else if (0 != (parseFlags & ParsingUtils.PARSE_FLAG_ALLOW_INFINITE_RECURSION)) {
331             // Start at 1 so that recursionLevels never gets to 0
332             parseLongestRecursive(segment, result, 1);
333         } else {
334             // Arbitrary recursion safety limit: 100 levels.
335             parseLongestRecursive(segment, result, -100);
336         }
337         for (NumberParseMatcher matcher : matchers) {
338             matcher.postProcess(result);
339         }
340         result.postProcess();
341     }
342 
343     private void parseGreedy(StringSegment segment, ParsedNumber result) {
344         // Note: this method is not recursive in order to avoid stack overflow.
345         for (int i = 0; i < matchers.size();) {
346             // Base Case
347             if (segment.length() == 0) {
348                 return;
349             }
350             NumberParseMatcher matcher = matchers.get(i);
351             if (!matcher.smokeTest(segment)) {
352                 // Matcher failed smoke test: try the next one
353                 i++;
354                 continue;
355             }
356             int initialOffset = segment.getOffset();
357             matcher.match(segment, result);
358             if (segment.getOffset() != initialOffset) {
359                 // Greedy heuristic: accept the match and loop back
360                 i = 0;
361                 continue;
362             } else {
363                 // Matcher did not match: try the next one
364                 i++;
365                 continue;
366             }
367         }
368 
369         // NOTE: If we get here, the greedy parse completed without consuming the entire string.
370     }
371 
372     private void parseLongestRecursive(StringSegment segment, ParsedNumber result, int recursionLevels) {
373         // Base Case
374         if (segment.length() == 0) {
375             return;
376         }
377 
378         // Safety against stack overflow
379         if (recursionLevels == 0) {
380             return;
381         }
382 
383         // TODO: Give a nice way for the matcher to reset the ParsedNumber?
384         ParsedNumber initial = new ParsedNumber();
385         initial.copyFrom(result);
386         ParsedNumber candidate = new ParsedNumber();
387 
388         int initialOffset = segment.getOffset();
389         for (int i = 0; i < matchers.size(); i++) {
390             NumberParseMatcher matcher = matchers.get(i);
391             if (!matcher.smokeTest(segment)) {
392                 continue;
393             }
394 
395             // In a non-greedy parse, we attempt all possible matches and pick the best.
396             for (int charsToConsume = 0; charsToConsume < segment.length();) {
397                 charsToConsume += Character.charCount(segment.codePointAt(charsToConsume));
398 
399                 // Run the matcher on a segment of the current length.
400                 candidate.copyFrom(initial);
401                 segment.setLength(charsToConsume);
402                 boolean maybeMore = matcher.match(segment, candidate);
403                 segment.resetLength();
404 
405                 // If the entire segment was consumed, recurse.
406                 if (segment.getOffset() - initialOffset == charsToConsume) {
407                     parseLongestRecursive(segment, candidate, recursionLevels + 1);
408                     if (candidate.isBetterThan(result)) {
409                         result.copyFrom(candidate);
410                     }
411                 }
412 
413                 // Since the segment can be re-used, reset the offset.
414                 // This does not have an effect if the matcher did not consume any chars.
415                 segment.setOffset(initialOffset);
416 
417                 // Unless the matcher wants to see the next char, continue to the next matcher.
418                 if (!maybeMore) {
419                     break;
420                 }
421             }
422         }
423     }
424 
425     @Override
426     public String toString() {
427         return "<NumberParserImpl matchers=" + matchers.toString() + ">";
428     }
429 }
430