From 2bfd74d789e3bd576266ba6988ae85f70412a2ef Mon Sep 17 00:00:00 2001 From: Andreas Eberhart Date: Wed, 4 Nov 2026 16:10:03 +0100 Subject: [PATCH 1/2] $each / $merge iterate arrays like JS for..in JS "for (var key in obj)" also iterates arrays, using the indexes as string keys. Arrays only reach these functions where the signature is not validated (e.g. partial application, see #79 / #95), where the Java port threw a ClassCastException. Co-Authored-By: Claude Opus 5.5 --- .../java/com/dashjoin/jsonata/Functions.java | 39 ++++++-- .../com/dashjoin/jsonata/EachMergeTest.java | 92 +++++++++++++++++++ 2 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 src/test/java/com/dashjoin/jsonata/EachMergeTest.java diff --git a/src/main/java/com/dashjoin/jsonata/Functions.java b/src/main/java/com/dashjoin/jsonata/Functions.java index 4619d53..aa82aa1 100644 --- a/src/main/java/com/dashjoin/jsonata/Functions.java +++ b/src/main/java/com/dashjoin/jsonata/Functions.java @@ -1831,8 +1831,15 @@ public static Object merge(List arg) { var result = new LinkedHashMap<>(); for (Object obj : arg) { - for (Entry entry : ((Map)obj).entrySet()) { - result.put(entry.getKey(), entry.getValue()); + if (obj instanceof List) { + // Java: JS "for (var prop in obj)" also iterates arrays (keys are the indexes as strings). + // Can happen where the signature is not validated, e.g. $merge(?) + for (int i=0; i<((List)obj).size(); i++) + result.put(""+i, ((List)obj).get(i)); + } else if (obj instanceof Map) { + for (Entry entry : ((Map)obj).entrySet()) { + result.put(entry.getKey(), entry.getValue()); + } } } return result; @@ -1865,19 +1872,33 @@ public static List reverse(List arr) { * @throws Throwable * @returns {Array} - the resultant array */ - public static List each(Map obj, Object func) throws Throwable { + public static List each(Object obj, Object func) throws Throwable { if (obj==null) { return null; } var result = createSequence(); - for (var key : obj.keySet()) { - var func_args = hofFuncArgs(func, obj.get(key), key, obj); - // invoke func - var val = funcApply(func, func_args); - if(val != null) { - result.add(val); + if (obj instanceof List) { + // Java: JS "for (var key in obj)" also iterates arrays (keys are the indexes as strings). + // Can happen where the signature is not validated, e.g. $each(?, ...) + var list = (List)obj; + for (int i=0; i Functions.each(args.get(0), args.get(1)), null)); + e.registerFunction("mergeRaw", + new JFunction((in, args) -> Functions.merge((List) args.get(0)), null)); + return e.evaluate(input); + } + + @Test + public void eachArrayKeys() { + Assertions.assertEquals(List.of("0", "1"), eval("$eachRaw([5,6], function($v,$k) { $k })", null)); + } + + @Test + public void eachArrayValues() { + Assertions.assertEquals(List.of(5, 6), eval("$eachRaw([5,6], function($v) { $v })", null)); + } + + @Test + public void eachArrayAllArgs() { + Assertions.assertEquals(List.of(Map.of("v", 5, "k", "0", "o", List.of(5))), + eval("[$eachRaw($, function($v,$k,$o) { {'v': $v, 'k': $k, 'o': $o} })]", List.of(5))); + } + + @Test + public void eachArraySkipsUndefined() { + Assertions.assertEquals(List.of(6, 7), eval("$eachRaw([5,6,7], function($v) { $v > 5 ? $v })", null)); + } + + @Test + public void eachEmptyArray() { + Assertions.assertNull(eval("$eachRaw([], function($v) { $v })", null)); + } + + @Test + public void eachUndefined() { + Assertions.assertNull(eval("$eachRaw(nothing, function($v) { $v })", null)); + } + + @Test + public void eachMap() { + Assertions.assertEquals(List.of("a=1", "b=2"), + eval("$eachRaw({'a': 1, 'b': 2}, function($v, $k) { $k & '=' & $v })", null)); + } + + @Test + public void mergeArray() { + Assertions.assertEquals(Map.of("0", 5, "1", 6), eval("$mergeRaw([[5,6]])", null)); + } + + @Test + public void mergeArrayAndMap() { + Assertions.assertEquals(Map.of("test", "data"), + eval("$mergeRaw($)", List.of(List.of(), Map.of("test", "data")))); + } + + @Test + public void mergeMaps() { + Assertions.assertEquals(Map.of("a", 1, "b", 3), eval("$merge([{'a':1,'b':2},{'b':3}])", null)); + } + + @Test + public void eachMapExpression() { + Assertions.assertEquals(List.of("a=1", "b=2"), + jsonata("$each({'a': 1, 'b': 2}, function($v, $k) { $k & '=' & $v })").evaluate(null)); + } + + @Test + public void eachArrayExpressionRejected() { + // the signature still rejects arrays when called directly + var ex = Assertions.assertThrows(JException.class, + () -> jsonata("$each([5,6], function($v,$k) { $k })").evaluate(null)); + Assertions.assertEquals("T0410", ex.getError()); + } +} From d680e54aa2d594585e8ac00a3fbc215276e4d1a2 Mon Sep 17 00:00:00 2001 From: Andreas Eberhart Date: Thu, 5 Nov 2026 10:03:06 +0100 Subject: [PATCH 2/2] Use for-each and Integer.toString for array keys in $each / $merge Addresses review comments on #131. Co-Authored-By: Claude Opus 5.5 --- src/main/java/com/dashjoin/jsonata/Functions.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/dashjoin/jsonata/Functions.java b/src/main/java/com/dashjoin/jsonata/Functions.java index aa82aa1..e2f94e8 100644 --- a/src/main/java/com/dashjoin/jsonata/Functions.java +++ b/src/main/java/com/dashjoin/jsonata/Functions.java @@ -1834,8 +1834,9 @@ public static Object merge(List arg) { if (obj instanceof List) { // Java: JS "for (var prop in obj)" also iterates arrays (keys are the indexes as strings). // Can happen where the signature is not validated, e.g. $merge(?) - for (int i=0; i<((List)obj).size(); i++) - result.put(""+i, ((List)obj).get(i)); + int i = 0; + for (Object item : (List)obj) + result.put(Integer.toString(i++), item); } else if (obj instanceof Map) { for (Entry entry : ((Map)obj).entrySet()) { result.put(entry.getKey(), entry.getValue()); @@ -1882,9 +1883,9 @@ public static List each(Object obj, Object func) throws Throwable { if (obj instanceof List) { // Java: JS "for (var key in obj)" also iterates arrays (keys are the indexes as strings). // Can happen where the signature is not validated, e.g. $each(?, ...) - var list = (List)obj; - for (int i=0; i