1 /* GENERATED SOURCE. DO NOT MODIFY. */ 2 // © 2018 and later: Unicode, Inc. and others. 3 // License & terms of use: http://www.unicode.org/copyright.html#License 4 package ohos.global.icu.number; 5 6 import java.math.BigDecimal; 7 import java.math.RoundingMode; 8 import java.util.Set; 9 10 import ohos.global.icu.impl.CacheBase; 11 import ohos.global.icu.impl.PatternProps; 12 import ohos.global.icu.impl.SoftCache; 13 import ohos.global.icu.impl.StringSegment; 14 import ohos.global.icu.impl.number.MacroProps; 15 import ohos.global.icu.impl.number.RoundingUtils; 16 import ohos.global.icu.number.NumberFormatter.DecimalSeparatorDisplay; 17 import ohos.global.icu.number.NumberFormatter.GroupingStrategy; 18 import ohos.global.icu.number.NumberFormatter.SignDisplay; 19 import ohos.global.icu.number.NumberFormatter.UnitWidth; 20 import ohos.global.icu.text.DecimalFormatSymbols; 21 import ohos.global.icu.text.NumberingSystem; 22 import ohos.global.icu.util.BytesTrie; 23 import ohos.global.icu.util.CharsTrie; 24 import ohos.global.icu.util.CharsTrieBuilder; 25 import ohos.global.icu.util.Currency; 26 import ohos.global.icu.util.Currency.CurrencyUsage; 27 import ohos.global.icu.util.MeasureUnit; 28 import ohos.global.icu.util.NoUnit; 29 import ohos.global.icu.util.StringTrieBuilder; 30 31 /** 32 * @author sffc 33 * 34 */ 35 class NumberSkeletonImpl { 36 37 /////////////////////////////////////////////////////////////////////////////////////// 38 // NOTE: For an example of how to add a new stem to the number skeleton parser, see: // 39 // http://bugs.icu-project.org/trac/changeset/41193 // 40 /////////////////////////////////////////////////////////////////////////////////////// 41 42 /** 43 * While parsing a skeleton, this enum records what type of option we expect to find next. 44 */ 45 static enum ParseState { 46 // Section 0: We expect whitespace or a stem, but not an option: 47 STATE_NULL, 48 49 // Section 1: We might accept an option, but it is not required: 50 STATE_SCIENTIFIC, 51 STATE_FRACTION_PRECISION, 52 53 // Section 2: An option is required: 54 STATE_INCREMENT_PRECISION, 55 STATE_MEASURE_UNIT, 56 STATE_PER_MEASURE_UNIT, 57 STATE_IDENTIFIER_UNIT, 58 STATE_CURRENCY_UNIT, 59 STATE_INTEGER_WIDTH, 60 STATE_NUMBERING_SYSTEM, 61 STATE_SCALE, 62 } 63 64 /** 65 * All possible stem literals have an entry in the StemEnum. The enum name is the kebab case stem 66 * string literal written in upper snake case. 67 * 68 * @see StemToObject 69 * @see #SERIALIZED_STEM_TRIE 70 */ 71 static enum StemEnum { 72 // Section 1: Stems that do not require an option: 73 STEM_COMPACT_SHORT, 74 STEM_COMPACT_LONG, 75 STEM_SCIENTIFIC, 76 STEM_ENGINEERING, 77 STEM_NOTATION_SIMPLE, 78 STEM_BASE_UNIT, 79 STEM_PERCENT, 80 STEM_PERMILLE, 81 STEM_PERCENT_100, // concise-only 82 STEM_PRECISION_INTEGER, 83 STEM_PRECISION_UNLIMITED, 84 STEM_PRECISION_CURRENCY_STANDARD, 85 STEM_PRECISION_CURRENCY_CASH, 86 STEM_ROUNDING_MODE_CEILING, 87 STEM_ROUNDING_MODE_FLOOR, 88 STEM_ROUNDING_MODE_DOWN, 89 STEM_ROUNDING_MODE_UP, 90 STEM_ROUNDING_MODE_HALF_EVEN, 91 STEM_ROUNDING_MODE_HALF_DOWN, 92 STEM_ROUNDING_MODE_HALF_UP, 93 STEM_ROUNDING_MODE_UNNECESSARY, 94 STEM_GROUP_OFF, 95 STEM_GROUP_MIN2, 96 STEM_GROUP_AUTO, 97 STEM_GROUP_ON_ALIGNED, 98 STEM_GROUP_THOUSANDS, 99 STEM_LATIN, 100 STEM_UNIT_WIDTH_NARROW, 101 STEM_UNIT_WIDTH_SHORT, 102 STEM_UNIT_WIDTH_FULL_NAME, 103 STEM_UNIT_WIDTH_ISO_CODE, 104 STEM_UNIT_WIDTH_FORMAL, 105 STEM_UNIT_WIDTH_VARIANT, 106 STEM_UNIT_WIDTH_HIDDEN, 107 STEM_SIGN_AUTO, 108 STEM_SIGN_ALWAYS, 109 STEM_SIGN_NEVER, 110 STEM_SIGN_ACCOUNTING, 111 STEM_SIGN_ACCOUNTING_ALWAYS, 112 STEM_SIGN_EXCEPT_ZERO, 113 STEM_SIGN_ACCOUNTING_EXCEPT_ZERO, 114 STEM_DECIMAL_AUTO, 115 STEM_DECIMAL_ALWAYS, 116 117 // Section 2: Stems that DO require an option: 118 STEM_PRECISION_INCREMENT, 119 STEM_MEASURE_UNIT, 120 STEM_PER_MEASURE_UNIT, 121 STEM_UNIT, 122 STEM_CURRENCY, 123 STEM_INTEGER_WIDTH, 124 STEM_NUMBERING_SYSTEM, 125 STEM_SCALE, 126 }; 127 128 /** Default wildcard char, accepted on input and printed in output */ 129 static final char WILDCARD_CHAR = '*'; 130 131 /** Alternative wildcard char, accept on input but not printed in output */ 132 static final char ALT_WILDCARD_CHAR = '+'; 133 134 /** Checks whether the char is a wildcard on input */ isWildcardChar(char c)135 static boolean isWildcardChar(char c) { 136 return c == WILDCARD_CHAR || c == ALT_WILDCARD_CHAR; 137 } 138 139 /** For mapping from ordinal back to StemEnum in Java. */ 140 static final StemEnum[] STEM_ENUM_VALUES = StemEnum.values(); 141 142 /** A data structure for mapping from stem strings to the stem enum. Built at startup. */ 143 static final String SERIALIZED_STEM_TRIE = buildStemTrie(); 144 buildStemTrie()145 static String buildStemTrie() { 146 CharsTrieBuilder b = new CharsTrieBuilder(); 147 148 // Section 1: 149 b.add("compact-short", StemEnum.STEM_COMPACT_SHORT.ordinal()); 150 b.add("compact-long", StemEnum.STEM_COMPACT_LONG.ordinal()); 151 b.add("scientific", StemEnum.STEM_SCIENTIFIC.ordinal()); 152 b.add("engineering", StemEnum.STEM_ENGINEERING.ordinal()); 153 b.add("notation-simple", StemEnum.STEM_NOTATION_SIMPLE.ordinal()); 154 b.add("base-unit", StemEnum.STEM_BASE_UNIT.ordinal()); 155 b.add("percent", StemEnum.STEM_PERCENT.ordinal()); 156 b.add("permille", StemEnum.STEM_PERMILLE.ordinal()); 157 b.add("precision-integer", StemEnum.STEM_PRECISION_INTEGER.ordinal()); 158 b.add("precision-unlimited", StemEnum.STEM_PRECISION_UNLIMITED.ordinal()); 159 b.add("precision-currency-standard", StemEnum.STEM_PRECISION_CURRENCY_STANDARD.ordinal()); 160 b.add("precision-currency-cash", StemEnum.STEM_PRECISION_CURRENCY_CASH.ordinal()); 161 b.add("rounding-mode-ceiling", StemEnum.STEM_ROUNDING_MODE_CEILING.ordinal()); 162 b.add("rounding-mode-floor", StemEnum.STEM_ROUNDING_MODE_FLOOR.ordinal()); 163 b.add("rounding-mode-down", StemEnum.STEM_ROUNDING_MODE_DOWN.ordinal()); 164 b.add("rounding-mode-up", StemEnum.STEM_ROUNDING_MODE_UP.ordinal()); 165 b.add("rounding-mode-half-even", StemEnum.STEM_ROUNDING_MODE_HALF_EVEN.ordinal()); 166 b.add("rounding-mode-half-down", StemEnum.STEM_ROUNDING_MODE_HALF_DOWN.ordinal()); 167 b.add("rounding-mode-half-up", StemEnum.STEM_ROUNDING_MODE_HALF_UP.ordinal()); 168 b.add("rounding-mode-unnecessary", StemEnum.STEM_ROUNDING_MODE_UNNECESSARY.ordinal()); 169 b.add("group-off", StemEnum.STEM_GROUP_OFF.ordinal()); 170 b.add("group-min2", StemEnum.STEM_GROUP_MIN2.ordinal()); 171 b.add("group-auto", StemEnum.STEM_GROUP_AUTO.ordinal()); 172 b.add("group-on-aligned", StemEnum.STEM_GROUP_ON_ALIGNED.ordinal()); 173 b.add("group-thousands", StemEnum.STEM_GROUP_THOUSANDS.ordinal()); 174 b.add("latin", StemEnum.STEM_LATIN.ordinal()); 175 b.add("unit-width-narrow", StemEnum.STEM_UNIT_WIDTH_NARROW.ordinal()); 176 b.add("unit-width-short", StemEnum.STEM_UNIT_WIDTH_SHORT.ordinal()); 177 b.add("unit-width-full-name", StemEnum.STEM_UNIT_WIDTH_FULL_NAME.ordinal()); 178 b.add("unit-width-iso-code", StemEnum.STEM_UNIT_WIDTH_ISO_CODE.ordinal()); 179 b.add("unit-width-formal", StemEnum.STEM_UNIT_WIDTH_FORMAL.ordinal()); 180 b.add("unit-width-variant", StemEnum.STEM_UNIT_WIDTH_VARIANT.ordinal()); 181 b.add("unit-width-hidden", StemEnum.STEM_UNIT_WIDTH_HIDDEN.ordinal()); 182 b.add("sign-auto", StemEnum.STEM_SIGN_AUTO.ordinal()); 183 b.add("sign-always", StemEnum.STEM_SIGN_ALWAYS.ordinal()); 184 b.add("sign-never", StemEnum.STEM_SIGN_NEVER.ordinal()); 185 b.add("sign-accounting", StemEnum.STEM_SIGN_ACCOUNTING.ordinal()); 186 b.add("sign-accounting-always", StemEnum.STEM_SIGN_ACCOUNTING_ALWAYS.ordinal()); 187 b.add("sign-except-zero", StemEnum.STEM_SIGN_EXCEPT_ZERO.ordinal()); 188 b.add("sign-accounting-except-zero", StemEnum.STEM_SIGN_ACCOUNTING_EXCEPT_ZERO.ordinal()); 189 b.add("decimal-auto", StemEnum.STEM_DECIMAL_AUTO.ordinal()); 190 b.add("decimal-always", StemEnum.STEM_DECIMAL_ALWAYS.ordinal()); 191 192 // Section 2: 193 b.add("precision-increment", StemEnum.STEM_PRECISION_INCREMENT.ordinal()); 194 b.add("measure-unit", StemEnum.STEM_MEASURE_UNIT.ordinal()); 195 b.add("per-measure-unit", StemEnum.STEM_PER_MEASURE_UNIT.ordinal()); 196 b.add("unit", StemEnum.STEM_UNIT.ordinal()); 197 b.add("currency", StemEnum.STEM_CURRENCY.ordinal()); 198 b.add("integer-width", StemEnum.STEM_INTEGER_WIDTH.ordinal()); 199 b.add("numbering-system", StemEnum.STEM_NUMBERING_SYSTEM.ordinal()); 200 b.add("scale", StemEnum.STEM_SCALE.ordinal()); 201 202 // Section 3 (concise tokens): 203 b.add("K", StemEnum.STEM_COMPACT_SHORT.ordinal()); 204 b.add("KK", StemEnum.STEM_COMPACT_LONG.ordinal()); 205 b.add("%", StemEnum.STEM_PERCENT.ordinal()); 206 b.add("%x100", StemEnum.STEM_PERCENT_100.ordinal()); 207 b.add(",_", StemEnum.STEM_GROUP_OFF.ordinal()); 208 b.add(",?", StemEnum.STEM_GROUP_MIN2.ordinal()); 209 b.add(",!", StemEnum.STEM_GROUP_ON_ALIGNED.ordinal()); 210 b.add("+!", StemEnum.STEM_SIGN_ALWAYS.ordinal()); 211 b.add("+_", StemEnum.STEM_SIGN_NEVER.ordinal()); 212 b.add("()", StemEnum.STEM_SIGN_ACCOUNTING.ordinal()); 213 b.add("()!", StemEnum.STEM_SIGN_ACCOUNTING_ALWAYS.ordinal()); 214 b.add("+?", StemEnum.STEM_SIGN_EXCEPT_ZERO.ordinal()); 215 b.add("()?", StemEnum.STEM_SIGN_ACCOUNTING_EXCEPT_ZERO.ordinal()); 216 217 // Build the CharsTrie 218 // TODO: Use SLOW or FAST here? 219 return b.buildCharSequence(StringTrieBuilder.Option.FAST).toString(); 220 } 221 222 /** 223 * Utility class for methods that convert from StemEnum to corresponding objects or enums. This 224 * applies to only the "Section 1" stems, those that are well-defined without an option. 225 */ 226 static final class StemToObject { 227 notation(StemEnum stem)228 private static Notation notation(StemEnum stem) { 229 switch (stem) { 230 case STEM_COMPACT_SHORT: 231 return Notation.compactShort(); 232 case STEM_COMPACT_LONG: 233 return Notation.compactLong(); 234 case STEM_SCIENTIFIC: 235 return Notation.scientific(); 236 case STEM_ENGINEERING: 237 return Notation.engineering(); 238 case STEM_NOTATION_SIMPLE: 239 return Notation.simple(); 240 default: 241 throw new AssertionError(); 242 } 243 } 244 unit(StemEnum stem)245 private static MeasureUnit unit(StemEnum stem) { 246 switch (stem) { 247 case STEM_BASE_UNIT: 248 return NoUnit.BASE; 249 case STEM_PERCENT: 250 return NoUnit.PERCENT; 251 case STEM_PERMILLE: 252 return NoUnit.PERMILLE; 253 default: 254 throw new AssertionError(); 255 } 256 } 257 precision(StemEnum stem)258 private static Precision precision(StemEnum stem) { 259 switch (stem) { 260 case STEM_PRECISION_INTEGER: 261 return Precision.integer(); 262 case STEM_PRECISION_UNLIMITED: 263 return Precision.unlimited(); 264 case STEM_PRECISION_CURRENCY_STANDARD: 265 return Precision.currency(CurrencyUsage.STANDARD); 266 case STEM_PRECISION_CURRENCY_CASH: 267 return Precision.currency(CurrencyUsage.CASH); 268 default: 269 throw new AssertionError(); 270 } 271 } 272 roundingMode(StemEnum stem)273 private static RoundingMode roundingMode(StemEnum stem) { 274 switch (stem) { 275 case STEM_ROUNDING_MODE_CEILING: 276 return RoundingMode.CEILING; 277 case STEM_ROUNDING_MODE_FLOOR: 278 return RoundingMode.FLOOR; 279 case STEM_ROUNDING_MODE_DOWN: 280 return RoundingMode.DOWN; 281 case STEM_ROUNDING_MODE_UP: 282 return RoundingMode.UP; 283 case STEM_ROUNDING_MODE_HALF_EVEN: 284 return RoundingMode.HALF_EVEN; 285 case STEM_ROUNDING_MODE_HALF_DOWN: 286 return RoundingMode.HALF_DOWN; 287 case STEM_ROUNDING_MODE_HALF_UP: 288 return RoundingMode.HALF_UP; 289 case STEM_ROUNDING_MODE_UNNECESSARY: 290 return RoundingMode.UNNECESSARY; 291 default: 292 throw new AssertionError(); 293 } 294 } 295 groupingStrategy(StemEnum stem)296 private static GroupingStrategy groupingStrategy(StemEnum stem) { 297 switch (stem) { 298 case STEM_GROUP_OFF: 299 return GroupingStrategy.OFF; 300 case STEM_GROUP_MIN2: 301 return GroupingStrategy.MIN2; 302 case STEM_GROUP_AUTO: 303 return GroupingStrategy.AUTO; 304 case STEM_GROUP_ON_ALIGNED: 305 return GroupingStrategy.ON_ALIGNED; 306 case STEM_GROUP_THOUSANDS: 307 return GroupingStrategy.THOUSANDS; 308 default: 309 return null; // for objects, throw; for enums, return null 310 } 311 } 312 unitWidth(StemEnum stem)313 private static UnitWidth unitWidth(StemEnum stem) { 314 switch (stem) { 315 case STEM_UNIT_WIDTH_NARROW: 316 return UnitWidth.NARROW; 317 case STEM_UNIT_WIDTH_SHORT: 318 return UnitWidth.SHORT; 319 case STEM_UNIT_WIDTH_FULL_NAME: 320 return UnitWidth.FULL_NAME; 321 case STEM_UNIT_WIDTH_ISO_CODE: 322 return UnitWidth.ISO_CODE; 323 case STEM_UNIT_WIDTH_FORMAL: 324 return UnitWidth.FORMAL; 325 case STEM_UNIT_WIDTH_VARIANT: 326 return UnitWidth.VARIANT; 327 case STEM_UNIT_WIDTH_HIDDEN: 328 return UnitWidth.HIDDEN; 329 default: 330 return null; // for objects, throw; for enums, return null 331 } 332 } 333 signDisplay(StemEnum stem)334 private static SignDisplay signDisplay(StemEnum stem) { 335 switch (stem) { 336 case STEM_SIGN_AUTO: 337 return SignDisplay.AUTO; 338 case STEM_SIGN_ALWAYS: 339 return SignDisplay.ALWAYS; 340 case STEM_SIGN_NEVER: 341 return SignDisplay.NEVER; 342 case STEM_SIGN_ACCOUNTING: 343 return SignDisplay.ACCOUNTING; 344 case STEM_SIGN_ACCOUNTING_ALWAYS: 345 return SignDisplay.ACCOUNTING_ALWAYS; 346 case STEM_SIGN_EXCEPT_ZERO: 347 return SignDisplay.EXCEPT_ZERO; 348 case STEM_SIGN_ACCOUNTING_EXCEPT_ZERO: 349 return SignDisplay.ACCOUNTING_EXCEPT_ZERO; 350 default: 351 return null; // for objects, throw; for enums, return null 352 } 353 } 354 decimalSeparatorDisplay(StemEnum stem)355 private static DecimalSeparatorDisplay decimalSeparatorDisplay(StemEnum stem) { 356 switch (stem) { 357 case STEM_DECIMAL_AUTO: 358 return DecimalSeparatorDisplay.AUTO; 359 case STEM_DECIMAL_ALWAYS: 360 return DecimalSeparatorDisplay.ALWAYS; 361 default: 362 return null; // for objects, throw; for enums, return null 363 } 364 } 365 } 366 367 /** 368 * Utility class for methods that convert from enums to stem strings. More complex object conversions 369 * take place in ObjectToStemString. 370 */ 371 static final class EnumToStemString { 372 roundingMode(RoundingMode value, StringBuilder sb)373 private static void roundingMode(RoundingMode value, StringBuilder sb) { 374 switch (value) { 375 case CEILING: 376 sb.append("rounding-mode-ceiling"); 377 break; 378 case FLOOR: 379 sb.append("rounding-mode-floor"); 380 break; 381 case DOWN: 382 sb.append("rounding-mode-down"); 383 break; 384 case UP: 385 sb.append("rounding-mode-up"); 386 break; 387 case HALF_EVEN: 388 sb.append("rounding-mode-half-even"); 389 break; 390 case HALF_DOWN: 391 sb.append("rounding-mode-half-down"); 392 break; 393 case HALF_UP: 394 sb.append("rounding-mode-half-up"); 395 break; 396 case UNNECESSARY: 397 sb.append("rounding-mode-unnecessary"); 398 break; 399 default: 400 throw new AssertionError(); 401 } 402 } 403 groupingStrategy(GroupingStrategy value, StringBuilder sb)404 private static void groupingStrategy(GroupingStrategy value, StringBuilder sb) { 405 switch (value) { 406 case OFF: 407 sb.append("group-off"); 408 break; 409 case MIN2: 410 sb.append("group-min2"); 411 break; 412 case AUTO: 413 sb.append("group-auto"); 414 break; 415 case ON_ALIGNED: 416 sb.append("group-on-aligned"); 417 break; 418 case THOUSANDS: 419 sb.append("group-thousands"); 420 break; 421 default: 422 throw new AssertionError(); 423 } 424 } 425 unitWidth(UnitWidth value, StringBuilder sb)426 private static void unitWidth(UnitWidth value, StringBuilder sb) { 427 switch (value) { 428 case NARROW: 429 sb.append("unit-width-narrow"); 430 break; 431 case SHORT: 432 sb.append("unit-width-short"); 433 break; 434 case FULL_NAME: 435 sb.append("unit-width-full-name"); 436 break; 437 case ISO_CODE: 438 sb.append("unit-width-iso-code"); 439 break; 440 case FORMAL: 441 sb.append("unit-width-formal"); 442 break; 443 case VARIANT: 444 sb.append("unit-width-variant"); 445 break; 446 case HIDDEN: 447 sb.append("unit-width-hidden"); 448 break; 449 default: 450 throw new AssertionError(); 451 } 452 } 453 signDisplay(SignDisplay value, StringBuilder sb)454 private static void signDisplay(SignDisplay value, StringBuilder sb) { 455 switch (value) { 456 case AUTO: 457 sb.append("sign-auto"); 458 break; 459 case ALWAYS: 460 sb.append("sign-always"); 461 break; 462 case NEVER: 463 sb.append("sign-never"); 464 break; 465 case ACCOUNTING: 466 sb.append("sign-accounting"); 467 break; 468 case ACCOUNTING_ALWAYS: 469 sb.append("sign-accounting-always"); 470 break; 471 case EXCEPT_ZERO: 472 sb.append("sign-except-zero"); 473 break; 474 case ACCOUNTING_EXCEPT_ZERO: 475 sb.append("sign-accounting-except-zero"); 476 break; 477 default: 478 throw new AssertionError(); 479 } 480 } 481 decimalSeparatorDisplay(DecimalSeparatorDisplay value, StringBuilder sb)482 private static void decimalSeparatorDisplay(DecimalSeparatorDisplay value, StringBuilder sb) { 483 switch (value) { 484 case AUTO: 485 sb.append("decimal-auto"); 486 break; 487 case ALWAYS: 488 sb.append("decimal-always"); 489 break; 490 default: 491 throw new AssertionError(); 492 } 493 } 494 } 495 496 ///// ENTRYPOINT FUNCTIONS ///// 497 498 /** Cache for parsed skeleton strings. */ 499 private static final CacheBase<String, UnlocalizedNumberFormatter, Void> cache = new SoftCache<String, UnlocalizedNumberFormatter, Void>() { 500 @Override 501 protected UnlocalizedNumberFormatter createInstance(String skeletonString, Void unused) { 502 return create(skeletonString); 503 } 504 }; 505 506 /** 507 * Gets the number formatter for the given number skeleton string from the cache, creating it if it 508 * does not exist in the cache. 509 * 510 * @param skeletonString 511 * A number skeleton string, possibly not in its shortest form. 512 * @return An UnlocalizedNumberFormatter with behavior defined by the given skeleton string. 513 */ getOrCreate(String skeletonString)514 public static UnlocalizedNumberFormatter getOrCreate(String skeletonString) { 515 // TODO: This does not currently check the cache for the normalized form of the skeleton. 516 // A new cache implementation would be required for that to work. 517 return cache.getInstance(skeletonString, null); 518 } 519 520 /** 521 * Creates a NumberFormatter corresponding to the given skeleton string. 522 * 523 * @param skeletonString 524 * A number skeleton string, possibly not in its shortest form. 525 * @return An UnlocalizedNumberFormatter with behavior defined by the given skeleton string. 526 */ create(String skeletonString)527 public static UnlocalizedNumberFormatter create(String skeletonString) { 528 MacroProps macros = parseSkeleton(skeletonString); 529 return NumberFormatter.with().macros(macros); 530 } 531 532 /** 533 * Create a skeleton string corresponding to the given NumberFormatter. 534 * 535 * @param macros 536 * The NumberFormatter options object. 537 * @return A skeleton string in normalized form. 538 */ generate(MacroProps macros)539 public static String generate(MacroProps macros) { 540 StringBuilder sb = new StringBuilder(); 541 generateSkeleton(macros, sb); 542 return sb.toString(); 543 } 544 545 ///// MAIN PARSING FUNCTIONS ///// 546 547 /** 548 * Converts from a skeleton string to a MacroProps. This method contains the primary parse loop. 549 */ parseSkeleton(String skeletonString)550 private static MacroProps parseSkeleton(String skeletonString) { 551 // Add a trailing whitespace to the end of the skeleton string to make code cleaner. 552 skeletonString += " "; 553 554 MacroProps macros = new MacroProps(); 555 StringSegment segment = new StringSegment(skeletonString, false); 556 CharsTrie stemTrie = new CharsTrie(SERIALIZED_STEM_TRIE, 0); 557 ParseState stem = ParseState.STATE_NULL; 558 int offset = 0; 559 560 // Primary skeleton parse loop: 561 while (offset < segment.length()) { 562 int cp = segment.codePointAt(offset); 563 boolean isTokenSeparator = PatternProps.isWhiteSpace(cp); 564 boolean isOptionSeparator = (cp == '/'); 565 566 if (!isTokenSeparator && !isOptionSeparator) { 567 // Non-separator token; consume it. 568 offset += Character.charCount(cp); 569 if (stem == ParseState.STATE_NULL) { 570 // We are currently consuming a stem. 571 // Go to the next state in the stem trie. 572 stemTrie.nextForCodePoint(cp); 573 } 574 continue; 575 } 576 577 // We are looking at a token or option separator. 578 // If the segment is nonempty, parse it and reset the segment. 579 // Otherwise, make sure it is a valid repeating separator. 580 if (offset != 0) { 581 segment.setLength(offset); 582 if (stem == ParseState.STATE_NULL) { 583 // The first separator after the start of a token. Parse it as a stem. 584 stem = parseStem(segment, stemTrie, macros); 585 stemTrie.reset(); 586 } else { 587 // A separator after the first separator of a token. Parse it as an option. 588 stem = parseOption(stem, segment, macros); 589 } 590 segment.resetLength(); 591 592 // Consume the segment: 593 segment.adjustOffset(offset); 594 offset = 0; 595 596 } else if (stem != ParseState.STATE_NULL) { 597 // A separator ('/' or whitespace) following an option separator ('/') 598 segment.setLength(Character.charCount(cp)); // for error message 599 throw new SkeletonSyntaxException("Unexpected separator character", segment); 600 601 } else { 602 // Two spaces in a row; this is OK. 603 } 604 605 // Does the current stem forbid options? 606 if (isOptionSeparator && stem == ParseState.STATE_NULL) { 607 segment.setLength(Character.charCount(cp)); // for error message 608 throw new SkeletonSyntaxException("Unexpected option separator", segment); 609 } 610 611 // Does the current stem require an option? 612 if (isTokenSeparator && stem != ParseState.STATE_NULL) { 613 switch (stem) { 614 case STATE_INCREMENT_PRECISION: 615 case STATE_MEASURE_UNIT: 616 case STATE_PER_MEASURE_UNIT: 617 case STATE_CURRENCY_UNIT: 618 case STATE_INTEGER_WIDTH: 619 case STATE_NUMBERING_SYSTEM: 620 case STATE_SCALE: 621 segment.setLength(Character.charCount(cp)); // for error message 622 throw new SkeletonSyntaxException("Stem requires an option", segment); 623 default: 624 break; 625 } 626 stem = ParseState.STATE_NULL; 627 } 628 629 // Consume the separator: 630 segment.adjustOffset(Character.charCount(cp)); 631 } 632 assert stem == ParseState.STATE_NULL; 633 return macros; 634 } 635 636 /** 637 * Given that the current segment represents a stem, parse it and save the result. 638 * 639 * @return The next state after parsing this stem, corresponding to what subset of options to expect. 640 */ parseStem(StringSegment segment, CharsTrie stemTrie, MacroProps macros)641 private static ParseState parseStem(StringSegment segment, CharsTrie stemTrie, MacroProps macros) { 642 // First check for "blueprint" stems, which start with a "signal char" 643 switch (segment.charAt(0)) { 644 case '.': 645 checkNull(macros.precision, segment); 646 BlueprintHelpers.parseFractionStem(segment, macros); 647 return ParseState.STATE_FRACTION_PRECISION; 648 case '@': 649 checkNull(macros.precision, segment); 650 BlueprintHelpers.parseDigitsStem(segment, macros); 651 return ParseState.STATE_NULL; 652 case 'E': 653 checkNull(macros.notation, segment); 654 BlueprintHelpers.parseScientificStem(segment, macros); 655 return ParseState.STATE_NULL; 656 case '0': 657 checkNull(macros.notation, segment); 658 BlueprintHelpers.parseIntegerStem(segment, macros); 659 return ParseState.STATE_NULL; 660 } 661 662 // Now look at the stemsTrie, which is already be pointing at our stem. 663 BytesTrie.Result stemResult = stemTrie.current(); 664 665 if (stemResult != BytesTrie.Result.INTERMEDIATE_VALUE 666 && stemResult != BytesTrie.Result.FINAL_VALUE) { 667 throw new SkeletonSyntaxException("Unknown stem", segment); 668 } 669 670 StemEnum stem = STEM_ENUM_VALUES[stemTrie.getValue()]; 671 switch (stem) { 672 673 // Stems with meaning on their own, not requiring an option: 674 675 case STEM_COMPACT_SHORT: 676 case STEM_COMPACT_LONG: 677 case STEM_SCIENTIFIC: 678 case STEM_ENGINEERING: 679 case STEM_NOTATION_SIMPLE: 680 checkNull(macros.notation, segment); 681 macros.notation = StemToObject.notation(stem); 682 switch (stem) { 683 case STEM_SCIENTIFIC: 684 case STEM_ENGINEERING: 685 return ParseState.STATE_SCIENTIFIC; // allows for scientific options 686 default: 687 return ParseState.STATE_NULL; 688 } 689 690 case STEM_BASE_UNIT: 691 case STEM_PERCENT: 692 case STEM_PERMILLE: 693 checkNull(macros.unit, segment); 694 macros.unit = StemToObject.unit(stem); 695 return ParseState.STATE_NULL; 696 697 case STEM_PERCENT_100: 698 checkNull(macros.scale, segment); 699 checkNull(macros.unit, segment); 700 macros.scale = Scale.powerOfTen(2); 701 macros.unit = NoUnit.PERCENT; 702 return ParseState.STATE_NULL; 703 704 case STEM_PRECISION_INTEGER: 705 case STEM_PRECISION_UNLIMITED: 706 case STEM_PRECISION_CURRENCY_STANDARD: 707 case STEM_PRECISION_CURRENCY_CASH: 708 checkNull(macros.precision, segment); 709 macros.precision = StemToObject.precision(stem); 710 switch (stem) { 711 case STEM_PRECISION_INTEGER: 712 return ParseState.STATE_FRACTION_PRECISION; // allows for "precision-integer/@##" 713 default: 714 return ParseState.STATE_NULL; 715 } 716 717 case STEM_ROUNDING_MODE_CEILING: 718 case STEM_ROUNDING_MODE_FLOOR: 719 case STEM_ROUNDING_MODE_DOWN: 720 case STEM_ROUNDING_MODE_UP: 721 case STEM_ROUNDING_MODE_HALF_EVEN: 722 case STEM_ROUNDING_MODE_HALF_DOWN: 723 case STEM_ROUNDING_MODE_HALF_UP: 724 case STEM_ROUNDING_MODE_UNNECESSARY: 725 checkNull(macros.roundingMode, segment); 726 macros.roundingMode = StemToObject.roundingMode(stem); 727 return ParseState.STATE_NULL; 728 729 case STEM_GROUP_OFF: 730 case STEM_GROUP_MIN2: 731 case STEM_GROUP_AUTO: 732 case STEM_GROUP_ON_ALIGNED: 733 case STEM_GROUP_THOUSANDS: 734 checkNull(macros.grouping, segment); 735 macros.grouping = StemToObject.groupingStrategy(stem); 736 return ParseState.STATE_NULL; 737 738 case STEM_LATIN: 739 checkNull(macros.symbols, segment); 740 macros.symbols = NumberingSystem.LATIN; 741 return ParseState.STATE_NULL; 742 743 case STEM_UNIT_WIDTH_NARROW: 744 case STEM_UNIT_WIDTH_SHORT: 745 case STEM_UNIT_WIDTH_FULL_NAME: 746 case STEM_UNIT_WIDTH_ISO_CODE: 747 case STEM_UNIT_WIDTH_FORMAL: 748 case STEM_UNIT_WIDTH_VARIANT: 749 case STEM_UNIT_WIDTH_HIDDEN: 750 checkNull(macros.unitWidth, segment); 751 macros.unitWidth = StemToObject.unitWidth(stem); 752 return ParseState.STATE_NULL; 753 754 case STEM_SIGN_AUTO: 755 case STEM_SIGN_ALWAYS: 756 case STEM_SIGN_NEVER: 757 case STEM_SIGN_ACCOUNTING: 758 case STEM_SIGN_ACCOUNTING_ALWAYS: 759 case STEM_SIGN_EXCEPT_ZERO: 760 case STEM_SIGN_ACCOUNTING_EXCEPT_ZERO: 761 checkNull(macros.sign, segment); 762 macros.sign = StemToObject.signDisplay(stem); 763 return ParseState.STATE_NULL; 764 765 case STEM_DECIMAL_AUTO: 766 case STEM_DECIMAL_ALWAYS: 767 checkNull(macros.decimal, segment); 768 macros.decimal = StemToObject.decimalSeparatorDisplay(stem); 769 return ParseState.STATE_NULL; 770 771 // Stems requiring an option: 772 773 case STEM_PRECISION_INCREMENT: 774 checkNull(macros.precision, segment); 775 return ParseState.STATE_INCREMENT_PRECISION; 776 777 case STEM_MEASURE_UNIT: 778 checkNull(macros.unit, segment); 779 return ParseState.STATE_MEASURE_UNIT; 780 781 case STEM_PER_MEASURE_UNIT: 782 checkNull(macros.perUnit, segment); 783 return ParseState.STATE_PER_MEASURE_UNIT; 784 785 case STEM_UNIT: 786 checkNull(macros.unit, segment); 787 checkNull(macros.perUnit, segment); 788 return ParseState.STATE_IDENTIFIER_UNIT; 789 790 case STEM_CURRENCY: 791 checkNull(macros.unit, segment); 792 return ParseState.STATE_CURRENCY_UNIT; 793 794 case STEM_INTEGER_WIDTH: 795 checkNull(macros.integerWidth, segment); 796 return ParseState.STATE_INTEGER_WIDTH; 797 798 case STEM_NUMBERING_SYSTEM: 799 checkNull(macros.symbols, segment); 800 return ParseState.STATE_NUMBERING_SYSTEM; 801 802 case STEM_SCALE: 803 checkNull(macros.scale, segment); 804 return ParseState.STATE_SCALE; 805 806 default: 807 throw new AssertionError(); 808 } 809 } 810 811 /** 812 * Given that the current segment represents an option, parse it and save the result. 813 * 814 * @return The next state after parsing this option, corresponding to what subset of options to 815 * expect next. 816 */ parseOption(ParseState stem, StringSegment segment, MacroProps macros)817 private static ParseState parseOption(ParseState stem, StringSegment segment, MacroProps macros) { 818 819 ///// Required options: ///// 820 821 switch (stem) { 822 case STATE_CURRENCY_UNIT: 823 BlueprintHelpers.parseCurrencyOption(segment, macros); 824 return ParseState.STATE_NULL; 825 case STATE_MEASURE_UNIT: 826 BlueprintHelpers.parseMeasureUnitOption(segment, macros); 827 return ParseState.STATE_NULL; 828 case STATE_PER_MEASURE_UNIT: 829 BlueprintHelpers.parseMeasurePerUnitOption(segment, macros); 830 return ParseState.STATE_NULL; 831 case STATE_IDENTIFIER_UNIT: 832 BlueprintHelpers.parseIdentifierUnitOption(segment, macros); 833 return ParseState.STATE_NULL; 834 case STATE_INCREMENT_PRECISION: 835 BlueprintHelpers.parseIncrementOption(segment, macros); 836 return ParseState.STATE_NULL; 837 case STATE_INTEGER_WIDTH: 838 BlueprintHelpers.parseIntegerWidthOption(segment, macros); 839 return ParseState.STATE_NULL; 840 case STATE_NUMBERING_SYSTEM: 841 BlueprintHelpers.parseNumberingSystemOption(segment, macros); 842 return ParseState.STATE_NULL; 843 case STATE_SCALE: 844 BlueprintHelpers.parseScaleOption(segment, macros); 845 return ParseState.STATE_NULL; 846 default: 847 break; 848 } 849 850 ///// Non-required options: ///// 851 852 // Scientific options 853 switch (stem) { 854 case STATE_SCIENTIFIC: 855 if (BlueprintHelpers.parseExponentWidthOption(segment, macros)) { 856 return ParseState.STATE_SCIENTIFIC; 857 } 858 if (BlueprintHelpers.parseExponentSignOption(segment, macros)) { 859 return ParseState.STATE_SCIENTIFIC; 860 } 861 break; 862 default: 863 break; 864 } 865 866 // Frac-sig option 867 switch (stem) { 868 case STATE_FRACTION_PRECISION: 869 if (BlueprintHelpers.parseFracSigOption(segment, macros)) { 870 return ParseState.STATE_NULL; 871 } 872 break; 873 default: 874 break; 875 } 876 877 // Unknown option 878 throw new SkeletonSyntaxException("Invalid option", segment); 879 } 880 881 ///// MAIN SKELETON GENERATION FUNCTION ///// 882 883 /** 884 * Main skeleton generator function. Appends the normalized skeleton for the MacroProps to the given 885 * StringBuilder. 886 */ generateSkeleton(MacroProps macros, StringBuilder sb)887 private static void generateSkeleton(MacroProps macros, StringBuilder sb) { 888 // Supported options 889 if (macros.notation != null && GeneratorHelpers.notation(macros, sb)) { 890 sb.append(' '); 891 } 892 if (macros.unit != null && GeneratorHelpers.unit(macros, sb)) { 893 sb.append(' '); 894 } 895 if (macros.perUnit != null && GeneratorHelpers.perUnit(macros, sb)) { 896 sb.append(' '); 897 } 898 if (macros.precision != null && GeneratorHelpers.precision(macros, sb)) { 899 sb.append(' '); 900 } 901 if (macros.roundingMode != null && GeneratorHelpers.roundingMode(macros, sb)) { 902 sb.append(' '); 903 } 904 if (macros.grouping != null && GeneratorHelpers.grouping(macros, sb)) { 905 sb.append(' '); 906 } 907 if (macros.integerWidth != null && GeneratorHelpers.integerWidth(macros, sb)) { 908 sb.append(' '); 909 } 910 if (macros.symbols != null && GeneratorHelpers.symbols(macros, sb)) { 911 sb.append(' '); 912 } 913 if (macros.unitWidth != null && GeneratorHelpers.unitWidth(macros, sb)) { 914 sb.append(' '); 915 } 916 if (macros.sign != null && GeneratorHelpers.sign(macros, sb)) { 917 sb.append(' '); 918 } 919 if (macros.decimal != null && GeneratorHelpers.decimal(macros, sb)) { 920 sb.append(' '); 921 } 922 if (macros.scale != null && GeneratorHelpers.scale(macros, sb)) { 923 sb.append(' '); 924 } 925 926 // Unsupported options 927 if (macros.padder != null) { 928 throw new UnsupportedOperationException( 929 "Cannot generate number skeleton with custom padder"); 930 } 931 if (macros.affixProvider != null) { 932 throw new UnsupportedOperationException( 933 "Cannot generate number skeleton with custom affix provider"); 934 } 935 if (macros.rules != null) { 936 throw new UnsupportedOperationException( 937 "Cannot generate number skeleton with custom plural rules"); 938 } 939 940 // Remove the trailing space 941 if (sb.length() > 0) { 942 sb.setLength(sb.length() - 1); 943 } 944 } 945 946 ///// BLUEPRINT HELPER FUNCTIONS ///// 947 948 /** 949 * Utility class for methods for processing stems and options that cannot be interpreted literally. 950 */ 951 static final class BlueprintHelpers { 952 953 /** @return Whether we successfully found and parsed an exponent width option. */ parseExponentWidthOption(StringSegment segment, MacroProps macros)954 private static boolean parseExponentWidthOption(StringSegment segment, MacroProps macros) { 955 if (!isWildcardChar(segment.charAt(0))) { 956 return false; 957 } 958 int offset = 1; 959 int minExp = 0; 960 for (; offset < segment.length(); offset++) { 961 if (segment.charAt(offset) == 'e') { 962 minExp++; 963 } else { 964 break; 965 } 966 } 967 if (offset < segment.length()) { 968 return false; 969 } 970 // Use the public APIs to enforce bounds checking 971 macros.notation = ((ScientificNotation) macros.notation).withMinExponentDigits(minExp); 972 return true; 973 } 974 generateExponentWidthOption(int minExponentDigits, StringBuilder sb)975 private static void generateExponentWidthOption(int minExponentDigits, StringBuilder sb) { 976 sb.append(WILDCARD_CHAR); 977 appendMultiple(sb, 'e', minExponentDigits); 978 } 979 980 /** @return Whether we successfully found and parsed an exponent sign option. */ parseExponentSignOption(StringSegment segment, MacroProps macros)981 private static boolean parseExponentSignOption(StringSegment segment, MacroProps macros) { 982 // Get the sign display type out of the CharsTrie data structure. 983 // TODO: Make this more efficient (avoid object allocation)? It shouldn't be very hot code. 984 CharsTrie tempStemTrie = new CharsTrie(SERIALIZED_STEM_TRIE, 0); 985 BytesTrie.Result result = tempStemTrie.next(segment, 0, segment.length()); 986 if (result != BytesTrie.Result.INTERMEDIATE_VALUE 987 && result != BytesTrie.Result.FINAL_VALUE) { 988 return false; 989 } 990 SignDisplay sign = StemToObject.signDisplay(STEM_ENUM_VALUES[tempStemTrie.getValue()]); 991 if (sign == null) { 992 return false; 993 } 994 macros.notation = ((ScientificNotation) macros.notation).withExponentSignDisplay(sign); 995 return true; 996 } 997 parseCurrencyOption(StringSegment segment, MacroProps macros)998 private static void parseCurrencyOption(StringSegment segment, MacroProps macros) { 999 String currencyCode = segment.subSequence(0, segment.length()).toString(); 1000 Currency currency; 1001 try { 1002 currency = Currency.getInstance(currencyCode); 1003 } catch (IllegalArgumentException e) { 1004 // Not 3 ascii chars 1005 throw new SkeletonSyntaxException("Invalid currency", segment, e); 1006 } 1007 macros.unit = currency; 1008 } 1009 generateCurrencyOption(Currency currency, StringBuilder sb)1010 private static void generateCurrencyOption(Currency currency, StringBuilder sb) { 1011 sb.append(currency.getCurrencyCode()); 1012 } 1013 parseMeasureUnitOption(StringSegment segment, MacroProps macros)1014 private static void parseMeasureUnitOption(StringSegment segment, MacroProps macros) { 1015 // NOTE: The category (type) of the unit is guaranteed to be a valid subtag (alphanumeric) 1016 // http://unicode.org/reports/tr35/#Validity_Data 1017 int firstHyphen = 0; 1018 while (firstHyphen < segment.length() && segment.charAt(firstHyphen) != '-') { 1019 firstHyphen++; 1020 } 1021 if (firstHyphen == segment.length()) { 1022 throw new SkeletonSyntaxException("Invalid measure unit option", segment); 1023 } 1024 String type = segment.subSequence(0, firstHyphen).toString(); 1025 String subType = segment.subSequence(firstHyphen + 1, segment.length()).toString(); 1026 Set<MeasureUnit> units = MeasureUnit.getAvailable(type); 1027 for (MeasureUnit unit : units) { 1028 if (subType.equals(unit.getSubtype())) { 1029 macros.unit = unit; 1030 return; 1031 } 1032 } 1033 throw new SkeletonSyntaxException("Unknown measure unit", segment); 1034 } 1035 generateMeasureUnitOption(MeasureUnit unit, StringBuilder sb)1036 private static void generateMeasureUnitOption(MeasureUnit unit, StringBuilder sb) { 1037 sb.append(unit.getType()); 1038 sb.append("-"); 1039 sb.append(unit.getSubtype()); 1040 } 1041 parseMeasurePerUnitOption(StringSegment segment, MacroProps macros)1042 private static void parseMeasurePerUnitOption(StringSegment segment, MacroProps macros) { 1043 // A little bit of a hack: save the current unit (numerator), call the main measure unit 1044 // parsing code, put back the numerator unit, and put the new unit into per-unit. 1045 MeasureUnit numerator = macros.unit; 1046 parseMeasureUnitOption(segment, macros); 1047 macros.perUnit = macros.unit; 1048 macros.unit = numerator; 1049 } 1050 parseIdentifierUnitOption(StringSegment segment, MacroProps macros)1051 private static void parseIdentifierUnitOption(StringSegment segment, MacroProps macros) { 1052 MeasureUnit[] units = MeasureUnit.parseCoreUnitIdentifier(segment.asString()); 1053 if (units == null) { 1054 throw new SkeletonSyntaxException("Invalid core unit identifier", segment); 1055 } 1056 macros.unit = units[0]; 1057 if (units.length == 2) { 1058 macros.perUnit = units[1]; 1059 } 1060 } 1061 parseFractionStem(StringSegment segment, MacroProps macros)1062 private static void parseFractionStem(StringSegment segment, MacroProps macros) { 1063 assert segment.charAt(0) == '.'; 1064 int offset = 1; 1065 int minFrac = 0; 1066 int maxFrac; 1067 for (; offset < segment.length(); offset++) { 1068 if (segment.charAt(offset) == '0') { 1069 minFrac++; 1070 } else { 1071 break; 1072 } 1073 } 1074 if (offset < segment.length()) { 1075 if (isWildcardChar(segment.charAt(offset))) { 1076 maxFrac = -1; 1077 offset++; 1078 } else { 1079 maxFrac = minFrac; 1080 for (; offset < segment.length(); offset++) { 1081 if (segment.charAt(offset) == '#') { 1082 maxFrac++; 1083 } else { 1084 break; 1085 } 1086 } 1087 } 1088 } else { 1089 maxFrac = minFrac; 1090 } 1091 if (offset < segment.length()) { 1092 throw new SkeletonSyntaxException("Invalid fraction stem", segment); 1093 } 1094 // Use the public APIs to enforce bounds checking 1095 if (maxFrac == -1) { 1096 if (minFrac == 0) { 1097 macros.precision = Precision.unlimited(); 1098 } else { 1099 macros.precision = Precision.minFraction(minFrac); 1100 } 1101 } else { 1102 macros.precision = Precision.minMaxFraction(minFrac, maxFrac); 1103 } 1104 } 1105 generateFractionStem(int minFrac, int maxFrac, StringBuilder sb)1106 private static void generateFractionStem(int minFrac, int maxFrac, StringBuilder sb) { 1107 if (minFrac == 0 && maxFrac == 0) { 1108 sb.append("precision-integer"); 1109 return; 1110 } 1111 sb.append('.'); 1112 appendMultiple(sb, '0', minFrac); 1113 if (maxFrac == -1) { 1114 sb.append(WILDCARD_CHAR); 1115 } else { 1116 appendMultiple(sb, '#', maxFrac - minFrac); 1117 } 1118 } 1119 parseDigitsStem(StringSegment segment, MacroProps macros)1120 private static void parseDigitsStem(StringSegment segment, MacroProps macros) { 1121 assert segment.charAt(0) == '@'; 1122 int offset = 0; 1123 int minSig = 0; 1124 int maxSig; 1125 for (; offset < segment.length(); offset++) { 1126 if (segment.charAt(offset) == '@') { 1127 minSig++; 1128 } else { 1129 break; 1130 } 1131 } 1132 if (offset < segment.length()) { 1133 if (isWildcardChar(segment.charAt(offset))) { 1134 maxSig = -1; 1135 offset++; 1136 } else { 1137 maxSig = minSig; 1138 for (; offset < segment.length(); offset++) { 1139 if (segment.charAt(offset) == '#') { 1140 maxSig++; 1141 } else { 1142 break; 1143 } 1144 } 1145 } 1146 } else { 1147 maxSig = minSig; 1148 } 1149 if (offset < segment.length()) { 1150 throw new SkeletonSyntaxException("Invalid significant digits stem", segment); 1151 } 1152 // Use the public APIs to enforce bounds checking 1153 if (maxSig == -1) { 1154 macros.precision = Precision.minSignificantDigits(minSig); 1155 } else { 1156 macros.precision = Precision.minMaxSignificantDigits(minSig, maxSig); 1157 } 1158 } 1159 generateDigitsStem(int minSig, int maxSig, StringBuilder sb)1160 private static void generateDigitsStem(int minSig, int maxSig, StringBuilder sb) { 1161 appendMultiple(sb, '@', minSig); 1162 if (maxSig == -1) { 1163 sb.append(WILDCARD_CHAR); 1164 } else { 1165 appendMultiple(sb, '#', maxSig - minSig); 1166 } 1167 } 1168 parseScientificStem(StringSegment segment, MacroProps macros)1169 private static void parseScientificStem(StringSegment segment, MacroProps macros) { 1170 assert(segment.charAt(0) == 'E'); 1171 block: 1172 { 1173 int offset = 1; 1174 if (segment.length() == offset) { 1175 break block; 1176 } 1177 boolean isEngineering = false; 1178 if (segment.charAt(offset) == 'E') { 1179 isEngineering = true; 1180 offset++; 1181 if (segment.length() == offset) { 1182 break block; 1183 } 1184 } 1185 SignDisplay signDisplay = SignDisplay.AUTO; 1186 if (segment.charAt(offset) == '+') { 1187 offset++; 1188 if (segment.length() == offset) { 1189 break block; 1190 } 1191 if (segment.charAt(offset) == '!') { 1192 signDisplay = SignDisplay.ALWAYS; 1193 } else if (segment.charAt(offset) == '?') { 1194 signDisplay = SignDisplay.EXCEPT_ZERO; 1195 } else { 1196 break block; 1197 } 1198 offset++; 1199 if (segment.length() == offset) { 1200 break block; 1201 } 1202 } 1203 int minDigits = 0; 1204 for (; offset < segment.length(); offset++) { 1205 if (segment.charAt(offset) != '0') { 1206 break block; 1207 } 1208 minDigits++; 1209 } 1210 macros.notation = (isEngineering ? Notation.engineering() : Notation.scientific()) 1211 .withExponentSignDisplay(signDisplay) 1212 .withMinExponentDigits(minDigits); 1213 return; 1214 } 1215 throw new SkeletonSyntaxException("Invalid scientific stem", segment); 1216 } 1217 parseIntegerStem(StringSegment segment, MacroProps macros)1218 private static void parseIntegerStem(StringSegment segment, MacroProps macros) { 1219 assert(segment.charAt(0) == '0'); 1220 int offset = 1; 1221 for (; offset < segment.length(); offset++) { 1222 if (segment.charAt(offset) != '0') { 1223 offset--; 1224 break; 1225 } 1226 } 1227 if (offset < segment.length()) { 1228 throw new SkeletonSyntaxException("Invalid integer stem", segment); 1229 } 1230 macros.integerWidth = IntegerWidth.zeroFillTo(offset); 1231 return; 1232 } 1233 1234 /** @return Whether we successfully found and parsed a frac-sig option. */ parseFracSigOption(StringSegment segment, MacroProps macros)1235 private static boolean parseFracSigOption(StringSegment segment, MacroProps macros) { 1236 if (segment.charAt(0) != '@') { 1237 return false; 1238 } 1239 int offset = 0; 1240 int minSig = 0; 1241 int maxSig; 1242 for (; offset < segment.length(); offset++) { 1243 if (segment.charAt(offset) == '@') { 1244 minSig++; 1245 } else { 1246 break; 1247 } 1248 } 1249 // For the frac-sig option, there must be minSig or maxSig but not both. 1250 // Valid: @+, @@+, @@@+ 1251 // Valid: @#, @##, @### 1252 // Invalid: @, @@, @@@ 1253 // Invalid: @@#, @@##, @@@# 1254 if (offset < segment.length()) { 1255 if (isWildcardChar(segment.charAt(offset))) { 1256 maxSig = -1; 1257 offset++; 1258 } else if (minSig > 1) { 1259 // @@#, @@##, @@@# 1260 throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", 1261 segment); 1262 } else { 1263 maxSig = minSig; 1264 for (; offset < segment.length(); offset++) { 1265 if (segment.charAt(offset) == '#') { 1266 maxSig++; 1267 } else { 1268 break; 1269 } 1270 } 1271 } 1272 } else { 1273 // @, @@, @@@ 1274 throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment); 1275 } 1276 if (offset < segment.length()) { 1277 throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment); 1278 } 1279 1280 FractionPrecision oldRounder = (FractionPrecision) macros.precision; 1281 if (maxSig == -1) { 1282 macros.precision = oldRounder.withMinDigits(minSig); 1283 } else { 1284 macros.precision = oldRounder.withMaxDigits(maxSig); 1285 } 1286 return true; 1287 } 1288 parseIncrementOption(StringSegment segment, MacroProps macros)1289 private static void parseIncrementOption(StringSegment segment, MacroProps macros) { 1290 // Call segment.subSequence() because segment.toString() doesn't create a clean string. 1291 String str = segment.subSequence(0, segment.length()).toString(); 1292 BigDecimal increment; 1293 try { 1294 increment = new BigDecimal(str); 1295 } catch (NumberFormatException e) { 1296 throw new SkeletonSyntaxException("Invalid rounding increment", segment, e); 1297 } 1298 macros.precision = Precision.increment(increment); 1299 } 1300 generateIncrementOption(BigDecimal increment, StringBuilder sb)1301 private static void generateIncrementOption(BigDecimal increment, StringBuilder sb) { 1302 sb.append(increment.toPlainString()); 1303 } 1304 parseIntegerWidthOption(StringSegment segment, MacroProps macros)1305 private static void parseIntegerWidthOption(StringSegment segment, MacroProps macros) { 1306 int offset = 0; 1307 int minInt = 0; 1308 int maxInt; 1309 if (isWildcardChar(segment.charAt(0))) { 1310 maxInt = -1; 1311 offset++; 1312 } else { 1313 maxInt = 0; 1314 } 1315 for (; offset < segment.length(); offset++) { 1316 if (maxInt != -1 && segment.charAt(offset) == '#') { 1317 maxInt++; 1318 } else { 1319 break; 1320 } 1321 } 1322 if (offset < segment.length()) { 1323 for (; offset < segment.length(); offset++) { 1324 if (segment.charAt(offset) == '0') { 1325 minInt++; 1326 } else { 1327 break; 1328 } 1329 } 1330 } 1331 if (maxInt != -1) { 1332 maxInt += minInt; 1333 } 1334 if (offset < segment.length()) { 1335 throw new SkeletonSyntaxException("Invalid integer width stem", segment); 1336 } 1337 // Use the public APIs to enforce bounds checking 1338 if (maxInt == -1) { 1339 macros.integerWidth = IntegerWidth.zeroFillTo(minInt); 1340 } else { 1341 macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt); 1342 } 1343 } 1344 generateIntegerWidthOption(int minInt, int maxInt, StringBuilder sb)1345 private static void generateIntegerWidthOption(int minInt, int maxInt, StringBuilder sb) { 1346 if (maxInt == -1) { 1347 sb.append(WILDCARD_CHAR); 1348 } else { 1349 appendMultiple(sb, '#', maxInt - minInt); 1350 } 1351 appendMultiple(sb, '0', minInt); 1352 } 1353 parseNumberingSystemOption(StringSegment segment, MacroProps macros)1354 private static void parseNumberingSystemOption(StringSegment segment, MacroProps macros) { 1355 String nsName = segment.subSequence(0, segment.length()).toString(); 1356 NumberingSystem ns = NumberingSystem.getInstanceByName(nsName); 1357 if (ns == null) { 1358 throw new SkeletonSyntaxException("Unknown numbering system", segment); 1359 } 1360 macros.symbols = ns; 1361 } 1362 generateNumberingSystemOption(NumberingSystem ns, StringBuilder sb)1363 private static void generateNumberingSystemOption(NumberingSystem ns, StringBuilder sb) { 1364 sb.append(ns.getName()); 1365 } 1366 parseScaleOption(StringSegment segment, MacroProps macros)1367 private static void parseScaleOption(StringSegment segment, MacroProps macros) { 1368 // Call segment.subSequence() because segment.toString() doesn't create a clean string. 1369 String str = segment.subSequence(0, segment.length()).toString(); 1370 BigDecimal bd; 1371 try { 1372 bd = new BigDecimal(str); 1373 } catch (NumberFormatException e) { 1374 throw new SkeletonSyntaxException("Invalid scale", segment, e); 1375 } 1376 // NOTE: If bd is a power of ten, the Scale API optimizes it for us. 1377 macros.scale = Scale.byBigDecimal(bd); 1378 } 1379 generateScaleOption(Scale scale, StringBuilder sb)1380 private static void generateScaleOption(Scale scale, StringBuilder sb) { 1381 BigDecimal bd = scale.arbitrary; 1382 if (bd == null) { 1383 bd = BigDecimal.ONE; 1384 } 1385 bd = bd.scaleByPowerOfTen(scale.magnitude); 1386 sb.append(bd.toPlainString()); 1387 } 1388 } 1389 1390 ///// STEM GENERATION HELPER FUNCTIONS ///// 1391 1392 /** 1393 * Utility class for methods for generating a token corresponding to each macro-prop. Each method 1394 * returns whether or not a token was written to the string builder. 1395 */ 1396 static final class GeneratorHelpers { 1397 notation(MacroProps macros, StringBuilder sb)1398 private static boolean notation(MacroProps macros, StringBuilder sb) { 1399 if (macros.notation instanceof CompactNotation) { 1400 if (macros.notation == Notation.compactLong()) { 1401 sb.append("compact-long"); 1402 return true; 1403 } else if (macros.notation == Notation.compactShort()) { 1404 sb.append("compact-short"); 1405 return true; 1406 } else { 1407 // Compact notation generated from custom data (not supported in skeleton) 1408 // The other compact notations are literals 1409 throw new UnsupportedOperationException( 1410 "Cannot generate number skeleton with custom compact data"); 1411 } 1412 } else if (macros.notation instanceof ScientificNotation) { 1413 ScientificNotation impl = (ScientificNotation) macros.notation; 1414 if (impl.engineeringInterval == 3) { 1415 sb.append("engineering"); 1416 } else { 1417 sb.append("scientific"); 1418 } 1419 if (impl.minExponentDigits > 1) { 1420 sb.append('/'); 1421 BlueprintHelpers.generateExponentWidthOption(impl.minExponentDigits, sb); 1422 } 1423 if (impl.exponentSignDisplay != SignDisplay.AUTO) { 1424 sb.append('/'); 1425 EnumToStemString.signDisplay(impl.exponentSignDisplay, sb); 1426 } 1427 return true; 1428 } else { 1429 assert macros.notation instanceof SimpleNotation; 1430 // Default value is not shown in normalized form 1431 return false; 1432 } 1433 } 1434 unit(MacroProps macros, StringBuilder sb)1435 private static boolean unit(MacroProps macros, StringBuilder sb) { 1436 if (macros.unit instanceof Currency) { 1437 sb.append("currency/"); 1438 BlueprintHelpers.generateCurrencyOption((Currency) macros.unit, sb); 1439 return true; 1440 } else if (macros.unit instanceof NoUnit) { 1441 if (macros.unit == NoUnit.PERCENT) { 1442 sb.append("percent"); 1443 return true; 1444 } else if (macros.unit == NoUnit.PERMILLE) { 1445 sb.append("permille"); 1446 return true; 1447 } else { 1448 assert macros.unit == NoUnit.BASE; 1449 // Default value is not shown in normalized form 1450 return false; 1451 } 1452 } else { 1453 sb.append("measure-unit/"); 1454 BlueprintHelpers.generateMeasureUnitOption(macros.unit, sb); 1455 return true; 1456 } 1457 } 1458 perUnit(MacroProps macros, StringBuilder sb)1459 private static boolean perUnit(MacroProps macros, StringBuilder sb) { 1460 // Per-units are currently expected to be only MeasureUnits. 1461 if (macros.perUnit instanceof Currency || macros.perUnit instanceof NoUnit) { 1462 throw new UnsupportedOperationException( 1463 "Cannot generate number skeleton with per-unit that is not a standard measure unit"); 1464 } else { 1465 sb.append("per-measure-unit/"); 1466 BlueprintHelpers.generateMeasureUnitOption(macros.perUnit, sb); 1467 return true; 1468 } 1469 } 1470 precision(MacroProps macros, StringBuilder sb)1471 private static boolean precision(MacroProps macros, StringBuilder sb) { 1472 if (macros.precision instanceof Precision.InfiniteRounderImpl) { 1473 sb.append("precision-unlimited"); 1474 } else if (macros.precision instanceof Precision.FractionRounderImpl) { 1475 Precision.FractionRounderImpl impl = (Precision.FractionRounderImpl) macros.precision; 1476 BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb); 1477 } else if (macros.precision instanceof Precision.SignificantRounderImpl) { 1478 Precision.SignificantRounderImpl impl = (Precision.SignificantRounderImpl) macros.precision; 1479 BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb); 1480 } else if (macros.precision instanceof Precision.FracSigRounderImpl) { 1481 Precision.FracSigRounderImpl impl = (Precision.FracSigRounderImpl) macros.precision; 1482 BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb); 1483 sb.append('/'); 1484 if (impl.minSig == -1) { 1485 BlueprintHelpers.generateDigitsStem(1, impl.maxSig, sb); 1486 } else { 1487 BlueprintHelpers.generateDigitsStem(impl.minSig, -1, sb); 1488 } 1489 } else if (macros.precision instanceof Precision.IncrementRounderImpl) { 1490 Precision.IncrementRounderImpl impl = (Precision.IncrementRounderImpl) macros.precision; 1491 sb.append("precision-increment/"); 1492 BlueprintHelpers.generateIncrementOption(impl.increment, sb); 1493 } else { 1494 assert macros.precision instanceof Precision.CurrencyRounderImpl; 1495 Precision.CurrencyRounderImpl impl = (Precision.CurrencyRounderImpl) macros.precision; 1496 if (impl.usage == CurrencyUsage.STANDARD) { 1497 sb.append("precision-currency-standard"); 1498 } else { 1499 sb.append("precision-currency-cash"); 1500 } 1501 } 1502 1503 // NOTE: Always return true for rounding because the default value depends on other options. 1504 return true; 1505 } 1506 roundingMode(MacroProps macros, StringBuilder sb)1507 private static boolean roundingMode(MacroProps macros, StringBuilder sb) { 1508 if (macros.roundingMode == RoundingUtils.DEFAULT_ROUNDING_MODE) { 1509 return false; // Default value 1510 } 1511 EnumToStemString.roundingMode(macros.roundingMode, sb); 1512 return true; 1513 } 1514 grouping(MacroProps macros, StringBuilder sb)1515 private static boolean grouping(MacroProps macros, StringBuilder sb) { 1516 if (macros.grouping instanceof GroupingStrategy) { 1517 if (macros.grouping == GroupingStrategy.AUTO) { 1518 return false; // Default value 1519 } 1520 EnumToStemString.groupingStrategy((GroupingStrategy) macros.grouping, sb); 1521 return true; 1522 } else { 1523 throw new UnsupportedOperationException( 1524 "Cannot generate number skeleton with custom Grouper"); 1525 } 1526 } 1527 integerWidth(MacroProps macros, StringBuilder sb)1528 private static boolean integerWidth(MacroProps macros, StringBuilder sb) { 1529 if (macros.integerWidth.equals(IntegerWidth.DEFAULT)) { 1530 return false; // Default 1531 } 1532 sb.append("integer-width/"); 1533 BlueprintHelpers.generateIntegerWidthOption(macros.integerWidth.minInt, 1534 macros.integerWidth.maxInt, 1535 sb); 1536 return true; 1537 } 1538 symbols(MacroProps macros, StringBuilder sb)1539 private static boolean symbols(MacroProps macros, StringBuilder sb) { 1540 if (macros.symbols instanceof NumberingSystem) { 1541 NumberingSystem ns = (NumberingSystem) macros.symbols; 1542 if (ns.getName().equals("latn")) { 1543 sb.append("latin"); 1544 } else { 1545 sb.append("numbering-system/"); 1546 BlueprintHelpers.generateNumberingSystemOption(ns, sb); 1547 } 1548 return true; 1549 } else { 1550 assert macros.symbols instanceof DecimalFormatSymbols; 1551 throw new UnsupportedOperationException( 1552 "Cannot generate number skeleton with custom DecimalFormatSymbols"); 1553 } 1554 } 1555 unitWidth(MacroProps macros, StringBuilder sb)1556 private static boolean unitWidth(MacroProps macros, StringBuilder sb) { 1557 if (macros.unitWidth == UnitWidth.SHORT) { 1558 return false; // Default value 1559 } 1560 EnumToStemString.unitWidth(macros.unitWidth, sb); 1561 return true; 1562 } 1563 sign(MacroProps macros, StringBuilder sb)1564 private static boolean sign(MacroProps macros, StringBuilder sb) { 1565 if (macros.sign == SignDisplay.AUTO) { 1566 return false; // Default value 1567 } 1568 EnumToStemString.signDisplay(macros.sign, sb); 1569 return true; 1570 } 1571 decimal(MacroProps macros, StringBuilder sb)1572 private static boolean decimal(MacroProps macros, StringBuilder sb) { 1573 if (macros.decimal == DecimalSeparatorDisplay.AUTO) { 1574 return false; // Default value 1575 } 1576 EnumToStemString.decimalSeparatorDisplay(macros.decimal, sb); 1577 return true; 1578 } 1579 scale(MacroProps macros, StringBuilder sb)1580 private static boolean scale(MacroProps macros, StringBuilder sb) { 1581 if (!macros.scale.isValid()) { 1582 return false; // Default value 1583 } 1584 sb.append("scale/"); 1585 BlueprintHelpers.generateScaleOption(macros.scale, sb); 1586 return true; 1587 } 1588 1589 } 1590 1591 ///// OTHER UTILITY FUNCTIONS ///// 1592 checkNull(Object value, CharSequence content)1593 private static void checkNull(Object value, CharSequence content) { 1594 if (value != null) { 1595 throw new SkeletonSyntaxException("Duplicated setting", content); 1596 } 1597 } 1598 appendMultiple(StringBuilder sb, int cp, int count)1599 private static void appendMultiple(StringBuilder sb, int cp, int count) { 1600 for (int i = 0; i < count; i++) { 1601 sb.appendCodePoint(cp); 1602 } 1603 } 1604 } 1605