Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions src/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,26 +538,91 @@ describe('Test parseExpression', () => {
expect(parseExpression('ASUM(a, MULTIPLY(b, c))', { a: [{b: 5, c: 1}, {b: 10, c: 2}, {b: 1000, c: 0}, {b: 15, c: 10}] })).toBe(175);
});

test('ASUM op with string quantities (fix NaN issue)', () => {
// Test case: 2 schedules with string quantities "2" and "3" should sum to 5
expect(parseExpression('ASUM(a, b)', { a: [{b: "2"}, {b: "3"}] })).toBe(5);

// Test case: Mixed string and number quantities
expect(parseExpression('ASUM(a, b)', { a: [{b: "2"}, {b: 3}, {b: "4"}] })).toBe(9);

// Test case: Missing/undefined quantities should not cause NaN
expect(parseExpression('ASUM(a, b)', { a: [{b: "2"}, {b: undefined}, {b: "3"}] })).toBe(5);
expect(parseExpression('ASUM(a, b)', { a: [{b: null}, {b: "5"}] })).toBe(5);
expect(parseExpression('ASUM(a, b)', { a: [{b: undefined}] })).toBe(0);
});

test('AMIN op', () => {
expect(parseExpression('AMIN(a, b)', { a: [{b: 5}, {b: 10}, {b: -5}, {b: 15}] })).toBe(-5);
expect(parseExpression('AMIN(a, SUM(b, c))', { a: [{b: 5, c: -5}, {b: 10, c: 0}, {b: -5, c: 5}, {b: 15, c: -30}] })).toBe(-15);
});

test('AMIN op with string quantities (fix NaN issue)', () => {
// Test case: string quantities should be handled gracefully
expect(parseExpression('AMIN(a, b)', { a: [{b: "5"}, {b: "3"}, {b: "10"}] })).toBe(3);

// Test case: Mixed string and number quantities
expect(parseExpression('AMIN(a, b)', { a: [{b: "5"}, {b: 3}, {b: "10"}] })).toBe(3);

// Test case: Missing/undefined quantities should not cause NaN
expect(parseExpression('AMIN(a, b)', { a: [{b: "5"}, {b: undefined}, {b: "10"}] })).toBe(0);
expect(parseExpression('AMIN(a, b)', { a: [{b: null}, {b: "5"}] })).toBe(0);
expect(parseExpression('AMIN(a, b)', { a: [{b: undefined}] })).toBe(0);
});

test('AMAX op', () => {
expect(parseExpression('AMAX(a, b)', { a: [{b: 5}, {b: 10}, {b: -5}, {b: 15}] })).toBe(15);
expect(parseExpression('AMAX(a, SUM(b, c))', { a: [{b: 5, c: -5}, {b: 10, c: 0}, {b: -5, c: 5}, {b: 15, c: -30}] })).toBe(10);
});

test('AMAX op with string quantities (fix NaN issue)', () => {
// Test case: string quantities should be handled gracefully
expect(parseExpression('AMAX(a, b)', { a: [{b: "5"}, {b: "3"}, {b: "10"}] })).toBe(10);

// Test case: Mixed string and number quantities
expect(parseExpression('AMAX(a, b)', { a: [{b: "5"}, {b: 3}, {b: "10"}] })).toBe(10);

// Test case: Missing/undefined quantities should not cause NaN
expect(parseExpression('AMAX(a, b)', { a: [{b: "5"}, {b: undefined}, {b: "10"}] })).toBe(10);
expect(parseExpression('AMAX(a, b)', { a: [{b: null}, {b: "5"}] })).toBe(5);
expect(parseExpression('AMAX(a, b)', { a: [{b: undefined}] })).toBe(0);
});

test('AAVG op', () => {
expect(parseExpression('AAVG(a, b)', { a: [{b: 5}, {b: 10}, {b: 0}, {b: 15}] })).toBe(7.5);
expect(parseExpression('AAVG(a, SUM(b, c))', { a: [{b: 5, c: -5}, {b: 10, c: 0}, {b: -5, c: 5}, {b: 15, c: -30}] })).toBe(-1.25);
});

test('AAVG op with string quantities (fix NaN issue)', () => {
// Test case: string quantities should be handled gracefully
expect(parseExpression('AAVG(a, b)', { a: [{b: "5"}, {b: "3"}, {b: "10"}] })).toBe(6);

// Test case: Mixed string and number quantities
expect(parseExpression('AAVG(a, b)', { a: [{b: "5"}, {b: 3}, {b: "10"}] })).toBe(6);

// Test case: Missing/undefined quantities should not cause NaN
expect(parseExpression('AAVG(a, b)', { a: [{b: "5"}, {b: undefined}, {b: "10"}] })).toBe(7.5);
expect(parseExpression('AAVG(a, b)', { a: [{b: null}, {b: "5"}] })).toBe(2.5);
expect(parseExpression('AAVG(a, b)', { a: [{b: undefined}] })).toBe(0);
});

test('AMUL op', () => {
expect(parseExpression('AMUL(a, b)', { a: [{b: 5}, {b: 10}, {b: 1}, {b: 15}] })).toBe(750);
expect(parseExpression('AMUL(a, SUM(b, c))', { a: [{b: 10, c: 0}, {b: 15, c: -30}] })).toBe(-150);
});

test('AMUL op with string quantities (fix NaN issue)', () => {
// Test case: string quantities should be handled gracefully
expect(parseExpression('AMUL(a, b)', { a: [{b: "5"}, {b: "2"}, {b: "3"}] })).toBe(30);

// Test case: Mixed string and number quantities
expect(parseExpression('AMUL(a, b)', { a: [{b: "5"}, {b: 2}, {b: "3"}] })).toBe(30);

// Test case: Missing/undefined quantities should not cause NaN
expect(parseExpression('AMUL(a, b)', { a: [{b: "5"}, {b: undefined}, {b: "3"}] })).toBe(0);
expect(parseExpression('AMUL(a, b)', { a: [{b: null}, {b: "5"}] })).toBe(0);
expect(parseExpression('AMUL(a, b)', { a: [{b: undefined}] })).toBe(0);
});

test('AAND op', () => {
expect(parseExpression('AAND(a, b)', { a: [{b: true}, {b: true}, {b: true}, {b: true}] })).toBe(true);
expect(parseExpression('AAND(a, b)', { a: [{b: true}, {b: true}, {b: false}, {b: true}] })).toBe(false);
Expand Down Expand Up @@ -800,4 +865,78 @@ A thousand generations live in you now.`))
expect(toSlug(new Date())).toBe('');
expect(toSlug(new RegExp('123'))).toBe('');
});
describe('NaN prevention tests', () => {
test('SUM binary op with string values', () => {
// Test that string values from Directus are properly converted to numbers
expect(parseExpression('SUM(a, b)', { a: "5", b: "3" })).toBe(8);
expect(parseExpression('SUM(a, b)', { a: "5", b: undefined })).toBe(5);
expect(parseExpression('SUM(a, b)', { a: null, b: "3" })).toBe(3);
expect(parseExpression('SUM(a, b)', { a: undefined, b: undefined })).toBe(0);
});

test('SUBTRACT binary op with string values', () => {
expect(parseExpression('SUBTRACT(a, b)', { a: "10", b: "3" })).toBe(7);
expect(parseExpression('SUBTRACT(a, b)', { a: "10", b: undefined })).toBe(10);
expect(parseExpression('SUBTRACT(a, b)', { a: null, b: "3" })).toBe(-3);
});

test('MULTIPLY binary op with string values', () => {
expect(parseExpression('MULTIPLY(a, b)', { a: "5", b: "3" })).toBe(15);
expect(parseExpression('MULTIPLY(a, b)', { a: "5", b: undefined })).toBe(0);
});

test('DIVIDE binary op with string values', () => {
expect(parseExpression('DIVIDE(a, b)', { a: "15", b: "3" })).toBe(5);
expect(parseExpression('DIVIDE(a, b)', { a: "15", b: undefined })).toBe(Infinity);
});

test('ABS unary op with string values', () => {
expect(parseExpression('ABS(a)', { a: "-5" })).toBe(5);
expect(parseExpression('ABS(a)', { a: undefined })).toBe(0);
expect(parseExpression('ABS(a)', { a: null })).toBe(0);
});

test('SQRT unary op with string values', () => {
expect(parseExpression('SQRT(a)', { a: "16" })).toBe(4);
expect(parseExpression('SQRT(a)', { a: undefined })).toBe(0);
});

test('SUM unary op with array containing strings', () => {
expect(parseExpression('SUM(a)', { a: ["1", "2", "3"] })).toBe(6);
expect(parseExpression('SUM(a)', { a: ["1", undefined, "3"] })).toBe(4);
expect(parseExpression('SUM(a)', { a: [null, "2", null] })).toBe(2);
});

test('AVERAGE unary op with array containing strings', () => {
expect(parseExpression('AVERAGE(a)', { a: ["2", "4", "6"] })).toBe(4);
expect(parseExpression('AVERAGE(a)', { a: ["2", undefined, "6"] })).toBe(4);
});

test('MAX unary op with array containing strings', () => {
expect(parseExpression('MAX(a)', { a: ["1", "5", "3"] })).toBe(5);
expect(parseExpression('MAX(a)', { a: ["1", undefined, "5"] })).toBe(5);
expect(parseExpression('MAX(a)', { a: [null, null, null] })).toBe(0);
});

test('MIN unary op with array containing strings', () => {
expect(parseExpression('MIN(a)', { a: ["1", "5", "3"] })).toBe(1);
expect(parseExpression('MIN(a)', { a: ["1", undefined, "5"] })).toBe(1);
expect(parseExpression('MIN(a)', { a: [null, null, null] })).toBe(0);
});

test('CEIL unary op with string values', () => {
expect(parseExpression('CEIL(a)', { a: "1.234" })).toBe(2);
expect(parseExpression('CEIL(a)', { a: undefined })).toBe(0);
});

test('FLOOR unary op with string values', () => {
expect(parseExpression('FLOOR(a)', { a: "1.234" })).toBe(1);
expect(parseExpression('FLOOR(a)', { a: undefined })).toBe(0);
});

test('ROUND unary op with string values', () => {
expect(parseExpression('ROUND(a)', { a: "1.234" })).toBe(1);
expect(parseExpression('ROUND(a)', { a: undefined })).toBe(0);
});
});
});
68 changes: 34 additions & 34 deletions src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function _parseExpression(

const opMatch = parseOp(exp);
if (opMatch) {
const { op, args } = opMatch;
const { op, args } = opMatch as { op: string; args: [string, string, string, ...string[]] };

// unary operators
if (args.length === 1) {
Expand Down Expand Up @@ -104,7 +104,7 @@ function _parseExpression(
}
if (['YEAR', 'MONTH', 'GET_DATE', 'DAY', 'HOURS', 'MINUTES', 'SECONDS', 'TIME'].includes(op)) {
if (valueA instanceof Date) {
const op2func = {
const op2func: any = {
YEAR: 'getFullYear',
MONTH: 'getMonth',
GET_DATE: 'getDate',
Expand All @@ -114,53 +114,53 @@ function _parseExpression(
SECONDS: 'getSeconds',
TIME: 'getTime',
};
return valueA[op2func[op]]();
return (valueA as any)[op2func[op]]();
}
return 0;
}
// arithmetic
if (op === 'ABS') {
return Math.abs(valueA);
return Math.abs(+valueA || 0);
}
if (op === 'SQRT') {
return Math.sqrt(valueA);
return Math.sqrt(+valueA || 0);
}
if (op === 'SUM') {
if (valueA instanceof Array) {
return valueA.reduce((partialSum, a) => partialSum + a, 0);
return valueA.reduce((partialSum: number, a: any) => partialSum + (+a || 0), 0);
}
return 0;
}
if (op === 'AVERAGE') {
if (valueA instanceof Array) {
return valueA.reduce((partialSum, a) => partialSum + a, 0) / valueA.length;
return valueA.reduce((partialSum: number, a: any) => partialSum + (+a || 0), 0) / valueA.length;
}
return 0;
}
if (op === 'CEIL') {
return Math.ceil(valueA);
return Math.ceil(+valueA || 0);
}
if (op === 'FLOOR') {
return Math.floor(valueA);
return Math.floor(+valueA || 0);
}
if (op === 'ROUND') {
return Math.round(valueA);
return Math.round(+valueA || 0);
}
if (op === 'EXP') {
return Math.exp(valueA);
return Math.exp(+valueA || 0);
}
if (op === 'LOG') {
return Math.log(valueA);
return Math.log(+valueA || 0);
}
if (op === 'MAX') {
if (valueA instanceof Array) {
return Math.max(...valueA);
return Math.max(...valueA.map(v => +v || 0));
}
return 0;
}
if (op === 'MIN') {
if (valueA instanceof Array) {
return Math.min(...valueA);
return Math.min(...valueA.map(v => +v || 0));
}
return 0;
}
Expand Down Expand Up @@ -236,36 +236,36 @@ function _parseExpression(
// aggregated operators
if (op === 'ASUM') {
// aggregated sum
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc + parseExpression(args[1], item as typeof values, {}, debug), 0) ?? 0;
return (values[args[0]] as unknown[])?.reduce((acc: number, item) => acc + (+parseExpression(args[1], item as typeof values, {}, debug) || 0), 0) ?? 0;
}
if (op === 'AMIN') {
// aggregated min
return (values[args[0]] as unknown[])?.reduce((acc, item) => Math.min(acc, parseExpression(args[1], item as typeof values, {}, debug)), Infinity) ?? 0;
return (values[args[0]] as unknown[])?.reduce((acc: number, item) => Math.min(acc, (+parseExpression(args[1], item as typeof values, {}, debug) || 0)), Infinity) ?? 0;
}
if (op === 'AMAX') {
// aggregated max
return (values[args[0]] as unknown[])?.reduce((acc, item) => Math.max(acc, parseExpression(args[1], item as typeof values, {}, debug)), -Infinity) ?? 0;
return (values[args[0]] as unknown[])?.reduce((acc: number, item) => Math.max(acc, (+parseExpression(args[1], item as typeof values, {}, debug) || 0)), -Infinity) ?? 0;
}
if (op === 'AAVG') {
// aggregated average
const arr = (values[args[0]] as unknown[]) ?? [];
return arr.reduce((acc, item) => acc + parseExpression(args[1], item as typeof values, {}, debug), 0) / arr.length;
return arr.reduce((acc: number, item) => acc + (+parseExpression(args[1], item as typeof values, {}, debug) || 0), 0) / arr.length;
}
if (op === 'AMUL') {
// aggregated multiplication
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc * parseExpression(args[1], item as typeof values, {}, debug), 1) ?? 0;
return (values[args[0]] as unknown[])?.reduce((acc: number, item) => acc * (+parseExpression(args[1], item as typeof values, {}, debug) || 0), 1) ?? 0;
}
if (op === 'AAND') {
// aggregated and
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc && parseExpression(args[1], item as typeof values, {}, debug), true) ?? false;
return (values[args[0]] as unknown[])?.reduce((acc: boolean, item) => acc && parseExpression(args[1], item as typeof values, {}, debug), true) ?? false;
}
if (op === 'AOR') {
// aggregated or
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc || parseExpression(args[1], item as typeof values, {}, debug), false) ?? false;
return (values[args[0]] as unknown[])?.reduce((acc: boolean, item) => acc || parseExpression(args[1], item as typeof values, {}, debug), false) ?? false;
}
if (op === 'ACOUNT') {
// aggregated count
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc + (parseExpression(args[1], item as typeof values, {}, debug) ? 1 : 0), 0) ?? 0;
return (values[args[0]] as unknown[])?.reduce((acc: number, item) => acc + (parseExpression(args[1], item as typeof values, {}, debug) ? 1 : 0), 0) ?? 0;
}
// loop operators
if (op === 'MAP') {
Expand Down Expand Up @@ -296,31 +296,31 @@ function _parseExpression(

// arithmetic
if (op === 'SUM') {
return valueA + valueB;
return (+valueA || 0) + (+valueB || 0);
}
if (op === 'SUBTRACT') {
return valueA - valueB;
return (+valueA || 0) - (+valueB || 0);
}
if (op === 'MULTIPLY') {
return valueA * valueB;
return (+valueA || 0) * (+valueB || 0);
}
if (op === 'DIVIDE') {
return valueA / valueB;
return (+valueA || 0) / (+valueB || 0);
}
if (op === 'REMAINDER') {
return valueA % valueB;
return (+valueA || 0) % (+valueB || 0);
}
if (op === 'ROUND') {
return (valueA as number).toFixed(valueB);
return ((+valueA || 0) as number).toFixed(+valueB || 0);
}
if (op === 'MAX') {
return Math.max(valueA, valueB);
return Math.max(+valueA || 0, +valueB || 0);
}
if (op === 'MIN') {
return Math.min(valueA, valueB);
return Math.min(+valueA || 0, +valueB || 0);
}
if (op === 'POWER') {
return Math.pow(valueA, valueB);
return Math.pow(+valueA || 0, +valueB || 0);
}
// string
if (op === 'LEFT') {
Expand Down Expand Up @@ -473,8 +473,8 @@ function _parseExpression(
} else if (args.length % 2 === 0) {
if (op === 'IFS') {
for (let i = 0; i < args.length; i += 2) {
if (parseExpression(args[i], values, defaultValues, debug) === true) {
return parseExpression(args[i + 1], values, defaultValues, debug);
if (parseExpression(args[i]!, values, defaultValues, debug) === true) {
return parseExpression(args[i + 1]!, values, defaultValues, debug);
}
}
return null;
Expand Down Expand Up @@ -554,4 +554,4 @@ export function toSlug(str: unknown) {
.replace(/-+/g, '-'); // collapse dashes

return res;
}
}