Skip to content

Commit fb80ccf

Browse files
jhorbulykandrew-coleman
authored andcommitted
Add $single() function. (#355)
1 parent 65fd351 commit fb80ccf

File tree

14 files changed

+156
-16
lines changed

14 files changed

+156
-16
lines changed

docs/higher-order-functions.md

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ __Signature:__ `$map(array, function)`
99

1010
Returns an array containing the results of applying the `function` parameter to each value in the `array` parameter.
1111

12-
The function that is supplied as the second parameter must have the following signature:
12+
The function that is supplied as the second parameter must have the following signature:
1313

14-
`function(value [, index [, array]])`
15-
16-
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.
14+
`function(value [, index [, array]])`
1715

18-
__Examples__
16+
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.
17+
18+
__Examples__
1919
- `$map([1..5], $string)` => `["1", "2", "3", "4", "5"]`
2020

2121
With user-defined (lambda) function:
@@ -41,11 +41,11 @@ __Signature:__ `$filter(array, function)`
4141

4242
Returns an array containing only the values in the `array` parameter that satisfy the `function` predicate (i.e. `function` returns Boolean `true` when passed the value).
4343

44-
The function that is supplied as the second parameter must have the following signature:
44+
The function that is supplied as the second parameter must have the following signature:
45+
46+
`function(value [, index [, array]])`
4547

46-
`function(value [, index [, array]])`
47-
48-
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.
48+
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.
4949

5050
__Example__
5151
The following expression returns all the products whose price is higher than average:
@@ -55,6 +55,25 @@ $filter(Account.Order.Product, function($v, $i, $a) {
5555
})
5656
```
5757

58+
## `$single()`
59+
__Signature:__ `$single(array, function)`
60+
61+
Returns the one and only one value in the `array` parameter that satisfy the `function` predicate (i.e. `function` returns Boolean `true` when passed the value). Throws an exception if the number of matching values is not exactly one.
62+
63+
The function that is supplied as the second parameter must have the following signature:
64+
65+
`function(value [, index [, array]])`
66+
67+
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.
68+
69+
__Example__
70+
The following expression the product in the order whose SKU is `"0406654608"`:
71+
```
72+
$single(Account.Order.Product, function($v, $i, $a) {
73+
$v.SKU = "0406654608"
74+
})
75+
```
76+
5877
## `$reduce()`
5978
__Signature:__ `$reduce(array, function [, init])`
6079

@@ -82,11 +101,11 @@ Returns an object that contains only the key/value pairs from the `object` param
82101

83102
If `object` is not specified, then the context value is used as the value of `object`. It is an error if `object` is not an object.
84103

85-
The function that is supplied as the second parameter must have the following signature:
104+
The function that is supplied as the second parameter must have the following signature:
105+
106+
`function(value [, key [, object]])`
86107

87-
`function(value [, key [, object]])`
88-
89-
Each value in the input object is passed in as the first parameter in the supplied function. The key (property name) of that value in the input object is passed in as the second parameter, if specified. The whole input object is passed in as the third parameter, if specified.
108+
Each value in the input object is passed in as the first parameter in the supplied function. The key (property name) of that value in the input object is passed in as the second parameter, if specified. The whole input object is passed in as the third parameter, if specified.
90109

91110
__Example__
92111

src/functions.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,7 +1325,7 @@ const functions = (() => {
13251325

13261326
/**
13271327
* Helper function to build the arguments to be supplied to the function arg of the
1328-
* HOFs map, filter, each and sift
1328+
* HOFs map, filter, each, sift and single
13291329
* @param {function} func - the function to be invoked
13301330
* @param {*} arg1 - the first (required) arg - the value
13311331
* @param {*} arg2 - the second (optional) arg - the position (index or key)
@@ -1398,6 +1398,55 @@ const functions = (() => {
13981398
return result;
13991399
}
14001400

1401+
/**
1402+
* Given an array, find the single element matching a specified condition
1403+
* Throws an exception if the number of matching elements is not exactly one
1404+
* @param {Array} [arr] - array to filter
1405+
* @param {Function} [func] - predicate function
1406+
* @returns {*} Matching element
1407+
*/
1408+
function* single(arr, func) { // eslint-disable-line require-yield
1409+
// undefined inputs always return undefined
1410+
if (typeof arr === 'undefined') {
1411+
return undefined;
1412+
}
1413+
1414+
var hasFoundMatch = false;
1415+
var result;
1416+
1417+
for (var i = 0; i < arr.length; i++) {
1418+
var entry = arr[i];
1419+
var positiveResult = true;
1420+
if (typeof func !== 'undefined') {
1421+
var func_args = hofFuncArgs(func, entry, i, arr);
1422+
// invoke func
1423+
var res = yield* func.apply(this, func_args);
1424+
positiveResult = boolean(res);
1425+
}
1426+
if (positiveResult) {
1427+
if(!hasFoundMatch) {
1428+
result = entry;
1429+
hasFoundMatch = true;
1430+
} else {
1431+
throw {
1432+
stack: (new Error()).stack,
1433+
code: "D3138",
1434+
index: i
1435+
};
1436+
}
1437+
}
1438+
}
1439+
1440+
if(!hasFoundMatch) {
1441+
throw {
1442+
stack: (new Error()).stack,
1443+
code: "D3139"
1444+
};
1445+
}
1446+
1447+
return result;
1448+
}
1449+
14011450
/**
14021451
* Convolves (zips) each value from a set of arrays
14031452
* @param {Array} [args] - arrays to zip
@@ -1790,7 +1839,7 @@ const functions = (() => {
17901839
match, contains, replace, split, join,
17911840
formatNumber, formatBase, number, floor, ceil, round, abs, sqrt, power, random,
17921841
boolean, not,
1793-
map, zip, filter, foldLeft, sift,
1842+
map, zip, filter, single, foldLeft, sift,
17941843
keys, lookup, append, exists, spread, merge, reverse, each, error, sort, shuffle,
17951844
base64encode, base64decode
17961845
};

src/jsonata.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,7 @@ var jsonata = (function() {
16891689
staticFrame.bind('map', defineFunction(fn.map, '<af>'));
16901690
staticFrame.bind('zip', defineFunction(fn.zip, '<a+>'));
16911691
staticFrame.bind('filter', defineFunction(fn.filter, '<af>'));
1692+
staticFrame.bind('single', defineFunction(fn.single, '<af?>'));
16921693
staticFrame.bind('reduce', defineFunction(fn.foldLeft, '<afj?:j>')); // TODO <f<jj:j>a<j>j?:j>
16931694
staticFrame.bind('sift', defineFunction(fn.sift, '<o-f?:o>'));
16941695
staticFrame.bind('keys', defineFunction(fn.keys, '<x-:a<s>>'));
@@ -1811,7 +1812,9 @@ var jsonata = (function() {
18111812
"D3134": "The timezone integer format specifier cannot have more than four digits",
18121813
"D3135": "No matching closing bracket ']' in date/time picture string",
18131814
"D3136": "The date/time picture string is missing specifiers required to parse the timestamp",
1814-
"D3137": "{{{message}}}"
1815+
"D3137": "{{{message}}}",
1816+
"D3138": "The $single() function expected exactly 1 matching result. Instead it matched more.",
1817+
"D3139": "The $single() function expected exactly 1 matching result. Instead it matched 0."
18151818
};
18161819

18171820
/**
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expr": "(library.books~>$single(λ($v, $i, $a) {$v.price = $max($a.price)})).isbn",
3+
"dataset": "dataset6",
4+
"bindings": {},
5+
"result": "9780262510875"
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expr": "nothing~>$single(λ($v, $i, $a) {$v.price = $max($a.price)})",
3+
"dataset": "dataset6",
4+
"bindings": {},
5+
"undefinedResult": true
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expr": "$single(data, function($d) { true })",
3+
"data": {"data": 5},
4+
"bindings": {},
5+
"result": 5
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expr": "$single([0, 1, 2], $boolean)",
3+
"data": {"data": 5},
4+
"bindings": {},
5+
"code": "D3138"
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expr": "$single([0, 1, 2], function($v) {$v = 3})",
3+
"data": {"data": 5},
4+
"bindings": {},
5+
"code": "D3139"
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expr": "$single([0, 1, 2])",
3+
"data": {"data": 5},
4+
"bindings": {},
5+
"code": "D3138"
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expr": "$single([0])",
3+
"data": {"data": 5},
4+
"bindings": {},
5+
"result": 0
6+
}

0 commit comments

Comments
 (0)