diff --git a/src/compile.test.ts b/src/compile.test.ts index 347524d..48bb969 100644 --- a/src/compile.test.ts +++ b/src/compile.test.ts @@ -97,7 +97,7 @@ describe('error handling', () => { actualErr = err } - expect(actualErr?.message).toBe("Cannot read properties of null (reading 'reduce')") + expect(actualErr?.message).toBe('Array expected') expect(actualErr?.jsonquery).toEqual([ { data: scoreData, query }, { @@ -108,18 +108,6 @@ describe('error handling', () => { { data: null, query: ['sum'] } ]) }) - - test('should throw an error when calculating the sum of an empty array', () => { - expect(() => go([], ['sum'])).toThrow('Reduce of empty array with no initial value') - }) - - test('should throw an error when calculating the prod of an empty array', () => { - expect(() => go([], ['prod'])).toThrow('Reduce of empty array with no initial value') - }) - - test('should throw an error when calculating the average of an empty array', () => { - expect(() => go([], ['average'])).toThrow('Reduce of empty array with no initial value') - }) }) describe('customization', () => { diff --git a/src/compile.ts b/src/compile.ts index 42433b1..3faf524 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -1,4 +1,4 @@ -import { functions } from './functions' +import { functions, throwTypeError } from './functions' import { isArray, isObject } from './is' import type { Fun, @@ -48,7 +48,3 @@ function compileFunction(query: JSONQueryFunction, functions: FunctionBuildersMa return fnBuilder(...args) } - -function throwTypeError(message: string): () => void { - throw new Error(message) -} diff --git a/src/functions.ts b/src/functions.ts index 6a50883..2124d11 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -142,9 +142,13 @@ export const functions: FunctionBuildersMap = { const sign = direction === 'desc' ? -1 : 1 function compare(itemA: unknown, itemB: unknown) { - const a = getter(itemA) - const b = getter(itemB) - return gt(a, b) ? sign : lt(a, b) ? -sign : 0 + try { + const a = getter(itemA) + const b = getter(itemB) + return gt(a, b) ? sign : lt(a, b) ? -sign : 0 + } catch { + return 0 // leave unsortable contents as-is + } } return (data: T[]) => data.slice().sort(compare) @@ -258,21 +262,16 @@ export const functions: FunctionBuildersMap = { data.length, keys: () => Object.keys, - values: () => Object.values, - prod: () => (data: number[]) => data.reduce((a, b) => a * b), - - sum: () => (data: number[]) => data.reduce((a, b) => a + b), - - average: () => (data: number[]) => (functions.sum()(data) as number) / data.length, + prod: () => (data: number[]) => reduce(data, (a, b) => a * b), + sum: () => (data: number[]) => reduce(data, (a, b) => a + b, 0), + average: () => (data: number[]) => reduce(data, (a, b) => a + b) / data.length, + min: () => (data: number[]) => reduce(data, (a, b) => Math.min(a, b), null), + max: () => (data: number[]) => reduce(data, (a, b) => Math.max(a, b), null), - min: () => (data: number[]) => Math.min(...data), - - max: () => (data: number[]) => Math.max(...data), - - and: buildFunction((...args) => args.reduce((a, b) => !!(a && b))), - or: buildFunction((...args) => args.reduce((a, b) => !!(a || b))), + and: buildFunction((...args: unknown[]) => reduce(args, (a, b) => !!(a && b))), + or: buildFunction((...args: unknown[]) => reduce(args, (a, b) => !!(a || b))), not: buildFunction((a: unknown) => !a), exists: (queryGet: JSONQueryFunction) => { @@ -343,3 +342,27 @@ export const functions: FunctionBuildersMap = { } const truthy = (x: unknown) => x !== null && x !== 0 && x !== false + +const reduce = ( + data: T[], + callback: (previousValue: T, currentValue: T) => T, + initialValue?: T +): T => { + if (!isArray(data)) { + throwTypeError('Array expected') + } + + if (initialValue !== undefined) { + return data.reduce(callback, initialValue) + } + + if (data.length === 0) { + throwTypeError('Non-empty array expected') + } + + return data.reduce(callback) +} + +export const throwTypeError = (message: string) => { + throw new TypeError(message) +} diff --git a/test-suite/compile.test.json b/test-suite/compile.test.json index bb622f1..fad2c0f 100644 --- a/test-suite/compile.test.json +++ b/test-suite/compile.test.json @@ -280,17 +280,17 @@ }, { "category": "sort", - "description": "should throw when sorting nested arrays", - "input": [[1], [2], [3]], + "description": "should leave content as-is when trying to sort nested arrays", + "input": [[3], [1], [2]], "query": ["sort"], - "throws": "Two numbers or two strings expected" + "output": [[3], [1], [2]] }, { "category": "sort", - "description": "should throw when sorting nested objects", + "description": "should leave content as-is when trying to sort nested objects", "input": [{ "a": 1 }, { "c": 3 }, { "b": 2 }], "query": ["sort"], - "throws": "Two numbers or two strings expected" + "output": [{ "a": 1 }, { "c": 3 }, { "b": 2 }] }, { @@ -706,6 +706,20 @@ "query": ["sum"], "output": 8.1 }, + { + "category": "sum", + "description": "should return 0 when calculating the sum of an empty array", + "input": [], + "query": ["sum"], + "output": 0 + }, + { + "category": "sum", + "description": "should throw an error when calculating the sum a string", + "input": "abc", + "query": ["sum"], + "throws": "Array expected" + }, { "category": "min", @@ -714,6 +728,20 @@ "query": ["min"], "output": -7 }, + { + "category": "min", + "description": "should throw an error when calculating min of an empty array", + "input": [], + "query": ["min"], + "output": null + }, + { + "category": "min", + "description": "should throw an error when calculating min on a string", + "input": "abc", + "query": ["min"], + "throws": "Array expected" + }, { "category": "max", @@ -722,6 +750,20 @@ "query": ["max"], "output": 3 }, + { + "category": "max", + "description": "should throw an error when calculating max of an empty array", + "input": [], + "query": ["max"], + "output": null + }, + { + "category": "max", + "description": "should throw an error when calculating max on a string", + "input": "abc", + "query": ["max"], + "throws": "Array expected" + }, { "category": "prod", @@ -730,6 +772,20 @@ "query": ["prod"], "output": 30 }, + { + "category": "prod", + "description": "should throw an error when calculating the prod of an empty array", + "input": [], + "query": ["prod"], + "throws": "Non-empty array expected" + }, + { + "category": "prod", + "description": "should throw an error when calculating the prod a string", + "input": "abc", + "query": ["prod"], + "throws": "Array expected" + }, { "category": "average", @@ -745,6 +801,20 @@ "query": ["average"], "output": 3 }, + { + "category": "average", + "description": "should throw an error when calculating the average of an empty array", + "input": [], + "query": ["average"], + "throws": "Non-empty array expected" + }, + { + "category": "average", + "description": "should throw an error when calculating the average a string", + "input": "abc", + "query": ["average"], + "throws": "Array expected" + }, { "category": "eq", @@ -1377,6 +1447,27 @@ "query": ["and", true, true, false], "output": false }, + { + "category": "and", + "description": "should calculate and with one argument (1)", + "input": null, + "query": ["and", false], + "output": false + }, + { + "category": "and", + "description": "should calculate and with one argument (2)", + "input": null, + "query": ["and", true], + "output": true + }, + { + "category": "and", + "description": "should throw when calculating and with no arguments", + "input": null, + "query": ["and"], + "throws": "Non-empty array expected" + }, { "category": "or", @@ -1434,6 +1525,27 @@ "query": ["or", false, false, true], "output": true }, + { + "category": "or", + "description": "should calculate or with one argument (1)", + "input": null, + "query": ["or", false], + "output": false + }, + { + "category": "or", + "description": "should calculate or with one argument (2)", + "input": null, + "query": ["or", true], + "output": true + }, + { + "category": "or", + "description": "should throw when calculating or with no arguments", + "input": null, + "query": ["or"], + "throws": "Non-empty array expected" + }, { "category": "not",