/** * Copyright (C) 2009-2013 FoundationDB, LLC * * 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.rowdata; import com.foundationdb.server.AkServerUtil; import com.foundationdb.server.types.common.BigDecimalWrapperImpl; import com.foundationdb.util.AkibanAppender; import java.math.BigDecimal; import java.math.RoundingMode; public final class ConversionHelperBigDecimal { // "public" methods (though still only available within-package) /** * Decodes the field from the given RowData into the given StringBuilder. * @param fieldDef the fieldDef whose type is a decimal * @param from the underlying byte array * @param locationAndOffset the byte's location and offset, packed as RowData packs them * @param appender the appender to use * @throws NullPointerException if any arguments are null * @throws ValueSourceException if the string can't be parsed to a BigDecimal */ public static void decodeToString(FieldDef fieldDef, byte[] from, long locationAndOffset, AkibanAppender appender) { final int precision = fieldDef.getTypeParameter1().intValue(); final int scale = fieldDef.getTypeParameter2().intValue(); final int location = (int) locationAndOffset; try { decodeToString(from, location, precision, scale, appender); } catch (NumberFormatException e) { StringBuilder errSb = new StringBuilder(); errSb.append("in field["); errSb.append(fieldDef.getRowDef().getRowDefId()).append('.').append(fieldDef.getFieldIndex()); errSb.append(" decimal("); errSb.append(fieldDef.getTypeParameter1()).append(',').append(fieldDef.getTypeParameter2()); errSb.append(")] 0x"); final int bytesLen = (int) (locationAndOffset >>> 32); AkServerUtil.hex(AkibanAppender.of(errSb), from, location, bytesLen); errSb.append(": ").append( e.getMessage() ); throw new RowDataException(errSb.toString(), e); } } public static int fromObject(FieldDef fieldDef, BigDecimal value, byte[] dest, int offset) { final int declPrec = fieldDef.getTypeParameter1().intValue(); final int declScale = fieldDef.getTypeParameter2().intValue(); return fromObject(value, dest, offset, declPrec, declScale); } public static byte[] bytesFromObject(BigDecimal value, int declPrec, int declScale) { final int declIntSize = calcBinSize(declPrec - declScale); final int declFracSize = calcBinSize(declScale); int size = declIntSize + declFracSize; byte[] results = new byte[size]; fromObject(value, results, 0, declPrec, declScale); return results; } private static int fromObject(BigDecimal value, byte[] dest, int offset, int declPrec, int declScale) { final String from = normalizeToString(value, declPrec, declScale); final int mask = (from.charAt(0) == '-') ? -1 : 0; int fromOff = 0; if (mask != 0) ++fromOff; int signSize = mask == 0 ? 0 : 1; int periodIndex = from.indexOf('.'); final int intCnt; final int fracCnt; if (periodIndex == -1) { intCnt = from.length() - signSize; fracCnt = 0; } else { intCnt = periodIndex - signSize; fracCnt = from.length() - intCnt - 1 - signSize; } final int intFull = intCnt / DECIMAL_DIGIT_PER; final int intPart = intCnt % DECIMAL_DIGIT_PER; final int fracFull = fracCnt / DECIMAL_DIGIT_PER; final int fracPart = fracCnt % DECIMAL_DIGIT_PER; final int intSize = calcBinSize(intCnt); final int declIntSize = calcBinSize(declPrec - declScale); final int declFracSize = calcBinSize(declScale); int toItOff = offset; int toEndOff = offset + declIntSize + declFracSize; for (int i = 0; (intSize + i) < declIntSize; ++i) dest[toItOff++] = (byte) mask; int sum = 0; // Partial integer if (intPart != 0) { for (int i = 0; i < intPart; ++i) { sum *= 10; sum += (from.charAt(fromOff + i) - '0'); } int count = DECIMAL_BYTE_DIGITS[intPart]; packIntegerByWidth(count, sum ^ mask, dest, toItOff); toItOff += count; fromOff += intPart; } // Full integers for (int i = 0; i < intFull; ++i) { sum = 0; for (int j = 0; j < DECIMAL_DIGIT_PER; ++j) { sum *= 10; sum += (from.charAt(fromOff + j) - '0'); } int count = DECIMAL_TYPE_SIZE; packIntegerByWidth(count, sum ^ mask, dest, toItOff); toItOff += count; fromOff += DECIMAL_DIGIT_PER; } // Move past decimal point (or to end) ++fromOff; // Full fractions for (int i = 0; i < fracFull; ++i) { sum = 0; for (int j = 0; j < DECIMAL_DIGIT_PER; ++j) { sum *= 10; sum += (from.charAt(fromOff + j) - '0'); } int count = DECIMAL_TYPE_SIZE; packIntegerByWidth(count, sum ^ mask, dest, toItOff); toItOff += count; fromOff += DECIMAL_DIGIT_PER; } // Fraction left over if (fracPart != 0) { sum = 0; for (int i = 0; i < fracPart; ++i) { sum *= 10; sum += (from.charAt(fromOff + i) - '0'); } int count = DECIMAL_BYTE_DIGITS[fracPart]; packIntegerByWidth(count, sum ^ mask, dest, toItOff); toItOff += count; } while (toItOff < toEndOff) dest[toItOff++] = (byte) mask; dest[offset] ^= 0x80; return declIntSize + declFracSize; } public static String normalizeToString(BigDecimal value, int declPrec, int declScale) { // First, we have to turn the value into one that fits the FieldDef's constraints. int valuePrec = BigDecimalWrapperImpl.sqlPrecision(value); int valueScale = BigDecimalWrapperImpl.sqlScale(value); assert valueScale >= 0 : value; int valueIntDigits = valuePrec - valueScale; int declIntDigits = declPrec - declScale; if (valueIntDigits > declIntDigits) { // A value that does not fit must have more digits, but // they still might be zero. boolean overflow = false; switch (value.signum()) { case 0: break; case +1: overflow = value.compareTo(BigDecimal.ONE.scaleByPowerOfTen(declIntDigits)) >= 0; break; case -1: overflow = value.compareTo(BigDecimal.valueOf(-1).scaleByPowerOfTen(declIntDigits)) <= 0; break; } if (overflow) { // truncate to something like "99.999" StringBuilder sb = new StringBuilder(declPrec+2); // one for minus sign, one for period if (value.signum() < 0) sb.append('-'); for (int i = declPrec; i > 0; --i) { if (i == declScale) sb.append('.'); sb.append('9'); } return sb.toString(); } } String from; if (valueScale != declScale) { // just truncate BigDecimal rounded = value.setScale(declScale, RoundingMode.HALF_UP); from = rounded.toPlainString(); } else { from = value.toPlainString(); } if (declIntDigits == 0) { if (value.signum() < 0) { assert ((from.length() > 3) && (from.charAt(0) == '-') && (from.charAt(1) == '0') && (from.charAt(2) == '.')) : from; from = "-" + from.substring(2); } else { assert ((from.length() > 2) && (from.charAt(0) == '0') && (from.charAt(1) == '.')) : from; from = from.substring(1); } } return from; } // for use within this package (for testing) /** * Decodes bytes representing the decimal value into the given AkibanAppender. * @param from the bytes to parse * @param location the starting offset within the "from" array * @param precision the decimal's precision * @param scale the decimal's scale * @param appender the StringBuilder to write to * @throws NullPointerException if from or appender are null * @throws NumberFormatException if the parse failed; the exception's message will be the String that we * tried to parse */ public static void decodeToString(byte[] from, int location, int precision, int scale, AkibanAppender appender) { final int intCount = precision - scale; final int intFull = intCount / DECIMAL_DIGIT_PER; final int intPartial = intCount % DECIMAL_DIGIT_PER; final int fracFull = scale / DECIMAL_DIGIT_PER; final int fracPartial = scale % DECIMAL_DIGIT_PER; int curOff = location; final int mask = (from[curOff] & 0x80) != 0 ? 0 : -1; // Flip high bit during processing from[curOff] ^= 0x80; if (mask != 0) appender.append('-'); boolean hadOutput = false; if (intPartial != 0) { int count = DECIMAL_BYTE_DIGITS[intPartial]; int x = unpackIntegerByWidth(count, from, curOff) ^ mask; curOff += count; if (x != 0) { hadOutput = true; appender.append(x); } } for (int i = 0; i < intFull; ++i) { int x = unpackIntegerByWidth(DECIMAL_TYPE_SIZE, from, curOff) ^ mask; curOff += DECIMAL_TYPE_SIZE; if (hadOutput) { appender.append(String.format("%09d", x)); } else if (x != 0) { hadOutput = true; appender.append(x); } } if (fracFull + fracPartial > 0) { if (hadOutput) { appender.append('.'); } else { appender.append("0."); } } else if(!hadOutput) appender.append('0'); for (int i = 0; i < fracFull; ++i) { int x = unpackIntegerByWidth(DECIMAL_TYPE_SIZE, from, curOff) ^ mask; curOff += DECIMAL_TYPE_SIZE; appender.append(String.format("%09d", x)); } if (fracPartial != 0) { int count = DECIMAL_BYTE_DIGITS[fracPartial]; int x = unpackIntegerByWidth(count, from, curOff) ^ mask; int width = scale - (fracFull * DECIMAL_DIGIT_PER); appender.append(String.format("%0" + width + "d", x)); } // Restore high bit from[location] ^= 0x80; } // private methods private static int calcBinSize(int digits) { int full = digits / DECIMAL_DIGIT_PER; int partial = digits % DECIMAL_DIGIT_PER; return (full * DECIMAL_TYPE_SIZE) + DECIMAL_BYTE_DIGITS[partial]; } /** * Pack an integer, of a given length, in big endian order into a byte array. * @param len length of integer * @param val value to store in the buffer * @param buf destination array to put bytes in * @param offset position to start at in buf */ private static void packIntegerByWidth(int len, int val, byte[] buf, int offset) { if (len == 1) { buf[offset] = (byte) (val); } else if (len == 2) { buf[offset + 1] = (byte) (val); buf[offset] = (byte) (val >> 8); } else if (len == 3) { buf[offset + 2] = (byte) (val); buf[offset + 1] = (byte) (val >> 8); buf[offset] = (byte) (val >> 16); } else if (len == 4) { buf[offset + 3] = (byte) (val); buf[offset + 2] = (byte) (val >> 8); buf[offset + 1] = (byte) (val >> 16); buf[offset] = (byte) (val >> 24); } else { throw new IllegalArgumentException("Unexpected length " + len); } } /** * Unpack a big endian integer, of a given length, from a byte array. * @param len length of integer to pull out of buffer * @param buf source array to get bytes from * @param offset position to start at in buf * @return The unpacked integer */ private static int unpackIntegerByWidth(int len, byte[] buf, int offset) { if (len == 1) { return buf[offset]; } else if (len == 2) { return (buf[offset] << 24 | (buf[offset+1] & 0xFF) << 16) >> 16; } else if (len == 3) { return (buf[offset] << 24 | (buf[offset+1] & 0xFF) << 16 | (buf[offset+2] & 0xFF) << 8) >> 8; } else if (len == 4) { return buf[offset] << 24 | (buf[offset+1] & 0xFF) << 16 | (buf[offset+2] & 0xFF) << 8 | (buf[offset+3] & 0xFF); } throw new IllegalArgumentException("Unexpected length " + len); } // hidden ctor private ConversionHelperBigDecimal() {} // consts // // DECIMAL related defines as specified at: // http://dev.mysql.com/doc/refman/5.4/en/storage-requirements.html // In short, up to 9 digits get packed into a 4 bytes. // private static final int DECIMAL_TYPE_SIZE = 4; private static final int DECIMAL_DIGIT_PER = 9; private static final int DECIMAL_BYTE_DIGITS[] = { 0, 1, 1, 2, 2, 3, 3, 4, 4, 4 }; }