Skip to content

Commit 529955b

Browse files
authored
Merge pull request eXist-db#4400 from evolvedbinary/feature/fn-round,#2
Implement fn:round#2
2 parents 6f8e3bf + 5359a29 commit 529955b

File tree

10 files changed

+439
-155
lines changed

10 files changed

+439
-155
lines changed

exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,10 @@ public class FnModule extends AbstractInternalModule {
181181
new FunctionDef(FunResolveURI.signatures[1], FunResolveURI.class),
182182
new FunctionDef(FunRoot.signatures[0], FunRoot.class),
183183
new FunctionDef(FunRoot.signatures[1], FunRoot.class),
184-
new FunctionDef(FunRound.signature, FunRound.class),
185-
new FunctionDef(FunRoundHalfToEven.signatures[0], FunRoundHalfToEven.class),
186-
new FunctionDef(FunRoundHalfToEven.signatures[1], FunRoundHalfToEven.class),
184+
new FunctionDef(FunRound.FN_ROUND_SIGNATURES[0], FunRound.class),
185+
new FunctionDef(FunRound.FN_ROUND_SIGNATURES[1], FunRound.class),
186+
new FunctionDef(FunRoundHalfToEven.FN_ROUND_HALF_TO_EVEN_SIGNATURES[0], FunRoundHalfToEven.class),
187+
new FunctionDef(FunRoundHalfToEven.FN_ROUND_HALF_TO_EVEN_SIGNATURES[1], FunRoundHalfToEven.class),
187188
new FunctionDef(FunSerialize.signatures[0], FunSerialize.class),
188189
new FunctionDef(FunSerialize.signatures[1], FunSerialize.class),
189190
new FunctionDef(FunStartsWith.signatures[0], FunStartsWith.class),

exist-core/src/main/java/org/exist/xquery/functions/fn/FunRound.java

Lines changed: 58 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -21,89 +21,77 @@
2121
*/
2222
package org.exist.xquery.functions.fn;
2323

24-
import org.exist.dom.QName;
2524
import org.exist.xquery.Cardinality;
26-
import org.exist.xquery.Dependency;
27-
import org.exist.xquery.Function;
2825
import org.exist.xquery.FunctionSignature;
29-
import org.exist.xquery.Profiler;
3026
import org.exist.xquery.XPathException;
3127
import org.exist.xquery.XQueryContext;
32-
import org.exist.xquery.value.FunctionParameterSequenceType;
33-
import org.exist.xquery.value.FunctionReturnSequenceType;
34-
import org.exist.xquery.value.Item;
35-
import org.exist.xquery.value.NumericValue;
36-
import org.exist.xquery.value.Sequence;
37-
import org.exist.xquery.value.SequenceType;
38-
import org.exist.xquery.value.Type;
28+
import org.exist.xquery.value.*;
3929

40-
public class FunRound extends Function {
30+
import java.math.RoundingMode;
4131

42-
public final static FunctionSignature signature =
43-
new FunctionSignature(
44-
new QName("round", Function.BUILTIN_FUNCTION_NS),
45-
"Returns the number with no fractional part that is closest " +
46-
"to the argument $arg. If there are two such numbers, then the one " +
47-
"that is closest to positive infinity is returned. If type of " +
48-
"$arg is one of the four numeric types xs:float, xs:double, " +
49-
"xs:decimal or xs:integer the type of the result is the same " +
50-
"as the type of $arg. If the type of $arg is a type derived " +
51-
"from one of the numeric types, the result is an instance of " +
52-
"the base numeric type.\n\n" +
53-
"For xs:float and xs:double arguments, if the argument is " +
54-
"positive infinity, then positive infinity is returned. " +
55-
"If the argument is negative infinity, then negative infinity " +
56-
"is returned. If the argument is positive zero, then positive " +
57-
"zero is returned. If the argument is negative zero, then " +
58-
"negative zero is returned. If the argument is less than zero, " +
59-
"but greater than or equal to -0.5, then negative zero is returned. " +
60-
"In the cases where positive zero or negative zero is returned, " +
61-
"negative zero or positive zero may be returned as " +
62-
"[XML Schema Part 2: Datatypes Second Edition] does not " +
63-
"distinguish between the values positive zero and negative zero.",
64-
new SequenceType[] { new FunctionParameterSequenceType("arg", Type.NUMBER, Cardinality.ZERO_OR_ONE, "The input number") },
65-
new FunctionReturnSequenceType(Type.NUMBER, Cardinality.ZERO_OR_ONE, "the rounded value")
66-
);
67-
68-
public FunRound(XQueryContext context) {
32+
import static org.exist.xquery.FunctionDSL.optParam;
33+
import static org.exist.xquery.functions.fn.FnModule.functionSignature;
34+
35+
/**
36+
* Implement fn:round() function
37+
*
38+
* Shares a base class and evaluator with {@link FunRoundHalfToEven}
39+
* They differ only in the rounding mode used.
40+
*/
41+
public class FunRound extends FunRoundBase {
42+
43+
private static final String FN_NAME = "round";
44+
private static final String description = "The function returns the nearest (that is, numerically closest) " +
45+
"value to $arg that is a multiple of ten to the power of minus $precision. " +
46+
"If two such values are equally near (for example, if the fractional part in $arg is exactly .5), " +
47+
"the function returns the one that is closest to positive infinity. " +
48+
"For the four types xs:float, xs:double, xs:decimal and xs:integer, " +
49+
"it is guaranteed that if the type of $arg is an instance of type T " +
50+
"then the result will also be an instance of T. " +
51+
"The result may also be an instance of a type derived from one of these four by restriction. " +
52+
"For example, if $arg is an instance of xs:decimal and $precision is less than one, " +
53+
"then the result may be an instance of xs:integer. " +
54+
"The single-argument version of this function produces the same result " +
55+
"as the two-argument version with $precision=0 (that is, it rounds to a whole number). " +
56+
"When $arg is of type xs:float and xs:double: " +
57+
"If $arg is NaN, positive or negative zero, or positive or negative infinity, " +
58+
"then the result is the same as the argument. " +
59+
"For other values, the argument is cast to xs:decimal " +
60+
"using an implementation of xs:decimal that imposes no limits on the number of digits " +
61+
"that can be represented. The function is applied to this xs:decimal value, " +
62+
"and the resulting xs:decimal is cast back to xs:float or xs:double as appropriate " +
63+
"to form the function result. If the resulting xs:decimal value is zero, " +
64+
"then positive or negative zero is returned according to the sign of $arg.";
65+
private static final FunctionReturnSequenceType returnType = new FunctionReturnSequenceType(Type.NUMBER, Cardinality.ZERO_OR_ONE, "the rounded value");
66+
67+
public static final FunctionSignature[] FN_ROUND_SIGNATURES = {
68+
functionSignature(FN_NAME, FunRound.description, FunRound.returnType,
69+
optParam("arg", Type.NUMBER, "The input number")),
70+
functionSignature(FN_NAME, FunRound.description, FunRound.returnType,
71+
optParam("arg", Type.NUMBER, "The input number"),
72+
optParam("precision", Type.INTEGER, "The input number"))
73+
};
74+
75+
public FunRound(final XQueryContext context, final FunctionSignature signature) {
6976
super(context, signature);
7077
}
7178

7279
public int returnsType() {
7380
return Type.NUMBER;
7481
}
7582

76-
public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException {
77-
if (context.getProfiler().isEnabled()) {
78-
context.getProfiler().start(this);
79-
context.getProfiler().message(this, Profiler.DEPENDENCIES, "DEPENDENCIES", Dependency.getDependenciesName(this.getDependencies()));
80-
if (contextSequence != null)
81-
{context.getProfiler().message(this, Profiler.START_SEQUENCES, "CONTEXT SEQUENCE", contextSequence);}
82-
if (contextItem != null)
83-
{context.getProfiler().message(this, Profiler.START_SEQUENCES, "CONTEXT ITEM", contextItem.toSequence());}
84-
}
85-
86-
if (contextItem != null)
87-
{contextSequence = contextItem.toSequence();}
88-
89-
Sequence result;
90-
final Sequence seq = getArgument(0).eval(contextSequence, contextItem);
91-
if (seq.isEmpty())
92-
{result = Sequence.EMPTY_SEQUENCE;}
93-
else {
94-
final Item item = seq.itemAt(0);
95-
NumericValue value;
96-
if (item instanceof NumericValue) {
97-
value = (NumericValue) item;
98-
} else {
99-
value = (NumericValue) item.convertTo(Type.NUMBER);
100-
}
101-
result = value.round();
102-
}
83+
/**
84+
* Work out the rounding mode for a particular value using fn:round
85+
*
86+
* @param value that has to be rounded
87+
* @return the rounding mode to use on this value
88+
*/
89+
@Override protected final RoundingMode getFunctionRoundingMode(final NumericValue value) {
10390

104-
if (context.getProfiler().isEnabled())
105-
{context.getProfiler().end(this, "", result);}
106-
107-
return result;
91+
if (value.isNegative()) {
92+
return RoundingMode.HALF_DOWN;
93+
} else {
94+
return RoundingMode.HALF_UP;
95+
}
10896
}
10997
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* eXist-db Open Source Native XML Database
3+
* Copyright (C) 2001 The eXist-db Authors
4+
*
5+
6+
* http://www.exist-db.org
7+
*
8+
* This library is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 2.1 of the License, or (at your option) any later version.
12+
*
13+
* This library is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, write to the Free Software
20+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21+
*/
22+
package org.exist.xquery.functions.fn;
23+
24+
import org.exist.xquery.BasicFunction;
25+
import org.exist.xquery.FunctionSignature;
26+
import org.exist.xquery.XPathException;
27+
import org.exist.xquery.XQueryContext;
28+
import org.exist.xquery.value.*;
29+
30+
import java.math.RoundingMode;
31+
import java.util.Objects;
32+
33+
/**
34+
* Base class for rounding mode functions
35+
*
36+
* Implements the eval function which knows how to round,
37+
* but defers to the subclass for the {@link RoundingMode} to use.
38+
*/
39+
abstract class FunRoundBase extends BasicFunction {
40+
41+
public FunRoundBase(final XQueryContext context, final FunctionSignature signature) {
42+
super(context, signature);
43+
}
44+
45+
public int returnsType() {
46+
return Type.NUMBER;
47+
}
48+
49+
abstract protected RoundingMode getFunctionRoundingMode(NumericValue value);
50+
51+
@Override
52+
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
53+
54+
if (args[0].isEmpty()) {
55+
return Sequence.EMPTY_SEQUENCE;
56+
}
57+
58+
final Item item = args[0].itemAt(0);
59+
final NumericValue value;
60+
if (item instanceof NumericValue) {
61+
value = (NumericValue) item;
62+
} else {
63+
value = (NumericValue) item.convertTo(Type.NUMBER);
64+
}
65+
66+
final RoundingMode roundingMode = getFunctionRoundingMode(value);
67+
68+
if (args.length > 1) {
69+
final Item precisionItem = args[1].itemAt(0);
70+
if (precisionItem instanceof IntegerValue) {
71+
final IntegerValue precision = (IntegerValue) precisionItem;
72+
return convertValue(precision, value, roundingMode);
73+
}
74+
}
75+
76+
return convertValue(IntegerValue.ZERO, value, roundingMode);
77+
}
78+
79+
/**
80+
* Apply necessary conversions to/from decimal to perform rounding in decimal
81+
*
82+
* @param precision precision of rounding
83+
* @param value to round
84+
* @param roundingMode mode to round in
85+
* @return rounded value in decimal converted back to the input type
86+
* @throws XPathException if a conversion goes wrong (it shouldn't)
87+
*/
88+
private static Sequence convertValue(final IntegerValue precision, final NumericValue value, final RoundingMode roundingMode) throws XPathException {
89+
90+
if (value.isInfinite() || value.isNaN()) {
91+
return value;
92+
}
93+
94+
final DecimalValue decimal = (DecimalValue)value.convertTo(Type.DECIMAL);
95+
// (AP) This is as much precision as we can need, and prevents overflows scaling BigInteger
96+
final IntegerValue usePrecision = truncatePrecision(decimal, precision);
97+
final DecimalValue rounded = (DecimalValue) Objects.requireNonNull(decimal).round(usePrecision, roundingMode);
98+
99+
if (value.isNegative() && rounded.isZero()) {
100+
//Extreme care!! (AP) -0 as DecimalValue will not be negative, -0.0f and -0.0d will be negative.
101+
//So we need to test that original value to decide whether to negate a "zero" result.
102+
//DecimalValue(s) are not necessarily normalized, but the 0-test will work..
103+
switch (value.getType()) {
104+
case Type.DOUBLE:
105+
return new DoubleValue(-0.0d);
106+
case Type.FLOAT:
107+
return new FloatValue(-0.0f);
108+
default:
109+
break;
110+
}
111+
}
112+
113+
return rounded.convertTo(value.getType());
114+
}
115+
116+
private static IntegerValue truncatePrecision(final DecimalValue decimal, final IntegerValue precision) throws XPathException {
117+
118+
final IntegerValue decimalPrecision = new IntegerValue(decimal.getValue().precision());
119+
if (decimalPrecision.compareTo(precision) < 0) {
120+
return decimalPrecision;
121+
}
122+
return precision;
123+
}
124+
}

0 commit comments

Comments
 (0)