/* GENERATED SOURCE. DO NOT MODIFY. */ // © 2018 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html package android.icu.number; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Set; import android.icu.impl.CacheBase; import android.icu.impl.PatternProps; import android.icu.impl.SoftCache; import android.icu.impl.StringSegment; import android.icu.impl.number.MacroProps; import android.icu.impl.number.RoundingUtils; import android.icu.number.NumberFormatter.DecimalSeparatorDisplay; import android.icu.number.NumberFormatter.GroupingStrategy; import android.icu.number.NumberFormatter.RoundingPriority; import android.icu.number.NumberFormatter.SignDisplay; import android.icu.number.NumberFormatter.TrailingZeroDisplay; import android.icu.number.NumberFormatter.UnitWidth; import android.icu.text.DecimalFormatSymbols; import android.icu.text.NumberingSystem; import android.icu.util.BytesTrie; import android.icu.util.CharsTrie; import android.icu.util.CharsTrieBuilder; import android.icu.util.Currency; import android.icu.util.Currency.CurrencyUsage; import android.icu.util.MeasureUnit; import android.icu.util.NoUnit; import android.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); } } }