1 // © 2020 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 3 package com.ibm.icu.impl.units; 4 5 import java.math.BigDecimal; 6 import java.util.ArrayList; 7 import java.util.Collections; 8 import java.util.Comparator; 9 10 import com.ibm.icu.util.BytesTrie; 11 import com.ibm.icu.util.CharsTrie; 12 import com.ibm.icu.util.CharsTrieBuilder; 13 import com.ibm.icu.util.ICUCloneNotSupportedException; 14 import com.ibm.icu.util.MeasureUnit; 15 import com.ibm.icu.util.StringTrieBuilder; 16 17 public class MeasureUnitImpl { 18 19 /** 20 * The full unit identifier. Null if not computed. 21 */ 22 private String identifier = null; 23 /** 24 * The complexity, either SINGLE, COMPOUND, or MIXED. 25 */ 26 private MeasureUnit.Complexity complexity = MeasureUnit.Complexity.SINGLE; 27 /** 28 * The list of single units. These may be summed or multiplied, based on the 29 * value of the complexity field. 30 * <p> 31 * The "dimensionless" unit (SingleUnitImpl default constructor) must not be added to this list. 32 * <p> 33 * The "dimensionless" <code>MeasureUnitImpl</code> has an empty <code>singleUnits</code>. 34 */ 35 private final ArrayList<SingleUnitImpl> singleUnits; 36 MeasureUnitImpl()37 public MeasureUnitImpl() { 38 singleUnits = new ArrayList<>(); 39 } 40 MeasureUnitImpl(SingleUnitImpl singleUnit)41 public MeasureUnitImpl(SingleUnitImpl singleUnit) { 42 this(); 43 this.appendSingleUnit(singleUnit); 44 } 45 46 /** 47 * Parse a unit identifier into a MeasureUnitImpl. 48 * 49 * @param identifier The unit identifier string. 50 * @return A newly parsed object. 51 * @throws IllegalArgumentException in case of incorrect/non-parsed identifier. 52 */ forIdentifier(String identifier)53 public static MeasureUnitImpl forIdentifier(String identifier) { 54 return UnitsParser.parseForIdentifier(identifier); 55 } 56 57 /** 58 * Used for currency units. 59 */ forCurrencyCode(String currencyCode)60 public static MeasureUnitImpl forCurrencyCode(String currencyCode) { 61 MeasureUnitImpl result = new MeasureUnitImpl(); 62 result.identifier = currencyCode; 63 return result; 64 } 65 copy()66 public MeasureUnitImpl copy() { 67 MeasureUnitImpl result = new MeasureUnitImpl(); 68 result.complexity = this.complexity; 69 result.identifier = this.identifier; 70 for (SingleUnitImpl singleUnit : this.singleUnits) { 71 result.singleUnits.add(singleUnit.copy()); 72 } 73 return result; 74 } 75 76 /** 77 * Returns a simplified version of the unit. 78 * NOTE: the simplification happen when there are two units equals in their base unit and their 79 * prefixes. 80 * 81 * Example 1: "square-meter-per-meter" --> "meter" 82 * Example 2: "square-millimeter-per-meter" --> "square-millimeter-per-meter" 83 */ copyAndSimplify()84 public MeasureUnitImpl copyAndSimplify() { 85 MeasureUnitImpl result = new MeasureUnitImpl(); 86 for (SingleUnitImpl singleUnit : this.getSingleUnits()) { 87 // This `for` loop will cause time complexity to be O(n^2). 88 // However, n is very small (number of units, generally, at maximum equal to 10) 89 boolean unitExist = false; 90 for (SingleUnitImpl resultSingleUnit : result.getSingleUnits()) { 91 if(resultSingleUnit.getSimpleUnitID().compareTo(singleUnit.getSimpleUnitID()) == 0 92 && 93 resultSingleUnit.getPrefix().getIdentifier().compareTo(singleUnit.getPrefix().getIdentifier()) == 0 94 ) { 95 unitExist = true; 96 resultSingleUnit.setDimensionality(resultSingleUnit.getDimensionality() + singleUnit.getDimensionality()); 97 break; 98 } 99 } 100 101 if(!unitExist) { 102 result.appendSingleUnit(singleUnit); 103 } 104 } 105 106 return result; 107 } 108 109 /** 110 * Returns the list of simple units. 111 */ getSingleUnits()112 public ArrayList<SingleUnitImpl> getSingleUnits() { 113 return singleUnits; 114 } 115 116 /** 117 * Mutates this MeasureUnitImpl to take the reciprocal. 118 */ takeReciprocal()119 public void takeReciprocal() { 120 this.identifier = null; 121 for (SingleUnitImpl singleUnit : 122 this.singleUnits) { 123 singleUnit.setDimensionality(singleUnit.getDimensionality() * -1); 124 } 125 } 126 extractIndividualUnitsWithIndices()127 public ArrayList<MeasureUnitImplWithIndex> extractIndividualUnitsWithIndices() { 128 ArrayList<MeasureUnitImplWithIndex> result = new ArrayList<>(); 129 if (this.getComplexity() == MeasureUnit.Complexity.MIXED) { 130 // In case of mixed units, each single unit can be considered as a stand alone MeasureUnitImpl. 131 int i = 0; 132 for (SingleUnitImpl singleUnit : 133 this.getSingleUnits()) { 134 result.add(new MeasureUnitImplWithIndex(i++, new MeasureUnitImpl(singleUnit))); 135 } 136 137 return result; 138 } 139 140 result.add(new MeasureUnitImplWithIndex(0, this.copy())); 141 return result; 142 } 143 144 /** 145 * Applies dimensionality to all the internal single units. 146 * For example: <b>square-meter-per-second</b>, when we apply dimensionality -2, it will be <b>square-second-per-p4-meter</b> 147 */ applyDimensionality(int dimensionality)148 public void applyDimensionality(int dimensionality) { 149 for (SingleUnitImpl singleUnit : 150 singleUnits) { 151 singleUnit.setDimensionality(singleUnit.getDimensionality() * dimensionality); 152 } 153 } 154 155 /** 156 * Mutates this MeasureUnitImpl to append a single unit. 157 * 158 * @return true if a new item was added. If unit is the dimensionless unit, 159 * it is never added: the return value will always be false. 160 */ appendSingleUnit(SingleUnitImpl singleUnit)161 public boolean appendSingleUnit(SingleUnitImpl singleUnit) { 162 identifier = null; 163 164 if (singleUnit == null) { 165 // Do not append dimensionless units. 166 return false; 167 } 168 169 // Find a similar unit that already exists, to attempt to coalesce 170 SingleUnitImpl oldUnit = null; 171 for (SingleUnitImpl candidate : this.singleUnits) { 172 if (candidate.isCompatibleWith(singleUnit)) { 173 oldUnit = candidate; 174 break; 175 } 176 } 177 178 if (oldUnit != null) { 179 // Both dimensionalities will be positive, or both will be negative, by 180 // virtue of isCompatibleWith(). 181 oldUnit.setDimensionality(oldUnit.getDimensionality() + singleUnit.getDimensionality()); 182 183 return false; 184 } 185 186 // Add a copy of singleUnit 187 this.singleUnits.add(singleUnit.copy()); 188 189 // If the MeasureUnitImpl is `UMEASURE_UNIT_SINGLE` and after the appending a unit, the singleUnits contains 190 // more than one. thus means the complexity should be `UMEASURE_UNIT_COMPOUND` 191 if (this.singleUnits.size() > 1 && this.complexity == MeasureUnit.Complexity.SINGLE) { 192 this.setComplexity(MeasureUnit.Complexity.COMPOUND); 193 } 194 195 return true; 196 } 197 198 /** 199 * Transform this MeasureUnitImpl into a MeasureUnit, simplifying if possible. 200 * <p> 201 * NOTE: this function must be called from a thread-safe class 202 */ build()203 public MeasureUnit build() { 204 return MeasureUnit.fromMeasureUnitImpl(this); 205 } 206 207 /** 208 * @return SingleUnitImpl 209 * @throws UnsupportedOperationException if the object could not be converted to SingleUnitImpl. 210 */ getSingleUnitImpl()211 public SingleUnitImpl getSingleUnitImpl() { 212 if (this.singleUnits.size() == 0) { 213 return new SingleUnitImpl(); 214 } 215 if (this.singleUnits.size() == 1) { 216 return this.singleUnits.get(0).copy(); 217 } 218 219 throw new UnsupportedOperationException(); 220 } 221 222 /** 223 * Returns the CLDR unit identifier and null if not computed. 224 */ getIdentifier()225 public String getIdentifier() { 226 return identifier; 227 } 228 getComplexity()229 public MeasureUnit.Complexity getComplexity() { 230 return complexity; 231 } 232 setComplexity(MeasureUnit.Complexity complexity)233 public void setComplexity(MeasureUnit.Complexity complexity) { 234 this.complexity = complexity; 235 } 236 237 /** 238 * Normalizes the MeasureUnitImpl and generates the identifier string in place. 239 */ serialize()240 public void serialize() { 241 if (this.getSingleUnits().size() == 0) { 242 // Dimensionless, constructed by the default constructor: no appending 243 // to this.result, we wish it to contain the zero-length string. 244 return; 245 } 246 247 248 if (this.complexity == MeasureUnit.Complexity.COMPOUND) { 249 // Note: don't sort a MIXED unit 250 Collections.sort(this.getSingleUnits(), new SingleUnitComparator()); 251 } 252 253 StringBuilder result = new StringBuilder(); 254 boolean beforePer = true; 255 boolean firstTimeNegativeDimension = false; 256 for (SingleUnitImpl singleUnit : 257 this.getSingleUnits()) { 258 if (beforePer && singleUnit.getDimensionality() < 0) { 259 beforePer = false; 260 firstTimeNegativeDimension = true; 261 } else if (singleUnit.getDimensionality() < 0) { 262 firstTimeNegativeDimension = false; 263 } 264 265 if (this.getComplexity() == MeasureUnit.Complexity.MIXED) { 266 if (result.length() != 0) { 267 result.append("-and-"); 268 } 269 } else { 270 if (firstTimeNegativeDimension) { 271 if (result.length() == 0) { 272 result.append("per-"); 273 } else { 274 result.append("-per-"); 275 } 276 } else { 277 if (result.length() != 0) { 278 result.append("-"); 279 } 280 } 281 } 282 283 result.append(singleUnit.getNeutralIdentifier()); 284 } 285 286 this.identifier = result.toString(); 287 } 288 289 @Override toString()290 public String toString() { 291 return "MeasureUnitImpl [" + build().getIdentifier() + "]"; 292 } 293 294 public enum CompoundPart { 295 // Represents "-per-" 296 PER(0), 297 // Represents "-" 298 TIMES(1), 299 // Represents "-and-" 300 AND(2); 301 302 private final int index; 303 CompoundPart(int index)304 CompoundPart(int index) { 305 this.index = index; 306 } 307 getCompoundPartFromTrieIndex(int trieIndex)308 public static CompoundPart getCompoundPartFromTrieIndex(int trieIndex) { 309 int index = trieIndex - UnitsData.Constants.kCompoundPartOffset; 310 switch (index) { 311 case 0: 312 return CompoundPart.PER; 313 case 1: 314 return CompoundPart.TIMES; 315 case 2: 316 return CompoundPart.AND; 317 default: 318 throw new AssertionError("CompoundPart index must be 0, 1 or 2"); 319 } 320 } 321 getTrieIndex()322 public int getTrieIndex() { 323 return this.index + UnitsData.Constants.kCompoundPartOffset; 324 } 325 getValue()326 public int getValue() { 327 return index; 328 } 329 } 330 331 public enum PowerPart { 332 P2(2), 333 P3(3), 334 P4(4), 335 P5(5), 336 P6(6), 337 P7(7), 338 P8(8), 339 P9(9), 340 P10(10), 341 P11(11), 342 P12(12), 343 P13(13), 344 P14(14), 345 P15(15); 346 347 private final int power; 348 PowerPart(int power)349 PowerPart(int power) { 350 this.power = power; 351 } 352 getPowerFromTrieIndex(int trieIndex)353 public static int getPowerFromTrieIndex(int trieIndex) { 354 return trieIndex - UnitsData.Constants.kPowerPartOffset; 355 } 356 getTrieIndex()357 public int getTrieIndex() { 358 return this.power + UnitsData.Constants.kPowerPartOffset; 359 } 360 getValue()361 public int getValue() { 362 return power; 363 } 364 } 365 366 public enum InitialCompoundPart { 367 368 // Represents "per-", the only compound part that can appear at the start of 369 // an identifier. 370 INITIAL_COMPOUND_PART_PER(0); 371 372 private final int index; 373 InitialCompoundPart(int powerIndex)374 InitialCompoundPart(int powerIndex) { 375 this.index = powerIndex; 376 } 377 getInitialCompoundPartFromTrieIndex(int trieIndex)378 public static InitialCompoundPart getInitialCompoundPartFromTrieIndex(int trieIndex) { 379 int index = trieIndex - UnitsData.Constants.kInitialCompoundPartOffset; 380 if (index == 0) { 381 return INITIAL_COMPOUND_PART_PER; 382 } 383 384 throw new IllegalArgumentException("Incorrect trieIndex"); 385 } 386 getTrieIndex()387 public int getTrieIndex() { 388 return this.index + UnitsData.Constants.kInitialCompoundPartOffset; 389 } 390 getValue()391 public int getValue() { 392 return index; 393 } 394 395 } 396 397 public static class MeasureUnitImplWithIndex { 398 int index; 399 MeasureUnitImpl unitImpl; 400 MeasureUnitImplWithIndex(int index, MeasureUnitImpl unitImpl)401 MeasureUnitImplWithIndex(int index, MeasureUnitImpl unitImpl) { 402 this.index = index; 403 this.unitImpl = unitImpl; 404 } 405 } 406 407 public static class UnitsParser { 408 // This used only to not build the trie each time we use the parser 409 private volatile static CharsTrie savedTrie = null; 410 411 // This trie used in the parsing operation. 412 private final CharsTrie trie; 413 private final String fSource; 414 // Tracks parser progress: the offset into fSource. 415 private int fIndex = 0; 416 // Set to true when we've seen a "-per-" or a "per-", after which all units 417 // are in the denominator. Until we find an "-and-", at which point the 418 // identifier is invalid pending TODO(CLDR-13701). 419 private boolean fAfterPer = false; 420 // If an "-and-" was parsed prior to finding the "single 421 // * unit", sawAnd is set to true. If not, it is left as is. 422 private boolean fSawAnd = false; 423 424 // Cache the MeasurePrefix values array to make getPrefixFromTrieIndex() 425 // more efficient 426 private static MeasureUnit.MeasurePrefix[] measurePrefixValues = 427 MeasureUnit.MeasurePrefix.values(); 428 UnitsParser(String identifier)429 private UnitsParser(String identifier) { 430 this.fSource = identifier; 431 432 try { 433 this.trie = UnitsParser.savedTrie.clone(); 434 } catch (CloneNotSupportedException e) { 435 throw new ICUCloneNotSupportedException(); 436 } 437 } 438 439 static { 440 // Build Units trie. 441 CharsTrieBuilder trieBuilder; 442 trieBuilder = new CharsTrieBuilder(); 443 444 // Add SI and binary prefixes 445 for (MeasureUnit.MeasurePrefix unitPrefix : measurePrefixValues) { unitPrefix.getIdentifier()446 trieBuilder.add(unitPrefix.getIdentifier(), getTrieIndexForPrefix(unitPrefix)); 447 } 448 449 // Add syntax parts (compound, power prefixes) 450 trieBuilder.add("-per-", CompoundPart.PER.getTrieIndex()); 451 trieBuilder.add("-", CompoundPart.TIMES.getTrieIndex()); 452 trieBuilder.add("-and-", CompoundPart.AND.getTrieIndex()); 453 trieBuilder.add("per-", InitialCompoundPart.INITIAL_COMPOUND_PART_PER.getTrieIndex()); 454 trieBuilder.add("square-", PowerPart.P2.getTrieIndex()); 455 trieBuilder.add("cubic-", PowerPart.P3.getTrieIndex()); 456 trieBuilder.add("pow2-", PowerPart.P2.getTrieIndex()); 457 trieBuilder.add("pow3-", PowerPart.P3.getTrieIndex()); 458 trieBuilder.add("pow4-", PowerPart.P4.getTrieIndex()); 459 trieBuilder.add("pow5-", PowerPart.P5.getTrieIndex()); 460 trieBuilder.add("pow6-", PowerPart.P6.getTrieIndex()); 461 trieBuilder.add("pow7-", PowerPart.P7.getTrieIndex()); 462 trieBuilder.add("pow8-", PowerPart.P8.getTrieIndex()); 463 trieBuilder.add("pow9-", PowerPart.P9.getTrieIndex()); 464 trieBuilder.add("pow10-", PowerPart.P10.getTrieIndex()); 465 trieBuilder.add("pow11-", PowerPart.P11.getTrieIndex()); 466 trieBuilder.add("pow12-", PowerPart.P12.getTrieIndex()); 467 trieBuilder.add("pow13-", PowerPart.P13.getTrieIndex()); 468 trieBuilder.add("pow14-", PowerPart.P14.getTrieIndex()); 469 trieBuilder.add("pow15-", PowerPart.P15.getTrieIndex()); 470 471 // Add simple units 472 String[] simpleUnits = UnitsData.getSimpleUnits(); 473 for (int i = 0; i < simpleUnits.length; i++) { trieBuilder.add(simpleUnits[i], i + UnitsData.Constants.kSimpleUnitOffset)474 trieBuilder.add(simpleUnits[i], i + UnitsData.Constants.kSimpleUnitOffset); 475 476 } 477 478 // TODO: Use SLOW or FAST here? 479 UnitsParser.savedTrie = trieBuilder.build(StringTrieBuilder.Option.FAST); 480 } 481 482 /** 483 * Construct a MeasureUnit from a CLDR Unit Identifier, defined in UTS 35. 484 * Validates and canonicalizes the identifier. 485 * 486 * @return MeasureUnitImpl object or null if the identifier is empty. 487 * @throws IllegalArgumentException in case of invalid identifier. 488 */ parseForIdentifier(String identifier)489 public static MeasureUnitImpl parseForIdentifier(String identifier) { 490 if (identifier == null || identifier.isEmpty()) { 491 return null; 492 } 493 494 UnitsParser parser = new UnitsParser(identifier); 495 return parser.parse(); 496 497 } 498 getPrefixFromTrieIndex(int trieIndex)499 private static MeasureUnit.MeasurePrefix getPrefixFromTrieIndex(int trieIndex) { 500 return measurePrefixValues[trieIndex - UnitsData.Constants.kPrefixOffset]; 501 } 502 getTrieIndexForPrefix(MeasureUnit.MeasurePrefix prefix)503 private static int getTrieIndexForPrefix(MeasureUnit.MeasurePrefix prefix) { 504 return prefix.ordinal() + UnitsData.Constants.kPrefixOffset; 505 } 506 parse()507 private MeasureUnitImpl parse() { 508 MeasureUnitImpl result = new MeasureUnitImpl(); 509 510 if (fSource.isEmpty()) { 511 // The dimensionless unit: nothing to parse. return null. 512 return null; 513 } 514 515 while (hasNext()) { 516 fSawAnd = false; 517 SingleUnitImpl singleUnit = nextSingleUnit(); 518 519 boolean added = result.appendSingleUnit(singleUnit); 520 if (fSawAnd && !added) { 521 throw new IllegalArgumentException("Two similar units are not allowed in a mixed unit."); 522 } 523 524 if ((result.singleUnits.size()) >= 2) { 525 // nextSingleUnit fails appropriately for "per" and "and" in the 526 // same identifier. It doesn't fail for other compound units 527 // (COMPOUND_PART_TIMES). Consequently we take care of that 528 // here. 529 MeasureUnit.Complexity complexity = 530 fSawAnd ? MeasureUnit.Complexity.MIXED : MeasureUnit.Complexity.COMPOUND; 531 if (result.getSingleUnits().size() == 2) { 532 // After appending two singleUnits, the complexity will be MeasureUnit.Complexity.COMPOUND 533 assert result.getComplexity() == MeasureUnit.Complexity.COMPOUND; 534 result.setComplexity(complexity); 535 } else if (result.getComplexity() != complexity) { 536 throw new IllegalArgumentException("Can't have mixed compound units"); 537 } 538 } 539 } 540 541 return result; 542 } 543 544 /** 545 * Returns the next "single unit" via result. 546 * <p> 547 * If a "-per-" was parsed, the result will have appropriate negative 548 * dimensionality. 549 * <p> 550 * 551 * @throws IllegalArgumentException if we parse both compound units and "-and-", since mixed 552 * compound units are not yet supported - TODO(CLDR-13701). 553 */ nextSingleUnit()554 private SingleUnitImpl nextSingleUnit() { 555 SingleUnitImpl result = new SingleUnitImpl(); 556 557 // state: 558 // 0 = no tokens seen yet (will accept power, SI or binary prefix, or simple unit) 559 // 1 = power token seen (will not accept another power token) 560 // 2 = SI or binary prefix token seen (will not accept a power, or SI or binary prefix token) 561 int state = 0; 562 563 boolean atStart = fIndex == 0; 564 Token token = nextToken(); 565 566 if (atStart) { 567 // Identifiers optionally start with "per-". 568 if (token.getType() == Token.Type.TYPE_INITIAL_COMPOUND_PART) { 569 assert token.getInitialCompoundPart() == InitialCompoundPart.INITIAL_COMPOUND_PART_PER; 570 571 fAfterPer = true; 572 result.setDimensionality(-1); 573 574 token = nextToken(); 575 } 576 } else { 577 // All other SingleUnit's are separated from previous SingleUnit's 578 // via a compound part: 579 if (token.getType() != Token.Type.TYPE_COMPOUND_PART) { 580 throw new IllegalArgumentException("token type must be TYPE_COMPOUND_PART"); 581 } 582 583 CompoundPart compoundPart = CompoundPart.getCompoundPartFromTrieIndex(token.getMatch()); 584 switch (compoundPart) { 585 case PER: 586 if (fSawAnd) { 587 throw new IllegalArgumentException("Mixed compound units not yet supported"); 588 // TODO(CLDR-13701). 589 } 590 591 fAfterPer = true; 592 result.setDimensionality(-1); 593 break; 594 595 case TIMES: 596 if (fAfterPer) { 597 result.setDimensionality(-1); 598 } 599 break; 600 601 case AND: 602 if (fAfterPer) { 603 // not yet supported, TODO(CLDR-13701). 604 throw new IllegalArgumentException("Can't start with \"-and-\", and mixed compound units"); 605 } 606 fSawAnd = true; 607 break; 608 } 609 610 token = nextToken(); 611 } 612 613 // Read tokens until we have a complete SingleUnit or we reach the end. 614 while (true) { 615 switch (token.getType()) { 616 case TYPE_POWER_PART: 617 if (state > 0) { 618 throw new IllegalArgumentException(); 619 } 620 621 result.setDimensionality(result.getDimensionality() * token.getPower()); 622 state = 1; 623 break; 624 625 case TYPE_PREFIX: 626 if (state > 1) { 627 throw new IllegalArgumentException(); 628 } 629 630 result.setPrefix(token.getPrefix()); 631 state = 2; 632 break; 633 634 case TYPE_SIMPLE_UNIT: 635 result.setSimpleUnit(token.getSimpleUnitIndex(), UnitsData.getSimpleUnits()); 636 return result; 637 638 default: 639 throw new IllegalArgumentException(); 640 } 641 642 if (!hasNext()) { 643 throw new IllegalArgumentException("We ran out of tokens before finding a complete single unit."); 644 } 645 646 token = nextToken(); 647 } 648 } 649 hasNext()650 private boolean hasNext() { 651 return fIndex < fSource.length(); 652 } 653 nextToken()654 private Token nextToken() { 655 trie.reset(); 656 int match = -1; 657 // Saves the position in the fSource string for the end of the most 658 // recent matching token. 659 int previ = -1; 660 661 // Find the longest token that matches a value in the trie: 662 while (fIndex < fSource.length()) { 663 BytesTrie.Result result = trie.next(fSource.charAt(fIndex++)); 664 if (result == BytesTrie.Result.NO_MATCH) { 665 break; 666 } else if (result == BytesTrie.Result.NO_VALUE) { 667 continue; 668 } 669 670 match = trie.getValue(); 671 previ = fIndex; 672 673 if (result == BytesTrie.Result.FINAL_VALUE) { 674 break; 675 } 676 677 if (result != BytesTrie.Result.INTERMEDIATE_VALUE) { 678 throw new IllegalArgumentException("result must has an intermediate value"); 679 } 680 681 // continue; 682 } 683 684 685 if (match < 0) { 686 throw new IllegalArgumentException("Encountered unknown token starting at index " + previ); 687 } else { 688 fIndex = previ; 689 } 690 691 return new Token(match); 692 } 693 694 static class Token { 695 696 private final int fMatch; 697 private final Type type; 698 Token(int fMatch)699 public Token(int fMatch) { 700 this.fMatch = fMatch; 701 type = calculateType(fMatch); 702 } 703 getType()704 public Type getType() { 705 return this.type; 706 } 707 getPrefix()708 public MeasureUnit.MeasurePrefix getPrefix() { 709 assert this.type == Type.TYPE_PREFIX; 710 return getPrefixFromTrieIndex(this.fMatch); 711 } 712 713 // Valid only for tokens with type TYPE_COMPOUND_PART. getMatch()714 public int getMatch() { 715 assert getType() == Type.TYPE_COMPOUND_PART; 716 return fMatch; 717 } 718 719 // Even if there is only one InitialCompoundPart value, we have this 720 // function for the simplicity of code consistency. getInitialCompoundPart()721 public InitialCompoundPart getInitialCompoundPart() { 722 assert (this.type == Type.TYPE_INITIAL_COMPOUND_PART 723 && 724 fMatch == InitialCompoundPart.INITIAL_COMPOUND_PART_PER.getTrieIndex()); 725 return InitialCompoundPart.getInitialCompoundPartFromTrieIndex(fMatch); 726 } 727 getPower()728 public int getPower() { 729 assert this.type == Type.TYPE_POWER_PART; 730 return PowerPart.getPowerFromTrieIndex(this.fMatch); 731 } 732 getSimpleUnitIndex()733 public int getSimpleUnitIndex() { 734 assert this.type == Type.TYPE_SIMPLE_UNIT; 735 return this.fMatch - UnitsData.Constants.kSimpleUnitOffset; 736 } 737 738 // Calling calculateType() is invalid, resulting in an assertion failure, if Token 739 // value isn't positive. calculateType(int fMatch)740 private Type calculateType(int fMatch) { 741 if (fMatch <= 0) { 742 throw new AssertionError("fMatch must have a positive value"); 743 } 744 745 if (fMatch < UnitsData.Constants.kCompoundPartOffset) { 746 return Type.TYPE_PREFIX; 747 } 748 if (fMatch < UnitsData.Constants.kInitialCompoundPartOffset) { 749 return Type.TYPE_COMPOUND_PART; 750 } 751 if (fMatch < UnitsData.Constants.kPowerPartOffset) { 752 return Type.TYPE_INITIAL_COMPOUND_PART; 753 } 754 if (fMatch < UnitsData.Constants.kSimpleUnitOffset) { 755 return Type.TYPE_POWER_PART; 756 } 757 758 return Type.TYPE_SIMPLE_UNIT; 759 } 760 761 enum Type { 762 TYPE_UNDEFINED, 763 TYPE_PREFIX, 764 // Token type for "-per-", "-", and "-and-". 765 TYPE_COMPOUND_PART, 766 // Token type for "per-". 767 TYPE_INITIAL_COMPOUND_PART, 768 TYPE_POWER_PART, 769 TYPE_SIMPLE_UNIT, 770 } 771 } 772 } 773 774 static class MeasureUnitImplComparator implements Comparator<MeasureUnitImpl> { 775 private final ConversionRates conversionRates; 776 MeasureUnitImplComparator(ConversionRates conversionRates)777 public MeasureUnitImplComparator(ConversionRates conversionRates) { 778 this.conversionRates = conversionRates; 779 } 780 781 @Override compare(MeasureUnitImpl o1, MeasureUnitImpl o2)782 public int compare(MeasureUnitImpl o1, MeasureUnitImpl o2) { 783 BigDecimal factor1 = this.conversionRates.getFactorToBase(o1).getConversionRate(); 784 BigDecimal factor2 = this.conversionRates.getFactorToBase(o2).getConversionRate(); 785 786 return factor1.compareTo(factor2); 787 } 788 } 789 790 static class MeasureUnitImplWithIndexComparator implements Comparator<MeasureUnitImplWithIndex> { 791 private MeasureUnitImplComparator measureUnitImplComparator; 792 MeasureUnitImplWithIndexComparator(ConversionRates conversionRates)793 public MeasureUnitImplWithIndexComparator(ConversionRates conversionRates) { 794 this.measureUnitImplComparator = new MeasureUnitImplComparator(conversionRates); 795 } 796 797 @Override compare(MeasureUnitImplWithIndex o1, MeasureUnitImplWithIndex o2)798 public int compare(MeasureUnitImplWithIndex o1, MeasureUnitImplWithIndex o2) { 799 return this.measureUnitImplComparator.compare(o1.unitImpl, o2.unitImpl); 800 } 801 } 802 803 static class SingleUnitComparator implements Comparator<SingleUnitImpl> { 804 @Override compare(SingleUnitImpl o1, SingleUnitImpl o2)805 public int compare(SingleUnitImpl o1, SingleUnitImpl o2) { 806 return o1.compareTo(o2); 807 } 808 } 809 } 810