This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 8ee2d3485f7c CAMEL-26329: camel-support - Compare a number and a 
numeric String by their values in typeCoerceEquals (#28752)
8ee2d3485f7c is described below

commit 8ee2d3485f7c375b95164faa1278bee160c7fe5c
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Oct 31 14:58:41 2026 +0530

    CAMEL-26329: camel-support - Compare a number and a numeric String by their 
values in typeCoerceEquals (#28752)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/converter/TypeCoerceCompareTest.java     |  69 ++++++++++++++
 .../camel/language/simple/SimpleOperatorTest.java  |  21 +++++
 .../org/apache/camel/support/ObjectHelper.java     | 100 +++++++++++++++++++--
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 +++
 4 files changed, 197 insertions(+), 6 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/converter/TypeCoerceCompareTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/converter/TypeCoerceCompareTest.java
index 5b197229cadd..0d14c42ff801 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/converter/TypeCoerceCompareTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/converter/TypeCoerceCompareTest.java
@@ -84,6 +84,75 @@ public class TypeCoerceCompareTest extends 
ContextTestSupport {
         assertFalse(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("2.51"), 
new BigDecimal("2.5")));
     }
 
+    @Test
+    void testEqualsNumberAndNumericStringWrittenDifferently() {
+        TypeConverter tc = context.getTypeConverter();
+        // a number and a numeric String with the same value are equal, as <= 
and >= say, also when they are not
+        // written the same way
+        assertEquals(0, ObjectHelper.typeCoerceCompare(tc, new 
BigDecimal("10.00"), "10"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("10.00"), 
"10"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, "10", new 
BigDecimal("10.00")));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("10"), 
"10.0"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, "10.00", 10));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, 10, "10.0"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, 10L, "1e1"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, "1e1", 10L));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, 10, " 10"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, (short) 3, "3.0"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, BigInteger.TEN, "10.0"));
+        // a double was already compared with a numeric String by its value
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, 10d, "10.0"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, 10d, "1e1"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, 10d, " 10"));
+        // other values, text that is not a number, and null are not equal
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("10.00"), 
"10.01"));
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, 10, "10.5"));
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, "10.5", 10));
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, 10, "ten"));
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("10"), 
""));
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, 10, null));
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, null, "10"));
+    }
+
+    @Test
+    void testCompareNumberAndNumericStringWithSpaceOrExponent() {
+        TypeConverter tc = context.getTypeConverter();
+        // compared by their exact values, as == compares them, and not as 
doubles
+        assertTrue(ObjectHelper.typeCoerceCompare(tc, 9007199254740993L, " 
9007199254740992") > 0);
+        assertTrue(ObjectHelper.typeCoerceCompare(tc, " 9007199254740992", 
9007199254740993L) < 0);
+        assertTrue(ObjectHelper.typeCoerceCompare(tc, new 
BigDecimal("1.00000000000000001"), "1e0") > 0);
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, new 
BigDecimal("1.00000000000000001"), "1e0"));
+        assertEquals(0, ObjectHelper.typeCoerceCompare(tc, 10, " 1e1 "));
+    }
+
+    @Test
+    void testEqualsNumberAndTextThatIsNotANumber() {
+        TypeConverter tc = context.getTypeConverter();
+        // text made of the characters of a number is not always a number
+        for (String text : new String[] {
+                "-", "+", ".", "e", "1e", "1e+", "e1", "--1", "+-1", "1.2.3", 
"1e1.5", "2026-10-26" }) {
+            assertFalse(ObjectHelper.typeCoerceEquals(tc, 1, text), text);
+            assertFalse(ObjectHelper.typeCoerceEquals(tc, text, 
BigDecimal.ONE), text);
+        }
+        // a sign, a decimal point at either end, and an exponent with a sign 
are numbers, as BigDecimal parses them
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, 10, "+10"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, 10, "10."));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("0.5"), 
".5"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, -10, "-1e+1"));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("0.01"), 
"1E-2"));
+    }
+
+    @Test
+    void testEqualsNumberAndTooLongNumericString() {
+        TypeConverter tc = context.getTypeConverter();
+        // text of up to 1000 characters is compared as a number; longer text 
is not parsed, as parsing a BigDecimal
+        // takes time that grows with the square of the number of digits
+        String longest = "0".repeat(996) + "10.0";
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, 10, longest));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, longest, 10L));
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, 10, "0" + longest));
+    }
+
     @Test
     public void testCompareStringStringTooBigForLong() {
         TypeConverter tc = context.getTypeConverter();
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
index 3e144f57640f..1b71b92ab02b 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
@@ -672,6 +672,27 @@ public class SimpleOperatorTest extends 
LanguageTestSupport {
         assertPredicate("${header.price} range '2..3'", true);
     }
 
+    @Test
+    void testEqualsNumberWithNumericTextWrittenDifferently() {
+        exchange.getIn().setHeader("amount", new BigDecimal("10.00"));
+        exchange.getIn().setHeader("ten", "10");
+        exchange.getIn().setHeader("count", 3);
+        exchange.getIn().setHeader("text", "10.00");
+        // == agrees with <= and >=, which compare the values
+        assertPredicate("${header.amount} <= '10'", true);
+        assertPredicate("${header.amount} >= '10'", true);
+        assertPredicate("${header.amount} == '10'", true);
+        assertPredicate("${header.amount} != '10'", false);
+        assertPredicate("${header.amount} == ${header.ten}", true);
+        assertPredicate("${header.amount} == '10.01'", false);
+        assertPredicate("${header.count} == '3.0'", true);
+        assertPredicate("${header.count} in '3.0'", true);
+        assertPredicate("${header.count} == '3.5'", false);
+        assertPredicate("${header.text} == 10", true);
+        assertPredicate("${header.text} != 10", false);
+        assertPredicate("${header.text} == 11", false);
+    }
+
     @Test
     void testInWithDecimals() {
         exchange.getIn().setHeader("n", 2);
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java 
b/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java
index a07fb7568cdb..85125f9ba57d 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java
@@ -71,6 +71,12 @@ public final class ObjectHelper {
     private static final Pattern PARENTHESIS_PATTERN;
     private static final String DEFAULT_DELIMITER = ",";
     private static final char DEFAULT_DELIMITER_CHAR = ',';
+    /**
+     * The longest text that is parsed as a number to compare it with a 
number. Parsing a {@link BigDecimal} takes time
+     * that grows with the square of the number of digits (a million digits 
take seconds), and no number in a message is
+     * this long.
+     */
+    private static final int MAX_NUMBER_LENGTH = 1000;
 
     /**
      * Utility classes should not have a public constructor.
@@ -118,6 +124,13 @@ public final class ObjectHelper {
             // converting one to the type of the other may drop the decimals, 
and BigDecimal.equals also compares the
             // scale (2.50 is not equal to 2.5)
             return compareNumbers(leftNum, rightNum) == 0;
+        } else if (rightValue instanceof String text && 
isExactNumber(leftValue)) {
+            // a number and a numeric String (such as 10.00 and "10", or 10 
and "1e1") are equal if their values are
+            // equal, as the <=, >= and in operators compare them, even when 
the String is written differently
+            BigDecimal rightNum = decimalOrNull(text);
+            if (rightNum != null) {
+                return compareNumbers((Number) leftValue, rightNum) == 0;
+            }
         }
 
         // try without type coerce
@@ -134,6 +147,12 @@ public final class ObjectHelper {
             return stringDoubleComparison((String) leftValue, doubleValue);
         } else if (rightValue instanceof Boolean) {
             return stringBooleanComparison(leftValue, rightValue);
+        } else if (isExactNumber(rightValue)) {
+            // a numeric String and a number are equal if their values are 
equal (see typeCoerceEquals)
+            BigDecimal leftNum = decimalOrNull((String) leftValue);
+            if (leftNum != null) {
+                return compareNumbers(leftNum, (Number) rightValue) == 0;
+            }
         }
 
         return tryConverters(converter, leftValue, rightValue, ignoreCase);
@@ -146,16 +165,85 @@ public final class ObjectHelper {
     private static Number asNumber(Object value) {
         if (value instanceof Number number) {
             return number;
-        } else if (value instanceof String text && (isNumber(text) || 
isFloatingNumber(text))) {
-            try {
-                return new BigDecimal(text);
-            } catch (NumberFormatException e) {
-                return null;
-            }
+        } else if (value instanceof String text) {
+            return decimalOrNull(text);
         }
         return null;
     }
 
+    /**
+     * Parses the text as a decimal number, such as {@code 10}, {@code 10.00}, 
{@code -2.5} or {@code 1e1}, ignoring
+     * leading and trailing whitespace, or returns {@code null} if it is not a 
number. Text longer than
+     * {@link #MAX_NUMBER_LENGTH} is not parsed.
+     */
+    private static BigDecimal decimalOrNull(String text) {
+        String number = text.trim();
+        if (number.isEmpty() || number.length() > MAX_NUMBER_LENGTH || 
!isDecimalNumber(number)) {
+            return null;
+        }
+        try {
+            return new BigDecimal(number);
+        } catch (NumberFormatException e) {
+            // such as an exponent that is too large
+            return null;
+        }
+    }
+
+    /**
+     * Whether the text is a number as {@link BigDecimal} parses it: an 
optional sign, digits with an optional decimal
+     * point, and an optional exponent, such as {@code +10}, {@code 10.}, 
{@code .5} or {@code 1e-3}. This is checked
+     * before parsing, so text that is not a number (most text) does not throw 
an exception.
+     */
+    private static boolean isDecimalNumber(String text) {
+        int len = text.length();
+        int i = 0;
+        char ch = text.charAt(0);
+        if (ch == '-' || ch == '+') {
+            i++;
+        }
+        boolean digits = false;
+        boolean dot = false;
+        for (; i < len; i++) {
+            ch = text.charAt(i);
+            if (Character.isDigit(ch)) {
+                digits = true;
+            } else if (ch == '.' && !dot) {
+                dot = true;
+            } else {
+                break;
+            }
+        }
+        if (!digits) {
+            return false;
+        } else if (i == len) {
+            return true;
+        } else if (ch != 'e' && ch != 'E') {
+            return false;
+        }
+        // the exponent
+        i++;
+        if (i < len && (text.charAt(i) == '-' || text.charAt(i) == '+')) {
+            i++;
+        }
+        if (i == len) {
+            return false;
+        }
+        for (; i < len; i++) {
+            if (!Character.isDigit(text.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Whether the value is a number other than a {@link Double} or {@link 
Float}. A double or float is compared with a
+     * numeric String by converting the String to a double or float, which 
compares their values already.
+     */
+    private static boolean isExactNumber(Object value) {
+        return value instanceof Number && !(value instanceof Double) && 
!(value instanceof Float);
+    }
+
     /**
      * Compares two numbers of any type by their numeric values.
      */
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 936a5773a1be..bd8443700b2d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2747,6 +2747,19 @@ route of its route builder or file. The route structure 
now leaves these clauses
 `route-structure` dev console, `camel cmd route-structure`, and the route 
diagrams drawn from it. Before, they were
 listed among the steps of the route.
 
+=== camel-core - simple `==` compares a number and a numeric String by their 
values
+
+The `==` and `!=` operators of the simple language compare a number with a 
`String` that holds a number by their
+values, as `<`, `<=`, `>` and `>=` (and `in` when the left value is a number) 
do since Camel 4.23. Before, when the two
+were written differently, the `String` was converted to the type of the number 
and compared with `equals` (which also
+compares the scale of a `BigDecimal`), or the two were compared as text. So 
`${header.amount} == '10'` is now `true`
+for a `BigDecimal` of `10.00` (it was `false`, while `${header.amount} <= 
'10'` and `>= '10'` were `true`), and so are
+`${header.count} == '3.0'` for an `Integer` of `3` and `${header.text} == 10` 
for a `String` of `10.00`. Spaces around
+the number and an exponent (such as `' 10'` and `'1e1'`) are allowed, as in 
the `in` operator, and `<` and `>` compare
+such a `String` by its exact value too (it was compared as a double). The same 
applies to `=~`, to `contains` on a
+collection, and to the Java DSL and mock predicates such as 
`header("amount").isEqualTo("10")`. Two `String` values,
+and a `Double` or `Float` compared with a `String`, compare as before. Use 
`equals` to compare the two values as text.
+
 === camel-diagram - a route diagram draws the steps of a route
 
 A route diagram drew the `onException` clauses of a route among its steps, 
though they are not steps: a global

Reply via email to