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 5ab46d99 JEXL-417: fix precision loss in JexlArithmetic; - added tests;
5ab46d99 is described below

commit 5ab46d9935c992eab7ab7dce4f1fde196976c789
Author: Henri Biestro <hbies...@cloudera.com>
AuthorDate: Tue Nov 28 10:48:30 2023 +0100

    JEXL-417: fix precision loss in JexlArithmetic;
    - added tests;
---
 RELEASE-NOTES.txt                                  |   1 +
 src/changes/changes.xml                            |   3 +
 .../org/apache/commons/jexl3/JexlArithmetic.java   | 125 ++++++++++++---------
 .../org/apache/commons/jexl3/ArithmeticTest.java   |  51 ++++++---
 4 files changed, 114 insertions(+), 66 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 5de131f8..b53f7d24 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -37,6 +37,7 @@ New Features in 3.3.1:
 
 Bugs Fixed in 3.3.1:
 ===================
+* 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
 * JEXL-414:     SoftCache may suffer from race conditions
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index d058c067..e626ca33 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -42,6 +42,9 @@
                 Allow 'trailing commas' or ellipsis while defining array, map 
and set literals
             </action>
             <!-- FIX -->
+            <action dev="henrib" type="fix" issue="JEXL-417" due-to="Robert 
Lucas" >
+                JexlArithmetic looses precision during arithmetic operator 
execution
+            </action>
             <action dev="henrib" type="fix" issue="JEXL-416" due-to="William 
Price" >
                 Null-valued pragma throws NPE in 3.3
             </action>
diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java 
b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java
index c51d11a3..701422ba 100644
--- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java
+++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java
@@ -29,6 +29,7 @@ import java.math.MathContext;
 import java.util.Collection;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -1062,14 +1063,14 @@ public class JexlArithmetic {
     protected Number narrowBigInteger(final Object lhs, final Object rhs, 
final BigInteger bigi) {
         //coerce to long if possible
         if ((isNumberable(lhs) || isNumberable(rhs))
-                && bigi.compareTo(BIGI_LONG_MAX_VALUE) <= 0
-                && bigi.compareTo(BIGI_LONG_MIN_VALUE) >= 0) {
+            && bigi.compareTo(BIGI_LONG_MAX_VALUE) <= 0
+            && bigi.compareTo(BIGI_LONG_MIN_VALUE) >= 0) {
             // coerce to int if possible
             final long l = bigi.longValue();
             // coerce to int when possible (int being so often used in method 
parms)
             if (!(lhs instanceof Long || rhs instanceof Long)
-                    && l <= Integer.MAX_VALUE
-                    && l >= Integer.MIN_VALUE) {
+                && l <= Integer.MAX_VALUE
+                && l >= Integer.MIN_VALUE) {
                 return (int) l;
             }
             return l;
@@ -1079,7 +1080,7 @@ public class JexlArithmetic {
 
     /**
      * Given a BigDecimal, attempt to narrow it to an Integer or Long if it 
fits and
-     * one of the arguments is a numberable.
+     * one of the arguments is numberable.
      *
      * @param lhs  the left-hand side operand that lead to the bigd result
      * @param rhs  the right-hand side operand that lead to the bigd result
@@ -1144,17 +1145,42 @@ public class JexlArithmetic {
 
     /**
      * Checks if value class is a number that can be represented exactly in a 
long.
+     * <p>For convenience, booleans are converted as 1/0 (true/false).</p>
      *
      * @param value  argument
      * @return true if argument can be represented by a long
      */
     protected Number asLongNumber(final Object value) {
-        return value instanceof Long
-                || value instanceof Integer
-                || value instanceof Short
-                || value instanceof Byte
-                        ? (Number) value
-                        : null;
+        return asLongNumber(strict, value);
+    }
+
+    /**
+     * Checks if value class is a number that can be represented exactly in a 
long.
+     * <p>For convenience, booleans are converted as 1/0 (true/false).</p>
+     *
+     * @param strict whether null argument is converted as 0 or remains null
+     * @param value  argument
+     * @return a non-null value if argument can be represented by a long
+     */
+    protected Number asLongNumber(final boolean strict, final Object value) {
+        if (value instanceof Long
+            || value instanceof Integer
+            || value instanceof Short
+            || value instanceof Byte) {
+            return (Number) value;
+        }
+        if (value instanceof Boolean) {
+            Boolean b = (Boolean) value;
+            return b ? 1L : 0L;
+        }
+        if (value instanceof AtomicBoolean) {
+            AtomicBoolean b = (AtomicBoolean) value;
+            return b.get() ? 1L : 0L;
+        }
+        if (value == null && !strict) {
+            return 0L;
+        }
+        return null;
     }
 
     /**
@@ -1234,9 +1260,10 @@ public class JexlArithmetic {
                             : left instanceof String && right instanceof 
String;
         if (!strconcat) {
             try {
+                final boolean strictCast = isStrict(JexlOperator.ADD);
                 // if both (non-null) args fit as long
-                final Number ln = asLongNumber(left);
-                final Number rn = asLongNumber(right);
+                final Number ln = asLongNumber(strictCast, left);
+                final Number rn = asLongNumber(strictCast, right);
                 if (ln != null && rn != null) {
                     final long x = ln.longValue();
                     final long y = rn.longValue();
@@ -1247,21 +1274,19 @@ public class JexlArithmetic {
                     }
                     return narrowLong(left, right, result);
                 }
-                final boolean strictCast = isStrict(JexlOperator.ADD);
-                // if either are bigdecimal use that type
+                // if either are BigDecimal, use that type
                 if (left instanceof BigDecimal || right instanceof BigDecimal) 
{
                     final BigDecimal l = toBigDecimal(strictCast, left);
                     final BigDecimal r = toBigDecimal(strictCast, right);
-                    final BigDecimal result = l.add(r, getMathContext());
-                    return narrowBigDecimal(left, right, result);
+                    return l.add(r, getMathContext());
                 }
-                // if either are floating point (double or float) use double
+                // if either are floating point (double or float), use double
                 if (isFloatingPointNumber(left) || 
isFloatingPointNumber(right)) {
                     final double l = toDouble(strictCast, left);
                     final double r = toDouble(strictCast, right);
                     return l + r;
                 }
-                // otherwise treat as (big) integers
+                // otherwise treat as BigInteger
                 final BigInteger l = toBigInteger(strictCast, left);
                 final BigInteger r = toBigInteger(strictCast, right);
                 final BigInteger result = l.add(r);
@@ -1285,9 +1310,10 @@ public class JexlArithmetic {
         if (left == null && right == null) {
             return controlNullNullOperands(JexlOperator.DIVIDE);
         }
+        final boolean strictCast = isStrict(JexlOperator.DIVIDE);
         // if both (non-null) args fit as long
-        final Number ln = asLongNumber(left);
-        final Number rn = asLongNumber(right);
+        final Number ln = asLongNumber(strictCast, left);
+        final Number rn = asLongNumber(strictCast, right);
         if (ln != null && rn != null) {
             final long x = ln.longValue();
             final long y = rn.longValue();
@@ -1297,18 +1323,16 @@ public class JexlArithmetic {
             final long result = x  / y;
             return narrowLong(left, right, result);
         }
-        final boolean strictCast = isStrict(JexlOperator.DIVIDE);
-        // if either are bigdecimal use that type
+        // if either are BigDecimal, use that type
         if (left instanceof BigDecimal || right instanceof BigDecimal) {
             final BigDecimal l = toBigDecimal(strictCast, left);
             final BigDecimal r = toBigDecimal(strictCast, right);
             if (BigDecimal.ZERO.equals(r)) {
                 throw new ArithmeticException("/");
             }
-            final BigDecimal result = l.divide(r, getMathContext());
-            return narrowBigDecimal(left, right, result);
+            return l.divide(r, getMathContext());
         }
-        // if either are floating point (double or float) use double
+        // if either are floating point (double or float), use double
         if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
             final double l = toDouble(strictCast, left);
             final double r = toDouble(strictCast, right);
@@ -1317,7 +1341,7 @@ public class JexlArithmetic {
             }
             return l / r;
         }
-        // otherwise treat as integers
+        // otherwise treat as BigInteger
         final BigInteger l = toBigInteger(strictCast, left);
         final BigInteger r = toBigInteger(strictCast, right);
         if (BigInteger.ZERO.equals(r)) {
@@ -1339,9 +1363,10 @@ public class JexlArithmetic {
         if (left == null && right == null) {
             return controlNullNullOperands(JexlOperator.MOD);
         }
+        final boolean strictCast = isStrict(JexlOperator.MOD);
         // if both (non-null) args fit as long
-        final Number ln = asLongNumber(left);
-        final Number rn = asLongNumber(right);
+        final Number ln = asLongNumber(strictCast, left);
+        final Number rn = asLongNumber(strictCast, right);
         if (ln != null && rn != null) {
             final long x = ln.longValue();
             final long y = rn.longValue();
@@ -1351,18 +1376,16 @@ public class JexlArithmetic {
             final long result = x % y;
             return narrowLong(left, right,  result);
         }
-        final boolean strictCast = isStrict(JexlOperator.MOD);
-        // if either are bigdecimal use that type
+        // if either are BigDecimal, use that type
         if (left instanceof BigDecimal || right instanceof BigDecimal) {
             final BigDecimal l = toBigDecimal(strictCast, left);
             final BigDecimal r = toBigDecimal(strictCast, right);
             if (BigDecimal.ZERO.equals(r)) {
                 throw new ArithmeticException("%");
             }
-            final BigDecimal remainder = l.remainder(r, getMathContext());
-            return narrowBigDecimal(left, right, remainder);
+            return l.remainder(r, getMathContext());
         }
-        // if either are floating point (double or float) use double
+        // if either are floating point (double or float), use double
         if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
             final double l = toDouble(strictCast, left);
             final double r = toDouble(strictCast, right);
@@ -1371,7 +1394,7 @@ public class JexlArithmetic {
             }
             return l % r;
         }
-        // otherwise treat as integers
+        // otherwise treat as BigInteger
         final BigInteger l = toBigInteger(strictCast, left);
         final BigInteger r = toBigInteger(strictCast, right);
         if (BigInteger.ZERO.equals(r)) {
@@ -1412,9 +1435,10 @@ public class JexlArithmetic {
         if (left == null && right == null) {
             return controlNullNullOperands(JexlOperator.MULTIPLY);
         }
-        // if both (non-null) args fit as int
-        final Number ln = asLongNumber(left);
-        final Number rn = asLongNumber(right);
+        final boolean strictCast = isStrict(JexlOperator.MULTIPLY);
+        // if both (non-null) args fit as long
+        final Number ln = asLongNumber(strictCast, left);
+        final Number rn = asLongNumber(strictCast, right);
         if (ln != null && rn != null) {
             final long x = ln.longValue();
             final long y = rn.longValue();
@@ -1425,21 +1449,19 @@ public class JexlArithmetic {
             }
             return narrowLong(left, right, result);
         }
-        final boolean strictCast = isStrict(JexlOperator.MULTIPLY);
-        // if either are bigdecimal use that type
+        // if either are BigDecimal, use that type
         if (left instanceof BigDecimal || right instanceof BigDecimal) {
             final BigDecimal l = toBigDecimal(strictCast, left);
             final BigDecimal r = toBigDecimal(strictCast, right);
-            final BigDecimal result = l.multiply(r, getMathContext());
-            return narrowBigDecimal(left, right, result);
+            return l.multiply(r, getMathContext());
         }
-        // if either are floating point (double or float) use double
+        // if either are floating point (double or float), use double
         if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
             final double l = toDouble(strictCast, left);
             final double r = toDouble(strictCast, right);
             return l * r;
         }
-        // otherwise treat as integers
+        // otherwise treat as BigInteger
         final BigInteger l = toBigInteger(strictCast, left);
         final BigInteger r = toBigInteger(strictCast, right);
         final BigInteger result = l.multiply(r);
@@ -1457,9 +1479,10 @@ public class JexlArithmetic {
         if (left == null && right == null) {
             return controlNullNullOperands(JexlOperator.SUBTRACT);
         }
+        final boolean strictCast = isStrict(JexlOperator.SUBTRACT);
         // if both (non-null) args fit as long
-        final Number ln = asLongNumber(left);
-        final Number rn = asLongNumber(right);
+        final Number ln = asLongNumber(strictCast, left);
+        final Number rn = asLongNumber(strictCast, right);
         if (ln != null && rn != null) {
             final long x = ln.longValue();
             final long y = rn.longValue();
@@ -1470,21 +1493,19 @@ public class JexlArithmetic {
             }
             return narrowLong(left, right, result);
         }
-        final boolean strictCast = isStrict(JexlOperator.SUBTRACT);
-        // if either are bigdecimal use that type
+        // if either are BigDecimal, use that type
         if (left instanceof BigDecimal || right instanceof BigDecimal) {
             final BigDecimal l = toBigDecimal(strictCast, left);
             final BigDecimal r = toBigDecimal(strictCast, right);
-            final BigDecimal result = l.subtract(r, getMathContext());
-            return narrowBigDecimal(left, right, result);
+            return l.subtract(r, getMathContext());
         }
-        // if either are floating point (double or float) use double
+        // if either are floating point (double or float), use double
         if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
             final double l = toDouble(strictCast, left);
             final double r = toDouble(strictCast, right);
             return l - r;
         }
-        // otherwise treat as integers
+        // otherwise treat as BigInteger
         final BigInteger l = toBigInteger(strictCast, left);
         final BigInteger r = toBigInteger(strictCast, right);
         final BigInteger result = l.subtract(r);
diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java 
b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java
index a2cc3618..51f0f69a 100644
--- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java
+++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java
@@ -917,6 +917,17 @@ public class ArithmeticTest extends JexlTestCase {
         Assert.assertEquals(BigDecimal.valueOf(10), ja.toBigDecimal("10"));
         Assert.assertEquals(BigDecimal.valueOf(1.), ja.toBigDecimal(true));
         Assert.assertEquals(BigDecimal.valueOf(0.), ja.toBigDecimal(false));
+        // BigDecimal precision is kept when used as argument
+        BigDecimal a42 = BigDecimal.valueOf(42);
+        BigDecimal a49 = BigDecimal.valueOf(49);
+        JexlScript bde = JEXL.createScript("a * 6 / 7", "a");
+        Assert.assertEquals(a42, bde.execute(null, a49));
+        bde = JEXL.createScript("(a - 12) / 12", "a");
+        MathContext mc = ja.getMathContext();
+        BigDecimal b56 = BigDecimal.valueOf(56);
+        BigDecimal b12 = BigDecimal.valueOf(12);
+        BigDecimal b3dot666 = b56.subtract(b12, mc).divide(b12, mc);
+        Assert.assertEquals(b3dot666, bde.execute(null, b56));
     }
 
     @Test
@@ -1564,26 +1575,38 @@ public class ArithmeticTest extends JexlTestCase {
     }
 
     @Test
-    public void testNarrowBig() throws Exception {
+    public void testNarrowBigInteger() throws Exception {
         final List<String> ls = Arrays.asList("zero", "one", "two");
         asserter.setVariable("list",ls);
-        asserter.setVariable("aBigDecimal", new BigDecimal("1"));
-        asserter.setVariable("aBigInteger", new BigDecimal("1"));
-        asserter.assertExpression("list.get(aBigDecimal)", "one");
-        asserter.assertExpression("list.get(aBigInteger)", "one");
+        asserter.assertExpression("a -> list.get(a)", "zero", BigInteger.ZERO);
+        asserter.assertExpression("a -> list.get(a)", "one", BigInteger.ONE);
+        asserter.assertExpression("a -> list.get(2H)", "two");
+        BigInteger b42 = BigInteger.valueOf(42);
+        asserter.setVariable("bi10", BigInteger.valueOf(10));
+        asserter.setVariable("bi420", BigInteger.valueOf(420));
+        asserter.assertExpression("420 / bi10", b42);
+        asserter.assertExpression("420l / bi10", b42);
+        asserter.assertExpression("bi420 / 420", BigInteger.ONE);
+        asserter.assertExpression("bi420 / 420l", BigInteger.ONE);
+        asserter.assertExpression("bi420 / 420H", BigInteger.ONE);
     }
 
     @Test
     public void testNarrowBigDecimal() throws Exception {
-        asserter.setVariable("bi420", BigInteger.valueOf(420));
-        asserter.setVariable("bi10", BigInteger.valueOf(10));
-        asserter.setVariable("bd420", new BigDecimal("420"));
-        asserter.setVariable("bd10", new BigDecimal("10"));
-        asserter.assertExpression("420 / bi10", 42);
-        asserter.assertExpression("420l / bi10", 42L);
-        asserter.assertExpression("bi420 / 420", 1);
-        asserter.assertExpression("bi420 / 420l", 1L);
-        asserter.assertExpression("bd420 / 10", new BigDecimal("42"));
+        final List<String> ls = Arrays.asList("zero", "one", "two");
+        asserter.setVariable("list", ls);
+        asserter.assertExpression("a -> list.get(a.intValue())", "zero", 
BigDecimal.ZERO);
+        asserter.assertExpression("a -> list.get(a.intValue())", "one", 
BigDecimal.ONE);
+        asserter.assertExpression("a -> list.get(2B.intValue())", "two");
+        BigDecimal bd42 = BigDecimal.valueOf(42);
+        asserter.setVariable("bd10", BigDecimal.valueOf(10d));
+        asserter.setVariable("bd420",BigDecimal.valueOf(420d));
+        asserter.assertExpression("420 / bd10", bd42);
+        asserter.assertExpression("420l / bd10", bd42);
+        asserter.assertExpression("420H / bd10", bd42);
+        asserter.assertExpression("bd420 / 10", bd42);
+        asserter.assertExpression("bd420 / 10H", bd42);
+        asserter.assertExpression("bd420 / 10B", bd42);
     }
 
     @Test

Reply via email to