Skip to content

Commit 8b9e7ed

Browse files
committed
feat: max_by and min_by function
1 parent 918d346 commit 8b9e7ed

File tree

2 files changed

+123
-14
lines changed

2 files changed

+123
-14
lines changed

Diff for: packages/jmespath/src/functions/Functions.ts

+104
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,58 @@ class Functions {
183183
}
184184
}
185185

186+
/**
187+
* Get the item in the provided array that has the maximum value when the provided expression is evaluated.
188+
*
189+
* @param args The array of items to get the maximum value of
190+
* @param expression The expression to evaluate for each item in the array
191+
* @returns The item in the array that has the maximum value when the expression is evaluated
192+
*/
193+
@Functions.signature({
194+
argumentsSpecs: [['array'], ['expression']],
195+
})
196+
public funcMaxBy(
197+
args: Array<JSONObject>,
198+
expression: Expression
199+
): JSONObject | null {
200+
if (args.length === 0) {
201+
return null;
202+
}
203+
204+
const visitedArgs = args.map((arg) => ({
205+
arg,
206+
visited: expression.visit(arg),
207+
}));
208+
209+
const max = visitedArgs.reduce((max, current) => {
210+
const type = getType(current.visited);
211+
if (type !== 'string' && type !== 'number') {
212+
throw new JMESPathTypeError({
213+
currentValue: current.visited,
214+
expectedTypes: ['string'],
215+
actualType: type,
216+
});
217+
}
218+
219+
if (
220+
(max.visited === null || max.visited === undefined) &&
221+
(current.visited === null || current.visited === undefined)
222+
) {
223+
return max;
224+
} else if (max.visited === null || max.visited === undefined) {
225+
return current;
226+
} else if (current.visited === null || current.visited === undefined) {
227+
return max;
228+
} else if (max.visited === current.visited) {
229+
return max;
230+
} else {
231+
return max.visited > current.visited ? max : current;
232+
}
233+
}, visitedArgs[0]);
234+
235+
return max.arg;
236+
}
237+
186238
/**
187239
* Merge the provided objects into a single object.
188240
*
@@ -219,6 +271,58 @@ class Functions {
219271
}
220272
}
221273

274+
/**
275+
* Get the item in the provided array that has the minimum value when the provided expression is evaluated.
276+
*
277+
* @param args The array of items to get the minimum value of
278+
* @param expression The expression to evaluate for each item in the array
279+
* @returns The item in the array that has the minimum value when the expression is evaluated
280+
*/
281+
@Functions.signature({
282+
argumentsSpecs: [['array'], ['expression']],
283+
})
284+
public funcMinBy(
285+
args: Array<JSONObject>,
286+
expression: Expression
287+
): JSONObject | null {
288+
if (args.length === 0) {
289+
return null;
290+
}
291+
292+
const visitedArgs = args.map((arg) => ({
293+
arg,
294+
visited: expression.visit(arg),
295+
}));
296+
297+
const min = visitedArgs.reduce((min, current) => {
298+
const type = getType(current.visited);
299+
if (type !== 'string' && type !== 'number') {
300+
throw new JMESPathTypeError({
301+
currentValue: current.visited,
302+
expectedTypes: ['string'],
303+
actualType: type,
304+
});
305+
}
306+
307+
if (
308+
(min.visited === null || min.visited === undefined) &&
309+
(current.visited === null || current.visited === undefined)
310+
) {
311+
return min;
312+
} else if (min.visited === null || min.visited === undefined) {
313+
return current;
314+
} else if (current.visited === null || current.visited === undefined) {
315+
return min;
316+
} else if (min.visited === current.visited) {
317+
return min;
318+
} else {
319+
return min.visited < current.visited ? min : current;
320+
}
321+
}, visitedArgs[0]);
322+
323+
return min.arg;
324+
}
325+
222326
/**
223327
* Get the first argument that does not evaluate to null.
224328
* If all arguments evaluate to null, then null is returned.

Diff for: packages/jmespath/tests/unit/functions.test.ts

+19-14
Original file line numberDiff line numberDiff line change
@@ -1849,7 +1849,7 @@ describe('Functions tests', () => {
18491849
expect(() => search(expression, data)).toThrow(error);
18501850
});
18511851

1852-
/* it.each([
1852+
it.each([
18531853
{
18541854
expression: 'max_by(people, &age)',
18551855
expected: {
@@ -1921,18 +1921,20 @@ describe('Functions tests', () => {
19211921

19221922
// Assess
19231923
expect(result).toStrictEqual(expected);
1924-
}); */
1925-
/* it.each([
1924+
});
1925+
1926+
it.each([
19261927
{
19271928
expression: 'max_by(people, &bool)',
1928-
error: 'TypeError: expected one of (number | string), received boolean',
1929+
error:
1930+
'Invalid argument type for function max_by(), expected "string" but found "boolean" in expression: max_by(people, &bool)',
19291931
},
19301932
{
19311933
expression: 'max_by(people, &extra)',
1932-
error: 'TypeError: expected one of (number | string), received null',
1934+
error:
1935+
'Invalid argument type for function max_by(), expected "string" but found "null" in expression: max_by(people, &extra)',
19331936
},
19341937
])('max_by() function special cases errors', ({ expression, error }) => {
1935-
// TODO: see if we can assert the error type as well in max_by() function special cases errors tests
19361938
// Prepare
19371939
const data = {
19381940
people: [
@@ -1973,8 +1975,9 @@ describe('Functions tests', () => {
19731975

19741976
// Act & Assess
19751977
expect(() => search(expression, data)).toThrow(error);
1976-
}); */
1977-
/* it.each([
1978+
});
1979+
1980+
it.each([
19781981
{
19791982
expression: 'min_by(people, &age)',
19801983
expected: {
@@ -2046,18 +2049,20 @@ describe('Functions tests', () => {
20462049

20472050
// Assess
20482051
expect(result).toStrictEqual(expected);
2049-
}); */
2050-
/* it.each([
2052+
});
2053+
2054+
it.each([
20512055
{
20522056
expression: 'min_by(people, &bool)',
2053-
error: 'TypeError: expected one of (number | string), received boolean',
2057+
error:
2058+
'Invalid argument type for function min_by(), expected "string" but found "boolean" in expression: min_by(people, &bool)',
20542059
},
20552060
{
20562061
expression: 'min_by(people, &extra)',
2057-
error: 'TypeError: expected one of (number | string), received null',
2062+
error:
2063+
'Invalid argument type for function min_by(), expected "string" but found "null" in expression: min_by(people, &extra)',
20582064
},
20592065
])('min_by() function special cases errors', ({ expression, error }) => {
2060-
// TODO: see if we can assert the error type as well in min_by() function special cases errors tests
20612066
// Prepare
20622067
const data = {
20632068
people: [
@@ -2099,7 +2104,7 @@ describe('Functions tests', () => {
20992104
// Act & Assess
21002105
expect(() => search(expression, data)).toThrow(error);
21012106
});
2102-
*/
2107+
21032108
it.each([
21042109
{
21052110
description: 'stable sort order',

0 commit comments

Comments
 (0)