com.ibm.icu.number.NumberSkeletonImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of icu4j Show documentation
Show all versions of icu4j Show documentation
International Component for Unicode for Java (ICU4J) is a mature, widely used Java library
providing Unicode and Globalization support
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.number;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Set;
import com.ibm.icu.impl.CacheBase;
import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.impl.SoftCache;
import com.ibm.icu.impl.StringSegment;
import com.ibm.icu.impl.number.MacroProps;
import com.ibm.icu.impl.number.RoundingUtils;
import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
import com.ibm.icu.number.NumberFormatter.RoundingPriority;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.NumberFormatter.TrailingZeroDisplay;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.BytesTrie;
import com.ibm.icu.util.CharsTrie;
import com.ibm.icu.util.CharsTrieBuilder;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.NoUnit;
import com.ibm.icu.util.StringTrieBuilder;
/**
* @author sffc
*
*/
class NumberSkeletonImpl {
//////////////////////////////////////////////////////////////////////////////////////////
// NOTE: For examples of how to add a new stem to the number skeleton parser, see: //
// https://github.com/unicode-org/icu/commit/a2a7982216b2348070dc71093775ac7195793d73 //
// and //
// https://github.com/unicode-org/icu/commit/6fe86f3934a8a5701034f648a8f7c5087e84aa28 //
//////////////////////////////////////////////////////////////////////////////////////////
/**
* 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_FRACTION_PRECISION,
STATE_PRECISION,
// Section 2: An option is required:
STATE_INCREMENT_PRECISION,
STATE_MEASURE_UNIT,
STATE_PER_MEASURE_UNIT,
STATE_IDENTIFIER_UNIT,
STATE_UNIT_USAGE,
STATE_CURRENCY_UNIT,
STATE_INTEGER_WIDTH,
STATE_NUMBERING_SYSTEM,
STATE_SCALE,
}
/**
* 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,
STEM_SCIENTIFIC,
STEM_ENGINEERING,
STEM_NOTATION_SIMPLE,
STEM_BASE_UNIT,
STEM_PERCENT,
STEM_PERMILLE,
STEM_PERCENT_100, // concise-only
STEM_PRECISION_INTEGER,
STEM_PRECISION_UNLIMITED,
STEM_PRECISION_CURRENCY_STANDARD,
STEM_PRECISION_CURRENCY_CASH,
STEM_ROUNDING_MODE_CEILING,
STEM_ROUNDING_MODE_FLOOR,
STEM_ROUNDING_MODE_DOWN,
STEM_ROUNDING_MODE_UP,
STEM_ROUNDING_MODE_HALF_EVEN,
STEM_ROUNDING_MODE_HALF_DOWN,
STEM_ROUNDING_MODE_HALF_UP,
STEM_ROUNDING_MODE_UNNECESSARY,
STEM_INTEGER_WIDTH_TRUNC,
STEM_GROUP_OFF,
STEM_GROUP_MIN2,
STEM_GROUP_AUTO,
STEM_GROUP_ON_ALIGNED,
STEM_GROUP_THOUSANDS,
STEM_LATIN,
STEM_UNIT_WIDTH_NARROW,
STEM_UNIT_WIDTH_SHORT,
STEM_UNIT_WIDTH_FULL_NAME,
STEM_UNIT_WIDTH_ISO_CODE,
STEM_UNIT_WIDTH_FORMAL,
STEM_UNIT_WIDTH_VARIANT,
STEM_UNIT_WIDTH_HIDDEN,
STEM_SIGN_AUTO,
STEM_SIGN_ALWAYS,
STEM_SIGN_NEVER,
STEM_SIGN_ACCOUNTING,
STEM_SIGN_ACCOUNTING_ALWAYS,
STEM_SIGN_EXCEPT_ZERO,
STEM_SIGN_ACCOUNTING_EXCEPT_ZERO,
STEM_SIGN_NEGATIVE,
STEM_SIGN_ACCOUNTING_NEGATIVE,
STEM_DECIMAL_AUTO,
STEM_DECIMAL_ALWAYS,
// Section 2: Stems that DO require an option:
STEM_PRECISION_INCREMENT,
STEM_MEASURE_UNIT,
STEM_PER_MEASURE_UNIT,
STEM_UNIT,
STEM_UNIT_USAGE,
STEM_CURRENCY,
STEM_INTEGER_WIDTH,
STEM_NUMBERING_SYSTEM,
STEM_SCALE,
};
/** Default wildcard char, accepted on input and printed in output */
static final char WILDCARD_CHAR = '*';
/** Alternative wildcard char, accept on input but not printed in output */
static final char ALT_WILDCARD_CHAR = '+';
/** Checks whether the char is a wildcard on input */
static boolean isWildcardChar(char c) {
return c == WILDCARD_CHAR || c == ALT_WILDCARD_CHAR;
}
/** For mapping from ordinal back to StemEnum in Java. */
static final StemEnum[] STEM_ENUM_VALUES = StemEnum.values();
/** 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", 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("precision-integer", StemEnum.STEM_PRECISION_INTEGER.ordinal());
b.add("precision-unlimited", StemEnum.STEM_PRECISION_UNLIMITED.ordinal());
b.add("precision-currency-standard", StemEnum.STEM_PRECISION_CURRENCY_STANDARD.ordinal());
b.add("precision-currency-cash", StemEnum.STEM_PRECISION_CURRENCY_CASH.ordinal());
b.add("rounding-mode-ceiling", StemEnum.STEM_ROUNDING_MODE_CEILING.ordinal());
b.add("rounding-mode-floor", StemEnum.STEM_ROUNDING_MODE_FLOOR.ordinal());
b.add("rounding-mode-down", StemEnum.STEM_ROUNDING_MODE_DOWN.ordinal());
b.add("rounding-mode-up", StemEnum.STEM_ROUNDING_MODE_UP.ordinal());
b.add("rounding-mode-half-even", StemEnum.STEM_ROUNDING_MODE_HALF_EVEN.ordinal());
b.add("rounding-mode-half-down", StemEnum.STEM_ROUNDING_MODE_HALF_DOWN.ordinal());
b.add("rounding-mode-half-up", StemEnum.STEM_ROUNDING_MODE_HALF_UP.ordinal());
b.add("rounding-mode-unnecessary", StemEnum.STEM_ROUNDING_MODE_UNNECESSARY.ordinal());
b.add("integer-width-trunc", StemEnum.STEM_INTEGER_WIDTH_TRUNC.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-formal", StemEnum.STEM_UNIT_WIDTH_FORMAL.ordinal());
b.add("unit-width-variant", StemEnum.STEM_UNIT_WIDTH_VARIANT.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("sign-negative", StemEnum.STEM_SIGN_NEGATIVE.ordinal());
b.add("sign-accounting-negative", StemEnum.STEM_SIGN_ACCOUNTING_NEGATIVE.ordinal());
b.add("decimal-auto", StemEnum.STEM_DECIMAL_AUTO.ordinal());
b.add("decimal-always", StemEnum.STEM_DECIMAL_ALWAYS.ordinal());
// Section 2:
b.add("precision-increment", StemEnum.STEM_PRECISION_INCREMENT.ordinal());
b.add("measure-unit", StemEnum.STEM_MEASURE_UNIT.ordinal());
b.add("per-measure-unit", StemEnum.STEM_PER_MEASURE_UNIT.ordinal());
b.add("unit", StemEnum.STEM_UNIT.ordinal());
b.add("usage", StemEnum.STEM_UNIT_USAGE.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());
b.add("scale", StemEnum.STEM_SCALE.ordinal());
// Section 3 (concise tokens):
b.add("K", StemEnum.STEM_COMPACT_SHORT.ordinal());
b.add("KK", StemEnum.STEM_COMPACT_LONG.ordinal());
b.add("%", StemEnum.STEM_PERCENT.ordinal());
b.add("%x100", StemEnum.STEM_PERCENT_100.ordinal());
b.add(",_", StemEnum.STEM_GROUP_OFF.ordinal());
b.add(",?", StemEnum.STEM_GROUP_MIN2.ordinal());
b.add(",!", StemEnum.STEM_GROUP_ON_ALIGNED.ordinal());
b.add("+!", StemEnum.STEM_SIGN_ALWAYS.ordinal());
b.add("+_", StemEnum.STEM_SIGN_NEVER.ordinal());
b.add("()", StemEnum.STEM_SIGN_ACCOUNTING.ordinal());
b.add("()!", StemEnum.STEM_SIGN_ACCOUNTING_ALWAYS.ordinal());
b.add("+?", StemEnum.STEM_SIGN_EXCEPT_ZERO.ordinal());
b.add("()?", StemEnum.STEM_SIGN_ACCOUNTING_EXCEPT_ZERO.ordinal());
b.add("+-", StemEnum.STEM_SIGN_NEGATIVE.ordinal());
b.add("()-", StemEnum.STEM_SIGN_ACCOUNTING_NEGATIVE.ordinal());
// Build the CharsTrie
// TODO: Use SLOW or FAST here?
return b.buildCharSequence(StringTrieBuilder.Option.FAST).toString();
}
/**
* 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();
case STEM_COMPACT_LONG:
return Notation.compactLong();
case STEM_SCIENTIFIC:
return Notation.scientific();
case STEM_ENGINEERING:
return Notation.engineering();
case STEM_NOTATION_SIMPLE:
return Notation.simple();
default:
throw new AssertionError();
}
}
private static MeasureUnit unit(StemEnum stem) {
switch (stem) {
case STEM_BASE_UNIT:
return NoUnit.BASE;
case STEM_PERCENT:
return NoUnit.PERCENT;
case STEM_PERMILLE:
return NoUnit.PERMILLE;
default:
throw new AssertionError();
}
}
private static Precision precision(StemEnum stem) {
switch (stem) {
case STEM_PRECISION_INTEGER:
return Precision.integer();
case STEM_PRECISION_UNLIMITED:
return Precision.unlimited();
case STEM_PRECISION_CURRENCY_STANDARD:
return Precision.currency(CurrencyUsage.STANDARD);
case STEM_PRECISION_CURRENCY_CASH:
return Precision.currency(CurrencyUsage.CASH);
default:
throw new AssertionError();
}
}
private static RoundingMode roundingMode(StemEnum stem) {
switch (stem) {
case STEM_ROUNDING_MODE_CEILING:
return RoundingMode.CEILING;
case STEM_ROUNDING_MODE_FLOOR:
return RoundingMode.FLOOR;
case STEM_ROUNDING_MODE_DOWN:
return RoundingMode.DOWN;
case STEM_ROUNDING_MODE_UP:
return RoundingMode.UP;
case STEM_ROUNDING_MODE_HALF_EVEN:
return RoundingMode.HALF_EVEN;
case STEM_ROUNDING_MODE_HALF_DOWN:
return RoundingMode.HALF_DOWN;
case STEM_ROUNDING_MODE_HALF_UP:
return RoundingMode.HALF_UP;
case STEM_ROUNDING_MODE_UNNECESSARY:
return RoundingMode.UNNECESSARY;
default:
throw new AssertionError();
}
}
private static GroupingStrategy groupingStrategy(StemEnum stem) {
switch (stem) {
case STEM_GROUP_OFF:
return GroupingStrategy.OFF;
case STEM_GROUP_MIN2:
return GroupingStrategy.MIN2;
case STEM_GROUP_AUTO:
return GroupingStrategy.AUTO;
case STEM_GROUP_ON_ALIGNED:
return GroupingStrategy.ON_ALIGNED;
case STEM_GROUP_THOUSANDS:
return GroupingStrategy.THOUSANDS;
default:
return null; // for objects, throw; for enums, return null
}
}
private static UnitWidth unitWidth(StemEnum stem) {
switch (stem) {
case STEM_UNIT_WIDTH_NARROW:
return UnitWidth.NARROW;
case STEM_UNIT_WIDTH_SHORT:
return UnitWidth.SHORT;
case STEM_UNIT_WIDTH_FULL_NAME:
return UnitWidth.FULL_NAME;
case STEM_UNIT_WIDTH_ISO_CODE:
return UnitWidth.ISO_CODE;
case STEM_UNIT_WIDTH_FORMAL:
return UnitWidth.FORMAL;
case STEM_UNIT_WIDTH_VARIANT:
return UnitWidth.VARIANT;
case STEM_UNIT_WIDTH_HIDDEN:
return UnitWidth.HIDDEN;
default:
return null; // for objects, throw; for enums, return null
}
}
private static SignDisplay signDisplay(StemEnum stem) {
switch (stem) {
case STEM_SIGN_AUTO:
return SignDisplay.AUTO;
case STEM_SIGN_ALWAYS:
return SignDisplay.ALWAYS;
case STEM_SIGN_NEVER:
return SignDisplay.NEVER;
case STEM_SIGN_ACCOUNTING:
return SignDisplay.ACCOUNTING;
case STEM_SIGN_ACCOUNTING_ALWAYS:
return SignDisplay.ACCOUNTING_ALWAYS;
case STEM_SIGN_EXCEPT_ZERO:
return SignDisplay.EXCEPT_ZERO;
case STEM_SIGN_ACCOUNTING_EXCEPT_ZERO:
return SignDisplay.ACCOUNTING_EXCEPT_ZERO;
case STEM_SIGN_NEGATIVE:
return SignDisplay.NEGATIVE;
case STEM_SIGN_ACCOUNTING_NEGATIVE:
return SignDisplay.ACCOUNTING_NEGATIVE;
default:
return null; // for objects, throw; for enums, return null
}
}
private static DecimalSeparatorDisplay decimalSeparatorDisplay(StemEnum stem) {
switch (stem) {
case STEM_DECIMAL_AUTO:
return DecimalSeparatorDisplay.AUTO;
case STEM_DECIMAL_ALWAYS:
return DecimalSeparatorDisplay.ALWAYS;
default:
return null; // for objects, throw; for enums, return null
}
}
}
/**
* 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 roundingMode(RoundingMode value, StringBuilder sb) {
switch (value) {
case CEILING:
sb.append("rounding-mode-ceiling");
break;
case FLOOR:
sb.append("rounding-mode-floor");
break;
case DOWN:
sb.append("rounding-mode-down");
break;
case UP:
sb.append("rounding-mode-up");
break;
case HALF_EVEN:
sb.append("rounding-mode-half-even");
break;
case HALF_DOWN:
sb.append("rounding-mode-half-down");
break;
case HALF_UP:
sb.append("rounding-mode-half-up");
break;
case UNNECESSARY:
sb.append("rounding-mode-unnecessary");
break;
default:
throw new AssertionError();
}
}
private static void groupingStrategy(GroupingStrategy value, StringBuilder sb) {
switch (value) {
case OFF:
sb.append("group-off");
break;
case MIN2:
sb.append("group-min2");
break;
case AUTO:
sb.append("group-auto");
break;
case ON_ALIGNED:
sb.append("group-on-aligned");
break;
case THOUSANDS:
sb.append("group-thousands");
break;
default:
throw new AssertionError();
}
}
private static void unitWidth(UnitWidth value, StringBuilder sb) {
switch (value) {
case NARROW:
sb.append("unit-width-narrow");
break;
case SHORT:
sb.append("unit-width-short");
break;
case FULL_NAME:
sb.append("unit-width-full-name");
break;
case ISO_CODE:
sb.append("unit-width-iso-code");
break;
case FORMAL:
sb.append("unit-width-formal");
break;
case VARIANT:
sb.append("unit-width-variant");
break;
case HIDDEN:
sb.append("unit-width-hidden");
break;
default:
throw new AssertionError();
}
}
private static void signDisplay(SignDisplay value, StringBuilder sb) {
switch (value) {
case AUTO:
sb.append("sign-auto");
break;
case ALWAYS:
sb.append("sign-always");
break;
case NEVER:
sb.append("sign-never");
break;
case ACCOUNTING:
sb.append("sign-accounting");
break;
case ACCOUNTING_ALWAYS:
sb.append("sign-accounting-always");
break;
case EXCEPT_ZERO:
sb.append("sign-except-zero");
break;
case ACCOUNTING_EXCEPT_ZERO:
sb.append("sign-accounting-except-zero");
break;
case NEGATIVE:
sb.append("sign-negative");
break;
case ACCOUNTING_NEGATIVE:
sb.append("sign-accounting-negative");
break;
default:
throw new AssertionError();
}
}
private static void decimalSeparatorDisplay(DecimalSeparatorDisplay value, StringBuilder sb) {
switch (value) {
case AUTO:
sb.append("decimal-auto");
break;
case ALWAYS:
sb.append("decimal-always");
break;
default:
throw new AssertionError();
}
}
}
///// ENTRYPOINT FUNCTIONS /////
/** Cache for parsed skeleton strings. */
private static final CacheBase cache = new SoftCache() {
@Override
protected UnlocalizedNumberFormatter createInstance(String skeletonString, Void unused) {
return create(skeletonString);
}
};
/**
* Gets the number formatter for the given number skeleton string from the cache, creating it if it
* does not exist in the cache.
*
* @param skeletonString
* A number skeleton string, possibly not in its shortest form.
* @return An UnlocalizedNumberFormatter with behavior defined by the given skeleton string.
*/
public static UnlocalizedNumberFormatter getOrCreate(String skeletonString) {
// TODO: This does not currently check the cache for the normalized form of the skeleton.
// A new cache implementation would be required for that to work.
return cache.getInstance(skeletonString, null);
}
/**
* Creates a NumberFormatter corresponding to the given skeleton string.
*
* @param skeletonString
* A number skeleton string, possibly not in its shortest form.
* @return An UnlocalizedNumberFormatter with behavior defined by the given skeleton string.
*/
public static UnlocalizedNumberFormatter create(String skeletonString) {
MacroProps macros = parseSkeleton(skeletonString);
return NumberFormatter.with().macros(macros);
}
/**
* Create a skeleton string corresponding to the given NumberFormatter.
*
* @param macros
* The NumberFormatter options object.
* @return A skeleton string in normalized form.
*/
public static String generate(MacroProps macros) {
StringBuilder sb = new StringBuilder();
generateSkeleton(macros, sb);
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 += " ";
MacroProps macros = new MacroProps();
StringSegment segment = new StringSegment(skeletonString, false);
CharsTrie stemTrie = new CharsTrie(SERIALIZED_STEM_TRIE, 0);
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);
boolean isOptionSeparator = (cp == '/');
if (!isTokenSeparator && !isOptionSeparator) {
// Non-separator token; consume it.
offset += Character.charCount(cp);
if (stem == ParseState.STATE_NULL) {
// We are currently consuming a stem.
// Go to the next state in the stem trie.
stemTrie.nextForCodePoint(cp);
}
continue;
}
// We are looking at a token or option separator.
// If the segment is nonempty, parse it and reset the segment.
// Otherwise, make sure it is a valid repeating separator.
if (offset != 0) {
segment.setLength(offset);
if (stem == ParseState.STATE_NULL) {
// The first separator after the start of a token. Parse it as a stem.
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();
// Consume the segment:
segment.adjustOffset(offset);
offset = 0;
} 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.
}
// 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 STATE_INCREMENT_PRECISION:
case STATE_MEASURE_UNIT:
case STATE_PER_MEASURE_UNIT:
case STATE_IDENTIFIER_UNIT:
case STATE_UNIT_USAGE:
case STATE_CURRENCY_UNIT:
case STATE_INTEGER_WIDTH:
case STATE_NUMBERING_SYSTEM:
case STATE_SCALE:
segment.setLength(Character.charCount(cp)); // for error message
throw new SkeletonSyntaxException("Stem requires an option", segment);
default:
break;
}
stem = ParseState.STATE_NULL;
}
// Consume the separator:
segment.adjustOffset(Character.charCount(cp));
}
assert stem == ParseState.STATE_NULL;
return macros;
}
/**
* Given that the current segment represents a 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.precision, segment);
BlueprintHelpers.parseFractionStem(segment, macros);
return ParseState.STATE_FRACTION_PRECISION;
case '@':
checkNull(macros.precision, segment);
BlueprintHelpers.parseDigitsStem(segment, macros);
return ParseState.STATE_PRECISION;
case 'E':
checkNull(macros.notation, segment);
BlueprintHelpers.parseScientificStem(segment, macros);
return ParseState.STATE_NULL;
case '0':
checkNull(macros.integerWidth, segment);
BlueprintHelpers.parseIntegerStem(segment, macros);
return ParseState.STATE_NULL;
}
// Now look at the stemsTrie, which is already be pointing at our stem.
BytesTrie.Result stemResult = stemTrie.current();
if (stemResult != BytesTrie.Result.INTERMEDIATE_VALUE
&& stemResult != BytesTrie.Result.FINAL_VALUE) {
throw new SkeletonSyntaxException("Unknown stem", segment);
}
StemEnum stem = STEM_ENUM_VALUES[stemTrie.getValue()];
switch (stem) {
// Stems with meaning on their own, not requiring an option:
case STEM_COMPACT_SHORT:
case STEM_COMPACT_LONG:
case STEM_SCIENTIFIC:
case STEM_ENGINEERING:
case STEM_NOTATION_SIMPLE:
checkNull(macros.notation, segment);
macros.notation = StemToObject.notation(stem);
switch (stem) {
case STEM_SCIENTIFIC:
case STEM_ENGINEERING:
return ParseState.STATE_SCIENTIFIC; // allows for scientific options
default:
return ParseState.STATE_NULL;
}
case STEM_BASE_UNIT:
case STEM_PERCENT:
case STEM_PERMILLE:
checkNull(macros.unit, segment);
macros.unit = StemToObject.unit(stem);
return ParseState.STATE_NULL;
case STEM_PERCENT_100:
checkNull(macros.scale, segment);
checkNull(macros.unit, segment);
macros.scale = Scale.powerOfTen(2);
macros.unit = NoUnit.PERCENT;
return ParseState.STATE_NULL;
case STEM_PRECISION_INTEGER:
case STEM_PRECISION_UNLIMITED:
case STEM_PRECISION_CURRENCY_STANDARD:
case STEM_PRECISION_CURRENCY_CASH:
checkNull(macros.precision, segment);
macros.precision = StemToObject.precision(stem);
switch (stem) {
case STEM_PRECISION_INTEGER:
return ParseState.STATE_FRACTION_PRECISION; // allows for "precision-integer/@##"
default:
return ParseState.STATE_PRECISION;
}
case STEM_ROUNDING_MODE_CEILING:
case STEM_ROUNDING_MODE_FLOOR:
case STEM_ROUNDING_MODE_DOWN:
case STEM_ROUNDING_MODE_UP:
case STEM_ROUNDING_MODE_HALF_EVEN:
case STEM_ROUNDING_MODE_HALF_DOWN:
case STEM_ROUNDING_MODE_HALF_UP:
case STEM_ROUNDING_MODE_UNNECESSARY:
checkNull(macros.roundingMode, segment);
macros.roundingMode = StemToObject.roundingMode(stem);
return ParseState.STATE_NULL;
case STEM_INTEGER_WIDTH_TRUNC:
checkNull(macros.integerWidth, segment);
macros.integerWidth = IntegerWidth.zeroFillTo(0).truncateAt(0);
return ParseState.STATE_NULL;
case STEM_GROUP_OFF:
case STEM_GROUP_MIN2:
case STEM_GROUP_AUTO:
case STEM_GROUP_ON_ALIGNED:
case STEM_GROUP_THOUSANDS:
checkNull(macros.grouping, segment);
macros.grouping = StemToObject.groupingStrategy(stem);
return ParseState.STATE_NULL;
case STEM_LATIN:
checkNull(macros.symbols, segment);
macros.symbols = NumberingSystem.LATIN;
return ParseState.STATE_NULL;
case STEM_UNIT_WIDTH_NARROW:
case STEM_UNIT_WIDTH_SHORT:
case STEM_UNIT_WIDTH_FULL_NAME:
case STEM_UNIT_WIDTH_ISO_CODE:
case STEM_UNIT_WIDTH_FORMAL:
case STEM_UNIT_WIDTH_VARIANT:
case STEM_UNIT_WIDTH_HIDDEN:
checkNull(macros.unitWidth, segment);
macros.unitWidth = StemToObject.unitWidth(stem);
return ParseState.STATE_NULL;
case STEM_SIGN_AUTO:
case STEM_SIGN_ALWAYS:
case STEM_SIGN_NEVER:
case STEM_SIGN_ACCOUNTING:
case STEM_SIGN_ACCOUNTING_ALWAYS:
case STEM_SIGN_EXCEPT_ZERO:
case STEM_SIGN_ACCOUNTING_EXCEPT_ZERO:
case STEM_SIGN_NEGATIVE:
case STEM_SIGN_ACCOUNTING_NEGATIVE:
checkNull(macros.sign, segment);
macros.sign = StemToObject.signDisplay(stem);
return ParseState.STATE_NULL;
case STEM_DECIMAL_AUTO:
case STEM_DECIMAL_ALWAYS:
checkNull(macros.decimal, segment);
macros.decimal = StemToObject.decimalSeparatorDisplay(stem);
return ParseState.STATE_NULL;
// Stems requiring an option:
case STEM_PRECISION_INCREMENT:
checkNull(macros.precision, segment);
return ParseState.STATE_INCREMENT_PRECISION;
case STEM_MEASURE_UNIT:
checkNull(macros.unit, segment);
return ParseState.STATE_MEASURE_UNIT;
case STEM_PER_MEASURE_UNIT:
// In C++, STEM_CURRENCY's checks mark perUnit as "seen". Here we do
// the inverse: checking that macros.unit is not set to a currency.
if (macros.unit instanceof Currency) {
throw new SkeletonSyntaxException("Duplicated setting", segment);
}
checkNull(macros.perUnit, segment);
return ParseState.STATE_PER_MEASURE_UNIT;
case STEM_UNIT:
checkNull(macros.unit, segment);
checkNull(macros.perUnit, segment);
return ParseState.STATE_IDENTIFIER_UNIT;
case STEM_UNIT_USAGE:
checkNull(macros.usage, segment);
return ParseState.STATE_UNIT_USAGE;
case STEM_CURRENCY:
checkNull(macros.unit, segment);
checkNull(macros.perUnit, segment);
return ParseState.STATE_CURRENCY_UNIT;
case STEM_INTEGER_WIDTH:
checkNull(macros.integerWidth, segment);
return ParseState.STATE_INTEGER_WIDTH;
case STEM_NUMBERING_SYSTEM:
checkNull(macros.symbols, segment);
return ParseState.STATE_NUMBERING_SYSTEM;
case STEM_SCALE:
checkNull(macros.scale, segment);
return ParseState.STATE_SCALE;
default:
throw new AssertionError();
}
}
/**
* 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 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_IDENTIFIER_UNIT:
BlueprintHelpers.parseIdentifierUnitOption(segment, macros);
return ParseState.STATE_NULL;
case STATE_UNIT_USAGE:
BlueprintHelpers.parseUnitUsageOption(segment, macros);
return ParseState.STATE_NULL;
case STATE_INCREMENT_PRECISION:
BlueprintHelpers.parseIncrementOption(segment, macros);
return ParseState.STATE_PRECISION;
case STATE_INTEGER_WIDTH:
BlueprintHelpers.parseIntegerWidthOption(segment, macros);
return ParseState.STATE_NULL;
case STATE_NUMBERING_SYSTEM:
BlueprintHelpers.parseNumberingSystemOption(segment, macros);
return ParseState.STATE_NULL;
case STATE_SCALE:
BlueprintHelpers.parseScaleOption(segment, macros);
return ParseState.STATE_NULL;
default:
break;
}
///// Non-required options: /////
// Scientific options
switch (stem) {
case STATE_SCIENTIFIC:
if (BlueprintHelpers.parseExponentWidthOption(segment, macros)) {
return ParseState.STATE_SCIENTIFIC;
}
if (BlueprintHelpers.parseExponentSignOption(segment, macros)) {
return ParseState.STATE_SCIENTIFIC;
}
break;
default:
break;
}
// Frac-sig option
switch (stem) {
case STATE_FRACTION_PRECISION:
if (BlueprintHelpers.parseFracSigOption(segment, macros)) {
return ParseState.STATE_PRECISION;
}
// If the fracSig option was not found, try normal precision options.
stem = ParseState.STATE_PRECISION;
break;
default:
break;
}
// Trailing zeros option
switch (stem) {
case STATE_PRECISION:
if (BlueprintHelpers.parseTrailingZeroOption(segment, macros)) {
return ParseState.STATE_NULL;
}
break;
default:
break;
}
// Unknown option
throw new SkeletonSyntaxException("Invalid option", segment);
}
///// MAIN SKELETON GENERATION FUNCTION /////
/**
* Main skeleton generator function. Appends the normalized skeleton for the MacroProps to the given
* StringBuilder.
*/
private static void generateSkeleton(MacroProps macros, StringBuilder sb) {
// Supported options
if (macros.notation != null && GeneratorHelpers.notation(macros, sb)) {
sb.append(' ');
}
if (macros.unit != null && GeneratorHelpers.unit(macros, sb)) {
sb.append(' ');
}
if (macros.usage != null && GeneratorHelpers.usage(macros, sb)) {
sb.append(' ');
}
if (macros.precision != null && GeneratorHelpers.precision(macros, sb)) {
sb.append(' ');
}
if (macros.roundingMode != null && GeneratorHelpers.roundingMode(macros, sb)) {
sb.append(' ');
}
if (macros.grouping != null && GeneratorHelpers.grouping(macros, sb)) {
sb.append(' ');
}
if (macros.integerWidth != null && GeneratorHelpers.integerWidth(macros, sb)) {
sb.append(' ');
}
if (macros.symbols != null && GeneratorHelpers.symbols(macros, sb)) {
sb.append(' ');
}
if (macros.unitWidth != null && GeneratorHelpers.unitWidth(macros, sb)) {
sb.append(' ');
}
if (macros.sign != null && GeneratorHelpers.sign(macros, sb)) {
sb.append(' ');
}
if (macros.decimal != null && GeneratorHelpers.decimal(macros, sb)) {
sb.append(' ');
}
if (macros.scale != null && GeneratorHelpers.scale(macros, sb)) {
sb.append(' ');
}
// Unsupported options
if (macros.padder != null) {
throw new UnsupportedOperationException(
"Cannot generate number skeleton with custom padder");
}
if (macros.unitDisplayCase != null && !macros.unitDisplayCase.isEmpty()) {
throw new UnsupportedOperationException(
"Cannot generate number skeleton with custom unit display case");
}
if (macros.affixProvider != null) {
throw new UnsupportedOperationException(
"Cannot generate number skeleton with custom affix provider");
}
if (macros.rules != null) {
throw new UnsupportedOperationException(
"Cannot generate number skeleton with custom plural rules");
}
// Remove the trailing space
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
}
///// BLUEPRINT HELPER FUNCTIONS /////
/**
* Utility class for methods for processing stems and options that cannot be interpreted literally.
*/
static final class BlueprintHelpers {
/** @return Whether we successfully found and parsed an exponent width option. */
private static boolean parseExponentWidthOption(StringSegment segment, MacroProps macros) {
if (!isWildcardChar(segment.charAt(0))) {
return false;
}
int offset = 1;
int minExp = 0;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == 'e') {
minExp++;
} else {
break;
}
}
if (offset < segment.length()) {
return false;
}
// Use the public APIs to enforce bounds checking
macros.notation = ((ScientificNotation) macros.notation).withMinExponentDigits(minExp);
return true;
}
private static void generateExponentWidthOption(int minExponentDigits, StringBuilder sb) {
sb.append(WILDCARD_CHAR);
appendMultiple(sb, 'e', minExponentDigits);
}
/** @return Whether we successfully found and parsed an exponent sign option. */
private static boolean parseExponentSignOption(StringSegment segment, MacroProps macros) {
// Get the sign display type out of the CharsTrie data structure.
// 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) {
return false;
}
SignDisplay sign = StemToObject.signDisplay(STEM_ENUM_VALUES[tempStemTrie.getValue()]);
if (sign == null) {
return false;
}
macros.notation = ((ScientificNotation) macros.notation).withExponentSignDisplay(sign);
return true;
}
private static void parseCurrencyOption(StringSegment segment, MacroProps macros) {
String currencyCode = segment.subSequence(0, segment.length()).toString();
Currency currency;
try {
currency = Currency.getInstance(currencyCode);
} catch (IllegalArgumentException e) {
// Not 3 ascii chars
throw new SkeletonSyntaxException("Invalid currency", segment, e);
}
macros.unit = currency;
}
private static void generateCurrencyOption(Currency currency, StringBuilder sb) {
sb.append(currency.getCurrencyCode());
}
// "measure-unit/" is deprecated in favour of "unit/".
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
int firstHyphen = 0;
while (firstHyphen < segment.length() && segment.charAt(firstHyphen) != '-') {
firstHyphen++;
}
if (firstHyphen == segment.length()) {
throw new SkeletonSyntaxException("Invalid measure unit option", segment);
}
String type = segment.subSequence(0, firstHyphen).toString();
String subType = segment.subSequence(firstHyphen + 1, segment.length()).toString();
Set units = MeasureUnit.getAvailable(type);
for (MeasureUnit unit : units) {
if (subType.equals(unit.getSubtype())) {
macros.unit = unit;
return;
}
}
throw new SkeletonSyntaxException("Unknown measure unit", segment);
}
// "per-measure-unit/" is deprecated in favour of "unit/".
private static void parseMeasurePerUnitOption(StringSegment segment, MacroProps macros) {
// A little bit of a hack: save 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);
macros.perUnit = macros.unit;
macros.unit = numerator;
}
/**
* Parses unit identifiers like "meter-per-second" and "foot-and-inch", as
* specified via a "unit/" concise skeleton.
*/
private static void parseIdentifierUnitOption(StringSegment segment, MacroProps macros) {
try {
macros.unit = MeasureUnit.forIdentifier(segment.asString());
} catch (IllegalArgumentException e) {
throw new SkeletonSyntaxException("Invalid unit stem", segment);
}
}
private static void parseUnitUsageOption(StringSegment segment, MacroProps macros) {
macros.usage = segment.asString();
// We do not do any validation of the usage string: it depends on the
// unitPreferenceData in the units resources.
}
private static void parseFractionStem(StringSegment segment, MacroProps macros) {
assert segment.charAt(0) == '.';
int offset = 1;
int minFrac = 0;
int maxFrac;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == '0') {
minFrac++;
} else {
break;
}
}
if (offset < segment.length()) {
if (isWildcardChar(segment.charAt(offset))) {
maxFrac = -1;
offset++;
} else {
maxFrac = minFrac;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == '#') {
maxFrac++;
} else {
break;
}
}
}
} else {
maxFrac = minFrac;
}
if (offset < segment.length()) {
throw new SkeletonSyntaxException("Invalid fraction stem", segment);
}
// Use the public APIs to enforce bounds checking
if (maxFrac == -1) {
if (minFrac == 0) {
macros.precision = Precision.unlimited();
} else {
macros.precision = Precision.minFraction(minFrac);
}
} else {
macros.precision = Precision.minMaxFraction(minFrac, maxFrac);
}
}
private static void generateFractionStem(int minFrac, int maxFrac, StringBuilder sb) {
if (minFrac == 0 && maxFrac == 0) {
sb.append("precision-integer");
return;
}
sb.append('.');
appendMultiple(sb, '0', minFrac);
if (maxFrac == -1) {
sb.append(WILDCARD_CHAR);
} else {
appendMultiple(sb, '#', maxFrac - minFrac);
}
}
private static void parseDigitsStem(StringSegment segment, MacroProps macros) {
assert segment.charAt(0) == '@';
int offset = 0;
int minSig = 0;
int maxSig;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == '@') {
minSig++;
} else {
break;
}
}
if (offset < segment.length()) {
if (isWildcardChar(segment.charAt(offset))) {
maxSig = -1;
offset++;
} else {
maxSig = minSig;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == '#') {
maxSig++;
} else {
break;
}
}
}
} else {
maxSig = minSig;
}
if (offset < segment.length()) {
throw new SkeletonSyntaxException("Invalid significant digits stem", segment);
}
// Use the public APIs to enforce bounds checking
if (maxSig == -1) {
macros.precision = Precision.minSignificantDigits(minSig);
} else {
macros.precision = Precision.minMaxSignificantDigits(minSig, maxSig);
}
}
private static void generateDigitsStem(int minSig, int maxSig, StringBuilder sb) {
appendMultiple(sb, '@', minSig);
if (maxSig == -1) {
sb.append(WILDCARD_CHAR);
} else {
appendMultiple(sb, '#', maxSig - minSig);
}
}
private static void parseScientificStem(StringSegment segment, MacroProps macros) {
assert(segment.charAt(0) == 'E');
block:
{
int offset = 1;
if (segment.length() == offset) {
break block;
}
boolean isEngineering = false;
if (segment.charAt(offset) == 'E') {
isEngineering = true;
offset++;
if (segment.length() == offset) {
break block;
}
}
SignDisplay signDisplay = SignDisplay.AUTO;
if (segment.charAt(offset) == '+') {
offset++;
if (segment.length() == offset) {
break block;
}
if (segment.charAt(offset) == '!') {
signDisplay = SignDisplay.ALWAYS;
} else if (segment.charAt(offset) == '?') {
signDisplay = SignDisplay.EXCEPT_ZERO;
} else {
// NOTE: Other sign displays are not included because they aren't useful in this context
break block;
}
offset++;
if (segment.length() == offset) {
break block;
}
}
int minDigits = 0;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) != '0') {
break block;
}
minDigits++;
}
macros.notation = (isEngineering ? Notation.engineering() : Notation.scientific())
.withExponentSignDisplay(signDisplay)
.withMinExponentDigits(minDigits);
return;
}
throw new SkeletonSyntaxException("Invalid scientific stem", segment);
}
private static void parseIntegerStem(StringSegment segment, MacroProps macros) {
assert(segment.charAt(0) == '0');
int offset = 1;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) != '0') {
offset--;
break;
}
}
if (offset < segment.length()) {
throw new SkeletonSyntaxException("Invalid integer stem", segment);
}
macros.integerWidth = IntegerWidth.zeroFillTo(offset);
return;
}
/** @return Whether we successfully found and parsed a frac-sig option. */
private static boolean parseFracSigOption(StringSegment segment, MacroProps macros) {
if (segment.charAt(0) != '@') {
return false;
}
int offset = 0;
int minSig = 0;
int maxSig;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == '@') {
minSig++;
} else {
break;
}
}
if (offset < segment.length()) {
if (isWildcardChar(segment.charAt(offset))) {
// @+, @@+, @@@+
maxSig = -1;
offset++;
} else {
// @#, @##, @###
// @@#, @@##, @@@#
maxSig = minSig;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == '#') {
maxSig++;
} else {
break;
}
}
}
} else {
// @, @@, @@@
maxSig = minSig;
}
FractionPrecision oldRounder = (FractionPrecision) macros.precision;
if (offset < segment.length()) {
RoundingPriority priority;
if (maxSig == -1) {
throw new SkeletonSyntaxException(
"Invalid digits option: Wildcard character not allowed with the priority annotation", segment);
}
if (segment.codePointAt(offset) == 'r') {
priority = RoundingPriority.RELAXED;
offset++;
} else if (segment.codePointAt(offset) == 's') {
priority = RoundingPriority.STRICT;
offset++;
} else {
assert offset < segment.length();
priority = RoundingPriority.RELAXED; // make compiler happy (uninitialized variable)
}
if (offset < segment.length()) {
throw new SkeletonSyntaxException(
"Invalid digits option for fraction rounder", segment);
}
macros.precision = oldRounder.withSignificantDigits(minSig, maxSig, priority);
} else if (maxSig == -1) {
// withMinDigits
macros.precision = oldRounder.withMinDigits(minSig);
} else if (minSig == 1) {
// withMaxDigits
macros.precision = oldRounder.withMaxDigits(maxSig);
} else {
throw new SkeletonSyntaxException(
"Invalid digits option: Priority annotation required", segment);
}
return true;
}
/** @return Whether we successfully found and parsed a trailing zero option. */
private static boolean parseTrailingZeroOption(StringSegment segment, MacroProps macros) {
if (segment.contentEquals("w")) {
macros.precision = macros.precision.trailingZeroDisplay(TrailingZeroDisplay.HIDE_IF_WHOLE);
return true;
}
return false;
}
private static void parseIncrementOption(StringSegment segment, MacroProps macros) {
// Call segment.subSequence() because segment.toString() doesn't create a clean string.
String str = segment.subSequence(0, segment.length()).toString();
BigDecimal increment;
try {
increment = new BigDecimal(str);
} catch (NumberFormatException e) {
throw new SkeletonSyntaxException("Invalid rounding increment", segment, e);
}
macros.precision = Precision.increment(increment);
}
private static void generateIncrementOption(BigDecimal increment, StringBuilder sb) {
sb.append(increment.toPlainString());
}
private static void parseIntegerWidthOption(StringSegment segment, MacroProps macros) {
int offset = 0;
int minInt = 0;
int maxInt;
if (isWildcardChar(segment.charAt(0))) {
maxInt = -1;
offset++;
} else {
maxInt = 0;
}
for (; offset < segment.length(); offset++) {
if (maxInt != -1 && segment.charAt(offset) == '#') {
maxInt++;
} else {
break;
}
}
if (offset < segment.length()) {
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == '0') {
minInt++;
} else {
break;
}
}
}
if (maxInt != -1) {
maxInt += minInt;
}
if (offset < segment.length()) {
throw new SkeletonSyntaxException("Invalid integer width stem", segment);
}
// Use the public APIs to enforce bounds checking
if (maxInt == -1) {
macros.integerWidth = IntegerWidth.zeroFillTo(minInt);
} else {
macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
}
}
private static void generateIntegerWidthOption(int minInt, int maxInt, StringBuilder sb) {
if (maxInt == -1) {
sb.append(WILDCARD_CHAR);
} else {
appendMultiple(sb, '#', maxInt - minInt);
}
appendMultiple(sb, '0', minInt);
}
private static void parseNumberingSystemOption(StringSegment segment, MacroProps macros) {
String nsName = segment.subSequence(0, segment.length()).toString();
NumberingSystem ns = NumberingSystem.getInstanceByName(nsName);
if (ns == null) {
throw new SkeletonSyntaxException("Unknown numbering system", segment);
}
macros.symbols = ns;
}
private static void generateNumberingSystemOption(NumberingSystem ns, StringBuilder sb) {
sb.append(ns.getName());
}
private static void parseScaleOption(StringSegment segment, MacroProps macros) {
// Call segment.subSequence() because segment.toString() doesn't create a clean string.
String str = segment.subSequence(0, segment.length()).toString();
BigDecimal bd;
try {
bd = new BigDecimal(str);
} catch (NumberFormatException e) {
throw new SkeletonSyntaxException("Invalid scale", segment, e);
}
// NOTE: If bd is a power of ten, the Scale API optimizes it for us.
macros.scale = Scale.byBigDecimal(bd);
}
private static void generateScaleOption(Scale scale, StringBuilder sb) {
BigDecimal bd = scale.arbitrary;
if (bd == null) {
bd = BigDecimal.ONE;
}
bd = bd.scaleByPowerOfTen(scale.magnitude);
sb.append(bd.toPlainString());
}
}
///// STEM GENERATION HELPER FUNCTIONS /////
/**
* Utility class for methods for generating a token corresponding to each macro-prop. Each method
* returns whether or not a token was written to the string builder.
*/
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");
return true;
} else if (macros.notation == Notation.compactShort()) {
sb.append("compact-short");
return true;
} else {
// Compact notation generated from custom data (not supported in skeleton)
// The other compact notations are literals
throw new UnsupportedOperationException(
"Cannot generate number skeleton with custom compact data");
}
} else if (macros.notation instanceof ScientificNotation) {
ScientificNotation impl = (ScientificNotation) macros.notation;
if (impl.engineeringInterval == 3) {
sb.append("engineering");
} else {
sb.append("scientific");
}
if (impl.minExponentDigits > 1) {
sb.append('/');
BlueprintHelpers.generateExponentWidthOption(impl.minExponentDigits, sb);
}
if (impl.exponentSignDisplay != SignDisplay.AUTO) {
sb.append('/');
EnumToStemString.signDisplay(impl.exponentSignDisplay, sb);
}
return true;
} else {
assert macros.notation instanceof SimpleNotation;
// Default value is not shown in normalized form
return false;
}
}
private static boolean unit(MacroProps macros, StringBuilder sb) {
MeasureUnit unit = macros.unit;
if (macros.perUnit != null) {
if (macros.unit instanceof Currency || macros.perUnit instanceof Currency) {
throw new UnsupportedOperationException(
"Cannot generate number skeleton with currency unit and per-unit");
}
unit = unit.product(macros.perUnit.reciprocal());
}
if (unit instanceof Currency) {
sb.append("currency/");
BlueprintHelpers.generateCurrencyOption((Currency)unit, sb);
return true;
} else if (unit.equals(MeasureUnit.PERCENT)) {
sb.append("percent");
return true;
} else if (unit.equals(MeasureUnit.PERMILLE)) {
sb.append("permille");
return true;
} else {
sb.append("unit/");
sb.append(unit.getIdentifier());
return true;
}
}
private static boolean usage(MacroProps macros, StringBuilder sb) {
if (macros.usage != null && macros.usage.length() > 0) {
sb.append("usage/");
sb.append(macros.usage);
return true;
}
return false;
}
private static boolean precision(MacroProps macros, StringBuilder sb) {
if (macros.precision instanceof Precision.InfiniteRounderImpl) {
sb.append("precision-unlimited");
} else if (macros.precision instanceof Precision.FractionRounderImpl) {
Precision.FractionRounderImpl impl = (Precision.FractionRounderImpl) macros.precision;
BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
} else if (macros.precision instanceof Precision.SignificantRounderImpl) {
Precision.SignificantRounderImpl impl = (Precision.SignificantRounderImpl) macros.precision;
BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
} else if (macros.precision instanceof Precision.FracSigRounderImpl) {
Precision.FracSigRounderImpl impl = (Precision.FracSigRounderImpl) macros.precision;
BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
sb.append('/');
if (impl.retain) {
if (impl.priority == RoundingPriority.RELAXED) {
BlueprintHelpers.generateDigitsStem(impl.maxSig, -1, sb);
} else {
BlueprintHelpers.generateDigitsStem(1, impl.maxSig, sb);
}
} else {
BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
if (impl.priority == RoundingPriority.RELAXED) {
sb.append('r');
} else {
sb.append('s');
}
}
} else if (macros.precision instanceof Precision.IncrementRounderImpl) {
Precision.IncrementRounderImpl impl = (Precision.IncrementRounderImpl) macros.precision;
sb.append("precision-increment/");
BlueprintHelpers.generateIncrementOption(impl.increment, sb);
} else {
assert macros.precision instanceof Precision.CurrencyRounderImpl;
Precision.CurrencyRounderImpl impl = (Precision.CurrencyRounderImpl) macros.precision;
if (impl.usage == CurrencyUsage.STANDARD) {
sb.append("precision-currency-standard");
} else {
sb.append("precision-currency-cash");
}
}
if (macros.precision.trailingZeroDisplay == TrailingZeroDisplay.HIDE_IF_WHOLE) {
sb.append("/w");
}
// NOTE: Always return true for rounding because the default value depends on other options.
return true;
}
private static boolean roundingMode(MacroProps macros, StringBuilder sb) {
if (macros.roundingMode == RoundingUtils.DEFAULT_ROUNDING_MODE) {
return false; // Default value
}
EnumToStemString.roundingMode(macros.roundingMode, sb);
return true;
}
private static boolean grouping(MacroProps macros, StringBuilder sb) {
if (macros.grouping instanceof GroupingStrategy) {
if (macros.grouping == GroupingStrategy.AUTO) {
return false; // Default value
}
EnumToStemString.groupingStrategy((GroupingStrategy) macros.grouping, sb);
return true;
} else {
throw new UnsupportedOperationException(
"Cannot generate number skeleton with custom Grouper");
}
}
private static boolean integerWidth(MacroProps macros, StringBuilder sb) {
if (macros.integerWidth.equals(IntegerWidth.DEFAULT)) {
return false; // Default
}
if (macros.integerWidth.minInt == 0 && macros.integerWidth.maxInt == 0) {
sb.append("integer-width-trunc");
return true;
}
sb.append("integer-width/");
BlueprintHelpers.generateIntegerWidthOption(macros.integerWidth.minInt,
macros.integerWidth.maxInt,
sb);
return true;
}
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/");
BlueprintHelpers.generateNumberingSystemOption(ns, sb);
}
return true;
} else {
assert macros.symbols instanceof DecimalFormatSymbols;
throw new UnsupportedOperationException(
"Cannot generate number skeleton with custom DecimalFormatSymbols");
}
}
private static boolean unitWidth(MacroProps macros, StringBuilder sb) {
if (macros.unitWidth == UnitWidth.SHORT) {
return false; // Default value
}
EnumToStemString.unitWidth(macros.unitWidth, sb);
return true;
}
private static boolean sign(MacroProps macros, StringBuilder sb) {
if (macros.sign == SignDisplay.AUTO) {
return false; // Default value
}
EnumToStemString.signDisplay(macros.sign, sb);
return true;
}
private static boolean decimal(MacroProps macros, StringBuilder sb) {
if (macros.decimal == DecimalSeparatorDisplay.AUTO) {
return false; // Default value
}
EnumToStemString.decimalSeparatorDisplay(macros.decimal, sb);
return true;
}
private static boolean scale(MacroProps macros, StringBuilder sb) {
if (!macros.scale.isValid()) {
return false; // Default value
}
sb.append("scale/");
BlueprintHelpers.generateScaleOption(macros.scale, sb);
return true;
}
}
///// OTHER UTILITY FUNCTIONS /////
private static void checkNull(Object value, CharSequence content) {
if (value != null) {
throw new SkeletonSyntaxException("Duplicated setting", content);
}
}
private static void appendMultiple(StringBuilder sb, int cp, int count) {
for (int i = 0; i < count; i++) {
sb.appendCodePoint(cp);
}
}
}