| /* GENERATED SOURCE. DO NOT MODIFY. */ |
| // © 2018 and later: Unicode, Inc. and others. |
| // License & terms of use: http://www.unicode.org/copyright.html |
| package android.icu.impl.number.parse; |
| |
| import java.util.Iterator; |
| |
| import android.icu.impl.StandardPlural; |
| import android.icu.impl.StringSegment; |
| import android.icu.impl.TextTrieMap; |
| import android.icu.text.DecimalFormatSymbols; |
| import android.icu.util.Currency; |
| import android.icu.util.Currency.CurrencyStringInfo; |
| |
| /** |
| * Matches a currency, either a custom currency or one from the data bundle. The class is called |
| * "combined" to emphasize that the currency string may come from one of multiple sources. |
| * |
| * Will match currency spacing either before or after the number depending on whether we are currently in |
| * the prefix or suffix. |
| * |
| * The implementation of this class is slightly different between J and C. See #13584 for a follow-up. |
| * |
| * @author sffc |
| * @hide Only a subset of ICU is exposed in Android |
| */ |
| public class CombinedCurrencyMatcher implements NumberParseMatcher { |
| |
| private final String isoCode; |
| private final String currency1; |
| private final String currency2; |
| |
| private final String[] localLongNames; |
| |
| private final String afterPrefixInsert; |
| private final String beforeSuffixInsert; |
| |
| private final TextTrieMap<CurrencyStringInfo> longNameTrie; |
| private final TextTrieMap<CurrencyStringInfo> symbolTrie; |
| |
| // TODO: See comments in constructor. |
| // private final UnicodeSet leadCodePoints; |
| |
| public static CombinedCurrencyMatcher getInstance(Currency currency, DecimalFormatSymbols dfs, int parseFlags) { |
| // TODO: Cache these instances. They are somewhat expensive. |
| return new CombinedCurrencyMatcher(currency, dfs, parseFlags); |
| } |
| |
| private CombinedCurrencyMatcher(Currency currency, DecimalFormatSymbols dfs, int parseFlags) { |
| this.isoCode = currency.getSubtype(); |
| this.currency1 = currency.getSymbol(dfs.getULocale()); |
| this.currency2 = currency.getCurrencyCode(); |
| |
| afterPrefixInsert = dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_INSERT, |
| false); |
| beforeSuffixInsert = dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_INSERT, |
| true); |
| |
| if (0 == (parseFlags & ParsingUtils.PARSE_FLAG_NO_FOREIGN_CURRENCIES)) { |
| // TODO: Currency trie does not currently have an option for case folding. It defaults to use |
| // case folding on long-names but not symbols. |
| longNameTrie = Currency.getParsingTrie(dfs.getULocale(), Currency.LONG_NAME); |
| symbolTrie = Currency.getParsingTrie(dfs.getULocale(), Currency.SYMBOL_NAME); |
| localLongNames = null; |
| |
| } else { |
| longNameTrie = null; |
| symbolTrie = null; |
| localLongNames = new String[StandardPlural.COUNT]; |
| for (int i = 0; i < StandardPlural.COUNT; i++) { |
| String pluralKeyword = StandardPlural.VALUES.get(i).getKeyword(); |
| localLongNames[i] = currency |
| .getName(dfs.getLocale(), Currency.PLURAL_LONG_NAME, pluralKeyword, null); |
| } |
| } |
| |
| // TODO: Figure out how to make this faster and re-enable. |
| // Computing the "lead code points" set for fastpathing is too slow to use in production. |
| // See https://unicode-org.atlassian.net/browse/ICU-13584 |
| // // Compute the full set of characters that could be the first in a currency to allow for |
| // // efficient smoke test. |
| // leadCodePoints = new UnicodeSet(); |
| // leadCodePoints.add(currency1.codePointAt(0)); |
| // leadCodePoints.add(currency2.codePointAt(0)); |
| // leadCodePoints.add(beforeSuffixInsert.codePointAt(0)); |
| // longNameTrie.putLeadCodePoints(leadCodePoints); |
| // symbolTrie.putLeadCodePoints(leadCodePoints); |
| // // Always apply case mapping closure for currencies |
| // leadCodePoints.closeOver(UnicodeSet.ADD_CASE_MAPPINGS); |
| // leadCodePoints.freeze(); |
| } |
| |
| @Override |
| public boolean match(StringSegment segment, ParsedNumber result) { |
| if (result.currencyCode != null) { |
| return false; |
| } |
| |
| // Try to match a currency spacing separator. |
| int initialOffset = segment.getOffset(); |
| boolean maybeMore = false; |
| if (result.seenNumber() && !beforeSuffixInsert.isEmpty()) { |
| int overlap = segment.getCommonPrefixLength(beforeSuffixInsert); |
| if (overlap == beforeSuffixInsert.length()) { |
| segment.adjustOffset(overlap); |
| // Note: let currency spacing be a weak match. Don't update chars consumed. |
| } |
| maybeMore = maybeMore || overlap == segment.length(); |
| } |
| |
| // Match the currency string, and reset if we didn't find one. |
| maybeMore = maybeMore || matchCurrency(segment, result); |
| if (result.currencyCode == null) { |
| segment.setOffset(initialOffset); |
| return maybeMore; |
| } |
| |
| // Try to match a currency spacing separator. |
| if (!result.seenNumber() && !afterPrefixInsert.isEmpty()) { |
| int overlap = segment.getCommonPrefixLength(afterPrefixInsert); |
| if (overlap == afterPrefixInsert.length()) { |
| segment.adjustOffset(overlap); |
| // Note: let currency spacing be a weak match. Don't update chars consumed. |
| } |
| maybeMore = maybeMore || overlap == segment.length(); |
| } |
| |
| return maybeMore; |
| } |
| |
| /** Matches the currency string without concern for currency spacing. */ |
| private boolean matchCurrency(StringSegment segment, ParsedNumber result) { |
| boolean maybeMore = false; |
| |
| int overlap1; |
| if (!currency1.isEmpty()) { |
| overlap1 = segment.getCaseSensitivePrefixLength(currency1); |
| } else { |
| overlap1 = -1; |
| } |
| maybeMore = maybeMore || overlap1 == segment.length(); |
| if (overlap1 == currency1.length()) { |
| result.currencyCode = isoCode; |
| segment.adjustOffset(overlap1); |
| result.setCharsConsumed(segment); |
| return maybeMore; |
| } |
| |
| int overlap2; |
| if (!currency2.isEmpty()) { |
| // ISO codes should be accepted case-insensitive. |
| // https://unicode-org.atlassian.net/browse/ICU-13696 |
| overlap2 = segment.getCommonPrefixLength(currency2); |
| } else { |
| overlap2 = -1; |
| } |
| maybeMore = maybeMore || overlap2 == segment.length(); |
| if (overlap2 == currency2.length()) { |
| result.currencyCode = isoCode; |
| segment.adjustOffset(overlap2); |
| result.setCharsConsumed(segment); |
| return maybeMore; |
| } |
| |
| if (longNameTrie != null) { |
| // Use the full currency data. |
| TextTrieMap.Output trieOutput = new TextTrieMap.Output(); |
| Iterator<CurrencyStringInfo> values = longNameTrie.get(segment, 0, trieOutput); |
| maybeMore = maybeMore || trieOutput.partialMatch; |
| if (values == null) { |
| values = symbolTrie.get(segment, 0, trieOutput); |
| maybeMore = maybeMore || trieOutput.partialMatch; |
| } |
| if (values != null) { |
| result.currencyCode = values.next().getISOCode(); |
| segment.adjustOffset(trieOutput.matchLength); |
| result.setCharsConsumed(segment); |
| return maybeMore; |
| } |
| |
| } else { |
| // Use the locale long names. |
| int longestFullMatch = 0; |
| for (int i=0; i<StandardPlural.COUNT; i++) { |
| String name = localLongNames[i]; |
| if (name.isEmpty()) { |
| continue; |
| } |
| int overlap = segment.getCommonPrefixLength(name); |
| if (overlap == name.length() && name.length() > longestFullMatch) { |
| longestFullMatch = name.length(); |
| } |
| maybeMore = maybeMore || overlap > 0; |
| } |
| if (longestFullMatch > 0) { |
| result.currencyCode = isoCode; |
| segment.adjustOffset(longestFullMatch); |
| result.setCharsConsumed(segment); |
| return maybeMore; |
| } |
| } |
| |
| // No match found. |
| return maybeMore; |
| } |
| |
| @Override |
| public boolean smokeTest(StringSegment segment) { |
| // TODO: See constructor |
| return true; |
| // return segment.startsWith(leadCodePoints); |
| } |
| |
| @Override |
| public void postProcess(ParsedNumber result) { |
| // No-op |
| } |
| |
| @Override |
| public String toString() { |
| return "<CombinedCurrencyMatcher " + isoCode + ">"; |
| } |
| |
| } |