package de.jpaw.bonaparte.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.jpaw.bonaparte.core.BonaPortable;
import de.jpaw.bonaparte.core.DataAndMeta;
import de.jpaw.bonaparte.core.MessageParserException;
import de.jpaw.bonaparte.core.ObjectValidationException;
import de.jpaw.bonaparte.pojos.meta.NumericElementaryDataItem;
/** A class which provides some support functions which simplify working with BigDecimals.
* The key issue we try to solve here is to provide a semantic where 2.5 == 2.50, while
* the default BigDecimal implementation assumes 2.5 <> 2.50 (due to different scaling).
* As bonaparte specifies a specific number of fractional digits, we scale to the bonaparte size
* for hashCode(). The rounding mode is selected so that no Exception occurs.
* As a consequence, we have to provide an implementation of equals, which is consistent with that (in contrast to using compareTo).
*
*/
public class BigDecimalTools {
private static final Logger LOG = LoggerFactory.getLogger(BigDecimalTools.class);
private static final String DECIMALS_KEYWORD_MIN = "min";
private static final String DECIMALS_KEYWORD_MAX = "max";
/** Scales the BigDecimal to some predefined scale */
static public BigDecimal scale(BigDecimal a, int decimals) {
if (a != null && a.scale() != decimals)
a = a.setScale(decimals, RoundingMode.HALF_EVEN);
return a;
}
/** Computes the hashCode of a BigDecimal at a specific scale. */
static public int hashCode(BigDecimal a, int decimals) {
if (a == null)
return 0;
return scale(a, decimals).hashCode();
}
/** Compares to BigDecimal numbers. They can only be the same, if their rounded values are the same. */
static public boolean equals(BigDecimal a, int aDecimals, BigDecimal b, int bDecimals) {
if (a == null && b == null)
return true;
if (a == null || b == null)
return false; // exactly one of them if null, the other not
return scale(a, aDecimals).compareTo(scale(b, bDecimals)) == 0;
}
/** Check a parsed BigDecimal for allowed digits, and perform (if desired) scaling. Use the second form with the metadata parameter instead. */
static public BigDecimal checkAndScale(BigDecimal r, NumericElementaryDataItem di, int parseIndex, String currentClass) throws MessageParserException {
String fieldname = di.getName();
int decimals = di.getDecimalDigits();
try {
if (r.scale() > decimals)
r = r.setScale(decimals, di.getRounding() ? RoundingMode.HALF_EVEN : RoundingMode.UNNECESSARY);
if (di.getAutoScale() && r.scale() < decimals) // round for smaller as well!
r = r.setScale(decimals, RoundingMode.UNNECESSARY);
} catch (ArithmeticException a) {
throw new MessageParserException(MessageParserException.TOO_MANY_DECIMALS, fieldname, parseIndex, currentClass);
}
if (!di.getIsSigned() && r.signum() < 0)
throw new MessageParserException(MessageParserException.SUPERFLUOUS_SIGN, fieldname, parseIndex, currentClass);
// check for overflow
if (di.getTotalDigits() - decimals < r.precision() - r.scale())
throw new MessageParserException(MessageParserException.TOO_MANY_DIGITS, fieldname, parseIndex, currentClass);
return r;
}
/** Check a BigDecimal for compliance of the spec. */
static public void validate(BigDecimal r, NumericElementaryDataItem meta, String classname) throws ObjectValidationException {
try {
if (r.scale() > meta.getDecimalDigits())
r = r.setScale(meta.getDecimalDigits(), meta.getRounding() ? RoundingMode.HALF_EVEN : RoundingMode.UNNECESSARY);
} catch (ArithmeticException a) {
throw new ObjectValidationException(ObjectValidationException.TOO_MANY_FRACTIONAL_DIGITS, meta.getName(), classname);
}
if (!meta.getIsSigned() && r.signum() < 0)
throw new ObjectValidationException(ObjectValidationException.NO_NEGATIVE_ALLOWED, meta.getName(), classname);
// check for overflow
if (meta.getTotalDigits() - meta.getDecimalDigits() < r.precision() - r.scale())
throw new ObjectValidationException(ObjectValidationException.TOO_MANY_DIGITS, meta.getName(), classname);
}
/** Given an object tree and a pathname within this tree (which should point to some BigDecimal number),
* retrieve the number and scale it to the desired precision, as indicated by the property "decimals".
* The possible values are:
* min: scale the number to the smallest number of decimals
* max: scale the number to the precision of the underlying field
* (number): scale to the number of digits provided
* (pathname): retrieve the object from the tree and use its value (if it is a string, it is interpreted as a currency)
*
* If no decimals property is found, the algorithm looks tree upwards (to the root).
* If no decimals property can be found at any level, "min" is assumed.
*
* @param root
* @param path the path of the field,, in dot notation. At least the field name must be here (indicating a field at the root level)
* @returns the scaled number
*/
static public BigDecimal retrieveScaled(BonaPortable root, String path) {
DataAndMeta value = FieldGetter.getSingleFieldWithMeta(root, path);
if (value == null || value.data == null || !(value.data instanceof BigDecimal))
return null; // wrong type
BigDecimal numValue = (BigDecimal)value.data;
NumericElementaryDataItem meta = (NumericElementaryDataItem)value.meta;
StringRef prefix = new StringRef();
prefix.prefix = path;
String props = getFieldPropertyWithDescend(root, path, prefix, "decimals", DECIMALS_KEYWORD_MIN);
if (props.length() == 0)
props = DECIMALS_KEYWORD_MIN;
if (DECIMALS_KEYWORD_MIN.equals(props)) {
BigDecimal tmp = numValue.stripTrailingZeros();
if (tmp.scale() < 0)
tmp = tmp.setScale(0);
return tmp;
}
if (DECIMALS_KEYWORD_MAX.equals(props)) {
return numValue.setScale(meta.getDecimalDigits());
}
// check for numeric immediate specification
if (Character.isDigit(props.charAt(0)))
return numValue.setScale(Integer.parseInt(props));
// last resort: assume it is another pathname, retrieve that value
Object precision = FieldGetter.getField(root, prefix.prefix + props);
if (precision == null) {
LOG.warn("Did not find referenced precision field for root object {} and path {}", root.ret$PQON(), path);
return numValue; // this should not happen, but fall back instead of throwing an NPE
}
if (precision instanceof Integer)
return numValue.setScale(((Integer)precision).intValue(), RoundingMode.HALF_EVEN);
// it's not an integer, assume it is a String
int currencyPrecision = Currency.getInstance((String)precision).getDefaultFractionDigits();
return numValue.setScale(currencyPrecision, RoundingMode.HALF_EVEN);
}
// utility class to pass back a second return value
static private class StringRef {
private String prefix;
}
static public String getFieldPropertyWithDescend(BonaPortable root, String path, StringRef prefix, String propertyName, String defaultProperty) {
String workingPath = path;
String props = null;
// if the pathname contains no dot, the only containing object is the root
for (;;) {
int lastDot = workingPath.lastIndexOf('.');
if (lastDot < 0) {
// no more component
props = root.ret$BonaPortableClass().getProperty(naked(workingPath) + "." + propertyName);
prefix.prefix = "";
break; // must stop here!
} else {
// get the meta from the parent object
String container = workingPath.substring(0, lastDot);
String fieldname = workingPath.substring(lastDot+1);
Object parent = FieldGetter.getFieldOrObj(root, container);
props = ((BonaPortable)parent).ret$BonaPortableClass().getProperty(naked(fieldname) + "." + propertyName);
if (props != null) {
LOG.debug("found property " + props + " at path " + container);
prefix.prefix = container + ".";
break;
}
// not found here, descend
workingPath = container;
}
}
return props == null ? defaultProperty : props;
}
/** Returns a path fragment without any array / index. */
static private String naked(String path) {
int indexBracket = path.indexOf('[');
if (indexBracket < 0)
return path; // contains no array index
else
return path.substring(0, indexBracket);
}
}