Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions src/main/java/org/apache/commons/jxpath/ri/InfoSetUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package org.apache.commons.jxpath.ri;

import java.util.regex.Pattern;

import org.apache.commons.jxpath.Pointer;
import org.apache.commons.jxpath.ri.model.NodePointer;
import org.apache.commons.jxpath.ri.model.VariablePointer;
Expand All @@ -30,6 +32,27 @@ public class InfoSetUtil {
private static final Double ONE = Double.valueOf(1);
private static final Double NOT_A_NUMBER = Double.valueOf(Double.NaN);

/**
* The lexical space a string may occupy to be converted to a number per the XPath 1.0 Number production: optional surrounding whitespace and an optional
* leading minus around {@code Digits ('.' Digits?)? | '.' Digits}. {@link Double#parseDouble(String)} additionally accepts Java-only forms (a leading
* {@code +}, exponents, {@code d}/{@code f} type suffixes, hexadecimal floats and the {@code Infinity}/{@code NaN} words), none of which are XPath numbers
* and all of which must convert to NaN.
*/
private static final Pattern XPATH_NUMBER = Pattern.compile("[ \t\r\n]*-?(?:[0-9]+(?:\\.[0-9]*)?|\\.[0-9]+)[ \t\r\n]*");

/**
* Converts a string to a double using XPath rules, returning NaN for any string outside the XPath number lexical space.
*
* @param string value to convert
* @return double value or NaN
*/
private static double parseNumber(final String string) {
if (XPATH_NUMBER.matcher(string).matches()) {
return Double.parseDouble(string.trim());
}
return Double.NaN;
}

/**
* Converts the supplied object to boolean.
*
Expand Down Expand Up @@ -81,11 +104,7 @@ public static double doubleValue(final Object object) {
if (object.equals("")) {
return 0.0;
}
try {
return Double.parseDouble((String) object);
} catch (final NumberFormatException ex) {
return Double.NaN;
}
return parseNumber((String) object);
}
if (object instanceof NodePointer) {
return doubleValue(((NodePointer) object).getValue());
Expand All @@ -112,11 +131,8 @@ public static Number number(final Object object) {
return ((Boolean) object).booleanValue() ? ONE : ZERO;
}
if (object instanceof String) {
try {
return Double.valueOf((String) object);
} catch (final NumberFormatException ex) {
return NOT_A_NUMBER;
}
final double value = parseNumber((String) object);
return Double.isNaN(value) ? NOT_A_NUMBER : Double.valueOf(value);
}
if (object instanceof EvalContext) {
final EvalContext ctx = (EvalContext) object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,24 @@ void testCoreFunctions() {
assertXPathValue(context, "round(2 div 0)", Double.valueOf(Double.POSITIVE_INFINITY));
}

@Test
void testNumberConversionIsXPathConformant() {
// Java number literals that are outside the XPath 1.0 number grammar must convert to NaN.
assertXPathValue(context, "number('1e3')", Double.valueOf(Double.NaN));
assertXPathValue(context, "number('5d')", Double.valueOf(Double.NaN));
assertXPathValue(context, "number('5f')", Double.valueOf(Double.NaN));
assertXPathValue(context, "number('+5')", Double.valueOf(Double.NaN));
assertXPathValue(context, "number('Infinity')", Double.valueOf(Double.NaN));
// Valid XPath numbers still convert.
assertXPathValue(context, "number('1')", Double.valueOf(1));
assertXPathValue(context, "number('1.5')", Double.valueOf(1.5));
assertXPathValue(context, "number('-.5')", Double.valueOf(-0.5));
assertXPathValue(context, "number(' 42 ')", Double.valueOf(42));
// doubleValue() agrees: a non-XPath number is NaN, so numeric comparisons are false.
assertXPathValue(context, "'5d' >= 5", Boolean.FALSE);
assertXPathValue(context, "'1e3' = 1000", Boolean.FALSE);
}

@Test
void testExtendedKeyFunction() {
context.setKeyManager(new ExtendedKeyManager() {
Expand Down