diff --git a/.npmignore b/.npmignore index 789f467..88b1dca 100644 --- a/.npmignore +++ b/.npmignore @@ -10,4 +10,5 @@ bench/ perf*.js *.test.js experiments/ -suites/ \ No newline at end of file +suites/ +precision/ \ No newline at end of file diff --git a/compatibility.js b/compatibility.js index 0c27844..004555e 100644 --- a/compatibility.js +++ b/compatibility.js @@ -1,7 +1,9 @@ import defaultMethods from './defaultMethods.js' +import { Sync } from './constants.js' const oldAll = defaultMethods.all const all = { + [Sync]: defaultMethods.all[Sync], method: (args, context, above, engine) => { if (Array.isArray(args)) { const first = engine.run(args[0], context, above) diff --git a/compatible.test.js b/compatible.test.js index 338da5b..c2f221f 100644 --- a/compatible.test.js +++ b/compatible.test.js @@ -1,5 +1,8 @@ import fs from 'fs' import { LogicEngine, AsyncLogicEngine } from './index.js' +import { configurePrecision } from './precision/index.js' +import Decimal from 'decimal.js' + const tests = [] // get all json files from "suites" directory @@ -13,141 +16,62 @@ for (const file of files) { } } -// eslint-disable-next-line no-labels -inline: { - const logic = new LogicEngine(undefined, { compatible: true }) - const asyncLogic = new AsyncLogicEngine(undefined, { compatible: true }) - const logicWithoutOptimization = new LogicEngine(undefined, { compatible: true }) - const asyncLogicWithoutOptimization = new AsyncLogicEngine(undefined, { compatible: true }) - - describe('All of the compatible tests', () => { - tests.forEach((testCase) => { - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )}`, () => { - expect(logic.run(testCase[0], testCase[1])).toStrictEqual(testCase[2]) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (async)`, async () => { - expect(await asyncLogic.run(testCase[0], testCase[1])).toStrictEqual( - testCase[2] - ) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (built)`, () => { - const f = logic.build(testCase[0]) - expect(f(testCase[1])).toStrictEqual(testCase[2]) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (asyncBuilt)`, async () => { - const f = await asyncLogic.build(testCase[0]) - expect(await f(testCase[1])).toStrictEqual(testCase[2]) - }) +const engines = [] - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (noOptimization)`, () => { - expect(logicWithoutOptimization.run(testCase[0], testCase[1])).toStrictEqual(testCase[2]) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (asyncNoOptimization)`, async () => { - expect(await asyncLogicWithoutOptimization.run(testCase[0], testCase[1])).toStrictEqual( - testCase[2] - ) - }) +for (let i = 0; i < 16; i++) { + let res = 'sync' + let engine = new LogicEngine(undefined, { compatible: true }) + // sync / async + if (i & 1) { + engine = new AsyncLogicEngine(undefined, { compatible: true }) + res = 'async' + } + // inline / disabled + if (i & 2) { + engine.disableInline = true + res += ' no-inline' + } + // optimized / not optimized + if (i & 4) { + engine.disableInterpretedOptimization = true + res += ' no-optimized' + } + // ieee754 / decimal + if (i & 8) { + configurePrecision(engine, Decimal) + res += ' decimal' + + // Copy in another decimal engine + const preciseEngine = (i & 1) ? new AsyncLogicEngine(undefined, { compatible: true }) : new LogicEngine(undefined, { compatible: true }) + preciseEngine.disableInline = engine.disableInline + preciseEngine.disableInterpretedOptimization = engine.disableInterpretedOptimization + configurePrecision(preciseEngine, 'precise') + engines.push([preciseEngine, res + ' improved']) + } + engines.push([engine, res]) +} - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( +describe('All of the compatible tests', () => { + for (const testCase of tests) { + for (const engine of engines) { + test(`${engine[1]} ${JSON.stringify(testCase[0])} ${JSON.stringify( testCase[1] - )} (builtNoOptimization)`, () => { - const f = logicWithoutOptimization.build(testCase[0]) - expect(f(testCase[1])).toStrictEqual(testCase[2]) + )}`, async () => { + let result = await engine[0].run(testCase[0], testCase[1]) + if ((result || 0).toNumber) result = Number(result) + if (Array.isArray(result)) result = result.map(i => (i || 0).toNumber ? Number(i) : i) + expect(result).toStrictEqual(testCase[2]) }) - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( + test(`${engine[1]} ${JSON.stringify(testCase[0])} ${JSON.stringify( testCase[1] - )} (asyncBuiltNoOptimization)`, async () => { - const f = await asyncLogicWithoutOptimization.build(testCase[0]) - expect(await f(testCase[1])).toStrictEqual(testCase[2]) + )} (built)`, async () => { + const f = await engine[0].build(testCase[0]) + let result = await f(testCase[1]) + if ((result || 0).toNumber) result = Number(result) + if (Array.isArray(result)) result = result.map(i => i.toNumber ? Number(i) : i) + expect(result).toStrictEqual(testCase[2]) }) - }) - }) -} -// eslint-disable-next-line no-labels -notInline: { - const logic = new LogicEngine(undefined, { compatible: true }) - const asyncLogic = new AsyncLogicEngine(undefined, { compatible: true }) - const logicWithoutOptimization = new LogicEngine(undefined, { compatible: true }) - const asyncLogicWithoutOptimization = new AsyncLogicEngine(undefined, { compatible: true }) - - logicWithoutOptimization.disableInline = true - logic.disableInline = true - asyncLogic.disableInline = true - asyncLogicWithoutOptimization.disableInline = true - - // using a loop to disable the inline compilation mechanism. - tests.forEach((testCase) => { - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )}`, () => { - expect(logic.run(testCase[0], testCase[1])).toStrictEqual(testCase[2]) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (async)`, async () => { - expect(await asyncLogic.run(testCase[0], testCase[1])).toStrictEqual( - testCase[2] - ) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (built)`, () => { - const f = logic.build(testCase[0]) - expect(f(testCase[1])).toStrictEqual(testCase[2]) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (asyncBuilt)`, async () => { - const f = await asyncLogic.build(testCase[0]) - expect(await f(testCase[1])).toStrictEqual(testCase[2]) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (noOptimization)`, () => { - expect(logicWithoutOptimization.run(testCase[0], testCase[1])).toStrictEqual(testCase[2]) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (asyncNoOptimization)`, async () => { - expect(await asyncLogicWithoutOptimization.run(testCase[0], testCase[1])).toStrictEqual( - testCase[2] - ) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (builtNoOptimization)`, () => { - const f = logicWithoutOptimization.build(testCase[0]) - expect(f(testCase[1])).toStrictEqual(testCase[2]) - }) - - test(`${JSON.stringify(testCase[0])} ${JSON.stringify( - testCase[1] - )} (asyncBuiltNoOptimization)`, async () => { - const f = await asyncLogicWithoutOptimization.build(testCase[0]) - expect(await f(testCase[1])).toStrictEqual(testCase[2]) - }) - }) -} + } + } +}) diff --git a/defaultMethods.js b/defaultMethods.js index c3d5458..8db33a4 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -81,8 +81,20 @@ const defaultMethods = { for (let i = 1; i < data.length; i++) res %= +data[i] return res }, - max: (data) => Math.max(...data), - min: (data) => Math.min(...data), + max: (data) => { + let maximum = data[0] + for (let i = 1; i < data.length; i++) { + if (data[i] > maximum) maximum = data[i] + } + return maximum + }, + min: (data) => { + let minimum = data[0] + for (let i = 1; i < data.length; i++) { + if (data[i] < minimum) minimum = data[i] + } + return minimum + }, in: ([item, array]) => (array || []).includes(item), preserve: { traverse: false, @@ -212,6 +224,7 @@ const defaultMethods = { // Why "executeInLoop"? Because if it needs to execute to get an array, I do not want to execute the arguments, // Both for performance and safety reasons. or: { + [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, _1, _2, engine) => { // See "executeInLoop" above const executeInLoop = Array.isArray(arr) @@ -250,6 +263,7 @@ const defaultMethods = { traverse: false }, and: { + [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, _1, _2, engine) => { // See "executeInLoop" above const executeInLoop = Array.isArray(arr) @@ -286,6 +300,8 @@ const defaultMethods = { } }, substr: ([string, from, end]) => { + if (from) from = +from + if (end) end = +end if (end < 0) { const result = string.substr(from) return result.substr(0, result.length + end) @@ -298,6 +314,7 @@ const defaultMethods = { return 0 }, get: { + [Sync]: true, method: ([data, key, defaultValue], context, above, engine) => { const notFound = defaultValue === undefined ? null : defaultValue @@ -379,6 +396,7 @@ const defaultMethods = { some: createArrayIterativeMethod('some', true), all: createArrayIterativeMethod('every', true), none: { + [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), traverse: false, // todo: add async build & build method: (val, context, above, engine) => { @@ -399,7 +417,7 @@ const defaultMethods = { }, merge: (arrays) => (Array.isArray(arrays) ? [].concat(...arrays) : [arrays]), every: createArrayIterativeMethod('every'), - filter: createArrayIterativeMethod('filter'), + filter: createArrayIterativeMethod('filter', true), reduce: { deterministic: (data, buildState) => { return ( @@ -508,11 +526,26 @@ const defaultMethods = { }, '!': (value, _1, _2, engine) => Array.isArray(value) ? !engine.truthy(value[0]) : !engine.truthy(value), '!!': (value, _1, _2, engine) => Boolean(Array.isArray(value) ? engine.truthy(value[0]) : engine.truthy(value)), - cat: (arr) => { - if (typeof arr === 'string') return arr - let res = '' - for (let i = 0; i < arr.length; i++) res += arr[i] - return res + cat: { + [Sync]: true, + method: (arr) => { + if (typeof arr === 'string') return arr + if (!Array.isArray(arr)) return arr.toString() + let res = '' + for (let i = 0; i < arr.length; i++) res += arr[i].toString() + return res + }, + deterministic: true, + traverse: true, + optimizeUnary: true, + compile: (data, buildState) => { + if (typeof data === 'string') return JSON.stringify(data) + if (typeof data === 'number') return '"' + JSON.stringify(data) + '"' + if (!Array.isArray(data)) return false + let res = buildState.compile`''` + for (let i = 0; i < data.length; i++) res = buildState.compile`${res} + ${data[i]}` + return buildState.compile`(${res})` + } }, keys: ([obj]) => typeof obj === 'object' ? Object.keys(obj) : [], pipe: { @@ -633,8 +666,8 @@ function createArrayIterativeMethod (name, useTruthy = false) { (await engine.run(selector, context, { above })) || [] - return asyncIterators[name](selector, (i, index) => { - const result = engine.run(mapper, i, { + return asyncIterators[name](selector, async (i, index) => { + const result = await engine.run(mapper, i, { above: [{ iterator: selector, index }, context, above] }) return useTruthy ? engine.truthy(result) : result @@ -654,15 +687,16 @@ function createArrayIterativeMethod (name, useTruthy = false) { const method = build(mapper, mapState) const aboveArray = method.aboveDetected ? buildState.compile`[{ iterator: z, index: x }, context, above]` : buildState.compile`null` + const useTruthyMethod = useTruthy ? buildState.compile`engine.truthy` : buildState.compile`` if (async) { if (!isSyncDeep(mapper, buildState.engine, buildState)) { buildState.detectAsync = true - return buildState.compile`await asyncIterators[${name}](${selector} || [], async (i, x, z) => ${method}(i, x, ${aboveArray}))` + return buildState.compile`await asyncIterators[${name}](${selector} || [], async (i, x, z) => ${useTruthyMethod}(${method}(i, x, ${aboveArray})))` } } - return buildState.compile`(${selector} || [])[${name}]((i, x, z) => ${method}(i, x, ${aboveArray}))` + return buildState.compile`(${selector} || [])[${name}]((i, x, z) => ${useTruthyMethod}(${method}(i, x, ${aboveArray})))` }, traverse: false } @@ -705,20 +739,6 @@ defaultMethods['<='].compile = function (data, buildState) { return res } // @ts-ignore Allow custom attribute -defaultMethods.min.compile = function (data, buildState) { - if (!Array.isArray(data)) return false - return `Math.min(${data - .map((i) => buildString(i, buildState)) - .join(', ')})` -} -// @ts-ignore Allow custom attribute -defaultMethods.max.compile = function (data, buildState) { - if (!Array.isArray(data)) return false - return `Math.max(${data - .map((i) => buildString(i, buildState)) - .join(', ')})` -} -// @ts-ignore Allow custom attribute defaultMethods['>'].compile = function (data, buildState) { if (!Array.isArray(data)) return false if (data.length < 2) return false @@ -845,14 +865,6 @@ defaultMethods['*'].compile = function (data, buildState) { return `(${buildString(data, buildState)}).reduce((a,b) => (+a)*(+b))` } } -// @ts-ignore Allow custom attribute -defaultMethods.cat.compile = function (data, buildState) { - if (typeof data === 'string') return JSON.stringify(data) - if (!Array.isArray(data)) return false - let res = buildState.compile`''` - for (let i = 0; i < data.length; i++) res = buildState.compile`${res} + ${data[i]}` - return buildState.compile`(${res})` -} // @ts-ignore Allow custom attribute defaultMethods['!'].compile = function ( diff --git a/index.js b/index.js index 7a36636..75b9b45 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ import Constants from './constants.js' import defaultMethods from './defaultMethods.js' import { asLogicSync, asLogicAsync } from './asLogic.js' import { splitPath, splitPathMemoized } from './utilities/splitPath.js' +import { configurePrecision } from './precision/index.js' export { splitPath, splitPathMemoized } export { LogicEngine } @@ -17,5 +18,6 @@ export { Constants } export { defaultMethods } export { asLogicSync } export { asLogicAsync } +export { configurePrecision } -export default { LogicEngine, AsyncLogicEngine, Compiler, Constants, defaultMethods, asLogicSync, asLogicAsync, splitPath, splitPathMemoized } +export default { LogicEngine, AsyncLogicEngine, Compiler, Constants, defaultMethods, asLogicSync, asLogicAsync, splitPath, splitPathMemoized, configurePrecision } diff --git a/logic.js b/logic.js index 45e8a0e..2051625 100644 --- a/logic.js +++ b/logic.js @@ -32,6 +32,7 @@ class LogicEngine { this.disableInline = options.disableInline this.disableInterpretedOptimization = options.disableInterpretedOptimization this.methods = { ...methods } + this.precision = null this.optimizedMap = new WeakMap() this.missesSinceSeen = 0 @@ -67,7 +68,7 @@ class LogicEngine { const [func] = Object.keys(logic) const data = logic[func] - if (this.isData(logic, func)) return logic + if (this.isData(logic, func) || (this.precision && logic instanceof this.precision)) return logic if (!this.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`) @@ -77,9 +78,9 @@ class LogicEngine { } if (typeof this.methods[func] === 'object') { - const { method, traverse } = this.methods[func] + const { method, traverse, precise } = this.methods[func] const shouldTraverse = typeof traverse === 'undefined' ? true : traverse - const parsedData = shouldTraverse ? ((!data || typeof data !== 'object') ? [data] : coerceArray(this.run(data, context, { above }))) : data + const parsedData = shouldTraverse ? ((!data || typeof data !== 'object') ? [data] : coerceArray(this.run(data, context, { above, precise }))) : data return method(parsedData, context, above, this) } @@ -123,7 +124,7 @@ class LogicEngine { * * @param {*} logic The logic to be executed * @param {*} data The data being passed in to the logic to be executed against. - * @param {{ above?: any }} options Options for the invocation + * @param {{ above?: any, precise?: boolean }} options Options for the invocation * @returns {*} */ run (logic, data = {}, options = {}) { @@ -149,7 +150,7 @@ class LogicEngine { if (Array.isArray(logic)) { const res = [] - for (let i = 0; i < logic.length; i++) res.push(this.run(logic[i], data, { above })) + for (let i = 0; i < logic.length; i++) res.push(this.run(logic[i], data, options)) return res } diff --git a/optimizer.js b/optimizer.js index dc90e2f..8a4d047 100644 --- a/optimizer.js +++ b/optimizer.js @@ -53,7 +53,7 @@ export function optimize (logic, engine, above = []) { const keys = Object.keys(logic) const methodName = keys[0] - const isData = engine.isData(logic, methodName) + const isData = engine.isData(logic, methodName) || (engine.precision && logic instanceof engine.precision) if (isData) return () => logic // If we have a deterministic function, we can just return the result of the evaluation, diff --git a/package.json b/package.json index bffdbde..b277f62 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,11 @@ "coverage": "coveralls < coverage/lcov.info", "prepublish": "npm run build", "build": "run-script-os", - "build:win32": "rm -rf dist && rm -f *.d.ts && rm -f utilities/*.d.ts && rollup index.js --file dist/cjs/index.js --format cjs --exports named && rollup index.js --file dist/esm/index.js --format esm && echo { \"type\": \"module\" } > dist/esm/package.json && echo { \"type\": \"commonjs\" } > dist/cjs/package.json && cd dist && standard --fix */*.js && tsc ../index.js --declaration --allowJs --emitDeclarationOnly --target ESNext --moduleResolution node", - "build:default": "rm -rf dist && rm -f *.d.ts && rm -f utilities/*.d.ts && rollup index.js --file dist/cjs/index.js --format cjs --exports named && rollup index.js --file dist/esm/index.js --format esm && echo '{ \"type\": \"module\" }' > dist/esm/package.json && echo '{ \"type\": \"commonjs\" }' > dist/cjs/package.json && cd dist && standard --fix */*.js && tsc ../index.js --declaration --allowJs --emitDeclarationOnly --target ESNext --moduleResolution node" + "build:win32": "rm -rf dist && rm -f *.d.ts precision/*.d.ts && rm -f utilities/*.d.ts && rollup index.js --file dist/cjs/index.js --format cjs --exports named && rollup index.js --file dist/esm/index.js --format esm && echo { \"type\": \"module\" } > dist/esm/package.json && echo { \"type\": \"commonjs\" } > dist/cjs/package.json && cd dist && standard --fix */*.js && tsc ../index.js --declaration --allowJs --emitDeclarationOnly --target ESNext --moduleResolution node", + "build:default": "rm -rf dist && rm -f *.d.ts precision/*.d.ts && rm -f utilities/*.d.ts && rollup index.js --file dist/cjs/index.js --format cjs --exports named && rollup index.js --file dist/esm/index.js --format esm && echo '{ \"type\": \"module\" }' > dist/esm/package.json && echo '{ \"type\": \"commonjs\" }' > dist/cjs/package.json && cd dist && standard --fix */*.js && tsc ../index.js --declaration --allowJs --emitDeclarationOnly --target ESNext --moduleResolution node" }, "devDependencies": { + "decimal.js": "^10.4.3", "coveralls": "^3.1.1", "cross-env": "^7.0.3", "eslint": "^7.32.0", @@ -37,7 +38,8 @@ }, "jest": { "testPathIgnorePatterns": [ - "./bench" + "./bench", + "./precision" ] }, "exports": { diff --git a/precision/index.js b/precision/index.js new file mode 100644 index 0000000..a6a1606 --- /dev/null +++ b/precision/index.js @@ -0,0 +1,369 @@ +import defaultMethods from '../defaultMethods.js' + +function configurePrecisionDecimalJs (engine, constructor, compatible = true) { + engine.precision = constructor + + engine.truthy = (data) => { + if ((data || false).toNumber) return Number(data) + if (compatible && Array.isArray(data) && data.length === 0) return false + return data + } + + if (engine.fallback) engine.fallback.truthy = engine.truthy + + engine.addMethod('+', { + method: (data) => { + if (typeof data === 'string') return new constructor(data) + if (typeof data === 'number') return new constructor(data) + let res = new constructor(data[0]) + for (let i = 1; i < data.length; i++) res = res.plus(data[i]) + return res + }, + compile: (args, buildState) => { + if (Array.isArray(args)) { + let res = buildState.compile`(new engine.precision(${args[0]}))` + for (let i = 1; i < args.length; i++) res = buildState.compile`(${res}.plus(${args[i]}))` + return res + } + return false + }, + traverse: true + }, { optimizeUnary: true, sync: true, deterministic: true }) + + engine.addMethod('-', { + method: (data) => { + if (typeof data === 'string') return new constructor(data).mul(-1) + if (typeof data === 'number') return new constructor(data).mul(-1) + let res = new constructor(data[0]) + if (data.length === 1) return res.mul(-1) + for (let i = 1; i < data.length; i++) res = res.minus(data[i]) + return res + }, + compile: (args, buildState) => { + if (Array.isArray(args)) { + let res = buildState.compile`(new engine.precision(${args[0]}))` + if (args.length === 1) return buildState.compile`(${res}.mul(-1))` + for (let i = 1; i < args.length; i++) res = buildState.compile`(${res}.minus(${args[i]}))` + return res + } + return false + }, + traverse: true + }, { optimizeUnary: true, sync: true, deterministic: true }) + + engine.addMethod('*', { + method: (data) => { + let res = new constructor(data[0]) + for (let i = 1; i < data.length; i++) res = res.mul(data[i]) + return res + }, + compile: (args, buildState) => { + if (Array.isArray(args)) { + let res = buildState.compile`(new engine.precision(${args[0]}))` + for (let i = 1; i < args.length; i++) res = buildState.compile`(${res}.mul(${args[i]}))` + return res + } + return false + }, + traverse: true + }, { sync: true, deterministic: true }) + + engine.addMethod('/', { + method: (data) => { + let res = new constructor(data[0]) + for (let i = 1; i < data.length; i++) res = res.div(data[i]) + return res + }, + compile: (args, buildState) => { + if (Array.isArray(args)) { + let res = buildState.compile`(new engine.precision(${args[0]}))` + for (let i = 1; i < args.length; i++) res = buildState.compile`(${res}.div(${args[i]}))` + return res + } + return false + }, + traverse: true + }, { sync: true, deterministic: true }) + + engine.addMethod('%', { + method: (data) => { + let res = new constructor(data[0]) + for (let i = 1; i < data.length; i++) res = res.mod(data[i]) + return res + }, + compile: (args, buildState) => { + if (Array.isArray(args)) { + let res = buildState.compile`(new engine.precision(${args[0]}))` + for (let i = 1; i < args.length; i++) res = buildState.compile`(${res}.mod(${args[i]}))` + return res + } + return false + }, + traverse: true + }, { sync: true, deterministic: true }) + + engine.addMethod('===', { + method: (args) => { + if (args.length === 2) { + if (args[0].eq && (args[1].eq || typeof args[1] === 'number')) return args[0].eq(args[1]) + if (args[1].eq && (args[0].eq || typeof args[0] === 'number')) return args[1].eq(args[0]) + return args[0] === args[1] + } + for (let i = 1; i < args.length; i++) { + if (args[i - 1].eq && !args[i - 1].eq(args[i])) return false + if (args[i].eq && !args[i].eq(args[i - 1])) return false + if (args[i - 1] !== args[i]) return false + } + return true + }, + traverse: true + }, { sync: true, deterministic: true }) + + engine.addMethod('==', { + method: (args) => { + if (args.length === 2) { + if (args[0].eq) return args[0].eq(args[1]) + if (args[1].eq) return args[1].eq(args[0]) + // eslint-disable-next-line eqeqeq + return args[0] == args[1] + } + for (let i = 1; i < args.length; i++) { + if (args[i - 1].eq && !args[i - 1].eq(args[i])) return false + if (args[i].eq && !args[i].eq(args[i - 1])) return false + // eslint-disable-next-line eqeqeq + if (args[i - 1] != args[i]) return false + } + return true + }, + traverse: true + }, { sync: true, deterministic: true }) + + engine.addMethod('!=', { + method: (args) => { + if (args.length === 2) { + if (args[0].eq) return !args[0].eq(args[1]) + if (args[1].eq) return !args[1].eq(args[0]) + // eslint-disable-next-line eqeqeq + return args[0] != args[1] + } + for (let i = 1; i < args.length; i++) { + if (args[i - 1].eq && args[i - 1].eq(args[i])) return false + if (args[i].eq && args[i].eq(args[i - 1])) return false + // eslint-disable-next-line eqeqeq + if (args[i - 1] !== args[i]) return true + } + return true + }, + traverse: true + }, { sync: true, deterministic: true }) + + engine.addMethod('!==', { + method: (args) => { + if (args.length === 2) { + if (args[0].eq && (args[1].eq || typeof args[1] === 'number')) return !args[0].eq(args[1]) + if (args[1].eq && (args[0].eq || typeof args[0] === 'number')) return !args[1].eq(args[0]) + return args[0] !== args[1] + } + for (let i = 1; i < args.length; i++) { + if (args[i - 1].eq && args[i - 1].eq(args[i])) return false + if (args[i].eq && args[i].eq(args[i - 1])) return false + if (args[i - 1] !== args[i]) return true + } + return false + }, + traverse: true + }, { sync: true, deterministic: true }) + + engine.addMethod('>', { + method: (args) => { + if (args.length === 2) { + if (args[0].gt) return args[0].gt(args[1]) + if (args[1].lt) return args[1].lt(args[0]) + return args[0] > args[1] + } + for (let i = 1; i < args.length; i++) { + if (args[i - 1].gt && !args[i - 1].gt(args[i])) return false + if (args[i].lt && !args[i].lt(args[i - 1])) return false + if (args[i - 1] <= args[i]) return false + } + return true + }, + traverse: true + }, { sync: true, deterministic: true }) + + engine.addMethod('>=', { + method: (args) => { + if (args.length === 2) { + if (args[0].gte) return args[0].gte(args[1]) + if (args[1].lte) return args[1].lte(args[0]) + return args[0] >= args[1] + } + for (let i = 1; i < args.length; i++) { + if (args[i - 1].gte && !args[i - 1].gte(args[i])) return false + if (args[i].lte && !args[i].lte(args[i - 1])) return false + if (args[i - 1] < args[i]) return false + } + return true + }, + traverse: true + }, { sync: true, deterministic: true }) + + engine.addMethod('<', { + method: (args) => { + if (args.length === 2) { + if (args[0].lt) return args[0].lt(args[1]) + if (args[1].gt) return args[1].gt(args[0]) + return args[0] < args[1] + } + for (let i = 1; i < args.length; i++) { + if (args[i - 1].lt && !args[i - 1].lt(args[i])) return false + if (args[i].gt && !args[i].gt(args[i - 1])) return false + if (args[i - 1] >= args[i]) return false + } + return true + }, + traverse: true + }, { sync: true, deterministic: true }) + + engine.addMethod('<=', { + method: (args) => { + if (args.length === 2) { + if (args[0].lte) return args[0].lte(args[1]) + if (args[1].gte) return args[1].gte(args[0]) + return args[0] <= args[1] + } + for (let i = 1; i < args.length; i++) { + if (args[i - 1].lte && !args[i - 1].lte(args[i])) return false + if (args[i].gte && !args[i].gte(args[i - 1])) return false + if (args[i - 1] > args[i]) return false + } + return true + }, + traverse: true + }, { sync: true, deterministic: true }) +} + +/** + * Allows you to configure precision for JSON Logic Engine. + * + * You can pass the following in: + * - `ieee754` - Uses the IEEE 754 standard for calculations. + * - `precise` - Tries to improve accuracy of calculations by scaling numbers during operations. + * - A constructor for decimal.js. + * + * @example ```js + * import { LogicEngine, configurePrecision } from 'json-logic-js' + * import { Decimal } from 'decimal.js' // or decimal.js-light + * + * const engine = new LogicEngine() + * configurePrecision(engine, Decimal) + * ``` + * + * The class this mechanism uses requires the following methods to be implemented: + * - `eq` + * - `gt` + * - `gte` + * - `lt` + * - `lte` + * - `plus` + * - `minus` + * - `mul` + * - `div` + * - `mod` + * - `toNumber` + * + * ### FAQ: + * + * Q: Why is this not included in the class? + * + * A: This mechanism reimplements a handful of operators. Keeping this method separate makes it possible to tree-shake this code out + * if you don't need it. + * + * @param {import('../logic.d.ts').default | import('../asyncLogic.d.ts').default} engine + * @param {'precise' | 'ieee754' | (...args: any[]) => any} constructor + * @param {Boolean} compatible + */ +export function configurePrecision (engine, constructor, compatible = true) { + if (typeof constructor === 'function') return configurePrecisionDecimalJs(engine, constructor, compatible) + + if (constructor === 'ieee754') { + const operators = ['+', '-', '*', '/', '%', '===', '==', '!=', '!==', '>', '>=', '<', '<='] + for (const operator of operators) engine.methods[operator] = defaultMethods[operator] + } + + if (constructor !== 'precise') throw new Error('Unsupported precision type') + + engine.addMethod('+', (data) => { + if (typeof data === 'string') return +data + if (typeof data === 'number') return +data + let res = 0 + let overflow = 0 + for (let i = 0; i < data.length; i++) { + const item = +data[i] + if (Number.isInteger(item)) res += item + else { + res += item | 0 + overflow += +('0.' + item.toString().split('.')[1]) * 1e6 + } + } + + return res + (overflow / 1e6) + }, { deterministic: true, sync: true, optimizeUnary: true }) + + engine.addMethod('*', (data) => { + const SCALE_FACTOR = 1e6 // Fixed scale for precision + let result = 1 + + for (let i = 0; i < data.length; i++) { + const item = +data[i] + + if (item > 1e6 || result > 1e6) { + result *= item + continue + } + + result *= (item * SCALE_FACTOR) | 0 + result /= SCALE_FACTOR + } + + return result + }, { deterministic: true, sync: true }) + + engine.addMethod('/', (data) => { + let res = data[0] + for (let i = 1; i < data.length; i++) res /= +data[i] + // if the value is really close to 0, we'll just return 0 + if (Math.abs(res) < 1e-10) return 0 + return res + }, { deterministic: true, sync: true }) + + engine.addMethod('-', (data) => { + if (typeof data === 'string') return -data + if (typeof data === 'number') return -data + if (data.length === 1) return -data[0] + let res = data[0] + let overflow = 0 + for (let i = 1; i < data.length; i++) { + const item = +data[i] + if (Number.isInteger(item)) res -= item + else { + res -= item | 0 + overflow += +('0.' + item.toString().split('.')[1]) * 1e6 + } + } + return res - (overflow / 1e6) + }, { deterministic: true, sync: true, optimizeUnary: true }) + + engine.addMethod('%', (data) => { + let res = data[0] + + if (data.length === 2) { + if (data[0] < 1e6 && data[1] < 1e6) return ((data[0] * 10e3) % (data[1] * 10e3)) / 10e3 + } + + for (let i = 1; i < data.length; i++) res %= +data[i] + // if the value is really close to 0, we'll just return 0 + if (Math.abs(res) < 1e-10) return 0 + return res + }, { deterministic: true, sync: true }) +} diff --git a/precision/package.json b/precision/package.json new file mode 100644 index 0000000..a407585 --- /dev/null +++ b/precision/package.json @@ -0,0 +1,10 @@ +{ + "name": "precision", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "decimal.js": "^10.4.3" + }, + "type": "module" +} diff --git a/precision/scratch.js b/precision/scratch.js new file mode 100644 index 0000000..21ac512 --- /dev/null +++ b/precision/scratch.js @@ -0,0 +1,29 @@ +import { LogicEngine } from '../index.js' +import { Decimal } from 'decimal.js' +import { configurePrecision } from './index.js' + +Decimal.prototype.toString = function () { + return this.toFixed() +} +const ieee754Engine = new LogicEngine() +const improvedEngine = new LogicEngine() +const decimalEngine = new LogicEngine() + +configurePrecision(decimalEngine, Decimal) +configurePrecision(improvedEngine, 'precise') + +console.log(ieee754Engine.build({ '*': [9007199254740991, 5] })()) // 45035996273704950, inaccurate +console.log(improvedEngine.build({ '*': [9007199254740991, 5] })()) // 45035996273704950, inaccurate +console.log(decimalEngine.build({ '*': [9007199254740991, 5] })()) // 45035996273704955, accurate + +console.log(ieee754Engine.run({ '+': [0.1, 0.2] })) // 0.30000000000000004 +console.log(improvedEngine.run({ '+': [0.1, 0.2] })) // 0.3 +console.log(decimalEngine.run({ '+': [0.1, 0.2] })) // 0.3 + +console.log(ieee754Engine.run({ '>': [{ '+': [0.1, 0.2] }, 0.3] })) // true, because 0.1 + 0.2 = 0.30000000000000004 +console.log(improvedEngine.run({ '>': [{ '+': [0.1, 0.2] }, 0.3] })) // false, because 0.1 + 0.2 = 0.3 +console.log(decimalEngine.run({ '>': [{ '+': [0.1, 0.2] }, 0.3] })) // false, because 0.1 + 0.2 = 0.3 + +console.log(ieee754Engine.run({ '%': [0.0075, 0.0001] })) // 0.00009999999999999937 +console.log(improvedEngine.run({ '%': [0.0075, 0.0001] })) // 0 +console.log(decimalEngine.run({ '%': [0.0075, 0.0001] })) // 0 diff --git a/precision/yarn.lock b/precision/yarn.lock new file mode 100644 index 0000000..edae5da --- /dev/null +++ b/precision/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +decimal.js@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== diff --git a/yarn.lock b/yarn.lock index 1f16981..1316453 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1209,6 +1209,11 @@ decimal.js@^10.2.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== +decimal.js@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"