001package com.hfg.units; 002 003 004import java.math.BigDecimal; 005import java.math.MathContext; 006import java.math.RoundingMode; 007import java.util.*; 008import java.util.regex.Pattern; 009 010import com.hfg.exception.ProgrammingException; 011import com.hfg.util.CompareUtil; 012import com.hfg.util.StringBuilderPlus; 013import com.hfg.util.StringUtil; 014import com.hfg.util.collection.CollectionUtil; 015import com.hfg.util.collection.OrderedSet; 016 017// NOTE: Add new unit classes to the /META-DATA/services/com.hfg.units.Unit file 018// for them to be properly discoverable!!! 019//------------------------------------------------------------------------------ 020/** 021 Container for units. 022 <div> 023 @author J. Alex Taylor, hairyfatguy.com 024 </div> 025 */ 026//------------------------------------------------------------------------------ 027// com.hfg XML/HTML Coding Library 028// 029// This library is free software; you can redistribute it and/or 030// modify it under the terms of the GNU Lesser General Public 031// License as published by the Free Software Foundation; either 032// version 2.1 of the License, or (at your option) any later version. 033// 034// This library is distributed in the hope that it will be useful, 035// but WITHOUT ANY WARRANTY; without even the implied warranty of 036// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 037// Lesser General Public License for more details. 038// 039// You should have received a copy of the GNU Lesser General Public 040// License along with this library; if not, write to the Free Software 041// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 042// 043// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com 044// jataylor@hairyfatguy.com 045//------------------------------------------------------------------------------ 046 047public class Unit implements Comparable 048{ 049 private static MathContext sDefaultMathContext = new MathContext(16, RoundingMode.HALF_UP); 050 private static Set<Unit> sInstances = new OrderedSet<>(50); 051 private static Map<String, Unit> sLookupMap = new HashMap<>(); 052 private static boolean sInitialized = false; 053 054 private MeasurementSystem mMeasurementSystem; 055 private String mName; 056 private Set<String> mAlternateNames; 057 private String mPlural; 058 private String mAbbrev; 059 private String mPluralAbbrev; 060 private QuantityType mQuantityType; 061 private Integer mPow; 062 private BaseSIUnitConverter mConversionToBaseSIUnit; 063 private List<SubUnit> mSubUnits; 064 private MathContext mMathContext; 065 066 067 //--------------------------------------------------------------------------- 068 protected Unit(QuantityType inQuantityType, String inName, String inAbbrev, Integer inPow) 069 { 070 this(null, inQuantityType, inName, inAbbrev, inPow, null); 071 } 072 073 //--------------------------------------------------------------------------- 074 protected Unit(MeasurementSystem inSystem, QuantityType inQuantityType, String inName, String inAbbrev, Integer inPow) 075 { 076 this(inSystem, inQuantityType, inName, inAbbrev, inPow, null); 077 } 078 079 //--------------------------------------------------------------------------- 080 protected Unit(QuantityType inQuantityType, String inName, String inAbbrev, Integer inPow, BaseSIUnitConverter inConversionToBaseSIUnit) 081 { 082 this(null, inQuantityType, inName, inAbbrev, inPow, inConversionToBaseSIUnit); 083 } 084 085 //--------------------------------------------------------------------------- 086 protected Unit(MeasurementSystem inSystem, QuantityType inQuantityType, String inName, String inAbbrev, Integer inPow, BaseSIUnitConverter inConversionToBaseSIUnit) 087 { 088 mMeasurementSystem = inSystem; 089 mQuantityType = inQuantityType; 090 mName = inName; 091 mAbbrev = inAbbrev; 092 mPow = inPow; 093 mConversionToBaseSIUnit = inConversionToBaseSIUnit; 094 095 addToMaps(this); 096 } 097 098 //--------------------------------------------------------------------------- 099 protected Unit(MeasurementSystem inSystem, QuantityType inQuantityType, String inName, String inAbbrev, Integer inPow, BaseSIUnitConverter inConversionToBaseSIUnit, SubUnit[] inSubUnits) 100 { 101 mMeasurementSystem = inSystem; 102 mQuantityType = inQuantityType; 103 mName = inName; 104 mAbbrev = inAbbrev; 105 mPow = inPow; 106 mConversionToBaseSIUnit = inConversionToBaseSIUnit; 107 108 if (inSubUnits != null 109 && inSubUnits.length > 0) 110 { 111 mSubUnits = new ArrayList<>(Arrays.asList(inSubUnits)); 112 } 113 114 addToMaps(this); 115 } 116 117 //--------------------------------------------------------------------------- 118 protected Unit(MeasurementSystem inSystem, QuantityType inQuantityType, SubUnit inSubUnit) 119 { 120 mMeasurementSystem = inSystem; 121 mQuantityType = inQuantityType; 122 mSubUnits = new ArrayList<>(1); 123 mSubUnits.add(inSubUnit); 124 125 addToMaps(this); 126 } 127 128 //--------------------------------------------------------------------------- 129 protected Unit(Unit inSubUnit, SI_ScalingFactor inScalingFactor) 130 { 131 this(new SubUnit(inSubUnit, inScalingFactor)); 132 } 133 134 //--------------------------------------------------------------------------- 135 protected Unit(SubUnit... inSubUnits) 136 { 137 if (null == inSubUnits 138 || 0 == inSubUnits.length) 139 { 140 throw new RuntimeException("No SubUnits specified!"); 141 } 142 143 initFromSubUnits(Arrays.asList(inSubUnits)); 144 } 145 146 //--------------------------------------------------------------------------- 147 protected Unit(List<SubUnit> inSubUnits) 148 { 149 initFromSubUnits(inSubUnits); 150 } 151 152 //--------------------------------------------------------------------------- 153 protected Unit(MeasurementSystem inSystem, QuantityType inQuantityType, String inName, String inAbbrev, List<SubUnit> inSubUnits) 154 { 155 mMeasurementSystem = inSystem; 156 mQuantityType = inQuantityType; 157 mName = inName; 158 mAbbrev = inAbbrev; 159 mSubUnits = inSubUnits; 160 161 addToMaps(this); 162 } 163 164 //--------------------------------------------------------------------------- 165 protected Unit(MeasurementSystem inSystem, QuantityType inQuantityType, String inName, String inAbbrev, SubUnit... inSubUnits) 166 { 167 mMeasurementSystem = inSystem; 168 mQuantityType = inQuantityType; 169 mName = inName; 170 mAbbrev = inAbbrev; 171 172 if (inSubUnits != null 173 && inSubUnits.length > 0) 174 { 175 mSubUnits = new ArrayList<>(Arrays.asList(inSubUnits)); 176 } 177 178 addToMaps(this); 179 } 180 181 //--------------------------------------------------------------------------- 182 private void initFromSubUnits(List<SubUnit> inSubUnits) 183 { 184 mSubUnits = inSubUnits; 185 186 mMeasurementSystem = mSubUnits.get(0).getUnit().getMeasurementSystem(); 187 188 // Add to the lookup map but not to the list of instances since these are not "official" 189 String name = name(); 190 if (! sLookupMap.containsKey(name) // Don't override an existing mapping 191 && (getSubUnits().size() > 1 192 || null == getSubUnits().get(0).getScalingFactor())) // Don't add scaled simple base units 193 { 194 sLookupMap.put(name, this); 195 } 196 } 197 198 //--------------------------------------------------------------------------- 199 private void addToMaps(Unit inValue) 200 { 201 sInstances.add(inValue); 202 sLookupMap.put(inValue.name(), inValue); 203 } 204 205 //########################################################################### 206 // PUBLIC METHODS 207 //########################################################################### 208 209 //--------------------------------------------------------------------------- 210 /** 211 Sets the default MathContext to specify precision and rounding during operations. 212 * @param inValue the MathContext to use 213 */ 214 public static void setDefaultMathContext(MathContext inValue) 215 { 216 sDefaultMathContext = inValue; 217 } 218 219 //--------------------------------------------------------------------------- 220 /** 221 Sets the MathContext to specify precision and rounding during operations with this Unit. 222 * @param inValue the MathContext to use 223 */ 224 public Unit setMathContext(MathContext inValue) 225 { 226 mMathContext = inValue; 227 return this; 228 } 229 230 //--------------------------------------------------------------------------- 231 public MathContext getMathContext() 232 { 233 return mMathContext != null ? mMathContext : sDefaultMathContext; 234 } 235 236 //--------------------------------------------------------------------------- 237 public static Collection<? extends Unit> values() 238 { 239 ensureExtendingClassesAreInitialized(); 240 241 return Collections.unmodifiableCollection(sInstances); 242 } 243 244 //--------------------------------------------------------------------------- 245 public static Unit valueOf(SubUnit inSubUnit) 246 { 247 List<SubUnit> subUnits = new ArrayList<>(1); 248 subUnits.add(inSubUnit); 249 250 return valueOf(subUnits); 251 } 252 253 //--------------------------------------------------------------------------- 254 public static Unit valueOf(List<SubUnit> inSubUnits) 255 { 256 Unit unit = null; 257 258 if (CollectionUtil.hasValues(inSubUnits)) 259 { 260 for (Unit existingUnit : sInstances) 261 { 262 if (0 == existingUnit.compareTo(inSubUnits)) 263 { 264 unit = existingUnit; 265 break; 266 } 267 } 268 269 if (null == unit) 270 { 271 unit = new Unit(inSubUnits); 272 } 273 } 274 275 return unit; 276 } 277 278 //--------------------------------------------------------------------------- 279 public static Unit valueOf(String inString) 280 { 281 ensureExtendingClassesAreInitialized(); 282 283 Unit unit = null; 284 285 if (StringUtil.isSet(inString)) 286 { 287 String string = inString.trim(); 288 289 List<SubUnit> subUnits = new ArrayList<>(5); 290 291 if (string.contains("/")) 292 { 293 String[] pieces = string.split("/"); 294 if (pieces.length > 2) 295 { 296 throw new UnitParseException("Multiple '/' operators in a unit string is ambiguous!"); 297 } 298 299 // Special nasty case: '% v/v' 300 if (string.equalsIgnoreCase(ConcentrationUnit.pct_by_volume.getAbbrev()) 301 || ConcentrationUnit.pct_by_volume.getAlternateNames().contains(string)) 302 { 303 subUnits.addAll(ConcentrationUnit.pct_by_volume.getSubUnits()); 304 } // Special nasty case: '% w/w' 305 else if (string.equalsIgnoreCase(ConcentrationUnit.pct_by_weight.getAbbrev()) 306 || ConcentrationUnit.pct_by_weight.getAlternateNames().contains(string)) 307 { 308 subUnits.addAll(ConcentrationUnit.pct_by_weight.getSubUnits()); 309 } 310 else 311 { 312 // Numerator 313 subUnits.addAll(parseNumeratorBlockOfUnits(pieces[0].trim())); 314 315 // Denominator 316 subUnits.addAll(parseDenominatorBlockOfUnits(pieces[1].trim())); 317 } 318 } 319 else 320 { 321 subUnits.addAll(parseNumeratorBlockOfUnits(string)); 322 } 323 324 // Does the configuration of subunits match a defined unit? 325 for (Unit stdUnit : sInstances) 326 { 327 if (stdUnit.hasSubUnits() 328 && 0 == CompareUtil.compare(stdUnit.getSubUnits(), subUnits)) 329 { 330 unit = stdUnit; 331 break; 332 } 333 } 334 335 if (null == unit 336 && 1 == subUnits.size()) 337 { 338 SubUnit subUnit = subUnits.get(0); 339 for (Unit stdUnit : sInstances) 340 { 341 if (stdUnit.equals(subUnit.getUnit()) 342 && null == subUnit.getPow() 343 && null == subUnit.getScalingFactor()) 344 { 345 unit = stdUnit; 346 break; 347 } 348 } 349 } 350 351 if (null == unit) 352 { 353 // If all the subunits are of the same type, use that type as the constructor. 354 Set<QuantityType> quantityTypes = new HashSet<>(2); 355 for (SubUnit subUnit : subUnits) 356 { 357 quantityTypes.add(subUnit.getUnit().getQuantityType()); 358 } 359 360 if (1 == quantityTypes.size()) 361 { 362 QuantityType quantityType = quantityTypes.iterator().next(); 363 364 if (quantityType.equals(QuantityType.TIME)) 365 { 366 unit = new TimeUnit(subUnits); 367 } 368 else if (quantityType.equals(QuantityType.LENGTH)) 369 { 370 unit = new LengthUnit(subUnits); 371 } 372 else if (quantityType.equals(QuantityType.MASS)) 373 { 374 unit = new MassUnit(subUnits); 375 } 376 else if (quantityType.equals(QuantityType.VOLUME)) 377 { 378 unit = new VolumeUnit(subUnits); 379 } 380 else if (quantityType.equals(QuantityType.CONCENTRATION)) 381 { 382 unit = new ConcentrationUnit(subUnits); 383 } 384 else if (quantityType.equals(QuantityType.AREA)) 385 { 386 unit = new AreaUnit(subUnits); 387 } 388 else if (quantityType.equals(QuantityType.BIOLOGICAL_POTENCY)) 389 { 390 unit = new BiologicalPotencyUnit(subUnits); 391 } 392 else if (quantityType.equals(QuantityType.CATALYTIC_ACTIVITY)) 393 { 394 unit = new CatalyticActivityUnit(subUnits); 395 } 396 else if (quantityType.equals(QuantityType.ELECTRIC_CURRENT)) 397 { 398 unit = new ElectricCurrentUnit(subUnits); 399 } 400 } 401 402 // As a last resort just create a Unit from the SubUnits. 403 if (null == unit) 404 { 405 unit = new Unit(subUnits); 406 } 407 } 408 } 409 410 return unit; 411 } 412 413 //--------------------------------------------------------------------------- 414 @Override 415 public String toString() 416 { 417 StringBuilderPlus buffer = new StringBuilderPlus().setDelimiter(" "); 418 if (StringUtil.isSet(getAbbrev())) 419 { 420 buffer.append(getAbbrev()); 421 if (getPow() != null) 422 { 423 buffer.append(StringUtil.toSuperscript(getPow())); 424 } 425 } 426 else if (hasSubUnits()) 427 { 428 for (SubUnit subUnit : getSubUnits()) 429 { 430 if (subUnit.getPow() != null 431 && subUnit.getPow().equals(-1) 432 && 2 == getSubUnits().size() 433 && 1 == getSubUnits().indexOf(subUnit) 434 && (null == getSubUnits().get(0).getPow() 435 || getSubUnits().get(0).getPow() > 0)) 436 { 437 buffer.append("/"); 438 if (subUnit.getScalingFactor() != null) 439 { 440 buffer.append(subUnit.getScalingFactor().getSymbol()); 441 } 442 buffer.append(subUnit.toStringMinusPow()); 443 } 444 else 445 { 446 buffer.delimitedAppend(subUnit); 447 } 448 } 449 } 450 else 451 { 452 throw new ProgrammingException("Unit with no abbreviation or SubUnits!"); 453 } 454 455 return buffer.toString(); 456 } 457 458 //--------------------------------------------------------------------------- 459 @Override 460 public boolean equals(Object inObj2) 461 { 462 return (inObj2 != null 463 && inObj2 instanceof Unit 464 && (this == inObj2 465 || 0 == compareTo(inObj2))); 466 } 467 468 //--------------------------------------------------------------------------- 469 @Override 470 public int hashCode() 471 { 472 int hashCode = 0; 473 474 if (getMeasurementSystem() != null) 475 { 476 hashCode += 31 * getMeasurementSystem().hashCode(); 477 } 478 479 if (getQuantityType() != null) 480 { 481 hashCode += 31 * getQuantityType().hashCode(); 482 } 483 484 if (getPow() != null) 485 { 486 hashCode += 31 * getPow().hashCode(); 487 } 488 489 if (hasSubUnits()) 490 { 491 for (SubUnit subUnit : getSubUnits()) 492 { 493 hashCode += 31 * subUnit.hashCode(); 494 } 495 } 496 else 497 { 498 if (name() != null) 499 { 500 hashCode += 31 * name().hashCode(); 501 } 502 } 503 504 return hashCode; 505 } 506 507 //--------------------------------------------------------------------------- 508 public int compareTo(Object inObj2) 509 { 510 int result = -1; 511 if (inObj2 != null) 512 { 513 if (inObj2 instanceof Unit) 514 { 515 Unit unit2 = (Unit) inObj2; 516 517 result = CompareUtil.compare(getMeasurementSystem(), unit2.getMeasurementSystem()); 518 519/* 520 if (0 == result) 521 { 522 result = CompareUtil.compare(name(), unit2.name()); 523 } 524*/ 525 if (0 == result) 526 { 527 result = CompareUtil.compare(getQuantityType(), unit2.getQuantityType()); 528 } 529 530 if (0 == result) 531 { 532 result = CompareUtil.compare(getPow(), unit2.getPow()); 533 } 534 535 if (0 == result) 536 { 537 if (hasSubUnits()) 538 { 539 result = CompareUtil.compare(getSubUnits(), unit2.getSubUnits()); 540 } 541 else 542 { 543 result = CompareUtil.compare(name(), unit2.name()); 544 } 545 } 546 } 547 } 548 549 return result; 550 } 551 552 //--------------------------------------------------------------------------- 553 public int compareTo(List<SubUnit> inSubUnits) 554 { 555 int result = -1; 556 if (inSubUnits != null) 557 { 558 if (1 == inSubUnits.size()) 559 { 560 // Does this unit equal the specified subunit? 561 562 SubUnit subUnit = inSubUnits.get(0); 563 564 result = CompareUtil.compare(this, subUnit.getUnit()); 565 566 if (0 == result) 567 { 568 result = CompareUtil.compare(getPow(), subUnit.getPow()); 569 } 570 571 if (0 == result 572 && subUnit.getScalingFactor() != null) 573 { 574 result = -1; 575 } 576 } 577 578 if (result != 0 579 && hasSubUnits()) 580 { 581 result = CompareUtil.compare(getSubUnits(), inSubUnits); 582 583 if (0 == result) 584 { 585 if (getPow() != null) 586 { 587 result = 1; 588 } 589 } 590 } 591 } 592 593 return result; 594 } 595 596 //--------------------------------------------------------------------------- 597 public String name() 598 { 599 if (null == mName 600 && CollectionUtil.hasValues(mSubUnits)) 601 { 602 StringBuilderPlus name = new StringBuilderPlus().setDelimiter(" "); 603 for (SubUnit subUnit : mSubUnits) 604 { 605 String subUnitName = subUnit.getUnit().name(); 606 if (null == subUnitName) 607 { 608 name = null; 609 break; 610 } 611 612 if (subUnit.getScalingFactor() != null) 613 { 614 subUnitName = subUnit.getScalingFactor().getPrefix() + subUnitName; 615 } 616 617 if (subUnit.getPow() != null) 618 { 619 subUnitName += StringUtil.toSuperscript(subUnit.getPow()); 620 } 621 622 name.delimitedAppend(subUnitName); 623 } 624 625 if (StringUtil.isSet(name)) 626 { 627 mName = name.toString(); 628 } 629 } 630 631 return mName; 632 } 633 634 //--------------------------------------------------------------------------- 635 public String getAbbrev() 636 { 637 if (null == mAbbrev 638 && CollectionUtil.hasValues(mSubUnits)) 639 { 640 StringBuilderPlus abbrev = new StringBuilderPlus().setDelimiter(" "); 641 for (int i = 0; i < mSubUnits.size(); i++) 642 { 643 SubUnit subUnit = mSubUnits.get(i); 644 645 String subUnitAbbrev = subUnit.getUnit().getAbbrev(); 646 if (null == subUnitAbbrev) 647 { 648 abbrev = null; 649 break; 650 } 651 652 String powString = ""; 653 654 if (subUnit.getPow() != null) 655 { 656 if (1 == i 657 && -1 == subUnit.getPow() 658 && (null == mSubUnits.get(0).getPow() 659 || mSubUnits.get(0).getPow() > 1)) 660 { 661 abbrev.append("/"); 662 } 663 else 664 { 665 if (i > 0) 666 { 667 abbrev.append(" "); 668 } 669 powString = StringUtil.toSuperscript(subUnit.getPow()); 670 } 671 } 672 673 if (subUnit.getScalingFactor() != null) 674 { 675 subUnitAbbrev = subUnit.getScalingFactor().getSymbol() + subUnitAbbrev; 676 } 677 678 subUnitAbbrev += powString; 679 680 abbrev.append(subUnitAbbrev); 681 } 682 683 if (StringUtil.isSet(abbrev)) 684 { 685 mAbbrev = abbrev.toString(); 686 } 687 } 688 689 return mAbbrev; 690 } 691 692 //--------------------------------------------------------------------------- 693 public Unit addAlternateName(String inValue) 694 { 695 if (null == mAlternateNames) 696 { 697 mAlternateNames = new HashSet<>(4); 698 } 699 700 sLookupMap.put(inValue, this); 701 return this; 702 } 703 704 //--------------------------------------------------------------------------- 705 public Set<String> getAlternateNames() 706 { 707 return mAlternateNames; 708 } 709 710 //--------------------------------------------------------------------------- 711 public Unit setPlural(String inValue) 712 { 713 mPlural = inValue; 714 sLookupMap.put(mPlural, this); 715 return this; 716 } 717 718 //--------------------------------------------------------------------------- 719 public String getPlural() 720 { 721 return mPlural; 722 } 723 724 725 //--------------------------------------------------------------------------- 726 public Unit setPluralAbbrev(String inValue) 727 { 728 mPluralAbbrev = inValue; 729 sLookupMap.put(mPluralAbbrev, this); 730 return this; 731 } 732 733 //--------------------------------------------------------------------------- 734 public String getPluralAbbrev() 735 { 736 return mPluralAbbrev; 737 } 738 739 740 //--------------------------------------------------------------------------- 741 public Unit setMeasurementSystem(MeasurementSystem inValue) 742 { 743 mMeasurementSystem = inValue; 744 return this; 745 } 746 747 //--------------------------------------------------------------------------- 748 public MeasurementSystem getMeasurementSystem() 749 { 750 return mMeasurementSystem; 751 } 752 753 //--------------------------------------------------------------------------- 754 public QuantityType getQuantityType() 755 { 756 if (null == mQuantityType 757 && CollectionUtil.hasValues(getSubUnits()) 758 && 1 == getSubUnits().size()) 759 { 760 mQuantityType = getSubUnits().get(0).getUnit().getQuantityType(); 761 } 762 763 return mQuantityType; 764 } 765 766 //--------------------------------------------------------------------------- 767 public Integer getPow() 768 { 769 if (null == mPow 770 && CollectionUtil.hasValues(getSubUnits()) 771 && 1 == getSubUnits().size()) 772 { 773 mPow = getSubUnits().get(0).getUnit().getPow(); 774 } 775 776 return mPow; 777 } 778 779 //--------------------------------------------------------------------------- 780 public boolean hasSubUnits() 781 { 782 return CollectionUtil.hasValues(mSubUnits); 783 } 784 785 //--------------------------------------------------------------------------- 786 public List<SubUnit> getSubUnits() 787 { 788 return mSubUnits; 789 } 790 791 //--------------------------------------------------------------------------- 792 public double computeBaseSIValue(double inValue) 793 { 794 double value = inValue; 795 796 if (mConversionToBaseSIUnit != null) 797 { 798 value = mConversionToBaseSIUnit.apply(value); 799 } 800 else if (hasSubUnits() 801 && 1 == getSubUnits().size() 802 && getSubUnits().get(0).getUnit().mConversionToBaseSIUnit != null) 803 { 804 // The unit has a converter but was wrapped by another unit 805 value = getSubUnits().get(0).getUnit().mConversionToBaseSIUnit.apply(value); 806 } 807 else if (getMeasurementSystem() != null 808 && (getMeasurementSystem().equals(MeasurementSystem.Metric) 809 || getMeasurementSystem().equals(MeasurementSystem.SI))) 810 { 811 // Put the value in SI base units by scaling 812 Map<QuantityType, SI_ScalingFactor> scalingFactorMap = new HashMap<>(3); 813 814 if (getQuantityType() != null 815 || ! hasSubUnits()) 816 { 817 scalingFactorMap.put(getQuantityType(), SI_ScalingFactor.one); 818 } 819 else 820 { 821 for (SubUnit subUnit : getSubUnits()) 822 { 823 QuantityType quantityType = subUnit.getUnit().getQuantityType(); 824 scalingFactorMap.put(quantityType, SI_ScalingFactor.one); 825 } 826 } 827 828 value = computeScaledValue(value, scalingFactorMap); 829 } 830 else 831 { 832 throw new UnitException("Couldn't convert " + name() + " to base SI units! No converter specified."); 833 } 834 835 return value; 836 } 837 838 //--------------------------------------------------------------------------- 839 public double computeValueFromBaseSIValue(double inValue) 840 { 841 double value = inValue; 842 843 if (CollectionUtil.hasValues(getSubUnits())) 844 { 845 for (SubUnit subUnit : getSubUnits()) 846 { 847 if (subUnit.getScalingFactor() != null) 848 { 849 if (subUnit.getPow() != null 850 && subUnit.getPow() < 0) 851 { 852 value *= subUnit.getScalingFactor().getScalingFactor(); 853 } 854 else 855 { 856 value = value / subUnit.getScalingFactor().getScalingFactor(); 857 } 858 } 859 } 860 } 861 862 if (mConversionToBaseSIUnit != null) 863 { 864 value = mConversionToBaseSIUnit.reverse(value); 865 } 866 867 return value; 868 } 869 870 //--------------------------------------------------------------------------- 871 public double computeScaledValue(double inValue, Map<QuantityType, SI_ScalingFactor> inScalingFactorMap) 872 { 873 BigDecimal value = allocateBigDecimal(inValue); 874 875 if (CollectionUtil.hasValues(getSubUnits())) 876 { 877 for (SubUnit subUnit : getSubUnits()) 878 { 879 SI_ScalingFactor scalingFactor = inScalingFactorMap.get(subUnit.getUnit().getQuantityType()); 880 if (scalingFactor != null) 881 { 882 BigDecimal adjustedScalingFactor = allocateBigDecimal(scalingFactor.getScalingFactor()) 883 .multiply(allocateBigDecimal(subUnit.getScalingFactor() != null ? subUnit.getScalingFactor().getScalingFactor() : 1d)); 884 885 if (subUnit.getPow() != null 886 && subUnit.getPow() < 0) 887 { 888 value = value.divide(adjustedScalingFactor, getMathContext()); 889 } 890 else 891 { 892 value = value.multiply(adjustedScalingFactor); 893 } 894 } 895 } 896 } 897 else 898 { 899 SI_ScalingFactor scalingFactor = inScalingFactorMap.get(getQuantityType()); 900 if (scalingFactor != null) 901 { 902 if (getPow() != null 903 && getPow() < 0) 904 { 905 value = value.multiply(allocateBigDecimal(scalingFactor.getScalingFactor())); 906 } 907 else 908 { 909 value = value.divide(allocateBigDecimal(scalingFactor.getScalingFactor()), getMathContext()); 910 } 911 } 912 } 913 914 return value.doubleValue(); 915 } 916 917 //--------------------------------------------------------------------------- 918 public String computeUnitLabel(Map<QuantityType, SI_ScalingFactor> inScalingFactorMap, boolean isPlural) 919 { 920 StringBuilderPlus buffer = new StringBuilderPlus().setDelimiter(" "); 921 922 if (! CollectionUtil.hasValues(inScalingFactorMap) 923 && StringUtil.isSet(getAbbrev())) 924 { 925 buffer.delimitedAppend(isPlural && StringUtil.isSet(getPluralAbbrev()) ? getPluralAbbrev() : getAbbrev()); 926 } 927 else 928// if (StringUtil.isSet(getAbbrev()) 929// || ! hasSubUnits()) 930 if (! hasSubUnits() 931// || (1 == getSubUnits().size())) 932 || getQuantityType() != null) 933 { 934 String prefix = ""; 935 if (CollectionUtil.hasValues(inScalingFactorMap)) 936 { 937 SI_ScalingFactor scalingFactor = inScalingFactorMap.get(getQuantityType()); 938 if (scalingFactor != null) 939 { 940 prefix = scalingFactor.getSymbol(); 941 } 942 } 943 944 buffer.delimitedAppend(prefix + toString()); 945 if (getPow() != null) 946 { 947 buffer.append(StringUtil.toSuperscript(getPow())); 948 } 949 } 950 else 951 { 952 for (SubUnit subUnit : getSubUnits()) 953 { 954 String prefix = ""; 955 boolean prefixApplied = false; 956 if (inScalingFactorMap != null) 957 { 958 SI_ScalingFactor scalingFactor = inScalingFactorMap.get(subUnit.getUnit().getQuantityType()); 959 if (scalingFactor != null) 960 { 961 prefix = scalingFactor.getSymbol(); 962 prefixApplied = true; 963 } 964 } 965 966 if (subUnit.getPow() != null 967 && subUnit.getPow().equals(-1) 968 && 2 == getSubUnits().size() 969 && 1 == getSubUnits().indexOf(subUnit) 970 && (null == getSubUnits().get(0).getPow() 971 || getSubUnits().get(0).getPow() > 0)) 972 { 973 buffer.append("/" + prefix); 974 if (! prefixApplied 975 && subUnit.getScalingFactor() != null) 976 { 977 buffer.append(subUnit.getScalingFactor().getSymbol()); 978 } 979 buffer.append(subUnit.getUnit().getAbbrev()); 980 } 981 else 982 { 983 buffer.delimitedAppend(prefix); 984 if (! prefixApplied 985 && subUnit.getScalingFactor() != null) 986 { 987 buffer.append(subUnit.getScalingFactor().getSymbol()); 988 } 989 990 buffer.append(subUnit.getUnit().getAbbrev()); 991 992 if (subUnit.getPow() != null) 993 { 994 buffer.append(StringUtil.toSuperscript(subUnit.getPow())); 995 } 996 } 997 998 } 999 } 1000 1001 return buffer.toString(); 1002 } 1003 1004 //--------------------------------------------------------------------------- 1005 public Unit getNumerator() 1006 { 1007 Unit numerator = null; 1008 1009 if (CollectionUtil.hasValues(getSubUnits())) 1010 { 1011 List<SubUnit> numeratorSubUnits = new ArrayList<>(4); 1012 1013 for (SubUnit subUnit : getSubUnits()) 1014 { 1015 if (null == subUnit.getPow() 1016 || subUnit.getPow() > 0) 1017 { 1018 if (subUnit.getUnit().hasSubUnits() 1019 && ! subUnit.getUnit().getQuantityType().equals(QuantityType.VOLUME)) // Don't unroll L to dm3 1020 { 1021 for (SubUnit subSubUnit : subUnit.getUnit().getSubUnits()) 1022 { 1023 if (null == subSubUnit.getPow() 1024 || subSubUnit.getPow() > 0) 1025 { 1026 numeratorSubUnits.add(new SubUnit(subSubUnit.getUnit(), subUnit.getScalingFactor(), subUnit.getPow())); 1027 } 1028 } 1029 } 1030 else 1031 { 1032 numeratorSubUnits.add(subUnit); 1033 } 1034 } 1035 } 1036 1037 if (CollectionUtil.hasValues(numeratorSubUnits)) 1038 { 1039 numerator = Unit.valueOf(numeratorSubUnits); 1040 } 1041 } 1042 else if (null == getPow() 1043 || getPow() > 0) 1044 { 1045 numerator = this; 1046 } 1047 1048 return numerator; 1049 } 1050 1051 //--------------------------------------------------------------------------- 1052 public Unit getDenominator() 1053 { 1054 Unit denominator = null; 1055 1056 if (CollectionUtil.hasValues(getSubUnits())) 1057 { 1058 List<SubUnit> denominatorSubUnits = new ArrayList<>(4); 1059 for (SubUnit subUnit : getSubUnits()) 1060 { 1061 1062 if (subUnit.getUnit().hasSubUnits() 1063 && subUnit.getUnit().getSubUnits().size() > 1) // or L will unroll into dm3 1064 { 1065 for (SubUnit subSubUnit : subUnit.getUnit().getSubUnits()) 1066 { 1067 if (subSubUnit.getPow() != null 1068 && subSubUnit.getPow() < 0) 1069 { 1070 // Don't use the scaling factor of the parent subunit 1071 denominatorSubUnits.add(new SubUnit(subSubUnit.getUnit(), subSubUnit.getScalingFactor(), subSubUnit.getPow().equals(-1) ? null : -subSubUnit.getPow())); 1072 } 1073 } 1074 } 1075 else if (subUnit.getPow() != null 1076 && subUnit.getPow() < 0) 1077 { 1078 denominatorSubUnits.add(new SubUnit(subUnit.getUnit(), subUnit.getScalingFactor(), subUnit.getPow().equals(-1) ? null : -subUnit.getPow())); 1079 } 1080 } 1081 1082 if (CollectionUtil.hasValues(denominatorSubUnits)) 1083 { 1084 denominator = Unit.valueOf(denominatorSubUnits); 1085 } 1086 } 1087 else if (getPow() != null 1088 && getPow() < 0) 1089 { 1090 if (getPow().equals(-1)) 1091 { 1092 denominator = Unit.valueOf(getAbbrev()); 1093 } 1094 else 1095 { 1096 denominator = Unit.valueOf(new SubUnit(this, -getPow())); 1097 } 1098 } 1099 1100 return denominator; 1101 } 1102 1103 //########################################################################### 1104 // PRIVATE METHODS 1105 //########################################################################### 1106 1107 //--------------------------------------------------------------------------- 1108 private BigDecimal allocateBigDecimal(double inValue) 1109 { 1110 return new BigDecimal(Double.toString(inValue), getMathContext()); 1111 } 1112 1113 //--------------------------------------------------------------------------- 1114 // This is done to ensure that calls to values() and valueOf() have registered values to work with. 1115 // The file /META-DATA/services/com.hfg.units.Unit holds the names of the classes to be initialized. 1116 // (Had to create a custom ServiceLoader that doesn't try to instantiate the properties but just 1117 // calls Class.forName() to initialize the static definitions.) 1118 private static void ensureExtendingClassesAreInitialized() 1119 { 1120 if (! sInitialized) 1121 { 1122 try 1123 { 1124 new UnitServiceLoader().load(); 1125 } 1126 catch (Exception e) 1127 { 1128 // Ignore 1129 } 1130 1131 sInitialized = true; 1132 } 1133 } 1134 1135 1136 //--------------------------------------------------------------------------- 1137 private static List<SubUnit> parseNumeratorBlockOfUnits(String inString) 1138 { 1139 List<SubUnit> subUnits = new ArrayList<>(4); 1140 1141 String[] pieces = inString.split("\\s+"); 1142 String unparsableBit = null; 1143 for (String piece : pieces) 1144 { 1145 if (StringUtil.isSet(unparsableBit)) 1146 { 1147 piece = unparsableBit + " " + piece; 1148 } 1149 1150 SubUnit subUnit = parseSubUnit(piece); 1151 if (null == subUnit) 1152 { 1153 // Not recognized? Perhaps it's part of a multi-word unit? 1154 unparsableBit = piece; 1155 } 1156 else 1157 { 1158 unparsableBit = null; 1159 1160 // Special nasty case: 'fl oz' can get misinterpreted as femto liters and ounce. 1161 if (subUnit.getUnit().equals(MassUnit.ounce) 1162 && subUnits.size() > 0 1163 && subUnits.get(subUnits.size() - 1).getUnit().equals(VolumeUnit.liter) 1164 && subUnits.get(subUnits.size() - 1).getScalingFactor().equals(SI_ScalingFactor.femto)) 1165 { 1166 subUnits.remove(subUnits.size() - 1); 1167 subUnit = new SubUnit(VolumeUnit.fluid_ounce, subUnit.getPow()); 1168 } 1169 1170 subUnits.add(subUnit); 1171 } 1172 } 1173 1174 if (StringUtil.isSet(unparsableBit)) 1175 { 1176 // Special nasty case: "mole equivalent" 1177 if (unparsableBit.equalsIgnoreCase("equivalent") 1178 && 1 == subUnits.size() 1179 && subUnits.get(0).equals(AmountOfSubstanceUnit.mole)) 1180 { 1181 subUnits.set(0, new SubUnit(AmountOfSubstanceUnit.mole_equivalent)); 1182 } 1183 // Special nasty case: "fluid dram". 'fl' can get misinterpreted as femto liters. 1184 else if (unparsableBit.equalsIgnoreCase("dr") 1185 && 1 == subUnits.size() 1186 && subUnits.get(0).getUnit().equals(VolumeUnit.liter) 1187 && subUnits.get(0).getScalingFactor().equals(SI_ScalingFactor.femto)) 1188 { 1189 subUnits.set(0, new SubUnit(VolumeUnit.fluid_dram)); 1190 } 1191 else 1192 { 1193 throw new UnitParseException("Couldn't match " + StringUtil.singleQuote(unparsableBit) + " to a unit!"); 1194 } 1195 } 1196 1197 return subUnits; 1198 } 1199 1200 //--------------------------------------------------------------------------- 1201 private static List<SubUnit> parseDenominatorBlockOfUnits(String inString) 1202 { 1203 List<SubUnit> subUnits = new ArrayList<>(4); 1204 1205 String[] pieces = inString.split("\\s+"); 1206 String unparsableBit = null; 1207 for (String piece : pieces) 1208 { 1209 if (StringUtil.isSet(unparsableBit)) 1210 { 1211 piece = unparsableBit + " " + piece; 1212 } 1213 1214 SubUnit subUnit = parseSubUnit(piece); 1215 if (null == subUnit) 1216 { 1217 // Not recognized? Perhaps it's part of a multi-word unit? 1218 unparsableBit = piece; 1219 } 1220 else 1221 { 1222 unparsableBit = null; 1223 1224 // Special nasty case: 'fl oz' can get misinterpreted as femto liters and ounce. 1225 if (subUnit.getUnit().equals(MassUnit.ounce) 1226 && subUnits.size() > 0 1227 && subUnits.get(subUnits.size() - 1).getUnit().equals(VolumeUnit.liter) 1228 && subUnits.get(subUnits.size() - 1).getScalingFactor().equals(SI_ScalingFactor.femto)) 1229 { 1230 subUnits.remove(subUnits.size() - 1); 1231 subUnit = new SubUnit(VolumeUnit.fluid_ounce, subUnit.getPow()); 1232 } 1233 1234 Integer pow = subUnit.getPow(); 1235 if (pow != null) 1236 { 1237 pow = - pow; 1238 } 1239 else 1240 { 1241 pow = -1; 1242 } 1243 1244 subUnits.add(new SubUnit(subUnit.getUnit(), subUnit.getScalingFactor(), pow)); 1245 } 1246 } 1247 1248 if (StringUtil.isSet(unparsableBit)) 1249 { 1250 throw new UnitParseException("Couldn't match " + StringUtil.singleQuote(unparsableBit) + " to a unit!"); 1251 } 1252 1253 return subUnits; 1254 } 1255 1256 //--------------------------------------------------------------------------- 1257 private static SubUnit parseSubUnit(String inString) 1258 { 1259 SubUnit subUnit = null; 1260 1261 String string = inString; 1262 Integer pow = extractPow(string); 1263 1264 Unit unit = null; 1265 SI_ScalingFactor scalingFactor = null; 1266 1267 if (pow != null) 1268 { 1269 // Remove the pow from the end of the string 1270 string = string.substring(0, string.length() - pow.toString().length()); 1271 } 1272 1273 // Attempt to match using unit abbreviations 1274 for (Unit unitValue : sInstances) 1275 { 1276 if (StringUtil.isSet(unitValue.getAbbrev()) 1277 && string.endsWith(unitValue.getAbbrev())) 1278 { 1279 // Is there a prefix? 1280 int endIndex = string.length() - unitValue.getAbbrev().length(); 1281 if (endIndex > 0) 1282 { 1283 if (unitValue.getMeasurementSystem().equals(MeasurementSystem.SI) 1284 || unitValue.getMeasurementSystem().equals(MeasurementSystem.Metric)) 1285 { 1286 String prefix = string.substring(0, endIndex); 1287 SI_ScalingFactor matchingScalingFactor = SI_ScalingFactor.valueOf(prefix); 1288 if (matchingScalingFactor != null) 1289 { 1290 // We found a match 1291 unit = unitValue; 1292 scalingFactor = matchingScalingFactor; 1293 break; 1294 } 1295 } 1296 } 1297 else 1298 { 1299 // We found a match that has no scaling factor 1300 unit = unitValue; 1301 break; 1302 } 1303 } 1304 } 1305 1306 if (null == unit) 1307 { 1308 // Attempt to match using unit names 1309 for (String key : sLookupMap.keySet()) 1310 { 1311 if (StringUtil.isSet(key)) 1312 { 1313 // Does the string end with the unit name? 1314 if (Pattern.compile(key + "$", Pattern.CASE_INSENSITIVE).matcher(string).find()) 1315 { 1316 Unit unitValue = sLookupMap.get(key); 1317 1318 // Is there a prefix? 1319 int endIndex = string.length() - key.length(); 1320 if (endIndex > 0) 1321 { 1322 String prefix = string.substring(0, endIndex); 1323 SI_ScalingFactor matchingScalingFactor = SI_ScalingFactor.valueOf(prefix); 1324 if (matchingScalingFactor != null) 1325 { 1326 // We found a match 1327 unit = unitValue; 1328 scalingFactor = matchingScalingFactor; 1329 break; 1330 } 1331 } 1332 else 1333 { 1334 // We found a match that has no scaling factor 1335 unit = unitValue; 1336 break; 1337 } 1338 } 1339 } 1340 } 1341 } 1342 1343 if (null == unit 1344 && inString.endsWith("s")) 1345 { 1346 // The units may have been pluralized 1347 subUnit = parseSubUnit(inString.substring(0, inString.length() - 1)); 1348 } 1349 1350 if (null == unit 1351 && inString.endsWith(".")) 1352 { 1353 // The units may have had a period at the end of its abbreviation 1354 subUnit = parseSubUnit(inString.substring(0, inString.length() - 1)); 1355 } 1356 1357 1358 return (subUnit != null ? subUnit : unit != null ? new SubUnit(unit, scalingFactor, pow) : null); 1359 } 1360 1361 //--------------------------------------------------------------------------- 1362 private static Integer extractPow(String inString) 1363 { 1364 StringBuilder buffer = new StringBuilder(); 1365 for (int i = inString.length() - 1; i > 0; i--) 1366 { 1367 Character superscriptChar = null; 1368 switch (inString.charAt(i)) 1369 { 1370 case '\u207B': 1371 superscriptChar = '-'; 1372 break; 1373 case '\u2070': 1374 superscriptChar = '0'; 1375 break; 1376 case 0xB9: 1377 superscriptChar = '1'; 1378 break; 1379 case 0xB2: 1380 superscriptChar = '2'; 1381 break; 1382 case 0xB3: 1383 superscriptChar = '3'; 1384 break; 1385 case '\u2074': 1386 superscriptChar = '4'; 1387 break; 1388 case '\u2075': 1389 superscriptChar = '5'; 1390 break; 1391 case '\u2076': 1392 superscriptChar = '6'; 1393 break; 1394 case '\u2077': 1395 superscriptChar = '7'; 1396 break; 1397 case '\u2078': 1398 superscriptChar = '8'; 1399 break; 1400 case '\u2079': 1401 superscriptChar = '9'; 1402 break; 1403 } 1404 1405 if (superscriptChar != null) 1406 { 1407 buffer.insert(0, superscriptChar); 1408 } 1409 else 1410 { 1411 break; 1412 } 1413 } 1414 1415 return (buffer.length() > 0 ? Integer.parseInt(buffer.toString()) : null); 1416 } 1417 1418}