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}