// $HeadURL$
// $Id$
//
// Copyright © 2006, 2010, 2011, 2012 by the President and Fellows of Harvard College.
//
// Screensaver is an open-source project developed by the ICCB-L and NSRB labs
// at Harvard Medical School. This software is distributed under the terms of
// the GNU General Public License.
package edu.harvard.med.screensaver.model;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.apache.log4j.Logger;
/*package*/ abstract class Quantity<Q extends Quantity<Q,T>, T extends Enum<T> & QuantityUnit<T>> // have to match the bounds for QuantityUnit
implements Comparable<Q>
{
private static Logger log = Logger.getLogger(Quantity.class);
private T _unit;
private BigDecimal _value;
private BigDecimal _displayValue;
protected abstract Q newQuantity(BigDecimal value, T unit);
public Quantity(Integer value, T unit)
{
this(value.toString(), unit);
}
public Quantity(Long value, T unit)
{
this(value.toString(), unit);
}
/**
* Create a Quantity using a String representation of a decimal number, provided
* in the specified units. If value has a fractional value, converts to units
* that are small enough to hold the provided value as a whole number.
*
* @param value
* @param unit
*/
public Quantity(String value, T unit)
{
this(new BigDecimal(value), unit, RoundingMode.UNNECESSARY);
}
public Quantity(String value, T unit, RoundingMode roundingMode)
{
this(new BigDecimal(value), unit);
}
public Quantity(BigDecimal value, T unit)
{
_value = scaleValue(value, unit, RoundingMode.UNNECESSARY);
_unit = unit;
}
public Quantity(BigDecimal value, T unit, RoundingMode roundingMode)
{
_value = scaleValue(value, unit, roundingMode);
_unit = unit;
}
/**
* Return a Quantity scaled to the specified units. Protects against loss of
* precision during conversion.
*
* @throws ArithmeticException if non-zero trailing decimal places would be
* lost in the conversion
* @return a Quantity, scaled to the specified units
*/
public Q convert(T newUnit)
{
if (_unit == newUnit) {
return (Q) this;
}
return newQuantity(convertUnits((Q) this, newUnit)/*.toString()*/, newUnit);
}
/**
* Return a Quantity scaled to the smallest units that can represent the value
* as a whole number.
*/
public Q convertToReasonableUnits()
{
//Q newQuantity = newQuantity(_value, _unit);
BigDecimal value = getValue();
T unit = getUnits();
BigDecimal newValue = value;
T newUnitChoice = unit;
for (T newUnit : _unit.getValues()) {
newValue = convertUnits(value, unit.getScale(), newUnit.getScale());
newUnitChoice = newUnit;
if (newValue.abs().compareTo(BigDecimal.ONE) >= 0) {
break;
}
}
newValue = scaleValue(newValue, newUnitChoice);
return newQuantity(newValue,newUnitChoice);
}
protected BigDecimal scaleValue(BigDecimal value, QuantityUnit<T> unit)
{
return scaleValue(value, unit, RoundingMode.UNNECESSARY);
}
protected BigDecimal scaleValue(BigDecimal value, QuantityUnit<T> unit, RoundingMode roundingMode)
{
return value.setScale(unit.getValues()[unit.getValues().length - 1].getScale() - unit.getScale(),
roundingMode);
}
protected BigDecimal convertUnits(Q q, T newUnits)
{
return convertUnits(q.getValue(), q.getUnits().getScale(), newUnits.getScale());
}
private BigDecimal convertUnits(BigDecimal value, int oldScale, int newScale)
{
int scaleDelta = newScale - oldScale;
return value.scaleByPowerOfTen(scaleDelta);
}
public Q add(Q value)
{
return newQuantity(getValue().add(value.convert(_unit).getValue()), getUnits());
}
public Q subtract(Q value)
{
return newQuantity(getValue().subtract(value.convert(_unit).getValue()), getUnits());
}
public Q negate()
{
return newQuantity(getValue().negate()/*.toString()*/, getUnits());
}
public T getUnits()
{
return _unit;
}
public BigDecimal getValue()
{
return _value;
}
public BigDecimal getValue(T units)
{
return convertUnits((Q) this, units);
}
private BigDecimal stripTrailingFractionalZeros(BigDecimal value)
{
int scale = 0;
do {
BigDecimal truncated = _value.setScale(scale, RoundingMode.DOWN);
if (truncated.setScale(value.scale()).equals(value)) {
return truncated;
}
++scale;
} while (scale <= _unit.getValues()[ _unit.getValues().length - 1].getScale());
return value;
}
public BigDecimal getDisplayValue()
{
if (_displayValue == null) {
_displayValue = stripTrailingFractionalZeros(_value);
}
return _displayValue;
}
@Override
public String toString()
{
if (_displayValue == null) {
_displayValue = stripTrailingFractionalZeros(_value);
}
return _displayValue.toString() + " " + _unit.getSymbol();
}
public int compareTo(Q o)
{
return getValue().compareTo(o.convert(_unit).getValue());
}
@Override
public boolean equals(Object obj)
{
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Quantity)) {
return false;
}
if (this.getClass() != obj.getClass()) return false;
Q other = (Q) obj;
if (getValue().equals( other.getValue() ) && getUnits() == other.getUnits()) return true;
return (this.getValue().equals(convertUnits(other.getValue(), other.getUnits().getScale(), getUnits().getScale())));
}
@Override
public int hashCode()
{
return _value.hashCode() * 7 + _unit.hashCode() * 11;
}
// private methods
protected boolean isFractional()
{
return !_value.remainder(BigDecimal.ONE).equals(BigDecimal.ZERO.setScale(_value.scale()));
}
}