001package com.hfg.units;
002
003
004import java.text.DecimalFormat;
005import java.util.ArrayList;
006import java.util.HashMap;
007import java.util.List;
008import java.util.Map;
009import java.util.regex.Matcher;
010import java.util.regex.Pattern;
011
012import com.hfg.exception.InvalidValueException;
013import com.hfg.util.CompareUtil;
014import com.hfg.util.StringBuilderPlus;
015import com.hfg.util.StringUtil;
016import com.hfg.util.collection.CollectionUtil;
017
018//------------------------------------------------------------------------------
019/**
020 Quantifiable amount. Amount plus units.
021 <div>
022 @author J. Alex Taylor, hairyfatguy.com
023 </div>
024 */
025//------------------------------------------------------------------------------
026// com.hfg XML/HTML Coding Library
027//
028// This library is free software; you can redistribute it and/or
029// modify it under the terms of the GNU Lesser General Public
030// License as published by the Free Software Foundation; either
031// version 2.1 of the License, or (at your option) any later version.
032//
033// This library is distributed in the hope that it will be useful,
034// but WITHOUT ANY WARRANTY; without even the implied warranty of
035// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
036// Lesser General Public License for more details.
037//
038// You should have received a copy of the GNU Lesser General Public
039// License along with this library; if not, write to the Free Software
040// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
041//
042// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
043// jataylor@hairyfatguy.com
044//------------------------------------------------------------------------------
045
046public class Quantity implements Comparable
047{
048   private Double   mDoubleValue;
049   private Integer  mIntValue;
050   private Long     mLongValue;
051   private Unit     mUnit;
052   private Map<QuantityType, SI_ScalingFactor> mScalingFactorMap;
053   private List<Quantity> mSubQuantities;
054
055   private static final Pattern sIntRegex = Pattern.compile("([\\-+]?\\d+)\\s?+([^\\d\\.]+\\.?)");
056   private static final Pattern sScientificRegex = Pattern.compile("([\\-+]?(?:\\d+)?(?:\\.\\d+)?(?:E\\-?\\d+)?)\\s?+(.+)");
057
058   //###########################################################################
059   // CONSTRUCTORS
060   //###########################################################################
061
062   //---------------------------------------------------------------------------
063   /**
064    Convenience constructor that will call Unit.valueOf() on the specified unit string.
065    * @param inValue the numeric amount as a double
066    * @param inUnit the volume unit of the specified amount
067    */
068   public Quantity(Double inValue, String inUnit)
069   {
070      this(inValue, Unit.valueOf(inUnit));
071   }
072
073   //---------------------------------------------------------------------------
074   /**
075    Convenience constructor that will call Unit.valueOf() on the specified unit string.
076    * @param inValue the numeric amount as a float
077    * @param inUnit the volume unit of the specified amount
078    */
079   public Quantity(Float inValue, String inUnit)
080   {
081      this(inValue, Unit.valueOf(inUnit));
082   }
083
084   //---------------------------------------------------------------------------
085   /**
086    Convenience constructor that will call Unit.valueOf() on the specified unit string.
087    * @param inValue the numeric amount as an integer
088    * @param inUnit the volume unit of the specified amount
089    */
090   public Quantity(Integer inValue, String inUnit)
091   {
092      this(inValue, Unit.valueOf(inUnit));
093   }
094
095   //---------------------------------------------------------------------------
096   /**
097    Convenience constructor that will call Unit.valueOf() on the specified unit string.
098    * @param inValue the numeric amount as an integer
099    * @param inUnit the volume unit of the specified amount
100    */
101   public Quantity(Long inValue, String inUnit)
102   {
103      this(inValue, Unit.valueOf(inUnit));
104   }
105
106   //---------------------------------------------------------------------------
107   /**
108    Convenience constructor.
109    * @param inValue the string value
110    */
111   public Quantity(String inValue)
112   {
113      if (inValue != null)
114      {
115         String trimmedValue = inValue.trim();
116         int length = trimmedValue.length();
117
118         // To allow for compound quantities we'll chew off a piece at a time
119         int start = 0;
120         while (start < length)
121         {
122            Matcher m = sIntRegex.matcher(trimmedValue);
123            if (m.find(start)
124                && m.start() == start)
125            {
126               if (start > 0
127                   || m.end() < length)
128               {
129                  String unitString = m.group(2).trim();
130                  if (unitString.endsWith(" and"))
131                  {
132                     unitString = unitString.substring(0, unitString.length() - 4);
133                  }
134
135                  if (unitString.endsWith(",") || unitString.endsWith("."))
136                  {
137                     unitString = unitString.substring(0, unitString.length() - 1);
138                  }
139
140                  addSubQuantity(Integer.parseInt(m.group(1)), Unit.valueOf(unitString));
141               }
142               else
143               {
144                  mIntValue = Integer.parseInt(m.group(1));
145                  mUnit = Unit.valueOf(m.group(2));
146               }
147
148               start = m.end();
149            }
150            else
151            {
152               m = sScientificRegex.matcher(trimmedValue);
153               if (m.find(start)
154                   && m.start() == start)
155               {
156                  if (start > 0
157                      || m.end() < length)
158                  {
159                     String unitString = m.group(2);
160                     if (unitString.endsWith(","))
161                     {
162                        unitString = unitString.substring(0, unitString.length() - 1);
163                     }
164
165                     addSubQuantity(Double.parseDouble(m.group(1)), Unit.valueOf(unitString));
166                  }
167                  else
168                  {
169                     mDoubleValue = Double.parseDouble(m.group(1));
170                     mUnit = Unit.valueOf(m.group(2));
171                  }
172
173                  start = m.end();
174               }
175               else
176               {
177                  throw new UnitException("Couldn't parse " + StringUtil.singleQuote(inValue) + " into a Quantity!");
178               }
179            }
180         }
181      }
182   }
183
184   //---------------------------------------------------------------------------
185   public Quantity(Double inValue, Unit inUnit)
186   {
187      nullCheckAmount(inValue);
188      mDoubleValue = inValue;
189      mUnit = inUnit;
190   }
191
192   //---------------------------------------------------------------------------
193   public Quantity(Float inValue, Unit inUnit)
194   {
195      nullCheckAmount(inValue);
196      mDoubleValue = (inValue != null ? inValue.doubleValue() : null);
197      mUnit = inUnit;
198   }
199
200   //---------------------------------------------------------------------------
201   public Quantity(Integer inValue, Unit inUnit)
202   {
203      nullCheckAmount(inValue);
204      mIntValue = inValue;
205      mUnit = inUnit;
206   }
207
208   //---------------------------------------------------------------------------
209   public Quantity(Long inValue, Unit inUnit)
210   {
211      nullCheckAmount(inValue);
212      mLongValue = inValue;
213      mUnit = inUnit;
214   }
215
216   //---------------------------------------------------------------------------
217   private Quantity(List<Quantity> inSubQuantities)
218   {
219      mSubQuantities = inSubQuantities;
220   }
221
222   //---------------------------------------------------------------------------
223   private void nullCheckAmount(Number inValue)
224   {
225      if (null == inValue)
226      {
227         throw new InvalidValueException("A non-null amount must be specified!");
228      }
229   }
230
231   //###########################################################################
232   // PUBLIC METHODS
233   //###########################################################################
234
235   //---------------------------------------------------------------------------
236   @Override
237   public String toString()
238   {
239      return toString((String)null);
240   }
241
242   //---------------------------------------------------------------------------
243   /**
244    * Outputs the Quantity as a numeric value followed by the units where the
245    * numeric portion is formatted according to the specified sprintf string.
246    * @param inFmtString a sprintf format specification
247    * @return the formatted Quantity value
248    */
249   public String toString(String inFmtString)
250   {
251      StringBuilderPlus buffer = new StringBuilderPlus().setDelimiter(" ");
252
253      if (mSubQuantities != null)
254      {
255         for (Quantity subQuantity : mSubQuantities)
256         {
257            buffer.delimitedAppend(subQuantity.toString(inFmtString));
258         }
259      }
260      else
261      {
262         Double doubleValue = doubleValue();
263         if (doubleValue != null)
264         {
265            if (StringUtil.isSet(inFmtString))
266            {
267               // Apply the specified formatting
268               buffer.append(String.format(inFmtString, doubleValue));
269            }
270            else if (doubleValue == Math.floor(doubleValue)
271                     && !Double.isInfinite(doubleValue))
272            {
273               // No formatting specified but the value can be represented by an integer
274               buffer.append(doubleValue.intValue());
275            }
276            else
277            {
278               buffer.append(doubleValue + "");
279            }
280
281            boolean isPlural = (doubleValue != 1.0);
282            buffer.delimitedAppend(mUnit.computeUnitLabel(mScalingFactorMap, isPlural));
283         }
284      }
285
286      return buffer.toString();
287   }
288
289   //---------------------------------------------------------------------------
290   /**
291    * Outputs the Quantity as a numeric value followed by the units where the
292    * numeric portion is formatted according to the specified DecimalFormat object.
293    * @param inFormat a DecimalFormat format specification
294    * @return the formatted Quantity value
295    */
296   public String toString(DecimalFormat inFormat)
297   {
298      StringBuilderPlus buffer = new StringBuilderPlus().setDelimiter(" ");
299
300      if (mSubQuantities != null)
301      {
302         for (Quantity subQuantity : mSubQuantities)
303         {
304            buffer.delimitedAppend(subQuantity.toString(inFormat));
305         }
306      }
307      else
308      {
309         Double doubleValue = doubleValue();
310         if (doubleValue != null)
311         {
312            if (inFormat != null)
313            {
314               buffer.append(inFormat.format(doubleValue));
315            }
316            else if (doubleValue == Math.floor(doubleValue)
317                     && !Double.isInfinite(doubleValue))
318            {
319               // No formatting specified but the value can be represented by an integer
320               buffer.append(doubleValue.intValue());
321            }
322            else
323            {
324               buffer.append(doubleValue + "");
325            }
326
327            boolean isPlural = (doubleValue != 1.0);
328            buffer.delimitedAppend(mUnit.computeUnitLabel(mScalingFactorMap, isPlural));
329         }
330      }
331
332      return buffer.toString();
333   }
334
335   //---------------------------------------------------------------------------
336   @Override
337   public boolean equals(Object inObj2)
338   {
339      return (inObj2 != null
340            && inObj2 instanceof Quantity
341            && (this == inObj2
342            || 0 == compareTo(inObj2)));
343   }
344
345   //---------------------------------------------------------------------------
346   @Override
347   public int hashCode()
348   {
349      int hashCode = 0;
350
351      Double doubleValue = doubleValue();
352      if (doubleValue != null)
353      {
354         hashCode += doubleValue.hashCode();
355      }
356
357      if (getUnit() != null)
358      {
359         hashCode += 31 * getUnit().hashCode();
360      }
361
362      if (CollectionUtil.hasValues(mScalingFactorMap))
363      {
364         for (QuantityType quantityType : mScalingFactorMap.keySet())
365         {
366            hashCode += 31 * quantityType.hashCode() + mScalingFactorMap.get(quantityType).hashCode();
367         }
368      }
369
370      return hashCode;
371   }
372
373   //---------------------------------------------------------------------------
374   @Override
375   public int compareTo(Object inObj2)
376   {
377      int result = -1;
378      if (inObj2 != null)
379      {
380         if (inObj2 instanceof Quantity)
381         {
382            Quantity quantity2 = (Quantity) inObj2;
383
384            result = CompareUtil.compare(getUnit(), quantity2.getUnit());
385
386            if (0 == result)
387            {
388               result = CompareUtil.compare(doubleValue(), quantity2.doubleValue());
389            }
390
391            if (0 == result)
392            {
393               result = CompareUtil.compare(mScalingFactorMap, quantity2.mScalingFactorMap);
394            }
395         }
396      }
397
398      return result;
399   }
400
401   //---------------------------------------------------------------------------
402   public void scale(QuantityType inQuantityType, SI_ScalingFactor inScalingFactor)
403   {
404      if (null == mScalingFactorMap)
405      {
406         mScalingFactorMap = new HashMap<>(4);
407      }
408
409      mScalingFactorMap.put(inQuantityType, inScalingFactor);
410   }
411
412   //---------------------------------------------------------------------------
413   public Quantity convertTo(Unit inUnit)
414   {
415      Quantity convertedQuantity = null;
416
417      if (mSubQuantities != null)
418      {
419         // It's a compound quantity. Convert sub-quantities to the unit of the last
420         // sub-quantity and total them.
421         for (Quantity subquantity : mSubQuantities)
422         {
423            if (null == convertedQuantity)
424            {
425               convertedQuantity = subquantity.convertTo(inUnit);
426            }
427            else
428            {
429               convertedQuantity = convertedQuantity.add(subquantity.convertTo(inUnit));
430            }
431         }
432      }
433      else
434      {
435         // It's a regular quantity
436         if (getUnit().equals(inUnit))
437         {
438            // It's already in the desired units
439            convertedQuantity = this;
440         }
441         else
442         {
443            // Our standard of interchange is the base SI unit
444            Double doubleValue = mUnit.computeBaseSIValue(doubleValue());
445
446            convertedQuantity = new Quantity(inUnit.computeValueFromBaseSIValue(doubleValue), inUnit);
447         }
448      }
449
450      return convertedQuantity;
451   }
452
453   //---------------------------------------------------------------------------
454   public Quantity invert()
455   {
456      return new Quantity( - doubleValue(), getUnit());
457   }
458
459   //---------------------------------------------------------------------------
460   public Quantity multiplyBy(double inValue)
461   {
462      Quantity newQuantity;
463      if (mSubQuantities != null)
464      {
465         List<Quantity> newSubQuantities = new ArrayList<>(2);
466         for (Quantity subQuantity : mSubQuantities)
467         {
468            newSubQuantities.add(subQuantity.multiplyBy(inValue));
469         }
470
471         newQuantity = new Quantity(newSubQuantities);
472      }
473      else
474      {
475         newQuantity = new Quantity(doubleValue() * inValue, getUnit());
476      }
477
478
479      return newQuantity;
480   }
481
482   //---------------------------------------------------------------------------
483   public Quantity divideBy(double inValue)
484   {
485      Quantity newQuantity;
486      if (mSubQuantities != null)
487      {
488         List<Quantity> newSubQuantities = new ArrayList<>(2);
489         for (Quantity subQuantity : mSubQuantities)
490         {
491            newSubQuantities.add(subQuantity.divideBy(inValue));
492         }
493
494         newQuantity = new Quantity(newSubQuantities);
495      }
496      else
497      {
498         newQuantity = new Quantity(doubleValue() / inValue, getUnit());
499      }
500
501      return newQuantity;
502   }
503
504   //---------------------------------------------------------------------------
505   public Quantity add(Quantity inValue)
506   {
507      testQuantityTypeEquivalence(inValue);
508
509      Quantity result;
510      if (inValue != null)
511      {
512         Quantity convertedAdditionalValue = inValue.convertTo(getUnit());
513
514         result = new Quantity(doubleValue() + convertedAdditionalValue.doubleValue(), getUnit());
515      }
516      else
517      {
518         result = this;
519      }
520
521      return result;
522   }
523
524   //---------------------------------------------------------------------------
525   public Quantity subtract(Quantity inValue)
526   {
527      testQuantityTypeEquivalence(inValue);
528
529      Quantity result;
530      if (inValue != null)
531      {
532         Quantity convertedAdditionalValue = inValue.convertTo(getUnit());
533
534         result = new Quantity(doubleValue() - convertedAdditionalValue.doubleValue(), getUnit());
535      }
536      else
537      {
538         result = this;
539      }
540
541      return result;
542   }
543
544   //---------------------------------------------------------------------------
545   public boolean lessThan(Quantity inValue)
546   {
547      testQuantityTypeEquivalence(inValue);
548
549      boolean result = false;
550      if (inValue != null)
551      {
552         Quantity convertedValue = inValue.convertTo(getUnit());
553
554         result = doubleValue() < convertedValue.doubleValue();
555      }
556
557      return result;
558   }
559
560   //---------------------------------------------------------------------------
561   public boolean lessThanOrEqualTo(Quantity inValue)
562   {
563      testQuantityTypeEquivalence(inValue);
564
565      boolean result = false;
566      if (inValue != null)
567      {
568         Quantity convertedValue = inValue.convertTo(getUnit());
569
570         result = doubleValue() <= convertedValue.doubleValue();
571      }
572
573      return result;
574   }
575
576   //---------------------------------------------------------------------------
577   public boolean greaterThan(Quantity inValue)
578   {
579      testQuantityTypeEquivalence(inValue);
580
581      boolean result = false;
582      if (inValue != null)
583      {
584         Quantity convertedValue = inValue.convertTo(getUnit());
585
586         result = doubleValue() > convertedValue.doubleValue();
587      }
588
589      return result;
590   }
591
592   //---------------------------------------------------------------------------
593   public boolean greaterThanOrEqualTo(Quantity inValue)
594   {
595      testQuantityTypeEquivalence(inValue);
596
597      boolean result = false;
598      if (inValue != null)
599      {
600         Quantity convertedValue = inValue.convertTo(getUnit());
601
602         result = doubleValue() >= convertedValue.doubleValue();
603      }
604
605      return result;
606   }
607
608   //---------------------------------------------------------------------------
609   public Unit getUnit()
610   {
611      if (null == mUnit
612          && mSubQuantities != null)
613      {
614         mUnit = mSubQuantities.get(mSubQuantities.size() - 1).getUnit();
615      }
616
617      return mUnit;
618   }
619
620   //---------------------------------------------------------------------------
621   public Double doubleValue()
622   {
623      Double value;
624
625      if (mSubQuantities != null)
626      {
627         // It's a compound quantity. Convert sub-quantities to the unit of the last
628         // sub-quantity and total them.
629         Unit targetUnit = mSubQuantities.get(mSubQuantities.size() - 1).getUnit();
630
631         double totalDoubleValue = 0;
632         for (Quantity subquantity : mSubQuantities)
633         {
634            totalDoubleValue += subquantity.convertTo(targetUnit).doubleValue();
635         }
636
637         value = totalDoubleValue;
638      }
639      else
640      {
641         // It's a regular quantity
642         value = mDoubleValue;
643         if (null == value)
644         {
645            if (mIntValue != null)
646            {
647               value = mIntValue.doubleValue();
648            }
649            else if (mLongValue != null)
650            {
651               value = mLongValue.doubleValue();
652            }
653         }
654
655         // Scale the value?
656         if (value != null
657             && CollectionUtil.hasValues(mScalingFactorMap))
658         {
659            value = mUnit.computeScaledValue(value, mScalingFactorMap);
660         }
661      }
662
663      return value;
664   }
665
666   //---------------------------------------------------------------------------
667   public Float floatValue()
668   {
669      Double doubleValue = doubleValue();
670
671      return (doubleValue != null ? doubleValue.floatValue() : null);
672   }
673
674   //---------------------------------------------------------------------------
675   public Integer intValue()
676   {
677      Integer value;
678
679      if (mSubQuantities != null)
680      {
681         // It's a compound quantity. Convert sub-quantities to the unit of the last
682         // sub-quantity and total them.
683         Unit targetUnit = mSubQuantities.get(mSubQuantities.size() - 1).getUnit();
684
685         int totalIntValue = 0;
686         for (Quantity subquantity : mSubQuantities)
687         {
688            totalIntValue += subquantity.convertTo(targetUnit).intValue();
689         }
690
691         value = totalIntValue;
692      }
693      else
694      {
695         // It's a regular quantity
696         value = mIntValue;
697         if (null == value)
698         {
699            if (mDoubleValue != null)
700            {
701               value = mDoubleValue.intValue();
702            }
703            else if (mLongValue != null)
704            {
705               value = mLongValue.intValue();
706            }
707         }
708
709         // Scale the value?
710         if (value != null
711             && CollectionUtil.hasValues(mScalingFactorMap))
712         {
713            value = (int) mUnit.computeScaledValue(value, mScalingFactorMap);
714         }
715      }
716
717      return value;
718   }
719
720   //---------------------------------------------------------------------------
721   public Long longValue()
722   {
723      Long value;
724
725      if (mSubQuantities != null)
726      {
727         // It's a compound quantity. Convert sub-quantities to the unit of the last
728         // sub-quantity and total them.
729         Unit targetUnit = mSubQuantities.get(mSubQuantities.size() - 1).getUnit();
730
731         long totalLongValue = 0;
732         for (Quantity subquantity : mSubQuantities)
733         {
734            totalLongValue += subquantity.convertTo(targetUnit).longValue();
735         }
736
737         value = totalLongValue;
738      }
739      else
740      {
741         // It's a regular quantity
742         value = mLongValue;
743         if (null == value)
744         {
745            if (mDoubleValue != null)
746            {
747               value = mDoubleValue.longValue();
748            }
749            else if (mIntValue != null)
750            {
751               value = mIntValue.longValue();
752            }
753         }
754
755         // Scale the value?
756         if (value != null
757             && CollectionUtil.hasValues(mScalingFactorMap))
758         {
759            value = (long) mUnit.computeScaledValue(value, mScalingFactorMap);
760         }
761      }
762
763      return value;
764   }
765
766   //---------------------------------------------------------------------------
767   public Quantity autoScale()
768   {
769      return autoScale(SI_ScalingFactor.values());
770   }
771
772   //---------------------------------------------------------------------------
773   public Quantity autoScale(SI_ScalingFactor[] inScalingValues)
774   {
775      Quantity scaledQuantity = null;
776
777      double doubleValue = doubleValue();
778      if (doubleValue > 900
779            || doubleValue < 0.1)
780      {
781         for (int i = 1; i < inScalingValues.length; i++)
782         {
783            if (doubleValue < inScalingValues[i - 1].getScalingFactor()
784                  && doubleValue >= inScalingValues[i].getScalingFactor())
785            {
786               if (StringUtil.isSet(getUnit().getAbbrev())
787                           || ! getUnit().hasSubUnits())
788               {
789                  scaledQuantity = this.convertTo(new Unit(getUnit(), inScalingValues[i]));
790               }
791               else
792               {
793                  List<SubUnit> subUnits = new ArrayList<>(getUnit().getSubUnits());
794                  subUnits.set(0, new SubUnit(subUnits.get(0).getUnit(), inScalingValues[i], subUnits.get(0).getPow()));
795                  scaledQuantity = this.convertTo(new Unit(subUnits));
796               }
797
798               break;
799            }
800         }
801      }
802
803      return (scaledQuantity != null ? scaledQuantity : this);
804   }
805
806   //---------------------------------------------------------------------------
807   /**
808    Performs a validation test to ensure that the unit is of the expected quantity type.
809    @param inExpectedQuantityType the quantity type that is expected.
810    @throws InvalidValueException if the unit isn't set or is not of the expected type.
811    */
812   public void verifyQuantityType(QuantityType inExpectedQuantityType)
813   {
814      if (null == getUnit()
815          || ! getUnit().getQuantityType().equals(inExpectedQuantityType))
816      {
817         throw new InvalidValueException("The quantity " + StringUtil.singleQuote(toString()) + " isn't specified in " + inExpectedQuantityType + " units!");
818      }
819   }
820
821   //###########################################################################
822   // PRIVATE METHODS
823   //###########################################################################
824
825   //---------------------------------------------------------------------------
826   private void addSubQuantity(Integer inValue, Unit inUnit)
827   {
828      if (null == mSubQuantities)
829      {
830         mSubQuantities = new ArrayList<>(2);
831      }
832
833      mSubQuantities.add(new Quantity(inValue, inUnit));
834   }
835
836   //---------------------------------------------------------------------------
837   private void addSubQuantity(Double inValue, Unit inUnit)
838   {
839      if (null == mSubQuantities)
840      {
841         mSubQuantities = new ArrayList<>(2);
842      }
843
844      mSubQuantities.add(new Quantity(inValue, inUnit));
845   }
846
847   //---------------------------------------------------------------------------
848   private void testQuantityTypeEquivalence(Quantity inValue)
849   {
850      Unit unit1 = getUnit();
851      if (null == unit1
852          && mSubQuantities != null)
853      {
854         unit1 = mSubQuantities.get(0).getUnit();
855      }
856
857      Unit unit2 = inValue.getUnit();
858      if (null == unit2
859          && inValue.mSubQuantities != null)
860      {
861         unit2 = inValue.mSubQuantities.get(0).getUnit();
862      }
863
864      if (! unit1.getQuantityType().equals(unit2.getQuantityType()))
865      {
866         throw new UnitException("Quantity Type mismatch (" + unit1.getQuantityType() + " vs. " + unit2.getQuantityType() + ")!");
867      }
868   }
869
870}