diff --git a/src/compile.test.ts b/src/compile.test.ts index 0038d89..1da6aae 100644 --- a/src/compile.test.ts +++ b/src/compile.test.ts @@ -94,7 +94,11 @@ describe('error handling', () => { { name: 'Joe', age: 32, scores: [6.1, 8.1] } ] } - const query = ['pipe', ['get', 'participants'], ['map', ['pipe', ['get', 'scores'], ['sum']]]] + const query = [ + 'pipe', + ['get', 'participants'], + ['map', ['pipe', ['get', 'scores'], ['map', ['round']], ['sum']]] + ] let actualErr = undefined try { @@ -103,15 +107,18 @@ describe('error handling', () => { actualErr = err } - expect(actualErr?.message).toBe('Array expected') + expect(actualErr?.message).toBe("Cannot read properties of null (reading 'map')") expect(actualErr?.jsonquery).toEqual([ { data: scoreData, query }, { data: scoreData.participants, - query: ['map', ['pipe', ['get', 'scores'], ['sum']]] + query: ['map', ['pipe', ['get', 'scores'], ['map', ['round']], ['sum']]] }, - { data: { name: 'Emily', age: 19 }, query: ['pipe', ['get', 'scores'], ['sum']] }, - { data: null, query: ['sum'] } + { + data: { name: 'Emily', age: 19 }, + query: ['pipe', ['get', 'scores'], ['map', ['round']], ['sum']] + }, + { data: null, query: ['map', ['round']] } ]) }) }) diff --git a/src/functions.ts b/src/functions.ts index 577c189..9d235b2 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -30,17 +30,15 @@ export function buildFunction(fn: (...args: unknown[]) => unknown): FunctionBuil const sortableTypes = { boolean: 0, number: 1, string: 2 } const otherTypes = 3 -const gt = (a: unknown, b: unknown) => { - if (typeof a !== typeof b || !((typeof a) in sortableTypes)) { - throwTypeError('Two numbers, strings, or booleans expected') - } - - return a > b -} +const gt = (a: unknown, b: unknown) => + typeof a === typeof b && (typeof a) in sortableTypes ? a > b : false const gte = (a: unknown, b: unknown) => isEqual(a, b) || gt(a, b) -const lt = (a: unknown, b: unknown) => gt(b, a) -const lte = (a: unknown, b: unknown) => gte(b, a) + +const lt = (a: unknown, b: unknown) => + typeof a === typeof b && (typeof a) in sortableTypes ? a < b : false + +const lte = (a: unknown, b: unknown) => isEqual(a, b) || lt(a, b) export const functions: FunctionBuildersMap = { pipe: (...entries: JSONQuery[]) => { @@ -277,13 +275,22 @@ export const functions: FunctionBuildersMap = { values: () => Object.values, 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), - and: buildFunction((...args: unknown[]) => reduce(args, (a, b) => !!(a && b))), - or: buildFunction((...args: unknown[]) => reduce(args, (a, b) => !!(a || b))), + sum: () => (data: number[]) => + isArray(data) ? data.reduce((a, b) => a + b, 0) : throwArrayExpected(), + + average: () => (data: number[]) => + isArray(data) + ? data.length > 0 + ? data.reduce((a, b) => a + b) / data.length + : null + : throwArrayExpected(), + + min: () => (data: number[]) => reduce(data, (a, b) => Math.min(a, b)), + max: () => (data: number[]) => reduce(data, (a, b) => Math.max(a, b)), + + and: buildFunction((...data: unknown[]) => reduce(data, (a, b) => !!(a && b))), + or: buildFunction((...data: unknown[]) => reduce(data, (a, b) => !!(a || b))), not: buildFunction((a: unknown) => !a), exists: (queryGet: JSONQueryFunction) => { @@ -355,26 +362,22 @@ 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 => { +const reduce = (data: T[], callback: (previousValue: T, currentValue: T) => T): T => { if (!isArray(data)) { - throwTypeError('Array expected') - } - - if (initialValue !== undefined) { - return data.reduce(callback, initialValue) + throwArrayExpected() } if (data.length === 0) { - throwTypeError('Non-empty array expected') + return null } return data.reduce(callback) } +const throwArrayExpected = () => { + throwTypeError('Array expected') +} + export const throwTypeError = (message: string) => { throw new TypeError(message) } diff --git a/test-suite/compile.test.json b/test-suite/compile.test.json index 3fc0806..293d118 100644 --- a/test-suite/compile.test.json +++ b/test-suite/compile.test.json @@ -1127,12 +1127,12 @@ }, { "category": "prod", - "description": "should throw an error when calculating the prod of an empty array", + "description": "should return null when calculating the prod of an empty array", "tests": [ { "input": [], "query": ["prod"], - "throws": "Non-empty array expected" + "output": null } ] }, @@ -1157,18 +1157,18 @@ }, { "category": "average", - "description": "should throw an error when calculating the average of an empty array", + "description": "should return null when calculating the average of an empty array", "tests": [ { "input": [], "query": ["average"], - "throws": "Non-empty array expected" + "output": null } ] }, { "category": "average", - "description": "should throw an error when calculating the average a string", + "description": "should throw an error when calculating the average of a string", "tests": [ { "input": "abc", @@ -1331,34 +1331,16 @@ }, { "category": "gt", - "description": "should throw when calculating greater than with mixed data types", - "tests": [ - { - "input": null, - "query": ["gt", "3", 2], - "throws": "Two numbers, strings, or booleans expected" - } - ] + "description": "should return false when calculating greater than with mixed data types", + "tests": [{ "input": null, "query": ["gt", "3", 2], "output": false }] }, { "category": "gt", - "description": "should throw when calculating greater than with an unsupported data type", + "description": "should return false when calculating greater than with an unsupported data type", "tests": [ - { - "input": null, - "query": ["gt", 2, ["array", 1, 2, 3]], - "throws": "Two numbers, strings, or booleans expected" - }, - { - "input": null, - "query": ["gt", ["array", 1, 2, 4], ["array", 1, 2, 3]], - "throws": "Two numbers, strings, or booleans expected" - }, - { - "input": null, - "query": ["gt", 2, ["object", { "a": 1 }]], - "throws": "Two numbers, strings, or booleans expected" - } + { "input": null, "query": ["gt", 2, ["array", 1, 2, 3]], "output": false }, + { "input": null, "query": ["gt", ["array", 1, 2, 4], ["array", 1, 2, 3]], "output": false }, + { "input": null, "query": ["gt", 2, ["object", { "a": 1 }]], "output": false } ] }, { @@ -1406,34 +1388,20 @@ }, { "category": "gte", - "description": "should throw when calculating greater than or equal to with mixed data types", - "tests": [ - { - "input": null, - "query": ["gte", "3", 2], - "throws": "Two numbers, strings, or booleans expected" - } - ] + "description": "should return false when calculating greater than or equal to with mixed data types", + "tests": [{ "input": null, "query": ["gte", "3", 2], "output": false }] }, { "category": "gte", - "description": "should throw when calculating greater than or equal to with an unsupported data type", + "description": "should return false when calculating greater than or equal to with an unsupported data type", "tests": [ - { - "input": null, - "query": ["gte", 2, ["array", 1, 2, 3]], - "throws": "Two numbers, strings, or booleans expected" - }, + { "input": null, "query": ["gte", 2, ["array", 1, 2, 3]], "output": false }, { "input": null, "query": ["gte", ["array", 1, 2, 4], ["array", 1, 2, 3]], - "throws": "Two numbers, strings, or booleans expected" + "output": false }, - { - "input": null, - "query": ["gte", 2, ["object", { "a": 1 }]], - "throws": "Two numbers, strings, or booleans expected" - } + { "input": null, "query": ["gte", 2, ["object", { "a": 1 }]], "output": false } ] }, { @@ -1481,34 +1449,16 @@ }, { "category": "lt", - "description": "should throw when calculating less than with mixed data types", - "tests": [ - { - "input": null, - "query": ["lt", 2, "3"], - "throws": "Two numbers, strings, or booleans expected" - } - ] + "description": "should return false when calculating less than with mixed data types", + "tests": [{ "input": null, "query": ["lt", 2, "3"], "output": false }] }, { "category": "lt", - "description": "should throw when calculating less than with an unsupported data type", + "description": "should return false when calculating less than with an unsupported data type", "tests": [ - { - "input": null, - "query": ["lt", 2, ["array", 1, 2, 3]], - "throws": "Two numbers, strings, or booleans expected" - }, - { - "input": null, - "query": ["lt", ["array", 1, 2, 4], ["array", 1, 2, 3]], - "throws": "Two numbers, strings, or booleans expected" - }, - { - "input": null, - "query": ["lt", 2, ["object", { "a": 1 }]], - "throws": "Two numbers, strings, or booleans expected" - } + { "input": null, "query": ["lt", 2, ["array", 1, 2, 3]], "output": false }, + { "input": null, "query": ["lt", ["array", 1, 2, 4], ["array", 1, 2, 3]], "output": false }, + { "input": null, "query": ["lt", 2, ["object", { "a": 1 }]], "output": false } ] }, { @@ -1560,34 +1510,20 @@ }, { "category": "lte", - "description": "should throw when calculating less than or equal to with mixed data types", - "tests": [ - { - "input": null, - "query": ["lte", "3", 2], - "throws": "Two numbers, strings, or booleans expected" - } - ] + "description": "should return false when calculating less than or equal to with mixed data types", + "tests": [{ "input": null, "query": ["lte", "3", 2], "output": false }] }, { "category": "lte", - "description": "should throw when calculating less than or equal to with an unsupported data type", + "description": "should return false when calculating less than or equal to with an unsupported data type", "tests": [ - { - "input": null, - "query": ["lte", 2, ["array", 1, 2, 3]], - "throws": "Two numbers, strings, or booleans expected" - }, + { "input": null, "query": ["lte", 2, ["array", 1, 2, 3]], "output": false }, { "input": null, "query": ["lte", ["array", 1, 2, 4], ["array", 1, 2, 3]], - "throws": "Two numbers, strings, or booleans expected" + "output": false }, - { - "input": null, - "query": ["lte", 2, ["object", { "a": 1 }]], - "throws": "Two numbers, strings, or booleans expected" - } + { "input": null, "query": ["lte", 2, ["object", { "a": 1 }]], "output": false } ] }, { @@ -1750,12 +1686,12 @@ }, { "category": "and", - "description": "should throw when calculating and with no arguments", + "description": "should return null calculating and with no arguments", "tests": [ { "input": null, "query": ["and"], - "throws": "Non-empty array expected" + "output": null } ] }, @@ -1821,12 +1757,12 @@ }, { "category": "or", - "description": "should throw when calculating or with no arguments", + "description": "should return null when calculating or with no arguments", "tests": [ { "input": null, "query": ["or"], - "throws": "Non-empty array expected" + "output": null } ] },