Repository: jsonata-js/jsonata (js)

Issue:
Need a good way to locate elements in an array, or at least that element's index
I'm trying to do something that I thought would be easy, but running into some roadblocks...

As an example, let's try to convert an input Month abbreviation into the Date month number (0-based). It's easy to find out if the incoming string is in the array of months, using `payload in $months`, but finding out what position it occupies in the array is not so easy:
```
(
    $months := [
        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    ];
    $months~>$map(function($val, $idx) {
        $val = payload ? $idx : undefined
    })
)
```
A bit clunky, perhaps, but it returns the index number for every array element that is equal to the incoming string. I tried using the `$reduce($array, function($total, $value))` -- but unlike the Javascript function of the same name, there is no index value passed as the 3rd argument. **At a minimum, I think the `$reduce()` function should be extended to also pass $index as an optional argument**.

But perhaps a pair of new functions would be more intuitive and simpler to use, such as:
* `$find(array, input)` and the overloaded `$find(array, function($value))`
* `$indexOf(array, input)` and the overloaded `$indexOf(array, function($value))`

Both functions take an array of "things", and either a similar "thing" to find, or a function that determines which element is a match (returns true or false). 

Pull request description:
Enhance $reduce to support 4 arg function
The second argument can now be an arity-4 function with the following signature
`myfunc($accumulator, $value[, $index[, $array]])`

resolves #102 

Patch:
diff --git a/docs/higher-order-functions.md b/docs/higher-order-functions.md
index 598881a..95ea8bd 100644
--- a/docs/higher-order-functions.md
+++ b/docs/higher-order-functions.md
@@ -47,7 +47,7 @@ The function that is supplied as the second parameter must have the following si
 
 Each value in the input array is passed in as the first parameter in the supplied function.  The index (position) of that value in the input array is passed in as the second parameter, if specified.  The whole input array is passed in as the third parameter, if specified.
 
-__Example__  
+__Example__
 The following expression returns all the products whose price is higher than average:
 ```
 $filter(Account.Order.Product, function($v, $i, $a) {
@@ -66,7 +66,7 @@ The function that is supplied as the second parameter must have the following si
 
 Each value in the input array is passed in as the first parameter in the supplied function.  The index (position) of that value in the input array is passed in as the second parameter, if specified.  The whole input array is passed in as the third parameter, if specified.
 
-__Example__  
+__Example__
 The following expression the product in the order whose SKU is `"0406654608"`:
 ```
 $single(Account.Order.Product, function($v, $i, $a) {
@@ -79,7 +79,9 @@ __Signature:__ `$reduce(array, function [, init])`
 
 Returns an aggregated value derived from applying the `function` parameter successively to each value in `array` in combination with the result of the previous application of the function.
 
-The function must accept two arguments, and behaves like an infix operator between each value within the `array`.
+The `function` must accept at least two arguments, and behaves like an infix operator between each value within the `array`.  The signature of this supplied function must be of the form:
+
+`myfunc($accumulator, $value[, $index[, $array]])`
 
 __Example__
 
diff --git a/src/functions.js b/src/functions.js
index 7c0fa07..e0a7a3b 100644
--- a/src/functions.js
+++ b/src/functions.js
@@ -1594,7 +1594,8 @@ const functions = (() => {
 
         var result;
 
-        if (getFunctionArity(func) !== 2) {
+        var arity = getFunctionArity(func);
+        if (arity < 2) {
             throw {
                 stack: (new Error()).stack,
                 code: "D3050",
@@ -1612,7 +1613,14 @@ const functions = (() => {
         }
 
         while (index < sequence.length) {
-            result = yield* func.apply(this, [result, sequence[index]]);
+            var args = [result, sequence[index]];
+            if (arity >= 3) {
+                args.push(index);
+            }
+            if (arity >= 4) {
+                args.push(sequence);
+            }
+            result = yield* func.apply(this, args);
             index++;
         }
 
diff --git a/src/jsonata.js b/src/jsonata.js
index b82b7a7..0ecef20 100644
--- a/src/jsonata.js
+++ b/src/jsonata.js
@@ -1787,7 +1787,7 @@ var jsonata = (function() {
         "D3020": "Third argument of split function must evaluate to a positive number",
         "D3030": "Unable to cast value to a number: {{value}}",
         "D3040": "Third argument of match function must evaluate to a positive number",
-        "D3050": "First argument of reduce function must be a function with two arguments",
+        "D3050": "The second argument of reduce function must be a function with at least two arguments",
         "D3060": "The sqrt function cannot be applied to a negative number: {{value}}",
         "D3061": "The power function has resulted in a value that cannot be represented as a JSON number: base={{value}}, exponent={{exp}}",
         "D3070": "The single argument form of the sort function can only be applied to an array of strings or an array of numbers.  Use the second argument to specify a comparison function",


Test patch:
diff --git a/test/test-suite/groups/hof-reduce/case009.json b/test/test-suite/groups/hof-reduce/case009.json
new file mode 100644
index 0000000..e5a42ff
--- /dev/null
+++ b/test/test-suite/groups/hof-reduce/case009.json
@@ -0,0 +1,6 @@
+{
+    "expr-file": "case009.jsonata",
+    "data": null,
+    "bindings": {},
+    "result": 4
+}
diff --git a/test/test-suite/groups/hof-reduce/case009.jsonata b/test/test-suite/groups/hof-reduce/case009.jsonata
new file mode 100644
index 0000000..371c2fc
--- /dev/null
+++ b/test/test-suite/groups/hof-reduce/case009.jsonata
@@ -0,0 +1,12 @@
+(
+ $months := [
+        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+ ];
+ $indexof := λ($array, $value) {
+  $reduce($array, λ($acc, $v, $i) {
+   $v=$value ? $i : $acc
+  })
+ };
+ $indexof($months, 'May')
+)
diff --git a/test/test-suite/groups/hof-reduce/case010.json b/test/test-suite/groups/hof-reduce/case010.json
new file mode 100644
index 0000000..b80b2f6
--- /dev/null
+++ b/test/test-suite/groups/hof-reduce/case010.json
@@ -0,0 +1,6 @@
+{
+    "expr-file": "case010.jsonata",
+    "data": null,
+    "bindings": {},
+    "result": 6
+}
diff --git a/test/test-suite/groups/hof-reduce/case010.jsonata b/test/test-suite/groups/hof-reduce/case010.jsonata
new file mode 100644
index 0000000..e7f0d6c
--- /dev/null
+++ b/test/test-suite/groups/hof-reduce/case010.jsonata
@@ -0,0 +1,9 @@
+(
+  $mean := $reduce(?, λ($acc, $v, $i, $arr) {(
+    $total := $acc + $v;
+    $length := $count($arr);
+    $i = $length - 1 ? $total / $length : $total
+  )});
+
+  $mean([7,3,8])
+)