/
MathUtil.java
193 lines (173 loc) · 7.19 KB
/
MathUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/*******************************************************************************
* Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*******************************************************************************/
package org.eclipse.rdf4j.query.algebra.evaluation.util;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.datatypes.XMLDatatypeUtil;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.vocabulary.XMLSchema;
import org.eclipse.rdf4j.query.algebra.MathExpr.MathOp;
import org.eclipse.rdf4j.query.algebra.evaluation.ValueExprEvaluationException;
/**
* A utility class for evaluation of mathematical expressions on RDF literals.
*
* @author Jeen Broekstra
*/
public class MathUtil {
/**
* The default expansion scale used in division operations resulting in a decimal value with non-terminating decimal
* expansion. The OpenRDF default is 24 digits, a value used in various other SPARQL implementations, to make
* comparison between these systems easy.
*/
public static final int DEFAULT_DECIMAL_EXPANSION_SCALE = 24;
private static int decimalExpansionScale = DEFAULT_DECIMAL_EXPANSION_SCALE;
/**
* Computes the result of applying the supplied math operator on the supplied left and right operand.
*
* @param leftLit a numeric datatype literal
* @param rightLit a numeric datatype literal
* @param op a mathematical operator, as definied by MathExpr.MathOp.
* @return a numeric datatype literal containing the result of the operation. The result will be datatype according
* to the most specific data type the two operands have in common per the SPARQL/XPath spec.
* @throws ValueExprEvaluationException
*/
public static Literal compute(Literal leftLit, Literal rightLit, MathOp op) throws ValueExprEvaluationException {
final ValueFactory vf = SimpleValueFactory.getInstance();
IRI leftDatatype = leftLit.getDatatype();
IRI rightDatatype = rightLit.getDatatype();
// Only numeric value can be used in math expressions
if (!XMLDatatypeUtil.isNumericDatatype(leftDatatype)) {
throw new ValueExprEvaluationException("Not a number: " + leftLit);
}
if (!XMLDatatypeUtil.isNumericDatatype(rightDatatype)) {
throw new ValueExprEvaluationException("Not a number: " + rightLit);
}
// Determine most specific datatype that the arguments have in common,
// choosing from xsd:integer, xsd:decimal, xsd:float and xsd:double as
// per the SPARQL/XPATH spec
IRI commonDatatype;
if (leftDatatype.equals(XMLSchema.DOUBLE) || rightDatatype.equals(XMLSchema.DOUBLE)) {
commonDatatype = XMLSchema.DOUBLE;
} else if (leftDatatype.equals(XMLSchema.FLOAT) || rightDatatype.equals(XMLSchema.FLOAT)) {
commonDatatype = XMLSchema.FLOAT;
} else if (leftDatatype.equals(XMLSchema.DECIMAL) || rightDatatype.equals(XMLSchema.DECIMAL)) {
commonDatatype = XMLSchema.DECIMAL;
} else if (op == MathOp.DIVIDE) {
// Result of integer divide is decimal and requires the arguments to
// be handled as such, see for details:
// http://www.w3.org/TR/xpath-functions/#func-numeric-divide
commonDatatype = XMLSchema.DECIMAL;
} else {
commonDatatype = XMLSchema.INTEGER;
}
// Note: Java already handles cases like divide-by-zero appropriately
// for floats and doubles, see:
// http://www.particle.kth.se/~lindsey/JavaCourse/Book/Part1/Tech/
// Chapter02/floatingPt2.html
try {
if (commonDatatype.equals(XMLSchema.DOUBLE)) {
double left = leftLit.doubleValue();
double right = rightLit.doubleValue();
switch (op) {
case PLUS:
return vf.createLiteral(left + right);
case MINUS:
return vf.createLiteral(left - right);
case MULTIPLY:
return vf.createLiteral(left * right);
case DIVIDE:
return vf.createLiteral(left / right);
default:
throw new IllegalArgumentException("Unknown operator: " + op);
}
} else if (commonDatatype.equals(XMLSchema.FLOAT)) {
float left = leftLit.floatValue();
float right = rightLit.floatValue();
switch (op) {
case PLUS:
return vf.createLiteral(left + right);
case MINUS:
return vf.createLiteral(left - right);
case MULTIPLY:
return vf.createLiteral(left * right);
case DIVIDE:
return vf.createLiteral(left / right);
default:
throw new IllegalArgumentException("Unknown operator: " + op);
}
} else if (commonDatatype.equals(XMLSchema.DECIMAL)) {
BigDecimal left = leftLit.decimalValue();
BigDecimal right = rightLit.decimalValue();
switch (op) {
case PLUS:
return vf.createLiteral(left.add(right));
case MINUS:
return vf.createLiteral(left.subtract(right));
case MULTIPLY:
return vf.createLiteral(left.multiply(right));
case DIVIDE:
// Divide by zero handled through NumberFormatException
BigDecimal result = null;
try {
// try to return the exact quotient if possible.
result = left.divide(right, MathContext.UNLIMITED);
} catch (ArithmeticException e) {
// non-terminating decimal expansion in quotient, using
// scaling and rounding.
result = left.setScale(getDecimalExpansionScale(), RoundingMode.HALF_UP)
.divide(right, RoundingMode.HALF_UP);
}
return vf.createLiteral(result);
default:
throw new IllegalArgumentException("Unknown operator: " + op);
}
} else { // XMLSchema.INTEGER
BigInteger left = leftLit.integerValue();
BigInteger right = rightLit.integerValue();
switch (op) {
case PLUS:
return vf.createLiteral(left.add(right));
case MINUS:
return vf.createLiteral(left.subtract(right));
case MULTIPLY:
return vf.createLiteral(left.multiply(right));
case DIVIDE:
throw new RuntimeException("Integer divisions should be processed as decimal divisions");
default:
throw new IllegalArgumentException("Unknown operator: " + op);
}
}
} catch (NumberFormatException | ArithmeticException e) {
throw new ValueExprEvaluationException(e);
}
}
/**
* Returns the decimal expansion scale used in division operations resulting in a decimal value with non-terminating
* decimal expansion. By default, this value is set to 24.
*
* @return The decimal expansion scale.
*/
public static int getDecimalExpansionScale() {
return decimalExpansionScale;
}
/**
* Sets the decimal expansion scale used in divisions resulting in a decimal value with non-terminating decimal
* expansion.
*
* @param decimalExpansionScale The decimal expansion scale to set. Note that a mimimum of 18 is required to stay
* compliant with the XPath specification of xsd:decimal operations.
*/
public static void setDecimalExpansionScale(int decimalExpansionScale) {
MathUtil.decimalExpansionScale = decimalExpansionScale;
}
}