| // © 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_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("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<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_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_NULL; |
| 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_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: |
| 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_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.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<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); |
| } |
| |
| // "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; |
| } |
| } |
| // 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) { |
| 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.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); |
| } |
| } |
| } |