ICU-13513 Starting to tie in with existing code. Working through the data-driven test file first.

X-SVN-Rev: 40726
This commit is contained in:
Shane Carr 2017-12-13 10:04:56 +00:00
parent 47d7ebe968
commit a0de8d89c5
11 changed files with 526 additions and 244 deletions

View File

@ -0,0 +1,56 @@
// © 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);
}
}

View File

@ -0,0 +1,128 @@
// © 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);
}
}

View File

@ -16,22 +16,37 @@ import com.ibm.icu.text.UnicodeSet;
*/
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;
@ -39,6 +54,7 @@ public class DecimalMatcher implements NumberParseMatcher {
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;
@ -51,7 +67,11 @@ public class DecimalMatcher implements NumberParseMatcher {
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;
}
@ -65,6 +85,7 @@ public class DecimalMatcher implements NumberParseMatcher {
int currGroup = 0;
int separator = -1;
int lastSeparatorOffset = segment.getOffset();
int scientificAdjustment = 0;
boolean hasPartialPrefix = false;
boolean seenBothSeparators = false;
while (segment.length() > 0) {
@ -97,7 +118,7 @@ public class DecimalMatcher implements NumberParseMatcher {
// 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();
@ -117,13 +138,13 @@ public class DecimalMatcher implements NumberParseMatcher {
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;
}
@ -141,10 +162,13 @@ public class DecimalMatcher implements NumberParseMatcher {
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);

View File

@ -2,14 +2,19 @@
// 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;
@ -23,7 +28,9 @@ import com.ibm.icu.util.ULocale;
*
*/
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);
@ -32,10 +39,7 @@ public class NumberParserImpl {
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;
@ -45,18 +49,77 @@ public class NumberParserImpl {
}
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;
}
@ -67,6 +130,7 @@ public class NumberParserImpl {
public NumberParserImpl() {
matchers = new ArrayList<NumberParseMatcher>();
comparator = ParsedNumber.COMPARATOR; // default value
frozen = false;
}

View File

@ -2,7 +2,8 @@
// 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;
/**
@ -12,17 +13,46 @@ 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
*/
@ -32,7 +62,6 @@ public class ParsedNumber {
flags = other.flags;
prefix = other.prefix;
suffix = other.suffix;
scientificAdjustment = other.scientificAdjustment;
currencyCode = other.currencyCode;
}
@ -41,9 +70,7 @@ public class ParsedNumber {
}
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;
}

View File

@ -11,10 +11,12 @@ import com.ibm.icu.text.DecimalFormatSymbols;
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);
}
@ -26,19 +28,33 @@ public class ScientificMatcher implements NumberParseMatcher {
}
// 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;
}

View File

@ -0,0 +1,23 @@
// © 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?
}
}
}

View File

@ -5,16 +5,15 @@ package com.ibm.icu.number;
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;
@ -22,7 +21,6 @@ import com.ibm.icu.number.Rounder.FractionRounderImpl;
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;
@ -60,7 +58,7 @@ final class NumberPropertyMapper {
* @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,
@ -334,178 +332,4 @@ final class NumberPropertyMapper {
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);
}
}
}

View File

@ -441,11 +441,11 @@ en_US 1 123,456 123456
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
@ -736,7 +736,7 @@ parse output breaks
(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
@ -750,7 +750,8 @@ parse output breaks
(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
@ -775,18 +776,19 @@ parse output breaks
( 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
@ -829,7 +831,7 @@ parse output breaks
(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

View File

@ -7,6 +7,7 @@ import java.math.RoundingMode;
import java.text.ParseException;
import java.text.ParsePosition;
import org.junit.Ignore;
import org.junit.Test;
import com.ibm.icu.dev.test.TestUtil;
@ -16,6 +17,7 @@ import com.ibm.icu.impl.number.Parse;
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;
@ -573,6 +575,100 @@ public class NumberFormatDataDrivenTest {
}
};
/**
* 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.
*/
@ -738,6 +834,7 @@ public class NumberFormatDataDrivenTest {
};
@Test
@Ignore
public void TestDataDrivenICU58() {
// Android can't access DecimalFormat_ICU58 for testing (ticket #13283).
if (TestUtil.getJavaVendor() == TestUtil.JavaVendor.Android) return;
@ -750,6 +847,7 @@ public class NumberFormatDataDrivenTest {
// 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.
@ -764,12 +862,20 @@ public class NumberFormatDataDrivenTest {
}
@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);

View File

@ -22,6 +22,7 @@ public class NumberParserTest {
// 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
@ -34,13 +35,13 @@ public class NumberParserTest {
{ 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. },
@ -51,6 +52,7 @@ public class NumberParserTest {
{ 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 } };
@ -60,7 +62,7 @@ public class NumberParserTest {
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)) {
@ -80,6 +82,16 @@ public class NumberParserTest {
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);
}
}
}
}