com.hfg.units.Quantity Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com_hfg Show documentation
Show all versions of com_hfg Show documentation
com.hfg xml, html, svg, and bioinformatics utility library
package com.hfg.units;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.hfg.exception.InvalidValueException;
import com.hfg.exception.ProgrammingException;
import com.hfg.util.CompareUtil;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;
//------------------------------------------------------------------------------
/**
Quantifiable amount. Amount plus units.
@author J. Alex Taylor, hairyfatguy.com
*/
//------------------------------------------------------------------------------
// com.hfg XML/HTML Coding Library
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
// [email protected]
//------------------------------------------------------------------------------
public class Quantity implements Comparable, Cloneable
{
private Double mDoubleValue;
private Integer mIntValue;
private Long mLongValue;
private Unit mUnit;
private Map mScalingFactorMap;
private List mSubQuantities;
private static final Pattern sIntRegex = Pattern.compile("([\\-+]?\\d+)\\s?+([^\\d\\.]+\\.?)");
private static final Pattern sScientificRegex = Pattern.compile("([\\-+]?(?:\\d+)?(?:\\.\\d+)?(?:E\\-?\\d+)?)\\s?+(.+)");
//###########################################################################
// CONSTRUCTORS
//###########################################################################
//---------------------------------------------------------------------------
/**
Convenience constructor that will call Unit.valueOf() on the specified unit string.
* @param inValue the numeric amount as a double
* @param inUnit the volume unit of the specified amount
*/
public Quantity(Double inValue, String inUnit)
{
this(inValue, Unit.valueOf(inUnit));
}
//---------------------------------------------------------------------------
/**
Convenience constructor that will call Unit.valueOf() on the specified unit string.
* @param inValue the numeric amount as a float
* @param inUnit the volume unit of the specified amount
*/
public Quantity(Float inValue, String inUnit)
{
this(inValue, Unit.valueOf(inUnit));
}
//---------------------------------------------------------------------------
/**
Convenience constructor that will call Unit.valueOf() on the specified unit string.
* @param inValue the numeric amount as an integer
* @param inUnit the volume unit of the specified amount
*/
public Quantity(Integer inValue, String inUnit)
{
this(inValue, Unit.valueOf(inUnit));
}
//---------------------------------------------------------------------------
/**
Convenience constructor that will call Unit.valueOf() on the specified unit string.
* @param inValue the numeric amount as an integer
* @param inUnit the volume unit of the specified amount
*/
public Quantity(Long inValue, String inUnit)
{
this(inValue, Unit.valueOf(inUnit));
}
//---------------------------------------------------------------------------
/**
Convenience constructor.
* @param inValue the string value
*/
public Quantity(String inValue)
{
if (inValue != null)
{
String trimmedValue = inValue.trim();
int length = trimmedValue.length();
// To allow for compound quantities we'll chew off a piece at a time
int start = 0;
while (start < length)
{
Matcher m = sIntRegex.matcher(trimmedValue);
if (m.find(start)
&& m.start() == start)
{
if (start > 0
|| m.end() < length)
{
String unitString = m.group(2).trim();
if (unitString.endsWith(" and"))
{
unitString = unitString.substring(0, unitString.length() - 4);
}
if (unitString.endsWith(",") || unitString.endsWith("."))
{
unitString = unitString.substring(0, unitString.length() - 1);
}
addSubQuantity(Integer.parseInt(m.group(1)), Unit.valueOf(unitString));
}
else
{
mIntValue = Integer.parseInt(m.group(1));
mUnit = Unit.valueOf(m.group(2));
}
start = m.end();
}
else
{
m = sScientificRegex.matcher(trimmedValue);
if (m.find(start)
&& m.start() == start)
{
if (start > 0
|| m.end() < length)
{
String unitString = m.group(2);
if (unitString.endsWith(","))
{
unitString = unitString.substring(0, unitString.length() - 1);
}
addSubQuantity(Double.parseDouble(m.group(1)), Unit.valueOf(unitString));
}
else
{
mDoubleValue = Double.parseDouble(m.group(1));
mUnit = Unit.valueOf(m.group(2));
}
start = m.end();
}
else
{
throw new UnitException("Couldn't parse " + StringUtil.singleQuote(inValue) + " into a Quantity!");
}
}
}
}
}
//---------------------------------------------------------------------------
public Quantity(Double inValue, Unit inUnit)
{
nullCheckAmount(inValue);
mDoubleValue = inValue;
mUnit = inUnit;
}
//---------------------------------------------------------------------------
public Quantity(Float inValue, Unit inUnit)
{
nullCheckAmount(inValue);
mDoubleValue = (inValue != null ? inValue.doubleValue() : null);
mUnit = inUnit;
}
//---------------------------------------------------------------------------
public Quantity(Integer inValue, Unit inUnit)
{
nullCheckAmount(inValue);
mIntValue = inValue;
mUnit = inUnit;
}
//---------------------------------------------------------------------------
public Quantity(Long inValue, Unit inUnit)
{
nullCheckAmount(inValue);
mLongValue = inValue;
mUnit = inUnit;
}
//---------------------------------------------------------------------------
private Quantity(List inSubQuantities)
{
mSubQuantities = inSubQuantities;
}
//---------------------------------------------------------------------------
private void nullCheckAmount(Number inValue)
{
if (null == inValue)
{
throw new InvalidValueException("A non-null amount must be specified!");
}
}
//###########################################################################
// PUBLIC METHODS
//###########################################################################
//---------------------------------------------------------------------------
@Override
public String toString()
{
return toString((String)null);
}
//---------------------------------------------------------------------------
/**
* Outputs the Quantity as a numeric value followed by the units where the
* numeric portion is formatted according to the specified sprintf string.
* @param inFmtString a sprintf format specification
* @return the formatted Quantity value
*/
public String toString(String inFmtString)
{
StringBuilderPlus buffer = new StringBuilderPlus().setDelimiter(" ");
if (mSubQuantities != null)
{
for (Quantity subQuantity : mSubQuantities)
{
buffer.delimitedAppend(subQuantity.toString(inFmtString));
}
}
else
{
Double doubleValue = doubleValue();
if (doubleValue != null)
{
if (StringUtil.isSet(inFmtString))
{
// Apply the specified formatting
buffer.append(String.format(inFmtString, doubleValue));
}
else if (doubleValue == Math.floor(doubleValue)
&& !Double.isInfinite(doubleValue))
{
// No formatting specified but the value can be represented by an integer
buffer.append(doubleValue.intValue());
}
else
{
buffer.append(doubleValue + "");
}
boolean isPlural = (doubleValue != 1.0);
buffer.delimitedAppend(mUnit.computeUnitLabel(mScalingFactorMap, isPlural));
}
}
return buffer.toString();
}
//---------------------------------------------------------------------------
/**
* Outputs the Quantity as a numeric value followed by the units where the
* numeric portion is formatted according to the specified DecimalFormat object.
* @param inFormat a DecimalFormat format specification
* @return the formatted Quantity value
*/
public String toString(DecimalFormat inFormat)
{
StringBuilderPlus buffer = new StringBuilderPlus().setDelimiter(" ");
if (mSubQuantities != null)
{
for (Quantity subQuantity : mSubQuantities)
{
buffer.delimitedAppend(subQuantity.toString(inFormat));
}
}
else
{
Double doubleValue = doubleValue();
if (doubleValue != null)
{
if (inFormat != null)
{
buffer.append(inFormat.format(doubleValue));
}
else if (doubleValue == Math.floor(doubleValue)
&& !Double.isInfinite(doubleValue))
{
// No formatting specified but the value can be represented by an integer
buffer.append(doubleValue.intValue());
}
else
{
buffer.append(doubleValue + "");
}
boolean isPlural = (doubleValue != 1.0);
buffer.delimitedAppend(mUnit.computeUnitLabel(mScalingFactorMap, isPlural));
}
}
return buffer.toString();
}
//---------------------------------------------------------------------------
@Override
public boolean equals(Object inObj2)
{
return (inObj2 != null
&& inObj2 instanceof Quantity
&& (this == inObj2
|| 0 == compareTo(inObj2)));
}
//---------------------------------------------------------------------------
@Override
public int hashCode()
{
int hashCode = 0;
Double doubleValue = doubleValue();
if (doubleValue != null)
{
hashCode += doubleValue.hashCode();
}
if (getUnit() != null)
{
hashCode += 31 * getUnit().hashCode();
}
if (CollectionUtil.hasValues(mScalingFactorMap))
{
for (QuantityType quantityType : mScalingFactorMap.keySet())
{
hashCode += 31 * quantityType.hashCode() + mScalingFactorMap.get(quantityType).hashCode();
}
}
return hashCode;
}
//---------------------------------------------------------------------------
@Override
public int compareTo(Object inObj2)
{
int result = -1;
if (inObj2 != null)
{
if (inObj2 instanceof Quantity)
{
Quantity quantity2 = (Quantity) inObj2;
result = CompareUtil.compare(getUnit(), quantity2.getUnit());
if (0 == result)
{
result = CompareUtil.compare(doubleValue(), quantity2.doubleValue());
}
if (0 == result)
{
result = CompareUtil.compare(mScalingFactorMap, quantity2.mScalingFactorMap);
}
}
}
return result;
}
//---------------------------------------------------------------------------
@Override
public Quantity clone()
{
Quantity clone;
try
{
clone = (Quantity) super.clone();
}
catch (CloneNotSupportedException e)
{
throw new ProgrammingException(e);
}
if (mScalingFactorMap != null)
{
clone.mScalingFactorMap = new HashMap<>(mScalingFactorMap);
}
if (mSubQuantities != null)
{
clone.mSubQuantities = new ArrayList<>(mSubQuantities);
}
return clone;
}
//---------------------------------------------------------------------------
public void scale(QuantityType inQuantityType, SI_ScalingFactor inScalingFactor)
{
if (null == mScalingFactorMap)
{
mScalingFactorMap = new HashMap<>(4);
}
mScalingFactorMap.put(inQuantityType, inScalingFactor);
}
//---------------------------------------------------------------------------
public Quantity convertTo(Unit inUnit)
{
Quantity convertedQuantity = null;
if (mSubQuantities != null)
{
// It's a compound quantity. Convert sub-quantities to the unit of the last
// sub-quantity and total them.
for (Quantity subquantity : mSubQuantities)
{
if (null == convertedQuantity)
{
convertedQuantity = subquantity.convertTo(inUnit);
}
else
{
convertedQuantity = convertedQuantity.add(subquantity.convertTo(inUnit));
}
}
}
else
{
// It's a regular quantity
if (getUnit().equals(inUnit))
{
// It's already in the desired units
convertedQuantity = this;
}
else
{
// Our standard of interchange is the base SI unit
Double doubleValue = mUnit.computeBaseSIValue(doubleValue());
convertedQuantity = new Quantity(inUnit.computeValueFromBaseSIValue(doubleValue), inUnit);
}
}
return convertedQuantity;
}
//---------------------------------------------------------------------------
public Quantity invert()
{
return new Quantity( - doubleValue(), getUnit());
}
//---------------------------------------------------------------------------
public Quantity multiplyBy(double inValue)
{
Quantity newQuantity;
if (mSubQuantities != null)
{
List newSubQuantities = new ArrayList<>(2);
for (Quantity subQuantity : mSubQuantities)
{
newSubQuantities.add(subQuantity.multiplyBy(inValue));
}
newQuantity = new Quantity(newSubQuantities);
}
else
{
newQuantity = new Quantity(doubleValue() * inValue, getUnit());
}
return newQuantity;
}
//---------------------------------------------------------------------------
public Quantity divideBy(double inValue)
{
Quantity newQuantity;
if (mSubQuantities != null)
{
List newSubQuantities = new ArrayList<>(2);
for (Quantity subQuantity : mSubQuantities)
{
newSubQuantities.add(subQuantity.divideBy(inValue));
}
newQuantity = new Quantity(newSubQuantities);
}
else
{
newQuantity = new Quantity(doubleValue() / inValue, getUnit());
}
return newQuantity;
}
//---------------------------------------------------------------------------
public Quantity add(Quantity inValue)
{
testQuantityTypeEquivalence(inValue);
Quantity result;
if (inValue != null)
{
Quantity convertedAdditionalValue = inValue.convertTo(getUnit());
result = new Quantity(doubleValue() + convertedAdditionalValue.doubleValue(), getUnit());
}
else
{
result = this;
}
return result;
}
//---------------------------------------------------------------------------
public Quantity subtract(Quantity inValue)
{
testQuantityTypeEquivalence(inValue);
Quantity result;
if (inValue != null)
{
Quantity convertedAdditionalValue = inValue.convertTo(getUnit());
result = new Quantity(doubleValue() - convertedAdditionalValue.doubleValue(), getUnit());
}
else
{
result = this;
}
return result;
}
//---------------------------------------------------------------------------
public boolean lessThan(Quantity inValue)
{
testQuantityTypeEquivalence(inValue);
boolean result = false;
if (inValue != null)
{
Quantity convertedValue = inValue.convertTo(getUnit());
result = doubleValue() < convertedValue.doubleValue();
}
return result;
}
//---------------------------------------------------------------------------
public boolean lessThanOrEqualTo(Quantity inValue)
{
testQuantityTypeEquivalence(inValue);
boolean result = false;
if (inValue != null)
{
Quantity convertedValue = inValue.convertTo(getUnit());
result = doubleValue() <= convertedValue.doubleValue();
}
return result;
}
//---------------------------------------------------------------------------
public boolean greaterThan(Quantity inValue)
{
testQuantityTypeEquivalence(inValue);
boolean result = false;
if (inValue != null)
{
Quantity convertedValue = inValue.convertTo(getUnit());
result = doubleValue() > convertedValue.doubleValue();
}
return result;
}
//---------------------------------------------------------------------------
public boolean greaterThanOrEqualTo(Quantity inValue)
{
testQuantityTypeEquivalence(inValue);
boolean result = false;
if (inValue != null)
{
Quantity convertedValue = inValue.convertTo(getUnit());
result = doubleValue() >= convertedValue.doubleValue();
}
return result;
}
//---------------------------------------------------------------------------
public Unit getUnit()
{
if (null == mUnit
&& mSubQuantities != null)
{
mUnit = mSubQuantities.get(mSubQuantities.size() - 1).getUnit();
}
return mUnit;
}
//---------------------------------------------------------------------------
public Double doubleValue()
{
Double value;
if (mSubQuantities != null)
{
// It's a compound quantity. Convert sub-quantities to the unit of the last
// sub-quantity and total them.
Unit targetUnit = mSubQuantities.get(mSubQuantities.size() - 1).getUnit();
double totalDoubleValue = 0;
for (Quantity subquantity : mSubQuantities)
{
totalDoubleValue += subquantity.convertTo(targetUnit).doubleValue();
}
value = totalDoubleValue;
}
else
{
// It's a regular quantity
value = mDoubleValue;
if (null == value)
{
if (mIntValue != null)
{
value = mIntValue.doubleValue();
}
else if (mLongValue != null)
{
value = mLongValue.doubleValue();
}
}
// Scale the value?
if (value != null
&& CollectionUtil.hasValues(mScalingFactorMap))
{
value = mUnit.computeScaledValue(value, mScalingFactorMap);
}
}
return value;
}
//---------------------------------------------------------------------------
public Float floatValue()
{
Double doubleValue = doubleValue();
return (doubleValue != null ? doubleValue.floatValue() : null);
}
//---------------------------------------------------------------------------
public Integer intValue()
{
Integer value;
if (mSubQuantities != null)
{
// It's a compound quantity. Convert sub-quantities to the unit of the last
// sub-quantity and total them.
Unit targetUnit = mSubQuantities.get(mSubQuantities.size() - 1).getUnit();
int totalIntValue = 0;
for (Quantity subquantity : mSubQuantities)
{
totalIntValue += subquantity.convertTo(targetUnit).intValue();
}
value = totalIntValue;
}
else
{
// It's a regular quantity
value = mIntValue;
if (null == value)
{
if (mDoubleValue != null)
{
value = mDoubleValue.intValue();
}
else if (mLongValue != null)
{
value = mLongValue.intValue();
}
}
// Scale the value?
if (value != null
&& CollectionUtil.hasValues(mScalingFactorMap))
{
value = (int) mUnit.computeScaledValue(value, mScalingFactorMap);
}
}
return value;
}
//---------------------------------------------------------------------------
public Long longValue()
{
Long value;
if (mSubQuantities != null)
{
// It's a compound quantity. Convert sub-quantities to the unit of the last
// sub-quantity and total them.
Unit targetUnit = mSubQuantities.get(mSubQuantities.size() - 1).getUnit();
long totalLongValue = 0;
for (Quantity subquantity : mSubQuantities)
{
totalLongValue += subquantity.convertTo(targetUnit).longValue();
}
value = totalLongValue;
}
else
{
// It's a regular quantity
value = mLongValue;
if (null == value)
{
if (mDoubleValue != null)
{
value = mDoubleValue.longValue();
}
else if (mIntValue != null)
{
value = mIntValue.longValue();
}
}
// Scale the value?
if (value != null
&& CollectionUtil.hasValues(mScalingFactorMap))
{
value = (long) mUnit.computeScaledValue(value, mScalingFactorMap);
}
}
return value;
}
//---------------------------------------------------------------------------
public Quantity autoScale()
{
return autoScale(SI_ScalingFactor.values());
}
//---------------------------------------------------------------------------
public Quantity autoScale(SI_ScalingFactor[] inScalingValues)
{
Quantity scaledQuantity = null;
double doubleValue = doubleValue();
if (doubleValue > 900
|| doubleValue < 0.1)
{
for (int i = 1; i < inScalingValues.length; i++)
{
if (doubleValue < inScalingValues[i - 1].getScalingFactor()
&& doubleValue >= inScalingValues[i].getScalingFactor())
{
if (StringUtil.isSet(getUnit().getAbbrev())
|| ! getUnit().hasSubUnits())
{
scaledQuantity = this.convertTo(new Unit(getUnit(), inScalingValues[i]));
}
else
{
List subUnits = new ArrayList<>(getUnit().getSubUnits());
subUnits.set(0, new SubUnit(subUnits.get(0).getUnit(), inScalingValues[i], subUnits.get(0).getPow()));
scaledQuantity = this.convertTo(new Unit(subUnits));
}
break;
}
}
}
return (scaledQuantity != null ? scaledQuantity : this);
}
//---------------------------------------------------------------------------
/**
Performs a validation test to ensure that the unit is of the expected quantity type.
@param inExpectedQuantityType the quantity type that is expected.
@throws InvalidValueException if the unit isn't set or is not of the expected type.
*/
public void verifyQuantityType(QuantityType inExpectedQuantityType)
{
if (null == getUnit()
|| ! getUnit().getQuantityType().equals(inExpectedQuantityType))
{
throw new InvalidValueException("The quantity " + StringUtil.singleQuote(toString()) + " isn't specified in " + inExpectedQuantityType + " units!");
}
}
//###########################################################################
// PRIVATE METHODS
//###########################################################################
//---------------------------------------------------------------------------
private void addSubQuantity(Integer inValue, Unit inUnit)
{
if (null == mSubQuantities)
{
mSubQuantities = new ArrayList<>(2);
}
mSubQuantities.add(new Quantity(inValue, inUnit));
}
//---------------------------------------------------------------------------
private void addSubQuantity(Double inValue, Unit inUnit)
{
if (null == mSubQuantities)
{
mSubQuantities = new ArrayList<>(2);
}
mSubQuantities.add(new Quantity(inValue, inUnit));
}
//---------------------------------------------------------------------------
private void testQuantityTypeEquivalence(Quantity inValue)
{
Unit unit1 = getUnit();
if (null == unit1
&& mSubQuantities != null)
{
unit1 = mSubQuantities.get(0).getUnit();
}
Unit unit2 = inValue.getUnit();
if (null == unit2
&& inValue.mSubQuantities != null)
{
unit2 = inValue.mSubQuantities.get(0).getUnit();
}
if (! unit1.getQuantityType().equals(unit2.getQuantityType()))
{
throw new UnitException("Quantity Type mismatch (" + unit1.getQuantityType() + " vs. " + unit2.getQuantityType() + ")!");
}
}
}