/* * Licensed 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 com.facebook.presto.spi.type; import com.facebook.presto.spi.PrestoException; import com.facebook.presto.spi.block.BlockBuilder; import io.airlift.slice.Slice; import java.math.BigDecimal; import java.math.BigInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.facebook.presto.spi.StandardErrorCode.NUMERIC_VALUE_OUT_OF_RANGE; import static com.facebook.presto.spi.type.DecimalType.createDecimalType; import static com.facebook.presto.spi.type.UnscaledDecimal128Arithmetic.toUnscaledString; import static com.facebook.presto.spi.type.UnscaledDecimal128Arithmetic.unscaledDecimal; import static com.facebook.presto.spi.type.UnscaledDecimal128Arithmetic.unscaledDecimalToBigInteger; import static java.lang.Math.abs; import static java.lang.Math.pow; import static java.lang.Math.round; import static java.lang.String.format; import static java.math.BigInteger.TEN; import static java.math.RoundingMode.UNNECESSARY; public final class Decimals { private Decimals() {} public static final int MAX_PRECISION = 38; public static final int MAX_SHORT_PRECISION = 18; public static final BigInteger MAX_DECIMAL_UNSCALED_VALUE = new BigInteger( // repeat digit '9' MAX_PRECISION times new String(new char[MAX_PRECISION]).replace("\0", "9")); public static final BigInteger MIN_DECIMAL_UNSCALED_VALUE = MAX_DECIMAL_UNSCALED_VALUE.negate(); private static final Pattern DECIMAL_PATTERN = Pattern.compile("(\\+?|-?)((0*)(\\d*))(\\.(\\d+))?"); private static final int LONG_POWERS_OF_TEN_TABLE_LENGTH = 19; private static final int BIG_INTEGER_POWERS_OF_TEN_TABLE_LENGTH = 100; private static final long[] LONG_POWERS_OF_TEN = new long[LONG_POWERS_OF_TEN_TABLE_LENGTH]; private static final BigInteger[] BIG_INTEGER_POWERS_OF_TEN = new BigInteger[BIG_INTEGER_POWERS_OF_TEN_TABLE_LENGTH]; static { for (int i = 0; i < LONG_POWERS_OF_TEN.length; ++i) { LONG_POWERS_OF_TEN[i] = round(pow(10, i)); } for (int i = 0; i < BIG_INTEGER_POWERS_OF_TEN.length; ++i) { BIG_INTEGER_POWERS_OF_TEN[i] = TEN.pow(i); } } public static long longTenToNth(int n) { return LONG_POWERS_OF_TEN[n]; } public static BigInteger bigIntegerTenToNth(int n) { return BIG_INTEGER_POWERS_OF_TEN[n]; } public static DecimalParseResult parse(String stringValue) { return parse(stringValue, false); } // visible for testing public static DecimalParseResult parseIncludeLeadingZerosInPrecision(String stringValue) { return parse(stringValue, true); } private static DecimalParseResult parse(String stringValue, boolean includeLeadingZerosInPrecision) { Matcher matcher = DECIMAL_PATTERN.matcher(stringValue); if (!matcher.matches()) { throw new IllegalArgumentException("invalid decimal value '" + stringValue + "'"); } String sign = getMatcherGroup(matcher, 1); if (sign.isEmpty()) { sign = "+"; } String leadingZeros = getMatcherGroup(matcher, 3); String integralPart = getMatcherGroup(matcher, 4); String fractionalPart = getMatcherGroup(matcher, 6); int scale = fractionalPart.length(); int precision; if (includeLeadingZerosInPrecision) { precision = leadingZeros.length() + integralPart.length() + scale; } else { precision = integralPart.length() + scale; if (precision == 0) { precision = 1; } } String unscaledValue = sign + leadingZeros + integralPart + fractionalPart; Object value; if (precision <= MAX_SHORT_PRECISION) { value = Long.parseLong(unscaledValue); } else { value = encodeUnscaledValue(new BigInteger(unscaledValue)); } return new DecimalParseResult(value, createDecimalType(precision, scale)); } private static String getMatcherGroup(Matcher matcher, int group) { String groupValue = matcher.group(group); if (groupValue == null) { groupValue = ""; } return groupValue; } @SuppressWarnings("NumericCastThatLosesPrecision") public static Slice encodeUnscaledValue(BigInteger unscaledValue) { return unscaledDecimal(unscaledValue); } public static Slice encodeUnscaledValue(long unscaledValue) { return unscaledDecimal(unscaledValue); } public static Slice encodeScaledValue(BigDecimal value) { return encodeUnscaledValue(value.unscaledValue()); } public static BigInteger decodeUnscaledValue(Slice valueSlice) { return unscaledDecimalToBigInteger(valueSlice); } public static String toString(long unscaledValue, int scale) { return toString(Long.toString(unscaledValue), scale); } public static String toString(Slice unscaledValue, int scale) { return toString(toUnscaledString(unscaledValue), scale); } public static String toString(BigInteger unscaledValue, int scale) { return toString(unscaledValue.toString(), scale); } private static String toString(String unscaledValueString, int scale) { StringBuilder resultBuilder = new StringBuilder(); // add sign if (unscaledValueString.startsWith("-")) { resultBuilder.append("-"); unscaledValueString = unscaledValueString.substring(1); } // integral part if (unscaledValueString.length() <= scale) { resultBuilder.append("0"); } else { resultBuilder.append(unscaledValueString.substring(0, unscaledValueString.length() - scale)); } // fractional part if (scale > 0) { resultBuilder.append("."); if (unscaledValueString.length() < scale) { // prepend zeros to fractional part if unscaled value length is shorter than scale for (int i = 0; i < scale - unscaledValueString.length(); ++i) { resultBuilder.append("0"); } resultBuilder.append(unscaledValueString); } else { // otherwise just use scale last digits of unscaled value resultBuilder.append(unscaledValueString.substring(unscaledValueString.length() - scale)); } } return resultBuilder.toString(); } public static boolean overflows(long value, int precision) { if (precision > MAX_SHORT_PRECISION) { throw new IllegalArgumentException("expected precision to be less than " + MAX_SHORT_PRECISION); } return abs(value) >= longTenToNth(precision); } public static boolean overflows(BigInteger value, int precision) { return value.abs().compareTo(bigIntegerTenToNth(precision)) >= 0; } public static boolean overflows(BigInteger value) { return value.compareTo(MAX_DECIMAL_UNSCALED_VALUE) > 0 || value.compareTo(MIN_DECIMAL_UNSCALED_VALUE) < 0; } public static boolean overflows(BigDecimal value, long precision) { return value.precision() > precision; } public static void checkOverflow(BigInteger value) { if (overflows(value)) { throw new PrestoException(NUMERIC_VALUE_OUT_OF_RANGE, format("Value is out of range: %s", value.toString())); } } public static void writeBigDecimal(DecimalType decimalType, BlockBuilder blockBuilder, BigDecimal value) { decimalType.writeSlice(blockBuilder, encodeScaledValue(value)); } public static BigDecimal rescale(BigDecimal value, DecimalType type) { value = value.setScale(type.getScale(), UNNECESSARY); if (value.precision() > type.getPrecision()) { throw new IllegalArgumentException("decimal precision larger than column precision"); } return value; } public static void writeShortDecimal(BlockBuilder blockBuilder, long value) { blockBuilder.writeLong(value).closeEntry(); } public static long rescale(long value, int fromScale, int toScale) { if (toScale < fromScale) { throw new IllegalArgumentException("target scale must be larger than source scale"); } return value * longTenToNth(toScale - fromScale); } public static BigInteger rescale(BigInteger value, int fromScale, int toScale) { if (toScale < fromScale) { throw new IllegalArgumentException("target scale must be larger than source scale"); } return value.multiply(bigIntegerTenToNth(toScale - fromScale)); } public static boolean isShortDecimal(Type type) { return type instanceof ShortDecimalType; } public static boolean isLongDecimal(Type type) { return type instanceof LongDecimalType; } }