This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
The following commit(s) were added to refs/heads/master by this push: new f5bf27ca JEXL-420: improved arithmetic coherence; - added tests; f5bf27ca is described below commit f5bf27caaf7c577ed039232e5db81bcb835bc228 Author: Henri Biestro <hbies...@cloudera.com> AuthorDate: Fri Feb 9 10:03:13 2024 +0100 JEXL-420: improved arithmetic coherence; - added tests; --- RELEASE-NOTES.txt | 1 + src/changes/changes.xml | 3 + .../org/apache/commons/jexl3/JexlArithmetic.java | 141 ++++++++++++--------- .../org/apache/commons/jexl3/ArithmeticTest.java | 27 +++- 4 files changed, 109 insertions(+), 63 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index a059780d..d8bac03e 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -38,6 +38,7 @@ New Features in 3.3.1: Bugs Fixed in 3.3.1: =================== +* JEXL-420: Error while comparing float and string value * JEXL-417: JexlArithmetic looses precision during arithmetic operator execution * JEXL-416: Null-valued pragma throws NPE in 3.3 * JEXL-415: Incorrect template eval result diff --git a/src/changes/changes.xml b/src/changes/changes.xml index b614ba05..b5666812 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -46,6 +46,9 @@ </action> <action type="add" dev="ggregory" due-to="Gary Gregory">Add Maven property project.build.outputTimestamp for build reproducibility.</action> <!-- FIX --> + <action dev="henrib" type="fix" issue="JEXL-420" due-to="Xu Pengcheng"> + Error while comparing float and string value + </action> <action dev="henrib" type="fix" issue="JEXL-417" due-to="Robert Lucas" > JexlArithmetic looses precision during arithmetic operator execution </action> diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java index 006bc426..044c3791 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java +++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java @@ -18,6 +18,7 @@ package org.apache.commons.jexl3; import static java.lang.StrictMath.floor; +import static org.apache.commons.jexl3.JexlOperator.EQ; import java.lang.reflect.Array; import java.lang.reflect.Constructor; @@ -58,12 +59,24 @@ import org.apache.commons.jexl3.introspection.JexlMethod; * @since 2.0 */ public class JexlArithmetic { - /** Marker class for null operand exceptions. */ public static class NullOperand extends ArithmeticException { private static final long serialVersionUID = 4720876194840764770L; } + /** Marker class for coercion operand exceptions. */ + public static class CoercionException extends ArithmeticException { + private static final long serialVersionUID = 202402081150L; + + /** + * Simple ctor. + * @param msg the exception message + */ + public CoercionException(String msg) { + super(msg); + } + } + /** Double.MAX_VALUE as BigDecimal. */ protected static final BigDecimal BIGD_DOUBLE_MAX_VALUE = BigDecimal.valueOf(Double.MAX_VALUE); @@ -525,7 +538,7 @@ public class JexlArithmetic { if (val == null) { return controlNullOperand(strict, 0); } - throw new ArithmeticException("Integer coercion: " + throw new CoercionException("Integer coercion: " + val.getClass().getName() + ":(" + val + ")"); } @@ -576,7 +589,7 @@ public class JexlArithmetic { if (val == null) { return controlNullOperand(strict, 0L); } - throw new ArithmeticException("Long coercion: " + throw new CoercionException("Long coercion: " + val.getClass().getName() + ":(" + val + ")"); } @@ -637,7 +650,7 @@ public class JexlArithmetic { if (val == null) { return controlNullOperand(strict, BigInteger.ZERO); } - throw new ArithmeticException("BigInteger coercion: " + throw new CoercionException("BigInteger coercion: " + val.getClass().getName() + ":(" + val + ")"); } @@ -676,7 +689,7 @@ public class JexlArithmetic { return roundBigDecimal(new BigDecimal(val.toString(), getMathContext())); } if (val instanceof Number) { - return roundBigDecimal(new BigDecimal(val.toString(), getMathContext())); + return roundBigDecimal(parseBigDecimal(val.toString())); } if (val instanceof Boolean) { return BigDecimal.valueOf((Boolean) val ? 1. : 0.); @@ -685,20 +698,15 @@ public class JexlArithmetic { return BigDecimal.valueOf(((AtomicBoolean) val).get() ? 1L : 0L); } if (val instanceof String) { - final String string = (String) val; - if (string.isEmpty()) { - return BigDecimal.ZERO; - } - return roundBigDecimal(new BigDecimal(string, getMathContext())); + return roundBigDecimal(parseBigDecimal((String) val)); } if (val instanceof Character) { - final int i = (Character) val; - return new BigDecimal(i); + return new BigDecimal((Character) val); } if (val == null) { return controlNullOperand(strict, BigDecimal.ZERO); } - throw new ArithmeticException("BigDecimal coercion: " + throw new CoercionException("BigDecimal coercion: " + val.getClass().getName() + ":(" + val + ")"); } @@ -731,9 +739,7 @@ public class JexlArithmetic { return (Double) val; } if (val instanceof Number) { - //The below construct is used rather than ((Number)val).doubleValue() to ensure - //equality between comparing new Double( 6.4 / 3 ) and the jexl expression of 6.4 / 3 - return Double.parseDouble(String.valueOf(val)); + return ((Number) val).doubleValue(); } if (val instanceof Boolean) { return (Boolean) val ? 1. : 0.; @@ -750,7 +756,7 @@ public class JexlArithmetic { if (val == null) { return controlNullOperand(strict, 0.d); } - throw new ArithmeticException("Double coercion: " + throw new CoercionException("Double coercion: " + val.getClass().getName() + ":(" + val + ")"); } @@ -1937,7 +1943,7 @@ public class JexlArithmetic { operator = JexlOperator.valueOf(symbol); } catch (final IllegalArgumentException xill) { // ignore - operator = JexlOperator.EQ; + operator = EQ; } return doCompare(left, right, operator); } @@ -1982,54 +1988,54 @@ public class JexlArithmetic { private int doCompare(final Object left, final Object right, final JexlOperator operator) { final boolean strictCast = isStrict(operator); if (left != null && right != null) { - if (left instanceof BigDecimal || right instanceof BigDecimal) { - final BigDecimal l = toBigDecimal(strictCast, left); - final BigDecimal r = toBigDecimal(strictCast, right); - return l.compareTo(r); - } - if (left instanceof BigInteger || right instanceof BigInteger) { - try { + try { + if (left instanceof BigDecimal || right instanceof BigDecimal) { + final BigDecimal l = toBigDecimal(strictCast, left); + final BigDecimal r = toBigDecimal(strictCast, right); + return l.compareTo(r); + } + if (left instanceof BigInteger || right instanceof BigInteger) { final BigInteger l = toBigInteger(strictCast, left); final BigInteger r = toBigInteger(strictCast, right); return l.compareTo(r); - } catch (final ArithmeticException xconvert) { - // ignore it, continue in sequence } - } - if (isFloatingPoint(left) || isFloatingPoint(right)) { - final double lhs = toDouble(strictCast, left); - final double rhs = toDouble(strictCast, right); - if (Double.isNaN(lhs)) { + if (isFloatingPoint(left) || isFloatingPoint(right)) { + final double lhs = toDouble(strictCast, left); + final double rhs = toDouble(strictCast, right); + if (Double.isNaN(lhs)) { + if (Double.isNaN(rhs)) { + return 0; + } + return -1; + } if (Double.isNaN(rhs)) { - return 0; + // lhs is not NaN + return +1; } - return -1; - } - if (Double.isNaN(rhs)) { - // lhs is not NaN - return +1; + return Double.compare(lhs, rhs); } - return Double.compare(lhs, rhs); - } - if (isNumberable(left) || isNumberable(right)) { - try { + if (isNumberable(left) || isNumberable(right)) { final long lhs = toLong(strictCast, left); final long rhs = toLong(strictCast, right); return Long.compare(lhs, rhs); - } catch (final ArithmeticException xconvert) { - // ignore it, continue in sequence } + if (left instanceof String || right instanceof String) { + return toString(left).compareTo(toString(right)); + } + } catch (final CoercionException ignore) { + // ignore it, continue in sequence } - if (left instanceof String || right instanceof String) { - return toString(left).compareTo(toString(right)); - } - if (JexlOperator.EQ == operator) { + if (EQ == operator) { return left.equals(right) ? 0 : -1; } if (left instanceof Comparable<?>) { @SuppressWarnings("unchecked") // OK because of instanceof check above final Comparable<Object> comparable = (Comparable<Object>) left; - return comparable.compareTo(right); + try { + return comparable.compareTo(right); + } catch(ClassCastException castException) { + // ignore it, continue in sequence + } } } throw new ArithmeticException("Object comparison:(" + left + " " + operator + " " + right + ")"); @@ -2049,11 +2055,11 @@ public class JexlArithmetic { if (left == null || right == null) { return false; } - final boolean strictCast = isStrict(JexlOperator.EQ); + final boolean strictCast = isStrict(EQ); if (left instanceof Boolean || right instanceof Boolean) { return toBoolean(left) == toBoolean(strictCast, right); } - return compare(left, right, JexlOperator.EQ) == 0; + return compare(left, right, EQ) == 0; } /** @@ -2130,7 +2136,7 @@ public class JexlArithmetic { try { return arg.isEmpty()? Double.NaN : Double.parseDouble(arg); } catch (final NumberFormatException e) { - final ArithmeticException arithmeticException = new ArithmeticException("Double coercion: ("+ arg +")"); + final ArithmeticException arithmeticException = new CoercionException("Double coercion: ("+ arg +")"); arithmeticException.initCause(e); throw arithmeticException; } @@ -2152,7 +2158,7 @@ public class JexlArithmetic { if (d == f) { return (long) d; } - throw new ArithmeticException("Long coercion: ("+ arg +")"); + throw new CoercionException("Long coercion: ("+ arg +")"); } /** @@ -2168,7 +2174,7 @@ public class JexlArithmetic { if (i == l) { return i; } - throw new ArithmeticException("Int coercion: ("+ arg +")"); + throw new CoercionException("Int coercion: ("+ arg +")"); } /** @@ -2179,15 +2185,30 @@ public class JexlArithmetic { * @throws ArithmeticException if the string can not be coerced into a big integer */ private BigInteger parseBigInteger(final String arg) throws ArithmeticException { - if (arg.isEmpty()) { - return BigInteger.ZERO; + try { + return arg.isEmpty()? BigInteger.ZERO : new BigInteger(arg); + } catch (final NumberFormatException e) { + final ArithmeticException arithmeticException = new CoercionException("BigDecimal coercion: ("+ arg +")"); + arithmeticException.initCause(e); + throw arithmeticException; } + } + + /** + * Convert a string to a BigDecimal. + * <>Empty string is considered as 0.</> + * @param arg the arg + * @return a BigDecimal + * @throws CoercionException if the string can not be coerced into a BigDecimal + */ + private BigDecimal parseBigDecimal(final String arg) throws ArithmeticException { try { - return new BigInteger(arg); - } catch (final NumberFormatException xformat) { - // ignore, try harder + return arg.isEmpty()? BigDecimal.ZERO : new BigDecimal(arg, getMathContext()); + } catch (final NumberFormatException e) { + final ArithmeticException arithmeticException = new CoercionException("BigDecimal coercion: ("+ arg +")"); + arithmeticException.initCause(e); + throw arithmeticException; } - return BigInteger.valueOf(parseLong(arg)); } /** diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java index fa019b6c..23ecabf6 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java @@ -1089,6 +1089,12 @@ public class ArithmeticTest extends JexlTestCase { "'1.00000001' == 1h", false, "1.0 >= '1'", true, "1.0 > '1'", false, + "1.0 == 'a'", false, + "10 == 'a'", false, + "10 > 'a'", ArithmeticException.class, + "10.0 > 'a'", ArithmeticException.class, + "'a' <= 10b", ArithmeticException.class, + "'a' >= 1h", ArithmeticException.class }; final JexlEngine jexl = new JexlBuilder().create(); final JexlContext jc = new EmptyTestContext(); @@ -1098,8 +1104,15 @@ public class ArithmeticTest extends JexlTestCase { final String stext = (String) EXPRESSIONS[e]; final Object expected = EXPRESSIONS[e + 1]; expression = jexl.createExpression(stext); - final Object result = expression.evaluate(jc); - Assert.assertEquals("failed on " + stext, expected, result); + try { + final Object result = expression.evaluate(jc); + Assert.assertEquals("failed on " + stext, expected, result); + } catch(JexlException jexlException) { + Throwable cause = jexlException.getCause(); + if (cause == null || !cause.getClass().equals(expected)) { + Assert.fail(stext); + } + } } } @@ -1769,7 +1782,7 @@ public class ArithmeticTest extends JexlTestCase { } @Test - public void testRealCoercionEdges() { + public void testRealCoercionEdges() throws Exception { assertNullOperand(() -> jexla.toDouble(null)); Assert.assertEquals(0.0d, jexlb.toDouble(null), EPSILON); Assert.assertEquals(32.0d, jexlb.toDouble((char) 32), EPSILON); @@ -1789,6 +1802,14 @@ public class ArithmeticTest extends JexlTestCase { Assert.assertEquals(BigDecimal.ZERO, jexla.toBigDecimal(Double.NaN)); Assert.assertEquals(BigDecimal.ZERO, jexla.toBigDecimal("")); Assert.assertEquals(BigDecimal.ZERO, jexla.toBigDecimal((char) 0)); + + Double d64d3 = new Double( 6.4 / 3 ); + Assert.assertEquals(d64d3, ((Number) JEXL.createExpression("6.4 / 3").evaluate(null)).doubleValue(), EPSILON); + asserter.assertExpression("6.4 / 3", d64d3); + Assert.assertEquals(d64d3, ((Number) JEXL.createExpression("6.4 / 3d").evaluate(null)).doubleValue(), EPSILON); + asserter.assertExpression("6.4 / 3d", d64d3); + Assert.assertEquals(64d / 3, ((Number) JEXL.createExpression("64d / 3").evaluate(null)).doubleValue(), EPSILON); + asserter.assertExpression("64 / 3d", 64 / 3d); } @Test