--- /dev/null
+// ยฉ 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo;
+import com.ibm.icu.text.CurrencyPluralInfo;
+
+public class CurrencyPluralInfoAffixProvider implements AffixPatternProvider {
+ private final AffixPatternProvider[] affixesByPlural;
+
+ public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) {
+ affixesByPlural = new ParsedPatternInfo[StandardPlural.COUNT];
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ affixesByPlural[plural.ordinal()] = PatternStringParser
+ .parseToPatternInfo(cpi.getCurrencyPluralPattern(plural.getKeyword()));
+ }
+ }
+
+ @Override
+ public char charAt(int flags, int i) {
+ int pluralOrdinal = (flags & Flags.PLURAL_MASK);
+ return affixesByPlural[pluralOrdinal].charAt(flags, i);
+ }
+
+ @Override
+ public int length(int flags) {
+ int pluralOrdinal = (flags & Flags.PLURAL_MASK);
+ return affixesByPlural[pluralOrdinal].length(flags);
+ }
+
+ @Override
+ public boolean positiveHasPlusSign() {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].positiveHasPlusSign();
+ }
+
+ @Override
+ public boolean hasNegativeSubpattern() {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].hasNegativeSubpattern();
+ }
+
+ @Override
+ public boolean negativeHasMinusSign() {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].negativeHasMinusSign();
+ }
+
+ @Override
+ public boolean hasCurrencySign() {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].hasCurrencySign();
+ }
+
+ @Override
+ public boolean containsSymbolType(int type) {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].containsSymbolType(type);
+ }
+}
\ No newline at end of file
--- /dev/null
+// ยฉ 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+public class PropertiesAffixPatternProvider implements AffixPatternProvider {
+ private final String posPrefix;
+ private final String posSuffix;
+ private final String negPrefix;
+ private final String negSuffix;
+
+ public PropertiesAffixPatternProvider(DecimalFormatProperties properties) {
+ // There are two ways to set affixes in DecimalFormat: via the pattern string (applyPattern), and via the
+ // explicit setters (setPositivePrefix and friends). The way to resolve the settings is as follows:
+ //
+ // 1) If the explicit setting is present for the field, use it.
+ // 2) Otherwise, follows UTS 35 rules based on the pattern string.
+ //
+ // Importantly, the explicit setters affect only the one field they override. If you set the positive
+ // prefix, that should not affect the negative prefix. Since it is impossible for the user of this class
+ // to know whether the origin for a string was the override or the pattern, we have to say that we always
+ // have a negative subpattern and perform all resolution logic here.
+
+ // Convenience: Extract the properties into local variables.
+ // Variables are named with three chars: [p/n][p/s][o/p]
+ // [p/n] => p for positive, n for negative
+ // [p/s] => p for prefix, s for suffix
+ // [o/p] => o for escaped custom override string, p for pattern string
+ String ppo = AffixUtils.escape(properties.getPositivePrefix());
+ String pso = AffixUtils.escape(properties.getPositiveSuffix());
+ String npo = AffixUtils.escape(properties.getNegativePrefix());
+ String nso = AffixUtils.escape(properties.getNegativeSuffix());
+ String ppp = properties.getPositivePrefixPattern();
+ String psp = properties.getPositiveSuffixPattern();
+ String npp = properties.getNegativePrefixPattern();
+ String nsp = properties.getNegativeSuffixPattern();
+
+ if (ppo != null) {
+ posPrefix = ppo;
+ } else if (ppp != null) {
+ posPrefix = ppp;
+ } else {
+ // UTS 35: Default positive prefix is empty string.
+ posPrefix = "";
+ }
+
+ if (pso != null) {
+ posSuffix = pso;
+ } else if (psp != null) {
+ posSuffix = psp;
+ } else {
+ // UTS 35: Default positive suffix is empty string.
+ posSuffix = "";
+ }
+
+ if (npo != null) {
+ negPrefix = npo;
+ } else if (npp != null) {
+ negPrefix = npp;
+ } else {
+ // UTS 35: Default negative prefix is "-" with positive prefix.
+ // Important: We prepend the "-" to the pattern, not the override!
+ negPrefix = ppp == null ? "-" : "-" + ppp;
+ }
+
+ if (nso != null) {
+ negSuffix = nso;
+ } else if (nsp != null) {
+ negSuffix = nsp;
+ } else {
+ // UTS 35: Default negative prefix is the positive prefix.
+ negSuffix = psp == null ? "" : psp;
+ }
+ }
+
+ @Override
+ public char charAt(int flags, int i) {
+ return getStringForFlags(flags).charAt(i);
+ }
+
+ @Override
+ public int length(int flags) {
+ return getStringForFlags(flags).length();
+ }
+
+ private String getStringForFlags(int flags) {
+ boolean prefix = (flags & Flags.PREFIX) != 0;
+ boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
+ if (prefix && negative) {
+ return negPrefix;
+ } else if (prefix) {
+ return posPrefix;
+ } else if (negative) {
+ return negSuffix;
+ } else {
+ return posSuffix;
+ }
+ }
+
+ @Override
+ public boolean positiveHasPlusSign() {
+ return AffixUtils.containsType(posPrefix, AffixUtils.TYPE_PLUS_SIGN)
+ || AffixUtils.containsType(posSuffix, AffixUtils.TYPE_PLUS_SIGN);
+ }
+
+ @Override
+ public boolean hasNegativeSubpattern() {
+ // See comments in the constructor for more information on why this is always true.
+ return true;
+ }
+
+ @Override
+ public boolean negativeHasMinusSign() {
+ return AffixUtils.containsType(negPrefix, AffixUtils.TYPE_MINUS_SIGN)
+ || AffixUtils.containsType(negSuffix, AffixUtils.TYPE_MINUS_SIGN);
+ }
+
+ @Override
+ public boolean hasCurrencySign() {
+ return AffixUtils.hasCurrencySymbols(posPrefix) || AffixUtils.hasCurrencySymbols(posSuffix)
+ || AffixUtils.hasCurrencySymbols(negPrefix) || AffixUtils.hasCurrencySymbols(negSuffix);
+ }
+
+ @Override
+ public boolean containsSymbolType(int type) {
+ return AffixUtils.containsType(posPrefix, type) || AffixUtils.containsType(posSuffix, type)
+ || AffixUtils.containsType(negPrefix, type) || AffixUtils.containsType(negSuffix, type);
+ }
+}
\ No newline at end of file
*/
public class DecimalMatcher implements NumberParseMatcher {
- /**
- * @return
- */
+ // TODO: Re-generate these sets from the database. They probably haven't been updated in a while.
+ private static final UnicodeSet UNISET_PERIOD_LIKE = new UnicodeSet("[.\\u2024\\u3002\\uFE12\\uFE52\\uFF0E\\uFF61]")
+ .freeze();
+ private static final UnicodeSet UNISET_STRICT_PERIOD_LIKE = new UnicodeSet("[.\\u2024\\uFE52\\uFF0E\\uFF61]")
+ .freeze();
+ private static final UnicodeSet UNISET_COMMA_LIKE = new UnicodeSet(
+ "[,\\u060C\\u066B\\u3001\\uFE10\\uFE11\\uFE50\\uFE51\\uFF0C\\uFF64]").freeze();
+ private static final UnicodeSet UNISET_STRICT_COMMA_LIKE = new UnicodeSet("[,\\u066B\\uFE10\\uFE50\\uFF0C]")
+ .freeze();
+ private static final UnicodeSet UNISET_OTHER_GROUPING_SEPARATORS = new UnicodeSet(
+ "[\\ '\\u00A0\\u066C\\u2000-\\u200A\\u2018\\u2019\\u202F\\u205F\\u3000\\uFF07]").freeze();
+
public static DecimalMatcher getInstance(DecimalFormatSymbols symbols) {
- // TODO(sffc): Auto-generated method stub
- return new DecimalMatcher(symbols.getDigitStrings(),
- new UnicodeSet("[,]").freeze(),
- new UnicodeSet("[.]").freeze(),
- false);
+ String groupingSeparator = symbols.getGroupingSeparatorString();
+ UnicodeSet groupingSet = UNISET_COMMA_LIKE.contains(groupingSeparator) ? UNISET_COMMA_LIKE.cloneAsThawed().addAll(UNISET_OTHER_GROUPING_SEPARATORS).freeze()
+ : UNISET_PERIOD_LIKE.contains(groupingSeparator) ? UNISET_PERIOD_LIKE.cloneAsThawed().addAll(UNISET_OTHER_GROUPING_SEPARATORS).freeze()
+ : UNISET_OTHER_GROUPING_SEPARATORS.contains(groupingSeparator)
+ ? UNISET_OTHER_GROUPING_SEPARATORS
+ : new UnicodeSet().addAll(groupingSeparator).freeze();
+
+ String decimalSeparator = symbols.getDecimalSeparatorString();
+ UnicodeSet decimalSet = UNISET_COMMA_LIKE.contains(decimalSeparator) ? UNISET_COMMA_LIKE
+ : UNISET_PERIOD_LIKE.contains(decimalSeparator) ? UNISET_PERIOD_LIKE
+ : new UnicodeSet().addAll(decimalSeparator).freeze();
+
+ return new DecimalMatcher(symbols.getDigitStrings(), groupingSet, decimalSet, false);
}
public static DecimalMatcher getExponentInstance(DecimalFormatSymbols symbols) {
- return new DecimalMatcher(symbols.getDigitStrings(),
- new UnicodeSet("[,]").freeze(),
- new UnicodeSet("[.]").freeze(),
- true);
+ return new DecimalMatcher(symbols
+ .getDigitStrings(), new UnicodeSet("[,]").freeze(), new UnicodeSet("[.]").freeze(), true);
}
private final String[] digitStrings;
private final UnicodeSet decimalUniSet;
private final UnicodeSet separatorSet;
public boolean requireGroupingMatch = false;
+ public boolean groupingEnabled = true;
private final int grouping1 = 3;
private final int grouping2 = 3;
private final boolean isScientific;
this.digitStrings = digitStrings;
this.groupingUniSet = groupingUniSet;
this.decimalUniSet = decimalUniSet;
- separatorSet = groupingUniSet.cloneAsThawed().addAll(decimalUniSet).freeze();
+ if (groupingEnabled) {
+ separatorSet = groupingUniSet.cloneAsThawed().addAll(decimalUniSet).freeze();
+ } else {
+ separatorSet = decimalUniSet;
+ }
this.isScientific = isScientific;
}
int currGroup = 0;
int separator = -1;
int lastSeparatorOffset = segment.getOffset();
+ int scientificAdjustment = 0;
boolean hasPartialPrefix = false;
boolean seenBothSeparators = false;
while (segment.length() > 0) {
// If found, save it in the DecimalQuantity or scientific adjustment.
if (digit >= 0) {
if (isScientific) {
- result.scientificAdjustment = digit + result.scientificAdjustment * 10;
+ scientificAdjustment = digit + scientificAdjustment * 10;
} else {
if (result.quantity == null) {
result.quantity = new DecimalQuantity_DualStorageBCD();
if (requireGroupingMatch && currGroup == 0) {
break;
}
- } else if (separator == cp && groupingUniSet.contains(cp)) {
+ } else if (groupingEnabled && separator == cp && groupingUniSet.contains(cp)) {
// Second or later grouping separator.
if (requireGroupingMatch && currGroup != grouping2) {
break;
}
- } else if (separator != cp && decimalUniSet.contains(cp)) {
- // Decimal separator.
+ } else if (groupingEnabled && separator != cp && decimalUniSet.contains(cp)) {
+ // Decimal separator after a grouping separator.
if (requireGroupingMatch && currGroup != grouping1) {
break;
}
break;
}
- if (seenBothSeparators || (separator != -1 && decimalUniSet.contains(separator))) {
+ if (isScientific) {
+ result.quantity.adjustMagnitude(scientificAdjustment);
+ } else if (seenBothSeparators || (separator != -1 && decimalUniSet.contains(separator))) {
result.quantity.adjustMagnitude(-currGroup);
- } else if (requireGroupingMatch && separator != -1 && groupingUniSet.contains(separator)
- && currGroup != grouping1) {
+ } else if (separator != -1
+ && ((requireGroupingMatch && groupingUniSet.contains(separator) && currGroup != grouping1)
+ || !groupingEnabled)) {
result.quantity.adjustMagnitude(-currGroup);
result.quantity.roundToMagnitude(0, RoundingUtils.mathContextUnlimited(RoundingMode.FLOOR));
segment.setOffset(lastSeparatorOffset);
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.parse;
+import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import com.ibm.icu.impl.number.AffixPatternProvider;
import com.ibm.icu.impl.number.AffixUtils;
+import com.ibm.icu.impl.number.CustomSymbolCurrency;
+import com.ibm.icu.impl.number.DecimalFormatProperties;
import com.ibm.icu.impl.number.MutablePatternModifier;
+import com.ibm.icu.impl.number.Parse.ParseMode;
import com.ibm.icu.impl.number.PatternStringParser;
+import com.ibm.icu.impl.number.PropertiesAffixPatternProvider;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.text.DecimalFormatSymbols;
*
*/
public class NumberParserImpl {
- public static NumberParserImpl createParserFromPattern(String pattern) {
+ public static NumberParserImpl createParserFromPattern(String pattern, boolean strictGrouping) {
+ // Temporary frontend for testing.
+
NumberParserImpl parser = new NumberParserImpl();
ULocale locale = ULocale.ENGLISH;
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
AffixPatternProvider provider = PatternStringParser.parseToPatternInfo(pattern);
mod.setPatternInfo(provider);
mod.setPatternAttributes(SignDisplay.AUTO, false);
- mod.setSymbols(symbols,
- Currency.getInstance("USD"),
- UnitWidth.FULL_NAME,
- null);
+ mod.setSymbols(symbols, Currency.getInstance("USD"), UnitWidth.FULL_NAME, null);
int flags = 0;
if (provider.containsSymbolType(AffixUtils.TYPE_PERCENT)) {
flags |= ParsedNumber.FLAG_PERCENT;
}
AffixMatcher.generateFromPatternModifier(mod, flags, parser);
- parser.addMatcher(DecimalMatcher.getInstance(symbols));
+ DecimalMatcher decimalMatcher = DecimalMatcher.getInstance(symbols);
+ decimalMatcher.requireGroupingMatch = strictGrouping;
+ parser.addMatcher(decimalMatcher);
parser.addMatcher(WhitespaceMatcher.getInstance());
parser.addMatcher(new MinusSignMatcher());
parser.addMatcher(new ScientificMatcher(symbols));
parser.addMatcher(new CurrencyMatcher(locale));
- parser.setComparator(new Comparator<ParsedNumber>() {
- @Override
- public int compare(ParsedNumber o1, ParsedNumber o2) {
- return o1.charsConsumed - o2.charsConsumed;
- }
- });
+ parser.freeze();
+ return parser;
+ }
+
+ public static Number parseStatic(String input,
+ ParsePosition ppos,
+ DecimalFormatProperties properties,
+ DecimalFormatSymbols symbols) {
+ NumberParserImpl parser = createParserFromProperties(properties, symbols);
+ ParsedNumber result = new ParsedNumber();
+ parser.parse(input, true, result);
+ ppos.setIndex(result.charsConsumed);
+ return result.getDouble();
+ }
+
+ public static NumberParserImpl createParserFromProperties(
+ DecimalFormatProperties properties,
+ DecimalFormatSymbols symbols) {
+ NumberParserImpl parser = new NumberParserImpl();
+ ULocale locale = symbols.getULocale();
+ Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols);
+
+ //////////////////////
+ /// AFFIX MATCHERS ///
+ //////////////////////
+
+ // Set up a pattern modifier with mostly defaults to generate AffixMatchers.
+ MutablePatternModifier mod = new MutablePatternModifier(false);
+ AffixPatternProvider provider = new PropertiesAffixPatternProvider(properties);
+ mod.setPatternInfo(provider);
+ mod.setPatternAttributes(SignDisplay.AUTO, false);
+ mod.setSymbols(symbols, currency, UnitWidth.SHORT, null);
+
+ // Figure out which flags correspond to this pattern modifier. Note: negatives are taken care of in the
+ // generateFromPatternModifier function.
+ int flags = 0;
+ if (provider.containsSymbolType(AffixUtils.TYPE_PERCENT)) {
+ flags |= ParsedNumber.FLAG_PERCENT;
+ }
+ if (provider.containsSymbolType(AffixUtils.TYPE_PERMILLE)) {
+ flags |= ParsedNumber.FLAG_PERMILLE;
+ }
+
+ AffixMatcher.generateFromPatternModifier(mod, flags, parser);
+
+ ///////////////////////////////
+ /// OTHER STANDARD MATCHERS ///
+ ///////////////////////////////
+
+ DecimalMatcher decimalMatcher = DecimalMatcher.getInstance(symbols);
+ decimalMatcher.groupingEnabled = properties.getGroupingSize() > 0;
+ decimalMatcher.requireGroupingMatch = properties.getParseMode() == ParseMode.STRICT;
+ parser.addMatcher(decimalMatcher);
+ parser.addMatcher(WhitespaceMatcher.getInstance());
+ parser.addMatcher(new MinusSignMatcher());
+ parser.addMatcher(new ScientificMatcher(symbols));
+
+ ////////////////////////
+ /// CURRENCY MATCHER ///
+ ////////////////////////
+
+ parser.addMatcher(new CurrencyMatcher(locale));
+
parser.freeze();
return parser;
}
public NumberParserImpl() {
matchers = new ArrayList<NumberParseMatcher>();
+ comparator = ParsedNumber.COMPARATOR; // default value
frozen = false;
}
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.parse;
-import com.ibm.icu.impl.number.DecimalQuantity;
+import java.util.Comparator;
+
import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
/**
public class ParsedNumber {
public DecimalQuantity_DualStorageBCD quantity = null;
+
+ /**
+ * The number of chars accepted during parsing. This is NOT necessarily the same as the StringSegment offset; "weak"
+ * chars, like whitespace, change the offset, but the charsConsumed is not touched until a "strong" char is
+ * encountered.
+ */
public int charsConsumed = 0;
+
+ /**
+ * Boolean flags (see constants below).
+ */
public int flags = 0;
+
+ /**
+ * The prefix string that got consumed.
+ */
public String prefix = null;
+
+ /**
+ * The suffix string that got consumed.
+ */
public String suffix = null;
- public int scientificAdjustment = 0;
+
+ /**
+ * The currency that got consumed.
+ */
public String currencyCode = null;
public static final int FLAG_NEGATIVE = 0x0001;
public static final int FLAG_PERCENT = 0x0002;
public static final int FLAG_PERMILLE = 0x0004;
+ /** A Comparator that favors ParsedNumbers with the most chars consumed. */
+ public static final Comparator<ParsedNumber> COMPARATOR = new Comparator<ParsedNumber>() {
+ @Override
+ public int compare(ParsedNumber o1, ParsedNumber o2) {
+ return o1.charsConsumed - o2.charsConsumed;
+ }
+ };
+
/**
* @param other
*/
flags = other.flags;
prefix = other.prefix;
suffix = other.suffix;
- scientificAdjustment = other.scientificAdjustment;
currencyCode = other.currencyCode;
}
}
public double getDouble() {
- DecimalQuantity copy = quantity.createCopy();
- copy.adjustMagnitude(scientificAdjustment);
- double d = copy.toDouble();
+ double d = quantity.toDouble();
if (0 != (flags & FLAG_NEGATIVE)) {
d = -d;
}
public class ScientificMatcher implements NumberParseMatcher {
private final String exponentSeparatorString;
+ private final String minusSignString;
private final DecimalMatcher exponentMatcher;
public ScientificMatcher(DecimalFormatSymbols symbols) {
exponentSeparatorString = symbols.getExponentSeparator();
+ minusSignString = symbols.getMinusSignString();
exponentMatcher = DecimalMatcher.getExponentInstance(symbols);
}
}
// First match the scientific separator, and then match another number after it.
- int overlap = segment.getCommonPrefixLength(exponentSeparatorString);
- if (overlap == exponentSeparatorString.length()) {
- // Full exponent separator match; try to match digits.
- segment.adjustOffset(overlap);
+ int overlap1 = segment.getCommonPrefixLength(exponentSeparatorString);
+ if (overlap1 == exponentSeparatorString.length()) {
+ // Full exponent separator match; allow a sign, and then try to match digits.
+ segment.adjustOffset(overlap1);
+ int overlap2 = segment.getCommonPrefixLength(minusSignString);
+ boolean sign = false;
+ if (overlap2 == minusSignString.length()) {
+ sign = true;
+ segment.adjustOffset(overlap2);
+ } else if (overlap2 == segment.length()) {
+ // Partial sign match
+ return true;
+ }
+
int digitsOffset = segment.getOffset();
+ int oldMagnitude = result.quantity.getMagnitude();
boolean digitsReturnValue = exponentMatcher.match(segment, result);
+ if (result.quantity.getMagnitude() != oldMagnitude && sign) {
+ result.quantity.adjustMagnitude(2*(oldMagnitude - result.quantity.getMagnitude()));
+ }
if (segment.getOffset() == digitsOffset) {
// No digits were matched; un-match the exponent separator.
- segment.adjustOffset(-overlap);
+ segment.adjustOffset(-overlap1);
}
return digitsReturnValue;
- } else if (overlap == segment.length()) {
+ } else if (overlap1 == segment.length()) {
// Partial exponent separator match
return true;
}
--- /dev/null
+// ยฉ 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.parse;
+
+/**
+ * @author sffc
+ *
+ */
+public class StrictMatcher implements NumberParseMatcher {
+
+ @Override
+ public boolean match(StringSegment segment, ParsedNumber result) {
+ return false;
+ }
+
+ @Override
+ public void postProcess(ParsedNumber result) {
+ if (result.prefix == null && result.suffix == null) {
+ // Do something?
+ }
+ }
+
+}
import java.math.BigDecimal;
import java.math.MathContext;
-import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.AffixPatternProvider;
-import com.ibm.icu.impl.number.AffixUtils;
+import com.ibm.icu.impl.number.CurrencyPluralInfoAffixProvider;
import com.ibm.icu.impl.number.CustomSymbolCurrency;
import com.ibm.icu.impl.number.DecimalFormatProperties;
import com.ibm.icu.impl.number.MacroProps;
import com.ibm.icu.impl.number.MultiplierImpl;
import com.ibm.icu.impl.number.Padder;
import com.ibm.icu.impl.number.PatternStringParser;
-import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo;
+import com.ibm.icu.impl.number.PropertiesAffixPatternProvider;
import com.ibm.icu.impl.number.RoundingUtils;
import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.Rounder.IncrementRounderImpl;
import com.ibm.icu.number.Rounder.SignificantRounderImpl;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
-import com.ibm.icu.text.CurrencyPluralInfo;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
* @param symbols
* The symbols associated with the property bag.
* @param exportedProperties
- * A property bag in which to store validated properties.
+ * A property bag in which to store validated properties. Used by some DecimalFormat getters.
* @return A new MacroProps containing all of the information in the Properties.
*/
public static MacroProps oldToNew(DecimalFormatProperties properties, DecimalFormatSymbols symbols,
return macros;
}
-
- private static class PropertiesAffixPatternProvider implements AffixPatternProvider {
- private final String posPrefix;
- private final String posSuffix;
- private final String negPrefix;
- private final String negSuffix;
-
- public PropertiesAffixPatternProvider(DecimalFormatProperties properties) {
- // There are two ways to set affixes in DecimalFormat: via the pattern string (applyPattern), and via the
- // explicit setters (setPositivePrefix and friends). The way to resolve the settings is as follows:
- //
- // 1) If the explicit setting is present for the field, use it.
- // 2) Otherwise, follows UTS 35 rules based on the pattern string.
- //
- // Importantly, the explicit setters affect only the one field they override. If you set the positive
- // prefix, that should not affect the negative prefix. Since it is impossible for the user of this class
- // to know whether the origin for a string was the override or the pattern, we have to say that we always
- // have a negative subpattern and perform all resolution logic here.
-
- // Convenience: Extract the properties into local variables.
- // Variables are named with three chars: [p/n][p/s][o/p]
- // [p/n] => p for positive, n for negative
- // [p/s] => p for prefix, s for suffix
- // [o/p] => o for escaped custom override string, p for pattern string
- String ppo = AffixUtils.escape(properties.getPositivePrefix());
- String pso = AffixUtils.escape(properties.getPositiveSuffix());
- String npo = AffixUtils.escape(properties.getNegativePrefix());
- String nso = AffixUtils.escape(properties.getNegativeSuffix());
- String ppp = properties.getPositivePrefixPattern();
- String psp = properties.getPositiveSuffixPattern();
- String npp = properties.getNegativePrefixPattern();
- String nsp = properties.getNegativeSuffixPattern();
-
- if (ppo != null) {
- posPrefix = ppo;
- } else if (ppp != null) {
- posPrefix = ppp;
- } else {
- // UTS 35: Default positive prefix is empty string.
- posPrefix = "";
- }
-
- if (pso != null) {
- posSuffix = pso;
- } else if (psp != null) {
- posSuffix = psp;
- } else {
- // UTS 35: Default positive suffix is empty string.
- posSuffix = "";
- }
-
- if (npo != null) {
- negPrefix = npo;
- } else if (npp != null) {
- negPrefix = npp;
- } else {
- // UTS 35: Default negative prefix is "-" with positive prefix.
- // Important: We prepend the "-" to the pattern, not the override!
- negPrefix = ppp == null ? "-" : "-" + ppp;
- }
-
- if (nso != null) {
- negSuffix = nso;
- } else if (nsp != null) {
- negSuffix = nsp;
- } else {
- // UTS 35: Default negative prefix is the positive prefix.
- negSuffix = psp == null ? "" : psp;
- }
- }
-
- @Override
- public char charAt(int flags, int i) {
- return getStringForFlags(flags).charAt(i);
- }
-
- @Override
- public int length(int flags) {
- return getStringForFlags(flags).length();
- }
-
- private String getStringForFlags(int flags) {
- boolean prefix = (flags & Flags.PREFIX) != 0;
- boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
- if (prefix && negative) {
- return negPrefix;
- } else if (prefix) {
- return posPrefix;
- } else if (negative) {
- return negSuffix;
- } else {
- return posSuffix;
- }
- }
-
- @Override
- public boolean positiveHasPlusSign() {
- return AffixUtils.containsType(posPrefix, AffixUtils.TYPE_PLUS_SIGN)
- || AffixUtils.containsType(posSuffix, AffixUtils.TYPE_PLUS_SIGN);
- }
-
- @Override
- public boolean hasNegativeSubpattern() {
- // See comments in the constructor for more information on why this is always true.
- return true;
- }
-
- @Override
- public boolean negativeHasMinusSign() {
- return AffixUtils.containsType(negPrefix, AffixUtils.TYPE_MINUS_SIGN)
- || AffixUtils.containsType(negSuffix, AffixUtils.TYPE_MINUS_SIGN);
- }
-
- @Override
- public boolean hasCurrencySign() {
- return AffixUtils.hasCurrencySymbols(posPrefix) || AffixUtils.hasCurrencySymbols(posSuffix)
- || AffixUtils.hasCurrencySymbols(negPrefix) || AffixUtils.hasCurrencySymbols(negSuffix);
- }
-
- @Override
- public boolean containsSymbolType(int type) {
- return AffixUtils.containsType(posPrefix, type) || AffixUtils.containsType(posSuffix, type)
- || AffixUtils.containsType(negPrefix, type) || AffixUtils.containsType(negSuffix, type);
- }
- }
-
- private static class CurrencyPluralInfoAffixProvider implements AffixPatternProvider {
- private final AffixPatternProvider[] affixesByPlural;
-
- public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) {
- affixesByPlural = new ParsedPatternInfo[StandardPlural.COUNT];
- for (StandardPlural plural : StandardPlural.VALUES) {
- affixesByPlural[plural.ordinal()] = PatternStringParser
- .parseToPatternInfo(cpi.getCurrencyPluralPattern(plural.getKeyword()));
- }
- }
-
- @Override
- public char charAt(int flags, int i) {
- int pluralOrdinal = (flags & Flags.PLURAL_MASK);
- return affixesByPlural[pluralOrdinal].charAt(flags, i);
- }
-
- @Override
- public int length(int flags) {
- int pluralOrdinal = (flags & Flags.PLURAL_MASK);
- return affixesByPlural[pluralOrdinal].length(flags);
- }
-
- @Override
- public boolean positiveHasPlusSign() {
- return affixesByPlural[StandardPlural.OTHER.ordinal()].positiveHasPlusSign();
- }
-
- @Override
- public boolean hasNegativeSubpattern() {
- return affixesByPlural[StandardPlural.OTHER.ordinal()].hasNegativeSubpattern();
- }
-
- @Override
- public boolean negativeHasMinusSign() {
- return affixesByPlural[StandardPlural.OTHER.ordinal()].negativeHasMinusSign();
- }
-
- @Override
- public boolean hasCurrencySign() {
- return affixesByPlural[StandardPlural.OTHER.ordinal()].hasCurrencySign();
- }
-
- @Override
- public boolean containsSymbolType(int type) {
- return affixesByPlural[StandardPlural.OTHER.ordinal()].containsSymbolType(type);
- }
- }
}
en_US 0 123,456 123
en_US 1 123.456 123.456
en_US 0 123.456 123.456
-fr_FR 1 123,456 123.456
-fr_FR 0 123,456 123.456
+it_IT 1 123,456 123.456
+it_IT 0 123,456 123.456
// JDK returns 123 here; not sure why.
-fr_FR 1 123.456 123456 K
-fr_FR 0 123.456 123
+it_IT 1 123.456 123456 K
+it_IT 0 123.456 123
test no grouping in pattern with parsing
set pattern 0
(5,347.25) -5347.25
// J requires prefix and suffix for lenient parsing, but C doesn't
5,347.25 5347.25 JK
-(5,347.25 -5347.25 J
+(5,347.25 -5347.25 JP
// S is successful at parsing this as -5347.25 in lenient mode
-5,347.25 -5347.25 CJK
+3.52E4 35200
(34,,25 E-1) -342.5 CJK
// Spaces are not allowed after exponent symbol
// C parses up to the E but J bails
-(34 25E -1) -3425 JK
+// P does not make the number negative
+(34 25E -1) -3425 JKP
+3.52EE4 3.52
+1,234,567.8901 1234567.8901
+1,23,4567.8901 1234567.8901
( 19 45 ) -1945 JK
(,,19,45) -1945
// C parses to the space, but J bails
-(,,19 45) -19 J
+// P makes the number positive
+(,,19 45) -19 JP
// J bails b/c comma different separator than space. C doesn't treat leading spaces
// as a separator.
-( 19,45) -1945 JK
+( 19,45) -1945 JKP
// J bails. Doesn't allow trailing separators when there is prefix and suffix.
(,,19,45,) -1945 J
// J bails on next 4 because J doesn't allow letters inside prefix and suffix.
// C will parse up to the letter.
-(,,19,45,d1) -1945 J
-(,,19,45d1) -1945 J
-( 19 45 d1) -1945 JK
-( 19 45d1) -1945 JK
+(,,19,45,d1) -1945 JP
+(,,19,45d1) -1945 JP
+( 19 45 d1) -1945 JKP
+( 19 45d1) -1945 JKP
// J does allow trailing separator before a decimal point
(19,45,.25) -1945.25
// 2nd decimal points are ignored
(65347.25) -65347.25
(65,347.25) -65347.25
// JDK does allow separators in the wrong place and parses as -5347.25
-(53,47.25) fail K
+(53,47.25) fail KP
// strict requires prefix or suffix, except in C
65,347.25 fail
+3.52E4 35200
import java.text.ParseException;
import java.text.ParsePosition;
+import org.junit.Ignore;
import org.junit.Test;
import com.ibm.icu.dev.test.TestUtil;
import com.ibm.icu.impl.number.Parse.ParseMode;
import com.ibm.icu.impl.number.PatternStringParser;
import com.ibm.icu.impl.number.PatternStringUtils;
+import com.ibm.icu.impl.number.parse.NumberParserImpl;
import com.ibm.icu.number.LocalizedNumberFormatter;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.text.DecimalFormat;
}
};
+ /**
+ * Parsing, but no other features.
+ */
+ private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU60_Parsing =
+ new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
+
+ @Override
+ public Character Id() {
+ return 'P';
+ }
+
+ @Override
+ public String parse(DataDrivenNumberFormatTestData tuple) {
+ String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+ DecimalFormatProperties properties;
+ ParsePosition ppos = new ParsePosition(0);
+ Number actual;
+ try {
+ properties = PatternStringParser.parseToProperties(pattern,
+ tuple.currency != null ? PatternStringParser.IGNORE_ROUNDING_ALWAYS
+ : PatternStringParser.IGNORE_ROUNDING_NEVER);
+ propertiesFromTuple(tuple, properties);
+ actual = NumberParserImpl.parseStatic(tuple.parse,
+ ppos,
+ properties,
+ DecimalFormatSymbols.getInstance(tuple.locale));
+ } catch (IllegalArgumentException e) {
+ return "parse exception: " + e.getMessage();
+ }
+ if (actual == null && ppos.getIndex() != 0) {
+ throw new AssertionError("Error: value is null but parse position is not zero");
+ }
+ if (ppos.getIndex() == 0) {
+ return "Parse failed; got " + actual + ", but expected " + tuple.output;
+ }
+ if (tuple.output.equals("NaN")) {
+ if (!Double.isNaN(actual.doubleValue())) {
+ return "Expected NaN, but got: " + actual;
+ }
+ return null;
+ } else if (tuple.output.equals("Inf")) {
+ if (!Double.isInfinite(actual.doubleValue()) || Double.compare(actual.doubleValue(), 0.0) < 0) {
+ return "Expected Inf, but got: " + actual;
+ }
+ return null;
+ } else if (tuple.output.equals("-Inf")) {
+ if (!Double.isInfinite(actual.doubleValue()) || Double.compare(actual.doubleValue(), 0.0) > 0) {
+ return "Expected -Inf, but got: " + actual;
+ }
+ return null;
+ } else if (tuple.output.equals("fail")) {
+ return null;
+ } else if (new BigDecimal(tuple.output).compareTo(new BigDecimal(actual.toString())) != 0) {
+ return "Expected: " + tuple.output + ", got: " + actual;
+ } else {
+ return null;
+ }
+ }
+
+// @Override
+// public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
+// String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+// DecimalFormatProperties properties;
+// ParsePosition ppos = new ParsePosition(0);
+// CurrencyAmount actual;
+// try {
+// properties = PatternStringParser.parseToProperties(
+// pattern,
+// tuple.currency != null ? PatternStringParser.IGNORE_ROUNDING_ALWAYS
+// : PatternStringParser.IGNORE_ROUNDING_NEVER);
+// propertiesFromTuple(tuple, properties);
+// actual = NumberParserImpl.parseStatic(tuple.parse,
+// ppos,
+// properties,
+// DecimalFormatSymbols.getInstance(tuple.locale));
+// } catch (ParseException e) {
+// e.printStackTrace();
+// return "parse exception: " + e.getMessage();
+// }
+// if (ppos.getIndex() == 0 || actual.getCurrency().getCurrencyCode().equals("XXX")) {
+// return "Parse failed; got " + actual + ", but expected " + tuple.output;
+// }
+// BigDecimal expectedNumber = new BigDecimal(tuple.output);
+// if (expectedNumber.compareTo(new BigDecimal(actual.getNumber().toString())) != 0) {
+// return "Wrong number: Expected: " + expectedNumber + ", got: " + actual;
+// }
+// String expectedCurrency = tuple.outputCurrency;
+// if (!expectedCurrency.equals(actual.getCurrency().toString())) {
+// return "Wrong currency: Expected: " + expectedCurrency + ", got: " + actual;
+// }
+// return null;
+// }
+ };
+
/**
* All features except formatting.
*/
};
@Test
+ @Ignore
public void TestDataDrivenICU58() {
// Android can't access DecimalFormat_ICU58 for testing (ticket #13283).
if (TestUtil.getJavaVendor() == TestUtil.JavaVendor.Android) return;
// something may or may not work. However the test data assumes a specific
// Java runtime version. We should probably disable this test case - #13372
@Test
+ @Ignore
public void TestDataDrivenJDK() {
// Android implements java.text.DecimalFormat with ICU4J (ticket #13322).
// Oracle/OpenJDK 9's behavior is not exactly same with Oracle/OpenJDK 8.
}
@Test
+ @Ignore
public void TestDataDrivenICULatest_Format() {
DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
"numberformattestspecification.txt", ICU60);
}
@Test
+ public void TestDataDrivenICULatest_Parsing() {
+ DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
+ "numberformattestspecification.txt", ICU60_Parsing);
+ }
+
+ @Test
+ @Ignore
public void TestDataDrivenICULatest_Other() {
DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
"numberformattestspecification.txt", ICU60_Other);
// a) Flags:
// --- Bit 0x01 => Test greedy implementation
// --- Bit 0x02 => Test slow implementation
+ // --- Bit 0x04 => Test strict grouping separators
// b) Input string
// c) Pattern
// d) Expected chars consumed
{ 3, "๐ฑ๐ญ๐ฐ๐ฎ๐ฏx", "0", 10, 51423. },
{ 3, " ๐ฑ๐ญ๐ฐ๐ฎ๐ฏ", "0", 11, 51423. },
{ 3, "๐ฑ๐ญ๐ฐ๐ฎ๐ฏ ", "0", 10, 51423. },
- { 3, "๐ฑ๐ญ,๐ฐ๐ฎ๐ฏ", "0", 11, 51423. },
- { 3, "๐ณ๐ด,๐ต๐ฑ๐ญ,๐ฐ๐ฎ๐ฏ", "0", 18, 78951423. },
- { 3, "๐ณ๐ด,๐ต๐ฑ๐ญ.๐ฐ๐ฎ๐ฏ", "0", 18, 78951.423 },
- { 3, "๐ณ๐ด,๐ฌ๐ฌ๐ฌ", "0", 11, 78000. },
- { 3, "๐ณ๐ด,๐ฌ๐ฌ๐ฌ.๐ฌ๐ฌ๐ฌ", "0", 18, 78000. },
- { 3, "๐ณ๐ด,๐ฌ๐ฌ๐ฌ.๐ฌ๐ฎ๐ฏ", "0", 18, 78000.023 },
- { 3, "๐ณ๐ด.๐ฌ๐ฌ๐ฌ.๐ฌ๐ฎ๐ฏ", "0", 11, 78. },
+ { 7, "๐ฑ๐ญ,๐ฐ๐ฎ๐ฏ", "0", 11, 51423. },
+ { 7, "๐ณ๐ด,๐ต๐ฑ๐ญ,๐ฐ๐ฎ๐ฏ", "0", 18, 78951423. },
+ { 7, "๐ณ๐ด,๐ต๐ฑ๐ญ.๐ฐ๐ฎ๐ฏ", "0", 18, 78951.423 },
+ { 7, "๐ณ๐ด,๐ฌ๐ฌ๐ฌ", "0", 11, 78000. },
+ { 7, "๐ณ๐ด,๐ฌ๐ฌ๐ฌ.๐ฌ๐ฌ๐ฌ", "0", 18, 78000. },
+ { 7, "๐ณ๐ด,๐ฌ๐ฌ๐ฌ.๐ฌ๐ฎ๐ฏ", "0", 18, 78000.023 },
+ { 7, "๐ณ๐ด.๐ฌ๐ฌ๐ฌ.๐ฌ๐ฎ๐ฏ", "0", 11, 78. },
{ 3, "-๐ฑ๐ญ๐ฐ๐ฎ๐ฏ", "0", 11, -51423. },
{ 3, "-๐ฑ๐ญ๐ฐ๐ฎ๐ฏ-", "0", 11, -51423. },
{ 3, "a51423US dollars", "a0ยคยคยค", 16, 51423. },
{ 1, "a40b", "a0'0b'", 3, 40. }, // greedy code path thinks "40" is the number
{ 2, "a40b", "a0'0b'", 4, 4. }, // slow code path find the suffix "0b"
{ 3, "๐ฑ.๐ญ๐ฐ๐ฎE๐ฏ", "0", 12, 5142. },
+ { 3, "๐ฑ.๐ญ๐ฐ๐ฎE-๐ฏ", "0", 13, 0.005142 },
{ 3, "5,142.50 Canadian dollars", "0", 25, 5142.5 },
{ 3, "0", "0", 1, 0.0 } };
String pattern = (String) cas[2];
int expectedCharsConsumed = (Integer) cas[3];
double resultDouble = (Double) cas[4];
- NumberParserImpl parser = NumberParserImpl.createParserFromPattern(pattern);
+ NumberParserImpl parser = NumberParserImpl.createParserFromPattern(pattern, false);
String message = "Input <" + input + "> Parser " + parser;
if (0 != (flags & 0x01)) {
assertEquals(message, resultDouble, resultObject.getDouble(), 0.0);
assertEquals(message, expectedCharsConsumed, resultObject.charsConsumed);
}
+
+ if (0 != (flags & 0x04)) {
+ // Test with strict separators
+ parser = NumberParserImpl.createParserFromPattern(pattern, true);
+ ParsedNumber resultObject = new ParsedNumber();
+ parser.parse(input, true, resultObject);
+ assertNotNull(message, resultObject.quantity);
+ assertEquals(message, resultDouble, resultObject.getDouble(), 0.0);
+ assertEquals(message, expectedCharsConsumed, resultObject.charsConsumed);
+ }
}
}
}