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