| /* |
| * 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.phoenix.expression.function; |
| |
| import java.io.DataInput; |
| import java.io.DataOutput; |
| import java.io.IOException; |
| import java.math.BigDecimal; |
| import java.math.RoundingMode; |
| import java.sql.SQLException; |
| import java.util.Collections; |
| import java.util.List; |
| |
| import org.apache.hadoop.hbase.filter.CompareFilter; |
| import org.apache.hadoop.hbase.io.ImmutableBytesWritable; |
| import org.apache.hadoop.io.WritableUtils; |
| import org.apache.phoenix.compile.KeyPart; |
| import org.apache.phoenix.expression.Determinism; |
| import org.apache.phoenix.expression.Expression; |
| import org.apache.phoenix.expression.LiteralExpression; |
| import org.apache.phoenix.parse.FunctionParseNode.Argument; |
| import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunction; |
| import org.apache.phoenix.parse.FunctionParseNode.FunctionClassType; |
| import org.apache.phoenix.query.KeyRange; |
| import org.apache.phoenix.schema.IllegalDataException; |
| import org.apache.phoenix.schema.PColumn; |
| import org.apache.phoenix.schema.PTable; |
| import org.apache.phoenix.schema.tuple.Tuple; |
| import org.apache.phoenix.schema.types.PDataType; |
| import org.apache.phoenix.schema.types.PDecimal; |
| import org.apache.phoenix.schema.types.PInteger; |
| import org.apache.phoenix.schema.types.PLong; |
| import org.apache.phoenix.schema.types.PVarchar; |
| |
| import com.google.common.collect.Lists; |
| |
| /** |
| * |
| * Class encapsulating the process for rounding off a column/literal of type |
| * {@link org.apache.phoenix.schema.types.PDecimal} |
| * |
| * |
| * @since 3.0.0 |
| */ |
| @BuiltInFunction(name = RoundFunction.NAME, |
| args = { |
| @Argument(allowedTypes={PDecimal.class}), |
| @Argument(allowedTypes={PVarchar.class, PInteger.class}, defaultValue = "null", isConstant=true), |
| @Argument(allowedTypes={PInteger.class}, defaultValue="1", isConstant=true) |
| }, |
| classType = FunctionClassType.DERIVED |
| ) |
| public class RoundDecimalExpression extends ScalarFunction { |
| |
| private int scale; |
| |
| /** |
| * Creates a {@link RoundDecimalExpression} with rounding scale given by @param scale. |
| * |
| */ |
| public static Expression create(Expression expr, int scale) throws SQLException { |
| if (expr.getDataType().isCoercibleTo(PLong.INSTANCE)) { |
| return expr; |
| } |
| Expression scaleExpr = LiteralExpression.newConstant(scale, PInteger.INSTANCE, Determinism.ALWAYS); |
| List<Expression> expressions = Lists.newArrayList(expr, scaleExpr); |
| return new RoundDecimalExpression(expressions); |
| } |
| |
| /** |
| * Creates a {@link RoundDecimalExpression} with a default scale of 0 used for rounding. |
| * |
| */ |
| public static Expression create(Expression expr) throws SQLException { |
| return create(expr, 0); |
| } |
| |
| public static Expression create(List<Expression> exprs) throws SQLException { |
| Expression expr = exprs.get(0); |
| if (expr.getDataType().isCoercibleTo(PLong.INSTANCE)) { |
| return expr; |
| } |
| if (exprs.size() == 1) { |
| Expression scaleExpr = LiteralExpression.newConstant(0, PInteger.INSTANCE, Determinism.ALWAYS); |
| exprs = Lists.newArrayList(expr, scaleExpr); |
| } |
| return new RoundDecimalExpression(exprs); |
| } |
| |
| public RoundDecimalExpression() {} |
| |
| public RoundDecimalExpression(List<Expression> children) { |
| super(children); |
| LiteralExpression scaleChild = (LiteralExpression)children.get(1); |
| PDataType scaleType = scaleChild.getDataType(); |
| Object scaleValue = scaleChild.getValue(); |
| if(scaleValue != null) { |
| if (scaleType.isCoercibleTo(PInteger.INSTANCE, scaleValue)) { |
| int scale = (Integer) PInteger.INSTANCE.toObject(scaleValue, scaleType); |
| if (scale <= PDataType.MAX_PRECISION) { |
| this.scale = scale; |
| return; |
| } |
| } |
| throw new IllegalDataException("Invalid second argument for scale: " + scaleValue + ". The scale must be between 0 and " + PDataType.MAX_PRECISION + " inclusive."); |
| } |
| } |
| |
| @Override |
| public boolean evaluate(Tuple tuple, ImmutableBytesWritable ptr) { |
| Expression childExpr = children.get(0); |
| if(childExpr.evaluate(tuple, ptr)) { |
| if (ptr.getLength()==0) { |
| return true; |
| } |
| BigDecimal value = (BigDecimal) PDecimal.INSTANCE.toObject(ptr, childExpr.getDataType(), childExpr.getSortOrder()); |
| BigDecimal scaledValue = value.setScale(scale, getRoundingMode()); |
| ptr.set(PDecimal.INSTANCE.toBytes(scaledValue)); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public PDataType getDataType() { |
| return PDecimal.INSTANCE; |
| } |
| |
| protected RoundingMode getRoundingMode() { |
| return RoundingMode.HALF_UP; |
| } |
| |
| protected final int getRoundingScale() { |
| return scale; |
| } |
| |
| @Override |
| public void readFields(DataInput input) throws IOException { |
| super.readFields(input); |
| scale = WritableUtils.readVInt(input); |
| } |
| |
| @Override |
| public void write(DataOutput output) throws IOException { |
| super.write(output); |
| WritableUtils.writeVInt(output, scale); |
| } |
| |
| @Override |
| public String getName() { |
| return RoundFunction.NAME; |
| } |
| |
| @Override |
| public OrderPreserving preservesOrder() { |
| return OrderPreserving.YES; |
| } |
| |
| @Override |
| public int getKeyFormationTraversalIndex() { |
| return 0; |
| } |
| |
| @Override |
| public KeyPart newKeyPart(final KeyPart childPart) { |
| return new KeyPart() { |
| private final List<Expression> extractNodes = Collections.<Expression>singletonList(RoundDecimalExpression.this); |
| |
| @Override |
| public PColumn getColumn() { |
| return childPart.getColumn(); |
| } |
| |
| @Override |
| public List<Expression> getExtractNodes() { |
| return extractNodes; |
| } |
| |
| @Override |
| public KeyRange getKeyRange(CompareFilter.CompareOp op, Expression rhs) { |
| final BigDecimal rhsDecimal = (BigDecimal) PDecimal.INSTANCE.toObject(evaluateExpression(rhs)); |
| |
| // equality requires an exact match. if rounding would cut off more precision |
| // than needed for a match, it's impossible for there to be any matches |
| if(op == CompareFilter.CompareOp.EQUAL && !hasEnoughPrecisionToProduce(rhsDecimal)) { |
| return KeyRange.EMPTY_RANGE; |
| } |
| |
| // if the decimal needs to be rounded, round it such that the given |
| // operator will still be valid |
| BigDecimal roundedDecimal = roundAndPreserveOperator(rhsDecimal, op); |
| |
| // the range of big decimals that could be rounded to produce the rounded result |
| // alternatively, the "rounding bucket" that this decimal falls into |
| final KeyRange equalityRange = getInputRangeProducing(roundedDecimal); |
| boolean lowerInclusive = equalityRange.isLowerInclusive(); |
| boolean upperInclusive = equalityRange.isUpperInclusive(); |
| byte[] lowerRange = KeyRange.UNBOUND; |
| byte[] upperRange = KeyRange.UNBOUND; |
| |
| switch(op) { |
| case EQUAL: |
| return equalityRange; |
| case GREATER: |
| // from the equality range and up, NOT including the equality range |
| lowerRange = equalityRange.getUpperRange(); |
| lowerInclusive = !equalityRange.isUpperInclusive(); |
| break; |
| case GREATER_OR_EQUAL: |
| // from the equality range and up, including the equality range |
| lowerRange = equalityRange.getLowerRange(); |
| break; |
| case LESS: |
| // from the equality range and down, NOT including the equality range |
| upperRange = equalityRange.getLowerRange(); |
| upperInclusive = !equalityRange.isLowerInclusive(); |
| break; |
| case LESS_OR_EQUAL: |
| // from the equality range and down, including the equality range |
| upperRange = equalityRange.getUpperRange(); |
| break; |
| default: |
| throw new AssertionError("Invalid CompareOp: " + op); |
| } |
| |
| return KeyRange.getKeyRange(lowerRange, lowerInclusive, upperRange, upperInclusive); |
| } |
| |
| /** |
| * Produces a the given decimal rounded to this rounding expression's scale. If the |
| * decimal requires more scale precision to produce than this expression has, as in |
| * ROUND(?, 2) > 2.0098974, it ensures that the decimal is rounded such that the |
| * given operator will still produce correct results. |
| * @param decimal the decimal to round with this expression's scale |
| * @param op the operator to preserve comparison with in the event of lost precision |
| * @return the rounded decimal |
| */ |
| private BigDecimal roundAndPreserveOperator(BigDecimal decimal, CompareFilter.CompareOp op) { |
| final BigDecimal rounded = roundToScale(decimal); |
| |
| // if we lost information, make sure that the rounding didn't break the operator |
| if(!hasEnoughPrecisionToProduce(decimal)) { |
| switch(op) { |
| case GREATER_OR_EQUAL: |
| // e.g. 'ROUND(dec, 2) >= 2.013' would be converted to |
| // 'ROUND(dec, 2) >= 2.01' but should be 'ROUND(dec, 2) >= 2.02' |
| if(decimal.compareTo(rounded) > 0) { |
| return stepNextInScale(rounded); |
| } |
| break; |
| case GREATER: |
| // e.g. 'ROUND(dec, 2) > 2.017' would be converted to |
| // 'ROUND(dec, 2) > 2.02' but should be 'ROUND(dec, 2) > 2.01' |
| if(decimal.compareTo(rounded) < 0) { |
| return stepPrevInScale(rounded); |
| } |
| break; |
| case LESS_OR_EQUAL: |
| // e.g. 'ROUND(dec, 2) < 2.017' would be converted to |
| // 'ROUND(dec, 2) < 2.02' but should be 'ROUND(dec, 2) < 2.01' |
| if(decimal.compareTo(rounded) < 0) { |
| return stepPrevInScale(rounded); |
| } |
| break; |
| case LESS: |
| // e.g. 'ROUND(dec, 2) <= 2.013' would be converted to |
| // 'ROUND(dec, 2) <= 2.01' but should be 'ROUND(dec, 2) <= 2.02' |
| if(decimal.compareTo(rounded) > 0) { |
| return stepNextInScale(rounded); |
| } |
| break; |
| } |
| } |
| |
| // otherwise, rounding has not affected the operator, so return normally |
| return rounded; |
| } |
| |
| @Override |
| public PTable getTable() { |
| return childPart.getTable(); |
| } |
| }; |
| } |
| |
| /** |
| * Finds the Decimal KeyRange that will produce the given result when fed into this |
| * rounding expression. For example, a ROUND expression with scale 2 will produce the |
| * result "2.05" with any decimal in the range [2.045, 2.0545). |
| * The result must be pre-rounded to within this rounding expression's scale. |
| * @param result the result to find an input range for. Must be producable. |
| * @return a KeyRange of DECIMAL keys that can be rounded by this expression to produce result |
| * @throws IllegalArgumentException if the result has more scale than this expression can produce |
| */ |
| protected KeyRange getInputRangeProducing(BigDecimal result) { |
| if(!hasEnoughPrecisionToProduce(result)) { |
| throw new IllegalArgumentException("Cannot produce input range for decimal " + result |
| + ", not enough precision with scale " + getRoundingScale()); |
| } |
| byte[] lowerRange = PDecimal.INSTANCE.toBytes(halfStepPrevInScale(result)); |
| byte[] upperRange = PDecimal.INSTANCE.toBytes(halfStepNextInScale(result)); |
| // inclusiveness changes depending on sign |
| // e.g. -0.5 rounds "up" to -1 even though it is the lower boundary |
| boolean lowerInclusive = result.signum() > 0; |
| boolean upperInclusive = result.signum() < 0; |
| return KeyRange.getKeyRange(lowerRange, lowerInclusive, upperRange, upperInclusive); |
| } |
| |
| /** |
| * Determines whether this rounding expression's scale has enough precision to produce the |
| * minimum precision for the input decimal. In other words, determines whether the given |
| * decimal can be rounded to this scale without losing ordering information. |
| * For example, an expression with a scale of 2 has enough precision to produce "2.3", "2.71" |
| * and "2.100000", but does not have enough precision to produce "2.001" |
| * @param result the decimal to round |
| * @return true if the given decimal can be precisely matched by this rounding expression |
| */ |
| protected final boolean hasEnoughPrecisionToProduce(BigDecimal result) { |
| // use compareTo so that 2.0 and 2.00 are treated as "equal" |
| return roundToScale(result).compareTo(result) == 0; |
| } |
| |
| /** |
| * Returns the given decimal rounded to this rounding expression's scale. |
| * For example, with scale 2 the decimal "2.453" would be rounded to either 2.45 or |
| * 2.46 depending on the rounding mode, while "2.38" and "2.7" would be unchanged. |
| * @param decimal the decimal to round |
| * @return the rounded result decimal |
| */ |
| protected final BigDecimal roundToScale(BigDecimal decimal) { |
| return decimal.setScale(getRoundingScale(), getRoundingMode()); |
| } |
| |
| /** |
| * Produces a value half of a "step" back in this expression's rounding scale. |
| * For example with a scale of 2, "2.5" would be stepped back to "2.495". |
| */ |
| protected final BigDecimal halfStepPrevInScale(BigDecimal decimal) { |
| BigDecimal step = BigDecimal.ONE.scaleByPowerOfTen(-getRoundingScale()); |
| BigDecimal halfStep = step.divide(BigDecimal.valueOf(2)); |
| return decimal.subtract(halfStep); |
| } |
| |
| /** |
| * Produces a value half of a "step" forward in this expression's rounding scale. |
| * For example with a scale of 2, "2.5" would be stepped forward to "2.505". |
| */ |
| protected final BigDecimal halfStepNextInScale(BigDecimal decimal) { |
| BigDecimal step = BigDecimal.ONE.scaleByPowerOfTen(-getRoundingScale()); |
| BigDecimal halfStep = step.divide(BigDecimal.valueOf(2)); |
| return decimal.add(halfStep); |
| } |
| |
| /** |
| * Produces a value one "step" back in this expression's rounding scale. |
| * For example with a scale of 2, "2.5" would be stepped back to "2.49". |
| */ |
| protected final BigDecimal stepPrevInScale(BigDecimal decimal) { |
| BigDecimal step = BigDecimal.ONE.scaleByPowerOfTen(-getRoundingScale()); |
| return decimal.subtract(step); |
| } |
| |
| /** |
| * Produces a value one "step" forward in this expression's rounding scale. |
| * For example with a scale of 2, "2.5" would be stepped forward to "2.51". |
| */ |
| protected final BigDecimal stepNextInScale(BigDecimal decimal) { |
| BigDecimal step = BigDecimal.ONE.scaleByPowerOfTen(-getRoundingScale()); |
| return decimal.add(step); |
| } |
| |
| } |