Skip to content

Commit

Permalink
Merge pull request eXist-db#4400 from evolvedbinary/feature/fn-round,#2
Browse files Browse the repository at this point in the history
Implement fn:round#2
  • Loading branch information
dizzzz authored May 24, 2022
2 parents 6f8e3bf + 5359a29 commit 529955b
Show file tree
Hide file tree
Showing 10 changed files with 439 additions and 155 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,10 @@ public class FnModule extends AbstractInternalModule {
new FunctionDef(FunResolveURI.signatures[1], FunResolveURI.class),
new FunctionDef(FunRoot.signatures[0], FunRoot.class),
new FunctionDef(FunRoot.signatures[1], FunRoot.class),
new FunctionDef(FunRound.signature, FunRound.class),
new FunctionDef(FunRoundHalfToEven.signatures[0], FunRoundHalfToEven.class),
new FunctionDef(FunRoundHalfToEven.signatures[1], FunRoundHalfToEven.class),
new FunctionDef(FunRound.FN_ROUND_SIGNATURES[0], FunRound.class),
new FunctionDef(FunRound.FN_ROUND_SIGNATURES[1], FunRound.class),
new FunctionDef(FunRoundHalfToEven.FN_ROUND_HALF_TO_EVEN_SIGNATURES[0], FunRoundHalfToEven.class),
new FunctionDef(FunRoundHalfToEven.FN_ROUND_HALF_TO_EVEN_SIGNATURES[1], FunRoundHalfToEven.class),
new FunctionDef(FunSerialize.signatures[0], FunSerialize.class),
new FunctionDef(FunSerialize.signatures[1], FunSerialize.class),
new FunctionDef(FunStartsWith.signatures[0], FunStartsWith.class),
Expand Down
128 changes: 58 additions & 70 deletions exist-core/src/main/java/org/exist/xquery/functions/fn/FunRound.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,89 +21,77 @@
*/
package org.exist.xquery.functions.fn;

import org.exist.dom.QName;
import org.exist.xquery.Cardinality;
import org.exist.xquery.Dependency;
import org.exist.xquery.Function;
import org.exist.xquery.FunctionSignature;
import org.exist.xquery.Profiler;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.value.FunctionParameterSequenceType;
import org.exist.xquery.value.FunctionReturnSequenceType;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.NumericValue;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceType;
import org.exist.xquery.value.Type;
import org.exist.xquery.value.*;

public class FunRound extends Function {
import java.math.RoundingMode;

public final static FunctionSignature signature =
new FunctionSignature(
new QName("round", Function.BUILTIN_FUNCTION_NS),
"Returns the number with no fractional part that is closest " +
"to the argument $arg. If there are two such numbers, then the one " +
"that is closest to positive infinity is returned. If type of " +
"$arg is one of the four numeric types xs:float, xs:double, " +
"xs:decimal or xs:integer the type of the result is the same " +
"as the type of $arg. If the type of $arg is a type derived " +
"from one of the numeric types, the result is an instance of " +
"the base numeric type.\n\n" +
"For xs:float and xs:double arguments, if the argument is " +
"positive infinity, then positive infinity is returned. " +
"If the argument is negative infinity, then negative infinity " +
"is returned. If the argument is positive zero, then positive " +
"zero is returned. If the argument is negative zero, then " +
"negative zero is returned. If the argument is less than zero, " +
"but greater than or equal to -0.5, then negative zero is returned. " +
"In the cases where positive zero or negative zero is returned, " +
"negative zero or positive zero may be returned as " +
"[XML Schema Part 2: Datatypes Second Edition] does not " +
"distinguish between the values positive zero and negative zero.",
new SequenceType[] { new FunctionParameterSequenceType("arg", Type.NUMBER, Cardinality.ZERO_OR_ONE, "The input number") },
new FunctionReturnSequenceType(Type.NUMBER, Cardinality.ZERO_OR_ONE, "the rounded value")
);

public FunRound(XQueryContext context) {
import static org.exist.xquery.FunctionDSL.optParam;
import static org.exist.xquery.functions.fn.FnModule.functionSignature;

/**
* Implement fn:round() function
*
* Shares a base class and evaluator with {@link FunRoundHalfToEven}
* They differ only in the rounding mode used.
*/
public class FunRound extends FunRoundBase {

private static final String FN_NAME = "round";
private static final String description = "The function returns the nearest (that is, numerically closest) " +
"value to $arg that is a multiple of ten to the power of minus $precision. " +
"If two such values are equally near (for example, if the fractional part in $arg is exactly .5), " +
"the function returns the one that is closest to positive infinity. " +
"For the four types xs:float, xs:double, xs:decimal and xs:integer, " +
"it is guaranteed that if the type of $arg is an instance of type T " +
"then the result will also be an instance of T. " +
"The result may also be an instance of a type derived from one of these four by restriction. " +
"For example, if $arg is an instance of xs:decimal and $precision is less than one, " +
"then the result may be an instance of xs:integer. " +
"The single-argument version of this function produces the same result " +
"as the two-argument version with $precision=0 (that is, it rounds to a whole number). " +
"When $arg is of type xs:float and xs:double: " +
"If $arg is NaN, positive or negative zero, or positive or negative infinity, " +
"then the result is the same as the argument. " +
"For other values, the argument is cast to xs:decimal " +
"using an implementation of xs:decimal that imposes no limits on the number of digits " +
"that can be represented. The function is applied to this xs:decimal value, " +
"and the resulting xs:decimal is cast back to xs:float or xs:double as appropriate " +
"to form the function result. If the resulting xs:decimal value is zero, " +
"then positive or negative zero is returned according to the sign of $arg.";
private static final FunctionReturnSequenceType returnType = new FunctionReturnSequenceType(Type.NUMBER, Cardinality.ZERO_OR_ONE, "the rounded value");

public static final FunctionSignature[] FN_ROUND_SIGNATURES = {
functionSignature(FN_NAME, FunRound.description, FunRound.returnType,
optParam("arg", Type.NUMBER, "The input number")),
functionSignature(FN_NAME, FunRound.description, FunRound.returnType,
optParam("arg", Type.NUMBER, "The input number"),
optParam("precision", Type.INTEGER, "The input number"))
};

public FunRound(final XQueryContext context, final FunctionSignature signature) {
super(context, signature);
}

public int returnsType() {
return Type.NUMBER;
}

public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException {
if (context.getProfiler().isEnabled()) {
context.getProfiler().start(this);
context.getProfiler().message(this, Profiler.DEPENDENCIES, "DEPENDENCIES", Dependency.getDependenciesName(this.getDependencies()));
if (contextSequence != null)
{context.getProfiler().message(this, Profiler.START_SEQUENCES, "CONTEXT SEQUENCE", contextSequence);}
if (contextItem != null)
{context.getProfiler().message(this, Profiler.START_SEQUENCES, "CONTEXT ITEM", contextItem.toSequence());}
}

if (contextItem != null)
{contextSequence = contextItem.toSequence();}

Sequence result;
final Sequence seq = getArgument(0).eval(contextSequence, contextItem);
if (seq.isEmpty())
{result = Sequence.EMPTY_SEQUENCE;}
else {
final Item item = seq.itemAt(0);
NumericValue value;
if (item instanceof NumericValue) {
value = (NumericValue) item;
} else {
value = (NumericValue) item.convertTo(Type.NUMBER);
}
result = value.round();
}
/**
* Work out the rounding mode for a particular value using fn:round
*
* @param value that has to be rounded
* @return the rounding mode to use on this value
*/
@Override protected final RoundingMode getFunctionRoundingMode(final NumericValue value) {

if (context.getProfiler().isEnabled())
{context.getProfiler().end(this, "", result);}

return result;
if (value.isNegative()) {
return RoundingMode.HALF_DOWN;
} else {
return RoundingMode.HALF_UP;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
* info@exist-db.org
* http://www.exist-db.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.exist.xquery.functions.fn;

import org.exist.xquery.BasicFunction;
import org.exist.xquery.FunctionSignature;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.value.*;

import java.math.RoundingMode;
import java.util.Objects;

/**
* Base class for rounding mode functions
*
* Implements the eval function which knows how to round,
* but defers to the subclass for the {@link RoundingMode} to use.
*/
abstract class FunRoundBase extends BasicFunction {

public FunRoundBase(final XQueryContext context, final FunctionSignature signature) {
super(context, signature);
}

public int returnsType() {
return Type.NUMBER;
}

abstract protected RoundingMode getFunctionRoundingMode(NumericValue value);

@Override
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {

if (args[0].isEmpty()) {
return Sequence.EMPTY_SEQUENCE;
}

final Item item = args[0].itemAt(0);
final NumericValue value;
if (item instanceof NumericValue) {
value = (NumericValue) item;
} else {
value = (NumericValue) item.convertTo(Type.NUMBER);
}

final RoundingMode roundingMode = getFunctionRoundingMode(value);

if (args.length > 1) {
final Item precisionItem = args[1].itemAt(0);
if (precisionItem instanceof IntegerValue) {
final IntegerValue precision = (IntegerValue) precisionItem;
return convertValue(precision, value, roundingMode);
}
}

return convertValue(IntegerValue.ZERO, value, roundingMode);
}

/**
* Apply necessary conversions to/from decimal to perform rounding in decimal
*
* @param precision precision of rounding
* @param value to round
* @param roundingMode mode to round in
* @return rounded value in decimal converted back to the input type
* @throws XPathException if a conversion goes wrong (it shouldn't)
*/
private static Sequence convertValue(final IntegerValue precision, final NumericValue value, final RoundingMode roundingMode) throws XPathException {

if (value.isInfinite() || value.isNaN()) {
return value;
}

final DecimalValue decimal = (DecimalValue)value.convertTo(Type.DECIMAL);
// (AP) This is as much precision as we can need, and prevents overflows scaling BigInteger
final IntegerValue usePrecision = truncatePrecision(decimal, precision);
final DecimalValue rounded = (DecimalValue) Objects.requireNonNull(decimal).round(usePrecision, roundingMode);

if (value.isNegative() && rounded.isZero()) {
//Extreme care!! (AP) -0 as DecimalValue will not be negative, -0.0f and -0.0d will be negative.
//So we need to test that original value to decide whether to negate a "zero" result.
//DecimalValue(s) are not necessarily normalized, but the 0-test will work..
switch (value.getType()) {
case Type.DOUBLE:
return new DoubleValue(-0.0d);
case Type.FLOAT:
return new FloatValue(-0.0f);
default:
break;
}
}

return rounded.convertTo(value.getType());
}

private static IntegerValue truncatePrecision(final DecimalValue decimal, final IntegerValue precision) throws XPathException {

final IntegerValue decimalPrecision = new IntegerValue(decimal.getValue().precision());
if (decimalPrecision.compareTo(precision) < 0) {
return decimalPrecision;
}
return precision;
}
}
Loading

0 comments on commit 529955b

Please sign in to comment.