/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.types;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* A class for VoltDB decimal numbers.
*
* The main task of this class is serializing and deserializing Volt's 16-byte fixed precision and scale decimal format. The decimal's
* are converted to/from Java's {@link java.math.BigDecimal BigDecimal} class. <code>BigDecimal</code> stores values
* as an unscaled (unscaled means no trailing 0s) fixed point {@link java.math.BigInteger BigInteger} and a separate
* scale value. An exception (either {@link java.lang.RuntimeException RuntimeException} or
* {@link java.io.IOException IOException}) if a <code>BigDecimal</code> with a scale > 12 or precision greater then
* 38 is used. {@link java.math.BigDecimal#setScale(int) BigDecimal.setScale(int)} can be used to reduce the scale of
* a value before serialization.
*
* There is also a static method, stringToDecimal, to convert a string value to an acceptable BigDecimal value
* for VoltDB's DECIMAL type.
*/
public class VoltDecimalHelper {
/**
* The scale of decimals in Volt
*/
public static final int kDefaultScale = 12;
/**
* The precision of decimals in Volt
*/
public static final int kDefaultPrecision = 38;
/**
* Array containing the smallest 16-byte twos complement value that is used
* as SQL null.
*/
private static final byte[] NULL_INDICATOR =
new BigInteger("-170141183460469231731687303715884105728").toByteArray();
/**
* Math context specifying the precision of decimals in Volt.
*/
private static final MathContext context = new MathContext( kDefaultPrecision );
/**
* Array of scale factors used to scale up <code>BigInteger</code>s retrieved from
* <code>BigDecimal</code>s
*/
private static final BigInteger scaleFactors[] = new BigInteger[] {
BigInteger.ONE,
BigInteger.TEN,
BigInteger.TEN.pow(2),
BigInteger.TEN.pow(3),
BigInteger.TEN.pow(4),
BigInteger.TEN.pow(5),
BigInteger.TEN.pow(6),
BigInteger.TEN.pow(7),
BigInteger.TEN.pow(8),
BigInteger.TEN.pow(9),
BigInteger.TEN.pow(10),
BigInteger.TEN.pow(11),
BigInteger.TEN.pow(12),
BigInteger.TEN.pow(13),
BigInteger.TEN.pow(14),
BigInteger.TEN.pow(15),
BigInteger.TEN.pow(16),
BigInteger.TEN.pow(17),
BigInteger.TEN.pow(18),
BigInteger.TEN.pow(19),
BigInteger.TEN.pow(20),
BigInteger.TEN.pow(21),
BigInteger.TEN.pow(22),
BigInteger.TEN.pow(23),
BigInteger.TEN.pow(24),
BigInteger.TEN.pow(25),
BigInteger.TEN.pow(26),
BigInteger.TEN.pow(27),
BigInteger.TEN.pow(28),
BigInteger.TEN.pow(29),
BigInteger.TEN.pow(30),
BigInteger.TEN.pow(31),
BigInteger.TEN.pow(32),
BigInteger.TEN.pow(33),
BigInteger.TEN.pow(34),
BigInteger.TEN.pow(35),
BigInteger.TEN.pow(36),
BigInteger.TEN.pow(37),
BigInteger.TEN.pow(38)
};
private final static String m_roundingEnabledProperty = "BIGDECIMAL_ROUND";
private final static String m_defaultRoundingEnablement = "true";
private final static String m_roundingModeProperty = "BIGDECIMAL_ROUND_POLICY";
private final static String m_defaultRoundingMode = "HALF_UP";
/*
* This is the class of rounding configurations. This is really
* a pair, dressed up in glad rags. Note that the only way to set
* this is to set both components.
*/
private static class RoundingConfiguration {
private RoundingMode m_roundingMode;
private boolean m_roundingIsEnabled;
public RoundingConfiguration(boolean enabled, RoundingMode mode) {
m_roundingIsEnabled = enabled;
m_roundingMode = mode;
}
public final RoundingMode getRoundingMode() {
return m_roundingMode;
}
public final Boolean getRoundingIsEnabled() {
return m_roundingIsEnabled;
}
public final void setConfig(boolean enabled, RoundingMode roundingMode) {
m_roundingIsEnabled = enabled;
m_roundingMode = roundingMode;
}
};
private static RoundingConfiguration m_roundingConfiguration
= new RoundingConfiguration(Boolean.valueOf(System.getProperty(m_roundingEnabledProperty, m_defaultRoundingEnablement)),
RoundingMode.valueOf(System.getProperty(m_roundingModeProperty, m_defaultRoundingMode)));
/**
* Serialize the null decimal sigil to a the provided {@link java.nio.ByteBuffer ByteBuffer}
* @param buf <code>ByteBuffer</code> to serialize the decimal into
*/
static public void serializeNull(ByteBuffer buf) {
buf.put(NULL_INDICATOR);
}
/**
* Converts BigInteger's byte representation containing a scaled magnitude to a fixed size 16 byte array
* and set the sign in the most significant byte's most significant bit.
* @param scaledValue Scaled twos complement representation of the decimal
* @param isNegative Determines whether the sign bit is set
* @return
*/
private static final byte[] expandToLength16(byte scaledValue[], final boolean isNegative) {
if (scaledValue.length == 16) {
return scaledValue;
}
byte replacement[] = new byte[16];
if (isNegative) {
Arrays.fill(replacement, (byte)-1);
}
int shift = (16 - scaledValue.length);
for (int ii = 0; ii < scaledValue.length; ++ii) {
replacement[ii+shift] = scaledValue[ii];
}
return replacement;
}
static public byte[] serializeBigDecimal(BigDecimal bd) {
ByteBuffer buf = ByteBuffer.allocate(16);
serializeBigDecimal(bd, buf);
return buf.array();
}
public static final boolean isRoundingEnabled() {
return m_roundingConfiguration.getRoundingIsEnabled();
}
public static synchronized final void setRoundingConfig(boolean enabled, RoundingMode mode) {
m_roundingConfiguration.setConfig(enabled, mode);
}
public static final RoundingMode getRoundingMode() {
return m_roundingConfiguration.getRoundingMode();
}
/**
* Round a BigDecimal number to a scale, given the rounding mode.
* Note that the precision of the result can depend not only on its original
* precision and scale and the desired scale, but also on its value.
* For example, when rounding up with scale 2:<br>
* 9.1999 with input scale 4 and precision 5 returns 9.20 with precision 3 (down 2).<br>
* 9.9999 with input scale 4 and precision 5 returns 10.00 with precision 4 (down 1).<br>
* 91.9999 with input scale 4 and precision 6 returns 92.00 with precision 4 (down 2).
* @param bd the input value of arbitrary scale and precision
* @param scale the desired scale of the return value
* @param mode the rounding algorithm to use
* @return the rounded value approximately equal to bd, but having the desired scale
*/
static private final BigDecimal roundToScale(BigDecimal bd, int scale, RoundingMode mode) throws RuntimeException
{
int lostScaleDigits = bd.scale() - scale;
if (lostScaleDigits <= 0) {
return bd;
}
if (!isRoundingEnabled()) {
throw new RuntimeException(String.format("Decimal scale %d is greater than the maximum %d", bd.scale(), kDefaultScale));
}
int desiredPrecision = Math.max(1, bd.precision() - lostScaleDigits);
MathContext mc = new MathContext(desiredPrecision, mode);
BigDecimal nbd = bd.round(mc);
if (nbd.scale() != scale) {
nbd = nbd.setScale(scale);
}
assert(nbd.scale() == scale);
return nbd;
}
/**
* Serialize the {@link java.math.BigDecimal BigDecimal} to Volt's fixed precision and scale 16-byte format.
* @param bd {@link java.math.BigDecimal BigDecimal} to serialize
* @param buf {@link java.nio.ByteBuffer ByteBuffer} to serialize the <code>BigDecimal</code> to
* @throws RuntimeException Thrown if the precision is out of range, or the scale is out of range and rounding is not enabled.
*/
static public void serializeBigDecimal(BigDecimal bd, ByteBuffer buf)
{
if (bd == null) {
serializeNull(buf);
return;
}
int decimalScale = bd.scale();
if (decimalScale > kDefaultScale) {
bd = roundToScale(bd, kDefaultScale, getRoundingMode());
decimalScale = bd.scale();
}
int overallPrecision = bd.precision();
final int wholeNumberPrecision = overallPrecision - decimalScale;
if (wholeNumberPrecision > 26) {
throw new RuntimeException("Precision of " + bd + " to the left of the decimal point is " +
wholeNumberPrecision + " and the max is 26");
}
final int scalingFactor = Math.max(0, kDefaultScale - decimalScale);
BigInteger scalableBI = bd.unscaledValue();
//* enable to debug */ System.out.println("DEBUG BigDecimal: " + bd);
//* enable to debug */ System.out.println("DEBUG unscaled: " + scalableBI);
scalableBI = scalableBI.multiply(scaleFactors[scalingFactor]);
//* enable to debug */ System.out.println("DEBUG scaled to picos: " + scalableBI);
final byte wholePicos[] = scalableBI.toByteArray();
if (wholePicos.length > 16) {
throw new RuntimeException("Precision of " + bd + " is > 38 digits");
}
boolean isNegative = (scalableBI.signum() < 0);
buf.put(expandToLength16(wholePicos, isNegative));
}
/**
* Deserialize a Volt fixed precision and scale 16-byte decimal from a String representation
* @param decimal <code>String</code> representation of the decimal
*/
public static BigDecimal deserializeBigDecimalFromString(String decimal) throws IOException
{
if (decimal == null) {
return null;
}
BigDecimal bd = new BigDecimal(decimal);
// if the scale is too large, check for trailing zeros
if (bd.scale() > kDefaultScale) {
bd = bd.stripTrailingZeros();
if (bd.scale() > kDefaultScale) {
bd = roundToScale(bd, kDefaultScale, getRoundingMode());
}
}
// enforce scale 12 to make the precision check right
if (bd.scale() < kDefaultScale) {
bd = bd.setScale(kDefaultScale);
}
if (bd.precision() > 38) {
throw new RuntimeException(
"Decimal " + bd + " has more than " + kDefaultPrecision + " digits of precision.");
}
return bd;
}
/**
* Deserialize a Volt fixed precision and scale 16-byte decimal and return
* it as a {@link java.math.BigDecimal BigDecimal} .
* @param buffer {@link java.nio.ByteBuffer ByteBuffer} to read from
*/
public static BigDecimal deserializeBigDecimal(ByteBuffer buffer) {
byte decimalBytes[] = new byte[16];
buffer.get(decimalBytes);
if (java.util.Arrays.equals(decimalBytes, NULL_INDICATOR)) {
return null;
}
final BigDecimal bd = new BigDecimal(
new BigInteger(decimalBytes),
kDefaultScale, context);
if (bd.precision() > 38) {
throw new RuntimeException("Decimal " + bd + " has more than 38 digits of precision.");
}
return bd;
}
public static BigDecimal setDefaultScale(BigDecimal bd) {
return bd.setScale(kDefaultScale, getRoundingMode());
}
/**
* Convert a string to a VoltDB DECIMAL number with the default
* (and only possible) scale.
*
* @param valueStr
*/
public static BigDecimal stringToDecimal(String valueStr) {
BigInteger bi = new BigInteger(valueStr);
BigDecimal bd = new BigDecimal(bi);
bd = VoltDecimalHelper.setDefaultScale(bd);
return bd;
}
}