/** * 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.types.mcompat.mcasts; import com.foundationdb.server.types.TExecutionContext; import com.foundationdb.server.types.common.BigDecimalWrapper; import com.foundationdb.server.types.common.BigDecimalWrapperImpl; import com.foundationdb.server.types.common.types.DecimalAttribute; import com.foundationdb.server.types.common.types.TBigDecimal; import com.foundationdb.server.types.value.ValueTarget; import com.google.common.primitives.UnsignedLongs; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class CastUtils { public static long round (long max, long min, double val, TExecutionContext context) { long rounded = Math.round(val); if (Double.compare(rounded, val) != 0) context.reportTruncate(Double.toString(val), Long.toString(rounded)); return getInRange(max, min, rounded, context); } public static long getInRange (long max, long min, long val, TExecutionContext context) { if (val > max) { context.reportTruncate(Long.toString(val), Long.toString(max)); return max; } else if (val < min) { context.reportTruncate(Long.toString(val), Long.toString(min)); return min; } else return val; } public static long getInRange (long max, long min, long val, long defaultVal, TExecutionContext context) { if (val > max) { context.reportTruncate(Long.toString(val), Long.toString(defaultVal)); return defaultVal; } else if (val < min) { context.reportTruncate(Long.toString(val), Long.toString(defaultVal)); return defaultVal; } else return val; } public static String getNum(int scale, int precision) { assert precision >= scale : "precision has to be >= scale"; char val[] = new char[precision + 1]; Arrays.fill(val, '9'); val[precision - scale] = '.'; return new String(val); } public static void doCastDecimal(TExecutionContext context, BigDecimalWrapper num, ValueTarget out) { int pre = num.getPrecision(); int scale = num.getScale(); int expectedPre = context.outputType().attribute(DecimalAttribute.PRECISION); int expectedScale = context.outputType().attribute(DecimalAttribute.SCALE); BigDecimalWrapper meta[] = (BigDecimalWrapper[]) context.outputType().getMetaData(); if (meta == null) { // compute the max value: meta = new BigDecimalWrapper[2]; meta[TBigDecimal.MAX_INDEX] = new BigDecimalWrapperImpl(getNum(expectedScale, expectedPre)); meta[TBigDecimal.MIN_INDEX] = new BigDecimalWrapperImpl(meta[TBigDecimal.MAX_INDEX].asBigDecimal().negate()); context.outputType().setMetaData(meta); } if (num.compareTo(meta[TBigDecimal.MAX_INDEX]) >= 0) out.putObject(meta[TBigDecimal.MAX_INDEX]); else if (num.compareTo(meta[TBigDecimal.MIN_INDEX]) <= 0) out.putObject(meta[TBigDecimal.MIN_INDEX]); else if (scale != expectedScale) // check the sacle out.putObject(num.round(expectedScale)); else // else put the original value out.putObject(num); } public static BigDecimalWrapperImpl parseDecimalString (String st, TExecutionContext context) { Matcher m = DOUBLE_PATTERN.matcher(st); m.lookingAt(); String truncated = st.substring(0, m.end()); if (!truncated.equals(st)) context.reportTruncate(st, truncated); BigDecimalWrapperImpl ret = BigDecimalWrapperImpl.ZERO; try { ret = new BigDecimalWrapperImpl(truncated); } catch (NumberFormatException e) { context.reportBadValue(e.getMessage()); } return ret; } /** * Parse the st for a double value * MySQL compat in that illegal digits will be truncated and won't cause * NumberFormatException * * @param st * @param context * @return */ public static double parseDoubleString(String st, TExecutionContext context) { double ret = 0; Matcher m = DOUBLE_PATTERN.matcher(st); if (m.lookingAt()) { String truncated = st.substring(0, m.end()); if (!truncated.equals(st)) { context.reportTruncate(st, truncated); } try { ret = Double.parseDouble(truncated); } catch (NumberFormatException e) { context.reportBadValue(e.getMessage()); } } else context.reportBadValue(st); return ret; } public static long parseInRange(String st, long max, long min, TExecutionContext context) { Object truncated; // first attempt try { return CastUtils.getInRange(max, min, Long.parseLong(st), context); } catch (NumberFormatException e) // This could be an overflow, but there is no way to know { truncated = CastUtils.truncateNonDigits(st, context); } // second attemp if (truncated instanceof String) { String truncatedStr = (String)truncated; try { return CastUtils.getInRange(max, min, Long.parseLong(truncatedStr), context); } catch (NumberFormatException e) // overflow { context.reportOverflow(e.getMessage()); // check wether the number is too big or too small char first = truncatedStr.charAt(0); if (first == '-') return getInRange(max, min, Long.MIN_VALUE, context); else return getInRange(max, min, Long.MAX_VALUE, context); } } else // must be a BigDecimal object { BigDecimal num = (BigDecimal)truncated; // check overflow if (num.compareTo(MAX_LONG) > 0) { context.reportTruncate(st, Long.toString(max)); return max; } else if (num.compareTo(MIN_LONG) < 0) { context.reportTruncate(st, Long.toString(min)); return min; } try { return getInRange(max, min, num.longValueExact(), context); } catch (ArithmeticException e) // has non-zero fractional parts { long ret = num.setScale(0, RoundingMode.HALF_UP).longValue(); context.reportTruncate(st, Long.toString(ret)); return getInRange(max, min, ret, context); } } } public static long parseUnsignedLong(String st, TExecutionContext context) { Object truncated = CastUtils.truncateNonDigits(st, context); if (truncated instanceof String) st = (String)truncated; else st = CastUtils.truncateNonDigitPlainString(((BigDecimal)truncated).toPlainString(), context); long value; try { value = UnsignedLongs.parseUnsignedLong(st); } catch (NumberFormatException e) { // overflow error context.reportOverflow(e.getMessage()); // check wether the value is too big or too small if (st.charAt(0) == '-') value = 0; else value = UnsignedLongs.MAX_VALUE; } return value; } /** * Truncate non-digits part of a string that represents an integer. This also rounds the string. The rounding * is here (as opposed to at the call site) for ease of use, especially when parsing this as an unsigned long. * If the input string doesn't contain a valid integer, returns "0". * @param st the string to parse * @return a non-empty, non-null string which contains all digits, with a possible leading '-' */ public static Object truncateNonDigits(String st, TExecutionContext context) { if (st.isEmpty()) return "0"; Matcher m = DOUBLE_PATTERN.matcher(st); String truncated; int last; if (m.lookingAt() && !(truncated = st.substring(0, last = m.end())).isEmpty()) { --last; // m.end() returns an offset from the beginning, not an index if (truncated.charAt(last) != st.charAt(last)) context.reportTruncate(st, truncated); // If the exponent exists, use BigDecimal if ( m.group(EXP_PART) != null) return new BigDecimal(st); // otherwise, truncate non-digit chars return truncateNonDigitPlainString(truncated, context); } else // not a valid numeric string { context.reportBadValue(st); return "0"; } } public static String truncateNonDigitPlainString(String st, TExecutionContext context) { final int max = st.length(); if (max == 0) return "0"; final boolean neg; final int firstIndex = ((neg = st.charAt(0) == '-') || st.charAt(0) == '+') ? 1 : 0; boolean needsRoundingUp = false; // whether the number ends in "\.[5-9]" int truncatedLength; for (truncatedLength = firstIndex; truncatedLength < max; ++truncatedLength) { char c = st.charAt(truncatedLength); if (!Character.isDigit(c)) { needsRoundingUp = (c == '.') && isFiveOrHigher(st, truncatedLength + 1); break; } } if (truncatedLength == firstIndex && !needsRoundingUp) return "0"; // no digits String ret; if (needsRoundingUp) { StringBuilder sb = new StringBuilder(truncatedLength + 2); // 1 for '-', 1 for the carry digit // Go right to left on the string, not counting the leading '-'. Assume every char is a digit. If you see // a '9', set it to 0 and continue the loop. Otherwise, set needsRoundingUp to false and break. // Once the loop is done, if needsRoundingUp is still true, append a 1. Finally, append the '-' if we need // it, reverse the string, and return it. for (int i = truncatedLength - 1; i >= firstIndex; --i) { char c = st.charAt(i); assert (c >= '0') && (c <= '9') : c + " at index " + i + " of: " + st; if (needsRoundingUp && c == '9') { sb.append('0'); } else { if (needsRoundingUp) ++c; sb.append(c); needsRoundingUp = false; } } if (needsRoundingUp) sb.append('1'); if (neg) sb.append('-'); sb.reverse(); ret = sb.toString(); } else { ret = st.substring(0, truncatedLength); } return ret; } private static boolean isFiveOrHigher(String string, int index) { if (index >= string.length()) return false; char c = string.charAt(index); return (c >= '5') && (c <= '9'); } /** Clamp {@code raw} to the range of [0,255] representing 0000 or an offset from 1900. */ public static short adjustYear(long raw, TExecutionContext context) { // Too small, too big or invalid 2 digit/out of 4 digit range if(raw < 0 || raw > 2155 || (raw >= 100 && raw <= 1900)) { context.reportTruncate(String.valueOf(raw), "0000"); return 0; } if(raw == 0) { return 0; } // 2000 + raw if(raw <= 69) { return (short)(100 + raw); } // 1900 + raw if(raw <= 99) { return (short)raw; } // 1901-2155 return (short)(raw - 1900); } private static final Pattern DOUBLE_PATTERN = Pattern.compile("([-+]?\\d*)(\\.?\\d+)?([eE][-+]?\\d+)?"); private static int WHOLE_PART = 1; private static int FLOAT_PART = 2; private static int EXP_PART = 3; public static final long MAX_TINYINT = 127; public static final long MAX_UNSIGNED_TINYINT = 255; public static final long MIN_TINYINT = -128; public static final long MAX_SMALLINT = 32767; public static final long MAX_UNSIGNED_SMALLINT = 65535; public static final long MIN_SMALLINT = -32768; public static final long MAX_MEDINT = 8388607; public static final long MAX_UNSIGNED_MEDINT = 16777215; public static final long MIN_MEDINT = -8388608; public static final long MAX_INT = 2147483647; public static final long MAX_UNSIGNED_INT = 4294967295L; public static final long MIN_INT = -2147483648; public static final long MAX_BIGINT = 9223372036854775807L; public static final long MIN_BIGINT = -9223372036854775808L; private static final BigDecimal MAX_LONG = BigDecimal.valueOf(Long.MAX_VALUE); private static final BigDecimal MIN_LONG = BigDecimal.valueOf(Long.MIN_VALUE); }