Skip to content

Commit d496f54

Browse files
authored
feat: define the behavior for handling empty arrays and non-array input in functions (#21)
1 parent 36678da commit d496f54

File tree

4 files changed

+157
-38
lines changed

4 files changed

+157
-38
lines changed

src/compile.test.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('error handling', () => {
9797
actualErr = err
9898
}
9999

100-
expect(actualErr?.message).toBe("Cannot read properties of null (reading 'reduce')")
100+
expect(actualErr?.message).toBe('Array expected')
101101
expect(actualErr?.jsonquery).toEqual([
102102
{ data: scoreData, query },
103103
{
@@ -108,18 +108,6 @@ describe('error handling', () => {
108108
{ data: null, query: ['sum'] }
109109
])
110110
})
111-
112-
test('should throw an error when calculating the sum of an empty array', () => {
113-
expect(() => go([], ['sum'])).toThrow('Reduce of empty array with no initial value')
114-
})
115-
116-
test('should throw an error when calculating the prod of an empty array', () => {
117-
expect(() => go([], ['prod'])).toThrow('Reduce of empty array with no initial value')
118-
})
119-
120-
test('should throw an error when calculating the average of an empty array', () => {
121-
expect(() => go([], ['average'])).toThrow('Reduce of empty array with no initial value')
122-
})
123111
})
124112

125113
describe('customization', () => {

src/compile.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { functions } from './functions'
1+
import { functions, throwTypeError } from './functions'
22
import { isArray, isObject } from './is'
33
import type {
44
Fun,
@@ -48,7 +48,3 @@ function compileFunction(query: JSONQueryFunction, functions: FunctionBuildersMa
4848

4949
return fnBuilder(...args)
5050
}
51-
52-
function throwTypeError(message: string): () => void {
53-
throw new Error(message)
54-
}

src/functions.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,13 @@ export const functions: FunctionBuildersMap = {
142142
const sign = direction === 'desc' ? -1 : 1
143143

144144
function compare(itemA: unknown, itemB: unknown) {
145-
const a = getter(itemA)
146-
const b = getter(itemB)
147-
return gt(a, b) ? sign : lt(a, b) ? -sign : 0
145+
try {
146+
const a = getter(itemA)
147+
const b = getter(itemB)
148+
return gt(a, b) ? sign : lt(a, b) ? -sign : 0
149+
} catch {
150+
return 0 // leave unsortable contents as-is
151+
}
148152
}
149153

150154
return (data: T[]) => data.slice().sort(compare)
@@ -258,21 +262,16 @@ export const functions: FunctionBuildersMap = {
258262
data.length,
259263

260264
keys: () => Object.keys,
261-
262265
values: () => Object.values,
263266

264-
prod: () => (data: number[]) => data.reduce((a, b) => a * b),
265-
266-
sum: () => (data: number[]) => data.reduce((a, b) => a + b),
267-
268-
average: () => (data: number[]) => (functions.sum()(data) as number) / data.length,
267+
prod: () => (data: number[]) => reduce(data, (a, b) => a * b),
268+
sum: () => (data: number[]) => reduce(data, (a, b) => a + b, 0),
269+
average: () => (data: number[]) => reduce(data, (a, b) => a + b) / data.length,
270+
min: () => (data: number[]) => reduce(data, (a, b) => Math.min(a, b), null),
271+
max: () => (data: number[]) => reduce(data, (a, b) => Math.max(a, b), null),
269272

270-
min: () => (data: number[]) => Math.min(...data),
271-
272-
max: () => (data: number[]) => Math.max(...data),
273-
274-
and: buildFunction((...args) => args.reduce((a, b) => !!(a && b))),
275-
or: buildFunction((...args) => args.reduce((a, b) => !!(a || b))),
273+
and: buildFunction((...args: unknown[]) => reduce(args, (a, b) => !!(a && b))),
274+
or: buildFunction((...args: unknown[]) => reduce(args, (a, b) => !!(a || b))),
276275
not: buildFunction((a: unknown) => !a),
277276

278277
exists: (queryGet: JSONQueryFunction) => {
@@ -343,3 +342,27 @@ export const functions: FunctionBuildersMap = {
343342
}
344343

345344
const truthy = (x: unknown) => x !== null && x !== 0 && x !== false
345+
346+
const reduce = <T>(
347+
data: T[],
348+
callback: (previousValue: T, currentValue: T) => T,
349+
initialValue?: T
350+
): T => {
351+
if (!isArray(data)) {
352+
throwTypeError('Array expected')
353+
}
354+
355+
if (initialValue !== undefined) {
356+
return data.reduce(callback, initialValue)
357+
}
358+
359+
if (data.length === 0) {
360+
throwTypeError('Non-empty array expected')
361+
}
362+
363+
return data.reduce(callback)
364+
}
365+
366+
export const throwTypeError = (message: string) => {
367+
throw new TypeError(message)
368+
}

test-suite/compile.test.json

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,17 +280,17 @@
280280
},
281281
{
282282
"category": "sort",
283-
"description": "should throw when sorting nested arrays",
284-
"input": [[1], [2], [3]],
283+
"description": "should leave content as-is when trying to sort nested arrays",
284+
"input": [[3], [1], [2]],
285285
"query": ["sort"],
286-
"throws": "Two numbers or two strings expected"
286+
"output": [[3], [1], [2]]
287287
},
288288
{
289289
"category": "sort",
290-
"description": "should throw when sorting nested objects",
290+
"description": "should leave content as-is when trying to sort nested objects",
291291
"input": [{ "a": 1 }, { "c": 3 }, { "b": 2 }],
292292
"query": ["sort"],
293-
"throws": "Two numbers or two strings expected"
293+
"output": [{ "a": 1 }, { "c": 3 }, { "b": 2 }]
294294
},
295295

296296
{
@@ -706,6 +706,20 @@
706706
"query": ["sum"],
707707
"output": 8.1
708708
},
709+
{
710+
"category": "sum",
711+
"description": "should return 0 when calculating the sum of an empty array",
712+
"input": [],
713+
"query": ["sum"],
714+
"output": 0
715+
},
716+
{
717+
"category": "sum",
718+
"description": "should throw an error when calculating the sum a string",
719+
"input": "abc",
720+
"query": ["sum"],
721+
"throws": "Array expected"
722+
},
709723

710724
{
711725
"category": "min",
@@ -714,6 +728,20 @@
714728
"query": ["min"],
715729
"output": -7
716730
},
731+
{
732+
"category": "min",
733+
"description": "should throw an error when calculating min of an empty array",
734+
"input": [],
735+
"query": ["min"],
736+
"output": null
737+
},
738+
{
739+
"category": "min",
740+
"description": "should throw an error when calculating min on a string",
741+
"input": "abc",
742+
"query": ["min"],
743+
"throws": "Array expected"
744+
},
717745

718746
{
719747
"category": "max",
@@ -722,6 +750,20 @@
722750
"query": ["max"],
723751
"output": 3
724752
},
753+
{
754+
"category": "max",
755+
"description": "should throw an error when calculating max of an empty array",
756+
"input": [],
757+
"query": ["max"],
758+
"output": null
759+
},
760+
{
761+
"category": "max",
762+
"description": "should throw an error when calculating max on a string",
763+
"input": "abc",
764+
"query": ["max"],
765+
"throws": "Array expected"
766+
},
725767

726768
{
727769
"category": "prod",
@@ -730,6 +772,20 @@
730772
"query": ["prod"],
731773
"output": 30
732774
},
775+
{
776+
"category": "prod",
777+
"description": "should throw an error when calculating the prod of an empty array",
778+
"input": [],
779+
"query": ["prod"],
780+
"throws": "Non-empty array expected"
781+
},
782+
{
783+
"category": "prod",
784+
"description": "should throw an error when calculating the prod a string",
785+
"input": "abc",
786+
"query": ["prod"],
787+
"throws": "Array expected"
788+
},
733789

734790
{
735791
"category": "average",
@@ -745,6 +801,20 @@
745801
"query": ["average"],
746802
"output": 3
747803
},
804+
{
805+
"category": "average",
806+
"description": "should throw an error when calculating the average of an empty array",
807+
"input": [],
808+
"query": ["average"],
809+
"throws": "Non-empty array expected"
810+
},
811+
{
812+
"category": "average",
813+
"description": "should throw an error when calculating the average a string",
814+
"input": "abc",
815+
"query": ["average"],
816+
"throws": "Array expected"
817+
},
748818

749819
{
750820
"category": "eq",
@@ -1377,6 +1447,27 @@
13771447
"query": ["and", true, true, false],
13781448
"output": false
13791449
},
1450+
{
1451+
"category": "and",
1452+
"description": "should calculate and with one argument (1)",
1453+
"input": null,
1454+
"query": ["and", false],
1455+
"output": false
1456+
},
1457+
{
1458+
"category": "and",
1459+
"description": "should calculate and with one argument (2)",
1460+
"input": null,
1461+
"query": ["and", true],
1462+
"output": true
1463+
},
1464+
{
1465+
"category": "and",
1466+
"description": "should throw when calculating and with no arguments",
1467+
"input": null,
1468+
"query": ["and"],
1469+
"throws": "Non-empty array expected"
1470+
},
13801471

13811472
{
13821473
"category": "or",
@@ -1434,6 +1525,27 @@
14341525
"query": ["or", false, false, true],
14351526
"output": true
14361527
},
1528+
{
1529+
"category": "or",
1530+
"description": "should calculate or with one argument (1)",
1531+
"input": null,
1532+
"query": ["or", false],
1533+
"output": false
1534+
},
1535+
{
1536+
"category": "or",
1537+
"description": "should calculate or with one argument (2)",
1538+
"input": null,
1539+
"query": ["or", true],
1540+
"output": true
1541+
},
1542+
{
1543+
"category": "or",
1544+
"description": "should throw when calculating or with no arguments",
1545+
"input": null,
1546+
"query": ["or"],
1547+
"throws": "Non-empty array expected"
1548+
},
14371549

14381550
{
14391551
"category": "not",

0 commit comments

Comments
 (0)