ICU-8610 Refactoring and renaming entities in Java implementation. Adding lots of comments. Should be ready to start C++ port.

X-SVN-Rev: 41141
This commit is contained in:
Shane Carr 2018-03-23 04:40:01 +00:00
parent fa6c8972ea
commit c725920cff
2 changed files with 905 additions and 832 deletions

View File

@ -32,29 +32,35 @@ import com.ibm.icu.util.StringTrieBuilder;
*/
class NumberSkeletonImpl {
static enum StemType {
OTHER,
COMPACT_NOTATION,
SCIENTIFIC_NOTATION,
SIMPLE_NOTATION,
NO_UNIT,
CURRENCY,
MEASURE_UNIT,
PER_MEASURE_UNIT,
ROUNDER,
FRACTION_ROUNDER,
MAYBE_INCREMENT_ROUNDER,
CURRENCY_ROUNDER,
GROUPING,
INTEGER_WIDTH,
LATIN,
NUMBERING_SYSTEM,
UNIT_WIDTH,
SIGN_DISPLAY,
DECIMAL_DISPLAY
/**
* While parsing a skeleton, this enum records what type of option we expect to find next.
*/
static enum ParseState {
// Section 0: We expect whitespace or a stem, but not an option:
STATE_NULL,
// Section 1: We might accept an option, but it is not required:
STATE_SCIENTIFIC,
STATE_ROUNDER,
STATE_FRACTION_ROUNDER,
// Section 2: An option is required:
STATE_INCREMENT_ROUNDER,
STATE_MEASURE_UNIT,
STATE_PER_MEASURE_UNIT,
STATE_CURRENCY_UNIT,
STATE_INTEGER_WIDTH,
STATE_NUMBERING_SYSTEM,
}
static enum ActualStem {
/**
* All possible stem literals have an entry in the StemEnum. The enum name is the kebab case stem
* string literal written in upper snake case.
*
* @see StemToObject
* @see #SERIALIZED_STEM_TRIE
*/
static enum StemEnum {
// Section 1: Stems that do not require an option:
STEM_COMPACT_SHORT,
STEM_COMPACT_LONG,
@ -98,9 +104,16 @@ class NumberSkeletonImpl {
STEM_NUMBERING_SYSTEM,
};
static final ActualStem[] ACTUAL_STEM_VALUES = ActualStem.values();
/** For mapping from ordinal back to StemEnum in Java. */
static final StemEnum[] STEM_ENUM_VALUES = StemEnum.values();
private static Notation stemToNotation(ActualStem stem) {
/**
* Utility class for methods that convert from StemEnum to corresponding objects or enums. This
* applies to only the "Section 1" stems, those that are well-defined without an option.
*/
static final class StemToObject {
private static Notation notation(StemEnum stem) {
switch (stem) {
case STEM_COMPACT_SHORT:
return Notation.compactShort();
@ -117,7 +130,7 @@ class NumberSkeletonImpl {
}
}
private static MeasureUnit stemToUnit(ActualStem stem) {
private static MeasureUnit unit(StemEnum stem) {
switch (stem) {
case STEM_BASE_UNIT:
return NoUnit.BASE;
@ -130,7 +143,7 @@ class NumberSkeletonImpl {
}
}
private static Rounder stemToRounder(ActualStem stem) {
private static Rounder rounder(StemEnum stem) {
switch (stem) {
case STEM_ROUND_INTEGER:
return Rounder.integer();
@ -145,7 +158,7 @@ class NumberSkeletonImpl {
}
}
private static GroupingStrategy stemToGroupingStrategy(ActualStem stem) {
private static GroupingStrategy groupingStrategy(StemEnum stem) {
switch (stem) {
case STEM_GROUP_OFF:
return GroupingStrategy.OFF;
@ -162,7 +175,7 @@ class NumberSkeletonImpl {
}
}
private static UnitWidth stemToUnitWidth(ActualStem stem) {
private static UnitWidth unitWidth(StemEnum stem) {
switch (stem) {
case STEM_UNIT_WIDTH_NARROW:
return UnitWidth.NARROW;
@ -179,7 +192,7 @@ class NumberSkeletonImpl {
}
}
private static SignDisplay stemToSignDisplay(ActualStem stem) {
private static SignDisplay signDisplay(StemEnum stem) {
switch (stem) {
case STEM_SIGN_AUTO:
return SignDisplay.AUTO;
@ -200,7 +213,7 @@ class NumberSkeletonImpl {
}
}
private static DecimalSeparatorDisplay stemToDecimalSeparatorDisplay(ActualStem stem) {
private static DecimalSeparatorDisplay decimalSeparatorDisplay(StemEnum stem) {
switch (stem) {
case STEM_DECIMAL_AUTO:
return DecimalSeparatorDisplay.AUTO;
@ -210,8 +223,15 @@ class NumberSkeletonImpl {
return null;
}
}
}
private static void groupingStrategyToStemString(GroupingStrategy value, StringBuilder sb) {
/**
* Utility class for methods that convert from enums to stem strings. More complex object conversions
* take place in ObjectToStemString.
*/
static final class EnumToStemString {
private static void groupingStrategy(GroupingStrategy value, StringBuilder sb) {
switch (value) {
case OFF:
sb.append("group-off");
@ -233,7 +253,7 @@ class NumberSkeletonImpl {
}
}
private static void unitWidthToStemString(UnitWidth value, StringBuilder sb) {
private static void unitWidth(UnitWidth value, StringBuilder sb) {
switch (value) {
case NARROW:
sb.append("unit-width-narrow");
@ -255,7 +275,7 @@ class NumberSkeletonImpl {
}
}
private static void signDisplayToStemString(SignDisplay value, StringBuilder sb) {
private static void signDisplay(SignDisplay value, StringBuilder sb) {
switch (value) {
case AUTO:
sb.append("sign-auto");
@ -283,7 +303,7 @@ class NumberSkeletonImpl {
}
}
private static void decimalSeparatorDisplayToStemString(DecimalSeparatorDisplay value, StringBuilder sb) {
private static void decimalSeparatorDisplay(DecimalSeparatorDisplay value, StringBuilder sb) {
switch (value) {
case AUTO:
sb.append("decimal-auto");
@ -295,58 +315,61 @@ class NumberSkeletonImpl {
throw new AssertionError();
}
}
}
/** A data structure for mapping from stem strings to the stem enum. Built at startup. */
static final String SERIALIZED_STEM_TRIE = buildStemTrie();
static String buildStemTrie() {
CharsTrieBuilder b = new CharsTrieBuilder();
// Section 1:
b.add("compact-short", ActualStem.STEM_COMPACT_SHORT.ordinal());
b.add("compact-long", ActualStem.STEM_COMPACT_LONG.ordinal());
b.add("scientific", ActualStem.STEM_SCIENTIFIC.ordinal());
b.add("engineering", ActualStem.STEM_ENGINEERING.ordinal());
b.add("notation-simple", ActualStem.STEM_NOTATION_SIMPLE.ordinal());
b.add("base-unit", ActualStem.STEM_BASE_UNIT.ordinal());
b.add("percent", ActualStem.STEM_PERCENT.ordinal());
b.add("permille", ActualStem.STEM_PERMILLE.ordinal());
b.add("round-integer", ActualStem.STEM_ROUND_INTEGER.ordinal());
b.add("round-unlimited", ActualStem.STEM_ROUND_UNLIMITED.ordinal());
b.add("round-currency-standard", ActualStem.STEM_ROUND_CURRENCY_STANDARD.ordinal());
b.add("round-currency-cash", ActualStem.STEM_ROUND_CURRENCY_CASH.ordinal());
b.add("group-off", ActualStem.STEM_GROUP_OFF.ordinal());
b.add("group-min2", ActualStem.STEM_GROUP_MIN2.ordinal());
b.add("group-auto", ActualStem.STEM_GROUP_AUTO.ordinal());
b.add("group-on-aligned", ActualStem.STEM_GROUP_ON_ALIGNED.ordinal());
b.add("group-thousands", ActualStem.STEM_GROUP_THOUSANDS.ordinal());
b.add("latin", ActualStem.STEM_LATIN.ordinal());
b.add("unit-width-narrow", ActualStem.STEM_UNIT_WIDTH_NARROW.ordinal());
b.add("unit-width-short", ActualStem.STEM_UNIT_WIDTH_SHORT.ordinal());
b.add("unit-width-full-name", ActualStem.STEM_UNIT_WIDTH_FULL_NAME.ordinal());
b.add("unit-width-iso-code", ActualStem.STEM_UNIT_WIDTH_ISO_CODE.ordinal());
b.add("unit-width-hidden", ActualStem.STEM_UNIT_WIDTH_HIDDEN.ordinal());
b.add("sign-auto", ActualStem.STEM_SIGN_AUTO.ordinal());
b.add("sign-always", ActualStem.STEM_SIGN_ALWAYS.ordinal());
b.add("sign-never", ActualStem.STEM_SIGN_NEVER.ordinal());
b.add("sign-accounting", ActualStem.STEM_SIGN_ACCOUNTING.ordinal());
b.add("sign-accounting-always", ActualStem.STEM_SIGN_ACCOUNTING_ALWAYS.ordinal());
b.add("sign-except-zero", ActualStem.STEM_SIGN_EXCEPT_ZERO.ordinal());
b.add("sign-accounting-except-zero", ActualStem.STEM_SIGN_ACCOUNTING_EXCEPT_ZERO.ordinal());
b.add("decimal-auto", ActualStem.STEM_DECIMAL_AUTO.ordinal());
b.add("decimal-always", ActualStem.STEM_DECIMAL_ALWAYS.ordinal());
b.add("compact-short", StemEnum.STEM_COMPACT_SHORT.ordinal());
b.add("compact-long", StemEnum.STEM_COMPACT_LONG.ordinal());
b.add("scientific", StemEnum.STEM_SCIENTIFIC.ordinal());
b.add("engineering", StemEnum.STEM_ENGINEERING.ordinal());
b.add("notation-simple", StemEnum.STEM_NOTATION_SIMPLE.ordinal());
b.add("base-unit", StemEnum.STEM_BASE_UNIT.ordinal());
b.add("percent", StemEnum.STEM_PERCENT.ordinal());
b.add("permille", StemEnum.STEM_PERMILLE.ordinal());
b.add("round-integer", StemEnum.STEM_ROUND_INTEGER.ordinal());
b.add("round-unlimited", StemEnum.STEM_ROUND_UNLIMITED.ordinal());
b.add("round-currency-standard", StemEnum.STEM_ROUND_CURRENCY_STANDARD.ordinal());
b.add("round-currency-cash", StemEnum.STEM_ROUND_CURRENCY_CASH.ordinal());
b.add("group-off", StemEnum.STEM_GROUP_OFF.ordinal());
b.add("group-min2", StemEnum.STEM_GROUP_MIN2.ordinal());
b.add("group-auto", StemEnum.STEM_GROUP_AUTO.ordinal());
b.add("group-on-aligned", StemEnum.STEM_GROUP_ON_ALIGNED.ordinal());
b.add("group-thousands", StemEnum.STEM_GROUP_THOUSANDS.ordinal());
b.add("latin", StemEnum.STEM_LATIN.ordinal());
b.add("unit-width-narrow", StemEnum.STEM_UNIT_WIDTH_NARROW.ordinal());
b.add("unit-width-short", StemEnum.STEM_UNIT_WIDTH_SHORT.ordinal());
b.add("unit-width-full-name", StemEnum.STEM_UNIT_WIDTH_FULL_NAME.ordinal());
b.add("unit-width-iso-code", StemEnum.STEM_UNIT_WIDTH_ISO_CODE.ordinal());
b.add("unit-width-hidden", StemEnum.STEM_UNIT_WIDTH_HIDDEN.ordinal());
b.add("sign-auto", StemEnum.STEM_SIGN_AUTO.ordinal());
b.add("sign-always", StemEnum.STEM_SIGN_ALWAYS.ordinal());
b.add("sign-never", StemEnum.STEM_SIGN_NEVER.ordinal());
b.add("sign-accounting", StemEnum.STEM_SIGN_ACCOUNTING.ordinal());
b.add("sign-accounting-always", StemEnum.STEM_SIGN_ACCOUNTING_ALWAYS.ordinal());
b.add("sign-except-zero", StemEnum.STEM_SIGN_EXCEPT_ZERO.ordinal());
b.add("sign-accounting-except-zero", StemEnum.STEM_SIGN_ACCOUNTING_EXCEPT_ZERO.ordinal());
b.add("decimal-auto", StemEnum.STEM_DECIMAL_AUTO.ordinal());
b.add("decimal-always", StemEnum.STEM_DECIMAL_ALWAYS.ordinal());
// Section 2:
b.add("round-increment", ActualStem.STEM_ROUND_INCREMENT.ordinal());
b.add("measure-unit", ActualStem.STEM_MEASURE_UNIT.ordinal());
b.add("per-measure-unit", ActualStem.STEM_PER_MEASURE_UNIT.ordinal());
b.add("currency", ActualStem.STEM_CURRENCY.ordinal());
b.add("integer-width", ActualStem.STEM_INTEGER_WIDTH.ordinal());
b.add("numbering-system", ActualStem.STEM_NUMBERING_SYSTEM.ordinal());
b.add("round-increment", StemEnum.STEM_ROUND_INCREMENT.ordinal());
b.add("measure-unit", StemEnum.STEM_MEASURE_UNIT.ordinal());
b.add("per-measure-unit", StemEnum.STEM_PER_MEASURE_UNIT.ordinal());
b.add("currency", StemEnum.STEM_CURRENCY.ordinal());
b.add("integer-width", StemEnum.STEM_INTEGER_WIDTH.ordinal());
b.add("numbering-system", StemEnum.STEM_NUMBERING_SYSTEM.ordinal());
// TODO: Use SLOW or FAST here?
return b.buildCharSequence(StringTrieBuilder.Option.FAST).toString();
}
/** Kebab case versions of the rounding mode enum. */
static final String[] ROUNDING_MODE_STRINGS = {
"up",
"down",
@ -357,6 +380,9 @@ class NumberSkeletonImpl {
"half-even",
"unnecessary" };
///// ENTRYPOINT FUNCTIONS /////
/** Cache for parsed skeleton strings. */
private static final CacheBase<String, UnlocalizedNumberFormatter, Void> cache = new SoftCache<String, UnlocalizedNumberFormatter, Void>() {
@Override
protected UnlocalizedNumberFormatter createInstance(String skeletonString, Void unused) {
@ -403,8 +429,11 @@ class NumberSkeletonImpl {
return sb.toString();
}
/////
///// MAIN PARSING FUNCTIONS /////
/**
* Converts from a skeleton string to a MacroProps. This method contains the primary parse loop.
*/
private static MacroProps parseSkeleton(String skeletonString) {
// Add a trailing whitespace to the end of the skeleton string to make code cleaner.
skeletonString += " ";
@ -412,8 +441,9 @@ class NumberSkeletonImpl {
MacroProps macros = new MacroProps();
StringSegment segment = new StringSegment(skeletonString, false);
CharsTrie stemTrie = new CharsTrie(SERIALIZED_STEM_TRIE, 0);
StemType stem = null;
ParseState stem = ParseState.STATE_NULL;
int offset = 0;
// Primary skeleton parse loop:
while (offset < segment.length()) {
int cp = segment.codePointAt(offset);
boolean isTokenSeparator = PatternProps.isWhiteSpace(cp);
@ -422,7 +452,7 @@ class NumberSkeletonImpl {
if (!isTokenSeparator && !isOptionSeparator) {
// Non-separator token; consume it.
offset += Character.charCount(cp);
if (stem == null) {
if (stem == ParseState.STATE_NULL) {
// We are currently consuming a stem.
// Go to the next state in the stem trie.
stemTrie.nextForCodePoint(cp);
@ -435,58 +465,75 @@ class NumberSkeletonImpl {
// Otherwise, make sure it is a valid repeating separator.
if (offset != 0) {
segment.setLength(offset);
if (stem == null) {
if (stem == ParseState.STATE_NULL) {
// The first separator after the start of a token. Parse it as a stem.
stem = parseStem2(segment, stemTrie, macros);
stem = parseStem(segment, stemTrie, macros);
stemTrie.reset();
} else {
// A separator after the first separator of a token. Parse it as an option.
stem = parseOption(stem, segment, macros);
}
segment.resetLength();
segment.adjustOffset(offset + 1);
// Consume the segment:
segment.adjustOffset(offset);
offset = 0;
} else if (stem != null) {
} else if (stem != ParseState.STATE_NULL) {
// A separator ('/' or whitespace) following an option separator ('/')
segment.setLength(Character.charCount(cp)); // for error message
throw new SkeletonSyntaxException("Unexpected separator character", segment);
} else {
// Two spaces in a row; this is OK.
segment.adjustOffset(Character.charCount(cp));
}
// Make sure we aren't in a state requiring an option, and then reset the state.
if (isTokenSeparator && stem != null) {
// Does the current stem forbid options?
if (isOptionSeparator && stem == ParseState.STATE_NULL) {
segment.setLength(Character.charCount(cp)); // for error message
throw new SkeletonSyntaxException("Unexpected option separator", segment);
}
// Does the current stem require an option?
if (isTokenSeparator && stem != ParseState.STATE_NULL) {
switch (stem) {
case MAYBE_INCREMENT_ROUNDER:
case MEASURE_UNIT:
case PER_MEASURE_UNIT:
case CURRENCY:
case INTEGER_WIDTH:
case NUMBERING_SYSTEM:
case STATE_INCREMENT_ROUNDER:
case STATE_MEASURE_UNIT:
case STATE_PER_MEASURE_UNIT:
case STATE_CURRENCY_UNIT:
case STATE_INTEGER_WIDTH:
case STATE_NUMBERING_SYSTEM:
segment.setLength(Character.charCount(cp)); // for error message
throw new SkeletonSyntaxException("Stem requires an option", segment);
default:
break;
}
stem = null;
stem = ParseState.STATE_NULL;
}
// Consume the separator:
segment.adjustOffset(Character.charCount(cp));
}
assert stem == null;
assert stem == ParseState.STATE_NULL;
return macros;
}
private static StemType parseStem2(StringSegment segment, CharsTrie stemTrie, MacroProps macros) {
/**
* Given that the current segment represents an stem, parse it and save the result.
*
* @return The next state after parsing this stem, corresponding to what subset of options to expect.
*/
private static ParseState parseStem(StringSegment segment, CharsTrie stemTrie, MacroProps macros) {
// First check for "blueprint" stems, which start with a "signal char"
switch (segment.charAt(0)) {
case '.':
checkNull(macros.rounder, segment);
parseFractionStem(segment, macros);
return StemType.FRACTION_ROUNDER;
BlueprintHelpers.parseFractionStem(segment, macros);
return ParseState.STATE_FRACTION_ROUNDER;
case '@':
checkNull(macros.rounder, segment);
parseDigitsStem(segment, macros);
return StemType.OTHER;
BlueprintHelpers.parseDigitsStem(segment, macros);
return ParseState.STATE_NULL;
}
// Now look at the stemsTrie, which is already be pointing at our stem.
@ -497,8 +544,8 @@ class NumberSkeletonImpl {
throw new SkeletonSyntaxException("Unknown stem", segment);
}
ActualStem stemEnum = ACTUAL_STEM_VALUES[stemTrie.getValue()];
switch (stemEnum) {
StemEnum stem = STEM_ENUM_VALUES[stemTrie.getValue()];
switch (stem) {
// Stems with meaning on their own, not requiring an option:
@ -508,33 +555,33 @@ class NumberSkeletonImpl {
case STEM_ENGINEERING:
case STEM_NOTATION_SIMPLE:
checkNull(macros.notation, segment);
macros.notation = stemToNotation(stemEnum);
switch (stemEnum) {
macros.notation = StemToObject.notation(stem);
switch (stem) {
case STEM_SCIENTIFIC:
case STEM_ENGINEERING:
return StemType.SCIENTIFIC_NOTATION; // allows for scientific options
return ParseState.STATE_SCIENTIFIC; // allows for scientific options
default:
return StemType.OTHER;
return ParseState.STATE_NULL;
}
case STEM_BASE_UNIT:
case STEM_PERCENT:
case STEM_PERMILLE:
checkNull(macros.unit, segment);
macros.unit = stemToUnit(stemEnum);
return StemType.OTHER;
macros.unit = StemToObject.unit(stem);
return ParseState.STATE_NULL;
case STEM_ROUND_INTEGER:
case STEM_ROUND_UNLIMITED:
case STEM_ROUND_CURRENCY_STANDARD:
case STEM_ROUND_CURRENCY_CASH:
checkNull(macros.rounder, segment);
macros.rounder = stemToRounder(stemEnum);
switch (stemEnum) {
macros.rounder = StemToObject.rounder(stem);
switch (stem) {
case STEM_ROUND_INTEGER:
return StemType.FRACTION_ROUNDER; // allows for "round-integer/@##"
return ParseState.STATE_FRACTION_ROUNDER; // allows for "round-integer/@##"
default:
return StemType.ROUNDER; // allows for rounding mode options
return ParseState.STATE_ROUNDER; // allows for rounding mode options
}
case STEM_GROUP_OFF:
@ -543,13 +590,13 @@ class NumberSkeletonImpl {
case STEM_GROUP_ON_ALIGNED:
case STEM_GROUP_THOUSANDS:
checkNull(macros.grouping, segment);
macros.grouping = stemToGroupingStrategy(stemEnum);
return StemType.OTHER;
macros.grouping = StemToObject.groupingStrategy(stem);
return ParseState.STATE_NULL;
case STEM_LATIN:
checkNull(macros.symbols, segment);
macros.symbols = NumberingSystem.LATIN;
return StemType.OTHER;
return ParseState.STATE_NULL;
case STEM_UNIT_WIDTH_NARROW:
case STEM_UNIT_WIDTH_SHORT:
@ -557,8 +604,8 @@ class NumberSkeletonImpl {
case STEM_UNIT_WIDTH_ISO_CODE:
case STEM_UNIT_WIDTH_HIDDEN:
checkNull(macros.unitWidth, segment);
macros.unitWidth = stemToUnitWidth(stemEnum);
return StemType.OTHER;
macros.unitWidth = StemToObject.unitWidth(stem);
return ParseState.STATE_NULL;
case STEM_SIGN_AUTO:
case STEM_SIGN_ALWAYS:
@ -568,99 +615,104 @@ class NumberSkeletonImpl {
case STEM_SIGN_EXCEPT_ZERO:
case STEM_SIGN_ACCOUNTING_EXCEPT_ZERO:
checkNull(macros.sign, segment);
macros.sign = stemToSignDisplay(stemEnum);
return StemType.OTHER;
macros.sign = StemToObject.signDisplay(stem);
return ParseState.STATE_NULL;
case STEM_DECIMAL_AUTO:
case STEM_DECIMAL_ALWAYS:
checkNull(macros.decimal, segment);
macros.decimal = stemToDecimalSeparatorDisplay(stemEnum);
return StemType.OTHER;
macros.decimal = StemToObject.decimalSeparatorDisplay(stem);
return ParseState.STATE_NULL;
// Stems requiring an option:
case STEM_ROUND_INCREMENT:
checkNull(macros.rounder, segment);
return StemType.MAYBE_INCREMENT_ROUNDER;
return ParseState.STATE_INCREMENT_ROUNDER;
case STEM_MEASURE_UNIT:
checkNull(macros.unit, segment);
return StemType.MEASURE_UNIT;
return ParseState.STATE_MEASURE_UNIT;
case STEM_PER_MEASURE_UNIT:
checkNull(macros.perUnit, segment);
return StemType.PER_MEASURE_UNIT;
return ParseState.STATE_PER_MEASURE_UNIT;
case STEM_CURRENCY:
checkNull(macros.unit, segment);
return StemType.CURRENCY;
return ParseState.STATE_CURRENCY_UNIT;
case STEM_INTEGER_WIDTH:
checkNull(macros.integerWidth, segment);
return StemType.INTEGER_WIDTH;
return ParseState.STATE_INTEGER_WIDTH;
case STEM_NUMBERING_SYSTEM:
checkNull(macros.symbols, segment);
return StemType.NUMBERING_SYSTEM;
return ParseState.STATE_NUMBERING_SYSTEM;
default:
throw new AssertionError();
}
}
private static StemType parseOption(StemType stem, StringSegment segment, MacroProps macros) {
/**
* Given that the current segment represents an option, parse it and save the result.
*
* @return The next state after parsing this option, corresponding to what subset of options to
* expect next.
*/
private static ParseState parseOption(ParseState stem, StringSegment segment, MacroProps macros) {
///// Required options: /////
switch (stem) {
case CURRENCY:
parseCurrencyOption(segment, macros);
return StemType.OTHER;
case MEASURE_UNIT:
parseMeasureUnitOption(segment, macros);
return StemType.OTHER;
case PER_MEASURE_UNIT:
parseMeasurePerUnitOption(segment, macros);
return StemType.OTHER;
case MAYBE_INCREMENT_ROUNDER:
parseIncrementOption(segment, macros);
return StemType.ROUNDER;
case INTEGER_WIDTH:
parseIntegerWidthOption(segment, macros);
return StemType.OTHER;
case NUMBERING_SYSTEM:
parseNumberingSystemOption(segment, macros);
return StemType.OTHER;
case STATE_CURRENCY_UNIT:
BlueprintHelpers.parseCurrencyOption(segment, macros);
return ParseState.STATE_NULL;
case STATE_MEASURE_UNIT:
BlueprintHelpers.parseMeasureUnitOption(segment, macros);
return ParseState.STATE_NULL;
case STATE_PER_MEASURE_UNIT:
BlueprintHelpers.parseMeasurePerUnitOption(segment, macros);
return ParseState.STATE_NULL;
case STATE_INCREMENT_ROUNDER:
BlueprintHelpers.parseIncrementOption(segment, macros);
return ParseState.STATE_ROUNDER;
case STATE_INTEGER_WIDTH:
BlueprintHelpers.parseIntegerWidthOption(segment, macros);
return ParseState.STATE_NULL;
case STATE_NUMBERING_SYSTEM:
BlueprintHelpers.parseNumberingSystemOption(segment, macros);
return ParseState.STATE_NULL;
}
///// Non-required options: /////
// Scientific options
switch (stem) {
case SCIENTIFIC_NOTATION:
if (parseExponentWidthOption(segment, macros)) {
return StemType.SCIENTIFIC_NOTATION;
case STATE_SCIENTIFIC:
if (BlueprintHelpers.parseExponentWidthOption(segment, macros)) {
return ParseState.STATE_SCIENTIFIC;
}
if (parseExponentSignOption(segment, macros)) {
return StemType.SCIENTIFIC_NOTATION;
if (BlueprintHelpers.parseExponentSignOption(segment, macros)) {
return ParseState.STATE_SCIENTIFIC;
}
}
// Frac-sig option
switch (stem) {
case FRACTION_ROUNDER:
if (parseFracSigOption(segment, macros)) {
return StemType.ROUNDER;
case STATE_FRACTION_ROUNDER:
if (BlueprintHelpers.parseFracSigOption(segment, macros)) {
return ParseState.STATE_ROUNDER;
}
}
// Rounding mode option
switch (stem) {
case ROUNDER:
case FRACTION_ROUNDER:
case CURRENCY_ROUNDER:
if (parseRoundingModeOption(segment, macros)) {
return StemType.ROUNDER;
case STATE_ROUNDER:
case STATE_FRACTION_ROUNDER:
if (BlueprintHelpers.parseRoundingModeOption(segment, macros)) {
return ParseState.STATE_ROUNDER;
}
}
@ -668,36 +720,38 @@ class NumberSkeletonImpl {
throw new SkeletonSyntaxException("Invalid option", segment);
}
///// MAIN SKELETON GENERATION FUNCTION /////
private static void generateSkeleton(MacroProps macros, StringBuilder sb) {
// Supported options
if (macros.notation != null && generateNotationValue(macros, sb)) {
if (macros.notation != null && GeneratorHelpers.notation(macros, sb)) {
sb.append(' ');
}
if (macros.unit != null && generateUnitValue(macros, sb)) {
if (macros.unit != null && GeneratorHelpers.unit(macros, sb)) {
sb.append(' ');
}
if (macros.perUnit != null && generatePerUnitValue(macros, sb)) {
if (macros.perUnit != null && GeneratorHelpers.perUnit(macros, sb)) {
sb.append(' ');
}
if (macros.rounder != null && generateRoundingValue(macros, sb)) {
if (macros.rounder != null && GeneratorHelpers.rounding(macros, sb)) {
sb.append(' ');
}
if (macros.grouping != null && generateGroupingValue(macros, sb)) {
if (macros.grouping != null && GeneratorHelpers.grouping(macros, sb)) {
sb.append(' ');
}
if (macros.integerWidth != null && generateIntegerWidthValue(macros, sb)) {
if (macros.integerWidth != null && GeneratorHelpers.integerWidth(macros, sb)) {
sb.append(' ');
}
if (macros.symbols != null && generateSymbolsValue(macros, sb)) {
if (macros.symbols != null && GeneratorHelpers.symbols(macros, sb)) {
sb.append(' ');
}
if (macros.unitWidth != null && generateUnitWidthValue(macros, sb)) {
if (macros.unitWidth != null && GeneratorHelpers.unitWidth(macros, sb)) {
sb.append(' ');
}
if (macros.sign != null && generateSignValue(macros, sb)) {
if (macros.sign != null && GeneratorHelpers.sign(macros, sb)) {
sb.append(' ');
}
if (macros.decimal != null && generateDecimalValue(macros, sb)) {
if (macros.decimal != null && GeneratorHelpers.decimal(macros, sb)) {
sb.append(' ');
}
@ -725,8 +779,9 @@ class NumberSkeletonImpl {
}
}
/////
///// BLUEPRINT HELPER FUNCTIONS (stem and options that cannot be interpreted literally) /////
static final class BlueprintHelpers {
private static boolean parseExponentWidthOption(StringSegment segment, MacroProps macros) {
if (segment.charAt(0) != '+') {
return false;
@ -758,10 +813,11 @@ class NumberSkeletonImpl {
// TODO: Make this more efficient (avoid object allocation)? It shouldn't be very hot code.
CharsTrie tempStemTrie = new CharsTrie(SERIALIZED_STEM_TRIE, 0);
BytesTrie.Result result = tempStemTrie.next(segment, 0, segment.length());
if (result != BytesTrie.Result.INTERMEDIATE_VALUE && result != BytesTrie.Result.FINAL_VALUE) {
if (result != BytesTrie.Result.INTERMEDIATE_VALUE
&& result != BytesTrie.Result.FINAL_VALUE) {
return false;
}
SignDisplay sign = stemToSignDisplay(ACTUAL_STEM_VALUES[tempStemTrie.getValue()]);
SignDisplay sign = StemToObject.signDisplay(STEM_ENUM_VALUES[tempStemTrie.getValue()]);
if (sign == null) {
return false;
}
@ -769,10 +825,6 @@ class NumberSkeletonImpl {
return true;
}
private static void generateCurrencyOption(Currency currency, StringBuilder sb) {
sb.append(currency.getCurrencyCode());
}
private static void parseCurrencyOption(StringSegment segment, MacroProps macros) {
String currencyCode = segment.subSequence(0, segment.length()).toString();
try {
@ -782,6 +834,10 @@ class NumberSkeletonImpl {
}
}
private static void generateCurrencyOption(Currency currency, StringBuilder sb) {
sb.append(currency.getCurrencyCode());
}
private static void parseMeasureUnitOption(StringSegment segment, MacroProps macros) {
// NOTE: The category (type) of the unit is guaranteed to be a valid subtag (alphanumeric)
// http://unicode.org/reports/tr35/#Validity_Data
@ -809,7 +865,8 @@ class NumberSkeletonImpl {
}
private static void parseMeasurePerUnitOption(StringSegment segment, MacroProps macros) {
// A little bit of a hack: safe the current unit (numerator), call the main measure unit parsing
// A little bit of a hack: safe the current unit (numerator), call the main measure unit
// parsing
// code, put back the numerator unit, and put the new unit into per-unit.
MeasureUnit numerator = macros.unit;
parseMeasureUnitOption(segment, macros);
@ -945,7 +1002,8 @@ class NumberSkeletonImpl {
offset++;
} else if (minSig > 1) {
// @@#, @@##, @@@#
throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
throw new SkeletonSyntaxException("Invalid digits option for fraction rounder",
segment);
} else {
maxSig = minSig;
for (; offset < segment.length(); offset++) {
@ -1065,10 +1123,13 @@ class NumberSkeletonImpl {
private static void generateNumberingSystemOption(NumberingSystem ns, StringBuilder sb) {
sb.append(ns.getName());
}
}
/////
///// STEM GENERATION HELPER FUNCTIONS /////
private static boolean generateNotationValue(MacroProps macros, StringBuilder sb) {
static final class GeneratorHelpers {
private static boolean notation(MacroProps macros, StringBuilder sb) {
if (macros.notation instanceof CompactNotation) {
if (macros.notation == Notation.compactLong()) {
sb.append("compact-long");
@ -1091,11 +1152,11 @@ class NumberSkeletonImpl {
}
if (impl.minExponentDigits > 1) {
sb.append('/');
generateExponentWidthOption(impl.minExponentDigits, sb);
BlueprintHelpers.generateExponentWidthOption(impl.minExponentDigits, sb);
}
if (impl.exponentSignDisplay != SignDisplay.AUTO) {
sb.append('/');
signDisplayToStemString(impl.exponentSignDisplay, sb);
EnumToStemString.signDisplay(impl.exponentSignDisplay, sb);
}
return true;
} else {
@ -1105,10 +1166,10 @@ class NumberSkeletonImpl {
}
}
private static boolean generateUnitValue(MacroProps macros, StringBuilder sb) {
private static boolean unit(MacroProps macros, StringBuilder sb) {
if (macros.unit instanceof Currency) {
sb.append("currency/");
generateCurrencyOption((Currency) macros.unit, sb);
BlueprintHelpers.generateCurrencyOption((Currency) macros.unit, sb);
return true;
} else if (macros.unit instanceof NoUnit) {
if (macros.unit == NoUnit.PERCENT) {
@ -1124,45 +1185,45 @@ class NumberSkeletonImpl {
}
} else {
sb.append("measure-unit/");
generateMeasureUnitOption(macros.unit, sb);
BlueprintHelpers.generateMeasureUnitOption(macros.unit, sb);
return true;
}
}
private static boolean generatePerUnitValue(MacroProps macros, StringBuilder sb) {
private static boolean perUnit(MacroProps macros, StringBuilder sb) {
// Per-units are currently expected to be only MeasureUnits.
if (macros.unit instanceof Currency || macros.unit instanceof NoUnit) {
throw new UnsupportedOperationException(
"Cannot generate number skeleton with per-unit that is not a standard measure unit");
} else {
sb.append("per-measure-unit/");
generateMeasureUnitOption(macros.perUnit, sb);
BlueprintHelpers.generateMeasureUnitOption(macros.perUnit, sb);
return true;
}
}
private static boolean generateRoundingValue(MacroProps macros, StringBuilder sb) {
private static boolean rounding(MacroProps macros, StringBuilder sb) {
if (macros.rounder instanceof Rounder.InfiniteRounderImpl) {
sb.append("round-unlimited");
} else if (macros.rounder instanceof Rounder.FractionRounderImpl) {
Rounder.FractionRounderImpl impl = (Rounder.FractionRounderImpl) macros.rounder;
generateFractionStem(impl.minFrac, impl.maxFrac, sb);
BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
} else if (macros.rounder instanceof Rounder.SignificantRounderImpl) {
Rounder.SignificantRounderImpl impl = (Rounder.SignificantRounderImpl) macros.rounder;
generateDigitsStem(impl.minSig, impl.maxSig, sb);
BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
} else if (macros.rounder instanceof Rounder.FracSigRounderImpl) {
Rounder.FracSigRounderImpl impl = (Rounder.FracSigRounderImpl) macros.rounder;
generateFractionStem(impl.minFrac, impl.maxFrac, sb);
BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
sb.append('/');
if (impl.minSig == -1) {
generateDigitsStem(1, impl.maxSig, sb);
BlueprintHelpers.generateDigitsStem(1, impl.maxSig, sb);
} else {
generateDigitsStem(impl.minSig, -1, sb);
BlueprintHelpers.generateDigitsStem(impl.minSig, -1, sb);
}
} else if (macros.rounder instanceof Rounder.IncrementRounderImpl) {
Rounder.IncrementRounderImpl impl = (Rounder.IncrementRounderImpl) macros.rounder;
sb.append("round-increment/");
generateIncrementOption(impl.increment, sb);
BlueprintHelpers.generateIncrementOption(impl.increment, sb);
} else if (macros.rounder instanceof Rounder.InfiniteRounderImpl) {
sb.append("round-unlimited");
} else {
@ -1178,19 +1239,20 @@ class NumberSkeletonImpl {
// Generate the options
if (macros.rounder.mathContext != Rounder.DEFAULT_MATH_CONTEXT) {
sb.append('/');
generateRoundingModeOption(macros.rounder.mathContext.getRoundingMode(), sb);
BlueprintHelpers.generateRoundingModeOption(macros.rounder.mathContext.getRoundingMode(),
sb);
}
// NOTE: Always return true for rounding because the default value depends on other options.
return true;
}
private static boolean generateGroupingValue(MacroProps macros, StringBuilder sb) {
private static boolean grouping(MacroProps macros, StringBuilder sb) {
if (macros.grouping instanceof GroupingStrategy) {
if (macros.grouping == GroupingStrategy.AUTO) {
return false; // Default value
}
groupingStrategyToStemString((GroupingStrategy) macros.grouping, sb);
EnumToStemString.groupingStrategy((GroupingStrategy) macros.grouping, sb);
return true;
} else {
throw new UnsupportedOperationException(
@ -1198,23 +1260,25 @@ class NumberSkeletonImpl {
}
}
private static boolean generateIntegerWidthValue(MacroProps macros, StringBuilder sb) {
private static boolean integerWidth(MacroProps macros, StringBuilder sb) {
if (macros.integerWidth.equals(IntegerWidth.DEFAULT)) {
return false; // Default
}
sb.append("integer-width/");
generateIntegerWidthOption(macros.integerWidth.minInt, macros.integerWidth.maxInt, sb);
BlueprintHelpers.generateIntegerWidthOption(macros.integerWidth.minInt,
macros.integerWidth.maxInt,
sb);
return true;
}
private static boolean generateSymbolsValue(MacroProps macros, StringBuilder sb) {
private static boolean symbols(MacroProps macros, StringBuilder sb) {
if (macros.symbols instanceof NumberingSystem) {
NumberingSystem ns = (NumberingSystem) macros.symbols;
if (ns.getName().equals("latn")) {
sb.append("latin");
} else {
sb.append("numbering-system/");
generateNumberingSystemOption(ns, sb);
BlueprintHelpers.generateNumberingSystemOption(ns, sb);
}
return true;
} else {
@ -1224,31 +1288,33 @@ class NumberSkeletonImpl {
}
}
private static boolean generateUnitWidthValue(MacroProps macros, StringBuilder sb) {
private static boolean unitWidth(MacroProps macros, StringBuilder sb) {
if (macros.unitWidth == UnitWidth.SHORT) {
return false; // Default value
}
unitWidthToStemString(macros.unitWidth, sb);
EnumToStemString.unitWidth(macros.unitWidth, sb);
return true;
}
private static boolean generateSignValue(MacroProps macros, StringBuilder sb) {
private static boolean sign(MacroProps macros, StringBuilder sb) {
if (macros.sign == SignDisplay.AUTO) {
return false; // Default value
}
signDisplayToStemString(macros.sign, sb);
EnumToStemString.signDisplay(macros.sign, sb);
return true;
}
private static boolean generateDecimalValue(MacroProps macros, StringBuilder sb) {
private static boolean decimal(MacroProps macros, StringBuilder sb) {
if (macros.decimal == DecimalSeparatorDisplay.AUTO) {
return false; // Default value
}
decimalSeparatorDisplayToStemString(macros.decimal, sb);
EnumToStemString.decimalSeparatorDisplay(macros.decimal, sb);
return true;
}
/////
}
///// OTHER UTILITY FUNCTIONS /////
private static void checkNull(Object value, CharSequence content) {
if (value != null) {

View File

@ -3,7 +3,6 @@
package com.ibm.icu.dev.test.number;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@ -129,7 +128,6 @@ public class NumberSkeletonTest {
"scientific/ee",
"round-increment/xxx",
"round-increment/0.1.2",
"group-thousands/foo",
"currency/dummy",
"measure-unit/foo",
"integer-width/xxx",
@ -161,6 +159,25 @@ public class NumberSkeletonTest {
}
}
@Test
public void unexpectedTokens() {
String[] cases = {
"group-thousands/foo",
"round-integer//ceiling group-off",
"round-integer//ceiling group-off",
"round-integer/ group-off",
"round-integer// group-off" };
for (String cas : cases) {
try {
NumberFormatter.fromSkeleton(cas);
fail();
} catch (SkeletonSyntaxException expected) {
assertTrue(expected.getMessage(), expected.getMessage().contains("Unexpected"));
}
}
}
@Test
public void stemsRequiringOption() {
String[] stems = { "round-increment", "currency", "measure-unit", "integer-width", };
@ -202,24 +219,14 @@ public class NumberSkeletonTest {
{ "round-integer group-off", "5142" },
{ "round-integer group-off", "5142" },
{ "round-integer/ceiling group-off", "5143" },
{ "round-integer//ceiling group-off", null },
{ "round-integer/ceiling group-off", "5143" },
{ "round-integer//ceiling group-off", null },
{ "round-integer/ group-off", null },
{ "round-integer// group-off", null } };
{ "round-integer/ceiling group-off", "5143" }, };
for (String[] cas : cases) {
String skeleton = cas[0];
String expected = cas[1];
try {
String actual = NumberFormatter.fromSkeleton(skeleton).locale(ULocale.ENGLISH)
.format(5142.3).toString();
String actual = NumberFormatter.fromSkeleton(skeleton).locale(ULocale.ENGLISH).format(5142.3)
.toString();
assertEquals(skeleton, expected, actual);
} catch (SkeletonSyntaxException e) {
// Expected failure?
assertNull(skeleton, expected);
}
}
}