diff --git a/package.json b/package.json index b0006c4..b2ce50e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@time-loop/hot-formula-parser", - "version": "4.2.0", + "version": "4.3.0", "description": "Formula parser", "type": "commonjs", "main": "dist/index.js", diff --git a/src/clickup/formulajsProxy.ts b/src/clickup/formulajsProxy.ts index 18e8887..86795d5 100644 --- a/src/clickup/formulajsProxy.ts +++ b/src/clickup/formulajsProxy.ts @@ -1,8 +1,5 @@ import * as formulajs from '@formulajs/formulajs'; - -const hasNil = (...args: unknown[]) => - args.some((arg) => arg === null || arg === undefined || arg === '' || arg === false); -const nullToZero = (arg: unknown) => (hasNil(arg) ? 0 : arg); +import { hasNil, nilToZero } from './utils'; const overrides = { DATE: (year: unknown, month: unknown, day: unknown) => @@ -14,11 +11,11 @@ const overrides = { DAYS360: (startDate: unknown, endDate: unknown, method: unknown) => hasNil(startDate, endDate) ? Number.NaN : formulajs.DAYS360(startDate, endDate, method), EDATE: (startDate: unknown, months: unknown) => - hasNil(startDate) ? Number.NaN : formulajs.EDATE(startDate, nullToZero(months)), + hasNil(startDate) ? Number.NaN : formulajs.EDATE(startDate, nilToZero(months)), EOMONTH: (startDate: unknown, months: unknown) => - hasNil(startDate) ? Number.NaN : formulajs.EOMONTH(startDate, nullToZero(months)), + hasNil(startDate) ? Number.NaN : formulajs.EOMONTH(startDate, nilToZero(months)), HOUR: (date: unknown) => (hasNil(date) ? Number.NaN : formulajs.HOUR(date)), - INTERVAL: (seconds: unknown) => formulajs.INTERVAL(nullToZero(seconds)), + INTERVAL: (seconds: unknown) => formulajs.INTERVAL(nilToZero(seconds)), ISOWEEKNUM: (date: unknown) => (hasNil(date) ? Number.NaN : formulajs.ISOWEEKNUM(date)), MINUTE: (serialNumber: unknown) => (hasNil(serialNumber) ? Number.NaN : formulajs.MINUTE(serialNumber)), MONTH: (date: unknown) => (hasNil(date) ? Number.NaN : formulajs.MONTH(date)), @@ -34,7 +31,7 @@ const overrides = { YEARFRAC: (startDate: unknown, endDate: unknown, basis: unknown) => hasNil(startDate, endDate) ? Number.NaN : formulajs.YEARFRAC(startDate, endDate, basis), WORKDAY: (startDate: unknown, days: unknown, holidays: unknown) => - hasNil(startDate) ? Number.NaN : formulajs.WORKDAY(startDate, nullToZero(days), holidays), + hasNil(startDate) ? Number.NaN : formulajs.WORKDAY(startDate, nilToZero(days), holidays), NETWORKDAYS: (startDate: unknown, endDate: unknown, holidays: unknown) => hasNil(startDate, endDate) ? Number.NaN : formulajs.NETWORKDAYS(startDate, endDate, holidays), }; diff --git a/src/clickup/utils.ts b/src/clickup/utils.ts new file mode 100644 index 0000000..47f92d3 --- /dev/null +++ b/src/clickup/utils.ts @@ -0,0 +1,4 @@ +export const hasNil = (...args: unknown[]) => + args.some((arg) => arg === null || arg === undefined || arg === '' || arg === false); + +export const nilToZero = (arg: unknown) => (hasNil(arg) ? 0 : arg); diff --git a/src/evaluate-by-operator/operator/add.js b/src/evaluate-by-operator/operator/add.js index c5586d9..c832b5a 100644 --- a/src/evaluate-by-operator/operator/add.js +++ b/src/evaluate-by-operator/operator/add.js @@ -1,10 +1,12 @@ import { toNumber } from '../../helper/number'; import { ERROR_VALUE } from '../../error'; +import { nilToZero } from '../../clickup/utils'; export const SYMBOL = '+'; export default function func(first, ...rest) { - const result = rest.reduce((acc, value) => acc + toNumber(value), toNumber(first)); + // in addition, we convert unset values to zero + const result = rest.reduce((acc, value) => acc + toNumber(nilToZero(value)), toNumber(nilToZero(first))); if (isNaN(result)) { throw Error(ERROR_VALUE); diff --git a/src/evaluate-by-operator/operator/divide.js b/src/evaluate-by-operator/operator/divide.js index b3f7c46..f0ee22d 100644 --- a/src/evaluate-by-operator/operator/divide.js +++ b/src/evaluate-by-operator/operator/divide.js @@ -1,9 +1,15 @@ import { toNumber } from '../../helper/number'; import { ERROR_DIV_ZERO, ERROR_VALUE } from '../../error'; +import { hasNil } from '../../clickup/utils'; export const SYMBOL = '/'; export default function func(first, ...rest) { + if (hasNil(first, ...rest)) { + // some of the arguments are unset, return NaN + return Number.NaN; + } + const result = rest.reduce((acc, value) => acc / Number(toNumber(value)), Number(toNumber(first))); if (result === Infinity) { diff --git a/src/evaluate-by-operator/operator/minus.js b/src/evaluate-by-operator/operator/minus.js index b849ef7..b5f4bc8 100644 --- a/src/evaluate-by-operator/operator/minus.js +++ b/src/evaluate-by-operator/operator/minus.js @@ -1,6 +1,7 @@ import { toNumber } from '../../helper/number'; -import { ERROR_VALUE } from '../../error'; import ClickUpConfiguration from '../../clickup.config'; +import { ERROR_VALUE } from '../../error'; +import { hasNil } from '../../clickup/utils'; export const SYMBOL = '-'; @@ -10,12 +11,20 @@ export default function func(first, ...rest) { convertFormulasInNumbers: ClickUpConfiguration.ConvertFormulasInNumbers, }; + if (hasNil(first, ...rest)) { + // some of the arguments are unset, return NaN + return Number.NaN; + } + const result = rest.reduce((acc, value) => { const subtrahend = toNumber(value, toNumberConfig); - return acc - (subtrahend || 0); + if (Number.isNaN(subtrahend)) { + return Number.NaN; + } + return acc - subtrahend; }, toNumber(first, toNumberConfig)); - if (isNaN(result)) { + if (Number.isNaN(result)) { throw Error(ERROR_VALUE); } diff --git a/src/evaluate-by-operator/operator/multiply.js b/src/evaluate-by-operator/operator/multiply.js index c200b06..eb942bf 100644 --- a/src/evaluate-by-operator/operator/multiply.js +++ b/src/evaluate-by-operator/operator/multiply.js @@ -1,9 +1,15 @@ import { toNumber } from '../../helper/number'; import { ERROR_VALUE } from '../../error'; +import { hasNil } from '../../clickup/utils'; export const SYMBOL = '*'; export default function func(first, ...rest) { + if (hasNil(first, ...rest)) { + // some of the arguments are unset, return NaN + return Number.NaN; + } + const result = rest.reduce((acc, value) => { const num = toNumber(value); if (num === undefined) { diff --git a/src/evaluate-by-operator/operator/power.js b/src/evaluate-by-operator/operator/power.js index 0325139..8232c0d 100644 --- a/src/evaluate-by-operator/operator/power.js +++ b/src/evaluate-by-operator/operator/power.js @@ -1,9 +1,15 @@ import { toNumber } from '../../helper/number'; import { ERROR_VALUE } from '../../error'; +import { hasNil } from '../../clickup/utils'; export const SYMBOL = '^'; export default function func(exp1, exp2) { + if (hasNil(exp1, exp2)) { + // some of the arguments are unset, return NaN + return Number.NaN; + } + const exp1Number = toNumber(exp1); const exp2Number = toNumber(exp2); if (exp1Number === undefined || exp2Number === undefined) { diff --git a/src/helper/number.js b/src/helper/number.js index a2da138..4ec2913 100644 --- a/src/helper/number.js +++ b/src/helper/number.js @@ -41,7 +41,7 @@ export function toNumber( return getNumberOfDaysSinceEpoch(value); } - return undefined; + return Number.NaN; } /** diff --git a/test/unit/evaluate-by-operator/operator/add.js b/test/unit/evaluate-by-operator/operator/add.js index c9f2744..96c1043 100644 --- a/test/unit/evaluate-by-operator/operator/add.js +++ b/test/unit/evaluate-by-operator/operator/add.js @@ -14,10 +14,10 @@ describe('add operator', () => { expect(func(2, undefined)).toBe(2); expect(func(null, 2)).toBe(2); expect(func(undefined, 2)).toBe(2); + expect(func(2, '')).toBe(2); + expect(func('', 2)).toBe(2); expect(() => func('foo', ' ', 'bar', ' baz')).toThrow('VALUE'); expect(() => func('foo', 2)).toThrow('VALUE'); - expect(() => func(2, '')).toThrow('VALUE'); - expect(() => func('', 2)).toThrow('VALUE'); }); describe('ClickUp Overrides', () => { diff --git a/test/unit/evaluate-by-operator/operator/divide.js b/test/unit/evaluate-by-operator/operator/divide.js index e1b3167..1847fca 100644 --- a/test/unit/evaluate-by-operator/operator/divide.js +++ b/test/unit/evaluate-by-operator/operator/divide.js @@ -10,13 +10,13 @@ describe('divide operator', () => { expect(func('2', 8.8)).toBe(0.22727272727272727); expect(func('2', '-8.8', 6, 0.4)).toBe(-0.0946969696969697); expect(func(0, 1)).toBe(0); - expect(func(null, 2)).toBe(0); - expect(func(undefined, 2)).toBe(0); + expect(func(null, 2)).toBe(Number.NaN); + expect(func(undefined, 2)).toBe(Number.NaN); + expect(func('', 2)).toBe(Number.NaN); + expect(func(2, '')).toBe(Number.NaN); + expect(func(2, null)).toBe(Number.NaN); + expect(func(2, undefined)).toBe(Number.NaN); expect(() => func(1, 0)).toThrow('DIV/0'); expect(() => func('foo', ' ', 'bar', ' baz')).toThrow('VALUE'); - expect(() => func('', 2)).toThrow('VALUE'); - expect(() => func(2, null)).toThrow('DIV/0'); - expect(() => func(2, undefined)).toThrow('DIV/0'); - expect(() => func(2, '')).toThrow('VALUE'); }); }); diff --git a/test/unit/evaluate-by-operator/operator/minus.js b/test/unit/evaluate-by-operator/operator/minus.js index 2354774..33f75ac 100644 --- a/test/unit/evaluate-by-operator/operator/minus.js +++ b/test/unit/evaluate-by-operator/operator/minus.js @@ -11,6 +11,12 @@ describe('minus operator', () => { expect(func('2', 8.8)).toBe(-6.800000000000001); expect(func('2', '8.8')).toBe(-6.800000000000001); expect(func('2', '-8.8', 6, 0.4)).toBe(4.4); + expect(func(null, null)).toBe(Number.NaN); + expect(func(null, false)).toBe(Number.NaN); + expect(func(false, false)).toBe(Number.NaN); + expect(func(false, null)).toBe(Number.NaN); + expect(func(null, 'foo')).toBe(Number.NaN); + expect(func('foo', null)).toBe(Number.NaN); expect(() => func('foo', ' ', 'bar', ' baz')).toThrow('VALUE'); expect(() => func('foo', 2)).toThrow('VALUE'); }); diff --git a/test/unit/evaluate-by-operator/operator/multiply.js b/test/unit/evaluate-by-operator/operator/multiply.js index 925eee9..1a0fb4e 100644 --- a/test/unit/evaluate-by-operator/operator/multiply.js +++ b/test/unit/evaluate-by-operator/operator/multiply.js @@ -10,13 +10,13 @@ describe('multiply operator', () => { expect(func('2', 8.8)).toBe(17.6); expect(func('2', '8.8')).toBe(17.6); expect(func('2', '-8.8', 6, 0.4)).toBe(-42.24000000000001); - expect(func(2, null)).toBe(0); - expect(func(2, undefined)).toBe(0); - expect(func(null, 2)).toBe(0); - expect(func(undefined, 2)).toBe(0); + expect(func(2, null)).toBe(Number.NaN); + expect(func(2, undefined)).toBe(Number.NaN); + expect(func(null, 2)).toBe(Number.NaN); + expect(func(undefined, 2)).toBe(Number.NaN); + expect(func(2, '')).toBe(Number.NaN); + expect(func('', 2)).toBe(Number.NaN); expect(() => func('foo', ' ', 'bar', ' baz')).toThrow('VALUE'); expect(() => func('foo', 2)).toThrow('VALUE'); - expect(() => func(2, '')).toThrow('VALUE'); - expect(() => func('', 2)).toThrow('VALUE'); }); }); diff --git a/test/unit/evaluate-by-operator/operator/power.js b/test/unit/evaluate-by-operator/operator/power.js index 5305f23..f7557ff 100644 --- a/test/unit/evaluate-by-operator/operator/power.js +++ b/test/unit/evaluate-by-operator/operator/power.js @@ -10,13 +10,13 @@ describe('power operator', () => { expect(func('2', 8.8)).toBe(445.7218884076158); expect(func('2', '8.8')).toBe(445.7218884076158); expect(func('2', '8.8')).toBe(445.7218884076158); - expect(func(2, null)).toBe(1); - expect(func(2, undefined)).toBe(1); - expect(func(null, 2)).toBe(0); - expect(func(undefined, 2)).toBe(0); + expect(func(2, null)).toBe(Number.NaN); + expect(func(2, undefined)).toBe(Number.NaN); + expect(func(null, 2)).toBe(Number.NaN); + expect(func(undefined, 2)).toBe(Number.NaN); + expect(func(2, '')).toBe(Number.NaN); + expect(func('', 2)).toBe(Number.NaN); expect(() => func('foo', ' ')).toThrow('VALUE'); expect(() => func('foo', 2)).toThrow('VALUE'); - expect(() => func(2, '')).toThrow('VALUE'); - expect(() => func('', 2)).toThrow('VALUE'); }); });