blob: 92d688c4e38497c3dd33a9e709945c00ac910e02 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.marshal;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.util.Objects;
import com.google.common.primitives.Ints;
import org.apache.cassandra.cql3.CQL3Type;
import org.apache.cassandra.cql3.Constants;
import org.apache.cassandra.cql3.Term;
import org.apache.cassandra.serializers.TypeSerializer;
import org.apache.cassandra.serializers.DecimalSerializer;
import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
import org.apache.cassandra.utils.bytecomparable.ByteSource;
import ch.obermuhlner.math.big.BigDecimalMath;
public class DecimalType extends NumberType<BigDecimal>
{
public static final DecimalType instance = new DecimalType();
private static final ByteBuffer MASKED_VALUE = instance.decompose(BigDecimal.ZERO);
private static final int MIN_SCALE = 32;
private static final int MIN_SIGNIFICANT_DIGITS = MIN_SCALE;
private static final int MAX_SCALE = 1000;
private static final MathContext MAX_PRECISION = new MathContext(10000);
// Constants or escaping values needed to encode/decode variable-length floating point numbers (decimals) in our
// custom byte-ordered encoding scheme.
private static final int POSITIVE_DECIMAL_HEADER_MASK = 0x80;
private static final int NEGATIVE_DECIMAL_HEADER_MASK = 0x00;
private static final int DECIMAL_EXPONENT_LENGTH_HEADER_MASK = 0x40;
private static final byte DECIMAL_LAST_BYTE = (byte) 0x00;
private static final BigInteger HUNDRED = BigInteger.valueOf(100);
private static final ByteBuffer ZERO_BUFFER = instance.decompose(BigDecimal.ZERO);
DecimalType() {super(ComparisonType.CUSTOM);} // singleton
public boolean isEmptyValueMeaningless()
{
return true;
}
@Override
public boolean isFloatingPoint()
{
return true;
}
public <VL, VR> int compareCustom(VL left, ValueAccessor<VL> accessorL, VR right, ValueAccessor<VR> accessorR)
{
return compareComposed(left, accessorL, right, accessorR, this);
}
/**
* Constructs a byte-comparable representation.
* This is rather difficult and involves reconstructing the decimal.
*
* To compare, we need a normalized value, i.e. one with a sign, exponent and (0,1) mantissa. To avoid
* loss of precision, both exponent and mantissa need to be base-100. We can't get this directly off the serialized
* bytes, as they have base-10 scale and base-256 unscaled part.
*
* We store:
* - sign bit inverted * 0x80 + 0x40 + signed exponent length, where exponent is negated if value is negative
* - zero or more exponent bytes (as given by length)
* - 0x80 + first pair of decimal digits, negative if value is negative, rounded to -inf
* - zero or more 0x80 + pair of decimal digits, always positive
* - trailing 0x00
* Zero is special-cased as 0x80.
*
* Because the trailing 00 cannot be produced from a pair of decimal digits (positive or not), no value can be
* a prefix of another.
*
* Encoding examples:
* 1.1 as c1 = 0x80 (positive number) + 0x40 + (positive exponent) 0x01 (exp length 1)
* 01 = exponent 1 (100^1)
* 81 = 0x80 + 01 (0.01)
* 8a = 0x80 + 10 (....10) 0.0110e2
* 00
* -1 as 3f = 0x00 (negative number) + 0x40 - (negative exponent) 0x01 (exp length 1)
* ff = exponent -1. negative number, thus 100^1
* 7f = 0x80 - 01 (-0.01) -0.01e2
* 00
* -99.9 as 3f = 0x00 (negative number) + 0x40 - (negative exponent) 0x01 (exp length 1)
* ff = exponent -1. negative number, thus 100^1
* 1c = 0x80 - 100 (-1.00)
* 8a = 0x80 + 10 (+....10) -0.999e2
* 00
*
*/
@Override
public <V> ByteSource asComparableBytes(ValueAccessor<V> accessor, V data, ByteComparable.Version version)
{
BigDecimal value = compose(data, accessor);
if (value == null)
return null;
if (value.compareTo(BigDecimal.ZERO) == 0) // Note: 0.equals(0.0) returns false!
return ByteSource.oneByte(POSITIVE_DECIMAL_HEADER_MASK);
long scale = (((long) value.scale()) - value.precision()) & ~1;
boolean negative = value.signum() < 0;
// Make a base-100 exponent (this will always fit in an int).
int exponent = Math.toIntExact(-scale >> 1);
// Flip the exponent sign for negative numbers, so that ones with larger magnitudes are propely treated as smaller.
final int modulatedExponent = negative ? -exponent : exponent;
// We should never have scale > Integer.MAX_VALUE, as we're always subtracting the non-negative precision of
// the encoded BigDecimal, and furthermore we're rounding to negative infinity.
assert scale <= Integer.MAX_VALUE;
// However, we may end up overflowing on the negative side.
if (scale < Integer.MIN_VALUE)
{
// As scaleByPowerOfTen needs an int scale, do the scaling in two steps.
int mv = Integer.MIN_VALUE;
value = value.scaleByPowerOfTen(mv);
scale -= mv;
}
final BigDecimal mantissa = value.scaleByPowerOfTen(Ints.checkedCast(scale)).stripTrailingZeros();
// We now have a smaller-than-one signed mantissa, and a signed and modulated base-100 exponent.
assert mantissa.abs().compareTo(BigDecimal.ONE) < 0;
return new ByteSource()
{
// Start with up to 5 bytes for sign + exponent.
int exponentBytesLeft = 5;
BigDecimal current = mantissa;
@Override
public int next()
{
if (exponentBytesLeft > 0)
{
--exponentBytesLeft;
if (exponentBytesLeft == 4)
{
// Skip leading zero bytes in the modulatedExponent.
exponentBytesLeft -= Integer.numberOfLeadingZeros(Math.abs(modulatedExponent)) / 8;
// Now prepare the leading byte which includes the sign of the number plus the sign and length of the modulatedExponent.
int explen = DECIMAL_EXPONENT_LENGTH_HEADER_MASK + (modulatedExponent < 0 ? -exponentBytesLeft : exponentBytesLeft);
return explen + (negative ? NEGATIVE_DECIMAL_HEADER_MASK : POSITIVE_DECIMAL_HEADER_MASK);
}
else
return (modulatedExponent >> (exponentBytesLeft * 8)) & 0xFF;
}
else if (current == null)
{
return END_OF_STREAM;
}
else if (current.compareTo(BigDecimal.ZERO) == 0)
{
current = null;
return 0x00;
}
else
{
BigDecimal v = current.scaleByPowerOfTen(2);
BigDecimal floor = v.setScale(0, RoundingMode.FLOOR);
current = v.subtract(floor);
return floor.byteValueExact() + 0x80;
}
}
};
}
@Override
public <V> V fromComparableBytes(ValueAccessor<V> accessor, ByteSource.Peekable comparableBytes, ByteComparable.Version version)
{
if (comparableBytes == null)
return accessor.empty();
int headerBits = comparableBytes.next();
if (headerBits == POSITIVE_DECIMAL_HEADER_MASK)
return accessor.valueOf(ZERO_BUFFER);
// I. Extract the exponent.
// The sign of the decimal, and the sign and the length (in bytes) of the decimal exponent, are all encoded in
// the first byte.
// Get the sign of the decimal...
boolean isNegative = headerBits < POSITIVE_DECIMAL_HEADER_MASK;
headerBits -= isNegative ? NEGATIVE_DECIMAL_HEADER_MASK : POSITIVE_DECIMAL_HEADER_MASK;
headerBits -= DECIMAL_EXPONENT_LENGTH_HEADER_MASK;
// Get the sign and the length of the exponent (the latter is encoded as its negative if the sign of the
// exponent is negative)...
boolean isExponentNegative = headerBits < 0;
headerBits = isExponentNegative ? -headerBits : headerBits;
// Now consume the exponent bytes. If the exponent is negative and uses less than 4 bytes, the remaining bytes
// should be padded with 1s, in order for the constructed int to contain the correct (negative) exponent value.
// So, if the exponent is negative, we can just start with all bits set to 1 (i.e. we can start with -1).
int exponent = isExponentNegative ? -1 : 0;
for (int i = 0; i < headerBits; ++i)
exponent = (exponent << 8) | comparableBytes.next();
// The encoded exponent also contains the decimal sign, in order to correctly compare exponents in case of
// negative decimals (e.g. x * 10^y > x * 10^z if x < 0 && y < z). After the decimal sign is "removed", what's
// left is a base-100 exponent following BigDecimal's convention for the exponent sign.
exponent = isNegative ? -exponent : exponent;
// II. Extract the mantissa as a BigInteger value. It was encoded as a BigDecimal value between 0 and 1, in
// order to be used for comparison (after the sign of the decimal and the sign and the value of the exponent),
// but when decoding we don't need that property on the transient mantissa value.
BigInteger mantissa = BigInteger.ZERO;
int curr = comparableBytes.next();
while (curr != DECIMAL_LAST_BYTE)
{
// The mantissa value is constructed by a standard positional notation value calculation.
// The value of the next digit is the next most-significant mantissa byte as an unsigned integer,
// offset by a predetermined value (in this case, 0x80)...
int currModified = curr - 0x80;
// ...multiply the current value by the base (in this case, 100)...
mantissa = mantissa.multiply(HUNDRED);
// ...then add the next digit to the modified current value...
mantissa = mantissa.add(BigInteger.valueOf(currModified));
// ...and finally, adjust the base-100, BigDecimal format exponent accordingly.
--exponent;
curr = comparableBytes.next();
}
// III. Construct the final BigDecimal value, by combining the mantissa and the exponent, guarding against
// underflow or overflow when exponents are close to their boundary values.
long base10NonBigDecimalFormatExp = 2L * exponent;
// When expressing a sufficiently big decimal, BigDecimal's internal scale value will be negative with very
// big absolute value. To compute the encoded exponent, this internal scale has the number of digits of the
// unscaled value subtracted from it, after which it's divided by 2, rounding down to negative infinity
// (before accounting for the decimal sign). When decoding, this exponent is converted to a base-10 exponent in
// non-BigDecimal format, which means that it can very well overflow Integer.MAX_VALUE.
// For example, see how <code>new BigDecimal(BigInteger.TEN, Integer.MIN_VALUE)</code> is encoded and decoded.
if (base10NonBigDecimalFormatExp > Integer.MAX_VALUE)
{
// If the base-10 exponent will result in an overflow, some of its powers of 10 need to be absorbed by the
// mantissa. How much exactly? As little as needed, in order to avoid complex BigInteger operations, which
// means exactly as much as to have a scale of -Integer.MAX_VALUE.
int exponentReduction = (int) (base10NonBigDecimalFormatExp - Integer.MAX_VALUE);
mantissa = mantissa.multiply(BigInteger.TEN.pow(exponentReduction));
base10NonBigDecimalFormatExp = Integer.MAX_VALUE;
}
assert base10NonBigDecimalFormatExp >= Integer.MIN_VALUE && base10NonBigDecimalFormatExp <= Integer.MAX_VALUE;
// Here we negate the exponent, as we are not using BigDecimal.scaleByPowerOfTen, where a positive number means
// "multiplying by a positive power of 10", but to BigDecimal's internal scale representation, where a positive
// number means "dividing by a positive power of 10".
byte[] mantissaBytes = mantissa.toByteArray();
V resultBuf = accessor.allocate(4 + mantissaBytes.length);
accessor.putInt(resultBuf, 0, (int) -base10NonBigDecimalFormatExp);
accessor.copyByteArrayTo(mantissaBytes, 0, resultBuf, 4, mantissaBytes.length);
return resultBuf;
}
public ByteBuffer fromString(String source) throws MarshalException
{
// Return an empty ByteBuffer for an empty string.
if (source.isEmpty()) return ByteBufferUtil.EMPTY_BYTE_BUFFER;
BigDecimal decimal;
try
{
decimal = new BigDecimal(source);
}
catch (Exception e)
{
throw new MarshalException(String.format("unable to make BigDecimal from '%s'", source), e);
}
return decompose(decimal);
}
@Override
public Term fromJSONObject(Object parsed) throws MarshalException
{
try
{
return new Constants.Value(fromString(Objects.toString(parsed)));
}
catch (NumberFormatException | MarshalException exc)
{
throw new MarshalException(String.format("Value '%s' is not a valid representation of a decimal value", parsed));
}
}
@Override
public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
{
return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
}
public CQL3Type asCQL3Type()
{
return CQL3Type.Native.DECIMAL;
}
public TypeSerializer<BigDecimal> getSerializer()
{
return DecimalSerializer.instance;
}
@Override
protected int toInt(ByteBuffer value)
{
throw new UnsupportedOperationException();
}
@Override
protected float toFloat(ByteBuffer value)
{
throw new UnsupportedOperationException();
}
@Override
protected long toLong(ByteBuffer value)
{
throw new UnsupportedOperationException();
}
@Override
protected double toDouble(ByteBuffer value)
{
throw new UnsupportedOperationException();
}
@Override
protected BigInteger toBigInteger(ByteBuffer value)
{
throw new UnsupportedOperationException();
}
@Override
protected BigDecimal toBigDecimal(ByteBuffer value)
{
return compose(value);
}
public ByteBuffer add(NumberType<?> leftType, ByteBuffer left, NumberType<?> rightType, ByteBuffer right)
{
return decompose(leftType.toBigDecimal(left).add(rightType.toBigDecimal(right), MAX_PRECISION));
}
public ByteBuffer substract(NumberType<?> leftType, ByteBuffer left, NumberType<?> rightType, ByteBuffer right)
{
return decompose(leftType.toBigDecimal(left).subtract(rightType.toBigDecimal(right), MAX_PRECISION));
}
public ByteBuffer multiply(NumberType<?> leftType, ByteBuffer left, NumberType<?> rightType, ByteBuffer right)
{
return decompose(leftType.toBigDecimal(left).multiply(rightType.toBigDecimal(right), MAX_PRECISION));
}
public ByteBuffer divide(NumberType<?> leftType, ByteBuffer left, NumberType<?> rightType, ByteBuffer right)
{
BigDecimal leftOperand = leftType.toBigDecimal(left);
BigDecimal rightOperand = rightType.toBigDecimal(right);
// Predict position of first significant digit in the quotient.
// Note: it is possible to improve prediction accuracy by comparing first significant digits in operands
// but it requires additional computations so this step is omitted
int quotientFirstDigitPos = (leftOperand.precision() - leftOperand.scale()) - (rightOperand.precision() - rightOperand.scale());
int scale = MIN_SIGNIFICANT_DIGITS - quotientFirstDigitPos;
scale = Math.max(scale, leftOperand.scale());
scale = Math.max(scale, rightOperand.scale());
scale = Math.max(scale, MIN_SCALE);
scale = Math.min(scale, MAX_SCALE);
return decompose(leftOperand.divide(rightOperand, scale, RoundingMode.HALF_UP).stripTrailingZeros());
}
public ByteBuffer mod(NumberType<?> leftType, ByteBuffer left, NumberType<?> rightType, ByteBuffer right)
{
return decompose(leftType.toBigDecimal(left).remainder(rightType.toBigDecimal(right)));
}
public ByteBuffer negate(ByteBuffer input)
{
return decompose(toBigDecimal(input).negate());
}
@Override
public ByteBuffer abs(ByteBuffer input)
{
return decompose(toBigDecimal(input).abs());
}
@Override
public ByteBuffer exp(ByteBuffer input)
{
return decompose(exp(toBigDecimal(input)));
}
protected BigDecimal exp(BigDecimal input)
{
int precision = input.precision();
precision = Math.max(MIN_SIGNIFICANT_DIGITS, precision);
precision = Math.min(MAX_PRECISION.getPrecision(), precision);
return BigDecimalMath.exp(input, new MathContext(precision, RoundingMode.HALF_EVEN));
}
@Override
public ByteBuffer log(ByteBuffer input)
{
return decompose(log(toBigDecimal(input)));
}
protected BigDecimal log(BigDecimal input)
{
if (input.compareTo(BigDecimal.ZERO) <= 0) throw new ArithmeticException("Natural log of number zero or less");
int precision = input.precision();
precision = Math.max(MIN_SIGNIFICANT_DIGITS, precision);
precision = Math.min(MAX_PRECISION.getPrecision(), precision);
return BigDecimalMath.log(input, new MathContext(precision, RoundingMode.HALF_EVEN));
}
@Override
public ByteBuffer log10(ByteBuffer input)
{
return decompose(log10(toBigDecimal(input)));
}
protected BigDecimal log10(BigDecimal input)
{
if (input.compareTo(BigDecimal.ZERO) <= 0) throw new ArithmeticException("Log10 of number zero or less");
int precision = input.precision();
precision = Math.max(MIN_SIGNIFICANT_DIGITS, precision);
precision = Math.min(MAX_PRECISION.getPrecision(), precision);
return BigDecimalMath.log10(input, new MathContext(precision, RoundingMode.HALF_EVEN));
}
@Override
public ByteBuffer round(ByteBuffer input)
{
return DecimalType.instance.decompose(
toBigDecimal(input).setScale(0, RoundingMode.HALF_UP));
}
@Override
public ByteBuffer getMaskedValue()
{
return MASKED_VALUE;
}
}