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

davsclaus pushed a commit to branch fix/CAMEL-24962
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 2c518175f101ede3d52d0b99038816a59fe6a435
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 23 18:39:52 2026 +0200

    CAMEL-24969: simple - range(max) starts at 0 as documented; CAMEL-24968: 
fix substring docs and catalog
    
    range(max) now returns 0..max-1 (as Python, and as range(0,max)); it used 
to start at 1.
    The substring catalog entry described positions and had wrong examples; the 
numbers
    are how many characters to clip from each end.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/docs/simple-functions.adoc      |  7 +++++--
 .../resources/org/apache/camel/catalog/languages/simple.json |  2 +-
 .../META-INF/org/apache/camel/language/simple/simple.json    |  2 +-
 .../main/docs/modules/languages/pages/simple-functions.adoc  |  7 +++++--
 .../org/apache/camel/language/simple/SimpleConstants.java    | 12 ++++++------
 .../language/simple/functions/CollectionFunctionFactory.java |  2 +-
 .../java/org/apache/camel/language/simple/SimpleTest.java    |  9 +++++++++
 .../simple/functions/CollectionFunctionFactoryTest.java      |  4 ++--
 .../modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc      |  4 ++++
 9 files changed, 34 insertions(+), 15 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
index deeed77f9c76..525d77ecd2f1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
@@ -526,8 +526,8 @@ The string functions is various functions to work with 
values that are String an
 |`safeQuote()` | `String` | Returns the message body safely quoted if needed
 |`safeQuote(exp)` | `String` | Returns the expression safely quoted if needed
 |`substring(num1)` | `String` | Returns a substring of the message body. If 
the number is positive, then the returned string is clipped from the beginning. 
If the number is negative, then the returned string is clipped from the ending.
-|`substring(num1,num2)` | `String` | Returns a substring of the message body. 
`num1` clips from the beginning, `num2` clips from the ending. Negative values 
clip from the opposite end.
-|`substring(num1,num2,exp)` | `String` | Returns a substring of the given 
expression. `num1` clips from the beginning, `num2` clips from the ending. 
Negative values clip from the opposite end.
+|`substring(num1,num2)` | `String` | Returns a substring of the message body. 
`num1` is how many characters to clip from the beginning, and `num2` how many 
characters to clip from the ending. The sign of `num2` does not matter, so `-1` 
and `1` both clip one character.
+|`substring(num1,num2,exp)` | `String` | Returns a substring of the given 
expression. `num1` is how many characters to clip from the beginning, and 
`num2` how many characters to clip from the ending. The sign of `num2` does not 
matter, so `-1` and `1` both clip one character.
 |`substringAfter(after)` | `String` | Returns a substring of the message body 
that comes after. Returns `null` if nothing comes after.
 |`substringAfter(exp,after)` | `String` | Returns a substring of the 
expression that comes after. Returns `null` if nothing comes after.
 |`substringBefore(before)` | `String` | Returns a substring of the message 
body that comes before. Returns `null` if nothing comes before.
@@ -600,6 +600,9 @@ The `substring`, `substringBefore`, and `substringAfter` 
functions are all simil
 Suppose the message body contains `ABCDEFGHIJK` then `${substring(3)}` returns 
`DEFGHIJK`, and `${substring(-3)}` returns `ABCDEFGH`.
 If you want to clip the first and last character you can use 
`${substring(1,-1)}` returning `BCDEFGHIJ`.
 
+The numbers are how many characters to clip, not positions as in a Python 
slice: `${substring(3,-2)}` returns `DEFGHI`
+(three characters clipped from the beginning and two from the end), and 
`${substring(1,0)}` returns `BCDEFGHIJK`.
+
 [NOTE]
 ====
 These `substring` functions are Camel functions with their own semantics (such 
as negative numbers clipping
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
index 0ca682b5a72c..a1df2fe174f9 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
@@ -159,7 +159,7 @@
     "skip(num)": { "index": 130, "kind": "function", "displayName": "Skip 
First Items from the Message Body", "group": "collection", "label": 
"collection", "required": false, "javaType": "java.util.Iterator", "prefix": 
"${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The skip function iterates the message body and skips 
the first number of items. This can be used with the Splitter EIP to split a 
message body and skip the first N numbe [...]
     "split(exp,separator)": { "index": 131, "kind": "function", "displayName": 
"Split String Values", "group": "collection", "label": "collection", 
"required": false, "javaType": "String[]", "prefix": "${", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Splits the message body\/expression as a String value using the separator into 
a String array", "ognl": false, "suffix": "}", "params": [ { "name": "exp", 
"javaType": "Object", "required": [...]
     "stepId": { "index": 132, "kind": "function", "displayName": "Step Id", 
"group": "core", "label": "core", "required": false, "javaType": "String", 
"prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "Returns the id of the current step the 
Exchange is being routed.", "ognl": false, "suffix": "}" },
-    "substring(head,tail)": { "index": 133, "kind": "function", "displayName": 
"Substring", "group": "string", "label": "string", "required": false, 
"javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Returns a substring of 
the message body\/expression. If only one positive number, then the returned 
string is clipped from the beginning. If only one negative number, then the 
returned string is clipped from  [...]
+    "substring(head,tail)": { "index": 133, "kind": "function", "displayName": 
"Substring", "group": "string", "label": "string", "required": false, 
"javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Returns a substring of 
the message body\/expression by clipping characters off its ends (not by 
positions). With one number, a positive number clips that many characters from 
the beginning, and a negative num [...]
     "substringAfter(exp,before)": { "index": 134, "kind": "function", 
"displayName": "Substring After", "group": "string", "label": "string", 
"required": false, "javaType": "String", "prefix": "${", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Returns a substring of the message body\/expression that comes after. Returns 
null if nothing comes after.", "ognl": false, "suffix": "}", "params": [ { 
"name": "exp", "javaType": "Object", "requi [...]
     "substringBefore(exp,before)": { "index": 135, "kind": "function", 
"displayName": "Substring Before", "group": "string", "label": "string", 
"required": false, "javaType": "String", "prefix": "${", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Returns a substring of the message body\/expression that comes before. Returns 
null if nothing comes before.", "ognl": false, "suffix": "}", "params": [ { 
"name": "exp", "javaType": "Object", "r [...]
     "substringBetween(exp,after,before)": { "index": 136, "kind": "function", 
"displayName": "Substring Between", "group": "string", "label": "string", 
"required": false, "javaType": "String", "prefix": "${", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Returns a substring of the message body\/expression that are between after and 
before. Returns null if nothing comes between.", "ognl": false, "suffix": "}", 
"params": [ { "name": "exp", [...]
diff --git 
a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
 
b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
index 0ca682b5a72c..a1df2fe174f9 100644
--- 
a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
+++ 
b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
@@ -159,7 +159,7 @@
     "skip(num)": { "index": 130, "kind": "function", "displayName": "Skip 
First Items from the Message Body", "group": "collection", "label": 
"collection", "required": false, "javaType": "java.util.Iterator", "prefix": 
"${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The skip function iterates the message body and skips 
the first number of items. This can be used with the Splitter EIP to split a 
message body and skip the first N numbe [...]
     "split(exp,separator)": { "index": 131, "kind": "function", "displayName": 
"Split String Values", "group": "collection", "label": "collection", 
"required": false, "javaType": "String[]", "prefix": "${", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Splits the message body\/expression as a String value using the separator into 
a String array", "ognl": false, "suffix": "}", "params": [ { "name": "exp", 
"javaType": "Object", "required": [...]
     "stepId": { "index": 132, "kind": "function", "displayName": "Step Id", 
"group": "core", "label": "core", "required": false, "javaType": "String", 
"prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "Returns the id of the current step the 
Exchange is being routed.", "ognl": false, "suffix": "}" },
-    "substring(head,tail)": { "index": 133, "kind": "function", "displayName": 
"Substring", "group": "string", "label": "string", "required": false, 
"javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Returns a substring of 
the message body\/expression. If only one positive number, then the returned 
string is clipped from the beginning. If only one negative number, then the 
returned string is clipped from  [...]
+    "substring(head,tail)": { "index": 133, "kind": "function", "displayName": 
"Substring", "group": "string", "label": "string", "required": false, 
"javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Returns a substring of 
the message body\/expression by clipping characters off its ends (not by 
positions). With one number, a positive number clips that many characters from 
the beginning, and a negative num [...]
     "substringAfter(exp,before)": { "index": 134, "kind": "function", 
"displayName": "Substring After", "group": "string", "label": "string", 
"required": false, "javaType": "String", "prefix": "${", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Returns a substring of the message body\/expression that comes after. Returns 
null if nothing comes after.", "ognl": false, "suffix": "}", "params": [ { 
"name": "exp", "javaType": "Object", "requi [...]
     "substringBefore(exp,before)": { "index": 135, "kind": "function", 
"displayName": "Substring Before", "group": "string", "label": "string", 
"required": false, "javaType": "String", "prefix": "${", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Returns a substring of the message body\/expression that comes before. Returns 
null if nothing comes before.", "ognl": false, "suffix": "}", "params": [ { 
"name": "exp", "javaType": "Object", "r [...]
     "substringBetween(exp,after,before)": { "index": 136, "kind": "function", 
"displayName": "Substring Between", "group": "string", "label": "string", 
"required": false, "javaType": "String", "prefix": "${", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Returns a substring of the message body\/expression that are between after and 
before. Returns null if nothing comes between.", "ognl": false, "suffix": "}", 
"params": [ { "name": "exp", [...]
diff --git 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
index deeed77f9c76..525d77ecd2f1 100644
--- 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
+++ 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
@@ -526,8 +526,8 @@ The string functions is various functions to work with 
values that are String an
 |`safeQuote()` | `String` | Returns the message body safely quoted if needed
 |`safeQuote(exp)` | `String` | Returns the expression safely quoted if needed
 |`substring(num1)` | `String` | Returns a substring of the message body. If 
the number is positive, then the returned string is clipped from the beginning. 
If the number is negative, then the returned string is clipped from the ending.
-|`substring(num1,num2)` | `String` | Returns a substring of the message body. 
`num1` clips from the beginning, `num2` clips from the ending. Negative values 
clip from the opposite end.
-|`substring(num1,num2,exp)` | `String` | Returns a substring of the given 
expression. `num1` clips from the beginning, `num2` clips from the ending. 
Negative values clip from the opposite end.
+|`substring(num1,num2)` | `String` | Returns a substring of the message body. 
`num1` is how many characters to clip from the beginning, and `num2` how many 
characters to clip from the ending. The sign of `num2` does not matter, so `-1` 
and `1` both clip one character.
+|`substring(num1,num2,exp)` | `String` | Returns a substring of the given 
expression. `num1` is how many characters to clip from the beginning, and 
`num2` how many characters to clip from the ending. The sign of `num2` does not 
matter, so `-1` and `1` both clip one character.
 |`substringAfter(after)` | `String` | Returns a substring of the message body 
that comes after. Returns `null` if nothing comes after.
 |`substringAfter(exp,after)` | `String` | Returns a substring of the 
expression that comes after. Returns `null` if nothing comes after.
 |`substringBefore(before)` | `String` | Returns a substring of the message 
body that comes before. Returns `null` if nothing comes before.
@@ -600,6 +600,9 @@ The `substring`, `substringBefore`, and `substringAfter` 
functions are all simil
 Suppose the message body contains `ABCDEFGHIJK` then `${substring(3)}` returns 
`DEFGHIJK`, and `${substring(-3)}` returns `ABCDEFGH`.
 If you want to clip the first and last character you can use 
`${substring(1,-1)}` returning `BCDEFGHIJ`.
 
+The numbers are how many characters to clip, not positions as in a Python 
slice: `${substring(3,-2)}` returns `DEFGHI`
+(three characters clipped from the beginning and two from the end), and 
`${substring(1,0)}` returns `BCDEFGHIJK`.
+
 [NOTE]
 ====
 These `substring` functions are Camel functions with their own semantics (such 
as negative numbers clipping
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
index e5b61924a59a..992ea86f809e 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
@@ -851,15 +851,15 @@ public final class SimpleConstants {
               label = "core")
     public static final String STEP_ID = "stepId";
 
-    @Metadata(description = "Returns a substring of the message 
body/expression. If only one positive number, then the returned string is 
clipped from the beginning. If only one negative number, then the returned 
string is clipped from the beginning. Otherwise the returned string is clipped 
between the head and tail positions.",
+    @Metadata(description = "Returns a substring of the message 
body/expression by clipping characters off its ends (not by positions). With 
one number, a positive number clips that many characters from the beginning, 
and a negative number clips that many characters from the end. With two 
numbers, head is how many characters to clip from the beginning and tail how 
many to clip from the end; the sign of tail does not matter, so -1 and 1 both 
clip one character.",
               label = "string", javaType = "String",
               examples = {
-                      "${substring(0,5)} -> Hello // when body is 'Hello 
World'",
-                      "${substring(6)} -> World // from position 6 to end",
-                      "${substring(-5)} -> World // last 5 characters" },
+                      "${substring(6)} -> World // when body is 'Hello World', 
clips the first 6 characters",
+                      "${substring(-6)} -> Hello // clips the last 6 
characters",
+                      "${substring(1,-1)} -> ello Worl // clips the first and 
the last character" },
               annotations = {
-                      "param=head:int:required::The start position 
(inclusive). Negative counts from end",
-                      "param=tail:int:optional::The end position (exclusive)" 
})
+                      "param=head:int:required::How many characters to clip 
from the beginning. When it is the only argument, a negative value clips that 
many characters from the end instead",
+                      "param=tail:int:optional::How many characters to clip 
from the end" })
     public static final String SUBSTRING = "substring(head,tail)";
 
     @Metadata(description = "Returns a substring of the message 
body/expression that comes after. Returns null if nothing comes after.",
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CollectionFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CollectionFunctionFactory.java
index f671dc96eb65..aee3d6a1a7a9 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CollectionFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CollectionFunctionFactory.java
@@ -107,7 +107,7 @@ public final class CollectionFunctionFactory implements 
SimpleLanguageFunctionFa
                 }
                 return 
CollectionExpressionBuilder.rangeExpression(tokens[0].trim(), tokens[1].trim());
             } else {
-                return CollectionExpressionBuilder.rangeExpression("1", 
values.trim());
+                return CollectionExpressionBuilder.rangeExpression("0", 
values.trim());
             }
         }
 
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
index 97ce538fc685..165cc21096e4 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
@@ -1862,6 +1862,15 @@ public class SimpleTest extends LanguageTestSupport {
         assertExpression("${substring(0,-99)}", "");
         assertExpression("${substring(99,0)}", "");
         assertExpression("${substring(0,0)}", "ABCDEFGHIJK");
+        // the sign of the tail does not matter
+        assertExpression("${substring(3,-2)}", "DEFGHI");
+        assertExpression("${substring(3,2)}", "DEFGHI");
+
+        // the examples in the simple catalog
+        exchange.getMessage().setBody("Hello World");
+        assertExpression("${substring(6)}", "World");
+        assertExpression("${substring(-6)}", "Hello");
+        assertExpression("${substring(1,-1)}", "ello Worl");
 
         exchange.getMessage().setBody("Hello World");
         exchange.getMessage().setHeader("foo", "1234567890");
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/CollectionFunctionFactoryTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/CollectionFunctionFactoryTest.java
index 3bf323a54c70..a1fbdb462d27 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/CollectionFunctionFactoryTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/CollectionFunctionFactoryTest.java
@@ -66,9 +66,9 @@ public class CollectionFunctionFactoryTest extends 
AbstractSimpleFunctionFactory
     @Test
     @SuppressWarnings("unchecked")
     public void testRangeMax() {
-        // range(max) uses min=1, exclusive upper: range(3) -> [1,2]
+        // range(max) starts at 0, exclusive upper (as Python): range(3) -> 
[0,1,2]
         List<Integer> result = evaluate("range(3)", List.class);
-        assertEquals(List.of(1, 2), result);
+        assertEquals(List.of(0, 1, 2), result);
     }
 
     // --- shuffle toString ---
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 1cc7bcf85852..de0993249bb3 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -37,6 +37,10 @@ body when the map has no `sku()` method, the same value 
`${body[sku]}` gives. A
 `${body.size}` calls `size()` as before, and a name that is neither a method 
nor a key still fails. Only expressions
 that used to throw can now return a value.
 
+The `range(max)` function now starts at 0, as documented and as `range` does 
in Python: `${range(5)}` returns
+`[0, 1, 2, 3, 4]` where it used to return `[1, 2, 3, 4]`. Routes that relied 
on the range starting at 1 must use
+`${range(1,5)}` instead. The two-argument form `range(min,max)` is unchanged.
+
 === Circuit Breaker EIP
 
 The exchange property `CamelCircuitBreakerResponseRejected` is now also set 
inside the `onFallback`,

Reply via email to