blob: 07e81c45ec9e87187d2a7a96a7295d04543e3e64 [file] [log] [blame]
// © 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.SignDisplay;
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,
// 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_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_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("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("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());
// 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;
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;
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<String, UnlocalizedNumberFormatter, Void> cache = new SoftCache<String, UnlocalizedNumberFormatter, Void>() {
@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_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_NULL;
case 'E':
checkNull(macros.notation, segment);
BlueprintHelpers.parseScientificStem(segment, macros);
return ParseState.STATE_NULL;
case '0':
checkNull(macros.notation, 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_NULL;
}
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_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:
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:
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);
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_NULL;
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_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.perUnit != null && GeneratorHelpers.perUnit(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.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());
}
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<MeasureUnit> units = MeasureUnit.getAvailable(type);
for (MeasureUnit unit : units) {
if (subType.equals(unit.getSubtype())) {
macros.unit = unit;
return;
}
}
throw new SkeletonSyntaxException("Unknown measure unit", segment);
}
private static void generateMeasureUnitOption(MeasureUnit unit, StringBuilder sb) {
sb.append(unit.getType());
sb.append("-");
sb.append(unit.getSubtype());
}
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) {
MeasureUnit[] units = MeasureUnit.parseCoreUnitIdentifier(segment.asString());
if (units == null) {
throw new SkeletonSyntaxException("Invalid core unit identifier", segment);
}
macros.unit = units[0];
if (units.length == 2) {
macros.perUnit = units[1];
}
}
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 {
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;
}
}
// For the frac-sig option, there must be minSig or maxSig but not both.
// Valid: @+, @@+, @@@+
// Valid: @#, @##, @###
// Invalid: @, @@, @@@
// Invalid: @@#, @@##, @@@#
if (offset < segment.length()) {
if (isWildcardChar(segment.charAt(offset))) {
maxSig = -1;
offset++;
} else if (minSig > 1) {
// @@#, @@##, @@@#
throw new SkeletonSyntaxException("Invalid digits option for fraction rounder",
segment);
} else {
maxSig = minSig;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == '#') {
maxSig++;
} else {
break;
}
}
}
} else {
// @, @@, @@@
throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
}
if (offset < segment.length()) {
throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
}
FractionPrecision oldRounder = (FractionPrecision) macros.precision;
if (maxSig == -1) {
macros.precision = oldRounder.withMinDigits(minSig);
} else {
macros.precision = oldRounder.withMaxDigits(maxSig);
}
return true;
}
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) {
if (macros.unit instanceof Currency) {
sb.append("currency/");
BlueprintHelpers.generateCurrencyOption((Currency) macros.unit, sb);
return true;
} else if (macros.unit == MeasureUnit.PERCENT) {
sb.append("percent");
return true;
} else if (macros.unit == MeasureUnit.PERMILLE) {
sb.append("permille");
return true;
} else if (macros.unit.getType() != null) {
sb.append("measure-unit/");
BlueprintHelpers.generateMeasureUnitOption(macros.unit, sb);
return true;
} else {
// TODO(icu-units#35): add support for not-built-in units.
throw new UnsupportedOperationException();
}
}
private static boolean perUnit(MacroProps macros, StringBuilder sb) {
// Per-units are currently expected to be only MeasureUnits.
if (macros.perUnit instanceof Currency) {
throw new UnsupportedOperationException(
"Cannot generate number skeleton with per-unit that is not a standard measure unit");
} else {
sb.append("per-measure-unit/");
BlueprintHelpers.generateMeasureUnitOption(macros.perUnit, sb);
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.minSig == -1) {
BlueprintHelpers.generateDigitsStem(1, impl.maxSig, sb);
} else {
BlueprintHelpers.generateDigitsStem(impl.minSig, -1, sb);
}
} 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");
}
}
// 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
}
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);
}
}
}